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:
- 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.
- 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)