Guide

Sections

A Config object can be used directly without any initialization:

import profig
cfg = profig.Config()
cfg['server.host'] = '8.8.8.8'
cfg['server.port'] = 8181

Configuration keys are . delimited strings where each name refers to a section. In the example above, server is a section name, and host and port are sections which are assigned values.

For a quick look at the hierarchical structure of the options, you can easily get the dict representation:

>>> cfg.as_dict()
{'server': {'host': '8.8.8.8', 'port': 8181}}

Section can also be organized and accessed in hierarchies:

>>> cfg['server.port']
8181
>>> cfg.section('server').as_dict()
{'host': 'localhost', 'port': 8181}

Initialization

One of the most useful features of the profig library is the ability to initialize the options that are expected to be set on a Config object.

If you were to sync the following config file without any initialization:

[server]
host = 127.0.0.1
port = 8080

The port number would be interpreted as a string, and you would have to convert it manually. You can avoid doing this each time you access the value by using the init() method:

>>> cfg.init('server.host', '127.0.0.1')
>>> cfg.init('server.port', 8080, int)

The second argument to init() specifies a default value for the option. The third argument is optional and is used by an internal Coercer to automatically (de)serialize any value that is set for the option. If the third argument is not provided, the type of the default value will be used to select the correct coercer.

Strict Mode

Strict mode can be enabled by passing strict=True to the Config constructor:

>>> cfg = profig.Config(strict=True)
# or
>>> cfg.strict = True

By default, Config objects can be assigned a new value simply by setting the value on a key, just as with the standard dict. When strict mode is enabled, however, assignments are only allowed for keys that have already been initialized using init().

Strict mode prevents typos in the names of configuration options. In addition, any sync performed while in strict mode will clear old or deprecated options from the output file.

Automatic Typing (Coercion)

Automatic typing is referred to as “coercion” within the profig library.

Functions that adapt (object -> string) or convert (string -> object) values are referred to as “coercers”.

  • Values are “adapted” into strings.
  • Strings are “converted” into values.

Defining Types

Type information can be assigned by initializing a key with a default value:

>>> cfg.init('server.port', 8080)
>>> cfg.sync()
>>> port = cfg['server.port']
>>> port
9090
>>> type(port)
<class 'int'>

When a type cannot be easily inferred, a specific type can by specified as the third argument to init():

>>> cfg.init('editor.open_files', [], 'path_list')

In this case we are using a custom coercer type that has been registered to handle lists of paths. Now, supposing we have the following in a config file:

[editor]
open_files = profig.py:test_profig.py

We can the sync the config file, and access the stored value:

>>> cfg.sync()
>>> cfg['editor.open_files']
['profig.py', 'test_profig.py']

We can look at how the value is stored in the config file by using the section directly:

>>> sect = cfg.section('editor.open_files')
>>> sect.value(convert=False)
'profig.py:test_profig.py'

Note

When using coercion, it is important to take into account that, by default, no validation of the values is performed. It is, however, simple to have a coercer validate as well. See Using Coercers as Validators for details.

Adding Coercers

Functions that define how an object is coerced (adapted or converted) can be defined using the Coercer object that is accessible as an attribute of the root Config object.

Defining a new coercer (or overriding an existing one) is as simple as passing two functions to register(). For example, a simple coercer for a bool type could be defined like this:

>>> cfg.coercer.register(bool, lambda x: str(int(x)), lambda x: bool(int(x)))

The first argument to register() represents a type value that will be used to determine the correct coercer to use. A class object, when available, is most convenient, because it allows using a call to type(obj) to determine which coercer to use. However, any value can be used for the registration, including, e.g. strings or tuples.

The second argument specifies how to adapt a value to a string, while the third argument specifies host to convert a string to a value.

Using Coercers as Validators

By default, a coercer will only raise exceptions if there is a fundamental incompatibility in the values it is trying to coerce. What this means is that even if you register a key with a type of int, no restriction is placed on the value that can actually be set for the key. This can lead to unexpected errors:

>>> cfg.init('default.value', 1) # sets the type of the key to 'int'
>>> cfg['default.value'] = 4.5   # a float value can be set

# sync the float value to the config file
>>> buf = io.BytesIO()
>>> cfg.sync(buf)
>>> buf.getvalue()
'[default]\nvalue: 4.5\n'

# here, we change the float value
>>> buf = io.BytesIO('[default]\nvalue: 5.6\n')
>>> cfg.sync(buf)
>>> cfg['default.value'] # this now raises an exception
Traceback (most recent call last):
...
ConvertError: invalid literal for int() with base 10: '5.6'

This behavior can be changed by defining or overriding coercers in order to, for example, raise exceptions for unexpected ranges of inputs or other restrictions that should be in place for a given configuration value.

Synchronization

A sync is a combination of the read and write operations, executed in the following order:

  1. Read in the changed values from all sources, except the values that have changed on the config object since the last time it was synced.
  2. Write any values changed on the config object back to the primary source.

This is done using the sync() method:

>>> cfg.sync('app.cfg')

After a call to sync(), a new file with the following contents will be created in the current working directory:

[server]
host = 127.0.0.1
port = 8080

Sources

The sources a config object uses when it syncs can also be set in the Config constructor:

>>> cfg = profig.Config('app.cfg')

Sources can be either paths or file objects. Multiple sources can be provided:

>>> cfg = profig.Config('<appdir>/app.cfg', '<userdir>/app.cfg', '<sysdir>/app.cfg')

If more than one source is provided then a sync will update the config object with values from each of the sources in the order given. It will then write the values changed on the config object back to the first source.

Once sources have been provided, they will be used for any future syncs.

>>> cfg.sync()

Formats

A Format subclass defines how a configuration should be read/written from/to a source.

profig provides Format subclasses for both INI formatted files, and the Windows registry.

INI

INIFormat
Syncs configuration with a file based on the standard INI format.

Because INIFormat is the default, an INI file can be sourced as follows:

>>> cfg = profig.Config('~/.config/black_knight.cfg')

The INI file format has no strict standard, however profig tries not to stray far from the most commonly supported features. In particular, it should read any INI file that the standard library’s configparser can read. If it does not, please file an issue so that the discrepency can be fixed.

The only deviation profig makes is to support the fact that it is possible to set a value on any ConfigSection, including those at the top-level, which become section headers when output to an INI file. This is represented in the INI files as values set on the header:

[server] = true
host = localhost

Windows Registry

RegistryFormat
Syncs configuration with the Windows registry.

To use the RegistryFormat, you have to specify which format to use and specify a path relative to HKEY_CURRENT_USER (configurable as a class attribute):

>>> cfg = profig.Config(r'Software\BlackKnight', format='registry')

RegistryFormat is, of course, only available on Windows machines.

Support for additional formats can be added easily by subclassing Format.

Defining a new Format

To add support for a new format you must subclass Format() and override the, read() and write() methods.

read() should assign values to config, which is a local instance of the Config object. A context value can optionally be returned which will be passed to write() during a sync. It can be used, for example, to track comments and ordering of the source. None can be returned if no context is needed.

write() accepts any context returned from a call to read() and should read the values to write from config.

Unicode

profig fully supports unicode. The encoding to use for all encoding/decoding operations can be set in the Config constructor:

>>> cfg = profig.Config(encoding='utf-8')

The default is to use the system’s preferred encoding.

Keys are handled in a special way. Both byte-string keys and unicode keys are considered equivalent, but are stored internally as unicode keys. If a byte-string key is used, it will be decoded using the configured encoding.

Values are coerced into a unicode representation before being output to a config file. Note that this means that by specifically storing a byte-string as a value, profig will interpret the value as binary data. The specifics of how binary data is stored depends on the format being used. The default format (INIFormat) operates on binary files, therefore binary data is written directly to its sources.

So, if we consider the following examples using a Python 2 interpreter, where str objects are byte-strings:

>>> cfg['default.a'] = '\x00asdf'

Will be stored to a config file as binary data:

[default]
a = \x00asdf

If this is not the desired behavior, there are other options available when calling init(). First, we can explicitely use unicode values:

>>> cfg.init('a', u'asdf')

This ensures that only data matching the set encoding will be accepted.

If we expect the data to be binary data, but don’t want to store it directly, we can use one of the available encodings: ‘hex’, and ‘base64’:

>>> cfg.init('a', 'asdf', 'hex')

Or third, we can register different coercers for byte-strings:

>>> cfg.coercer.register(bytes, compress, decompress)