From 74b2c2d495b2c7c5b812d8bd88a286ffb3e20e99 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 25 Aug 2022 00:07:11 -0400 Subject: [PATCH 1/3] Correctly handle Axes subclasses that override cla This fixes, e.g., Cartopy, but probably most other third-party packages that will subclass `Axes`. --- .../deprecations/23735-ES.rst | 13 +++++ lib/matplotlib/axes/_base.py | 39 +++++++++++-- lib/matplotlib/tests/test_axes.py | 57 ++++++++++++++++++- 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/23735-ES.rst 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..52f0a6e451a0 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,19 @@ 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 for `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,7 +1214,7 @@ 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__() @@ -1318,6 +1333,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 +1514,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"]) From 6e0ba4cb3b9bbe94e2cd5d1bdafe23e67724f973 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 26 Aug 2022 14:05:06 -0400 Subject: [PATCH 2/3] DOC: improve docstrings Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axes/_base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 52f0a6e451a0..9a16e855df7b 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -709,7 +709,7 @@ def __init_subclass__(cls, **kwargs): pending=True, message=f'Overriding `Axes.cla` in {cls.__qualname__} is ' 'pending deprecation in %(since)s and will be fully ' - 'deprecated for `Axes.clear` in the future. Please report ' + '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) @@ -1216,7 +1216,11 @@ def sharey(self, other): 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'): From f569d1a8e4bc8eeea13279d310d76c3041fe3a86 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 26 Aug 2022 14:32:54 -0400 Subject: [PATCH 3/3] MNT: remane _clear -> __clear --- lib/matplotlib/axes/_base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 9a16e855df7b..ebe1ef7911d4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -709,7 +709,8 @@ def __init_subclass__(cls, **kwargs): 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 ' + '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) @@ -1214,13 +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.""" # 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'): @@ -1344,14 +1344,14 @@ def clear(self): if self._subclass_uses_cla: self.cla() else: - self._clear() + 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() + self.__clear() else: self.clear()