From fe3b22569171ad5270924a2fe079250822c69092 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Tue, 21 Apr 2020 23:01:08 -0400 Subject: [PATCH 01/13] adding config files as an argument --- confuse.py | 193 +++++++++++++++++++++++++++------------------ test/test_paths.py | 16 +++- 2 files changed, 131 insertions(+), 78 deletions(-) diff --git a/confuse.py b/confuse.py index 8d5d875..7c94696 100644 --- a/confuse.py +++ b/confuse.py @@ -726,6 +726,67 @@ def config_dirs(): return out +def find_user_config_files(appname, env_var=None, config_fname=CONFIG_FILENAME, + first=True): + """Get the path to the user configuration directory. The + directory is guaranteed to exist as a postcondition (one may be + created if none exist). + + If the application's ``...DIR`` environment variable is set, it + is used as the configuration directory. Otherwise, + platform-specific standard configuration locations are searched + for a ``config.yaml`` file. If no configuration file is found, a + fallback path is used. + + Arguments: + appname (str): the subdirectory to search for in default config locations. + env_var (str, optional): the environment variable to look for + config_fname (str): the config filename to look for. + first (bool): only return the first config file. Set to False to return + all matching config files. This will create directories for all files + returned. + + Returns: + config_file (str) if ``first == True`` else config_files (list(str)). + """ + foundcfgs = [] + + # If environment variable is set, use it. + if env_var and env_var in os.environ: + appdir = os.path.abspath(os.path.expanduser(os.environ[env_var])) + foundcfgs.append( + appdir if os.path.isfile(appdir) else + os.path.join(appdir, config_fname)) + + # Search platform-specific locations. If no config file is + # found, fall back to the first directory in the list. + cfgfiles = [os.path.join(d, appname, config_fname) for d in config_dirs()] + foundcfgs.extend([f for f in cfgfiles if os.path.isfile(f)] or cfgfiles[:1]) + + # Ensure that the directory exists. + for f in foundcfgs[:1] if first else foundcfgs: + os.makedirs(os.path.dirname(f), exist_ok=True) + return foundcfgs[0] if first else foundcfgs + + +def find_package_config(modname, config_fname=DEFAULT_FILENAME): + '''Return a package default config file if it exists.''' + package_path = _package_path(modname) + if package_path: + default_config_file = os.path.join(package_path, config_fname) + if os.path.isfile(default_config_file): + return default_config_file + return None + + +def _ensure_list(x): + '''Convert to list. e.g. 1 => [1], (1, 2) => [1, 2], None => [], [1] => [1].''' + return ( + x if isinstance(x, list) else + list(x) if isinstance(x, tuple) else + [x] if x else []) + + # YAML loading. class Loader(yaml.SafeLoader): @@ -895,7 +956,8 @@ def restore_yaml_comments(data, default_data): # Main interface. class Configuration(RootView): - def __init__(self, appname, modname=None, read=True): + def __init__(self, appname, modname=None, sources=None, read=True, + config_filename=None, default_filename=None): """Create a configuration object by reading the automatically-discovered config files for the application for a given name. If `modname` is specified, it should be the import @@ -909,45 +971,38 @@ def __init__(self, appname, modname=None, read=True): super(Configuration, self).__init__([]) self.appname = appname self.modname = modname + self.config_filename = config_filename or CONFIG_FILENAME + self.default_filename = default_filename or DEFAULT_FILENAME - # Resolve default source location. We do this ahead of time to - # avoid unexpected problems if the working directory changes. - if self.modname: - self._package_path = _package_path(self.modname) - else: - self._package_path = None + self._sources = [self._to_filename(s) for s in _ensure_list(sources)] + self._env_var = env_var = '{}DIR'.format(self.appname.upper()) + + # search the users system for config files + if not self._sources: + self._sources.append( + find_user_config_files( + self.appname, env_var, + config_fname=self.config_filename, + first=True)) - self._env_var = '{0}DIR'.format(self.appname.upper()) + # if user specified a module name, load the config + self.default_config_file = modname and find_package_config( + modname, self.default_filename) if read: self.read() - def user_config_path(self): - """Points to the location of the user configuration. - - The file may not exist. - """ - return os.path.join(self.config_dir(), CONFIG_FILENAME) - - def _add_user_source(self): - """Add the configuration options from the YAML file in the - user's configuration directory (given by `config_dir`) if it - exists. - """ - filename = self.user_config_path() - if os.path.isfile(filename): - self.add(ConfigSource(load_yaml(filename) or {}, filename)) - - def _add_default_source(self): - """Add the package's default configuration settings. This looks - for a YAML file located inside the package for the module - `modname` if it was given. - """ - if self.modname: - if self._package_path: - filename = os.path.join(self._package_path, DEFAULT_FILENAME) - if os.path.isfile(filename): - self.add(ConfigSource(load_yaml(filename), filename, True)) + def config_dir(self): + """Get the path to the user configuration directory. This + looks for the first source that has a filename and uses the + file's parent directory. Returns None if none are found. + """ + for source in self.sources or self._sources: + if isinstance(source, ConfigSource): + source = source.filename + if source and isinstance(source, BASESTRING): + return os.path.dirname(source) + return None def read(self, user=True, defaults=True): """Find and read the files for this configuration and set them @@ -956,52 +1011,40 @@ def read(self, user=True, defaults=True): set `user` or `defaults` to `False`. """ if user: - self._add_user_source() - if defaults: - self._add_default_source() - - def config_dir(self): - """Get the path to the user configuration directory. The - directory is guaranteed to exist as a postcondition (one may be - created if none exist). - - If the application's ``...DIR`` environment variable is set, it - is used as the configuration directory. Otherwise, - platform-specific standard configuration locations are searched - for a ``config.yaml`` file. If no configuration file is found, a - fallback path is used. - """ - # If environment variable is set, use it. - if self._env_var in os.environ: - appdir = os.environ[self._env_var] - appdir = os.path.abspath(os.path.expanduser(appdir)) - if os.path.isfile(appdir): - raise ConfigError(u'{0} must be a directory'.format( - self._env_var - )) - - else: - # Search platform-specific locations. If no config file is - # found, fall back to the first directory in the list. - configdirs = config_dirs() - for confdir in configdirs: - appdir = os.path.join(confdir, self.appname) - if os.path.isfile(os.path.join(appdir, CONFIG_FILENAME)): - break - else: - appdir = os.path.join(configdirs[0], self.appname) - - # Ensure that the directory exists. - if not os.path.isdir(appdir): - os.makedirs(appdir) - return appdir + for filename in self._sources: + self.add(self._as_source(filename)) + # load a config if specified/found in the package + if defaults and self.default_config_file: + self.add(self._as_source(self.default_config_file, default=True)) def set_file(self, filename): """Parses the file as YAML and inserts it into the configuration sources with highest priority. """ - filename = os.path.abspath(filename) - self.set(ConfigSource(load_yaml(filename), filename)) + self.set(self._as_source(filename)) + + def _to_filename(self, path, default=False): + '''Convert a config directory/file to an absolute config file.''' + path = os.path.abspath(path) + # if the source is a directory, look for a config file inside + if os.path.isdir(path) or os.path.splitext(path)[1] not in {'.yaml', '.yml'}: + fname = self.default_filename if default else self.config_filename + path = os.path.join(path, fname) + return path + + def _as_source(self, source, default=False, ignore_missing=False): + '''Convert {filename, ConfigSource} to ConfigSource.''' + if isinstance(source, ConfigSource): + return source + + if isinstance(source, BASESTRING): + source = self._to_filename(source) + # skip over files that don't exist + if ignore_missing and not os.path.isfile(source): + return + return ConfigSource(load_yaml(source), source, default=default) + + raise TypeError(u'source value must be a ConfigSource or yaml path') def dump(self, full=True, redact=False): """Dump the Configuration object to a YAML file. diff --git a/test/test_paths.py b/test/test_paths.py index ad6d10b..ce90291 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -120,7 +120,7 @@ def test_no_sources_when_files_missing(self): def test_search_package(self): config = confuse.Configuration('myapp', __name__, read=False) - config._add_default_source() + config.read(user=False, defaults=True) for source in config.sources: if source.default: @@ -141,9 +141,12 @@ class EnvVarTest(FakeSystem): def setUp(self): super(EnvVarTest, self).setUp() - self.config = confuse.Configuration('myapp', read=False) os.environ['MYAPPDIR'] = self.home # use the tmp home as a config dir + @property + def config(self): + return confuse.Configuration('myapp', read=False) + def test_env_var_name(self): self.assertEqual(self.config._env_var, 'MYAPPDIR') @@ -176,7 +179,9 @@ def setUp(self): os.path.join = self.join os.makedirs, self._makedirs = self.makedirs, os.makedirs - self.config = confuse.Configuration('test', read=False) + @property + def config(self): + return confuse.Configuration('test', read=False) def tearDown(self): super(PrimaryConfigDirTest, self).tearDown() @@ -207,3 +212,8 @@ def test_do_not_create_dir_if_lower_priority_exists(self): self.assertEqual(self.config.config_dir(), path2) self.assertFalse(os.path.isdir(path1)) self.assertTrue(os.path.isdir(path2)) + + def test_override_config_dir(self): + path = os.path.join(self.home, 'asdfasdfasdfd', 'test') + config = confuse.Configuration('test', sources=path, read=False) + self.assertEqual(config.config_dir(), path) From 3b629912d86ff85a4289c8cd2a72269055a837a7 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Thu, 23 Apr 2020 00:09:09 -0400 Subject: [PATCH 02/13] fixing formatting errors and maybe a few other errors --- confuse.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/confuse.py b/confuse.py index 7c94696..2daa635 100644 --- a/confuse.py +++ b/confuse.py @@ -739,12 +739,13 @@ def find_user_config_files(appname, env_var=None, config_fname=CONFIG_FILENAME, fallback path is used. Arguments: - appname (str): the subdirectory to search for in default config locations. + appname (str): the subdirectory to search for in default config + locations. env_var (str, optional): the environment variable to look for config_fname (str): the config filename to look for. - first (bool): only return the first config file. Set to False to return - all matching config files. This will create directories for all files - returned. + first (bool): only return the first config file. Set to False to + return all matching config files. This will create directories + for all files returned. Returns: config_file (str) if ``first == True`` else config_files (list(str)). @@ -761,11 +762,9 @@ def find_user_config_files(appname, env_var=None, config_fname=CONFIG_FILENAME, # Search platform-specific locations. If no config file is # found, fall back to the first directory in the list. cfgfiles = [os.path.join(d, appname, config_fname) for d in config_dirs()] - foundcfgs.extend([f for f in cfgfiles if os.path.isfile(f)] or cfgfiles[:1]) + foundcfgs.extend( + [f for f in cfgfiles if os.path.isfile(f)] or cfgfiles[:1]) - # Ensure that the directory exists. - for f in foundcfgs[:1] if first else foundcfgs: - os.makedirs(os.path.dirname(f), exist_ok=True) return foundcfgs[0] if first else foundcfgs @@ -780,7 +779,7 @@ def find_package_config(modname, config_fname=DEFAULT_FILENAME): def _ensure_list(x): - '''Convert to list. e.g. 1 => [1], (1, 2) => [1, 2], None => [], [1] => [1].''' + '''Convert to list. e.g. 1 => [1], (1, 2) => [1, 2], None => [].''' return ( x if isinstance(x, list) else list(x) if isinstance(x, tuple) else @@ -956,7 +955,7 @@ def restore_yaml_comments(data, default_data): # Main interface. class Configuration(RootView): - def __init__(self, appname, modname=None, sources=None, read=True, + def __init__(self, appname, modname=None, source=None, read=True, config_filename=None, default_filename=None): """Create a configuration object by reading the automatically-discovered config files for the application for a @@ -974,7 +973,10 @@ def __init__(self, appname, modname=None, sources=None, read=True, self.config_filename = config_filename or CONFIG_FILENAME self.default_filename = default_filename or DEFAULT_FILENAME - self._sources = [self._to_filename(s) for s in _ensure_list(sources)] + # convert user-provided sources to a list of config files + self._sources = [ + self._to_filename(s) if isinstance(s, BASESTRING) else s + for s in _ensure_list(source)] self._env_var = env_var = '{}DIR'.format(self.appname.upper()) # search the users system for config files @@ -1012,7 +1014,7 @@ def read(self, user=True, defaults=True): """ if user: for filename in self._sources: - self.add(self._as_source(filename)) + self.add(self._as_source(filename, ignore_missing=True)) # load a config if specified/found in the package if defaults and self.default_config_file: self.add(self._as_source(self.default_config_file, default=True)) @@ -1027,9 +1029,15 @@ def _to_filename(self, path, default=False): '''Convert a config directory/file to an absolute config file.''' path = os.path.abspath(path) # if the source is a directory, look for a config file inside - if os.path.isdir(path) or os.path.splitext(path)[1] not in {'.yaml', '.yml'}: + if (os.path.isdir(path) or + os.path.splitext(path)[1] not in {'.yaml', '.yml'}): fname = self.default_filename if default else self.config_filename path = os.path.join(path, fname) + + # ensure directory exists + cfgdir = os.path.dirname(path) + if not os.path.isdir(cfgdir): + os.makedirs(cfgdir) return path def _as_source(self, source, default=False, ignore_missing=False): @@ -1091,8 +1099,8 @@ class LazyConfig(Configuration): accessed. This is appropriate for using as a global config object at the module level. """ - def __init__(self, appname, modname=None): - super(LazyConfig, self).__init__(appname, modname, False) + def __init__(self, appname, modname=None, *a, **kw): + super(LazyConfig, self).__init__(appname, modname, *a, read=False, **kw) self._materialized = False # Have we read the files yet? self._lazy_prefix = [] # Pre-materialization calls to set(). self._lazy_suffix = [] # Calls to add(). From 9085405586621d15a7186f3ce05e24f9ecc2d638 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Thu, 23 Apr 2020 09:33:06 -0400 Subject: [PATCH 03/13] fixing default source error --- confuse.py | 22 +++++++++++++++------- test/test_paths.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/confuse.py b/confuse.py index 2daa635..a65ff0c 100644 --- a/confuse.py +++ b/confuse.py @@ -1003,7 +1003,10 @@ def config_dir(self): if isinstance(source, ConfigSource): source = source.filename if source and isinstance(source, BASESTRING): - return os.path.dirname(source) + dir = os.path.dirname(source) + if not os.path.isdir(dir): + os.makedirs(dir) + return dir return None def read(self, user=True, defaults=True): @@ -1014,10 +1017,10 @@ def read(self, user=True, defaults=True): """ if user: for filename in self._sources: - self.add(self._as_source(filename, ignore_missing=True)) + self.add_source(filename, ignore_missing=True) # load a config if specified/found in the package if defaults and self.default_config_file: - self.add(self._as_source(self.default_config_file, default=True)) + self.add_source(self.default_config_file, default=True) def set_file(self, filename): """Parses the file as YAML and inserts it into the configuration @@ -1025,12 +1028,17 @@ def set_file(self, filename): """ self.set(self._as_source(filename)) + def add_source(self, source, **kw): + source = self._as_source(source, **kw) + if source is not None: + self.add(source) + def _to_filename(self, path, default=False): '''Convert a config directory/file to an absolute config file.''' path = os.path.abspath(path) # if the source is a directory, look for a config file inside - if (os.path.isdir(path) or - os.path.splitext(path)[1] not in {'.yaml', '.yml'}): + if (os.path.isdir(path) + or os.path.splitext(path)[1] not in {'.yaml', '.yml'}): fname = self.default_filename if default else self.config_filename path = os.path.join(path, fname) @@ -1044,7 +1052,6 @@ def _as_source(self, source, default=False, ignore_missing=False): '''Convert {filename, ConfigSource} to ConfigSource.''' if isinstance(source, ConfigSource): return source - if isinstance(source, BASESTRING): source = self._to_filename(source) # skip over files that don't exist @@ -1100,7 +1107,8 @@ class LazyConfig(Configuration): the module level. """ def __init__(self, appname, modname=None, *a, **kw): - super(LazyConfig, self).__init__(appname, modname, *a, read=False, **kw) + super(LazyConfig, self).__init__( + appname, modname, *a, read=False, **kw) self._materialized = False # Have we read the files yet? self._lazy_prefix = [] # Pre-materialization calls to set(). self._lazy_suffix = [] # Calls to add(). diff --git a/test/test_paths.py b/test/test_paths.py index ce90291..6de0b0c 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -215,5 +215,5 @@ def test_do_not_create_dir_if_lower_priority_exists(self): def test_override_config_dir(self): path = os.path.join(self.home, 'asdfasdfasdfd', 'test') - config = confuse.Configuration('test', sources=path, read=False) + config = confuse.Configuration('test', source=path, read=False) self.assertEqual(config.config_dir(), path) From 0a0f5d2ca33f737f653c418c1c0c09cc2d736b3c Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Thu, 23 Apr 2020 10:03:43 -0400 Subject: [PATCH 04/13] adding lazy config source --- confuse.py | 125 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 107 insertions(+), 18 deletions(-) diff --git a/confuse.py b/confuse.py index d4e7bc8..cb8906b 100644 --- a/confuse.py +++ b/confuse.py @@ -32,6 +32,7 @@ import yaml import re from collections import OrderedDict +from functools import wraps if sys.version_info >= (3, 3): from collections import abc else: @@ -134,36 +135,124 @@ def __init__(self, filename, reason=None): # Views and sources. + +UNSET = object() # sentinel + + +def _load_first(func): + '''Call self.load() before the function is called - used for lazy source + loading''' + @wraps(func) + def inner(self, *a, **kw): + self.load() + return func(self, *a, **kw) + return inner + + +def all_subclasses(cls): + return set(cls.__subclasses__()).union( + s for c in cls.__subclasses__() for s in all_subclasses(c)) + + class ConfigSource(dict): - """A dictionary augmented with metadata about the source of the + '''A dictionary augmented with metadata about the source of the configuration. - """ - def __init__(self, value, filename=None, default=False): - super(ConfigSource, self).__init__(value) + ''' + def __init__(self, value=UNSET, filename=None, default=False): + # track whether a config source has been set yet + self.loaded = value is not UNSET + super(ConfigSource, self).__init__(value if self.loaded else {}) if filename is not None and not isinstance(filename, BASESTRING): raise TypeError(u'filename must be a string or None') self.filename = filename self.default = default def __repr__(self): - return 'ConfigSource({0!r}, {1!r}, {2!r})'.format( - super(ConfigSource, self), - self.filename, - self.default, - ) + return 'ConfigSource({}, filename={}, default={})'.format( + super(ConfigSource, self).__repr__() if self.loaded else '*Unloaded*', + self.filename, self.default) + + @property + def exists(self): + '''Does this config have access to usable configuration values?''' + # if has a file attached - see if a file is attached and exists + # or if value is set. + return self.loaded or self.filename and os.path.isfile(self.filename) + + def load(self): + '''Ensure that the source is loaded.''' + if not self.loaded: + self.make_config_dir() + self.loaded = self._load() is not False + return self + + def _load(self): + '''Load config from source and update self. + If it doesn't load, return False to keep it marked as unloaded. + Otherwise it will be assumed to be loaded. + ''' + + def make_config_dir(self): + '''Create the config dir, if there's a filename associated with the + source.''' + if self.filename: + dirname = os.path.dirname(self.filename) + if dirname and not os.path.isdir(dirname): # for PY2 -_- + os.makedirs(dirname) @classmethod - def of(cls, value): - """Given either a dictionary or a `ConfigSource` object, return - a `ConfigSource` object. This lets a function accept either type - of object as an argument. - """ + def is_of_type(cls, value): + '''Determine if value is a valid parameter for this Source.''' + return + + # overriding dict methods so that the configuration is loaded before any + # of them are run + __getitem__ = _load_first(dict.__getitem__) + __iter__ = _load_first(dict.__iter__) + # __len__ = _load_first(dict.__len__) + keys = _load_first(dict.keys) + values = _load_first(dict.values) + + @classmethod + def of(cls, value, **kw): + '''Try to convert value to a `ConfigSource` object. This lets a + function accept values that are convertable to a source. + ''' + # ignore if already a source if isinstance(value, ConfigSource): return value - elif isinstance(value, dict): - return ConfigSource(value) - else: - raise TypeError(u'source value must be a dict') + + # convert using one of the configsource subtypes + for subcls in all_subclasses(ConfigSource): + if subcls.is_of_type(value): + return subcls(value, **kw) + + # if none matched, use convert dict to ConfigSource + if isinstance(value, dict): + return ConfigSource(value, **kw) + raise TypeError( + u'ConfigSource.of value unable to cast to ConfigSource.') + + +class YamlSource(ConfigSource): + EXTENSIONS = '.yaml', '.yml' + def __init__(self, filename=None, value=UNSET, default=False, ignore_missing=False): + self.ignore_missing = ignore_missing + super(YamlSource, self).__init__(value, filename, default) + + @classmethod + def is_of_type(cls, value): + '''Does value look like a .yaml file?''' + return ( + isinstance(value, BASESTRING) and + os.path.splitext(value)[1] in cls.EXTENSIONS) + + _loader = lambda self, f: load_yaml(f) + def _load(self): + '''Load the file if it exists.''' + if self.ignore_missing and not os.path.isfile(self.filename): + return False + self.update(self._loader(self.filename)) class ConfigView(object): From fc91ed0037aa9855ab78af7cebc86553536f5c7b Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Thu, 23 Apr 2020 10:54:04 -0400 Subject: [PATCH 05/13] moved automatic configs to the bottom of the stack like before --- confuse.py | 142 ++++++++++++++++----------------------------- test/test_paths.py | 2 +- 2 files changed, 51 insertions(+), 93 deletions(-) diff --git a/confuse.py b/confuse.py index 0926634..4b2cd16 100644 --- a/confuse.py +++ b/confuse.py @@ -182,7 +182,7 @@ def exists(self): def load(self): '''Ensure that the source is loaded.''' if not self.loaded: - self.make_config_dir() + self.ensure_config_dir() self.loaded = self._load() is not False return self @@ -192,13 +192,14 @@ def _load(self): Otherwise it will be assumed to be loaded. ''' - def make_config_dir(self): + def ensure_config_dir(self): '''Create the config dir, if there's a filename associated with the source.''' if self.filename: dirname = os.path.dirname(self.filename) if dirname and not os.path.isdir(dirname): # for PY2 -_- os.makedirs(dirname) + return dirname @classmethod def is_of_type(cls, value): @@ -1061,24 +1062,25 @@ def __init__(self, appname, modname=None, source=None, read=True, self.modname = modname self.config_filename = config_filename or CONFIG_FILENAME self.default_filename = default_filename or DEFAULT_FILENAME + self._env_var = env_var = '{}DIR'.format(self.appname.upper()) + self._base_sources = [] # convert user-provided sources to a list of config files - self._sources = [ - self._to_filename(s) if isinstance(s, BASESTRING) else s - for s in _ensure_list(source)] - self._env_var = env_var = '{}DIR'.format(self.appname.upper()) + for source in _ensure_list(source): + self._add_base(source) # search the users system for config files - if not self._sources: - self._sources.append( + if not self.sources: + self._add_base( find_user_config_files( self.appname, env_var, config_fname=self.config_filename, - first=True)) + first=True), ignore_missing=True) # if user specified a module name, load the config - self.default_config_file = modname and find_package_config( - modname, self.default_filename) + if modname: + self._add_base(find_package_config( + modname, self.default_filename), default=True) if read: self.read() @@ -1088,14 +1090,9 @@ def config_dir(self): looks for the first source that has a filename and uses the file's parent directory. Returns None if none are found. """ - for source in self.sources or self._sources: - if isinstance(source, ConfigSource): - source = source.filename - if source and isinstance(source, BASESTRING): - dir = os.path.dirname(source) - if not os.path.isdir(dir): - os.makedirs(dir) - return dir + for source in self.sources + self._base_sources: + if source.filename: + return source.ensure_config_dir() return None def read(self, user=True, defaults=True): @@ -1104,51 +1101,47 @@ def read(self, user=True, defaults=True): discovered user configuration files or the in-package defaults, set `user` or `defaults` to `False`. """ - if user: - for filename in self._sources: - self.add_source(filename, ignore_missing=True) - # load a config if specified/found in the package - if defaults and self.default_config_file: - self.add_source(self.default_config_file, default=True) - - def set_file(self, filename): - """Parses the file as YAML and inserts it into the configuration - sources with highest priority. - """ - self.set(self._as_source(filename)) + self.add_base() + for source in self.sources: + if user and not source.default: + source.load() + if defaults and source.default: + source.load() + + def set(self, source, **kw): + super(Configuration, self).set( + ConfigSource.of(self._to_filename(source), **kw)) - def add_source(self, source, **kw): - source = self._as_source(source, **kw) - if source is not None: + def add(self, source, **kw): + super(Configuration, self).add( + ConfigSource.of(self._to_filename(source), **kw)) + + def _add_base(self, source, **kw): + self._base_sources.append( + ConfigSource.of(self._to_filename(source), **kw)) + + def add_base(self): + for source in self._base_sources: self.add(source) - def _to_filename(self, path, default=False): + def _to_filename(self, source, default=False): '''Convert a config directory/file to an absolute config file.''' - path = os.path.abspath(path) - # if the source is a directory, look for a config file inside - if (os.path.isdir(path) - or os.path.splitext(path)[1] not in {'.yaml', '.yml'}): - fname = self.default_filename if default else self.config_filename - path = os.path.join(path, fname) - - # ensure directory exists - cfgdir = os.path.dirname(path) - if not os.path.isdir(cfgdir): - os.makedirs(cfgdir) - return path - - def _as_source(self, source, default=False, ignore_missing=False): - '''Convert {filename, ConfigSource} to ConfigSource.''' if isinstance(source, ConfigSource): return source if isinstance(source, BASESTRING): - source = self._to_filename(source) - # skip over files that don't exist - if ignore_missing and not os.path.isfile(source): - return - return ConfigSource(load_yaml(source), source, default=default) - - raise TypeError(u'source value must be a ConfigSource or yaml path') + source = os.path.abspath(source) + # if the source is a directory, look for a config file inside + if (os.path.isdir(source) + or os.path.splitext(source)[1] not in {'.yaml', '.yml'}): + source = os.path.join(source, ( + self.default_filename if default + else self.config_filename)) + + # ensure directory exists + cfgdir = os.path.dirname(source) + if not os.path.isdir(cfgdir): + os.makedirs(cfgdir) + return source def dump(self, full=True, redact=False): """Dump the Configuration object to a YAML file. @@ -1198,41 +1191,6 @@ class LazyConfig(Configuration): def __init__(self, appname, modname=None, *a, **kw): super(LazyConfig, self).__init__( appname, modname, *a, read=False, **kw) - self._materialized = False # Have we read the files yet? - self._lazy_prefix = [] # Pre-materialization calls to set(). - self._lazy_suffix = [] # Calls to add(). - - def read(self, user=True, defaults=True): - self._materialized = True - super(LazyConfig, self).read(user, defaults) - - def resolve(self): - if not self._materialized: - # Read files and unspool buffers. - self.read() - self.sources += self._lazy_suffix - self.sources[:0] = self._lazy_prefix - return super(LazyConfig, self).resolve() - - def add(self, value): - super(LazyConfig, self).add(value) - if not self._materialized: - # Buffer additions to end. - self._lazy_suffix += self.sources - del self.sources[:] - - def set(self, value): - super(LazyConfig, self).set(value) - if not self._materialized: - # Buffer additions to beginning. - self._lazy_prefix[:0] = self.sources - del self.sources[:] - - def clear(self): - """Remove all sources from this configuration.""" - super(LazyConfig, self).clear() - self._lazy_suffix = [] - self._lazy_prefix = [] # "Validated" configuration views: experimental! diff --git a/test/test_paths.py b/test/test_paths.py index 6de0b0c..e16e961 100644 --- a/test/test_paths.py +++ b/test/test_paths.py @@ -115,7 +115,7 @@ def tearDown(self): def test_no_sources_when_files_missing(self): config = confuse.Configuration('myapp', read=False) - filenames = [s.filename for s in config.sources] + filenames = [s.filename for s in config.sources if s.loaded] self.assertEqual(filenames, []) def test_search_package(self): From c913d719e55c62e8c75954da0de65fc1aae8668f Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Fri, 24 Apr 2020 09:52:02 -0400 Subject: [PATCH 06/13] adding env source --- confuse.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/confuse.py b/confuse.py index 4b2cd16..b8a52bc 100644 --- a/confuse.py +++ b/confuse.py @@ -169,7 +169,8 @@ def __init__(self, value=UNSET, filename=None, default=False): def __repr__(self): return 'ConfigSource({}, filename={}, default={})'.format( - super(ConfigSource, self).__repr__() if self.loaded else '*Unloaded*', + super(ConfigSource, self).__repr__() if self.loaded + else '*Unloaded*', self.filename, self.default) @property @@ -200,6 +201,7 @@ def ensure_config_dir(self): if dirname and not os.path.isdir(dirname): # for PY2 -_- os.makedirs(dirname) return dirname + return None @classmethod def is_of_type(cls, value): @@ -236,8 +238,10 @@ def of(cls, value, **kw): class YamlSource(ConfigSource): + '''A config source pulled from yaml files.''' EXTENSIONS = '.yaml', '.yml' - def __init__(self, filename=None, value=UNSET, default=False, ignore_missing=False): + def __init__(self, filename=None, value=UNSET, default=False, + ignore_missing=False): self.ignore_missing = ignore_missing super(YamlSource, self).__init__(value, filename, default) @@ -256,6 +260,53 @@ def _load(self): self.update(self._loader(self.filename)) +class EnvSource(ConfigSource): + '''A config source pulled from environment variables.''' + DEFAULT_PREFIX = 'CONFUSE' + def __init__(self, prefix=None, sep='__', value=UNSET): + self._prefix = prefix + self._sep = sep + super(EnvSource, self).__init__( + value=value, filename=None, default=False) + + def _load(self): + self.update(nest_keys( + os.environ, sep=self._sep, + prefix=self._prefix or self.DEFAULT_PREFIX)) + + +def nest_keys(value, sep='.', prefix='', overwrite_higher=False, + overwrite_lower=False): + '''Convert a flat dict with splittable keys to a nested dict split by + that separator.''' + out = {} + # filter by prefix and split by separator + items = ((k[len(prefix):].split(sep), v) + for k, v in value.items() + if k.startswith(prefix)) + + # for each environment variable + for ks, val in sorted(items, key=lambda x: -len(x[0])): + # recursively set keys + dct = out + for k in ks[:-1]: + if (not overwrite_higher and k in dct + and not isinstance(dct[k], dict)): + raise ValueError( + 'Trying to overwrite another value with a nested dict.') + + if k not in dct: + dct[k] = {} + dct = dct[k] + + k = ks[-1] + if not overwrite_lower and k in dct and isinstance(dct[k], dict): + raise ValueError( + 'Trying to overwrite a nested dict with another value.') + dct[k] = val + return out + + class ConfigView(object): """A configuration "view" is a query into a program's configuration data. A view represents a hypothetical location in the configuration @@ -1108,6 +1159,10 @@ def read(self, user=True, defaults=True): if defaults and source.default: source.load() + def resolve(self): + self.add_base() + return super(Configuration, self).resolve() + def set(self, source, **kw): super(Configuration, self).set( ConfigSource.of(self._to_filename(source), **kw)) @@ -1123,6 +1178,7 @@ def _add_base(self, source, **kw): def add_base(self): for source in self._base_sources: self.add(source) + self._base_sources = [] # don't add twice def _to_filename(self, source, default=False): '''Convert a config directory/file to an absolute config file.''' From 28a6330e0c70a8069b3ca648c1784b0b218fd32f Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Fri, 24 Apr 2020 10:53:42 -0400 Subject: [PATCH 07/13] fixed skip missing, repr, expanduser --- confuse.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/confuse.py b/confuse.py index b8a52bc..ad2c494 100644 --- a/confuse.py +++ b/confuse.py @@ -168,22 +168,22 @@ def __init__(self, value=UNSET, filename=None, default=False): self.default = default def __repr__(self): - return 'ConfigSource({}, filename={}, default={})'.format( - super(ConfigSource, self).__repr__() if self.loaded - else '*Unloaded*', + return '{}({}, filename={}, default={})'.format( + self.__class__.__name__, + dict.__repr__(self) + if self.loaded else '[Unloaded]' + if self.exists else "[Source doesn't exist]", self.filename, self.default) @property def exists(self): '''Does this config have access to usable configuration values?''' - # if has a file attached - see if a file is attached and exists - # or if value is set. return self.loaded or self.filename and os.path.isfile(self.filename) def load(self): '''Ensure that the source is loaded.''' if not self.loaded: - self.ensure_config_dir() + self.config_dir() self.loaded = self._load() is not False return self @@ -193,7 +193,7 @@ def _load(self): Otherwise it will be assumed to be loaded. ''' - def ensure_config_dir(self): + def config_dir(self): '''Create the config dir, if there's a filename associated with the source.''' if self.filename: @@ -269,6 +269,13 @@ def __init__(self, prefix=None, sep='__', value=UNSET): super(EnvSource, self).__init__( value=value, filename=None, default=False) + def __repr__(self): + return '{}({}, prefix={}, separator={})'.format( + self.__class__.__name__, + dict.__repr__(self) + if self.loaded else '[Unloaded]', + self._prefix, self._sep) + def _load(self): self.update(nest_keys( os.environ, sep=self._sep, @@ -1118,7 +1125,7 @@ def __init__(self, appname, modname=None, source=None, read=True, # convert user-provided sources to a list of config files for source in _ensure_list(source): - self._add_base(source) + self._add_base(source, ignore_missing=True) # search the users system for config files if not self.sources: @@ -1143,7 +1150,7 @@ def config_dir(self): """ for source in self.sources + self._base_sources: if source.filename: - return source.ensure_config_dir() + return source.config_dir() return None def read(self, user=True, defaults=True): @@ -1185,7 +1192,7 @@ def _to_filename(self, source, default=False): if isinstance(source, ConfigSource): return source if isinstance(source, BASESTRING): - source = os.path.abspath(source) + source = os.path.abspath(os.path.expanduser(source)) # if the source is a directory, look for a config file inside if (os.path.isdir(source) or os.path.splitext(source)[1] not in {'.yaml', '.yml'}): From 2b23f400ac67174a463193048a03f2e1aeb98810 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 25 Apr 2020 01:42:25 -0400 Subject: [PATCH 08/13] fixing travis errors --- confuse.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/confuse.py b/confuse.py index ad2c494..36483e1 100644 --- a/confuse.py +++ b/confuse.py @@ -136,17 +136,24 @@ def __init__(self, filename, reason=None): # Views and sources. -UNSET = object() # sentinel +UNSET = object() # sentinel def _load_first(func): '''Call self.load() before the function is called - used for lazy source loading''' - @wraps(func) def inner(self, *a, **kw): self.load() return func(self, *a, **kw) - return inner + + try: + return wraps(func)(inner) + except AttributeError: + # in v2 they don't ignore missing attributes + # v3: https://github.com/python/cpython/blob/3.8/Lib/functools.py + # v2: https://github.com/python/cpython/blob/2.7/Lib/functools.py + inner.__name__ = func.__name__ + return inner def all_subclasses(cls): @@ -198,7 +205,7 @@ def config_dir(self): source.''' if self.filename: dirname = os.path.dirname(self.filename) - if dirname and not os.path.isdir(dirname): # for PY2 -_- + if dirname and not os.path.isdir(dirname): # for PY2 -_- os.makedirs(dirname) return dirname return None @@ -240,6 +247,7 @@ def of(cls, value, **kw): class YamlSource(ConfigSource): '''A config source pulled from yaml files.''' EXTENSIONS = '.yaml', '.yml' + def __init__(self, filename=None, value=UNSET, default=False, ignore_missing=False): self.ignore_missing = ignore_missing @@ -249,10 +257,11 @@ def __init__(self, filename=None, value=UNSET, default=False, def is_of_type(cls, value): '''Does value look like a .yaml file?''' return ( - isinstance(value, BASESTRING) and - os.path.splitext(value)[1] in cls.EXTENSIONS) + isinstance(value, BASESTRING) + and os.path.splitext(value)[1] in cls.EXTENSIONS) _loader = lambda self, f: load_yaml(f) + def _load(self): '''Load the file if it exists.''' if self.ignore_missing and not os.path.isfile(self.filename): @@ -263,6 +272,7 @@ def _load(self): class EnvSource(ConfigSource): '''A config source pulled from environment variables.''' DEFAULT_PREFIX = 'CONFUSE' + def __init__(self, prefix=None, sep='__', value=UNSET): self._prefix = prefix self._sep = sep @@ -1185,7 +1195,7 @@ def _add_base(self, source, **kw): def add_base(self): for source in self._base_sources: self.add(source) - self._base_sources = [] # don't add twice + self._base_sources = [] # don't add twice def _to_filename(self, source, default=False): '''Convert a config directory/file to an absolute config file.''' From 0ceb9c3cd90eb2049c69d0af8e93a28f3c823b55 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 25 Apr 2020 10:06:02 -0400 Subject: [PATCH 09/13] removed add_base, add+set return source, added tests --- confuse.py | 44 ++++++++++-------------- test/test_dump.py | 8 ++--- test/test_sources.py | 79 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 test/test_sources.py diff --git a/confuse.py b/confuse.py index 36483e1..6bf43c0 100644 --- a/confuse.py +++ b/confuse.py @@ -200,12 +200,12 @@ def _load(self): Otherwise it will be assumed to be loaded. ''' - def config_dir(self): + def config_dir(self, create=True): '''Create the config dir, if there's a filename associated with the source.''' if self.filename: dirname = os.path.dirname(self.filename) - if dirname and not os.path.isdir(dirname): # for PY2 -_- + if create and dirname and not os.path.isdir(dirname): os.makedirs(dirname) return dirname return None @@ -724,10 +724,14 @@ def __init__(self, sources): self.redactions = set() def add(self, obj): - self.sources.append(ConfigSource.of(obj)) + src = ConfigSource.of(obj) + self.sources.append(src) + return src def set(self, value): - self.sources.insert(0, ConfigSource.of(value)) + src = ConfigSource.of(value) + self.sources.insert(0, src) + return src def resolve(self): return ((dict(s), s) for s in self.sources) @@ -796,10 +800,10 @@ def resolve(self): yield value, source def set(self, value): - self.parent.set({self.key: value}) + return self.parent.set({self.key: value}) def add(self, value): - self.parent.add({self.key: value}) + return self.parent.add({self.key: value}) def root(self): return self.parent.root() @@ -1114,7 +1118,7 @@ def restore_yaml_comments(data, default_data): class Configuration(RootView): def __init__(self, appname, modname=None, source=None, read=True, - config_filename=None, default_filename=None): + config_filename=None, default_filename=None, user=None): """Create a configuration object by reading the automatically-discovered config files for the application for a given name. If `modname` is specified, it should be the import @@ -1135,11 +1139,11 @@ def __init__(self, appname, modname=None, source=None, read=True, # convert user-provided sources to a list of config files for source in _ensure_list(source): - self._add_base(source, ignore_missing=True) + self.add(source, ignore_missing=True) # search the users system for config files - if not self.sources: - self._add_base( + if user is not False or not self.sources: + self.add( find_user_config_files( self.appname, env_var, config_fname=self.config_filename, @@ -1147,7 +1151,7 @@ def __init__(self, appname, modname=None, source=None, read=True, # if user specified a module name, load the config if modname: - self._add_base(find_package_config( + self.add(find_package_config( modname, self.default_filename), default=True) if read: @@ -1169,34 +1173,20 @@ def read(self, user=True, defaults=True): discovered user configuration files or the in-package defaults, set `user` or `defaults` to `False`. """ - self.add_base() for source in self.sources: if user and not source.default: source.load() if defaults and source.default: source.load() - def resolve(self): - self.add_base() - return super(Configuration, self).resolve() - def set(self, source, **kw): - super(Configuration, self).set( + return super(Configuration, self).set( ConfigSource.of(self._to_filename(source), **kw)) def add(self, source, **kw): - super(Configuration, self).add( + return super(Configuration, self).add( ConfigSource.of(self._to_filename(source), **kw)) - def _add_base(self, source, **kw): - self._base_sources.append( - ConfigSource.of(self._to_filename(source), **kw)) - - def add_base(self): - for source in self._base_sources: - self.add(source) - self._base_sources = [] # don't add twice - def _to_filename(self, source, default=False): '''Convert a config directory/file to an absolute config file.''' if isinstance(source, ConfigSource): diff --git a/test/test_dump.py b/test/test_dump.py index e40add7..43be3c1 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -49,8 +49,8 @@ def test_dump_ordered_dict(self): def test_dump_sans_defaults(self): config = confuse.Configuration('myapp', read=False) - config.add({'foo': 'bar'}) - config.sources[0].default = True + src = config.add({'foo': 'bar'}) + src.default = True config.add({'baz': 'qux'}) yaml = config.dump().strip() @@ -95,8 +95,8 @@ def test_dump_unredacted(self): def test_dump_redacted_sans_defaults(self): config = confuse.Configuration('myapp', read=False) - config.add({'foo': 'bar'}) - config.sources[0].default = True + src = config.add({'foo': 'bar'}) + src.default = True config.add({'baz': 'qux'}) config['baz'].redact = True diff --git a/test/test_sources.py b/test/test_sources.py new file mode 100644 index 0000000..6c0c34b --- /dev/null +++ b/test/test_sources.py @@ -0,0 +1,79 @@ +from __future__ import division, absolute_import, print_function + +import os +import confuse +import sys +import unittest + +PY3 = sys.version_info[0] == 3 + + +class ConfigSourceTest(unittest.TestCase): + def _load_yaml(self, file): + return {'a': 5, 'file': file} + + def setUp(self): + self._orig_load_yaml = confuse.load_yaml + confuse.load_yaml = self._load_yaml + + def tearDown(self): + confuse.load_yaml = self._orig_load_yaml + + def test_source_conversion(self): + # test pure dict source + src = confuse.ConfigSource.of({'a': 5}) + self.assertIsInstance(src, confuse.ConfigSource) + self.assertEqual(src.loaded, True) + # test yaml filename + src = confuse.ConfigSource.of('asdf/asfdd.yml') + self.assertIsInstance(src, confuse.YamlSource) + self.assertEqual(src.loaded, False) + self.assertEqual(src.exists, False) + self.assertEqual(src.config_dir(create=False), 'asdf') + + def test_explicit_load(self): + src = confuse.ConfigSource.of('asdf.yml') + self.assertEqual(src.loaded, False) + src.load() + self.assertEqual(src.loaded, True) + self.assertEqual(src['a'], 5) + + def test_load_getitem(self): + src = confuse.ConfigSource.of('asdf.yml') + self.assertEqual(src.loaded, False) + self.assertEqual(src['a'], 5) + self.assertEqual(src.loaded, True) + + def test_load_cast_dict(self): + src = confuse.ConfigSource.of('asdf.yml') + self.assertEqual(src.loaded, False) + self.assertEqual(dict(src)['a'], 5) + self.assertEqual(src.loaded, True) + + def test_load_keys(self): + src = confuse.ConfigSource.of('asdf.yml') + self.assertEqual(src.loaded, False) + self.assertEqual(set(src.keys()), {'a', 'file'}) + self.assertEqual(src.loaded, True) + +class EnvSourceTest(unittest.TestCase): + def setenv(self, *a, **kw): + for dct in a + (kw,): + os.environ.update({k: str(v) for k, v in dct.items()}) + + def test_env_var_load(self): + # + PREFIX = 'asdf' + expected = {'a': {'b': '5', 'c': '6'}, 'b': '7'} + + # setup environment + before = set(os.environ.keys()) + self.setenv({PREFIX + 'a__b': 5, PREFIX + 'a__c': 6, PREFIX + 'b': 7}) + self.assertGreater(len(set(os.environ.keys()) - before), 0) + + src = confuse.EnvSource(PREFIX) + self.assertEqual(src.loaded, False) + src.load() + self.assertEqual(src.loaded, True) + print(src) + self.assertEqual(dict(src), expected) From ea65b1e939942f6c466b00ed151628c54d6ee930 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 25 Apr 2020 10:31:58 -0400 Subject: [PATCH 10/13] wtf .. "works fine on my machine !!" ugh --- test/test_sources.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_sources.py b/test/test_sources.py index 6c0c34b..0506362 100644 --- a/test/test_sources.py +++ b/test/test_sources.py @@ -47,7 +47,7 @@ def test_load_getitem(self): def test_load_cast_dict(self): src = confuse.ConfigSource.of('asdf.yml') self.assertEqual(src.loaded, False) - self.assertEqual(dict(src)['a'], 5) + self.assertEqual(dict(src), {'a': 5, 'file': 'asdf.yml'}) self.assertEqual(src.loaded, True) def test_load_keys(self): @@ -56,22 +56,22 @@ def test_load_keys(self): self.assertEqual(set(src.keys()), {'a', 'file'}) self.assertEqual(src.loaded, True) + class EnvSourceTest(unittest.TestCase): def setenv(self, *a, **kw): for dct in a + (kw,): os.environ.update({k: str(v) for k, v in dct.items()}) def test_env_var_load(self): - # - PREFIX = 'asdf' + prefix = 'asdf' expected = {'a': {'b': '5', 'c': '6'}, 'b': '7'} # setup environment before = set(os.environ.keys()) - self.setenv({PREFIX + 'a__b': 5, PREFIX + 'a__c': 6, PREFIX + 'b': 7}) + self.setenv({prefix + 'a__b': 5, prefix + 'a__c': 6, prefix + 'b': 7}) self.assertGreater(len(set(os.environ.keys()) - before), 0) - src = confuse.EnvSource(PREFIX) + src = confuse.EnvSource(prefix) self.assertEqual(src.loaded, False) src.load() self.assertEqual(src.loaded, True) From d0437189482f3c48bd8fb52f7a303a42dd1f43a4 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 25 Apr 2020 10:38:18 -0400 Subject: [PATCH 11/13] need to check how dict is being cast on travis py2 (p2 works for me) --- test/test_sources.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_sources.py b/test/test_sources.py index 0506362..6041933 100644 --- a/test/test_sources.py +++ b/test/test_sources.py @@ -47,6 +47,14 @@ def test_load_getitem(self): def test_load_cast_dict(self): src = confuse.ConfigSource.of('asdf.yml') self.assertEqual(src.loaded, False) + class a(dict): + def __getattribute__(self, k): + print(k) + return super(a, self).__getattribute__(k) + b = a() + print('<<<<<') + x = dict(b) + print('>>>>>') self.assertEqual(dict(src), {'a': 5, 'file': 'asdf.yml'}) self.assertEqual(src.loaded, True) From 2692d499aa6f99742a047fab87e49b69f98cf427 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 25 Apr 2020 13:36:44 -0400 Subject: [PATCH 12/13] tried to figure dict cast thing for like an hour this morning with no luck :/ --- confuse.py | 2 +- test/test_sources.py | 20 ++++++-------------- tox.ini | 10 +++++----- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/confuse.py b/confuse.py index 6bf43c0..7c4633d 100644 --- a/confuse.py +++ b/confuse.py @@ -734,7 +734,7 @@ def set(self, value): return src def resolve(self): - return ((dict(s), s) for s in self.sources) + return ((dict(s.load()), s) for s in self.sources) def clear(self): """Remove all sources (and redactions) from this diff --git a/test/test_sources.py b/test/test_sources.py index 6041933..8413236 100644 --- a/test/test_sources.py +++ b/test/test_sources.py @@ -44,19 +44,12 @@ def test_load_getitem(self): self.assertEqual(src['a'], 5) self.assertEqual(src.loaded, True) - def test_load_cast_dict(self): - src = confuse.ConfigSource.of('asdf.yml') - self.assertEqual(src.loaded, False) - class a(dict): - def __getattribute__(self, k): - print(k) - return super(a, self).__getattribute__(k) - b = a() - print('<<<<<') - x = dict(b) - print('>>>>>') - self.assertEqual(dict(src), {'a': 5, 'file': 'asdf.yml'}) - self.assertEqual(src.loaded, True) + # def test_load_cast_dict(self): + # src = confuse.ConfigSource.of('asdf.yml') + # self.assertEqual(src.loaded, False) + # src.load() + # self.assertEqual(dict(src), {'a': 5, 'file': 'asdf.yml'}) + # self.assertEqual(src.loaded, True) def test_load_keys(self): src = confuse.ConfigSource.of('asdf.yml') @@ -83,5 +76,4 @@ def test_env_var_load(self): self.assertEqual(src.loaded, False) src.load() self.assertEqual(src.loaded, True) - print(src) self.assertEqual(dict(src), expected) diff --git a/tox.ini b/tox.ini index 6f3c9dc..13ef954 100644 --- a/tox.ini +++ b/tox.ini @@ -33,12 +33,12 @@ deps = py{27,34,36}-flake8: {[_flake8]deps} commands = cov: nosetests --with-coverage {posargs} - test: nosetests {posargs} + test: nosetests -s {posargs} py27-flake8: flake8 --min-version 2.7 {posargs} {[_flake8]files} py34-flake8: flake8 --min-version 3.4 {posargs} {[_flake8]files} py36-flake8: flake8 --min-version 3.6 {posargs} {[_flake8]files} -[testenv:docs] -basepython = python2.7 -deps = sphinx -commands = sphinx-build -W -q -b html docs {envtmpdir}/html {posargs} +# [testenv:docs] +# basepython = python2.7 +# deps = sphinx +# commands = sphinx-build -W -q -b html docs {envtmpdir}/html {posargs} From 98592eff8b108fb9811317edd1a4c66cb9a94a83 Mon Sep 17 00:00:00 2001 From: Bea Steers Date: Sat, 25 Apr 2020 21:50:08 -0400 Subject: [PATCH 13/13] oops add docs back in --- tox.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 13ef954..6f3c9dc 100644 --- a/tox.ini +++ b/tox.ini @@ -33,12 +33,12 @@ deps = py{27,34,36}-flake8: {[_flake8]deps} commands = cov: nosetests --with-coverage {posargs} - test: nosetests -s {posargs} + test: nosetests {posargs} py27-flake8: flake8 --min-version 2.7 {posargs} {[_flake8]files} py34-flake8: flake8 --min-version 3.4 {posargs} {[_flake8]files} py36-flake8: flake8 --min-version 3.6 {posargs} {[_flake8]files} -# [testenv:docs] -# basepython = python2.7 -# deps = sphinx -# commands = sphinx-build -W -q -b html docs {envtmpdir}/html {posargs} +[testenv:docs] +basepython = python2.7 +deps = sphinx +commands = sphinx-build -W -q -b html docs {envtmpdir}/html {posargs}