Overview of the process
Contents
Overview of the process¶
How is a configuration processed by configpile
, for example when calling
from_command_line_()
?
First, the configuration class, which is a
dataclass()
inheriting fromConfig
is inspected. All the dataclass fields should be annotated with aParam
instance, see Params (parameters).configpile
makes a list of triggers, for environment variables, INI files key/value pairs, and command-line arguments.For each parameter,
configpile
prepares a list of values. If the parameter does not have a default value, the list is initially empty. If the parameter has a default value, this value is parsed (see Parsers) and stored in the list.Then,
configpile
inspects environment variables. If any of the environment variables corresponds to a parameter, the variable content is parsed and added to the list.If any environment variable points to an INI file, the file is read, and for each parameter key present in the INI file, the corresponding value is parsed and added to the list.
After that,
configpile
processes command-line arguments. Some arguments will point to an INI file: again, those files are parsed and the corresponding values added to the list of the corresponding parameter. Some arguments provide parameter values directly: those values are parsed and again added to the corresponding list.At this point,
configpile
may have failed to parse some of the strings, have had problems reading a configuration file, or some values may have failed validation (see Parameter and configuration validation). All these errors are collected and reported together to the user.If everything is fine,
configpile
looks at each parameter in turn. Now, each parameter corresponds to a list of values. The question is how to process this list to populate the configuration with a single value. This is the job of theCollector
: the standard collectors provided byconfigpile
either append the values together (useful when collecting a list of files to process, for example), or return the last value (useful when later parameter value override the values provided previously).This process can eventually fail for some or all the parameters: for example, if a collector needs to return the last value provided, and nothing was provided at all, it will error. All the errors are collected together, in that case, and reported to the user.
The next step is the construction of the configuration dataclass, with the computed parameter values. After that, the validation methods of the configuration dataclass are called for the final validation round, see Parameter and configuration validation. If everything works, the constructed dataclass is returned.
This whole process is short-circuited in a few cases.
By default, the command-line flags
-h
and--help
display the usage information and exit the program usingsys.exit()
.In the future, we may support other special actions such as displaying the program version using a
--version
flag for example.
There are two static methods on Config
that can be called to construct
a configuration.
The
from_command_line_()
method, in case of errors, displays usage information and exits throughsys.exit()
.The
parse_command_line_()
method, always returns to the caller, and returns either a constructed configuration dataclass, a token corresponding to a special action (such as a command to display help information or the version number), or finally aErr
error.
Demonstrating the process¶
Here is a simple configuration class, and we will make its construction fail at several points in the construction.
from configpile import *
from dataclasses import dataclass
from typing import Optional
from typing_extensions import Annotated
@dataclass(frozen=True)
class TestConfig(Config):
"""
A superb program
"""
#: First parameter, must be greater than the second parameter
a: Annotated[int, Param.store(parsers.int_parser, short_flag_name='-a')]
#: Second parameter
b: Annotated[int, Param.store(parsers.int_parser, short_flag_name='-b')]
def validate_a_greater_than_b(self) -> Optional[Err]:
if not self.a > self.b:
return Err.make(f"Parameter a={self.a} should be > than b={self.b}")
return None
Parse errors¶
Note that all parse errors are collected and reported. The rest of the construction process is not done.
res = TestConfig.parse_command_line_(args = ['-a', 'invalid', '-b','also_invalid', '-a', 'last_invalid'], env = {})
res
ManyErr(errs=[Err1(msg="Error 'invalid literal for int() with base 10: 'invalid'' in 'invalid'", contexts=[('flag', '-a'), ('param', 'a')]), Err1(msg="Error 'invalid literal for int() with base 10: 'also_invalid'' in 'also_invalid'", contexts=[('flag', '-b'), ('param', 'b')]), Err1(msg="Error 'invalid literal for int() with base 10: 'last_invalid'' in 'last_invalid'", contexts=[('flag', '-a'), ('param', 'a')])])
res.pretty_print()
• In flag: -a • In param: a 0 Error 'invalid literal for int() with base 10: 'invalid'' in 'invalid' 1 Error 'invalid literal for int() with base 10: 'last_invalid'' in 'last_invalid' 0 In flag: -b In param: b Error 'invalid literal for int() with base 10: 'also_invalid'' in 'also_invalid'
Collection errors¶
Those errors happen during the collection process. Here, because of missing values.
res = TestConfig.parse_command_line_(args = ['-a', '2'], env = {})
res
Err1(msg='Argument is required', contexts=[('param', 'b')])
res.pretty_print()
In param: b
Argument is required
Instance-level validation errors¶
Those errors are reported in a last step, and will only be shown if the previous steps were completed successfully.
res = TestConfig.parse_command_line_(args = ['-a', '2', '-b','3'], env = {})
res
Err1(msg='Parameter a=2 should be > than b=3', contexts=[])