Source code for pytes.deprecation

#
# Renamed names and their deprecation
#
"""
Renamed public names (DESIGN D17).

Every rename of a public name is one :class:`Rename` in :data:`RENAMES`. While its alias
is active, the old name still works and warns with :class:`PyTESDeprecationWarning`; once
the alias is retired, the old name raises an error that gives the new one. The entry is
never deleted, so that the error keeps pointing to the new name.

The kinds of names covered:

- ``"object"``: a class or function, by its full dotted path. The module that held the old
  name sets ``__getattr__ = module_getattr(__name__)``.
- ``"option"``: the name of a model option, checked by
  :meth:`pytes.BaseModel.process_options`.

Variable and parameter names are not covered yet (DESIGN §3): PyBaMM looks them up itself.

To silence the warnings, filter :class:`PyTESDeprecationWarning`; to find old names in your
own code, turn it into an error (``python -W error::DeprecationWarning``).
"""

import importlib
import sys
import warnings
from dataclasses import dataclass


[docs] class PyTESDeprecationWarning(DeprecationWarning): """A PyTES name was renamed; the old name still works, but will stop working"""
[docs] @dataclass(frozen=True) class Rename: """ A renamed public name. Parameters ---------- kind : str ``"object"`` or ``"option"`` (see :mod:`pytes.deprecation`). old : str The old name; for an object, its full dotted path. new : str The new name; for an object, its full dotted path. finding : str The entry in ``docs/design/findings.md`` that logs the rename, e.g. ``"F-050"``. renamed_in : str The release that renamed it. removed_in : str The release from which the old name no longer works. Equal to ``renamed_in`` for a rename without an alias. alias : bool, optional Whether the old name still works, with a warning (default). Set to False when a release reaches ``removed_in``; the tests fail until then. """ kind: str old: str new: str finding: str renamed_in: str removed_in: str alias: bool = True @property def message(self): """What happened to the old name and what to use instead""" label = f"option '{self.old}'" if self.kind == "option" else f"'{self.old}'" message = ( f"{label} was renamed to '{self.new}' in PyTES {self.renamed_in} " f"({self.finding})" ) if self.alias: message += f"; the old name will stop working in PyTES {self.removed_in}" return message
#: Every renamed public name, oldest first RENAMES = ( Rename( "option", "storage", "storage along flow path", finding="F-050", renamed_in="0.1.0a3", removed_in="0.1.0a3", alias=False, ), Rename( "object", "pytes.experiments.step.ConstantInputs", "pytes.experiments.step.Inlet", finding="F-054", renamed_in="0.1.0a3", removed_in="0.2.0", ), Rename( "object", "pytes.experiments.step.ConstantPower", "pytes.experiments.step.Power", finding="F-054", renamed_in="0.1.0a3", removed_in="0.2.0", ), Rename( "object", "pytes.submodels.LedgerWallCoupling", "pytes.submodels.WallCoupling", finding="F-054", renamed_in="0.1.0a3", removed_in="0.2.0", ), Rename( "object", "pytes.submodels.coupling.LedgerWallCoupling", "pytes.submodels.coupling.WallCoupling", finding="F-054", renamed_in="0.1.0a3", removed_in="0.2.0", ), Rename( "object", "pytes.submodels.FinPCMCoupling", "pytes.submodels.FinStorageCoupling", finding="F-054", renamed_in="0.1.0a3", removed_in="0.2.0", ), Rename( "object", "pytes.submodels.coupling.FinPCMCoupling", "pytes.submodels.coupling.FinStorageCoupling", finding="F-054", renamed_in="0.1.0a3", removed_in="0.2.0", ), )
[docs] def find(kind, old): """The :class:`Rename` of an old name of the given kind, or None""" for rename in RENAMES: if rename.kind == kind and rename.old == old: return rename return None
def _user_stacklevel(): """ The ``stacklevel`` of the first frame outside PyTES, for :func:`warnings.warn` called by :func:`check`, so that the warning points to the user's own line """ # Frame 2 is the caller of check, which is stacklevel 2 for a warning check issues frame, level = sys._getframe(2), 2 while frame.f_back is not None and frame.f_globals.get("__name__", "").startswith( "pytes." ): frame, level = frame.f_back, level + 1 return level
[docs] def check(rename, error): """ Warn that the old name is deprecated, or raise if its alias is retired. Parameters ---------- rename : :class:`Rename` error : type The exception to raise if the alias is retired. Raises ------ error If the alias is retired. """ if not rename.alias: raise error(rename.message) warnings.warn( rename.message, PyTESDeprecationWarning, stacklevel=_user_stacklevel() )
[docs] def module_getattr(module): """ A module-level ``__getattr__`` that resolves the module's renamed objects. Parameters ---------- module : str The module's ``__name__``. """ def __getattr__(name): rename = find("object", f"{module}.{name}") if rename is None: raise AttributeError(f"module {module!r} has no attribute {name!r}") check(rename, AttributeError) new_module, _, new_name = rename.new.rpartition(".") return getattr(importlib.import_module(new_module), new_name) return __getattr__