From c62f491cddf10c39a4820824e9212c955bbf2da2 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 13 Jan 2019 14:50:43 +0100 Subject: [PATCH] Decorator for deleting a parameter with a deprecation period. As an example application, deprecate the unused shape and imlim args to imshow() (unused since around 4d1107c (2006)). --- doc/api/next_api_changes/2018-01-10-AL.rst | 5 ++ lib/matplotlib/axes/_axes.py | 5 +- lib/matplotlib/cbook/__init__.py | 2 +- lib/matplotlib/cbook/deprecation.py | 57 ++++++++++++++++++++++ lib/matplotlib/pyplot.py | 3 +- lib/matplotlib/tests/test_image.py | 15 ++++++ tools/boilerplate.py | 4 +- 7 files changed, 85 insertions(+), 6 deletions(-) diff --git a/doc/api/next_api_changes/2018-01-10-AL.rst b/doc/api/next_api_changes/2018-01-10-AL.rst index 1c422ea59c2f..3807b764a27b 100644 --- a/doc/api/next_api_changes/2018-01-10-AL.rst +++ b/doc/api/next_api_changes/2018-01-10-AL.rst @@ -16,6 +16,11 @@ In each case, the old parameter name remains supported (it cannot be used simultaneously with the new name), but suppport for it will be dropped in Matplotlib 3.3. +- The unused ``shape`` and ``imlim`` parameters to `Axes.imshow` are + deprecated. To avoid triggering the deprecation warning, the ``filternorm``, + ``filterrad``, ``resample``, and ``url`` arguments should be passed by + keyword. + Deprecations ```````````` diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index cad4e3d68269..1b7cee64bbec 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5395,6 +5395,8 @@ def get_interp_point(ind): #### plotting z(x,y): imshow, pcolor and relatives, contour @_preprocess_data() + @cbook._delete_parameter("3.1", "shape") + @cbook._delete_parameter("3.1", "imlim") def imshow(self, X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, shape=None, filternorm=1, @@ -5508,9 +5510,6 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, See the example :doc:`/tutorials/intermediate/imshow_extent` for a more detailed description. - shape : scalars (columns, rows), optional, default: None - For raw buffer images. - filternorm : bool, optional, default: True A parameter for the antigrain image resize filter (see the antigrain documentation). If *filternorm* is set, the filter diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 4d1439bd4404..40a513be0ba1 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -32,7 +32,7 @@ import matplotlib from .deprecation import ( - deprecated, warn_deprecated, _rename_parameter, + deprecated, warn_deprecated, _rename_parameter, _delete_parameter, _suppress_matplotlib_deprecation_warning, MatplotlibDeprecationWarning, mplDeprecation) diff --git a/lib/matplotlib/cbook/deprecation.py b/lib/matplotlib/cbook/deprecation.py index 9c9918364319..05088b2414ba 100644 --- a/lib/matplotlib/cbook/deprecation.py +++ b/lib/matplotlib/cbook/deprecation.py @@ -310,6 +310,63 @@ def wrapper(*args, **kwargs): return wrapper +class _deprecated_parameter_class: + def __repr__(self): + return "" + + +_deprecated_parameter = _deprecated_parameter_class() + + +def _delete_parameter(since, name, func=None): + """ + Decorator indicating that parameter *name* of *func* is being deprecated. + + The actual implementation of *func* should keep the *name* parameter in its + signature. + + 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. + + 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) + + signature = inspect.signature(func) + assert name in signature.parameters, ( + f"Matplotlib internal error: {name!r} must be a parameter for " + f"{func.__name__}()") + func.__signature__ = signature.replace(parameters=[ + param.replace(default=_deprecated_parameter) if param.name == name + else param + for param in signature.parameters.values()]) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + arguments = func.__signature__.bind(*args, **kwargs).arguments + # We cannot just check `name not in arguments` because the pyplot + # wrappers always pass all arguments explicitly. + if name in arguments and arguments[name] != _deprecated_parameter: + warn_deprecated( + since, message=f"The {name!r} parameter of {func.__name__}() " + f"is deprecated since Matplotlib {since} and will be removed " + f"%(removal)s. If any parameter follows {name!r}, they " + f"should be pass as keyword, not positionally.") + return func(*args, **kwargs) + + return wrapper + + @contextlib.contextmanager def _suppress_matplotlib_deprecation_warning(): with warnings.catch_warnings(): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 9304975ec222..9bc0b3666c02 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2644,7 +2644,8 @@ def hlines( def imshow( X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, - shape=None, filternorm=1, filterrad=4.0, imlim=None, + shape=cbook.deprecation._deprecated_parameter, filternorm=1, + filterrad=4.0, imlim=cbook.deprecation._deprecated_parameter, resample=None, url=None, *, data=None, **kwargs): __ret = gca().imshow( X, cmap=cmap, norm=norm, aspect=aspect, diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 8fd67a67f730..7edfc38bda69 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -15,6 +15,7 @@ from matplotlib import ( colors, image as mimage, patches, pyplot as plt, rc_context, rcParams) +from matplotlib.cbook import MatplotlibDeprecationWarning from matplotlib.image import (AxesImage, BboxImage, FigureImage, NonUniformImage, PcolorImage) from matplotlib.testing.decorators import image_comparison @@ -944,3 +945,17 @@ def test_relim(): ax.relim() ax.autoscale() assert ax.get_xlim() == ax.get_ylim() == (0, 1) + + +def test_deprecation(): + data = [[1, 2], [3, 4]] + ax = plt.figure().subplots() + for obj in [ax, plt]: + with pytest.warns(None) as record: + obj.imshow(data) + assert len(record) == 0 + with pytest.warns(MatplotlibDeprecationWarning): + obj.imshow(data, shape=None) + with pytest.warns(MatplotlibDeprecationWarning): + # Enough arguments to pass "shape" positionally. + obj.imshow(data, *[None] * 10) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 74a3a4d065b8..976846b4cdf2 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -20,7 +20,7 @@ # This line imports the installed copy of matplotlib, and not the local copy. import numpy as np -from matplotlib import mlab +from matplotlib import cbook, mlab from matplotlib.axes import Axes @@ -175,6 +175,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" else: self._repr = repr(value)