diff options
-rw-r--r-- | polymatrix/utils/deprecation.py | 88 |
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 """ |