From f0f1fce2a2e47078660f8d17fa05783756f69032 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 26 Jun 2015 20:43:32 -0700 Subject: [PATCH] Add a moved function helper This is useful for moving functions to newer names or newer modules/locations and retaining the old function so that the old function proxies to the new function and emits a deprecation warning when it is called. Also fixes how the 'get_callable_name' needs to filter for none, as it appears that when ran under sphinx the '__module__' attribute is set to none and this causes issues when joining into a string. Change-Id: I290f1b71c141c6647da1750aec348ea590a8d9bd --- debtcollector/_utils.py | 8 ++++- debtcollector/moves.py | 42 +++++++++++++++++++++---- debtcollector/tests/test_deprecation.py | 23 ++++++++++++++ doc/source/examples.rst | 26 +++++++++++++++ 4 files changed, 92 insertions(+), 7 deletions(-) diff --git a/debtcollector/_utils.py b/debtcollector/_utils.py index 09334a1..0086970 100644 --- a/debtcollector/_utils.py +++ b/debtcollector/_utils.py @@ -164,4 +164,10 @@ def get_callable_name(function): parts = (im_class.__module__, im_class.__qualname__) except AttributeError: parts = (im_class.__module__, im_class.__name__) - return '.'.join(parts) + # When running under sphinx it appears this can be none? if so just + # don't include it... + mod, rest = (parts[0], parts[1:]) + if not mod: + return '.'.join(rest) + else: + return '.'.join(parts) diff --git a/debtcollector/moves.py b/debtcollector/moves.py index 8899c09..d0dd532 100644 --- a/debtcollector/moves.py +++ b/debtcollector/moves.py @@ -22,7 +22,8 @@ from debtcollector import _utils _KIND_MOVED_PREFIX_TPL = "%s '%s' has moved to '%s'" _CLASS_MOVED_PREFIX_TPL = "Class '%s' has moved to '%s'" -_MOVED_METHOD_POSTFIX = "()" +_MOVED_CALLABLE_POSTFIX = "()" +_FUNC_MOVED_PREFIX_TPL = "Function '%s' has moved to '%s'" def _moved_decorator(kind, new_attribute_name, message=None, @@ -56,23 +57,52 @@ def _moved_decorator(kind, new_attribute_name, message=None, return decorator +def moved_function(new_func, old_func_name, old_module_name, + message=None, version=None, removal_version=None, + stacklevel=3, category=None): + """Deprecates a function that was moved to another location. + + This generates a wrapper around ``new_func`` that will emit a deprecation + warning when called. The warning message will include the new location + to obtain the function from. + """ + new_func_full_name = _utils.get_callable_name(new_func) + new_func_full_name += _MOVED_CALLABLE_POSTFIX + old_func_full_name = ".".join([old_module_name, old_func_name]) + old_func_full_name += _MOVED_CALLABLE_POSTFIX + prefix = _FUNC_MOVED_PREFIX_TPL % (old_func_full_name, new_func_full_name) + out_message = _utils.generate_message(prefix, + message=message, version=version, + removal_version=removal_version) + + @six.wraps(new_func) + def old_new_func(*args, **kwargs): + _utils.deprecation(out_message, stacklevel=stacklevel, + category=category) + return new_func(*args, **kwargs) + + old_new_func.__name__ = old_func_name + old_new_func.__module__ = old_module_name + return old_new_func + + def moved_method(new_method_name, message=None, version=None, removal_version=None, stacklevel=3, category=None): - """Decorates a *instance* method that was moved to another location.""" - if not new_method_name.endswith(_MOVED_METHOD_POSTFIX): - new_method_name += _MOVED_METHOD_POSTFIX + """Decorates an *instance* method that was moved to another location.""" + if not new_method_name.endswith(_MOVED_CALLABLE_POSTFIX): + new_method_name += _MOVED_CALLABLE_POSTFIX return _moved_decorator('Method', new_method_name, message=message, version=version, removal_version=removal_version, stacklevel=stacklevel, - attr_postfix=_MOVED_METHOD_POSTFIX, + attr_postfix=_MOVED_CALLABLE_POSTFIX, category=category) def moved_property(new_attribute_name, message=None, version=None, removal_version=None, stacklevel=3, category=None): - """Decorates a *instance* property that was moved to another location.""" + """Decorates an *instance* property that was moved to another location.""" return _moved_decorator('Property', new_attribute_name, message=message, version=version, removal_version=removal_version, stacklevel=stacklevel, category=category) diff --git a/debtcollector/tests/test_deprecation.py b/debtcollector/tests/test_deprecation.py index be79f80..120fc6c 100644 --- a/debtcollector/tests/test_deprecation.py +++ b/debtcollector/tests/test_deprecation.py @@ -91,6 +91,14 @@ def blue_comet(): return True +def yellow_sun(): + """Yellow.""" + return True + + +yellowish_sun = moves.moved_function(yellow_sun, 'yellowish_sun', __name__) + + @removals.remove() class EFSF(object): pass @@ -217,6 +225,21 @@ class MovedPropertyTest(test_base.TestCase): self.assertEqual(0, len(capture)) +class MovedFunctionTest(test_base.TestCase): + def test_basics(self): + self.assertTrue(yellowish_sun()) + self.assertTrue(yellow_sun()) + self.assertEqual("Yellow.", yellowish_sun.__doc__) + + def test_warnings_emitted(self): + with warnings.catch_warnings(record=True) as capture: + warnings.simplefilter("always") + self.assertTrue(yellowish_sun()) + self.assertEqual(1, len(capture)) + w = capture[0] + self.assertEqual(DeprecationWarning, w.category) + + class MovedMethodTest(test_base.TestCase): def test_basics(self): c = KittyKat() diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 3ad6edb..6065278 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -113,6 +113,32 @@ A basic example to do just this (on a ``__init__`` method): __main__:1: DeprecationWarning: Using the 'bleep' argument is deprecated +Moving a function +----------------- + +To change the name or location of a regular function use the +:py:func:`~debtcollector.moves.moved_function` function: + +.. doctest:: + + >>> from debtcollector import moves + >>> import warnings + >>> warnings.simplefilter('always') + >>> def new_thing(): + ... return "new thing" + ... + >>> old_thing = moves.moved_function(new_thing, 'old_thing', __name__) + >>> new_thing() + 'new thing' + >>> old_thing() + 'new thing' + +**Expected output:** + +.. testoutput:: + + __main__:1: DeprecationWarning: Function '__main__.old_thing()' has moved to '__main__.new_thing()' + Moving a method ---------------