Source code for argdeco.config

"""Working with configurations

A common pattern making use of configuration files::

    from argdeco import main, command, arg, opt, config_factory
    form os.path import expanduser

    main.configure(compiler_factory=config_factory(
        config_file=arg('--config-file', '-C', help="configuration file", default=expanduser('~/.config/myconfig.yaml'))
    ))

    @command('ls', opt('--all'))
    def mycmd(cfg):
        if cfg['ls.all']:
            pass

    main()

If you want to have ``foo.bar`` expanded to ``{'foo': {'bar': ...}}``, use
following::

    from argdeco import main, command, arg, opt, config_factory, ConfigDict
    form os.path import expanduser

    main.configure(compiler_factory=config_factory(ConfigDict,
        config_file=arg('--config-file', '-C', help="configuration file", default=expanduser('~/.config/myconfig.yaml'))
    ))

    @command('ls', opt('--all'))
    def mycmd(cfg):
        if cfg['ls']['all']:
            pass

    main()


"""

import logging
log = logging.getLogger('argdeco.config')
log.setLevel(logging.NOTSET)

[docs]class ConfigDict(dict): '''dictionary-like class This class implements a dictionary, which creates deep objects from keys like "foo.bar". Example: >>> c = Config() >>> c['foo.bar'] = 'x' >>> c {'foo': {'bar': 'x'}} >>> c['foo.bar'] 'x' ''' def __init__(self, E=None, **F): super(ConfigDict, self).__init__() self.update(E, **F) def __getitem__(self, name): key_parts = name.split('.') value = super(ConfigDict, self).__getitem__(key_parts[0]) for k in key_parts[1:]: if isinstance(value, dict): value = value[k] else: raise KeyError(name) return value def __setitem__(self, name, value): key_parts = name.split('.') if len(key_parts) == 1: super(ConfigDict, self).__setitem__(key_parts[0], value) else: try: val = super(ConfigDict, self).__getitem__(key_parts[0]) except KeyError: super(ConfigDict, self).__setitem__(key_parts[0], self.__class__()) val = super(ConfigDict, self).__getitem__(key_parts[0]) for k in key_parts[1:-1]: if k not in val: val[k] = self.__class__() val = val[k] if isinstance(value, dict) and not isinstance(value, self.__class__): val[key_parts[-1]] = self.assimilate(value) else: val[key_parts[-1]] = value
[docs] def assimilate(self, value): '''If value is a dictionary, then make it beeing a dictionary of same class like this. Copy all attributes, which are not controlled by dict class ''' if not isinstance(value, dict): return value if isinstance(value, self.__class__): return value #log.debug("assimilate %s", value) result = self.__class__() result.update(value) for a in dir(value): if not hasattr({}, a): #log.debug("assimilate, %s => %s", a, getattr(value, a)) setattr(result, a, getattr(value, a)) return result
def __contains__(self, name): try: self[name] return True except KeyError: return False
[docs] def flatten(self, D): '''flatten a nested dictionary D to a flat dictionary nested keys are separated by '.' ''' if not isinstance(D, dict): return D result = {} for k,v in D.items(): if isinstance(v, dict): for _k,_v in self.flatten(v).items(): result['.'.join([k,_k])] = _v else: result[k] = v return result
[docs] def update(self, E=None, **F): '''flatten nested dictionaries to update pathwise >>> Config({'foo': {'bar': 'glork'}}).update({'foo': {'blub': 'bla'}}) {'foo': {'bar': 'glork', 'blub': 'bla'} In contrast to: >>> {'foo': {'bar': 'glork'}}.update({'foo': {'blub': 'bla'}}) {'foo: {'blub': 'bla'}'} ''' def _update(D): for k,v in D.items(): if super(ConfigDict, self).__contains__(k): if isinstance(self[k], ConfigDict): self[k].update(v) else: self[k] = self.assimilate(v) else: self[k] = self.assimilate(v) if E is not None: if not hasattr(E, 'keys'): E = self.assimilate(dict(E)) _update(E) _update(F) return self
[docs]def config_factory(ConfigClass=dict, prefix=None, config_file=None ): '''return a class, which implements the compiler_factory API :param ConfigClass: defaults to dict. A simple factory (without parameter) for a dictionary-like object, which implements __setitem__() method. Additionally you can implement following methods: :``init_args``: A method to be called to initialize the config object by passing :py:class:`~argparse.Namespace` object resulting from :py:class:`~argparse.ArgumentParser.parseargs` method. You could load data from a configuration file here. :``compile_args``: A method, which can return the same like a ``compile`` function does. If there is no such method, a tuple with a ConfigClass instance as single element is returned. :param prefix: Add this prefix to config_name. (e.g. if prefix="foo" and you have config_name="x.y" final config_path results in "foo.x.y") :param config_file: An :py:class:`~argdeco.arguments.arg` to provide a config file. If you provide this argument, you can implement one of the following methods in your ``ConfigClass`` to load data from the configfile: :``load``: If you pass ``config_file`` argument, this method can be implemented to load configuration data from resulting stream. If config_file is '-', stdin stream is passed. :``load_from_file``: If you prefer to open the file yourself, you can do this, by implementing ``load_from_file`` instead which has the filename as its single argument. :``update``: method like :py:meth:`dict.update`. If neither of ``load`` or ``load_from_file`` is present, but ``update`` is, it is assumed, that config_file is of type YAML (or JSON) and configuration is updated by calling ``update`` with the parsed data as parameter. If you implement neither of these, it is assumed, that configuration file is of type YAML (or plain JSON, as YAML is a superset of it). Data is loaded from file and will update configuration object using dict-like :py:meth:`dict.update` method. :type config_file: argdeco.arguments.arg :returns: ConfigFactory class, which implements compiler_factory API. ''' config_factory = ConfigClass class ConfigFactory: def __init__(self, command): self.command = command log.debug("command: %s", command) log.debug("config_file: %s", config_file) if config_file: from .arguments import arg assert isinstance(config_file, arg), "config_file must be of type arg" try: self.command.add_argument(config_file) except: pass def __call__(self, args, **opts): cfg = ConfigClass() if hasattr(cfg, 'init_args'): cfg.init_args(args) if config_file is not None: if hasattr(args, config_file.dest): fn = getattr(args, config_file.dest) if fn is not None: if hasattr(cfg, 'load'): if config_file.dest == '-': cfg.load(sys.stdin) else: with open(fn, 'r') as f: cfg.load(f) elif hasattr(cfg, 'load_from_file'): cfg.load_from_file(fn) elif hasattr(cfg, 'update'): # assume yaml file import yaml with open(fn, 'r') as f: data = yaml.load(f) cfg.update(data) for k,v in opts.items(): config_name = self.command.get_config_name(args.action, k) if config_name is None: continue if config_name.startswith('.'): config_name = config_name[1:] if prefix is not None: config_name = '.'.join([prefix, config_name]) log.debug("config_name: %s", config_name) if v is not None: cfg[config_name] = v if hasattr(cfg, 'compile_args'): return cfg.compile_args() else: return (cfg,) return ConfigFactory