Skip to content
Draft
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
3 changes: 3 additions & 0 deletions ci/mypy-stubtest-allowlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ matplotlib\.animation\.EventSourceProtocol
# Avoid a regression in NewType handling for stubtest
# https://github.com/python/mypy/issues/19877
matplotlib\.ft2font\.GlyphIndexType\.__init__

# 3.12 deprecation
matplotlib\.axes\._base\._AxesBase\.ArtistList
10 changes: 10 additions & 0 deletions doc/api/artist_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,14 @@ Functions
getp
setp
kwdoc

Helper classes
==============

.. autosummary::
:template: autosummary.rst
:toctree: _as_gen
:nosignatures:

ArtistInspector
ArtistList
12 changes: 12 additions & 0 deletions doc/api/axes__base_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
*************************
``matplotlib.axes._base``
*************************

.. This is a stub which is only included to make inheritance links work. It
deliberately does not include any docstrings, so the class-doc-from directive
specifies a non-existent docstring.

.. autoclass:: matplotlib.axes._base._AxesBase
:no-members:
:class-doc-from: class
:show-inheritance:
2 changes: 0 additions & 2 deletions doc/api/axes_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -632,5 +632,3 @@ Other
Axes.get_figure
Axes.figure
Axes.remove

.. autoclass:: matplotlib.axes.Axes.ArtistList
4 changes: 4 additions & 0 deletions doc/api/image_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: _ImageBase
:class-doc-from: class
:show-inheritance:
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Alphabetical list of modules:
_type1font.rst
_tight_bbox_api.rst
_tight_layout_api.rst
axes__base_api.rst
toolkits/mplot3d.rst
toolkits/axes_grid1.rst
toolkits/axisartist.rst
Expand Down
12 changes: 12 additions & 0 deletions doc/api/next_api_changes/deprecations/31746_REC.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Direct modification of ``(Sub)Figure`` artist lists
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Previously it was possible to modify the ``artists``, ``images``, ``lines``,
``legends``, ``patches`` and ``texts`` attributes of `.Figure` and `.SubFigure`
instances using standard `list` functionality. This is now deprecated.
Instead use `~.Figure.add_artist` to add an artist to the figure, or use the
artist's `~.Artist.remove` method to remove it.


The ``Axes.AxesList`` attribute
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... is deprecated. Use `.artist.ArtistList` instead.
3 changes: 3 additions & 0 deletions doc/api/text_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
:undoc-members:
:show-inheritance:

.. autoclass:: matplotlib.text._AnnotationBase
:class-doc-from: class

.. autoclass:: matplotlib.text.Annotation
:members:
:undoc-members:
Expand Down
2 changes: 1 addition & 1 deletion galleries/examples/widgets/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def __init__(self, fig, menuitems):

item.set_extent(left, bottom, width, height, depth)

fig.artists.append(item)
fig.add_artist(item)
y0 -= maxh + MenuItem.pady

fig.canvas.mpl_connect('motion_notify_event', self.on_move)
Expand Down
52 changes: 28 additions & 24 deletions galleries/tutorials/artists.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,24 +314,26 @@ class in the Matplotlib API, and the one you will be working with most
#
#
# The figure also has its own ``images``, ``lines``, ``patches`` and ``text``
# attributes, which you can use to add primitives directly. When doing so, the
# default coordinate system for the ``Figure`` will simply be in pixels (which
# is not usually what you want). If you instead use Figure-level methods to add
# Artists (e.g., using `.Figure.text` to add text), then the default coordinate
# system will be "figure coordinates" where (0, 0) is the bottom-left of the
# figure and (1, 1) is the top-right of the figure.
#
# As with all ``Artist``\s, you can control this coordinate system by setting
# the transform property. You can explicitly use "figure coordinates" by
# setting the ``Artist`` transform to :attr:`!fig.transFigure`:
# attributes, which you can use to access any primitives that are its direct
# children. Adding images and text is usually achieved with the
# `~.Figure.figimage` and `~.Figure.text` methods. Other artists may be added
# with the `~.Figure.add_artist` method.
#
# As with all ``Artist``\s, you can control the coordinate system by setting
# the transform property (see :ref:`transforms_tutorial`). When using
# `~.Figure.figimage`, the default coordinate system is simply pixels. When
# using `~.Figure.text` or `~.Figure.add_artist`, the default coordinate system
# will be "figure coordinates" where (0, 0) is the bottom-left of the figure
# and (1, 1) is the top-right of the figure.

import matplotlib.lines as lines

fig = plt.figure()

l1 = lines.Line2D([0, 1], [0, 1], transform=fig.transFigure, figure=fig)
l2 = lines.Line2D([0, 1], [1, 0], transform=fig.transFigure, figure=fig)
fig.lines.extend([l1, l2])
line1 = lines.Line2D([0, 1], [0, 1])
line2 = lines.Line2D([0, 1], [1, 0])
for line in line1, line2:
fig.add_artist(line)

plt.show()

Expand All @@ -342,16 +344,18 @@ class in the Matplotlib API, and the one you will be working with most
# Figure attribute Description
# ================ ============================================================
# axes A list of `~.axes.Axes` instances
# subfigures A list of `.SubFigure` instances
# patch The `.Rectangle` background
# images A list of `.FigureImage` patches -
# images An `~.artist.ArtistList` of `.FigureImage` patches -
# useful for raw pixel display
# legends A list of Figure `.Legend` instances
# legends An `~.artist.ArtistList` of Figure `.Legend` instances
# (different from ``Axes.get_legend()``)
# lines A list of Figure `.Line2D` instances
# lines An `~.artist.ArtistList` of Figure `.Line2D` instances
# (rarely used, see ``Axes.lines``)
# patches A list of Figure `.Patch`\s
# patches An `~.artist.ArtistList` of Figure `.Patch`\s
# (rarely used, see ``Axes.patches``)
# texts A list Figure `.Text` instances
# texts An `~.artist.ArtistList` of Figure `.Text` instances
# artists An `~.artist.ArtistList` of all other `.Artist` instances
# ================ ============================================================
#
# .. _axes-container:
Expand Down Expand Up @@ -562,13 +566,13 @@ class in the Matplotlib API, and the one you will be working with most
# ============== =========================================
# Axes attribute Description
# ============== =========================================
# artists An `.ArtistList` of `.Artist` instances
# artists An `~.artist.ArtistList` of `.Artist` instances
# patch `.Rectangle` instance for Axes background
# collections An `.ArtistList` of `.Collection` instances
# images An `.ArtistList` of `.AxesImage`
# lines An `.ArtistList` of `.Line2D` instances
# patches An `.ArtistList` of `.Patch` instances
# texts An `.ArtistList` of `.Text` instances
# collections An `~.artist.ArtistList` of `.Collection` instances
# images An `~.artist.ArtistList` of `.AxesImage`
# lines An `~.artist.ArtistList` of `.Line2D` instances
# patches An `~.artist.ArtistList` of `.Patch` instances
# texts An `~.artist.ArtistList` of `.Text` instances
# xaxis A `matplotlib.axis.XAxis` instance
# yaxis A `matplotlib.axis.YAxis` instance
# ============== =========================================
Expand Down
2 changes: 1 addition & 1 deletion galleries/users_explain/text/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def __call__(self, x0, y0, width, height, mutation_size):

# %%
# Similarly, you can define a custom `.ConnectionStyle` and a custom `.ArrowStyle`. View
# the source code at `.patches` to learn how each class is defined.
# the source code at `~matplotlib.patches` to learn how each class is defined.
#
# .. _annotation_with_custom_arrow:
#
Expand Down
28 changes: 24 additions & 4 deletions lib/matplotlib/_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import functools
import itertools
import pathlib
import sys
import warnings

from .deprecation import ( # noqa: F401
Expand Down Expand Up @@ -470,7 +471,26 @@ def warn_external(message, category=None):
"""
# Go to Python's `site-packages` or `lib` from an editable install.
basedir = pathlib.Path(__file__).parents[2]
skip_file_prefixes = (str(basedir / 'matplotlib'),
str(basedir / 'mpl_toolkits'))

warnings.warn(message, category, skip_file_prefixes=skip_file_prefixes)
skip_file_prefixes = (
str(basedir / 'matplotlib'),
str(basedir / 'mpl_toolkits'),
# If we subclass a collections.abc class, the user may call an abc method that
# calls our method. For example if we warn within insert on a MutableSequence,
# and the user calls append or extend.
'<frozen _collections_abc>')

stacklevel = 2
if sys.version_info[:2] < (3, 14):
# Including the collections.abc string in skip_file_prefixes is not yet honored.
# Add the relevant frame count to the stacklevel instead.
Comment on lines +483 to +485

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't find anything in the python changenotes to understand what changed here, but I have verified that the relevant test passes with python 3.14.0.

frame = sys._getframe()
while True:
if frame.f_globals.get("__name__") == 'collections.abc':
stacklevel += 1

frame = frame.f_back
if frame is None:
break

warnings.warn(message, category, skip_file_prefixes=skip_file_prefixes,
stacklevel=stacklevel)
69 changes: 69 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import namedtuple
from collections.abc import Sequence
import contextlib
from functools import cache, reduce, wraps
import inspect
Expand Down Expand Up @@ -1789,6 +1790,74 @@ def pprint_getters(self):
return lines


class ArtistList(Sequence):
"""
A sublist of Axes or Figure children based on their type.

The Axes' type-specific children sublists were made immutable in Matplotlib
3.7. In the future these artist lists may be replaced by tuples. Use
as if this is a tuple already.
"""
def __init__(self, parent, prop_name, valid_types=None, invalid_types=None):
"""
Parameters
----------
parent : `~matplotlib.axes.Axes` or `~matplotlib.figure.FigureBase`
The Axes or (Sub)Figure from which this sublist will pull the children
Artists.
prop_name : str
The property name used to access this sublist from the parent.
valid_types : list of type, optional
A list of types that determine which children will be returned
by this sublist. If specified, then the Artists in the sublist
must be instances of any of these types. If unspecified, then
any type of Artist is valid (unless limited by
*invalid_types*.)
invalid_types : tuple, optional
A list of types that determine which children will *not* be
returned by this sublist. If specified, then Artists in the
sublist will never be an instance of these types. Otherwise, no
types will be excluded.
"""
self._parent = parent
self._prop_name = prop_name
self._type_check = lambda artist: (
(not valid_types or isinstance(artist, valid_types)) and
(not invalid_types or not isinstance(artist, invalid_types))
)

def __repr__(self):
parent_type = self._parent.__class__.__name__
return f'<{parent_type}.ArtistList of {len(self)} {self._prop_name}>'

def __len__(self):
return sum(self._type_check(artist) for artist in self._parent._children)

def __iter__(self):
for artist in list(self._parent._children):
if self._type_check(artist):
yield artist

def __getitem__(self, key):
return [artist
for artist in self._parent._children
if self._type_check(artist)][key]

def __add__(self, other):
if isinstance(other, (list, ArtistList)):
return [*self, *other]
if isinstance(other, (tuple, ArtistList)):
return (*self, *other)
return NotImplemented

def __radd__(self, other):
if isinstance(other, list):
return other + list(self)
if isinstance(other, tuple):
return other + tuple(self)
return NotImplemented


def getp(obj, property=None):
"""
Return the value of an `.Artist`'s *property*, or print all of them.
Expand Down
32 changes: 31 additions & 1 deletion lib/matplotlib/artist.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ from .transforms import (

import numpy as np

from collections.abc import Callable, Iterable
from collections.abc import Callable, Iterable, Iterator, Sequence
from typing import Any, Literal, NamedTuple, TextIO, overload, TypeVar
from numpy.typing import ArrayLike

Expand Down Expand Up @@ -189,6 +189,36 @@ class ArtistInspector:
def properties(self) -> dict[str, Any]: ...
def pprint_getters(self) -> list[str]: ...


class ArtistList(Sequence[_T_Artist]):
def __init__(
self,
parent: _AxesBase | Figure | SubFigure,
prop_name: str,
valid_types: type | Iterable[type] | None = ...,
invalid_types: type | Iterable[type] | None = ...,
) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[_T_Artist]: ...
@overload
def __getitem__(self, key: int) -> _T_Artist: ...
@overload
def __getitem__(self, key: slice) -> list[_T_Artist]: ...

@overload
def __add__(self, other: ArtistList[_T_Artist]) -> list[_T_Artist]: ...
@overload
def __add__(self, other: list[Any]) -> list[Any]: ...
@overload
def __add__(self, other: tuple[Any]) -> tuple[Any]: ...

@overload
def __radd__(self, other: ArtistList[_T_Artist]) -> list[_T_Artist]: ...
@overload
def __radd__(self, other: list[Any]) -> list[Any]: ...
@overload
def __radd__(self, other: tuple[Any]) -> tuple[Any]: ...

def getp(obj: Artist, property: str | None = ...) -> Any: ...

get = getp
Expand Down
Loading
Loading