Skip to content

Fix interaction between make_keyword_only and pyplot generation. #20686

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/api/next_api_changes/deprecations/20686-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
All parameters of ``imshow`` starting from *aspect* will become keyword-only
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 changes: 31 additions & 9 deletions lib/matplotlib/_api/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand All @@ -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, (
Expand All @@ -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


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -399,17 +412,24 @@ def wrapper(*inner_args, **inner_kwargs):
**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 functools.partial(make_keyword_only, since, name)
return decorator

signature = inspect.signature(func)
POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
Expand All @@ -419,26 +439,28 @@ 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 "
"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


Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 3 additions & 16 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion tools/boilerplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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=[
Expand Down