summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--polymatrix/utils/deprecation.py88
1 files changed, 88 insertions, 0 deletions
diff --git a/polymatrix/utils/deprecation.py b/polymatrix/utils/deprecation.py
new file mode 100644
index 0000000..c9dea71
--- /dev/null
+++ b/polymatrix/utils/deprecation.py
@@ -0,0 +1,88 @@
+""" Provides tools to mark parts of code a deprecated, for a smoother
+transition to new code. """
+from __future__ import annotations
+from typing import Callable, Any, overload
+from warnings import warn
+from functools import wraps
+from abc import ABCMeta
+
+@overload
+def deprecated(alias: Callable, pending: bool = False) -> Callable:
+ """ Mark a function as deprecated, and that the function `alias` should be
+ used instead. """
+ ...
+
+@overload
+def deprecated(reason: str | None, pending: bool = False) -> Callable:
+ """ Mark a function as deprecated. """
+
+
+def deprecated(reason_or_alias, pending=False):
+ """ Mark a function or method as deprecated """
+ def decorator(fn: Callable) -> Callable:
+ if callable(reason_or_alias):
+ reason = f"{fn.__name__} has been replaced by {reason_or_alias.__name__}," \
+ + "the alias will be removed in the future"
+
+ elif isinstance(reason_or_alias, str):
+ reason = reason_or_alias
+
+ elif reason_or_alias is None:
+ reason = f"{fn.__name__} has been deprecated (no reason was provided)"
+
+ else:
+ raise TypeError(f"{reason_or_alias} must be a callable or a string!")
+
+ @wraps(fn)
+ def wrapper(*args, **kwargs):
+ w = PendingDeprecationWarning if pending else DeprecationWarning
+ warn(reason, category=w, stacklevel=2)
+ return fn(*args, **kwargs)
+
+ return wrapper
+ return decorator
+
+
+class DeprecatedMeta(type):
+ """ Metaclass to mark a class as deprecated. """
+ # Forbidden metaclass black magic adapted from
+ # https://stackoverflow.com/questions/9008444/how-to-warn-about-class-name-deprecation
+
+ def __new__(cls: type, name: str, bases: tuple[type],
+ classdict: dict[str, Any], *args, **kwargs) -> DeprecatedMeta:
+ # Get class type that replaces the deprecated class, the "alias" class
+ alias = classdict.get("_DeprecatedClass_Alias")
+ if alias is not None:
+ warn("{} has been renamed to {}, the alias will be "
+ "removed in the future".format(name, alias.__name__),
+ DeprecationWarning, stacklevel=2)
+
+ # Deal with inheritance of deprecated classes. I.e. if a base class was
+ # deprecated and has an alias, replace base with alias type
+ new_bases: set[type] = set()
+
+ for b in bases:
+ base_alias = getattr(b, '_DeprecatedClass_Alias', None)
+ if base_alias is not None:
+ warn("{} has been renamed to {}, the alias will be "
+ "removed in the future".format(b.__name__, base_alias.__name__),
+ DeprecationWarning, stacklevel=2)
+
+ new_bases.add(base_alias or b)
+
+ # typecheker bug here? https://github.com/python/mypy/issues/12885
+ return super().__new__(cls, name, bases, classdict, *args, **kwargs) # type: ignore[misc]
+
+ def __instancecheck__(cls: type, instance: Any) -> bool:
+ return any(cls.__subclasscheck__(c)
+ for c in {type(instance), instance.__class__})
+
+ def __subclasscheck__(cls: type, subclass: type) -> bool:
+ if subclass is cls:
+ return True
+
+ return issubclass(subclass, getattr(cls, '_DeprecatedClass_Alias'))
+
+
+class DeprecatedABCMeta(DeprecatedMeta, ABCMeta):
+ """ Metaclass to mark an abstract base class as deprecated """