Skip to content
This repository was archived by the owner on Feb 11, 2023. It is now read-only.

Files

Latest commit

 

History

History
394 lines (260 loc) · 11.9 KB

index.rst

File metadata and controls

394 lines (260 loc) · 11.9 KB

configmanager

Questions and Answers

  • If you come from the world of Django, it is the settings object you import from django.conf.
  • If you have built one before, you probably built it around standard library's ConfigParser, or perhaps just a plain dictionary.
  • It is the party responsible for loading the configuration from different sources and for making it accessible to the rest of your application through uniform interface.
  • It requires you to declare your configuration schema beforehand.
  • Every configuration item is a rich object with a type, a name, a default value, a custom value, and other attributes. All these and other, easy-to-add attributes can be used to calculate configuration item's effective value when application requests it.
  • It allows you to choose how you will access values of configuration items: through dictionary-like access, through attributes, or through method calls.
  • It does not limit you to a specific configuration file format, or to a specific limit of configuration tree depth.
  • It can be easily composed of other configuration managers.
  • It does not support value interpolation.
pip install configmanager

It depends on what you are after. If you are just looking for something to parse different files and put the values in one object, then you want to use PlainConfig interface:

>>> from configmanager import PlainConfig

>>> config = PlainConfig(schema={'greeting': 'Hello, world!'})

>>> config.greeting
'Hello, world!'

If you are after the rich configuration item functionality which configmanager was designed for, then you want to use Config interface:

>>> from configmanager import Config

>>> config = Config(schema={'greeting': 'Hello, world!'})

>>> config.greeting.value
'Hello, world!'

Further answers will assume that you are using the rich Config interface.

If there is a list of locations that you want to always inspect when initialising the configuration manager, you can pass them using the load_sources= setting of configmanager. Make sure you also enable auto-loading:

config = Config(
    schema={'greeting': 'Hello, world'},
    load_sources=['/etc/helloworld/config.ini', '~/.config/helloworld/config.json'],
    auto_load=True,
)

If you want to reload these same sources later, or load them for the first time because you didn't specify auto_load=True, you can do so with config.load().

To load configuration from a specific file at a later point in manager's lifetime, you can use load(source) method on the appropriate persistence adapter:

config.configparser.load('/etc/helloworld/config.ini')
config.yaml.load('~/.config/helloworld/config.yaml')
config.json.load('~/.config/helloworld/config.json')

Similarly to reading, you find the appropriate persistence adapter, and use the dump method on it:

config.json.dump('~/.config/helloworld/config.json', with_defaults=True)

Unless you also pass with_defaults=True, dump will exclude values for items who have no custom value set.

You can export effective values with :meth:`.Section.dump_values` method:

>>> config.dump_values()
{'greeting': 'Hello, world!'}

By default, :meth:`.Section.dump_values` includes values for all items which have a custom value or a default value. You can also dump just custom values with with_defaults=False which may result in an empty dictionary if none of your configuration items have custom values.

config.load_values({
    'greeting': 'Hey!',
})

The richness lies in configuration items:

>>> greeting = config.greeting

>>> greeting
<Item greeting 'Hello, world!'>

>>> greeting.has_value
True

>>> greeting.default
'Hello, world!'

>>> greeting.is_default
True

>>> greeting.value = 'Hey!'
>>> greeting.value
'Hey!'

>>> greeting.is_default
False

>>> greeting.reset()
>>> greeting.value
'Hello, world!'

In normal circumstances, we consider a configuration item with no default value an anti-pattern. However, if you want to force your application user to provide a value for an item for which no default value would be acceptable, for example, it can be done either by using an explicit Item instance in configuration schema, or by using dictionary notation with meta keys:

# Option 1
from configmanager import Item
config.add_schema({'enabled': Item(required=True)})

# Option 2
config.add_schema({'enabled': {'@required': True}})

Once you have a reference to the item, you can call its .get(fallback) method:

>>> config.enabled.get(False)
False
>>> config.enabled.value
# .. stack-trace skipped ..
configmanager.exceptions.RequiredValueMissing: enabled
config = Config({'greeting': 'Hello, world!'})

@config.item_attribute
def all_caps_value(item=None, **kwargs):
    return item.value.upper()

assert config.greeting.all_caps_value == 'HELLO, WORLD!'

If you need to work with items after the configuration tree has been fully constructed, you can iterate over all items with config.iter_items() which can be customised in many different ways.

for path, item in config.iter_items(recursive=True):
    print(path, item.is_default)

If you need to process item objects during configuration schema parsing, you can register an item_added_to_section hook before adding schemas:

config = Config()

@config.hooks.item_added_to_section
def item_added_to_section(subject=None, section=None, **kwargs):
    print('Item {} was added to a section').format(subject.name)

# Add schemas afterwards
config.add_schema({'greeting': 'Hello, world!'})

If you have meaningful section names and you don't mind configmanager's default naming schema, then you can just declare the particular items with envvar=True:

# dictionary notation
config = Config({
    'greeting': {
        '@default': 'Hello, world!',
        '@envvar': True,
    },
})

# same thing with object notation
config = Config({
    'greeting': Item(
        default='Hello, world!',
        envvar=True,
    ),
})

Now, to set a value override, your application user would have to set environment variable GREETING. Had the greeting item been declared under a section called hello_world, you would have to override it by setting HELLO_WORLD_GREETING.

If this is not up to your taste, you can specify a custom environment variable name by replacing envvar=True with something more likeable:

config = Config({
    'greeting': Item(
        default='Hello, world!',
        envvar='MY_APP_GREETING',
    ),
})

If you want to generate a custom environment variable name dynamically based on item for which the environment variable name is requested, you can do so by overriding envvar_name attribute:

config = Config({
    'greeting': {
        '@default': 'Hello, world',
        '@envvar': True,
    }
})

@config.item_attribute
def envvar_name(item=None, **kwargs):
    return 'GGG_{}'.format('_'.join(item.get_path()).upper())

assert config.greeting.envvar_name == 'GGG_GREETING'

Note that when calculating item value, config.greeting.envvar_name is only consulted if config.greeting.envvar is set to True. If it is set to a string, that will be used instead. Or, if it is set to a falsy value, environment variables won't be consulted at all.

If you request a non-existent configuration item, a :class:`.NotFound` exception is raised. You could catch these as any other Python exception, or you could register a callback function to be called when this exception is raised:

@config.hooks.not_found
def not_found(name, section):
    print('A section or item called {} was requested, but it does not exist'.format(name))

If this function returns anything other than None, the exception will not be raised.

When writing unit tests, or in other scenarios when you need to change configuration briefly just to execute some particular part of your code, you can create an auto-resetting configuration context by calling your Config instance as a function.

with config():
    config.greeting.value = 'Bon jour!'

    # do some French things here
    pass

# French settings have been reset:
assert config.greeting.get() == 'Hello, world!'

If you prefer to set the temporary configuration on initialisation of the context, you can do so by passing a values dictionary:

with config({'greeting': 'Bon jour!'}):
    # do some French things here
    pass

Note that you cannot pass keyword arguments there, just a dictionary.

The previous example used a special case of what we call a changeset context. You can explicitly create one with :meth:`.Config.changeset_context`.

Unlike the special context demonstrated above, a default changeset context does not reset changes made while program control was inside it.

>>> config.greeting.get()
'Hello, world!'

>>> with config.changeset_context() as ctx:
...     config.greeting.set('Hey, what is up!')

>>> len(ctx)
1

>>> ctx.values[config.greeting]
'Hey, what is up!'

>>> ctx.changes[config.greeting]
Change(old_value=<NotSet>, new_value='Hey, what is up!', old_raw_str_value=<NotSet>, new_raw_str_value='Hey, what is up!')

>>> ctx.reset()
>>> ctx.changes
{}

>>> config.greeting.get()
'Hello, world!'

A changeset context comes handy when you want to create a sub-context of changes which you want to be able to export or persist separately from the rest of configuration changes.