././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1393616048.0 confuse-1.7.0/.coveragerc0000644000000000000000000000031100000000000012164 0ustar00[report] omit = */pyshared/* */python?.?/* */site-packages/nose/* */test/* */example/* exclude_lines = assert False raise NotImplementedError if __name__ == .__main__.: ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1593264429.54552 confuse-1.7.0/.github/workflows/build.yml0000644000000000000000000000360200000000000015270 0ustar00name: Build on: push: branches: - master pull_request: branches: - master jobs: test: name: '${{ matrix.os }}: ${{ matrix.tox-env }}' runs-on: ${{ matrix.os }} strategy: matrix: tox-env: [py27-test, py35-test, py36-test, py37-test, py38-test, pypy-test] os: [ubuntu-latest, windows-latest] # Only test on a couple of versions on Windows. exclude: - os: windows-latest tox-env: py35-test - os: windows-latest tox-env: py36-test - os: windows-latest tox-env: py37-test - os: windows-latest tox-env: pypy-test # Python interpreter versions. :/ include: - tox-env: py27-test python: 2.7 - tox-env: py35-test python: 3.5 - tox-env: py36-test python: 3.6 - tox-env: py37-test python: 3.7 - tox-env: py38-test python: 3.8 - tox-env: pypy-test python: pypy3 steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: Install Tox run: pip install tox - name: Tox run: tox -e ${{ matrix.tox-env }} style: name: Style runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: TrueBrain/actions-flake8@master ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605982760.9017317 confuse-1.7.0/.gitignore0000644000000000000000000000035200000000000012040 0ustar00.DS_Store .idea *~ .python-version __pycache__/ *.py[cod] _build/ build/ dist/ docs/_build/ *.egg-info/ # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1449293036.0 confuse-1.7.0/LICENSE0000644000000000000000000000206300000000000011056 0ustar00The MIT License Copyright (c) 2015 Adrian Sampson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1605982760.901971 confuse-1.7.0/Makefile0000644000000000000000000000123200000000000011506 0ustar00.PHONY: all virtualenv install nopyc clean docs SHELL := /usr/bin/env bash PYTHON_BIN ?= python all: virtualenv install virtualenv: @if [ ! -d "venv" ]; then \ $(PYTHON_BIN) -m pip install virtualenv --user; \ $(PYTHON_BIN) -m virtualenv venv; \ fi install: virtualenv @( \ source venv/bin/activate; \ python -m pip install -r requirements.txt; \ ) nopyc: find . -name '*.pyc' | xargs rm -f || true find . -name __pycache__ | xargs rm -rf || true clean: nopyc rm -rf _build dist *.egg-info venv docs: install @( \ source venv/bin/activate; \ python -m pip install -r docs/requirements.txt; \ sphinx-build -M html docs _build/docs; \ ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3412995 confuse-1.7.0/README.rst0000644000000000000000000000573600000000000011552 0ustar00Confuse: painless YAML config files =================================== .. image:: https://github.com/beetbox/confuse/workflows/Build/badge.svg?branch=master :target: https://github.com/beetbox/confuse/actions .. image:: http://img.shields.io/pypi/v/confuse.svg :target: https://pypi.python.org/pypi/confuse **Confuse** is a configuration library for Python that uses `YAML`_. It takes care of defaults, overrides, type checking, command-line integration, environment variable support, human-readable errors, and standard OS-specific locations. What It Does ------------ Here’s what Confuse brings to the table: - An **utterly sensible API** resembling dictionary-and-list structures but providing **transparent validation** without lots of boilerplate code. Type ``config['num_goats'].get(int)`` to get the configured number of goats and ensure that it’s an integer. - Combine configuration data from **multiple sources**. Using *layering*, Confuse allows user-specific configuration to seamlessly override system-wide configuration, which in turn overrides built-in defaults. An in-package ``config_default.yaml`` can be used to provide bottom-layer defaults using the same syntax that users will see. A runtime overlay allows the program to programmatically override and add configuration values. - Look for configuration files in **platform-specific paths**. Like ``$XDG_CONFIG_HOME`` or ``~/.config`` on Unix; "Application Support" on macOS; ``%APPDATA%`` on Windows. Your program gets its own directory, which you can use to store additional data. You can transparently create this directory on demand if, for example, you need to initialize the configuration file on first run. And an environment variable can be used to override the directory's location. - Integration with **command-line arguments** via `argparse`_ or `optparse`_ from the standard library. Use argparse's declarative API to allow command-line options to override configured defaults. - Include configuration values from **environment variables**. Values undergo automatic type conversion, and nested dicts and lists are supported. Installation ------------ Confuse is available on `PyPI `_ and can be installed using :code:`pip`: .. code-block:: sh pip install confuse Using Confuse ------------- `Confuse's documentation`_ describes its API in detail. Credits ------- Confuse was made to power `beets`_. Like beets, it is available under the `MIT license`_. .. _ConfigParser: http://docs.python.org/library/configparser.html .. _YAML: http://yaml.org/ .. _optparse: http://docs.python.org/dev/library/optparse.html .. _argparse: http://docs.python.org/dev/library/argparse.html .. _logging: http://docs.python.org/library/logging.html .. _Confuse's documentation: http://confuse.readthedocs.org/en/latest/usage.html .. _MIT license: http://www.opensource.org/licenses/mit-license.php .. _beets: https://github.com/beetbox/beets ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1467474853.0 confuse-1.7.0/codecov.yml0000644000000000000000000000026000000000000012213 0ustar00# Don't post a comment on pull requests. comment: off # I think this disables commit statuses? coverage: status: project: no patch: no changes: no ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960218.4771929 confuse-1.7.0/confuse/__init__.py0000644000000000000000000000046300000000000013626 0ustar00"""Painless YAML configuration. """ from __future__ import division, absolute_import, print_function __version__ = '1.7.0' from .exceptions import * # NOQA from .util import * # NOQA from .yaml_util import * # NOQA from .sources import * # NOQA from .templates import * # NOQA from .core import * # NOQA ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3429353 confuse-1.7.0/confuse/core.py0000644000000000000000000006211400000000000013020 0ustar00# -*- coding: utf-8 -*- # This file is part of Confuse. # Copyright 2016, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. """Worry-free YAML configuration files. """ from __future__ import division, absolute_import, print_function import errno import os import yaml from collections import OrderedDict from . import util from . import templates from . import yaml_util from .sources import ConfigSource, EnvSource, YamlSource from .exceptions import ConfigTypeError, NotFoundError, ConfigError CONFIG_FILENAME = 'config.yaml' DEFAULT_FILENAME = 'config_default.yaml' ROOT_NAME = 'root' REDACTED_TOMBSTONE = 'REDACTED' # Views and sources. class ConfigView(object): """A configuration "view" is a query into a program's configuration data. A view represents a hypothetical location in the configuration tree; to extract the data from the location, a client typically calls the ``view.get()`` method. The client can access children in the tree (subviews) by subscripting the parent view (i.e., ``view[key]``). """ name = None """The name of the view, depicting the path taken through the configuration in Python-like syntax (e.g., ``foo['bar'][42]``). """ def resolve(self): """The core (internal) data retrieval method. Generates (value, source) pairs for each source that contains a value for this view. May raise `ConfigTypeError` if a type error occurs while traversing a source. """ raise NotImplementedError def first(self): """Return a (value, source) pair for the first object found for this view. This amounts to the first element returned by `resolve`. If no values are available, a `NotFoundError` is raised. """ pairs = self.resolve() try: return util.iter_first(pairs) except ValueError: raise NotFoundError(u"{0} not found".format(self.name)) def exists(self): """Determine whether the view has a setting in any source. """ try: self.first() except NotFoundError: return False return True def add(self, value): """Set the *default* value for this configuration view. The specified value is added as the lowest-priority configuration data source. """ raise NotImplementedError def set(self, value): """*Override* the value for this configuration view. The specified value is added as the highest-priority configuration data source. """ raise NotImplementedError def root(self): """The RootView object from which this view is descended. """ raise NotImplementedError def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self.name) def __iter__(self): """Iterate over the keys of a dictionary view or the *subviews* of a list view. """ # Try iterating over the keys, if this is a dictionary view. try: for key in self.keys(): yield key except ConfigTypeError: # Otherwise, try iterating over a list view. try: for subview in self.sequence(): yield subview except ConfigTypeError: item, _ = self.first() raise ConfigTypeError( u'{0} must be a dictionary or a list, not {1}'.format( self.name, type(item).__name__ ) ) def __getitem__(self, key): """Get a subview of this view.""" return Subview(self, key) def __setitem__(self, key, value): """Create an overlay source to assign a given key under this view. """ self.set({key: value}) def __contains__(self, key): return self[key].exists() def set_args(self, namespace, dots=False): """Overlay parsed command-line arguments, generated by a library like argparse or optparse, onto this view's value. :param namespace: Dictionary or Namespace to overlay this config with. Supports nested Dictionaries and Namespaces. :type namespace: dict or Namespace :param dots: If True, any properties on namespace that contain dots (.) will be broken down into child dictionaries. :Example: {'foo.bar': 'car'} # Will be turned into {'foo': {'bar': 'car'}} :type dots: bool """ self.set(util.build_dict(namespace, sep='.' if dots else '')) # Magical conversions. These special methods make it possible to use # View objects somewhat transparently in certain circumstances. For # example, rather than using ``view.get(bool)``, it's possible to # just say ``bool(view)`` or use ``view`` in a conditional. def __str__(self): """Get the value for this view as a bytestring. """ if util.PY3: return self.__unicode__() else: return bytes(self.get()) def __unicode__(self): """Get the value for this view as a Unicode string. """ return util.STRING(self.get()) def __nonzero__(self): """Gets the value for this view as a boolean. (Python 2 only.) """ return self.__bool__() def __bool__(self): """Gets the value for this view as a boolean. (Python 3 only.) """ return bool(self.get()) # Dictionary emulation methods. def keys(self): """Returns a list containing all the keys available as subviews of the current views. This enumerates all the keys in *all* dictionaries matching the current view, in contrast to ``view.get(dict).keys()``, which gets all the keys for the *first* dict matching the view. If the object for this view in any source is not a dict, then a `ConfigTypeError` is raised. The keys are ordered according to how they appear in each source. """ keys = [] for dic, _ in self.resolve(): try: cur_keys = dic.keys() except AttributeError: raise ConfigTypeError( u'{0} must be a dict, not {1}'.format( self.name, type(dic).__name__ ) ) for key in cur_keys: if key not in keys: keys.append(key) return keys def items(self): """Iterates over (key, subview) pairs contained in dictionaries from *all* sources at this view. If the object for this view in any source is not a dict, then a `ConfigTypeError` is raised. """ for key in self.keys(): yield key, self[key] def values(self): """Iterates over all the subviews contained in dictionaries from *all* sources at this view. If the object for this view in any source is not a dict, then a `ConfigTypeError` is raised. """ for key in self.keys(): yield self[key] # List/sequence emulation. def sequence(self): """Iterates over the subviews contained in lists from the *first* source at this view. If the object for this view in the first source is not a list or tuple, then a `ConfigTypeError` is raised. """ try: collection, _ = self.first() except NotFoundError: return if not isinstance(collection, (list, tuple)): raise ConfigTypeError( u'{0} must be a list, not {1}'.format( self.name, type(collection).__name__ ) ) # Yield all the indices in the sequence. for index in range(len(collection)): yield self[index] def all_contents(self): """Iterates over all subviews from collections at this view from *all* sources. If the object for this view in any source is not iterable, then a `ConfigTypeError` is raised. This method is intended to be used when the view indicates a list; this method will concatenate the contents of the list from all sources. """ for collection, _ in self.resolve(): try: it = iter(collection) except TypeError: raise ConfigTypeError( u'{0} must be an iterable, not {1}'.format( self.name, type(collection).__name__ ) ) for value in it: yield value # Validation and conversion. def flatten(self, redact=False): """Create a hierarchy of OrderedDicts containing the data from this view, recursively reifying all views to get their represented values. If `redact` is set, then sensitive values are replaced with the string "REDACTED". """ od = OrderedDict() for key, view in self.items(): if redact and view.redact: od[key] = REDACTED_TOMBSTONE else: try: od[key] = view.flatten(redact=redact) except ConfigTypeError: od[key] = view.get() return od def get(self, template=templates.REQUIRED): """Retrieve the value for this view according to the template. The `template` against which the values are checked can be anything convertible to a `Template` using `as_template`. This means you can pass in a default integer or string value, for example, or a type to just check that something matches the type you expect. May raise a `ConfigValueError` (or its subclass, `ConfigTypeError`) or a `NotFoundError` when the configuration doesn't satisfy the template. """ return templates.as_template(template).value(self, template) # Shortcuts for common templates. def as_filename(self): """Get the value as a path. Equivalent to `get(Filename())`. """ return self.get(templates.Filename()) def as_path(self): """Get the value as a `pathlib.Path` object. Equivalent to `get(Path())`. """ return self.get(templates.Path()) def as_choice(self, choices): """Get the value from a list of choices. Equivalent to `get(Choice(choices))`. """ return self.get(templates.Choice(choices)) def as_number(self): """Get the value as any number type: int or float. Equivalent to `get(Number())`. """ return self.get(templates.Number()) def as_str_seq(self, split=True): """Get the value as a sequence of strings. Equivalent to `get(StrSeq(split=split))`. """ return self.get(templates.StrSeq(split=split)) def as_pairs(self, default_value=None): """Get the value as a sequence of pairs of two strings. Equivalent to `get(Pairs(default_value=default_value))`. """ return self.get(templates.Pairs(default_value=default_value)) def as_str(self): """Get the value as a (Unicode) string. Equivalent to `get(unicode)` on Python 2 and `get(str)` on Python 3. """ return self.get(templates.String()) def as_str_expanded(self): """Get the value as a (Unicode) string, with env vars expanded by `os.path.expandvars()`. """ return self.get(templates.String(expand_vars=True)) # Redaction. @property def redact(self): """Whether the view contains sensitive information and should be redacted from output. """ return () in self.get_redactions() @redact.setter def redact(self, flag): self.set_redaction((), flag) def set_redaction(self, path, flag): """Add or remove a redaction for a key path, which should be an iterable of keys. """ raise NotImplementedError() def get_redactions(self): """Get the set of currently-redacted sub-key-paths at this view. """ raise NotImplementedError() class RootView(ConfigView): """The base of a view hierarchy. This view keeps track of the sources that may be accessed by subviews. """ def __init__(self, sources): """Create a configuration hierarchy for a list of sources. At least one source must be provided. The first source in the list has the highest priority. """ self.sources = list(sources) self.name = ROOT_NAME self.redactions = set() def add(self, obj): self.sources.append(ConfigSource.of(obj)) def set(self, value): self.sources.insert(0, ConfigSource.of(value)) def resolve(self): return ((dict(s), s) for s in self.sources) def clear(self): """Remove all sources (and redactions) from this configuration. """ del self.sources[:] self.redactions.clear() def root(self): return self def set_redaction(self, path, flag): if flag: self.redactions.add(path) elif path in self.redactions: self.redactions.remove(path) def get_redactions(self): return self.redactions class Subview(ConfigView): """A subview accessed via a subscript of a parent view.""" def __init__(self, parent, key): """Make a subview of a parent view for a given subscript key. """ self.parent = parent self.key = key # Choose a human-readable name for this view. if isinstance(self.parent, RootView): self.name = '' else: self.name = self.parent.name if not isinstance(self.key, int): self.name += '.' if isinstance(self.key, int): self.name += u'#{0}'.format(self.key) elif isinstance(self.key, bytes): self.name += self.key.decode('utf-8') elif isinstance(self.key, util.STRING): self.name += self.key else: self.name += repr(self.key) def resolve(self): for collection, source in self.parent.resolve(): try: value = collection[self.key] except IndexError: # List index out of bounds. continue except KeyError: # Dict key does not exist. continue except TypeError: # Not subscriptable. raise ConfigTypeError( u"{0} must be a collection, not {1}".format( self.parent.name, type(collection).__name__ ) ) yield value, source def set(self, value): self.parent.set({self.key: value}) def add(self, value): self.parent.add({self.key: value}) def root(self): return self.parent.root() def set_redaction(self, path, flag): self.parent.set_redaction((self.key,) + path, flag) def get_redactions(self): return (kp[1:] for kp in self.parent.get_redactions() if kp and kp[0] == self.key) # Main interface. class Configuration(RootView): def __init__(self, appname, modname=None, read=True, loader=yaml_util.Loader): """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 name of a module whose package will be searched for a default config file. (Otherwise, no defaults are used.) Pass `False` for `read` to disable automatic reading of all discovered configuration files. Use this when creating a configuration object at module load time and then call the `read` method later. Specify the Loader class as `loader`. """ super(Configuration, self).__init__([]) self.appname = appname self.modname = modname self.loader = loader # 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 = util.find_package_path(self.modname) else: self._package_path = None self._env_var = '{0}DIR'.format(self.appname.upper()) 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() self.add(YamlSource(filename, loader=self.loader, optional=True)) 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) self.add(YamlSource(filename, loader=self.loader, optional=True, default=True)) def read(self, user=True, defaults=True): """Find and read the files for this configuration and set them as the sources for this configuration. To disable either discovered user configuration files or the in-package defaults, 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 = util.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. try: os.makedirs(appdir) except OSError as e: if e.errno != errno.EEXIST: raise return appdir def set_file(self, filename, base_for_paths=False): """Parses the file as YAML and inserts it into the configuration sources with highest priority. :param filename: Filename of the YAML file to load. :param base_for_paths: Indicates whether the directory containing the YAML file will be used as the base directory for resolving relative path values stored in the YAML file. Otherwise, by default, the directory returned by `config_dir()` will be used as the base. """ self.set(YamlSource(filename, base_for_paths=base_for_paths, loader=self.loader)) def set_env(self, prefix=None, sep='__'): """Create a configuration overlay at the highest priority from environment variables. After prefix matching and removal, environment variable names will be converted to lowercase for use as keys within the configuration. If there are nested keys, list-like dicts (ie, `{0: 'a', 1: 'b'}`) will be converted into corresponding lists (ie, `['a', 'b']`). The values of all environment variables will be parsed as YAML scalars using the `self.loader` Loader class to ensure type conversion is consistent with YAML file sources. Use the `EnvSource` class directly to load environment variables using non-default behavior and to enable full YAML parsing of values. :param prefix: The prefix to identify the environment variables to use. Defaults to uppercased `self.appname` followed by an underscore. :param sep: Separator within variable names to define nested keys. """ if prefix is None: prefix = '{0}_'.format(self.appname.upper()) self.set(EnvSource(prefix, sep=sep, loader=self.loader)) def dump(self, full=True, redact=False): """Dump the Configuration object to a YAML file. The order of the keys is determined from the default configuration file. All keys not in the default configuration will be appended to the end of the file. :param full: Dump settings that don't differ from the defaults as well :param redact: Remove sensitive information (views with the `redact` flag set) from the output """ if full: out_dict = self.flatten(redact=redact) else: # Exclude defaults when flattening. sources = [s for s in self.sources if not s.default] temp_root = RootView(sources) temp_root.redactions = self.redactions out_dict = temp_root.flatten(redact=redact) yaml_out = yaml.dump(out_dict, Dumper=yaml_util.Dumper, default_flow_style=None, indent=4, width=1000) # Restore comments to the YAML text. default_source = None for source in self.sources: if source.default: default_source = source break if default_source and default_source.filename: with open(default_source.filename, 'rb') as fp: default_data = fp.read() yaml_out = yaml_util.restore_yaml_comments( yaml_out, default_data.decode('utf-8')) return yaml_out def reload(self): """Reload all sources from the file system. This only affects sources that come from files (i.e., `YamlSource` objects); other sources, such as dictionaries inserted with `add` or `set`, will remain unchanged. """ for source in self.sources: if isinstance(source, YamlSource): source.load() class LazyConfig(Configuration): """A Configuration at reads files on demand when it is first 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) 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! ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3442993 confuse-1.7.0/confuse/exceptions.py0000644000000000000000000000321000000000000014241 0ustar00from __future__ import division, absolute_import, print_function import yaml __all__ = [ 'ConfigError', 'NotFoundError', 'ConfigValueError', 'ConfigTypeError', 'ConfigTemplateError', 'ConfigReadError'] YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token" # Exceptions. class ConfigError(Exception): """Base class for exceptions raised when querying a configuration. """ class NotFoundError(ConfigError): """A requested value could not be found in the configuration trees. """ class ConfigValueError(ConfigError): """The value in the configuration is illegal.""" class ConfigTypeError(ConfigValueError): """The value in the configuration did not match the expected type. """ class ConfigTemplateError(ConfigError): """Base class for exceptions raised because of an invalid template. """ class ConfigReadError(ConfigError): """A configuration source could not be read.""" def __init__(self, name, reason=None): self.name = name self.reason = reason message = u'{0} could not be read'.format(name) if (isinstance(reason, yaml.scanner.ScannerError) and reason.problem == YAML_TAB_PROBLEM): # Special-case error message for tab indentation in YAML markup. message += u': found tab character at line {0}, column {1}'.format( reason.problem_mark.line + 1, reason.problem_mark.column + 1, ) elif reason: # Generic error message uses exception's message. message += u': {0}'.format(reason) super(ConfigReadError, self).__init__(message) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3454492 confuse-1.7.0/confuse/sources.py0000644000000000000000000001677300000000000013565 0ustar00from __future__ import division, absolute_import, print_function from .util import BASESTRING, build_dict from . import yaml_util import os class ConfigSource(dict): """A dictionary augmented with metadata about the source of the configuration. """ def __init__(self, value, filename=None, default=False, base_for_paths=False): """Create a configuration source from a dictionary. :param filename: The file with the data for this configuration source. :param default: Indicates whether this source provides the application's default configuration settings. :param base_for_paths: Indicates whether the source file's directory (i.e., the directory component of `self.filename`) should be used as the base directory for resolving relative path values provided by this source, instead of using the application's configuration directory. If no `filename` is provided, `base_for_paths` will be treated as False. See `templates.Filename` for details of the relative path resolution behavior. """ super(ConfigSource, self).__init__(value) 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 self.base_for_paths = base_for_paths if filename is not None else False def __repr__(self): return 'ConfigSource({0!r}, {1!r}, {2!r}, {3!r})'.format( super(ConfigSource, self), self.filename, self.default, self.base_for_paths, ) @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. """ if isinstance(value, ConfigSource): return value elif isinstance(value, dict): return ConfigSource(value) else: raise TypeError(u'source value must be a dict') class YamlSource(ConfigSource): """A configuration data source that reads from a YAML file. """ def __init__(self, filename=None, default=False, base_for_paths=False, optional=False, loader=yaml_util.Loader): """Create a YAML data source by reading data from a file. May raise a `ConfigReadError`. However, if `optional` is enabled, this exception will not be raised in the case when the file does not exist---instead, the source will be silently empty. """ filename = os.path.abspath(filename) super(YamlSource, self).__init__({}, filename, default, base_for_paths) self.loader = loader self.optional = optional self.load() def load(self): """Load YAML data from the source's filename. """ if self.optional and not os.path.isfile(self.filename): value = {} else: value = yaml_util.load_yaml(self.filename, loader=self.loader) or {} self.update(value) class EnvSource(ConfigSource): """A configuration data source loaded from environment variables. """ def __init__(self, prefix, sep='__', lower=True, handle_lists=True, parse_yaml_docs=False, loader=yaml_util.Loader): """Create a configuration source from the environment. :param prefix: The prefix used to identify the environment variables to be loaded into this configuration source. :param sep: Separator within variable names to define nested keys. :param lower: Indicates whether to convert variable names to lowercase after prefix matching. :param handle_lists: If variables are split into nested keys, indicates whether to search for sub-dicts with keys that are sequential integers starting from 0 and convert those dicts to lists. :param parse_yaml_docs: Enable parsing the values of environment variables as full YAML documents. By default, when False, values are parsed only as YAML scalars. :param loader: PyYAML Loader class to use to parse YAML values. """ super(EnvSource, self).__init__({}, filename=None, default=False, base_for_paths=False) self.prefix = prefix self.sep = sep self.lower = lower self.handle_lists = handle_lists self.parse_yaml_docs = parse_yaml_docs self.loader = loader self.load() def load(self): """Load configuration data from the environment. """ # Read config variables with prefix from the environment. config_vars = {} for var, value in os.environ.items(): if var.startswith(self.prefix): key = var[len(self.prefix):] if self.lower: key = key.lower() if self.parse_yaml_docs: # Parse the value as a YAML document, which will convert # string representations of dicts and lists into the # appropriate object (ie, '{foo: bar}' to {'foo': 'bar'}). # Will raise a ConfigReadError if YAML parsing fails. value = yaml_util.load_yaml_string(value, 'env variable ' + var, loader=self.loader) else: # Parse the value as a YAML scalar so that values are type # converted using the same rules as the YAML Loader (ie, # numeric string to int/float, 'true' to True, etc.). Will # not raise a ConfigReadError. value = yaml_util.parse_as_scalar(value, loader=self.loader) config_vars[key] = value if self.sep: # Build a nested dict, keeping keys with `None` values to allow # environment variables to unset values from lower priority sources config_vars = build_dict(config_vars, self.sep, keep_none=True) if self.handle_lists: for k, v in config_vars.items(): config_vars[k] = self._convert_dict_lists(v) self.update(config_vars) @classmethod def _convert_dict_lists(cls, obj): """Recursively search for dicts where all of the keys are integers from 0 to the length of the dict, and convert them to lists. """ # We only deal with dictionaries if not isinstance(obj, dict): return obj # Recursively search values for additional dicts to convert to lists for k, v in obj.items(): obj[k] = cls._convert_dict_lists(v) try: # Convert the keys to integers, mapping the ints back to the keys int_to_key = {int(k): k for k in obj.keys()} except (ValueError): # Not all of the keys represent integers return obj try: # For the integers from 0 to the length of the dict, try to create # a list from the dict values using the integer to key mapping return [obj[int_to_key[i]] for i in range(len(obj))] except (KeyError): # At least one integer within the range is not a key of the dict return obj ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1624888656.9341137 confuse-1.7.0/confuse/templates.py0000644000000000000000000006071700000000000014075 0ustar00from __future__ import division, absolute_import, print_function import os import re import sys from . import util from . import exceptions try: import enum SUPPORTS_ENUM = True except ImportError: SUPPORTS_ENUM = False try: import pathlib SUPPORTS_PATHLIB = True except ImportError: SUPPORTS_PATHLIB = False if sys.version_info >= (3, 3): from collections import abc else: import collections as abc REQUIRED = object() """A sentinel indicating that there is no default value and an exception should be raised when the value is missing. """ class Template(object): """A value template for configuration fields. The template works like a type and instructs Confuse about how to interpret a deserialized YAML value. This includes type conversions, providing a default value, and validating for errors. For example, a filepath type might expand tildes and check that the file exists. """ def __init__(self, default=REQUIRED): """Create a template with a given default value. If `default` is the sentinel `REQUIRED` (as it is by default), then an error will be raised when a value is missing. Otherwise, missing values will instead return `default`. """ self.default = default def __call__(self, view): """Invoking a template on a view gets the view's value according to the template. """ return self.value(view, self) def value(self, view, template=None): """Get the value for a `ConfigView`. May raise a `NotFoundError` if the value is missing (and the template requires it) or a `ConfigValueError` for invalid values. """ try: value, _ = view.first() return self.convert(value, view) except exceptions.NotFoundError: pass # Get default value, or raise if required. return self.get_default_value(view.name) def get_default_value(self, key_name='default'): """Get the default value to return when the value is missing. May raise a `NotFoundError` if the value is required. """ if not hasattr(self, 'default') or self.default is REQUIRED: # The value is required. A missing value is an error. raise exceptions.NotFoundError(u"{} not found".format(key_name)) # The value is not required. return self.default def convert(self, value, view): """Convert the YAML-deserialized value to a value of the desired type. Subclasses should override this to provide useful conversions. May raise a `ConfigValueError` when the configuration is wrong. """ # Default implementation does no conversion. return value def fail(self, message, view, type_error=False): """Raise an exception indicating that a value cannot be accepted. `type_error` indicates whether the error is due to a type mismatch rather than a malformed value. In this case, a more specific exception is raised. """ exc_class = ( exceptions.ConfigTypeError if type_error else exceptions.ConfigValueError) raise exc_class(u'{0}: {1}'.format(view.name, message)) def __repr__(self): return '{0}({1})'.format( type(self).__name__, '' if self.default is REQUIRED else repr(self.default), ) class Integer(Template): """An integer configuration value template. """ def convert(self, value, view): """Check that the value is an integer. Floats are rounded. """ if isinstance(value, int): return value elif isinstance(value, float): return int(value) else: self.fail(u'must be a number', view, True) class Number(Template): """A numeric type: either an integer or a floating-point number. """ def convert(self, value, view): """Check that the value is an int or a float. """ if isinstance(value, util.NUMERIC_TYPES): return value else: self.fail( u'must be numeric, not {0}'.format(type(value).__name__), view, True ) class MappingTemplate(Template): """A template that uses a dictionary to specify other types for the values for a set of keys and produce a validated `AttrDict`. """ def __init__(self, mapping): """Create a template according to a dict (mapping). The mapping's values should themselves either be Types or convertible to Types. """ subtemplates = {} for key, typ in mapping.items(): subtemplates[key] = as_template(typ) self.subtemplates = subtemplates def value(self, view, template=None): """Get a dict with the same keys as the template and values validated according to the value types. """ out = AttrDict() for key, typ in self.subtemplates.items(): out[key] = typ.value(view[key], self) return out def __repr__(self): return 'MappingTemplate({0})'.format(repr(self.subtemplates)) class Sequence(Template): """A template used to validate lists of similar items, based on a given subtemplate. """ def __init__(self, subtemplate): """Create a template for a list with items validated on a given subtemplate. """ self.subtemplate = as_template(subtemplate) def value(self, view, template=None): """Get a list of items validated against the template. """ out = [] for item in view.sequence(): out.append(self.subtemplate.value(item, self)) return out def __repr__(self): return 'Sequence({0})'.format(repr(self.subtemplate)) class MappingValues(Template): """A template used to validate mappings of similar items, based on a given subtemplate applied to the values. All keys in the mapping are considered valid, but values must pass validation by the subtemplate. Similar to the Sequence template but for mappings. """ def __init__(self, subtemplate): """Create a template for a mapping with variable keys and item values validated on a given subtemplate. """ self.subtemplate = as_template(subtemplate) def value(self, view, template=None): """Get a dict with the same keys as the view and the value of each item validated against the subtemplate. """ out = {} for key, item in view.items(): out[key] = self.subtemplate.value(item, self) return out def __repr__(self): return 'MappingValues({0})'.format(repr(self.subtemplate)) class String(Template): """A string configuration value template. """ def __init__(self, default=REQUIRED, pattern=None, expand_vars=False): """Create a template with the added optional `pattern` argument, a regular expression string that the value should match. """ super(String, self).__init__(default) self.pattern = pattern self.expand_vars = expand_vars if pattern: self.regex = re.compile(pattern) def __repr__(self): args = [] if self.default is not REQUIRED: args.append(repr(self.default)) if self.pattern is not None: args.append('pattern=' + repr(self.pattern)) return 'String({0})'.format(', '.join(args)) def convert(self, value, view): """Check that the value is a string and matches the pattern. """ if not isinstance(value, util.BASESTRING): self.fail(u'must be a string', view, True) if self.pattern and not self.regex.match(value): self.fail( u"must match the pattern {0}".format(self.pattern), view ) if self.expand_vars: return os.path.expandvars(value) else: return value class Choice(Template): """A template that permits values from a sequence of choices. """ def __init__(self, choices, default=REQUIRED): """Create a template that validates any of the values from the iterable `choices`. If `choices` is a map, then the corresponding value is emitted. Otherwise, the value itself is emitted. If `choices` is a `Enum`, then the enum entry with the value is emitted. """ super(Choice, self).__init__(default) self.choices = choices def convert(self, value, view): """Ensure that the value is among the choices (and remap if the choices are a mapping). """ if (SUPPORTS_ENUM and isinstance(self.choices, type) and issubclass(self.choices, enum.Enum)): try: return self.choices(value) except ValueError: self.fail( u'must be one of {0!r}, not {1!r}'.format( [c.value for c in self.choices], value ), view ) if value not in self.choices: self.fail( u'must be one of {0!r}, not {1!r}'.format( list(self.choices), value ), view ) if isinstance(self.choices, abc.Mapping): return self.choices[value] else: return value def __repr__(self): return 'Choice({0!r})'.format(self.choices) class OneOf(Template): """A template that permits values complying to one of the given templates. """ def __init__(self, allowed, default=REQUIRED): super(OneOf, self).__init__(default) self.allowed = list(allowed) def __repr__(self): args = [] if self.allowed is not None: args.append('allowed=' + repr(self.allowed)) if self.default is not REQUIRED: args.append(repr(self.default)) return 'OneOf({0})'.format(', '.join(args)) def value(self, view, template): self.template = template return super(OneOf, self).value(view, template) def convert(self, value, view): """Ensure that the value follows at least one template. """ is_mapping = isinstance(self.template, MappingTemplate) for candidate in self.allowed: try: if is_mapping: if isinstance(candidate, Filename) and \ candidate.relative_to: next_template = candidate.template_with_relatives( view, self.template ) next_template.subtemplates[view.key] = as_template( candidate ) else: next_template = MappingTemplate({view.key: candidate}) return view.parent.get(next_template)[view.key] else: return view.get(candidate) except exceptions.ConfigTemplateError: raise except exceptions.ConfigError: pass except ValueError as exc: raise exceptions.ConfigTemplateError(exc) self.fail( u'must be one of {0}, not {1}'.format( repr(self.allowed), repr(value) ), view ) class StrSeq(Template): """A template for values that are lists of strings. Validates both actual YAML string lists and single strings. Strings can optionally be split on whitespace. """ def __init__(self, split=True, default=REQUIRED): """Create a new template. `split` indicates whether, when the underlying value is a single string, it should be split on whitespace. Otherwise, the resulting value is a list containing a single string. """ super(StrSeq, self).__init__(default) self.split = split def _convert_value(self, x, view): if isinstance(x, util.STRING): return x elif isinstance(x, bytes): return x.decode('utf-8', 'ignore') else: self.fail(u'must be a list of strings', view, True) def convert(self, value, view): if isinstance(value, bytes): value = value.decode('utf-8', 'ignore') if isinstance(value, util.STRING): if self.split: value = value.split() else: value = [value] else: try: value = list(value) except TypeError: self.fail(u'must be a whitespace-separated string or a list', view, True) return [self._convert_value(v, view) for v in value] class Pairs(StrSeq): """A template for ordered key-value pairs. This can either be given with the same syntax as for `StrSeq` (i.e. without values), or as a list of strings and/or single-element mappings such as:: - key: value - [key, value] - key The result is a list of two-element tuples. If no value is provided, the `default_value` will be returned as the second element. """ def __init__(self, default_value=None): """Create a new template. `default` is the dictionary value returned for items that are not a mapping, but a single string. """ super(Pairs, self).__init__(split=True) self.default_value = default_value def _convert_value(self, x, view): try: return (super(Pairs, self)._convert_value(x, view), self.default_value) except exceptions.ConfigTypeError: if isinstance(x, abc.Mapping): if len(x) != 1: self.fail(u'must be a single-element mapping', view, True) k, v = util.iter_first(x.items()) elif isinstance(x, abc.Sequence): if len(x) != 2: self.fail(u'must be a two-element list', view, True) k, v = x else: # Is this even possible? -> Likely, if some !directive cause # YAML to parse this to some custom type. self.fail(u'must be a single string, mapping, or a list' u'' + str(x), view, True) return (super(Pairs, self)._convert_value(k, view), super(Pairs, self)._convert_value(v, view)) class Filename(Template): """A template that validates strings as filenames. Filenames are returned as absolute, tilde-free paths. Relative paths are relative to the template's `cwd` argument when it is specified. Otherwise, if the paths come from a file, they will be relative to the configuration directory (see the `config_dir` method) by default or to the base directory of the config file if either the source has `base_for_paths` set to True or the template has `in_source_dir` set to True. Paths from sources without a file are relative to the current working directory. This helps attain the expected behavior when using command-line options. """ def __init__(self, default=REQUIRED, cwd=None, relative_to=None, in_app_dir=False, in_source_dir=False): """`relative_to` is the name of a sibling value that is being validated at the same time. `in_app_dir` indicates whether the path should be resolved inside the application's config directory (even when the setting does not come from a file). `in_source_dir` indicates whether the path should be resolved relative to the directory containing the source file, if there is one, taking precedence over the application's config directory. """ super(Filename, self).__init__(default) self.cwd = cwd self.relative_to = relative_to self.in_app_dir = in_app_dir self.in_source_dir = in_source_dir def __repr__(self): args = [] if self.default is not REQUIRED: args.append(repr(self.default)) if self.cwd is not None: args.append('cwd=' + repr(self.cwd)) if self.relative_to is not None: args.append('relative_to=' + repr(self.relative_to)) if self.in_app_dir: args.append('in_app_dir=True') if self.in_source_dir: args.append('in_source_dir=True') return 'Filename({0})'.format(', '.join(args)) def resolve_relative_to(self, view, template): if not isinstance(template, (abc.Mapping, MappingTemplate)): # disallow config.get(Filename(relative_to='foo')) raise exceptions.ConfigTemplateError( u'relative_to may only be used when getting multiple values.' ) elif self.relative_to == view.key: raise exceptions.ConfigTemplateError( u'{0} is relative to itself'.format(view.name) ) elif self.relative_to not in view.parent.keys(): # self.relative_to is not in the config self.fail( ( u'needs sibling value "{0}" to expand relative path' ).format(self.relative_to), view ) old_template = {} old_template.update(template.subtemplates) # save time by skipping MappingTemplate's init loop next_template = MappingTemplate({}) next_relative = self.relative_to # gather all the needed templates and nothing else while next_relative is not None: try: # pop to avoid infinite loop because of recursive # relative paths rel_to_template = old_template.pop(next_relative) except KeyError: if next_relative in template.subtemplates: # we encountered this config key previously raise exceptions.ConfigTemplateError(( u'{0} and {1} are recursively relative' ).format(view.name, self.relative_to)) else: raise exceptions.ConfigTemplateError(( u'missing template for {0}, needed to expand {1}\'s' u'relative path' ).format(self.relative_to, view.name)) next_template.subtemplates[next_relative] = rel_to_template next_relative = rel_to_template.relative_to return view.parent.get(next_template)[self.relative_to] def value(self, view, template=None): try: path, source = view.first() except exceptions.NotFoundError: return self.get_default_value(view.name) if not isinstance(path, util.BASESTRING): self.fail( u'must be a filename, not {0}'.format(type(path).__name__), view, True ) path = os.path.expanduser(util.STRING(path)) if not os.path.isabs(path): if self.cwd is not None: # relative to the template's argument path = os.path.join(self.cwd, path) elif self.relative_to is not None: path = os.path.join( self.resolve_relative_to(view, template), path, ) elif ((source.filename and self.in_source_dir) or (source.base_for_paths and not self.in_app_dir)): # relative to the directory the source file is in. path = os.path.join(os.path.dirname(source.filename), path) elif source.filename or self.in_app_dir: # From defaults: relative to the app's directory. path = os.path.join(view.root().config_dir(), path) return os.path.abspath(path) class Path(Filename): """A template that validates strings as `pathlib.Path` objects. Filenames are parsed equivalent to the `Filename` template and then converted to `pathlib.Path` objects. For Python 2 it returns the original path as returned by the `Filename` template. """ def value(self, view, template=None): value = super(Path, self).value(view, template) if value is None: return import pathlib return pathlib.Path(value) class Optional(Template): """A template that makes a subtemplate optional. If the value is present and not null, it must validate against the subtemplate. However, if the value is null or missing, the template will still validate, returning a default value. If `allow_missing` is False, the template will not allow missing values while still permitting null. """ def __init__(self, subtemplate, default=None, allow_missing=True): self.subtemplate = as_template(subtemplate) if default is None: # When no default is passed, try to use the subtemplate's # default value as the default for this template try: default = self.subtemplate.get_default_value() except exceptions.NotFoundError: pass self.default = default self.allow_missing = allow_missing def value(self, view, template=None): try: value, _ = view.first() except exceptions.NotFoundError: if self.allow_missing: # Value is missing but not required return self.default # Value must be present even though it can be null. Raise an error. raise exceptions.NotFoundError(u'{} not found'.format(view.name)) if value is None: # None (ie, null) is always a valid value return self.default return self.subtemplate.value(view, self) def __repr__(self): return 'Optional({0}, {1}, allow_missing={2})'.format( repr(self.subtemplate), repr(self.default), self.allow_missing, ) class TypeTemplate(Template): """A simple template that checks that a value is an instance of a desired Python type. """ def __init__(self, typ, default=REQUIRED): """Create a template that checks that the value is an instance of `typ`. """ super(TypeTemplate, self).__init__(default) self.typ = typ def convert(self, value, view): if not isinstance(value, self.typ): self.fail( u'must be a {0}, not {1}'.format( self.typ.__name__, type(value).__name__, ), view, True ) return value class AttrDict(dict): """A `dict` subclass that can be accessed via attributes (dot notation) for convenience. """ def __getattr__(self, key): if key in self: return self[key] else: raise AttributeError(key) def __setattr__(self, key, value): self[key] = value def as_template(value): """Convert a simple "shorthand" Python value to a `Template`. """ if isinstance(value, Template): # If it's already a Template, pass it through. return value elif isinstance(value, abc.Mapping): # Dictionaries work as templates. return MappingTemplate(value) elif value is int: return Integer() elif isinstance(value, int): return Integer(value) elif isinstance(value, type) and issubclass(value, util.BASESTRING): return String() elif isinstance(value, util.BASESTRING): return String(value) elif isinstance(value, set): # convert to list to avoid hash related problems return Choice(list(value)) elif (SUPPORTS_ENUM and isinstance(value, type) and issubclass(value, enum.Enum)): return Choice(value) elif isinstance(value, list): return OneOf(value) elif value is float: return Number() elif isinstance(value, float): return Number(value) elif SUPPORTS_PATHLIB and isinstance(value, pathlib.PurePath): return Path(value) elif value is None: return Template(None) elif value is REQUIRED: return Template() elif value is dict: return TypeTemplate(abc.Mapping) elif value is list: return TypeTemplate(abc.Sequence) elif isinstance(value, type): return TypeTemplate(value) else: raise ValueError(u'cannot convert to template: {0!r}'.format(value)) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1637960190.345762 confuse-1.7.0/confuse/util.py0000644000000000000000000001320200000000000013037 0ustar00from __future__ import division, absolute_import, print_function import os import sys import argparse import optparse import platform import pkgutil PY3 = sys.version_info[0] == 3 STRING = str if PY3 else unicode # noqa: F821 BASESTRING = str if PY3 else basestring # noqa: F821 NUMERIC_TYPES = (int, float) if PY3 else (int, float, long) # noqa: F821 UNIX_DIR_FALLBACK = '~/.config' WINDOWS_DIR_VAR = 'APPDATA' WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming' MAC_DIR = '~/Library/Application Support' def iter_first(sequence): """Get the first element from an iterable or raise a ValueError if the iterator generates no values. """ it = iter(sequence) try: return next(it) except StopIteration: raise ValueError() def namespace_to_dict(obj): """If obj is argparse.Namespace or optparse.Values we'll return a dict representation of it, else return the original object. Redefine this method if using other parsers. :param obj: * :return: :rtype: dict or * """ if isinstance(obj, (argparse.Namespace, optparse.Values)): return vars(obj) return obj def build_dict(obj, sep='', keep_none=False): """Recursively builds a dictionary from an argparse.Namespace, optparse.Values, or dict object. Additionally, if `sep` is a non-empty string, the keys will be split by `sep` and expanded into a nested dict. Keys with a `None` value are dropped by default to avoid unsetting options but can be kept by setting `keep_none` to `True`. :param obj: Namespace, Values, or dict to iterate over. Other values will simply be returned. :type obj: argparse.Namespace or optparse.Values or dict or * :param sep: Separator to use for splitting properties/keys of `obj` for expansion into nested dictionaries. :type sep: str :param keep_none: Whether to keep keys whose value is `None`. :type keep_none: bool :return: A new dictionary or the value passed if obj was not a dict, Namespace, or Values. :rtype: dict or * """ # We expect our root object to be a dict, but it may come in as # a namespace obj = namespace_to_dict(obj) # We only deal with dictionaries if not isinstance(obj, dict): return obj # Get keys iterator keys = obj.keys() if PY3 else obj.iterkeys() if sep: # Splitting keys by `sep` needs sorted keys to prevent parents # from clobbering children keys = sorted(list(keys)) output = {} for key in keys: value = obj[key] if value is None and not keep_none: # Avoid unset options. continue save_to = output result = build_dict(value, sep, keep_none) if sep: # Split keys by `sep` as this signifies nesting split = key.split(sep) if len(split) > 1: # The last index will be the key we assign result to key = split.pop() # Build the dict tree if needed and change where # we're saving to for child_key in split: if child_key in save_to and \ isinstance(save_to[child_key], dict): save_to = save_to[child_key] else: # Clobber or create save_to[child_key] = {} save_to = save_to[child_key] # Save if key in save_to: save_to[key].update(result) else: save_to[key] = result return output # Config file paths, including platform-specific paths and in-package # defaults. def find_package_path(name): """Returns the path to the package containing the named module or None if the path could not be identified (e.g., if ``name == "__main__"``). """ # Based on get_root_path from Flask by Armin Ronacher. loader = pkgutil.get_loader(name) if loader is None or name == '__main__': return None if hasattr(loader, 'get_filename'): filepath = loader.get_filename(name) else: # Fall back to importing the specified module. __import__(name) filepath = sys.modules[name].__file__ return os.path.dirname(os.path.abspath(filepath)) def xdg_config_dirs(): """Returns a list of paths taken from the XDG_CONFIG_DIRS and XDG_CONFIG_HOME environment varibables if they exist """ paths = [] if 'XDG_CONFIG_HOME' in os.environ: paths.append(os.environ['XDG_CONFIG_HOME']) if 'XDG_CONFIG_DIRS' in os.environ: paths.extend(os.environ['XDG_CONFIG_DIRS'].split(':')) else: paths.append('/etc/xdg') paths.append('/etc') return paths def config_dirs(): """Return a platform-specific list of candidates for user configuration directories on the system. The candidates are in order of priority, from highest to lowest. The last element is the "fallback" location to be used when no higher-priority config file exists. """ paths = [] if platform.system() == 'Darwin': paths.append(UNIX_DIR_FALLBACK) paths.append(MAC_DIR) paths.extend(xdg_config_dirs()) elif platform.system() == 'Windows': paths.append(WINDOWS_DIR_FALLBACK) if WINDOWS_DIR_VAR in os.environ: paths.append(os.environ[WINDOWS_DIR_VAR]) else: # Assume Unix. paths.append(UNIX_DIR_FALLBACK) paths.extend(xdg_config_dirs()) # Expand and deduplicate paths. out = [] for path in paths: path = os.path.abspath(os.path.expanduser(path)) if path not in out: out.append(path) return out ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1637960190.347602 confuse-1.7.0/confuse/yaml_util.py0000644000000000000000000002011000000000000014055 0ustar00from __future__ import division, absolute_import, print_function from collections import OrderedDict import yaml from .exceptions import ConfigReadError from .util import BASESTRING # YAML loading. class Loader(yaml.SafeLoader): """A customized YAML loader. This loader deviates from the official YAML spec in a few convenient ways: - All strings as are Unicode objects. - All maps are OrderedDicts. - Strings can begin with % without quotation. """ # All strings should be Unicode objects, regardless of contents. def _construct_unicode(self, node): return self.construct_scalar(node) # Use ordered dictionaries for every YAML map. # From https://gist.github.com/844388 def construct_yaml_map(self, node): data = OrderedDict() yield data value = self.construct_mapping(node) data.update(value) def construct_mapping(self, node, deep=False): if isinstance(node, yaml.MappingNode): self.flatten_mapping(node) else: raise yaml.constructor.ConstructorError( None, None, u'expected a mapping node, but found %s' % node.id, node.start_mark ) mapping = OrderedDict() for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) try: hash(key) except TypeError as exc: raise yaml.constructor.ConstructorError( u'while constructing a mapping', node.start_mark, 'found unacceptable key (%s)' % exc, key_node.start_mark ) value = self.construct_object(value_node, deep=deep) mapping[key] = value return mapping # Allow bare strings to begin with %. Directives are still detected. def check_plain(self): plain = super(Loader, self).check_plain() return plain or self.peek() == '%' @staticmethod def add_constructors(loader): """Modify a PyYAML Loader class to add extra constructors for strings and maps. Call this method on a custom Loader class to make it behave like Confuse's own Loader """ loader.add_constructor('tag:yaml.org,2002:str', Loader._construct_unicode) loader.add_constructor('tag:yaml.org,2002:map', Loader.construct_yaml_map) loader.add_constructor('tag:yaml.org,2002:omap', Loader.construct_yaml_map) Loader.add_constructors(Loader) def load_yaml(filename, loader=Loader): """Read a YAML document from a file. If the file cannot be read or parsed, a ConfigReadError is raised. loader is the PyYAML Loader class to use to parse the YAML. By default, this is Confuse's own Loader class, which is like SafeLoader with extra constructors. """ try: with open(filename, 'rb') as f: return yaml.load(f, Loader=loader) except (IOError, yaml.error.YAMLError) as exc: raise ConfigReadError(filename, exc) def load_yaml_string(yaml_string, name, loader=Loader): """Read a YAML document from a string. If the string cannot be parsed, a ConfigReadError is raised. `yaml_string` is a string to be parsed as a YAML document. `name` is the name to use in error messages. `loader` is the PyYAML Loader class to use to parse the YAML. By default, this is Confuse's own Loader class, which is like SafeLoader with extra constructors. """ try: return yaml.load(yaml_string, Loader=loader) except yaml.error.YAMLError as exc: raise ConfigReadError(name, exc) def parse_as_scalar(value, loader=Loader): """Parse a value as if it were a YAML scalar to perform type conversion that is consistent with YAML documents. `value` should be a string. Non-string inputs or strings that raise YAML errors will be returned unchanged. `Loader` is the PyYAML Loader class to use for parsing, defaulting to Confuse's own Loader class. Examples with the default Loader: - '1' will return 1 as an integer - '1.0' will return 1 as a float - 'true' will return True - The empty string '' will return None """ # We only deal with strings if not isinstance(value, BASESTRING): return value try: loader = loader('') tag = loader.resolve(yaml.ScalarNode, value, (True, False)) node = yaml.ScalarNode(tag, value) return loader.construct_object(node) except yaml.error.YAMLError: # Fallback to returning the value unchanged return value # YAML dumping. class Dumper(yaml.SafeDumper): """A PyYAML Dumper that represents OrderedDicts as ordinary mappings (in order, of course). """ # From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py def represent_mapping(self, tag, mapping, flow_style=None): value = [] node = yaml.MappingNode(tag, value, flow_style=flow_style) if self.alias_key is not None: self.represented_objects[self.alias_key] = node best_style = False if hasattr(mapping, 'items'): mapping = list(mapping.items()) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style): best_style = False if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style): best_style = False value.append((node_key, node_value)) if flow_style is None: if self.default_flow_style is not None: node.flow_style = self.default_flow_style else: node.flow_style = best_style return node def represent_list(self, data): """If a list has less than 4 items, represent it in inline style (i.e. comma separated, within square brackets). """ node = super(Dumper, self).represent_list(data) length = len(data) if self.default_flow_style is None and length < 4: node.flow_style = True elif self.default_flow_style is None: node.flow_style = False return node def represent_bool(self, data): """Represent bool as 'yes' or 'no' instead of 'true' or 'false'. """ if data: value = u'yes' else: value = u'no' return self.represent_scalar('tag:yaml.org,2002:bool', value) def represent_none(self, data): """Represent a None value with nothing instead of 'none'. """ return self.represent_scalar('tag:yaml.org,2002:null', '') Dumper.add_representer(OrderedDict, Dumper.represent_dict) Dumper.add_representer(bool, Dumper.represent_bool) Dumper.add_representer(type(None), Dumper.represent_none) Dumper.add_representer(list, Dumper.represent_list) def restore_yaml_comments(data, default_data): """Scan default_data for comments (we include empty lines in our definition of comments) and place them before the same keys in data. Only works with comments that are on one or more own lines, i.e. not next to a yaml mapping. """ comment_map = dict() default_lines = iter(default_data.splitlines()) for line in default_lines: if not line: comment = "\n" elif line.startswith("#"): comment = "{0}\n".format(line) else: continue while True: line = next(default_lines) if line and not line.startswith("#"): break comment += "{0}\n".format(line) key = line.split(':')[0].strip() comment_map[key] = comment out_lines = iter(data.splitlines()) out_data = "" for line in out_lines: key = line.split(':')[0].strip() if key in comment_map: out_data += comment_map[key] out_data += "{0}\n".format(line) return out_data ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605982760.9050467 confuse-1.7.0/docs/api.rst0000644000000000000000000000145700000000000012312 0ustar00================= API Documentation ================= This part of the documentation covers the interfaces used to develop with :code:`confuse`. Core ---- .. automodule:: confuse.core :members: :private-members: :show-inheritance: Exceptions ---------- .. automodule:: confuse.exceptions :members: :private-members: :show-inheritance: Sources ------- .. automodule:: confuse.sources :members: :private-members: :show-inheritance: Templates --------- .. automodule:: confuse.templates :members: :private-members: :show-inheritance: Utility ------- .. automodule:: confuse.util :members: :private-members: :show-inheritance: YAML Utility ------------ .. automodule:: confuse.yaml_util :members: :private-members: :show-inheritance: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960294.0743856 confuse-1.7.0/docs/changelog.rst0000644000000000000000000000412600000000000013464 0ustar00Changelog --------- v1.7.0 '''''' - Add support for reading configuration values from environment variables (see `EnvSource`). - Resolve a possible race condition when creating configuration directories. v1.6.0 '''''' - A new `Configuration.reload` method makes it convenient to reload and re-parse all YAML files from the file system. v1.5.0 '''''' - A new `MappingValues` template behaves like `Sequence` but for mappings with arbitrary keys. - A new `Optional` template allows other templates to be null. - `Filename` templates now have an option to resolve relative to a specific directory. Also, configuration sources now have a corresponding global option to resolve relative to the base configuration directory instead of the location of the specific configuration file. - There is a better error message for `Sequence` templates when the data from the configuration is not a sequence. v1.4.0 '''''' - `pathlib.PurePath` objects can now be converted to `Path` templates. - `AttrDict` now properly supports (over)writing attributes via dot notation. v1.3.0 '''''' - Break up the `confuse` module into a package. (All names should still be importable from `confuse`.) - When using `None` as a template, the result is a value whose default is `None`. Previously, this was equivalent to leaving the key off entirely, i.e., a template with no default. To get the same effect now, use `confuse.REQUIRED` in the template. v1.2.0 '''''' - `float` values (like ``4.2``) can now be used in templates (just like ``42`` works as an `int` template). - The `Filename` and `Path` templates now correctly accept default values. - It's now possible to provide custom PyYAML `Loader` objects for parsing config files. v1.1.0 '''''' - A new ``Path`` template produces a `pathlib`_ Path object. - Drop support for Python 3.4 (following in the footsteps of PyYAML). - String templates support environment variable expansion. .. _pathlib: https://docs.python.org/3/library/pathlib.html v1.0.0 '''''' The first stable release, and the first that `beets`_ depends on externally. .. _beets: https://beets.io ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1637960231.764546 confuse-1.7.0/docs/conf.py0000644000000000000000000000120100000000000012271 0ustar00import os import sys import datetime as dt sys.path.insert(0, os.path.abspath("..")) extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'sphinx.ext.autosectionlabel', 'sphinx_rtd_theme', ] source_suffix = '.rst' master_doc = 'index' project = u'Confuse' copyright = '2012-{}, Adrian Sampson & contributors'.format( dt.date.today().year ) version = '1.7' release = '1.7.0' exclude_patterns = ['_build'] pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- html_theme = 'sphinx_rtd_theme' htmlhelp_basename = 'Confusedoc' ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1624888656.93458 confuse-1.7.0/docs/examples.rst0000644000000000000000000005601000000000000013352 0ustar00Template Examples ================= These examples demonstrate how the confuse templates work to validate configuration values. Sequence -------- A ``Sequence`` template allows validation of a sequence of configuration items that all must match a subtemplate. The items in the sequence can be simple values or more complex objects, as defined by the subtemplate. When the view is defined in multiple sources, the highest priority source will override the entire list of items, rather than appending new items to the list from lower sources. If the view is not defined in any source of the configuration, an empty list will be returned. As an example of using the ``Sequence`` template, consider a configuration that includes a list of servers, where each server is required to have a host string and an optional port number that defaults to 80. For this example, an initial configuration file named ``servers_example.yaml`` has the following contents: .. code-block:: yaml servers: - host: one.example.com - host: two.example.com port: 8000 - host: three.example.com port: 8080 Validation of this configuration could be performed like this: >>> import confuse >>> import pprint >>> source = confuse.YamlSource('servers_example.yaml') >>> config = confuse.RootView([source]) >>> template = { ... 'servers': confuse.Sequence({ ... 'host': str, ... 'port': 80, ... }), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) {'servers': [{'host': 'one.example.com', 'port': 80}, {'host': 'two.example.com', 'port': 8000}, {'host': 'three.example.com', 'port': 8080}]} The list of items in the initial configuration can be overridden by setting a higher priority source. Continuing the previous example: >>> config.set({ ... 'servers': [ ... {'host': 'four.example.org'}, ... {'host': 'five.example.org', 'port': 9000}, ... ], ... }) >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'servers': [{'host': 'four.example.org', 'port': 80}, {'host': 'five.example.org', 'port': 9000}]} If the requested view is missing, ``Sequence`` returns an empty list: >>> config.clear() >>> config.get(template) {'servers': []} However, if an item within the sequence does not match the subtemplate provided to ``Sequence``, then an error will be raised: >>> config.set({ ... 'servers': [ ... {'host': 'bad_port.example.net', 'port': 'default'} ... ] ... }) >>> try: ... config.get(template) ... except confuse.ConfigError as err: ... print(err) ... servers#0.port: must be a number .. note:: A python list is not the shortcut for defining a ``Sequence`` template but will instead produce a ``OneOf`` template. For example, ``config.get([str])`` is equivalent to ``config.get(confuse.OneOf([str]))`` and *not* ``config.get(confuse.Sequence(str))``. MappingValues ------------- A ``MappingValues`` template allows validation of a mapping of configuration items where the keys can be arbitrary but all the values need to match a subtemplate. Use cases include simple user-defined key:value pairs or larger configuration blocks that all follow the same structure, but where the keys naming each block are user-defined. In addition, individual items in the mapping can be overridden and new items can be added by higher priority configuration sources. This is in contrast to the ``Sequence`` template, in which a higher priority source overrides the entire list of configuration items provided by a lower source. In the following example, a hypothetical todo list program can be configured with user-defined colors and category labels. Colors are required to be in hex format. For each category, a description is required and a priority level is optional, with a default value of 0. An initial configuration file named ``todo_example.yaml`` has the following contents: .. code-block:: yaml colors: red: '#FF0000' green: '#00FF00' blue: '#0000FF' categories: default: description: Things to do high: description: These are important priority: 50 low: description: Will get to it eventually priority: -10 Validation of this configuration could be performed like this: >>> import confuse >>> import pprint >>> source = confuse.YamlSource('todo_example.yaml') >>> config = confuse.RootView([source]) >>> template = { ... 'colors': confuse.MappingValues( ... confuse.String(pattern='#[0-9a-fA-F]{6,6}') ... ), ... 'categories': confuse.MappingValues({ ... 'description': str, ... 'priority': 0, ... }), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) {'categories': {'default': {'description': 'Things to do', 'priority': 0}, 'high': {'description': 'These are important', 'priority': 50}, 'low': {'description': 'Will get to it eventually', 'priority': -10}}, 'colors': {'blue': '#0000FF', 'green': '#00FF00', 'red': '#FF0000'}} Items in the initial configuration can be overridden and the mapping can be extended by setting a higher priority source. Continuing the previous example: >>> config.set({ ... 'colors': { ... 'green': '#008000', ... 'orange': '#FFA500', ... }, ... 'categories': { ... 'urgent': { ... 'description': 'Must get done now', ... 'priority': 100, ... }, ... 'high': { ... 'description': 'Important, but not urgent', ... 'priority': 20, ... }, ... }, ... }) >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'categories': {'default': {'description': 'Things to do', 'priority': 0}, 'high': {'description': 'Important, but not urgent', 'priority': 20}, 'low': {'description': 'Will get to it eventually', 'priority': -10}, 'urgent': {'description': 'Must get done now', 'priority': 100}}, 'colors': {'blue': '#0000FF', 'green': '#008000', 'orange': '#FFA500', 'red': '#FF0000'}} If the requested view is missing, ``MappingValues`` returns an empty dict: >>> config.clear() >>> config.get(template) {'colors': {}, 'categories': {}} However, if an item within the mapping does not match the subtemplate provided to ``MappingValues``, then an error will be raised: >>> config.set({ ... 'categories': { ... 'no_description': { ... 'priority': 10, ... }, ... }, ... }) >>> try: ... config.get(template) ... except confuse.ConfigError as err: ... print(err) ... categories.no_description.description not found Filename -------- A ``Filename`` template validates a string as a filename, which is normalized and returned as an absolute, tilde-free path. By default, relative path values that are provided in config files are resolved relative to the application's configuration directory, as returned by ``Configuration.config_dir()``, while relative paths from command-line options are resolved from the current working directory. However, these default relative path behaviors can be changed using the ``cwd``, ``relative_to``, ``in_app_dir``, or ``in_source_dir`` parameters to the ``Filename`` template. In addition, relative path resolution for an entire source file can be changed by creating a ``ConfigSource`` with the ``base_for_paths`` parameter set to True. Setting the behavior at the source-level can be useful when all ``Filename`` templates should be relative to the source. The template-level parameters provide more fine-grained control. While the directory used for resolving relative paths can be controlled, the ``Filename`` template should not be used to guarantee that a file is contained within a given directory, because an absolute path may be provided and will not be subject to resolution. In addition, ``Filename`` validation only ensures that the filename is a valid path on the platform where the application is running, not that the file or any parent directories exist or could be created. .. note:: Running the example below will create the application config directory ``~/.config/ExampleApp/`` on MacOS and Unix machines or ``%APPDATA%\ExampleApp\`` on Windows machines. The filenames in the sample output will also be different on your own machine because the paths to the config files and the current working directory will be different. For this example, we will validate a configuration with filenames that should be resolved as follows: * ``library``: a filename that should always be resolved relative to the application's config directory * ``media_dir``: a directory that should always be resolved relative to the source config file that provides that value * ``photo_dir`` and ``video_dir``: subdirectories that should be resolved relative of the value of ``media_dir`` * ``temp_dir``: a directory that should be resolved relative to ``/tmp/`` * ``log``: a filename that follows the default ``Filename`` template behavior The initial user config file will be at ``~/.config/ExampleApp/config.yaml``, where it will be discovered automatically using the :ref:`Search Paths`, and has the following contents: .. code-block:: yaml library: library.db media_dir: media photo_dir: my_photos video_dir: my_videos temp_dir: example_tmp log: example.log Validation of this initial user configuration could be performed as follows: >>> import confuse >>> import pprint >>> config = confuse.Configuration('ExampleApp', __name__) # Loads user config >>> print(config.config_dir()) # Application config directory /home/user/.config/ExampleApp >>> template = { ... 'library': confuse.Filename(in_app_dir=True), ... 'media_dir': confuse.Filename(in_source_dir=True), ... 'photo_dir': confuse.Filename(relative_to='media_dir'), ... 'video_dir': confuse.Filename(relative_to='media_dir'), ... 'temp_dir': confuse.Filename(cwd='/tmp'), ... 'log': confuse.Filename(), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) {'library': '/home/user/.config/ExampleApp/library.db', 'log': '/home/user/.config/ExampleApp/example.log', 'media_dir': '/home/user/.config/ExampleApp/media', 'photo_dir': '/home/user/.config/ExampleApp/media/my_photos', 'temp_dir': '/tmp/example_tmp', 'video_dir': '/home/user/.config/ExampleApp/media/my_videos'} Because the user configuration file ``config.yaml`` was in the application's configuration directory of ``/home/user/.config/ExampleApp/``, all of the filenames are below ``/home/user/.config/ExampleApp/`` except for ``temp_dir``, whose template used the ``cwd`` parameter. However, if the following YAML file is then loaded from ``/var/tmp/example/config.yaml`` as a higher-level source, some of the paths will no longer be relative to the application config directory: .. code-block:: yaml library: new_library.db media_dir: new_media photo_dir: new_photos # video_dir: my_videos # Not overridden temp_dir: ./new_example_tmp log: new_example.log Continuing the example code from above: >>> config.set_file('/var/tmp/example/config.yaml') >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'library': '/home/user/.config/ExampleApp/new_library.db', 'log': '/home/user/.config/ExampleApp/new_example.log', 'media_dir': '/var/tmp/example/new_media', 'photo_dir': '/var/tmp/example/new_media/new_photos', 'temp_dir': '/tmp/new_example_tmp', 'video_dir': '/var/tmp/example/new_media/my_videos'} Now, the ``media_dir`` and its subdirectories are relative to the directory containing the new source file, because the ``media_dir`` template used the ``in_source_dir`` parameter. However, ``log`` remains in the application config directory because it uses the default ``Filename`` template behavior. The base directories for the ``library`` and ``temp_dir`` items are also not affected. If the previous YAML file is instead loaded with the ``base_for_paths`` parameter set to True, then a default ``Filename`` template will use that config file's directory as the base for resolving relative paths: >>> config.set_file('/var/tmp/example/config.yaml', base_for_paths=True) >>> updated_config = config.get(template) >>> pprint.pprint(updated_config) {'library': '/home/user/.config/ExampleApp/new_library.db', 'log': '/var/tmp/example/new_example.log', 'media_dir': '/var/tmp/example/new_media', 'photo_dir': '/var/tmp/example/new_media/new_photos', 'temp_dir': '/tmp/new_example_tmp', 'video_dir': '/var/tmp/example/new_media/my_videos'} The filename for ``log`` is now within the directory containing the new source file. However, the directory for the ``library`` file has not changed since its template uses the ``in_app_dir`` parameter, which takes precedence over the source's ``base_for_paths`` setting. The template-level ``cwd`` parameter, used with ``temp_dir``, also takes precedence over the source setting. For configuration values set from command-line options, relative paths will be resolved from the current working directory by default, but the ``cwd``, ``relative_to``, and ``in_app_dir`` template parameters alter that behavior. Continuing the example code from above, command-line options are mimicked here by splitting a mock command line string and parsing it with ``argparse``: >>> import os >>> print(os.getcwd()) # Current working directory /home/user >>> import argparse >>> parser = argparse.ArgumentParser() >>> parser.add_argument('--library') >>> parser.add_argument('--media_dir') >>> parser.add_argument('--photo_dir') >>> parser.add_argument('--temp_dir') >>> parser.add_argument('--log') >>> cmd_line=('--library cmd_line_library --media_dir cmd_line_media ' ... '--photo_dir cmd_line_photo --temp_dir cmd_line_tmp ' ... '--log cmd_line_log') >>> args = parser.parse_args(cmd_line.split()) >>> config.set_args(args) >>> config_with_cmdline = config.get(template) >>> pprint.pprint(config_with_cmdline) {'library': '/home/user/.config/ExampleApp/cmd_line_library', 'log': '/home/user/cmd_line_log', 'media_dir': '/home/user/cmd_line_media', 'photo_dir': '/home/user/cmd_line_media/cmd_line_photo', 'temp_dir': '/tmp/cmd_line_tmp', 'video_dir': '/home/user/cmd_line_media/my_videos'} Now the ``log`` and ``media_dir`` paths are relative to the current working directory of ``/home/user``, while the ``photo_dir`` and ``video_dir`` paths remain relative to the updated ``media_dir`` path. The ``library`` and ``temp_dir`` paths are still resolved as before, because those templates used ``in_app_dir`` and ``cwd``, respectively. If a configuration value is provided as an absolute path, the path will be normalized but otherwise unchanged. Here is an example of overridding earlier values with absolute paths: >>> config.set({ ... 'library': '~/home_library.db', ... 'media_dir': '/media', ... 'video_dir': '/video_not_under_media', ... 'temp_dir': '/var/./remove_me/..//tmp', ... 'log': '/var/log/example.log', ... }) >>> absolute_config = config.get(template) >>> pprint.pprint(absolute_config) {'library': '/home/user/home_library.db', 'log': '/var/log/example.log', 'media_dir': '/media', 'photo_dir': '/media/cmd_line_photo', 'temp_dir': '/var/tmp', 'video_dir': '/video_not_under_media'} The paths for ``library`` and ``temp_dir`` have been normalized, but are not impacted by their template parameters. Since ``photo_dir`` was not overridden, the previous relative path value is now being resolved from the new ``media_dir`` absolute path. However, the ``video_dir`` was set to an absolute path and is no longer a subdirectory of ``media_dir``. Path ---- A ``Path`` template works the same as a ``Filename`` template, but returns a ``pathlib.Path`` object instead of a string. Using the same initial example as above for ``Filename`` but with ``Path`` templates gives the following: >>> import confuse >>> import pprint >>> config = confuse.Configuration('ExampleApp', __name__) >>> print(config.config_dir()) # Application config directory /home/user/.config/ExampleApp >>> template = { ... 'library': confuse.Path(in_app_dir=True), ... 'media_dir': confuse.Path(in_source_dir=True), ... 'photo_dir': confuse.Path(relative_to='media_dir'), ... 'video_dir': confuse.Path(relative_to='media_dir'), ... 'temp_dir': confuse.Path(cwd='/tmp'), ... 'log': confuse.Path(), ... } >>> valid_config = config.get(template) >>> pprint.pprint(valid_config) {'library': PosixPath('/home/user/.config/ExampleApp/library.db'), 'log': PosixPath('/home/user/.config/ExampleApp/example.log'), 'media_dir': PosixPath('/home/user/.config/ExampleApp/media'), 'photo_dir': PosixPath('/home/user/.config/ExampleApp/media/my_photos'), 'temp_dir': PosixPath('/tmp/example_tmp'), 'video_dir': PosixPath('/home/user/.config/ExampleApp/media/my_videos')} Optional -------- While many templates like ``Integer`` and ``String`` can be configured to return a default value if the requested view is missing, validation with these templates will fail if the value is left blank in the YAML file or explicitly set to ``null`` in YAML (ie, ``None`` in python). The ``Optional`` template can be used with other templates to allow its subtemplate to accept ``null`` as valid and return a default value. The default behavior of ``Optional`` allows the requested view to be missing, but this behavior can be changed by passing ``allow_missing=False``, in which case the view must be present but its value can still be ``null``. In all cases, any value other than ``null`` will be passed to the subtemplate for validation, and an appropriate ``ConfigError`` will be raised if validation fails. ``Optional`` can also be used with more complex templates like ``MappingTemplate`` to make entire sections of the configuration optional. Consider a configuration where ``log`` can be set to a filename to enable logging to that file or set to ``null`` or not included in the configuration to indicate logging to the console. All of the following are valid configurations using the ``Optional`` template with ``Filename`` as the subtemplate: >>> import sys >>> import confuse >>> def get_log_output(config): ... output = config['log'].get(confuse.Optional(confuse.Filename())) ... if output is None: ... return sys.stderr ... return output ... >>> config = confuse.RootView([]) >>> config.set({'log': '/tmp/log.txt'}) # `log` set to a filename >>> get_log_output(config) '/tmp/log.txt' >>> config.set({'log': None}) # `log` set to None (ie, null in YAML) >>> get_log_output(config) <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'> >>> config.clear() # Clear config so that `log` is missing >>> get_log_output(config) <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'> However, validation will still fail with ``Optional`` if a value is given that is invalid for the subtemplate: >>> config.set({'log': True}) >>> try: ... get_log_output(config) ... except confuse.ConfigError as err: ... print(err) ... log: must be a filename, not bool And without wrapping the ``Filename`` subtemplate in ``Optional``, ``null`` values are not valid: >>> config.set({'log': None}) >>> try: ... config['log'].get(confuse.Filename()) ... except confuse.ConfigError as err: ... print(err) ... log: must be a filename, not NoneType If a program wants to require an item to be present in the configuration, while still allowing ``null`` to be valid, pass ``allow_missing=False`` when creating the ``Optional`` template: >>> def get_log_output_no_missing(config): ... output = config['log'].get(confuse.Optional(confuse.Filename(), ... allow_missing=False)) ... if output is None: ... return sys.stderr ... return output ... >>> config.set({'log': None}) # `log` set to None is still OK... >>> get_log_output_no_missing(config) <_io.TextIOWrapper name='' mode='w' encoding='UTF-8'> >>> config.clear() # but `log` missing now raises an error >>> try: ... get_log_output_no_missing(config) ... except confuse.ConfigError as err: ... print(err) ... log not found The default value returned by ``Optional`` can be set explicitly by passing a value to its ``default`` parameter. However, if no explicit default is passed to ``Optional`` and the subtemplate has a default value defined, then ``Optional`` will return the subtemplate's default value. For subtemplates that do not define default values, like ``MappingTemplate``, ``None`` will be returned as the default unless an explicit default is provided. In the following example, ``Optional`` is used to make an ``Integer`` template more lenient, allowing blank values to validate. In addition, the entire ``extra_config`` block can be left out without causing validation errors. If we have a file named ``optional.yaml`` with the following contents: .. code-block:: yaml favorite_number: # No favorite number provided, but that's OK # This part of the configuration is optional. Uncomment to include. # extra_config: # fruit: apple # number: 10 Then the configuration can be validated as follows: >>> import confuse >>> source = confuse.YamlSource('optional.yaml') >>> config = confuse.RootView([source]) >>> # The following `Optional` templates are all equivalent ... config['favorite_number'].get(confuse.Optional(5)) 5 >>> config['favorite_number'].get(confuse.Optional(confuse.Integer(5))) 5 >>> config['favorite_number'].get(confuse.Optional(int, default=5)) 5 >>> # But a default passed to `Optional` takes precedence and can be any type ... config['favorite_number'].get(confuse.Optional(5, default='five')) 'five' >>> # `Optional` with `MappingTemplate` returns `None` by default ... extra_config = config['extra_config'].get(confuse.Optional( ... {'fruit': str, 'number': int}, ... )) >>> print(extra_config is None) True >>> # But any default value can be provided, like an empty dict... ... config['extra_config'].get(confuse.Optional( ... {'fruit': str, 'number': int}, ... default={}, ... )) {} >>> # or a dict with default values ... config['extra_config'].get(confuse.Optional( ... {'fruit': str, 'number': int}, ... default={'fruit': 'orange', 'number': 3}, ... )) {'fruit': 'orange', 'number': 3} Without the ``Optional`` template wrapping the ``Integer``, the blank value in the YAML file will cause an error: >>> try: ... config['favorite_number'].get(5) ... except confuse.ConfigError as err: ... print(err) ... favorite_number: must be a number If the ``extra_config`` for this example configuration is supplied, it must still match the subtemplate. Therefore, this will fail: >>> config.set({'extra_config': {}}) >>> try: ... config['extra_config'].get(confuse.Optional( ... {'fruit': str, 'number': int}, ... )) ... except confuse.ConfigError as err: ... print(err) ... extra_config.fruit not found But this override of the example configuration will validate: >>> config.set({'extra_config': {'fruit': 'banana', 'number': 1}}) >>> config['extra_config'].get(confuse.Optional( ... {'fruit': str, 'number': int}, ... )) {'fruit': 'banana', 'number': 1} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1621100356.210438 confuse-1.7.0/docs/index.rst0000644000000000000000000000015700000000000012644 0ustar00.. include:: ../README.rst .. toctree:: :maxdepth: 3 :hidden: usage examples changelog api ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1605982760.907031 confuse-1.7.0/docs/requirements.txt0000644000000000000000000000003600000000000014263 0ustar00Sphinx sphinx_rtd_theme PyYAML././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1637960190.348742 confuse-1.7.0/docs/usage.rst0000644000000000000000000005136600000000000012651 0ustar00Confuse: Painless Configuration =============================== `Confuse`_ is a straightforward, full-featured configuration system for Python. .. _Confuse: https://github.com/beetbox/confuse Basic Usage ----------- Set up your Configuration object, which provides unified access to all of your application’s config settings: .. code-block:: python config = confuse.Configuration('MyGreatApp', __name__) The first parameter is required; it’s the name of your application, which will be used to search the system for a config file named ``config.yaml``. See :ref:`Search Paths` for the specific locations searched. The second parameter is optional: it’s the name of a module that will guide the search for a *defaults* file. Use this if you want to include a ``config_default.yaml`` file inside your package. (The included ``example`` package does exactly this.) Now, you can access your configuration data as if it were a simple structure consisting of nested dicts and lists—except that you need to call the method ``.get()`` on the leaf of this tree to get the result as a value: .. code-block:: python value = config['foo'][2]['bar'].get() Under the hood, accessing items in your configuration tree builds up a *view* into your app’s configuration. Then, ``get()`` flattens this view into a value, performing a search through each configuration data source to find an answer. (More on views later.) If you know that a configuration value should have a specific type, just pass that type to ``get()``: .. code-block:: python int_value = config['number_of_goats'].get(int) This way, Confuse will either give you an integer or raise a ``ConfigTypeError`` if the user has messed up the configuration. You’re safe to assume after this call that ``int_value`` has the right type. If the key doesn’t exist in any configuration file, Confuse will raise a ``NotFoundError``. Together, catching these exceptions (both subclasses of ``confuse.ConfigError``) lets you painlessly validate the user’s configuration as you go. View Theory ----------- The Confuse API is based on the concept of *views*. You can think of a view as a *place to look* in a config file: for example, one view might say “get the value for key ``number_of_goats``”. Another might say “get the value at index 8 inside the sequence for key ``animal_counts``”. To get the value for a given view, you *resolve* it by calling the ``get()`` method. This concept separates the specification of a location from the mechanism for retrieving data from a location. (In this sense, it’s a little like `XPath`_: you specify a path to data you want and *then* you retrieve it.) Using views, you can write ``config['animal_counts'][8]`` and know that no exceptions will be raised until you call ``get()``, even if the ``animal_counts`` key does not exist. More importantly, it lets you write a single expression to search many different data sources without preemptively merging all sources together into a single data structure. Views also solve an important problem with overriding collections. Imagine, for example, that you have a dictionary called ``deliciousness`` in your config file that maps food names to tastiness ratings. If the default configuration gives carrots a rating of 8 and the user’s config rates them a 10, then clearly ``config['deliciousness']['carrots'].get()`` should return 10. But what if the two data sources have different sets of vegetables? If the user provides a value for broccoli and zucchini but not carrots, should carrots have a default deliciousness value of 8 or should Confuse just throw an exception? With Confuse’s views, the application gets to decide. The above expression, ``config['deliciousness']['carrots'].get()``, returns 10 (falling back on the default). However, you can also write ``config['deliciousness'].get()``. This expression will cause the *entire* user-specified mapping to override the default one, providing a dict object like ``{'broccoli': 7, 'zucchini': 9}``. As a rule, then, resolve a view at the same granularity you want config files to override each other. .. _XPath: http://www.w3.org/TR/xpath/ Validation ---------- We saw above that you can easily assert that a configuration value has a certain type by passing that type to ``get()``. But sometimes you need to do more than just type checking. For this reason, Confuse provides a few methods on views that perform fancier validation or even conversion: * ``as_filename()``: Normalize a filename, substituting tildes and absolute-ifying relative paths. For filenames defined in a config file, by default the filename is relative to the application's config directory (``Configuration.config_dir()``, as described below). However, if the config file was loaded with the ``base_for_paths`` parameter set to ``True`` (see :ref:`Manually Specifying Config Files`), then a relative path refers to the directory containing the config file. A relative path from any other source (e.g., command-line options) is relative to the working directory. For full control over relative path resolution, use the ``Filename`` template directly (see :ref:`Filename`). * ``as_choice(choices)``: Check that a value is one of the provided choices. The argument should be a sequence of possible values. If the sequence is a ``dict``, then this method returns the associated value instead of the key. * ``as_number()``: Raise an exception unless the value is of a numeric type. * ``as_pairs()``: Get a collection as a list of pairs. The collection should be a list of elements that are either pairs (i.e., two-element lists) already or single-entry dicts. This can be helpful because, in YAML, lists of single-element mappings have a simple syntax (``- key: value``) and, unlike real mappings, preserve order. * ``as_str_seq()``: Given either a string or a list of strings, return a list of strings. A single string is split on whitespace. * ``as_str_expanded()``: Expand any environment variables contained in a string using `os.path.expandvars()`_. .. _os.path.expandvars(): https://docs.python.org/library/os.path.html#os.path.expandvars For example, ``config['path'].as_filename()`` ensures that you get a reasonable filename string from the configuration. And calling ``config['direction'].as_choice(['up', 'down'])`` will raise a ``ConfigValueError`` unless the ``direction`` value is either "up" or "down". Command-Line Options -------------------- Arguments to command-line programs can be seen as just another *source* for configuration options. Just as options in a user-specific configuration file should override those from a system-wide config, command-line options should take priority over all configuration files. You can use the `argparse`_ and `optparse`_ modules from the standard library with Confuse to accomplish this. Just call the ``set_args`` method on any view and pass in the object returned by the command-line parsing library. Values from the command-line option namespace object will be added to the overlay for the view in question. For example, with argparse: .. code-block:: python args = parser.parse_args() config.set_args(args) Correspondingly, with optparse: .. code-block:: python options, args = parser.parse_args() config.set_args(options) This call will turn all of the command-line options into a top-level source in your configuration. The key associated with each option in the parser will become a key available in your configuration. For example, consider this argparse script: .. code-block:: python config = confuse.Configuration('myapp') parser = argparse.ArgumentParser() parser.add_argument('--foo', help='a parameter') args = parser.parse_args() config.set_args(args) print(config['foo'].get()) This will allow the user to override the configured value for key ``foo`` by passing ``--foo `` on the command line. Overriding nested values can be accomplished by passing `dots=True` and have dot-delimited properties on the incoming object. .. code-block:: python parser.add_argument('--bar', help='nested parameter', dest='foo.bar') args = parser.parse_args() # args looks like: {'foo.bar': 'value'} config.set_args(args, dots=True) print(config['foo']['bar'].get()) `set_args` works with generic dictionaries too. .. code-block:: python args = { 'foo': { 'bar': 1 } } config.set_args(args, dots=True) print(config['foo']['bar'].get()) .. _argparse: http://docs.python.org/dev/library/argparse.html .. _parse_args: http://docs.python.org/library/argparse.html#the-parse-args-method .. _optparse: http://docs.python.org/library/optparse.html Note that, while you can use the full power of your favorite command-line parsing library, you'll probably want to avoid specifying defaults in your argparse or optparse setup. This way, Confuse can use other configuration sources---possibly your ``config_default.yaml``---to fill in values for unspecified command-line switches. Otherwise, the argparse/optparse default value will hide options configured elsewhere. Environment Variables --------------------- Confuse supports using environment variables as another source to provide an additional layer of configuration. The environment variables to include are identified by a prefix, which defaults to the uppercased name of your application followed by an underscore. Matching environment variable names are first stripped of this prefix and then lowercased to determine the corresponding configuration option. To load the environment variables for your application using the default prefix, just call ``set_env`` on your ``Configuration`` object. Config values from the environment will then be added as an overlay at the highest precedence. For example: .. code-block:: sh export MYAPP_FOO=something .. code-block:: python import confuse config = confuse.Configuration('myapp', __name__) config.set_env() print(config['foo'].get()) Nested config values can be overridden by using a separator string in the environment variable name. By default, double underscores are used as the separator for nesting, to avoid clashes with config options that contain single underscores. Note that most shells restrict environment variable names to alphanumeric and underscore characters, so dots are not a valid separator. .. code-block:: sh export MYAPP_FOO__BAR=something .. code-block:: python import confuse config = confuse.Configuration('myapp', __name__) config.set_env() print(config['foo']['bar'].get()) Both the prefix and the separator can be customized when using ``set_env``. Note that prefix matching is done to the environment variables *prior* to lowercasing, while the separator is matched *after* lowercasing. .. code-block:: sh export APPFOO_NESTED_BAR=something .. code-block:: python import confuse config = confuse.Configuration('myapp', __name__) config.set_env(prefix='APP', sep='_nested_') print(config['foo']['bar'].get()) For configurations that include lists, use integers starting from 0 as nested keys to invoke "list conversion." If any of the sibling nested keys are not integers or the integers are not sequential starting from 0, then conversion will not be performed. Nested lists and combinations of nested dicts and lists are supported. .. code-block:: sh export MYAPP_FOO__0=first export MYAPP_FOO__1=second export MYAPP_FOO__2__BAR__0=nested .. code-block:: python import confuse config = confuse.Configuration('myapp', __name__) config.set_env() print(config['foo'].get()) # ['first', 'second', {'bar': ['nested']}] For consistency with YAML config files, the values of environment variables are type converted using the same YAML parser used for file-based configs. This means that numeric strings will be converted to integers or floats, "true" and "false" will be converted to booleans, and the empty string or "null" will be converted to ``None``. Setting an environment variable to the empty string or "null" allows unsetting a config value from a lower-precedence source. To change the lowercasing and list handling behaviors when loading environment variables or to enable full YAML parsing of environment variables, you can initialize an ``EnvSource`` configuration source directly. If you use config overlays from both command-line args and environment variables, the order of calls to ``set_args`` and ``set_env`` will determine the precedence, with the last call having the highest precedence. Search Paths ------------ Confuse looks in a number of locations for your application's configurations. The locations are determined by the platform. For each platform, Confuse has a list of directories in which it looks for a directory named after the application. For example, the first search location on Unix-y systems is ``$XDG_CONFIG_HOME/AppName`` for an application called ``AppName``. Here are the default search paths for each platform: * macOS: ``~/.config/app`` and ``~/Library/Application Support/app`` * Other Unix: ``~/.config/app`` and ``/etc/app`` * Windows: ``%APPDATA%\app`` where the `APPDATA` environment variable falls back to ``%HOME%\AppData\Roaming`` if undefined Both macOS and other Unix operating sytems also try to use the ``XDG_CONFIG_HOME`` and ``XDG_CONFIG_DIRS`` environment variables if set then search those directories as well. Users can also add an override configuration directory with an environment variable. The variable name is the application name in capitals with "DIR" appended: for an application named ``AppName``, the environment variable is ``APPNAMEDIR``. Manually Specifying Config Files -------------------------------- You may want to leverage Confuse's features without :ref:`Search Paths`. This can be done by manually specifying the YAML files you want to include, which also allows changing how relative paths in the file will be resolved: .. code-block:: python import confuse # Instantiates config. Confuse searches for a config_default.yaml config = confuse.Configuration('MyGreatApp', __name__) # Add config items from specified file. Relative path values within the # file are resolved relative to the application's configuration directory. config.set_file('subdirectory/default_config.yaml') # Add config items from a second file. If some items were already defined, # they will be overwritten (new file precedes the previous ones). With # `base_for_paths` set to True, relative path values in this file will be # resolved relative to the config file's directory (i.e., 'subdirectory'). config.set_file('subdirectory/local_config.yaml', base_for_paths=True) val = config['foo']['bar'].get(int) Your Application Directory -------------------------- Confuse provides a simple helper, ``Configuration.config_dir()``, that gives you a directory used to store your application's configuration. If a configuration file exists in any of the searched locations, then the highest-priority directory containing a config file is used. Otherwise, a directory is created for you and returned. So you can always expect this method to give you a directory that actually exists. As an example, you may want to migrate a user's settings to Confuse from an older configuration system such as `ConfigParser`_. Just do something like this: .. code-block:: python config_filename = os.path.join(config.config_dir(), confuse.CONFIG_FILENAME) with open(config_filename, 'w') as f: yaml.dump(migrated_config, f) .. _ConfigParser: http://docs.python.org/library/configparser.html Dynamic Updates --------------- Occasionally, a program will need to modify its configuration while it's running. For example, an interactive prompt from the user might cause the program to change a setting for the current execution only. Or the program might need to add a *derived* configuration value that the user doesn't specify. To facilitate this, Confuse lets you *assign* to view objects using ordinary Python assignment. Assignment will add an overlay source that precedes all other configuration sources in priority. Here's an example of programmatically setting a configuration value based on a ``DEBUG`` constant: .. code-block:: python if DEBUG: config['verbosity'] = 100 ... my_logger.setLevel(config['verbosity'].get(int)) This example allows the constant to override the default verbosity level, which would otherwise come from a configuration file. Assignment works by creating a new "source" for configuration data at the top of the stack. This new source takes priority over all other, previously-loaded sources. You can cause this explicitly by calling the ``set()`` method on any view. A related method, ``add()``, works similarly but instead adds a new *lowest-priority* source to the bottom of the stack. This can be used to provide defaults for options that may be overridden by previously-loaded configuration files. YAML Tweaks ----------- Confuse uses the `PyYAML`_ module to parse YAML configuration files. However, it deviates very slightly from the official YAML specification to provide a few niceties suited to human-written configuration files. Those tweaks are: .. _pyyaml: http://pyyaml.org/ - All strings are returned as Python Unicode objects. - YAML maps are parsed as Python `OrderedDict`_ objects. This means that you can recover the order that the user wrote down a dictionary. - Bare strings can begin with the % character. In stock PyYAML, this will throw a parse error. .. _OrderedDict: http://docs.python.org/2/library/collections.html#collections.OrderedDict To produce a YAML string reflecting a configuration, just call ``config.dump()``. This does not cleanly round-trip YAML, but it does play some tricks to preserve comments and spacing in the original file. Custom YAML Loaders ''''''''''''''''''' You can also specify your own `PyYAML`_ `Loader` object to parse YAML files. Supply the `loader` parameter to a `Configuration` constructor, like this: .. code-block:: python config = confuse.Configuration("name", loader=yaml.Loaded) To imbue a loader with Confuse's special parser overrides, use its `add_constructors` method: .. code-block:: python class MyLoader(yaml.Loader): ... confuse.Loader.add_constructors(MyLoader) config = confuse.Configuration("name", loader=MyLoader) Configuring Large Programs -------------------------- One problem that must be solved by a configuration system is the issue of global configuration for complex applications. In a large program with many components and many config options, it can be unwieldy to explicitly pass configuration values from component to component. You quickly end up with monstrous function signatures with dozens of keyword arguments, decreasing code legibility and testability. In such systems, one option is to pass a single `Configuration` object through to each component. To avoid even this, however, it's sometimes appropriate to use a little bit of shared global state. As evil as shared global state usually is, configuration is (in my opinion) one valid use: since configuration is mostly read-only, it's relatively unlikely to cause the sorts of problems that global values sometimes can. And having a global repository for configuration option can vastly reduce the amount of boilerplate threading-through needed to explicitly pass configuration from call to call. To use global configuration, consider creating a configuration object in a well-known module (say, the root of a package). But since this object will be initialized at module load time, Confuse provides a `LazyConfig` object that loads your configuration files on demand instead of when the object is constructed. (Doing complicated stuff like parsing YAML at module load time is generally considered a Bad Idea.) Global state can cause problems for unit testing. To alleviate this, consider adding code to your test fixtures (e.g., `setUp`_ in the `unittest`_ module) that clears out the global configuration before each test is run. Something like this: .. code-block:: python config.clear() config.read(user=False) These lines will empty out the current configuration and then re-load the defaults (but not the user's configuration files). Your tests can then modify the global configuration values without affecting other tests since these modifications will be cleared out before the next test runs. .. _unittest: http://docs.python.org/2/library/unittest.html .. _setUp: http://docs.python.org/2/library/unittest.html#unittest.TestCase.setUp Redaction --------- You can also mark certain configuration values as "sensitive" and avoid including them in output. Just set the `redact` flag: .. code-block:: python config['key'].redact = True Then flatten or dump the configuration like so: .. code-block:: python config.dump(redact=True) The resulting YAML will contain "key: REDACTED" instead of the original data. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1466461602.0 confuse-1.7.0/example.py0000755000000000000000000000006500000000000012061 0ustar00#!/usr/bin/env python import example example.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1555796024.0 confuse-1.7.0/example/__init__.py0000644000000000000000000000344100000000000013616 0ustar00"""An example application using Confuse for configuration.""" from __future__ import division, absolute_import, print_function import confuse import argparse template = { 'library': confuse.Filename(), 'import_write': confuse.OneOf([bool, 'ask', 'skip']), 'ignore': confuse.StrSeq(), 'plugins': list, 'paths': { 'directory': confuse.Filename(), 'default': confuse.Filename(relative_to='directory'), }, 'servers': confuse.Sequence( { 'hostname': str, 'options': confuse.StrSeq(), } ) } config = confuse.LazyConfig('ConfuseExample', __name__) def main(): parser = argparse.ArgumentParser(description='example Confuse program') parser.add_argument('--library', '-l', dest='library', metavar='LIBPATH', help='library database file') parser.add_argument('--directory', '-d', dest='paths.directory', metavar='DIRECTORY', help='destination music directory') parser.add_argument('--verbose', '-v', dest='verbose', action='store_true', help='print debugging messages') args = parser.parse_args() config.set_args(args, dots=True) print('configuration directory is', config.config_dir()) # Use a boolean flag and the transient overlay. if config['verbose']: print('verbose mode') config['log']['level'] = 2 else: config['log']['level'] = 0 print('logging level is', config['log']['level'].get(int)) valid = config.get(template) # Some validated/converted values. print('library is', valid.library) print('directory is', valid.paths.directory) print('paths.default is', valid.paths.default) print('servers are', [s.hostname for s in valid.servers]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1555796024.0 confuse-1.7.0/example/config_default.yaml0000644000000000000000000000151100000000000015336 0ustar00library: library.db import_write: yes import_copy: yes import_move: no import_resume: ask import_incremental: yes import_quiet_fallback: skip import_timid: no import_log: ignore: [".*", "*~"] replace: '[\\/]': _ '^\.': _ '[\x00-\x1f]': _ '[<>:"\?\*\|]': _ '\.$': _ '\s+$': '' art_filename: cover plugins: [] pluginpath: [] threaded: yes color: yes timeout: 5.0 per_disc_numbering: no verbose: no list_format_item: $artist - $album - $title list_format_album: $albumartist - $album paths: directory: ~/Music default: $albumartist/$album%aunique{}/$track $title singleton: Non-Album/$artist/$title comp: Compilations/$album%aunique{}/$track $title servers: - hostname: test1.example.com options: - foo - hostname: test2.example.com options: - bar - baz ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1624891885.7074401 confuse-1.7.0/pyproject.toml0000644000000000000000000000174600000000000012774 0ustar00[build-system] requires = ["flit_core >=2,<4"] build-backend = "flit_core.buildapi" [tool.flit.metadata] module = "confuse" author = "Adrian Sampson" author-email = "adrian@radbox.org" home-page = "https://github.com/beetbox/confuse" requires = [ "pyyaml" ] description-file = "README.rst" requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" classifiers = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', ] [tool.flit.metadata.requires-extra] test = [ "pathlib; python_version == '2.7'" ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1605982760.9076028 confuse-1.7.0/requirements.txt0000644000000000000000000000000600000000000013330 0ustar00PyYAML././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1540408710.0 confuse-1.7.0/setup.cfg0000644000000000000000000000154000000000000011671 0ustar00[nosetests] verbosity=1 logging-clear-handlers=1 eval-attr="!=slow" [flake8] min-version=2.7 # Default pyflakes errors we ignore: # - E241: missing whitespace after ',' (used to align visually) # - E221: multiple spaces before operator (used to align visually) # - E731: do not assign a lambda expression, use a def # - C901: function/method complexity # `flake8-future-import` errors we ignore: # - FI50: `__future__` import "division" present # - FI51: `__future__` import "absolute_import" present # - FI12: `__future__` import "with_statement" missing # - FI53: `__future__` import "print_function" present # - FI14: `__future__` import "unicode_literals" missing # - FI15: `__future__` import "generator_stop" missing # pycodestyle warnings ignored: # - W503: line breaks before binary operators ignore=C901,E241,E221,E731,FI50,FI51,FI12,FI53,FI14,FI15,W503 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1466461602.0 confuse-1.7.0/test/__init__.py0000644000000000000000000000156000000000000013142 0ustar00from __future__ import division, absolute_import, print_function import confuse import tempfile import shutil import os def _root(*sources): return confuse.RootView([confuse.ConfigSource.of(s) for s in sources]) class TempDir(object): """Context manager that creates and destroys a temporary directory. """ def __init__(self): self.path = tempfile.mkdtemp() def __enter__(self): return self def __exit__(self, *errstuff): shutil.rmtree(self.path) def sub(self, name, contents=None): """Get a path to a file named `name` inside this temporary directory. If `contents` is provided, then the bytestring is written to the file. """ path = os.path.join(self.path, name) if contents: with open(path, 'wb') as f: f.write(contents) return path ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1525538727.0 confuse-1.7.0/test/test_cli.py0000644000000000000000000001407600000000000013217 0ustar00from __future__ import division, absolute_import, print_function import confuse import argparse from argparse import Namespace import optparse import unittest class ArgparseTest(unittest.TestCase): def setUp(self): self.config = confuse.Configuration('test', read=False) self.parser = argparse.ArgumentParser() def _parse(self, args, **kwargs): args = self.parser.parse_args(args.split()) self.config.set_args(args, **kwargs) def test_text_argument_parsed(self): self.parser.add_argument('--foo', metavar='BAR') self._parse('--foo bar') self.assertEqual(self.config['foo'].get(), 'bar') def test_boolean_argument_parsed(self): self.parser.add_argument('--foo', action='store_true') self._parse('--foo') self.assertEqual(self.config['foo'].get(), True) def test_missing_optional_argument_not_included(self): self.parser.add_argument('--foo', metavar='BAR') self._parse('') with self.assertRaises(confuse.NotFoundError): self.config['foo'].get() def test_argument_overrides_default(self): self.config.add({'foo': 'baz'}) self.parser.add_argument('--foo', metavar='BAR') self._parse('--foo bar') self.assertEqual(self.config['foo'].get(), 'bar') def test_nested_destination_single(self): self.parser.add_argument('--one', dest='one.foo') self.parser.add_argument('--two', dest='one.two.foo') self._parse('--two TWO', dots=True) self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') def test_nested_destination_nested(self): self.parser.add_argument('--one', dest='one.foo') self.parser.add_argument('--two', dest='one.two.foo') self._parse('--two TWO --one ONE', dots=True) self.assertEqual(self.config['one']['foo'].get(), 'ONE') self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') def test_nested_destination_nested_rev(self): self.parser.add_argument('--one', dest='one.foo') self.parser.add_argument('--two', dest='one.two.foo') # Reverse to ensure order doesn't matter self._parse('--one ONE --two TWO', dots=True) self.assertEqual(self.config['one']['foo'].get(), 'ONE') self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') def test_nested_destination_clobber(self): self.parser.add_argument('--one', dest='one.two') self.parser.add_argument('--two', dest='one.two.foo') self._parse('--two TWO --one ONE', dots=True) # Clobbered self.assertEqual(self.config['one']['two'].get(), {'foo': 'TWO'}) self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') def test_nested_destination_clobber_rev(self): # Reversed order self.parser.add_argument('--two', dest='one.two.foo') self.parser.add_argument('--one', dest='one.two') self._parse('--one ONE --two TWO', dots=True) # Clobbered just the same self.assertEqual(self.config['one']['two'].get(), {'foo': 'TWO'}) self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') class OptparseTest(unittest.TestCase): def setUp(self): self.config = confuse.Configuration('test', read=False) self.parser = optparse.OptionParser() def _parse(self, args, **kwargs): options, _ = self.parser.parse_args(args.split()) self.config.set_args(options, **kwargs) def test_text_argument_parsed(self): self.parser.add_option('--foo', metavar='BAR') self._parse('--foo bar') self.assertEqual(self.config['foo'].get(), 'bar') def test_boolean_argument_parsed(self): self.parser.add_option('--foo', action='store_true') self._parse('--foo') self.assertEqual(self.config['foo'].get(), True) def test_missing_optional_argument_not_included(self): self.parser.add_option('--foo', metavar='BAR') self._parse('') with self.assertRaises(confuse.NotFoundError): self.config['foo'].get() def test_argument_overrides_default(self): self.config.add({'foo': 'baz'}) self.parser.add_option('--foo', metavar='BAR') self._parse('--foo bar') self.assertEqual(self.config['foo'].get(), 'bar') def test_nested_destination_single(self): self.parser.add_option('--one', dest='one.foo') self.parser.add_option('--two', dest='one.two.foo') self._parse('--two TWO', dots=True) self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') def test_nested_destination_nested(self): self.parser.add_option('--one', dest='one.foo') self.parser.add_option('--two', dest='one.two.foo') self._parse('--two TWO --one ONE', dots=True) self.assertEqual(self.config['one']['foo'].get(), 'ONE') self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') def test_nested_destination_nested_rev(self): self.parser.add_option('--one', dest='one.foo') self.parser.add_option('--two', dest='one.two.foo') # Reverse to ensure order doesn't matter self._parse('--one ONE --two TWO', dots=True) self.assertEqual(self.config['one']['foo'].get(), 'ONE') self.assertEqual(self.config['one']['two']['foo'].get(), 'TWO') class GenericNamespaceTest(unittest.TestCase): def setUp(self): self.config = confuse.Configuration('test', read=False) def test_value_added_to_root(self): self.config.set_args(Namespace(foo='bar')) self.assertEqual(self.config['foo'].get(), 'bar') def test_value_added_to_subview(self): self.config['baz'].set_args(Namespace(foo='bar')) self.assertEqual(self.config['baz']['foo'].get(), 'bar') def test_nested_namespace(self): args = Namespace( first="Hello", nested=Namespace( second="World" ) ) self.config.set_args(args, dots=True) self.assertEqual(self.config['first'].get(), 'Hello') self.assertEqual(self.config['nested']['second'].get(), 'World') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1466461602.0 confuse-1.7.0/test/test_dump.py0000644000000000000000000000650000000000000013406 0ustar00from __future__ import division, absolute_import, print_function import confuse import textwrap import unittest from . import _root class PrettyDumpTest(unittest.TestCase): def test_dump_null(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': None}) yaml = config.dump().strip() self.assertEqual(yaml, 'foo:') def test_dump_true(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': True}) yaml = config.dump().strip() self.assertEqual(yaml, 'foo: yes') def test_dump_false(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': False}) yaml = config.dump().strip() self.assertEqual(yaml, 'foo: no') def test_dump_short_list(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': ['bar', 'baz']}) yaml = config.dump().strip() self.assertEqual(yaml, 'foo: [bar, baz]') def test_dump_ordered_dict(self): odict = confuse.OrderedDict() odict['foo'] = 'bar' odict['bar'] = 'baz' odict['baz'] = 'qux' config = confuse.Configuration('myapp', read=False) config.add({'key': odict}) yaml = config.dump().strip() self.assertEqual(yaml, textwrap.dedent(""" key: foo: bar bar: baz baz: qux """).strip()) def test_dump_sans_defaults(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': 'bar'}) config.sources[0].default = True config.add({'baz': 'qux'}) yaml = config.dump().strip() self.assertEqual(yaml, "foo: bar\nbaz: qux") yaml = config.dump(full=False).strip() self.assertEqual(yaml, "baz: qux") class RedactTest(unittest.TestCase): def test_no_redaction(self): config = _root({'foo': 'bar'}) data = config.flatten(redact=True) self.assertEqual(data, {'foo': 'bar'}) def test_redact_key(self): config = _root({'foo': 'bar'}) config['foo'].redact = True data = config.flatten(redact=True) self.assertEqual(data, {'foo': 'REDACTED'}) def test_unredact(self): config = _root({'foo': 'bar'}) config['foo'].redact = True config['foo'].redact = False data = config.flatten(redact=True) self.assertEqual(data, {'foo': 'bar'}) def test_dump_redacted(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': 'bar'}) config['foo'].redact = True yaml = config.dump(redact=True).strip() self.assertEqual(yaml, 'foo: REDACTED') def test_dump_unredacted(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': 'bar'}) config['foo'].redact = True yaml = config.dump(redact=False).strip() self.assertEqual(yaml, 'foo: bar') def test_dump_redacted_sans_defaults(self): config = confuse.Configuration('myapp', read=False) config.add({'foo': 'bar'}) config.sources[0].default = True config.add({'baz': 'qux'}) config['baz'].redact = True yaml = config.dump(redact=True, full=False).strip() self.assertEqual(yaml, "baz: REDACTED") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3492064 confuse-1.7.0/test/test_env.py0000644000000000000000000002503100000000000013231 0ustar00from __future__ import division, absolute_import, print_function import confuse import os import unittest from . import _root ENVIRON = os.environ class EnvSourceTest(unittest.TestCase): def setUp(self): os.environ = {} def tearDown(self): os.environ = ENVIRON def test_prefix(self): os.environ['TEST_FOO'] = 'a' os.environ['BAR'] = 'b' config = _root(confuse.EnvSource('TEST_')) self.assertEqual(config.get(), {'foo': 'a'}) def test_number_type_conversion(self): os.environ['TEST_FOO'] = '1' os.environ['TEST_BAR'] = '2.0' config = _root(confuse.EnvSource('TEST_')) foo = config['foo'].get() bar = config['bar'].get() self.assertIsInstance(foo, int) self.assertEqual(foo, 1) self.assertIsInstance(bar, float) self.assertEqual(bar, 2.0) def test_bool_type_conversion(self): os.environ['TEST_FOO'] = 'true' os.environ['TEST_BAR'] = 'FALSE' config = _root(confuse.EnvSource('TEST_')) self.assertIs(config['foo'].get(), True) self.assertIs(config['bar'].get(), False) def test_null_type_conversion(self): os.environ['TEST_FOO'] = 'null' os.environ['TEST_BAR'] = '' config = _root(confuse.EnvSource('TEST_')) self.assertIs(config['foo'].get(), None) self.assertIs(config['bar'].get(), None) def test_unset_lower_config(self): os.environ['TEST_FOO'] = 'null' config = _root({'foo': 'bar'}) self.assertEqual(config['foo'].get(), 'bar') config.set(confuse.EnvSource('TEST_')) self.assertIs(config['foo'].get(), None) def test_sep_default(self): os.environ['TEST_FOO__BAR'] = 'a' os.environ['TEST_FOO_BAZ'] = 'b' config = _root(confuse.EnvSource('TEST_')) self.assertEqual(config['foo']['bar'].get(), 'a') self.assertEqual(config['foo_baz'].get(), 'b') def test_sep_single_underscore_adjacent_seperators(self): os.environ['TEST_FOO__BAR'] = 'a' os.environ['TEST_FOO_BAZ'] = 'b' config = _root(confuse.EnvSource('TEST_', sep='_')) self.assertEqual(config['foo']['']['bar'].get(), 'a') self.assertEqual(config['foo']['baz'].get(), 'b') def test_nested(self): os.environ['TEST_FOO__BAR'] = 'a' os.environ['TEST_FOO__BAZ__QUX'] = 'b' config = _root(confuse.EnvSource('TEST_')) self.assertEqual(config['foo']['bar'].get(), 'a') self.assertEqual(config['foo']['baz']['qux'].get(), 'b') def test_nested_rev(self): # Reverse to ensure order doesn't matter os.environ['TEST_FOO__BAZ__QUX'] = 'b' os.environ['TEST_FOO__BAR'] = 'a' config = _root(confuse.EnvSource('TEST_')) self.assertEqual(config['foo']['bar'].get(), 'a') self.assertEqual(config['foo']['baz']['qux'].get(), 'b') def test_nested_clobber(self): os.environ['TEST_FOO__BAR'] = 'a' os.environ['TEST_FOO__BAR__BAZ'] = 'b' config = _root(confuse.EnvSource('TEST_')) # Clobbered self.assertEqual(config['foo']['bar'].get(), {'baz': 'b'}) self.assertEqual(config['foo']['bar']['baz'].get(), 'b') def test_nested_clobber_rev(self): # Reverse to ensure order doesn't matter os.environ['TEST_FOO__BAR__BAZ'] = 'b' os.environ['TEST_FOO__BAR'] = 'a' config = _root(confuse.EnvSource('TEST_')) # Clobbered self.assertEqual(config['foo']['bar'].get(), {'baz': 'b'}) self.assertEqual(config['foo']['bar']['baz'].get(), 'b') def test_lower_applied_after_prefix_match(self): os.environ['TEST_FOO'] = 'a' config = _root(confuse.EnvSource('test_', lower=True)) self.assertEqual(config.get(), {}) def test_lower_already_lowercase(self): os.environ['TEST_foo'] = 'a' config = _root(confuse.EnvSource('TEST_', lower=True)) self.assertEqual(config.get(), {'foo': 'a'}) def test_lower_does_not_alter_value(self): os.environ['TEST_FOO'] = 'UPPER' config = _root(confuse.EnvSource('TEST_', lower=True)) self.assertEqual(config.get(), {'foo': 'UPPER'}) def test_lower_false(self): os.environ['TEST_FOO'] = 'a' config = _root(confuse.EnvSource('TEST_', lower=False)) self.assertEqual(config.get(), {'FOO': 'a'}) def test_handle_lists_good_list(self): os.environ['TEST_FOO__0'] = 'a' os.environ['TEST_FOO__1'] = 'b' os.environ['TEST_FOO__2'] = 'c' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), ['a', 'b', 'c']) def test_handle_lists_good_list_rev(self): # Reverse to ensure order doesn't matter os.environ['TEST_FOO__2'] = 'c' os.environ['TEST_FOO__1'] = 'b' os.environ['TEST_FOO__0'] = 'a' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), ['a', 'b', 'c']) def test_handle_lists_nested_lists(self): os.environ['TEST_FOO__0__0'] = 'a' os.environ['TEST_FOO__0__1'] = 'b' os.environ['TEST_FOO__1__0'] = 'c' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), [['a', 'b'], ['c']]) def test_handle_lists_bad_list_missing_index(self): os.environ['TEST_FOO__0'] = 'a' os.environ['TEST_FOO__2'] = 'b' os.environ['TEST_FOO__3'] = 'c' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), {'0': 'a', '2': 'b', '3': 'c'}) def test_handle_lists_bad_list_non_zero_start(self): os.environ['TEST_FOO__1'] = 'a' os.environ['TEST_FOO__2'] = 'b' os.environ['TEST_FOO__3'] = 'c' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), {'1': 'a', '2': 'b', '3': 'c'}) def test_handle_lists_bad_list_non_numeric(self): os.environ['TEST_FOO__0'] = 'a' os.environ['TEST_FOO__ONE'] = 'b' os.environ['TEST_FOO__2'] = 'c' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), {'0': 'a', 'one': 'b', '2': 'c'}) def test_handle_lists_top_level_always_dict(self): os.environ['TEST_0'] = 'a' os.environ['TEST_1'] = 'b' os.environ['TEST_2'] = 'c' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config.get(), {'0': 'a', '1': 'b', '2': 'c'}) def test_handle_lists_not_a_list(self): os.environ['TEST_FOO__BAR'] = 'a' os.environ['TEST_FOO__BAZ'] = 'b' config = _root(confuse.EnvSource('TEST_', handle_lists=True)) self.assertEqual(config['foo'].get(), {'bar': 'a', 'baz': 'b'}) def test_parse_yaml_docs_scalar(self): os.environ['TEST_FOO'] = 'a' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) self.assertEqual(config['foo'].get(), 'a') def test_parse_yaml_docs_list(self): os.environ['TEST_FOO'] = '[a, b]' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) self.assertEqual(config['foo'].get(), ['a', 'b']) def test_parse_yaml_docs_dict(self): os.environ['TEST_FOO'] = '{bar: a, baz: b}' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) self.assertEqual(config['foo'].get(), {'bar': 'a', 'baz': 'b'}) def test_parse_yaml_docs_nested(self): os.environ['TEST_FOO'] = '{bar: [a, b], baz: {qux: c}}' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) self.assertEqual(config['foo']['bar'].get(), ['a', 'b']) self.assertEqual(config['foo']['baz'].get(), {'qux': 'c'}) def test_parse_yaml_docs_number_conversion(self): os.environ['TEST_FOO'] = '{bar: 1, baz: 2.0}' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) bar = config['foo']['bar'].get() baz = config['foo']['baz'].get() self.assertIsInstance(bar, int) self.assertEqual(bar, 1) self.assertIsInstance(baz, float) self.assertEqual(baz, 2.0) def test_parse_yaml_docs_bool_conversion(self): os.environ['TEST_FOO'] = '{bar: true, baz: FALSE}' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) self.assertIs(config['foo']['bar'].get(), True) self.assertIs(config['foo']['baz'].get(), False) def test_parse_yaml_docs_null_conversion(self): os.environ['TEST_FOO'] = '{bar: null, baz: }' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) self.assertIs(config['foo']['bar'].get(), None) self.assertIs(config['foo']['baz'].get(), None) def test_parse_yaml_docs_syntax_error(self): os.environ['TEST_FOO'] = '{:}' try: _root(confuse.EnvSource('TEST_', parse_yaml_docs=True)) except confuse.ConfigError as exc: self.assertTrue('TEST_FOO' in exc.name) else: self.fail('ConfigError not raised') def test_parse_yaml_docs_false(self): os.environ['TEST_FOO'] = '{bar: a, baz: b}' config = _root(confuse.EnvSource('TEST_', parse_yaml_docs=False)) self.assertEqual(config['foo'].get(), '{bar: a, baz: b}') with self.assertRaises(confuse.ConfigError): config['foo']['bar'].get() class ConfigEnvTest(unittest.TestCase): def setUp(self): self.config = confuse.Configuration('TestApp', read=False) os.environ = { 'TESTAPP_FOO': 'a', 'TESTAPP_BAR__NESTED': 'b', 'TESTAPP_BAZ_SEP_NESTED': 'c', 'MYAPP_QUX_SEP_NESTED': 'd' } def tearDown(self): os.environ = ENVIRON def test_defaults(self): self.config.set_env() self.assertEqual(self.config.get(), {'foo': 'a', 'bar': {'nested': 'b'}, 'baz_sep_nested': 'c'}) def test_with_prefix(self): self.config.set_env(prefix='MYAPP_') self.assertEqual(self.config.get(), {'qux_sep_nested': 'd'}) def test_with_sep(self): self.config.set_env(sep='_sep_') self.assertEqual(self.config.get(), {'foo': 'a', 'bar__nested': 'b', 'baz': {'nested': 'c'}}) def test_with_prefix_and_sep(self): self.config.set_env(prefix='MYAPP_', sep='_sep_') self.assertEqual(self.config.get(), {'qux': {'nested': 'd'}}) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1593264429.546534 confuse-1.7.0/test/test_paths.py0000644000000000000000000001603400000000000013563 0ustar00from __future__ import division, absolute_import, print_function import confuse import confuse.yaml_util import ntpath import os import platform import posixpath import shutil import tempfile import unittest DEFAULT = [platform.system, os.environ, os.path] SYSTEMS = { 'Linux': [{'HOME': '/home/test', 'XDG_CONFIG_HOME': '~/xdgconfig'}, posixpath], 'Darwin': [{'HOME': '/Users/test'}, posixpath], 'Windows': [{ 'APPDATA': '~\\winconfig', 'HOME': 'C:\\Users\\test', 'USERPROFILE': 'C:\\Users\\test', }, ntpath] } def _touch(path): open(path, 'a').close() class FakeSystem(unittest.TestCase): SYS_NAME = None TMP_HOME = False def setUp(self): if self.SYS_NAME in SYSTEMS: self.os_path = os.path os.environ = {} environ, os.path = SYSTEMS[self.SYS_NAME] os.environ.update(environ) # copy platform.system = lambda: self.SYS_NAME if self.TMP_HOME: self.home = tempfile.mkdtemp() os.environ['HOME'] = self.home os.environ['USERPROFILE'] = self.home def tearDown(self): platform.system, os.environ, os.path = DEFAULT if hasattr(self, 'home'): shutil.rmtree(self.home) class LinuxTestCases(FakeSystem): SYS_NAME = 'Linux' def test_both_xdg_and_fallback_dirs(self): self.assertEqual(confuse.config_dirs(), ['/home/test/.config', '/home/test/xdgconfig', '/etc/xdg', '/etc']) def test_fallback_only(self): del os.environ['XDG_CONFIG_HOME'] self.assertEqual(confuse.config_dirs(), ['/home/test/.config', '/etc/xdg', '/etc']) def test_xdg_matching_fallback_not_duplicated(self): os.environ['XDG_CONFIG_HOME'] = '~/.config' self.assertEqual(confuse.config_dirs(), ['/home/test/.config', '/etc/xdg', '/etc']) def test_xdg_config_dirs(self): os.environ['XDG_CONFIG_DIRS'] = '/usr/local/etc/xdg:/etc/xdg' self.assertEqual(confuse.config_dirs(), ['/home/test/.config', '/home/test/xdgconfig', '/usr/local/etc/xdg', '/etc/xdg', '/etc']) class OSXTestCases(FakeSystem): SYS_NAME = 'Darwin' def test_mac_dirs(self): self.assertEqual(confuse.config_dirs(), ['/Users/test/.config', '/Users/test/Library/Application Support', '/etc/xdg', '/etc']) def test_xdg_config_dirs(self): os.environ['XDG_CONFIG_DIRS'] = '/usr/local/etc/xdg:/etc/xdg' self.assertEqual(confuse.config_dirs(), ['/Users/test/.config', '/Users/test/Library/Application Support', '/usr/local/etc/xdg', '/etc/xdg', '/etc']) class WindowsTestCases(FakeSystem): SYS_NAME = 'Windows' def test_dir_from_environ(self): self.assertEqual(confuse.config_dirs(), ['C:\\Users\\test\\AppData\\Roaming', 'C:\\Users\\test\\winconfig']) def test_fallback_dir(self): del os.environ['APPDATA'] self.assertEqual(confuse.config_dirs(), ['C:\\Users\\test\\AppData\\Roaming']) class ConfigFilenamesTest(unittest.TestCase): def setUp(self): self._old = os.path.isfile, confuse.yaml_util.load_yaml os.path.isfile = lambda x: True confuse.yaml_util.load_yaml = lambda *args, **kwargs: {} def tearDown(self): confuse.yaml_util.load_yaml, os.path.isfile = self._old def test_no_sources_when_files_missing(self): config = confuse.Configuration('myapp', read=False) filenames = [s.filename for s in config.sources] self.assertEqual(filenames, []) def test_search_package(self): config = confuse.Configuration('myapp', __name__, read=False) config._add_default_source() for source in config.sources: if source.default: default_source = source break else: self.fail("no default source") self.assertEqual( default_source.filename, os.path.join(os.path.dirname(__file__), 'config_default.yaml') ) self.assertTrue(source.default) class EnvVarTest(FakeSystem): TMP_HOME = True 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 def test_env_var_name(self): self.assertEqual(self.config._env_var, 'MYAPPDIR') def test_env_var_dir_has_first_priority(self): self.assertEqual(self.config.config_dir(), self.home) def test_env_var_missing(self): del os.environ['MYAPPDIR'] self.assertNotEqual(self.config.config_dir(), self.home) class PrimaryConfigDirTest(FakeSystem): SYS_NAME = 'Linux' # conversion from posix to nt is easy TMP_HOME = True if platform.system() == 'Windows': # wrap these functions as they need to work on the host system which is # only needed on Windows as we are using `posixpath` def join(self, *args): return self.os_path.normpath(self.os_path.join(*args)) def makedirs(self, path, *args): os.path, os_path = self.os_path, os.path self._makedirs(path, *args) os.path = os_path def setUp(self): super(PrimaryConfigDirTest, self).setUp() if hasattr(self, 'join'): os.path.join = self.join os.makedirs, self._makedirs = self.makedirs, os.makedirs self.config = confuse.Configuration('test', read=False) def tearDown(self): super(PrimaryConfigDirTest, self).tearDown() if hasattr(self, '_makedirs'): os.makedirs = self._makedirs def test_create_dir_if_none_exists(self): path = os.path.join(self.home, '.config', 'test') assert not os.path.exists(path) self.assertEqual(self.config.config_dir(), path) self.assertTrue(os.path.isdir(path)) def test_return_existing_dir(self): path = os.path.join(self.home, 'xdgconfig', 'test') os.makedirs(path) _touch(os.path.join(path, confuse.CONFIG_FILENAME)) self.assertEqual(self.config.config_dir(), path) def test_do_not_create_dir_if_lower_priority_exists(self): path1 = os.path.join(self.home, 'xdgconfig', 'test') path2 = os.path.join(self.home, '.config', 'test') os.makedirs(path2) _touch(os.path.join(path2, confuse.CONFIG_FILENAME)) assert not os.path.exists(path1) assert os.path.exists(path2) self.assertEqual(self.config.config_dir(), path2) self.assertFalse(os.path.isdir(path1)) self.assertTrue(os.path.isdir(path2)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3494594 confuse-1.7.0/test/test_utils.py0000644000000000000000000000555300000000000013610 0ustar00from __future__ import division, absolute_import, print_function from argparse import Namespace from collections import OrderedDict import confuse import unittest class BuildDictTests(unittest.TestCase): def test_pure_dicts(self): config = {'foo': {'bar': 1}} result = confuse.util.build_dict(config) self.assertEqual(1, result['foo']['bar']) def test_namespaces(self): config = Namespace(foo=Namespace(bar=2), another=1) result = confuse.util.build_dict(config) self.assertEqual(2, result['foo']['bar']) self.assertEqual(1, result['another']) def test_dot_sep_keys(self): config = {'foo.bar': 1} result = confuse.util.build_dict(config.copy()) self.assertEqual(1, result['foo.bar']) result = confuse.util.build_dict(config.copy(), sep='.') self.assertEqual(1, result['foo']['bar']) def test_dot_sep_keys_clobber(self): args = [('foo.bar', 1), ('foo.bar.zar', 2)] config = OrderedDict(args) result = confuse.util.build_dict(config.copy(), sep='.') self.assertEqual({'zar': 2}, result['foo']['bar']) self.assertEqual(2, result['foo']['bar']['zar']) # Reverse and do it again! (should be stable) args.reverse() config = OrderedDict(args) result = confuse.util.build_dict(config.copy(), sep='.') self.assertEqual({'zar': 2}, result['foo']['bar']) self.assertEqual(2, result['foo']['bar']['zar']) def test_dot_sep_keys_no_clobber(self): args = [('foo.bar', 1), ('foo.far', 2), ('foo.zar.dar', 4)] config = OrderedDict(args) result = confuse.util.build_dict(config.copy(), sep='.') self.assertEqual(1, result['foo']['bar']) self.assertEqual(2, result['foo']['far']) self.assertEqual(4, result['foo']['zar']['dar']) def test_adjacent_underscores_sep_keys(self): config = {'foo__bar_baz': 1} result = confuse.util.build_dict(config.copy()) self.assertEqual(1, result['foo__bar_baz']) result = confuse.util.build_dict(config.copy(), sep='_') self.assertEqual(1, result['foo']['']['bar']['baz']) result = confuse.util.build_dict(config.copy(), sep='__') self.assertEqual(1, result['foo']['bar_baz']) def test_keep_none(self): config = {'foo': None} result = confuse.util.build_dict(config.copy()) with self.assertRaises(KeyError): result['foo'] result = confuse.util.build_dict(config.copy(), keep_none=True) self.assertIs(None, result['foo']) def test_keep_none_with_nested(self): config = {'foo': {'bar': None}} result = confuse.util.build_dict(config.copy()) self.assertEqual({}, result['foo']) result = confuse.util.build_dict(config.copy(), keep_none=True) self.assertIs(None, result['foo']['bar']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1624888656.9367201 confuse-1.7.0/test/test_valid.py0000644000000000000000000006566400000000000013560 0ustar00from __future__ import division, absolute_import, print_function try: import enum SUPPORTS_ENUM = True except ImportError: SUPPORTS_ENUM = False try: from collections.abc import Mapping, Sequence except ImportError: from collections import Mapping, Sequence import confuse import os import unittest from . import _root class ValidConfigTest(unittest.TestCase): def test_validate_simple_dict(self): config = _root({'foo': 5}) valid = config.get({'foo': confuse.Integer()}) self.assertEqual(valid['foo'], 5) def test_default_value(self): config = _root({}) valid = config.get({'foo': confuse.Integer(8)}) self.assertEqual(valid['foo'], 8) def test_undeclared_key_raises_keyerror(self): config = _root({'foo': 5}) valid = config.get({'foo': confuse.Integer()}) with self.assertRaises(KeyError): valid['bar'] def test_undeclared_key_ignored_from_input(self): config = _root({'foo': 5, 'bar': 6}) valid = config.get({'foo': confuse.Integer()}) with self.assertRaises(KeyError): valid['bar'] def test_int_template_shortcut(self): config = _root({'foo': 5}) valid = config.get({'foo': int}) self.assertEqual(valid['foo'], 5) def test_int_default_shortcut(self): config = _root({}) valid = config.get({'foo': 9}) self.assertEqual(valid['foo'], 9) def test_attribute_access(self): config = _root({'foo': 5}) valid = config.get({'foo': confuse.Integer()}) self.assertEqual(valid.foo, 5) def test_missing_required_value_raises_error_on_validate(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): config.get({'foo': confuse.Integer()}) def test_none_as_default(self): config = _root({}) valid = config.get({'foo': confuse.Integer(None)}) self.assertIsNone(valid['foo']) def test_wrong_type_raises_error_on_validate(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.ConfigTypeError): config.get({'foo': confuse.Integer()}) def test_validate_individual_value(self): config = _root({'foo': 5}) valid = config['foo'].get(confuse.Integer()) self.assertEqual(valid, 5) def test_nested_dict_template(self): config = _root({ 'foo': {'bar': 9}, }) valid = config.get({ 'foo': {'bar': confuse.Integer()}, }) self.assertEqual(valid['foo']['bar'], 9) def test_nested_attribute_access(self): config = _root({ 'foo': {'bar': 8}, }) valid = config.get({ 'foo': {'bar': confuse.Integer()}, }) self.assertEqual(valid.foo.bar, 8) class AsTemplateTest(unittest.TestCase): def test_plain_int_as_template(self): typ = confuse.as_template(int) self.assertIsInstance(typ, confuse.Integer) self.assertEqual(typ.default, confuse.REQUIRED) def test_concrete_int_as_template(self): typ = confuse.as_template(2) self.assertIsInstance(typ, confuse.Integer) self.assertEqual(typ.default, 2) def test_plain_string_as_template(self): typ = confuse.as_template(str) self.assertIsInstance(typ, confuse.String) self.assertEqual(typ.default, confuse.REQUIRED) def test_concrete_string_as_template(self): typ = confuse.as_template('foo') self.assertIsInstance(typ, confuse.String) self.assertEqual(typ.default, 'foo') @unittest.skipIf(confuse.PY3, "unicode only present in Python 2") def test_unicode_type_as_template(self): typ = confuse.as_template(unicode) # noqa ignore=F821 self.assertIsInstance(typ, confuse.String) self.assertEqual(typ.default, confuse.REQUIRED) @unittest.skipIf(confuse.PY3, "basestring only present in Python 2") def test_basestring_as_template(self): typ = confuse.as_template(basestring) # noqa ignore=F821 self.assertIsInstance(typ, confuse.String) self.assertEqual(typ.default, confuse.REQUIRED) def test_dict_as_template(self): typ = confuse.as_template({'key': 9}) self.assertIsInstance(typ, confuse.MappingTemplate) self.assertIsInstance(typ.subtemplates['key'], confuse.Integer) self.assertEqual(typ.subtemplates['key'].default, 9) def test_nested_dict_as_template(self): typ = confuse.as_template({'outer': {'inner': 2}}) self.assertIsInstance(typ, confuse.MappingTemplate) self.assertIsInstance(typ.subtemplates['outer'], confuse.MappingTemplate) self.assertIsInstance(typ.subtemplates['outer'].subtemplates['inner'], confuse.Integer) self.assertEqual(typ.subtemplates['outer'].subtemplates['inner'] .default, 2) def test_list_as_template(self): typ = confuse.as_template(list()) self.assertIsInstance(typ, confuse.OneOf) self.assertEqual(typ.default, confuse.REQUIRED) def test_set_as_template(self): typ = confuse.as_template(set()) self.assertIsInstance(typ, confuse.Choice) @unittest.skipUnless(SUPPORTS_ENUM, "enum not supported in this version of Python.") def test_enum_type_as_template(self): typ = confuse.as_template(enum.Enum) self.assertIsInstance(typ, confuse.Choice) def test_float_type_as_tempalte(self): typ = confuse.as_template(float) self.assertIsInstance(typ, confuse.Number) self.assertEqual(typ.default, confuse.REQUIRED) def test_concrete_float_as_template(self): typ = confuse.as_template(2.) self.assertIsInstance(typ, confuse.Number) self.assertEqual(typ.default, 2.) def test_none_as_template(self): typ = confuse.as_template(None) self.assertIs(type(typ), confuse.Template) self.assertEqual(typ.default, None) def test_required_as_template(self): typ = confuse.as_template(confuse.REQUIRED) self.assertIs(type(typ), confuse.Template) self.assertEqual(typ.default, confuse.REQUIRED) def test_dict_type_as_template(self): typ = confuse.as_template(dict) self.assertIsInstance(typ, confuse.TypeTemplate) self.assertEqual(typ.typ, Mapping) self.assertEqual(typ.default, confuse.REQUIRED) def test_list_type_as_template(self): typ = confuse.as_template(list) self.assertIsInstance(typ, confuse.TypeTemplate) self.assertEqual(typ.typ, Sequence) self.assertEqual(typ.default, confuse.REQUIRED) def test_set_type_as_template(self): typ = confuse.as_template(set) self.assertIsInstance(typ, confuse.TypeTemplate) self.assertEqual(typ.typ, set) self.assertEqual(typ.default, confuse.REQUIRED) def test_other_type_as_template(self): class MyClass(object): pass typ = confuse.as_template(MyClass) self.assertIsInstance(typ, confuse.TypeTemplate) self.assertEqual(typ.typ, MyClass) self.assertEqual(typ.default, confuse.REQUIRED) class StringTemplateTest(unittest.TestCase): def test_validate_string(self): config = _root({'foo': 'bar'}) valid = config.get({'foo': confuse.String()}) self.assertEqual(valid['foo'], 'bar') def test_string_default_value(self): config = _root({}) valid = config.get({'foo': confuse.String('baz')}) self.assertEqual(valid['foo'], 'baz') def test_pattern_matching(self): config = _root({'foo': 'bar', 'baz': 'zab'}) valid = config.get({'foo': confuse.String(pattern='^ba.$')}) self.assertEqual(valid['foo'], 'bar') with self.assertRaises(confuse.ConfigValueError): config.get({'baz': confuse.String(pattern='!')}) def test_string_template_shortcut(self): config = _root({'foo': 'bar'}) valid = config.get({'foo': str}) self.assertEqual(valid['foo'], 'bar') def test_string_default_shortcut(self): config = _root({}) valid = config.get({'foo': 'bar'}) self.assertEqual(valid['foo'], 'bar') def test_check_string_type(self): config = _root({'foo': 5}) with self.assertRaises(confuse.ConfigTypeError): config.get({'foo': confuse.String()}) class NumberTest(unittest.TestCase): def test_validate_int_as_number(self): config = _root({'foo': 2}) valid = config['foo'].get(confuse.Number()) self.assertIsInstance(valid, int) self.assertEqual(valid, 2) def test_validate_float_as_number(self): config = _root({'foo': 3.0}) valid = config['foo'].get(confuse.Number()) self.assertIsInstance(valid, float) self.assertEqual(valid, 3.0) def test_validate_string_as_number(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.Number()) class ChoiceTest(unittest.TestCase): def test_validate_good_choice_in_list(self): config = _root({'foo': 2}) valid = config['foo'].get(confuse.Choice([1, 2, 4, 8, 16])) self.assertEqual(valid, 2) def test_validate_bad_choice_in_list(self): config = _root({'foo': 3}) with self.assertRaises(confuse.ConfigValueError): config['foo'].get(confuse.Choice([1, 2, 4, 8, 16])) def test_validate_good_choice_in_dict(self): config = _root({'foo': 2}) valid = config['foo'].get(confuse.Choice({2: 'two', 4: 'four'})) self.assertEqual(valid, 'two') def test_validate_bad_choice_in_dict(self): config = _root({'foo': 3}) with self.assertRaises(confuse.ConfigValueError): config['foo'].get(confuse.Choice({2: 'two', 4: 'four'})) class OneOfTest(unittest.TestCase): def test_default_value(self): config = _root({}) valid = config['foo'].get(confuse.OneOf([], default='bar')) self.assertEqual(valid, 'bar') def test_validate_good_choice_in_list(self): config = _root({'foo': 2}) valid = config['foo'].get(confuse.OneOf([ confuse.String(), confuse.Integer(), ])) self.assertEqual(valid, 2) def test_validate_first_good_choice_in_list(self): config = _root({'foo': 3.14}) valid = config['foo'].get(confuse.OneOf([ confuse.Integer(), confuse.Number(), ])) self.assertEqual(valid, 3) def test_validate_no_choice_in_list(self): config = _root({'foo': None}) with self.assertRaises(confuse.ConfigValueError): config['foo'].get(confuse.OneOf([ confuse.String(), confuse.Integer(), ])) def test_validate_bad_template(self): class BadTemplate(object): pass config = _root({}) with self.assertRaises(confuse.ConfigTemplateError): config.get(confuse.OneOf([BadTemplate()])) del BadTemplate class StrSeqTest(unittest.TestCase): def test_string_list(self): config = _root({'foo': ['bar', 'baz']}) valid = config['foo'].get(confuse.StrSeq()) self.assertEqual(valid, ['bar', 'baz']) def test_string_tuple(self): config = _root({'foo': ('bar', 'baz')}) valid = config['foo'].get(confuse.StrSeq()) self.assertEqual(valid, ['bar', 'baz']) def test_whitespace_separated_string(self): config = _root({'foo': 'bar baz'}) valid = config['foo'].get(confuse.StrSeq()) self.assertEqual(valid, ['bar', 'baz']) def test_invalid_type(self): config = _root({'foo': 9}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.StrSeq()) def test_invalid_sequence_type(self): config = _root({'foo': ['bar', 2126]}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.StrSeq()) class FilenameTest(unittest.TestCase): def test_default_value(self): config = _root({}) valid = config['foo'].get(confuse.Filename('foo/bar')) self.assertEqual(valid, 'foo/bar') def test_default_none(self): config = _root({}) valid = config['foo'].get(confuse.Filename(None)) self.assertEqual(valid, None) def test_missing_required_value(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): config['foo'].get(confuse.Filename()) def test_filename_relative_to_working_dir(self): config = _root({'foo': 'bar'}) valid = config['foo'].get(confuse.Filename(cwd='/dev/null')) self.assertEqual(valid, os.path.realpath('/dev/null/bar')) def test_filename_relative_to_sibling(self): config = _root({'foo': '/', 'bar': 'baz'}) valid = config.get({ 'foo': confuse.Filename(), 'bar': confuse.Filename(relative_to='foo') }) self.assertEqual(valid.foo, os.path.realpath('/')) self.assertEqual(valid.bar, os.path.realpath('/baz')) def test_filename_working_dir_overrides_sibling(self): config = _root({'foo': 'bar'}) valid = config.get({ 'foo': confuse.Filename(cwd='/dev/null', relative_to='baz') }) self.assertEqual(valid.foo, os.path.realpath('/dev/null/bar')) def test_filename_relative_to_sibling_with_recursion(self): config = _root({'foo': '/', 'bar': 'r', 'baz': 'z'}) with self.assertRaises(confuse.ConfigTemplateError): config.get({ 'foo': confuse.Filename(relative_to='bar'), 'bar': confuse.Filename(relative_to='baz'), 'baz': confuse.Filename(relative_to='foo') }) def test_filename_relative_to_self(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.ConfigTemplateError): config.get({ 'foo': confuse.Filename(relative_to='foo') }) def test_filename_relative_to_sibling_needs_siblings(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.ConfigTemplateError): config['foo'].get(confuse.Filename(relative_to='bar')) def test_filename_relative_to_sibling_needs_template(self): config = _root({'foo': '/', 'bar': 'baz'}) with self.assertRaises(confuse.ConfigTemplateError): config.get({ 'bar': confuse.Filename(relative_to='foo') }) def test_filename_with_non_file_source(self): config = _root({'foo': 'foo/bar'}) valid = config['foo'].get(confuse.Filename()) self.assertEqual(valid, os.path.join(os.getcwd(), 'foo', 'bar')) def test_filename_with_file_source(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml') config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename()) self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) def test_filename_with_default_source(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml', default=True) config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename()) self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) def test_filename_use_config_source_dir(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml', base_for_paths=True) config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename()) self.assertEqual(valid, os.path.realpath('/baz/foo/bar')) def test_filename_in_source_dir(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml') config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename(in_source_dir=True)) self.assertEqual(valid, os.path.realpath('/baz/foo/bar')) def test_filename_in_source_dir_overrides_in_app_dir(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml') config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename(in_source_dir=True, in_app_dir=True)) self.assertEqual(valid, os.path.realpath('/baz/foo/bar')) def test_filename_in_app_dir_non_file_source(self): source = confuse.ConfigSource({'foo': 'foo/bar'}) config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename(in_app_dir=True)) self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) def test_filename_in_app_dir_overrides_config_source_dir(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml', base_for_paths=True) config = _root(source) config.config_dir = lambda: '/config/path' valid = config['foo'].get(confuse.Filename(in_app_dir=True)) self.assertEqual(valid, os.path.realpath('/config/path/foo/bar')) def test_filename_wrong_type(self): config = _root({'foo': 8}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.Filename()) class PathTest(unittest.TestCase): def test_path_value(self): import pathlib config = _root({'foo': 'foo/bar'}) valid = config['foo'].get(confuse.Path()) self.assertEqual(valid, pathlib.Path(os.path.abspath('foo/bar'))) def test_default_value(self): import pathlib config = _root({}) valid = config['foo'].get(confuse.Path('foo/bar')) self.assertEqual(valid, pathlib.Path('foo/bar')) def test_default_none(self): config = _root({}) valid = config['foo'].get(confuse.Path(None)) self.assertEqual(valid, None) def test_missing_required_value(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): config['foo'].get(confuse.Path()) class BaseTemplateTest(unittest.TestCase): def test_base_template_accepts_any_value(self): config = _root({'foo': 4.2}) valid = config['foo'].get(confuse.Template()) self.assertEqual(valid, 4.2) def test_base_template_required(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): config['foo'].get(confuse.Template()) def test_base_template_with_default(self): config = _root({}) valid = config['foo'].get(confuse.Template('bar')) self.assertEqual(valid, 'bar') class TypeTemplateTest(unittest.TestCase): def test_correct_type(self): config = _root({'foo': set()}) valid = config['foo'].get(confuse.TypeTemplate(set)) self.assertEqual(valid, set()) def test_incorrect_type(self): config = _root({'foo': dict()}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.TypeTemplate(set)) def test_missing_required_value(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): config['foo'].get(confuse.TypeTemplate(set)) def test_default_value(self): config = _root({}) valid = config['foo'].get(confuse.TypeTemplate(set, set([1, 2]))) self.assertEqual(valid, set([1, 2])) class SequenceTest(unittest.TestCase): def test_int_list(self): config = _root({'foo': [1, 2, 3]}) valid = config['foo'].get(confuse.Sequence(int)) self.assertEqual(valid, [1, 2, 3]) def test_dict_list(self): config = _root({'foo': [{'bar': 1, 'baz': 2}, {'bar': 3, 'baz': 4}]}) valid = config['foo'].get(confuse.Sequence( {'bar': int, 'baz': int} )) self.assertEqual(valid, [ {'bar': 1, 'baz': 2}, {'bar': 3, 'baz': 4} ]) def test_invalid_item(self): config = _root({'foo': [{'bar': 1, 'baz': 2}, {'bar': 3, 'bak': 4}]}) with self.assertRaises(confuse.NotFoundError): config['foo'].get(confuse.Sequence( {'bar': int, 'baz': int} )) def test_wrong_type(self): config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.Sequence(int)) def test_missing(self): config = _root({'foo': [1, 2, 3]}) valid = config['bar'].get(confuse.Sequence(int)) self.assertEqual(valid, []) class MappingValuesTest(unittest.TestCase): def test_int_dict(self): config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}}) valid = config['foo'].get(confuse.MappingValues(int)) self.assertEqual(valid, {'one': 1, 'two': 2, 'three': 3}) def test_dict_dict(self): config = _root({'foo': {'first': {'bar': 1, 'baz': 2}, 'second': {'bar': 3, 'baz': 4}}}) valid = config['foo'].get(confuse.MappingValues( {'bar': int, 'baz': int} )) self.assertEqual(valid, { 'first': {'bar': 1, 'baz': 2}, 'second': {'bar': 3, 'baz': 4}, }) def test_invalid_item(self): config = _root({'foo': {'first': {'bar': 1, 'baz': 2}, 'second': {'bar': 3, 'bak': 4}}}) with self.assertRaises(confuse.NotFoundError): config['foo'].get(confuse.MappingValues( {'bar': int, 'baz': int} )) def test_wrong_type(self): config = _root({'foo': [1, 2, 3]}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.MappingValues(int)) def test_missing(self): config = _root({'foo': {'one': 1, 'two': 2, 'three': 3}}) valid = config['bar'].get(confuse.MappingValues(int)) self.assertEqual(valid, {}) class OptionalTest(unittest.TestCase): def test_optional_string_valid_type(self): config = _root({'foo': 'bar'}) valid = config['foo'].get(confuse.Optional(confuse.String())) self.assertEqual(valid, 'bar') def test_optional_string_invalid_type(self): config = _root({'foo': 5}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(confuse.Optional(confuse.String())) def test_optional_string_null(self): config = _root({'foo': None}) valid = config['foo'].get(confuse.Optional(confuse.String())) self.assertIsNone(valid) def test_optional_string_null_default_value(self): config = _root({'foo': None}) valid = config['foo'].get(confuse.Optional(confuse.String(), 'baz')) self.assertEqual(valid, 'baz') def test_optional_string_null_string_provides_default(self): config = _root({'foo': None}) valid = config['foo'].get(confuse.Optional(confuse.String('baz'))) self.assertEqual(valid, 'baz') def test_optional_string_null_string_default_override(self): config = _root({'foo': None}) valid = config['foo'].get(confuse.Optional(confuse.String('baz'), default='bar')) self.assertEqual(valid, 'bar') def test_optional_string_allow_missing_no_explicit_default(self): config = _root({}) valid = config['foo'].get(confuse.Optional(confuse.String())) self.assertIsNone(valid) def test_optional_string_allow_missing_default_value(self): config = _root({}) valid = config['foo'].get(confuse.Optional(confuse.String(), 'baz')) self.assertEqual(valid, 'baz') def test_optional_string_missing_not_allowed(self): config = _root({}) with self.assertRaises(confuse.NotFoundError): config['foo'].get( confuse.Optional(confuse.String(), allow_missing=False) ) def test_optional_string_null_missing_not_allowed(self): config = _root({'foo': None}) valid = config['foo'].get( confuse.Optional(confuse.String(), allow_missing=False) ) self.assertIsNone(valid) def test_optional_mapping_template_valid(self): config = _root({'foo': {'bar': 5, 'baz': 'bak'}}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} valid = config.get({'foo': confuse.Optional(template)}) self.assertEqual(valid['foo']['bar'], 5) self.assertEqual(valid['foo']['baz'], 'bak') def test_optional_mapping_template_invalid(self): config = _root({'foo': {'bar': 5, 'baz': 10}}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} with self.assertRaises(confuse.ConfigTypeError): config.get({'foo': confuse.Optional(template)}) def test_optional_mapping_template_null(self): config = _root({'foo': None}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} valid = config.get({'foo': confuse.Optional(template)}) self.assertIsNone(valid['foo']) def test_optional_mapping_template_null_default_value(self): config = _root({'foo': None}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} valid = config.get({'foo': confuse.Optional(template, {})}) self.assertIsInstance(valid['foo'], dict) def test_optional_mapping_template_allow_missing_no_explicit_default(self): config = _root({}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} valid = config.get({'foo': confuse.Optional(template)}) self.assertIsNone(valid['foo']) def test_optional_mapping_template_allow_missing_default_value(self): config = _root({}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} valid = config.get({'foo': confuse.Optional(template, {})}) self.assertIsInstance(valid['foo'], dict) def test_optional_mapping_template_missing_not_allowed(self): config = _root({}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} with self.assertRaises(confuse.NotFoundError): config.get({'foo': confuse.Optional(template, allow_missing=False)}) def test_optional_mapping_template_null_missing_not_allowed(self): config = _root({'foo': None}) template = {'bar': confuse.Integer(), 'baz': confuse.String()} valid = config.get({'foo': confuse.Optional(template, allow_missing=False)}) self.assertIsNone(valid['foo']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1582500218.4474714 confuse-1.7.0/test/test_validation.py0000644000000000000000000001254500000000000014601 0ustar00from __future__ import division, absolute_import, print_function try: import enum SUPPORTS_ENUM = True except ImportError: SUPPORTS_ENUM = False import confuse import os import unittest from . import _root class TypeCheckTest(unittest.TestCase): def test_str_type_correct(self): config = _root({'foo': 'bar'}) value = config['foo'].get(str) self.assertEqual(value, 'bar') def test_str_type_incorrect(self): config = _root({'foo': 2}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(str) def test_int_type_correct(self): config = _root({'foo': 2}) value = config['foo'].get(int) self.assertEqual(value, 2) def test_int_type_incorrect(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].get(int) class BuiltInValidatorTest(unittest.TestCase): def test_as_filename_with_non_file_source(self): config = _root({'foo': 'foo/bar'}) value = config['foo'].as_filename() self.assertEqual(value, os.path.join(os.getcwd(), 'foo', 'bar')) def test_as_filename_with_file_source(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml') config = _root(source) config.config_dir = lambda: '/config/path' value = config['foo'].as_filename() self.assertEqual(value, os.path.realpath('/config/path/foo/bar')) def test_as_filename_with_default_source(self): source = confuse.ConfigSource({'foo': 'foo/bar'}, filename='/baz/config.yaml', default=True) config = _root(source) config.config_dir = lambda: '/config/path' value = config['foo'].as_filename() self.assertEqual(value, os.path.realpath('/config/path/foo/bar')) def test_as_filename_wrong_type(self): config = _root({'foo': None}) with self.assertRaises(confuse.ConfigTypeError): config['foo'].as_filename() def test_as_path(self): config = _root({'foo': 'foo/bar'}) path = os.path.join(os.getcwd(), 'foo', 'bar') try: import pathlib except ImportError: with self.assertRaises(ImportError): value = config['foo'].as_path() else: value = config['foo'].as_path() path = pathlib.Path(path) self.assertEqual(value, path) def test_as_choice_correct(self): config = _root({'foo': 'bar'}) value = config['foo'].as_choice(['foo', 'bar', 'baz']) self.assertEqual(value, 'bar') def test_as_choice_error(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.ConfigValueError): config['foo'].as_choice(['foo', 'baz']) def test_as_choice_with_dict(self): config = _root({'foo': 'bar'}) res = config['foo'].as_choice({ 'bar': 'baz', 'x': 'y', }) self.assertEqual(res, 'baz') @unittest.skipUnless(SUPPORTS_ENUM, "enum not supported in this version of Python.") def test_as_choice_with_enum(self): class Foobar(enum.Enum): Foo = 'bar' config = _root({'foo': Foobar.Foo.value}) res = config['foo'].as_choice(Foobar) self.assertEqual(res, Foobar.Foo) @unittest.skipUnless(SUPPORTS_ENUM, "enum not supported in this version of Python.") def test_as_choice_with_enum_error(self): class Foobar(enum.Enum): Foo = 'bar' config = _root({'foo': 'foo'}) with self.assertRaises(confuse.ConfigValueError): config['foo'].as_choice(Foobar) def test_as_number_float(self): config = _root({'f': 1.0}) config['f'].as_number() def test_as_number_int(self): config = _root({'i': 2}) config['i'].as_number() @unittest.skipIf(confuse.PY3, "long only present in Python 2") def test_as_number_long_in_py2(self): config = _root({'l': long(3)}) # noqa ignore=F821 config['l'].as_number() def test_as_number_string(self): config = _root({'s': 'a'}) with self.assertRaises(confuse.ConfigTypeError): config['s'].as_number() def test_as_str_seq_str(self): config = _root({'k': 'a b c'}) self.assertEqual( config['k'].as_str_seq(), ['a', 'b', 'c'] ) def test_as_str_seq_list(self): config = _root({'k': ['a b', 'c']}) self.assertEqual( config['k'].as_str_seq(), ['a b', 'c'] ) def test_as_str(self): config = _root({'s': 'foo'}) config['s'].as_str() def test_as_str_non_string(self): config = _root({'f': 1.0}) with self.assertRaises(confuse.ConfigTypeError): config['f'].as_str() def test_as_str_expanded(self): config = _root({'s': '${CONFUSE_TEST_VAR}/bar'}) os.environ["CONFUSE_TEST_VAR"] = 'foo' self.assertEqual(config['s'].as_str_expanded(), 'foo/bar') def test_as_pairs(self): config = _root({'k': [{'a': 'A'}, 'b', ['c', 'C']]}) self.assertEqual( [('a', 'A'), ('b', None), ('c', 'C')], config['k'].as_pairs() ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3506715 confuse-1.7.0/test/test_views.py0000644000000000000000000002344700000000000013607 0ustar00from __future__ import division, absolute_import, print_function import confuse import sys import unittest from . import _root PY3 = sys.version_info[0] == 3 class SingleSourceTest(unittest.TestCase): def test_dict_access(self): config = _root({'foo': 'bar'}) value = config['foo'].get() self.assertEqual(value, 'bar') def test_list_access(self): config = _root({'foo': ['bar', 'baz']}) value = config['foo'][1].get() self.assertEqual(value, 'baz') def test_missing_key(self): config = _root({'foo': 'bar'}) with self.assertRaises(confuse.NotFoundError): config['baz'].get() def test_missing_index(self): config = _root({'l': ['foo', 'bar']}) with self.assertRaises(confuse.NotFoundError): config['l'][5].get() def test_dict_iter(self): config = _root({'foo': 'bar', 'baz': 'qux'}) keys = [key for key in config] self.assertEqual(set(keys), set(['foo', 'baz'])) def test_list_iter(self): config = _root({'l': ['foo', 'bar']}) items = [subview.get() for subview in config['l']] self.assertEqual(items, ['foo', 'bar']) def test_int_iter(self): config = _root({'n': 2}) with self.assertRaises(confuse.ConfigTypeError): [item for item in config['n']] def test_dict_keys(self): config = _root({'foo': 'bar', 'baz': 'qux'}) keys = config.keys() self.assertEqual(set(keys), set(['foo', 'baz'])) def test_dict_values(self): config = _root({'foo': 'bar', 'baz': 'qux'}) values = [value.get() for value in config.values()] self.assertEqual(set(values), set(['bar', 'qux'])) def test_dict_items(self): config = _root({'foo': 'bar', 'baz': 'qux'}) items = [(key, value.get()) for (key, value) in config.items()] self.assertEqual(set(items), set([('foo', 'bar'), ('baz', 'qux')])) def test_list_keys_error(self): config = _root({'l': ['foo', 'bar']}) with self.assertRaises(confuse.ConfigTypeError): config['l'].keys() def test_list_sequence(self): config = _root({'l': ['foo', 'bar']}) items = [item.get() for item in config['l'].sequence()] self.assertEqual(items, ['foo', 'bar']) def test_dict_sequence_error(self): config = _root({'foo': 'bar', 'baz': 'qux'}) with self.assertRaises(confuse.ConfigTypeError): list(config.sequence()) def test_dict_contents(self): config = _root({'foo': 'bar', 'baz': 'qux'}) contents = config.all_contents() self.assertEqual(set(contents), set(['foo', 'baz'])) def test_list_contents(self): config = _root({'l': ['foo', 'bar']}) contents = config['l'].all_contents() self.assertEqual(list(contents), ['foo', 'bar']) def test_int_contents(self): config = _root({'n': 2}) with self.assertRaises(confuse.ConfigTypeError): list(config['n'].all_contents()) class ConverstionTest(unittest.TestCase): def test_str_conversion_from_str(self): config = _root({'foo': 'bar'}) value = str(config['foo']) self.assertEqual(value, 'bar') def test_str_conversion_from_int(self): config = _root({'foo': 2}) value = str(config['foo']) self.assertEqual(value, '2') @unittest.skipIf(confuse.PY3, "unicode only present in Python 2") def test_unicode_conversion_from_int(self): config = _root({'foo': 2}) value = unicode(config['foo']) # noqa ignore=F821 self.assertEqual(value, unicode('2')) # noqa ignore=F821 def test_bool_conversion_from_bool(self): config = _root({'foo': True}) value = bool(config['foo']) self.assertEqual(value, True) def test_bool_conversion_from_int(self): config = _root({'foo': 0}) value = bool(config['foo']) self.assertEqual(value, False) class NameTest(unittest.TestCase): def test_root_name(self): config = _root() self.assertEqual(config.name, 'root') def test_string_access_name(self): config = _root() name = config['foo'].name self.assertEqual(name, "foo") def test_int_access_name(self): config = _root() name = config[5].name self.assertEqual(name, "#5") def test_nested_access_name(self): config = _root() name = config[5]['foo']['bar'][20].name self.assertEqual(name, "#5.foo.bar#20") class MultipleSourceTest(unittest.TestCase): def test_dict_access_shadowed(self): config = _root({'foo': 'bar'}, {'foo': 'baz'}) value = config['foo'].get() self.assertEqual(value, 'bar') def test_dict_access_fall_through(self): config = _root({'qux': 'bar'}, {'foo': 'baz'}) value = config['foo'].get() self.assertEqual(value, 'baz') def test_dict_access_missing(self): config = _root({'qux': 'bar'}, {'foo': 'baz'}) with self.assertRaises(confuse.NotFoundError): config['fred'].get() def test_list_access_shadowed(self): config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) value = config['l'][1].get() self.assertEqual(value, 'b') def test_list_access_fall_through(self): config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) value = config['l'][2].get() self.assertEqual(value, 'e') def test_list_access_missing(self): config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) with self.assertRaises(confuse.NotFoundError): config['l'][3].get() def test_access_dict_replaced(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) value = config['foo'].get() self.assertEqual(value, {'bar': 'baz'}) def test_dict_keys_merged(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) keys = config['foo'].keys() self.assertEqual(set(keys), set(['bar', 'qux'])) def test_dict_keys_replaced(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) keys = config['foo'].keys() self.assertEqual(list(keys), ['bar']) def test_dict_values_merged(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) values = [value.get() for value in config['foo'].values()] self.assertEqual(set(values), set(['baz', 'fred'])) def test_dict_values_replaced(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) values = [value.get() for value in config['foo'].values()] self.assertEqual(list(values), ['baz']) def test_dict_items_merged(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) items = [(key, value.get()) for (key, value) in config['foo'].items()] self.assertEqual(set(items), set([('bar', 'baz'), ('qux', 'fred')])) def test_dict_items_replaced(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) items = [(key, value.get()) for (key, value) in config['foo'].items()] self.assertEqual(list(items), [('bar', 'baz')]) def test_list_sequence_shadowed(self): config = _root({'l': ['a', 'b']}, {'l': ['c', 'd', 'e']}) items = [item.get() for item in config['l'].sequence()] self.assertEqual(items, ['a', 'b']) def test_list_sequence_shadowed_by_dict(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': ['qux', 'fred']}) with self.assertRaises(confuse.ConfigTypeError): list(config['foo'].sequence()) def test_dict_contents_concatenated(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'qux': 'fred'}}) contents = config['foo'].all_contents() self.assertEqual(set(contents), set(['bar', 'qux'])) def test_dict_contents_concatenated_not_replaced(self): config = _root({'foo': {'bar': 'baz'}}, {'foo': {'bar': 'fred'}}) contents = config['foo'].all_contents() self.assertEqual(list(contents), ['bar', 'bar']) def test_list_contents_concatenated(self): config = _root({'foo': ['bar', 'baz']}, {'foo': ['qux', 'fred']}) contents = config['foo'].all_contents() self.assertEqual(list(contents), ['bar', 'baz', 'qux', 'fred']) def test_int_contents_error(self): config = _root({'foo': ['bar', 'baz']}, {'foo': 5}) with self.assertRaises(confuse.ConfigTypeError): list(config['foo'].all_contents()) def test_list_and_dict_contents_concatenated(self): config = _root({'foo': ['bar', 'baz']}, {'foo': {'qux': 'fred'}}) contents = config['foo'].all_contents() self.assertEqual(list(contents), ['bar', 'baz', 'qux']) def test_add_source(self): config = _root({'foo': 'bar'}) config.add({'baz': 'qux'}) self.assertEqual(config['foo'].get(), 'bar') self.assertEqual(config['baz'].get(), 'qux') class SetTest(unittest.TestCase): def test_set_missing_top_level_key(self): config = _root({}) config['foo'] = 'bar' self.assertEqual(config['foo'].get(), 'bar') def test_override_top_level_key(self): config = _root({'foo': 'bar'}) config['foo'] = 'baz' self.assertEqual(config['foo'].get(), 'baz') def test_set_second_level_key(self): config = _root({}) config['foo']['bar'] = 'baz' self.assertEqual(config['foo']['bar'].get(), 'baz') def test_override_second_level_key(self): config = _root({'foo': {'bar': 'qux'}}) config['foo']['bar'] = 'baz' self.assertEqual(config['foo']['bar'].get(), 'baz') def test_override_list_index(self): config = _root({'foo': ['a', 'b', 'c']}) config['foo'][1] = 'bar' self.assertEqual(config['foo'][1].get(), 'bar') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1637960190.3524253 confuse-1.7.0/test/test_yaml.py0000644000000000000000000001046400000000000013407 0ustar00from __future__ import division, absolute_import, print_function import confuse import yaml import unittest from . import TempDir def load(s): return yaml.load(s, Loader=confuse.Loader) class ParseTest(unittest.TestCase): def test_dict_parsed_as_ordereddict(self): v = load("a: b\nc: d") self.assertTrue(isinstance(v, confuse.OrderedDict)) self.assertEqual(list(v), ['a', 'c']) def test_string_beginning_with_percent(self): v = load("foo: %bar") self.assertEqual(v['foo'], '%bar') class FileParseTest(unittest.TestCase): def _parse_contents(self, contents): with TempDir() as temp: path = temp.sub('test_config.yaml', contents) return confuse.load_yaml(path) def test_load_file(self): v = self._parse_contents(b'foo: bar') self.assertEqual(v['foo'], 'bar') def test_syntax_error(self): try: self._parse_contents(b':') except confuse.ConfigError as exc: self.assertTrue('test_config.yaml' in exc.name) else: self.fail('ConfigError not raised') def test_reload_conf(self): with TempDir() as temp: path = temp.sub('test_config.yaml', b'foo: bar') config = confuse.Configuration('test', __name__) config.set_file(filename=path) self.assertEqual(config['foo'].get(), 'bar') temp.sub('test_config.yaml', b'foo: bar2\ntest: hello world') config.reload() self.assertEqual(config['foo'].get(), 'bar2') self.assertEqual(config['test'].get(), 'hello world') def test_tab_indentation_error(self): try: self._parse_contents(b"foo:\n\tbar: baz") except confuse.ConfigError as exc: self.assertTrue('found tab' in exc.args[0]) else: self.fail('ConfigError not raised') class StringParseTest(unittest.TestCase): def test_load_string(self): v = confuse.load_yaml_string('foo: bar', 'test') self.assertEqual(v['foo'], 'bar') def test_string_syntax_error(self): try: confuse.load_yaml_string(':', 'test') except confuse.ConfigError as exc: self.assertTrue('test' in exc.name) else: self.fail('ConfigError not raised') def test_string_tab_indentation_error(self): try: confuse.load_yaml_string('foo:\n\tbar: baz', 'test') except confuse.ConfigError as exc: self.assertTrue('found tab' in exc.args[0]) else: self.fail('ConfigError not raised') class ParseAsScalarTest(unittest.TestCase): def test_text_string(self): v = confuse.yaml_util.parse_as_scalar('foo', confuse.Loader) self.assertEqual(v, 'foo') def test_number_string_to_int(self): v = confuse.yaml_util.parse_as_scalar('1', confuse.Loader) self.assertIsInstance(v, int) self.assertEqual(v, 1) def test_number_string_to_float(self): v = confuse.yaml_util.parse_as_scalar('1.0', confuse.Loader) self.assertIsInstance(v, float) self.assertEqual(v, 1.0) def test_bool_string_to_bool(self): v = confuse.yaml_util.parse_as_scalar('true', confuse.Loader) self.assertIs(v, True) def test_empty_string_to_none(self): v = confuse.yaml_util.parse_as_scalar('', confuse.Loader) self.assertIs(v, None) def test_null_string_to_none(self): v = confuse.yaml_util.parse_as_scalar('null', confuse.Loader) self.assertIs(v, None) def test_dict_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar('{"foo": "bar"}', confuse.Loader) self.assertEqual(v, '{"foo": "bar"}') def test_dict_unchanged(self): v = confuse.yaml_util.parse_as_scalar({'foo': 'bar'}, confuse.Loader) self.assertEqual(v, {'foo': 'bar'}) def test_list_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar('["foo", "bar"]', confuse.Loader) self.assertEqual(v, '["foo", "bar"]') def test_list_unchanged(self): v = confuse.yaml_util.parse_as_scalar(['foo', 'bar'], confuse.Loader) self.assertEqual(v, ['foo', 'bar']) def test_invalid_yaml_string_unchanged(self): v = confuse.yaml_util.parse_as_scalar('!', confuse.Loader) self.assertEqual(v, '!') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1607534078.0 confuse-1.7.0/tox.ini0000644000000000000000000000221000000000000011356 0ustar00# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py{27,34,35,36,37,38}-test, py27-flake8, docs isolated_build = True [tox:.package] basepython = python3 [_test] deps = coverage nose nose-show-skipped pyyaml pathlib [_flake8] deps = flake8 flake8-future-import pep8-naming files = example confuse test docs [testenv] passenv = NOSE_SHOW_SKIPPED # Undocumented feature of nose-show-skipped. deps = {test,cov}: {[_test]deps} py{27,34,36}-flake8: {[_flake8]deps} commands = cov: nosetests --with-coverage {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 sphinx-rtd-theme commands = sphinx-build -W -q -b html docs {envtmpdir}/html {posargs} confuse-1.7.0/setup.py0000644000000000000000000000130200000000000011556 0ustar00#!/usr/bin/env python # setup.py generated by flit for tools that don't yet use PEP 517 from distutils.core import setup packages = \ ['confuse'] package_data = \ {'': ['*']} install_requires = \ ['pyyaml'] extras_require = \ {"test:python_version == '2.7'": ['pathlib']} setup(name='confuse', version='1.7.0', description='Painless YAML configuration.', author='Adrian Sampson', author_email='adrian@radbox.org', url='https://github.com/beetbox/confuse', packages=packages, package_data=package_data, install_requires=install_requires, extras_require=extras_require, python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4', ) confuse-1.7.0/PKG-INFO0000644000000000000000000000027600000000000011152 0ustar00Metadata-Version: 1.1 Name: confuse Version: 1.7.0 Summary: Painless YAML configuration. Home-page: https://github.com/beetbox/confuse Author: Adrian Sampson Author-email: adrian@radbox.org