123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- """
- Helper functions for deprecating parts of the Matplotlib API.
- This documentation is only relevant for Matplotlib developers, not for users.
- .. warning::
- This module is for internal use only. Do not use it in your own code.
- We may change the API at any time with no warning.
- """
- import contextlib
- import functools
- import inspect
- import math
- import warnings
- class MatplotlibDeprecationWarning(DeprecationWarning):
- """A class for issuing deprecation warnings for Matplotlib users."""
- def _generate_deprecation_warning(
- since, message='', name='', alternative='', pending=False, obj_type='',
- addendum='', *, removal=''):
- if pending:
- if removal:
- raise ValueError(
- "A pending deprecation cannot have a scheduled removal")
- else:
- removal = f"in {removal}" if removal else "two minor releases later"
- if not message:
- message = (
- ("The %(name)s %(obj_type)s" if obj_type else "%(name)s")
- + (" will be deprecated in a future version"
- if pending else
- (" was deprecated in Matplotlib %(since)s"
- + (" and will be removed %(removal)s" if removal else "")))
- + "."
- + (" Use %(alternative)s instead." if alternative else "")
- + (" %(addendum)s" if addendum else ""))
- warning_cls = (PendingDeprecationWarning if pending
- else MatplotlibDeprecationWarning)
- return warning_cls(message % dict(
- func=name, name=name, obj_type=obj_type, since=since, removal=removal,
- alternative=alternative, addendum=addendum))
- def warn_deprecated(
- since, *, message='', name='', alternative='', pending=False,
- obj_type='', addendum='', removal=''):
- """
- Display a standardized deprecation.
- Parameters
- ----------
- since : str
- The release at which this API became deprecated.
- message : str, optional
- Override the default deprecation message. The ``%(since)s``,
- ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``,
- and ``%(removal)s`` format specifiers will be replaced by the values
- of the respective arguments passed to this function.
- name : str, optional
- The name of the deprecated object.
- alternative : str, optional
- An alternative API that the user may use in place of the deprecated
- API. The deprecation warning will tell the user about this alternative
- if provided.
- pending : bool, optional
- If True, uses a PendingDeprecationWarning instead of a
- DeprecationWarning. Cannot be used together with *removal*.
- obj_type : str, optional
- The object type being deprecated.
- addendum : str, optional
- Additional text appended directly to the final message.
- removal : str, optional
- The expected removal version. With the default (an empty string), a
- removal version is automatically computed from *since*. Set to other
- Falsy values to not schedule a removal date. Cannot be used together
- with *pending*.
- Examples
- --------
- ::
- # To warn of the deprecation of "matplotlib.name_of_module"
- warn_deprecated('1.4.0', name='matplotlib.name_of_module',
- obj_type='module')
- """
- warning = _generate_deprecation_warning(
- since, message, name, alternative, pending, obj_type, addendum,
- removal=removal)
- from . import warn_external
- warn_external(warning, category=MatplotlibDeprecationWarning)
- def deprecated(since, *, message='', name='', alternative='', pending=False,
- obj_type=None, addendum='', removal=''):
- """
- Decorator to mark a function, a class, or a property as deprecated.
- When deprecating a classmethod, a staticmethod, or a property, the
- ``@deprecated`` decorator should go *under* ``@classmethod`` and
- ``@staticmethod`` (i.e., `deprecated` should directly decorate the
- underlying callable), but *over* ``@property``.
- When deprecating a class ``C`` intended to be used as a base class in a
- multiple inheritance hierarchy, ``C`` *must* define an ``__init__`` method
- (if ``C`` instead inherited its ``__init__`` from its own base class, then
- ``@deprecated`` would mess up ``__init__`` inheritance when installing its
- own (deprecation-emitting) ``C.__init__``).
- Parameters are the same as for `warn_deprecated`, except that *obj_type*
- defaults to 'class' if decorating a class, 'attribute' if decorating a
- property, and 'function' otherwise.
- Examples
- --------
- ::
- @deprecated('1.4.0')
- def the_function_to_deprecate():
- pass
- """
- def deprecate(obj, message=message, name=name, alternative=alternative,
- pending=pending, obj_type=obj_type, addendum=addendum):
- from matplotlib._api import classproperty
- if isinstance(obj, type):
- if obj_type is None:
- obj_type = "class"
- func = obj.__init__
- name = name or obj.__name__
- old_doc = obj.__doc__
- def finalize(wrapper, new_doc):
- try:
- obj.__doc__ = new_doc
- except AttributeError: # Can't set on some extension objects.
- pass
- obj.__init__ = functools.wraps(obj.__init__)(wrapper)
- return obj
- elif isinstance(obj, (property, classproperty)):
- if obj_type is None:
- obj_type = "attribute"
- func = None
- name = name or obj.fget.__name__
- old_doc = obj.__doc__
- class _deprecated_property(type(obj)):
- def __get__(self, instance, owner=None):
- if instance is not None or owner is not None \
- and isinstance(self, classproperty):
- emit_warning()
- return super().__get__(instance, owner)
- def __set__(self, instance, value):
- if instance is not None:
- emit_warning()
- return super().__set__(instance, value)
- def __delete__(self, instance):
- if instance is not None:
- emit_warning()
- return super().__delete__(instance)
- def __set_name__(self, owner, set_name):
- nonlocal name
- if name == "<lambda>":
- name = set_name
- def finalize(_, new_doc):
- return _deprecated_property(
- fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc)
- else:
- if obj_type is None:
- obj_type = "function"
- func = obj
- name = name or obj.__name__
- old_doc = func.__doc__
- def finalize(wrapper, new_doc):
- wrapper = functools.wraps(func)(wrapper)
- wrapper.__doc__ = new_doc
- return wrapper
- def emit_warning():
- warn_deprecated(
- since, message=message, name=name, alternative=alternative,
- pending=pending, obj_type=obj_type, addendum=addendum,
- removal=removal)
- def wrapper(*args, **kwargs):
- emit_warning()
- return func(*args, **kwargs)
- old_doc = inspect.cleandoc(old_doc or '').strip('\n')
- notes_header = '\nNotes\n-----'
- second_arg = ' '.join([t.strip() for t in
- (message, f"Use {alternative} instead."
- if alternative else "", addendum) if t])
- new_doc = (f"[*Deprecated*] {old_doc}\n"
- f"{notes_header if notes_header not in old_doc else ''}\n"
- f".. deprecated:: {since}\n"
- f" {second_arg}")
- if not old_doc:
- # This is to prevent a spurious 'unexpected unindent' warning from
- # docutils when the original docstring was blank.
- new_doc += r'\ '
- return finalize(wrapper, new_doc)
- return deprecate
- class deprecate_privatize_attribute:
- """
- Helper to deprecate public access to an attribute (or method).
- This helper should only be used at class scope, as follows::
- class Foo:
- attr = _deprecate_privatize_attribute(*args, **kwargs)
- where *all* parameters are forwarded to `deprecated`. This form makes
- ``attr`` a property which forwards read and write access to ``self._attr``
- (same name but with a leading underscore), with a deprecation warning.
- Note that the attribute name is derived from *the name this helper is
- assigned to*. This helper also works for deprecating methods.
- """
- def __init__(self, *args, **kwargs):
- self.deprecator = deprecated(*args, **kwargs)
- def __set_name__(self, owner, name):
- setattr(owner, name, self.deprecator(
- property(lambda self: getattr(self, f"_{name}"),
- lambda self, value: setattr(self, f"_{name}", value)),
- name=name))
- # Used by _copy_docstring_and_deprecators to redecorate pyplot wrappers and
- # boilerplate.py to retrieve original signatures. It may seem natural to store
- # this information as an attribute on the wrapper, but if the wrapper gets
- # itself functools.wraps()ed, then such attributes are silently propagated to
- # the outer wrapper, which is not desired.
- DECORATORS = {}
- def rename_parameter(since, old, new, func=None):
- """
- Decorator indicating that parameter *old* of *func* is renamed to *new*.
- The actual implementation of *func* should use *new*, not *old*. If *old*
- is passed to *func*, a DeprecationWarning is emitted, and its value is
- used, even if *new* is also passed by keyword (this is to simplify pyplot
- wrapper functions, which always pass *new* explicitly to the Axes method).
- If *new* is also passed but positionally, a TypeError will be raised by the
- underlying function during argument binding.
- Examples
- --------
- ::
- @_api.rename_parameter("3.1", "bad_name", "good_name")
- def func(good_name): ...
- """
- decorator = functools.partial(rename_parameter, since, old, new)
- if func is None:
- return decorator
- signature = inspect.signature(func)
- assert old not in signature.parameters, (
- f"Matplotlib internal error: {old!r} cannot be a parameter for "
- f"{func.__name__}()")
- assert new in signature.parameters, (
- f"Matplotlib internal error: {new!r} must be a parameter for "
- f"{func.__name__}()")
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- if old in kwargs:
- warn_deprecated(
- since, message=f"The {old!r} parameter of {func.__name__}() "
- f"has been renamed {new!r} since Matplotlib {since}; support "
- f"for the old name will be dropped %(removal)s.")
- kwargs[new] = kwargs.pop(old)
- return func(*args, **kwargs)
- # wrapper() must keep the same documented signature as func(): if we
- # instead made both *old* and *new* appear in wrapper()'s signature, they
- # would both show up in the pyplot function for an Axes method as well and
- # pyplot would explicitly pass both arguments to the Axes method.
- DECORATORS[wrapper] = decorator
- return wrapper
- class _deprecated_parameter_class:
- def __repr__(self):
- return "<deprecated parameter>"
- _deprecated_parameter = _deprecated_parameter_class()
- def delete_parameter(since, name, func=None, **kwargs):
- """
- Decorator indicating that parameter *name* of *func* is being deprecated.
- The actual implementation of *func* should keep the *name* parameter in its
- signature, or accept a ``**kwargs`` argument (through which *name* would be
- passed).
- Parameters that come after the deprecated parameter effectively become
- keyword-only (as they cannot be passed positionally without triggering the
- DeprecationWarning on the deprecated parameter), and should be marked as
- such after the deprecation period has passed and the deprecated parameter
- is removed.
- Parameters other than *since*, *name*, and *func* are keyword-only and
- forwarded to `.warn_deprecated`.
- Examples
- --------
- ::
- @_api.delete_parameter("3.1", "unused")
- def func(used_arg, other_arg, unused, more_args): ...
- """
- decorator = functools.partial(delete_parameter, since, name, **kwargs)
- if func is None:
- return decorator
- signature = inspect.signature(func)
- # Name of `**kwargs` parameter of the decorated function, typically
- # "kwargs" if such a parameter exists, or None if the decorated function
- # doesn't accept `**kwargs`.
- kwargs_name = next((param.name for param in signature.parameters.values()
- if param.kind == inspect.Parameter.VAR_KEYWORD), None)
- if name in signature.parameters:
- kind = signature.parameters[name].kind
- is_varargs = kind is inspect.Parameter.VAR_POSITIONAL
- is_varkwargs = kind is inspect.Parameter.VAR_KEYWORD
- if not is_varargs and not is_varkwargs:
- name_idx = (
- # Deprecated parameter can't be passed positionally.
- math.inf if kind is inspect.Parameter.KEYWORD_ONLY
- # If call site has no more than this number of parameters, the
- # deprecated parameter can't have been passed positionally.
- else [*signature.parameters].index(name))
- func.__signature__ = signature = signature.replace(parameters=[
- param.replace(default=_deprecated_parameter)
- if param.name == name else param
- for param in signature.parameters.values()])
- else:
- name_idx = -1 # Deprecated parameter can always have been passed.
- else:
- is_varargs = is_varkwargs = False
- # Deprecated parameter can't be passed positionally.
- name_idx = math.inf
- assert kwargs_name, (
- f"Matplotlib internal error: {name!r} must be a parameter for "
- f"{func.__name__}()")
- addendum = kwargs.pop('addendum', None)
- @functools.wraps(func)
- def wrapper(*inner_args, **inner_kwargs):
- if len(inner_args) <= name_idx and name not in inner_kwargs:
- # Early return in the simple, non-deprecated case (much faster than
- # calling bind()).
- return func(*inner_args, **inner_kwargs)
- arguments = signature.bind(*inner_args, **inner_kwargs).arguments
- if is_varargs and arguments.get(name):
- warn_deprecated(
- since, message=f"Additional positional arguments to "
- f"{func.__name__}() are deprecated since %(since)s and "
- f"support for them will be removed %(removal)s.")
- elif is_varkwargs and arguments.get(name):
- warn_deprecated(
- since, message=f"Additional keyword arguments to "
- f"{func.__name__}() are deprecated since %(since)s and "
- f"support for them will be removed %(removal)s.")
- # We cannot just check `name not in arguments` because the pyplot
- # wrappers always pass all arguments explicitly.
- elif any(name in d and d[name] != _deprecated_parameter
- for d in [arguments, arguments.get(kwargs_name, {})]):
- deprecation_addendum = (
- f"If any parameter follows {name!r}, they should be passed as "
- f"keyword, not positionally.")
- warn_deprecated(
- since,
- name=repr(name),
- obj_type=f"parameter of {func.__name__}()",
- addendum=(addendum + " " + deprecation_addendum) if addendum
- else deprecation_addendum,
- **kwargs)
- return func(*inner_args, **inner_kwargs)
- DECORATORS[wrapper] = decorator
- return wrapper
- def make_keyword_only(since, name, func=None):
- """
- Decorator indicating that passing parameter *name* (or any of the following
- ones) positionally to *func* is being deprecated.
- When used on a method that has a pyplot wrapper, this should be the
- outermost decorator, so that :file:`boilerplate.py` can access the original
- signature.
- """
- decorator = functools.partial(make_keyword_only, since, name)
- if func is None:
- return decorator
- signature = inspect.signature(func)
- POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
- KWO = inspect.Parameter.KEYWORD_ONLY
- assert (name in signature.parameters
- and signature.parameters[name].kind == POK), (
- f"Matplotlib internal error: {name!r} must be a positional-or-keyword "
- f"parameter for {func.__name__}()")
- names = [*signature.parameters]
- name_idx = names.index(name)
- kwonly = [name for name in names[name_idx:]
- if signature.parameters[name].kind == POK]
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- # Don't use signature.bind here, as it would fail when stacked with
- # rename_parameter and an "old" argument name is passed in
- # (signature.bind would fail, but the actual call would succeed).
- if len(args) > name_idx:
- warn_deprecated(
- since, message="Passing the %(name)s %(obj_type)s "
- "positionally is deprecated since Matplotlib %(since)s; the "
- "parameter will become keyword-only %(removal)s.",
- name=name, obj_type=f"parameter of {func.__name__}()")
- return func(*args, **kwargs)
- # Don't modify *func*'s signature, as boilerplate.py needs it.
- wrapper.__signature__ = signature.replace(parameters=[
- param.replace(kind=KWO) if param.name in kwonly else param
- for param in signature.parameters.values()])
- DECORATORS[wrapper] = decorator
- return wrapper
- def deprecate_method_override(method, obj, *, allow_empty=False, **kwargs):
- """
- Return ``obj.method`` with a deprecation if it was overridden, else None.
- Parameters
- ----------
- method
- An unbound method, i.e. an expression of the form
- ``Class.method_name``. Remember that within the body of a method, one
- can always use ``__class__`` to refer to the class that is currently
- being defined.
- obj
- Either an object of the class where *method* is defined, or a subclass
- of that class.
- allow_empty : bool, default: False
- Whether to allow overrides by "empty" methods without emitting a
- warning.
- **kwargs
- Additional parameters passed to `warn_deprecated` to generate the
- deprecation warning; must at least include the "since" key.
- """
- def empty(): pass
- def empty_with_docstring(): """doc"""
- name = method.__name__
- bound_child = getattr(obj, name)
- bound_base = (
- method # If obj is a class, then we need to use unbound methods.
- if isinstance(bound_child, type(empty)) and isinstance(obj, type)
- else method.__get__(obj))
- if (bound_child != bound_base
- and (not allow_empty
- or (getattr(getattr(bound_child, "__code__", None),
- "co_code", None)
- not in [empty.__code__.co_code,
- empty_with_docstring.__code__.co_code]))):
- warn_deprecated(**{"name": name, "obj_type": "method", **kwargs})
- return bound_child
- return None
- @contextlib.contextmanager
- def suppress_matplotlib_deprecation_warning():
- with warnings.catch_warnings():
- warnings.simplefilter("ignore", MatplotlibDeprecationWarning)
- yield
|