-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Better signature and docstring for Artist.set #20569
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -606,3 +606,4 @@ Other | |
Axes.get_default_bbox_extra_artists | ||
Axes.get_transformed_clip_path_and_affine | ||
Axes.has_data | ||
Axes.set |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 = _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) | ||
|
||
|
@@ -1385,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', | ||
|
@@ -1394,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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are non-linkable targets basically the methods defined on a private parent? If so, perhaps you can just check whether the class name starts with an underscore, rather than hard coding the entire list. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what I first thought (and checked with the print statement). Turns out it is more complicated than that. As a general statement, a setter is not linkable if it has not doc entry. All non-private classes can (and now do) write documentation entries setters. We don't autodoc private classes, so there is no default place to document their setters. However, sublcasses may reimplement the setter and document that, or explicit entries can be created (e.g. for I'm not even sure this is the whole story. Anyway, I've chosen to only explicitly backout for the properties that sphinx was complaining about. The other ones are found one way or the other. |
||
return f'``{s}``' | ||
|
||
aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) | ||
return ':meth:`%s <%s>`%s' % (s, target, aliases) | ||
|
||
|
@@ -1656,3 +1729,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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you just move the definition of ArtistInspector above Artist and avoid this deferred call? (And then _update_set_signature_and_docstring can be inlined into init_subclass.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No strong opinion. But that's quite a bit moving of code, which makes digging in the history more complicated. I don't think we can inline, because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's leave things as they are for now. |
Uh oh!
There was an error while loading. Please reload this page.