diff --git a/doc/api/next_api_changes/deprecations/20686-AL.rst b/doc/api/next_api_changes/deprecations/20686-AL.rst new file mode 100644 index 000000000000..929d3eaeaa6f --- /dev/null +++ b/doc/api/next_api_changes/deprecations/20686-AL.rst @@ -0,0 +1,2 @@ +All parameters of ``imshow`` starting from *aspect* will become keyword-only +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index 4b8f50f188ee..ae14f29fea5d 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -249,6 +249,14 @@ def __set_name__(self, owner, name): 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*. @@ -268,8 +276,10 @@ def rename_parameter(since, old, new, func=None): def func(good_name): ... """ + decorator = functools.partial(rename_parameter, since, old, new) + if func is None: - return functools.partial(rename_parameter, since, old, new) + return decorator signature = inspect.signature(func) assert old not in signature.parameters, ( @@ -294,6 +304,7 @@ def wrapper(*args, **kwargs): # 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 @@ -330,8 +341,10 @@ def delete_parameter(since, name, func=None, **kwargs): def func(used_arg, other_arg, unused, more_args): ... """ + decorator = functools.partial(delete_parameter, since, name, **kwargs) + if func is None: - return functools.partial(delete_parameter, since, name, **kwargs) + return decorator signature = inspect.signature(func) # Name of `**kwargs` parameter of the decorated function, typically @@ -399,6 +412,7 @@ def wrapper(*inner_args, **inner_kwargs): **kwargs) return func(*inner_args, **inner_kwargs) + DECORATORS[wrapper] = decorator return wrapper @@ -406,10 +420,16 @@ 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 functools.partial(make_keyword_only, since, name) + return decorator signature = inspect.signature(func) POK = inspect.Parameter.POSITIONAL_OR_KEYWORD @@ -419,19 +439,16 @@ def make_keyword_only(since, name, func=None): 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):] + name_idx = names.index(name) + kwonly = [name for name in names[name_idx:] 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: + if len(args) > name_idx: warn_deprecated( since, message="Passing the %(name)s %(obj_type)s " "positionally is deprecated since Matplotlib %(since)s; the " @@ -439,6 +456,11 @@ def wrapper(*args, **kwargs): 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 diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 92a6d319ab3c..a52ed419c0ff 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5285,6 +5285,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None, replace_names=["y", "x1", "x2", "where"]) #### plotting z(x, y): imshow, pcolor and relatives, contour + @_api.make_keyword_only("3.5", "aspect") @_preprocess_data() def imshow(self, X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 6e56cd4f4d8e..7c29cb1b7ba1 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -72,14 +72,6 @@ _log = logging.getLogger(__name__) -_code_objs = { - _api.rename_parameter: - _api.rename_parameter("", "old", "new", lambda new: None).__code__, - _api.make_keyword_only: - _api.make_keyword_only("", "p", lambda p: None).__code__, -} - - def _copy_docstring_and_deprecators(method, func=None): if func is None: return functools.partial(_copy_docstring_and_deprecators, method) @@ -88,14 +80,9 @@ def _copy_docstring_and_deprecators(method, func=None): # or @_api.make_keyword_only decorators; if so, propagate them to the # pyplot wrapper as well. while getattr(method, "__wrapped__", None) is not None: - for decorator_maker, code in _code_objs.items(): - if method.__code__ is code: - kwargs = { - k: v.cell_contents - for k, v in zip(code.co_freevars, method.__closure__)} - assert kwargs["func"] is method.__wrapped__ - kwargs.pop("func") - decorators.append(decorator_maker(**kwargs)) + decorator = _api.deprecation.DECORATORS.get(method) + if decorator: + decorators.append(decorator) method = method.__wrapped__ for decorator in decorators[::-1]: func = decorator(func) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 214cd408a344..46c8d3656373 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -138,7 +138,13 @@ def generate_function(name, called_fullname, template, **kwargs): class_name, called_name = called_fullname.split('.') class_ = {'Axes': Axes, 'Figure': Figure}[class_name] - signature = inspect.signature(getattr(class_, called_name)) + meth = getattr(class_, called_name) + decorator = _api.deprecation.DECORATORS.get(meth) + # Generate the wrapper with the non-kwonly signature, as it will get + # redecorated with make_keyword_only by _copy_docstring_and_deprecators. + if decorator and decorator.func is _api.make_keyword_only: + meth = meth.__wrapped__ + signature = inspect.signature(meth) # Replace self argument. params = list(signature.parameters.values())[1:] signature = str(signature.replace(parameters=[