From c27cbd8dd8d0bb57428a3f6bc5253a3e167c870a Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 8 Jun 2016 12:37:10 +1000 Subject: [PATCH] Provide a normal method for deprecation warnings. The deprecated decorator is very useful in situations where you have a complete function that you want to deprecate. However it doesn't let me simply trigger a deprecation warning in a code path that i know is deprecated but doesn't exist in a function. Create a standard deprecated warning function that has almost the same signature (what becomes required) and produces the same warning and fatal_deprecations behaviour for situations where a deprecator is not useful. For example: if CONF.deprecated_value: versionutils.deprecated_warning('deprecated_value', as_of=versionutils.OCATA) # do stuff Change-Id: Ifd83cd74ac397e7bc6def11a38b46de5dff2acfa Closes-Bug: #1590223 --- oslo_log/tests/unit/test_versionutils.py | 23 +++- oslo_log/versionutils.py | 151 +++++++++++++---------- 2 files changed, 102 insertions(+), 72 deletions(-) diff --git a/oslo_log/tests/unit/test_versionutils.py b/oslo_log/tests/unit/test_versionutils.py index ea46f7bd..f3a65f96 100644 --- a/oslo_log/tests/unit/test_versionutils.py +++ b/oslo_log/tests/unit/test_versionutils.py @@ -25,20 +25,19 @@ from oslo_log import versionutils class DeprecatedTestCase(test_base.BaseTestCase): def assert_deprecated(self, mock_reporter, no_removal=False, **expected_details): - decorator = versionutils.deprecated if 'in_favor_of' in expected_details: if no_removal is False: - expected_msg = decorator._deprecated_msg_with_alternative + expected_msg = versionutils._deprecated_msg_with_alternative else: expected_msg = getattr( - decorator, + versionutils, '_deprecated_msg_with_alternative_no_removal') else: if no_removal is False: - expected_msg = decorator._deprecated_msg_no_alternative + expected_msg = versionutils._deprecated_msg_no_alternative else: expected_msg = getattr( - decorator, + versionutils, '_deprecated_msg_with_no_alternative_no_removal') # The first argument is the logger, and we don't care about # that, so ignore it with ANY. @@ -356,3 +355,17 @@ class DeprecatedTestCase(test_base.BaseTestCase): what='OutdatedClass()', as_of='Newton', remove_in='P') + + @mock.patch('oslo_log.versionutils.report_deprecated_feature') + def test_deprecated_message(self, mock_reporter): + + versionutils.deprecation_warning('outdated_stuff', + as_of=versionutils.deprecated.KILO, + in_favor_of='different_stuff', + remove_in=+2) + + self.assert_deprecated(mock_reporter, + what='outdated_stuff', + in_favor_of='different_stuff', + as_of='Kilo', + remove_in='Mitaka') diff --git a/oslo_log/versionutils.py b/oslo_log/versionutils.py index f8526f32..bc1e17b2 100644 --- a/oslo_log/versionutils.py +++ b/oslo_log/versionutils.py @@ -39,6 +39,38 @@ deprecated_opts = [ ] +_deprecated_msg_with_alternative = _( + '%(what)s is deprecated as of %(as_of)s in favor of ' + '%(in_favor_of)s and may be removed in %(remove_in)s.') + +_deprecated_msg_no_alternative = _( + '%(what)s is deprecated as of %(as_of)s and may be ' + 'removed in %(remove_in)s. It will not be superseded.') + +_deprecated_msg_with_alternative_no_removal = _( + '%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s.') + +_deprecated_msg_with_no_alternative_no_removal = _( + '%(what)s is deprecated as of %(as_of)s. It will not be superseded.') + + +_RELEASES = { + # NOTE(morganfainberg): Bexar is used for unit test purposes, it is + # expected we maintain a gap between Bexar and Folsom in this list. + 'B': 'Bexar', + 'F': 'Folsom', + 'G': 'Grizzly', + 'H': 'Havana', + 'I': 'Icehouse', + 'J': 'Juno', + 'K': 'Kilo', + 'L': 'Liberty', + 'M': 'Mitaka', + 'N': 'Newton', + 'O': 'Ocata', +} + + def register_options(): """Register configuration options used by this library. @@ -109,36 +141,6 @@ class deprecated(object): NEWTON = 'N' OCATA = 'O' - _RELEASES = { - # NOTE(morganfainberg): Bexar is used for unit test purposes, it is - # expected we maintain a gap between Bexar and Folsom in this list. - 'B': 'Bexar', - 'F': 'Folsom', - 'G': 'Grizzly', - 'H': 'Havana', - 'I': 'Icehouse', - 'J': 'Juno', - 'K': 'Kilo', - 'L': 'Liberty', - 'M': 'Mitaka', - 'N': 'Newton', - 'O': 'Ocata', - } - - _deprecated_msg_with_alternative = _( - '%(what)s is deprecated as of %(as_of)s in favor of ' - '%(in_favor_of)s and may be removed in %(remove_in)s.') - - _deprecated_msg_no_alternative = _( - '%(what)s is deprecated as of %(as_of)s and may be ' - 'removed in %(remove_in)s. It will not be superseded.') - - _deprecated_msg_with_alternative_no_removal = _( - '%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s.') - - _deprecated_msg_with_no_alternative_no_removal = _( - '%(what)s is deprecated as of %(as_of)s. It will not be superseded.') - def __init__(self, as_of, in_favor_of=None, remove_in=2, what=None): """Initialize decorator @@ -157,15 +159,18 @@ class deprecated(object): self.what = what def __call__(self, func_or_cls): - if not self.what: - self.what = func_or_cls.__name__ + '()' - msg, details = self._build_message() + report_deprecated = functools.partial( + deprecation_warning, + what=self.what or func_or_cls.__name__ + '()', + as_of=self.as_of, + in_favor_of=self.in_favor_of, + remove_in=self.remove_in) if inspect.isfunction(func_or_cls): @six.wraps(func_or_cls) def wrapped(*args, **kwargs): - report_deprecated_feature(LOG, msg, details) + report_deprecated() return func_or_cls(*args, **kwargs) return wrapped elif inspect.isclass(func_or_cls): @@ -178,7 +183,7 @@ class deprecated(object): @functools.wraps(orig_init, assigned=('__name__', '__doc__')) def new_init(self, *args, **kwargs): if self.__class__ in _DEPRECATED_EXCEPTIONS: - report_deprecated_feature(LOG, msg, details) + report_deprecated() orig_init(self, *args, **kwargs) func_or_cls.__init__ = new_init _DEPRECATED_EXCEPTIONS.add(func_or_cls) @@ -197,7 +202,7 @@ class deprecated(object): class ExceptionMeta(type): def __subclasscheck__(self, subclass): if self in _DEPRECATED_EXCEPTIONS: - report_deprecated_feature(LOG, msg, details) + report_deprecated() return super(ExceptionMeta, self).__subclasscheck__(subclass) func_or_cls = six.add_metaclass(ExceptionMeta)(func_or_cls) @@ -208,40 +213,52 @@ class deprecated(object): raise TypeError('deprecated can be used only with functions or ' 'classes') - def _get_safe_to_remove_release(self, release): - # TODO(dstanek): this method will have to be reimplemented once - # when we get to the X release because once we get to the Y - # release, what is Y+2? - remove_in = self.remove_in - if remove_in is None: - remove_in = 0 - new_release = chr(ord(release) + remove_in) - if new_release in self._RELEASES: - return self._RELEASES[new_release] - else: - return new_release - def _build_message(self): - details = dict(what=self.what, - as_of=self._RELEASES[self.as_of], - remove_in=self._get_safe_to_remove_release(self.as_of)) +def _get_safe_to_remove_release(release, remove_in): + # TODO(dstanek): this method will have to be reimplemented once + # when we get to the X release because once we get to the Y + # release, what is Y+2? + if remove_in is None: + remove_in = 0 + new_release = chr(ord(release) + remove_in) + if new_release in _RELEASES: + return _RELEASES[new_release] + else: + return new_release - if self.in_favor_of: - details['in_favor_of'] = self.in_favor_of - if self.remove_in is not None and self.remove_in > 0: - msg = self._deprecated_msg_with_alternative - else: - # There are no plans to remove this function, but it is - # now deprecated. - msg = self._deprecated_msg_with_alternative_no_removal + +def deprecation_warning(what, as_of, in_favor_of=None, + remove_in=2, logger=LOG): + """Warn about the deprecation of a feature. + + :param what: name of the thing being deprecated. + :param as_of: the release deprecating the callable. + :param in_favor_of: the replacement for the callable (optional) + :param remove_in: an integer specifying how many releases to wait + before removing (default: 2) + :param logger: the logging object to use for reporting (optional). + """ + details = dict(what=what, + as_of=_RELEASES[as_of], + remove_in=_get_safe_to_remove_release(as_of, remove_in)) + + if in_favor_of: + details['in_favor_of'] = in_favor_of + if remove_in is not None and remove_in > 0: + msg = _deprecated_msg_with_alternative else: - if self.remove_in is not None and self.remove_in > 0: - msg = self._deprecated_msg_no_alternative - else: - # There are no plans to remove this function, but it is - # now deprecated. - msg = self._deprecated_msg_with_no_alternative_no_removal - return msg, details + # There are no plans to remove this function, but it is + # now deprecated. + msg = _deprecated_msg_with_alternative_no_removal + else: + if remove_in is not None and remove_in > 0: + msg = _deprecated_msg_no_alternative + else: + # There are no plans to remove this function, but it is + # now deprecated. + msg = _deprecated_msg_with_no_alternative_no_removal + + report_deprecated_feature(logger, msg, details) # Track the messages we have sent already. See