6447da75af
Main cfg change is: e3e5e0e Fixes "is not", "not in" syntax usage d156150 Implements import_group Change-Id: Ie6c0fe0d9d35ef6178a121e4f566207576c8d1e6
1750 lines
58 KiB
Python
1750 lines
58 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# 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 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_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)
|
|
|
|
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 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 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
|
|
openstack.common.iniparser. 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.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 it no group attributes are required other than the group name, the group
|
|
need not be explicitly registered e.g.
|
|
|
|
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
|
|
|
|
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 '$$'.
|
|
|
|
Options may be declared as required so that an error is raised if the user
|
|
does not supply a value for the option.
|
|
|
|
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),
|
|
...
|
|
]
|
|
|
|
This module also contains a global instance of the ConfigOpts class
|
|
in order to support a common usage pattern in OpenStack::
|
|
|
|
from quantum.openstack.common 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 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']
|
|
|
|
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 functools
|
|
import glob
|
|
import os
|
|
import string
|
|
import sys
|
|
|
|
from quantum.openstack.common import iniparser
|
|
|
|
|
|
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, 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 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 _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 directors 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 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, e.g. 'glance-api'
|
|
:param extension: the file extension, e.g. '.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 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]
|
|
break
|
|
|
|
|
|
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
|
|
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, dest=None, short=None, default=None,
|
|
positional=False, metavar=None, help=None,
|
|
secret=False, required=False, deprecated_name=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 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
|
|
"""
|
|
self.name = name
|
|
if dest is None:
|
|
self.dest = self.name.replace('-', '_')
|
|
else:
|
|
self.dest = dest
|
|
self.short = short
|
|
self.default = default
|
|
self.positional = positional
|
|
self.metavar = metavar
|
|
self.help = help
|
|
self.secret = secret
|
|
self.required = required
|
|
if deprecated_name is not None:
|
|
self.deprecated_name = deprecated_name.replace('-', '_')
|
|
else:
|
|
self.deprecated_name = None
|
|
|
|
def __ne__(self, another):
|
|
return vars(self) != vars(another)
|
|
|
|
def _get_from_config_parser(self, cparser, section):
|
|
"""Retrieves the option value from a MultiConfigParser 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 self._cparser_get_with_deprecated(cparser, section)
|
|
|
|
def _cparser_get_with_deprecated(self, cparser, section):
|
|
"""If cannot find option as dest try deprecated_name alias."""
|
|
if self.deprecated_name is not None:
|
|
return cparser.get(section, [self.dest, self.deprecated_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_argparse_container(parser, group)
|
|
kwargs = self._get_argparse_kwargs(group)
|
|
prefix = self._get_argparse_prefix('', group)
|
|
self._add_to_argparse(container, self.name, self.short, kwargs, prefix,
|
|
self.positional, self.deprecated_name)
|
|
|
|
def _add_to_argparse(self, container, name, short, kwargs, prefix='',
|
|
positional=False, deprecated_name=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 position: whether the optional is a positional CLI argument
|
|
:raises: DuplicateOptError if a naming confict is detected
|
|
"""
|
|
def hyphen(arg):
|
|
return arg if not positional else ''
|
|
|
|
args = [hyphen('--') + prefix + name]
|
|
if short:
|
|
args.append(hyphen('-') + short)
|
|
if deprecated_name:
|
|
args.append(hyphen('--') + prefix + deprecated_name)
|
|
|
|
try:
|
|
container.add_argument(*args, **kwargs)
|
|
except argparse.ArgumentError as e:
|
|
raise DuplicateOptError(e)
|
|
|
|
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):
|
|
"""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.
|
|
"""
|
|
|
|
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
|
|
'0': False, 'no': False, 'false': False, 'off': False}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
if 'positional' in kwargs:
|
|
raise ValueError('positional boolean args not supported')
|
|
super(BoolOpt, self).__init__(*args, **kwargs)
|
|
|
|
def _get_from_config_parser(self, cparser, section):
|
|
"""Retrieve the opt value as a boolean from ConfigParser."""
|
|
def convert_bool(v):
|
|
value = self._boolean_states.get(v.lower())
|
|
if value is None:
|
|
raise ValueError('Unexpected boolean value %r' % v)
|
|
|
|
return value
|
|
|
|
return [convert_bool(v) for v in
|
|
self._cparser_get_with_deprecated(cparser, section)]
|
|
|
|
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)
|
|
kwargs["help"] = "The inverse of --" + self.name
|
|
self._add_to_argparse(container, self.name, None, kwargs, prefix,
|
|
self.positional, self.deprecated_name)
|
|
|
|
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)
|
|
|
|
# metavar has no effect for BoolOpt
|
|
if 'metavar' in kwargs:
|
|
del kwargs['metavar']
|
|
|
|
if action != 'store_true':
|
|
action = 'store_false'
|
|
|
|
kwargs['action'] = action
|
|
|
|
return 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 [int(v) for v in self._cparser_get_with_deprecated(cparser,
|
|
section)]
|
|
|
|
def _get_argparse_kwargs(self, group, **kwargs):
|
|
"""Extends the base argparse keyword dict for integer options."""
|
|
return super(IntOpt,
|
|
self)._get_argparse_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 [float(v) for v in
|
|
self._cparser_get_with_deprecated(cparser, section)]
|
|
|
|
def _get_argparse_kwargs(self, group, **kwargs):
|
|
"""Extends the base argparse keyword dict for float options."""
|
|
return super(FloatOpt, self)._get_argparse_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.
|
|
"""
|
|
|
|
class _StoreListAction(argparse.Action):
|
|
"""
|
|
An argparse action for parsing an option value into a list.
|
|
"""
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
if values is not None:
|
|
values = [a.strip() for a in values.split(',')]
|
|
setattr(namespace, self.dest, values)
|
|
|
|
def _get_from_config_parser(self, cparser, section):
|
|
"""Retrieve the opt value as a list from ConfigParser."""
|
|
return [[a.strip() for a in v.split(',')] for v in
|
|
self._cparser_get_with_deprecated(cparser, section)]
|
|
|
|
def _get_argparse_kwargs(self, group, **kwargs):
|
|
"""Extends the base argparse keyword dict for list options."""
|
|
return Opt._get_argparse_kwargs(self,
|
|
group,
|
|
action=ListOpt._StoreListAction,
|
|
**kwargs)
|
|
|
|
|
|
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.
|
|
"""
|
|
multi = True
|
|
|
|
def _get_argparse_kwargs(self, group, **kwargs):
|
|
"""Extends the base argparse keyword dict for multi str options."""
|
|
kwargs = super(MultiStrOpt, self)._get_argparse_kwargs(group)
|
|
if not self.positional:
|
|
kwargs['action'] = 'append'
|
|
else:
|
|
kwargs['nargs'] = '*'
|
|
return kwargs
|
|
|
|
def _cparser_get_with_deprecated(self, cparser, section):
|
|
"""If cannot find option as dest try deprecated_name alias."""
|
|
if self.deprecated_name is not None:
|
|
return cparser.get(section, [self.dest, self.deprecated_name],
|
|
multi=True)
|
|
return cparser.get(section, [self.dest], multi=True)
|
|
|
|
|
|
class SubCommandOpt(Opt):
|
|
|
|
"""
|
|
Sub-command options allow argparse sub-parsers to be used to parse
|
|
additional command line arguments.
|
|
|
|
The handler argument to the SubCommandOpt contructor 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, 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)
|
|
|
|
if self.handler is not None:
|
|
self.handler(subparsers)
|
|
|
|
|
|
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._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.section = None
|
|
|
|
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, {})
|
|
|
|
def assignment(self, key, value):
|
|
if not self.section:
|
|
raise self.error_no_section()
|
|
|
|
self.sections[self.section].setdefault(key, [])
|
|
self.sections[self.section][key].append('\n'.join(value))
|
|
|
|
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)
|
|
|
|
|
|
class MultiConfigParser(object):
|
|
def __init__(self):
|
|
self.parsed = []
|
|
|
|
def read(self, config_files):
|
|
read_ok = []
|
|
|
|
for filename in config_files:
|
|
sections = {}
|
|
parser = ConfigParser(filename, sections)
|
|
|
|
try:
|
|
parser.parse()
|
|
except IOError:
|
|
continue
|
|
self.parsed.insert(0, sections)
|
|
read_ok.append(filename)
|
|
|
|
return read_ok
|
|
|
|
def get(self, section, names, multi=False):
|
|
rvalue = []
|
|
for sections in self.parsed:
|
|
if section not in sections:
|
|
continue
|
|
for name in names:
|
|
if name in sections[section]:
|
|
if multi:
|
|
rvalue = sections[section][name] + rvalue
|
|
else:
|
|
return sections[section][name]
|
|
if multi and rvalue != []:
|
|
return rvalue
|
|
raise KeyError
|
|
|
|
|
|
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._cparser = None
|
|
self._cli_values = {}
|
|
self.__cache = {}
|
|
self._config_opts = []
|
|
|
|
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 = argparse.ArgumentParser(prog=prog, usage=usage)
|
|
self._oparser.add_argument('--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 = [
|
|
MultiStrOpt('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: %s' % (default_config_files, )),
|
|
StrOpt('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), if any, specified via --config-file, '
|
|
'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):
|
|
self.__cache.clear()
|
|
return f(self, *args, **kwargs)
|
|
|
|
return __inner
|
|
|
|
def __call__(self,
|
|
args=None,
|
|
project=None,
|
|
prog=None,
|
|
version=None,
|
|
usage=None,
|
|
default_config_files=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.
|
|
|
|
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
|
|
:returns: the list of arguments left over after parsing options
|
|
:raises: SystemExit, ConfigFilesNotFoundError, ConfigFileParseError,
|
|
RequiredOptError, DuplicateOptError
|
|
"""
|
|
|
|
self.clear()
|
|
|
|
prog, default_config_files = self._pre_setup(project,
|
|
prog,
|
|
version,
|
|
usage,
|
|
default_config_files)
|
|
|
|
self._setup(project, prog, version, usage, default_config_files)
|
|
|
|
self._cli_values = self._parse_cli_opts(args)
|
|
|
|
self._parse_config_files()
|
|
|
|
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 subsititution) or a GroupAttr
|
|
:raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError
|
|
"""
|
|
return self._get(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 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._cli_values.clear()
|
|
self._oparser = argparse.ArgumentParser()
|
|
self._cparser = None
|
|
self.unregister_opts(self._config_opts)
|
|
for group in self._groups.values():
|
|
group._clear()
|
|
|
|
@__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 register, True otherwise
|
|
:raises: DuplicateOptError
|
|
"""
|
|
if group is not None:
|
|
group = self._get_group(group, autocreate=True)
|
|
return group._register_opt(opt, cli)
|
|
|
|
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
|
|
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 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 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 info, group in self._all_opt_infos():
|
|
if info['cli']:
|
|
yield info['opt'], 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 basename: the filename, e.g. '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 (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)
|
|
|
|
def _sanitize(opt, value):
|
|
"""Obfuscate values of options declared secret"""
|
|
return value if not opt.secret else '*' * len(str(value))
|
|
|
|
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."""
|
|
self._oparser.print_usage(file)
|
|
|
|
def print_help(self, file=None):
|
|
"""Print the help message for the current program."""
|
|
self._oparser.print_help(file)
|
|
|
|
def _get(self, name, group=None):
|
|
if isinstance(group, OptGroup):
|
|
key = (group.name, name)
|
|
else:
|
|
key = (group, name)
|
|
try:
|
|
return self.__cache[key]
|
|
except KeyError:
|
|
value = self._substitute(self._do_get(name, group))
|
|
self.__cache[key] = value
|
|
return value
|
|
|
|
def _do_get(self, name, group=None):
|
|
"""Look up an option value.
|
|
|
|
:param name: the opt name (or 'dest', more precisely)
|
|
:param group: an 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, 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 info['override']
|
|
|
|
values = []
|
|
if self._cparser is not None:
|
|
section = group.name if group is not None else 'DEFAULT'
|
|
try:
|
|
value = opt._get_from_config_parser(self._cparser, section)
|
|
except KeyError:
|
|
pass
|
|
except ValueError as ve:
|
|
raise ConfigFileValueError(str(ve))
|
|
else:
|
|
if not opt.multi:
|
|
# No need to continue since the last value wins
|
|
return value[-1]
|
|
values.extend(value)
|
|
|
|
name = name if group is None else group.name + '_' + name
|
|
value = self._cli_values.get(name)
|
|
if value is not None:
|
|
if not opt.multi:
|
|
return value
|
|
|
|
# argparse ignores default=None for nargs='*'
|
|
if opt.positional and not value:
|
|
value = opt.default
|
|
|
|
return value + values
|
|
|
|
if values:
|
|
return values
|
|
|
|
if 'default' in info:
|
|
return info['default']
|
|
|
|
return opt.default
|
|
|
|
def _substitute(self, value):
|
|
"""Perform string template substitution.
|
|
|
|
Substitute 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 isinstance(value, list):
|
|
return [self._substitute(i) for i in value]
|
|
elif isinstance(value, str):
|
|
tmpl = string.Template(value)
|
|
return tmpl.safe_substitute(self.StrSubWrapper(self))
|
|
else:
|
|
return 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.
|
|
|
|
: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 group is not None or not autocreate:
|
|
raise NoSuchGroupError(group_name)
|
|
|
|
self.register_group(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 _parse_config_files(self):
|
|
"""Parse the config files from --config-file and --config-dir.
|
|
|
|
:raises: ConfigFilesNotFoundError, ConfigFileParseError
|
|
"""
|
|
config_files = list(self.config_file)
|
|
|
|
if self.config_dir:
|
|
config_dir_glob = os.path.join(self.config_dir, '*.conf')
|
|
config_files += sorted(glob.glob(config_dir_glob))
|
|
|
|
config_files = [_fixpath(p) for p in config_files]
|
|
|
|
self._cparser = MultiConfigParser()
|
|
|
|
try:
|
|
read_ok = self._cparser.read(config_files)
|
|
except iniparser.ParseError as pe:
|
|
raise ConfigFileParseError(pe.filename, str(pe))
|
|
|
|
if read_ok != config_files:
|
|
not_read_ok = filter(lambda f: f not in read_ok, config_files)
|
|
raise ConfigFilesNotFoundError(not_read_ok)
|
|
|
|
def _check_required_opts(self):
|
|
"""Check that all opts marked as required have values specified.
|
|
|
|
: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) 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 dict of parsed option values
|
|
:raises: SystemExit, DuplicateOptError
|
|
|
|
"""
|
|
self._args = args
|
|
|
|
for opt, group in self._all_cli_opts():
|
|
opt._add_to_cli(self._oparser, group)
|
|
|
|
return vars(self._oparser.parse_args(args))
|
|
|
|
class GroupAttr(collections.Mapping):
|
|
|
|
"""
|
|
A helper class representing 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):
|
|
|
|
"""
|
|
A helper class representing 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 self._conf._cli_values[name]
|
|
|
|
if name in self._conf:
|
|
raise DuplicateOptError(name)
|
|
|
|
try:
|
|
return self._conf._cli_values[name]
|
|
except KeyError:
|
|
raise NoSuchOptError(name)
|
|
|
|
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
|
|
|
|
|
|
CONF = ConfigOpts()
|