summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark McLoughlin <markmc@redhat.com>2011-11-28 14:38:34 +0000
committerMark McLoughlin <markmc@redhat.com>2011-12-07 15:26:43 +0000
commitfabebb356eec47607aae05ea174b0aeef4fef5b1 (patch)
tree9ed774438b12d8e16d6f0295c8570bf3c3f8f070
parent0c78c2ed61c84ff967c2e6894f5dcff0987c2dec (diff)
downloadnova-fabebb356eec47607aae05ea174b0aeef4fef5b1.tar.gz
nova-fabebb356eec47607aae05ea174b0aeef4fef5b1.tar.xz
nova-fabebb356eec47607aae05ea174b0aeef4fef5b1.zip
Add new cfg module
As discussed on the mailing list and described here: http://wiki.openstack.org/CommonConfigModule The module implements an API for defining configuration options and reading values for those options that a user may have set in a config file or on the command line. The module will be part of openstack-common and glance will consume it from there once openstack-common makes a release with an API compatibility commitment. Change-Id: Ib604c3de75d9066cd913c2de3007024b1f5ec9f8
-rw-r--r--nova/common/__init__.py15
-rw-r--r--nova/common/cfg.py1126
-rw-r--r--nova/tests/test_cfg.py794
3 files changed, 1935 insertions, 0 deletions
diff --git a/nova/common/__init__.py b/nova/common/__init__.py
new file mode 100644
index 000000000..0a3b98867
--- /dev/null
+++ b/nova/common/__init__.py
@@ -0,0 +1,15 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2011 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.
diff --git a/nova/common/cfg.py b/nova/common/cfg.py
new file mode 100644
index 000000000..d0e7cc82c
--- /dev/null
+++ b/nova/common/cfg.py
@@ -0,0 +1,1126 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 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 sub-classes e.g.
+
+ common_opts = [
+ cfg.StrOpt('bind_host',
+ default='0.0.0.0',
+ help='IP address to listen on'),
+ cfg.IntOpt('bind_port',
+ default=9292,
+ help='Port number to listen on')
+ ]
+
+Options can be strings, integers, floats, booleans, lists or 'multi strings':
+
+ enabled_apis_opt = \
+ cfg.ListOpt('enabled_apis',
+ default=['ec2', 'osapi'],
+ help='List of APIs to enable by default')
+
+ DEFAULT_EXTENSIONS = [
+ 'nova.api.openstack.contrib.standard_extensions'
+ ]
+ osapi_extension_opt = \
+ cfg.MultiStrOpt('osapi_extension',
+ default=DEFAULT_EXTENSIONS)
+
+Option schemas are registered with 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_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 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)
+
+The config manager has a single CLI option defined by default, --config-file:
+
+ class ConfigOpts(object):
+
+ config_file_opt = \
+ MultiStrOpt('config-file',
+ ...
+
+ def __init__(self, ...):
+ ...
+ self.register_cli_opt(self.config_file_opt)
+
+Option values are parsed from any supplied config files using SafeConfigParser.
+If none are specified, a default set is used e.g. 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 override those on the command line. Config files
+are parsed in order, with values in later files overriding those in earlier
+files.
+
+The parsing of CLI args and config files is initiated by invoking the config
+manager e.g.
+
+ conf = ConfigOpts()
+ conf.register_opt(BoolOpt('verbose', ...))
+ conf(sys.argv[1:])
+ if conf.verbose:
+ ...
+
+Options can be registered as belonging to a group:
+
+ rabbit_group = cfg.OptionGroup(name='rabbit',
+ title='RabbitMQ options')
+
+ rabbit_host_opt = \
+ cfg.StrOpt('host',
+ group='rabbit',
+ default='localhost',
+ help='IP/hostname to listen on'),
+ rabbit_port_opt = \
+ cfg.IntOpt('port',
+ default=5672,
+ help='Port number to listen on')
+ rabbit_ssl_opt = \
+ conf.BoolOpt('use_ssl',
+ default=False,
+ help='Whether to support SSL connections')
+
+ def register_rabbit_opts(conf):
+ conf.register_group(rabbit_group)
+ # options can be registered under a group in any of these ways:
+ conf.register_opt(rabbit_host_opt)
+ conf.register_opt(rabbit_port_opt, group='rabbit')
+ conf.register_opt(rabbit_ssl_opt, group=rabbit_group)
+
+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-use-ssl False
+
+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 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 that interpolation can be avoided by using '$$'.
+"""
+
+import sys
+import ConfigParser
+import copy
+import optparse
+import os
+import string
+
+
+class Error(Exception):
+ """Base class for cfg exceptions."""
+
+ def __init__(self, msg=None):
+ self.msg = msg
+
+ def __str__(self):
+ return self.msg
+
+
+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):
+ """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 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 read some config files: %s' % \
+ string.join(self.config_files, ',')
+
+
+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 find_config_files(project=None, prog=None):
+ """Return a list of default configuration files.
+
+ 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}.
+
+ :param project: an optional project name
+ :param prog: the program name, defaulting to the basename of sys.argv[0]
+ """
+ if prog is None:
+ prog = os.path.basename(sys.argv[0])
+
+ fix_path = lambda p: os.path.abspath(os.path.expanduser(p))
+
+ cfg_dirs = [
+ fix_path(os.path.join('~', '.' + project)) if project else None,
+ fix_path('~'),
+ os.path.join('/etc', project) if project else None,
+ '/etc'
+ ]
+ cfg_dirs = filter(bool, cfg_dirs)
+
+ def search_dirs(dirs, basename):
+ for d in dirs:
+ path = os.path.join(d, basename)
+ if os.path.exists(path):
+ return path
+
+ config_files = []
+ if project:
+ config_files.append(search_dirs(cfg_dirs, '%s.conf' % project))
+ config_files.append(search_dirs(cfg_dirs, '%s.conf' % prog))
+
+ return 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'] is not opt:
+ raise DuplicateOptError(opt.name)
+ return True
+ else:
+ return False
+
+
+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
+ 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
+ metavar:
+ the name shown as the argument to a CLI option in --help output
+ help:
+ an string explaining how the options value is used
+ """
+
+ def __init__(self, name, dest=None, short=None,
+ default=None, metavar=None, help=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 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 metavar: the option argument to show in --help
+ :param help: an explanation of how the option is used
+ """
+ self.name = name
+ if dest is None:
+ self.dest = self.name.replace('-', '_')
+ else:
+ self.dest = dest
+ self.short = short
+ self.default = default
+ self.metavar = metavar
+ self.help = help
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieves the option value from a ConfigParser object.
+
+ This is the method ConfigOpts uses to look up the option value from
+ config files. Most opt types override this method in order to perform
+ type appropriate conversion of the returned value.
+
+ :param cparser: a ConfigParser object
+ :param section: a section name
+ """
+ return cparser.get(section, self.dest)
+
+ 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_optparse_container(parser, group)
+ kwargs = self._get_optparse_kwargs(group)
+ prefix = self._get_optparse_prefix('', group)
+ self._add_to_optparse(container, self.name, self.short, kwargs, prefix)
+
+ def _add_to_optparse(self, container, name, short, kwargs, prefix=''):
+ """Add an option to an optparse parser or group.
+
+ :param container: an optparse.OptionContainer object
+ :param name: the opt name
+ :param short: the short opt name
+ :param kwargs: the keyword arguments for add_option()
+ :param prefix: an optional prefix to prepend to the opt name
+ :raises: DuplicateOptError if a naming confict is detected
+ """
+ args = ['--' + prefix + name]
+ if short:
+ args += ['-' + short]
+ for a in args:
+ if container.has_option(a):
+ raise DuplicateOptError(a)
+ container.add_option(*args, **kwargs)
+
+ def _get_optparse_container(self, parser, group):
+ """Returns an optparse.OptionContainer.
+
+ :param parser: an optparse.OptionParser
+ :param group: an (optional) OptGroup object
+ :returns: an optparse.OptionGroup if a group is given, else the parser
+ """
+ if group is not None:
+ return group._get_optparse_group(parser)
+ else:
+ return parser
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Build a dict of keyword arguments for optparse's add_option().
+
+ Most opt types extend this method to customize the behaviour of the
+ options added to optparse.
+
+ :param group: an optional group
+ :param kwargs: optional keyword arguments to add to
+ :returns: a dict of keyword arguments
+ """
+ dest = self.dest
+ if group is not None:
+ dest = group.name + '_' + dest
+ kwargs.update({
+ 'dest': dest,
+ 'metavar': self.metavar,
+ 'help': self.help,
+ })
+ return kwargs
+
+ def _get_optparse_prefix(self, prefix, group):
+ """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 (e.g. 'no' or '')
+ :param group: an optional OptGroup object
+ :returns: a CLI option prefix including the group name, if appropriate
+ """
+ if group is not None:
+ return group.name + '-' + prefix
+ else:
+ return prefix
+
+
+class StrOpt(Opt):
+ """
+ String opts do not have their values transformed and are returned as
+ str objects.
+ """
+ pass
+
+
+class BoolOpt(Opt):
+
+ """
+ Bool opts are set to True or False on the command line using --optname or
+ --noopttname respectively.
+
+ In config files, boolean values are case insensitive and can be set using
+ 1/0, yes/no, true/false or on/off.
+ """
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a boolean from ConfigParser."""
+ return cparser.getboolean(section, self.dest)
+
+ 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_optparse(parser, group)
+
+ def _add_inverse_to_optparse(self, parser, group):
+ """Add the --nooptname option to the option parser."""
+ container = self._get_optparse_container(parser, group)
+ kwargs = self._get_optparse_kwargs(group, action='store_false')
+ prefix = self._get_optparse_prefix('no', group)
+ self._add_to_optparse(container, self.name, None, kwargs, prefix)
+
+ def _get_optparse_kwargs(self, group, action='store_true', **kwargs):
+ """Extends the base optparse keyword dict for boolean options."""
+ return super(BoolOpt,
+ self)._get_optparse_kwargs(group, action=action, **kwargs)
+
+
+class IntOpt(Opt):
+
+ """Int opt values are converted to integers using the int() builtin."""
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a integer from ConfigParser."""
+ return cparser.getint(section, self.dest)
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for integer options."""
+ return super(IntOpt,
+ self)._get_optparse_kwargs(group, type='int', **kwargs)
+
+
+class FloatOpt(Opt):
+
+ """Float opt values are converted to floats using the float() builtin."""
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a float from ConfigParser."""
+ return cparser.getfloat(section, self.dest)
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for float options."""
+ return super(FloatOpt,
+ self)._get_optparse_kwargs(group, type='float', **kwargs)
+
+
+class ListOpt(Opt):
+
+ """
+ List opt values are simple string values separated by commas. The opt value
+ is a list containing these strings.
+ """
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a list from ConfigParser."""
+ return cparser.get(section, self.dest).split(',')
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for list options."""
+ return super(ListOpt,
+ self)._get_optparse_kwargs(group,
+ type='string',
+ action='callback',
+ callback=self._parse_list,
+ **kwargs)
+
+ def _parse_list(self, option, opt, value, parser):
+ """An optparse callback for parsing an option value into a list."""
+ setattr(parser.values, self.dest, value.split(','))
+
+
+class MultiStrOpt(Opt):
+
+ """
+ Multistr opt values are string opts which may be specified multiple times.
+ The opt value is a list containing all the string values specified.
+ """
+
+ def _get_from_config_parser(self, cparser, section):
+ """Retrieve the opt value as a multistr from ConfigParser."""
+ # FIXME(markmc): values spread across the CLI and multiple
+ # config files should be appended
+ value = \
+ super(MultiStrOpt, self)._get_from_config_parser(cparser, section)
+ return value if value is None else [value]
+
+ def _get_optparse_kwargs(self, group, **kwargs):
+ """Extends the base optparse keyword dict for multi str options."""
+ return super(MultiStrOpt,
+ self)._get_optparse_kwargs(group, action='append')
+
+
+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
+ if title is None:
+ self.title = "%s options" % title
+ else:
+ self.title = title
+ self.help = help
+
+ self._opts = {} # dict of dicts of {opt:, override:, default:)
+ self._optparse_group = None
+
+ def _register_opt(self, opt):
+ """Add an opt to this group.
+
+ :param opt: an Opt object
+ :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, 'override': None, 'default': None}
+
+ return True
+
+ def _get_optparse_group(self, parser):
+ """Build an optparse.OptionGroup for this group."""
+ if self._optparse_group is None:
+ self._optparse_group = \
+ optparse.OptionGroup(parser, self.title, self.help)
+ return self._optparse_group
+
+
+class ConfigOpts(object):
+
+ """
+ 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,
+ project=None,
+ prog=None,
+ version=None,
+ usage=None,
+ default_config_files=None):
+ """Construct a ConfigOpts object.
+
+ Automatically registers the --config-file option with either a supplied
+ list of default config files, or a list from find_config_files().
+
+ :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
+ """
+ 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.project = project
+ self.prog = prog
+ self.version = version
+ self.usage = usage
+ self.default_config_files = default_config_files
+
+ self._opts = {} # dict of dicts of (opt:, override:, default:)
+ self._groups = {}
+
+ self._args = None
+ self._cli_values = {}
+
+ self._oparser = optparse.OptionParser(prog=self.prog,
+ version=self.version,
+ usage=self.usage)
+ self._cparser = None
+
+ self.register_cli_opt(\
+ MultiStrOpt('config-file',
+ default=self.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: %s' % (self.default_config_files, )))
+
+ def __call__(self, args=None):
+ """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.
+
+ :params args: command line arguments (defaults to sys.argv[1:])
+ :returns: the list of arguments left over after parsing options
+ :raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError
+ """
+ self.reset()
+
+ self._args = args
+
+ (values, args) = self._oparser.parse_args(self._args)
+
+ self._cli_values = vars(values)
+
+ if self.config_file:
+ self._parse_config_files(self.config_file)
+
+ return args
+
+ 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 subsititution) or a GroupAttr
+ :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError
+ """
+ return self._substitute(self._get(name))
+
+ def reset(self):
+ """Reset the state of the object to before it was called."""
+ self._args = None
+ self._cli_values = None
+ self._cparser = None
+
+ def register_opt(self, opt, group=None):
+ """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 group: an optional OptGroup object or group name
+ :return: False if the opt was already register, True otherwise
+ :raises: DuplicateOptError
+ """
+ if group is not None:
+ return self._get_group(group)._register_opt(opt)
+
+ if _is_opt_registered(self._opts, opt):
+ return False
+
+ self._opts[opt.dest] = {'opt': opt, 'override': None, 'default': None}
+
+ return True
+
+ 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.
+
+ CLI option schemas must be registered before the command line and
+ config files are parsed. This is to ensure that all CLI options are
+ show 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 register, True otherwise
+ :raises: DuplicateOptError, ArgsAlreadyParsedError
+ """
+ if self._args != None:
+ raise ArgsAlreadyParsedError("cannot register CLI option")
+
+ if not self.register_opt(opt, group):
+ return False
+
+ if group is not None:
+ group = self._get_group(group)
+
+ opt._add_to_cli(self._oparser, group)
+
+ return True
+
+ 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.
+
+ 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)
+
+ 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
+
+ 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
+
+ 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 (e.g. 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)
+
+ for opt_name in sorted(self._opts):
+ logger.log(lvl, "%-30s = %s", opt_name, getattr(self, opt_name))
+
+ for group_name in self._groups:
+ group_attr = self.GroupAttr(self, group_name)
+ for opt_name in sorted(self._groups[group_name]._opts):
+ logger.log(lvl, "%-30s = %s",
+ "%s.%s" % (group_name, opt_name),
+ getattr(group_attr, opt_name))
+
+ logger.log(lvl, "*" * 80)
+
+ def print_usage(self, file=None):
+ """Print the usage message for the current program."""
+ self._oparser.print_usage(file)
+
+ def _get(self, name, group=None):
+ """Look up an option value.
+
+ :param name: the opt name (or 'dest', more precisely)
+ :param group: an option OptGroup
+ :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, name)
+
+ if group is not None:
+ group = self._get_group(group)
+
+ info = self._get_opt_info(name, group)
+ default, opt, override = map(lambda k: info[k], sorted(info.keys()))
+
+ if override is not None:
+ return override
+
+ if self._cparser is not None:
+ section = group.name if group is not None else 'DEFAULT'
+ try:
+ return opt._get_from_config_parser(self._cparser, section)
+ except (ConfigParser.NoOptionError,
+ ConfigParser.NoSectionError):
+ pass
+ except ValueError, ve:
+ raise ConfigFileValueError(str(ve))
+
+ name = name if group is None else group.name + '_' + name
+ value = self._cli_values.get(name, None)
+ if value is not None:
+ return value
+
+ if default is not None:
+ return default
+
+ return opt.default
+
+ def _substitute(self, value):
+ """Perform string template substitution.
+
+ Substititue any template variables (e.g. $foo, ${bar}) in the supplied
+ string value(s) with opt values.
+
+ :param value: the string value, or list of string values
+ :returns: the substituted string(s)
+ """
+ if type(value) is list:
+ return [self._substitute(i) for i in value]
+ elif type(value) is str:
+ tmpl = string.Template(value)
+ return tmpl.safe_substitute(self.StrSubWrapper(self))
+ else:
+ return value
+
+ def _get_group(self, group_or_name):
+ """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.
+
+ :param group_or_name: the group's name or the OptGroup object itself
+ :raises: NoSuchGroupError
+ """
+ if isinstance(group_or_name, OptGroup):
+ group_name = group_or_name.name
+ else:
+ group_name = group_or_name
+
+ if not group_name in self._groups:
+ raise NoSuchGroupError(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 not opt_name in opts:
+ raise NoSuchOptError(opt_name, group)
+
+ return opts[opt_name]
+
+ def _parse_config_files(self, config_files):
+ """Parse the supplied configuration files.
+
+ :raises: ConfigFilesNotFoundError, ConfigFileParseError
+ """
+ self._cparser = ConfigParser.SafeConfigParser(allow_no_value=True)
+
+ try:
+ read_ok = self._cparser.read(config_files)
+ except ConfigParser.ParsingError, cpe:
+ raise ConfigFileParseError(cpe.filename, cpe.message)
+
+ if read_ok != config_files:
+ not_read_ok = filter(lambda f: f not in read_ok, config_files)
+ raise ConfigFilesNotFoundError(not_read_ok)
+
+ class GroupAttr(object):
+
+ """
+ A helper class representing the option values of a group as attributes.
+ """
+
+ def __init__(self, conf, group):
+ """Construct a GroupAttr object.
+
+ :param conf: a ConfigOpts object
+ :param group: a group name or OptGroup object
+ """
+ self.conf = conf
+ self.group = group
+
+ def __getattr__(self, name):
+ """Look up an option value and perform template substitution."""
+ return self.conf._substitute(self.conf._get(name, self.group))
+
+ class StrSubWrapper(object):
+
+ """
+ A helper class exposing opt values as a dict for string substitution.
+ """
+
+ def __init__(self, conf):
+ """Construct a StrSubWrapper object.
+
+ :param conf: a ConfigOpts object
+ """
+ self.conf = conf
+
+ 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
+ """
+ value = getattr(self.conf, key)
+ if isinstance(value, self.conf.GroupAttr):
+ raise TemplateSubstitutionError(
+ 'substituting group %s not supported' % key)
+ return value
+
+
+class CommonConfigOpts(ConfigOpts):
+
+ DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
+ DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+ common_cli_opts = [
+ BoolOpt('debug',
+ short='d',
+ default=False,
+ help='Print debugging output'),
+ BoolOpt('verbose',
+ short='v',
+ default=False,
+ help='Print more verbose output'),
+ ]
+
+ logging_cli_opts = [
+ StrOpt('log-config',
+ metavar='PATH',
+ help='If this option is specified, the logging configuration '
+ 'file specified is used and overrides any other logging '
+ 'options specified. Please see the Python logging module '
+ 'documentation for details on logging configuration '
+ 'files.'),
+ StrOpt('log-format',
+ default=DEFAULT_LOG_FORMAT,
+ metavar='FORMAT',
+ help='A logging.Formatter log message format string which may '
+ 'use any of the available logging.LogRecord attributes. '
+ 'Default: %default'),
+ StrOpt('log-date-format',
+ default=DEFAULT_LOG_DATE_FORMAT,
+ metavar='DATE_FORMAT',
+ help='Format string for %(asctime)s in log records. '
+ 'Default: %default'),
+ StrOpt('log-file',
+ metavar='PATH',
+ help='(Optional) Name of log file to output to. '
+ 'If not set, logging will go to stdout.'),
+ StrOpt('log-dir',
+ help='(Optional) The directory to keep log files in '
+ '(will be prepended to --logfile)'),
+ BoolOpt('use-syslog',
+ default=False,
+ help='Use syslog for logging.'),
+ ]
+
+ def __init__(self, **kwargs):
+ super(CommonConfigOpts, self).__init__(**kwargs)
+ self.register_cli_opts(self.common_cli_opts)
+ self.register_cli_opts(self.logging_cli_opts)
diff --git a/nova/tests/test_cfg.py b/nova/tests/test_cfg.py
new file mode 100644
index 000000000..90147f589
--- /dev/null
+++ b/nova/tests/test_cfg.py
@@ -0,0 +1,794 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 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 os
+import sys
+import StringIO
+import tempfile
+import unittest
+
+import stubout
+
+from nova import test
+from nova.common.cfg import *
+
+
+class BaseTestCase(test.TestCase):
+
+ def setUp(self):
+ self.conf = ConfigOpts(prog='test',
+ version='1.0',
+ usage='%prog FOO BAR',
+ default_config_files=[])
+ self.tempfiles = []
+ self.stubs = stubout.StubOutForTesting()
+
+ def tearDown(self):
+ self.remove_tempfiles()
+ self.stubs.UnsetAll()
+
+ def create_tempfiles(self, files):
+ for (basename, contents) in files:
+ (fd, path) = tempfile.mkstemp(prefix=basename)
+ self.tempfiles.append(path)
+ try:
+ os.write(fd, contents)
+ finally:
+ os.close(fd)
+ return self.tempfiles[-len(files):]
+
+ def remove_tempfiles(self):
+ for p in self.tempfiles:
+ os.remove(p)
+
+
+class LeftoversTestCase(BaseTestCase):
+
+ def test_leftovers(self):
+ self.conf.register_cli_opt(StrOpt('foo'))
+ self.conf.register_cli_opt(StrOpt('bar'))
+
+ leftovers = self.conf(['those', '--foo', 'this',
+ 'thems', '--bar', 'that', 'these'])
+
+ self.assertEquals(leftovers, ['those', 'thems', 'these'])
+
+
+class FindConfigFilesTestCase(BaseTestCase):
+
+ def test_find_config_files(self):
+ config_files = \
+ [os.path.expanduser('~/.blaa/blaa.conf'), '/etc/foo.conf']
+
+ self.stubs.Set(os.path, 'exists', lambda p: p in config_files)
+
+ self.assertEquals(find_config_files(project='blaa', prog='foo'),
+ config_files)
+
+
+class CliOptsTestCase(BaseTestCase):
+
+ def _do_cli_test(self, opt_class, default, cli_args, value):
+ self.conf.register_cli_opt(opt_class('foo', default=default))
+
+ self.conf(cli_args)
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, value)
+
+ def test_str_default(self):
+ self._do_cli_test(StrOpt, None, [], None)
+
+ def test_str_arg(self):
+ self._do_cli_test(StrOpt, None, ['--foo', 'bar'], 'bar')
+
+ def test_bool_default(self):
+ self._do_cli_test(BoolOpt, False, [], False)
+
+ def test_bool_arg(self):
+ self._do_cli_test(BoolOpt, None, ['--foo'], True)
+
+ def test_bool_arg_inverse(self):
+ self._do_cli_test(BoolOpt, None, ['--foo', '--nofoo'], False)
+
+ def test_int_default(self):
+ self._do_cli_test(IntOpt, 10, [], 10)
+
+ def test_int_arg(self):
+ self._do_cli_test(IntOpt, None, ['--foo=20'], 20)
+
+ def test_float_default(self):
+ self._do_cli_test(FloatOpt, 1.0, [], 1.0)
+
+ def test_float_arg(self):
+ self._do_cli_test(FloatOpt, None, ['--foo', '2.0'], 2.0)
+
+ def test_list_default(self):
+ self._do_cli_test(ListOpt, ['bar'], [], ['bar'])
+
+ def test_list_arg(self):
+ self._do_cli_test(ListOpt, None,
+ ['--foo', 'blaa,bar'], ['blaa', 'bar'])
+
+ def test_multistr_default(self):
+ self._do_cli_test(MultiStrOpt, ['bar'], [], ['bar'])
+
+ def test_multistr_arg(self):
+ self._do_cli_test(MultiStrOpt, None,
+ ['--foo', 'blaa', '--foo', 'bar'], ['blaa', 'bar'])
+
+ def test_help(self):
+ self.stubs.Set(sys, 'stdout', StringIO.StringIO())
+ self.assertRaisesRegexp(SystemExit, '0', self.conf, ['--help'])
+ self.assertIn('FOO BAR', sys.stdout.getvalue())
+ self.assertIn('--version', sys.stdout.getvalue())
+ self.assertIn('--help', sys.stdout.getvalue())
+ self.assertIn('--config-file=PATH', sys.stdout.getvalue())
+
+ def test_version(self):
+ self.stubs.Set(sys, 'stdout', StringIO.StringIO())
+ self.assertRaisesRegexp(SystemExit, '0', self.conf, ['--version'])
+ self.assertIn('1.0', sys.stdout.getvalue())
+
+ def test_config_file(self):
+ paths = self.create_tempfiles([('1.conf', '[DEFAULT]'),
+ ('2.conf', '[DEFAULT]')])
+
+ self.conf(['--config-file', paths[0], '--config-file', paths[1]])
+
+ self.assertEquals(self.conf.config_file, paths)
+
+
+class ConfigFileOptsTestCase(BaseTestCase):
+
+ def test_str_default(self):
+ self.conf.register_opt(StrOpt('foo', default='bar'))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 'bar')
+
+ def test_str_value(self):
+ self.conf.register_opt(StrOpt('foo'))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n'
+ 'foo = bar\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 'bar')
+
+ def test_str_value_override(self):
+ self.conf.register_cli_opt(StrOpt('foo'))
+
+ paths = self.create_tempfiles([('1.conf',
+ '[DEFAULT]\n'
+ 'foo = baar\n'),
+ ('2.conf',
+ '[DEFAULT]\n'
+ 'foo = baaar\n')])
+
+ self.conf(['--foo', 'bar',
+ '--config-file', paths[0],
+ '--config-file', paths[1]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 'baaar')
+
+ def test_int_default(self):
+ self.conf.register_opt(IntOpt('foo', default=666))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 666)
+
+ def test_int_value(self):
+ self.conf.register_opt(IntOpt('foo'))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n'
+ 'foo = 666\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 666)
+
+ def test_int_value_override(self):
+ self.conf.register_cli_opt(IntOpt('foo'))
+
+ paths = self.create_tempfiles([('1.conf',
+ '[DEFAULT]\n'
+ 'foo = 66\n'),
+ ('2.conf',
+ '[DEFAULT]\n'
+ 'foo = 666\n')])
+
+ self.conf(['--foo', '6',
+ '--config-file', paths[0],
+ '--config-file', paths[1]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 666)
+
+ def test_float_default(self):
+ self.conf.register_opt(FloatOpt('foo', default=6.66))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 6.66)
+
+ def test_float_value(self):
+ self.conf.register_opt(FloatOpt('foo'))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n'
+ 'foo = 6.66\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, 6.66)
+
+ def test_float_value_override(self):
+ self.conf.register_cli_opt(FloatOpt('foo'))
+
+ paths = self.create_tempfiles([('1.conf',
+ '[DEFAULT]\n'
+ 'foo = 6.6\n'),
+ ('2.conf',
+ '[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.assertEquals(self.conf.foo, 6.66)
+
+ def test_list_default(self):
+ self.conf.register_opt(ListOpt('foo', default=['bar']))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, ['bar'])
+
+ def test_list_value(self):
+ self.conf.register_opt(ListOpt('foo'))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n'
+ 'foo = bar\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, ['bar'])
+
+ def test_list_value_override(self):
+ self.conf.register_cli_opt(ListOpt('foo'))
+
+ paths = self.create_tempfiles([('1.conf',
+ '[DEFAULT]\n'
+ 'foo = bar,bar\n'),
+ ('2.conf',
+ '[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.assertEquals(self.conf.foo, ['b', 'a', 'r'])
+
+ def test_multistr_default(self):
+ self.conf.register_opt(MultiStrOpt('foo', default=['bar']))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, ['bar'])
+
+ def test_multistr_value(self):
+ self.conf.register_opt(MultiStrOpt('foo'))
+
+ paths = self.create_tempfiles([('test.conf',
+ '[DEFAULT]\n'
+ 'foo = bar\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, ['bar'])
+
+ @unittest.skip('FIXME(markmc): values spread across the CLI and multiple '
+ 'config files should be appended')
+ def test_multistr_values_append(self):
+ self.conf.register_cli_opt(ListOpt('foo'))
+
+ paths = self.create_tempfiles([('1.conf',
+ '[DEFAULT]\n'
+ 'foo = bar\n'),
+ ('2.conf',
+ '[DEFAULT]\n'
+ 'foo = bar\n')])
+
+ self.conf(['--foo', 'bar',
+ '--config-file', paths[0],
+ '--config-file', paths[1]])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, ['bar', 'bar', 'bar'])
+
+
+class OptGroupsTestCase(BaseTestCase):
+
+ def test_arg_group(self):
+ blaa_group = OptGroup('blaa')
+ self.conf.register_group(blaa_group)
+ self.conf.register_cli_opt(StrOpt('foo'), group=blaa_group)
+
+ self.conf(['--blaa-foo', 'bar'])
+
+ self.assertTrue(hasattr(self.conf, 'blaa'))
+ self.assertTrue(hasattr(self.conf.blaa, 'foo'))
+ self.assertEquals(self.conf.blaa.foo, 'bar')
+
+ def test_arg_group_by_name(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_cli_opt(StrOpt('foo'), group='blaa')
+
+ self.conf(['--blaa-foo', 'bar'])
+
+ self.assertTrue(hasattr(self.conf, 'blaa'))
+ self.assertTrue(hasattr(self.conf.blaa, 'foo'))
+ self.assertEquals(self.conf.blaa.foo, 'bar')
+
+ def test_arg_group_with_default(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_cli_opt(StrOpt('foo', default='bar'), group='blaa')
+
+ self.conf([])
+
+ self.assertTrue(hasattr(self.conf, 'blaa'))
+ self.assertTrue(hasattr(self.conf.blaa, 'foo'))
+ self.assertEquals(self.conf.blaa.foo, 'bar')
+
+ def test_arg_group_in_config_file(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_opt(StrOpt('foo'), group='blaa')
+
+ paths = self.create_tempfiles([('test.conf',
+ '[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.assertEquals(self.conf.blaa.foo, 'bar')
+
+
+class TemplateSubstitutionTestCase(BaseTestCase):
+
+ def _prep_test_str_sub(self, foo_default=None, bar_default=None):
+ self.conf.register_cli_opt(StrOpt('foo', default=foo_default))
+ self.conf.register_cli_opt(StrOpt('bar', default=bar_default))
+
+ def _assert_str_sub(self):
+ self.assertTrue(hasattr(self.conf, 'bar'))
+ self.assertEquals(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_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.conf',
+ '[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.conf',
+ '[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.conf',
+ '[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.conf',
+ '[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.conf',
+ '[DEFAULT]\n'
+ 'bar = $foo\n'
+ 'foo = blaa\n')])
+
+ self.conf(['--config-file', paths[0]])
+
+ self._assert_str_sub()
+
+ def test_str_sub_group_from_default(self):
+ self.conf.register_cli_opt(StrOpt('foo', default='blaa'))
+ self.conf.register_group(OptGroup('ba'))
+ self.conf.register_cli_opt(StrOpt('r', default='$foo'), group='ba')
+
+ self.conf([])
+
+ self.assertTrue(hasattr(self.conf, 'ba'))
+ self.assertTrue(hasattr(self.conf.ba, 'r'))
+ self.assertEquals(self.conf.ba.r, 'blaa')
+
+
+class ReparseTestCase(BaseTestCase):
+
+ def test_reparse(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_cli_opt(StrOpt('foo', default='r'), group='blaa')
+
+ paths = self.create_tempfiles([('test.conf',
+ '[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.assertEquals(self.conf.blaa.foo, 'b')
+
+ self.conf(['--blaa-foo', 'a'])
+
+ self.assertTrue(hasattr(self.conf, 'blaa'))
+ self.assertTrue(hasattr(self.conf.blaa, 'foo'))
+ self.assertEquals(self.conf.blaa.foo, 'a')
+
+ self.conf([])
+
+ self.assertTrue(hasattr(self.conf, 'blaa'))
+ self.assertTrue(hasattr(self.conf.blaa, 'foo'))
+ self.assertEquals(self.conf.blaa.foo, 'r')
+
+
+class OverridesTestCase(BaseTestCase):
+
+ def test_no_default_override(self):
+ self.conf.register_opt(StrOpt('foo'))
+ self.conf([])
+ self.assertEquals(self.conf.foo, None)
+ self.conf.set_default('foo', 'bar')
+ self.assertEquals(self.conf.foo, 'bar')
+
+ def test_default_override(self):
+ self.conf.register_opt(StrOpt('foo', default='foo'))
+ self.conf([])
+ self.assertEquals(self.conf.foo, 'foo')
+ self.conf.set_default('foo', 'bar')
+ self.assertEquals(self.conf.foo, 'bar')
+ self.conf.set_default('foo', None)
+ self.assertEquals(self.conf.foo, 'foo')
+
+ def test_override(self):
+ self.conf.register_opt(StrOpt('foo'))
+ self.conf.set_override('foo', 'bar')
+ self.conf([])
+ self.assertEquals(self.conf.foo, 'bar')
+
+ def test_group_no_default_override(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_opt(StrOpt('foo'), group='blaa')
+ self.conf([])
+ self.assertEquals(self.conf.blaa.foo, None)
+ self.conf.set_default('foo', 'bar', group='blaa')
+ self.assertEquals(self.conf.blaa.foo, 'bar')
+
+ def test_default_override(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_opt(StrOpt('foo', default='foo'), group='blaa')
+ self.conf([])
+ self.assertEquals(self.conf.blaa.foo, 'foo')
+ self.conf.set_default('foo', 'bar', group='blaa')
+ self.assertEquals(self.conf.blaa.foo, 'bar')
+ self.conf.set_default('foo', None, group='blaa')
+ self.assertEquals(self.conf.blaa.foo, 'foo')
+
+ def test_override(self):
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_opt(StrOpt('foo'), group='blaa')
+ self.conf.set_override('foo', 'bar', group='blaa')
+ self.conf([])
+ self.assertEquals(self.conf.blaa.foo, 'bar')
+
+
+class SadPathTestCase(BaseTestCase):
+
+ def test_unknown_attr(self):
+ self.conf([])
+ self.assertFalse(hasattr(self.conf, 'foo'))
+ self.assertRaisesRegexp(NoSuchOptError, 'foo',
+ getattr, self.conf, 'foo')
+
+ def test_unknown_group_attr(self):
+ self.conf.register_group(OptGroup('blaa'))
+
+ self.conf([])
+
+ self.assertTrue(hasattr(self.conf, 'blaa'))
+ self.assertFalse(hasattr(self.conf.blaa, 'foo'))
+ self.assertRaisesRegexp(NoSuchOptError, 'blaa.*foo',
+ getattr, self.conf.blaa, 'foo')
+
+ def test_ok_duplicate(self):
+ opt = StrOpt('foo')
+ self.conf.register_cli_opt(opt)
+ self.conf.register_cli_opt(opt)
+
+ self.conf([])
+
+ self.assertTrue(hasattr(self.conf, 'foo'))
+ self.assertEquals(self.conf.foo, None)
+
+ def test_error_duplicate(self):
+ self.conf.register_cli_opt(StrOpt('foo'))
+ self.assertRaisesRegexp(DuplicateOptError, 'foo',
+ self.conf.register_cli_opt,
+ StrOpt('foo'))
+
+ def test_error_duplicate_with_different_dest(self):
+ self.conf.register_cli_opt(StrOpt('foo', dest='f'))
+ self.assertRaisesRegexp(DuplicateOptError, 'foo',
+ self.conf.register_cli_opt,
+ StrOpt('foo'))
+
+ def test_error_duplicate_short(self):
+ self.conf.register_cli_opt(StrOpt('foo', short='f'))
+ self.assertRaises(DuplicateOptError,
+ self.conf.register_cli_opt,
+ StrOpt('bar', short='f'))
+
+ def test_no_such_group(self):
+ self.assertRaisesRegexp(NoSuchGroupError, 'blaa',
+ self.conf.register_cli_opt,
+ StrOpt('foo'), group='blaa')
+
+ def test_already_parsed(self):
+ self.conf([])
+
+ self.assertRaises(ArgsAlreadyParsedError,
+ self.conf.register_cli_opt, StrOpt('foo'))
+
+ def test_bad_cli_arg(self):
+ self.stubs.Set(sys, 'stderr', StringIO.StringIO())
+
+ self.assertRaisesRegexp(SystemExit, '2', self.conf, ['--foo'])
+
+ self.assertIn('error', sys.stderr.getvalue())
+ self.assertIn('--foo', sys.stderr.getvalue())
+
+ def _do_test_bad_cli_value(self, opt_class):
+ self.conf.register_cli_opt(opt_class('foo'))
+
+ self.stubs.Set(sys, 'stderr', StringIO.StringIO())
+
+ self.assertRaisesRegexp(SystemExit, '2', self.conf, ['--foo', 'bar'])
+
+ self.assertIn('foo', sys.stderr.getvalue())
+ self.assertIn('bar', sys.stderr.getvalue())
+
+ def test_bad_int_arg(self):
+ self._do_test_bad_cli_value(IntOpt)
+
+ def test_bad_float_arg(self):
+ self._do_test_bad_cli_value(FloatOpt)
+
+ def test_conf_file_not_found(self):
+ paths = self.create_tempfiles([('test.conf', '')])
+ os.remove(paths[0])
+ self.tempfiles.remove(paths[0])
+
+ self.assertRaisesRegexp(ConfigFilesNotFoundError, paths[0],
+ self.conf, ['--config-file', paths[0]])
+
+ def test_conf_file_not_found(self):
+ paths = self.create_tempfiles([('test.conf', 'foo')])
+
+ self.assertRaisesRegexp(ConfigFileParseError, paths[0],
+ self.conf, ['--config-file', paths[0]])
+
+ def _do_test_conf_file_bad_value(self, opt_class):
+ self.conf.register_opt(opt_class('foo'))
+
+ def test_conf_file_bad_bool(self):
+ self._do_test_conf_file_bad_value(BoolOpt)
+
+ def test_conf_file_bad_int(self):
+ self._do_test_conf_file_bad_value(IntOpt)
+
+ def test_conf_file_bad_float(self):
+ self._do_test_conf_file_bad_value(FloatOpt)
+
+ def test_str_sub_from_group(self):
+ self.conf.register_group(OptGroup('f'))
+ self.conf.register_cli_opt(StrOpt('oo', default='blaa'), group='f')
+ self.conf.register_cli_opt(StrOpt('bar', default='$f.oo'))
+
+ self.conf([])
+
+ self.assertFalse(hasattr(self.conf, 'bar'))
+ self.assertRaises(TemplateSubstitutionError, getattr, self.conf, 'bar')
+
+ def test_set_default_unknown_attr(self):
+ self.conf([])
+ self.assertRaisesRegexp(NoSuchOptError, 'foo',
+ self.conf.set_default, 'foo', 'bar')
+
+ def test_set_default_unknown_group(self):
+ self.conf([])
+ self.assertRaisesRegexp(NoSuchGroupError, 'blaa',
+ self.conf.set_default,
+ 'foo', 'bar', group='blaa')
+
+ def test_set_override_unknown_attr(self):
+ self.conf([])
+ self.assertRaisesRegexp(NoSuchOptError, 'foo',
+ self.conf.set_override, 'foo', 'bar')
+
+ def test_set_override_unknown_group(self):
+ self.conf([])
+ self.assertRaisesRegexp(NoSuchGroupError, 'blaa',
+ self.conf.set_override,
+ 'foo', 'bar', group='blaa')
+
+
+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.assertEquals(lvl, self.expected_lvl)
+ self.logged.append(fmt % args)
+
+ def test_log_opt_values(self):
+ self.conf.register_cli_opt(StrOpt('foo'))
+ self.conf.register_group(OptGroup('blaa'))
+ self.conf.register_cli_opt(StrOpt('bar'), 'blaa')
+
+ self.conf(['--foo', 'this', '--blaa-bar', 'that'])
+
+ logger = self.FakeLogger(self, 666)
+
+ self.conf.log_opt_values(logger, 666)
+
+ self.assertEquals(logger.logged, [
+ "*" * 80,
+ "Configuration options gathered from:",
+ "command line args: ['--foo', 'this', '--blaa-bar', 'that']",
+ "config files: []",
+ "=" * 80,
+ "config_file = []",
+ "foo = this",
+ "blaa.bar = that",
+ "*" * 80,
+ ])
+
+
+class CommonOptsTestCase(BaseTestCase):
+
+ def setUp(self):
+ super(CommonOptsTestCase, self).setUp()
+ self.conf = CommonConfigOpts()
+
+ def test_debug_verbose(self):
+ self.conf(['--debug', '--verbose'])
+
+ self.assertEquals(self.conf.debug, True)
+ self.assertEquals(self.conf.verbose, True)
+
+ def test_logging_opts(self):
+ self.conf([])
+
+ self.assertIsNone(self.conf.log_config)
+ self.assertIsNone(self.conf.log_file)
+ self.assertIsNone(self.conf.log_dir)
+
+ self.assertEquals(self.conf.log_format,
+ CommonConfigOpts.DEFAULT_LOG_FORMAT)
+ self.assertEquals(self.conf.log_date_format,
+ CommonConfigOpts.DEFAULT_LOG_DATE_FORMAT)
+
+ self.assertEquals(self.conf.use_syslog, False)