From 08ea277eceed8070ef263f8b4051f27e71c5ce03 Mon Sep 17 00:00:00 2001 From: Corenthin ZOZOR Date: Mon, 16 Jun 2025 16:33:39 +0200 Subject: [PATCH 1/5] Add xlim / ylim autogeneration - Create get_signature method - Create AXES_GETTER_SETTER_TEMPLATE - Create call_param method on generate_function (cherry picked from commit e57b32165cae851b3f9cb846e37e3e49339aed43) --- lib/matplotlib/pyplot.py | 66 +++++++++++++++++++++ tools/boilerplate.py | 120 +++++++++++++++++++++++++++------------ 2 files changed, 151 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index cf5c9b4b739f..e6bde60671d5 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -4457,6 +4457,72 @@ def yscale(value: str | ScaleBase, **kwargs) -> None: gca().set_yscale(value, **kwargs) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@overload +@_copy_docstring_and_deprecators(Axes.get_xlim) +def xlim() -> tuple[float, float]: + ... + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@overload +@_copy_docstring_and_deprecators(Axes.set_xlim) +def xlim( + left: float | tuple[float, float] | None = None, + right: float | None = None, + *, + emit: bool = True, + auto: bool | None = False, + xmin: float | None = None, + xmax: float | None = None, +) -> tuple[float, float]: + ... + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Axes.get_xlim) +def xlim(*args, **kwargs): + ax = gca() + if not args and not kwargs: + return ax.get_xlim() + + ret = ax.set_xlim(*args, **kwargs) + return ret + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@overload +@_copy_docstring_and_deprecators(Axes.get_ylim) +def ylim() -> tuple[float, float]: + ... + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@overload +@_copy_docstring_and_deprecators(Axes.set_ylim) +def ylim( + bottom: float | tuple[float, float] | None = None, + top: float | None = None, + *, + emit: bool = True, + auto: bool | None = False, + ymin: float | None = None, + ymax: float | None = None, +) -> tuple[float, float]: + ... + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Axes.get_ylim) +def ylim(*args, **kwargs): + ax = gca() + if not args and not kwargs: + return ax.get_ylim() + + ret = ax.set_ylim(*args, **kwargs) + return ret + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. def autumn() -> None: """ diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 11ec15ac1c44..72e4100f0f16 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -54,6 +54,25 @@ def {name}{signature}: {return_statement}gca().{called_name}{call} """ +AXES_GETTER_SETTER_TEMPLATE = AUTOGEN_MSG + """ +@overload +@_copy_docstring_and_deprecators(Axes.get_{called_name}) +def {name}() -> {get_return_type}: ... +""" + AUTOGEN_MSG + """ +@overload +@_copy_docstring_and_deprecators(Axes.set_{called_name}) +def {name}{signature}: ... +""" + AUTOGEN_MSG + """ +@_copy_docstring_and_deprecators(Axes.get_{called_name}) +def {name}(*args, **kwargs): + ax = gca() + if not args and not kwargs: + return ax.get_{called_name}() + + ret = ax.set_{called_name}(*args, **kwargs) + return ret +""" + FIGURE_METHOD_TEMPLATE = AUTOGEN_MSG + """ @_copy_docstring_and_deprecators(Figure.{called_name}) def {name}{signature}: @@ -102,6 +121,7 @@ class direct_repr: """ A placeholder class to destringify annotations from ast """ + def __init__(self, value): self._repr = value @@ -109,7 +129,7 @@ def __repr__(self): return self._repr -def generate_function(name, called_fullname, template, **kwargs): +def generate_function(name, called_fullname, template, gettersetter=False, **kwargs): """ Create a wrapper function *pyplot_name* calling *call_name*. @@ -127,6 +147,11 @@ def generate_function(name, called_fullname, template, **kwargs): - signature: The function signature (including parentheses). - called_name: The name of the called function. - call: Parameters passed to *called_name* (including parentheses). + gettersetter : bool + Indicate if the method to be wrapped is correponding to a getter and setter. A new placeholdr is filled : + + - get_return_type: The type returned by the getter + - set_return_type: The type returned by the setter **kwargs Additional parameters are passed to ``template.format()``. @@ -135,15 +160,14 @@ def generate_function(name, called_fullname, template, **kwargs): class_name, called_name = called_fullname.split('.') class_ = {'Axes': Axes, 'Figure': Figure}[class_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__ + if not gettersetter: + signature = get_signature(class_, called_name) + else: + getter_signature = get_signature(class_, f"get_{called_name}") + kwargs.setdefault("get_return_type", str(getter_signature.return_annotation)) - annotated_trees = get_ast_mro_trees(class_) - signature = get_matching_signature(meth, annotated_trees) + signature = get_signature(class_, f"set_{called_name}") + kwargs.setdefault('return_type', str(signature.return_annotation)) # Replace self argument. params = list(signature.parameters.values())[1:] @@ -152,30 +176,30 @@ def generate_function(name, called_fullname, template, **kwargs): param.replace(default=value_formatter(param.default)) if param.default is not param.empty else param for param in params])) + # How to call the wrapped function. - call = '(' + ', '.join(( - # Pass "intended-as-positional" parameters positionally to avoid - # forcing third-party subclasses to reproduce the parameter names. - '{0}' - if param.kind in [ - Parameter.POSITIONAL_OR_KEYWORD] - and param.default is Parameter.empty else - # Only pass the data kwarg if it is actually set, to avoid forcing - # third-party subclasses to support it. - '**({{"data": data}} if data is not None else {{}})' - if param.name == "data" else - '{0}={0}' - if param.kind in [ - Parameter.POSITIONAL_OR_KEYWORD, - Parameter.KEYWORD_ONLY] else - '{0}' - if param.kind is Parameter.POSITIONAL_ONLY else - '*{0}' - if param.kind is Parameter.VAR_POSITIONAL else - '**{0}' - if param.kind is Parameter.VAR_KEYWORD else - None).format(param.name) - for param in params) + ')' + + def call_param(param: Parameter): + match param.kind: + # Pass "intended-as-positional" parameters positionally to avoid + # forcing third-party subclasses to reproduce the parameter names. + case Parameter.POSITIONAL_OR_KEYWORD if param.default is Parameter.empty: + return '{0}' + # Only pass the data kwarg if it is actually set, to avoid forcing + # third-party subclasses to support it. + case _ if param.name == "data": + return '**({{"data": data}} if data is not None else {{}})' + case Parameter.POSITIONAL_OR_KEYWORD | Parameter.KEYWORD_ONLY: + return '{0}={0}' + case Parameter.POSITIONAL_ONLY: + return '{0}' + case Parameter.VAR_POSITIONAL: + return '*{0}' + case Parameter.VAR_KEYWORD: + return '**{0}' + return None + + call = '(' + ', '.join((call_param(param)).format(param.name) for param in params) + ')' return_statement = 'return ' if has_return_value else '' # Bail out in case of name collision. for reserved in ('gca', 'gci', 'gcf', '__ret'): @@ -286,7 +310,12 @@ def boilerplate_gen(): 'xlabel:set_xlabel', 'ylabel:set_ylabel', 'xscale:set_xscale', - 'yscale:set_yscale', + 'yscale:set_yscale' + ) + + _axes_getter_setters = ( + 'xlim', + 'ylim', ) cmappable = { @@ -341,6 +370,14 @@ def boilerplate_gen(): yield generate_function(name, f'Axes.{called_name}', template, sci_command=cmappable.get(name)) + for spec in _axes_getter_setters: + if ':' in spec: + name, called_name = spec.split(':') + else: + name = called_name = spec + yield generate_function(name, f'Axes.{called_name}', + AXES_GETTER_SETTER_TEMPLATE, True) + cmaps = ( 'autumn', 'bone', @@ -405,6 +442,19 @@ def get_ast_mro_trees(cls): return [get_ast_tree(c) for c in cls.__mro__ if c.__module__ != "builtins"] +def get_signature(class_, name): + meth = getattr(class_, 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__ + + annotated_trees = get_ast_mro_trees(class_) + return get_matching_signature(meth, annotated_trees) + + def get_matching_signature(method, trees): sig = inspect.signature(method) for tree in trees: @@ -460,10 +510,10 @@ def update_sig_from_node(node, sig): if len(sys.argv) > 1: pyplot_path = Path(sys.argv[1]) else: - mpl_path = (Path(__file__).parent / ".." /"lib"/"matplotlib").resolve() + mpl_path = (Path(__file__).parent / ".." / "lib" / "matplotlib").resolve() pyplot_path = mpl_path / "pyplot.py" for cls in [Axes, Figure]: - if mpl_path not in Path(inspect.getfile(cls)).parents: + if mpl_path not in Path(inspect.getfile(cls)).parents: raise RuntimeError( f"{cls.__name__} import path is not {mpl_path}.\n" "Please make sure your Matplotlib installation " From f78a4a5f46532341dd8cba6f5fc5ea75cc27d0a4 Mon Sep 17 00:00:00 2001 From: Corenthin ZOZOR Date: Fri, 20 Jun 2025 21:12:24 +0200 Subject: [PATCH 2/5] Format with ruff (cherry picked from commit 64e7921b0b3f56c88c1f449a4f2081e862289279) --- tools/boilerplate.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 72e4100f0f16..778729abe12d 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -129,7 +129,13 @@ def __repr__(self): return self._repr -def generate_function(name, called_fullname, template, gettersetter=False, **kwargs): +def generate_function( + name, + called_fullname, + template, + gettersetter=False, + **kwargs +): """ Create a wrapper function *pyplot_name* calling *call_name*. @@ -199,7 +205,9 @@ def call_param(param: Parameter): return '**{0}' return None - call = '(' + ', '.join((call_param(param)).format(param.name) for param in params) + ')' + call = '(' + ', '.join( + (call_param(param)).format(param.name) for param in params + ) + ')' return_statement = 'return ' if has_return_value else '' # Bail out in case of name collision. for reserved in ('gca', 'gci', 'gcf', '__ret'): From 8e41f892af7aadd6e1c961cfc51d77c7845e809e Mon Sep 17 00:00:00 2001 From: Corenthin ZOZOR Date: Fri, 20 Jun 2025 20:24:18 +0200 Subject: [PATCH 3/5] Remove old xlim and ylim (cherry picked from commit 66ee0714ff310e0693e05c4616bbb702e45a6407) --- lib/matplotlib/pyplot.py | 75 ---------------------------------------- 1 file changed, 75 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e6bde60671d5..0855f0f97fdf 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2095,81 +2095,6 @@ def box(on: bool | None = None) -> None: ## Axis ## - -def xlim(*args, **kwargs) -> tuple[float, float]: - """ - Get or set the x limits of the current Axes. - - Call signatures:: - - left, right = xlim() # return the current xlim - xlim((left, right)) # set the xlim to left, right - xlim(left, right) # set the xlim to left, right - - If you do not specify args, you can pass *left* or *right* as kwargs, - i.e.:: - - xlim(right=3) # adjust the right leaving left unchanged - xlim(left=1) # adjust the left leaving right unchanged - - Setting limits turns autoscaling off for the x-axis. - - Returns - ------- - left, right - A tuple of the new x-axis limits. - - Notes - ----- - Calling this function with no arguments (e.g. ``xlim()``) is the pyplot - equivalent of calling `~.Axes.get_xlim` on the current Axes. - Calling this function with arguments is the pyplot equivalent of calling - `~.Axes.set_xlim` on the current Axes. All arguments are passed though. - """ - ax = gca() - if not args and not kwargs: - return ax.get_xlim() - ret = ax.set_xlim(*args, **kwargs) - return ret - - -def ylim(*args, **kwargs) -> tuple[float, float]: - """ - Get or set the y-limits of the current Axes. - - Call signatures:: - - bottom, top = ylim() # return the current ylim - ylim((bottom, top)) # set the ylim to bottom, top - ylim(bottom, top) # set the ylim to bottom, top - - If you do not specify args, you can alternatively pass *bottom* or - *top* as kwargs, i.e.:: - - ylim(top=3) # adjust the top leaving bottom unchanged - ylim(bottom=1) # adjust the bottom leaving top unchanged - - Setting limits turns autoscaling off for the y-axis. - - Returns - ------- - bottom, top - A tuple of the new y-axis limits. - - Notes - ----- - Calling this function with no arguments (e.g. ``ylim()``) is the pyplot - equivalent of calling `~.Axes.get_ylim` on the current Axes. - Calling this function with arguments is the pyplot equivalent of calling - `~.Axes.set_ylim` on the current Axes. All arguments are passed though. - """ - ax = gca() - if not args and not kwargs: - return ax.get_ylim() - ret = ax.set_ylim(*args, **kwargs) - return ret - - def xticks( ticks: ArrayLike | None = None, labels: Sequence[str] | None = None, From 880e2628152a10014ddf688df9d05a9a601bf1c8 Mon Sep 17 00:00:00 2001 From: Corenthin ZOZOR Date: Sat, 21 Jun 2025 13:09:29 +0200 Subject: [PATCH 4/5] Format with ruff --- tools/boilerplate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 778729abe12d..4bbc75395a14 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -130,11 +130,11 @@ def __repr__(self): def generate_function( - name, - called_fullname, - template, - gettersetter=False, - **kwargs + name, + called_fullname, + template, + gettersetter=False, + **kwargs ): """ Create a wrapper function *pyplot_name* calling *call_name*. From 3f3743fcabf1411e27bfaf65178bc49bcb051ea5 Mon Sep 17 00:00:00 2001 From: Corenthin ZOZOR Date: Sat, 21 Jun 2025 13:14:59 +0200 Subject: [PATCH 5/5] Revert superfluous changes --- tools/boilerplate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 4bbc75395a14..6d08c6b9df4c 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -518,10 +518,10 @@ def update_sig_from_node(node, sig): if len(sys.argv) > 1: pyplot_path = Path(sys.argv[1]) else: - mpl_path = (Path(__file__).parent / ".." / "lib" / "matplotlib").resolve() + mpl_path = (Path(__file__).parent / ".." /"lib"/"matplotlib").resolve() pyplot_path = mpl_path / "pyplot.py" for cls in [Axes, Figure]: - if mpl_path not in Path(inspect.getfile(cls)).parents: + if mpl_path not in Path(inspect.getfile(cls)).parents: raise RuntimeError( f"{cls.__name__} import path is not {mpl_path}.\n" "Please make sure your Matplotlib installation "