summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurent Peuch <cortex@worlddomination.be>2020-08-25 22:11:27 +0200
committerLaurent Peuch <cortex@worlddomination.be>2020-08-25 22:11:27 +0200
commit0aedd28cb0eca8ba51f54db4fd2e171971569c7d (patch)
treeca1f9456c538e32523f64924ff5fc45605024858
parent34e17836ea4c00f200941a07d0b50147b061dd12 (diff)
downloadlogilab-common-0aedd28cb0eca8ba51f54db4fd2e171971569c7d.tar.gz
feat(deprecation): add structured informations to deprecation warnings
-rw-r--r--logilab/common/deprecation.py246
-rw-r--r--test/test_deprecation.py241
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()