From 894ccade059de8cab667187e93ea27daf7c75658 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 26 Aug 2020 17:26:22 +0200 Subject: fix(deprecation): stacked decorators breaks getting the real callable __name__ attribute --- logilab/common/deprecation.py | 30 ++++++++++++++++++++++++------ test/test_deprecation.py | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/logilab/common/deprecation.py b/logilab/common/deprecation.py index 0ddedf2..b9f989c 100644 --- a/logilab/common/deprecation.py +++ b/logilab/common/deprecation.py @@ -27,6 +27,24 @@ from typing import Any, Callable, Dict, Optional from typing_extensions import Protocol +def get_real__name__(some_callable: Callable) -> str: + """ + This is another super edge magic case which is needed because we uses + lazy_wraps because of logilab.common.modutils.LazyObject and because + __name__ has special behavior and doesn't work like a normal attribute and + that __getattribute__ of lazy_wraps is bypassed. + + Therefor, to get the real callable name when several lazy_wrapped + decorator are used we need to travers the __wrapped__ attributes chain. + """ + + targeted_callable = some_callable + while hasattr(targeted_callable, "__wrapped__"): + targeted_callable = targeted_callable.__wrapped__ # type: ignore + + return targeted_callable.__name__ + + def lazy_wraps(wrapped: Callable) -> Callable: """ This is the equivalent of the @wraps decorator of functools except it won't @@ -151,8 +169,8 @@ def callable_renamed( def wrapped(*args, **kwargs): send_warning( ( - f"{old_name} has been renamed and is deprecated, uses {new_function.__name__} " - f"instead" + f"{old_name} has been renamed and is deprecated, uses " + f"{get_real__name__(new_function)} instead" ), stacklevel=3, version=version, @@ -186,7 +204,7 @@ def argument_removed(old_argument_name: str, version: Optional[str] = None) -> C def check_kwargs(*args, **kwargs): if old_argument_name in kwargs: send_warning( - f"argument {old_argument_name} of callable {func.__name__} has been " + f"argument {old_argument_name} of callable {get_real__name__(func)} has been " f"removed and is deprecated", stacklevel=3, version=version, @@ -215,7 +233,7 @@ def callable_deprecated( def wrapped(*args, **kwargs) -> Callable: message: str = reason or 'The function "%s" is deprecated' if "%s" in message: - message %= func.__name__ + message %= get_real__name__(func) send_warning(message, version, stacklevel + 1, module_name=func.__module__) return func(*args, **kwargs) @@ -328,14 +346,14 @@ def argument_renamed(old_name: str, new_name: str, version: Optional[str] = None def check_kwargs(*args, **kwargs) -> Callable: if old_name in kwargs and new_name in kwargs: raise ValueError( - f"argument {old_name} of callable {func.__name__} has been " + f"argument {old_name} of callable {get_real__name__(func)} has been " f"renamed to {new_name} but you are both using {old_name} and " f"{new_name} has keyword arguments, only uses {new_name}" ) if old_name in kwargs: send_warning( - f"argument {old_name} of callable {func.__name__} has been renamed " + f"argument {old_name} of callable {get_real__name__(func)} has been renamed " f"and is deprecated, use keyword argument {new_name} instead", stacklevel=3, version=version, diff --git a/test/test_deprecation.py b/test/test_deprecation.py index 3cbce9c..2508208 100644 --- a/test/test_deprecation.py +++ b/test/test_deprecation.py @@ -143,6 +143,31 @@ class RawInputTC(TestCase): # by default. # See: https://forge.extranet.logilab.fr/cubicweb/cubicweb/blob/3.24.0/cubicweb/schemas/__init__.py#L51 # noqa + def test_lazy_wraps_function_name(self): + """ + Avoid conflict from lazy_wraps where __name__ isn't correctly set on + the wrapper from the wrapped and we end up with the name of the wrapper + instead of the wrapped. + + Like here it would fail if "check_kwargs" is the name of the new + function instead of new_function_name, this is because the wrapper in + argument_renamed is called check_kwargs and doesn't transmit the + __name__ of the wrapped (new_function_name) correctly. + """ + + @deprecation.argument_renamed(old_name="a", new_name="b") + def new_function_name(b): + pass + + old_function_name = deprecation.callable_renamed( + old_name="old_function_name", new_function=new_function_name + ) + old_function_name(None) + + assert "old_function_name" in self.messages[0] + assert "new_function_name" in self.messages[0] + assert "check_kwargs" not in self.messages[0] + def test_attribute_renamed(self): @deprecation.attribute_renamed(old_name="old", new_name="new") class SomeClass: -- cgit v1.2.1