Quickstart
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:
Import
cfig
into your moduleImport
typing
into your module and alias it ast
for ease of useCreate 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 namedval
, 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:
The decorator is
@config.optional()
instead of@config.required()
;Since the passed
val
can beNone
, it is given a signature oftyping.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.