Source code for manim.utils.deprecation

"""Decorators for deprecating classes, functions and function parameters."""

from __future__ import annotations

__all__ = ["deprecated", "deprecated_params"]


import inspect
import re
from typing import Any, Callable, Iterable

from decorator import decorate, decorator

from .. import logger


def _get_callable_info(callable: Callable) -> tuple[str, str]:
    """Returns type and name of a callable.

    Parameters
    ----------
    callable
        The callable

    Returns
    -------
    Tuple[str, str]
        The type and name of the callable. Type can can be one of "class", "method" (for
        functions defined in classes) or "function"). For methods, name is Class.method.
    """
    what = type(callable).__name__
    name = callable.__qualname__
    if what == "function" and "." in name:
        what = "method"
    elif what != "function":
        what = "class"
    return (what, name)


def _deprecation_text_component(
    since: str | None,
    until: str | None,
    message: str,
) -> str:
    """Generates a text component used in deprecation messages.

    Parameters
    ----------
    since
        The version or date since deprecation
    until
        The version or date until removal of the deprecated callable
    message
        The reason for why the callable has been deprecated

    Returns
    -------
    str
        The deprecation message text component.
    """
    since = f"since {since} " if since else ""
    until = (
        f"is expected to be removed after {until}"
        if until
        else "may be removed in a later version"
    )
    msg = " " + message if message else ""
    return f"deprecated {since}and {until}.{msg}"


[docs]def deprecated( func: Callable = None, since: str | None = None, until: str | None = None, replacement: str | None = None, message: str | None = "", ) -> Callable: """Decorator to mark a callable as deprecated. The decorated callable will cause a warning when used. The docstring of the deprecated callable is adjusted to indicate that this callable is deprecated. Parameters ---------- func The function to be decorated. Should not be set by the user. since The version or date since deprecation. until The version or date until removal of the deprecated callable. replacement The identifier of the callable replacing the deprecated one. message The reason for why the callable has been deprecated. Returns ------- Callable The decorated callable. Examples -------- Basic usage:: from manim.utils.deprecation import deprecated @deprecated def foo(**kwargs): pass @deprecated class Bar: def __init__(self): pass @deprecated def baz(self): pass foo() # WARNING The function foo has been deprecated and may be removed in a later version. a = Bar() # WARNING The class Bar has been deprecated and may be removed in a later version. a.baz() # WARNING The method Bar.baz has been deprecated and may be removed in a later version. You can specify additional information for a more precise warning:: from manim.utils.deprecation import deprecated @deprecated( since="v0.2", until="v0.4", replacement="bar", message="It is cooler." ) def foo(): pass foo() # WARNING The function foo has been deprecated since v0.2 and is expected to be removed after v0.4. Use bar instead. It is cooler. You may also use dates instead of versions:: from manim.utils.deprecation import deprecated @deprecated(since="05/01/2021", until="06/01/2021") def foo(): pass foo() # WARNING The function foo has been deprecated since 05/01/2021 and is expected to be removed after 06/01/2021. """ # If used as factory: if func is None: return lambda func: deprecated(func, since, until, replacement, message) what, name = _get_callable_info(func) def warning_msg(for_docs: bool = False) -> str: """Generate the deprecation warning message. Parameters ---------- for_docs Whether or not to format the message for use in documentation. Returns ------- str The deprecation message. """ msg = message if replacement is not None: repl = replacement if for_docs: mapper = {"class": "class", "method": "meth", "function": "func"} repl = f":{mapper[what]}:`~.{replacement}`" msg = f"Use {repl} instead.{' ' + message if message else ''}" deprecated = _deprecation_text_component(since, until, msg) return f"The {what} {name} has been {deprecated}" def deprecate_docs(func: Callable): """Adjust docstring to indicate the deprecation. Parameters ---------- func The callable whose docstring to adjust. """ warning = warning_msg(True) doc_string = func.__doc__ or "" func.__doc__ = f"{doc_string}\n\n.. attention:: Deprecated\n {warning}" def deprecate(func: Callable, *args, **kwargs): """The actual decorator used to extend the callables behavior. Logs a warning message. Parameters ---------- func The callable to decorate. args The arguments passed to the given callable. kwargs The keyword arguments passed to the given callable. Returns ------- Any The return value of the given callable when being passed the given arguments. """ logger.warning(warning_msg()) return func(*args, **kwargs) if type(func).__name__ != "function": deprecate_docs(func) func.__init__ = decorate(func.__init__, deprecate) return func func = decorate(func, deprecate) deprecate_docs(func) return func
[docs]def deprecated_params( params: str | Iterable[str] | None = None, since: str | None = None, until: str | None = None, message: str | None = "", redirections: None | (Iterable[tuple[str, str] | Callable[..., dict[str, Any]]]) = None, ) -> Callable: """Decorator to mark parameters of a callable as deprecated. It can also be used to automatically redirect deprecated parameter values to their replacements. Parameters ---------- params The parameters to be deprecated. Can consist of: * An iterable of strings, with each element representing a parameter to deprecate * A single string, with parameter names separated by commas or spaces. since The version or date since deprecation. until The version or date until removal of the deprecated callable. message The reason for why the callable has been deprecated. redirections A list of parameter redirections. Each redirection can be one of the following: * A tuple of two strings. The first string defines the name of the deprecated parameter; the second string defines the name of the parameter to redirect to, when attempting to use the first string. * A function performing the mapping operation. The parameter names of the function determine which parameters are used as input. The function must return a dictionary which contains the redirected arguments. Redirected parameters are also implicitly deprecated. Returns ------- Callable The decorated callable. Raises ------ ValueError If no parameters are defined (neither explicitly nor implicitly). ValueError If defined parameters are invalid python identifiers. Examples -------- Basic usage:: from manim.utils.deprecation import deprecated_params @deprecated_params(params="a, b, c") def foo(**kwargs): pass foo(x=2, y=3, z=4) # No warning foo(a=2, b=3, z=4) # WARNING The parameters a and b of method foo have been deprecated and may be removed in a later version. You can also specify additional information for a more precise warning:: from manim.utils.deprecation import deprecated_params @deprecated_params( params="a, b, c", since="v0.2", until="v0.4", message="The letters x, y, z are cooler." ) def foo(**kwargs): pass foo(a=2) # WARNING The parameter a of method foo has been deprecated since v0.2 and is expected to be removed after v0.4. The letters x, y, z are cooler. Basic parameter redirection:: from manim.utils.deprecation import deprecated_params @deprecated_params(redirections=[ # Two ways to redirect one parameter to another: ("old_param", "new_param"), lambda old_param2: {"new_param22": old_param2} ]) def foo(**kwargs): return kwargs foo(x=1, old_param=2) # WARNING The parameter old_param of method foo has been deprecated and may be removed in a later version. # returns {"x": 1, "new_param": 2} Redirecting using a calculated value:: from manim.utils.deprecation import deprecated_params @deprecated_params(redirections=[ lambda runtime_in_ms: {"run_time": runtime_in_ms / 1000} ]) def foo(**kwargs): return kwargs foo(runtime_in_ms=500) # WARNING The parameter runtime_in_ms of method foo has been deprecated and may be removed in a later version. # returns {"run_time": 0.5} Redirecting multiple parameter values to one:: from manim.utils.deprecation import deprecated_params @deprecated_params(redirections=[ lambda buff_x=1, buff_y=1: {"buff": (buff_x, buff_y)} ]) def foo(**kwargs): return kwargs foo(buff_x=2) # WARNING The parameter buff_x of method foo has been deprecated and may be removed in a later version. # returns {"buff": (2, 1)} Redirect one parameter to multiple:: from manim.utils.deprecation import deprecated_params @deprecated_params(redirections=[ lambda buff=1: {"buff_x": buff[0], "buff_y": buff[1]} if isinstance(buff, tuple) else {"buff_x": buff, "buff_y": buff} ]) def foo(**kwargs): return kwargs foo(buff=0) # WARNING The parameter buff of method foo has been deprecated and may be removed in a later version. # returns {"buff_x": 0, buff_y: 0} foo(buff=(1,2)) # WARNING The parameter buff of method foo has been deprecated and may be removed in a later version. # returns {"buff_x": 1, buff_y: 2} """ # Check if decorator is used without parenthesis if callable(params): raise ValueError("deprecate_parameters requires arguments to be specified.") if params is None: params = [] # Construct params list params = re.split(r"[,\s]+", params) if isinstance(params, str) else list(params) # Add params which are only implicitly given via redirections if redirections is None: redirections = [] for redirector in redirections: if isinstance(redirector, tuple): params.append(redirector[0]) else: params.extend(list(inspect.signature(redirector).parameters)) # Keep ordering of params so that warning message is consistently the same # This will also help pass unit testing params = list(dict.fromkeys(params)) # Make sure params only contains valid identifiers identifier = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) if not all(re.match(identifier, param) for param in params): raise ValueError("Given parameter values are invalid.") redirections = list(redirections) def warning_msg(func: Callable, used: list[str]): """Generate the deprecation warning message. Parameters ---------- func The callable with deprecated parameters. used The list of deprecated parameters used in a call. Returns ------- str The deprecation message. """ what, name = _get_callable_info(func) plural = len(used) > 1 parameter_s = "s" if plural else "" used_ = ", ".join(used[:-1]) + " and " + used[-1] if plural else used[0] has_have_been = "have been" if plural else "has been" deprecated = _deprecation_text_component(since, until, message) return f"The parameter{parameter_s} {used_} of {what} {name} {has_have_been} {deprecated}" def redirect_params(kwargs: dict, used: list[str]): """Adjust the keyword arguments as defined by the redirections. Parameters ---------- kwargs The keyword argument dictionary to be updated. used The list of deprecated parameters used in a call. """ for redirector in redirections: if isinstance(redirector, tuple): old_param, new_param = redirector if old_param in used: kwargs[new_param] = kwargs.pop(old_param) else: redirector_params = list(inspect.signature(redirector).parameters) redirector_args = {} for redirector_param in redirector_params: if redirector_param in used: redirector_args[redirector_param] = kwargs.pop(redirector_param) if len(redirector_args) > 0: kwargs.update(redirector(**redirector_args)) def deprecate_params(func, *args, **kwargs): """The actual decorator function used to extend the callables behavior. Logs a warning message when a deprecated parameter is used and redirects it if specified. Parameters ---------- func The callable to decorate. args The arguments passed to the given callable. kwargs The keyword arguments passed to the given callable. Returns ------- Any The return value of the given callable when being passed the given arguments. """ used = [] for param in params: if param in kwargs: used.append(param) if len(used) > 0: logger.warning(warning_msg(func, used)) redirect_params(kwargs, used) return func(*args, **kwargs) return decorator(deprecate_params)