track the location where configuration options are set

Add a get_location() method to ConfigOpts to ask where the option
value was set. Update _do_get() to return this information based on
the search criteria.

The LocationInfo data structure has 2 fields. We are only using the
location for now, but the detail field will be filled in by changes
later in this series.

Change-Id: I3643c49b3de1850139913ce395199c238dbe6cf0
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2018-01-23 16:18:27 -05:00
parent c18ac34f5b
commit a9625c78d3
3 changed files with 132 additions and 9 deletions

View File

@ -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.

View File

@ -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,
)

View File

@ -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