Params (parameters)
Contents
Params (parameters)¶
Parameters are what constitute a configuration. The values in a configuration dataclass are fields that are annotated with a Param
instance, using the typing.Annotated
type.
(Note that the typing.Annotated
type needs to be imported from the
typing_extensions
package instead if one is not using Python >= 3.9).
Param
is modeled after the add_argument method in the argparse standard library module.
How parameters handle their values¶
First, you need to decide how your parameter is going to behave when fed with multiple values.
If you construct the parameter with the store()
static method, the
parameter value will be the last value provided. See Overview of the process for the standard
order of processing.
If you construct the parameter with the append1()
static method, you
provide a parser (see Parsers) that parses single values, and the provided values
are all appended in a sequence.
If you construct the parameter with the append()
static method, you
provide a parser that parses a sequence of values (for example, comma-separated), and the
parameter value with correspond to all the provided sequences, joined together.
Finally, the special construction provided by configpile.arg.Param.config()
handles
INI configuration files. Here, not only the INI file paths are returned in a parameter, but
each time an INI file is provided, the files are parsed so that their content can influence the
configuration.
Here is an example.
from configpile import *
from typing import Sequence
from pathlib import Path
from typing_extensions import Annotated # or `from typing import Annotated` if Python >= 3.9
from dataclasses import dataclass
@dataclass(frozen=True)
class HandlingValues(Config):
"""
Empty description
"""
#: Here is a parameter with a single value
radius: Annotated[float, Param.store(parsers.float_parser)]
#: Here is a parameter that accumulates
files: Annotated[Sequence[Path], Param.append1(parsers.path_parser)]
HandlingValues.from_command_line_(args=['--radius', '0.1', '--files', 'input1.dat', '--files',
'input2.dat', '--radius', '0.2', '--files', 'input3.dat'])
HandlingValues(radius=0.2, files=[PosixPath('input1.dat'), PosixPath('input2.dat'), PosixPath('input3.dat')])
How parameters are described (help)¶
Normally, parameters are described using a #:
Sphinx autodoc-style comment.
One can override this behavior by specifying a help
keyword argument.
In particular, configpile
cannot retrieve autodoc-style comments when the class is not defined
in a regular .py
file. See example below, where the radius
help string is not recognized
because we are in a Jupyter notebook.
from typing import ClassVar
@dataclass(frozen=True)
class Description(Config):
"""
Example of handling parameter help
"""
#: Here is a parameter with a single value
radius: Annotated[float, Param.store(parsers.float_parser)]
files: Annotated[Sequence[Path], Param.append1(parsers.path_parser,
help="Here is a parameter that accumulates")]
prog_: ClassVar[str] = "Description"
Description.get_argument_parser_().print_help()
usage: Description [--radius RADIUS] [--files FILES]
Example of handling parameter help
optional arguments:
--files FILES Here is a parameter that accumulates
required arguments:
--radius RADIUS
Where parameter values are taken¶
Parameter values can come from:
environment variables, if
env_var_name
is set (it is not, by default),INI configuration files, if
config_key_name
(it is set toKEBAB_CASE
, by default)command-line arguments, after a long flag (i.e.
--flag
), iflong_flag_name is set (it is set to {attr}
~configpile.arg.Derive.KEBAB_CASE`, by default)command-line arguments, after a short flag (i.e.
-f
), ifshort_flag_name
is set (it is not, by default),positional command-line arguments, if
positional
is notNone
(it isNone
by default).
Environment variables¶
Here we demonstrate how to fill the radius
and shift
parameters using environment variables.
@dataclass(frozen=True)
class EnvVarDemo(Config):
env_prefix_ = "DEMO_"
radius: Annotated[float, Param.store(parsers.float_parser,
env_var_name=Derived.SNAKE_CASE_UPPER_CASE)]
shift: Annotated[float, Param.store(parsers.float_parser, env_var_name="SHIFT")]
scale: Annotated[float, Param.store(parsers.float_parser,
env_var_name=None)] # or leave env_var_name out as None is the default
EnvVarDemo.processor_().env_handlers.keys()
dict_keys(['DEMO_RADIUS', 'SHIFT'])
EnvVarDemo.parse_command_line_(args=["--scale", "0.3"], env ={"SHIFT": "0.5", "DEMO_RADIUS": "0.7"})
EnvVarDemo(radius=0.7, shift=0.5, scale=0.3)
INI configuration files¶
Here is an example. The config_key_name
keyword argument can be set to None
to forbid its
presence in INI files. This part is not demonstrated below.
@dataclass(frozen=True)
class INIDemo(Config):
radius: Annotated[float, Param.store(parsers.float_parser, config_key_name="rad")]
shift: Annotated[float, Param.store(parsers.float_parser,
config_key_name=Derived.KEBAB_CASE)] # or leave config_key_name out as this is the default
INIDemo.parse_ini_contents_("""
[common]
rad = 0.3
shift = 0.5
""")
INIDemo(radius=0.3, shift=0.5)
Command-line arguments using flags¶
Command-line argument, by default, are triggered by a long-style flag starting with two hyphens,
followed by the argument name in kebab-case (example: --radius
). The short flag variant is
disabled by default (example: -R
).
@dataclass(frozen=True)
class FlagDemo(Config):
env_prefix_ = "DEMO_"
radius: Annotated[float, Param.store(parsers.float_parser, short_flag_name='-R',
long_flag_name=None)]
shift: Annotated[float, Param.store(parsers.float_parser)]
scale: Annotated[float, Param.store(parsers.float_parser,
long_flag_name = Derived.KEBAB_CASE)] # or long_flag_name out as this is the default
FlagDemo.processor_().cl_handler.flags.keys()
dict_keys(['-R', '--shift', '--scale', '-h', '--help'])
Positional arguments¶
Positional arguments are command-line arguments that are not prefixed by a --flag
or -f
short
flag.
There can be more than one positional argument present. They are filled in order by the command-line strings that are not recognized as flags for other parameters.
Their order is given by the order of declaration in the dataclass.
We advise setting long_flag_name
to None
as using a parameter both in a positional manner
and with flags can lead to counter-intuitive behavior.
@dataclass(frozen=True)
class PositionalDemo(Config):
shift: Annotated[float, Param.store(parsers.float_parser, positional=Positional.ONCE,
long_flag_name=None)]
radii: Annotated[Sequence[float], Param.append1(parsers.float_parser,
positional=Positional.ONE_OR_MORE, long_flag_name=None)]
PositionalDemo.from_command_line_(args=["1.0", "0.2", "0.4"])
PositionalDemo(shift=1.0, radii=[0.2, 0.4])