Skip to content

Commit bbe4b95

Browse files
committed
MNT: Track if we ever set the Axes or Figure when cleared
closes matplotlib#6793
1 parent 4f56e1f commit bbe4b95

3 files changed

Lines changed: 84 additions & 13 deletions

File tree

‎lib/matplotlib/artist.py‎

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
_log = logging.getLogger(__name__)
2222

23+
# a tombstone sentinel to ensure we never reset the axes or figure on an Artist
24+
_tombstone = object()
25+
2326

2427
def _prevent_rasterization(draw):
2528
# We assume that by default artists are not allowed to rasterize (unless
@@ -181,7 +184,7 @@ def __init__(self):
181184
self._stale = True
182185
self.stale_callback = None
183186
self._axes = None
184-
self.figure = None
187+
self._figure = None
185188

186189
self._transform = None
187190
self._transformSet = False
@@ -293,18 +296,60 @@ def convert_yunits(self, y):
293296
@property
294297
def axes(self):
295298
"""The `~.axes.Axes` instance the artist resides in, or *None*."""
296-
return self._axes
299+
return self._axes if self._axes is not _tombstone else None
297300

298301
@axes.setter
299302
def axes(self, new_axes):
300303
if (new_axes is not None and self._axes is not None
301304
and new_axes != self._axes):
302-
raise ValueError("Can not reset the Axes. You are probably trying to reuse "
303-
"an artist in more than one Axes which is not supported")
305+
if self._axes is _tombstone:
306+
extra = (
307+
f"The artist {self!r} has had its Axes reset to None " +
308+
"but was previously included in one."
309+
)
310+
else:
311+
extra = (
312+
f"The artist {self!r} is currently in {self._axes!r} " +
313+
f"in the figure {self._axes.figure!r}."
314+
)
315+
raise ValueError("Can not reset the Axes. You are probably " +
316+
"trying to re-use an artist in more than one " +
317+
"Axes which is not supported. " +
318+
extra
319+
)
320+
if self._axes is not None and new_axes is None:
321+
new_axes = _tombstone
304322
self._axes = new_axes
305323
if new_axes is not None and new_axes is not self:
306324
self.stale_callback = _stale_axes_callback
307325

326+
@property
327+
def figure(self):
328+
"""The `~.figure.Figure` instance the artist resides in, or *None*."""
329+
return self._figure if self._figure is not _tombstone else None
330+
331+
@figure.setter
332+
def figure(self, new_figure):
333+
if (new_figure is not None and self._figure is not None
334+
and new_figure != self._figure and new_figure is not True):
335+
if self._figure is _tombstone:
336+
extra = (
337+
f"The artist {self!r} has had its Figure reset to None " +
338+
"but was previously included in one."
339+
)
340+
else:
341+
extra = (
342+
f"The artist {self!r} is currently in {self._figure!r}."
343+
)
344+
raise ValueError("Can not reset the figure. You are probably " +
345+
"trying to re-use an artist in more than one " +
346+
"Figure which is not supported. " +
347+
extra
348+
)
349+
if self._figure is not None and new_figure is None:
350+
new_figure = _tombstone
351+
self._figure = new_figure
352+
308353
@property
309354
def stale(self):
310355
"""
@@ -738,14 +783,6 @@ def set_figure(self, fig):
738783
# if this is a no-op just return
739784
if self.figure is fig:
740785
return
741-
# if we currently have a figure (the case of both `self.figure`
742-
# and *fig* being none is taken care of above) we then user is
743-
# trying to change the figure an artist is associated with which
744-
# is not allowed for the same reason as adding the same instance
745-
# to more than one Axes
746-
if self.figure is not None:
747-
raise RuntimeError("Can not put single artist in "
748-
"more than one figure")
749786
self.figure = fig
750787
if self.figure and self.figure is not self:
751788
self.pchanged()

‎lib/matplotlib/figure.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def __init__(self, **kwargs):
137137
# axis._get_tick_boxes_siblings
138138
self._align_label_groups = {"x": cbook.Grouper(), "y": cbook.Grouper()}
139139

140+
self._figure = self
140141
self._localaxes = [] # track all Axes
141142
self.artists = []
142143
self.lines = []
@@ -2162,7 +2163,7 @@ def __init__(self, parent, subplotspec, *,
21622163

21632164
self._subplotspec = subplotspec
21642165
self._parent = parent
2165-
self.figure = parent.figure
2166+
self._figure = parent.figure
21662167

21672168
# subfigures use the parent axstack
21682169
self._axstack = parent._axstack

‎lib/matplotlib/tests/test_artist.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,36 @@ 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_no_reset():
568+
fig, (ax1, ax2) = plt.subplots(2)
569+
fig2 = plt.figure()
570+
ln, = ax1.plot(range(5))
571+
with pytest.raises(
572+
ValueError, match=f"The artist {ln!r} is currently in {ax1!r}"
573+
):
574+
ax2.add_line(ln)
575+
with pytest.raises(
576+
ValueError, match=f"The artist {ln!r} is currently in {ax1!r}"
577+
):
578+
ln.axes = ax2
579+
with pytest.raises(
580+
ValueError, match=f"The artist {ln!r} is currently in {fig!r}"
581+
):
582+
ln.figure = fig2
583+
584+
ln.remove()
585+
assert ln.figure is None
586+
assert ln.axes is None
587+
with pytest.raises(
588+
ValueError, match=f"The artist {ln!r} has had its Figure reset "
589+
):
590+
ax2.add_line(ln)
591+
with pytest.raises(
592+
ValueError, match=f"The artist {ln!r} has had its Axes reset "
593+
):
594+
ln.axes = ax2
595+
596+
assert ln.axes is None
597+
assert ln.figure is None

0 commit comments

Comments
 (0)