123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857 |
- # encoding: utf-8
- """A simple configuration system."""
- # Copyright (c) IPython Development Team.
- # Distributed under the terms of the Modified BSD License.
- import argparse
- import copy
- import logging
- import os
- import re
- import sys
- import json
- from ast import literal_eval
- from ipython_genutils.path import filefind
- from ipython_genutils import py3compat
- from ipython_genutils.encoding import DEFAULT_ENCODING
- from six import text_type
- from traitlets.traitlets import HasTraits, List, Any
- #-----------------------------------------------------------------------------
- # Exceptions
- #-----------------------------------------------------------------------------
- class ConfigError(Exception):
- pass
- class ConfigLoaderError(ConfigError):
- pass
- class ConfigFileNotFound(ConfigError):
- pass
- class ArgumentError(ConfigLoaderError):
- pass
- #-----------------------------------------------------------------------------
- # Argparse fix
- #-----------------------------------------------------------------------------
- # Unfortunately argparse by default prints help messages to stderr instead of
- # stdout. This makes it annoying to capture long help screens at the command
- # line, since one must know how to pipe stderr, which many users don't know how
- # to do. So we override the print_help method with one that defaults to
- # stdout and use our class instead.
- class ArgumentParser(argparse.ArgumentParser):
- """Simple argparse subclass that prints help to stdout by default."""
- def print_help(self, file=None):
- if file is None:
- file = sys.stdout
- return super(ArgumentParser, self).print_help(file)
- print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
- #-----------------------------------------------------------------------------
- # Config class for holding config information
- #-----------------------------------------------------------------------------
- class LazyConfigValue(HasTraits):
- """Proxy object for exposing methods on configurable containers
-
- Exposes:
-
- - append, extend, insert on lists
- - update on dicts
- - update, add on sets
- """
-
- _value = None
-
- # list methods
- _extend = List()
- _prepend = List()
-
- def append(self, obj):
- self._extend.append(obj)
-
- def extend(self, other):
- self._extend.extend(other)
-
- def prepend(self, other):
- """like list.extend, but for the front"""
- self._prepend[:0] = other
-
- _inserts = List()
- def insert(self, index, other):
- if not isinstance(index, int):
- raise TypeError("An integer is required")
- self._inserts.append((index, other))
-
- # dict methods
- # update is used for both dict and set
- _update = Any()
- def update(self, other):
- if self._update is None:
- if isinstance(other, dict):
- self._update = {}
- else:
- self._update = set()
- self._update.update(other)
-
- # set methods
- def add(self, obj):
- self.update({obj})
-
- def get_value(self, initial):
- """construct the value from the initial one
-
- after applying any insert / extend / update changes
- """
- if self._value is not None:
- return self._value
- value = copy.deepcopy(initial)
- if isinstance(value, list):
- for idx, obj in self._inserts:
- value.insert(idx, obj)
- value[:0] = self._prepend
- value.extend(self._extend)
-
- elif isinstance(value, dict):
- if self._update:
- value.update(self._update)
- elif isinstance(value, set):
- if self._update:
- value.update(self._update)
- self._value = value
- return value
-
- def to_dict(self):
- """return JSONable dict form of my data
-
- Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
- """
- d = {}
- if self._update:
- d['update'] = self._update
- if self._extend:
- d['extend'] = self._extend
- if self._prepend:
- d['prepend'] = self._prepend
- elif self._inserts:
- d['inserts'] = self._inserts
- return d
- def _is_section_key(key):
- """Is a Config key a section name (does it start with a capital)?"""
- if key and key[0].upper()==key[0] and not key.startswith('_'):
- return True
- else:
- return False
- class Config(dict):
- """An attribute based dict that can do smart merges."""
- def __init__(self, *args, **kwds):
- dict.__init__(self, *args, **kwds)
- self._ensure_subconfig()
-
- def _ensure_subconfig(self):
- """ensure that sub-dicts that should be Config objects are
-
- casts dicts that are under section keys to Config objects,
- which is necessary for constructing Config objects from dict literals.
- """
- for key in self:
- obj = self[key]
- if _is_section_key(key) \
- and isinstance(obj, dict) \
- and not isinstance(obj, Config):
- setattr(self, key, Config(obj))
-
- def _merge(self, other):
- """deprecated alias, use Config.merge()"""
- self.merge(other)
-
- def merge(self, other):
- """merge another config object into this one"""
- to_update = {}
- for k, v in other.items():
- if k not in self:
- to_update[k] = v
- else: # I have this key
- if isinstance(v, Config) and isinstance(self[k], Config):
- # Recursively merge common sub Configs
- self[k].merge(v)
- else:
- # Plain updates for non-Configs
- to_update[k] = v
- self.update(to_update)
-
- def collisions(self, other):
- """Check for collisions between two config objects.
-
- Returns a dict of the form {"Class": {"trait": "collision message"}}`,
- indicating which values have been ignored.
-
- An empty dict indicates no collisions.
- """
- collisions = {}
- for section in self:
- if section not in other:
- continue
- mine = self[section]
- theirs = other[section]
- for key in mine:
- if key in theirs and mine[key] != theirs[key]:
- collisions.setdefault(section, {})
- collisions[section][key] = "%r ignored, using %r" % (mine[key], theirs[key])
- return collisions
-
- def __contains__(self, key):
- # allow nested contains of the form `"Section.key" in config`
- if '.' in key:
- first, remainder = key.split('.', 1)
- if first not in self:
- return False
- return remainder in self[first]
-
- return super(Config, self).__contains__(key)
-
- # .has_key is deprecated for dictionaries.
- has_key = __contains__
-
- def _has_section(self, key):
- return _is_section_key(key) and key in self
-
- def copy(self):
- return type(self)(dict.copy(self))
- def __copy__(self):
- return self.copy()
- def __deepcopy__(self, memo):
- new_config = type(self)()
- for key, value in self.items():
- if isinstance(value, (Config, LazyConfigValue)):
- # deep copy config objects
- value = copy.deepcopy(value, memo)
- elif type(value) in {dict, list, set, tuple}:
- # shallow copy plain container traits
- value = copy.copy(value)
- new_config[key] = value
- return new_config
-
- def __getitem__(self, key):
- try:
- return dict.__getitem__(self, key)
- except KeyError:
- if _is_section_key(key):
- c = Config()
- dict.__setitem__(self, key, c)
- return c
- elif not key.startswith('_'):
- # undefined, create lazy value, used for container methods
- v = LazyConfigValue()
- dict.__setitem__(self, key, v)
- return v
- else:
- raise KeyError
- def __setitem__(self, key, value):
- if _is_section_key(key):
- if not isinstance(value, Config):
- raise ValueError('values whose keys begin with an uppercase '
- 'char must be Config instances: %r, %r' % (key, value))
- dict.__setitem__(self, key, value)
- def __getattr__(self, key):
- if key.startswith('__'):
- return dict.__getattr__(self, key)
- try:
- return self.__getitem__(key)
- except KeyError as e:
- raise AttributeError(e)
- def __setattr__(self, key, value):
- if key.startswith('__'):
- return dict.__setattr__(self, key, value)
- try:
- self.__setitem__(key, value)
- except KeyError as e:
- raise AttributeError(e)
- def __delattr__(self, key):
- if key.startswith('__'):
- return dict.__delattr__(self, key)
- try:
- dict.__delitem__(self, key)
- except KeyError as e:
- raise AttributeError(e)
- #-----------------------------------------------------------------------------
- # Config loading classes
- #-----------------------------------------------------------------------------
- class ConfigLoader(object):
- """A object for loading configurations from just about anywhere.
- The resulting configuration is packaged as a :class:`Config`.
- Notes
- -----
- A :class:`ConfigLoader` does one thing: load a config from a source
- (file, command line arguments) and returns the data as a :class:`Config` object.
- There are lots of things that :class:`ConfigLoader` does not do. It does
- not implement complex logic for finding config files. It does not handle
- default values or merge multiple configs. These things need to be
- handled elsewhere.
- """
- def _log_default(self):
- from traitlets.log import get_logger
- return get_logger()
- def __init__(self, log=None):
- """A base class for config loaders.
- log : instance of :class:`logging.Logger` to use.
- By default loger of :meth:`traitlets.config.application.Application.instance()`
- will be used
- Examples
- --------
- >>> cl = ConfigLoader()
- >>> config = cl.load_config()
- >>> config
- {}
- """
- self.clear()
- if log is None:
- self.log = self._log_default()
- self.log.debug('Using default logger')
- else:
- self.log = log
- def clear(self):
- self.config = Config()
- def load_config(self):
- """Load a config from somewhere, return a :class:`Config` instance.
- Usually, this will cause self.config to be set and then returned.
- However, in most cases, :meth:`ConfigLoader.clear` should be called
- to erase any previous state.
- """
- self.clear()
- return self.config
- class FileConfigLoader(ConfigLoader):
- """A base class for file based configurations.
- As we add more file based config loaders, the common logic should go
- here.
- """
- def __init__(self, filename, path=None, **kw):
- """Build a config loader for a filename and path.
- Parameters
- ----------
- filename : str
- The file name of the config file.
- path : str, list, tuple
- The path to search for the config file on, or a sequence of
- paths to try in order.
- """
- super(FileConfigLoader, self).__init__(**kw)
- self.filename = filename
- self.path = path
- self.full_filename = ''
- def _find_file(self):
- """Try to find the file by searching the paths."""
- self.full_filename = filefind(self.filename, self.path)
- class JSONFileConfigLoader(FileConfigLoader):
- """A JSON file loader for config
- Can also act as a context manager that rewrite the configuration file to disk on exit.
- Example::
- with JSONFileConfigLoader('myapp.json','/home/jupyter/configurations/') as c:
- c.MyNewConfigurable.new_value = 'Updated'
- """
- def load_config(self):
- """Load the config from a file and return it as a Config object."""
- self.clear()
- try:
- self._find_file()
- except IOError as e:
- raise ConfigFileNotFound(str(e))
- dct = self._read_file_as_dict()
- self.config = self._convert_to_config(dct)
- return self.config
- def _read_file_as_dict(self):
- with open(self.full_filename) as f:
- return json.load(f)
- def _convert_to_config(self, dictionary):
- if 'version' in dictionary:
- version = dictionary.pop('version')
- else:
- version = 1
- if version == 1:
- return Config(dictionary)
- else:
- raise ValueError('Unknown version of JSON config file: {version}'.format(version=version))
- def __enter__(self):
- self.load_config()
- return self.config
- def __exit__(self, exc_type, exc_value, traceback):
- """
- Exit the context manager but do not handle any errors.
- In case of any error, we do not want to write the potentially broken
- configuration to disk.
- """
- self.config.version = 1
- json_config = json.dumps(self.config, indent=2)
- with open(self.full_filename, 'w') as f:
- f.write(json_config)
- class PyFileConfigLoader(FileConfigLoader):
- """A config loader for pure python files.
- This is responsible for locating a Python config file by filename and
- path, then executing it to construct a Config object.
- """
- def load_config(self):
- """Load the config from a file and return it as a Config object."""
- self.clear()
- try:
- self._find_file()
- except IOError as e:
- raise ConfigFileNotFound(str(e))
- self._read_file_as_dict()
- return self.config
-
- def load_subconfig(self, fname, path=None):
- """Injected into config file namespace as load_subconfig"""
- if path is None:
- path = self.path
-
- loader = self.__class__(fname, path)
- try:
- sub_config = loader.load_config()
- except ConfigFileNotFound:
- # Pass silently if the sub config is not there,
- # treat it as an empty config file.
- pass
- else:
- self.config.merge(sub_config)
-
- def _read_file_as_dict(self):
- """Load the config file into self.config, with recursive loading."""
- def get_config():
- """Unnecessary now, but a deprecation warning is more trouble than it's worth."""
- return self.config
-
- namespace = dict(
- c=self.config,
- load_subconfig=self.load_subconfig,
- get_config=get_config,
- __file__=self.full_filename,
- )
- fs_encoding = sys.getfilesystemencoding() or 'ascii'
- conf_filename = self.full_filename.encode(fs_encoding)
- py3compat.execfile(conf_filename, namespace)
- class CommandLineConfigLoader(ConfigLoader):
- """A config loader for command line arguments.
- As we add more command line based loaders, the common logic should go
- here.
- """
- def _exec_config_str(self, lhs, rhs):
- """execute self.config.<lhs> = <rhs>
-
- * expands ~ with expanduser
- * tries to assign with literal_eval, otherwise assigns with just the string,
- allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
- equivalent are `--C.a=4` and `--C.a='4'`.
- """
- rhs = os.path.expanduser(rhs)
- try:
- # Try to see if regular Python syntax will work. This
- # won't handle strings as the quote marks are removed
- # by the system shell.
- value = literal_eval(rhs)
- except (NameError, SyntaxError, ValueError):
- # This case happens if the rhs is a string.
- value = rhs
- exec(u'self.config.%s = value' % lhs)
- def _load_flag(self, cfg):
- """update self.config from a flag, which can be a dict or Config"""
- if isinstance(cfg, (dict, Config)):
- # don't clobber whole config sections, update
- # each section from config:
- for sec,c in cfg.items():
- self.config[sec].update(c)
- else:
- raise TypeError("Invalid flag: %r" % cfg)
- # raw --identifier=value pattern
- # but *also* accept '-' as wordsep, for aliases
- # accepts: --foo=a
- # --Class.trait=value
- # --alias-name=value
- # rejects: -foo=value
- # --foo
- # --Class.trait
- kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
- # just flags, no assignments, with two *or one* leading '-'
- # accepts: --foo
- # -foo-bar-again
- # rejects: --anything=anything
- # --two.word
- flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
- class KeyValueConfigLoader(CommandLineConfigLoader):
- """A config loader that loads key value pairs from the command line.
- This allows command line options to be gives in the following form::
- ipython --profile="foo" --InteractiveShell.autocall=False
- """
- def __init__(self, argv=None, aliases=None, flags=None, **kw):
- """Create a key value pair config loader.
- Parameters
- ----------
- argv : list
- A list that has the form of sys.argv[1:] which has unicode
- elements of the form u"key=value". If this is None (default),
- then sys.argv[1:] will be used.
- aliases : dict
- A dict of aliases for configurable traits.
- Keys are the short aliases, Values are the resolved trait.
- Of the form: `{'alias' : 'Configurable.trait'}`
- flags : dict
- A dict of flags, keyed by str name. Vaues can be Config objects,
- dicts, or "key=value" strings. If Config or dict, when the flag
- is triggered, The flag is loaded as `self.config.update(m)`.
- Returns
- -------
- config : Config
- The resulting Config object.
- Examples
- --------
- >>> from traitlets.config.loader import KeyValueConfigLoader
- >>> cl = KeyValueConfigLoader()
- >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
- >>> sorted(d.items())
- [('A', {'name': 'brian'}), ('B', {'number': 0})]
- """
- super(KeyValueConfigLoader, self).__init__(**kw)
- if argv is None:
- argv = sys.argv[1:]
- self.argv = argv
- self.aliases = aliases or {}
- self.flags = flags or {}
- def clear(self):
- super(KeyValueConfigLoader, self).clear()
- self.extra_args = []
- def _decode_argv(self, argv, enc=None):
- """decode argv if bytes, using stdin.encoding, falling back on default enc"""
- uargv = []
- if enc is None:
- enc = DEFAULT_ENCODING
- for arg in argv:
- if not isinstance(arg, text_type):
- # only decode if not already decoded
- arg = arg.decode(enc)
- uargv.append(arg)
- return uargv
- def load_config(self, argv=None, aliases=None, flags=None):
- """Parse the configuration and generate the Config object.
- After loading, any arguments that are not key-value or
- flags will be stored in self.extra_args - a list of
- unparsed command-line arguments. This is used for
- arguments such as input files or subcommands.
- Parameters
- ----------
- argv : list, optional
- A list that has the form of sys.argv[1:] which has unicode
- elements of the form u"key=value". If this is None (default),
- then self.argv will be used.
- aliases : dict
- A dict of aliases for configurable traits.
- Keys are the short aliases, Values are the resolved trait.
- Of the form: `{'alias' : 'Configurable.trait'}`
- flags : dict
- A dict of flags, keyed by str name. Values can be Config objects
- or dicts. When the flag is triggered, The config is loaded as
- `self.config.update(cfg)`.
- """
- self.clear()
- if argv is None:
- argv = self.argv
- if aliases is None:
- aliases = self.aliases
- if flags is None:
- flags = self.flags
- # ensure argv is a list of unicode strings:
- uargv = self._decode_argv(argv)
- for idx,raw in enumerate(uargv):
- # strip leading '-'
- item = raw.lstrip('-')
- if raw == '--':
- # don't parse arguments after '--'
- # this is useful for relaying arguments to scripts, e.g.
- # ipython -i foo.py --matplotlib=qt -- args after '--' go-to-foo.py
- self.extra_args.extend(uargv[idx+1:])
- break
- if kv_pattern.match(raw):
- lhs,rhs = item.split('=',1)
- # Substitute longnames for aliases.
- if lhs in aliases:
- lhs = aliases[lhs]
- if '.' not in lhs:
- # probably a mistyped alias, but not technically illegal
- self.log.warning("Unrecognized alias: '%s', it will probably have no effect.", raw)
- try:
- self._exec_config_str(lhs, rhs)
- except Exception:
- raise ArgumentError("Invalid argument: '%s'" % raw)
- elif flag_pattern.match(raw):
- if item in flags:
- cfg,help = flags[item]
- self._load_flag(cfg)
- else:
- raise ArgumentError("Unrecognized flag: '%s'"%raw)
- elif raw.startswith('-'):
- kv = '--'+item
- if kv_pattern.match(kv):
- raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
- else:
- raise ArgumentError("Invalid argument: '%s'"%raw)
- else:
- # keep all args that aren't valid in a list,
- # in case our parent knows what to do with them.
- self.extra_args.append(item)
- return self.config
- class ArgParseConfigLoader(CommandLineConfigLoader):
- """A loader that uses the argparse module to load from the command line."""
- def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
- """Create a config loader for use with argparse.
- Parameters
- ----------
- argv : optional, list
- If given, used to read command-line arguments from, otherwise
- sys.argv[1:] is used.
- parser_args : tuple
- A tuple of positional arguments that will be passed to the
- constructor of :class:`argparse.ArgumentParser`.
- parser_kw : dict
- A tuple of keyword arguments that will be passed to the
- constructor of :class:`argparse.ArgumentParser`.
- Returns
- -------
- config : Config
- The resulting Config object.
- """
- super(CommandLineConfigLoader, self).__init__(log=log)
- self.clear()
- if argv is None:
- argv = sys.argv[1:]
- self.argv = argv
- self.aliases = aliases or {}
- self.flags = flags or {}
- self.parser_args = parser_args
- self.version = parser_kw.pop("version", None)
- kwargs = dict(argument_default=argparse.SUPPRESS)
- kwargs.update(parser_kw)
- self.parser_kw = kwargs
- def load_config(self, argv=None, aliases=None, flags=None):
- """Parse command line arguments and return as a Config object.
- Parameters
- ----------
- args : optional, list
- If given, a list with the structure of sys.argv[1:] to parse
- arguments from. If not given, the instance's self.argv attribute
- (given at construction time) is used."""
- self.clear()
- if argv is None:
- argv = self.argv
- if aliases is None:
- aliases = self.aliases
- if flags is None:
- flags = self.flags
- self._create_parser(aliases, flags)
- self._parse_args(argv)
- self._convert_to_config()
- return self.config
- def get_extra_args(self):
- if hasattr(self, 'extra_args'):
- return self.extra_args
- else:
- return []
- def _create_parser(self, aliases=None, flags=None):
- self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
- self._add_arguments(aliases, flags)
- def _add_arguments(self, aliases=None, flags=None):
- raise NotImplementedError("subclasses must implement _add_arguments")
- def _parse_args(self, args):
- """self.parser->self.parsed_data"""
- # decode sys.argv to support unicode command-line options
- enc = DEFAULT_ENCODING
- uargs = [py3compat.cast_unicode(a, enc) for a in args]
- self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
- def _convert_to_config(self):
- """self.parsed_data->self.config"""
- for k, v in vars(self.parsed_data).items():
- exec("self.config.%s = v"%k, locals(), globals())
- class KVArgParseConfigLoader(ArgParseConfigLoader):
- """A config loader that loads aliases and flags with argparse,
- but will use KVLoader for the rest. This allows better parsing
- of common args, such as `ipython -c 'print 5'`, but still gets
- arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
- def _add_arguments(self, aliases=None, flags=None):
- self.alias_flags = {}
- # print aliases, flags
- if aliases is None:
- aliases = self.aliases
- if flags is None:
- flags = self.flags
- paa = self.parser.add_argument
- for key,value in aliases.items():
- if key in flags:
- # flags
- nargs = '?'
- else:
- nargs = None
- if len(key) is 1:
- paa('-'+key, '--'+key, type=text_type, dest=value, nargs=nargs)
- else:
- paa('--'+key, type=text_type, dest=value, nargs=nargs)
- for key, (value, help) in flags.items():
- if key in self.aliases:
- #
- self.alias_flags[self.aliases[key]] = value
- continue
- if len(key) is 1:
- paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
- else:
- paa('--'+key, action='append_const', dest='_flags', const=value)
- def _convert_to_config(self):
- """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
- # remove subconfigs list from namespace before transforming the Namespace
- if '_flags' in self.parsed_data:
- subcs = self.parsed_data._flags
- del self.parsed_data._flags
- else:
- subcs = []
- for k, v in vars(self.parsed_data).items():
- if v is None:
- # it was a flag that shares the name of an alias
- subcs.append(self.alias_flags[k])
- else:
- # eval the KV assignment
- self._exec_config_str(k, v)
- for subc in subcs:
- self._load_flag(subc)
- if self.extra_args:
- sub_parser = KeyValueConfigLoader(log=self.log)
- sub_parser.load_config(self.extra_args)
- self.config.merge(sub_parser.config)
- self.extra_args = sub_parser.extra_args
- def load_pyconfig_files(config_files, path):
- """Load multiple Python config files, merging each of them in turn.
- Parameters
- ==========
- config_files : list of str
- List of config files names to load and merge into the config.
- path : unicode
- The full path to the location of the config files.
- """
- config = Config()
- for cf in config_files:
- loader = PyFileConfigLoader(cf, path=path)
- try:
- next_config = loader.load_config()
- except ConfigFileNotFound:
- pass
- except:
- raise
- else:
- config.merge(next_config)
- return config
|