Quickstart

Note

This page assumes you have already installed cfig.

This page describes how to use cfig in an application.

Creating a configuration module

First, create a new .py file inside your package with the following contents:

import cfig
import typing as t

config = cfig.Configuration()

This will:

  1. Import cfig into your module

  2. Import typing into your module and alias it as t for ease of use

  3. Create a new Configuration with the default parameters, which will be able to be configured from environment variables and from environment files (files whose path is specified in an environment variable suffixed with _FILE)

Creating configurable variables

Basics

To make use of cfig, you’ll need to create one or more configurable variables in your module file:

import cfig
import typing as t

config = cfig.Configuration()

@config.required()
def SECRET_PASSWORD(val: str) -> str:
    """The secret password required to use this application!"""
    return val

The newly added lines create a new configurable value named SECRET_PASSWORD:

  • the name of the function is used as key of the configurable value;

  • the @config.required() decorator marks the value as required, preventing your application from launching if it is not set;

  • the function parameters consist of a single str parameter named val, which is the string read from the environment variable having the same name of the function;

  • the docstring defines the meaning of the configuration value in natural language;

  • the contents of the function are used to process the input string into more refined Python objects;

  • the return annotation of the function is used to let IDEs know what type this configuration value will be.

Optional

Configuration values can be optional:

import cfig
import typing as t

config = cfig.Configuration()

@config.required()
def SECRET_PASSWORD(val: str) -> str:
    """The secret password required to use this application!"""
    return val

@config.optional()
def SECRET_USERNAME(val: t.Optional[str]) -> str:
    """The username to require users to login as. If unset, defaults to `root`."""
    if val is None:
        return "root"
    return val

Optional values differ from required ones in their decorator and signature:

  1. The decorator is @config.optional() instead of @config.required();

  2. Since the passed val can be None, it is given a signature of typing.Optional.

Processing and validation

The function defining a new configurable variable is also called a resolver: it will be executed only once, when its value is first requested, then the result is cached in a special object called proxy.

This allows us to perform some expensive operations inside, such as connecting to a database, or performing API requests to validate tokens and passwords.

import cfig
import typing as t

config = cfig.Configuration()

@config.required()
def SECRET_PASSWORD(val: str) -> str:
    """The secret password required to use this application!"""
    return val

@config.optional()
def SECRET_USERNAME(val: t.Optional[str]) -> str:
    """The username to require users to login as. If unset, defaults to `root`."""
    if val is None:
        return "root"
    return val

@config.required()
def MAX_USERS(val: str) -> int:
    """The maximum number of users that will be able to login to this application."""
    try:
        return int(val)
    except (ValueError, TypeError):
        raise cfig.InvalidValueError("Not an int.")

We can see that the new MAX_USERS configurable value processes the input string by trying to cast it into an int, and raises a InvalidValueError containing the error message to display to the user if the cast fails.

Ideally, errors happening in resolvers should be caught by the programmer and re-raised as InvalidValueError, so that users can distinguish them from bugs.

Adding CLI support

Note

This requires the CLI extra to be installed. See Installation for more details.

To facilitate configuration on the users’ part, cfig provides an integrated command line interface previewing the values of variables, triggered by a call to cli():

import cfig
import typing as t

config = cfig.Configuration()

@config.required()
def SECRET_PASSWORD(val: str) -> str:
    """The secret password required to use this application!"""
    return val

@config.optional()
def SECRET_USERNAME(val: t.Optional[str]) -> str:
    """The username to require users to login as. If unset, defaults to `root`."""
    if val is None:
        return "root"
    return val

@config.required()
def MAX_USERS(val: str) -> int:
    """The maximum number of users that will be able to login to this application."""
    try:
        return int(val)
    except (ValueError, TypeError):
        raise cfig.InvalidValueError("Not an int.")

if __name__ == "__main__":
    config.cli()

By adding the cli() call to a __main__ clause, we allow users to access the CLI by manually running this module, but we prevent the CLI from starting when this module is accessed from another location.

Given our current configuration, something similar to this will be displayed:

$ python -m myproject.mydefinitionmodule
===== Configuration =====

SECRET_PASSWORD → Required, but not set.
The secret password required to use this application!

SECRET_USERNAME = 'root'
The username to require users to login as. If unset, defaults to `root`.

MAX_USERS       → Required, but not set.
The maximum number of users that will be able to login to this application.

===== End =====

Use the configuration

Finally, it is time to use in our application the configurable variables we defined!

In the modules of your application, you can import and use the variables directly from the definition module:

from .mydefinitionmodule import SECRET_PASSWORD, SECRET_USERNAME, MAX_USERS

if __name__ == "__main__":
    if username := input("Username: ") != SECRET_USERNAME:
        print("error: invalid username")
        sys.exit(1)
    if password := input("Password: ") != SECRET_PASSWORD:
        print("error: invalid password")
        sys.exit(2)

    print("Welcome, " + SECRET_USERNAME + "!")
    print(f"The current user limit is: {MAX_USERS}")

Warning

Since the values imported from the definition module are proxies to the real value, is comparisions won’t work with them, but you can do == comparsions with them:

@config.optional()
def ALWAYS_NONE(val: t.Optional[str]) -> None:
    """This configuration value will always be None."""
    return None

assert ALWAYS_NONE is not None
assert ALWAYS_NONE == None

Validate all variables at once

For a better user experience, you might want to ensure that all variables are correctly configured when your application is started.

For that goal, cfig provides the resolve() method, which immediately tries to resolve and cache all configurable values defined in the Configuration:

from .mydefinitionmodule import config

if __name__ == "__main__":
    config.proxies.resolve()

The method will gather all errors occurring during the resolution, and will raise all of them at once with a BatchResolutionFailure, which you may want to handle in a custom way:

from .mydefinitionmodule import config

if __name__ == "__main__":
    try:
        config.proxies.resolve()
    except cfig.BatchResolutionFailure as failure:
        ...

Conclusion

And that’s it! You’re using cfig in the best way possible :)

See Advanced usage for more features that may be useful in specific cases.