summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurent Peuch <cortex@worlddomination.be>2020-05-20 20:47:31 +0200
committerLaurent Peuch <cortex@worlddomination.be>2020-05-20 20:47:31 +0200
commit40e3922275355dec5e8ec99936b313adbe62c847 (patch)
treefbc2d6d0cd13e17dfe9effa485bd3f4333da736d
parent7a3203391ad5e067ae85531bdad6f0356a9d50e3 (diff)
downloadlogilab-common-40e3922275355dec5e8ec99936b313adbe62c847.tar.gz
[deprecation/fix] implement lazy_wraps, a lazy version of functools.wraps for LazyObject
functools.wraps was breaking the behavior of LazyObject because it tried to access attributes of the LazyObject that triggers an import which could sometime fails in this situation where some modules are marked as deprecated but aren't imported yet. For example see https://forge.extranet.logilab.fr/cubicweb/cubicweb/blob/3.24.0/cubicweb/schemas/__init__.py#L51
-rw-r--r--logilab/common/deprecation.py50
-rw-r--r--test/unittest_deprecation.py15
2 files changed, 59 insertions, 6 deletions
diff --git a/logilab/common/deprecation.py b/logilab/common/deprecation.py
index 3da3021..94b3b23 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):
@@ -147,12 +185,12 @@ def callable_deprecated(reason=None, version=None, stacklevel=2):
compatible version.
"""
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)
return func(*args, **kwargs)
return wrapped
diff --git a/test/unittest_deprecation.py b/test/unittest_deprecation.py
index 172b9fb..07988c4 100644
--- a/test/unittest_deprecation.py
+++ b/test/unittest_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
@@ -109,6 +110,20 @@ class RawInputTC(TestCase):
self.assertEqual(self.messages,
['[logilab.common] The function "any_func" is deprecated', '[logilab.common] message'])
+ 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: