From 51c38d4d44e0fc1b3019d573a99c506b766f4a95 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 3 Jul 2021 23:43:23 +0200 Subject: [PATCH 1/2] Better signature and docstring for Artist.set --- doc/api/axes_api.rst | 1 + lib/matplotlib/artist.py | 60 ++++++++++++++++++++++++++++- lib/matplotlib/tests/test_artist.py | 34 ++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 79aed3f43da2..2e94fa5f9d65 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -606,3 +606,4 @@ Other Axes.get_default_bbox_extra_artists Axes.get_transformed_clip_path_and_affine Axes.has_data + Axes.set diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 1fa0a8e5b23b..c30bbebd210c 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -2,6 +2,7 @@ import contextlib from functools import wraps import inspect +from inspect import Signature, Parameter import logging from numbers import Number import re @@ -84,6 +85,12 @@ def _stale_axes_callback(self, val): _XYPair = namedtuple("_XYPair", "x y") +class _Unset: + def __repr__(self): + return "" +_UNSET = _Unset() + + class Artist: """ Abstract base class for objects that render into a FigureCanvas. @@ -93,6 +100,51 @@ class Artist: zorder = 0 + def __init_subclass__(cls): + # Inject custom set() methods into the subclass with signature and + # docstring based on the subclasses' properties. + + if not hasattr(cls.set, '_autogenerated_signature'): + # Don't overwrite cls.set if the subclass or one of its parents + # has defined a set method set itself. + # If there was no explicit definition, cls.set is inherited from + # the hierarchy of auto-generated set methods, which hold the + # flag _autogenerated_signature. + return + + cls.set = lambda self, **kwargs: Artist.set(self, **kwargs) + cls.set.__name__ = "set" + cls.set.__qualname__ = f"{cls.__qualname__}.set" + cls._update_set_signature_and_docstring() + + _PROPERTIES_EXCLUDED_FROM_SET = [ + 'navigate_mode', # not a user-facing function + 'figure', # changing the figure is such a profound operation + # that we don't want this in set() + '3d_properties', # cannot be used as a keyword due to leading digit + ] + + @classmethod + def _update_set_signature_and_docstring(cls): + """ + Update the signature of the set function to list all properties + as keyword arguments. + + Property aliases are not listed in the signature for brevity, but + are still accepted as keyword arguments. + """ + cls.set.__signature__ = Signature( + [Parameter("self", Parameter.POSITIONAL_OR_KEYWORD), + *[Parameter(prop, Parameter.KEYWORD_ONLY, default=_UNSET) + for prop in ArtistInspector(cls).get_setters() + if prop not in Artist._PROPERTIES_EXCLUDED_FROM_SET]]) + cls.set._autogenerated_signature = True + + cls.set.__doc__ = ( + "Set multiple properties at once.\n\n" + "Supported properties are\n\n" + + kwdoc(cls)) + def __init__(self): self._stale = True self.stale_callback = None @@ -1096,7 +1148,9 @@ def properties(self): return ArtistInspector(self).properties() def set(self, **kwargs): - """A property batch setter. Pass *kwargs* to set properties.""" + # docstring and signature are auto-generated via + # Artist._update_set_signature_and_docstring() at the end of the + # module. kwargs = cbook.normalize_kwargs(kwargs, self) return self.update(kwargs) @@ -1656,3 +1710,7 @@ def kwdoc(artist): return ('\n'.join(ai.pprint_setters_rest(leadingspace=4)) if mpl.rcParams['docstring.hardcopy'] else 'Properties:\n' + '\n'.join(ai.pprint_setters(leadingspace=4))) + +# We defer this to the end of them module, because it needs ArtistInspector +# to be defined. +Artist._update_set_signature_and_docstring() diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index c7936be4b5fa..74c58d34d991 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -340,3 +340,37 @@ def func(artist): art.remove_callback(oid) art.pchanged() # must not call the callback anymore assert func.counter == 2 + + +def test_set_signature(): + """Test autogenerated ``set()`` for Artist subclasses.""" + class MyArtist1(martist.Artist): + def set_myparam1(self, val): + pass + + assert hasattr(MyArtist1.set, '_autogenerated_signature') + assert 'myparam1' in MyArtist1.set.__doc__ + + class MyArtist2(MyArtist1): + def set_myparam2(self, val): + pass + + assert hasattr(MyArtist2.set, '_autogenerated_signature') + assert 'myparam1' in MyArtist2.set.__doc__ + assert 'myparam2' in MyArtist2.set.__doc__ + + +def test_set_is_overwritten(): + """set() defined in Artist subclasses should not be overwritten.""" + class MyArtist3(martist.Artist): + + def set(self, **kwargs): + """Not overwritten.""" + + assert not hasattr(MyArtist3.set, '_autogenerated_signature') + assert MyArtist3.set.__doc__ == "Not overwritten." + + class MyArtist4(MyArtist3): + pass + + assert MyArtist4.set is MyArtist3.set From 2867517f071eba272c3a0eadba2c7d2c4ade1283 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 5 Jul 2021 23:59:14 +0200 Subject: [PATCH 2/2] Workaround to prevent "reference target not found" errors in doc builds This introduces a known blacklist of currently non-linkable items. --- doc/api/axis_api.rst | 3 +++ lib/matplotlib/artist.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 6c3630a7abda..7161671f2ab6 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -181,6 +181,7 @@ XAxis Specific XAxis.get_text_heights XAxis.get_ticks_position XAxis.set_ticks_position + XAxis.set_label_position XAxis.tick_bottom XAxis.tick_top @@ -197,6 +198,7 @@ YAxis Specific YAxis.get_ticks_position YAxis.set_offset_position YAxis.set_ticks_position + YAxis.set_label_position YAxis.tick_left YAxis.tick_right @@ -264,4 +266,5 @@ specify a matching series of labels. Calling ``set_ticks`` makes a Tick.set_label1 Tick.set_label2 Tick.set_pad + Tick.set_url Tick.update_position diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index c30bbebd210c..c6a371c684bf 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1439,6 +1439,21 @@ def aliased_name(self, s): aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) return s + aliases + _NOT_LINKABLE = { + # A set of property setter methods that are not available in our + # current docs. This is a workaround used to prevent trying to link + # these setters which would lead to "target reference not found" + # warnings during doc build. + 'matplotlib.image._ImageBase.set_alpha', + 'matplotlib.image._ImageBase.set_array', + 'matplotlib.image._ImageBase.set_data', + 'matplotlib.image._ImageBase.set_filternorm', + 'matplotlib.image._ImageBase.set_filterrad', + 'matplotlib.image._ImageBase.set_interpolation', + 'matplotlib.image._ImageBase.set_resample', + 'matplotlib.text._AnnotationBase.set_annotation_clip', + } + def aliased_name_rest(self, s, target): """ Return 'PROPNAME or alias' if *s* has an alias, else return 'PROPNAME', @@ -1448,6 +1463,10 @@ def aliased_name_rest(self, s, target): alias, return 'markerfacecolor or mfc' and for the transform property, which does not, return 'transform'. """ + # workaround to prevent "reference target not found" + if target in self._NOT_LINKABLE: + return f'``{s}``' + aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) return ':meth:`%s <%s>`%s' % (s, target, aliases)