diff options
-rw-r--r-- | logilab/common/deprecation.py | 50 | ||||
-rw-r--r-- | test/test_deprecation.py | 15 |
2 files changed, 59 insertions, 6 deletions
diff --git a/logilab/common/deprecation.py b/logilab/common/deprecation.py index 5dc23f7..ff6dc7c 100644 --- a/logilab/common/deprecation.py +++ b/logilab/common/deprecation.py @@ -22,7 +22,45 @@ __docformat__ = "restructuredtext en" import os import sys from warnings import warn -from functools import wraps +from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES, wraps + + +def lazy_wraps(wrapped): + """ + This is the equivalent of the @wraps decorator of functools except it won't + try to grabs attributes of the targeted function on decoration but on access. + + This is needed because of logilab.common.modutils.LazyObject. + + Indeed: if you try to decorate a LazyObject with @wraps, wraps will try to + access attributes of LazyObject and this will trigger the attempt to import + the module decorated by LazyObject which you don't want to do when you just + want to mark this LazyObject has been a deprecated objet that you only + wants to trigger if the user try to use it. + + Usage: like @wraps() + + >>> @lazy_wraps(function) + >>> def wrapper(*args, **kwargs): ... + """ + + def update_wrapper_attributes(wrapper): + def __getattribute__(self, attribute): + if attribute in WRAPPER_ASSIGNMENTS: + return getattr(wrapped, attribute) + + return super(self.__class__, self).__getattribute__(attribute) + + wrapper.__getattribute__ = __getattribute__ + + for attribute in WRAPPER_UPDATES: + getattr(wrapper, attribute).update(getattr(wrapped, attribute, {})) + + wrapper.__wrapped__ = wrapped + + return wrapper + + return update_wrapper_attributes class DeprecationWrapper(object): @@ -160,12 +198,12 @@ def callable_deprecated(reason=None, version=None, stacklevel=2): """ def decorator(func): - message = reason or 'The function "%s" is deprecated' - if "%s" in message: - message %= func.__name__ - - @wraps(func) + @lazy_wraps(func) def wrapped(*args, **kwargs): + message = reason or 'The function "%s" is deprecated' + if "%s" in message: + message %= func.__name__ + send_warning(message, version, stacklevel + 1, module_name=func.__module__) return func(*args, **kwargs) diff --git a/test/test_deprecation.py b/test/test_deprecation.py index 65ef8fd..3cbce9c 100644 --- a/test/test_deprecation.py +++ b/test/test_deprecation.py @@ -20,6 +20,7 @@ import warnings from logilab.common.testlib import TestCase, unittest_main +from logilab.common.modutils import LazyObject from logilab.common import deprecation @@ -128,6 +129,20 @@ class RawInputTC(TestCase): ], ) + def test_deprecated_decorator_bad_lazyobject(self): + # this should not raised an ImportationError + deprecation.deprecated("foobar")(LazyObject("cubes.localperms", "xperm")) + + # with or without giving it a message (because it shouldn't access + # attributes of the wrapped object before the object is called) + deprecation.deprecated()(LazyObject("cubes.localperms", "xperm")) + + # all of this is done because of the magical way LazyObject is working + # and that sometime CW used to use it to do fake import on deprecated + # modules to raise a warning if they were used but not importing them + # by default. + # See: https://forge.extranet.logilab.fr/cubicweb/cubicweb/blob/3.24.0/cubicweb/schemas/__init__.py#L51 # noqa + def test_attribute_renamed(self): @deprecation.attribute_renamed(old_name="old", new_name="new") class SomeClass: |