diff options
| author | Mark McLoughlin <markmc@redhat.com> | 2013-02-03 21:17:02 +0000 |
|---|---|---|
| committer | Mark McLoughlin <markmc@redhat.com> | 2013-02-18 16:28:21 +0000 |
| commit | bcba9e708e2067caca316a8499aa659f9802eeec (patch) | |
| tree | a7e69a8dd15fb5a3f60aed1688db4c22468abc32 | |
| parent | 538721d3fdab613ed3d3ab33df123f90da173e93 (diff) | |
| download | oslo-bcba9e708e2067caca316a8499aa659f9802eeec.tar.gz oslo-bcba9e708e2067caca316a8499aa659f9802eeec.tar.xz oslo-bcba9e708e2067caca316a8499aa659f9802eeec.zip | |
Add ConfigFilter wrapper class
Implements blueprint cfg-filter-view
At the moment, if a module requires a configuration option from another
module, we do:
CONF.import_opt('source.module', 'option_name')
but, in fact, all options from the imported module are available for
use.
The new ConfigFilter class makes it possible to enforce which options
are available within a module e.g. with
CONF = cfgfilter.ConfigFilter(cfg.CONF)
CONF.import_opt('foo', 'source.module')
CONF.register_opt(StrOpt('bar'))
then the foo and bar options would be the only options available via
this CONF object while still being available via the global cfg.CONF
object.
Change-Id: Ie3aa2cd090a626da8afd27ecb78853cbf279bc8b
| -rw-r--r-- | openstack/common/cfgfilter.py | 241 | ||||
| -rw-r--r-- | tests/testmods/baar_baa_opt.py | 21 | ||||
| -rw-r--r-- | tests/testmods/fbar_foo_opt.py | 21 | ||||
| -rw-r--r-- | tests/testmods/fblaa_opt.py | 21 | ||||
| -rw-r--r-- | tests/unit/test_cfgfilter.py | 119 |
5 files changed, 423 insertions, 0 deletions
diff --git a/openstack/common/cfgfilter.py b/openstack/common/cfgfilter.py new file mode 100644 index 0000000..f7b978b --- /dev/null +++ b/openstack/common/cfgfilter.py @@ -0,0 +1,241 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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""" +When using the global cfg.CONF object, it is quite common for a module +to require the existence of configuration options registered by other +modules. + +For example, if module 'foo' registers the 'blaa' option and the module +'bar' uses the 'blaa' option then 'bar' might do: + + import foo + + print CONF.blaa + +However, it's completely non-obvious why foo is being imported (is it +unused, can we remove the import) and where the 'blaa' option comes from. + +The CONF.import_opt() method allows such a dependency to be explicitly +declared: + + CONF.import_opt('blaa', 'foo') + print CONF.blaa + +However, import_opt() has a weakness - if 'bar' imports 'foo' using the +import builtin and doesn't use import_opt() to import 'blaa', then 'blaa' +can still be used without problems. Similarily, where multiple options +are registered a module imported via importopt(), a lazy programmer can +get away with only declaring a dependency on a single option. + +The ConfigFilter class provides a way to ensure that options are not +available unless they have been registered in the module or imported using +import_opt() e.g. with: + + CONF = ConfigFilter(cfg.CONF) + CONF.import_opt('blaa', 'foo') + print CONF.blaa + +no other options other than 'blaa' are available via CONF. +""" + +import collections +import itertools + +from oslo.config import cfg + + +class ConfigFilter(collections.Mapping): + + """ + A helper class which wraps a ConfigOpts object and enforces the + explicit declaration of dependencies on external options. + """ + + def __init__(self, conf): + """Construct a ConfigFilter object. + + :param conf: a ConfigOpts object + """ + self._conf = conf + self._opts = set() + self._groups = dict() + + def __getattr__(self, name): + """Look up an option value. + + :param name: the opt name (or 'dest', more precisely) + :returns: the option value (after string subsititution) or a GroupAttr + :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError + """ + if name in self._groups: + return self._groups[name] + if name not in self._opts: + raise cfg.NoSuchOptError(name) + return getattr(self._conf, name) + + def __getitem__(self, key): + """Look up an option value.""" + return getattr(self, key) + + def __contains__(self, key): + """Return True if key is the name of a registered opt or group.""" + return key in self._opts or key in self._groups + + def __iter__(self): + """Iterate over all registered opt and group names.""" + return itertools.chain(self._opts, self._groups.keys()) + + def __len__(self): + """Return the number of options and option groups.""" + return len(self._opts) + len(self._groups) + + def register_opt(self, opt, group=None): + """Register an option schema. + + :param opt: an instance of an Opt sub-class + :param group: an optional OptGroup object or group name + :return: False if the opt was already registered, True otherwise + :raises: DuplicateOptError + """ + if not self._conf.register_opt(opt, group): + return False + + self._register_opt(opt.dest, group) + 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. + + :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 not self._conf.register_cli_opt(opt, group): + return False + + self._register_opt(opt.dest, 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_opts(opt, group) + + def register_group(self, group): + """Register an option group. + + :param group: an OptGroup object + """ + self._conf.register_group(group) + self._get_group(group.name) + + def import_opt(self, opt_name, module_str, group=None): + """Import an option definition from a module. + + :param name: the name/dest of the opt + :param module_str: the name of a module to import + :param group: an option OptGroup object or group name + :raises: NoSuchOptError, NoSuchGroupError + """ + self._conf.import_opt(opt_name, module_str, group) + self._register_opt(opt_name, group) + + def import_group(self, group, module_str): + """Import an option group from a module. + + Note that this allows access to all options registered with + the group whether or not those options were registered by + the given module. + + :param group: an option OptGroup object or group name + :param module_str: the name of a module to import + :raises: ImportError, NoSuchGroupError + """ + self._conf.import_group(group, module_str) + group = self._get_group(group) + group._all_opts = True + + def _register_opt(self, opt_name, group): + if group is None: + self._opts.add(opt_name) + return True + else: + group = self._get_group(group) + return group._register_opt(opt_name) + + def _get_group(self, group_or_name): + if isinstance(group_or_name, cfg.OptGroup): + group_name = group_or_name.name + else: + group_name = group_or_name + + if group_name in self._groups: + return self._groups[group_name] + else: + group = self.GroupAttr(self._conf, group_name) + self._groups[group_name] = group + return group + + 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 + self._opts = set() + self._all_opts = False + + def __getattr__(self, name): + """Look up an option value.""" + if not self._all_opts and name not in self._opts: + raise cfg.NoSuchOptError(name) + return getattr(self._conf[self._group], name) + + def __getitem__(self, key): + """Look up an option value.""" + return getattr(self, key) + + def __contains__(self, key): + """Return True if key is the name of a registered opt or group.""" + return key in self._opts + + def __iter__(self): + """Iterate over all registered opt and group names.""" + for key in self._opts: + yield key + + def __len__(self): + """Return the number of options and option groups.""" + return len(self._opts) + + def _register_opt(self, opt_name): + self._opts.add(opt_name) diff --git a/tests/testmods/baar_baa_opt.py b/tests/testmods/baar_baa_opt.py new file mode 100644 index 0000000..bd0cef1 --- /dev/null +++ b/tests/testmods/baar_baa_opt.py @@ -0,0 +1,21 @@ +# 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. + +from oslo.config import cfg + +CONF = cfg.CONF + +CONF.register_opt(cfg.StrOpt('baa'), group='baar') diff --git a/tests/testmods/fbar_foo_opt.py b/tests/testmods/fbar_foo_opt.py new file mode 100644 index 0000000..fb206c5 --- /dev/null +++ b/tests/testmods/fbar_foo_opt.py @@ -0,0 +1,21 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg + +CONF = cfg.CONF + +CONF.register_opt(cfg.StrOpt('foo'), group='bbar') diff --git a/tests/testmods/fblaa_opt.py b/tests/testmods/fblaa_opt.py new file mode 100644 index 0000000..0c886af --- /dev/null +++ b/tests/testmods/fblaa_opt.py @@ -0,0 +1,21 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config import cfg + +CONF = cfg.CONF + +CONF.register_opt(cfg.StrOpt('fblaa')) diff --git a/tests/unit/test_cfgfilter.py b/tests/unit/test_cfgfilter.py new file mode 100644 index 0000000..e78acc8 --- /dev/null +++ b/tests/unit/test_cfgfilter.py @@ -0,0 +1,119 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo.config.cfg import * + +from openstack.common.cfgfilter import * +from tests import utils + + +class ConfigFilterTestCase(utils.BaseTestCase): + + def setUp(self): + super(ConfigFilterTestCase, self).setUp() + self.conf = ConfigOpts() + self.fconf = ConfigFilter(self.conf) + + def test_register_opt_default(self): + self.fconf.register_opt(StrOpt('foo', default='bar')) + + self.assertEquals(self.fconf.foo, 'bar') + self.assertEquals(self.fconf['foo'], 'bar') + self.assertTrue('foo' in self.fconf) + self.assertEquals(list(self.fconf), ['foo']) + self.assertEquals(len(self.fconf), 1) + + def test_register_opt_none_default(self): + self.fconf.register_opt(StrOpt('foo')) + + self.assertTrue(self.fconf.foo is None) + self.assertTrue(self.fconf['foo'] is None) + self.assertTrue('foo' in self.fconf) + self.assertEquals(list(self.fconf), ['foo']) + self.assertEquals(len(self.fconf), 1) + + def test_register_grouped_opt_default(self): + self.fconf.register_opt(StrOpt('foo', default='bar'), group='blaa') + + self.assertEquals(self.fconf.blaa.foo, 'bar') + self.assertEquals(self.fconf['blaa']['foo'], 'bar') + self.assertTrue('blaa' in self.fconf) + self.assertTrue('foo' in self.fconf.blaa) + self.assertEquals(list(self.fconf), ['blaa']) + self.assertEquals(list(self.fconf.blaa), ['foo']) + self.assertEquals(len(self.fconf), 1) + self.assertEquals(len(self.fconf.blaa), 1) + + def test_register_grouped_opt_none_default(self): + self.fconf.register_opt(StrOpt('foo'), group='blaa') + + self.assertTrue(self.fconf.blaa.foo is None) + self.assertTrue(self.fconf['blaa']['foo'] is None) + self.assertTrue('blaa' in self.fconf) + self.assertTrue('foo' in self.fconf.blaa) + self.assertEquals(list(self.fconf), ['blaa']) + self.assertEquals(list(self.fconf.blaa), ['foo']) + self.assertEquals(len(self.fconf), 1) + self.assertEquals(len(self.fconf.blaa), 1) + + def test_register_group(self): + group = OptGroup('blaa') + self.fconf.register_group(group) + self.fconf.register_opt(StrOpt('foo'), group=group) + + self.assertTrue(self.fconf.blaa.foo is None) + self.assertTrue(self.fconf['blaa']['foo'] is None) + self.assertTrue('blaa' in self.fconf) + self.assertTrue('foo' in self.fconf.blaa) + self.assertEquals(list(self.fconf), ['blaa']) + self.assertEquals(list(self.fconf.blaa), ['foo']) + self.assertEquals(len(self.fconf), 1) + self.assertEquals(len(self.fconf.blaa), 1) + + def test_unknown_opt(self): + self.assertFalse('foo' in self.fconf) + self.assertEquals(len(self.fconf), 0) + self.assertRaises(NoSuchOptError, getattr, self.fconf, 'foo') + + def test_blocked_opt(self): + self.conf.register_opt(StrOpt('foo')) + + self.assertTrue('foo' in self.conf) + self.assertEquals(len(self.conf), 1) + self.assertTrue(self.conf.foo is None) + self.assertFalse('foo' in self.fconf) + self.assertEquals(len(self.fconf), 0) + self.assertRaises(NoSuchOptError, getattr, self.fconf, 'foo') + + def test_import_opt(self): + self.fconf = ConfigFilter(CONF) + self.assertFalse(hasattr(self.fconf, 'blaa')) + self.fconf.import_opt('blaa', 'tests.testmods.blaa_opt') + self.assertTrue(hasattr(self.fconf, 'blaa')) + + def test_import_opt_in_group(self): + self.fconf = ConfigFilter(CONF) + self.assertFalse(hasattr(self.fconf, 'bar')) + self.fconf.import_opt('foo', 'tests.testmods.bar_foo_opt', group='bar') + self.assertTrue(hasattr(self.fconf, 'bar')) + self.assertTrue(hasattr(self.fconf.bar, 'foo')) + + def test_import_group(self): + self.fconf = ConfigFilter(CONF) + self.assertFalse(hasattr(self.fconf, 'baar')) + self.fconf.import_group('baar', 'tests.testmods.baar_baa_opt') + self.assertTrue(hasattr(self.fconf, 'baar')) + self.assertTrue(hasattr(self.fconf.baar, 'baa')) |
