############# configmanager ############# - `Documentation `_ - `Source Code and Issue Tracker on github.com `_ - `Build Status on travis-ci.org `_ ===================== Questions and Answers ===================== .. contents:: :local: What is it? ----------- - 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. How is it different from others? -------------------------------- - 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. How do I install it? -------------------- .. code-block:: shell pip install configmanager How do I use it? ---------------- 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: .. code-block:: python >>> 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: .. code-block:: python >>> 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. How do I read configuration from files? --------------------------------------- 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: .. code-block:: python :emphasize-lines: 3-4 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: .. code-block:: python config.configparser.load('/etc/helloworld/config.ini') config.yaml.load('~/.config/helloworld/config.yaml') config.json.load('~/.config/helloworld/config.json') How do I write configuration to files? -------------------------------------- Similarly to reading, you find the appropriate persistence adapter, and use the dump method on it: .. code-block:: python 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. How do I export all configuration values to a dictionary? --------------------------------------------------------- You can export effective values with :meth:`.Section.dump_values` method: .. code-block:: python >>> 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. How do I read configuration values from a dictionary? ----------------------------------------------------- .. code-block:: python config.load_values({ 'greeting': 'Hey!', }) Where is all the richness? -------------------------- The richness lies in configuration items: .. code-block:: python >>> greeting = config.greeting >>> greeting >>> 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!' How to create an item with no default value? -------------------------------------------- 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: .. code-block:: python # Option 1 from configmanager import Item config.add_schema({'enabled': Item(required=True)}) # Option 2 config.add_schema({'enabled': {'@required': True}}) How to provide a fallback value for an item with no default value? ------------------------------------------------------------------ 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 How to add a dynamic attribute to all items? -------------------------------------------- .. code-block:: python 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!' How to do something with all configuration items? ------------------------------------------------- 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. .. code-block:: python 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: .. code-block:: python 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!'}) How to allow item value override through an environment variable? ----------------------------------------------------------------- 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``: .. code-block:: python :emphasize-lines: 5,13 # 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: .. code-block:: python :emphasize-lines: 4 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: .. code-block:: python :emphasize-lines: 4,8-10 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. How to handle non-existent configuration items? ----------------------------------------------- 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: .. code-block:: python @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. How to set temporary configuration? ----------------------------------- 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. .. code-block:: python 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: .. code-block:: python with config({'greeting': 'Bon jour!'}): # do some French things here pass Note that you cannot pass keyword arguments there, just a dictionary. How do I manage changesets of config values? -------------------------------------------- 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. .. code-block:: python :emphasize-lines: 4,7,10,13,14 >>> 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=, new_value='Hey, what is up!', old_raw_str_value=, 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.