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 to KEBAB_CASE, by default)

  • command-line arguments, after a long flag (i.e. --flag), if long_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), if short_flag_name is set (it is not, by default),

  • positional command-line arguments, if positional is not None (it is None 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])