baldaquin.config — Configuration#

The module provides the abstract base class ConfigurationBase that users can inherit from in order to create concrete configuration objects with JSON I/O capabilities. This provides a convenient mechanism to create and update configuration objects suiting any specific need.

The basic ideas behind the mechanism implemented here is that:

  • each specific configuration has its own concrete class inheriting from ConfigurationBase (which is achieved with no boilerplate code);

  • the class contains a full set of default values for the configuration parameters, so that any instance of a configuration object is guaranteed to be valid at creation time—and, in addition, to remain valid as the parameter values are updated through the lifetime of the object;

  • type consistency is automatically enforced whenever a parameter is set or updated;

  • a minimal set of optional constraints can be enforced on any of the parameters;

  • a configuration object can be serialized/deserialized in JSON format so that it can be written to file, and the parameter values can be updated from file.

In the remaining of this section we shall see how to declare, instantiate and interact with concrete configuration objects.

Declaring a concrete configuration class#

A simple, and yet fully functional, concrete configuration class for an hypothetical network connection might look like the following snippet—all you really have to do is to override the TITLE and PARAMETER_SPECS top-level class members.

class SampleConfiguration(ConfigurationBase):

    TITLE = 'A simple test configuration'
    PARAMETER_SPECS = (
        ('enabled', 'bool', True, 'Enable connection', None, None, {}),
        ('ip_address', 'str', '127.0.0.1', 'IP address', None, None, {}),
        ('port', 'int', 20004, 'UDP port', None, None, dict(min=1024, max=65535)),
        ('timeout', 'float', 10., 'Connection timeout', 's', '.3f', dict(min=0.))
    )

While the TITLE thing is pretty much self-explaining, the parameter specification deserves some more in-depth explanation. PARAMETER_SPECS is supposed to be an iterable of tuples matching the ConfigurationParameter constructor, namely:

  1. the paramater name (str);

  2. the name of the parameter type (str);

  3. the default value—note its type should match that indicated by the previous name;

  4. a string expressing the intent of the parameter;

  5. an optional string indicating the physical units of the parameter value;

  6. an optional format string for the preferred rendering of the value;

  7. an optional dictionary encapsulating the constraints on the possible parameter values.

In this case we are saying that the integer port parameter should be within 1024 and 65535, and that the floating-point timeout parameter should be positive.

The supported optional constraints for the various data types are:

  • choices, step, min and max for integers;

  • min and max for floats;

  • choices for strings.

(And if you look this closely enough you will recognize that the constraints are designed so that the map naturally to the GUI widgets that might be used to control the configuration, e.g., spin boxes for integers, and combo boxes for strings to pulled out of a pre-defined list.)

Once you have a concrete class defined, you can instantiate an object, which will come up set up and ready to use, with all the default parameter values.

>>> from baldaquin.config import SampleConfiguration
>>> config = SampleConfiguration()
>>> print(config)
--------------------------------------------------------------------------------
A simple test configuration
--------------------------------------------------------------------------------
enabled.............: True
ip_address..........: 127.0.0.1
port................: 20004 {'min': 1024, 'max': 65535}
timeout.............: 10.000 s {'min': 0.0}
--------------------------------------------------------------------------------

Programmatically, you can retrieve the value of a specific parameter through the value() class method, and update the value with update_value().

>>> print(config.value('port'))
20004
>>> config.update_value('port', 20005)
<ParameterValidationError.PARAMETER_VALID: 0>
>>> print(config.value('port'))
20005
>>>

The save() method allows to dump the (JSON-encoded) content of the configuration into file looking like

{
    "enabled": {
        "name": "enabled",
        "type_name": "bool",
        "value": true,
        "intent": "Enable connection",
        "units": null,
        "fmt": null,
        "constraints": {}
    },
    "ip_address": {
        "name": "ip_address",
        "type_name": "str",
        "value": "127.0.0.1",
        "intent": "IP address",
        "units": null,
        "fmt": null,
        "constraints": {}
    },
    "port": {
        "name": "port",
        "type_name": "int",
        "value": 20003,
        "intent": "UDP port",
        "units": null,
        "fmt": null,
        "constraints": {
            "min": 1024,
            "max": 65535
        }
    },
    "timeout": {
        "name": "timeout",
        "type_name": "float",
        "value": 10.0,
        "intent": "Connection timeout",
        "units": "s",
        "fmt": ".3f",
        "constraints": {
            "min": 0.0
        }
    }
}

and this is the basic mechanism through which applications will interact with configuration objects, with update() allowing to update an existing configuration from a JSON file with the proper format.

Note

Keep in mind that configurations are never read from file—they come to life with all the parameters set to their default values, and then they can be updated from a JSON file.

When you think about, this makes extending and/or modifying existing configurations much easier as, once the concrete class is changed, all existing configuration files are automatically updated transparantly, and in case one edits a file by hand, any mistake will be promptly signaled (and corrected) without compromising the validity of the configuration object.

Module documentation#

Configuration facilities.

class baldaquin.config.ParameterValidationError(value)[source]#

Enum class for the possible errors occurring while checking the input parameter values.

PARAMETER_VALID = 0#
INVALID_TYPE = 1#
NUMBER_TOO_SMALL = 2#
NUMBER_TOO_LARGE = 3#
INVALID_CHOICE = 4#
INVALID_STEP = 5#
GENERIC_ERROR = 6#
class baldaquin.config.ConfigurationParameter(name: str, type_name: str, value: Any, intent: str, units: str | None = None, fmt: None = None, **constraints)[source]#

Class representing a configuration parameter.

This is a simple attempt at putting in place a generic configuration mechanism where we have some control on the values we are passing along.

A configuration parameter is fully specified by its name, type and value, and When setting the latter, we make sure that the its type matches. Additional, we can specify simple conditions on the parameters that are then enforced at runtime.

Parameters:
  • name (str) – The parameter name.

  • type_name (str) – The name of the parameter type.

  • value (anything) – The parameter value.

  • intent (str) – The intent of the parameter, acting as a comment in the corresponding configuration file.

  • units (str, optional) – The units for the configuration parameter.

  • fmt (str, optional) – An optional format string for the preferred rendering the parameter value.

  • constraints (dict, optional) – A dictionary containing optional specifications on the parameter value.

VALID_CONSTRAINTS = {'float': ('min', 'max'), 'int': ('choices', 'step', 'min', 'max'), 'str': ('choices',)}#
not_set() bool[source]#

Return true if the parameter value is not set.

_validation_error(value: Any, error_code: ParameterValidationError) ParameterValidationError[source]#

Utility function to log a parameter error (and forward the error code).

_check_range(value: Any) ParameterValidationError[source]#

Generic function to check that a given value is within a specified range.

This is used for validating int and float parameters.

_check_choice(value: Any) ParameterValidationError[source]#

Generic function to check that a parameter value is within the allowed choices.

_check_step(value: int) ParameterValidationError[source]#

Generic function to check the step size for an integer.

_check_int(value: int) ParameterValidationError[source]#

Validate an integer parameter value.

Note we check the choice specification first, and all the others after that (this is relevant as, if you provide inconsistent conditions the order becomes relevant).

_check_float(value: float) ParameterValidationError[source]#

Validate a floating-point parameter value.

_check_str(value: str) ParameterValidationError[source]#

Validate a string parameter value.

set_value(value: Any) ParameterValidationError[source]#

Set the paramater value.

class baldaquin.config.ConfigurationBase[source]#

Base class for configuration data structures.

The basic idea, here, is that specific configuration classes simply override the TITLE and PARAMETER_SPECS class members. PARAMETER_SPECS, particulalry, encodes the name, types and default values for all the configuration parameters, as well optional help strings and parameter constraints.

Configuration objects provide file I/O through the JSON protocol. One important notion, here, is that configuration objects are always created in place with all the parameters set to their default values, and then updated from a configuration file. This ensures that the configuration is always valid, and provides an effective mechanism to be robust against updates of the configuration structure.

TITLE = 'Configuration'#
PARAMETER_SPECS = ()#
add_parameter(*args, **kwargs) None[source]#

Add a new parameter to the configuration.

value(key) Any[source]#

Return the value for a given parameter.

update_value(key, value) ParameterValidationError[source]#

Update the value of a configuration parameter.

update(file_path: str) None[source]#

Update the configuration parameters from a JSON file.

to_json() str[source]#

Encode the configuration into JSON to be written to file.

save(file_path) None[source]#

Dump the configuration to a JSON file.

static terminal_line(character: str = '-', default_length: int = 50) str[source]#

Concatenate a series of characters as long as the terminal line.

Note that we need the try/except block to get this thing working into pytest—see https://stackoverflow.com/questions/63345739

static title(text: str) str[source]#

Pretty-print title.

class baldaquin.config.EmptyConfiguration[source]#

Empty configuration.

TITLE = 'Empty configuration'#
class baldaquin.config.SampleConfiguration[source]#

Sample configuration.

TITLE = 'A simple test configuration'#
PARAMETER_SPECS = (('enabled', 'bool', True, 'Enable connection', None, None, {}), ('protocol', 'str', 'UDP', 'Communication protocol', None, None, {'choices': ('UDP', 'TCP/IP')}), ('ip_address', 'str', '127.0.0.1', 'IP address', None, None, {}), ('port', 'int', 20004, 'Port', None, None, {'max': 65535, 'min': 1024}), ('timeout', 'float', 10.0, 'Connection timeout', 's', '.3f', {'min': 0.0}))#