diff --git a/oslo_config/cfg.py b/oslo_config/cfg.py index 5bd81bff..81545fa2 100644 --- a/oslo_config/cfg.py +++ b/oslo_config/cfg.py @@ -496,6 +496,7 @@ import string import sys from debtcollector import removals +import enum import six @@ -505,6 +506,20 @@ from oslo_config import types LOG = logging.getLogger(__name__) +class Locations(enum.Enum): + opt_default = (1, False) + set_default = (2, False) + set_override = (3, False) + user = (4, True) + + def __init__(self, num, is_user_controlled): + self.num = num + self.is_user_controlled = is_user_controlled + + +LocationInfo = collections.namedtuple('LocationInfo', ['location', 'detail']) + + class Error(Exception): """Base class for cfg exceptions.""" @@ -2937,7 +2952,7 @@ class ConfigOpts(collections.Mapping): return self.__cache[key] except KeyError: # nosec: Valid control flow instruction pass - value = self._do_get(name, group, namespace) + value, loc = self._do_get(name, group, namespace) self.__cache[key] = value return value @@ -2947,21 +2962,23 @@ class ConfigOpts(collections.Mapping): :param name: the opt name (or 'dest', more precisely) :param group: an OptGroup :param namespace: the namespace object to get the option value from - :returns: the option value, or a GroupAttr object + :returns: 2-tuple of the option value or a GroupAttr object + and LocationInfo or None :raises: NoSuchOptError, NoSuchGroupError, ConfigFileValueError, TemplateSubstitutionError """ if group is None and name in self._groups: - return self.GroupAttr(self, self._get_group(name)) + return (self.GroupAttr(self, self._get_group(name)), None) info = self._get_opt_info(name, group) opt = info['opt'] if isinstance(opt, SubCommandOpt): - return self.SubCommandAttr(self, group, opt.dest) + return (self.SubCommandAttr(self, group, opt.dest), None) if 'override' in info: - return self._substitute(info['override']) + return (self._substitute(info['override']), + LocationInfo(Locations.set_override, '')) def convert(value): return self._convert_value( @@ -2974,7 +2991,10 @@ class ConfigOpts(collections.Mapping): if namespace is not None: group_name = group.name if group else None try: - return convert(opt._get_from_namespace(namespace, group_name)) + return ( + convert(opt._get_from_namespace(namespace, group_name)), + LocationInfo(Locations.user, ''), + ) except KeyError: # nosec: Valid control flow instruction pass except ValueError as ve: @@ -2983,7 +3003,8 @@ class ConfigOpts(collections.Mapping): % (opt.name, str(ve))) if 'default' in info: - return self._substitute(info['default']) + return (self._substitute(info['default']), + LocationInfo(Locations.set_default, '')) if self._validate_default_values: if opt.default is not None: @@ -2995,9 +3016,10 @@ class ConfigOpts(collections.Mapping): % (opt.name, str(e))) if opt.default is not None: - return convert(opt.default) + return (convert(opt.default), + LocationInfo(Locations.opt_default, '')) - return None + return (None, None) def _substitute(self, value, group=None, namespace=None): """Perform string template substitution. @@ -3357,6 +3379,21 @@ class ConfigOpts(collections.Mapping): s |= set(self._namespace._sections()) return sorted(s) + def get_location(self, name, group=None): + """Return the location where the option is being set. + + :param name: The name of the option. + :type name: str + :param group: The name of the group of the option. Defaults to + ``'DEFAULT'``. + :type group: str + :return: LocationInfo + + .. versionadded:: 5.3.0 + """ + value, loc = self._do_get(name, group, None) + return loc + class GroupAttr(collections.Mapping): """Helper class. diff --git a/oslo_config/tests/test_get_location.py b/oslo_config/tests/test_get_location.py new file mode 100644 index 00000000..77e6682b --- /dev/null +++ b/oslo_config/tests/test_get_location.py @@ -0,0 +1,85 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslotest import base + +from oslo_config import cfg + + +class TestConfigOpts(cfg.ConfigOpts): + def __call__(self, args=None, default_config_files=[], + default_config_dirs=[]): + return cfg.ConfigOpts.__call__( + self, + args=args, + prog='test', + version='1.0', + usage='%(prog)s FOO BAR', + description='somedesc', + epilog='tepilog', + default_config_files=default_config_files, + default_config_dirs=default_config_dirs, + validate_default_values=True) + + +class LocationTestCase(base.BaseTestCase): + + def test_user_controlled(self): + self.assertTrue(cfg.Locations.user.is_user_controlled) + + def test_not_user_controlled(self): + self.assertFalse(cfg.Locations.opt_default.is_user_controlled) + self.assertFalse(cfg.Locations.set_default.is_user_controlled) + self.assertFalse(cfg.Locations.set_override.is_user_controlled) + + +class GetLocationTestCase(base.BaseTestCase): + + def setUp(self): + super(GetLocationTestCase, self).setUp() + self.conf = TestConfigOpts() + self.normal_opt = cfg.StrOpt( + 'normal_opt', + default='normal_opt_default', + ) + self.conf.register_opt(self.normal_opt) + self.cli_opt = cfg.StrOpt( + 'cli_opt', + default='cli_opt_default', + ) + self.conf.register_cli_opt(self.cli_opt) + + def test_opt_default(self): + self.conf([]) + loc = self.conf.get_location('normal_opt') + self.assertEqual( + cfg.Locations.opt_default, + loc.location, + ) + + def test_set_default_on_config_opt(self): + self.conf.set_default('normal_opt', self.id()) + self.conf([]) + loc = self.conf.get_location('normal_opt') + self.assertEqual( + cfg.Locations.set_default, + loc.location, + ) + + def test_set_override(self): + self.conf.set_override('normal_opt', self.id()) + self.conf([]) + loc = self.conf.get_location('normal_opt') + self.assertEqual( + cfg.Locations.set_override, + loc.location, + ) diff --git a/requirements.txt b/requirements.txt index 9a2bb87a..83158ed2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ stevedore>=1.20.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 rfc3986>=0.3.1 # Apache-2.0 PyYAML>=3.10 # MIT +enum34>=1.0.4;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD