diff --git a/doc/api/next_api_changes/deprecations/23735-ES.rst b/doc/api/next_api_changes/deprecations/23735-ES.rst new file mode 100644 index 000000000000..075abf73d9d4 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/23735-ES.rst @@ -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. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 7e6f1ab3e6c2..ebe1ef7911d4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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.""" @@ -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. @@ -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'): @@ -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. @@ -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() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 268eb957f470..a230af2ac1e0 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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"])