Skip to content

Correctly handle Axes subclasses that override cla #23735

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 3 commits into from
Aug 26, 2022
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
13 changes: 13 additions & 0 deletions doc/api/next_api_changes/deprecations/23735-ES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
``AXes`` subclasses should override ``clear`` instead of ``cla``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For clarity, `.axes.Axes.clear` is now preferred over `.Axes.cla`. However, for
backwards compatibility, the latter will remain as an alias for the former.

For additional compatibility with third-party libraries, Matplotlib will
continue to call the ``cla`` method of any `~.axes.Axes` subclasses if they
define it. In the future, this will no longer occur, and Matplotlib will only
call the ``clear`` method in `~.axes.Axes` subclasses.

It is recommended to define only the ``clear`` method when on Matplotlib 3.6,
and only ``cla`` for older versions.
45 changes: 39 additions & 6 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,8 @@ class _AxesBase(martist.Artist):
_shared_axes = {name: cbook.Grouper() for name in _axis_names}
_twinned_axes = cbook.Grouper()

_subclass_uses_cla = False

@property
def _axis_map(self):
"""A mapping of axis names, e.g. 'x', to `Axis` instances."""
Expand Down Expand Up @@ -699,6 +701,20 @@ def __init__(self, fig, rect,
rcParams['ytick.major.right']),
which='major')

def __init_subclass__(cls, **kwargs):
parent_uses_cla = super(cls, cls)._subclass_uses_cla
if 'cla' in cls.__dict__:
_api.warn_deprecated(
'3.6',
pending=True,
message=f'Overriding `Axes.cla` in {cls.__qualname__} is '
'pending deprecation in %(since)s and will be fully '
'deprecated in favor of `Axes.clear` in the future. '
'Please report '
f'this to the {cls.__module__!r} author.')
cls._subclass_uses_cla = 'cla' in cls.__dict__ or parent_uses_cla
super().__init_subclass__(**kwargs)

def __getstate__(self):
state = super().__getstate__()
# Prune the sharing & twinning info to only contain the current group.
Expand Down Expand Up @@ -1199,9 +1215,12 @@ def sharey(self, other):
self.set_ylim(y0, y1, emit=False, auto=other.get_autoscaley_on())
self.yaxis._scale = other.yaxis._scale

def clear(self):
def __clear(self):
"""Clear the Axes."""
# Note: this is called by Axes.__init__()
# The actual implementation of clear() as long as clear() has to be
# an adapter delegating to the correct implementation.
# The implementation can move back into clear() when the
# deprecation on cla() subclassing expires.

# stash the current visibility state
if hasattr(self, 'patch'):
Expand Down Expand Up @@ -1318,6 +1337,24 @@ def clear(self):

self.stale = True

def clear(self):
"""Clear the Axes."""
# Act as an alias, or as the superclass implementation depending on the
# subclass implementation.
if self._subclass_uses_cla:
self.cla()
else:
self.__clear()

def cla(self):
"""Clear the Axes."""
# Act as an alias, or as the superclass implementation depending on the
# subclass implementation.
if self._subclass_uses_cla:
self.__clear()
else:
self.clear()

class ArtistList(MutableSequence):
"""
A sublist of Axes children based on their type.
Expand Down Expand Up @@ -1481,10 +1518,6 @@ def texts(self):
return self.ArtistList(self, 'texts', 'add_artist',
valid_types=mtext.Text)

def cla(self):
"""Clear the Axes."""
self.clear()

def get_facecolor(self):
"""Get the facecolor of the Axes."""
return self.patch.get_facecolor()
Expand Down
57 changes: 54 additions & 3 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,10 +486,61 @@ def test_inverted_cla():
plt.close(fig)


def test_cla_not_redefined():
def test_subclass_clear_cla():
# Ensure that subclasses of Axes call cla/clear correctly.
# Note, we cannot use mocking here as we want to be sure that the
# superclass fallback does not recurse.

with pytest.warns(match='Overriding `Axes.cla`'):
class ClaAxes(Axes):
def cla(self):
nonlocal called
called = True

with pytest.warns(match='Overriding `Axes.cla`'):
class ClaSuperAxes(Axes):
def cla(self):
nonlocal called
called = True
super().cla()

class SubClaAxes(ClaAxes):
pass

class ClearAxes(Axes):
def clear(self):
nonlocal called
called = True

class ClearSuperAxes(Axes):
def clear(self):
nonlocal called
called = True
super().clear()

class SubClearAxes(ClearAxes):
pass

fig = Figure()
for axes_class in [ClaAxes, ClaSuperAxes, SubClaAxes,
ClearAxes, ClearSuperAxes, SubClearAxes]:
called = False
ax = axes_class(fig, [0, 0, 1, 1])
# Axes.__init__ has already called clear (which aliases to cla or is in
# the subclass).
assert called

called = False
ax.cla()
assert called


def test_cla_not_redefined_internally():
for klass in Axes.__subclasses__():
# check that cla does not get redefined in our Axes subclasses
assert 'cla' not in klass.__dict__
# Check that cla does not get redefined in our Axes subclasses, except
# for in the above test function.
if 'test_subclass_clear_cla' not in klass.__qualname__:
assert 'cla' not in klass.__dict__


@check_figures_equal(extensions=["png"])
Expand Down