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

Skip to content

Commit c1fc4cf

Browse files
committed
Rationalise artist get_figure; make figure attribute a property
1 parent 9387431 commit c1fc4cf

File tree

12 files changed

+164
-26
lines changed

12 files changed

+164
-26
lines changed

ci/mypy-stubtest-allowlist.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ matplotlib.tri.*TriInterpolator.gradient
4646
matplotlib.backend_bases.FigureCanvasBase._T
4747
matplotlib.backend_managers.ToolManager._T
4848
matplotlib.spines.Spine._T
49+
50+
# Parameter inconsistency due to 3.10 deprecation
51+
matplotlib.figure.FigureBase.get_figure
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
(Sub)Figure.get_figure
2+
~~~~~~~~~~~~~~~~~~~~~~
3+
4+
...in future will by default return the direct parent figure, which may be a SubFigure.
5+
This will make the default behavior consistent with the
6+
`~matplotlib.artist.Artist.get_figure` method of other artists. To control the
7+
behavior, use the *root* parameter.
8+
9+
10+
(Sub)Figure.set_figure
11+
~~~~~~~~~~~~~~~~~~~~~~
12+
13+
...in future will always raise an exception. The parent and root figures of a
14+
(Sub)Figure are set at instantiation and cannot be changed.

lib/matplotlib/artist.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def __init__(self):
181181
self._stale = True
182182
self.stale_callback = None
183183
self._axes = None
184-
self.figure = None
184+
self._parent_figure = None
185185

186186
self._transform = None
187187
self._transformSet = False
@@ -251,7 +251,7 @@ def remove(self):
251251
if self.figure:
252252
if not _ax_flag:
253253
self.figure.stale = True
254-
self.figure = None
254+
self._parent_figure = None
255255

256256
else:
257257
raise NotImplementedError('cannot remove artist')
@@ -720,34 +720,49 @@ def set_path_effects(self, path_effects):
720720
def get_path_effects(self):
721721
return self._path_effects
722722

723-
def get_figure(self):
724-
"""Return the `.Figure` instance the artist belongs to."""
725-
return self.figure
723+
def get_figure(self, root=False):
724+
"""
725+
Return the `.Figure` or `.SubFigure` instance the artist belongs to.
726+
727+
Parameters
728+
----------
729+
root : bool, default=False
730+
If False, return the (Sub)Figure this artist is on. If True,
731+
return the root Figure for a nested tree of SubFigures.
732+
"""
733+
if root and self._parent_figure is not None:
734+
return self._parent_figure.get_figure(root=True)
735+
736+
return self._parent_figure
726737

727738
def set_figure(self, fig):
728739
"""
729-
Set the `.Figure` instance the artist belongs to.
740+
Set the `.Figure` or `.SubFigure` instance the artist belongs to.
730741
731742
Parameters
732743
----------
733-
fig : `~matplotlib.figure.Figure`
744+
fig : `~matplotlib.figure.Figure` or `~matplotlib.figure.SubFigure`
734745
"""
735746
# if this is a no-op just return
736-
if self.figure is fig:
747+
if self._parent_figure is fig:
737748
return
738749
# if we currently have a figure (the case of both `self.figure`
739750
# and *fig* being none is taken care of above) we then user is
740751
# trying to change the figure an artist is associated with which
741752
# is not allowed for the same reason as adding the same instance
742753
# to more than one Axes
743-
if self.figure is not None:
754+
if self._parent_figure is not None:
744755
raise RuntimeError("Can not put single artist in "
745756
"more than one figure")
746-
self.figure = fig
747-
if self.figure and self.figure is not self:
757+
self._parent_figure = fig
758+
if self._parent_figure and self._parent_figure is not self:
748759
self.pchanged()
749760
self.stale = True
750761

762+
figure = property(get_figure, set_figure,
763+
doc=("The (Sub)Figure that the artist is on. For more "
764+
"control, use the `get_figure` method."))
765+
751766
def set_clip_box(self, clipbox):
752767
"""
753768
Set the artist's clip `.Bbox`.

lib/matplotlib/artist.pyi

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class _Unset: ...
3131
class Artist:
3232
zorder: float
3333
stale_callback: Callable[[Artist, bool], None] | None
34-
figure: Figure | SubFigure | None
34+
@property
35+
def figure(self) -> Figure | SubFigure: ...
3536
clipbox: BboxBase | None
3637
def __init__(self) -> None: ...
3738
def remove(self) -> None: ...
@@ -87,8 +88,8 @@ class Artist:
8788
) -> None: ...
8889
def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ...
8990
def get_path_effects(self) -> list[AbstractPathEffect]: ...
90-
def get_figure(self) -> Figure | None: ...
91-
def set_figure(self, fig: Figure) -> None: ...
91+
def get_figure(self, root: bool = ...) -> Figure | SubFigure | None: ...
92+
def set_figure(self, fig: Figure | SubFigure) -> None: ...
9293
def set_clip_box(self, clipbox: BboxBase | None) -> None: ...
9394
def set_clip_path(
9495
self,

lib/matplotlib/axes/_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1295,7 +1295,7 @@ def __clear(self):
12951295
self._gridOn = mpl.rcParams['axes.grid']
12961296
old_children, self._children = self._children, []
12971297
for chld in old_children:
1298-
chld.axes = chld.figure = None
1298+
chld.axes = chld._parent_figure = None
12991299
self._mouseover_set = _OrderedSet()
13001300
self.child_axes = []
13011301
self._current_image = None # strictly for pyplot via _sci, _gci

lib/matplotlib/axes/_base.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ from matplotlib.cm import ScalarMappable
1313
from matplotlib.legend import Legend
1414
from matplotlib.lines import Line2D
1515
from matplotlib.gridspec import SubplotSpec, GridSpec
16-
from matplotlib.figure import Figure
16+
from matplotlib.figure import Figure, SubFigure
1717
from matplotlib.image import AxesImage
1818
from matplotlib.patches import Patch
1919
from matplotlib.scale import ScaleBase
@@ -81,7 +81,7 @@ class _AxesBase(martist.Artist):
8181
def get_subplotspec(self) -> SubplotSpec | None: ...
8282
def set_subplotspec(self, subplotspec: SubplotSpec) -> None: ...
8383
def get_gridspec(self) -> GridSpec | None: ...
84-
def set_figure(self, fig: Figure) -> None: ...
84+
def set_figure(self, fig: Figure | SubFigure) -> None: ...
8585
@property
8686
def viewLim(self) -> Bbox: ...
8787
def get_xaxis_transform(

lib/matplotlib/figure.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from contextlib import ExitStack
3131
import inspect
3232
import itertools
33+
import functools
3334
import logging
3435
from numbers import Integral
3536
import threading
@@ -227,6 +228,60 @@ def get_children(self):
227228
*self.legends,
228229
*self.subfigs]
229230

231+
def get_figure(self, root=None):
232+
"""
233+
Return the `.Figure` or `.SubFigure` instance the (Sub)Figure belongs to.
234+
235+
Parameters
236+
----------
237+
root : bool, default=True
238+
If False, return the (Sub)Figure this artist is on. If True,
239+
return the root Figure for a nested tree of SubFigures.
240+
241+
.. deprecated:: 3.10
242+
243+
From version 3.12 *root* will default to False.
244+
"""
245+
if self._root_figure is self:
246+
# Top level Figure
247+
return self
248+
249+
if root is None:
250+
# When deprecation expires, consider removing the docstring and just
251+
# inheriting the one from Artist.
252+
message = ('From Matplotlib 3.12 SubFigure.get_figure will by default '
253+
'return the direct parent figure, which may be a SubFigure. '
254+
'To suppress this warning, pass the root parameter.')
255+
_api.warn_deprecated('3.10', message=message)
256+
root = True
257+
258+
if root:
259+
return self._root_figure
260+
261+
return self._parent
262+
263+
def set_figure(self, fig):
264+
"""
265+
.. deprecated:: 3.10
266+
Currently this method will raise an exception if *fig* is anything other
267+
than the root `.Figure` this (Sub)Figure is on. In future it will always
268+
raise an exception.
269+
"""
270+
no_switch = ("The parent and root figures of a (Sub)Figure are set at "
271+
"instantiation and cannot be changed.")
272+
if fig is self._root_figure:
273+
_api.warn_deprecated(
274+
"3.10",
275+
message=(f"{no_switch} From Matplotlib 3.12 this operation will raise "
276+
"an exception."))
277+
return
278+
279+
raise ValueError(no_switch)
280+
281+
figure = property(functools.partial(get_figure, root=True), set_figure,
282+
doc=("The root `Figure`. To get the parent of a `SubFigure`, "
283+
"use the `get_figure` method."))
284+
230285
def contains(self, mouseevent):
231286
"""
232287
Test whether the mouse event occurred on the figure.
@@ -2215,7 +2270,7 @@ def __init__(self, parent, subplotspec, *,
22152270

22162271
self._subplotspec = subplotspec
22172272
self._parent = parent
2218-
self.figure = parent.figure
2273+
self._root_figure = parent._root_figure
22192274

22202275
# subfigures use the parent axstack
22212276
self._axstack = parent._axstack
@@ -2493,7 +2548,7 @@ def __init__(self,
24932548
%(Figure:kwdoc)s
24942549
"""
24952550
super().__init__(**kwargs)
2496-
self.figure = self
2551+
self._root_figure = self
24972552
self._layout_engine = None
24982553

24992554
if layout is not None:

lib/matplotlib/figure.pyi

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ class FigureBase(Artist):
246246
) -> dict[Hashable, Axes]: ...
247247

248248
class SubFigure(FigureBase):
249-
figure: Figure
249+
@property
250+
def figure(self) -> Figure: ...
250251
subplotpars: SubplotParams
251252
dpi_scale_trans: Affine2D
252253
canvas: FigureCanvasBase
@@ -283,7 +284,8 @@ class SubFigure(FigureBase):
283284
def get_axes(self) -> list[Axes]: ...
284285

285286
class Figure(FigureBase):
286-
figure: Figure
287+
@property
288+
def figure(self) -> Figure: ...
287289
bbox_inches: Bbox
288290
dpi_scale_trans: Affine2D
289291
bbox: BboxBase

lib/matplotlib/offsetbox.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import matplotlib.artist as martist
22
from matplotlib.backend_bases import RendererBase, Event, FigureCanvasBase
33
from matplotlib.colors import Colormap, Normalize
44
import matplotlib.text as mtext
5-
from matplotlib.figure import Figure
5+
from matplotlib.figure import Figure, SubFigure
66
from matplotlib.font_manager import FontProperties
77
from matplotlib.image import BboxImage
88
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
@@ -26,7 +26,7 @@ class OffsetBox(martist.Artist):
2626
width: float | None
2727
height: float | None
2828
def __init__(self, *args, **kwargs) -> None: ...
29-
def set_figure(self, fig: Figure) -> None: ...
29+
def set_figure(self, fig: Figure | SubFigure) -> None: ...
3030
def set_offset(
3131
self,
3232
xy: tuple[float, float]
@@ -271,7 +271,7 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase):
271271
| Callable[[RendererBase], Bbox | Transform],
272272
) -> None: ...
273273
def get_children(self) -> list[martist.Artist]: ...
274-
def set_figure(self, fig: Figure) -> None: ...
274+
def set_figure(self, fig: Figure | SubFigure) -> None: ...
275275
def set_fontsize(self, s: str | float | None = ...) -> None: ...
276276
def get_fontsize(self) -> float: ...
277277
def get_tightbbox(self, renderer: RendererBase | None = ...) -> Bbox: ...

lib/matplotlib/quiver.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import matplotlib.artist as martist
22
import matplotlib.collections as mcollections
33
from matplotlib.axes import Axes
4-
from matplotlib.figure import Figure
4+
from matplotlib.figure import Figure, SubFigure
55
from matplotlib.text import Text
66
from matplotlib.transforms import Transform, Bbox
77

@@ -49,7 +49,7 @@ class QuiverKey(martist.Artist):
4949
) -> None: ...
5050
@property
5151
def labelsep(self) -> float: ...
52-
def set_figure(self, fig: Figure) -> None: ...
52+
def set_figure(self, fig: Figure | SubFigure) -> None: ...
5353

5454
class Quiver(mcollections.PolyCollection):
5555
X: ArrayLike

lib/matplotlib/tests/test_artist.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,35 @@ def draw(self, renderer, extra):
562562

563563
assert 'aardvark' == art.draw(renderer, 'aardvark')
564564
assert 'aardvark' == art.draw(renderer, extra='aardvark')
565+
566+
567+
def test_get_figure():
568+
fig = plt.figure()
569+
sfig1 = fig.subfigures()
570+
sfig2 = sfig1.subfigures()
571+
ax = sfig2.subplots()
572+
573+
assert fig.get_figure(root=True) is fig
574+
assert fig.get_figure(root=False) is fig
575+
576+
assert ax.get_figure() is sfig2
577+
assert ax.get_figure(root=False) is sfig2
578+
assert ax.get_figure(root=True) is fig
579+
580+
# SubFigure.get_figure has separate implementation but should give consistent
581+
# results to other artists.
582+
assert sfig2.get_figure(root=False) is sfig1
583+
assert sfig2.get_figure(root=True) is fig
584+
# Currently different results by default.
585+
with pytest.warns(mpl.MatplotlibDeprecationWarning):
586+
assert sfig2.get_figure() is fig
587+
588+
# An artist not yet attached to anything has no figure.
589+
ln = mlines.Line2D([], [])
590+
assert ln.get_figure(root=True) is None
591+
assert ln.get_figure(root=False) is None
592+
593+
# figure attribute is root for (Sub)Figures but parent for other artists.
594+
assert ax.figure == sfig2
595+
assert fig.figure == fig
596+
assert sfig2.figure == fig

lib/matplotlib/tests/test_figure.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,3 +1733,19 @@ def test_warn_colorbar_mismatch():
17331733
subfig3_1.colorbar(im3_2) # should not warn
17341734
with pytest.warns(UserWarning, match="different Figure"):
17351735
subfig3_1.colorbar(im4_1)
1736+
1737+
1738+
def test_set_figure():
1739+
fig = plt.figure()
1740+
sfig1 = fig.subfigures()
1741+
sfig2 = sfig1.subfigures()
1742+
1743+
for f in fig, sfig1, sfig2:
1744+
with pytest.warns(mpl.MatplotlibDeprecationWarning):
1745+
f.set_figure(fig)
1746+
1747+
with pytest.raises(ValueError, match="cannot be changed"):
1748+
sfig2.set_figure(sfig1)
1749+
1750+
with pytest.raises(ValueError, match="cannot be changed"):
1751+
sfig1.set_figure(plt.figure())

0 commit comments

Comments
 (0)