Skip to content

Commit 2271b49

Browse files
committed
Fix interaction between make_keyword_only and pyplot generation.
No independent test (because mocking boilerplate.py is too hard), but note that the signature change on imshow works.
1 parent 246a489 commit 2271b49

File tree

5 files changed

+36
-24
lines changed

5 files changed

+36
-24
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
All parameters of ``imshow`` starting from *aspect* will become keyword-only
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

lib/matplotlib/_api/deprecation.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,10 @@ def rename_parameter(since, old, new, func=None):
268268
def func(good_name): ...
269269
"""
270270

271+
decorator = functools.partial(rename_parameter, since, old, new)
272+
271273
if func is None:
272-
return functools.partial(rename_parameter, since, old, new)
274+
return decorator
273275

274276
signature = inspect.signature(func)
275277
assert old not in signature.parameters, (
@@ -294,6 +296,7 @@ def wrapper(*args, **kwargs):
294296
# would both show up in the pyplot function for an Axes method as well and
295297
# pyplot would explicitly pass both arguments to the Axes method.
296298

299+
wrapper._mpl_decorator = decorator # cf _copy_docstring_and_deprecators.
297300
return wrapper
298301

299302

@@ -330,8 +333,10 @@ def delete_parameter(since, name, func=None, **kwargs):
330333
def func(used_arg, other_arg, unused, more_args): ...
331334
"""
332335

336+
decorator = functools.partial(delete_parameter, since, name, **kwargs)
337+
333338
if func is None:
334-
return functools.partial(delete_parameter, since, name, **kwargs)
339+
return decorator
335340

336341
signature = inspect.signature(func)
337342
# Name of `**kwargs` parameter of the decorated function, typically
@@ -399,17 +404,24 @@ def wrapper(*inner_args, **inner_kwargs):
399404
**kwargs)
400405
return func(*inner_args, **inner_kwargs)
401406

407+
wrapper._mpl_decorator = decorator # cf _copy_docstring_and_deprecators.
402408
return wrapper
403409

404410

405411
def make_keyword_only(since, name, func=None):
406412
"""
407413
Decorator indicating that passing parameter *name* (or any of the following
408414
ones) positionally to *func* is being deprecated.
415+
416+
When used on a method that has a pyplot wrapper, this should be the
417+
outermost decorator, so that :file:`boilerplate.py` can access the original
418+
signature.
409419
"""
410420

421+
decorator = functools.partial(make_keyword_only, since, name)
422+
411423
if func is None:
412-
return functools.partial(make_keyword_only, since, name)
424+
return decorator
413425

414426
signature = inspect.signature(func)
415427
POK = inspect.Parameter.POSITIONAL_OR_KEYWORD
@@ -421,16 +433,13 @@ def make_keyword_only(since, name, func=None):
421433
names = [*signature.parameters]
422434
kwonly = [name for name in names[names.index(name):]
423435
if signature.parameters[name].kind == POK]
424-
func.__signature__ = signature.replace(parameters=[
425-
param.replace(kind=KWO) if param.name in kwonly else param
426-
for param in signature.parameters.values()])
427436

428437
@functools.wraps(func)
429438
def wrapper(*args, **kwargs):
430439
# Don't use signature.bind here, as it would fail when stacked with
431440
# rename_parameter and an "old" argument name is passed in
432441
# (signature.bind would fail, but the actual call would succeed).
433-
idx = [*func.__signature__.parameters].index(name)
442+
idx = names.index(name)
434443
if len(args) > idx:
435444
warn_deprecated(
436445
since, message="Passing the %(name)s %(obj_type)s "
@@ -439,6 +448,11 @@ def wrapper(*args, **kwargs):
439448
name=name, obj_type=f"parameter of {func.__name__}()")
440449
return func(*args, **kwargs)
441450

451+
# Don't modify *func*'s signature, as boilerplate.py needs it.
452+
wrapper.__signature__ = signature.replace(parameters=[
453+
param.replace(kind=KWO) if param.name in kwonly else param
454+
for param in signature.parameters.values()])
455+
wrapper._mpl_decorator = decorator # cf _copy_docstring_and_deprecators.
442456
return wrapper
443457

444458

lib/matplotlib/axes/_axes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5285,6 +5285,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None,
52855285
replace_names=["y", "x1", "x2", "where"])
52865286

52875287
#### plotting z(x, y): imshow, pcolor and relatives, contour
5288+
@_api.make_keyword_only("3.5", "aspect")
52885289
@_preprocess_data()
52895290
def imshow(self, X, cmap=None, norm=None, aspect=None,
52905291
interpolation=None, alpha=None, vmin=None, vmax=None,

lib/matplotlib/pyplot.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,6 @@
7272
_log = logging.getLogger(__name__)
7373

7474

75-
_code_objs = {
76-
_api.rename_parameter:
77-
_api.rename_parameter("", "old", "new", lambda new: None).__code__,
78-
_api.make_keyword_only:
79-
_api.make_keyword_only("", "p", lambda p: None).__code__,
80-
}
81-
82-
8375
def _copy_docstring_and_deprecators(method, func=None):
8476
if func is None:
8577
return functools.partial(_copy_docstring_and_deprecators, method)
@@ -88,14 +80,8 @@ def _copy_docstring_and_deprecators(method, func=None):
8880
# or @_api.make_keyword_only decorators; if so, propagate them to the
8981
# pyplot wrapper as well.
9082
while getattr(method, "__wrapped__", None) is not None:
91-
for decorator_maker, code in _code_objs.items():
92-
if method.__code__ is code:
93-
kwargs = {
94-
k: v.cell_contents
95-
for k, v in zip(code.co_freevars, method.__closure__)}
96-
assert kwargs["func"] is method.__wrapped__
97-
kwargs.pop("func")
98-
decorators.append(decorator_maker(**kwargs))
83+
if hasattr(method, "_mpl_decorator"):
84+
decorators.append(method._mpl_decorator)
9985
method = method.__wrapped__
10086
for decorator in decorators[::-1]:
10187
func = decorator(func)

tools/boilerplate.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ def {name}{signature}:
8282
CMAP_TEMPLATE = "def {name}(): set_cmap({name!r})\n" # Colormap functions.
8383

8484

85+
def unwrap_outer_make_keyword_only(meth):
86+
"""Remove an outer `._api.make_keyword_only`, if present, from *meth*."""
87+
return (meth.__wrapped__
88+
if (hasattr(meth, "_mpl_decorator")
89+
and meth._mpl_decorator.func is _api.make_keyword_only)
90+
else meth)
91+
92+
8593
class value_formatter:
8694
"""
8795
Format function default values as needed for inspect.formatargspec.
@@ -138,7 +146,8 @@ def generate_function(name, called_fullname, template, **kwargs):
138146
class_name, called_name = called_fullname.split('.')
139147
class_ = {'Axes': Axes, 'Figure': Figure}[class_name]
140148

141-
signature = inspect.signature(getattr(class_, called_name))
149+
signature = inspect.signature(
150+
unwrap_outer_make_keyword_only(getattr(class_, called_name)))
142151
# Replace self argument.
143152
params = list(signature.parameters.values())[1:]
144153
signature = str(signature.replace(parameters=[

0 commit comments

Comments
 (0)