14. config — A general yet simple configuration class.

(C) 2005, 2021 Benedict Verhegghe
Distributed under the GNU GPL version 3 or later
Why

I wrote this simple class because I wanted to use Python expressions in my configuration files. This is so much more fun than using .INI style config files. While there are some other Python config modules available on the web, I couldn’t find one that suited my needs and my taste: either they are intended for more complex configuration needs than mine, or they do not work with the simple Python syntax I expected.

What

Our Config class is just a normal Python dictionary which can hold anything. Fields can be accessed either as dictionary lookup (config[‘foo’]) or as object attributes (config.foo). The class provides a function for reading the dictionary from a flat text (multiline string or file). I will always use the word ‘file’ hereafter, because that is what you usually will read the configuration from. Your configuration file can have named sections. Sections are stored as other Python dicts inside the top Config dictionary. The current version is limited to one level of sectioning.

14.1. Classes defined in module config

class config.Section(default_factory=None, *args, **kargs)[source]

A class for storing a section in a Config.

This is an empty class definition. It only exists to give the sections another name.

class config.Config(data={}, default=<function Dict.returnNone>)[source]

A configuration class allowing Python expressions in the input.

The Config class is a subclass of the Dict mapping, which provides access to items by either dict-style key lookup config['foo'], attribute syntax config.foo or call syntax config('foo'). Furthermore, the Dict class allows a default_factory function to lookup in another Dict. This allows a chain of Config objects: session_prefs -> user_prefs -> factory_defaults.

The Config class is different from its parent Dict class in two ways:

  • Keys should only be strings that are valid Python variable names, except that they can also contain a ‘/’ character. The ‘/’ splits up the key in two parts: the first part becomes a key in the Config, and its value is a Section dict where the values are stored with the second part of the key. This allows for creating sections in the configuration. Currently only one level of sectioning is allowed (keys can only have a single ‘/’ character.

  • A Config instance can be initialized or updated with a text in Python source style. This provides an easy way to store configurations in files with Python style. The Config class also provides a way to export its contents to such a Python source text: updated configurations can thus be written back to a configuration file. See Notes below for details.

Parameters
  • data (dict or multiline string, optional) – Data to initialize the Config. If a dict, all keys should follow the rules for valid config keys formulated above. If a multiline string, it should be an executable Python source text, with the limitations and exceptions outlined in the Notes below.

  • default (Config object, optional) – If provided, this object will be used as default lookup for missing keys.

Notes

The configuration object can be initialized from a dict or from a multiline string. Using a dict is obvious: one only has to obey the restriction that keys should be valid Python variable names.

The format of the multiline config text is described hereafter. This is also the format in which config files are written and can be loaded.

All config lines should have the format: key = value, where key is a string and value is a Python expression The first ‘=’ character on the line is the delimiter between key and value. Blanks around both the key and the value are stripped. The value is then evaluated as a Python expression and stored in a variable with name specified by the key. This variable is available for use in subsequent configuration lines. It is an error to use a variable before it is defined. The key,value pair is also stored in the Config dictionary, unless the key starts with an underscore (‘_’): this provides for local variables.

Lines starting with ‘#’ are comments and are ignored, as are empty and blank lines. Lines ending with ‘’ are continued on the next line. A line starting with ‘[‘ starts a new section. A section is nothing more than a Python dictionary inside the Config dictionary. The section name is delimited by ‘[‘and ‘]’. All subsequent lines will be stored in the section dictionary instead of the toplevel dictionary.

All other lines are executed as Python statements. This allows e.g. for importing modules.

Whole dictionaries can be inserted at once in the Config with the update() function.

All defined variables while reading config files remain available for use in the config file statements, even over multiple calls to the read() function. Variables inserted with addSection() will not be available as individual variables though, but can be accessed as self['name'].

As far as the resulting Config contents is concerned, the following are equivalent:

C.update({'key':'value'})
C.read("key='value'\n")

There is an important difference though: the second line will make a variable key (with value ‘value’) available in subsequent Config read() method calls.

Examples

>>> C = Config('''# A simple config example
...     aa = 'bb'
...     bb = aa
...     [cc]
...     aa = 'aa'    # yes ! comments are allowed)
...     _n = 3       # local: will get stripped
...     rng = list(range(_n))
...     ''')
>>> C
Config({'aa': 'bb', 'bb': 'bb', 'cc': Section({'aa': 'aa', 'rng': [0, 1, 2]})})
>>> C['aa']
'bb'
>>> C['cc']
Section({'aa': 'aa', 'rng': [0, 1, 2]})
>>> C['cc/aa']
'aa'
>>> C.keys()
['aa', 'bb', 'cc', 'cc/aa', 'cc/rng']

Create a new Config with default lookup in C

>>> D = Config(default=C)
>>> D
Config({})
>>> D['aa']       # Get from C
'bb'
>>> D['cc']       # Get from C
Section({'aa': 'aa', 'rng': [0, 1, 2]})
>>> D['cc/aa']            # Get from C
'aa'
>>> D.get('cc/aa','zorro')      # but get method does not cascade!
'zorro'
>>> D.keys()
[]

Setting values in D will store them in D while C remains unchanged.

>>> D['aa'] = 'wel'
>>> D['dd'] = 'hoe'
>>> D['cc/aa'] = 'ziedewel'
>>> D
Config({'aa': 'wel', 'dd': 'hoe', 'cc': Section({'aa': 'ziedewel'})})
>>> C
Config({'aa': 'bb', 'bb': 'bb', 'cc': Section({'aa': 'aa', 'rng': [0, 1, 2]})})
>>> D['cc/aa']
'ziedewel'
>>> D['cc']
Section({'aa': 'ziedewel'})
>>> D['cc/rng']
[0, 1, 2]
>>> 'bb' in D
False
>>> 'cc/aa' in D
True
>>> 'cc/ee' in D
False
>>> D['cc/bb'] = 'ok'
>>> D.keys()
['aa', 'dd', 'cc', 'cc/aa', 'cc/bb']
>>> del D['aa']
>>> del D['cc/aa']
>>> D.keys()
['dd', 'cc', 'cc/bb']
>>> D.sections()
['cc']
>>> del D['cc']
>>> D.keys()
['dd']

14.2. Functions defined in module config

config.formatDict(d)[source]

Format a Config in Python source representation.

Each (key,value) pair is formatted on a line of the form:

key = value

If all the keys are strings containing only characters that are allowed in Python variable names, the resulting text is a legal Python script to define the items in the dict. It can be stored on a file and executed.

This format is the storage format of the Config class.

Examples

>>> print(formatDict({'str':'abc', 'path':Path(), 'int':4}))
str = 'abc'
path = Path('.')
int = 4