Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 2 commits into from
Jul 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -606,3 +606,4 @@ Other
Axes.get_default_bbox_extra_artists
Axes.get_transformed_clip_path_and_affine
Axes.has_data
Axes.set
3 changes: 3 additions & 0 deletions doc/api/axis_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
79 changes: 78 additions & 1 deletion lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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',
Expand All @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 _AxesBase setters).

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)

Expand Down Expand Up @@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The 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.)

Copy link
Member Author

Choose a reason for hiding this comment

The 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 __init_subclass__ is not called by Artist itself, but we want to adjust Artist.set as well. (I wouldn't want to explicitly call __init_subclass__ on Artist. That's more confusing than an extra function.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave things as they are for now.

34 changes: 34 additions & 0 deletions lib/matplotlib/tests/test_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -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