Source code for lightweaver.deprecation

"""
Deprecation shims for the names renamed to PEP 8 snake_case in Lightweaver 1.0.

The old names (see `lightweaver._renames.RENAMES`) keep working, emitting a
`LightweaverDeprecationWarning` that names the replacement, and will be removed
in a future release. Scripts can be updated automatically with
``python -m lightweaver.migrate <files>``, and the warnings can be silenced with
`silence_deprecations`.
"""

import dataclasses
import functools
import inspect
import warnings
from collections import defaultdict
from typing import Callable, Dict, Iterable, List, Set, TypeVar

from ._renames import RENAMES

__all__ = [
    'LightweaverDeprecationWarning',
    'accepts_old_kwargs',
    'deprecated_alias',
    'deprecated_names',
    'remap_old_keys',
    'silence_deprecations',
]

F = TypeVar('F', bound=Callable)
T = TypeVar('T', bound=type)

# new name -> old names (several old names can map to one new name, e.g. max_iter).
_OLD_NAMES: Dict[str, List[str]] = defaultdict(list)
for _old, _new in RENAMES.items():
    _OLD_NAMES[_new].append(_old)

# Callable short name (function, method, or class for its constructor) -> old
# keyword names it accepts. Used by `lightweaver.migrate`.
KWARG_REGISTRY: Dict[str, Set[str]] = defaultdict(set)


[docs] class LightweaverDeprecationWarning(FutureWarning): """ Warning for deprecated Lightweaver names. A `FutureWarning` subclass, so it is shown by default. """ pass
def _warn(old: str, new: str, stacklevel: int): warnings.warn( f'`{old}` is deprecated, use `{new}` instead (it will be removed in a future ' 'version of Lightweaver). Run `python -m lightweaver.migrate <files>` to update ' 'scripts automatically.', LightweaverDeprecationWarning, stacklevel=stacklevel + 1, )
[docs] def silence_deprecations(): """ Silence the warnings for deprecated Lightweaver names. """ warnings.filterwarnings('ignore', category=LightweaverDeprecationWarning)
[docs] def remap_old_keys(d: dict, stacklevel: int = 2, renames: Dict[str, str] = RENAMES) -> dict: """ Return a copy of a keyword dict (e.g. the `kwargs` of a `Context` state dict) with deprecated keys renamed (with a warning). If both an old key and its replacement are present, the old key's value wins: it can only have been added by the caller to override the stored value (e.g. ``dict(sd['kwargs'], formalSolver=...)``). `renames` is the old -> new table to apply (default: `RENAMES`). """ result = dict(d) for old in [k for k in result if k in renames]: new = renames[old] _warn(old, new, stacklevel=stacklevel + 1) result[new] = result.pop(old) return result
def _param_names(func: Callable) -> Set[str]: try: return set(inspect.signature(func).parameters) except (TypeError, ValueError): code = func.__code__ return set(code.co_varnames[: code.co_argcount + code.co_kwonlyargcount])
[docs] def accepts_old_kwargs(func: F = None, *, name: str = None) -> F: """ Decorator remapping deprecated keyword arguments to their new names (with a warning). Passing both the old and new name raises a `TypeError`. Parameters ---------- func : callable The function to wrap. name : str, optional The name to register for `lightweaver.migrate` (default: the function's `__name__`, or the class name for `__init__`). """ if func is None: return functools.partial(accepts_old_kwargs, name=name) params = _param_names(func) mapping = { old: new for new in params if new in _OLD_NAMES for old in _OLD_NAMES[new] if old not in params } if not mapping: return func if name is None: name = func.__name__ if name == '__init__': name = func.__qualname__.split('.')[-2] KWARG_REGISTRY[name].update(mapping) @functools.wraps(func) def wrapper(*args, **kwargs): for old in mapping.keys() & kwargs.keys(): new = mapping[old] if new in kwargs: raise TypeError( f'{func.__qualname__}() got both `{old}` and its replacement `{new}`' ) _warn(old, new, stacklevel=2) kwargs[new] = kwargs.pop(old) return func(*args, **kwargs) wrapper.__lw_renamed_kwargs__ = mapping return wrapper
[docs] class deprecated_alias: # noqa: N801 (descriptor, lowercase like `property`) """ Descriptor forwarding a deprecated attribute name to its replacement (with a warning) for reading, writing and deleting. """ def __init__(self, new: str): self.new = new self.old = None def __set_name__(self, owner, name): self.old = name def __get__(self, obj, objtype=None): if obj is None: return self _warn(self.old, self.new, stacklevel=2) return getattr(obj, self.new) def __set__(self, obj, value): _warn(self.old, self.new, stacklevel=2) setattr(obj, self.new, value) def __delete__(self, obj): _warn(self.old, self.new, stacklevel=2) delattr(obj, self.new)
[docs] def deprecated_names(cls: T = None, *, attrs: Iterable[str] = ()) -> T: """ Class decorator providing the deprecated names for a class: every method (including `__init__`) with renamed parameters accepts the old keyword names, and old-name aliases are added for renamed dataclass fields, properties, methods, and the instance attributes listed in `attrs`. For dataclasses, apply it above `@dataclass`. It must be applied to each class in a hierarchy that defines (or, for dataclasses, generates) methods. Parameters ---------- cls : type The class to decorate. attrs : iterable of str, optional New names of plain instance attributes (set in `__init__`) to alias. """ if cls is None: return functools.partial(deprecated_names, attrs=attrs) for key, value in list(vars(cls).items()): if isinstance(value, (staticmethod, classmethod)): wrapped = accepts_old_kwargs( value.__func__, name=cls.__name__ if key == '__init__' else key ) if wrapped is not value.__func__: setattr(cls, key, type(value)(wrapped)) elif inspect.isfunction(value): wrapped = accepts_old_kwargs(value, name=cls.__name__ if key == '__init__' else key) if wrapped is not value: setattr(cls, key, wrapped) names = set(attrs) if dataclasses.is_dataclass(cls): names.update(f.name for f in dataclasses.fields(cls)) names.update( k for k, v in vars(cls).items() if isinstance(v, property) or inspect.isfunction(v) ) for new in names: for old in _OLD_NAMES.get(new, ()): if old not in vars(cls): alias = deprecated_alias(new) setattr(cls, old, alias) alias.__set_name__(cls, old) return cls