Overriding configuration file settings with environment variables

We'll use a three-stage process to incorporate environment variables. For this application, the environment variables will be used to override configuration file settings. The first stage is to gather the default values from the various files. This is based on the examples shown in Chapter 14, Configuration Files and Persistence. We can use code like the following:

config_locations = (
Path.cwd(),
Path.home(),
Path.cwd() / "opt", # A testing stand-in for Path("/opt")
Path(__file__) / "config",
# Other common places...
# Path("~someapp").expanduser(),
)

candidate_paths = (dir / "ch18app.yaml" for dir in config_locations)
config_paths = (path for path in candidate_paths if path.exists())
files_values = [yaml.load(str(path)) for path in config_paths]

This example uses a sequence of locations, ranked in order of importance. The value in the current working directory provides the most immediate configuration. For values not set here, the user's home directory is a place to keep general settings. We should use an opt subdirectory of the current working directory, Path.cwd()/"opt"; this stands in place of Path("/etc") or Path("/opt"). A standard name, "ch18app.yaml", is put after the various directory paths to create a number of concrete paths for configuration files to set the candidate_paths variable. A generator expression assigned to config_paths will yield an iterable sequence of paths that actually exists.

The final result in files_values is a sequence of configuration values taken from the files that are found to exist. Each file should create a dictionary that maps parameter names to parameter values. This list can be used as part of a final ChainMap object.

The second stage is to build the user's environment-based settings. We can use code like the following to set this up:

env_settings = [
("samples", nint(os.environ.get("SIM_SAMPLES", None))),
("stake", nint(os.environ.get("SIM_STAKE", None))),
("rounds", nint(os.environ.get("SIM_ROUNDS", None))),
]
env_values = {k: v for k, v in env_settings if v is not None}

Creating a mapping like this has the effect of rewriting external environment variable names like SIM_SAMPLES into internal configuration names like samples. Internal names will match our application's configuration attributes. External names are often defined in a way that makes them unique in a complex environment.

For environment variables that were not defined, the nint() function, shown in the following code, will provide None as a default value if the environment variable is not defined. When we create the env_values, the None objects are removed from the initial collection of environment values.

Given a number of dictionaries, we can use ChainMap to combine them, as shown in the following code:

defaults = argparse.Namespace(
**ChainMap(
env_values, # Checks here first
*files_values # All of the files, in order
)
)

We combined the various mappings into a single ChainMap. The environment variables are searched first. When values are present there, the values are looked up from the user's configuration file first and then other configurations, if the user configuration file didn't provide a value.

The *files_values ensures that the list of values will be provided as a sequence of positional argument values. This allows a single sequence (or iterable) to provide values for a number of positional parameters. **ChainMap ensures that a dictionary is turned into a number of named parameter values. Each key becomes a parameter name associated with the value from the dictionary. Here's an example of how this works:

>>> argparse.Namespace(a=1, b=2)
Namespace(a=1, b=2)
>>> argparse.Namespace(**{'a': 1, 'b': 2})
Namespace(a=1, b=2)

The resulting Namespace object can be used to provide defaults when parsing the command-line arguments. We can use the following code to parse the command-line arguments and update these defaults:

config = parser.parse_args(sys.argv[1:], namespace=defaults) 

We transformed our ChainMap of configuration file settings into an argparse.Namespace object. Then we parsed the command-line options to update that namespace object. As the environment variables are first in ChainMap, they override any configuration files.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset