mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

* PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
203 lines
7.1 KiB
Python
203 lines
7.1 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
# pylint: enable=invalid-name
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import os
|
|
import pathlib
|
|
import traceback
|
|
from typing import Any, Callable, Union, no_type_check
|
|
|
|
from anki._vendor import stringcase
|
|
|
|
VariableTarget = tuple[Any, str]
|
|
DeprecatedAliasTarget = Union[Callable, VariableTarget]
|
|
|
|
|
|
def _target_to_string(target: DeprecatedAliasTarget | None) -> str:
|
|
if target is None:
|
|
return ""
|
|
if name := getattr(target, "__name__", None):
|
|
return name
|
|
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(msg: str, frame: int = 1) -> None:
|
|
# skip one frame to get to caller
|
|
# then by default, skip one more frame as caller themself usually wants to
|
|
# print their own caller
|
|
path, linenum, _, _ = traceback.extract_stack(limit=frame + 2)[0]
|
|
path = partial_path(path, components=3)
|
|
print(f"{path}:{linenum}:{msg}")
|
|
|
|
|
|
def _print_warning(old: str, doc: str, frame: int = 1) -> None:
|
|
return print_deprecation_warning(f"{old} is deprecated: {doc}", frame=frame + 1)
|
|
|
|
|
|
def _print_replacement_warning(old: str, new: str, frame: int = 1) -> None:
|
|
doc = f"please use '{new}'" if new else "please implement your own"
|
|
_print_warning(old, doc, frame=frame + 1)
|
|
|
|
|
|
def _get_remapped_and_replacement(
|
|
mixin: DeprecatedNamesMixin | DeprecatedNamesMixinForModule, name: str
|
|
) -> tuple[str, str | None]:
|
|
if some_tuple := mixin._deprecated_attributes.get(name):
|
|
return some_tuple
|
|
|
|
remapped = mixin._deprecated_aliases.get(name) or stringcase.snakecase(name)
|
|
if remapped == name:
|
|
raise AttributeError
|
|
return (remapped, remapped)
|
|
|
|
|
|
class DeprecatedNamesMixin:
|
|
"Expose instance methods/vars as camelCase for legacy callers."
|
|
|
|
# deprecated name -> new name
|
|
_deprecated_aliases: dict[str, str] = {}
|
|
# deprecated name -> [new internal name, new name shown to user]
|
|
_deprecated_attributes: dict[str, tuple[str, str | None]] = {}
|
|
|
|
# the @no_type_check lines are required to prevent mypy allowing arbitrary
|
|
# attributes on the consuming class
|
|
|
|
@no_type_check
|
|
def __getattr__(self, name: str) -> Any:
|
|
try:
|
|
remapped, replacement = _get_remapped_and_replacement(self, name)
|
|
out = getattr(self, remapped)
|
|
except AttributeError:
|
|
raise AttributeError(
|
|
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
) from None
|
|
|
|
_print_replacement_warning(name, replacement)
|
|
return out
|
|
|
|
@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()}
|
|
|
|
@classmethod
|
|
def register_deprecated_attributes(
|
|
cls,
|
|
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget | None],
|
|
) -> None:
|
|
"""Manually add deprecated attributes without exact substitutes.
|
|
|
|
Pass a tuple of (alias, replacement), where alias is the attribute's new
|
|
name (by convention: snakecase, prepended with '_legacy_'), and
|
|
replacement is any callable to be used instead in new code or None.
|
|
Also note the docstring of `register_deprecated_aliases`.
|
|
|
|
E.g. given `def oldFunc(args): return new_func(additionalLogic(args))`,
|
|
rename `oldFunc` to `_legacy_old_func` and call
|
|
`register_deprecated_attributes(oldFunc=(_legacy_old_func, new_func))`.
|
|
"""
|
|
cls._deprecated_attributes = {
|
|
k: (_target_to_string(v[0]), _target_to_string(v[1]))
|
|
for k, v in kwargs.items()
|
|
}
|
|
|
|
|
|
class DeprecatedNamesMixinForModule:
|
|
"""Provides the functionality of DeprecatedNamesMixin for modules.
|
|
|
|
It can be invoked like this:
|
|
```
|
|
_deprecated_names = DeprecatedNamesMixinForModule(globals())
|
|
_deprecated_names.register_deprecated_aliases(...
|
|
_deprecated_names.register_deprecated_attributes(...
|
|
|
|
@no_type_check
|
|
def __getattr__(name: str) -> Any:
|
|
return _deprecated_names.__getattr__(name)
|
|
```
|
|
See DeprecatedNamesMixin for more documentation.
|
|
"""
|
|
|
|
def __init__(self, module_globals: dict[str, Any]) -> None:
|
|
self.module_globals = module_globals
|
|
self._deprecated_aliases: dict[str, str] = {}
|
|
self._deprecated_attributes: dict[str, tuple[str, str | None]] = {}
|
|
|
|
@no_type_check
|
|
def __getattr__(self, name: str) -> Any:
|
|
try:
|
|
remapped, replacement = _get_remapped_and_replacement(self, name)
|
|
out = self.module_globals[remapped]
|
|
except (AttributeError, KeyError):
|
|
raise AttributeError(
|
|
f"Module '{self.module_globals['__name__']}' has no attribute '{name}'"
|
|
) from None
|
|
|
|
# skip an additional frame as we are called from the module `__getattr__`
|
|
_print_replacement_warning(name, replacement, frame=2)
|
|
return out
|
|
|
|
def register_deprecated_aliases(self, **kwargs: DeprecatedAliasTarget) -> None:
|
|
self._deprecated_aliases = {k: _target_to_string(v) for k, v in kwargs.items()}
|
|
|
|
def register_deprecated_attributes(
|
|
self,
|
|
**kwargs: tuple[DeprecatedAliasTarget, DeprecatedAliasTarget | None],
|
|
) -> None:
|
|
self._deprecated_attributes = {
|
|
k: (_target_to_string(v[0]), _target_to_string(v[1]))
|
|
for k, v in kwargs.items()
|
|
}
|
|
|
|
|
|
def deprecated(replaced_by: Callable | None = 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 info:
|
|
_print_warning(f"{func.__name__}()", info)
|
|
else:
|
|
_print_replacement_warning(func.__name__, replaced_by.__name__)
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
return decorated_func
|
|
|
|
return decorator
|
|
|
|
|
|
def deprecated_keywords(**replaced_keys: str) -> Callable:
|
|
"""Pass `oldKey="new_key"` to map the former to the latter, if passed to the
|
|
decorated function as a key word, and print a deprecation warning.
|
|
"""
|
|
|
|
def decorator(func: Callable) -> Callable:
|
|
@functools.wraps(func)
|
|
def decorated_func(*args: Any, **kwargs: Any) -> Any:
|
|
updated_kwargs = {}
|
|
for key, val in kwargs.items():
|
|
if replacement := replaced_keys.get(key):
|
|
_print_replacement_warning(key, replacement)
|
|
updated_kwargs[replacement or key] = val
|
|
|
|
return func(*args, **updated_kwargs)
|
|
|
|
return decorated_func
|
|
|
|
return decorator
|