Skip to content

Add a note to pyplot docstrings referencing the corresponding object methods #27909

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
Mar 20, 2024
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
2 changes: 2 additions & 0 deletions galleries/users_explain/figure/api_interfaces.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ is very flexible, and allows us to customize the objects after they are created,
but before they are displayed.


.. _pyplot_interface:

The implicit "pyplot" interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
33 changes: 26 additions & 7 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@
# All the other methods should go in the _AxesBase class.


def _make_axes_method(func):
"""
Patch the qualname for functions that are directly added to Axes.

Some Axes functionality is defined in functions in other submodules.
These are simply added as attributes to Axes. As a result, their
``__qualname__`` is e.g. only "table" and not "Axes.table". This
function fixes that.

Note that the function itself is patched, so that
Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, since you've wrapped all of them with this function, you could return a new function from here and not modify the original.

Copy link
Member Author

Choose a reason for hiding this comment

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

For the scope of this PR, I don't care too much how this is implemented, I just need a correct __qualname__ and changing is good enough.

Yes, you could create a new function, but that's an additional indirection. While negligible from a performance perspective, it shows up e.g. in stacktraces. I'm not even sure that this way of adding functionality to classes reasonable style and what other edge-case consequences that has. Alternatively, one could make the original functions private. But that's all for a different PR.

``matplotlib.table.table.__qualname__` will also show "Axes.table".
However, since these functions are not intended to be standalone,
this is bearable.
"""
func.__qualname__ = f"Axes.{func.__name__}"
return func


@_docstring.interpd
class Axes(_AxesBase):
"""
Expand Down Expand Up @@ -8541,18 +8559,19 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,

# Methods that are entirely implemented in other modules.

table = mtable.table
table = _make_axes_method(mtable.table)

# args can be either Y or y1, y2, ... and all should be replaced
stackplot = _preprocess_data()(mstack.stackplot)
stackplot = _preprocess_data()(_make_axes_method(mstack.stackplot))

streamplot = _preprocess_data(
replace_names=["x", "y", "u", "v", "start_points"])(mstream.streamplot)
replace_names=["x", "y", "u", "v", "start_points"])(
_make_axes_method(mstream.streamplot))

tricontour = mtri.tricontour
tricontourf = mtri.tricontourf
tripcolor = mtri.tripcolor
triplot = mtri.triplot
tricontour = _make_axes_method(mtri.tricontour)
tricontourf = _make_axes_method(mtri.tricontourf)
tripcolor = _make_axes_method(mtri.tripcolor)
triplot = _make_axes_method(mtri.triplot)

def _get_aspect_ratio(self):
"""
Expand Down
70 changes: 70 additions & 0 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,79 @@ def _copy_docstring_and_deprecators(
method = method.__wrapped__
for decorator in decorators[::-1]:
func = decorator(func)
_add_pyplot_note(func, method)
return func


_NO_PYPLOT_NOTE = [
'FigureBase._gci', # wrapped_func is private
'_AxesBase._sci', # wrapped_func is private
'Artist.findobj', # not a standard pyplot wrapper because it does not operate
# on the current Figure / Axes. Explanation of relation would
# be more complex and is not too important.
]


def _add_pyplot_note(func, wrapped_func):
"""
Add a note to the docstring of *func* that it is a pyplot wrapper.

The note is added to the "Notes" section of the docstring. If that does
not exist, a "Notes" section is created. In numpydoc, the "Notes"
section is the third last possible section, only potentially followed by
"References" and "Examples".
"""
if not func.__doc__:
return # nothing to do

qualname = wrapped_func.__qualname__
if qualname in _NO_PYPLOT_NOTE:
return

wrapped_func_is_method = True
if "." not in qualname:
# method qualnames are prefixed by the class and ".", e.g. "Axes.plot"
wrapped_func_is_method = False
link = f"{wrapped_func.__module__}.{qualname}"
elif qualname.startswith("Axes."): # e.g. "Axes.plot"
link = ".axes." + qualname
elif qualname.startswith("_AxesBase."): # e.g. "_AxesBase.set_xlabel"
link = ".axes.Axes" + qualname[9:]
elif qualname.startswith("Figure."): # e.g. "Figure.figimage"
link = "." + qualname
elif qualname.startswith("FigureBase."): # e.g. "FigureBase.gca"
link = ".Figure" + qualname[10:]
elif qualname.startswith("FigureCanvasBase."): # "FigureBaseCanvas.mpl_connect"
link = "." + qualname
else:
raise RuntimeError(f"Wrapped method from unexpected class: {qualname}")

if wrapped_func_is_method:
message = f"This is the :ref:`pyplot wrapper <pyplot_interface>` for `{link}`."
else:
message = f"This is equivalent to `{link}`."

# Find the correct insert position:
# - either we already have a "Notes" section into which we can insert
# - or we create one before the next present section. Note that in numpydoc, the
# "Notes" section is the third last possible section, only potentially followed
# by "References" and "Examples".
# - or we append a new "Notes" section at the end.
doc = inspect.cleandoc(func.__doc__)
if "\nNotes\n-----" in doc:
before, after = doc.split("\nNotes\n-----", 1)
elif (index := doc.find("\nReferences\n----------")) != -1:
before, after = doc[:index], doc[index:]
elif (index := doc.find("\nExamples\n--------")) != -1:
before, after = doc[:index], doc[index:]
else:
# No "Notes", "References", or "Examples" --> append to the end.
before = doc + "\n"
after = ""

func.__doc__ = f"{before}\nNotes\n-----\n\n.. note::\n\n {message}\n{after}"
Copy link
Member

Choose a reason for hiding this comment

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

Do you know examples of each type (Notes/no Notes/no Refs/etc) that we could check here?

Copy link
Member Author

Choose a reason for hiding this comment

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

For the four branch aternative to detect the insert position:
NOTES: margins
REFERENCES: cohere
SEE ALSO: hlines
NONE: title

Copy link
Member

Choose a reason for hiding this comment

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

Per https://numpydoc.readthedocs.io/en/latest/format.html "See Also" should come before "Notes" and we also have to check for "Examples"?

Copy link
Member

Choose a reason for hiding this comment

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

but it looks like its rendering in the correct order in hlines so I am confused...

Copy link
Member

Choose a reason for hiding this comment

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

It looks like something in the html pipeline is fixing the order as

In [2]: print(plt.hlines.__doc__)
Plot horizontal lines at each *y* from *xmin* to *xmax*.

Parameters
----------
y : float or array-like
    y-indexes where to plot the lines.

xmin, xmax : float or array-like
    Respective beginning and end of each line. If scalars are
    provided, all lines will have the same length.

colors : :mpltype:`color` or list of color , default: :rc:`lines.color`

linestyles : {'solid', 'dashed', 'dashdot', 'dotted'}, default: 'solid'

label : str, default: ''

Returns
-------
`~matplotlib.collections.LineCollection`

Other Parameters
----------------
data : indexable object, optional
    If given, the following parameters also accept a string ``s``, which is
    interpreted as ``data[s]`` (unless this raises an exception):

    *y*, *xmin*, *xmax*, *colors*
**kwargs :  `~matplotlib.collections.LineCollection` properties.

Notes
-----

.. note::

    This is the :ref:`pyplot wrapper <pyplot_interface>` for `.axes.Axes.hlines`.

See Also
--------
vlines : vertical lines
axhline : horizontal line across the Axes

but

image



## Global ##


Expand Down