Anki/pylib/anki/_legacy.py
2021-06-27 12:12:23 +10:00

84 lines
2.6 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
import functools
import os
import pathlib
import traceback
from typing import Any, Callable, Dict, Optional, Tuple, Union, no_type_check
import stringcase
VariableTarget = Tuple[Any, str]
DeprecatedAliasTarget = Union[Callable, VariableTarget]
def _target_to_string(target: DeprecatedAliasTarget) -> str:
if name := getattr(target, "__name__", None):
return name
else:
return target[1] # type: ignore
def partial_path(full_path: str, components: int) -> str:
path = pathlib.Path(full_path)
return os.path.join(*path.parts[-components:])
def _print_deprecation_warning(old: str, doc: str) -> None:
path, linenum, fn, y = traceback.extract_stack(limit=5)[2]
path = partial_path(path, components=3)
print(f"{path}:{linenum}:{old} is deprecated: {doc}")
class DeprecatedNamesMixin:
"Expose instance methods/vars as camelCase for legacy callers."
# the @no_type_check lines are required to prevent mypy allowing arbitrary
# attributes on the consuming class
_deprecated_aliases: Dict[str, str] = {}
@no_type_check
def __getattr__(self, name: str) -> Any:
remapped = self._deprecated_aliases.get(name) or stringcase.snakecase(name)
if remapped == name:
raise AttributeError
out = getattr(self, remapped)
_print_deprecation_warning(f"'{name}'", f"please use '{remapped}'")
return out
@no_type_check
@classmethod
def register_deprecated_aliases(cls, **kwargs: DeprecatedAliasTarget) -> None:
"""Manually add aliases that are not a simple transform.
Either pass in a method, or a tuple of (variable, "variable"). The
latter is required because we want to ensure the provided arguments
are valid symbols, and we can't get a variable's name easily.
"""
cls._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}
def deprecated(replaced_by: Optional[Callable] = None, info: str = "") -> Callable:
"""Print a deprecation warning, telling users to use `replaced_by`, or show `doc`."""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def decorated_func(*args: Any, **kwargs: Any) -> Any:
if replaced_by:
doc = f"please use {replaced_by.__name__} instead."
else:
doc = info
_print_deprecation_warning(f"{func.__name__}()", doc)
return func(*args, **kwargs)
return decorated_func
return decorator