Commit b92851a3 authored by Laurent Peuch's avatar Laurent Peuch
Browse files

[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
parent 1d5748f51a90
......@@ -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)
......
......@@ -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:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment