Skip to content

Mnt/no reset axes #24626

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
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
61 changes: 49 additions & 12 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

_log = logging.getLogger(__name__)

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


def _prevent_rasterization(draw):
# We assume that by default artists are not allowed to rasterize (unless
Expand Down Expand Up @@ -181,7 +184,7 @@ def __init__(self):
self._stale = True
self.stale_callback = None
self._axes = None
self.figure = None
self._figure = None

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

@axes.setter
def axes(self, new_axes):
if (new_axes is not None and self._axes is not None
and new_axes != self._axes):
raise ValueError("Can not reset the Axes. You are probably trying to reuse "
"an artist in more than one Axes which is not supported")
if self._axes is _tombstone:
extra = (
f"The artist {self!r} has had its Axes reset to None " +
"but was previously included in one."
)
else:
extra = (
f"The artist {self!r} is currently in {self._axes!r} " +
f"in the figure {self._axes.figure!r}."
)
raise ValueError("Can not reset the Axes. You are probably " +
"trying to re-use an artist in more than one " +
"Axes which is not supported. " +
extra
)
if self._axes is not None and new_axes is None:
new_axes = _tombstone
self._axes = new_axes
if new_axes is not None and new_axes is not self:
self.stale_callback = _stale_axes_callback

@property
def figure(self):
"""The `~.figure.Figure` instance the artist resides in, or *None*."""
return self._figure if self._figure is not _tombstone else None

@figure.setter
def figure(self, new_figure):
if (new_figure is not None and self._figure is not None
and new_figure != self._figure and new_figure is not True):
if self._figure is _tombstone:
extra = (
f"The artist {self!r} has had its Figure reset to None " +
"but was previously included in one."
)
else:
extra = (
f"The artist {self!r} is currently in {self._figure!r}."
)
raise ValueError("Can not reset the figure. You are probably " +
"trying to re-use an artist in more than one " +
"Figure which is not supported. " +
extra
)
if self._figure is not None and new_figure is None:
new_figure = _tombstone
self._figure = new_figure

@property
def stale(self):
"""
Expand Down Expand Up @@ -738,14 +783,6 @@ def set_figure(self, fig):
# if this is a no-op just return
if self.figure is fig:
return
# if we currently have a figure (the case of both `self.figure`
# and *fig* being none is taken care of above) we then user is
# trying to change the figure an artist is associated with which
# is not allowed for the same reason as adding the same instance
# to more than one Axes
if self.figure is not None:
raise RuntimeError("Can not put single artist in "
"more than one figure")
self.figure = fig
if self.figure and self.figure is not self:
self.pchanged()
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def __init__(self, **kwargs):
# axis._get_tick_boxes_siblings
self._align_label_groups = {"x": cbook.Grouper(), "y": cbook.Grouper()}

self._figure = self
self._localaxes = [] # track all Axes
self.artists = []
self.lines = []
Expand Down Expand Up @@ -2162,7 +2163,7 @@ def __init__(self, parent, subplotspec, *,

self._subplotspec = subplotspec
self._parent = parent
self.figure = parent.figure
self._figure = parent.figure

# subfigures use the parent axstack
self._axstack = parent._axstack
Expand Down
33 changes: 33 additions & 0 deletions lib/matplotlib/tests/test_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,36 @@ def draw(self, renderer, extra):

assert 'aardvark' == art.draw(renderer, 'aardvark')
assert 'aardvark' == art.draw(renderer, extra='aardvark')


def test_no_reset():
fig, (ax1, ax2) = plt.subplots(2)
fig2 = plt.figure()
ln, = ax1.plot(range(5))
with pytest.raises(
ValueError, match=f"The artist {ln!r} is currently in {ax1!r}"
):
ax2.add_line(ln)
with pytest.raises(
ValueError, match=f"The artist {ln!r} is currently in {ax1!r}"
):
ln.axes = ax2
with pytest.raises(
ValueError, match=f"The artist {ln!r} is currently in {fig!r}"
):
ln.figure = fig2

ln.remove()
assert ln.figure is None
assert ln.axes is None
with pytest.raises(
ValueError, match=f"The artist {ln!r} has had its Figure reset "
):
ax2.add_line(ln)
with pytest.raises(
ValueError, match=f"The artist {ln!r} has had its Axes reset "
):
ln.axes = ax2

assert ln.axes is None
assert ln.figure is None
3 changes: 2 additions & 1 deletion lib/matplotlib/tests/test_offsetbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,9 @@ def test_offsetbox_loc_codes():
'center': 10,
}
fig, ax = plt.subplots()
da = DrawingArea(100, 100)

for code in codes:
da = DrawingArea(100, 100)
anchored_box = AnchoredOffsetbox(loc=code, child=da)
ax.add_artist(anchored_box)
fig.canvas.draw()
Expand Down