From c1cbcf4fb1dd6ff6d21ba408a1dc68d24f0f6dcc Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 9 Nov 2022 10:58:37 +0100 Subject: [PATCH] Programmatically generate signature of Figure.savefig. Note that the previously documented "Call signature" did not mention "transparent". Also adjust tools/boilerplate.py so that pyplot.savefig can also benefit from this change; this implies generalizing axes_cmappable_method_template to support general postambles, also for Figure methods. See the new signature of pyplot.savefig for the end result. --- lib/matplotlib/figure.py | 19 +++++----- lib/matplotlib/pyplot.py | 25 ++++++++----- tools/boilerplate.py | 77 ++++++++++++++++++---------------------- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index b1e251940210..8e9762e3f389 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -3147,14 +3147,6 @@ def savefig(self, fname, *, transparent=None, **kwargs): """ Save the current figure. - Call signature:: - - savefig(fname, *, dpi='figure', format=None, metadata=None, - bbox_inches=None, pad_inches=0.1, - facecolor='auto', edgecolor='auto', - backend=None, **kwargs - ) - The available output formats depend on the backend being used. Parameters @@ -3251,7 +3243,6 @@ def savefig(self, fname, *, transparent=None, **kwargs): pil_kwargs : dict, optional Additional keyword arguments that are passed to `PIL.Image.Image.save` when saving the figure. - """ kwargs.setdefault('dpi', mpl.rcParams['savefig.dpi']) @@ -3268,6 +3259,16 @@ def savefig(self, fname, *, transparent=None, **kwargs): self.canvas.print_figure(fname, **kwargs) + savefig.__signature__ = inspect.Signature([ + *[p for p in inspect.signature(savefig).parameters.values() + if p.name != "kwargs"], # self, fname, transparent + *[p.replace(kind=(inspect.Parameter.KEYWORD_ONLY + if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + else p.kind)) + for p in inspect.signature( + FigureCanvasBase.print_figure).parameters.values() + if p.name not in ["self", "filename"]]]) # everything else + def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=MouseButton.LEFT, mouse_pop=MouseButton.RIGHT, diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index be26fad9cc6d..3b9198b6ef54 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -955,14 +955,6 @@ def draw(): gcf().canvas.draw_idle() -@_copy_docstring_and_deprecators(Figure.savefig) -def savefig(*args, **kwargs): - fig = gcf() - res = fig.savefig(*args, **kwargs) - fig.canvas.draw_idle() # Need this if 'transparent=True', to reset colors. - return res - - ## Putting things in figures ## @@ -2262,6 +2254,23 @@ def ginput( mouse_stop=mouse_stop) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Figure.savefig) +def savefig( + fname, *, transparent=None, dpi=None, facecolor=None, + edgecolor=None, orientation='portrait', format=None, + bbox_inches=None, pad_inches=None, bbox_extra_artists=None, + backend=None, **kwargs): + __ret = gcf().savefig( + fname, transparent=transparent, dpi=dpi, facecolor=facecolor, + edgecolor=edgecolor, orientation=orientation, format=format, + bbox_inches=bbox_inches, pad_inches=pad_inches, + bbox_extra_artists=bbox_extra_artists, backend=backend, + **kwargs) + gcf().canvas.draw_idle() # reset colors, in case transparent=True + return __ret + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Figure.subplots_adjust) def subplots_adjust( diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 86dc08620679..ea0f3bc66a84 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -59,24 +59,18 @@ def enum_str_back_compat_patch(self): # Autogenerated by boilerplate.py. Do not edit as changes will be lost.""" -AXES_CMAPPABLE_METHOD_TEMPLATE = AUTOGEN_MSG + """ -@_copy_docstring_and_deprecators(Axes.{called_name}) +METHOD_TEMPLATE_WITH_POSTAMBLE = AUTOGEN_MSG + """ +@_copy_docstring_and_deprecators({called_fullname}) def {name}{signature}: - __ret = gca().{called_name}{call} - {sci_command} + __ret = {gcx}().{called_name}{call} + {postamble} return __ret """ -AXES_METHOD_TEMPLATE = AUTOGEN_MSG + """ -@_copy_docstring_and_deprecators(Axes.{called_name}) +METHOD_TEMPLATE = AUTOGEN_MSG + """ +@_copy_docstring_and_deprecators({called_fullname}) def {name}{signature}: - return gca().{called_name}{call} -""" - -FIGURE_METHOD_TEMPLATE = AUTOGEN_MSG + """ -@_copy_docstring_and_deprecators(Figure.{called_name}) -def {name}{signature}: - return gcf().{called_name}{call} + return {gcx}().{called_name}{call} """ CMAP_TEMPLATE = ''' @@ -117,32 +111,36 @@ def __repr__(self): return self._repr -def generate_function(name, called_fullname, template, **kwargs): +def generate_function(name, called_fullname, *, postamble=None): """ Create a wrapper function *pyplot_name* calling *call_name*. + The following placeholders are filled into the method template: + + - called_fullname: The qualified name of the called function. + - name: The function name. + - signature: The function signature (including parentheses). + - gcx: Either "gcf" or "gca", based on the class owning the method. + - called_name: The name of the called function. + - call: Parameters passed to *called_name* (including parentheses). + Parameters ---------- name : str The function to be created. called_fullname : str The method to be wrapped in the format ``"Class.method"``. - template : str - The template to be used. The template must contain {}-style format - placeholders. The following placeholders are filled in: - - - name: The function name. - - signature: The function signature (including parentheses). - - called_name: The name of the called function. - - call: Parameters passed to *called_name* (including parentheses). - - **kwargs - Additional parameters are passed to ``template.format()``. + postamble : str or None + If set, METHOD_TEMPLATE_WITH_POSTAMBLE will be used, and this + placeholder will also be filled in. Otherwise, METHOD_TEMPLATE will be + used. """ text_wrapper = textwrap.TextWrapper( break_long_words=False, width=70, initial_indent=' ' * 8, subsequent_indent=' ' * 8) + template = METHOD_TEMPLATE_WITH_POSTAMBLE if postamble else METHOD_TEMPLATE + # Get signature of wrapped function. class_name, called_name = called_fullname.split('.') class_ = {'Axes': Axes, 'Figure': Figure}[class_name] @@ -200,10 +198,12 @@ def generate_function(name, called_fullname, template, **kwargs): return template.format( name=name, + called_fullname=called_fullname, called_name=called_name, signature=signature, + gcx={"Axes": "gca", "Figure": "gcf"}[class_name], call=call, - **kwargs) + postamble=postamble) def boilerplate_gen(): @@ -215,6 +215,7 @@ def boilerplate_gen(): 'gca', 'gci:_gci', 'ginput', + 'savefig', 'subplots_adjust', 'suptitle', 'tight_layout', @@ -302,7 +303,9 @@ def boilerplate_gen(): 'yscale:set_yscale', ) - cmappable = { + postambles = { + 'savefig': + 'gcf().canvas.draw_idle() # reset colors, in case transparent=True', 'contour': 'if __ret._A is not None: sci(__ret) # noqa', 'contourf': 'if __ret._A is not None: sci(__ret) # noqa', 'hexbin': 'sci(__ret)', @@ -321,23 +324,13 @@ def boilerplate_gen(): } for spec in _figure_commands: - if ':' in spec: - name, called_name = spec.split(':') - else: - name = called_name = spec + name, called_name = spec.split(':') if ':' in spec else (spec, spec) yield generate_function(name, f'Figure.{called_name}', - FIGURE_METHOD_TEMPLATE) - + postamble=postambles.get(name)) for spec in _axes_commands: - if ':' in spec: - name, called_name = spec.split(':') - else: - name = called_name = spec - - template = (AXES_CMAPPABLE_METHOD_TEMPLATE if name in cmappable else - AXES_METHOD_TEMPLATE) - yield generate_function(name, f'Axes.{called_name}', template, - sci_command=cmappable.get(name)) + name, called_name = spec.split(':') if ':' in spec else (spec, spec) + yield generate_function(name, f'Axes.{called_name}', + postamble=postambles.get(name)) cmaps = ( 'autumn',