From 70c5b67df353f70f0253dd7b8bcc66c871f842ff Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Fri, 5 Dec 2014 15:33:58 -0500 Subject: [PATCH] Move files out of the namespace package Move the public API out of oslo.config to oslo_config. Retain the ability to import from the old namespace package for backwards compatibility for this release cycle. bp/drop-namespace-packages Change-Id: I56274336802036de050efc62eb2ee6b5d4ede77b --- .gitignore | 1 + doc/source/cfg.rst | 2 +- doc/source/cfgfilter.rst | 2 +- doc/source/configopts.rst | 2 +- doc/source/exceptions.rst | 2 +- doc/source/fixture.rst | 2 +- doc/source/generator.rst | 4 +- doc/source/helpers.rst | 2 +- doc/source/opts.rst | 2 +- doc/source/parser.rst | 6 +- doc/source/types.rst | 2 +- oslo/config/__init__.py | 28 + oslo/config/cfg.py | 2460 +------------------ oslo/config/cfgfilter.py | 307 +-- oslo/config/fixture.py | 107 +- oslo/config/generator.py | 301 +-- oslo/config/iniparser.py | 116 +- oslo/config/types.py | 402 +-- oslo_config/__init__.py | 0 oslo_config/cfg.py | 2471 +++++++++++++++++++ oslo_config/cfgfilter.py | 318 +++ oslo_config/fixture.py | 118 + oslo_config/generator.py | 312 +++ oslo_config/iniparser.py | 127 + oslo_config/tests/__init__.py | 0 oslo_config/tests/test_cfg.py | 3535 +++++++++++++++++++++++++++ oslo_config/tests/test_cfgfilter.py | 280 +++ oslo_config/tests/test_fixture.py | 87 + oslo_config/tests/test_generator.py | 539 ++++ oslo_config/tests/test_iniparser.py | 124 + oslo_config/tests/test_types.py | 411 ++++ oslo_config/types.py | 413 ++++ setup.cfg | 3 +- tests/test_cfg.py | 49 +- tests/test_cfgfilter.py | 36 - tests/test_generator.py | 2 +- tests/test_warning.py | 61 + tests/testmods/bar_foo_opt.py | 2 +- tests/testmods/baz_qux_opt.py | 2 +- tests/testmods/blaa_opt.py | 2 +- tests/testmods/fbaar_baa_opt.py | 2 +- tests/testmods/fbar_foo_opt.py | 2 +- tests/testmods/fblaa_opt.py | 2 +- 43 files changed, 8856 insertions(+), 3790 deletions(-) create mode 100644 oslo_config/__init__.py create mode 100644 oslo_config/cfg.py create mode 100644 oslo_config/cfgfilter.py create mode 100644 oslo_config/fixture.py create mode 100644 oslo_config/generator.py create mode 100644 oslo_config/iniparser.py create mode 100644 oslo_config/tests/__init__.py create mode 100644 oslo_config/tests/test_cfg.py create mode 100644 oslo_config/tests/test_cfgfilter.py create mode 100644 oslo_config/tests/test_fixture.py create mode 100644 oslo_config/tests/test_generator.py create mode 100644 oslo_config/tests/test_iniparser.py create mode 100644 oslo_config/tests/test_types.py create mode 100644 oslo_config/types.py create mode 100644 tests/test_warning.py diff --git a/.gitignore b/.gitignore index 3abcf654..5ae5f035 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ dist/ .testrepository/ .project .pydevproject +pbr-*.egg/ diff --git a/doc/source/cfg.rst b/doc/source/cfg.rst index c444bcbe..5e3f9882 100644 --- a/doc/source/cfg.rst +++ b/doc/source/cfg.rst @@ -2,4 +2,4 @@ The cfg Module -------------- -.. automodule:: oslo.config.cfg +.. automodule:: oslo_config.cfg diff --git a/doc/source/cfgfilter.rst b/doc/source/cfgfilter.rst index a471f6ec..c11cddd9 100644 --- a/doc/source/cfgfilter.rst +++ b/doc/source/cfgfilter.rst @@ -2,4 +2,4 @@ The cfgfilter Module -------------------- -.. automodule:: oslo.config.cfgfilter +.. automodule:: oslo_config.cfgfilter diff --git a/doc/source/configopts.rst b/doc/source/configopts.rst index 5ee090d0..06c9c3b6 100644 --- a/doc/source/configopts.rst +++ b/doc/source/configopts.rst @@ -2,7 +2,7 @@ The ConfigOpts Class -------------------- -.. currentmodule:: oslo.config.cfg +.. currentmodule:: oslo_config.cfg .. autoclass:: ConfigOpts :members: diff --git a/doc/source/exceptions.rst b/doc/source/exceptions.rst index 5d090666..eaf5f491 100644 --- a/doc/source/exceptions.rst +++ b/doc/source/exceptions.rst @@ -2,7 +2,7 @@ Exceptions ---------- -.. currentmodule:: oslo.config.cfg +.. currentmodule:: oslo_config.cfg .. autoexception:: Error .. autoexception:: ArgsAlreadyParsedError diff --git a/doc/source/fixture.rst b/doc/source/fixture.rst index b40d972d..47c4939f 100644 --- a/doc/source/fixture.rst +++ b/doc/source/fixture.rst @@ -2,7 +2,7 @@ Test Fixture ------------ -.. currentmodule:: oslo.config.fixture +.. currentmodule:: oslo_config.fixture .. autoclass:: Config :members: diff --git a/doc/source/generator.rst b/doc/source/generator.rst index 32dc972a..d6bf6c01 100644 --- a/doc/source/generator.rst +++ b/doc/source/generator.rst @@ -2,9 +2,9 @@ oslo-config-generator --------------------- -.. automodule:: oslo.config.generator +.. automodule:: oslo_config.generator -.. currentmodule:: oslo.config.generator +.. currentmodule:: oslo_config.generator .. autofunction:: main .. autofunction:: generate diff --git a/doc/source/helpers.rst b/doc/source/helpers.rst index 66fbba47..e2d5cb40 100644 --- a/doc/source/helpers.rst +++ b/doc/source/helpers.rst @@ -2,7 +2,7 @@ Helper Functions ---------------- -.. currentmodule:: oslo.config.cfg +.. currentmodule:: oslo_config.cfg .. autofunction:: find_config_files .. autofunction:: set_defaults diff --git a/doc/source/opts.rst b/doc/source/opts.rst index 4136c1c0..de9b834c 100644 --- a/doc/source/opts.rst +++ b/doc/source/opts.rst @@ -2,7 +2,7 @@ Option Definitions ------------------ -.. currentmodule:: oslo.config.cfg +.. currentmodule:: oslo_config.cfg .. autoclass:: Opt .. autoclass:: StrOpt diff --git a/doc/source/parser.rst b/doc/source/parser.rst index 6cc0cd5c..64b8b384 100644 --- a/doc/source/parser.rst +++ b/doc/source/parser.rst @@ -2,10 +2,10 @@ File Parsing ------------ -.. autoclass:: oslo.config.iniparser.BaseParser +.. autoclass:: oslo_config.iniparser.BaseParser -.. autoclass:: oslo.config.cfg.ConfigParser +.. autoclass:: oslo_config.cfg.ConfigParser :members: parse -.. autoclass:: oslo.config.cfg.MultiConfigParser +.. autoclass:: oslo_config.cfg.MultiConfigParser :members: read, get diff --git a/doc/source/types.rst b/doc/source/types.rst index 57433efa..9f7ea131 100644 --- a/doc/source/types.rst +++ b/doc/source/types.rst @@ -2,5 +2,5 @@ Option Types and Validation --------------------------- -.. automodule:: oslo.config.types +.. automodule:: oslo_config.types :members: diff --git a/oslo/config/__init__.py b/oslo/config/__init__.py index e69de29b..331e0b08 100644 --- a/oslo/config/__init__.py +++ b/oslo/config/__init__.py @@ -0,0 +1,28 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import warnings + +from oslo_config import * # noqa + + +def deprecated(): + new_name = __name__.replace('.', '_') + warnings.warn( + ('The oslo namespace package is deprecated. Please use %s instead.' % + new_name), + DeprecationWarning, + stacklevel=3, + ) + + +deprecated() diff --git a/oslo/config/cfg.py b/oslo/config/cfg.py index a6fe1a15..28135252 100644 --- a/oslo/config/cfg.py +++ b/oslo/config/cfg.py @@ -1,5 +1,3 @@ -# Copyright 2012 Red Hat, Inc. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -12,2460 +10,4 @@ # License for the specific language governing permissions and limitations # under the License. -r""" -Configuration options which may be set on the command line or in config files. - -The schema for each option is defined using the Opt class or its sub-classes, -for example: - -:: - - from oslo.config import cfg - from oslo.config import types - - PortType = types.Integer(1, 65535) - - common_opts = [ - cfg.StrOpt('bind_host', - default='0.0.0.0', - help='IP address to listen on.'), - cfg.Opt('bind_port', - type=PortType(), - default=9292, - help='Port number to listen on.') - ] - -Option Types ------------- - -Options can have arbitrary types, you just need to pass type constructor -to Opt. Type constructor is a callable object that takes a string and returns -value of particular type or raises ValueError if given string can't be -converted to that type. - -There are predefined types: strings, integers, floats, booleans, lists, -'multi strings' and 'key/value pairs' (dictionary) :: - - enabled_apis_opt = cfg.ListOpt('enabled_apis', - default=['ec2', 'osapi_compute'], - help='List of APIs to enable by default.') - - DEFAULT_EXTENSIONS = [ - 'nova.api.openstack.compute.contrib.standard_extensions' - ] - osapi_compute_extension_opt = cfg.MultiStrOpt('osapi_compute_extension', - default=DEFAULT_EXTENSIONS) - -Registering Options -------------------- - -Option schemas are registered with the config manager at runtime, but before -the option is referenced:: - - class ExtensionManager(object): - - enabled_apis_opt = cfg.ListOpt(...) - - def __init__(self, conf): - self.conf = conf - self.conf.register_opt(enabled_apis_opt) - ... - - def _load_extensions(self): - for ext_factory in self.conf.osapi_compute_extension: - .... - -A common usage pattern is for each option schema to be defined in the module or -class which uses the option:: - - opts = ... - - def add_common_opts(conf): - conf.register_opts(opts) - - def get_bind_host(conf): - return conf.bind_host - - def get_bind_port(conf): - return conf.bind_port - -An option may optionally be made available via the command line. Such options -must be registered with the config manager before the command line is parsed -(for the purposes of --help and CLI arg validation):: - - cli_opts = [ - cfg.BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output.'), - cfg.BoolOpt('debug', - short='d', - default=False, - help='Print debugging output.'), - ] - - def add_common_opts(conf): - conf.register_cli_opts(cli_opts) - -Loading Config Files --------------------- - -The config manager has two CLI options defined by default, --config-file -and --config-dir:: - - class ConfigOpts(object): - - def __call__(self, ...): - - opts = [ - MultiStrOpt('config-file', - ...), - StrOpt('config-dir', - ...), - ] - - self.register_cli_opts(opts) - -Option values are parsed from any supplied config files using -oslo.config.iniparser. If none are specified, a default set is used -for example glance-api.conf and glance-common.conf:: - - glance-api.conf: - [DEFAULT] - bind_port = 9292 - - glance-common.conf: - [DEFAULT] - bind_host = 0.0.0.0 - -Option values in config files and those on the command line are parsed -in order. The same option can appear many times, in config files or on -the command line. Later values always override earlier ones. - -The parsing of CLI args and config files is initiated by invoking the config -manager for example:: - - conf = ConfigOpts() - conf.register_opt(BoolOpt('verbose', ...)) - conf(sys.argv[1:]) - if conf.verbose: - ... - -Option Groups -------------- - -Options can be registered as belonging to a group:: - - rabbit_group = cfg.OptGroup(name='rabbit', - title='RabbitMQ options') - - rabbit_host_opt = cfg.StrOpt('host', - default='localhost', - help='IP/hostname to listen on.'), - rabbit_port_opt = cfg.IntOpt('port', - default=5672, - help='Port number to listen on.') - - def register_rabbit_opts(conf): - conf.register_group(rabbit_group) - # options can be registered under a group in either of these ways: - conf.register_opt(rabbit_host_opt, group=rabbit_group) - conf.register_opt(rabbit_port_opt, group='rabbit') - -If no group attributes are required other than the group name, the group -need not be explicitly registered for example:: - - def register_rabbit_opts(conf): - # The group will automatically be created, equivalent calling:: - # conf.register_group(OptGroup(name='rabbit')) - conf.register_opt(rabbit_port_opt, group='rabbit') - -If no group is specified, options belong to the 'DEFAULT' section of config -files:: - - glance-api.conf: - [DEFAULT] - bind_port = 9292 - ... - - [rabbit] - host = localhost - port = 5672 - use_ssl = False - userid = guest - password = guest - virtual_host = / - -Command-line options in a group are automatically prefixed with the -group name:: - - --rabbit-host localhost --rabbit-port 9999 - -Accessing Option Values In Your Code ------------------------------------- - -Option values in the default group are referenced as attributes/properties on -the config manager; groups are also attributes on the config manager, with -attributes for each of the options associated with the group:: - - server.start(app, conf.bind_port, conf.bind_host, conf) - - self.connection = kombu.connection.BrokerConnection( - hostname=conf.rabbit.host, - port=conf.rabbit.port, - ...) - -Option Value Interpolation --------------------------- - -Option values may reference other values using PEP 292 string substitution:: - - opts = [ - cfg.StrOpt('state_path', - default=os.path.join(os.path.dirname(__file__), '../'), - help='Top-level directory for maintaining nova state.'), - cfg.StrOpt('sqlite_db', - default='nova.sqlite', - help='File name for SQLite.'), - cfg.StrOpt('sql_connection', - default='sqlite:///$state_path/$sqlite_db', - help='Connection string for SQL database.'), - ] - -.. note:: - - Interpolation can be avoided by using `$$`. - -.. warning:: - - Interpolation using the values of options in groups is not yet - supported. The interpolated option must be in the DEFAULT group - (i.e., ``"$state_path"`` works but ``"$database.state_path"`` does - not). - -Special Handling Instructions ------------------------------ - -Options may be declared as required so that an error is raised if the user -does not supply a value for the option:: - - opts = [ - cfg.StrOpt('service_name', required=True), - cfg.StrOpt('image_id', required=True), - ... - ] - -Options may be declared as secret so that their values are not leaked into -log files:: - - opts = [ - cfg.StrOpt('s3_store_access_key', secret=True), - cfg.StrOpt('s3_store_secret_key', secret=True), - ... - ] - -Global ConfigOpts ------------------ - -This module also contains a global instance of the ConfigOpts class -in order to support a common usage pattern in OpenStack:: - - from oslo.config import cfg - - opts = [ - cfg.StrOpt('bind_host', default='0.0.0.0'), - cfg.IntOpt('bind_port', default=9292), - ] - - CONF = cfg.CONF - CONF.register_opts(opts) - - def start(server, app): - server.start(app, CONF.bind_port, CONF.bind_host) - -Positional Command Line Arguments ---------------------------------- - -Positional command line arguments are supported via a 'positional' Opt -constructor argument:: - - >>> conf = ConfigOpts() - >>> conf.register_cli_opt(MultiStrOpt('bar', positional=True)) - True - >>> conf(['a', 'b']) - >>> conf.bar - ['a', 'b'] - -Sub-Parsers ------------ - -It is also possible to use argparse "sub-parsers" to parse additional -command line arguments using the SubCommandOpt class: - - >>> def add_parsers(subparsers): - ... list_action = subparsers.add_parser('list') - ... list_action.add_argument('id') - ... - >>> conf = ConfigOpts() - >>> conf.register_cli_opt(SubCommandOpt('action', handler=add_parsers)) - True - >>> conf(args=['list', '10']) - >>> conf.action.name, conf.action.id - ('list', '10') - -""" - -import argparse -import collections -import copy -import errno -import functools -import glob -import itertools -import logging -import os -import string -import sys - -import six -from six import moves - -from oslo.config import iniparser -from oslo.config import types - -LOG = logging.getLogger(__name__) - - -class Error(Exception): - """Base class for cfg exceptions.""" - - def __init__(self, msg=None): - self.msg = msg - - def __str__(self): - return self.msg - - -class NotInitializedError(Error): - """Raised if parser is not initialized yet.""" - - def __str__(self): - return "call expression on parser has not been invoked" - - -class ArgsAlreadyParsedError(Error): - """Raised if a CLI opt is registered after parsing.""" - - def __str__(self): - ret = "arguments already parsed" - if self.msg: - ret += ": " + self.msg - return ret - - -class NoSuchOptError(Error, AttributeError): - """Raised if an opt which doesn't exist is referenced.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "no such option: %s" % self.opt_name - else: - return "no such option in group %s: %s" % (self.group.name, - self.opt_name) - - -class NoSuchGroupError(Error): - """Raised if a group which doesn't exist is referenced.""" - - def __init__(self, group_name): - self.group_name = group_name - - def __str__(self): - return "no such group: %s" % self.group_name - - -class DuplicateOptError(Error): - """Raised if multiple opts with the same name are registered.""" - - def __init__(self, opt_name): - self.opt_name = opt_name - - def __str__(self): - return "duplicate option: %s" % self.opt_name - - -class RequiredOptError(Error): - """Raised if an option is required but no value is supplied by the user.""" - - def __init__(self, opt_name, group=None): - self.opt_name = opt_name - self.group = group - - def __str__(self): - if self.group is None: - return "value required for option: %s" % self.opt_name - else: - return "value required for option: %s.%s" % (self.group.name, - self.opt_name) - - -class TemplateSubstitutionError(Error): - """Raised if an error occurs substituting a variable in an opt value.""" - - def __str__(self): - return "template substitution error: %s" % self.msg - - -class ConfigFilesNotFoundError(Error): - """Raised if one or more config files are not found.""" - - def __init__(self, config_files): - self.config_files = config_files - - def __str__(self): - return ('Failed to find some config files: %s' % - ",".join(self.config_files)) - - -class ConfigFilesPermissionDeniedError(Error): - """Raised if one or more config files are not readable.""" - - def __init__(self, config_files): - self.config_files = config_files - - def __str__(self): - return ('Failed to open some config files: %s' % - ",".join(self.config_files)) - - -class ConfigDirNotFoundError(Error): - """Raised if the requested config-dir is not found.""" - - def __init__(self, config_dir): - self.config_dir = config_dir - - def __str__(self): - return ('Failed to read config file directory: %s' % self.config_dir) - - -class ConfigFileParseError(Error): - """Raised if there is an error parsing a config file.""" - - def __init__(self, config_file, msg): - self.config_file = config_file - self.msg = msg - - def __str__(self): - return 'Failed to parse %s: %s' % (self.config_file, self.msg) - - -class ConfigFileValueError(Error): - """Raised if a config file value does not match its opt type.""" - pass - - -def _fixpath(p): - """Apply tilde expansion and absolutization to a path.""" - return os.path.abspath(os.path.expanduser(p)) - - -def _get_config_dirs(project=None): - """Return a list of directories where config files may be located. - - :param project: an optional project name - - If a project is specified, following directories are returned:: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - Otherwise, these directories:: - - ~/ - /etc/ - """ - cfg_dirs = [ - _fixpath(os.path.join('~', '.' + project)) if project else None, - _fixpath('~'), - os.path.join('/etc', project) if project else None, - '/etc' - ] - - return list(moves.filter(bool, cfg_dirs)) - - -def _search_dirs(dirs, basename, extension=""): - """Search a list of directories for a given filename. - - Iterator over the supplied directories, returning the first file - found with the supplied name and extension. - - :param dirs: a list of directories - :param basename: the filename, for example 'glance-api' - :param extension: the file extension, for example '.conf' - :returns: the path to a matching file, or None - """ - for d in dirs: - path = os.path.join(d, '%s%s' % (basename, extension)) - if os.path.exists(path): - return path - - -def find_config_files(project=None, prog=None, extension='.conf'): - """Return a list of default configuration files. - - :param project: an optional project name - :param prog: the program name, defaulting to the basename of sys.argv[0] - :param extension: the type of the config file - - We default to two config files: [${project}.conf, ${prog}.conf] - - And we look for those config files in the following directories:: - - ~/.${project}/ - ~/ - /etc/${project}/ - /etc/ - - We return an absolute path for (at most) one of each the default config - files, for the topmost directory it exists in. - - For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf - and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf', - '~/.foo/bar.conf'] - - If no project name is supplied, we only look for ${prog.conf}. - """ - if prog is None: - prog = os.path.basename(sys.argv[0]) - - cfg_dirs = _get_config_dirs(project) - - config_files = [] - if project: - config_files.append(_search_dirs(cfg_dirs, project, extension)) - config_files.append(_search_dirs(cfg_dirs, prog, extension)) - - return list(moves.filter(bool, config_files)) - - -def _is_opt_registered(opts, opt): - """Check whether an opt with the same name is already registered. - - The same opt may be registered multiple times, with only the first - registration having any effect. However, it is an error to attempt - to register a different opt with the same name. - - :param opts: the set of opts already registered - :param opt: the opt to be registered - :returns: True if the opt was previously registered, False otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if opt.dest in opts: - if opts[opt.dest]['opt'] != opt: - raise DuplicateOptError(opt.name) - return True - else: - return False - - -def set_defaults(opts, **kwargs): - for opt in opts: - if opt.dest in kwargs: - opt.default = kwargs[opt.dest] - - -def _normalize_group_name(group_name): - if group_name == 'DEFAULT': - return group_name - return group_name.lower() - - -class Opt(object): - - """Base class for all configuration options. - - An Opt object has no public methods, but has a number of public string - properties: - - name: - the name of the option, which may include hyphens - type: - a callable object that takes string and returns - converted and validated value - dest: - the (hyphen-less) ConfigOpts property which contains the option value - short: - a single character CLI option name - default: - the default value of the option - sample_default: - a sample default value string to include in sample config files - positional: - True if the option is a positional CLI argument - metavar: - the name shown as the argument to a CLI option in --help output - help: - an string explaining how the options value is used - """ - multi = False - - def __init__(self, name, type=None, dest=None, short=None, - default=None, positional=False, metavar=None, help=None, - secret=False, required=False, - deprecated_name=None, deprecated_group=None, - deprecated_opts=None, sample_default=None): - """Construct an Opt object. - - The only required parameter is the option's name. However, it is - common to also supply a default and help string for all options. - - :param name: the option's name - :param type: the option's type. Must be a callable object that - takes string and returns converted and validated value - :param dest: the name of the corresponding ConfigOpts property - :param short: a single character CLI option name - :param default: the default value of the option - :param positional: True if the option is a positional CLI argument - :param metavar: the option argument to show in --help - :param help: an explanation of how the option is used - :param secret: true iff the value should be obfuscated in log output - :param required: true iff a value must be supplied for this option - :param deprecated_name: deprecated name option. Acts like an alias - :param deprecated_group: the group containing a deprecated alias - :param deprecated_opts: array of DeprecatedOpt(s) - :param sample_default: a default string for sample config files - """ - if name.startswith('_'): - raise ValueError('illegal name %s with prefix _' % (name,)) - self.name = name - - if type is None: - type = types.String() - - if not callable(type): - raise TypeError('type must be callable') - self.type = type - - if dest is None: - self.dest = self.name.replace('-', '_') - else: - self.dest = dest - self.short = short - self.default = default - self.sample_default = sample_default - self.positional = positional - self.metavar = metavar - self.help = help - self.secret = secret - self.required = required - if deprecated_name is not None: - deprecated_name = deprecated_name.replace('-', '_') - - self.deprecated_opts = copy.deepcopy(deprecated_opts) or [] - if deprecated_name is not None or deprecated_group is not None: - self.deprecated_opts.append(DeprecatedOpt(deprecated_name, - group=deprecated_group)) - self._assert_default_is_of_opt_type() - - def _default_is_ref(self): - """Check if default is a reference to another var.""" - if isinstance(self.default, six.string_types): - tmpl = self.default.replace('\$', '').replace('$$', '') - return '$' in tmpl - return False - - def _assert_default_is_of_opt_type(self): - if (self.default is not None - and not self._default_is_ref() - and hasattr(self.type, 'is_base_type') - and not self.type.is_base_type(self.default)): - # NOTE(tcammann) Change this to raise error after K relase - expected_types = ", ".join( - [t.__name__ for t in self.type.BASE_TYPES]) - LOG.debug(('Expected default value of type(s) %(extypes)s but got ' - '%(default)r of type %(deftypes)s'), - {'extypes': expected_types, - 'default': self.default, - 'deftypes': type(self.default).__name__}) - - def __ne__(self, another): - return vars(self) != vars(another) - - def __eq__(self, another): - return vars(self) == vars(another) - - __hash__ = object.__hash__ - - def _get_from_namespace(self, namespace, group_name): - """Retrieves the option value from a _Namespace object. - - :param namespace: a _Namespace object - :param group_name: a group name - """ - names = [(group_name, self.dest)] - - for opt in self.deprecated_opts: - dname, dgroup = opt.name, opt.group - if dname or dgroup: - names.append((dgroup if dgroup else group_name, - dname if dname else self.dest)) - - return namespace._get_value(names, self.multi, self.positional) - - def _add_to_cli(self, parser, group=None): - """Makes the option available in the command line interface. - - This is the method ConfigOpts uses to add the opt to the CLI interface - as appropriate for the opt type. Some opt types may extend this method, - others may just extend the helper methods it uses. - - :param parser: the CLI option parser - :param group: an optional OptGroup object - """ - container = self._get_argparse_container(parser, group) - kwargs = self._get_argparse_kwargs(group) - prefix = self._get_argparse_prefix('', group.name if group else None) - deprecated_names = [] - for opt in self.deprecated_opts: - deprecated_name = self._get_deprecated_cli_name(opt.name, - opt.group) - if deprecated_name is not None: - deprecated_names.append(deprecated_name) - self._add_to_argparse(parser, container, self.name, self.short, - kwargs, prefix, - self.positional, deprecated_names) - - def _add_to_argparse(self, parser, container, name, short, kwargs, - prefix='', positional=False, deprecated_names=None): - """Add an option to an argparse parser or group. - - :param container: an argparse._ArgumentGroup object - :param name: the opt name - :param short: the short opt name - :param kwargs: the keyword arguments for add_argument() - :param prefix: an optional prefix to prepend to the opt name - :param positional: whether the option is a positional CLI argument - """ - def hyphen(arg): - return arg if not positional else '' - - args = [hyphen('--') + prefix + name] - if short: - args.append(hyphen('-') + short) - for deprecated_name in deprecated_names: - args.append(hyphen('--') + deprecated_name) - - parser.add_parser_argument(container, *args, **kwargs) - - def _get_argparse_container(self, parser, group): - """Returns an argparse._ArgumentGroup. - - :param parser: an argparse.ArgumentParser - :param group: an (optional) OptGroup object - :returns: an argparse._ArgumentGroup if group is given, else parser - """ - if group is not None: - return group._get_argparse_group(parser) - else: - return parser - - def _get_argparse_kwargs(self, group, **kwargs): - """Build a dict of keyword arguments for argparse's add_argument(). - - Most opt types extend this method to customize the behaviour of the - options added to argparse. - - :param group: an optional group - :param kwargs: optional keyword arguments to add to - :returns: a dict of keyword arguments - """ - if not self.positional: - dest = self.dest - if group is not None: - dest = group.name + '_' + dest - kwargs['dest'] = dest - else: - kwargs['nargs'] = '?' - kwargs.update({'default': None, - 'metavar': self.metavar, - 'help': self.help, }) - return kwargs - - def _get_argparse_prefix(self, prefix, group_name): - """Build a prefix for the CLI option name, if required. - - CLI options in a group are prefixed with the group's name in order - to avoid conflicts between similarly named options in different - groups. - - :param prefix: an existing prefix to append to (for example 'no' or '') - :param group_name: an optional group name - :returns: a CLI option prefix including the group name, if appropriate - """ - if group_name is not None: - return group_name + '-' + prefix - else: - return prefix - - def _get_deprecated_cli_name(self, dname, dgroup, prefix=''): - """Build a CLi arg name for deprecated options. - - Either a deprecated name or a deprecated group or both or - neither can be supplied: - - dname, dgroup -> dgroup + '-' + dname - dname -> dname - dgroup -> dgroup + '-' + self.name - neither -> None - - :param dname: a deprecated name, which can be None - :param dgroup: a deprecated group, which can be None - :param prefix: an prefix to append to (for example 'no' or '') - :returns: a CLI argument name - """ - if dgroup == 'DEFAULT': - dgroup = None - - if dname is None and dgroup is None: - return None - - if dname is None: - dname = self.name - - return self._get_argparse_prefix(prefix, dgroup) + dname - - def __lt__(self, another): - return hash(self) < hash(another) - -# NOTE(jd) Not available for py2.6 -if six.PY3: - Opt = functools.total_ordering(Opt) - - -class DeprecatedOpt(object): - - """Represents a Deprecated option. - - Here's how you can use it:: - - oldopts = [cfg.DeprecatedOpt('oldfoo', group='oldgroup'), - cfg.DeprecatedOpt('oldfoo2', group='oldgroup2')] - cfg.CONF.register_group(cfg.OptGroup('blaa')) - cfg.CONF.register_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), - group='blaa') - - Multi-value options will return all new and deprecated - options. For single options, if the new option is present - ("[blaa]/foo" above) it will override any deprecated options - present. If the new option is not present and multiple - deprecated options are present, the option corresponding to - the first element of deprecated_opts will be chosen. - - If group is None, the DeprecatedOpt lookup will happen within the same - group the new option is in. For example:: - - oldopts = [cfg.DeprecatedOpt('oldfoo'), - cfg.DeprecatedOpt('oldfoo2', group='DEFAULT')] - - cfg.CONF.register_group(cfg.OptGroup('blaa')) - cfg.CONF.register_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), - group='blaa') - - In the example above, `oldfoo` will be looked up in the `blaa` group and - `oldfoo2` in the `DEFAULT` group. - """ - - def __init__(self, name, group=None): - """Constructs an DeprecatedOpt object. - - :param name: the name of the option - :param group: the group of the option - """ - self.name = name - self.group = group - - def __key(self): - return (self.name, self.group) - - def __eq__(self, other): - return self.__key() == other.__key() - - def __hash__(self): - return hash(self.__key()) - - -class StrOpt(Opt): - """Option with String type (for backward compatibility). - - :param choices: Optional sequence of valid values. - """ - - def __init__(self, name, choices=None, **kwargs): - super(StrOpt, self).__init__(name, - type=types.String(choices=choices), - **kwargs) - - -class BoolOpt(Opt): - - """Boolean options. - - Bool opts are set to True or False on the command line using --optname or - --noopttname respectively. - - In config files, boolean values are cast with Boolean type. - """ - - def __init__(self, name, **kwargs): - if 'positional' in kwargs: - raise ValueError('positional boolean args not supported') - super(BoolOpt, self).__init__(name, type=types.Boolean(), **kwargs) - - def _add_to_cli(self, parser, group=None): - """Extends the base class method to add the --nooptname option.""" - super(BoolOpt, self)._add_to_cli(parser, group) - self._add_inverse_to_argparse(parser, group) - - def _add_inverse_to_argparse(self, parser, group): - """Add the --nooptname option to the option parser.""" - container = self._get_argparse_container(parser, group) - kwargs = self._get_argparse_kwargs(group, action='store_false') - prefix = self._get_argparse_prefix('no', group.name if group else None) - deprecated_names = [] - for opt in self.deprecated_opts: - deprecated_name = self._get_deprecated_cli_name(opt.name, - opt.group, - prefix='no') - if deprecated_name is not None: - deprecated_names.append(deprecated_name) - kwargs["help"] = "The inverse of --" + self.name - self._add_to_argparse(parser, container, self.name, None, kwargs, - prefix, self.positional, deprecated_names) - - def _get_argparse_kwargs(self, group, action='store_true', **kwargs): - """Extends the base argparse keyword dict for boolean options.""" - - kwargs = super(BoolOpt, self)._get_argparse_kwargs(group, **kwargs) - # type has no effect for BoolOpt, it only matters for - # values that came from config files - if 'type' in kwargs: - del kwargs['type'] - - # metavar has no effect for BoolOpt - if 'metavar' in kwargs: - del kwargs['metavar'] - - kwargs['action'] = action - - return kwargs - - -class IntOpt(Opt): - - """Opt with Integer type (for backward compatibility).""" - - def __init__(self, name, **kwargs): - super(IntOpt, self).__init__(name, type=types.Integer(), **kwargs) - - -class FloatOpt(Opt): - - """Opt with Float type (for backward compatibility).""" - - def __init__(self, name, **kwargs): - super(FloatOpt, self).__init__(name, type=types.Float(), **kwargs) - - -class ListOpt(Opt): - - """Opt with List(String) type (for backward compatibility).""" - - def __init__(self, name, **kwargs): - super(ListOpt, self).__init__(name, type=types.List(), **kwargs) - - -class DictOpt(Opt): - - """Opt with Dict(String) type (for backward compatibility).""" - - def __init__(self, name, **kwargs): - super(DictOpt, self).__init__(name, type=types.Dict(), **kwargs) - - -class IPOpt(Opt): - - """Opt with IPAddress type (either IPv4, IPv6 or both).""" - - def __init__(self, name, version=None, **kwargs): - super(IPOpt, self).__init__(name, type=types.IPAddress(version), - **kwargs) - - -class MultiOpt(Opt): - - """Multi-value option. - - Multi opt values are typed opts which may be specified multiple times. - The opt value is a list containing all the values specified. - """ - multi = True - - def __init__(self, name, item_type, **kwargs): - super(MultiOpt, self).__init__(name, item_type, **kwargs) - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for multi value options.""" - kwargs = super(MultiOpt, self)._get_argparse_kwargs(group) - if not self.positional: - kwargs['action'] = 'append' - else: - kwargs['nargs'] = '*' - return kwargs - - -class MultiStrOpt(MultiOpt): - - """Multi opt with MultiString item type (for backward compatibility).""" - - def __init__(self, name, **kwargs): - super(MultiStrOpt, self).__init__(name, - item_type=types.MultiString(), - **kwargs) - - -class SubCommandOpt(Opt): - - """Sub-command options. - - Sub-command options allow argparse sub-parsers to be used to parse - additional command line arguments. - - The handler argument to the SubCommandOpt constructor is a callable - which is supplied an argparse subparsers object. Use this handler - callable to add sub-parsers. - - The opt value is SubCommandAttr object with the name of the chosen - sub-parser stored in the 'name' attribute and the values of other - sub-parser arguments available as additional attributes. - """ - - def __init__(self, name, dest=None, handler=None, - title=None, description=None, help=None): - """Construct an sub-command parsing option. - - This behaves similarly to other Opt sub-classes but adds a - 'handler' argument. The handler is a callable which is supplied - an subparsers object when invoked. The add_parser() method on - this subparsers object can be used to register parsers for - sub-commands. - - :param name: the option's name - :param dest: the name of the corresponding ConfigOpts property - :param title: title of the sub-commands group in help output - :param description: description of the group in help output - :param help: a help string giving an overview of available sub-commands - """ - super(SubCommandOpt, self).__init__(name, type=types.String(), - dest=dest, help=help) - self.handler = handler - self.title = title - self.description = description - - def _add_to_cli(self, parser, group=None): - """Add argparse sub-parsers and invoke the handler method.""" - dest = self.dest - if group is not None: - dest = group.name + '_' + dest - - subparsers = parser.add_subparsers(dest=dest, - title=self.title, - description=self.description, - help=self.help) - # NOTE(jd) Set explicitly to True for Python 3 - # See http://bugs.python.org/issue9253 for context - subparsers.required = True - - if self.handler is not None: - self.handler(subparsers) - - -class _ConfigFileOpt(Opt): - - """The --config-file option. - - This is an private option type which handles the special processing - required for --config-file options. - - As each --config-file option is encountered on the command line, we - parse the file and store the parsed values in the _Namespace object. - This allows us to properly handle the precedence of --config-file - options over previous command line arguments, but not over subsequent - arguments. - """ - - class ConfigFileAction(argparse.Action): - - """An argparse action for --config-file. - - As each --config-file option is encountered, this action adds the - value to the config_file attribute on the _Namespace object but also - parses the configuration file and stores the values found also in - the _Namespace object. - """ - - def __call__(self, parser, namespace, values, option_string=None): - """Handle a --config-file command line argument. - - :raises: ConfigFileParseError, ConfigFileValueError - """ - if getattr(namespace, self.dest, None) is None: - setattr(namespace, self.dest, []) - items = getattr(namespace, self.dest) - items.append(values) - - ConfigParser._parse_file(values, namespace) - - def __init__(self, name, **kwargs): - super(_ConfigFileOpt, self).__init__(name, lambda x: x, **kwargs) - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for the config file opt.""" - kwargs = super(_ConfigFileOpt, self)._get_argparse_kwargs(group) - kwargs['action'] = self.ConfigFileAction - return kwargs - - -class _ConfigDirOpt(Opt): - - """The --config-dir option. - - This is an private option type which handles the special processing - required for --config-dir options. - - As each --config-dir option is encountered on the command line, we - parse the files in that directory and store the parsed values in the - _Namespace object. This allows us to properly handle the precedence of - --config-dir options over previous command line arguments, but not - over subsequent arguments. - """ - - class ConfigDirAction(argparse.Action): - - """An argparse action for --config-dir. - - As each --config-dir option is encountered, this action sets the - config_dir attribute on the _Namespace object but also parses the - configuration files and stores the values found also in the - _Namespace object. - """ - - def __call__(self, parser, namespace, values, option_string=None): - """Handle a --config-dir command line argument. - - :raises: ConfigFileParseError, ConfigFileValueError, - ConfigDirNotFoundError - """ - setattr(namespace, self.dest, values) - - values = os.path.expanduser(values) - - if not os.path.exists(values): - raise ConfigDirNotFoundError(values) - - config_dir_glob = os.path.join(values, '*.conf') - - for config_file in sorted(glob.glob(config_dir_glob)): - ConfigParser._parse_file(config_file, namespace) - - def __init__(self, name, **kwargs): - super(_ConfigDirOpt, self).__init__(name, type=types.String(), - **kwargs) - - def _get_argparse_kwargs(self, group, **kwargs): - """Extends the base argparse keyword dict for the config dir option.""" - kwargs = super(_ConfigDirOpt, self)._get_argparse_kwargs(group) - kwargs['action'] = self.ConfigDirAction - return kwargs - - -class OptGroup(object): - - """Represents a group of opts. - - CLI opts in the group are automatically prefixed with the group name. - - Each group corresponds to a section in config files. - - An OptGroup object has no public methods, but has a number of public string - properties: - - name: - the name of the group - title: - the group title as displayed in --help - help: - the group description as displayed in --help - """ - - def __init__(self, name, title=None, help=None): - """Constructs an OptGroup object. - - :param name: the group name - :param title: the group title for --help - :param help: the group description for --help - """ - self.name = name - self.title = "%s options" % name if title is None else title - self.help = help - - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._argparse_group = None - - def _register_opt(self, opt, cli=False): - """Add an opt to this group. - - :param opt: an Opt object - :param cli: whether this is a CLI option - :returns: False if previously registered, True otherwise - :raises: DuplicateOptError if a naming conflict is detected - """ - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt, 'cli': cli} - - return True - - def _unregister_opt(self, opt): - """Remove an opt from this group. - - :param opt: an Opt object - """ - if opt.dest in self._opts: - del self._opts[opt.dest] - - def _get_argparse_group(self, parser): - if self._argparse_group is None: - """Build an argparse._ArgumentGroup for this group.""" - self._argparse_group = parser.add_argument_group(self.title, - self.help) - return self._argparse_group - - def _clear(self): - """Clear this group's option parsing state.""" - self._argparse_group = None - - -class ParseError(iniparser.ParseError): - def __init__(self, msg, lineno, line, filename): - super(ParseError, self).__init__(msg, lineno, line) - self.filename = filename - - def __str__(self): - return 'at %s:%d, %s: %r' % (self.filename, self.lineno, - self.msg, self.line) - - -class ConfigParser(iniparser.BaseParser): - def __init__(self, filename, sections): - super(ConfigParser, self).__init__() - self.filename = filename - self.sections = sections - self._normalized = None - self.section = None - - def _add_normalized(self, normalized): - self._normalized = normalized - - def parse(self): - with open(self.filename) as f: - return super(ConfigParser, self).parse(f) - - def new_section(self, section): - self.section = section - self.sections.setdefault(self.section, {}) - - if self._normalized is not None: - self._normalized.setdefault(_normalize_group_name(self.section), - {}) - - def assignment(self, key, value): - if not self.section: - raise self.error_no_section() - - value = '\n'.join(value) - - def append(sections, section): - sections[section].setdefault(key, []) - sections[section][key].append(value) - - append(self.sections, self.section) - if self._normalized is not None: - append(self._normalized, _normalize_group_name(self.section)) - - def parse_exc(self, msg, lineno, line=None): - return ParseError(msg, lineno, line, self.filename) - - def error_no_section(self): - return self.parse_exc('Section must be started before assignment', - self.lineno) - - @classmethod - def _parse_file(cls, config_file, namespace): - """Parse a config file and store any values in the namespace. - - :raises: ConfigFileParseError, ConfigFileValueError - """ - config_file = _fixpath(config_file) - - sections = {} - normalized = {} - parser = cls(config_file, sections) - parser._add_normalized(normalized) - - try: - parser.parse() - except iniparser.ParseError as pe: - raise ConfigFileParseError(pe.filename, str(pe)) - except IOError as err: - if err.errno == errno.ENOENT: - namespace._file_not_found(config_file) - return - if err.errno == errno.EACCES: - namespace._file_permission_denied(config_file) - return - raise - - namespace._add_parsed_config_file(sections, normalized) - - -class MultiConfigParser(object): - def __init__(self): - self.parsed = [] - self._normalized = [] - - def read(self, config_files): - read_ok = [] - - for filename in config_files: - sections = {} - normalized = {} - parser = ConfigParser(filename, sections) - parser._add_normalized(normalized) - - try: - parser.parse() - except IOError: - continue - self._add_parsed_config_file(sections, normalized) - read_ok.append(filename) - - return read_ok - - def _add_parsed_config_file(self, sections, normalized): - """Add a parsed config file to the list of parsed files. - - :param sections: a mapping of section name to dicts of config values - :param normalized: sections mapping with section names normalized - :raises: ConfigFileValueError - """ - self.parsed.insert(0, sections) - self._normalized.insert(0, normalized) - - def get(self, names, multi=False): - return self._get(names, multi=multi) - - def _get(self, names, multi=False, normalized=False): - """Fetch a config file value from the parsed files. - - :param names: a list of (section, name) tuples - :param multi: a boolean indicating whether to return multiple values - :param normalized: whether to normalize group names to lowercase - """ - rvalue = [] - - def normalize(name): - return _normalize_group_name(name) if normalized else name - - names = [(normalize(section), name) for section, name in names] - - for sections in (self._normalized if normalized else self.parsed): - for section, name in names: - if section not in sections: - continue - if name in sections[section]: - val = sections[section][name] - if multi: - rvalue = val + rvalue - else: - return val - if multi and rvalue != []: - return rvalue - raise KeyError - - -class _Namespace(argparse.Namespace): - - """An argparse namespace which also stores config file values. - - As we parse command line arguments, the values get set as attributes - on a namespace object. However, we also want to parse config files as - they are specified on the command line and collect the values alongside - the option values parsed from the command line. - - Note, we don't actually assign values from config files as attributes - on the namespace because config file options be registered after the - command line has been parsed, so we may not know how to properly parse - or convert a config file value at this point. - """ - - def __init__(self, conf): - self._conf = conf - self._parser = MultiConfigParser() - self._files_not_found = [] - self._files_permission_denied = [] - - def _parse_cli_opts_from_config_file(self, sections, normalized): - """Parse CLI options from a config file. - - CLI options are special - we require they be registered before the - command line is parsed. This means that as we parse config files, we - can go ahead and apply the appropriate option-type specific conversion - to the values in config files for CLI options. We can't do this for - non-CLI options, because the schema describing those options may not be - registered until after the config files are parsed. - - This method relies on that invariant in order to enforce proper - priority of option values - i.e. that the order in which an option - value is parsed, whether the value comes from the CLI or a config file, - determines which value specified for a given option wins. - - The way we implement this ordering is that as we parse each config - file, we look for values in that config file for CLI options only. Any - values for CLI options found in the config file are treated like they - had appeared on the command line and set as attributes on the namespace - objects. Values in later config files or on the command line will - override values found in this file. - """ - namespace = _Namespace(self._conf) - namespace._parser._add_parsed_config_file(sections, normalized) - - for opt, group in sorted(self._conf._all_cli_opts()): - group_name = group.name if group is not None else None - try: - value = opt._get_from_namespace(namespace, group_name) - except KeyError: - continue - except ValueError as ve: - raise ConfigFileValueError(str(ve)) - - if group_name is None: - dest = opt.dest - else: - dest = group_name + '_' + opt.dest - - if opt.multi: - if getattr(self, dest, None) is None: - setattr(self, dest, []) - values = getattr(self, dest) - values.extend(value) - else: - setattr(self, dest, value) - - def _add_parsed_config_file(self, sections, normalized): - """Add a parsed config file to the list of parsed files. - - :param sections: a mapping of section name to dicts of config values - :param normalized: sections mapping with section names normalized - :raises: ConfigFileValueError - """ - self._parse_cli_opts_from_config_file(sections, normalized) - self._parser._add_parsed_config_file(sections, normalized) - - def _file_not_found(self, config_file): - """Record that we were unable to open a config file. - - :param config_file: the path to the failed file - """ - self._files_not_found.append(config_file) - - def _file_permission_denied(self, config_file): - """Record that we have no permission to open a config file. - - :param config_file: the path to the failed file - """ - self._files_permission_denied.append(config_file) - - def _get_cli_value(self, names, positional): - """Fetch a CLI option value. - - Look up the value of a CLI option. The value itself may have come from - parsing the command line or parsing config files specified on the - command line. Type conversion have already been performed for CLI - options at this point. - - :param names: a list of (section, name) tuples - :param positional: whether this is a positional option - """ - for group_name, name in names: - name = name if group_name is None else group_name + '_' + name - value = getattr(self, name, None) - if value is not None: - # argparse ignores default=None for nargs='*' and returns [] - if positional and not value: - continue - - return value - - raise KeyError - - def _get_value(self, names, multi, positional): - """Fetch a value from config files. - - Multiple names for a given configuration option may be supplied so - that we can transparently handle files containing deprecated option - names or groups. - - :param names: a list of (section, name) tuples - :param multi: a boolean indicating whether to return multiple values - :param positional: whether this is a positional option - """ - try: - return self._get_cli_value(names, positional) - except KeyError: - pass - - names = [(g if g is not None else 'DEFAULT', n) for g, n in names] - values = self._parser._get(names, multi=multi, normalized=True) - return values if multi else values[-1] - - -class _CachedArgumentParser(argparse.ArgumentParser): - - """class for caching/collecting command line arguments. - - It also sorts the arguments before initializing the ArgumentParser. - We need to do this since ArgumentParser by default does not sort - the argument options and the only way to influence the order of - arguments in '--help' is to ensure they are added in the sorted - order. - """ - - def __init__(self, prog=None, usage=None, **kwargs): - super(_CachedArgumentParser, self).__init__(prog, usage, **kwargs) - self._args_cache = {} - - def add_parser_argument(self, container, *args, **kwargs): - values = [] - if container in self._args_cache: - values = self._args_cache[container] - values.append({'args': args, 'kwargs': kwargs}) - self._args_cache[container] = values - - def initialize_parser_arguments(self): - # NOTE(mfedosin): The code below looks a little bit weird, but - # it's done because we need to sort only optional opts and do - # not touch positional. For the reason optional opts go first in - # the values we only need to find an index of the first positional - # option and then sort the values slice. - for container, values in six.iteritems(self._args_cache): - index = 0 - for index, argument in enumerate(values): - if not argument['args'][0].startswith('-'): - break - values[:index] = sorted(values[:index], key=lambda x: x['args']) - for argument in values: - try: - container.add_argument(*argument['args'], - **argument['kwargs']) - except argparse.ArgumentError as e: - raise DuplicateOptError(e) - self._args_cache = {} - - def parse_args(self, args=None, namespace=None): - self.initialize_parser_arguments() - return super(_CachedArgumentParser, self).parse_args(args, namespace) - - def print_help(self, file=None): - self.initialize_parser_arguments() - super(_CachedArgumentParser, self).print_help(file) - - def print_usage(self, file=None): - self.initialize_parser_arguments() - super(_CachedArgumentParser, self).print_usage(file) - - -class ConfigOpts(collections.Mapping): - - """Config options which may be set on the command line or in config files. - - ConfigOpts is a configuration option manager with APIs for registering - option schemas, grouping options, parsing option values and retrieving - the values of options. - """ - - def __init__(self): - """Construct a ConfigOpts object.""" - self._opts = {} # dict of dicts of (opt:, override:, default:) - self._groups = {} - - self._args = None - - self._oparser = None - self._namespace = None - self.__cache = {} - self._config_opts = [] - self._cli_opts = collections.deque() - self._validate_default_values = False - - def _pre_setup(self, project, prog, version, usage, default_config_files): - """Initialize a ConfigCliParser object for option parsing.""" - - if prog is None: - prog = os.path.basename(sys.argv[0]) - - if default_config_files is None: - default_config_files = find_config_files(project, prog) - - self._oparser = _CachedArgumentParser(prog=prog, usage=usage) - self._oparser.add_parser_argument(self._oparser, - '--version', - action='version', - version=version) - - return prog, default_config_files - - def _setup(self, project, prog, version, usage, default_config_files): - """Initialize a ConfigOpts object for option parsing.""" - - self._config_opts = [ - _ConfigFileOpt('config-file', - default=default_config_files, - metavar='PATH', - help=('Path to a config file to use. Multiple ' - 'config files can be specified, with values ' - 'in later files taking precedence. The ' - 'default files used are: %(default)s.')), - _ConfigDirOpt('config-dir', - metavar='DIR', - help='Path to a config directory to pull *.conf ' - 'files from. This file set is sorted, so as to ' - 'provide a predictable parse order if ' - 'individual options are over-ridden. The set ' - 'is parsed after the file(s) specified via ' - 'previous --config-file, arguments hence ' - 'over-ridden options in the directory take ' - 'precedence.'), - ] - self.register_cli_opts(self._config_opts) - - self.project = project - self.prog = prog - self.version = version - self.usage = usage - self.default_config_files = default_config_files - - def __clear_cache(f): - @functools.wraps(f) - def __inner(self, *args, **kwargs): - if kwargs.pop('clear_cache', True): - result = f(self, *args, **kwargs) - self.__cache.clear() - return result - else: - return f(self, *args, **kwargs) - - return __inner - - def __call__(self, - args=None, - project=None, - prog=None, - version=None, - usage=None, - default_config_files=None, - validate_default_values=False): - """Parse command line arguments and config files. - - Calling a ConfigOpts object causes the supplied command line arguments - and config files to be parsed, causing opt values to be made available - as attributes of the object. - - The object may be called multiple times, each time causing the previous - set of values to be overwritten. - - Automatically registers the --config-file option with either a supplied - list of default config files, or a list from find_config_files(). - - If the --config-dir option is set, any *.conf files from this - directory are pulled in, after all the file(s) specified by the - --config-file option. - - :param args: command line arguments (defaults to sys.argv[1:]) - :param project: the toplevel project name, used to locate config files - :param prog: the name of the program (defaults to sys.argv[0] basename) - :param version: the program version (for --version) - :param usage: a usage string (%prog will be expanded) - :param default_config_files: config files to use by default - :param validate_default_values: whether to validate the default values - :returns: the list of arguments left over after parsing options - :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, - ConfigFilesPermissionDeniedError, - RequiredOptError, DuplicateOptError - """ - self.clear() - - self._validate_default_values = validate_default_values - - prog, default_config_files = self._pre_setup(project, - prog, - version, - usage, - default_config_files) - - self._setup(project, prog, version, usage, default_config_files) - - self._namespace = self._parse_cli_opts(args if args is not None - else sys.argv[1:]) - if self._namespace._files_not_found: - raise ConfigFilesNotFoundError(self._namespace._files_not_found) - if self._namespace._files_permission_denied: - raise ConfigFilesPermissionDeniedError( - self._namespace._files_permission_denied) - - self._check_required_opts() - - def __getattr__(self, name): - """Look up an option value and perform string substitution. - - :param name: the opt name (or 'dest', more precisely) - :returns: the option value (after string substitution) or a GroupAttr - :raises: NoSuchOptError - """ - try: - return self._get(name) - except Exception: - raise NoSuchOptError(name) - - def __getitem__(self, key): - """Look up an option value and perform string substitution.""" - return self.__getattr__(key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self._opts or key in self._groups - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in itertools.chain(self._opts.keys(), self._groups.keys()): - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self._opts) + len(self._groups) - - def reset(self): - """Clear the object state and unset overrides and defaults.""" - self._unset_defaults_and_overrides() - self.clear() - - @__clear_cache - def clear(self): - """Clear the state of the object to before it was called. - - Any subparsers added using the add_cli_subparsers() will also be - removed as a side-effect of this method. - """ - self._args = None - self._oparser = None - self._namespace = None - self._validate_default_values = False - self.unregister_opts(self._config_opts) - for group in self._groups.values(): - group._clear() - - def _add_cli_opt(self, opt, group): - if {'opt': opt, 'group': group} in self._cli_opts: - return - if opt.positional: - self._cli_opts.append({'opt': opt, 'group': group}) - else: - self._cli_opts.appendleft({'opt': opt, 'group': group}) - - @__clear_cache - def register_opt(self, opt, group=None, cli=False): - """Register an option schema. - - Registering an option schema makes any option value which is previously - or subsequently parsed from the command line or config files available - as an attribute of this object. - - :param opt: an instance of an Opt sub-class - :param cli: whether this is a CLI option - :param group: an optional OptGroup object or group name - :return: False if the opt was already registered, True otherwise - :raises: DuplicateOptError - """ - if group is not None: - group = self._get_group(group, autocreate=True) - if cli: - self._add_cli_opt(opt, group) - return group._register_opt(opt, cli) - - if cli: - self._add_cli_opt(opt, None) - - if _is_opt_registered(self._opts, opt): - return False - - self._opts[opt.dest] = {'opt': opt, 'cli': cli} - - return True - - @__clear_cache - def register_opts(self, opts, group=None): - """Register multiple option schemas at once.""" - for opt in opts: - self.register_opt(opt, group, clear_cache=False) - - @__clear_cache - def register_cli_opt(self, opt, group=None): - """Register a CLI option schema. - - CLI option schemas must be registered before the command line and - config files are parsed. This is to ensure that all CLI options are - shown in --help and option validation works as expected. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already registered, True otherwise - :raises: DuplicateOptError, ArgsAlreadyParsedError - """ - if self._args is not None: - raise ArgsAlreadyParsedError("cannot register CLI option") - - return self.register_opt(opt, group, cli=True, clear_cache=False) - - @__clear_cache - def register_cli_opts(self, opts, group=None): - """Register multiple CLI option schemas at once.""" - for opt in opts: - self.register_cli_opt(opt, group, clear_cache=False) - - def register_group(self, group): - """Register an option group. - - An option group must be registered before options can be registered - with the group. - - :param group: an OptGroup object - """ - if group.name in self._groups: - return - - self._groups[group.name] = copy.copy(group) - - @__clear_cache - def unregister_opt(self, opt, group=None): - """Unregister an option. - - :param opt: an Opt object - :param group: an optional OptGroup object or group name - :raises: ArgsAlreadyParsedError, NoSuchGroupError - """ - if self._args is not None: - raise ArgsAlreadyParsedError("reset before unregistering options") - - if {'opt': opt, 'group': group} in self._cli_opts: - self._cli_opts.remove({'opt': opt, 'group': group}) - - if group is not None: - self._get_group(group)._unregister_opt(opt) - elif opt.dest in self._opts: - del self._opts[opt.dest] - - @__clear_cache - def unregister_opts(self, opts, group=None): - """Unregister multiple CLI option schemas at once.""" - for opt in opts: - self.unregister_opt(opt, group, clear_cache=False) - - def import_opt(self, name, module_str, group=None): - """Import an option definition from a module. - - Import a module and check that a given option is registered. - - This is intended for use with global configuration objects - like cfg.CONF where modules commonly register options with - CONF at module load time. If one module requires an option - defined by another module it can use this method to explicitly - declare the dependency. - - :param name: the name/dest of the opt - :param module_str: the name of a module to import - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - __import__(module_str) - self._get_opt_info(name, group) - - def import_group(self, group, module_str): - """Import an option group from a module. - - Import a module and check that a given option group is registered. - - This is intended for use with global configuration objects - like cfg.CONF where modules commonly register options with - CONF at module load time. If one module requires an option group - defined by another module it can use this method to explicitly - declare the dependency. - - :param group: an option OptGroup object or group name - :param module_str: the name of a module to import - :raises: ImportError, NoSuchGroupError - """ - __import__(module_str) - self._get_group(group) - - @__clear_cache - def set_override(self, name, override, group=None): - """Override an opt value. - - Override the command line, config file and default values of a - given option. - - :param name: the name/dest of the opt - :param override: the override value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['override'] = override - - @__clear_cache - def set_default(self, name, default, group=None): - """Override an opt's default value. - - Override the default value of given option. A command line or - config file value will still take precedence over this default. - - :param name: the name/dest of the opt - :param default: the default value - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info['default'] = default - - @__clear_cache - def clear_override(self, name, group=None): - """Clear an override an opt value. - - Clear a previously set override of the command line, config file - and default values of a given option. - - :param name: the name/dest of the opt - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info.pop('override', None) - - @__clear_cache - def clear_default(self, name, group=None): - """Clear an override an opt's default value. - - Clear a previously set override of the default value of given option. - - :param name: the name/dest of the opt - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - opt_info = self._get_opt_info(name, group) - opt_info.pop('default', None) - - def _all_opt_infos(self): - """A generator function for iteration opt infos.""" - for info in self._opts.values(): - yield info, None - for group in self._groups.values(): - for info in group._opts.values(): - yield info, group - - def _all_cli_opts(self): - """A generator function for iterating CLI opts.""" - for item in self._cli_opts: - yield item['opt'], item['group'] - - def _unset_defaults_and_overrides(self): - """Unset any default or override on all options.""" - for info, group in self._all_opt_infos(): - info.pop('default', None) - info.pop('override', None) - - def find_file(self, name): - """Locate a file located alongside the config files. - - Search for a file with the supplied basename in the directories - which we have already loaded config files from and other known - configuration directories. - - The directory, if any, supplied by the config_dir option is - searched first. Then the config_file option is iterated over - and each of the base directories of the config_files values - are searched. Failing both of these, the standard directories - searched by the module level find_config_files() function is - used. The first matching file is returned. - - :param name: the filename, for example 'policy.json' - :returns: the path to a matching file, or None - """ - dirs = [] - if self.config_dir: - dirs.append(_fixpath(self.config_dir)) - - for cf in reversed(self.config_file): - dirs.append(os.path.dirname(_fixpath(cf))) - - dirs.extend(_get_config_dirs(self.project)) - - return _search_dirs(dirs, name) - - def log_opt_values(self, logger, lvl): - """Log the value of all registered opts. - - It's often useful for an app to log its configuration to a log file at - startup for debugging. This method dumps to the entire config state to - the supplied logger at a given log level. - - :param logger: a logging.Logger object - :param lvl: the log level (for example logging.DEBUG) arg to - logger.log() - """ - logger.log(lvl, "*" * 80) - logger.log(lvl, "Configuration options gathered from:") - logger.log(lvl, "command line args: %s", self._args) - logger.log(lvl, "config files: %s", self.config_file) - logger.log(lvl, "=" * 80) - - def _sanitize(opt, value): - """Obfuscate values of options declared secret.""" - return value if not opt.secret else '*' * 4 - - for opt_name in sorted(self._opts): - opt = self._get_opt_info(opt_name)['opt'] - logger.log(lvl, "%-30s = %s", opt_name, - _sanitize(opt, getattr(self, opt_name))) - - for group_name in self._groups: - group_attr = self.GroupAttr(self, self._get_group(group_name)) - for opt_name in sorted(self._groups[group_name]._opts): - opt = self._get_opt_info(opt_name, group_name)['opt'] - logger.log(lvl, "%-30s = %s", - "%s.%s" % (group_name, opt_name), - _sanitize(opt, getattr(group_attr, opt_name))) - - logger.log(lvl, "*" * 80) - - def print_usage(self, file=None): - """Print the usage message for the current program. - - This method is for use after all CLI options are known - registered using __call__() method. If this method is called - before the __call__() is invoked, it throws NotInitializedError - - :param file: the File object (if None, output is on sys.stdout) - :raises: NotInitializedError - """ - if not self._oparser: - raise NotInitializedError() - self._oparser.print_usage(file) - - def print_help(self, file=None): - """Print the help message for the current program. - - This method is for use after all CLI options are known - registered using __call__() method. If this method is called - before the __call__() is invoked, it throws NotInitializedError - - :param file: the File object (if None, output is on sys.stdout) - :raises: NotInitializedError - """ - if not self._oparser: - raise NotInitializedError() - self._oparser.print_help(file) - - def _get(self, name, group=None, namespace=None): - if isinstance(group, OptGroup): - key = (group.name, name) - else: - key = (group, name) - try: - if namespace is not None: - raise KeyError - - return self.__cache[key] - except KeyError: - value = self._do_get(name, group, namespace) - self.__cache[key] = value - return value - - def _do_get(self, name, group=None, namespace=None): - """Look up an option value. - - :param name: the opt name (or 'dest', more precisely) - :param group: an OptGroup - :param namespace: the namespace object that retrieves the option - value from - :returns: the option value, or a GroupAttr object - :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, - TemplateSubstitutionError - """ - if group is None and name in self._groups: - return self.GroupAttr(self, self._get_group(name)) - - info = self._get_opt_info(name, group) - opt = info['opt'] - - if isinstance(opt, SubCommandOpt): - return self.SubCommandAttr(self, group, opt.dest) - - if 'override' in info: - return self._substitute(info['override']) - - if namespace is None: - namespace = self._namespace - - def convert(value): - return self._convert_value( - self._substitute(value, group, namespace), opt) - - if namespace is not None: - group_name = group.name if group else None - try: - return convert(opt._get_from_namespace(namespace, group_name)) - except KeyError: - pass - except ValueError as ve: - raise ConfigFileValueError(str(ve)) - - if 'default' in info: - return self._substitute(info['default']) - - if self._validate_default_values: - if opt.default is not None: - try: - convert(opt.default) - except ValueError as e: - raise ConfigFileValueError( - "Default value for option %s is not valid: %s" - % (opt.name, str(e))) - - if opt.default is not None: - return convert(opt.default) - - return None - - def _substitute(self, value, group=None, namespace=None): - """Perform string template substitution. - - Substitute any template variables (for example $foo, ${bar}) in - the supplied string value(s) with opt values. - - :param value: the string value, or list of string values - :param group: the group that retrieves the option value from - :param namespace: the namespace object that retrieves the option - value from - :returns: the substituted string(s) - """ - if isinstance(value, list): - return [self._substitute(i, group=group, namespace=namespace) - for i in value] - elif isinstance(value, str): - # Treat a backslash followed by the dollar sign "\$" - # the same as the string template escape "$$" as it is - # a bit more natural for users - if '\$' in value: - value = value.replace('\$', '$$') - tmpl = string.Template(value) - ret = tmpl.safe_substitute( - self.StrSubWrapper(self, group=group, namespace=namespace)) - return ret - else: - return value - - def _convert_value(self, value, opt): - """Perform value type conversion. - - Converts values using option's type. Handles cases when value is - actually a list of values (for example for multi opts). - - :param value: the string value, or list of string values - :param opt: option definition (instance of Opt class or its subclasses) - :returns: converted value - """ - if opt.multi: - return [opt.type(v) for v in value] - else: - return opt.type(value) - - def _get_group(self, group_or_name, autocreate=False): - """Looks up a OptGroup object. - - Helper function to return an OptGroup given a parameter which can - either be the group's name or an OptGroup object. - - The OptGroup object returned is from the internal dict of OptGroup - objects, which will be a copy of any OptGroup object that users of - the API have access to. - - If autocreate is True, the group will be created if it's not found. If - group is an instance of OptGroup, that same instance will be - registered, otherwise a new instance of OptGroup will be created. - - :param group_or_name: the group's name or the OptGroup object itself - :param autocreate: whether to auto-create the group if it's not found - :raises: NoSuchGroupError - """ - group = group_or_name if isinstance(group_or_name, OptGroup) else None - group_name = group.name if group else group_or_name - - if group_name not in self._groups: - if not autocreate: - raise NoSuchGroupError(group_name) - - self.register_group(group or OptGroup(name=group_name)) - - return self._groups[group_name] - - def _get_opt_info(self, opt_name, group=None): - """Return the (opt, override, default) dict for an opt. - - :param opt_name: an opt name/dest - :param group: an optional group name or OptGroup object - :raises: NoSuchOptError, NoSuchGroupError - """ - if group is None: - opts = self._opts - else: - group = self._get_group(group) - opts = group._opts - - if opt_name not in opts: - raise NoSuchOptError(opt_name, group) - - return opts[opt_name] - - def _check_required_opts(self, namespace=None): - """Check that all opts marked as required have values specified. - - :param namespace: the namespace object be checked the required options - :raises: RequiredOptError - """ - for info, group in self._all_opt_infos(): - opt = info['opt'] - - if opt.required: - if 'default' in info or 'override' in info: - continue - - if self._get(opt.dest, group, namespace) is None: - raise RequiredOptError(opt.name, group) - - def _parse_cli_opts(self, args): - """Parse command line options. - - Initializes the command line option parser and parses the supplied - command line arguments. - - :param args: the command line arguments - :returns: a _Namespace object containing the parsed option values - :raises: SystemExit, DuplicateOptError - ConfigFileParseError, ConfigFileValueError - - """ - self._args = args - for opt, group in self._all_cli_opts(): - opt._add_to_cli(self._oparser, group) - - return self._parse_config_files() - - def _parse_config_files(self): - """Parse configure files options. - - :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, - ConfigFilesPermissionDeniedError, - RequiredOptError, DuplicateOptError - """ - namespace = _Namespace(self) - for arg in self._args: - if arg == '--config-file' or arg.startswith('--config-file='): - break - else: - for config_file in self.default_config_files: - ConfigParser._parse_file(config_file, namespace) - - self._oparser.parse_args(self._args, namespace) - - self._validate_cli_options(namespace) - - return namespace - - def _validate_cli_options(self, namespace): - for opt, group in sorted(self._all_cli_opts(), - key=lambda x: x[0].name): - group_name = group.name if group else None - try: - value = opt._get_from_namespace(namespace, group_name) - except KeyError: - continue - - value = self._substitute(value, group=group, namespace=namespace) - - try: - self._convert_value(value, opt) - except ValueError: - sys.stderr.write("argument --%s: Invalid %s value: %s\n" % ( - opt.dest, repr(opt.type), value)) - raise SystemExit - - @__clear_cache - def reload_config_files(self): - """Reload configure files and parse all options - - :return False if reload configure files failed or else return True - """ - try: - namespace = self._parse_config_files() - if namespace._files_not_found: - raise ConfigFilesNotFoundError(namespace._files_not_found) - if namespace._files_permission_denied: - raise ConfigFilesPermissionDeniedError( - namespace._files_permission_denied) - self._check_required_opts(namespace) - - except SystemExit as exc: - LOG.warn("Caught SystemExit while reloading configure files " - "with exit code: %d", exc.code) - return False - except Error as err: - LOG.warn("Caught Error while reloading configure files: %s", - err) - return False - else: - self._namespace = namespace - return True - - class GroupAttr(collections.Mapping): - - """Helper class. - - Represents the option values of a group as a mapping and attributes. - """ - - def __init__(self, conf, group): - """Construct a GroupAttr object. - - :param conf: a ConfigOpts object - :param group: an OptGroup object - """ - self._conf = conf - self._group = group - - def __getattr__(self, name): - """Look up an option value and perform template substitution.""" - return self._conf._get(name, self._group) - - def __getitem__(self, key): - """Look up an option value and perform string substitution.""" - return self.__getattr__(key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self._group._opts - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in self._group._opts.keys(): - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self._group._opts) - - class SubCommandAttr(object): - - """Helper class. - - Represents the name and arguments of an argparse sub-parser. - """ - - def __init__(self, conf, group, dest): - """Construct a SubCommandAttr object. - - :param conf: a ConfigOpts object - :param group: an OptGroup object - :param dest: the name of the sub-parser - """ - self._conf = conf - self._group = group - self._dest = dest - - def __getattr__(self, name): - """Look up a sub-parser name or argument value.""" - if name == 'name': - name = self._dest - if self._group is not None: - name = self._group.name + '_' + name - return getattr(self._conf._namespace, name) - - if name in self._conf: - raise DuplicateOptError(name) - - try: - return getattr(self._conf._namespace, name) - except AttributeError: - raise NoSuchOptError(name) - - class StrSubWrapper(object): - - """Helper class. - - Exposes opt values as a dict for string substitution. - """ - - def __init__(self, conf, group=None, namespace=None): - """Construct a StrSubWrapper object. - - :param conf: a ConfigOpts object - """ - self.conf = conf - self.namespace = namespace - self.group = group - - def __getitem__(self, key): - """Look up an opt value from the ConfigOpts object. - - :param key: an opt name - :returns: an opt value - :raises: TemplateSubstitutionError if attribute is a group - """ - try: - value = self.conf._get(key, group=self.group, - namespace=self.namespace) - except NoSuchOptError: - value = self.conf._get(key, namespace=self.namespace) - if isinstance(value, self.conf.GroupAttr): - raise TemplateSubstitutionError( - 'substituting group %s not supported' % key) - return value - - -CONF = ConfigOpts() +from oslo_config.cfg import * # noqa diff --git a/oslo/config/cfgfilter.py b/oslo/config/cfgfilter.py index 6efe9a76..4d45ac9b 100644 --- a/oslo/config/cfgfilter.py +++ b/oslo/config/cfgfilter.py @@ -1,5 +1,3 @@ -# Copyright 2014 Red Hat, Inc. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -12,307 +10,4 @@ # License for the specific language governing permissions and limitations # under the License. -r""" -There are two use cases for the ConfigFilter class: - -1. Help enforce that a given module does not access options registered - by another module, without first declaring those cross-module - dependencies using import_opt(). - -2. Prevent private configuration opts from being visible to modules - other than the one which registered it. - -Cross-Module Option Dependencies --------------------------------- - -When using the global cfg.CONF object, it is quite common for a module -to require the existence of configuration options registered by other -modules. - -For example, if module 'foo' registers the 'blaa' option and the module -'bar' uses the 'blaa' option then 'bar' might do:: - - import foo - - print(CONF.blaa) - -However, it's completely non-obvious why foo is being imported (is it -unused, can we remove the import) and where the 'blaa' option comes from. - -The CONF.import_opt() method allows such a dependency to be explicitly -declared:: - - CONF.import_opt('blaa', 'foo') - print(CONF.blaa) - -However, import_opt() has a weakness - if 'bar' imports 'foo' using the -import builtin and doesn't use import_opt() to import 'blaa', then 'blaa' -can still be used without problems. Similarly, where multiple options -are registered a module imported via importopt(), a lazy programmer can -get away with only declaring a dependency on a single option. - -The ConfigFilter class provides a way to ensure that options are not -available unless they have been registered in the module or imported using -import_opt() for example with:: - - CONF = ConfigFilter(cfg.CONF) - CONF.import_opt('blaa', 'foo') - print(CONF.blaa) - -no other options other than 'blaa' are available via CONF. - -Private Configuration Options ------------------------------ - -Libraries which register configuration options typically do not want -users of the library API to access those configuration options. If -API users do access private configuration options, those users will -be disrupted if and when a configuration option is renamed. In other -words, one does not typically wish for the name of the private config -options to be part of the public API. - -The ConfigFilter class provides a way for a library to register -options such that they are not visible via the ConfigOpts instance -which the API user supplies to the library. For example:: - - from __future__ import print_function - - from oslo.config.cfg import * - from oslo.config.cfgfilter import * - - class Widget(object): - - def __init__(self, conf): - self.conf = conf - self._private_conf = ConfigFilter(self.conf) - self._private_conf.register_opt(StrOpt('foo')) - - @property - def foo(self): - return self._private_conf.foo - - conf = ConfigOpts() - widget = Widget(conf) - print(widget.foo) - print(conf.foo) # raises NoSuchOptError - -""" - -import collections -import itertools - -from oslo.config import cfg - - -class ConfigFilter(collections.Mapping): - - """A helper class which wraps a ConfigOpts object. - - ConfigFilter enforces the explicit declaration of dependencies on external - options and allows private options which are not registered with the - wrapped Configopts object. - """ - - def __init__(self, conf): - """Construct a ConfigFilter object. - - :param conf: a ConfigOpts object - """ - self._conf = conf - self._fconf = cfg.ConfigOpts() - self._sync() - - self._imported_opts = set() - self._imported_groups = dict() - - def _sync(self): - if self._fconf._namespace is not self._conf._namespace: - self._fconf.clear() - self._fconf._namespace = self._conf._namespace - self._fconf._args = self._conf._args - - def __getattr__(self, name): - """Look up an option value. - - :param name: the opt name (or 'dest', more precisely) - :returns: the option value (after string subsititution) or a GroupAttr - :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError - """ - if name in self._imported_groups: - return self._imported_groups[name] - elif name in self._imported_opts: - return getattr(self._conf, name) - else: - self._sync() - return getattr(self._fconf, name) - - def __getitem__(self, key): - """Look up an option value.""" - return getattr(self, key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return (key in self._fconf or - key in self._imported_opts or - key in self._imported_groups) - - def __iter__(self): - """Iterate over all registered opt and group names.""" - return itertools.chain(self._fconf.keys(), - self._imported_opts, - self._imported_groups.keys()) - - def __len__(self): - """Return the number of options and option groups.""" - return (len(self._fconf) + - len(self._imported_opts) + - len(self._imported_groups)) - - @staticmethod - def _already_registered(conf, opt, group=None): - group_name = group.name if isinstance(group, cfg.OptGroup) else group - return ((group_name is None and - opt.dest in conf) or - (group_name is not None and - group_name in conf and - opt.dest in conf[group_name])) - - def register_opt(self, opt, group=None): - """Register an option schema. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already registered, True otherwise - :raises: DuplicateOptError - """ - if self._already_registered(self._conf, opt, group): - # Raises DuplicateError if there is another opt with the same name - ret = self._conf.register_opt(opt, group) - self._import_opt(opt.dest, group) - return ret - else: - return self._fconf.register_opt(opt, group) - - def register_opts(self, opts, group=None): - """Register multiple option schemas at once.""" - for opt in opts: - self.register_opt(opt, group) - - def register_cli_opt(self, opt, group=None): - """Register a CLI option schema. - - :param opt: an instance of an Opt sub-class - :param group: an optional OptGroup object or group name - :return: False if the opt was already register, True otherwise - :raises: DuplicateOptError, ArgsAlreadyParsedError - """ - if self._already_registered(self._conf, opt, group): - # Raises DuplicateError if there is another opt with the same name - ret = self._conf.register_cli_opt(opt, group) - self._import_opt(opt.dest, group) - return ret - else: - return self._fconf.register_cli_opt(opt, group) - - def register_cli_opts(self, opts, group=None): - """Register multiple CLI option schemas at once.""" - for opt in opts: - self.register_cli_opt(opt, group) - - def register_group(self, group): - """Register an option group. - - :param group: an OptGroup object - """ - self._fconf.register_group(group) - - def import_opt(self, opt_name, module_str, group=None): - """Import an option definition from a module. - - :param name: the name/dest of the opt - :param module_str: the name of a module to import - :param group: an option OptGroup object or group name - :raises: NoSuchOptError, NoSuchGroupError - """ - self._conf.import_opt(opt_name, module_str, group) - self._import_opt(opt_name, group) - - def import_group(self, group, module_str): - """Import an option group from a module. - - Note that this allows access to all options registered with - the group whether or not those options were registered by - the given module. - - :param group: an option OptGroup object or group name - :param module_str: the name of a module to import - :raises: ImportError, NoSuchGroupError - """ - self._conf.import_group(group, module_str) - group = self._import_group(group) - group._all_opts = True - - def _import_opt(self, opt_name, group): - if group is None: - self._imported_opts.add(opt_name) - return True - else: - group = self._import_group(group) - return group._import_opt(opt_name) - - def _import_group(self, group_or_name): - if isinstance(group_or_name, cfg.OptGroup): - group_name = group_or_name.name - else: - group_name = group_or_name - - if group_name in self._imported_groups: - return self._imported_groups[group_name] - else: - group = self.GroupAttr(self._conf, group_name) - self._imported_groups[group_name] = group - return group - - class GroupAttr(collections.Mapping): - - """Helper class to wrap a group object. - - Represents the option values of a group as a mapping and attributes. - """ - - def __init__(self, conf, group): - """Construct a GroupAttr object. - - :param conf: a ConfigOpts object - :param group: an OptGroup object - """ - self._conf = conf - self._group = group - self._imported_opts = set() - self._all_opts = False - - def __getattr__(self, name): - """Look up an option value.""" - if not self._all_opts and name not in self._imported_opts: - raise cfg.NoSuchOptError(name) - return getattr(self._conf[self._group], name) - - def __getitem__(self, key): - """Look up an option value.""" - return getattr(self, key) - - def __contains__(self, key): - """Return True if key is the name of a registered opt or group.""" - return key in self._imported_opts - - def __iter__(self): - """Iterate over all registered opt and group names.""" - for key in self._imported_opts: - yield key - - def __len__(self): - """Return the number of options and option groups.""" - return len(self._imported_opts) - - def _import_opt(self, opt_name): - self._imported_opts.add(opt_name) +from oslo_config.cfgfilter import * # noqa diff --git a/oslo/config/fixture.py b/oslo/config/fixture.py index 2bb6cf8e..ab3aacbc 100644 --- a/oslo/config/fixture.py +++ b/oslo/config/fixture.py @@ -1,8 +1,3 @@ -# -# Copyright 2013 Mirantis, Inc. -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -15,104 +10,4 @@ # License for the specific language governing permissions and limitations # under the License. -import fixtures -import six - -from oslo.config import cfg - - -class Config(fixtures.Fixture): - """Allows overriding configuration settings for the test. - - `conf` will be reset on cleanup. - - """ - - def __init__(self, conf=cfg.CONF): - self.conf = conf - - def setUp(self): - super(Config, self).setUp() - # NOTE(morganfainberg): unregister must be added to cleanup before - # reset is because cleanup works in reverse order of registered items, - # and a reset must occur before unregistering options can occur. - self.addCleanup(self._unregister_config_opts) - self.addCleanup(self.conf.reset) - self._registered_config_opts = {} - - def config(self, **kw): - """Override configuration values. - - The keyword arguments are the names of configuration options to - override and their values. - - If a `group` argument is supplied, the overrides are applied to - the specified configuration option group, otherwise the overrides - are applied to the ``default`` group. - - """ - - group = kw.pop('group', None) - for k, v in six.iteritems(kw): - self.conf.set_override(k, v, group) - - def _unregister_config_opts(self): - for group in self._registered_config_opts: - self.conf.unregister_opts(self._registered_config_opts[group], - group=group) - - def register_opt(self, opt, group=None): - """Register a single option for the test run. - - Options registered in this manner will automatically be unregistered - during cleanup. - - If a `group` argument is supplied, it will register the new option - to that group, otherwise the option is registered to the ``default`` - group. - """ - self.conf.register_opt(opt, group=group) - self._registered_config_opts.setdefault(group, set()).add(opt) - - def register_opts(self, opts, group=None): - """Register multiple options for the test run. - - This works in the same manner as register_opt() but takes a list of - options as the first argument. All arguments will be registered to the - same group if the ``group`` argument is supplied, otherwise all options - will be registered to the ``default`` group. - """ - for opt in opts: - self.register_opt(opt, group=group) - - def register_cli_opt(self, opt, group=None): - """Register a single CLI option for the test run. - - Options registered in this manner will automatically be unregistered - during cleanup. - - If a `group` argument is supplied, it will register the new option - to that group, otherwise the option is registered to the ``default`` - group. - - CLI options must be registered before the command line and config files - are parsed. This is to ensure that all CLI options are shown in --help - and option validation works as expected. - """ - self.conf.register_cli_opt(opt, group=group) - self._registered_config_opts.setdefault(group, set()).add(opt) - - def register_cli_opts(self, opts, group=None): - """Register multiple CLI options for the test run. - - This works in the same manner as register_opt() but takes a list of - options as the first argument. All arguments will be registered to the - same group if the ``group`` argument is supplied, otherwise all options - will be registered to the ``default`` group. - - CLI options must be registered before the command line and config files - are parsed. This is to ensure that all CLI options are shown in --help - and option validation works as expected. - """ - for opt in opts: - self.register_cli_opt(opt, group=group) +from oslo_config.fixture import * # noqa diff --git a/oslo/config/generator.py b/oslo/config/generator.py index 93594e05..f635dc7d 100644 --- a/oslo/config/generator.py +++ b/oslo/config/generator.py @@ -1,8 +1,3 @@ -# Copyright 2012 SINA Corporation -# Copyright 2014 Cisco Systems, Inc. -# All Rights Reserved. -# Copyright 2014 Red Hat, Inc. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -15,298 +10,4 @@ # License for the specific language governing permissions and limitations # under the License. -r""" -A sample configuration file generator. - -oslo-config-generator is a utility for generating sample config files. For -example, to generate a sample config file for oslo.messaging you would run:: - - $> oslo-config-generator --namespace oslo.messaging > oslo.messaging.conf - -This generated sample lists all of the available options, along with their help -string, type, deprecated aliases and defaults. - -The --namespace option specifies an entry point name registered under the -'oslo.config.opts' entry point namespace. For example, in oslo.messaging's -setup.cfg we have:: - - [entry_points] - oslo.config.opts = - oslo.messaging = oslo.messaging.opts:list_opts - -The callable referenced by the entry point should take no arguments and return -a list of (group_name, [opt_1, opt_2]) tuples. For example:: - - opts = [ - cfg.StrOpt('foo'), - cfg.StrOpt('bar'), - ] - - cfg.CONF.register_opts(opts, group='blaa') - - def list_opts(): - return [('blaa', opts)] - -You might choose to return a copy of the options so that the return value can't -be modified for nefarious purposes:: - - def list_opts(): - return [('blaa', copy.deepcopy(opts))] - -A single codebase might have multiple programs, each of which use a subset of -the total set of options registered by the codebase. In that case, you can -register multiple entry points:: - - [entry_points] - oslo.config.opts = - nova.common = nova.config:list_common_opts - nova.api = nova.config:list_api_opts - nova.compute = nova.config:list_compute_opts - -and generate a config file specific to each program:: - - $> oslo-config-generator --namespace oslo.messaging \ - --namespace nova.common \ - --namespace nova.api > nova-api.conf - $> oslo-config-generator --namespace oslo.messaging \ - --namespace nova.common \ - --namespace nova.compute > nova-compute.conf - -To make this more convenient, you can use config files to describe your config -files:: - - $> cat > config-generator/api.conf < cat > config-generator/compute.conf < oslo-config-generator --config-file config-generator/api.conf - $> oslo-config-generator --config-file config-generator/compute.conf - -The default runtime values of configuration options are not always the most -suitable values to include in sample config files - for example, rather than -including the IP address or hostname of the machine where the config file -was generated, you might want to include something like '10.0.0.1'. To -facilitate this, options can be supplied with a 'sample_default' attribute:: - - cfg.StrOpt('base_dir' - default=os.getcwd(), - sample_default='/usr/lib/myapp') -""" - -import logging -import operator -import sys -import textwrap - -from oslo.config import cfg -import stevedore.named # noqa - -LOG = logging.getLogger(__name__) - -_generator_opts = [ - cfg.StrOpt('output-file', - help='Path of the file to write to. Defaults to stdout.'), - cfg.IntOpt('wrap-width', - default=70, - help='The maximum length of help lines.'), - cfg.MultiStrOpt('namespace', - help='Option namespace under "oslo.config.opts" in which ' - 'to query for options.'), -] - - -def register_cli_opts(conf): - """Register the formatter's CLI options with a ConfigOpts instance. - - Note, this must be done before the ConfigOpts instance is called to parse - the configuration. - - :param conf: a ConfigOpts instance - :raises: DuplicateOptError, ArgsAlreadyParsedError - """ - conf.register_cli_opts(_generator_opts) - - -class _OptFormatter(object): - - """Format configuration option descriptions to a file.""" - - _TYPE_DESCRIPTIONS = { - cfg.StrOpt: 'string value', - cfg.BoolOpt: 'boolean value', - cfg.IntOpt: 'integer value', - cfg.FloatOpt: 'floating point value', - cfg.ListOpt: 'list value', - cfg.DictOpt: 'dict value', - cfg.MultiStrOpt: 'multi valued', - } - - def __init__(self, output_file=None, wrap_width=70): - """Construct an OptFormatter object. - - :param output_file: a writeable file object - :param wrap_width: The maximum length of help lines, 0 to not wrap - """ - self.output_file = output_file or sys.stdout - self.wrap_width = wrap_width - - def _format_help(self, help_text): - """Format the help for a group or option to the output file. - - :param help_text: The text of the help string - """ - if self.wrap_width is not None and self.wrap_width > 0: - lines = [textwrap.fill(help_text, - self.wrap_width, - initial_indent='# ', - subsequent_indent='# ') + '\n'] - else: - lines = ['# ' + help_text + '\n'] - return lines - - def format(self, opt): - """Format a description of an option to the output file. - - :param opt: a cfg.Opt instance - """ - if not opt.help: - LOG.warning('"%s" is missing a help string', opt.dest) - - opt_type = self._TYPE_DESCRIPTIONS.get(type(opt), 'unknown type') - - help_text = u'%s(%s)' % (opt.help + ' ' if opt.help else '', opt_type) - lines = self._format_help(help_text) - - for d in opt.deprecated_opts: - lines.append('# Deprecated group/name - [%s]/%s\n' % - (d.group or 'DEFAULT', d.name or opt.dest)) - - if isinstance(opt, cfg.MultiStrOpt): - if opt.sample_default is not None: - defaults = opt.sample_default - elif not opt.default: - defaults = [''] - else: - defaults = opt.default - else: - if opt.sample_default is not None: - default_str = str(opt.sample_default) - elif opt.default is None: - default_str = '' - elif isinstance(opt, cfg.StrOpt): - default_str = opt.default - elif isinstance(opt, cfg.BoolOpt): - default_str = str(opt.default).lower() - elif (isinstance(opt, cfg.IntOpt) or - isinstance(opt, cfg.FloatOpt)): - default_str = str(opt.default) - elif isinstance(opt, cfg.ListOpt): - default_str = ','.join(opt.default) - elif isinstance(opt, cfg.DictOpt): - sorted_items = sorted(opt.default.items(), - key=operator.itemgetter(0)) - default_str = ','.join(['%s:%s' % i for i in sorted_items]) - else: - LOG.warning('Unknown option type: %s', repr(opt)) - default_str = str(opt.default) - defaults = [default_str] - - for default_str in defaults: - if default_str.strip() != default_str: - default_str = '"%s"' % default_str - if default_str: - default_str = ' ' + default_str - lines.append('#%s =%s\n' % (opt.dest, default_str)) - - self.writelines(lines) - - def write(self, s): - """Write an arbitrary string to the output file. - - :param s: an arbitrary string - """ - self.output_file.write(s) - - def writelines(self, l): - """Write an arbitrary sequence of strings to the output file. - - :param l: a list of arbitrary strings - """ - self.output_file.writelines(l) - - -def _list_opts(namespaces): - """List the options available via the given namespaces. - - :param namespaces: a list of namespaces registered under 'oslo.config.opts' - :returns: a list of (namespace, [(group, [opt_1, opt_2])]) tuples - """ - mgr = stevedore.named.NamedExtensionManager('oslo.config.opts', - names=namespaces, - invoke_on_load=True) - return [(ep.name, ep.obj) for ep in mgr] - - -def generate(conf): - """Generate a sample config file. - - List all of the options available via the namespaces specified in the given - configuration and write a description of them to the specified output file. - - :param conf: a ConfigOpts instance containing the generator's configuration - """ - conf.register_opts(_generator_opts) - - output_file = (open(conf.output_file, 'w') - if conf.output_file else sys.stdout) - - formatter = _OptFormatter(output_file=output_file, - wrap_width=conf.wrap_width) - - groups = {'DEFAULT': []} - for namespace, listing in _list_opts(conf.namespace): - for group, opts in listing: - if not opts: - continue - namespaces = groups.setdefault(group or 'DEFAULT', []) - namespaces.append((namespace, - dict((opt.dest, opt) for opt in opts))) - - def _output_opts(f, group, namespaces): - f.write('[%s]\n' % group) - for (namespace, opts_by_dest) in sorted(namespaces, - key=operator.itemgetter(0)): - f.write('\n#\n# From %s\n#\n' % namespace) - for opt in sorted(opts_by_dest.values(), - key=operator.attrgetter('dest')): - f.write('\n') - f.format(opt) - - _output_opts(formatter, 'DEFAULT', groups.pop('DEFAULT')) - for group, namespaces in sorted(groups.items(), - key=operator.itemgetter(0)): - formatter.write('\n\n') - _output_opts(formatter, group, namespaces) - - -def main(args=None): - """The main function of oslo-config-generator.""" - logging.basicConfig(level=logging.WARN) - conf = cfg.ConfigOpts() - register_cli_opts(conf) - conf(args) - generate(conf) - - -if __name__ == '__main__': - main() +from oslo_config.generator import * # noqa diff --git a/oslo/config/iniparser.py b/oslo/config/iniparser.py index 90218e45..f46978ca 100644 --- a/oslo/config/iniparser.py +++ b/oslo/config/iniparser.py @@ -1,5 +1,3 @@ -# Copyright 2012 OpenStack Foundation -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -12,116 +10,4 @@ # License for the specific language governing permissions and limitations # under the License. - -class ParseError(Exception): - def __init__(self, message, lineno, line): - self.msg = message - self.line = line - self.lineno = lineno - - def __str__(self): - return 'at line %d, %s: %r' % (self.lineno, self.msg, self.line) - - -class BaseParser(object): - lineno = 0 - parse_exc = ParseError - - def _assignment(self, key, value): - self.assignment(key, value) - return None, [] - - def _get_section(self, line): - if not line.endswith(']'): - return self.error_no_section_end_bracket(line) - if len(line) <= 2: - return self.error_no_section_name(line) - - return line[1:-1] - - def _split_key_value(self, line): - colon = line.find(':') - equal = line.find('=') - if colon < 0 and equal < 0: - return self.error_invalid_assignment(line) - - if colon < 0 or (equal >= 0 and equal < colon): - key, value = line[:equal], line[equal + 1:] - else: - key, value = line[:colon], line[colon + 1:] - - value = value.strip() - if value and value[0] == value[-1] and value.startswith(("\"", "'")): - value = value[1:-1] - return key.strip(), [value] - - def parse(self, lineiter): - key = None - value = [] - - for line in lineiter: - self.lineno += 1 - - line = line.rstrip() - if not line: - # Blank line, ends multi-line values - if key: - key, value = self._assignment(key, value) - continue - elif line.startswith((' ', '\t')): - # Continuation of previous assignment - if key is None: - self.error_unexpected_continuation(line) - else: - value.append(line.lstrip()) - continue - - if key: - # Flush previous assignment, if any - key, value = self._assignment(key, value) - - if line.startswith('['): - # Section start - section = self._get_section(line) - if section: - self.new_section(section) - elif line.startswith(('#', ';')): - self.comment(line[1:].lstrip()) - else: - key, value = self._split_key_value(line) - if not key: - return self.error_empty_key(line) - - if key: - # Flush previous assignment, if any - self._assignment(key, value) - - def assignment(self, key, value): - """Called when a full assignment is parsed.""" - raise NotImplementedError() - - def new_section(self, section): - """Called when a new section is started.""" - raise NotImplementedError() - - def comment(self, comment): - """Called when a comment is parsed.""" - pass - - def error_invalid_assignment(self, line): - raise self.parse_exc("No ':' or '=' found in assignment", - self.lineno, line) - - def error_empty_key(self, line): - raise self.parse_exc('Key cannot be empty', self.lineno, line) - - def error_unexpected_continuation(self, line): - raise self.parse_exc('Unexpected continuation line', - self.lineno, line) - - def error_no_section_end_bracket(self, line): - raise self.parse_exc('Invalid section (must end with ])', - self.lineno, line) - - def error_no_section_name(self, line): - raise self.parse_exc('Empty section name', self.lineno, line) +from oslo_config.iniparser import * # noqa diff --git a/oslo/config/types.py b/oslo/config/types.py index 9b646e25..2ed9fa9d 100644 --- a/oslo/config/types.py +++ b/oslo/config/types.py @@ -1,5 +1,3 @@ -# Copyright 2013 Mirantis, Inc. -# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -12,402 +10,4 @@ # License for the specific language governing permissions and limitations # under the License. -"""Type conversion and validation classes for configuration options. - -Use these classes as values for the `type` argument to -:class:`oslo.config.cfg.Opt` and its subclasses. - -""" -import netaddr -import six - - -class ConfigType(object): - - BASE_TYPES = (None,) - - def is_base_type(self, other): - return isinstance(other, self.BASE_TYPES) - - -class String(ConfigType): - - """String type. - - String values do not get transformed and are returned as str objects. - - :param choices: Optional sequence of valid values. - :param quotes: If True and string is enclosed with single or double - quotes, will strip those quotes. Will signal error if - string have quote at the beginning and no quote at - the end. Turned off by default. Useful if used with - container types like List. - """ - - BASE_TYPES = six.string_types - - def __init__(self, choices=None, quotes=False): - super(String, self).__init__() - self.choices = choices - self.quotes = quotes - - def __call__(self, value): - value = str(value) - if self.quotes and value: - if value[0] in "\"'": - if value[-1] != value[0]: - raise ValueError('Non-closed quote: %s' % value) - value = value[1:-1] - - if self.choices is None or value in self.choices: - return value - - raise ValueError( - 'Valid values are [%s], but found %s' % ( - ', '.join([str(v) for v in self.choices]), - repr(value))) - - def __repr__(self): - if self.choices: - return 'String(choices=%s)' % repr(self.choices) - return 'String' - - def __eq__(self, other): - return ( - (self.__class__ == other.__class__) and - (self.choices == other.choices) and - (self.quotes == other.quotes) - ) - - -class MultiString(String): - - BASE_TYPES = six.string_types + (list,) - - -class Boolean(ConfigType): - - """Boolean type. - - Values are case insensitive and can be set using - 1/0, yes/no, true/false or on/off. - """ - TRUE_VALUES = ['true', '1', 'on', 'yes'] - FALSE_VALUES = ['false', '0', 'off', 'no'] - - BASE_TYPES = (bool,) - - def __call__(self, value): - if isinstance(value, bool): - return value - - s = value.lower() - if s in self.TRUE_VALUES: - return True - elif s in self.FALSE_VALUES: - return False - else: - raise ValueError('Unexpected boolean value %r' % value) - - def __repr__(self): - return 'Boolean' - - def __eq__(self, other): - return self.__class__ == other.__class__ - - -class Integer(ConfigType): - - """Integer type. - - Converts value to an integer optionally doing range checking. - If value is whitespace or empty string will return None. - - :param min: Optional check that value is greater than or equal to min - :param max: Optional check that value is less than or equal to max - """ - - BASE_TYPES = six.integer_types - - def __init__(self, min=None, max=None): - super(Integer, self).__init__() - self.min = min - self.max = max - if min and max and max < min: - raise ValueError('Max value is less than min value') - - def __call__(self, value): - if not isinstance(value, int): - s = str(value).strip() - if s == '': - value = None - else: - value = int(value) - - if value: - self._check_range(value) - - return value - - def _check_range(self, value): - if self.min and value < self.min: - raise ValueError('Should be greater than or equal to %d' % - self.min) - if self.max and value > self.max: - raise ValueError('Should be less than or equal to %d' % self.max) - - def __repr__(self): - props = [] - if self.min: - props.append('min=%d' % self.min) - if self.max: - props.append('max=%d' % self.max) - - if props: - return 'Integer(%s)' % ', '.join(props) - return 'Integer' - - def __eq__(self, other): - return ( - (self.__class__ == other.__class__) and - (self.min == other.min) and - (self.max == other.max) - ) - - -class Float(ConfigType): - - """Float type.""" - - # allow float to be set from int - BASE_TYPES = six.integer_types + (float,) - - def __call__(self, value): - if isinstance(value, float): - return value - - return float(value) - - def __repr__(self): - return 'Float' - - def __eq__(self, other): - return self.__class__ == other.__class__ - - -class List(ConfigType): - - """List type. - - Represent values of other (item) type, separated by commas. - The resulting value is a list containing those values. - - List doesn't know if item type can also contain commas. To workaround this - it tries the following: if the next part fails item validation, it appends - comma and next item until validation succeeds or there is no parts left. - In the later case it will signal validation error. - - :param item_type: type of list items - :param bounds: if True, value should be inside "[" and "]" pair - """ - - BASE_TYPES = (list,) - - def __init__(self, item_type=None, bounds=False): - super(List, self).__init__() - - if item_type is None: - item_type = String() - - if not callable(item_type): - raise TypeError('item_type must be callable') - self.item_type = item_type - self.bounds = bounds - - def __call__(self, value): - if isinstance(value, list): - return value - - result = [] - s = value.strip() - - if self.bounds: - if not s.startswith('['): - raise ValueError('Value should start with "["') - if not s.endswith(']'): - raise ValueError('Value should end with "]"') - s = s[1:-1] - - if s == '': - return result - - values = s.split(',') - while values: - value = values.pop(0) - while True: - first_error = None - try: - validated_value = self.item_type(value.strip()) - break - except ValueError as e: - if not first_error: - first_error = e - if len(values) == 0: - raise first_error - - value += ',' + values.pop(0) - - result.append(validated_value) - - return result - - def __repr__(self): - return 'List of %s' % repr(self.item_type) - - def __eq__(self, other): - return ( - (self.__class__ == other.__class__) and - (self.item_type == other.item_type) - ) - - -class Dict(ConfigType): - - """Dictionary type. - - Dictionary type values are key:value pairs separated by commas. - The resulting value is a dictionary of these key/value pairs. - Type of dictionary key is always string, but dictionary value - type can be customized. - - :param value_type: type of values in dictionary - :param bounds: if True, value should be inside "{" and "}" pair - """ - - BASE_TYPES = (dict,) - - def __init__(self, value_type=None, bounds=False): - super(Dict, self).__init__() - - if value_type is None: - value_type = String() - - if not callable(value_type): - raise TypeError('value_type must be callable') - self.value_type = value_type - self.bounds = bounds - - def __call__(self, value): - if isinstance(value, dict): - return value - - result = {} - s = value.strip() - - if self.bounds: - if not s.startswith('{'): - raise ValueError('Value should start with "{"') - if not s.endswith('}'): - raise ValueError('Value should end with "}"') - s = s[1:-1] - - if s == '': - return result - - pairs = s.split(',') - while pairs: - pair = pairs.pop(0) - - while True: - first_error = None - try: - key_value = pair.split(':', 1) - - if len(key_value) < 2: - raise ValueError('Value should be NAME:VALUE pairs ' - 'separated by ","') - - key, value = key_value - key = key.strip() - value = value.strip() - - value = self.value_type(value) - break - except ValueError as e: - if not first_error: - first_error = e - if not pairs: - raise first_error - - pair += ',' + pairs.pop(0) - - if key == '': - raise ValueError('Key name should not be empty') - - if key in result: - raise ValueError('Duplicate key %s' % key) - - result[key] = value - - return result - - def __repr__(self): - return 'Dict of %s' % repr(self.value_type) - - def __eq__(self, other): - return ( - (self.__class__ == other.__class__) and - (self.value_type == other.value_type) - ) - - -class IPAddress(ConfigType): - - """IP address type - - Represents either ipv4 or ipv6. Without specifying version parameter both - versions are checked - - :param version: defines which version should be explicitly checked (4 or 6) - - """ - - BASE_TYPES = six.string_types - - def __init__(self, version=None): - super(IPAddress, self).__init__() - version_checkers = { - None: self._check_both_versions, - 4: self._check_ipv4, - 6: self._check_ipv6 - } - - self.version_checker = version_checkers.get(version) - if self.version_checker is None: - raise TypeError("%s is not a valid IP version." % version) - - def __call__(self, value): - value = str(value) - if not value: - raise ValueError("IP address cannot be an empty string") - self.version_checker(value) - return value - - def __repr__(self): - return "IPAddress" - - def __eq__(self, other): - return self.__class__ == other.__class__ - - def _check_ipv4(self, address): - if not netaddr.valid_ipv4(address, netaddr.core.INET_PTON): - raise ValueError("%s is not an IPv4 address" % address) - - def _check_ipv6(self, address): - if not netaddr.valid_ipv6(address, netaddr.core.INET_PTON): - raise ValueError("%s is not an IPv6 address" % address) - - def _check_both_versions(self, address): - if not (netaddr.valid_ipv4(address, netaddr.core.INET_PTON) or - netaddr.valid_ipv6(address, netaddr.core.INET_PTON)): - raise ValueError("%s is not IPv4 or IPv6 address" % address) +from oslo_config.types import * # noqa diff --git a/oslo_config/__init__.py b/oslo_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oslo_config/cfg.py b/oslo_config/cfg.py new file mode 100644 index 00000000..a6fe1a15 --- /dev/null +++ b/oslo_config/cfg.py @@ -0,0 +1,2471 @@ +# Copyright 2012 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +r""" +Configuration options which may be set on the command line or in config files. + +The schema for each option is defined using the Opt class or its sub-classes, +for example: + +:: + + from oslo.config import cfg + from oslo.config import types + + PortType = types.Integer(1, 65535) + + common_opts = [ + cfg.StrOpt('bind_host', + default='0.0.0.0', + help='IP address to listen on.'), + cfg.Opt('bind_port', + type=PortType(), + default=9292, + help='Port number to listen on.') + ] + +Option Types +------------ + +Options can have arbitrary types, you just need to pass type constructor +to Opt. Type constructor is a callable object that takes a string and returns +value of particular type or raises ValueError if given string can't be +converted to that type. + +There are predefined types: strings, integers, floats, booleans, lists, +'multi strings' and 'key/value pairs' (dictionary) :: + + enabled_apis_opt = cfg.ListOpt('enabled_apis', + default=['ec2', 'osapi_compute'], + help='List of APIs to enable by default.') + + DEFAULT_EXTENSIONS = [ + 'nova.api.openstack.compute.contrib.standard_extensions' + ] + osapi_compute_extension_opt = cfg.MultiStrOpt('osapi_compute_extension', + default=DEFAULT_EXTENSIONS) + +Registering Options +------------------- + +Option schemas are registered with the config manager at runtime, but before +the option is referenced:: + + class ExtensionManager(object): + + enabled_apis_opt = cfg.ListOpt(...) + + def __init__(self, conf): + self.conf = conf + self.conf.register_opt(enabled_apis_opt) + ... + + def _load_extensions(self): + for ext_factory in self.conf.osapi_compute_extension: + .... + +A common usage pattern is for each option schema to be defined in the module or +class which uses the option:: + + opts = ... + + def add_common_opts(conf): + conf.register_opts(opts) + + def get_bind_host(conf): + return conf.bind_host + + def get_bind_port(conf): + return conf.bind_port + +An option may optionally be made available via the command line. Such options +must be registered with the config manager before the command line is parsed +(for the purposes of --help and CLI arg validation):: + + cli_opts = [ + cfg.BoolOpt('verbose', + short='v', + default=False, + help='Print more verbose output.'), + cfg.BoolOpt('debug', + short='d', + default=False, + help='Print debugging output.'), + ] + + def add_common_opts(conf): + conf.register_cli_opts(cli_opts) + +Loading Config Files +-------------------- + +The config manager has two CLI options defined by default, --config-file +and --config-dir:: + + class ConfigOpts(object): + + def __call__(self, ...): + + opts = [ + MultiStrOpt('config-file', + ...), + StrOpt('config-dir', + ...), + ] + + self.register_cli_opts(opts) + +Option values are parsed from any supplied config files using +oslo.config.iniparser. If none are specified, a default set is used +for example glance-api.conf and glance-common.conf:: + + glance-api.conf: + [DEFAULT] + bind_port = 9292 + + glance-common.conf: + [DEFAULT] + bind_host = 0.0.0.0 + +Option values in config files and those on the command line are parsed +in order. The same option can appear many times, in config files or on +the command line. Later values always override earlier ones. + +The parsing of CLI args and config files is initiated by invoking the config +manager for example:: + + conf = ConfigOpts() + conf.register_opt(BoolOpt('verbose', ...)) + conf(sys.argv[1:]) + if conf.verbose: + ... + +Option Groups +------------- + +Options can be registered as belonging to a group:: + + rabbit_group = cfg.OptGroup(name='rabbit', + title='RabbitMQ options') + + rabbit_host_opt = cfg.StrOpt('host', + default='localhost', + help='IP/hostname to listen on.'), + rabbit_port_opt = cfg.IntOpt('port', + default=5672, + help='Port number to listen on.') + + def register_rabbit_opts(conf): + conf.register_group(rabbit_group) + # options can be registered under a group in either of these ways: + conf.register_opt(rabbit_host_opt, group=rabbit_group) + conf.register_opt(rabbit_port_opt, group='rabbit') + +If no group attributes are required other than the group name, the group +need not be explicitly registered for example:: + + def register_rabbit_opts(conf): + # The group will automatically be created, equivalent calling:: + # conf.register_group(OptGroup(name='rabbit')) + conf.register_opt(rabbit_port_opt, group='rabbit') + +If no group is specified, options belong to the 'DEFAULT' section of config +files:: + + glance-api.conf: + [DEFAULT] + bind_port = 9292 + ... + + [rabbit] + host = localhost + port = 5672 + use_ssl = False + userid = guest + password = guest + virtual_host = / + +Command-line options in a group are automatically prefixed with the +group name:: + + --rabbit-host localhost --rabbit-port 9999 + +Accessing Option Values In Your Code +------------------------------------ + +Option values in the default group are referenced as attributes/properties on +the config manager; groups are also attributes on the config manager, with +attributes for each of the options associated with the group:: + + server.start(app, conf.bind_port, conf.bind_host, conf) + + self.connection = kombu.connection.BrokerConnection( + hostname=conf.rabbit.host, + port=conf.rabbit.port, + ...) + +Option Value Interpolation +-------------------------- + +Option values may reference other values using PEP 292 string substitution:: + + opts = [ + cfg.StrOpt('state_path', + default=os.path.join(os.path.dirname(__file__), '../'), + help='Top-level directory for maintaining nova state.'), + cfg.StrOpt('sqlite_db', + default='nova.sqlite', + help='File name for SQLite.'), + cfg.StrOpt('sql_connection', + default='sqlite:///$state_path/$sqlite_db', + help='Connection string for SQL database.'), + ] + +.. note:: + + Interpolation can be avoided by using `$$`. + +.. warning:: + + Interpolation using the values of options in groups is not yet + supported. The interpolated option must be in the DEFAULT group + (i.e., ``"$state_path"`` works but ``"$database.state_path"`` does + not). + +Special Handling Instructions +----------------------------- + +Options may be declared as required so that an error is raised if the user +does not supply a value for the option:: + + opts = [ + cfg.StrOpt('service_name', required=True), + cfg.StrOpt('image_id', required=True), + ... + ] + +Options may be declared as secret so that their values are not leaked into +log files:: + + opts = [ + cfg.StrOpt('s3_store_access_key', secret=True), + cfg.StrOpt('s3_store_secret_key', secret=True), + ... + ] + +Global ConfigOpts +----------------- + +This module also contains a global instance of the ConfigOpts class +in order to support a common usage pattern in OpenStack:: + + from oslo.config import cfg + + opts = [ + cfg.StrOpt('bind_host', default='0.0.0.0'), + cfg.IntOpt('bind_port', default=9292), + ] + + CONF = cfg.CONF + CONF.register_opts(opts) + + def start(server, app): + server.start(app, CONF.bind_port, CONF.bind_host) + +Positional Command Line Arguments +--------------------------------- + +Positional command line arguments are supported via a 'positional' Opt +constructor argument:: + + >>> conf = ConfigOpts() + >>> conf.register_cli_opt(MultiStrOpt('bar', positional=True)) + True + >>> conf(['a', 'b']) + >>> conf.bar + ['a', 'b'] + +Sub-Parsers +----------- + +It is also possible to use argparse "sub-parsers" to parse additional +command line arguments using the SubCommandOpt class: + + >>> def add_parsers(subparsers): + ... list_action = subparsers.add_parser('list') + ... list_action.add_argument('id') + ... + >>> conf = ConfigOpts() + >>> conf.register_cli_opt(SubCommandOpt('action', handler=add_parsers)) + True + >>> conf(args=['list', '10']) + >>> conf.action.name, conf.action.id + ('list', '10') + +""" + +import argparse +import collections +import copy +import errno +import functools +import glob +import itertools +import logging +import os +import string +import sys + +import six +from six import moves + +from oslo.config import iniparser +from oslo.config import types + +LOG = logging.getLogger(__name__) + + +class Error(Exception): + """Base class for cfg exceptions.""" + + def __init__(self, msg=None): + self.msg = msg + + def __str__(self): + return self.msg + + +class NotInitializedError(Error): + """Raised if parser is not initialized yet.""" + + def __str__(self): + return "call expression on parser has not been invoked" + + +class ArgsAlreadyParsedError(Error): + """Raised if a CLI opt is registered after parsing.""" + + def __str__(self): + ret = "arguments already parsed" + if self.msg: + ret += ": " + self.msg + return ret + + +class NoSuchOptError(Error, AttributeError): + """Raised if an opt which doesn't exist is referenced.""" + + def __init__(self, opt_name, group=None): + self.opt_name = opt_name + self.group = group + + def __str__(self): + if self.group is None: + return "no such option: %s" % self.opt_name + else: + return "no such option in group %s: %s" % (self.group.name, + self.opt_name) + + +class NoSuchGroupError(Error): + """Raised if a group which doesn't exist is referenced.""" + + def __init__(self, group_name): + self.group_name = group_name + + def __str__(self): + return "no such group: %s" % self.group_name + + +class DuplicateOptError(Error): + """Raised if multiple opts with the same name are registered.""" + + def __init__(self, opt_name): + self.opt_name = opt_name + + def __str__(self): + return "duplicate option: %s" % self.opt_name + + +class RequiredOptError(Error): + """Raised if an option is required but no value is supplied by the user.""" + + def __init__(self, opt_name, group=None): + self.opt_name = opt_name + self.group = group + + def __str__(self): + if self.group is None: + return "value required for option: %s" % self.opt_name + else: + return "value required for option: %s.%s" % (self.group.name, + self.opt_name) + + +class TemplateSubstitutionError(Error): + """Raised if an error occurs substituting a variable in an opt value.""" + + def __str__(self): + return "template substitution error: %s" % self.msg + + +class ConfigFilesNotFoundError(Error): + """Raised if one or more config files are not found.""" + + def __init__(self, config_files): + self.config_files = config_files + + def __str__(self): + return ('Failed to find some config files: %s' % + ",".join(self.config_files)) + + +class ConfigFilesPermissionDeniedError(Error): + """Raised if one or more config files are not readable.""" + + def __init__(self, config_files): + self.config_files = config_files + + def __str__(self): + return ('Failed to open some config files: %s' % + ",".join(self.config_files)) + + +class ConfigDirNotFoundError(Error): + """Raised if the requested config-dir is not found.""" + + def __init__(self, config_dir): + self.config_dir = config_dir + + def __str__(self): + return ('Failed to read config file directory: %s' % self.config_dir) + + +class ConfigFileParseError(Error): + """Raised if there is an error parsing a config file.""" + + def __init__(self, config_file, msg): + self.config_file = config_file + self.msg = msg + + def __str__(self): + return 'Failed to parse %s: %s' % (self.config_file, self.msg) + + +class ConfigFileValueError(Error): + """Raised if a config file value does not match its opt type.""" + pass + + +def _fixpath(p): + """Apply tilde expansion and absolutization to a path.""" + return os.path.abspath(os.path.expanduser(p)) + + +def _get_config_dirs(project=None): + """Return a list of directories where config files may be located. + + :param project: an optional project name + + If a project is specified, following directories are returned:: + + ~/.${project}/ + ~/ + /etc/${project}/ + /etc/ + + Otherwise, these directories:: + + ~/ + /etc/ + """ + cfg_dirs = [ + _fixpath(os.path.join('~', '.' + project)) if project else None, + _fixpath('~'), + os.path.join('/etc', project) if project else None, + '/etc' + ] + + return list(moves.filter(bool, cfg_dirs)) + + +def _search_dirs(dirs, basename, extension=""): + """Search a list of directories for a given filename. + + Iterator over the supplied directories, returning the first file + found with the supplied name and extension. + + :param dirs: a list of directories + :param basename: the filename, for example 'glance-api' + :param extension: the file extension, for example '.conf' + :returns: the path to a matching file, or None + """ + for d in dirs: + path = os.path.join(d, '%s%s' % (basename, extension)) + if os.path.exists(path): + return path + + +def find_config_files(project=None, prog=None, extension='.conf'): + """Return a list of default configuration files. + + :param project: an optional project name + :param prog: the program name, defaulting to the basename of sys.argv[0] + :param extension: the type of the config file + + We default to two config files: [${project}.conf, ${prog}.conf] + + And we look for those config files in the following directories:: + + ~/.${project}/ + ~/ + /etc/${project}/ + /etc/ + + We return an absolute path for (at most) one of each the default config + files, for the topmost directory it exists in. + + For example, if project=foo, prog=bar and /etc/foo/foo.conf, /etc/bar.conf + and ~/.foo/bar.conf all exist, then we return ['/etc/foo/foo.conf', + '~/.foo/bar.conf'] + + If no project name is supplied, we only look for ${prog.conf}. + """ + if prog is None: + prog = os.path.basename(sys.argv[0]) + + cfg_dirs = _get_config_dirs(project) + + config_files = [] + if project: + config_files.append(_search_dirs(cfg_dirs, project, extension)) + config_files.append(_search_dirs(cfg_dirs, prog, extension)) + + return list(moves.filter(bool, config_files)) + + +def _is_opt_registered(opts, opt): + """Check whether an opt with the same name is already registered. + + The same opt may be registered multiple times, with only the first + registration having any effect. However, it is an error to attempt + to register a different opt with the same name. + + :param opts: the set of opts already registered + :param opt: the opt to be registered + :returns: True if the opt was previously registered, False otherwise + :raises: DuplicateOptError if a naming conflict is detected + """ + if opt.dest in opts: + if opts[opt.dest]['opt'] != opt: + raise DuplicateOptError(opt.name) + return True + else: + return False + + +def set_defaults(opts, **kwargs): + for opt in opts: + if opt.dest in kwargs: + opt.default = kwargs[opt.dest] + + +def _normalize_group_name(group_name): + if group_name == 'DEFAULT': + return group_name + return group_name.lower() + + +class Opt(object): + + """Base class for all configuration options. + + An Opt object has no public methods, but has a number of public string + properties: + + name: + the name of the option, which may include hyphens + type: + a callable object that takes string and returns + converted and validated value + dest: + the (hyphen-less) ConfigOpts property which contains the option value + short: + a single character CLI option name + default: + the default value of the option + sample_default: + a sample default value string to include in sample config files + positional: + True if the option is a positional CLI argument + metavar: + the name shown as the argument to a CLI option in --help output + help: + an string explaining how the options value is used + """ + multi = False + + def __init__(self, name, type=None, dest=None, short=None, + default=None, positional=False, metavar=None, help=None, + secret=False, required=False, + deprecated_name=None, deprecated_group=None, + deprecated_opts=None, sample_default=None): + """Construct an Opt object. + + The only required parameter is the option's name. However, it is + common to also supply a default and help string for all options. + + :param name: the option's name + :param type: the option's type. Must be a callable object that + takes string and returns converted and validated value + :param dest: the name of the corresponding ConfigOpts property + :param short: a single character CLI option name + :param default: the default value of the option + :param positional: True if the option is a positional CLI argument + :param metavar: the option argument to show in --help + :param help: an explanation of how the option is used + :param secret: true iff the value should be obfuscated in log output + :param required: true iff a value must be supplied for this option + :param deprecated_name: deprecated name option. Acts like an alias + :param deprecated_group: the group containing a deprecated alias + :param deprecated_opts: array of DeprecatedOpt(s) + :param sample_default: a default string for sample config files + """ + if name.startswith('_'): + raise ValueError('illegal name %s with prefix _' % (name,)) + self.name = name + + if type is None: + type = types.String() + + if not callable(type): + raise TypeError('type must be callable') + self.type = type + + if dest is None: + self.dest = self.name.replace('-', '_') + else: + self.dest = dest + self.short = short + self.default = default + self.sample_default = sample_default + self.positional = positional + self.metavar = metavar + self.help = help + self.secret = secret + self.required = required + if deprecated_name is not None: + deprecated_name = deprecated_name.replace('-', '_') + + self.deprecated_opts = copy.deepcopy(deprecated_opts) or [] + if deprecated_name is not None or deprecated_group is not None: + self.deprecated_opts.append(DeprecatedOpt(deprecated_name, + group=deprecated_group)) + self._assert_default_is_of_opt_type() + + def _default_is_ref(self): + """Check if default is a reference to another var.""" + if isinstance(self.default, six.string_types): + tmpl = self.default.replace('\$', '').replace('$$', '') + return '$' in tmpl + return False + + def _assert_default_is_of_opt_type(self): + if (self.default is not None + and not self._default_is_ref() + and hasattr(self.type, 'is_base_type') + and not self.type.is_base_type(self.default)): + # NOTE(tcammann) Change this to raise error after K relase + expected_types = ", ".join( + [t.__name__ for t in self.type.BASE_TYPES]) + LOG.debug(('Expected default value of type(s) %(extypes)s but got ' + '%(default)r of type %(deftypes)s'), + {'extypes': expected_types, + 'default': self.default, + 'deftypes': type(self.default).__name__}) + + def __ne__(self, another): + return vars(self) != vars(another) + + def __eq__(self, another): + return vars(self) == vars(another) + + __hash__ = object.__hash__ + + def _get_from_namespace(self, namespace, group_name): + """Retrieves the option value from a _Namespace object. + + :param namespace: a _Namespace object + :param group_name: a group name + """ + names = [(group_name, self.dest)] + + for opt in self.deprecated_opts: + dname, dgroup = opt.name, opt.group + if dname or dgroup: + names.append((dgroup if dgroup else group_name, + dname if dname else self.dest)) + + return namespace._get_value(names, self.multi, self.positional) + + def _add_to_cli(self, parser, group=None): + """Makes the option available in the command line interface. + + This is the method ConfigOpts uses to add the opt to the CLI interface + as appropriate for the opt type. Some opt types may extend this method, + others may just extend the helper methods it uses. + + :param parser: the CLI option parser + :param group: an optional OptGroup object + """ + container = self._get_argparse_container(parser, group) + kwargs = self._get_argparse_kwargs(group) + prefix = self._get_argparse_prefix('', group.name if group else None) + deprecated_names = [] + for opt in self.deprecated_opts: + deprecated_name = self._get_deprecated_cli_name(opt.name, + opt.group) + if deprecated_name is not None: + deprecated_names.append(deprecated_name) + self._add_to_argparse(parser, container, self.name, self.short, + kwargs, prefix, + self.positional, deprecated_names) + + def _add_to_argparse(self, parser, container, name, short, kwargs, + prefix='', positional=False, deprecated_names=None): + """Add an option to an argparse parser or group. + + :param container: an argparse._ArgumentGroup object + :param name: the opt name + :param short: the short opt name + :param kwargs: the keyword arguments for add_argument() + :param prefix: an optional prefix to prepend to the opt name + :param positional: whether the option is a positional CLI argument + """ + def hyphen(arg): + return arg if not positional else '' + + args = [hyphen('--') + prefix + name] + if short: + args.append(hyphen('-') + short) + for deprecated_name in deprecated_names: + args.append(hyphen('--') + deprecated_name) + + parser.add_parser_argument(container, *args, **kwargs) + + def _get_argparse_container(self, parser, group): + """Returns an argparse._ArgumentGroup. + + :param parser: an argparse.ArgumentParser + :param group: an (optional) OptGroup object + :returns: an argparse._ArgumentGroup if group is given, else parser + """ + if group is not None: + return group._get_argparse_group(parser) + else: + return parser + + def _get_argparse_kwargs(self, group, **kwargs): + """Build a dict of keyword arguments for argparse's add_argument(). + + Most opt types extend this method to customize the behaviour of the + options added to argparse. + + :param group: an optional group + :param kwargs: optional keyword arguments to add to + :returns: a dict of keyword arguments + """ + if not self.positional: + dest = self.dest + if group is not None: + dest = group.name + '_' + dest + kwargs['dest'] = dest + else: + kwargs['nargs'] = '?' + kwargs.update({'default': None, + 'metavar': self.metavar, + 'help': self.help, }) + return kwargs + + def _get_argparse_prefix(self, prefix, group_name): + """Build a prefix for the CLI option name, if required. + + CLI options in a group are prefixed with the group's name in order + to avoid conflicts between similarly named options in different + groups. + + :param prefix: an existing prefix to append to (for example 'no' or '') + :param group_name: an optional group name + :returns: a CLI option prefix including the group name, if appropriate + """ + if group_name is not None: + return group_name + '-' + prefix + else: + return prefix + + def _get_deprecated_cli_name(self, dname, dgroup, prefix=''): + """Build a CLi arg name for deprecated options. + + Either a deprecated name or a deprecated group or both or + neither can be supplied: + + dname, dgroup -> dgroup + '-' + dname + dname -> dname + dgroup -> dgroup + '-' + self.name + neither -> None + + :param dname: a deprecated name, which can be None + :param dgroup: a deprecated group, which can be None + :param prefix: an prefix to append to (for example 'no' or '') + :returns: a CLI argument name + """ + if dgroup == 'DEFAULT': + dgroup = None + + if dname is None and dgroup is None: + return None + + if dname is None: + dname = self.name + + return self._get_argparse_prefix(prefix, dgroup) + dname + + def __lt__(self, another): + return hash(self) < hash(another) + +# NOTE(jd) Not available for py2.6 +if six.PY3: + Opt = functools.total_ordering(Opt) + + +class DeprecatedOpt(object): + + """Represents a Deprecated option. + + Here's how you can use it:: + + oldopts = [cfg.DeprecatedOpt('oldfoo', group='oldgroup'), + cfg.DeprecatedOpt('oldfoo2', group='oldgroup2')] + cfg.CONF.register_group(cfg.OptGroup('blaa')) + cfg.CONF.register_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), + group='blaa') + + Multi-value options will return all new and deprecated + options. For single options, if the new option is present + ("[blaa]/foo" above) it will override any deprecated options + present. If the new option is not present and multiple + deprecated options are present, the option corresponding to + the first element of deprecated_opts will be chosen. + + If group is None, the DeprecatedOpt lookup will happen within the same + group the new option is in. For example:: + + oldopts = [cfg.DeprecatedOpt('oldfoo'), + cfg.DeprecatedOpt('oldfoo2', group='DEFAULT')] + + cfg.CONF.register_group(cfg.OptGroup('blaa')) + cfg.CONF.register_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), + group='blaa') + + In the example above, `oldfoo` will be looked up in the `blaa` group and + `oldfoo2` in the `DEFAULT` group. + """ + + def __init__(self, name, group=None): + """Constructs an DeprecatedOpt object. + + :param name: the name of the option + :param group: the group of the option + """ + self.name = name + self.group = group + + def __key(self): + return (self.name, self.group) + + def __eq__(self, other): + return self.__key() == other.__key() + + def __hash__(self): + return hash(self.__key()) + + +class StrOpt(Opt): + """Option with String type (for backward compatibility). + + :param choices: Optional sequence of valid values. + """ + + def __init__(self, name, choices=None, **kwargs): + super(StrOpt, self).__init__(name, + type=types.String(choices=choices), + **kwargs) + + +class BoolOpt(Opt): + + """Boolean options. + + Bool opts are set to True or False on the command line using --optname or + --noopttname respectively. + + In config files, boolean values are cast with Boolean type. + """ + + def __init__(self, name, **kwargs): + if 'positional' in kwargs: + raise ValueError('positional boolean args not supported') + super(BoolOpt, self).__init__(name, type=types.Boolean(), **kwargs) + + def _add_to_cli(self, parser, group=None): + """Extends the base class method to add the --nooptname option.""" + super(BoolOpt, self)._add_to_cli(parser, group) + self._add_inverse_to_argparse(parser, group) + + def _add_inverse_to_argparse(self, parser, group): + """Add the --nooptname option to the option parser.""" + container = self._get_argparse_container(parser, group) + kwargs = self._get_argparse_kwargs(group, action='store_false') + prefix = self._get_argparse_prefix('no', group.name if group else None) + deprecated_names = [] + for opt in self.deprecated_opts: + deprecated_name = self._get_deprecated_cli_name(opt.name, + opt.group, + prefix='no') + if deprecated_name is not None: + deprecated_names.append(deprecated_name) + kwargs["help"] = "The inverse of --" + self.name + self._add_to_argparse(parser, container, self.name, None, kwargs, + prefix, self.positional, deprecated_names) + + def _get_argparse_kwargs(self, group, action='store_true', **kwargs): + """Extends the base argparse keyword dict for boolean options.""" + + kwargs = super(BoolOpt, self)._get_argparse_kwargs(group, **kwargs) + # type has no effect for BoolOpt, it only matters for + # values that came from config files + if 'type' in kwargs: + del kwargs['type'] + + # metavar has no effect for BoolOpt + if 'metavar' in kwargs: + del kwargs['metavar'] + + kwargs['action'] = action + + return kwargs + + +class IntOpt(Opt): + + """Opt with Integer type (for backward compatibility).""" + + def __init__(self, name, **kwargs): + super(IntOpt, self).__init__(name, type=types.Integer(), **kwargs) + + +class FloatOpt(Opt): + + """Opt with Float type (for backward compatibility).""" + + def __init__(self, name, **kwargs): + super(FloatOpt, self).__init__(name, type=types.Float(), **kwargs) + + +class ListOpt(Opt): + + """Opt with List(String) type (for backward compatibility).""" + + def __init__(self, name, **kwargs): + super(ListOpt, self).__init__(name, type=types.List(), **kwargs) + + +class DictOpt(Opt): + + """Opt with Dict(String) type (for backward compatibility).""" + + def __init__(self, name, **kwargs): + super(DictOpt, self).__init__(name, type=types.Dict(), **kwargs) + + +class IPOpt(Opt): + + """Opt with IPAddress type (either IPv4, IPv6 or both).""" + + def __init__(self, name, version=None, **kwargs): + super(IPOpt, self).__init__(name, type=types.IPAddress(version), + **kwargs) + + +class MultiOpt(Opt): + + """Multi-value option. + + Multi opt values are typed opts which may be specified multiple times. + The opt value is a list containing all the values specified. + """ + multi = True + + def __init__(self, name, item_type, **kwargs): + super(MultiOpt, self).__init__(name, item_type, **kwargs) + + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for multi value options.""" + kwargs = super(MultiOpt, self)._get_argparse_kwargs(group) + if not self.positional: + kwargs['action'] = 'append' + else: + kwargs['nargs'] = '*' + return kwargs + + +class MultiStrOpt(MultiOpt): + + """Multi opt with MultiString item type (for backward compatibility).""" + + def __init__(self, name, **kwargs): + super(MultiStrOpt, self).__init__(name, + item_type=types.MultiString(), + **kwargs) + + +class SubCommandOpt(Opt): + + """Sub-command options. + + Sub-command options allow argparse sub-parsers to be used to parse + additional command line arguments. + + The handler argument to the SubCommandOpt constructor is a callable + which is supplied an argparse subparsers object. Use this handler + callable to add sub-parsers. + + The opt value is SubCommandAttr object with the name of the chosen + sub-parser stored in the 'name' attribute and the values of other + sub-parser arguments available as additional attributes. + """ + + def __init__(self, name, dest=None, handler=None, + title=None, description=None, help=None): + """Construct an sub-command parsing option. + + This behaves similarly to other Opt sub-classes but adds a + 'handler' argument. The handler is a callable which is supplied + an subparsers object when invoked. The add_parser() method on + this subparsers object can be used to register parsers for + sub-commands. + + :param name: the option's name + :param dest: the name of the corresponding ConfigOpts property + :param title: title of the sub-commands group in help output + :param description: description of the group in help output + :param help: a help string giving an overview of available sub-commands + """ + super(SubCommandOpt, self).__init__(name, type=types.String(), + dest=dest, help=help) + self.handler = handler + self.title = title + self.description = description + + def _add_to_cli(self, parser, group=None): + """Add argparse sub-parsers and invoke the handler method.""" + dest = self.dest + if group is not None: + dest = group.name + '_' + dest + + subparsers = parser.add_subparsers(dest=dest, + title=self.title, + description=self.description, + help=self.help) + # NOTE(jd) Set explicitly to True for Python 3 + # See http://bugs.python.org/issue9253 for context + subparsers.required = True + + if self.handler is not None: + self.handler(subparsers) + + +class _ConfigFileOpt(Opt): + + """The --config-file option. + + This is an private option type which handles the special processing + required for --config-file options. + + As each --config-file option is encountered on the command line, we + parse the file and store the parsed values in the _Namespace object. + This allows us to properly handle the precedence of --config-file + options over previous command line arguments, but not over subsequent + arguments. + """ + + class ConfigFileAction(argparse.Action): + + """An argparse action for --config-file. + + As each --config-file option is encountered, this action adds the + value to the config_file attribute on the _Namespace object but also + parses the configuration file and stores the values found also in + the _Namespace object. + """ + + def __call__(self, parser, namespace, values, option_string=None): + """Handle a --config-file command line argument. + + :raises: ConfigFileParseError, ConfigFileValueError + """ + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, []) + items = getattr(namespace, self.dest) + items.append(values) + + ConfigParser._parse_file(values, namespace) + + def __init__(self, name, **kwargs): + super(_ConfigFileOpt, self).__init__(name, lambda x: x, **kwargs) + + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for the config file opt.""" + kwargs = super(_ConfigFileOpt, self)._get_argparse_kwargs(group) + kwargs['action'] = self.ConfigFileAction + return kwargs + + +class _ConfigDirOpt(Opt): + + """The --config-dir option. + + This is an private option type which handles the special processing + required for --config-dir options. + + As each --config-dir option is encountered on the command line, we + parse the files in that directory and store the parsed values in the + _Namespace object. This allows us to properly handle the precedence of + --config-dir options over previous command line arguments, but not + over subsequent arguments. + """ + + class ConfigDirAction(argparse.Action): + + """An argparse action for --config-dir. + + As each --config-dir option is encountered, this action sets the + config_dir attribute on the _Namespace object but also parses the + configuration files and stores the values found also in the + _Namespace object. + """ + + def __call__(self, parser, namespace, values, option_string=None): + """Handle a --config-dir command line argument. + + :raises: ConfigFileParseError, ConfigFileValueError, + ConfigDirNotFoundError + """ + setattr(namespace, self.dest, values) + + values = os.path.expanduser(values) + + if not os.path.exists(values): + raise ConfigDirNotFoundError(values) + + config_dir_glob = os.path.join(values, '*.conf') + + for config_file in sorted(glob.glob(config_dir_glob)): + ConfigParser._parse_file(config_file, namespace) + + def __init__(self, name, **kwargs): + super(_ConfigDirOpt, self).__init__(name, type=types.String(), + **kwargs) + + def _get_argparse_kwargs(self, group, **kwargs): + """Extends the base argparse keyword dict for the config dir option.""" + kwargs = super(_ConfigDirOpt, self)._get_argparse_kwargs(group) + kwargs['action'] = self.ConfigDirAction + return kwargs + + +class OptGroup(object): + + """Represents a group of opts. + + CLI opts in the group are automatically prefixed with the group name. + + Each group corresponds to a section in config files. + + An OptGroup object has no public methods, but has a number of public string + properties: + + name: + the name of the group + title: + the group title as displayed in --help + help: + the group description as displayed in --help + """ + + def __init__(self, name, title=None, help=None): + """Constructs an OptGroup object. + + :param name: the group name + :param title: the group title for --help + :param help: the group description for --help + """ + self.name = name + self.title = "%s options" % name if title is None else title + self.help = help + + self._opts = {} # dict of dicts of (opt:, override:, default:) + self._argparse_group = None + + def _register_opt(self, opt, cli=False): + """Add an opt to this group. + + :param opt: an Opt object + :param cli: whether this is a CLI option + :returns: False if previously registered, True otherwise + :raises: DuplicateOptError if a naming conflict is detected + """ + if _is_opt_registered(self._opts, opt): + return False + + self._opts[opt.dest] = {'opt': opt, 'cli': cli} + + return True + + def _unregister_opt(self, opt): + """Remove an opt from this group. + + :param opt: an Opt object + """ + if opt.dest in self._opts: + del self._opts[opt.dest] + + def _get_argparse_group(self, parser): + if self._argparse_group is None: + """Build an argparse._ArgumentGroup for this group.""" + self._argparse_group = parser.add_argument_group(self.title, + self.help) + return self._argparse_group + + def _clear(self): + """Clear this group's option parsing state.""" + self._argparse_group = None + + +class ParseError(iniparser.ParseError): + def __init__(self, msg, lineno, line, filename): + super(ParseError, self).__init__(msg, lineno, line) + self.filename = filename + + def __str__(self): + return 'at %s:%d, %s: %r' % (self.filename, self.lineno, + self.msg, self.line) + + +class ConfigParser(iniparser.BaseParser): + def __init__(self, filename, sections): + super(ConfigParser, self).__init__() + self.filename = filename + self.sections = sections + self._normalized = None + self.section = None + + def _add_normalized(self, normalized): + self._normalized = normalized + + def parse(self): + with open(self.filename) as f: + return super(ConfigParser, self).parse(f) + + def new_section(self, section): + self.section = section + self.sections.setdefault(self.section, {}) + + if self._normalized is not None: + self._normalized.setdefault(_normalize_group_name(self.section), + {}) + + def assignment(self, key, value): + if not self.section: + raise self.error_no_section() + + value = '\n'.join(value) + + def append(sections, section): + sections[section].setdefault(key, []) + sections[section][key].append(value) + + append(self.sections, self.section) + if self._normalized is not None: + append(self._normalized, _normalize_group_name(self.section)) + + def parse_exc(self, msg, lineno, line=None): + return ParseError(msg, lineno, line, self.filename) + + def error_no_section(self): + return self.parse_exc('Section must be started before assignment', + self.lineno) + + @classmethod + def _parse_file(cls, config_file, namespace): + """Parse a config file and store any values in the namespace. + + :raises: ConfigFileParseError, ConfigFileValueError + """ + config_file = _fixpath(config_file) + + sections = {} + normalized = {} + parser = cls(config_file, sections) + parser._add_normalized(normalized) + + try: + parser.parse() + except iniparser.ParseError as pe: + raise ConfigFileParseError(pe.filename, str(pe)) + except IOError as err: + if err.errno == errno.ENOENT: + namespace._file_not_found(config_file) + return + if err.errno == errno.EACCES: + namespace._file_permission_denied(config_file) + return + raise + + namespace._add_parsed_config_file(sections, normalized) + + +class MultiConfigParser(object): + def __init__(self): + self.parsed = [] + self._normalized = [] + + def read(self, config_files): + read_ok = [] + + for filename in config_files: + sections = {} + normalized = {} + parser = ConfigParser(filename, sections) + parser._add_normalized(normalized) + + try: + parser.parse() + except IOError: + continue + self._add_parsed_config_file(sections, normalized) + read_ok.append(filename) + + return read_ok + + def _add_parsed_config_file(self, sections, normalized): + """Add a parsed config file to the list of parsed files. + + :param sections: a mapping of section name to dicts of config values + :param normalized: sections mapping with section names normalized + :raises: ConfigFileValueError + """ + self.parsed.insert(0, sections) + self._normalized.insert(0, normalized) + + def get(self, names, multi=False): + return self._get(names, multi=multi) + + def _get(self, names, multi=False, normalized=False): + """Fetch a config file value from the parsed files. + + :param names: a list of (section, name) tuples + :param multi: a boolean indicating whether to return multiple values + :param normalized: whether to normalize group names to lowercase + """ + rvalue = [] + + def normalize(name): + return _normalize_group_name(name) if normalized else name + + names = [(normalize(section), name) for section, name in names] + + for sections in (self._normalized if normalized else self.parsed): + for section, name in names: + if section not in sections: + continue + if name in sections[section]: + val = sections[section][name] + if multi: + rvalue = val + rvalue + else: + return val + if multi and rvalue != []: + return rvalue + raise KeyError + + +class _Namespace(argparse.Namespace): + + """An argparse namespace which also stores config file values. + + As we parse command line arguments, the values get set as attributes + on a namespace object. However, we also want to parse config files as + they are specified on the command line and collect the values alongside + the option values parsed from the command line. + + Note, we don't actually assign values from config files as attributes + on the namespace because config file options be registered after the + command line has been parsed, so we may not know how to properly parse + or convert a config file value at this point. + """ + + def __init__(self, conf): + self._conf = conf + self._parser = MultiConfigParser() + self._files_not_found = [] + self._files_permission_denied = [] + + def _parse_cli_opts_from_config_file(self, sections, normalized): + """Parse CLI options from a config file. + + CLI options are special - we require they be registered before the + command line is parsed. This means that as we parse config files, we + can go ahead and apply the appropriate option-type specific conversion + to the values in config files for CLI options. We can't do this for + non-CLI options, because the schema describing those options may not be + registered until after the config files are parsed. + + This method relies on that invariant in order to enforce proper + priority of option values - i.e. that the order in which an option + value is parsed, whether the value comes from the CLI or a config file, + determines which value specified for a given option wins. + + The way we implement this ordering is that as we parse each config + file, we look for values in that config file for CLI options only. Any + values for CLI options found in the config file are treated like they + had appeared on the command line and set as attributes on the namespace + objects. Values in later config files or on the command line will + override values found in this file. + """ + namespace = _Namespace(self._conf) + namespace._parser._add_parsed_config_file(sections, normalized) + + for opt, group in sorted(self._conf._all_cli_opts()): + group_name = group.name if group is not None else None + try: + value = opt._get_from_namespace(namespace, group_name) + except KeyError: + continue + except ValueError as ve: + raise ConfigFileValueError(str(ve)) + + if group_name is None: + dest = opt.dest + else: + dest = group_name + '_' + opt.dest + + if opt.multi: + if getattr(self, dest, None) is None: + setattr(self, dest, []) + values = getattr(self, dest) + values.extend(value) + else: + setattr(self, dest, value) + + def _add_parsed_config_file(self, sections, normalized): + """Add a parsed config file to the list of parsed files. + + :param sections: a mapping of section name to dicts of config values + :param normalized: sections mapping with section names normalized + :raises: ConfigFileValueError + """ + self._parse_cli_opts_from_config_file(sections, normalized) + self._parser._add_parsed_config_file(sections, normalized) + + def _file_not_found(self, config_file): + """Record that we were unable to open a config file. + + :param config_file: the path to the failed file + """ + self._files_not_found.append(config_file) + + def _file_permission_denied(self, config_file): + """Record that we have no permission to open a config file. + + :param config_file: the path to the failed file + """ + self._files_permission_denied.append(config_file) + + def _get_cli_value(self, names, positional): + """Fetch a CLI option value. + + Look up the value of a CLI option. The value itself may have come from + parsing the command line or parsing config files specified on the + command line. Type conversion have already been performed for CLI + options at this point. + + :param names: a list of (section, name) tuples + :param positional: whether this is a positional option + """ + for group_name, name in names: + name = name if group_name is None else group_name + '_' + name + value = getattr(self, name, None) + if value is not None: + # argparse ignores default=None for nargs='*' and returns [] + if positional and not value: + continue + + return value + + raise KeyError + + def _get_value(self, names, multi, positional): + """Fetch a value from config files. + + Multiple names for a given configuration option may be supplied so + that we can transparently handle files containing deprecated option + names or groups. + + :param names: a list of (section, name) tuples + :param multi: a boolean indicating whether to return multiple values + :param positional: whether this is a positional option + """ + try: + return self._get_cli_value(names, positional) + except KeyError: + pass + + names = [(g if g is not None else 'DEFAULT', n) for g, n in names] + values = self._parser._get(names, multi=multi, normalized=True) + return values if multi else values[-1] + + +class _CachedArgumentParser(argparse.ArgumentParser): + + """class for caching/collecting command line arguments. + + It also sorts the arguments before initializing the ArgumentParser. + We need to do this since ArgumentParser by default does not sort + the argument options and the only way to influence the order of + arguments in '--help' is to ensure they are added in the sorted + order. + """ + + def __init__(self, prog=None, usage=None, **kwargs): + super(_CachedArgumentParser, self).__init__(prog, usage, **kwargs) + self._args_cache = {} + + def add_parser_argument(self, container, *args, **kwargs): + values = [] + if container in self._args_cache: + values = self._args_cache[container] + values.append({'args': args, 'kwargs': kwargs}) + self._args_cache[container] = values + + def initialize_parser_arguments(self): + # NOTE(mfedosin): The code below looks a little bit weird, but + # it's done because we need to sort only optional opts and do + # not touch positional. For the reason optional opts go first in + # the values we only need to find an index of the first positional + # option and then sort the values slice. + for container, values in six.iteritems(self._args_cache): + index = 0 + for index, argument in enumerate(values): + if not argument['args'][0].startswith('-'): + break + values[:index] = sorted(values[:index], key=lambda x: x['args']) + for argument in values: + try: + container.add_argument(*argument['args'], + **argument['kwargs']) + except argparse.ArgumentError as e: + raise DuplicateOptError(e) + self._args_cache = {} + + def parse_args(self, args=None, namespace=None): + self.initialize_parser_arguments() + return super(_CachedArgumentParser, self).parse_args(args, namespace) + + def print_help(self, file=None): + self.initialize_parser_arguments() + super(_CachedArgumentParser, self).print_help(file) + + def print_usage(self, file=None): + self.initialize_parser_arguments() + super(_CachedArgumentParser, self).print_usage(file) + + +class ConfigOpts(collections.Mapping): + + """Config options which may be set on the command line or in config files. + + ConfigOpts is a configuration option manager with APIs for registering + option schemas, grouping options, parsing option values and retrieving + the values of options. + """ + + def __init__(self): + """Construct a ConfigOpts object.""" + self._opts = {} # dict of dicts of (opt:, override:, default:) + self._groups = {} + + self._args = None + + self._oparser = None + self._namespace = None + self.__cache = {} + self._config_opts = [] + self._cli_opts = collections.deque() + self._validate_default_values = False + + def _pre_setup(self, project, prog, version, usage, default_config_files): + """Initialize a ConfigCliParser object for option parsing.""" + + if prog is None: + prog = os.path.basename(sys.argv[0]) + + if default_config_files is None: + default_config_files = find_config_files(project, prog) + + self._oparser = _CachedArgumentParser(prog=prog, usage=usage) + self._oparser.add_parser_argument(self._oparser, + '--version', + action='version', + version=version) + + return prog, default_config_files + + def _setup(self, project, prog, version, usage, default_config_files): + """Initialize a ConfigOpts object for option parsing.""" + + self._config_opts = [ + _ConfigFileOpt('config-file', + default=default_config_files, + metavar='PATH', + help=('Path to a config file to use. Multiple ' + 'config files can be specified, with values ' + 'in later files taking precedence. The ' + 'default files used are: %(default)s.')), + _ConfigDirOpt('config-dir', + metavar='DIR', + help='Path to a config directory to pull *.conf ' + 'files from. This file set is sorted, so as to ' + 'provide a predictable parse order if ' + 'individual options are over-ridden. The set ' + 'is parsed after the file(s) specified via ' + 'previous --config-file, arguments hence ' + 'over-ridden options in the directory take ' + 'precedence.'), + ] + self.register_cli_opts(self._config_opts) + + self.project = project + self.prog = prog + self.version = version + self.usage = usage + self.default_config_files = default_config_files + + def __clear_cache(f): + @functools.wraps(f) + def __inner(self, *args, **kwargs): + if kwargs.pop('clear_cache', True): + result = f(self, *args, **kwargs) + self.__cache.clear() + return result + else: + return f(self, *args, **kwargs) + + return __inner + + def __call__(self, + args=None, + project=None, + prog=None, + version=None, + usage=None, + default_config_files=None, + validate_default_values=False): + """Parse command line arguments and config files. + + Calling a ConfigOpts object causes the supplied command line arguments + and config files to be parsed, causing opt values to be made available + as attributes of the object. + + The object may be called multiple times, each time causing the previous + set of values to be overwritten. + + Automatically registers the --config-file option with either a supplied + list of default config files, or a list from find_config_files(). + + If the --config-dir option is set, any *.conf files from this + directory are pulled in, after all the file(s) specified by the + --config-file option. + + :param args: command line arguments (defaults to sys.argv[1:]) + :param project: the toplevel project name, used to locate config files + :param prog: the name of the program (defaults to sys.argv[0] basename) + :param version: the program version (for --version) + :param usage: a usage string (%prog will be expanded) + :param default_config_files: config files to use by default + :param validate_default_values: whether to validate the default values + :returns: the list of arguments left over after parsing options + :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, + ConfigFilesPermissionDeniedError, + RequiredOptError, DuplicateOptError + """ + self.clear() + + self._validate_default_values = validate_default_values + + prog, default_config_files = self._pre_setup(project, + prog, + version, + usage, + default_config_files) + + self._setup(project, prog, version, usage, default_config_files) + + self._namespace = self._parse_cli_opts(args if args is not None + else sys.argv[1:]) + if self._namespace._files_not_found: + raise ConfigFilesNotFoundError(self._namespace._files_not_found) + if self._namespace._files_permission_denied: + raise ConfigFilesPermissionDeniedError( + self._namespace._files_permission_denied) + + self._check_required_opts() + + def __getattr__(self, name): + """Look up an option value and perform string substitution. + + :param name: the opt name (or 'dest', more precisely) + :returns: the option value (after string substitution) or a GroupAttr + :raises: NoSuchOptError + """ + try: + return self._get(name) + except Exception: + raise NoSuchOptError(name) + + def __getitem__(self, key): + """Look up an option value and perform string substitution.""" + return self.__getattr__(key) + + def __contains__(self, key): + """Return True if key is the name of a registered opt or group.""" + return key in self._opts or key in self._groups + + def __iter__(self): + """Iterate over all registered opt and group names.""" + for key in itertools.chain(self._opts.keys(), self._groups.keys()): + yield key + + def __len__(self): + """Return the number of options and option groups.""" + return len(self._opts) + len(self._groups) + + def reset(self): + """Clear the object state and unset overrides and defaults.""" + self._unset_defaults_and_overrides() + self.clear() + + @__clear_cache + def clear(self): + """Clear the state of the object to before it was called. + + Any subparsers added using the add_cli_subparsers() will also be + removed as a side-effect of this method. + """ + self._args = None + self._oparser = None + self._namespace = None + self._validate_default_values = False + self.unregister_opts(self._config_opts) + for group in self._groups.values(): + group._clear() + + def _add_cli_opt(self, opt, group): + if {'opt': opt, 'group': group} in self._cli_opts: + return + if opt.positional: + self._cli_opts.append({'opt': opt, 'group': group}) + else: + self._cli_opts.appendleft({'opt': opt, 'group': group}) + + @__clear_cache + def register_opt(self, opt, group=None, cli=False): + """Register an option schema. + + Registering an option schema makes any option value which is previously + or subsequently parsed from the command line or config files available + as an attribute of this object. + + :param opt: an instance of an Opt sub-class + :param cli: whether this is a CLI option + :param group: an optional OptGroup object or group name + :return: False if the opt was already registered, True otherwise + :raises: DuplicateOptError + """ + if group is not None: + group = self._get_group(group, autocreate=True) + if cli: + self._add_cli_opt(opt, group) + return group._register_opt(opt, cli) + + if cli: + self._add_cli_opt(opt, None) + + if _is_opt_registered(self._opts, opt): + return False + + self._opts[opt.dest] = {'opt': opt, 'cli': cli} + + return True + + @__clear_cache + def register_opts(self, opts, group=None): + """Register multiple option schemas at once.""" + for opt in opts: + self.register_opt(opt, group, clear_cache=False) + + @__clear_cache + def register_cli_opt(self, opt, group=None): + """Register a CLI option schema. + + CLI option schemas must be registered before the command line and + config files are parsed. This is to ensure that all CLI options are + shown in --help and option validation works as expected. + + :param opt: an instance of an Opt sub-class + :param group: an optional OptGroup object or group name + :return: False if the opt was already registered, True otherwise + :raises: DuplicateOptError, ArgsAlreadyParsedError + """ + if self._args is not None: + raise ArgsAlreadyParsedError("cannot register CLI option") + + return self.register_opt(opt, group, cli=True, clear_cache=False) + + @__clear_cache + def register_cli_opts(self, opts, group=None): + """Register multiple CLI option schemas at once.""" + for opt in opts: + self.register_cli_opt(opt, group, clear_cache=False) + + def register_group(self, group): + """Register an option group. + + An option group must be registered before options can be registered + with the group. + + :param group: an OptGroup object + """ + if group.name in self._groups: + return + + self._groups[group.name] = copy.copy(group) + + @__clear_cache + def unregister_opt(self, opt, group=None): + """Unregister an option. + + :param opt: an Opt object + :param group: an optional OptGroup object or group name + :raises: ArgsAlreadyParsedError, NoSuchGroupError + """ + if self._args is not None: + raise ArgsAlreadyParsedError("reset before unregistering options") + + if {'opt': opt, 'group': group} in self._cli_opts: + self._cli_opts.remove({'opt': opt, 'group': group}) + + if group is not None: + self._get_group(group)._unregister_opt(opt) + elif opt.dest in self._opts: + del self._opts[opt.dest] + + @__clear_cache + def unregister_opts(self, opts, group=None): + """Unregister multiple CLI option schemas at once.""" + for opt in opts: + self.unregister_opt(opt, group, clear_cache=False) + + def import_opt(self, name, module_str, group=None): + """Import an option definition from a module. + + Import a module and check that a given option is registered. + + This is intended for use with global configuration objects + like cfg.CONF where modules commonly register options with + CONF at module load time. If one module requires an option + defined by another module it can use this method to explicitly + declare the dependency. + + :param name: the name/dest of the opt + :param module_str: the name of a module to import + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + __import__(module_str) + self._get_opt_info(name, group) + + def import_group(self, group, module_str): + """Import an option group from a module. + + Import a module and check that a given option group is registered. + + This is intended for use with global configuration objects + like cfg.CONF where modules commonly register options with + CONF at module load time. If one module requires an option group + defined by another module it can use this method to explicitly + declare the dependency. + + :param group: an option OptGroup object or group name + :param module_str: the name of a module to import + :raises: ImportError, NoSuchGroupError + """ + __import__(module_str) + self._get_group(group) + + @__clear_cache + def set_override(self, name, override, group=None): + """Override an opt value. + + Override the command line, config file and default values of a + given option. + + :param name: the name/dest of the opt + :param override: the override value + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + opt_info = self._get_opt_info(name, group) + opt_info['override'] = override + + @__clear_cache + def set_default(self, name, default, group=None): + """Override an opt's default value. + + Override the default value of given option. A command line or + config file value will still take precedence over this default. + + :param name: the name/dest of the opt + :param default: the default value + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + opt_info = self._get_opt_info(name, group) + opt_info['default'] = default + + @__clear_cache + def clear_override(self, name, group=None): + """Clear an override an opt value. + + Clear a previously set override of the command line, config file + and default values of a given option. + + :param name: the name/dest of the opt + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + opt_info = self._get_opt_info(name, group) + opt_info.pop('override', None) + + @__clear_cache + def clear_default(self, name, group=None): + """Clear an override an opt's default value. + + Clear a previously set override of the default value of given option. + + :param name: the name/dest of the opt + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + opt_info = self._get_opt_info(name, group) + opt_info.pop('default', None) + + def _all_opt_infos(self): + """A generator function for iteration opt infos.""" + for info in self._opts.values(): + yield info, None + for group in self._groups.values(): + for info in group._opts.values(): + yield info, group + + def _all_cli_opts(self): + """A generator function for iterating CLI opts.""" + for item in self._cli_opts: + yield item['opt'], item['group'] + + def _unset_defaults_and_overrides(self): + """Unset any default or override on all options.""" + for info, group in self._all_opt_infos(): + info.pop('default', None) + info.pop('override', None) + + def find_file(self, name): + """Locate a file located alongside the config files. + + Search for a file with the supplied basename in the directories + which we have already loaded config files from and other known + configuration directories. + + The directory, if any, supplied by the config_dir option is + searched first. Then the config_file option is iterated over + and each of the base directories of the config_files values + are searched. Failing both of these, the standard directories + searched by the module level find_config_files() function is + used. The first matching file is returned. + + :param name: the filename, for example 'policy.json' + :returns: the path to a matching file, or None + """ + dirs = [] + if self.config_dir: + dirs.append(_fixpath(self.config_dir)) + + for cf in reversed(self.config_file): + dirs.append(os.path.dirname(_fixpath(cf))) + + dirs.extend(_get_config_dirs(self.project)) + + return _search_dirs(dirs, name) + + def log_opt_values(self, logger, lvl): + """Log the value of all registered opts. + + It's often useful for an app to log its configuration to a log file at + startup for debugging. This method dumps to the entire config state to + the supplied logger at a given log level. + + :param logger: a logging.Logger object + :param lvl: the log level (for example logging.DEBUG) arg to + logger.log() + """ + logger.log(lvl, "*" * 80) + logger.log(lvl, "Configuration options gathered from:") + logger.log(lvl, "command line args: %s", self._args) + logger.log(lvl, "config files: %s", self.config_file) + logger.log(lvl, "=" * 80) + + def _sanitize(opt, value): + """Obfuscate values of options declared secret.""" + return value if not opt.secret else '*' * 4 + + for opt_name in sorted(self._opts): + opt = self._get_opt_info(opt_name)['opt'] + logger.log(lvl, "%-30s = %s", opt_name, + _sanitize(opt, getattr(self, opt_name))) + + for group_name in self._groups: + group_attr = self.GroupAttr(self, self._get_group(group_name)) + for opt_name in sorted(self._groups[group_name]._opts): + opt = self._get_opt_info(opt_name, group_name)['opt'] + logger.log(lvl, "%-30s = %s", + "%s.%s" % (group_name, opt_name), + _sanitize(opt, getattr(group_attr, opt_name))) + + logger.log(lvl, "*" * 80) + + def print_usage(self, file=None): + """Print the usage message for the current program. + + This method is for use after all CLI options are known + registered using __call__() method. If this method is called + before the __call__() is invoked, it throws NotInitializedError + + :param file: the File object (if None, output is on sys.stdout) + :raises: NotInitializedError + """ + if not self._oparser: + raise NotInitializedError() + self._oparser.print_usage(file) + + def print_help(self, file=None): + """Print the help message for the current program. + + This method is for use after all CLI options are known + registered using __call__() method. If this method is called + before the __call__() is invoked, it throws NotInitializedError + + :param file: the File object (if None, output is on sys.stdout) + :raises: NotInitializedError + """ + if not self._oparser: + raise NotInitializedError() + self._oparser.print_help(file) + + def _get(self, name, group=None, namespace=None): + if isinstance(group, OptGroup): + key = (group.name, name) + else: + key = (group, name) + try: + if namespace is not None: + raise KeyError + + return self.__cache[key] + except KeyError: + value = self._do_get(name, group, namespace) + self.__cache[key] = value + return value + + def _do_get(self, name, group=None, namespace=None): + """Look up an option value. + + :param name: the opt name (or 'dest', more precisely) + :param group: an OptGroup + :param namespace: the namespace object that retrieves the option + value from + :returns: the option value, or a GroupAttr object + :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, + TemplateSubstitutionError + """ + if group is None and name in self._groups: + return self.GroupAttr(self, self._get_group(name)) + + info = self._get_opt_info(name, group) + opt = info['opt'] + + if isinstance(opt, SubCommandOpt): + return self.SubCommandAttr(self, group, opt.dest) + + if 'override' in info: + return self._substitute(info['override']) + + if namespace is None: + namespace = self._namespace + + def convert(value): + return self._convert_value( + self._substitute(value, group, namespace), opt) + + if namespace is not None: + group_name = group.name if group else None + try: + return convert(opt._get_from_namespace(namespace, group_name)) + except KeyError: + pass + except ValueError as ve: + raise ConfigFileValueError(str(ve)) + + if 'default' in info: + return self._substitute(info['default']) + + if self._validate_default_values: + if opt.default is not None: + try: + convert(opt.default) + except ValueError as e: + raise ConfigFileValueError( + "Default value for option %s is not valid: %s" + % (opt.name, str(e))) + + if opt.default is not None: + return convert(opt.default) + + return None + + def _substitute(self, value, group=None, namespace=None): + """Perform string template substitution. + + Substitute any template variables (for example $foo, ${bar}) in + the supplied string value(s) with opt values. + + :param value: the string value, or list of string values + :param group: the group that retrieves the option value from + :param namespace: the namespace object that retrieves the option + value from + :returns: the substituted string(s) + """ + if isinstance(value, list): + return [self._substitute(i, group=group, namespace=namespace) + for i in value] + elif isinstance(value, str): + # Treat a backslash followed by the dollar sign "\$" + # the same as the string template escape "$$" as it is + # a bit more natural for users + if '\$' in value: + value = value.replace('\$', '$$') + tmpl = string.Template(value) + ret = tmpl.safe_substitute( + self.StrSubWrapper(self, group=group, namespace=namespace)) + return ret + else: + return value + + def _convert_value(self, value, opt): + """Perform value type conversion. + + Converts values using option's type. Handles cases when value is + actually a list of values (for example for multi opts). + + :param value: the string value, or list of string values + :param opt: option definition (instance of Opt class or its subclasses) + :returns: converted value + """ + if opt.multi: + return [opt.type(v) for v in value] + else: + return opt.type(value) + + def _get_group(self, group_or_name, autocreate=False): + """Looks up a OptGroup object. + + Helper function to return an OptGroup given a parameter which can + either be the group's name or an OptGroup object. + + The OptGroup object returned is from the internal dict of OptGroup + objects, which will be a copy of any OptGroup object that users of + the API have access to. + + If autocreate is True, the group will be created if it's not found. If + group is an instance of OptGroup, that same instance will be + registered, otherwise a new instance of OptGroup will be created. + + :param group_or_name: the group's name or the OptGroup object itself + :param autocreate: whether to auto-create the group if it's not found + :raises: NoSuchGroupError + """ + group = group_or_name if isinstance(group_or_name, OptGroup) else None + group_name = group.name if group else group_or_name + + if group_name not in self._groups: + if not autocreate: + raise NoSuchGroupError(group_name) + + self.register_group(group or OptGroup(name=group_name)) + + return self._groups[group_name] + + def _get_opt_info(self, opt_name, group=None): + """Return the (opt, override, default) dict for an opt. + + :param opt_name: an opt name/dest + :param group: an optional group name or OptGroup object + :raises: NoSuchOptError, NoSuchGroupError + """ + if group is None: + opts = self._opts + else: + group = self._get_group(group) + opts = group._opts + + if opt_name not in opts: + raise NoSuchOptError(opt_name, group) + + return opts[opt_name] + + def _check_required_opts(self, namespace=None): + """Check that all opts marked as required have values specified. + + :param namespace: the namespace object be checked the required options + :raises: RequiredOptError + """ + for info, group in self._all_opt_infos(): + opt = info['opt'] + + if opt.required: + if 'default' in info or 'override' in info: + continue + + if self._get(opt.dest, group, namespace) is None: + raise RequiredOptError(opt.name, group) + + def _parse_cli_opts(self, args): + """Parse command line options. + + Initializes the command line option parser and parses the supplied + command line arguments. + + :param args: the command line arguments + :returns: a _Namespace object containing the parsed option values + :raises: SystemExit, DuplicateOptError + ConfigFileParseError, ConfigFileValueError + + """ + self._args = args + for opt, group in self._all_cli_opts(): + opt._add_to_cli(self._oparser, group) + + return self._parse_config_files() + + def _parse_config_files(self): + """Parse configure files options. + + :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError, + ConfigFilesPermissionDeniedError, + RequiredOptError, DuplicateOptError + """ + namespace = _Namespace(self) + for arg in self._args: + if arg == '--config-file' or arg.startswith('--config-file='): + break + else: + for config_file in self.default_config_files: + ConfigParser._parse_file(config_file, namespace) + + self._oparser.parse_args(self._args, namespace) + + self._validate_cli_options(namespace) + + return namespace + + def _validate_cli_options(self, namespace): + for opt, group in sorted(self._all_cli_opts(), + key=lambda x: x[0].name): + group_name = group.name if group else None + try: + value = opt._get_from_namespace(namespace, group_name) + except KeyError: + continue + + value = self._substitute(value, group=group, namespace=namespace) + + try: + self._convert_value(value, opt) + except ValueError: + sys.stderr.write("argument --%s: Invalid %s value: %s\n" % ( + opt.dest, repr(opt.type), value)) + raise SystemExit + + @__clear_cache + def reload_config_files(self): + """Reload configure files and parse all options + + :return False if reload configure files failed or else return True + """ + try: + namespace = self._parse_config_files() + if namespace._files_not_found: + raise ConfigFilesNotFoundError(namespace._files_not_found) + if namespace._files_permission_denied: + raise ConfigFilesPermissionDeniedError( + namespace._files_permission_denied) + self._check_required_opts(namespace) + + except SystemExit as exc: + LOG.warn("Caught SystemExit while reloading configure files " + "with exit code: %d", exc.code) + return False + except Error as err: + LOG.warn("Caught Error while reloading configure files: %s", + err) + return False + else: + self._namespace = namespace + return True + + class GroupAttr(collections.Mapping): + + """Helper class. + + Represents the option values of a group as a mapping and attributes. + """ + + def __init__(self, conf, group): + """Construct a GroupAttr object. + + :param conf: a ConfigOpts object + :param group: an OptGroup object + """ + self._conf = conf + self._group = group + + def __getattr__(self, name): + """Look up an option value and perform template substitution.""" + return self._conf._get(name, self._group) + + def __getitem__(self, key): + """Look up an option value and perform string substitution.""" + return self.__getattr__(key) + + def __contains__(self, key): + """Return True if key is the name of a registered opt or group.""" + return key in self._group._opts + + def __iter__(self): + """Iterate over all registered opt and group names.""" + for key in self._group._opts.keys(): + yield key + + def __len__(self): + """Return the number of options and option groups.""" + return len(self._group._opts) + + class SubCommandAttr(object): + + """Helper class. + + Represents the name and arguments of an argparse sub-parser. + """ + + def __init__(self, conf, group, dest): + """Construct a SubCommandAttr object. + + :param conf: a ConfigOpts object + :param group: an OptGroup object + :param dest: the name of the sub-parser + """ + self._conf = conf + self._group = group + self._dest = dest + + def __getattr__(self, name): + """Look up a sub-parser name or argument value.""" + if name == 'name': + name = self._dest + if self._group is not None: + name = self._group.name + '_' + name + return getattr(self._conf._namespace, name) + + if name in self._conf: + raise DuplicateOptError(name) + + try: + return getattr(self._conf._namespace, name) + except AttributeError: + raise NoSuchOptError(name) + + class StrSubWrapper(object): + + """Helper class. + + Exposes opt values as a dict for string substitution. + """ + + def __init__(self, conf, group=None, namespace=None): + """Construct a StrSubWrapper object. + + :param conf: a ConfigOpts object + """ + self.conf = conf + self.namespace = namespace + self.group = group + + def __getitem__(self, key): + """Look up an opt value from the ConfigOpts object. + + :param key: an opt name + :returns: an opt value + :raises: TemplateSubstitutionError if attribute is a group + """ + try: + value = self.conf._get(key, group=self.group, + namespace=self.namespace) + except NoSuchOptError: + value = self.conf._get(key, namespace=self.namespace) + if isinstance(value, self.conf.GroupAttr): + raise TemplateSubstitutionError( + 'substituting group %s not supported' % key) + return value + + +CONF = ConfigOpts() diff --git a/oslo_config/cfgfilter.py b/oslo_config/cfgfilter.py new file mode 100644 index 00000000..6efe9a76 --- /dev/null +++ b/oslo_config/cfgfilter.py @@ -0,0 +1,318 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +r""" +There are two use cases for the ConfigFilter class: + +1. Help enforce that a given module does not access options registered + by another module, without first declaring those cross-module + dependencies using import_opt(). + +2. Prevent private configuration opts from being visible to modules + other than the one which registered it. + +Cross-Module Option Dependencies +-------------------------------- + +When using the global cfg.CONF object, it is quite common for a module +to require the existence of configuration options registered by other +modules. + +For example, if module 'foo' registers the 'blaa' option and the module +'bar' uses the 'blaa' option then 'bar' might do:: + + import foo + + print(CONF.blaa) + +However, it's completely non-obvious why foo is being imported (is it +unused, can we remove the import) and where the 'blaa' option comes from. + +The CONF.import_opt() method allows such a dependency to be explicitly +declared:: + + CONF.import_opt('blaa', 'foo') + print(CONF.blaa) + +However, import_opt() has a weakness - if 'bar' imports 'foo' using the +import builtin and doesn't use import_opt() to import 'blaa', then 'blaa' +can still be used without problems. Similarly, where multiple options +are registered a module imported via importopt(), a lazy programmer can +get away with only declaring a dependency on a single option. + +The ConfigFilter class provides a way to ensure that options are not +available unless they have been registered in the module or imported using +import_opt() for example with:: + + CONF = ConfigFilter(cfg.CONF) + CONF.import_opt('blaa', 'foo') + print(CONF.blaa) + +no other options other than 'blaa' are available via CONF. + +Private Configuration Options +----------------------------- + +Libraries which register configuration options typically do not want +users of the library API to access those configuration options. If +API users do access private configuration options, those users will +be disrupted if and when a configuration option is renamed. In other +words, one does not typically wish for the name of the private config +options to be part of the public API. + +The ConfigFilter class provides a way for a library to register +options such that they are not visible via the ConfigOpts instance +which the API user supplies to the library. For example:: + + from __future__ import print_function + + from oslo.config.cfg import * + from oslo.config.cfgfilter import * + + class Widget(object): + + def __init__(self, conf): + self.conf = conf + self._private_conf = ConfigFilter(self.conf) + self._private_conf.register_opt(StrOpt('foo')) + + @property + def foo(self): + return self._private_conf.foo + + conf = ConfigOpts() + widget = Widget(conf) + print(widget.foo) + print(conf.foo) # raises NoSuchOptError + +""" + +import collections +import itertools + +from oslo.config import cfg + + +class ConfigFilter(collections.Mapping): + + """A helper class which wraps a ConfigOpts object. + + ConfigFilter enforces the explicit declaration of dependencies on external + options and allows private options which are not registered with the + wrapped Configopts object. + """ + + def __init__(self, conf): + """Construct a ConfigFilter object. + + :param conf: a ConfigOpts object + """ + self._conf = conf + self._fconf = cfg.ConfigOpts() + self._sync() + + self._imported_opts = set() + self._imported_groups = dict() + + def _sync(self): + if self._fconf._namespace is not self._conf._namespace: + self._fconf.clear() + self._fconf._namespace = self._conf._namespace + self._fconf._args = self._conf._args + + def __getattr__(self, name): + """Look up an option value. + + :param name: the opt name (or 'dest', more precisely) + :returns: the option value (after string subsititution) or a GroupAttr + :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError + """ + if name in self._imported_groups: + return self._imported_groups[name] + elif name in self._imported_opts: + return getattr(self._conf, name) + else: + self._sync() + return getattr(self._fconf, name) + + def __getitem__(self, key): + """Look up an option value.""" + return getattr(self, key) + + def __contains__(self, key): + """Return True if key is the name of a registered opt or group.""" + return (key in self._fconf or + key in self._imported_opts or + key in self._imported_groups) + + def __iter__(self): + """Iterate over all registered opt and group names.""" + return itertools.chain(self._fconf.keys(), + self._imported_opts, + self._imported_groups.keys()) + + def __len__(self): + """Return the number of options and option groups.""" + return (len(self._fconf) + + len(self._imported_opts) + + len(self._imported_groups)) + + @staticmethod + def _already_registered(conf, opt, group=None): + group_name = group.name if isinstance(group, cfg.OptGroup) else group + return ((group_name is None and + opt.dest in conf) or + (group_name is not None and + group_name in conf and + opt.dest in conf[group_name])) + + def register_opt(self, opt, group=None): + """Register an option schema. + + :param opt: an instance of an Opt sub-class + :param group: an optional OptGroup object or group name + :return: False if the opt was already registered, True otherwise + :raises: DuplicateOptError + """ + if self._already_registered(self._conf, opt, group): + # Raises DuplicateError if there is another opt with the same name + ret = self._conf.register_opt(opt, group) + self._import_opt(opt.dest, group) + return ret + else: + return self._fconf.register_opt(opt, group) + + def register_opts(self, opts, group=None): + """Register multiple option schemas at once.""" + for opt in opts: + self.register_opt(opt, group) + + def register_cli_opt(self, opt, group=None): + """Register a CLI option schema. + + :param opt: an instance of an Opt sub-class + :param group: an optional OptGroup object or group name + :return: False if the opt was already register, True otherwise + :raises: DuplicateOptError, ArgsAlreadyParsedError + """ + if self._already_registered(self._conf, opt, group): + # Raises DuplicateError if there is another opt with the same name + ret = self._conf.register_cli_opt(opt, group) + self._import_opt(opt.dest, group) + return ret + else: + return self._fconf.register_cli_opt(opt, group) + + def register_cli_opts(self, opts, group=None): + """Register multiple CLI option schemas at once.""" + for opt in opts: + self.register_cli_opt(opt, group) + + def register_group(self, group): + """Register an option group. + + :param group: an OptGroup object + """ + self._fconf.register_group(group) + + def import_opt(self, opt_name, module_str, group=None): + """Import an option definition from a module. + + :param name: the name/dest of the opt + :param module_str: the name of a module to import + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + self._conf.import_opt(opt_name, module_str, group) + self._import_opt(opt_name, group) + + def import_group(self, group, module_str): + """Import an option group from a module. + + Note that this allows access to all options registered with + the group whether or not those options were registered by + the given module. + + :param group: an option OptGroup object or group name + :param module_str: the name of a module to import + :raises: ImportError, NoSuchGroupError + """ + self._conf.import_group(group, module_str) + group = self._import_group(group) + group._all_opts = True + + def _import_opt(self, opt_name, group): + if group is None: + self._imported_opts.add(opt_name) + return True + else: + group = self._import_group(group) + return group._import_opt(opt_name) + + def _import_group(self, group_or_name): + if isinstance(group_or_name, cfg.OptGroup): + group_name = group_or_name.name + else: + group_name = group_or_name + + if group_name in self._imported_groups: + return self._imported_groups[group_name] + else: + group = self.GroupAttr(self._conf, group_name) + self._imported_groups[group_name] = group + return group + + class GroupAttr(collections.Mapping): + + """Helper class to wrap a group object. + + Represents the option values of a group as a mapping and attributes. + """ + + def __init__(self, conf, group): + """Construct a GroupAttr object. + + :param conf: a ConfigOpts object + :param group: an OptGroup object + """ + self._conf = conf + self._group = group + self._imported_opts = set() + self._all_opts = False + + def __getattr__(self, name): + """Look up an option value.""" + if not self._all_opts and name not in self._imported_opts: + raise cfg.NoSuchOptError(name) + return getattr(self._conf[self._group], name) + + def __getitem__(self, key): + """Look up an option value.""" + return getattr(self, key) + + def __contains__(self, key): + """Return True if key is the name of a registered opt or group.""" + return key in self._imported_opts + + def __iter__(self): + """Iterate over all registered opt and group names.""" + for key in self._imported_opts: + yield key + + def __len__(self): + """Return the number of options and option groups.""" + return len(self._imported_opts) + + def _import_opt(self, opt_name): + self._imported_opts.add(opt_name) diff --git a/oslo_config/fixture.py b/oslo_config/fixture.py new file mode 100644 index 00000000..2bb6cf8e --- /dev/null +++ b/oslo_config/fixture.py @@ -0,0 +1,118 @@ +# +# Copyright 2013 Mirantis, Inc. +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import fixtures +import six + +from oslo.config import cfg + + +class Config(fixtures.Fixture): + """Allows overriding configuration settings for the test. + + `conf` will be reset on cleanup. + + """ + + def __init__(self, conf=cfg.CONF): + self.conf = conf + + def setUp(self): + super(Config, self).setUp() + # NOTE(morganfainberg): unregister must be added to cleanup before + # reset is because cleanup works in reverse order of registered items, + # and a reset must occur before unregistering options can occur. + self.addCleanup(self._unregister_config_opts) + self.addCleanup(self.conf.reset) + self._registered_config_opts = {} + + def config(self, **kw): + """Override configuration values. + + The keyword arguments are the names of configuration options to + override and their values. + + If a `group` argument is supplied, the overrides are applied to + the specified configuration option group, otherwise the overrides + are applied to the ``default`` group. + + """ + + group = kw.pop('group', None) + for k, v in six.iteritems(kw): + self.conf.set_override(k, v, group) + + def _unregister_config_opts(self): + for group in self._registered_config_opts: + self.conf.unregister_opts(self._registered_config_opts[group], + group=group) + + def register_opt(self, opt, group=None): + """Register a single option for the test run. + + Options registered in this manner will automatically be unregistered + during cleanup. + + If a `group` argument is supplied, it will register the new option + to that group, otherwise the option is registered to the ``default`` + group. + """ + self.conf.register_opt(opt, group=group) + self._registered_config_opts.setdefault(group, set()).add(opt) + + def register_opts(self, opts, group=None): + """Register multiple options for the test run. + + This works in the same manner as register_opt() but takes a list of + options as the first argument. All arguments will be registered to the + same group if the ``group`` argument is supplied, otherwise all options + will be registered to the ``default`` group. + """ + for opt in opts: + self.register_opt(opt, group=group) + + def register_cli_opt(self, opt, group=None): + """Register a single CLI option for the test run. + + Options registered in this manner will automatically be unregistered + during cleanup. + + If a `group` argument is supplied, it will register the new option + to that group, otherwise the option is registered to the ``default`` + group. + + CLI options must be registered before the command line and config files + are parsed. This is to ensure that all CLI options are shown in --help + and option validation works as expected. + """ + self.conf.register_cli_opt(opt, group=group) + self._registered_config_opts.setdefault(group, set()).add(opt) + + def register_cli_opts(self, opts, group=None): + """Register multiple CLI options for the test run. + + This works in the same manner as register_opt() but takes a list of + options as the first argument. All arguments will be registered to the + same group if the ``group`` argument is supplied, otherwise all options + will be registered to the ``default`` group. + + CLI options must be registered before the command line and config files + are parsed. This is to ensure that all CLI options are shown in --help + and option validation works as expected. + """ + for opt in opts: + self.register_cli_opt(opt, group=group) diff --git a/oslo_config/generator.py b/oslo_config/generator.py new file mode 100644 index 00000000..93594e05 --- /dev/null +++ b/oslo_config/generator.py @@ -0,0 +1,312 @@ +# Copyright 2012 SINA Corporation +# Copyright 2014 Cisco Systems, Inc. +# All Rights Reserved. +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +r""" +A sample configuration file generator. + +oslo-config-generator is a utility for generating sample config files. For +example, to generate a sample config file for oslo.messaging you would run:: + + $> oslo-config-generator --namespace oslo.messaging > oslo.messaging.conf + +This generated sample lists all of the available options, along with their help +string, type, deprecated aliases and defaults. + +The --namespace option specifies an entry point name registered under the +'oslo.config.opts' entry point namespace. For example, in oslo.messaging's +setup.cfg we have:: + + [entry_points] + oslo.config.opts = + oslo.messaging = oslo.messaging.opts:list_opts + +The callable referenced by the entry point should take no arguments and return +a list of (group_name, [opt_1, opt_2]) tuples. For example:: + + opts = [ + cfg.StrOpt('foo'), + cfg.StrOpt('bar'), + ] + + cfg.CONF.register_opts(opts, group='blaa') + + def list_opts(): + return [('blaa', opts)] + +You might choose to return a copy of the options so that the return value can't +be modified for nefarious purposes:: + + def list_opts(): + return [('blaa', copy.deepcopy(opts))] + +A single codebase might have multiple programs, each of which use a subset of +the total set of options registered by the codebase. In that case, you can +register multiple entry points:: + + [entry_points] + oslo.config.opts = + nova.common = nova.config:list_common_opts + nova.api = nova.config:list_api_opts + nova.compute = nova.config:list_compute_opts + +and generate a config file specific to each program:: + + $> oslo-config-generator --namespace oslo.messaging \ + --namespace nova.common \ + --namespace nova.api > nova-api.conf + $> oslo-config-generator --namespace oslo.messaging \ + --namespace nova.common \ + --namespace nova.compute > nova-compute.conf + +To make this more convenient, you can use config files to describe your config +files:: + + $> cat > config-generator/api.conf < cat > config-generator/compute.conf < oslo-config-generator --config-file config-generator/api.conf + $> oslo-config-generator --config-file config-generator/compute.conf + +The default runtime values of configuration options are not always the most +suitable values to include in sample config files - for example, rather than +including the IP address or hostname of the machine where the config file +was generated, you might want to include something like '10.0.0.1'. To +facilitate this, options can be supplied with a 'sample_default' attribute:: + + cfg.StrOpt('base_dir' + default=os.getcwd(), + sample_default='/usr/lib/myapp') +""" + +import logging +import operator +import sys +import textwrap + +from oslo.config import cfg +import stevedore.named # noqa + +LOG = logging.getLogger(__name__) + +_generator_opts = [ + cfg.StrOpt('output-file', + help='Path of the file to write to. Defaults to stdout.'), + cfg.IntOpt('wrap-width', + default=70, + help='The maximum length of help lines.'), + cfg.MultiStrOpt('namespace', + help='Option namespace under "oslo.config.opts" in which ' + 'to query for options.'), +] + + +def register_cli_opts(conf): + """Register the formatter's CLI options with a ConfigOpts instance. + + Note, this must be done before the ConfigOpts instance is called to parse + the configuration. + + :param conf: a ConfigOpts instance + :raises: DuplicateOptError, ArgsAlreadyParsedError + """ + conf.register_cli_opts(_generator_opts) + + +class _OptFormatter(object): + + """Format configuration option descriptions to a file.""" + + _TYPE_DESCRIPTIONS = { + cfg.StrOpt: 'string value', + cfg.BoolOpt: 'boolean value', + cfg.IntOpt: 'integer value', + cfg.FloatOpt: 'floating point value', + cfg.ListOpt: 'list value', + cfg.DictOpt: 'dict value', + cfg.MultiStrOpt: 'multi valued', + } + + def __init__(self, output_file=None, wrap_width=70): + """Construct an OptFormatter object. + + :param output_file: a writeable file object + :param wrap_width: The maximum length of help lines, 0 to not wrap + """ + self.output_file = output_file or sys.stdout + self.wrap_width = wrap_width + + def _format_help(self, help_text): + """Format the help for a group or option to the output file. + + :param help_text: The text of the help string + """ + if self.wrap_width is not None and self.wrap_width > 0: + lines = [textwrap.fill(help_text, + self.wrap_width, + initial_indent='# ', + subsequent_indent='# ') + '\n'] + else: + lines = ['# ' + help_text + '\n'] + return lines + + def format(self, opt): + """Format a description of an option to the output file. + + :param opt: a cfg.Opt instance + """ + if not opt.help: + LOG.warning('"%s" is missing a help string', opt.dest) + + opt_type = self._TYPE_DESCRIPTIONS.get(type(opt), 'unknown type') + + help_text = u'%s(%s)' % (opt.help + ' ' if opt.help else '', opt_type) + lines = self._format_help(help_text) + + for d in opt.deprecated_opts: + lines.append('# Deprecated group/name - [%s]/%s\n' % + (d.group or 'DEFAULT', d.name or opt.dest)) + + if isinstance(opt, cfg.MultiStrOpt): + if opt.sample_default is not None: + defaults = opt.sample_default + elif not opt.default: + defaults = [''] + else: + defaults = opt.default + else: + if opt.sample_default is not None: + default_str = str(opt.sample_default) + elif opt.default is None: + default_str = '' + elif isinstance(opt, cfg.StrOpt): + default_str = opt.default + elif isinstance(opt, cfg.BoolOpt): + default_str = str(opt.default).lower() + elif (isinstance(opt, cfg.IntOpt) or + isinstance(opt, cfg.FloatOpt)): + default_str = str(opt.default) + elif isinstance(opt, cfg.ListOpt): + default_str = ','.join(opt.default) + elif isinstance(opt, cfg.DictOpt): + sorted_items = sorted(opt.default.items(), + key=operator.itemgetter(0)) + default_str = ','.join(['%s:%s' % i for i in sorted_items]) + else: + LOG.warning('Unknown option type: %s', repr(opt)) + default_str = str(opt.default) + defaults = [default_str] + + for default_str in defaults: + if default_str.strip() != default_str: + default_str = '"%s"' % default_str + if default_str: + default_str = ' ' + default_str + lines.append('#%s =%s\n' % (opt.dest, default_str)) + + self.writelines(lines) + + def write(self, s): + """Write an arbitrary string to the output file. + + :param s: an arbitrary string + """ + self.output_file.write(s) + + def writelines(self, l): + """Write an arbitrary sequence of strings to the output file. + + :param l: a list of arbitrary strings + """ + self.output_file.writelines(l) + + +def _list_opts(namespaces): + """List the options available via the given namespaces. + + :param namespaces: a list of namespaces registered under 'oslo.config.opts' + :returns: a list of (namespace, [(group, [opt_1, opt_2])]) tuples + """ + mgr = stevedore.named.NamedExtensionManager('oslo.config.opts', + names=namespaces, + invoke_on_load=True) + return [(ep.name, ep.obj) for ep in mgr] + + +def generate(conf): + """Generate a sample config file. + + List all of the options available via the namespaces specified in the given + configuration and write a description of them to the specified output file. + + :param conf: a ConfigOpts instance containing the generator's configuration + """ + conf.register_opts(_generator_opts) + + output_file = (open(conf.output_file, 'w') + if conf.output_file else sys.stdout) + + formatter = _OptFormatter(output_file=output_file, + wrap_width=conf.wrap_width) + + groups = {'DEFAULT': []} + for namespace, listing in _list_opts(conf.namespace): + for group, opts in listing: + if not opts: + continue + namespaces = groups.setdefault(group or 'DEFAULT', []) + namespaces.append((namespace, + dict((opt.dest, opt) for opt in opts))) + + def _output_opts(f, group, namespaces): + f.write('[%s]\n' % group) + for (namespace, opts_by_dest) in sorted(namespaces, + key=operator.itemgetter(0)): + f.write('\n#\n# From %s\n#\n' % namespace) + for opt in sorted(opts_by_dest.values(), + key=operator.attrgetter('dest')): + f.write('\n') + f.format(opt) + + _output_opts(formatter, 'DEFAULT', groups.pop('DEFAULT')) + for group, namespaces in sorted(groups.items(), + key=operator.itemgetter(0)): + formatter.write('\n\n') + _output_opts(formatter, group, namespaces) + + +def main(args=None): + """The main function of oslo-config-generator.""" + logging.basicConfig(level=logging.WARN) + conf = cfg.ConfigOpts() + register_cli_opts(conf) + conf(args) + generate(conf) + + +if __name__ == '__main__': + main() diff --git a/oslo_config/iniparser.py b/oslo_config/iniparser.py new file mode 100644 index 00000000..90218e45 --- /dev/null +++ b/oslo_config/iniparser.py @@ -0,0 +1,127 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class ParseError(Exception): + def __init__(self, message, lineno, line): + self.msg = message + self.line = line + self.lineno = lineno + + def __str__(self): + return 'at line %d, %s: %r' % (self.lineno, self.msg, self.line) + + +class BaseParser(object): + lineno = 0 + parse_exc = ParseError + + def _assignment(self, key, value): + self.assignment(key, value) + return None, [] + + def _get_section(self, line): + if not line.endswith(']'): + return self.error_no_section_end_bracket(line) + if len(line) <= 2: + return self.error_no_section_name(line) + + return line[1:-1] + + def _split_key_value(self, line): + colon = line.find(':') + equal = line.find('=') + if colon < 0 and equal < 0: + return self.error_invalid_assignment(line) + + if colon < 0 or (equal >= 0 and equal < colon): + key, value = line[:equal], line[equal + 1:] + else: + key, value = line[:colon], line[colon + 1:] + + value = value.strip() + if value and value[0] == value[-1] and value.startswith(("\"", "'")): + value = value[1:-1] + return key.strip(), [value] + + def parse(self, lineiter): + key = None + value = [] + + for line in lineiter: + self.lineno += 1 + + line = line.rstrip() + if not line: + # Blank line, ends multi-line values + if key: + key, value = self._assignment(key, value) + continue + elif line.startswith((' ', '\t')): + # Continuation of previous assignment + if key is None: + self.error_unexpected_continuation(line) + else: + value.append(line.lstrip()) + continue + + if key: + # Flush previous assignment, if any + key, value = self._assignment(key, value) + + if line.startswith('['): + # Section start + section = self._get_section(line) + if section: + self.new_section(section) + elif line.startswith(('#', ';')): + self.comment(line[1:].lstrip()) + else: + key, value = self._split_key_value(line) + if not key: + return self.error_empty_key(line) + + if key: + # Flush previous assignment, if any + self._assignment(key, value) + + def assignment(self, key, value): + """Called when a full assignment is parsed.""" + raise NotImplementedError() + + def new_section(self, section): + """Called when a new section is started.""" + raise NotImplementedError() + + def comment(self, comment): + """Called when a comment is parsed.""" + pass + + def error_invalid_assignment(self, line): + raise self.parse_exc("No ':' or '=' found in assignment", + self.lineno, line) + + def error_empty_key(self, line): + raise self.parse_exc('Key cannot be empty', self.lineno, line) + + def error_unexpected_continuation(self, line): + raise self.parse_exc('Unexpected continuation line', + self.lineno, line) + + def error_no_section_end_bracket(self, line): + raise self.parse_exc('Invalid section (must end with ])', + self.lineno, line) + + def error_no_section_name(self, line): + raise self.parse_exc('Empty section name', self.lineno, line) diff --git a/oslo_config/tests/__init__.py b/oslo_config/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/oslo_config/tests/test_cfg.py b/oslo_config/tests/test_cfg.py new file mode 100644 index 00000000..79dd808d --- /dev/null +++ b/oslo_config/tests/test_cfg.py @@ -0,0 +1,3535 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import errno +import functools +import os +import shutil +import sys +import tempfile + +import fixtures +import mock +from oslotest import base +import six +from six import moves +import testscenarios + +from oslo_config import cfg + +load_tests = testscenarios.load_tests_apply_scenarios + + +class ExceptionsTestCase(base.BaseTestCase): + + def test_error(self): + msg = str(cfg.Error('foobar')) + self.assertEqual(msg, 'foobar') + + def test_args_already_parsed_error(self): + msg = str(cfg.ArgsAlreadyParsedError('foobar')) + self.assertEqual(msg, 'arguments already parsed: foobar') + + def test_no_such_opt_error(self): + msg = str(cfg.NoSuchOptError('foo')) + self.assertEqual(msg, 'no such option: foo') + + def test_no_such_opt_error_with_group(self): + msg = str(cfg.NoSuchOptError('foo', cfg.OptGroup('bar'))) + self.assertEqual(msg, 'no such option in group bar: foo') + + def test_no_such_group_error(self): + msg = str(cfg.NoSuchGroupError('bar')) + self.assertEqual(msg, 'no such group: bar') + + def test_duplicate_opt_error(self): + msg = str(cfg.DuplicateOptError('foo')) + self.assertEqual(msg, 'duplicate option: foo') + + def test_required_opt_error(self): + msg = str(cfg.RequiredOptError('foo')) + self.assertEqual(msg, 'value required for option: foo') + + def test_required_opt_error_with_group(self): + msg = str(cfg.RequiredOptError('foo', cfg.OptGroup('bar'))) + self.assertEqual(msg, 'value required for option: bar.foo') + + def test_template_substitution_error(self): + msg = str(cfg.TemplateSubstitutionError('foobar')) + self.assertEqual(msg, 'template substitution error: foobar') + + def test_config_files_not_found_error(self): + msg = str(cfg.ConfigFilesNotFoundError(['foo', 'bar'])) + self.assertEqual(msg, 'Failed to find some config files: foo,bar') + + def test_config_files_permission_denied_error(self): + msg = str(cfg.ConfigFilesPermissionDeniedError(['foo', 'bar'])) + self.assertEqual(msg, 'Failed to open some config files: foo,bar') + + def test_config_dir_not_found_error(self): + msg = str(cfg.ConfigDirNotFoundError('foobar')) + self.assertEqual(msg, 'Failed to read config file directory: foobar') + + def test_config_file_parse_error(self): + msg = str(cfg.ConfigFileParseError('foo', 'foobar')) + self.assertEqual(msg, 'Failed to parse foo: foobar') + + +class BaseTestCase(base.BaseTestCase): + + class TestConfigOpts(cfg.ConfigOpts): + def __call__(self, args=None, default_config_files=[]): + return cfg.ConfigOpts.__call__( + self, + args=args, + prog='test', + version='1.0', + usage='%(prog)s FOO BAR', + default_config_files=default_config_files, + validate_default_values=True) + + def setUp(self): + super(BaseTestCase, self).setUp() + self.useFixture(fixtures.NestedTempfile()) + self.conf = self.TestConfigOpts() + + self.tempdirs = [] + + def create_tempfiles(self, files, ext='.conf'): + tempfiles = [] + for (basename, contents) in files: + if not os.path.isabs(basename): + (fd, path) = tempfile.mkstemp(prefix=basename, suffix=ext) + else: + path = basename + ext + fd = os.open(path, os.O_CREAT | os.O_WRONLY) + tempfiles.append(path) + try: + os.write(fd, contents.encode('utf-8')) + finally: + os.close(fd) + return tempfiles + + +class UsageTestCase(BaseTestCase): + + def test_print_usage(self): + f = moves.StringIO() + self.conf([]) + self.conf.print_usage(file=f) + self.assertTrue('usage: test FOO BAR' in f.getvalue()) + self.assertTrue('optional:' not in f.getvalue()) + + +class HelpTestCase(BaseTestCase): + + def test_print_help(self): + f = moves.StringIO() + self.conf([]) + self.conf.print_help(file=f) + self.assertTrue('usage: test FOO BAR' in f.getvalue()) + self.assertTrue('optional' in f.getvalue()) + self.assertTrue('-h, --help' in f.getvalue()) + + def test_print_sorted_help(self): + f = moves.StringIO() + self.conf.register_cli_opt(cfg.StrOpt('zba')) + self.conf.register_cli_opt(cfg.StrOpt('abc')) + self.conf.register_cli_opt(cfg.StrOpt('ghi')) + self.conf.register_cli_opt(cfg.StrOpt('deb')) + self.conf([]) + self.conf.print_help(file=f) + zba = f.getvalue().find('--zba') + abc = f.getvalue().find('--abc') + ghi = f.getvalue().find('--ghi') + deb = f.getvalue().find('--deb') + list = [abc, deb, ghi, zba] + self.assertEqual(sorted(list), list) + + +class FindConfigFilesTestCase(BaseTestCase): + + def test_find_config_files(self): + config_files = [os.path.expanduser('~/.blaa/blaa.conf'), + '/etc/foo.conf'] + + self.useFixture(fixtures.MonkeyPatch('sys.argv', ['foo'])) + self.useFixture(fixtures.MonkeyPatch('os.path.exists', + lambda p: p in config_files)) + + self.assertEqual(cfg.find_config_files(project='blaa'), config_files) + + def test_find_config_files_with_extension(self): + config_files = ['/etc/foo.json'] + + self.useFixture(fixtures.MonkeyPatch('sys.argv', ['foo'])) + self.useFixture(fixtures.MonkeyPatch('os.path.exists', + lambda p: p in config_files)) + + self.assertEqual(cfg.find_config_files(project='blaa'), []) + self.assertEqual(cfg.find_config_files(project='blaa', + extension='.json'), + config_files) + + +class DefaultConfigFilesTestCase(BaseTestCase): + + def test_use_default(self): + self.conf.register_opt(cfg.StrOpt('foo')) + paths = self.create_tempfiles([('foo-', '[DEFAULT]\n''foo = bar\n')]) + + self.conf.register_cli_opt(cfg.StrOpt('config-file-foo')) + self.conf(args=['--config-file-foo', 'foo.conf'], + default_config_files=[paths[0]]) + + self.assertEqual(self.conf.config_file, [paths[0]]) + self.assertEqual(self.conf.foo, 'bar') + + def test_do_not_use_default_multi_arg(self): + self.conf.register_opt(cfg.StrOpt('foo')) + paths = self.create_tempfiles([('foo-', '[DEFAULT]\n''foo = bar\n')]) + + self.conf(args=['--config-file', paths[0]], + default_config_files=['bar.conf']) + + self.assertEqual(self.conf.config_file, [paths[0]]) + self.assertEqual(self.conf.foo, 'bar') + + def test_do_not_use_default_single_arg(self): + self.conf.register_opt(cfg.StrOpt('foo')) + paths = self.create_tempfiles([('foo-', '[DEFAULT]\n''foo = bar\n')]) + + self.conf(args=['--config-file=' + paths[0]], + default_config_files=['bar.conf']) + + self.assertEqual(self.conf.config_file, [paths[0]]) + self.assertEqual(self.conf.foo, 'bar') + + def test_no_default_config_file(self): + self.conf(args=[]) + self.assertEqual(self.conf.config_file, []) + + def test_find_default_config_file(self): + paths = self.create_tempfiles([('def', '[DEFAULT]')]) + + self.useFixture(fixtures.MonkeyPatch( + 'oslo_config.cfg.find_config_files', + lambda project, prog: paths)) + + self.conf(args=[], default_config_files=None) + self.assertEqual(self.conf.config_file, paths) + + def test_default_config_file(self): + paths = self.create_tempfiles([('def', '[DEFAULT]')]) + + self.conf(args=[], default_config_files=paths) + + self.assertEqual(self.conf.config_file, paths) + + def test_default_config_file_with_value(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + + paths = self.create_tempfiles([('def', '[DEFAULT]\n''foo = bar\n')]) + + self.conf(args=[], default_config_files=paths) + + self.assertEqual(self.conf.config_file, paths) + self.assertEqual(self.conf.foo, 'bar') + + def test_default_config_file_priority(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + + paths = self.create_tempfiles([('def', '[DEFAULT]\n''foo = bar\n')]) + + self.conf(args=['--foo=blaa'], default_config_files=paths) + + self.assertEqual(self.conf.config_file, paths) + self.assertEqual(self.conf.foo, 'blaa') + + +class CliOptsTestCase(BaseTestCase): + """Test CLI Options. + + Each test scenario takes a name for the scenarios, as well as a dict: + opt_class - class of the type of option that should be tested + default - a default value for the option + cli_args - a list containing a representation of an input command line + value - the result value that is expected to be found + deps - a tuple of deprecated name/group + """ + + IPv4Opt = functools.partial(cfg.IPOpt, version=4) + IPv6Opt = functools.partial(cfg.IPOpt, version=6) + + scenarios = [ + ('str_default', + dict(opt_class=cfg.StrOpt, default=None, cli_args=[], value=None, + deps=(None, None))), + ('str_arg', + dict(opt_class=cfg.StrOpt, default=None, cli_args=['--foo', 'bar'], + value='bar', deps=(None, None))), + ('str_arg_deprecated_name', + dict(opt_class=cfg.StrOpt, default=None, + cli_args=['--oldfoo', 'bar'], value='bar', + deps=('oldfoo', None))), + ('str_arg_deprecated_group', + dict(opt_class=cfg.StrOpt, default=None, + cli_args=['--old-foo', 'bar'], value='bar', + deps=(None, 'old'))), + ('str_arg_deprecated_group_default', + dict(opt_class=cfg.StrOpt, default=None, cli_args=['--foo', 'bar'], + value='bar', deps=(None, 'DEFAULT'))), + ('str_arg_deprecated_group_and_name', + dict(opt_class=cfg.StrOpt, default=None, + cli_args=['--old-oof', 'bar'], value='bar', + deps=('oof', 'old'))), + ('bool_default', + dict(opt_class=cfg.BoolOpt, default=False, + cli_args=[], value=False, deps=(None, None))), + ('bool_arg', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--foo'], value=True, deps=(None, None))), + ('bool_arg_deprecated_name', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--oldfoo'], value=True, + deps=('oldfoo', None))), + ('bool_arg_deprecated_group', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--old-foo'], value=True, + deps=(None, 'old'))), + ('bool_arg_deprecated_group_default', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--foo'], value=True, + deps=(None, 'DEFAULT'))), + ('bool_arg_deprecated_group_and_name', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--old-oof'], value=True, + deps=('oof', 'old'))), + ('bool_arg_inverse', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--foo', '--nofoo'], value=False, deps=(None, None))), + ('bool_arg_inverse_deprecated_name', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--oldfoo', '--nooldfoo'], value=False, + deps=('oldfoo', None))), + ('bool_arg_inverse_deprecated_group', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--old-foo', '--old-nofoo'], value=False, + deps=(None, 'old'))), + ('bool_arg_inverse_deprecated_group_default', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--foo', '--nofoo'], value=False, + deps=(None, 'DEFAULT'))), + ('bool_arg_inverse_deprecated_group_and_name', + dict(opt_class=cfg.BoolOpt, default=None, + cli_args=['--old-oof', '--old-nooof'], value=False, + deps=('oof', 'old'))), + ('int_default', + dict(opt_class=cfg.IntOpt, default=10, + cli_args=[], value=10, deps=(None, None))), + ('int_arg', + dict(opt_class=cfg.IntOpt, default=None, + cli_args=['--foo=20'], value=20, deps=(None, None))), + ('int_arg_deprecated_name', + dict(opt_class=cfg.IntOpt, default=None, + cli_args=['--oldfoo=20'], value=20, deps=('oldfoo', None))), + ('int_arg_deprecated_group', + dict(opt_class=cfg.IntOpt, default=None, + cli_args=['--old-foo=20'], value=20, deps=(None, 'old'))), + ('int_arg_deprecated_group_default', + dict(opt_class=cfg.IntOpt, default=None, + cli_args=['--foo=20'], value=20, deps=(None, 'DEFAULT'))), + ('int_arg_deprecated_group_and_name', + dict(opt_class=cfg.IntOpt, default=None, + cli_args=['--old-oof=20'], value=20, deps=('oof', 'old'))), + ('float_default', + dict(opt_class=cfg.FloatOpt, default=1.0, + cli_args=[], value=1.0, deps=(None, None))), + ('float_arg', + dict(opt_class=cfg.FloatOpt, default=None, + cli_args=['--foo', '2.0'], value=2.0, deps=(None, None))), + ('float_arg_deprecated_name', + dict(opt_class=cfg.FloatOpt, default=None, + cli_args=['--oldfoo', '2.0'], value=2.0, deps=('oldfoo', None))), + ('float_arg_deprecated_group', + dict(opt_class=cfg.FloatOpt, default=None, + cli_args=['--old-foo', '2.0'], value=2.0, deps=(None, 'old'))), + ('float_arg_deprecated_group_default', + dict(opt_class=cfg.FloatOpt, default=None, + cli_args=['--foo', '2.0'], value=2.0, deps=(None, 'DEFAULT'))), + ('float_arg_deprecated_group_and_name', + dict(opt_class=cfg.FloatOpt, default=None, + cli_args=['--old-oof', '2.0'], value=2.0, deps=('oof', 'old'))), + ('float_default_as_integer', + dict(opt_class=cfg.FloatOpt, default=2, + cli_args=['--old-oof', '2.0'], value=2.0, deps=('oof', 'old'))), + ('ipv4addr_arg', + dict(opt_class=IPv4Opt, default=None, + cli_args=['--foo', '192.168.0.1'], value='192.168.0.1', + deps=(None, None))), + ('ipaddr_arg_implicitv4', + dict(opt_class=cfg.IPOpt, default=None, + cli_args=['--foo', '192.168.0.1'], value='192.168.0.1', + deps=(None, None))), + ('ipaddr_arg_implicitv6', + dict(opt_class=cfg.IPOpt, default=None, + cli_args=['--foo', 'abcd:ef::1'], value='abcd:ef::1', + deps=(None, None))), + ('ipv6addr_arg', + dict(opt_class=IPv6Opt, default=None, + cli_args=['--foo', 'abcd:ef::1'], value='abcd:ef::1', + deps=(None, None))), + ('list_default', + dict(opt_class=cfg.ListOpt, default=['bar'], + cli_args=[], value=['bar'], deps=(None, None))), + ('list_arg', + dict(opt_class=cfg.ListOpt, default=None, + cli_args=['--foo', 'blaa,bar'], value=['blaa', 'bar'], + deps=(None, None))), + ('list_arg_with_spaces', + dict(opt_class=cfg.ListOpt, default=None, + cli_args=['--foo', 'blaa ,bar'], value=['blaa', 'bar'], + deps=(None, None))), + ('list_arg_deprecated_name', + dict(opt_class=cfg.ListOpt, default=None, + cli_args=['--oldfoo', 'blaa,bar'], value=['blaa', 'bar'], + deps=('oldfoo', None))), + ('list_arg_deprecated_group', + dict(opt_class=cfg.ListOpt, default=None, + cli_args=['--old-foo', 'blaa,bar'], value=['blaa', 'bar'], + deps=(None, 'old'))), + ('list_arg_deprecated_group_default', + dict(opt_class=cfg.ListOpt, default=None, + cli_args=['--foo', 'blaa,bar'], value=['blaa', 'bar'], + deps=(None, 'DEFAULT'))), + ('list_arg_deprecated_group_and_name', + dict(opt_class=cfg.ListOpt, default=None, + cli_args=['--old-oof', 'blaa,bar'], value=['blaa', 'bar'], + deps=('oof', 'old'))), + ('dict_default', + dict(opt_class=cfg.DictOpt, default={'foo': 'bar'}, + cli_args=[], value={'foo': 'bar'}, deps=(None, None))), + ('dict_arg', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--foo', 'key1:blaa,key2:bar'], + value={'key1': 'blaa', 'key2': 'bar'}, deps=(None, None))), + ('dict_arg_multiple_keys_last_wins', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--foo', 'key1:blaa', '--foo', 'key2:bar'], + value={'key2': 'bar'}, deps=(None, None))), + ('dict_arg_with_spaces', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--foo', 'key1:blaa ,key2:bar'], + value={'key1': 'blaa', 'key2': 'bar'}, deps=(None, None))), + ('dict_arg_deprecated_name', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--oldfoo', 'key1:blaa', '--oldfoo', 'key2:bar'], + value={'key2': 'bar'}, deps=('oldfoo', None))), + ('dict_arg_deprecated_group', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--old-foo', 'key1:blaa,key2:bar'], + value={'key1': 'blaa', 'key2': 'bar'}, deps=(None, 'old'))), + ('dict_arg_deprecated_group2', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--old-foo', 'key1:blaa', '--old-foo', 'key2:bar'], + value={'key2': 'bar'}, deps=(None, 'old'))), + ('dict_arg_deprecated_group_default', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--foo', 'key1:blaa', '--foo', 'key2:bar'], + value={'key2': 'bar'}, deps=(None, 'DEFAULT'))), + ('dict_arg_deprecated_group_and_name', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--old-oof', 'key1:blaa,key2:bar'], + value={'key1': 'blaa', 'key2': 'bar'}, deps=('oof', 'old'))), + ('dict_arg_deprecated_group_and_name2', + dict(opt_class=cfg.DictOpt, default=None, + cli_args=['--old-oof', 'key1:blaa', '--old-oof', 'key2:bar'], + value={'key2': 'bar'}, deps=('oof', 'old'))), + ('multistr_default', + dict(opt_class=cfg.MultiStrOpt, default=['bar'], cli_args=[], + value=['bar'], deps=(None, None))), + ('multistr_arg', + dict(opt_class=cfg.MultiStrOpt, default=None, + cli_args=['--foo', 'blaa', '--foo', 'bar'], + value=['blaa', 'bar'], deps=(None, None))), + ('multistr_arg_deprecated_name', + dict(opt_class=cfg.MultiStrOpt, default=None, + cli_args=['--oldfoo', 'blaa', '--oldfoo', 'bar'], + value=['blaa', 'bar'], deps=('oldfoo', None))), + ('multistr_arg_deprecated_group', + dict(opt_class=cfg.MultiStrOpt, default=None, + cli_args=['--old-foo', 'blaa', '--old-foo', 'bar'], + value=['blaa', 'bar'], deps=(None, 'old'))), + ('multistr_arg_deprecated_group_default', + dict(opt_class=cfg.MultiStrOpt, default=None, + cli_args=['--foo', 'blaa', '--foo', 'bar'], + value=['blaa', 'bar'], deps=(None, 'DEFAULT'))), + ('multistr_arg_deprecated_group_and_name', + dict(opt_class=cfg.MultiStrOpt, default=None, + cli_args=['--old-oof', 'blaa', '--old-oof', 'bar'], + value=['blaa', 'bar'], deps=('oof', 'old'))), + ] + + def test_cli(self): + + self.conf.register_cli_opt( + self.opt_class('foo', default=self.default, + deprecated_name=self.deps[0], + deprecated_group=self.deps[1])) + + self.conf(self.cli_args) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, self.value) + + +class CliSpecialOptsTestCase(BaseTestCase): + + def test_help(self): + self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO())) + self.assertRaises(SystemExit, self.conf, ['--help']) + self.assertTrue('FOO BAR' in sys.stdout.getvalue()) + self.assertTrue('--version' in sys.stdout.getvalue()) + self.assertTrue('--help' in sys.stdout.getvalue()) + self.assertTrue('--config-file' in sys.stdout.getvalue()) + + def test_version(self): + # In Python 3.4+, argparse prints the version on stdout; before 3.4, it + # printed it on stderr. + if sys.version_info >= (3, 4): + stream_name = 'stdout' + else: + stream_name = 'stderr' + self.useFixture(fixtures.MonkeyPatch("sys.%s" % stream_name, + moves.StringIO())) + self.assertRaises(SystemExit, self.conf, ['--version']) + self.assertTrue('1.0' in getattr(sys, stream_name).getvalue()) + + def test_config_file(self): + paths = self.create_tempfiles([('1', '[DEFAULT]'), + ('2', '[DEFAULT]')]) + + self.conf(['--config-file', paths[0], '--config-file', paths[1]]) + + self.assertEqual(self.conf.config_file, paths) + + +class PositionalTestCase(BaseTestCase): + + def _do_pos_test(self, opt_class, default, cli_args, value): + self.conf.register_cli_opt(opt_class('foo', + default=default, + positional=True)) + + self.conf(cli_args) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, value) + + def test_positional_str_none_default(self): + self._do_pos_test(cfg.StrOpt, None, [], None) + + def test_positional_str_default(self): + self._do_pos_test(cfg.StrOpt, 'bar', [], 'bar') + + def test_positional_str_arg(self): + self._do_pos_test(cfg.StrOpt, None, ['bar'], 'bar') + + def test_positional_int_none_default(self): + self._do_pos_test(cfg.IntOpt, None, [], None) + + def test_positional_int_default(self): + self._do_pos_test(cfg.IntOpt, 10, [], 10) + + def test_positional_int_arg(self): + self._do_pos_test(cfg.IntOpt, None, ['20'], 20) + + def test_positional_float_none_default(self): + self._do_pos_test(cfg.FloatOpt, None, [], None) + + def test_positional_float_default(self): + self._do_pos_test(cfg.FloatOpt, 1.0, [], 1.0) + + def test_positional_float_arg(self): + self._do_pos_test(cfg.FloatOpt, None, ['2.0'], 2.0) + + def test_positional_list_none_default(self): + self._do_pos_test(cfg.ListOpt, None, [], None) + + def test_positional_list_empty_default(self): + self._do_pos_test(cfg.ListOpt, [], [], []) + + def test_positional_list_default(self): + self._do_pos_test(cfg.ListOpt, ['bar'], [], ['bar']) + + def test_positional_list_arg(self): + self._do_pos_test(cfg.ListOpt, None, + ['blaa,bar'], ['blaa', 'bar']) + + def test_positional_dict_none_default(self): + self._do_pos_test(cfg.DictOpt, None, [], None) + + def test_positional_dict_empty_default(self): + self._do_pos_test(cfg.DictOpt, {}, [], {}) + + def test_positional_dict_default(self): + self._do_pos_test(cfg.DictOpt, {'key1': 'bar'}, [], {'key1': 'bar'}) + + def test_positional_dict_arg(self): + self._do_pos_test(cfg.DictOpt, None, + ['key1:blaa,key2:bar'], + {'key1': 'blaa', 'key2': 'bar'}) + + def test_positional_multistr_none_default(self): + self._do_pos_test(cfg.MultiStrOpt, None, [], None) + + def test_positional_multistr_empty_default(self): + self._do_pos_test(cfg.MultiStrOpt, [], [], []) + + def test_positional_multistr_default(self): + self._do_pos_test(cfg.MultiStrOpt, ['bar'], [], ['bar']) + + def test_positional_multistr_arg(self): + self._do_pos_test(cfg.MultiStrOpt, None, + ['blaa', 'bar'], ['blaa', 'bar']) + + def test_positional_bool(self): + self.assertRaises(ValueError, cfg.BoolOpt, 'foo', positional=True) + + def test_required_positional_opt(self): + self.conf.register_cli_opt( + cfg.StrOpt('foo', required=True, positional=True)) + + self.conf(['bar']) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + def test_missing_required_cli_opt(self): + self.conf.register_cli_opt( + cfg.StrOpt('foo', required=True, positional=True)) + self.assertRaises(cfg.RequiredOptError, self.conf, []) + + def test_positional_opts_order(self): + self.conf.register_cli_opts(( + cfg.StrOpt('command', positional=True), + cfg.StrOpt('arg1', positional=True), + cfg.StrOpt('arg2', positional=True)) + ) + + self.conf(['command', 'arg1', 'arg2']) + + self.assertEqual('command', self.conf.command) + self.assertEqual('arg1', self.conf.arg1) + self.assertEqual('arg2', self.conf.arg2) + + def test_positional_opt_order(self): + self.conf.register_cli_opt( + cfg.StrOpt('command', positional=True)) + self.conf.register_cli_opt( + cfg.StrOpt('arg1', positional=True)) + self.conf.register_cli_opt( + cfg.StrOpt('arg2', positional=True)) + + self.conf(['command', 'arg1', 'arg2']) + + self.assertEqual('command', self.conf.command) + self.assertEqual('arg1', self.conf.arg1) + self.assertEqual('arg2', self.conf.arg2) + + def test_positional_opt_unregister(self): + command = cfg.StrOpt('command', positional=True) + arg1 = cfg.StrOpt('arg1', positional=True) + arg2 = cfg.StrOpt('arg2', positional=True) + self.conf.register_cli_opt(command) + self.conf.register_cli_opt(arg1) + self.conf.register_cli_opt(arg2) + + self.conf(['command', 'arg1', 'arg2']) + + self.assertEqual('command', self.conf.command) + self.assertEqual('arg1', self.conf.arg1) + self.assertEqual('arg2', self.conf.arg2) + + self.conf.reset() + + self.conf.unregister_opt(arg1) + self.conf.unregister_opt(arg2) + + arg0 = cfg.StrOpt('arg0', positional=True) + self.conf.register_cli_opt(arg0) + self.conf.register_cli_opt(arg1) + + self.conf(['command', 'arg0', 'arg1']) + + self.assertEqual('command', self.conf.command) + self.assertEqual('arg0', self.conf.arg0) + self.assertEqual('arg1', self.conf.arg1) + + +class ConfigFileOptsTestCase(BaseTestCase): + + def _do_deprecated_test(self, opt_class, value, result, key, + section='DEFAULT', + dname=None, dgroup=None): + self.conf.register_opt(opt_class('newfoo', + deprecated_name=dname, + deprecated_group=dgroup)) + + paths = self.create_tempfiles([('test', + '[' + section + ']\n' + + key + ' = ' + value + '\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'newfoo')) + self.assertEqual(self.conf.newfoo, result) + + def _do_dname_test_use(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'oldfoo', + dname='oldfoo') + + def _do_dgroup_test_use(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'newfoo', + section='old', dgroup='old') + + def _do_default_dgroup_test_use(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'newfoo', + section='DEFAULT', dgroup='DEFAULT') + + def _do_dgroup_and_dname_test_use(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'oof', + section='old', dgroup='old', dname='oof') + + def _do_dname_test_ignore(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'newfoo', + dname='oldfoo') + + def _do_dgroup_test_ignore(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'newfoo', + section='DEFAULT', dgroup='old') + + def _do_dgroup_and_dname_test_ignore(self, opt_class, value, result): + self._do_deprecated_test(opt_class, value, result, 'oof', + section='old', dgroup='old', dname='oof') + + def test_conf_file_str_default(self): + self.conf.register_opt(cfg.StrOpt('foo', default='bar')) + + paths = self.create_tempfiles([('test', '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + def test_conf_file_str_value(self): + self.conf.register_opt(cfg.StrOpt('foo')) + + paths = self.create_tempfiles([('test', '[DEFAULT]\n''foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + def test_conf_file_str_value_override(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = baar\n'), + ('2', + '[DEFAULT]\n' + 'foo = baaar\n')]) + + self.conf(['--foo', 'bar', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'baaar') + + def test_conf_file_str_value_override_use_deprecated(self): + """last option should always win, even if last uses deprecated.""" + self.conf.register_cli_opt( + cfg.StrOpt('newfoo', deprecated_name='oldfoo')) + + paths = self.create_tempfiles([('0', + '[DEFAULT]\n' + 'newfoo = middle\n'), + ('1', + '[DEFAULT]\n' + 'oldfoo = last\n')]) + + self.conf(['--newfoo', 'first', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'newfoo')) + self.assertFalse(hasattr(self.conf, 'oldfoo')) + self.assertEqual(self.conf.newfoo, 'last') + + def test_conf_file_str_use_dname(self): + self._do_dname_test_use(cfg.StrOpt, 'value1', 'value1') + + def test_conf_file_str_use_dgroup(self): + self._do_dgroup_test_use(cfg.StrOpt, 'value1', 'value1') + + def test_conf_file_str_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.StrOpt, 'value1', 'value1') + + def test_conf_file_str_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.StrOpt, 'value1', 'value1') + + def test_conf_file_str_ignore_dname(self): + self._do_dname_test_ignore(cfg.StrOpt, 'value2', 'value2') + + def test_conf_file_str_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.StrOpt, 'value2', 'value2') + + def test_conf_file_str_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.StrOpt, 'value2', 'value2') + + def test_conf_file_bool_default(self): + self.conf.register_opt(cfg.BoolOpt('foo', default=False)) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, False) + + def test_conf_file_bool_value(self): + self.conf.register_opt(cfg.BoolOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = true\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, True) + + def test_conf_file_bool_cli_value_override(self): + self.conf.register_cli_opt(cfg.BoolOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = 0\n')]) + + self.conf(['--foo', + '--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, False) + + def test_conf_file_bool_cli_inverse_override(self): + self.conf.register_cli_opt(cfg.BoolOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = true\n')]) + + self.conf(['--nofoo', + '--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, True) + + def test_conf_file_bool_cli_order_override(self): + self.conf.register_cli_opt(cfg.BoolOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = false\n')]) + + self.conf(['--config-file', paths[0], + '--foo']) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, True) + + def test_conf_file_bool_file_value_override(self): + self.conf.register_cli_opt(cfg.BoolOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = 0\n'), + ('2', + '[DEFAULT]\n' + 'foo = yes\n')]) + + self.conf(['--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, True) + + def test_conf_file_bool_use_dname(self): + self._do_dname_test_use(cfg.BoolOpt, 'yes', True) + + def test_conf_file_bool_use_dgroup(self): + self._do_dgroup_test_use(cfg.BoolOpt, 'yes', True) + + def test_conf_file_bool_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.BoolOpt, 'yes', True) + + def test_conf_file_bool_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.BoolOpt, 'yes', True) + + def test_conf_file_bool_ignore_dname(self): + self._do_dname_test_ignore(cfg.BoolOpt, 'no', False) + + def test_conf_file_bool_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.BoolOpt, 'no', False) + + def test_conf_file_bool_ignore_group_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.BoolOpt, 'no', False) + + def test_conf_file_int_default(self): + self.conf.register_opt(cfg.IntOpt('foo', default=666)) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 666) + + @mock.patch.object(cfg, 'LOG') + def test_conf_file_int_wrong_default(self, mock_log): + cfg.IntOpt('foo', default='666') + mock_log.debug.assert_call_count(1) + + def test_conf_file_int_value(self): + self.conf.register_opt(cfg.IntOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = 666\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 666) + + def test_conf_file_int_value_override(self): + self.conf.register_cli_opt(cfg.IntOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = 66\n'), + ('2', + '[DEFAULT]\n' + 'foo = 666\n')]) + + self.conf(['--foo', '6', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 666) + + def test_conf_file_int_use_dname(self): + self._do_dname_test_use(cfg.IntOpt, '66', 66) + + def test_conf_file_int_use_dgroup(self): + self._do_dgroup_test_use(cfg.IntOpt, '66', 66) + + def test_conf_file_int_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.IntOpt, '66', 66) + + def test_conf_file_int_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.IntOpt, '66', 66) + + def test_conf_file_int_ignore_dname(self): + self._do_dname_test_ignore(cfg.IntOpt, '64', 64) + + def test_conf_file_int_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.IntOpt, '64', 64) + + def test_conf_file_int_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.IntOpt, '64', 64) + + def test_conf_file_float_default(self): + self.conf.register_opt(cfg.FloatOpt('foo', default=6.66)) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 6.66) + + @mock.patch.object(cfg, 'LOG') + def test_conf_file_float_default_wrong_type(self, mock_log): + cfg.FloatOpt('foo', default='foobar6.66') + mock_log.debug.assert_call_count(1) + + def test_conf_file_float_value(self): + self.conf.register_opt(cfg.FloatOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = 6.66\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 6.66) + + def test_conf_file_float_value_override(self): + self.conf.register_cli_opt(cfg.FloatOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = 6.6\n'), + ('2', + '[DEFAULT]\n' + 'foo = 6.66\n')]) + + self.conf(['--foo', '6', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 6.66) + + def test_conf_file_float_use_dname(self): + self._do_dname_test_use(cfg.FloatOpt, '66.54', 66.54) + + def test_conf_file_float_use_dgroup(self): + self._do_dgroup_test_use(cfg.FloatOpt, '66.54', 66.54) + + def test_conf_file_float_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.FloatOpt, '66.54', 66.54) + + def test_conf_file_float_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.FloatOpt, '66.54', 66.54) + + def test_conf_file_float_ignore_dname(self): + self._do_dname_test_ignore(cfg.FloatOpt, '64.54', 64.54) + + def test_conf_file_float_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.FloatOpt, '64.54', 64.54) + + def test_conf_file_float_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.FloatOpt, '64.54', 64.54) + + def test_conf_file_list_default(self): + self.conf.register_opt(cfg.ListOpt('foo', default=['bar'])) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, ['bar']) + + @mock.patch.object(cfg, 'LOG') + def test_conf_file_list_default_wrong_type(self, mock_log): + cfg.ListOpt('foo', default=25) + mock_log.debug.assert_called_once_with( + 'Expected default value of type(s) %(extypes)s but ' + 'got %(default)r of type %(deftypes)s', + {'extypes': 'list', + 'default': 25, + 'deftypes': 'int'}) + + def test_conf_file_list_value(self): + self.conf.register_opt(cfg.ListOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, ['bar']) + + def test_conf_file_list_value_override(self): + self.conf.register_cli_opt(cfg.ListOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = bar,bar\n'), + ('2', + '[DEFAULT]\n' + 'foo = b,a,r\n')]) + + self.conf(['--foo', 'bar', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, ['b', 'a', 'r']) + + def test_conf_file_list_use_dname(self): + self._do_dname_test_use(cfg.ListOpt, 'a,b,c', ['a', 'b', 'c']) + + def test_conf_file_list_use_dgroup(self): + self._do_dgroup_test_use(cfg.ListOpt, 'a,b,c', ['a', 'b', 'c']) + + def test_conf_file_list_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.ListOpt, 'a,b,c', ['a', 'b', 'c']) + + def test_conf_file_list_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.ListOpt, 'a,b,c', + ['a', 'b', 'c']) + + def test_conf_file_list_ignore_dname(self): + self._do_dname_test_ignore(cfg.ListOpt, 'd,e,f', ['d', 'e', 'f']) + + def test_conf_file_list_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.ListOpt, 'd,e,f', ['d', 'e', 'f']) + + def test_conf_file_list_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore( + cfg.ListOpt, 'd,e,f', ['d', 'e', 'f']) + + def test_conf_file_list_spaces_use_dname(self): + self._do_dname_test_use(cfg.ListOpt, 'a, b, c', ['a', 'b', 'c']) + + def test_conf_file_list_spaces_use_dgroup(self): + self._do_dgroup_test_use(cfg.ListOpt, 'a, b, c', ['a', 'b', 'c']) + + def test_conf_file_list_spaces_use_default_dgroup(self): + self._do_default_dgroup_test_use( + cfg.ListOpt, 'a, b, c', ['a', 'b', 'c']) + + def test_conf_file_list_spaces_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use( + cfg.ListOpt, 'a, b, c', ['a', 'b', 'c']) + + def test_conf_file_list_spaces_ignore_dname(self): + self._do_dname_test_ignore(cfg.ListOpt, 'd, e, f', ['d', 'e', 'f']) + + def test_conf_file_list_spaces_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.ListOpt, 'd, e, f', ['d', 'e', 'f']) + + def test_conf_file_list_spaces_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.ListOpt, 'd, e, f', + ['d', 'e', 'f']) + + def test_conf_file_dict_default(self): + self.conf.register_opt(cfg.DictOpt('foo', default={'key': 'bar'})) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, {'key': 'bar'}) + + def test_conf_file_dict_value(self): + self.conf.register_opt(cfg.DictOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = key:bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, {'key': 'bar'}) + + def test_conf_file_dict_colon_in_value(self): + self.conf.register_opt(cfg.DictOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = key:bar:baz\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, {'key': 'bar:baz'}) + + def test_conf_file_dict_value_no_colon(self): + self.conf.register_opt(cfg.DictOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = key:bar,baz\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo') + self.assertRaises(AttributeError, getattr, self.conf, 'foo') + + def test_conf_file_dict_value_duplicate_key(self): + self.conf.register_opt(cfg.DictOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = key:bar,key:baz\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo') + self.assertRaises(AttributeError, getattr, self.conf, 'foo') + + def test_conf_file_dict_values_override_deprecated(self): + self.conf.register_cli_opt(cfg.DictOpt('foo', + deprecated_name='oldfoo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = key1:bar1\n'), + ('2', + '[DEFAULT]\n' + 'oldfoo = key2:bar2\n' + 'oldfoo = key3:bar3\n')]) + + self.conf(['--foo', 'key0:bar0', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + + self.assertEqual(self.conf.foo, {'key3': 'bar3'}) + + def test_conf_file_dict_deprecated(self): + self.conf.register_opt(cfg.DictOpt('newfoo', deprecated_name='oldfoo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'oldfoo= key1:bar1\n' + 'oldfoo = key2:bar2\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'newfoo')) + self.assertEqual(self.conf.newfoo, {'key2': 'bar2'}) + + def test_conf_file_dict_value_override(self): + self.conf.register_cli_opt(cfg.DictOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = key:bar,key2:bar\n'), + ('2', + '[DEFAULT]\n' + 'foo = k1:v1,k2:v2\n')]) + + self.conf(['--foo', 'x:y,x2:y2', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, {'k1': 'v1', 'k2': 'v2'}) + + def test_conf_file_dict_use_dname(self): + self._do_dname_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_use_dgroup(self): + self._do_dgroup_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_ignore_dname(self): + self._do_dname_test_ignore(cfg.DictOpt, + 'k1:d,k2:e,k3:f', + {'k1': 'd', 'k2': 'e', 'k3': 'f'}) + + def test_conf_file_dict_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.DictOpt, + 'k1:d,k2:e,k3:f', + {'k1': 'd', 'k2': 'e', 'k3': 'f'}) + + def test_conf_file_dict_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.DictOpt, + 'k1:d,k2:e,k3:f', + {'k1': 'd', + 'k2': 'e', + 'k3': 'f'}) + + def test_conf_file_dict_spaces_use_dname(self): + self._do_dname_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_spaces_use_dgroup(self): + self._do_dgroup_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_spaces_use_default_dgroup(self): + self._do_default_dgroup_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_spaces_use_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_use(cfg.DictOpt, + 'k1:a,k2:b,k3:c', + {'k1': 'a', 'k2': 'b', 'k3': 'c'}) + + def test_conf_file_dict_spaces_ignore_dname(self): + self._do_dname_test_ignore(cfg.DictOpt, + 'k1:d,k2:e,k3:f', + {'k1': 'd', 'k2': 'e', 'k3': 'f'}) + + def test_conf_file_dict_spaces_ignore_dgroup(self): + self._do_dgroup_test_ignore(cfg.DictOpt, + 'k1:d,k2:e,k3:f', + {'k1': 'd', 'k2': 'e', 'k3': 'f'}) + + def test_conf_file_dict_spaces_ignore_dgroup_and_dname(self): + self._do_dgroup_and_dname_test_ignore(cfg.DictOpt, + 'k1:d,k2:e,k3:f', + {'k1': 'd', + 'k2': 'e', + 'k3': 'f'}) + + def test_conf_file_multistr_default(self): + self.conf.register_opt(cfg.MultiStrOpt('foo', default=['bar'])) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, ['bar']) + + def test_conf_file_multistr_value(self): + self.conf.register_opt(cfg.MultiStrOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, ['bar']) + + def test_conf_file_multistr_values_append_deprecated(self): + self.conf.register_cli_opt(cfg.MultiStrOpt('foo', + deprecated_name='oldfoo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = bar1\n'), + ('2', + '[DEFAULT]\n' + 'oldfoo = bar2\n' + 'oldfoo = bar3\n')]) + + self.conf(['--foo', 'bar0', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + + self.assertEqual(self.conf.foo, ['bar0', 'bar1', 'bar2', 'bar3']) + + def test_conf_file_multistr_values_append(self): + self.conf.register_cli_opt(cfg.MultiStrOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = bar1\n'), + ('2', + '[DEFAULT]\n' + 'foo = bar2\n' + 'foo = bar3\n')]) + + self.conf(['--foo', 'bar0', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + + self.assertEqual(self.conf.foo, ['bar0', 'bar1', 'bar2', 'bar3']) + + def test_conf_file_multistr_deprecated(self): + self.conf.register_opt( + cfg.MultiStrOpt('newfoo', deprecated_name='oldfoo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'oldfoo= bar1\n' + 'oldfoo = bar2\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'newfoo')) + self.assertEqual(self.conf.newfoo, ['bar1', 'bar2']) + + def test_conf_file_multiple_opts(self): + self.conf.register_opts([cfg.StrOpt('foo'), cfg.StrOpt('bar')]) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n' + 'bar = foo\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + self.assertTrue(hasattr(self.conf, 'bar')) + self.assertEqual(self.conf.bar, 'foo') + + def test_conf_file_raw_value(self): + self.conf.register_opt(cfg.StrOpt('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar-%08x\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar-%08x') + + +class ConfigFileReloadTestCase(BaseTestCase): + + def test_conf_files_reload(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = baar\n'), + ('2', + '[DEFAULT]\n' + 'foo = baaar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'baar') + + shutil.copy(paths[1], paths[0]) + + self.conf.reload_config_files() + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'baaar') + + def test_conf_files_reload_default(self): + self.conf.register_cli_opt(cfg.StrOpt('foo1')) + self.conf.register_cli_opt(cfg.StrOpt('foo2')) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo1 = default1\n'), + ('2', + '[DEFAULT]\n' + 'foo2 = default2\n')]) + + paths_change = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo1 = change_default1\n'), + ('2', + '[DEFAULT]\n' + 'foo2 = change_default2\n')]) + + self.conf(args=[], default_config_files=paths) + self.assertTrue(hasattr(self.conf, 'foo1')) + self.assertEqual(self.conf.foo1, 'default1') + self.assertTrue(hasattr(self.conf, 'foo2')) + self.assertEqual(self.conf.foo2, 'default2') + + shutil.copy(paths_change[0], paths[0]) + shutil.copy(paths_change[1], paths[1]) + + self.conf.reload_config_files() + self.assertTrue(hasattr(self.conf, 'foo1')) + self.assertEqual(self.conf.foo1, 'change_default1') + self.assertTrue(hasattr(self.conf, 'foo2')) + self.assertEqual(self.conf.foo2, 'change_default2') + + def test_conf_files_reload_file_not_found(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', required=True)) + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = baar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'baar') + + os.remove(paths[0]) + + self.conf.reload_config_files() + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'baar') + + def test_conf_files_reload_error(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', required=True)) + self.conf.register_cli_opt(cfg.StrOpt('foo1', required=True)) + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = test1\n' + 'foo1 = test11\n'), + ('2', + '[DEFAULT]\n' + 'foo2 = test2\n' + 'foo3 = test22\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'test1') + self.assertTrue(hasattr(self.conf, 'foo1')) + self.assertEqual(self.conf.foo1, 'test11') + + shutil.copy(paths[1], paths[0]) + + self.conf.reload_config_files() + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'test1') + self.assertTrue(hasattr(self.conf, 'foo1')) + self.assertEqual(self.conf.foo1, 'test11') + + +class OptGroupsTestCase(BaseTestCase): + + def test_arg_group(self): + blaa_group = cfg.OptGroup('blaa', 'blaa options') + self.conf.register_group(blaa_group) + self.conf.register_cli_opt(cfg.StrOpt('foo'), group=blaa_group) + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_autocreate_group_by_name(self): + self.conf.register_cli_opt(cfg.StrOpt('foo'), group='blaa') + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_autocreate_group_by_group(self): + group = cfg.OptGroup(name='blaa', title='Blaa options') + self.conf.register_cli_opt(cfg.StrOpt('foo'), group=group) + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_autocreate_title(self): + blaa_group = cfg.OptGroup('blaa') + self.assertEqual(blaa_group.title, 'blaa options') + + def test_arg_group_by_name(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt(cfg.StrOpt('foo'), group='blaa') + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_with_default(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt( + cfg.StrOpt('foo', default='bar'), group='blaa') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_with_conf_and_group_opts(self): + self.conf.register_cli_opt(cfg.StrOpt('conf'), group='blaa') + self.conf.register_cli_opt(cfg.StrOpt('group'), group='blaa') + + self.conf(['--blaa-conf', 'foo', '--blaa-group', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'conf')) + self.assertEqual(self.conf.blaa.conf, 'foo') + self.assertTrue(hasattr(self.conf.blaa, 'group')) + self.assertEqual(self.conf.blaa.group, 'bar') + + def test_arg_group_in_config_file(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo'), group='blaa') + + paths = self.create_tempfiles([('test', + '[blaa]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_with_deprecated_name(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', deprecated_name='oldfoo'), + group='blaa') + + paths = self.create_tempfiles([('test', + '[blaa]\n' + 'oldfoo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_with_deprecated_group(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', deprecated_group='DEFAULT'), + group='blaa') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_with_deprecated_group_and_name(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt( + cfg.StrOpt('foo', deprecated_group='DEFAULT', + deprecated_name='oldfoo'), group='blaa') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'oldfoo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_override_deprecated_name(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', deprecated_name='oldfoo'), + group='blaa') + + paths = self.create_tempfiles([('test', + '[blaa]\n' + 'foo = bar\n' + 'oldfoo = blabla\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_override_deprecated_group(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', deprecated_group='DEFAULT'), + group='blaa') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = blabla\n' + '[blaa]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_override_deprecated_group_and_name(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt( + cfg.StrOpt('foo', deprecated_group='DEFAULT', + deprecated_name='oldfoo'), group='blaa') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'oldfoo = blabla\n' + '[blaa]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_with_capital_name(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo'), group='blaa') + + paths = self.create_tempfiles([('test', + '[BLAA]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertFalse(hasattr(self.conf, 'BLAA')) + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_arg_group_in_config_file_with_capital_name_on_legacy_code(self): + self.conf.register_group(cfg.OptGroup('BLAA')) + self.conf.register_opt(cfg.StrOpt('foo'), group='BLAA') + + paths = self.create_tempfiles([('test', + '[BLAA]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertFalse(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf, 'BLAA')) + self.assertTrue(hasattr(self.conf.BLAA, 'foo')) + self.assertEqual(self.conf.BLAA.foo, 'bar') + + +class MappingInterfaceTestCase(BaseTestCase): + + def test_mapping_interface(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + + self.conf(['--foo', 'bar']) + + self.assertTrue('foo' in self.conf) + self.assertTrue('config_file' in self.conf) + self.assertEqual(len(self.conf), 3) + self.assertEqual(self.conf['foo'], 'bar') + self.assertEqual(self.conf.get('foo'), 'bar') + self.assertTrue('bar' in list(self.conf.values())) + + def test_mapping_interface_with_group(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt(cfg.StrOpt('foo'), group='blaa') + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue('blaa' in self.conf) + self.assertTrue('foo' in list(self.conf['blaa'])) + self.assertEqual(len(self.conf['blaa']), 1) + self.assertEqual(self.conf['blaa']['foo'], 'bar') + self.assertEqual(self.conf['blaa'].get('foo'), 'bar') + self.assertTrue('bar' in self.conf['blaa'].values()) + self.assertEqual(self.conf.blaa, self.conf['blaa']) + + +class OptNameSeparatorTestCast(BaseTestCase): + + scenarios = [ + ('hyphen', + dict(opt_name='foo-bar', + opt_dest='foo_bar', + broken_opt_dest='foo-bar', + cf_name='foo_bar', + broken_cf_name='foo-bar', + cli_name='foo-bar', + broken_cli_name='foo_bar', + broken=True)), # FIXME(markmc): see #1279973 + ('underscore', + dict(opt_name='foo_bar', + opt_dest='foo_bar', + broken_opt_dest='foo-bar', + cf_name='foo_bar', + broken_cf_name='foo-bar', + cli_name='foo_bar', + broken_cli_name='foo_bar', + broken=False)), + ] + + def test_attribute_and_key_name(self): + self.conf.register_opt(cfg.StrOpt(self.opt_name)) + + self.assertTrue(hasattr(self.conf, self.opt_dest)) + self.assertFalse(hasattr(self.conf, self.broken_opt_dest)) + self.assertIn(self.opt_dest, self.conf) + self.assertNotIn(self.broken_opt_dest, self.conf) + + def test_cli_opt_name(self): + self.conf.register_cli_opt(cfg.BoolOpt(self.opt_name)) + + self.conf(['--' + self.cli_name]) + + self.assertTrue(getattr(self.conf, self.opt_dest)) + + def test_config_file_opt_name(self): + self.conf.register_opt(cfg.BoolOpt(self.opt_name)) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + + self.cf_name + ' = True\n' + + self.broken_cf_name + ' = False\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(getattr(self.conf, self.opt_dest)) + + def test_deprecated_name(self): + self.conf.register_opt(cfg.StrOpt('foobar', + deprecated_name=self.opt_name)) + + self.assertTrue(hasattr(self.conf, 'foobar')) + self.assertFalse(hasattr(self.conf, self.opt_dest)) + self.assertFalse(hasattr(self.conf, self.broken_opt_dest)) + self.assertIn('foobar', self.conf) + self.assertNotIn(self.opt_dest, self.conf) + self.assertNotIn(self.broken_opt_dest, self.conf) + + def test_deprecated_name_cli(self): + self.conf.register_cli_opt(cfg.BoolOpt('foobar', + deprecated_name=self.opt_name)) + + # FIXME(markmc): this should be self.cli_name, see #1279973 + if self.broken: + self.conf(['--' + self.broken_cli_name]) + else: + self.conf(['--' + self.cli_name]) + + self.assertTrue(self.conf.foobar) + + def test_deprecated_name_config_file(self): + self.conf.register_opt(cfg.BoolOpt('foobar', + deprecated_name=self.opt_name)) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + + self.cf_name + ' = True\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(self.conf.foobar) + + def test_deprecated_opts(self): + oldopts = [cfg.DeprecatedOpt(self.opt_name)] + self.conf.register_opt(cfg.StrOpt('foobar', + deprecated_opts=oldopts)) + + self.assertTrue(hasattr(self.conf, 'foobar')) + self.assertFalse(hasattr(self.conf, self.opt_dest)) + self.assertFalse(hasattr(self.conf, self.broken_opt_dest)) + self.assertIn('foobar', self.conf) + self.assertNotIn(self.opt_dest, self.conf) + self.assertNotIn(self.broken_opt_dest, self.conf) + + def test_deprecated_opts_cli(self): + oldopts = [cfg.DeprecatedOpt(self.opt_name)] + self.conf.register_cli_opt(cfg.BoolOpt('foobar', + deprecated_opts=oldopts)) + + self.conf(['--' + self.cli_name]) + + self.assertTrue(self.conf.foobar) + + def test_deprecated_opts_config_file(self): + oldopts = [cfg.DeprecatedOpt(self.opt_name)] + self.conf.register_opt(cfg.BoolOpt('foobar', + deprecated_opts=oldopts)) + + # FIXME(markmc): this should be self.cf_name, see #1279973 + if self.broken: + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + + self.broken_cf_name + + ' = True\n')]) + else: + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + + self.cf_name + ' = True\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(self.conf.foobar) + + +class ReRegisterOptTestCase(BaseTestCase): + + def test_conf_file_re_register_opt(self): + opt = cfg.StrOpt('foo') + self.assertTrue(self.conf.register_opt(opt)) + self.assertFalse(self.conf.register_opt(opt)) + + def test_conf_file_re_register_opt_in_group(self): + group = cfg.OptGroup('blaa') + self.conf.register_group(group) + self.conf.register_group(group) # not an error + opt = cfg.StrOpt('foo') + self.assertTrue(self.conf.register_opt(opt, group=group)) + self.assertFalse(self.conf.register_opt(opt, group='blaa')) + + +class TemplateSubstitutionTestCase(BaseTestCase): + + def _prep_test_str_sub(self, foo_default=None, bar_default=None): + self.conf.register_cli_opt(cfg.StrOpt('foo', default=foo_default)) + self.conf.register_cli_opt(cfg.StrOpt('bar', default=bar_default)) + + def _assert_str_sub(self): + self.assertTrue(hasattr(self.conf, 'bar')) + self.assertEqual(self.conf.bar, 'blaa') + + def test_str_sub_default_from_default(self): + self._prep_test_str_sub(foo_default='blaa', bar_default='$foo') + + self.conf([]) + + self._assert_str_sub() + + def test_str_sub_default_from_default_recurse(self): + self.conf.register_cli_opt(cfg.StrOpt('blaa', default='blaa')) + self._prep_test_str_sub(foo_default='$blaa', bar_default='$foo') + + self.conf([]) + + self._assert_str_sub() + + def test_str_sub_default_from_arg(self): + self._prep_test_str_sub(bar_default='$foo') + + self.conf(['--foo', 'blaa']) + + self._assert_str_sub() + + def test_str_sub_default_from_config_file(self): + self._prep_test_str_sub(bar_default='$foo') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = blaa\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_str_sub() + + def test_str_sub_arg_from_default(self): + self._prep_test_str_sub(foo_default='blaa') + + self.conf(['--bar', '$foo']) + + self._assert_str_sub() + + def test_str_sub_arg_from_arg(self): + self._prep_test_str_sub() + + self.conf(['--foo', 'blaa', '--bar', '$foo']) + + self._assert_str_sub() + + def test_str_sub_arg_from_config_file(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = blaa\n')]) + + self.conf(['--config-file', paths[0], '--bar=$foo']) + + self._assert_str_sub() + + def test_str_sub_config_file_from_default(self): + self._prep_test_str_sub(foo_default='blaa') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar = $foo\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_str_sub() + + def test_str_sub_config_file_from_arg(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar = $foo\n')]) + + self.conf(['--config-file', paths[0], '--foo=blaa']) + + self._assert_str_sub() + + def test_str_sub_config_file_from_config_file(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar = $foo\n' + 'foo = blaa\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_str_sub() + + def test_str_sub_with_dollar_escape_char(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar=foo-somethin$$k2\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'bar')) + self.assertEqual(self.conf.bar, 'foo-somethin$k2') + + def test_str_sub_with_backslash_escape_char(self): + self._prep_test_str_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar=foo-somethin\$k2\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'bar')) + self.assertEqual(self.conf.bar, 'foo-somethin$k2') + + def test_str_sub_group_from_default(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', default='blaa')) + self.conf.register_group(cfg.OptGroup('ba')) + self.conf.register_cli_opt(cfg.StrOpt('r', default='$foo'), group='ba') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'ba')) + self.assertTrue(hasattr(self.conf.ba, 'r')) + self.assertEqual(self.conf.ba.r, 'blaa') + + def test_str_sub_set_default(self): + self._prep_test_str_sub() + self.conf.set_default('bar', '$foo') + self.conf.set_default('foo', 'blaa') + + self.conf([]) + + self._assert_str_sub() + + def test_str_sub_set_override(self): + self._prep_test_str_sub() + self.conf.set_override('bar', '$foo') + self.conf.set_override('foo', 'blaa') + + self.conf([]) + + self._assert_str_sub() + + def _prep_test_str_int_sub(self, foo_default=None, bar_default=None): + self.conf.register_cli_opt(cfg.StrOpt('foo', default=foo_default)) + self.conf.register_cli_opt(cfg.IntOpt('bar', default=bar_default)) + + def _assert_int_sub(self): + self.assertTrue(hasattr(self.conf, 'bar')) + self.assertEqual(self.conf.bar, 123) + + def test_sub_default_from_default(self): + self._prep_test_str_int_sub(foo_default='123', bar_default='$foo') + + self.conf([]) + + self._assert_int_sub() + + def test_sub_default_from_default_recurse(self): + self.conf.register_cli_opt(cfg.StrOpt('blaa', default='123')) + self._prep_test_str_int_sub(foo_default='$blaa', bar_default='$foo') + + self.conf([]) + + self._assert_int_sub() + + def test_sub_default_from_arg(self): + self._prep_test_str_int_sub(bar_default='$foo') + + self.conf(['--foo', '123']) + + self._assert_int_sub() + + def test_sub_default_from_config_file(self): + self._prep_test_str_int_sub(bar_default='$foo') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = 123\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_int_sub() + + def test_sub_arg_from_default(self): + self._prep_test_str_int_sub(foo_default='123') + + self.conf(['--bar', '$foo']) + + self._assert_int_sub() + + def test_sub_arg_from_arg(self): + self._prep_test_str_int_sub() + + self.conf(['--foo', '123', '--bar', '$foo']) + + self._assert_int_sub() + + def test_sub_arg_from_config_file(self): + self._prep_test_str_int_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = 123\n')]) + + self.conf(['--config-file', paths[0], '--bar=$foo']) + + self._assert_int_sub() + + def test_sub_config_file_from_default(self): + self._prep_test_str_int_sub(foo_default='123') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar = $foo\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_int_sub() + + def test_sub_config_file_from_arg(self): + self._prep_test_str_int_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar = $foo\n')]) + + self.conf(['--config-file', paths[0], '--foo=123']) + + self._assert_int_sub() + + def test_sub_config_file_from_config_file(self): + self._prep_test_str_int_sub() + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'bar = $foo\n' + 'foo = 123\n')]) + + self.conf(['--config-file', paths[0]]) + + self._assert_int_sub() + + def test_sub_group_from_default(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', default='123')) + self.conf.register_group(cfg.OptGroup('ba')) + self.conf.register_cli_opt(cfg.IntOpt('r', default='$foo'), group='ba') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'ba')) + self.assertTrue(hasattr(self.conf.ba, 'r')) + self.assertEqual(self.conf.foo, '123') + self.assertEqual(self.conf.ba.r, 123) + + def test_sub_group_from_default_deprecated(self): + self.conf.register_group(cfg.OptGroup('ba')) + self.conf.register_cli_opt(cfg.StrOpt( + 'foo', default='123', deprecated_group='DEFAULT'), group='ba') + self.conf.register_cli_opt(cfg.IntOpt('r', default='$foo'), group='ba') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'ba')) + self.assertTrue(hasattr(self.conf.ba, 'foo')) + self.assertEqual(self.conf.ba.foo, '123') + self.assertTrue(hasattr(self.conf.ba, 'r')) + self.assertEqual(self.conf.ba.r, 123) + + def test_sub_group_from_args_deprecated(self): + self.conf.register_group(cfg.OptGroup('ba')) + self.conf.register_cli_opt(cfg.StrOpt( + 'foo', default='123', deprecated_group='DEFAULT'), group='ba') + self.conf.register_cli_opt(cfg.IntOpt('r', default='$foo'), group='ba') + + self.conf(['--ba-foo=4242']) + + self.assertTrue(hasattr(self.conf, 'ba')) + self.assertTrue(hasattr(self.conf.ba, 'foo')) + self.assertTrue(hasattr(self.conf.ba, 'r')) + self.assertEqual(self.conf.ba.foo, '4242') + self.assertEqual(self.conf.ba.r, 4242) + + def test_sub_group_from_configfile_deprecated(self): + self.conf.register_group(cfg.OptGroup('ba')) + self.conf.register_cli_opt(cfg.StrOpt( + 'foo', default='123', deprecated_group='DEFAULT'), group='ba') + self.conf.register_cli_opt(cfg.IntOpt('r', default='$foo'), group='ba') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo=4242\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'ba')) + self.assertTrue(hasattr(self.conf.ba, 'foo')) + self.assertTrue(hasattr(self.conf.ba, 'r')) + self.assertEqual(self.conf.ba.foo, '4242') + self.assertEqual(self.conf.ba.r, 4242) + + +class ConfigDirTestCase(BaseTestCase): + + def test_config_dir(self): + snafu_group = cfg.OptGroup('snafu') + self.conf.register_group(snafu_group) + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.conf.register_cli_opt(cfg.StrOpt('bell'), group=snafu_group) + + dir = tempfile.mkdtemp() + self.tempdirs.append(dir) + + paths = self.create_tempfiles([(os.path.join(dir, '00-test'), + '[DEFAULT]\n' + 'foo = bar-00\n' + '[snafu]\n' + 'bell = whistle-00\n'), + (os.path.join(dir, '02-test'), + '[snafu]\n' + 'bell = whistle-02\n' + '[DEFAULT]\n' + 'foo = bar-02\n'), + (os.path.join(dir, '01-test'), + '[DEFAULT]\n' + 'foo = bar-01\n')]) + + self.conf(['--foo', 'bar', + '--config-dir', os.path.dirname(paths[0])]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar-02') + self.assertTrue(hasattr(self.conf, 'snafu')) + self.assertTrue(hasattr(self.conf.snafu, 'bell')) + self.assertEqual(self.conf.snafu.bell, 'whistle-02') + + def test_config_dir_file_precedence(self): + snafu_group = cfg.OptGroup('snafu') + self.conf.register_group(snafu_group) + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.conf.register_cli_opt(cfg.StrOpt('bell'), group=snafu_group) + + dir = tempfile.mkdtemp() + self.tempdirs.append(dir) + + paths = self.create_tempfiles([(os.path.join(dir, '00-test'), + '[DEFAULT]\n' + 'foo = bar-00\n'), + ('01-test', + '[snafu]\n' + 'bell = whistle-01\n' + '[DEFAULT]\n' + 'foo = bar-01\n'), + ('03-test', + '[snafu]\n' + 'bell = whistle-03\n' + '[DEFAULT]\n' + 'foo = bar-03\n'), + (os.path.join(dir, '02-test'), + '[DEFAULT]\n' + 'foo = bar-02\n')]) + + self.conf(['--foo', 'bar', + '--config-file', paths[1], + '--config-dir', os.path.dirname(paths[0]), + '--config-file', paths[2], ]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar-03') + self.assertTrue(hasattr(self.conf, 'snafu')) + self.assertTrue(hasattr(self.conf.snafu, 'bell')) + self.assertEqual(self.conf.snafu.bell, 'whistle-03') + + def test_config_dir_default_file_precedence(self): + snafu_group = cfg.OptGroup('snafu') + self.conf.register_group(snafu_group) + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.conf.register_cli_opt(cfg.StrOpt('bell'), group=snafu_group) + + dir = tempfile.mkdtemp() + self.tempdirs.append(dir) + + paths = self.create_tempfiles([(os.path.join(dir, '00-test'), + '[DEFAULT]\n' + 'foo = bar-00\n' + '[snafu]\n' + 'bell = whistle-11\n'), + ('01-test', + '[snafu]\n' + 'bell = whistle-01\n' + '[DEFAULT]\n' + 'foo = bar-01\n'), + ('03-test', + '[snafu]\n' + 'bell = whistle-03\n' + '[DEFAULT]\n' + 'foo = bar-03\n'), + (os.path.join(dir, '02-test'), + '[DEFAULT]\n' + 'foo = bar-02\n')]) + + self.conf(['--foo', 'bar', '--config-dir', os.path.dirname(paths[0])], + default_config_files=[paths[1], paths[2]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar-02') + self.assertTrue(hasattr(self.conf, 'snafu')) + self.assertTrue(hasattr(self.conf.snafu, 'bell')) + self.assertEqual(self.conf.snafu.bell, 'whistle-11') + + def test_config_dir_doesnt_exist(self): + tmpdir = '/tmp/foo' + + self.assertRaises(cfg.ConfigDirNotFoundError, + self.conf, + ['--config-dir', tmpdir] + ) + + +class ReparseTestCase(BaseTestCase): + + def test_reparse(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt( + cfg.StrOpt('foo', default='r'), group='blaa') + + paths = self.create_tempfiles([('test', + '[blaa]\n' + 'foo = b\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'b') + + self.conf(['--blaa-foo', 'a']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'a') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'r') + + +class OverridesTestCase(BaseTestCase): + + def test_default_none(self): + self.conf.register_opt(cfg.StrOpt('foo', default='foo')) + self.conf([]) + self.assertEqual(self.conf.foo, 'foo') + self.conf.set_default('foo', None) + self.assertIsNone(self.conf.foo) + self.conf.clear_default('foo') + self.assertEqual(self.conf.foo, 'foo') + + def test_override_none(self): + self.conf.register_opt(cfg.StrOpt('foo', default='foo')) + self.conf([]) + self.assertEqual(self.conf.foo, 'foo') + self.conf.set_override('foo', None) + self.assertIsNone(self.conf.foo) + self.conf.clear_override('foo') + self.assertEqual(self.conf.foo, 'foo') + + def test_no_default_override(self): + self.conf.register_opt(cfg.StrOpt('foo')) + self.conf([]) + self.assertIsNone(self.conf.foo) + self.conf.set_default('foo', 'bar') + self.assertEqual(self.conf.foo, 'bar') + self.conf.clear_default('foo') + self.assertIsNone(self.conf.foo) + + def test_default_override(self): + self.conf.register_opt(cfg.StrOpt('foo', default='foo')) + self.conf([]) + self.assertEqual(self.conf.foo, 'foo') + self.conf.set_default('foo', 'bar') + self.assertEqual(self.conf.foo, 'bar') + self.conf.clear_default('foo') + self.assertEqual(self.conf.foo, 'foo') + + def test_override(self): + self.conf.register_opt(cfg.StrOpt('foo')) + self.conf.set_override('foo', 'bar') + self.conf([]) + self.assertEqual(self.conf.foo, 'bar') + self.conf.clear_override('foo') + self.assertIsNone(self.conf.foo) + + def test_group_no_default_override(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo'), group='blaa') + self.conf([]) + self.assertIsNone(self.conf.blaa.foo) + self.conf.set_default('foo', 'bar', group='blaa') + self.assertEqual(self.conf.blaa.foo, 'bar') + self.conf.clear_default('foo', group='blaa') + self.assertIsNone(self.conf.blaa.foo) + + def test_group_default_override(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', default='foo'), group='blaa') + self.conf([]) + self.assertEqual(self.conf.blaa.foo, 'foo') + self.conf.set_default('foo', 'bar', group='blaa') + self.assertEqual(self.conf.blaa.foo, 'bar') + self.conf.clear_default('foo', group='blaa') + self.assertEqual(self.conf.blaa.foo, 'foo') + + def test_group_override(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo'), group='blaa') + self.assertIsNone(self.conf.blaa.foo) + self.conf.set_override('foo', 'bar', group='blaa') + self.conf([]) + self.assertEqual(self.conf.blaa.foo, 'bar') + self.conf.clear_override('foo', group='blaa') + self.assertIsNone(self.conf.blaa.foo) + + def test_cli_bool_default(self): + self.conf.register_cli_opt(cfg.BoolOpt('foo')) + self.conf.set_default('foo', True) + self.assertTrue(self.conf.foo) + self.conf([]) + self.assertTrue(self.conf.foo) + self.conf.set_default('foo', False) + self.assertFalse(self.conf.foo) + self.conf.clear_default('foo') + self.assertIsNone(self.conf.foo) + + def test_cli_bool_override(self): + self.conf.register_cli_opt(cfg.BoolOpt('foo')) + self.conf.set_override('foo', True) + self.assertTrue(self.conf.foo) + self.conf([]) + self.assertTrue(self.conf.foo) + self.conf.set_override('foo', False) + self.assertFalse(self.conf.foo) + self.conf.clear_override('foo') + self.assertIsNone(self.conf.foo) + + +class ResetAndClearTestCase(BaseTestCase): + + def test_clear(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.conf.register_cli_opt(cfg.StrOpt('bar'), group='blaa') + + self.assertIsNone(self.conf.foo) + self.assertIsNone(self.conf.blaa.bar) + + self.conf(['--foo', 'foo', '--blaa-bar', 'bar']) + + self.assertEqual(self.conf.foo, 'foo') + self.assertEqual(self.conf.blaa.bar, 'bar') + + self.conf.clear() + + self.assertIsNone(self.conf.foo) + self.assertIsNone(self.conf.blaa.bar) + + def test_reset_and_clear_with_defaults_and_overrides(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.conf.register_cli_opt(cfg.StrOpt('bar'), group='blaa') + + self.conf.set_default('foo', 'foo') + self.conf.set_override('bar', 'bar', group='blaa') + + self.conf(['--foo', 'foofoo']) + + self.assertEqual(self.conf.foo, 'foofoo') + self.assertEqual(self.conf.blaa.bar, 'bar') + + self.conf.clear() + + self.assertEqual(self.conf.foo, 'foo') + self.assertEqual(self.conf.blaa.bar, 'bar') + + self.conf.reset() + + self.assertIsNone(self.conf.foo) + self.assertIsNone(self.conf.blaa.bar) + + +class UnregisterOptTestCase(BaseTestCase): + + def test_unregister_opt(self): + opts = [cfg.StrOpt('foo'), cfg.StrOpt('bar')] + + self.conf.register_opts(opts) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertTrue(hasattr(self.conf, 'bar')) + + self.conf.unregister_opt(opts[0]) + + self.assertFalse(hasattr(self.conf, 'foo')) + self.assertTrue(hasattr(self.conf, 'bar')) + + self.conf([]) + + self.assertRaises(cfg.ArgsAlreadyParsedError, + self.conf.unregister_opt, opts[1]) + + self.conf.clear() + + self.assertTrue(hasattr(self.conf, 'bar')) + + self.conf.unregister_opts(opts) + + def test_unregister_opt_from_group(self): + opt = cfg.StrOpt('foo') + + self.conf.register_opt(opt, group='blaa') + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + + self.conf.unregister_opt(opt, group='blaa') + + self.assertFalse(hasattr(self.conf.blaa, 'foo')) + + +class ImportOptTestCase(BaseTestCase): + + def test_import_opt(self): + self.assertFalse(hasattr(cfg.CONF, 'blaa')) + cfg.CONF.import_opt('blaa', 'tests.testmods.blaa_opt') + self.assertTrue(hasattr(cfg.CONF, 'blaa')) + + def test_import_opt_in_group(self): + self.assertFalse(hasattr(cfg.CONF, 'bar')) + cfg.CONF.import_opt('foo', 'tests.testmods.bar_foo_opt', group='bar') + self.assertTrue(hasattr(cfg.CONF, 'bar')) + self.assertTrue(hasattr(cfg.CONF.bar, 'foo')) + + def test_import_opt_import_errror(self): + self.assertRaises(ImportError, cfg.CONF.import_opt, + 'blaa', 'tests.testmods.blaablaa_opt') + + def test_import_opt_no_such_opt(self): + self.assertRaises(cfg.NoSuchOptError, cfg.CONF.import_opt, + 'blaablaa', 'tests.testmods.blaa_opt') + + def test_import_opt_no_such_group(self): + self.assertRaises(cfg.NoSuchGroupError, cfg.CONF.import_opt, + 'blaa', 'tests.testmods.blaa_opt', group='blaa') + + +class ImportGroupTestCase(BaseTestCase): + + def test_import_group(self): + self.assertFalse(hasattr(cfg.CONF, 'qux')) + cfg.CONF.import_group('qux', 'tests.testmods.baz_qux_opt') + self.assertTrue(hasattr(cfg.CONF, 'qux')) + self.assertTrue(hasattr(cfg.CONF.qux, 'baz')) + + def test_import_group_import_error(self): + self.assertRaises(ImportError, cfg.CONF.import_group, + 'qux', 'tests.testmods.bazzz_quxxx_opt') + + def test_import_group_no_such_group(self): + self.assertRaises(cfg.NoSuchGroupError, cfg.CONF.import_group, + 'quxxx', 'tests.testmods.baz_qux_opt') + + +class RequiredOptsTestCase(BaseTestCase): + + def setUp(self): + BaseTestCase.setUp(self) + self.conf.register_opt(cfg.StrOpt('boo', required=False)) + + def test_required_opt(self): + self.conf.register_opt(cfg.StrOpt('foo', required=True)) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + def test_required_cli_opt(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', required=True)) + + self.conf(['--foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + def test_required_cli_opt_with_dash(self): + self.conf.register_cli_opt(cfg.StrOpt('foo-bar', required=True)) + + self.conf(['--foo-bar', 'baz']) + + self.assertTrue(hasattr(self.conf, 'foo_bar')) + self.assertEqual(self.conf.foo_bar, 'baz') + + def test_missing_required_opt(self): + self.conf.register_opt(cfg.StrOpt('foo', required=True)) + self.assertRaises(cfg.RequiredOptError, self.conf, []) + + def test_missing_required_cli_opt(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', required=True)) + self.assertRaises(cfg.RequiredOptError, self.conf, []) + + def test_required_group_opt(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', required=True), group='blaa') + + paths = self.create_tempfiles([('test', + '[blaa]\n' + 'foo = bar')]) + + self.conf(['--config-file', paths[0]]) + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_required_cli_group_opt(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt( + cfg.StrOpt('foo', required=True), group='blaa') + + self.conf(['--blaa-foo', 'bar']) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'foo')) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_missing_required_group_opt(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', required=True), group='blaa') + self.assertRaises(cfg.RequiredOptError, self.conf, []) + + def test_missing_required_cli_group_opt(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt( + cfg.StrOpt('foo', required=True), group='blaa') + self.assertRaises(cfg.RequiredOptError, self.conf, []) + + def test_required_opt_with_default(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', required=True)) + self.conf.set_default('foo', 'bar') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + def test_required_opt_with_override(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', required=True)) + self.conf.set_override('foo', 'bar') + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + +class SadPathTestCase(BaseTestCase): + + def test_unknown_attr(self): + self.conf([]) + self.assertFalse(hasattr(self.conf, 'foo')) + self.assertRaises(AttributeError, getattr, self.conf, 'foo') + self.assertRaises(cfg.NoSuchOptError, self.conf._get, 'foo') + self.assertRaises(cfg.NoSuchOptError, self.conf.__getattr__, 'foo') + + def test_unknown_attr_is_attr_error(self): + self.conf([]) + self.assertFalse(hasattr(self.conf, 'foo')) + self.assertRaises(AttributeError, getattr, self.conf, 'foo') + + def test_unknown_group_attr(self): + self.conf.register_group(cfg.OptGroup('blaa')) + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertFalse(hasattr(self.conf.blaa, 'foo')) + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf.blaa, 'foo') + + def test_ok_duplicate(self): + opt = cfg.StrOpt('foo') + self.conf.register_cli_opt(opt) + opt2 = cfg.StrOpt('foo') + self.conf.register_cli_opt(opt2) + + self.conf([]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertIsNone(self.conf.foo) + + def test_error_duplicate(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', help='bar')) + self.assertRaises(cfg.DuplicateOptError, + self.conf.register_cli_opt, cfg.StrOpt('foo')) + + def test_error_duplicate_with_different_dest(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', dest='f')) + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.assertRaises(cfg.DuplicateOptError, self.conf, []) + + def test_error_duplicate_short(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', short='f')) + self.conf.register_cli_opt(cfg.StrOpt('bar', short='f')) + self.assertRaises(cfg.DuplicateOptError, self.conf, []) + + def test_already_parsed(self): + self.conf([]) + + self.assertRaises(cfg.ArgsAlreadyParsedError, + self.conf.register_cli_opt, cfg.StrOpt('foo')) + + def test_bad_cli_arg(self): + self.conf.register_opt(cfg.BoolOpt('foo')) + + self.useFixture(fixtures.MonkeyPatch('sys.stderr', moves.StringIO())) + + self.assertRaises(SystemExit, self.conf, ['--foo']) + + self.assertTrue('error' in sys.stderr.getvalue()) + self.assertTrue('--foo' in sys.stderr.getvalue()) + + def _do_test_bad_cli_value(self, opt_class): + self.conf.register_cli_opt(opt_class('foo')) + + self.useFixture(fixtures.MonkeyPatch('sys.stderr', moves.StringIO())) + + self.assertRaises(SystemExit, self.conf, ['--foo', 'bar']) + + self.assertTrue('foo' in sys.stderr.getvalue()) + self.assertTrue('bar' in sys.stderr.getvalue()) + + def test_bad_int_arg(self): + self._do_test_bad_cli_value(cfg.IntOpt) + + def test_bad_float_arg(self): + self._do_test_bad_cli_value(cfg.FloatOpt) + + def test_conf_file_not_found(self): + (fd, path) = tempfile.mkstemp() + + os.remove(path) + + self.assertRaises(cfg.ConfigFilesNotFoundError, + self.conf, ['--config-file', path]) + + def test_conf_file_permission_denied(self): + (fd, path) = tempfile.mkstemp() + + os.chmod(path, 0x000) + + self.assertRaises(cfg.ConfigFilesPermissionDeniedError, + self.conf, ['--config-file', path]) + os.remove(path) + + def test_conf_file_broken(self): + paths = self.create_tempfiles([('test', 'foo')]) + + self.assertRaises(cfg.ConfigFileParseError, + self.conf, ['--config-file', paths[0]]) + + def _do_test_conf_file_bad_value(self, opt_class): + self.conf.register_opt(opt_class('foo')) + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertRaises(AttributeError, getattr, self.conf, 'foo') + self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo') + + def test_conf_file_bad_bool(self): + self._do_test_conf_file_bad_value(cfg.BoolOpt) + + def test_conf_file_bad_int(self): + self._do_test_conf_file_bad_value(cfg.IntOpt) + + def test_conf_file_bad_float(self): + self._do_test_conf_file_bad_value(cfg.FloatOpt) + + def test_str_sub_from_group(self): + self.conf.register_group(cfg.OptGroup('f')) + self.conf.register_cli_opt(cfg.StrOpt('oo', default='blaa'), group='f') + self.conf.register_cli_opt(cfg.StrOpt('bar', default='$f.oo')) + + self.conf([]) + + self.assertFalse(hasattr(self.conf, 'bar')) + self.assertRaises( + AttributeError, getattr, self.conf, 'bar') + self.assertRaises( + cfg.TemplateSubstitutionError, self.conf._get, 'bar') + + def test_set_default_unknown_attr(self): + self.conf([]) + self.assertRaises( + cfg.NoSuchOptError, self.conf.set_default, 'foo', 'bar') + + def test_set_default_unknown_group(self): + self.conf([]) + self.assertRaises(cfg.NoSuchGroupError, + self.conf.set_default, 'foo', 'bar', group='blaa') + + def test_set_override_unknown_attr(self): + self.conf([]) + self.assertRaises( + cfg.NoSuchOptError, self.conf.set_override, 'foo', 'bar') + + def test_set_override_unknown_group(self): + self.conf([]) + self.assertRaises(cfg.NoSuchGroupError, + self.conf.set_override, 'foo', 'bar', group='blaa') + + +class FindFileTestCase(BaseTestCase): + + def test_find_policy_file(self): + policy_file = '/etc/policy.json' + + self.useFixture(fixtures.MonkeyPatch( + 'os.path.exists', + lambda p: p == policy_file)) + + self.conf([]) + + self.assertIsNone(self.conf.find_file('foo.json')) + self.assertEqual(self.conf.find_file('policy.json'), policy_file) + + def test_find_policy_file_with_config_file(self): + dir = tempfile.mkdtemp() + self.tempdirs.append(dir) + + paths = self.create_tempfiles([(os.path.join(dir, 'test.conf'), + '[DEFAULT]'), + (os.path.join(dir, 'policy.json'), + '{}')], + ext='') + + self.conf(['--config-file', paths[0]]) + + self.assertEqual(self.conf.find_file('policy.json'), paths[1]) + + def test_find_policy_file_with_config_dir(self): + dir = tempfile.mkdtemp() + self.tempdirs.append(dir) + + path = self.create_tempfiles([(os.path.join(dir, 'policy.json'), + '{}')], + ext='')[0] + + self.conf(['--config-dir', dir]) + + self.assertEqual(self.conf.find_file('policy.json'), path) + + +class OptDumpingTestCase(BaseTestCase): + + class FakeLogger: + + def __init__(self, test_case, expected_lvl): + self.test_case = test_case + self.expected_lvl = expected_lvl + self.logged = [] + + def log(self, lvl, fmt, *args): + self.test_case.assertEqual(lvl, self.expected_lvl) + self.logged.append(fmt % args) + + def setUp(self): + super(OptDumpingTestCase, self).setUp() + self._args = ['--foo', 'this', '--blaa-bar', 'that', + '--blaa-key', 'admin', '--passwd', 'hush'] + + def _do_test_log_opt_values(self, args): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.conf.register_cli_opt(cfg.StrOpt('passwd', secret=True)) + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt(cfg.StrOpt('bar'), 'blaa') + self.conf.register_cli_opt(cfg.StrOpt('key', secret=True), 'blaa') + + self.conf(args) + + logger = self.FakeLogger(self, 666) + + self.conf.log_opt_values(logger, 666) + + self.assertEqual(logger.logged, [ + "*" * 80, + "Configuration options gathered from:", + "command line args: ['--foo', 'this', '--blaa-bar', " + "'that', '--blaa-key', 'admin', '--passwd', 'hush']", + "config files: []", + "=" * 80, + "config_dir = None", + "config_file = []", + "foo = this", + "passwd = ****", + "blaa.bar = that", + "blaa.key = ****", + "*" * 80, + ]) + + def test_log_opt_values(self): + self._do_test_log_opt_values(self._args) + + def test_log_opt_values_from_sys_argv(self): + self.useFixture(fixtures.MonkeyPatch('sys.argv', ['foo'] + self._args)) + self._do_test_log_opt_values(None) + + +class ConfigParserTestCase(BaseTestCase): + + def test_parse_file(self): + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n' + '[BLAA]\n' + 'bar = foo\n')]) + + sections = {} + parser = cfg.ConfigParser(paths[0], sections) + parser.parse() + + self.assertTrue('DEFAULT' in sections) + self.assertTrue('BLAA' in sections) + self.assertEqual(sections['DEFAULT']['foo'], ['bar']) + self.assertEqual(sections['BLAA']['bar'], ['foo']) + + def test_parse_file_with_normalized(self): + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n' + '[BLAA]\n' + 'bar = foo\n')]) + + sections = {} + normalized = {} + parser = cfg.ConfigParser(paths[0], sections) + parser._add_normalized(normalized) + parser.parse() + + self.assertTrue('DEFAULT' in sections) + self.assertTrue('DEFAULT' in normalized) + self.assertTrue('BLAA' in sections) + self.assertTrue('blaa' in normalized) + self.assertEqual(sections['DEFAULT']['foo'], ['bar']) + self.assertEqual(normalized['DEFAULT']['foo'], ['bar']) + self.assertEqual(sections['BLAA']['bar'], ['foo']) + self.assertEqual(normalized['blaa']['bar'], ['foo']) + + def test_no_section(self): + with tempfile.NamedTemporaryFile() as tmpfile: + tmpfile.write(six.b('foo = bar')) + tmpfile.flush() + + parser = cfg.ConfigParser(tmpfile.name, {}) + self.assertRaises(cfg.ParseError, parser.parse) + + def test__parse_file_ioerror(self): + # Test that IOErrors (other than 'No such file or directory') + # are propagated. + filename = 'fake' + namespace = mock.Mock() + with mock.patch('oslo_config.cfg.ConfigParser.parse') as parse: + parse.side_effect = IOError(errno.EMFILE, filename, + 'Too many open files') + self.assertRaises(IOError, cfg.ConfigParser._parse_file, filename, + namespace) + + +class MultiConfigParserTestCase(BaseTestCase): + + def test_parse_single_file(self): + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bar\n' + '[BLAA]\n' + 'bar = foo\n')]) + + parser = cfg.MultiConfigParser() + read_ok = parser.read(paths) + + self.assertEqual(read_ok, paths) + + self.assertTrue('DEFAULT' in parser.parsed[0]) + self.assertEqual(parser.parsed[0]['DEFAULT']['foo'], ['bar']) + self.assertEqual(parser.get([('DEFAULT', 'foo')]), ['bar']) + self.assertEqual(parser.get([('DEFAULT', 'foo')], multi=True), + ['bar']) + self.assertEqual(parser.get([('DEFAULT', 'foo')], multi=True), + ['bar']) + self.assertEqual(parser._get([('DEFAULT', 'foo')], + multi=True, normalized=True), + ['bar']) + + self.assertTrue('BLAA' in parser.parsed[0]) + self.assertEqual(parser.parsed[0]['BLAA']['bar'], ['foo']) + self.assertEqual(parser.get([('BLAA', 'bar')]), ['foo']) + self.assertEqual(parser.get([('BLAA', 'bar')], multi=True), + ['foo']) + self.assertEqual(parser._get([('blaa', 'bar')], + multi=True, normalized=True), + ['foo']) + + def test_parse_multiple_files(self): + paths = self.create_tempfiles([('test1', + '[DEFAULT]\n' + 'foo = bar\n' + '[BLAA]\n' + 'bar = foo'), + ('test2', + '[DEFAULT]\n' + 'foo = barbar\n' + '[BLAA]\n' + 'bar = foofoo\n' + '[bLAa]\n' + 'bar = foofoofoo\n')]) + + parser = cfg.MultiConfigParser() + read_ok = parser.read(paths) + + self.assertEqual(read_ok, paths) + + self.assertTrue('DEFAULT' in parser.parsed[0]) + self.assertEqual(parser.parsed[0]['DEFAULT']['foo'], ['barbar']) + self.assertTrue('DEFAULT' in parser.parsed[1]) + self.assertEqual(parser.parsed[1]['DEFAULT']['foo'], ['bar']) + self.assertEqual(parser.get([('DEFAULT', 'foo')]), ['barbar']) + self.assertEqual(parser.get([('DEFAULT', 'foo')], multi=True), + ['bar', 'barbar']) + + self.assertTrue('BLAA' in parser.parsed[0]) + self.assertTrue('bLAa' in parser.parsed[0]) + self.assertEqual(parser.parsed[0]['BLAA']['bar'], ['foofoo']) + self.assertEqual(parser.parsed[0]['bLAa']['bar'], ['foofoofoo']) + self.assertTrue('BLAA' in parser.parsed[1]) + self.assertEqual(parser.parsed[1]['BLAA']['bar'], ['foo']) + self.assertEqual(parser.get([('BLAA', 'bar')]), ['foofoo']) + self.assertEqual(parser.get([('bLAa', 'bar')]), ['foofoofoo']) + self.assertEqual(parser.get([('BLAA', 'bar')], multi=True), + ['foo', 'foofoo']) + self.assertEqual(parser._get([('BLAA', 'bar')], + multi=True, normalized=True), + ['foo', 'foofoo', 'foofoofoo']) + + +class TildeExpansionTestCase(BaseTestCase): + + def test_config_file_tilde(self): + homedir = os.path.expanduser('~') + tmpfile = tempfile.mktemp(dir=homedir, prefix='cfg-', suffix='.conf') + tmpbase = os.path.basename(tmpfile) + + try: + self.conf(['--config-file', os.path.join('~', tmpbase)]) + except cfg.ConfigFilesNotFoundError as cfnfe: + self.assertTrue(homedir in str(cfnfe)) + + self.useFixture(fixtures.MonkeyPatch( + 'os.path.exists', + lambda p: p == tmpfile)) + + self.assertEqual(self.conf.find_file(tmpbase), tmpfile) + + def test_config_dir_tilde(self): + homedir = os.path.expanduser('~') + try: + tmpdir = tempfile.mkdtemp(dir=homedir, + prefix='cfg-', + suffix='.d') + tmpfile = os.path.join(tmpdir, 'foo.conf') + + self.useFixture(fixtures.MonkeyPatch( + 'glob.glob', + lambda p: [tmpfile])) + + e = self.assertRaises(cfg.ConfigFilesNotFoundError, + self.conf, + ['--config-dir', + os.path.join('~', + os.path.basename(tmpdir))] + ) + self.assertIn(tmpdir, str(e)) + finally: + try: + shutil.rmtree(tmpdir) + except OSError as exc: + if exc.errno != 2: + raise + + +class SubCommandTestCase(BaseTestCase): + + def test_sub_command(self): + def add_parsers(subparsers): + sub = subparsers.add_parser('a') + sub.add_argument('bar', type=int) + + self.conf.register_cli_opt( + cfg.SubCommandOpt('cmd', handler=add_parsers)) + self.assertTrue(hasattr(self.conf, 'cmd')) + self.conf(['a', '10']) + self.assertTrue(hasattr(self.conf.cmd, 'name')) + self.assertTrue(hasattr(self.conf.cmd, 'bar')) + self.assertEqual(self.conf.cmd.name, 'a') + self.assertEqual(self.conf.cmd.bar, 10) + + def test_sub_command_with_parent(self): + def add_parsers(subparsers): + parent = argparse.ArgumentParser(add_help=False) + parent.add_argument('bar', type=int) + subparsers.add_parser('a', parents=[parent]) + + self.conf.register_cli_opt( + cfg.SubCommandOpt('cmd', handler=add_parsers)) + self.assertTrue(hasattr(self.conf, 'cmd')) + self.conf(['a', '10']) + self.assertTrue(hasattr(self.conf.cmd, 'name')) + self.assertTrue(hasattr(self.conf.cmd, 'bar')) + self.assertEqual(self.conf.cmd.name, 'a') + self.assertEqual(self.conf.cmd.bar, 10) + + def test_sub_command_with_dest(self): + def add_parsers(subparsers): + subparsers.add_parser('a') + + self.conf.register_cli_opt( + cfg.SubCommandOpt('cmd', dest='command', handler=add_parsers)) + self.assertTrue(hasattr(self.conf, 'command')) + self.conf(['a']) + self.assertEqual(self.conf.command.name, 'a') + + def test_sub_command_with_group(self): + def add_parsers(subparsers): + sub = subparsers.add_parser('a') + sub.add_argument('--bar', choices='XYZ') + + self.conf.register_cli_opt( + cfg.SubCommandOpt('cmd', handler=add_parsers), group='blaa') + self.assertTrue(hasattr(self.conf, 'blaa')) + self.assertTrue(hasattr(self.conf.blaa, 'cmd')) + self.conf(['a', '--bar', 'Z']) + self.assertTrue(hasattr(self.conf.blaa.cmd, 'name')) + self.assertTrue(hasattr(self.conf.blaa.cmd, 'bar')) + self.assertEqual(self.conf.blaa.cmd.name, 'a') + self.assertEqual(self.conf.blaa.cmd.bar, 'Z') + + def test_sub_command_not_cli(self): + self.conf.register_opt(cfg.SubCommandOpt('cmd')) + self.conf([]) + + def test_sub_command_resparse(self): + def add_parsers(subparsers): + subparsers.add_parser('a') + + self.conf.register_cli_opt( + cfg.SubCommandOpt('cmd', handler=add_parsers)) + + foo_opt = cfg.StrOpt('foo') + self.conf.register_cli_opt(foo_opt) + + self.conf(['--foo=bar', 'a']) + + self.assertTrue(hasattr(self.conf.cmd, 'name')) + self.assertEqual(self.conf.cmd.name, 'a') + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar') + + self.conf.clear() + self.conf.unregister_opt(foo_opt) + self.conf(['a']) + + self.assertTrue(hasattr(self.conf.cmd, 'name')) + self.assertEqual(self.conf.cmd.name, 'a') + self.assertFalse(hasattr(self.conf, 'foo')) + + def test_sub_command_no_handler(self): + self.conf.register_cli_opt(cfg.SubCommandOpt('cmd')) + self.useFixture(fixtures.MonkeyPatch('sys.stderr', moves.StringIO())) + self.assertRaises(SystemExit, self.conf, []) + self.assertTrue('error' in sys.stderr.getvalue()) + + def test_sub_command_with_help(self): + def add_parsers(subparsers): + subparsers.add_parser('a') + + self.conf.register_cli_opt(cfg.SubCommandOpt('cmd', + title='foo foo', + description='bar bar', + help='blaa blaa', + handler=add_parsers)) + self.useFixture(fixtures.MonkeyPatch('sys.stdout', moves.StringIO())) + self.assertRaises(SystemExit, self.conf, ['--help']) + self.assertTrue('foo foo' in sys.stdout.getvalue()) + self.assertTrue('bar bar' in sys.stdout.getvalue()) + self.assertTrue('blaa blaa' in sys.stdout.getvalue()) + + def test_sub_command_errors(self): + def add_parsers(subparsers): + sub = subparsers.add_parser('a') + sub.add_argument('--bar') + + self.conf.register_cli_opt(cfg.BoolOpt('bar')) + self.conf.register_cli_opt( + cfg.SubCommandOpt('cmd', handler=add_parsers)) + self.conf(['a']) + self.assertRaises(cfg.DuplicateOptError, getattr, self.conf.cmd, 'bar') + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf.cmd, 'foo') + + def test_sub_command_multiple(self): + self.conf.register_cli_opt(cfg.SubCommandOpt('cmd1')) + self.conf.register_cli_opt(cfg.SubCommandOpt('cmd2')) + self.useFixture(fixtures.MonkeyPatch('sys.stderr', moves.StringIO())) + self.assertRaises(SystemExit, self.conf, []) + self.assertTrue('multiple' in sys.stderr.getvalue()) + + +class SetDefaultsTestCase(BaseTestCase): + + def test_default_to_none(self): + opts = [cfg.StrOpt('foo', default='foo')] + self.conf.register_opts(opts) + cfg.set_defaults(opts, foo=None) + self.conf([]) + self.assertIsNone(self.conf.foo) + + def test_default_from_none(self): + opts = [cfg.StrOpt('foo')] + self.conf.register_opts(opts) + cfg.set_defaults(opts, foo='bar') + self.conf([]) + self.assertEqual(self.conf.foo, 'bar') + + def test_change_default(self): + opts = [cfg.StrOpt('foo', default='foo')] + self.conf.register_opts(opts) + cfg.set_defaults(opts, foo='bar') + self.conf([]) + self.assertEqual(self.conf.foo, 'bar') + + def test_change_default_many(self): + opts = [cfg.StrOpt('foo', default='foo'), + cfg.StrOpt('foo2', default='foo2')] + self.conf.register_opts(opts) + cfg.set_defaults(opts, foo='bar', foo2='bar2') + self.conf([]) + self.assertEqual(self.conf.foo, 'bar') + self.assertEqual(self.conf.foo2, 'bar2') + + def test_group_default_to_none(self): + opts = [cfg.StrOpt('foo', default='foo')] + self.conf.register_opts(opts, group='blaa') + cfg.set_defaults(opts, foo=None) + self.conf([]) + self.assertIsNone(self.conf.blaa.foo) + + def test_group_default_from_none(self): + opts = [cfg.StrOpt('foo')] + self.conf.register_opts(opts, group='blaa') + cfg.set_defaults(opts, foo='bar') + self.conf([]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_group_change_default(self): + opts = [cfg.StrOpt('foo', default='foo')] + self.conf.register_opts(opts, group='blaa') + cfg.set_defaults(opts, foo='bar') + self.conf([]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + +class DeprecatedOptionsTestCase(BaseTestCase): + + def test_deprecated_opts_equal(self): + d1 = cfg.DeprecatedOpt('oldfoo', group='oldgroup') + d2 = cfg.DeprecatedOpt('oldfoo', group='oldgroup') + self.assertEqual(d1, d2) + + def test_deprecated_opts_not_equal(self): + d1 = cfg.DeprecatedOpt('oldfoo', group='oldgroup') + d2 = cfg.DeprecatedOpt('oldfoo2', group='oldgroup') + self.assertNotEqual(d1, d2) + + +class MultipleDeprecatedOptionsTestCase(BaseTestCase): + + def test_conf_file_override_use_deprecated_name_and_group(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_opt(cfg.StrOpt('foo', + deprecated_name='oldfoo', + deprecated_group='oldgroup'), + group='blaa') + + paths = self.create_tempfiles([('test', + '[oldgroup]\n' + 'oldfoo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_conf_file_override_use_deprecated_opts(self): + self.conf.register_group(cfg.OptGroup('blaa')) + oldopts = [cfg.DeprecatedOpt('oldfoo', group='oldgroup')] + self.conf.register_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), + group='blaa') + + paths = self.create_tempfiles([('test', + '[oldgroup]\n' + 'oldfoo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_conf_file_override_use_deprecated_multi_opts(self): + self.conf.register_group(cfg.OptGroup('blaa')) + oldopts = [cfg.DeprecatedOpt('oldfoo', group='oldgroup'), + cfg.DeprecatedOpt('oldfoo2', group='oldgroup2')] + self.conf.register_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), + group='blaa') + + paths = self.create_tempfiles([('test', + '[oldgroup2]\n' + 'oldfoo2 = bar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + +class MultipleDeprecatedCliOptionsTestCase(BaseTestCase): + + def test_conf_file_override_use_deprecated_name_and_group(self): + self.conf.register_group(cfg.OptGroup('blaa')) + self.conf.register_cli_opt(cfg.StrOpt('foo', + deprecated_name='oldfoo', + deprecated_group='oldgroup'), + group='blaa') + + paths = self.create_tempfiles([('test', + '[oldgroup]\n' + 'oldfoo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_conf_file_override_use_deprecated_opts(self): + self.conf.register_group(cfg.OptGroup('blaa')) + oldopts = [cfg.DeprecatedOpt('oldfoo', group='oldgroup')] + self.conf.register_cli_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), + group='blaa') + + paths = self.create_tempfiles([('test', + '[oldgroup]\n' + 'oldfoo = bar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_conf_file_override_use_deprecated_multi_opts(self): + self.conf.register_group(cfg.OptGroup('blaa')) + oldopts = [cfg.DeprecatedOpt('oldfoo', group='oldgroup'), + cfg.DeprecatedOpt('oldfoo2', group='oldgroup2')] + self.conf.register_cli_opt(cfg.StrOpt('foo', deprecated_opts=oldopts), + group='blaa') + + paths = self.create_tempfiles([('test', + '[oldgroup2]\n' + 'oldfoo2 = bar\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.blaa.foo, 'bar') + + def test_conf_file_common_deprecated_group(self): + self.conf.register_group(cfg.OptGroup('foo')) + self.conf.register_group(cfg.OptGroup('bar')) + oldopts = [cfg.DeprecatedOpt('foo', group='DEFAULT')] + self.conf.register_opt(cfg.StrOpt('common_opt', + deprecated_opts=oldopts), + group='bar') + self.conf.register_opt(cfg.StrOpt('common_opt', + deprecated_opts=oldopts), + group='foo') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bla\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.foo.common_opt, 'bla') + self.assertEqual(self.conf.bar.common_opt, 'bla') + + paths = self.create_tempfiles([('test', + '[DEFAULT]\n' + 'foo = bla\n' + '[bar]\n' + 'common_opt = blabla\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.foo.common_opt, 'bla') + self.assertEqual(self.conf.bar.common_opt, 'blabla') + + paths = self.create_tempfiles([('test', + '[foo]\n' + 'common_opt = bla\n' + '[bar]\n' + 'common_opt = blabla\n')]) + + self.conf(['--config-file', paths[0]]) + self.assertEqual(self.conf.foo.common_opt, 'bla') + self.assertEqual(self.conf.bar.common_opt, 'blabla') + + +class ChoicesTestCase(BaseTestCase): + + def test_choice_default(self): + self.conf.register_cli_opt(cfg.StrOpt('protocol', + default='http', + choices=['http', 'https', 'ftp'])) + self.conf([]) + self.assertEqual(self.conf.protocol, 'http') + + def test_choice_good(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', + choices=['bar1', 'bar2'])) + self.conf(['--foo', 'bar1']) + self.assertEqual(self.conf.foo, 'bar1') + + def test_choice_bad(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', + choices=['bar1', 'bar2'])) + self.assertRaises(SystemExit, self.conf, ['--foo', 'bar3']) + + def test_conf_file_choice_value(self): + self.conf.register_opt(cfg.StrOpt('foo', + choices=['bar1', 'bar2'])) + + paths = self.create_tempfiles([('test', '[DEFAULT]\n''foo = bar1\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'bar1') + + def test_conf_file_bad_choice_value(self): + self.conf.register_opt(cfg.StrOpt('foo', + choices=['bar1', 'bar2'])) + + paths = self.create_tempfiles([('test', '[DEFAULT]\n''foo = bar3\n')]) + + self.conf(['--config-file', paths[0]]) + + self.assertRaises(cfg.ConfigFileValueError, self.conf._get, 'foo') + self.assertRaises(AttributeError, getattr, self.conf, 'foo') + + def test_conf_file_choice_value_override(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', + choices=['baar', 'baaar'])) + + paths = self.create_tempfiles([('1', + '[DEFAULT]\n' + 'foo = baar\n'), + ('2', + '[DEFAULT]\n' + 'foo = baaar\n')]) + + self.conf(['--foo', 'baar', + '--config-file', paths[0], + '--config-file', paths[1]]) + + self.assertTrue(hasattr(self.conf, 'foo')) + self.assertEqual(self.conf.foo, 'baaar') + + def test_conf_file_choice_bad_default(self): + self.conf.register_cli_opt(cfg.StrOpt('foo', + choices=['baar', 'baaar'], + default='foobaz')) + self.conf([]) + self.assertRaises(AttributeError, + getattr, + self.conf, + 'foobaz') + + +class PrintHelpTestCase(base.BaseTestCase): + + def test_print_help_without_init(self): + conf = cfg.ConfigOpts() + conf.register_opts([]) + self.assertRaises(cfg.NotInitializedError, + conf.print_help) + + def test_print_help_with_clear(self): + conf = cfg.ConfigOpts() + conf.register_opts([]) + conf([]) + conf.clear() + self.assertRaises(cfg.NotInitializedError, + conf.print_help) + + +class OptTestCase(base.BaseTestCase): + + def test_opt_eq(self): + d1 = cfg.ListOpt('oldfoo') + d2 = cfg.ListOpt('oldfoo') + self.assertEqual(d1, d2) + + def test_opt_not_eq(self): + d1 = cfg.ListOpt('oldfoo') + d2 = cfg.ListOpt('oldbar') + self.assertNotEqual(d1, d2) + + def test_illegal_name(self): + self.assertRaises(ValueError, cfg.BoolOpt, '_foo') diff --git a/oslo_config/tests/test_cfgfilter.py b/oslo_config/tests/test_cfgfilter.py new file mode 100644 index 00000000..63c34021 --- /dev/null +++ b/oslo_config/tests/test_cfgfilter.py @@ -0,0 +1,280 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base as test_base + +from oslo_config import cfg +from oslo_config import cfgfilter + + +class BaseTestCase(test_base.BaseTestCase): + + def setUp(self, conf=None): + super(BaseTestCase, self).setUp() + if conf is None: + self.conf = cfg.ConfigOpts() + else: + self.conf = conf + self.fconf = cfgfilter.ConfigFilter(self.conf) + + +class RegisterTestCase(BaseTestCase): + + def test_register_opt_default(self): + self.fconf.register_opt(cfg.StrOpt('foo', default='bar')) + + self.assertEqual('bar', self.fconf.foo) + self.assertEqual('bar', self.fconf['foo']) + self.assertIn('foo', self.fconf) + self.assertEqual(['foo'], list(self.fconf)) + self.assertEqual(1, len(self.fconf)) + + self.assertNotIn('foo', self.conf) + self.assertEqual(0, len(self.conf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf, 'foo') + + def test_register_opt_none_default(self): + self.fconf.register_opt(cfg.StrOpt('foo')) + + self.assertIsNone(self.fconf.foo) + self.assertIsNone(self.fconf['foo']) + self.assertIn('foo', self.fconf) + self.assertEqual(['foo'], list(self.fconf)) + self.assertEqual(1, len(self.fconf)) + + self.assertNotIn('foo', self.conf) + self.assertEqual(0, len(self.conf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf, 'foo') + + def test_register_grouped_opt_default(self): + self.fconf.register_opt(cfg.StrOpt('foo', default='bar'), + group='blaa') + + self.assertEqual('bar', self.fconf.blaa.foo) + self.assertEqual('bar', self.fconf['blaa']['foo']) + self.assertIn('blaa', self.fconf) + self.assertIn('foo', self.fconf.blaa) + self.assertEqual(['blaa'], list(self.fconf)) + self.assertEqual(['foo'], list(self.fconf.blaa)) + self.assertEqual(1, len(self.fconf)) + self.assertEqual(1, len(self.fconf.blaa)) + + self.assertNotIn('blaa', self.conf) + self.assertEqual(0, len(self.conf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf, 'blaa') + + def test_register_grouped_opt_none_default(self): + self.fconf.register_opt(cfg.StrOpt('foo'), group='blaa') + + self.assertIsNone(self.fconf.blaa.foo) + self.assertIsNone(self.fconf['blaa']['foo']) + self.assertIn('blaa', self.fconf) + self.assertIn('foo', self.fconf.blaa) + self.assertEqual(['blaa'], list(self.fconf)) + self.assertEqual(['foo'], list(self.fconf.blaa)) + self.assertEqual(1, len(self.fconf)) + self.assertEqual(1, len(self.fconf.blaa)) + + self.assertNotIn('blaa', self.conf) + self.assertEqual(0, len(self.conf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf, 'blaa') + + def test_register_group(self): + group = cfg.OptGroup('blaa') + self.fconf.register_group(group) + self.fconf.register_opt(cfg.StrOpt('foo'), group=group) + + self.assertIsNone(self.fconf.blaa.foo) + self.assertIsNone(self.fconf['blaa']['foo']) + self.assertIn('blaa', self.fconf) + self.assertIn('foo', self.fconf.blaa) + self.assertEqual(['blaa'], list(self.fconf)) + self.assertEqual(['foo'], list(self.fconf.blaa)) + self.assertEqual(1, len(self.fconf)) + self.assertEqual(1, len(self.fconf.blaa)) + + self.assertNotIn('blaa', self.conf) + self.assertEqual(0, len(self.conf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.conf, 'blaa') + + def test_register_opts(self): + self.fconf.register_opts([cfg.StrOpt('foo'), + cfg.StrOpt('bar')]) + self.assertIn('foo', self.fconf) + self.assertIn('bar', self.fconf) + self.assertNotIn('foo', self.conf) + self.assertNotIn('bar', self.conf) + + def test_register_cli_opt(self): + self.fconf.register_cli_opt(cfg.StrOpt('foo')) + self.assertIn('foo', self.fconf) + self.assertNotIn('foo', self.conf) + + def test_register_cli_opts(self): + self.fconf.register_cli_opts([cfg.StrOpt('foo'), cfg.StrOpt('bar')]) + self.assertIn('foo', self.fconf) + self.assertIn('bar', self.fconf) + self.assertNotIn('foo', self.conf) + self.assertNotIn('bar', self.conf) + + def test_register_opts_grouped(self): + self.fconf.register_opts([cfg.StrOpt('foo'), cfg.StrOpt('bar')], + group='blaa') + self.assertIn('foo', self.fconf.blaa) + self.assertIn('bar', self.fconf.blaa) + self.assertNotIn('blaa', self.conf) + + def test_register_cli_opt_grouped(self): + self.fconf.register_cli_opt(cfg.StrOpt('foo'), group='blaa') + self.assertIn('foo', self.fconf.blaa) + self.assertNotIn('blaa', self.conf) + + def test_register_cli_opts_grouped(self): + self.fconf.register_cli_opts([cfg.StrOpt('foo'), cfg.StrOpt('bar')], + group='blaa') + self.assertIn('foo', self.fconf.blaa) + self.assertIn('bar', self.fconf.blaa) + self.assertNotIn('blaa', self.conf) + + def test_unknown_opt(self): + self.assertNotIn('foo', self.fconf) + self.assertEqual(0, len(self.fconf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.fconf, 'foo') + self.assertNotIn('blaa', self.conf) + + def test_blocked_opt(self): + self.conf.register_opt(cfg.StrOpt('foo')) + + self.assertIn('foo', self.conf) + self.assertEqual(1, len(self.conf)) + self.assertIsNone(self.conf.foo) + self.assertNotIn('foo', self.fconf) + self.assertEqual(0, len(self.fconf)) + self.assertRaises(cfg.NoSuchOptError, getattr, self.fconf, 'foo') + + def test_already_registered_opt(self): + self.conf.register_opt(cfg.StrOpt('foo')) + self.fconf.register_opt(cfg.StrOpt('foo')) + + self.assertIn('foo', self.conf) + self.assertEqual(1, len(self.conf)) + self.assertIsNone(self.conf.foo) + self.assertIn('foo', self.fconf) + self.assertEqual(1, len(self.fconf)) + self.assertIsNone(self.fconf.foo) + + self.conf.set_override('foo', 'bar') + + self.assertEqual('bar', self.conf.foo) + self.assertEqual('bar', self.fconf.foo) + + def test_already_registered_opts(self): + self.conf.register_opts([cfg.StrOpt('foo'), + cfg.StrOpt('fu')]) + self.fconf.register_opts([cfg.StrOpt('foo'), + cfg.StrOpt('bu')]) + + self.assertIn('foo', self.conf) + self.assertIn('fu', self.conf) + self.assertNotIn('bu', self.conf) + self.assertEqual(2, len(self.conf)) + self.assertIsNone(self.conf.foo) + self.assertIsNone(self.conf.fu) + self.assertIn('foo', self.fconf) + self.assertIn('bu', self.fconf) + self.assertNotIn('fu', self.fconf) + self.assertEqual(2, len(self.fconf)) + self.assertIsNone(self.fconf.foo) + self.assertIsNone(self.fconf.bu) + + self.conf.set_override('foo', 'bar') + + self.assertEqual('bar', self.conf.foo) + self.assertEqual('bar', self.fconf.foo) + + def test_already_registered_cli_opt(self): + self.conf.register_cli_opt(cfg.StrOpt('foo')) + self.fconf.register_cli_opt(cfg.StrOpt('foo')) + + self.assertIn('foo', self.conf) + self.assertEqual(1, len(self.conf)) + self.assertIsNone(self.conf.foo) + self.assertIn('foo', self.fconf) + self.assertEqual(1, len(self.fconf)) + self.assertIsNone(self.fconf.foo) + + self.conf.set_override('foo', 'bar') + + self.assertEqual('bar', self.conf.foo) + self.assertEqual('bar', self.fconf.foo) + + def test_already_registered_cli_opts(self): + self.conf.register_cli_opts([cfg.StrOpt('foo'), + cfg.StrOpt('fu')]) + self.fconf.register_cli_opts([cfg.StrOpt('foo'), + cfg.StrOpt('bu')]) + + self.assertIn('foo', self.conf) + self.assertIn('fu', self.conf) + self.assertNotIn('bu', self.conf) + self.assertEqual(2, len(self.conf)) + self.assertIsNone(self.conf.foo) + self.assertIsNone(self.conf.fu) + self.assertIn('foo', self.fconf) + self.assertIn('bu', self.fconf) + self.assertNotIn('fu', self.fconf) + self.assertEqual(2, len(self.fconf)) + self.assertIsNone(self.fconf.foo) + self.assertIsNone(self.fconf.bu) + + self.conf.set_override('foo', 'bar') + + self.assertEqual('bar', self.conf.foo) + self.assertEqual('bar', self.fconf.foo) + + +class ImportTestCase(BaseTestCase): + + def setUp(self): + super(ImportTestCase, self).setUp(cfg.CONF) + + def test_import_opt(self): + self.assertFalse(hasattr(self.conf, 'fblaa')) + self.conf.import_opt('fblaa', 'tests.testmods.fblaa_opt') + self.assertTrue(hasattr(self.conf, 'fblaa')) + self.assertFalse(hasattr(self.fconf, 'fblaa')) + self.fconf.import_opt('fblaa', 'tests.testmods.fblaa_opt') + self.assertTrue(hasattr(self.fconf, 'fblaa')) + + def test_import_opt_in_group(self): + self.assertFalse(hasattr(self.conf, 'fbar')) + self.conf.import_opt('foo', 'tests.testmods.fbar_foo_opt', + group='fbar') + self.assertTrue(hasattr(self.conf, 'fbar')) + self.assertTrue(hasattr(self.conf.fbar, 'foo')) + self.assertFalse(hasattr(self.fconf, 'fbar')) + self.fconf.import_opt('foo', 'tests.testmods.fbar_foo_opt', + group='fbar') + self.assertTrue(hasattr(self.fconf, 'fbar')) + self.assertTrue(hasattr(self.fconf.fbar, 'foo')) + + def test_import_group(self): + self.assertFalse(hasattr(self.conf, 'fbaar')) + self.conf.import_group('fbaar', 'tests.testmods.fbaar_baa_opt') + self.assertTrue(hasattr(self.conf, 'fbaar')) + self.assertTrue(hasattr(self.conf.fbaar, 'baa')) + self.assertFalse(hasattr(self.fconf, 'fbaar')) + self.fconf.import_group('fbaar', 'tests.testmods.fbaar_baa_opt') + self.assertTrue(hasattr(self.fconf, 'fbaar')) + self.assertTrue(hasattr(self.fconf.fbaar, 'baa')) diff --git a/oslo_config/tests/test_fixture.py b/oslo_config/tests/test_fixture.py new file mode 100644 index 00000000..6d8b2a7b --- /dev/null +++ b/oslo_config/tests/test_fixture.py @@ -0,0 +1,87 @@ +# +# Copyright 2013 Mirantis, Inc. +# Copyright 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base + +from oslo_config import cfg +from oslo_config import fixture as config + +conf = cfg.CONF + + +class ConfigTestCase(base.BaseTestCase): + def setUp(self): + super(ConfigTestCase, self).setUp() + self.config_fixture = self.useFixture(config.Config(conf)) + self.config = self.config_fixture.config + self.config_fixture.register_opt(cfg.StrOpt( + 'testing_option', default='initial_value')) + + def test_overridden_value(self): + self.assertEqual(conf.get('testing_option'), 'initial_value') + self.config(testing_option='changed_value') + self.assertEqual(conf.get('testing_option'), + self.config_fixture.conf.get('testing_option')) + + def test_cleanup(self): + self.config(testing_option='changed_value') + self.assertEqual(self.config_fixture.conf.get('testing_option'), + 'changed_value') + self.config_fixture.conf.reset() + self.assertEqual(conf.get('testing_option'), 'initial_value') + + def test_register_option(self): + opt = cfg.StrOpt('new_test_opt', default='initial_value') + self.config_fixture.register_opt(opt) + self.assertEqual(conf.get('new_test_opt'), + opt.default) + + def test_register_options(self): + opt1 = cfg.StrOpt('first_test_opt', default='initial_value_1') + opt2 = cfg.StrOpt('second_test_opt', default='initial_value_2') + self.config_fixture.register_opts([opt1, opt2]) + self.assertEqual(conf.get('first_test_opt'), opt1.default) + self.assertEqual(conf.get('second_test_opt'), opt2.default) + + def test_cleanup_unregister_option(self): + opt = cfg.StrOpt('new_test_opt', default='initial_value') + self.config_fixture.register_opt(opt) + self.assertEqual(conf.get('new_test_opt'), + opt.default) + self.config_fixture.cleanUp() + self.assertRaises(cfg.NoSuchOptError, conf.get, 'new_test_opt') + + def test_register_cli_option(self): + opt = cfg.StrOpt('new_test_opt', default='initial_value') + self.config_fixture.register_cli_opt(opt) + self.assertEqual(conf.get('new_test_opt'), + opt.default) + + def test_register_cli_options(self): + opt1 = cfg.StrOpt('first_test_opt', default='initial_value_1') + opt2 = cfg.StrOpt('second_test_opt', default='initial_value_2') + self.config_fixture.register_cli_opts([opt1, opt2]) + self.assertEqual(conf.get('first_test_opt'), opt1.default) + self.assertEqual(conf.get('second_test_opt'), opt2.default) + + def test_cleanup_unregister_cli_option(self): + opt = cfg.StrOpt('new_test_opt', default='initial_value') + self.config_fixture.register_cli_opt(opt) + self.assertEqual(conf.get('new_test_opt'), + opt.default) + self.config_fixture.cleanUp() + self.assertRaises(cfg.NoSuchOptError, conf.get, 'new_test_opt') diff --git a/oslo_config/tests/test_generator.py b/oslo_config/tests/test_generator.py new file mode 100644 index 00000000..668274c2 --- /dev/null +++ b/oslo_config/tests/test_generator.py @@ -0,0 +1,539 @@ +# Copyright 2014 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +import fixtures +import mock +from oslotest import base +from six import moves +import testscenarios + +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslo_config import generator + +load_tests = testscenarios.load_tests_apply_scenarios + + +class GeneratorTestCase(base.BaseTestCase): + + opts = { + 'foo': cfg.StrOpt('foo', help='foo option'), + 'bar': cfg.StrOpt('bar', help='bar option'), + 'foo-bar': cfg.StrOpt('foo-bar', help='foobar'), + 'no_help': cfg.StrOpt('no_help'), + 'long_help': cfg.StrOpt('long_help', + help='Lorem ipsum dolor sit amet, consectetur ' + 'adipisicing elit, sed do eiusmod tempor ' + 'incididunt ut labore et dolore magna ' + 'aliqua. Ut enim ad minim veniam, quis ' + 'nostrud exercitation ullamco laboris ' + 'nisi ut aliquip ex ea commodo ' + 'consequat. Duis aute irure dolor in ' + 'reprehenderit in voluptate velit esse ' + 'cillum dolore eu fugiat nulla ' + 'pariatur. Excepteur sint occaecat ' + 'cupidatat non proident, sunt in culpa ' + 'qui officia deserunt mollit anim id est ' + 'laborum.'), + 'deprecated_opt': cfg.StrOpt('bar', + deprecated_name='foobar', + help='deprecated'), + 'deprecated_group': cfg.StrOpt('bar', + deprecated_group='group1', + deprecated_name='foobar', + help='deprecated'), + # Unknown Opt default must be a string + 'unknown_type': cfg.Opt('unknown_opt', + default='123', + help='unknown'), + 'str_opt': cfg.StrOpt('str_opt', + default='foo bar', + help='a string'), + 'str_opt_sample_default': cfg.StrOpt('str_opt', + default='fooishbar', + help='a string'), + 'str_opt_with_space': cfg.StrOpt('str_opt', + default=' foo bar ', + help='a string with spaces'), + 'bool_opt': cfg.BoolOpt('bool_opt', + default=False, + help='a boolean'), + 'int_opt': cfg.IntOpt('int_opt', + default=10, + help='an integer'), + 'float_opt': cfg.FloatOpt('float_opt', + default=0.1, + help='a float'), + 'list_opt': cfg.ListOpt('list_opt', + default=['1', '2', '3'], + help='a list'), + 'dict_opt': cfg.DictOpt('dict_opt', + default={'1': 'yes', '2': 'no'}, + help='a dict'), + 'multi_opt': cfg.MultiStrOpt('multi_opt', + default=['1', '2', '3'], + help='multiple strings'), + 'multi_opt_none': cfg.MultiStrOpt('multi_opt_none', + help='multiple strings'), + 'multi_opt_empty': cfg.MultiStrOpt('multi_opt_empty', + default=[], + help='multiple strings'), + 'multi_opt_sample_default': cfg.MultiStrOpt('multi_opt', + default=['1', '2', '3'], + sample_default=['5', '6'], + help='multiple strings'), + } + + content_scenarios = [ + ('empty', + dict(opts=[], expected='''[DEFAULT] +''')), + ('single_namespace', + dict(opts=[('test', [(None, [opts['foo']])])], + expected='''[DEFAULT] + +# +# From test +# + +# foo option (string value) +#foo = +''')), + ('multiple_namespaces', + dict(opts=[('test', [(None, [opts['foo']])]), + ('other', [(None, [opts['bar']])])], + expected='''[DEFAULT] + +# +# From other +# + +# bar option (string value) +#bar = + +# +# From test +# + +# foo option (string value) +#foo = +''')), + ('group', + dict(opts=[('test', [('group1', [opts['foo']])])], + expected='''[DEFAULT] + + +[group1] + +# +# From test +# + +# foo option (string value) +#foo = +''')), + ('empty_group', + dict(opts=[('test', [('group1', [])])], + expected='''[DEFAULT] +''')), + ('multiple_groups', + dict(opts=[('test', [('group1', [opts['foo']]), + ('group2', [opts['bar']])])], + expected='''[DEFAULT] + + +[group1] + +# +# From test +# + +# foo option (string value) +#foo = + + +[group2] + +# +# From test +# + +# bar option (string value) +#bar = +''')), + ('group_in_multiple_namespaces', + dict(opts=[('test', [('group1', [opts['foo']])]), + ('other', [('group1', [opts['bar']])])], + expected='''[DEFAULT] + + +[group1] + +# +# From other +# + +# bar option (string value) +#bar = + +# +# From test +# + +# foo option (string value) +#foo = +''')), + ('hyphenated_name', + dict(opts=[('test', [(None, [opts['foo-bar']])])], + expected='''[DEFAULT] + +# +# From test +# + +# foobar (string value) +#foo_bar = +''')), + ('no_help', + dict(opts=[('test', [(None, [opts['no_help']])])], + log_warning=('"%s" is missing a help string', 'no_help'), + expected='''[DEFAULT] + +# +# From test +# + +# (string value) +#no_help = +''')), + ('long_help', + dict(opts=[('test', [(None, [opts['long_help']])])], + expected='''[DEFAULT] + +# +# From test +# + +# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do +# eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim +# ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut +# aliquip ex ea commodo consequat. Duis aute irure dolor in +# reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla +# pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +# culpa qui officia deserunt mollit anim id est laborum. (string +# value) +#long_help = +''')), + ('long_help_wrap_at_40', + dict(opts=[('test', [(None, [opts['long_help']])])], + wrap_width=40, + expected='''[DEFAULT] + +# +# From test +# + +# Lorem ipsum dolor sit amet, +# consectetur adipisicing elit, sed do +# eiusmod tempor incididunt ut labore et +# dolore magna aliqua. Ut enim ad minim +# veniam, quis nostrud exercitation +# ullamco laboris nisi ut aliquip ex ea +# commodo consequat. Duis aute irure +# dolor in reprehenderit in voluptate +# velit esse cillum dolore eu fugiat +# nulla pariatur. Excepteur sint +# occaecat cupidatat non proident, sunt +# in culpa qui officia deserunt mollit +# anim id est laborum. (string value) +#long_help = +''')), + ('long_help_no_wrapping', + dict(opts=[('test', [(None, [opts['long_help']])])], + wrap_width=0, + expected='''[DEFAULT] + +# +# From test +# + +''' # noqa +'# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod ' +'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ' +'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo ' +'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse ' +'cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat ' +'non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ' +'(string value)' +''' +#long_help = +''')), + ('deprecated', + dict(opts=[('test', [('foo', [opts['deprecated_opt']])])], + expected='''[DEFAULT] + + +[foo] + +# +# From test +# + +# deprecated (string value) +# Deprecated group/name - [DEFAULT]/foobar +#bar = +''')), + ('deprecated_group', + dict(opts=[('test', [('foo', [opts['deprecated_group']])])], + expected='''[DEFAULT] + + +[foo] + +# +# From test +# + +# deprecated (string value) +# Deprecated group/name - [group1]/foobar +#bar = +''')), + ('unknown_type', + dict(opts=[('test', [(None, [opts['unknown_type']])])], + log_warning=('Unknown option type: %s', + repr(opts['unknown_type'])), + expected='''[DEFAULT] + +# +# From test +# + +# unknown (unknown type) +#unknown_opt = 123 +''')), + ('str_opt', + dict(opts=[('test', [(None, [opts['str_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a string (string value) +#str_opt = foo bar +''')), + ('str_opt_with_space', + dict(opts=[('test', [(None, [opts['str_opt_with_space']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a string with spaces (string value) +#str_opt = " foo bar " +''')), + ('bool_opt', + dict(opts=[('test', [(None, [opts['bool_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a boolean (boolean value) +#bool_opt = false +''')), + ('int_opt', + dict(opts=[('test', [(None, [opts['int_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# an integer (integer value) +#int_opt = 10 +''')), + ('float_opt', + dict(opts=[('test', [(None, [opts['float_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a float (floating point value) +#float_opt = 0.1 +''')), + ('list_opt', + dict(opts=[('test', [(None, [opts['list_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a list (list value) +#list_opt = 1,2,3 +''')), + ('dict_opt', + dict(opts=[('test', [(None, [opts['dict_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a dict (dict value) +#dict_opt = 1:yes,2:no +''')), + ('multi_opt', + dict(opts=[('test', [(None, [opts['multi_opt']])])], + expected='''[DEFAULT] + +# +# From test +# + +# multiple strings (multi valued) +#multi_opt = 1 +#multi_opt = 2 +#multi_opt = 3 +''')), + ('multi_opt_none', + dict(opts=[('test', [(None, [opts['multi_opt_none']])])], + expected='''[DEFAULT] + +# +# From test +# + +# multiple strings (multi valued) +#multi_opt_none = +''')), + ('multi_opt_empty', + dict(opts=[('test', [(None, [opts['multi_opt_empty']])])], + expected='''[DEFAULT] + +# +# From test +# + +# multiple strings (multi valued) +#multi_opt_empty = +''')), + ('str_opt_sample_default', + dict(opts=[('test', [(None, [opts['str_opt_sample_default']])])], + expected='''[DEFAULT] + +# +# From test +# + +# a string (string value) +#str_opt = fooishbar +''')), + ('multi_opt_sample_default', + dict(opts=[('test', [(None, [opts['multi_opt_sample_default']])])], + expected='''[DEFAULT] + +# +# From test +# + +# multiple strings (multi valued) +#multi_opt = 5 +#multi_opt = 6 +''')), + ] + + output_file_scenarios = [ + ('stdout', + dict(stdout=True, output_file=None)), + ('output_file', + dict(output_file='sample.conf', stdout=False)), + ] + + @classmethod + def generate_scenarios(cls): + cls.scenarios = testscenarios.multiply_scenarios( + cls.content_scenarios, + cls.output_file_scenarios) + + def setUp(self): + super(GeneratorTestCase, self).setUp() + + self.conf = cfg.ConfigOpts() + self.config_fixture = config_fixture.Config(self.conf) + self.config = self.config_fixture.config + self.useFixture(self.config_fixture) + + self.tempdir = self.useFixture(fixtures.TempDir()) + + def _capture_stream(self, stream_name): + self.useFixture(fixtures.MonkeyPatch("sys.%s" % stream_name, + moves.StringIO())) + return getattr(sys, stream_name) + + def _capture_stdout(self): + return self._capture_stream('stdout') + + @mock.patch('stevedore.named.NamedExtensionManager') + @mock.patch.object(generator, 'LOG') + def test_generate(self, mock_log, named_mgr): + generator.register_cli_opts(self.conf) + + namespaces = [i[0] for i in self.opts] + self.config(namespace=namespaces) + + wrap_width = getattr(self, 'wrap_width', None) + if wrap_width is not None: + self.config(wrap_width=wrap_width) + + if self.stdout: + stdout = self._capture_stdout() + else: + output_file = self.tempdir.join(self.output_file) + self.config(output_file=output_file) + + mock_eps = [] + for name, opts in self.opts: + mock_ep = mock.Mock() + mock_ep.configure_mock(name=name, obj=opts) + mock_eps.append(mock_ep) + named_mgr.return_value = mock_eps + + generator.generate(self.conf) + + if self.stdout: + self.assertEqual(self.expected, stdout.getvalue()) + else: + content = open(output_file).read() + self.assertEqual(self.expected, content) + + named_mgr.assert_called_once_with('oslo.config.opts', + names=namespaces, + invoke_on_load=True) + + log_warning = getattr(self, 'log_warning', None) + if log_warning is not None: + mock_log.warning.assert_called_once_with(*log_warning) + else: + self.assertFalse(mock_log.warning.called) + + +GeneratorTestCase.generate_scenarios() diff --git a/oslo_config/tests/test_iniparser.py b/oslo_config/tests/test_iniparser.py new file mode 100644 index 00000000..5c2166b0 --- /dev/null +++ b/oslo_config/tests/test_iniparser.py @@ -0,0 +1,124 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest + +from oslo_config import iniparser + + +class TestParser(iniparser.BaseParser): + comment_called = False + values = None + section = '' + + def __init__(self): + self.values = {} + + def assignment(self, key, value): + self.values.setdefault(self.section, {}) + self.values[self.section][key] = value + + def new_section(self, section): + self.section = section + + def comment(self, section): + self.comment_called = True + + +class BaseParserTestCase(unittest.TestCase): + def setUp(self): + self.parser = iniparser.BaseParser() + + def _assertParseError(self, *lines): + self.assertRaises(iniparser.ParseError, self.parser.parse, lines) + + def test_invalid_assignment(self): + self._assertParseError("foo - bar") + + def test_empty_key(self): + self._assertParseError(": bar") + + def test_unexpected_continuation(self): + self._assertParseError(" baz") + + def test_invalid_section(self): + self._assertParseError("[section") + + def test_no_section_name(self): + self._assertParseError("[]") + + +class ParserTestCase(unittest.TestCase): + def setUp(self): + self.parser = TestParser() + + def test_blank_line(self): + lines = [""] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {}) + + def test_assignment_equal(self): + lines = ["foo = bar"] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'': {'foo': ['bar']}}) + + def test_assignment_colon(self): + lines = ["foo: bar"] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'': {'foo': ['bar']}}) + + def test_assignment_multiline(self): + lines = ["foo = bar0", " bar1"] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'': {'foo': ['bar0', 'bar1']}}) + + def test_assignment_multline_empty(self): + lines = ["foo = bar0", "", " bar1"] + self.assertRaises(iniparser.ParseError, self.parser.parse, lines) + + def test_section_assignment(self): + lines = ["[test]", "foo = bar"] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'test': {'foo': ['bar']}}) + + def test_new_section(self): + lines = ["[foo]"] + self.parser.parse(lines) + self.assertEqual(self.parser.section, 'foo') + + def test_comment(self): + lines = ["# foobar"] + self.parser.parse(lines) + self.assertTrue(self.parser.comment_called) + + def test_empty_assignment(self): + lines = ["foo = "] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'': {'foo': ['']}}) + + def test_assignment_space_single_quote(self): + lines = ["foo = ' bar '"] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'': {'foo': [' bar ']}}) + + def test_assignment_space_double_quote(self): + lines = ["foo = \" bar \""] + self.parser.parse(lines) + self.assertEqual(self.parser.values, {'': {'foo': [' bar ']}}) + + +class ExceptionTestCase(unittest.TestCase): + def test_parseerror(self): + exc = iniparser.ParseError('test', 42, 'example') + self.assertEqual(str(exc), "at line 42, test: 'example'") diff --git a/oslo_config/tests/test_types.py b/oslo_config/tests/test_types.py new file mode 100644 index 00000000..1d8e160c --- /dev/null +++ b/oslo_config/tests/test_types.py @@ -0,0 +1,411 @@ +# Copyright 2013 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +import unittest + +from oslo_config import types + + +class TypeTestHelper(object): + def setUp(self): + super(TypeTestHelper, self).setUp() + self.type_instance = self.type + + def assertConvertedValue(self, s, expected): + self.assertEqual(expected, self.type_instance(s)) + + def assertInvalid(self, value): + self.assertRaises(ValueError, self.type_instance, value) + + +class StringTypeTests(TypeTestHelper, unittest.TestCase): + type = types.String() + + def test_empty_string_passes(self): + self.assertConvertedValue('', '') + + def test_should_return_same_string_if_valid(self): + self.assertConvertedValue('foo bar', 'foo bar') + + def test_listed_value(self): + self.type_instance = types.String(choices=['foo', 'bar']) + self.assertConvertedValue('foo', 'foo') + + def test_unlisted_value(self): + self.type_instance = types.String(choices=['foo', 'bar']) + self.assertInvalid('baz') + + def test_with_no_values_returns_error(self): + self.type_instance = types.String(choices=[]) + self.assertInvalid('foo') + + def test_string_with_non_closed_quote_is_invalid(self): + self.type_instance = types.String(quotes=True) + self.assertInvalid('"foo bar') + self.assertInvalid("'bar baz") + + def test_quotes_are_stripped(self): + self.type_instance = types.String(quotes=True) + self.assertConvertedValue('"foo bar"', 'foo bar') + + def test_trailing_quote_is_ok(self): + self.type_instance = types.String(quotes=True) + self.assertConvertedValue('foo bar"', 'foo bar"') + + def test_repr(self): + t = types.String() + self.assertEqual('String', repr(t)) + + def test_repr_with_choices(self): + t = types.String(choices=['foo', 'bar']) + self.assertEqual('String(choices=[\'foo\', \'bar\'])', repr(t)) + + def test_equal(self): + self.assertTrue(types.String() == types.String()) + + def test_equal_with_same_choices(self): + t1 = types.String(choices=['foo', 'bar']) + t2 = types.String(choices=['foo', 'bar']) + self.assertTrue(t1 == t2) + + def test_not_equal_with_different_choices(self): + t1 = types.String(choices=['foo', 'bar']) + t2 = types.String(choices=['foo', 'baz']) + self.assertFalse(t1 == t2) + + def test_equal_with_equal_quote_falgs(self): + t1 = types.String(quotes=True) + t2 = types.String(quotes=True) + self.assertTrue(t1 == t2) + + def test_not_equal_with_different_quote_falgs(self): + t1 = types.String(quotes=False) + t2 = types.String(quotes=True) + self.assertFalse(t1 == t2) + + def test_not_equal_to_other_class(self): + self.assertFalse(types.String() == types.Integer()) + + +class BooleanTypeTests(TypeTestHelper, unittest.TestCase): + type = types.Boolean() + + def test_True(self): + self.assertConvertedValue('True', True) + + def test_yes(self): + self.assertConvertedValue('yes', True) + + def test_on(self): + self.assertConvertedValue('on', True) + + def test_1(self): + self.assertConvertedValue('1', True) + + def test_False(self): + self.assertConvertedValue('False', False) + + def test_no(self): + self.assertConvertedValue('no', False) + + def test_off(self): + self.assertConvertedValue('off', False) + + def test_0(self): + self.assertConvertedValue('0', False) + + def test_other_values_produce_error(self): + self.assertInvalid('foo') + + def test_repr(self): + self.assertEqual('Boolean', repr(types.Boolean())) + + def test_equal(self): + self.assertEqual(types.Boolean(), types.Boolean()) + + def test_not_equal_to_other_class(self): + self.assertFalse(types.Boolean() == types.String()) + + +class IntegerTypeTests(TypeTestHelper, unittest.TestCase): + type = types.Integer() + + def test_empty_string(self): + self.assertConvertedValue('', None) + + def test_whitespace_string(self): + self.assertConvertedValue(" \t\t\t\t", None) + + def test_positive_values_are_valid(self): + self.assertConvertedValue('123', 123) + + def test_zero_is_valid(self): + self.assertConvertedValue('0', 0) + + def test_negative_values_are_valid(self): + self.assertConvertedValue('-123', -123) + + def test_leading_whitespace_is_ignored(self): + self.assertConvertedValue(' 5', 5) + + def test_trailing_whitespace_is_ignored(self): + self.assertConvertedValue('7 ', 7) + + def test_non_digits_are_invalid(self): + self.assertInvalid('12a45') + + def test_repr(self): + t = types.Integer() + self.assertEqual('Integer', repr(t)) + + def test_repr_with_min(self): + t = types.Integer(min=123) + self.assertEqual('Integer(min=123)', repr(t)) + + def test_repr_with_max(self): + t = types.Integer(max=456) + self.assertEqual('Integer(max=456)', repr(t)) + + def test_repr_with_min_and_max(self): + t = types.Integer(min=123, max=456) + self.assertEqual('Integer(min=123, max=456)', repr(t)) + + def test_equal(self): + self.assertTrue(types.Integer() == types.Integer()) + + def test_equal_with_same_min_and_no_max(self): + self.assertTrue(types.Integer(min=123) == types.Integer(min=123)) + + def test_equal_with_same_max_and_no_min(self): + self.assertTrue(types.Integer(max=123) == types.Integer(max=123)) + + def test_equal_with_same_min_and_max(self): + t1 = types.Integer(min=1, max=123) + t2 = types.Integer(min=1, max=123) + self.assertTrue(t1 == t2) + + def test_not_equal(self): + self.assertFalse(types.Integer(min=123) == types.Integer(min=456)) + + def test_not_equal_to_other_class(self): + self.assertFalse(types.Integer() == types.String()) + + def test_with_max_and_min(self): + t = types.Integer(min=123, max=456) + self.assertRaises(ValueError, t, 122) + t(123) + t(300) + t(456) + self.assertRaises(ValueError, t, 457) + + +class FloatTypeTests(TypeTestHelper, unittest.TestCase): + type = types.Float() + + def test_decimal_format(self): + v = self.type_instance('123.456') + self.assertAlmostEqual(v, 123.456) + + def test_decimal_format_negative_float(self): + v = self.type_instance('-123.456') + self.assertAlmostEqual(v, -123.456) + + def test_exponential_format(self): + v = self.type_instance('123e-2') + self.assertAlmostEqual(v, 1.23) + + def test_non_float_is_invalid(self): + self.assertInvalid('123,345') + self.assertInvalid('foo') + + def test_repr(self): + self.assertEqual('Float', repr(types.Float())) + + def test_equal(self): + self.assertTrue(types.Float() == types.Float()) + + def test_not_equal_to_other_class(self): + self.assertFalse(types.Float() == types.Integer()) + + +class ListTypeTests(TypeTestHelper, unittest.TestCase): + type = types.List() + + def test_empty_value(self): + self.assertConvertedValue('', []) + + def test_single_value(self): + self.assertConvertedValue(' foo bar ', + ['foo bar']) + + def test_list_of_values(self): + self.assertConvertedValue(' foo bar, baz ', + ['foo bar', + 'baz']) + + def test_list_of_values_containing_commas(self): + self.type_instance = types.List(types.String(quotes=True)) + self.assertConvertedValue('foo,"bar, baz",bam', + ['foo', + 'bar, baz', + 'bam']) + + def test_list_of_lists(self): + self.type_instance = types.List( + types.List(types.String(), bounds=True) + ) + self.assertConvertedValue('[foo],[bar, baz],[bam]', + [['foo'], ['bar', 'baz'], ['bam']]) + + def test_list_of_custom_type(self): + self.type_instance = types.List(types.Integer()) + self.assertConvertedValue('1,2,3,5', + [1, 2, 3, 5]) + + def test_bounds_parsing(self): + self.type_instance = types.List(types.Integer(), bounds=True) + self.assertConvertedValue('[1,2,3]', [1, 2, 3]) + + def test_bounds_required(self): + self.type_instance = types.List(types.Integer(), bounds=True) + self.assertInvalid('1,2,3') + self.assertInvalid('[1,2,3') + self.assertInvalid('1,2,3]') + + def test_repr(self): + t = types.List(types.Integer()) + self.assertEqual('List of Integer', repr(t)) + + def test_equal(self): + self.assertTrue(types.List() == types.List()) + + def test_equal_with_equal_custom_item_types(self): + it1 = types.Integer() + it2 = types.Integer() + self.assertTrue(types.List(it1) == types.List(it2)) + + def test_not_equal_with_non_equal_custom_item_types(self): + it1 = types.Integer() + it2 = types.String() + self.assertFalse(it1 == it2) + self.assertFalse(types.List(it1) == types.List(it2)) + + def test_not_equal_to_other_class(self): + self.assertFalse(types.List() == types.Integer()) + + +class DictTypeTests(TypeTestHelper, unittest.TestCase): + type = types.Dict() + + def test_empty_value(self): + self.assertConvertedValue('', {}) + + def test_single_value(self): + self.assertConvertedValue(' foo: bar ', + {'foo': 'bar'}) + + def test_dict_of_values(self): + self.assertConvertedValue(' foo: bar, baz: 123 ', + {'foo': 'bar', + 'baz': '123'}) + + def test_custom_value_type(self): + self.type_instance = types.Dict(types.Integer()) + self.assertConvertedValue('foo:123, bar: 456', + {'foo': 123, + 'bar': 456}) + + def test_dict_of_values_containing_commas(self): + self.type_instance = types.Dict(types.String(quotes=True)) + self.assertConvertedValue('foo:"bar, baz",bam:quux', + {'foo': 'bar, baz', + 'bam': 'quux'}) + + def test_dict_of_dicts(self): + self.type_instance = types.Dict( + types.Dict(types.String(), bounds=True) + ) + self.assertConvertedValue('k1:{k1:v1,k2:v2},k2:{k3:v3}', + {'k1': {'k1': 'v1', 'k2': 'v2'}, + 'k2': {'k3': 'v3'}}) + + def test_bounds_parsing(self): + self.type_instance = types.Dict(types.String(), bounds=True) + self.assertConvertedValue('{foo:bar,baz:123}', + {'foo': 'bar', + 'baz': '123'}) + + def test_bounds_required(self): + self.type_instance = types.Dict(types.String(), bounds=True) + self.assertInvalid('foo:bar,baz:123') + self.assertInvalid('{foo:bar,baz:123') + self.assertInvalid('foo:bar,baz:123}') + + def test_no_mapping_produces_error(self): + self.assertInvalid('foo,bar') + + def test_repr(self): + t = types.Dict(types.Integer()) + self.assertEqual('Dict of Integer', repr(t)) + + def test_equal(self): + self.assertTrue(types.Dict() == types.Dict()) + + def test_equal_with_equal_custom_item_types(self): + it1 = types.Integer() + it2 = types.Integer() + self.assertTrue(types.Dict(it1) == types.Dict(it2)) + + def test_not_equal_with_non_equal_custom_item_types(self): + it1 = types.Integer() + it2 = types.String() + self.assertFalse(it1 == it2) + self.assertFalse(types.Dict(it1) == types.Dict(it2)) + + def test_not_equal_to_other_class(self): + self.assertFalse(types.Dict() == types.Integer()) + + +class IPAddressTypeTests(TypeTestHelper, unittest.TestCase): + type = types.IPAddress() + + def test_ipv4_address(self): + self.assertConvertedValue('192.168.0.1', '192.168.0.1') + + def test_ipv6_address(self): + self.assertConvertedValue('abcd:ef::1', 'abcd:ef::1') + + def test_strings(self): + self.assertInvalid('') + self.assertInvalid('foo') + + def test_numbers(self): + self.assertInvalid(1) + self.assertInvalid(-1) + self.assertInvalid(3.14) + + +class IPv4AddressTypeTests(IPAddressTypeTests): + type = types.IPAddress(4) + + def test_ipv6_address(self): + self.assertInvalid('abcd:ef::1') + + +class IPv6AddressTypeTests(IPAddressTypeTests): + type = types.IPAddress(6) + + def test_ipv4_address(self): + self.assertInvalid('192.168.0.1') diff --git a/oslo_config/types.py b/oslo_config/types.py new file mode 100644 index 00000000..9b646e25 --- /dev/null +++ b/oslo_config/types.py @@ -0,0 +1,413 @@ +# Copyright 2013 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Type conversion and validation classes for configuration options. + +Use these classes as values for the `type` argument to +:class:`oslo.config.cfg.Opt` and its subclasses. + +""" +import netaddr +import six + + +class ConfigType(object): + + BASE_TYPES = (None,) + + def is_base_type(self, other): + return isinstance(other, self.BASE_TYPES) + + +class String(ConfigType): + + """String type. + + String values do not get transformed and are returned as str objects. + + :param choices: Optional sequence of valid values. + :param quotes: If True and string is enclosed with single or double + quotes, will strip those quotes. Will signal error if + string have quote at the beginning and no quote at + the end. Turned off by default. Useful if used with + container types like List. + """ + + BASE_TYPES = six.string_types + + def __init__(self, choices=None, quotes=False): + super(String, self).__init__() + self.choices = choices + self.quotes = quotes + + def __call__(self, value): + value = str(value) + if self.quotes and value: + if value[0] in "\"'": + if value[-1] != value[0]: + raise ValueError('Non-closed quote: %s' % value) + value = value[1:-1] + + if self.choices is None or value in self.choices: + return value + + raise ValueError( + 'Valid values are [%s], but found %s' % ( + ', '.join([str(v) for v in self.choices]), + repr(value))) + + def __repr__(self): + if self.choices: + return 'String(choices=%s)' % repr(self.choices) + return 'String' + + def __eq__(self, other): + return ( + (self.__class__ == other.__class__) and + (self.choices == other.choices) and + (self.quotes == other.quotes) + ) + + +class MultiString(String): + + BASE_TYPES = six.string_types + (list,) + + +class Boolean(ConfigType): + + """Boolean type. + + Values are case insensitive and can be set using + 1/0, yes/no, true/false or on/off. + """ + TRUE_VALUES = ['true', '1', 'on', 'yes'] + FALSE_VALUES = ['false', '0', 'off', 'no'] + + BASE_TYPES = (bool,) + + def __call__(self, value): + if isinstance(value, bool): + return value + + s = value.lower() + if s in self.TRUE_VALUES: + return True + elif s in self.FALSE_VALUES: + return False + else: + raise ValueError('Unexpected boolean value %r' % value) + + def __repr__(self): + return 'Boolean' + + def __eq__(self, other): + return self.__class__ == other.__class__ + + +class Integer(ConfigType): + + """Integer type. + + Converts value to an integer optionally doing range checking. + If value is whitespace or empty string will return None. + + :param min: Optional check that value is greater than or equal to min + :param max: Optional check that value is less than or equal to max + """ + + BASE_TYPES = six.integer_types + + def __init__(self, min=None, max=None): + super(Integer, self).__init__() + self.min = min + self.max = max + if min and max and max < min: + raise ValueError('Max value is less than min value') + + def __call__(self, value): + if not isinstance(value, int): + s = str(value).strip() + if s == '': + value = None + else: + value = int(value) + + if value: + self._check_range(value) + + return value + + def _check_range(self, value): + if self.min and value < self.min: + raise ValueError('Should be greater than or equal to %d' % + self.min) + if self.max and value > self.max: + raise ValueError('Should be less than or equal to %d' % self.max) + + def __repr__(self): + props = [] + if self.min: + props.append('min=%d' % self.min) + if self.max: + props.append('max=%d' % self.max) + + if props: + return 'Integer(%s)' % ', '.join(props) + return 'Integer' + + def __eq__(self, other): + return ( + (self.__class__ == other.__class__) and + (self.min == other.min) and + (self.max == other.max) + ) + + +class Float(ConfigType): + + """Float type.""" + + # allow float to be set from int + BASE_TYPES = six.integer_types + (float,) + + def __call__(self, value): + if isinstance(value, float): + return value + + return float(value) + + def __repr__(self): + return 'Float' + + def __eq__(self, other): + return self.__class__ == other.__class__ + + +class List(ConfigType): + + """List type. + + Represent values of other (item) type, separated by commas. + The resulting value is a list containing those values. + + List doesn't know if item type can also contain commas. To workaround this + it tries the following: if the next part fails item validation, it appends + comma and next item until validation succeeds or there is no parts left. + In the later case it will signal validation error. + + :param item_type: type of list items + :param bounds: if True, value should be inside "[" and "]" pair + """ + + BASE_TYPES = (list,) + + def __init__(self, item_type=None, bounds=False): + super(List, self).__init__() + + if item_type is None: + item_type = String() + + if not callable(item_type): + raise TypeError('item_type must be callable') + self.item_type = item_type + self.bounds = bounds + + def __call__(self, value): + if isinstance(value, list): + return value + + result = [] + s = value.strip() + + if self.bounds: + if not s.startswith('['): + raise ValueError('Value should start with "["') + if not s.endswith(']'): + raise ValueError('Value should end with "]"') + s = s[1:-1] + + if s == '': + return result + + values = s.split(',') + while values: + value = values.pop(0) + while True: + first_error = None + try: + validated_value = self.item_type(value.strip()) + break + except ValueError as e: + if not first_error: + first_error = e + if len(values) == 0: + raise first_error + + value += ',' + values.pop(0) + + result.append(validated_value) + + return result + + def __repr__(self): + return 'List of %s' % repr(self.item_type) + + def __eq__(self, other): + return ( + (self.__class__ == other.__class__) and + (self.item_type == other.item_type) + ) + + +class Dict(ConfigType): + + """Dictionary type. + + Dictionary type values are key:value pairs separated by commas. + The resulting value is a dictionary of these key/value pairs. + Type of dictionary key is always string, but dictionary value + type can be customized. + + :param value_type: type of values in dictionary + :param bounds: if True, value should be inside "{" and "}" pair + """ + + BASE_TYPES = (dict,) + + def __init__(self, value_type=None, bounds=False): + super(Dict, self).__init__() + + if value_type is None: + value_type = String() + + if not callable(value_type): + raise TypeError('value_type must be callable') + self.value_type = value_type + self.bounds = bounds + + def __call__(self, value): + if isinstance(value, dict): + return value + + result = {} + s = value.strip() + + if self.bounds: + if not s.startswith('{'): + raise ValueError('Value should start with "{"') + if not s.endswith('}'): + raise ValueError('Value should end with "}"') + s = s[1:-1] + + if s == '': + return result + + pairs = s.split(',') + while pairs: + pair = pairs.pop(0) + + while True: + first_error = None + try: + key_value = pair.split(':', 1) + + if len(key_value) < 2: + raise ValueError('Value should be NAME:VALUE pairs ' + 'separated by ","') + + key, value = key_value + key = key.strip() + value = value.strip() + + value = self.value_type(value) + break + except ValueError as e: + if not first_error: + first_error = e + if not pairs: + raise first_error + + pair += ',' + pairs.pop(0) + + if key == '': + raise ValueError('Key name should not be empty') + + if key in result: + raise ValueError('Duplicate key %s' % key) + + result[key] = value + + return result + + def __repr__(self): + return 'Dict of %s' % repr(self.value_type) + + def __eq__(self, other): + return ( + (self.__class__ == other.__class__) and + (self.value_type == other.value_type) + ) + + +class IPAddress(ConfigType): + + """IP address type + + Represents either ipv4 or ipv6. Without specifying version parameter both + versions are checked + + :param version: defines which version should be explicitly checked (4 or 6) + + """ + + BASE_TYPES = six.string_types + + def __init__(self, version=None): + super(IPAddress, self).__init__() + version_checkers = { + None: self._check_both_versions, + 4: self._check_ipv4, + 6: self._check_ipv6 + } + + self.version_checker = version_checkers.get(version) + if self.version_checker is None: + raise TypeError("%s is not a valid IP version." % version) + + def __call__(self, value): + value = str(value) + if not value: + raise ValueError("IP address cannot be an empty string") + self.version_checker(value) + return value + + def __repr__(self): + return "IPAddress" + + def __eq__(self, other): + return self.__class__ == other.__class__ + + def _check_ipv4(self, address): + if not netaddr.valid_ipv4(address, netaddr.core.INET_PTON): + raise ValueError("%s is not an IPv4 address" % address) + + def _check_ipv6(self, address): + if not netaddr.valid_ipv6(address, netaddr.core.INET_PTON): + raise ValueError("%s is not an IPv6 address" % address) + + def _check_both_versions(self, address): + if not (netaddr.valid_ipv4(address, netaddr.core.INET_PTON) or + netaddr.valid_ipv6(address, netaddr.core.INET_PTON)): + raise ValueError("%s is not IPv4 or IPv6 address" % address) diff --git a/setup.cfg b/setup.cfg index 7012627f..68d8ce06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,6 +24,7 @@ classifier = packages = oslo oslo.config + oslo_config namespace_packages = oslo @@ -33,7 +34,7 @@ setup-hooks = [entry_points] console_scripts = - oslo-config-generator = oslo.config.generator:main + oslo-config-generator = oslo_config.generator:main [build_sphinx] source-dir = doc/source diff --git a/tests/test_cfg.py b/tests/test_cfg.py index c9c25c9a..150e2159 100644 --- a/tests/test_cfg.py +++ b/tests/test_cfg.py @@ -225,7 +225,7 @@ class DefaultConfigFilesTestCase(BaseTestCase): paths = self.create_tempfiles([('def', '[DEFAULT]')]) self.useFixture(fixtures.MonkeyPatch( - 'oslo.config.cfg.find_config_files', + 'oslo_config.cfg.find_config_files', lambda project, prog: paths)) self.conf(args=[], default_config_files=None) @@ -912,7 +912,7 @@ class ConfigFileOptsTestCase(BaseTestCase): self.assertTrue(hasattr(self.conf, 'foo')) self.assertEqual(self.conf.foo, 666) - @mock.patch.object(cfg, 'LOG') + @mock.patch('oslo_config.cfg.LOG') def test_conf_file_int_wrong_default(self, mock_log): cfg.IntOpt('foo', default='666') mock_log.debug.assert_call_count(1) @@ -1044,7 +1044,7 @@ class ConfigFileOptsTestCase(BaseTestCase): self.assertTrue(hasattr(self.conf, 'foo')) self.assertEqual(self.conf.foo, ['bar']) - @mock.patch.object(cfg, 'LOG') + @mock.patch('oslo_config.cfg.LOG') def test_conf_file_list_default_wrong_type(self, mock_log): cfg.ListOpt('foo', default=25) mock_log.debug.assert_called_once_with( @@ -2537,49 +2537,6 @@ class UnregisterOptTestCase(BaseTestCase): self.assertFalse(hasattr(self.conf.blaa, 'foo')) -class ImportOptTestCase(BaseTestCase): - - def test_import_opt(self): - self.assertFalse(hasattr(cfg.CONF, 'blaa')) - cfg.CONF.import_opt('blaa', 'tests.testmods.blaa_opt') - self.assertTrue(hasattr(cfg.CONF, 'blaa')) - - def test_import_opt_in_group(self): - self.assertFalse(hasattr(cfg.CONF, 'bar')) - cfg.CONF.import_opt('foo', 'tests.testmods.bar_foo_opt', group='bar') - self.assertTrue(hasattr(cfg.CONF, 'bar')) - self.assertTrue(hasattr(cfg.CONF.bar, 'foo')) - - def test_import_opt_import_errror(self): - self.assertRaises(ImportError, cfg.CONF.import_opt, - 'blaa', 'tests.testmods.blaablaa_opt') - - def test_import_opt_no_such_opt(self): - self.assertRaises(cfg.NoSuchOptError, cfg.CONF.import_opt, - 'blaablaa', 'tests.testmods.blaa_opt') - - def test_import_opt_no_such_group(self): - self.assertRaises(cfg.NoSuchGroupError, cfg.CONF.import_opt, - 'blaa', 'tests.testmods.blaa_opt', group='blaa') - - -class ImportGroupTestCase(BaseTestCase): - - def test_import_group(self): - self.assertFalse(hasattr(cfg.CONF, 'qux')) - cfg.CONF.import_group('qux', 'tests.testmods.baz_qux_opt') - self.assertTrue(hasattr(cfg.CONF, 'qux')) - self.assertTrue(hasattr(cfg.CONF.qux, 'baz')) - - def test_import_group_import_error(self): - self.assertRaises(ImportError, cfg.CONF.import_group, - 'qux', 'tests.testmods.bazzz_quxxx_opt') - - def test_import_group_no_such_group(self): - self.assertRaises(cfg.NoSuchGroupError, cfg.CONF.import_group, - 'quxxx', 'tests.testmods.baz_qux_opt') - - class RequiredOptsTestCase(BaseTestCase): def setUp(self): diff --git a/tests/test_cfgfilter.py b/tests/test_cfgfilter.py index b2ec9028..d7dbc087 100644 --- a/tests/test_cfgfilter.py +++ b/tests/test_cfgfilter.py @@ -242,39 +242,3 @@ class RegisterTestCase(BaseTestCase): self.assertEqual('bar', self.conf.foo) self.assertEqual('bar', self.fconf.foo) - - -class ImportTestCase(BaseTestCase): - - def setUp(self): - super(ImportTestCase, self).setUp(cfg.CONF) - - def test_import_opt(self): - self.assertFalse(hasattr(self.conf, 'fblaa')) - self.conf.import_opt('fblaa', 'tests.testmods.fblaa_opt') - self.assertTrue(hasattr(self.conf, 'fblaa')) - self.assertFalse(hasattr(self.fconf, 'fblaa')) - self.fconf.import_opt('fblaa', 'tests.testmods.fblaa_opt') - self.assertTrue(hasattr(self.fconf, 'fblaa')) - - def test_import_opt_in_group(self): - self.assertFalse(hasattr(self.conf, 'fbar')) - self.conf.import_opt('foo', 'tests.testmods.fbar_foo_opt', - group='fbar') - self.assertTrue(hasattr(self.conf, 'fbar')) - self.assertTrue(hasattr(self.conf.fbar, 'foo')) - self.assertFalse(hasattr(self.fconf, 'fbar')) - self.fconf.import_opt('foo', 'tests.testmods.fbar_foo_opt', - group='fbar') - self.assertTrue(hasattr(self.fconf, 'fbar')) - self.assertTrue(hasattr(self.fconf.fbar, 'foo')) - - def test_import_group(self): - self.assertFalse(hasattr(self.conf, 'fbaar')) - self.conf.import_group('fbaar', 'tests.testmods.fbaar_baa_opt') - self.assertTrue(hasattr(self.conf, 'fbaar')) - self.assertTrue(hasattr(self.conf.fbaar, 'baa')) - self.assertFalse(hasattr(self.fconf, 'fbaar')) - self.fconf.import_group('fbaar', 'tests.testmods.fbaar_baa_opt') - self.assertTrue(hasattr(self.fconf, 'fbaar')) - self.assertTrue(hasattr(self.fconf.fbaar, 'baa')) diff --git a/tests/test_generator.py b/tests/test_generator.py index 92b81532..72c2b7e9 100644 --- a/tests/test_generator.py +++ b/tests/test_generator.py @@ -493,7 +493,7 @@ class GeneratorTestCase(base.BaseTestCase): return self._capture_stream('stdout') @mock.patch('stevedore.named.NamedExtensionManager') - @mock.patch.object(generator, 'LOG') + @mock.patch('oslo_config.generator.LOG') def test_generate(self, mock_log, named_mgr): generator.register_cli_opts(self.conf) diff --git a/tests/test_warning.py b/tests/test_warning.py new file mode 100644 index 00000000..2c0bdbc2 --- /dev/null +++ b/tests/test_warning.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import imp +import os +import warnings + +import mock +from oslotest import base as test_base +import six + + +class DeprecationWarningTest(test_base.BaseTestCase): + + @mock.patch('warnings.warn') + def test_warning(self, mock_warn): + import oslo.config + imp.reload(oslo.config) + self.assertTrue(mock_warn.called) + args = mock_warn.call_args + self.assertIn('oslo_config', args[0][0]) + self.assertIn('deprecated', args[0][0]) + self.assertTrue(issubclass(args[0][1], DeprecationWarning)) + + def test_real_warning(self): + with warnings.catch_warnings(record=True) as warning_msgs: + warnings.resetwarnings() + warnings.simplefilter('always', DeprecationWarning) + import oslo.config + + # Use a separate function to get the stack level correct + # so we know the message points back to this file. This + # corresponds to an import or reload, which isn't working + # inside the test under Python 3.3. That may be due to a + # difference in the import implementation not triggering + # warnings properly when the module is reloaded, or + # because the warnings module is mostly implemented in C + # and something isn't cleanly resetting the global state + # used to track whether a warning needs to be + # emitted. Whatever the cause, we definitely see the + # warnings.warn() being invoked on a reload (see the test + # above) and warnings are reported on the console when we + # run the tests. A simpler test script run outside of + # testr does correctly report the warnings. + def foo(): + oslo.config.deprecated() + + foo() + self.assertEqual(1, len(warning_msgs)) + msg = warning_msgs[0] + self.assertIn('oslo_config', six.text_type(msg.message)) + self.assertEqual('test_warning.py', os.path.basename(msg.filename)) diff --git a/tests/testmods/bar_foo_opt.py b/tests/testmods/bar_foo_opt.py index 3aeff61b..d6703141 100644 --- a/tests/testmods/bar_foo_opt.py +++ b/tests/testmods/bar_foo_opt.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.config import cfg +from oslo_config import cfg CONF = cfg.CONF diff --git a/tests/testmods/baz_qux_opt.py b/tests/testmods/baz_qux_opt.py index 0ac49e4f..c764a668 100644 --- a/tests/testmods/baz_qux_opt.py +++ b/tests/testmods/baz_qux_opt.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.config import cfg +from oslo_config import cfg CONF = cfg.CONF diff --git a/tests/testmods/blaa_opt.py b/tests/testmods/blaa_opt.py index cc93aa4e..5c291db7 100644 --- a/tests/testmods/blaa_opt.py +++ b/tests/testmods/blaa_opt.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.config import cfg +from oslo_config import cfg CONF = cfg.CONF diff --git a/tests/testmods/fbaar_baa_opt.py b/tests/testmods/fbaar_baa_opt.py index 68875b1e..4e8e6acc 100644 --- a/tests/testmods/fbaar_baa_opt.py +++ b/tests/testmods/fbaar_baa_opt.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.config import cfg +from oslo_config import cfg CONF = cfg.CONF diff --git a/tests/testmods/fbar_foo_opt.py b/tests/testmods/fbar_foo_opt.py index c0280a4b..634959ef 100644 --- a/tests/testmods/fbar_foo_opt.py +++ b/tests/testmods/fbar_foo_opt.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.config import cfg +from oslo_config import cfg CONF = cfg.CONF diff --git a/tests/testmods/fblaa_opt.py b/tests/testmods/fblaa_opt.py index 8b5258df..ceaffbb0 100644 --- a/tests/testmods/fblaa_opt.py +++ b/tests/testmods/fblaa_opt.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.config import cfg +from oslo_config import cfg CONF = cfg.CONF