diff options
author | Laurent Peuch <cortex@worlddomination.be> | 2020-08-25 22:11:27 +0200 |
---|---|---|
committer | Laurent Peuch <cortex@worlddomination.be> | 2020-08-25 22:11:27 +0200 |
commit | 0aedd28cb0eca8ba51f54db4fd2e171971569c7d (patch) | |
tree | ca1f9456c538e32523f64924ff5fc45605024858 | |
parent | 34e17836ea4c00f200941a07d0b50147b061dd12 (diff) | |
download | logilab-common-0aedd28cb0eca8ba51f54db4fd2e171971569c7d.tar.gz |
feat(deprecation): add structured informations to deprecation warnings
-rw-r--r-- | logilab/common/deprecation.py | 246 | ||||
-rw-r--r-- | test/test_deprecation.py | 241 |
2 files changed, 463 insertions, 24 deletions
diff --git a/logilab/common/deprecation.py b/logilab/common/deprecation.py index 84f3608..848f8e7 100644 --- a/logilab/common/deprecation.py +++ b/logilab/common/deprecation.py @@ -21,9 +21,10 @@ __docformat__ = "restructuredtext en" import os import sys +from enum import Enum from warnings import warn from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, Type from typing_extensions import Protocol @@ -86,20 +87,34 @@ def lazy_wraps(wrapped: Callable) -> Callable: class DeprecationWrapper(object): """proxy to print a warning on access to any attribute of the wrapped object""" - def __init__(self, proxied, msg: Optional[str] = None, version: Optional[str] = None) -> None: - self._proxied = proxied + def __init__( + self, proxied: Any, msg: Optional[str] = None, version: Optional[str] = None + ) -> None: + self._proxied: Any = proxied self._msg: str = msg if msg else "" self.version: Optional[str] = version def __getattr__(self, attr: str) -> Any: - send_warning(self._msg, stacklevel=3, version=self.version) + send_warning( + self._msg, + deprecation_class=DeprecationWarning, + deprecation_class_kwargs={}, + stacklevel=3, + version=self.version, + ) return getattr(self._proxied, attr) def __setattr__(self, attr: str, value: Any) -> None: if attr in ("_proxied", "_msg"): self.__dict__[attr] = value else: - send_warning(self._msg, stacklevel=3, version=self.version) + send_warning( + self._msg, + deprecation_class=DeprecationWarning, + deprecation_class_kwargs={}, + stacklevel=3, + version=self.version, + ) setattr(self._proxied, attr, value) @@ -132,6 +147,8 @@ def _get_module_name(number: int = 1) -> str: def send_warning( reason: str, + deprecation_class: Type[DeprecationWarning], + deprecation_class_kwargs: Dict[str, Any], version: Optional[str] = None, stacklevel: int = 2, module_name: Optional[str] = None, @@ -146,7 +163,80 @@ def send_warning( elif version: reason = "[%s] %s" % (version, reason) - warn(reason, DeprecationWarning, stacklevel=stacklevel) + warn( + deprecation_class(reason, **deprecation_class_kwargs), stacklevel=stacklevel # type: ignore + ) + + +class DeprecationWarningKind(Enum): + ARGUMENT = "argument" + ATTRIBUTE = "attribute" + CALLABLE = "callable" + CLASS = "class" + MODULE = "module" + + +class DeprecationWarningOperation(Enum): + DEPRECATED = "deprecated" + MOVED = "moved" + REMOVED = "removed" + RENAMED = "renamed" + + +class StructuredDeprecationWarning(DeprecationWarning): + """ + Base class for all structured DeprecationWarning + Mostly used with isinstance + """ + + def __init__(self, reason: str): + self.reason: str = reason + + def __str__(self) -> str: + return self.reason + + +class TargetRenamedDeprecationWarning(StructuredDeprecationWarning): + def __init__(self, reason: str, kind: DeprecationWarningKind, old_name: str, new_name: str): + super().__init__(reason) + self.operation = DeprecationWarningOperation.RENAMED + self.kind: DeprecationWarningKind = kind # callable, class, module, argument, attribute + self.old_name: str = old_name + self.new_name: str = new_name + + +class TargetDeprecatedDeprecationWarning(StructuredDeprecationWarning): + def __init__(self, reason: str, kind: DeprecationWarningKind): + super().__init__(reason) + self.operation = DeprecationWarningOperation.DEPRECATED + self.kind: DeprecationWarningKind = kind # callable, class, module, argument, attribute + + +class TargetRemovedDeprecationWarning(StructuredDeprecationWarning): + def __init__(self, reason: str, kind: DeprecationWarningKind, name: str): + super().__init__(reason) + self.operation = DeprecationWarningOperation.REMOVED + self.kind: DeprecationWarningKind = kind # callable, class, module, argument, attribute + self.name: str = name + + +class TargetMovedDeprecationWarning(StructuredDeprecationWarning): + def __init__( + self, + reason: str, + kind: DeprecationWarningKind, + old_name: str, + new_name: str, + old_module: str, + new_module: str, + ): + super().__init__(reason) + self.operation = DeprecationWarningOperation.MOVED + self.kind: DeprecationWarningKind = kind # callable, class, module, argument, attribute + self.old_name: str = old_name + self.new_name: str = new_name + self.old_module: str = old_module + self.new_module: str = new_module def callable_renamed( @@ -171,6 +261,12 @@ def callable_renamed( f"{old_name} has been renamed and is deprecated, uses " f"{get_real__name__(new_function)} instead" ), + TargetRenamedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.CALLABLE, + "old_name": old_name, + "new_name": get_real__name__(new_function), + }, stacklevel=3, version=version, module_name=new_function.__module__, @@ -205,6 +301,11 @@ def argument_removed(old_argument_name: str, version: Optional[str] = None) -> C send_warning( f"argument {old_argument_name} of callable {get_real__name__(func)} has been " f"removed and is deprecated", + deprecation_class=TargetRemovedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.ARGUMENT, + "name": old_argument_name, + }, stacklevel=3, version=version, module_name=func.__module__, @@ -234,7 +335,14 @@ def callable_deprecated( if "%s" in message: message %= get_real__name__(func) - send_warning(message, version, stacklevel + 1, module_name=func.__module__) + send_warning( + message, + TargetDeprecatedDeprecationWarning, + {"kind": DeprecationWarningKind.CALLABLE}, + version, + stacklevel + 1, + module_name=func.__module__, + ) return func(*args, **kwargs) return wrapped @@ -264,6 +372,14 @@ def _generate_class_deprecated(): } send_warning( message, + deprecation_class=getattr( + cls, "__deprecation_warning_class__", TargetDeprecatedDeprecationWarning + ), + deprecation_class_kwargs=getattr( + cls, + "__deprecation_warning_class_kwargs__", + {"kind": DeprecationWarningKind.CLASS}, + ), module_name=getattr( cls, "__deprecation_warning_module_name__", _get_module_name(1) ), @@ -308,15 +424,48 @@ def attribute_renamed(old_name: str, new_name: str, version: Optional[str] = Non ) def _get_old(self) -> Any: - send_warning(reason, stacklevel=3, version=version, module_name=klass.__module__) + send_warning( + reason, + deprecation_class=TargetRenamedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.ATTRIBUTE, + "old_name": old_name, + "new_name": new_name, + }, + stacklevel=3, + version=version, + module_name=klass.__module__, + ) return getattr(self, new_name) - def _set_old(self, value: Any) -> None: - send_warning(reason, stacklevel=3, version=version, module_name=klass.__module__) + def _set_old(self, value) -> None: + send_warning( + reason, + deprecation_class=TargetRenamedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.ATTRIBUTE, + "old_name": old_name, + "new_name": new_name, + }, + stacklevel=3, + version=version, + module_name=klass.__module__, + ) setattr(self, new_name, value) - def _del_old(self) -> None: - send_warning(reason, stacklevel=3, version=version, module_name=klass.__module__) + def _del_old(self): + send_warning( + reason, + deprecation_class=TargetRenamedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.ATTRIBUTE, + "old_name": old_name, + "new_name": new_name, + }, + stacklevel=3, + version=version, + module_name=klass.__module__, + ) delattr(self, new_name) setattr(klass, old_name, property(_get_old, _set_old, _del_old)) @@ -354,6 +503,12 @@ def argument_renamed(old_name: str, new_name: str, version: Optional[str] = None send_warning( f"argument {old_name} of callable {get_real__name__(func)} has been renamed " f"and is deprecated, use keyword argument {new_name} instead", + deprecation_class=TargetRenamedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.ARGUMENT, + "old_name": old_name, + "new_name": new_name, + }, stacklevel=3, version=version, module_name=func.__module__, @@ -371,7 +526,11 @@ def argument_renamed(old_name: str, new_name: str, version: Optional[str] = None @argument_renamed(old_name="modpath", new_name="module_path") @argument_renamed(old_name="objname", new_name="object_name") def callable_moved( - module_name: str, object_name: str, version: Optional[str] = None, stacklevel: int = 2 + module_name: str, + object_name: str, + version: Optional[str] = None, + stacklevel: int = 2, + new_name: Optional[str] = None, ) -> Callable: """use to tell that a callable has been moved to a new module. @@ -383,13 +542,33 @@ def callable_moved( wrapper is use in a class ancestors list, use the `class_moved` function instead (which has no lazy import feature though). """ - message = "object %s has been moved to module %s" % (object_name, module_name) + # in case the callable has been renamed + new_name = new_name if new_name is not None else object_name + old_module = _get_module_name(3) + + message = "object %s.%s has been moved to %s.%s" % ( + old_module, + object_name, + module_name, + object_name, + ) def callnew(*args, **kwargs): from logilab.common.modutils import load_module_from_name send_warning( - message, version=version, stacklevel=stacklevel + 1, module_name=_get_module_name(1) + message, + deprecation_class=TargetMovedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.CALLABLE, + "old_name": object_name, + "new_name": new_name, + "old_module": old_module, + "new_module": module_name, + }, + version=version, + stacklevel=stacklevel + 1, + module_name=old_module, ) m = load_module_from_name(module_name) @@ -409,6 +588,8 @@ def class_renamed( message: Optional[str] = None, version: Optional[str] = None, module_name: Optional[str] = None, + deprecated_warning_class=TargetRenamedDeprecationWarning, + deprecated_warning_kwargs=None, ) -> type: """automatically creates a class which fires a DeprecationWarning when instantiated. @@ -424,6 +605,15 @@ def class_renamed( message = "%s is deprecated, use %s instead" % (old_name, new_class.__name__) class_dict["__deprecation_warning__"] = message + class_dict["__deprecation_warning_class__"] = deprecated_warning_class + if deprecated_warning_kwargs is None: + class_dict["__deprecation_warning_class_kwargs__"] = { + "kind": DeprecationWarningKind.CLASS, + "old_name": old_name, + "new_name": new_class.__name__, + } + else: + class_dict["__deprecation_warning_class_kwargs__"] = deprecated_warning_kwargs class_dict["__deprecation_warning_version__"] = version class_dict["__deprecation_warning_stacklevel__"] = 3 @@ -445,6 +635,12 @@ def class_renamed( ) send_warning( msg, + deprecation_class=TargetRenamedDeprecationWarning, + deprecation_class_kwargs={ + "kind": DeprecationWarningKind.CLASS, + "old_name": old_name, + "new_name": new_class.__name__, + }, stacklevel=class_dict.get("__deprecation_warning_stacklevel__", 3), version=class_dict.get("__deprecation_warning_version__", None), ) @@ -465,8 +661,11 @@ def class_moved( if old_name is None: old_name = new_class.__name__ + old_module = _get_module_name(1) + if message is None: - message = "class %s is now available as %s.%s" % ( + message = "class %s.%s is now available as %s.%s" % ( + old_module, old_name, new_class.__module__, new_class.__name__, @@ -474,4 +673,17 @@ def class_moved( module_name = _get_module_name(1) - return class_renamed(old_name, new_class, message=message, module_name=module_name) + return class_renamed( + old_name, + new_class, + message=message, + module_name=module_name, + deprecated_warning_class=TargetMovedDeprecationWarning, + deprecated_warning_kwargs={ + "kind": DeprecationWarningKind.CLASS, + "old_module": old_module, + "new_module": new_class.__module__, + "old_name": old_name, + "new_name": new_class.__name__, + }, + ) diff --git a/test/test_deprecation.py b/test/test_deprecation.py index 2508208..93fc0eb 100644 --- a/test/test_deprecation.py +++ b/test/test_deprecation.py @@ -31,7 +31,7 @@ class RawInputTC(TestCase): # instead we just make sure it does not crash def mock_warn(self, *args, **kwargs): - self.messages.append(args[0]) + self.messages.append(str(args[0])) def setUp(self): self.messages = [] @@ -83,7 +83,10 @@ class RawInputTC(TestCase): OldClass() self.assertEqual( self.messages, - ["[test_deprecation] class OldName is now available as test_deprecation.AnyClass"], + [ + "[test_deprecation] class test_deprecation.OldName is now available as " + "test_deprecation.AnyClass" + ], ) self.messages = [] @@ -93,7 +96,10 @@ class RawInputTC(TestCase): AnyClass() self.assertEqual( self.messages, - ["[test_deprecation] class AnyClass is now available as test_deprecation.AnyClass"], + [ + "[test_deprecation] class test_deprecation.AnyClass is now available as " + "test_deprecation.AnyClass" + ], ) def test_deprecated_func(self): @@ -242,15 +248,236 @@ class RawInputTC(TestCase): ], ) - def test_moved(self): + def test_callable_moved(self): module = "data.deprecation" - any_func = deprecation.callable_moved(module, "moving_target") - any_func() + moving_target = deprecation.callable_moved(module, "moving_target") + moving_target() self.assertEqual( self.messages, - ["[test_deprecation] object moving_target has been moved to module data.deprecation"], + [ + "[test_deprecation] object test_deprecation.moving_target has been moved to " + "data.deprecation.moving_target" + ], ) +class StructuredDeprecatedWarningsTest(TestCase): + def mock_warn(self, *args, **kwargs): + self.collected_warnings.append(args[0]) + + def setUp(self): + self.collected_warnings = [] + deprecation.warn = self.mock_warn + + def tearDown(self): + deprecation.warn = warnings.warn + + def mk_func(self): + def any_func(): + pass + + return any_func + + def test_class_deprecated(self): + class AnyClass(metaclass=deprecation.class_deprecated): + pass + + AnyClass() + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.DEPRECATED) + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CLASS) + + def test_class_renamed(self): + class AnyClass: + pass + + OldClass = deprecation.class_renamed("OldClass", AnyClass) + + OldClass() + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.RENAMED) + self.assertEqual(warning.old_name, "OldClass") + self.assertEqual(warning.new_name, "AnyClass") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CLASS) + + def test_class_moved(self): + class AnyClass: + pass + + OldClass = deprecation.class_moved(new_class=AnyClass, old_name="OldName") + OldClass() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.MOVED) + self.assertEqual(warning.old_module, "test_deprecation") + self.assertEqual(warning.new_module, "test_deprecation") + self.assertEqual(warning.old_name, "OldName") + self.assertEqual(warning.new_name, "AnyClass") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CLASS) + + self.collected_warnings = [] + + AnyClass = deprecation.class_moved(new_class=AnyClass) + + AnyClass() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.MOVED) + self.assertEqual(warning.old_module, "test_deprecation") + self.assertEqual(warning.new_module, "test_deprecation") + self.assertEqual(warning.old_name, "AnyClass") + self.assertEqual(warning.new_name, "AnyClass") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CLASS) + + def test_deprecated_func(self): + any_func = deprecation.callable_deprecated()(self.mk_func()) + any_func() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.DEPRECATED) + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CALLABLE) + + any_func = deprecation.callable_deprecated("message")(self.mk_func()) + any_func() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.DEPRECATED) + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CALLABLE) + + def test_deprecated_decorator(self): + @deprecation.callable_deprecated() + def any_func(): + pass + + any_func() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.DEPRECATED) + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CALLABLE) + + @deprecation.callable_deprecated("message") + def any_func(): + pass + + any_func() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.DEPRECATED) + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CALLABLE) + + def test_attribute_renamed(self): + @deprecation.attribute_renamed(old_name="old", new_name="new") + class SomeClass: + def __init__(self): + self.new = 42 + + some_class = SomeClass() + + some_class.old == some_class.new + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.RENAMED) + self.assertEqual(warning.old_name, "old") + self.assertEqual(warning.new_name, "new") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.ATTRIBUTE) + + some_class.old = 43 + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.RENAMED) + self.assertEqual(warning.old_name, "old") + self.assertEqual(warning.new_name, "new") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.ATTRIBUTE) + + del some_class.old + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.RENAMED) + self.assertEqual(warning.old_name, "old") + self.assertEqual(warning.new_name, "new") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.ATTRIBUTE) + + def test_argument_renamed(self): + @deprecation.argument_renamed(old_name="old", new_name="new") + def some_function(new): + return new + + some_function(old=42) + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.RENAMED) + self.assertEqual(warning.old_name, "old") + self.assertEqual(warning.new_name, "new") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.ARGUMENT) + + def test_argument_removed(self): + @deprecation.argument_removed("old") + def some_function(new): + return new + + some_function(new=10, old=20) + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.REMOVED) + self.assertEqual(warning.name, "old") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.ARGUMENT) + + def test_callable_renamed(self): + def any_func(): + pass + + old_func = deprecation.callable_renamed("old_func", any_func) + old_func() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.RENAMED) + self.assertEqual(warning.old_name, "old_func") + self.assertEqual(warning.new_name, "any_func") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CALLABLE) + + def test_callable_moved(self): + module = "data.deprecation" + moving_target = deprecation.callable_moved(module, "moving_target") + moving_target() + + self.assertEqual(len(self.collected_warnings), 1) + warning = self.collected_warnings.pop() + + self.assertEqual(warning.operation, deprecation.DeprecationWarningOperation.MOVED) + self.assertEqual(warning.old_module, "test_deprecation") + self.assertEqual(warning.new_module, "data.deprecation") + self.assertEqual(warning.old_name, "moving_target") + self.assertEqual(warning.new_name, "moving_target") + self.assertEqual(warning.kind, deprecation.DeprecationWarningKind.CALLABLE) + + if __name__ == "__main__": unittest_main() |