diff --git a/.flake8 b/.flake8 index f08feffa7a55..60d0a5518674 100644 --- a/.flake8 +++ b/.flake8 @@ -58,6 +58,7 @@ per-file-ignores = lib/matplotlib/backends/backend_*.py: F401 lib/matplotlib/backends/qt_editor/formlayout.py: F401, F403 lib/matplotlib/cbook/__init__.py: F401 + lib/matplotlib/cbook/deprecation.py: F401 lib/matplotlib/font_manager.py: E221, E251, E501 lib/matplotlib/image.py: F401, F403 lib/matplotlib/lines.py: F401 diff --git a/doc/api/_api_api.rst b/doc/api/_api_api.rst new file mode 100644 index 000000000000..a41af9009bcf --- /dev/null +++ b/doc/api/_api_api.rst @@ -0,0 +1,13 @@ +******************* +``matplotlib._api`` +******************* + +.. automodule:: matplotlib._api + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: matplotlib._api.deprecation + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/cbook_api.rst b/doc/api/cbook_api.rst index f434fb9c6a26..4c8ef9cc50fa 100644 --- a/doc/api/cbook_api.rst +++ b/doc/api/cbook_api.rst @@ -6,8 +6,3 @@ :members: :undoc-members: :show-inheritance: - -.. automodule:: matplotlib.cbook.deprecation - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/index.rst b/doc/api/index.rst index cd1d120335ce..dba86c35ad0a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -126,6 +126,7 @@ Matplotlib consists of the following submodules: type1font.rst units_api.rst widgets_api.rst + _api_api.rst Toolkits -------- diff --git a/doc/api/next_api_changes/deprecations/18657-TH.rst b/doc/api/next_api_changes/deprecations/18657-TH.rst new file mode 100644 index 000000000000..6465b52f0074 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18657-TH.rst @@ -0,0 +1,3 @@ +``matplotlib.cbook.deprecation`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The module is considered internal and will be removed from the public API. diff --git a/doc/api/prev_api_changes/api_changes_3.0.0.rst b/doc/api/prev_api_changes/api_changes_3.0.0.rst index b0153c50d884..a6224c352179 100644 --- a/doc/api/prev_api_changes/api_changes_3.0.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.0.0.rst @@ -405,7 +405,7 @@ The following classes, methods, functions, and attributes are deprecated: - ``textpath.TextToPath.tex_font_map`` - ``matplotlib.cbook.deprecation.mplDeprecation`` will be removed in future versions. It is just an alias for - :class:`matplotlib.cbook.deprecation.MatplotlibDeprecationWarning`. Please + ``matplotlib.cbook.deprecation.MatplotlibDeprecationWarning``. Please use ``matplotlib.cbook.MatplotlibDeprecationWarning`` directly if necessary. - The ``matplotlib.cbook.Bunch`` class has been deprecated. Instead, use `types.SimpleNamespace` from the standard library which provides the same diff --git a/doc/api/prev_api_changes/api_changes_3.1.0.rst b/doc/api/prev_api_changes/api_changes_3.1.0.rst index 0504f954ca2e..f3737889841f 100644 --- a/doc/api/prev_api_changes/api_changes_3.1.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.1.0.rst @@ -767,12 +767,12 @@ The following signature related behaviours are deprecated: keyword. - The *interp_at_native* parameter to `.BboxImage`, which has had no effect since Matplotlib 2.0, is deprecated. -- All arguments to the `~.cbook.deprecation.deprecated` decorator and - `~.cbook.deprecation.warn_deprecated` function, except the first one (the - version where the deprecation occurred), are now keyword-only. The goal is - to avoid accidentally setting the "message" argument when the "name" (or - "alternative") argument was intended, as this has repeatedly occurred in the - past. +- All arguments to the ``matplotlib.cbook.deprecation.deprecated`` decorator + and ``matplotlib.cbook.deprecation.warn_deprecated`` function, except the + first one (the version where the deprecation occurred), are now keyword-only. + The goal is to avoid accidentally setting the "message" argument when the + "name" (or "alternative") argument was intended, as this has repeatedly + occurred in the past. - The arguments of `matplotlib.testing.compare.calculate_rms` have been renamed from ``expectedImage, actualImage``, to ``expected_image, actual_image``. - Passing positional arguments to `.Axis.set_ticklabels` beyond *ticklabels* @@ -1076,8 +1076,8 @@ Undeprecations -------------- The following API elements have been un-deprecated: -- The *obj_type* keyword argument to the `~.cbook.deprecation.deprecated` - decorator. +- The *obj_type* keyword argument to the + ``matplotlib.cbook.deprecation.deprecated`` decorator. - *xmin*, *xmax* keyword arguments to `.Axes.set_xlim` and *ymin*, *ymax* keyword arguments to `.Axes.set_ylim` diff --git a/doc/devel/contributing.rst b/doc/devel/contributing.rst index 9a8cf50ab546..021d46190f3b 100644 --- a/doc/devel/contributing.rst +++ b/doc/devel/contributing.rst @@ -511,7 +511,7 @@ There are five levels at which you can emit messages. - `logging.critical` and `logging.error` are really only there for errors that will end the use of the library but not kill the interpreter. -- `logging.warning` and `.cbook._warn_external` are used to warn the user, +- `logging.warning` and `._api.warn_external` are used to warn the user, see below. - `logging.info` is for information that the user may want to know if the program behaves oddly. They are not displayed by default. For instance, if @@ -527,16 +527,16 @@ By default, `logging` displays all log messages at levels higher than ``logging.WARNING`` to `sys.stderr`. The `logging tutorial`_ suggests that the difference between `logging.warning` -and `.cbook._warn_external` (which uses `warnings.warn`) is that -`.cbook._warn_external` should be used for things the user must change to stop +and `._api.warn_external` (which uses `warnings.warn`) is that +`._api.warn_external` should be used for things the user must change to stop the warning (typically in the source), whereas `logging.warning` can be more -persistent. Moreover, note that `.cbook._warn_external` will by default only +persistent. Moreover, note that `._api.warn_external` will by default only emit a given warning *once* for each line of user code, whereas `logging.warning` will display the message every time it is called. By default, `warnings.warn` displays the line of code that has the ``warn`` call. This usually isn't more informative than the warning message itself. -Therefore, Matplotlib uses `.cbook._warn_external` which uses `warnings.warn`, +Therefore, Matplotlib uses `._api.warn_external` which uses `warnings.warn`, but goes up the stack and displays the first line of code outside of Matplotlib. For example, for the module:: @@ -559,13 +559,13 @@ will display:: UserWarning: Attempting to set identical bottom==top warnings.warn('Attempting to set identical bottom==top') -Modifying the module to use `.cbook._warn_external`:: +Modifying the module to use `._api.warn_external`:: - from matplotlib import cbook + from matplotlib import _api def set_range(bottom, top): if bottom == top: - cbook._warn_external('Attempting to set identical bottom==top') + _api.warn_external('Attempting to set identical bottom==top') and running the same script will display:: diff --git a/lib/matplotlib/_api.py b/lib/matplotlib/_api/__init__.py similarity index 70% rename from lib/matplotlib/_api.py rename to lib/matplotlib/_api/__init__.py index 856a7e0a063a..b2778410e0cd 100644 --- a/lib/matplotlib/_api.py +++ b/lib/matplotlib/_api/__init__.py @@ -1,4 +1,19 @@ +""" +Helper functions for managing the Matplotlib API. + +This documentation is only relevant for Matplotlib developers, not for users. + +.. warning: + + This module and its submodules are for internal use only. Do not use them + in your own code. We may change the API at any time with no warning. + +""" + import itertools +import re +import sys +import warnings def check_in_list(_values, *, _print_supported_values=True, **kwargs): @@ -93,3 +108,25 @@ def check_getitem(_mapping, **kwargs): raise ValueError( "{!r} is not a valid value for {}; supported values are {}" .format(v, k, ', '.join(map(repr, mapping)))) from None + + +def warn_external(message, category=None): + """ + `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". + + The original emitter of the warning can be obtained by patching this + function back to `warnings.warn`, i.e. ``_api.warn_external = + warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, + etc.). + """ + frame = sys._getframe() + for stacklevel in itertools.count(1): # lgtm[py/unused-loop-variable] + if frame is None: + # when called in embedded context may hit frame is None + break + if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", + # Work around sphinx-gallery not setting __name__. + frame.f_globals.get("__name__", "")): + break + frame = frame.f_back + warnings.warn(message, category, stacklevel) diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py new file mode 100644 index 000000000000..c94b76c741c1 --- /dev/null +++ b/lib/matplotlib/_api/deprecation.py @@ -0,0 +1,523 @@ +""" +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 warnings + + +class MatplotlibDeprecationWarning(UserWarning): + """ + A class for issuing deprecation warnings for Matplotlib users. + + In light of the fact that Python builtin DeprecationWarnings are ignored + by default as of Python 2.7 (see link below), this class was put in to + allow for the signaling of deprecation, but via UserWarnings which are not + ignored by default. + + https://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x + """ + + +# mplDeprecation is deprecated. Use MatplotlibDeprecationWarning instead. +mplDeprecation = MatplotlibDeprecationWarning + + +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: + if removal: + removal = "in {}".format(removal) + else: + removal = {"2.2": "in 3.1", "3.0": "in 3.2", "3.1": "in 3.3"}.get( + since, "two minor releases later") + if not message: + message = ( + "\nThe %(name)s %(obj_type)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 + -------- + Basic example:: + + # 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 + ---------- + 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 used in the deprecation message; if not provided, the name + is automatically determined from 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; by default, 'class' if decorating + a class, 'attribute' if decorating a property, 'function' otherwise. + + 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 + -------- + Basic example:: + + @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): + + 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): + obj_type = "attribute" + func = None + name = name or obj.fget.__name__ + old_doc = obj.__doc__ + + class _deprecated_property(property): + def __get__(self, instance, owner): + if instance is not None: + 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 == "": + 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-----' + new_doc = (f"[*Deprecated*] {old_doc}\n" + f"{notes_header if notes_header not in old_doc else ''}\n" + f".. deprecated:: {since}\n" + f" {message.strip()}") + + 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. + + 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 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*. + """ + + 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}")), name=name)) + + +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 + -------- + :: + + @_rename_parameter("3.1", "bad_name", "good_name") + def func(good_name): ... + """ + + if func is None: + return functools.partial(_rename_parameter, since, old, new) + + 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. + + return wrapper + + +class _deprecated_parameter_class: + def __repr__(self): + return "" + + +_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 + -------- + :: + + @_delete_parameter("3.1", "unused") + def func(used_arg, other_arg, unused, more_args): ... + """ + + if func is None: + return functools.partial(_delete_parameter, since, name, **kwargs) + + 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: + func.__signature__ = signature = signature.replace(parameters=[ + param.replace(default=_deprecated_parameter) + if param.name == name else param + for param in signature.parameters.values()]) + else: + is_varargs = is_varkwargs = False + 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): + 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) + + 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. + """ + + if func is None: + return functools.partial(_make_keyword_only, since, name) + + 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] + kwonly = [name for name in names[names.index(name):] + if signature.parameters[name].kind == POK] + func.__signature__ = signature.replace(parameters=[ + param.replace(kind=KWO) if param.name in kwonly else param + for param in signature.parameters.values()]) + + @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). + idx = [*func.__signature__.parameters].index(name) + if len(args) > 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) + + 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 diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index b34c98f66b0f..d673f6597a67 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -29,7 +29,8 @@ import matplotlib from matplotlib import _c_internal_utils -from .deprecation import ( +from matplotlib._api import warn_external as _warn_external +from matplotlib._api.deprecation import ( deprecated, warn_deprecated, _rename_parameter, _delete_parameter, _make_keyword_only, _deprecate_method_override, _deprecate_privatize_attribute, @@ -2096,30 +2097,6 @@ def _setattr_cm(obj, **kwargs): setattr(obj, attr, orig) -def _warn_external(message, category=None): - """ - `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". - - The original emitter of the warning can be obtained by patching this - function back to `warnings.warn`, i.e. ``cbook._warn_external = - warnings.warn`` (or ``functools.partial(warnings.warn, stacklevel=2)``, - etc.). - - :meta public: - """ - frame = sys._getframe() - for stacklevel in itertools.count(1): # lgtm[py/unused-loop-variable] - if frame is None: - # when called in embedded context may hit frame is None - break - if not re.match(r"\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))", - # Work around sphinx-gallery not setting __name__. - frame.f_globals.get("__name__", "")): - break - frame = frame.f_back - warnings.warn(message, category, stacklevel) - - class _OrderedSet(collections.abc.MutableSet): def __init__(self): self._od = collections.OrderedDict() diff --git a/lib/matplotlib/cbook/deprecation.py b/lib/matplotlib/cbook/deprecation.py index 2c0a5bb72ecd..ba15795fda0f 100644 --- a/lib/matplotlib/cbook/deprecation.py +++ b/lib/matplotlib/cbook/deprecation.py @@ -1,511 +1,7 @@ -import contextlib -import functools -import inspect -import warnings +# imports are for backward compatibility +from matplotlib._api.deprecation import ( + MatplotlibDeprecationWarning, mplDeprecation, warn_deprecated, deprecated) - -class MatplotlibDeprecationWarning(UserWarning): - """ - A class for issuing deprecation warnings for Matplotlib users. - - In light of the fact that Python builtin DeprecationWarnings are ignored - by default as of Python 2.7 (see link below), this class was put in to - allow for the signaling of deprecation, but via UserWarnings which are not - ignored by default. - - https://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x - """ - - -# mplDeprecation is deprecated. Use MatplotlibDeprecationWarning instead. -mplDeprecation = MatplotlibDeprecationWarning - - -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: - if removal: - removal = "in {}".format(removal) - else: - removal = {"2.2": "in 3.1", "3.0": "in 3.2", "3.1": "in 3.3"}.get( - since, "two minor releases later") - if not message: - message = ( - "\nThe %(name)s %(obj_type)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 - -------- - Basic example:: - - # 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 - ---------- - 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 used in the deprecation message; if not provided, the name - is automatically determined from 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; by default, 'class' if decorating - a class, 'attribute' if decorating a property, 'function' otherwise. - - 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 - -------- - Basic example:: - - @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): - - 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): - obj_type = "attribute" - func = None - name = name or obj.fget.__name__ - old_doc = obj.__doc__ - - class _deprecated_property(property): - def __get__(self, instance, owner): - if instance is not None: - 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 == "": - 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-----' - new_doc = (f"[*Deprecated*] {old_doc}\n" - f"{notes_header if notes_header not in old_doc else ''}\n" - f".. deprecated:: {since}\n" - f" {message.strip()}") - - 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. - - 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 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*. - """ - - 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}")), name=name)) - - -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 - -------- - :: - - @_rename_parameter("3.1", "bad_name", "good_name") - def func(good_name): ... - """ - - if func is None: - return functools.partial(_rename_parameter, since, old, new) - - 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. - - return wrapper - - -class _deprecated_parameter_class: - def __repr__(self): - return "" - - -_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 - -------- - :: - - @_delete_parameter("3.1", "unused") - def func(used_arg, other_arg, unused, more_args): ... - """ - - if func is None: - return functools.partial(_delete_parameter, since, name, **kwargs) - - 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: - func.__signature__ = signature = signature.replace(parameters=[ - param.replace(default=_deprecated_parameter) - if param.name == name else param - for param in signature.parameters.values()]) - else: - is_varargs = is_varkwargs = False - 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): - 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) - - 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. - """ - - if func is None: - return functools.partial(_make_keyword_only, since, name) - - 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] - kwonly = [name for name in names[names.index(name):] - if signature.parameters[name].kind == POK] - func.__signature__ = signature.replace(parameters=[ - param.replace(kind=KWO) if param.name in kwonly else param - for param in signature.parameters.values()]) - - @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). - idx = [*func.__signature__.parameters].index(name) - if len(args) > 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) - - 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 +warn_deprecated("3.4", + "The module matplotlib.cbook.deprecation is considered " + "internal and it will be made private in the future.") diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index e87dd728440d..ab06984e9479 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -24,7 +24,6 @@ from matplotlib import _api, colors, cbook from matplotlib._cm import datad from matplotlib._cm_listed import cmaps as cmaps_listed -from matplotlib.cbook import _warn_external LUTSIZE = mpl.rcParams['image.lut'] @@ -150,7 +149,7 @@ def register_cmap(name=None, cmap=None, *, override_builtin=False): raise ValueError(msg) else: msg = f"Trying to register the cmap {name!r} which already exists." - _warn_external(msg) + _api.warn_external(msg) if not isinstance(cmap, colors.Colormap): raise ValueError("You must pass a Colormap instance. " diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 3338fb3de44c..d62b54015c47 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -9,7 +9,7 @@ import numpy as np import matplotlib as mpl -from . import (artist, cbook, colors, docstring, hatch as mhatch, +from . import (_api, artist, cbook, colors, docstring, hatch as mhatch, lines as mlines, transforms) from .bezier import ( NonIntersectingPathException, get_cos_sin, get_intersection, @@ -2007,8 +2007,8 @@ def __init_subclass__(cls): @cbook._delete_parameter("3.4", "mutation_aspect") def call_wrapper( self, x0, y0, width, height, mutation_size, - mutation_aspect=cbook.deprecation._deprecated_parameter): - if mutation_aspect is cbook.deprecation._deprecated_parameter: + mutation_aspect=_api.deprecation._deprecated_parameter): + if mutation_aspect is _api.deprecation._deprecated_parameter: # Don't trigger deprecation warning internally. return __call__(self, x0, y0, width, height, mutation_size) else: diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 04feb22a94ab..935d364ff461 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -35,6 +35,7 @@ import matplotlib import matplotlib.colorbar import matplotlib.image +from matplotlib import _api from matplotlib import rcsetup, style from matplotlib import _pylab_helpers, interactive from matplotlib import cbook @@ -2986,8 +2987,8 @@ def quiverkey(Q, X, Y, U, label, **kw): def scatter( x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, - verts=cbook.deprecation._deprecated_parameter, - edgecolors=None, *, plotnonfinite=False, data=None, **kwargs): + verts=_api.deprecation._deprecated_parameter, edgecolors=None, + *, plotnonfinite=False, data=None, **kwargs): __ret = gca().scatter( x, y, s=s, c=c, marker=marker, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax, alpha=alpha, linewidths=linewidths, diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 357fc2bb50c6..8158333cfd35 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -11,6 +11,7 @@ assert_array_almost_equal) import pytest +from matplotlib import _api import matplotlib.cbook as cbook import matplotlib.colors as mcolors from matplotlib.cbook import MatplotlibDeprecationWarning, delete_masked_points @@ -618,7 +619,7 @@ def func2(**kwargs): with pytest.warns(MatplotlibDeprecationWarning): func(foo="bar") - def pyplot_wrapper(foo=cbook.deprecation._deprecated_parameter): + def pyplot_wrapper(foo=_api.deprecation._deprecated_parameter): func1(foo) pyplot_wrapper() # No warning. diff --git a/tools/boilerplate.py b/tools/boilerplate.py index fa75a1403c8c..477665625085 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -22,7 +22,7 @@ # This line imports the installed copy of matplotlib, and not the local copy. import numpy as np -from matplotlib import cbook, mlab +from matplotlib import _api, mlab from matplotlib.axes import Axes from matplotlib.figure import Figure @@ -74,8 +74,8 @@ def __init__(self, value): self._repr = "mlab.window_hanning" elif value is np.mean: self._repr = "np.mean" - elif value is cbook.deprecation._deprecated_parameter: - self._repr = "cbook.deprecation._deprecated_parameter" + elif value is _api.deprecation._deprecated_parameter: + self._repr = "_api.deprecation._deprecated_parameter" elif isinstance(value, Enum): # Enum str is Class.Name whereas their repr is . self._repr = str(value)