From 2d5e34580fb01358a8f80239e8cd59d096404aed Mon Sep 17 00:00:00 2001 From: Leo Singer Date: Wed, 18 Nov 2020 20:39:28 -0500 Subject: [PATCH 1/2] MNT: Remove deprecated axes kwargs collision detection In Matplotlib 2.1, the behavior of reusing existing axes when created with the same arguments was deprecated (see #9037). This behavior is now removed. Functions that create new axes (`axes`, `add_axes`, `subplot`, etc.) will now always create new axes, regardless of whether the kwargs passed to them match already existing axes. Passing kwargs to `gca` is deprecated. If `gca` is called with kwargs that do not match the current axes, then an exception is raised. Fixes #18832. --- .../deprecations/18978-LPS.rst | 5 + .../development/18978-LPS.rst | 8 ++ .../next_whats_new/axes_kwargs_collision.rst | 18 +++ lib/matplotlib/figure.py | 111 ++++-------------- lib/matplotlib/pyplot.py | 7 +- lib/matplotlib/tests/test_axes.py | 32 +++-- lib/matplotlib/tests/test_figure.py | 33 ++++-- 7 files changed, 104 insertions(+), 110 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/18978-LPS.rst create mode 100644 doc/api/next_api_changes/development/18978-LPS.rst create mode 100644 doc/users/next_whats_new/axes_kwargs_collision.rst diff --git a/doc/api/next_api_changes/deprecations/18978-LPS.rst b/doc/api/next_api_changes/deprecations/18978-LPS.rst new file mode 100644 index 000000000000..705f672d33b5 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/18978-LPS.rst @@ -0,0 +1,5 @@ +pyplot.gca() +~~~~~~~~~~~~ + +Passing keyword arguments to ``.pyplot.gca`` will not be supported in a future +release. diff --git a/doc/api/next_api_changes/development/18978-LPS.rst b/doc/api/next_api_changes/development/18978-LPS.rst new file mode 100644 index 000000000000..cd590764492d --- /dev/null +++ b/doc/api/next_api_changes/development/18978-LPS.rst @@ -0,0 +1,8 @@ +Changes to _AxesStack, preparing for its removal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The behavior of the internal ``.figure._AxesStack`` class has changed +significantly in the process of removing the old behavior of gca() with regard +to keyword arguments. When the deprecated behavior has been fully removed and +gca() no longer takes keyword arguments, the ``.figure._AxesStack`` class will +be removed. diff --git a/doc/users/next_whats_new/axes_kwargs_collision.rst b/doc/users/next_whats_new/axes_kwargs_collision.rst new file mode 100644 index 000000000000..f04f5e3f82b1 --- /dev/null +++ b/doc/users/next_whats_new/axes_kwargs_collision.rst @@ -0,0 +1,18 @@ +Changes to behavior of Axes creation methods (gca(), add_axes(), add_subplot()) +------------------------------------------------------------------------------- + +The behavior of the functions to create new axes (``.pyplot.subplot``, +``.figure.Figure.add_axes``, ``.figure.Figure.add_subplot``) has changed. In +the past, these functions would detect if you were attempting to create Axes +with the same keyword arguments as already-existing axes in the current figure, +and if so, they would return the existing Axes. Now, these functions will +always create new Axes. + +Correspondingly, the behavior of the functions to get the current Axes +(``.pyplot.gca``, ``.figure.Figure.gca``) has changed. In the past, these +functions accepted keyword arguments. If the keyword arguments matched an +already-existing Axes, then that Axes would be returned, otherwise new Axes +would be created with those keyword arguments. Now, an exception is raised if +there are Axes and the current Axes were not created with the same keyword +arguments. In a future release, these functions will not accept keyword +arguments at all. diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 67436b15f444..7c2d723e30b1 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -56,48 +56,22 @@ class _AxesStack(cbook.Stack): Specialization of `.Stack`, to handle all tracking of `~.axes.Axes` in a `.Figure`. - This stack stores ``key, (ind, axes)`` pairs, where: - - * **key** is a hash of the args and kwargs used in generating the Axes. - * **ind** is a serial index tracking the order in which Axes were added. + This stack stores ``key, axes`` pairs, where **key** is a hash of the args + and kwargs used in generating the Axes. AxesStack is a callable; calling it returns the current Axes. The `current_key_axes` method returns the current key and associated Axes. """ - def __init__(self): - super().__init__() - self._ind = 0 - def as_list(self): """ Return a list of the Axes instances that have been added to the figure. """ - ia_list = [a for k, a in self._elements] - ia_list.sort() - return [a for i, a in ia_list] - - def get(self, key): - """ - Return the Axes instance that was added with *key*. - If it is not present, return *None*. - """ - item = dict(self._elements).get(key) - if item is None: - return None - cbook.warn_deprecated( - "2.1", - message="Adding an axes using the same arguments as a previous " - "axes currently reuses the earlier instance. In a future " - "version, a new instance will always be created and returned. " - "Meanwhile, this warning can be suppressed, and the future " - "behavior ensured, by passing a unique label to each axes " - "instance.") - return item[1] + return [a for k, a in self._elements] def _entry_from_axes(self, e): - ind, k = {a: (ind, k) for k, (ind, a) in self._elements}[e] - return (k, (ind, e)) + k = {a: k for k, a in self._elements}[e] + return (k, e) def remove(self, a): """Remove the Axes from the stack.""" @@ -114,30 +88,13 @@ def add(self, key, a): """ Add Axes *a*, with key *key*, to the stack, and return the stack. - If *key* is unhashable, replace it by a unique, arbitrary object. - If *a* is already on the stack, don't add it again, but return *None*. """ # All the error checking may be unnecessary; but this method # is called so seldom that the overhead is negligible. cbook._check_isinstance(Axes, a=a) - try: - hash(key) - except TypeError: - key = object() - - a_existing = self.get(key) - if a_existing is not None: - super().remove((key, a_existing)) - cbook._warn_external( - "key {!r} already existed; Axes is being replaced".format(key)) - # I don't think the above should ever happen. - - if a in self: - return None - self._ind += 1 - return super().push((key, (self._ind, a))) + return super().push((key, a)) def current_key_axes(self): """ @@ -145,14 +102,10 @@ def current_key_axes(self): If no Axes exists on the stack, then returns ``(None, None)``. """ - if not len(self._elements): - return self._default, self._default - else: - key, (index, axes) = self._elements[self._pos] - return key, axes + return super().__call__() or (None, None) def __call__(self): - return self.current_key_axes()[1] + ka = self.current_key_axes()[1] def __contains__(self, a): return a in self.as_list() @@ -689,15 +642,8 @@ def add_axes(self, *args, **kwargs): "add_axes() got multiple values for argument 'rect'") args = (kwargs.pop('rect'), ) - # shortcut the projection "key" modifications later on, if an axes - # with the exact args/kwargs exists, return it immediately. - key = self._make_key(*args, **kwargs) - ax = self._axstack.get(key) - if ax is not None: - self.sca(ax) - return ax - if isinstance(args[0], Axes): + key = self._make_key(*args, **kwargs) a = args[0] if a.get_figure() is not self: raise ValueError( @@ -710,13 +656,6 @@ def add_axes(self, *args, **kwargs): projection_class, kwargs, key = \ self._process_projection_requirements(*args, **kwargs) - # check that an axes of this type doesn't already exist, if it - # does, set it as active and return it - ax = self._axstack.get(key) - if isinstance(ax, projection_class): - self.sca(ax) - return ax - # create the new axes using the axes class given a = projection_class(self, rect, **kwargs) return self._add_axes_internal(key, a) @@ -861,19 +800,6 @@ def add_subplot(self, *args, **kwargs): args = tuple(map(int, str(args[0]))) projection_class, kwargs, key = \ self._process_projection_requirements(*args, **kwargs) - ax = self._axstack.get(key) # search axes with this key in stack - if ax is not None: - if isinstance(ax, projection_class): - # the axes already existed, so set it as active & return - self.sca(ax) - return ax - else: - # Undocumented convenience behavior: - # subplot(111); subplot(111, projection='polar') - # will replace the first with the second. - # Without this, add_subplot would be simpler and - # more similar to add_axes. - self._axstack.remove(ax) ax = subplot_class_factory(projection_class)(self, *args, **kwargs) return self._add_axes_internal(key, ax) @@ -1601,6 +1527,16 @@ def gca(self, **kwargs): %(Axes)s """ + if kwargs: + cbook.warn_deprecated( + "3.4", + message="Calling gca() with keyword arguments is deprecated. " + "In a future version, gca() will take no keyword arguments. " + "The gca() function should only be used to get the current " + "axes, or if no axes exist, create new axes with default " + "keyword arguments. To create a new axes with non-default " + "arguments, use plt.axes() or plt.subplot().") + ckey, cax = self._axstack.current_key_axes() # if there exists an axes on the stack see if it matches # the desired axes configuration @@ -1627,10 +1563,11 @@ def gca(self, **kwargs): if key == ckey and isinstance(cax, projection_class): return cax else: - cbook._warn_external('Requested projection is different ' - 'from current axis projection, ' - 'creating new axis with requested ' - 'projection.') + raise ValueError( + "The arguments passed to gca() did not match the " + "arguments with which the current axes were " + "originally created. To create new axes, use " + "axes() or subplot().") # no axes found, so create one which spans the figure return self.add_subplot(1, 1, 1, **kwargs) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 7b35433bd3c2..a8a9b81a56bf 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2410,10 +2410,13 @@ def polar(*args, **kwargs): """ # If an axis already exists, check if it has a polar projection if gcf().get_axes(): - if not isinstance(gca(), PolarAxes): + ax = gca() + if isinstance(ax, PolarAxes): + return ax + else: cbook._warn_external('Trying to create polar plot on an axis ' 'that does not have a polar projection.') - ax = gca(polar=True) + ax = axes(polar=True) ret = ax.plot(*args, **kwargs) return ret diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 49627e9ce433..e284cff6717e 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2387,28 +2387,36 @@ def _as_mpl_axes(self): # testing axes creation with plt.axes ax = plt.axes([0, 0, 1, 1], projection=prj) assert type(ax) == PolarAxes - ax_via_gca = plt.gca(projection=prj) + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + ax_via_gca = plt.gca(projection=prj) assert ax_via_gca is ax plt.close() # testing axes creation with gca - ax = plt.gca(projection=prj) + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + ax = plt.gca(projection=prj) assert type(ax) == mpl.axes._subplots.subplot_class_factory(PolarAxes) - ax_via_gca = plt.gca(projection=prj) + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + ax_via_gca = plt.gca(projection=prj) assert ax_via_gca is ax # try getting the axes given a different polar projection - with pytest.warns(UserWarning) as rec: + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'), \ + pytest.raises( + ValueError, match=r'arguments passed to gca\(\) did not match'): ax_via_gca = plt.gca(projection=prj2) - assert len(rec) == 1 - assert 'Requested projection is different' in str(rec[0].message) - assert ax_via_gca is not ax - assert ax.get_theta_offset() == 0 - assert ax_via_gca.get_theta_offset() == np.pi # try getting the axes given an == (not is) polar projection - with pytest.warns(UserWarning): + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): ax_via_gca = plt.gca(projection=prj3) - assert len(rec) == 1 - assert 'Requested projection is different' in str(rec[0].message) assert ax_via_gca is ax plt.close() diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index c5ab3cf6d232..15a20b1bd08b 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -8,6 +8,7 @@ import matplotlib as mpl from matplotlib import cbook, rcParams +from matplotlib.cbook import MatplotlibDeprecationWarning from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.axes import Axes from matplotlib.figure import Figure @@ -154,30 +155,44 @@ def test_gca(): assert fig.add_axes() is None ax0 = fig.add_axes([0, 0, 1, 1]) - assert fig.gca(projection='rectilinear') is ax0 + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + assert fig.gca(projection='rectilinear') is ax0 assert fig.gca() is ax0 ax1 = fig.add_axes(rect=[0.1, 0.1, 0.8, 0.8]) - assert fig.gca(projection='rectilinear') is ax1 + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + assert fig.gca(projection='rectilinear') is ax1 assert fig.gca() is ax1 ax2 = fig.add_subplot(121, projection='polar') assert fig.gca() is ax2 - assert fig.gca(polar=True) is ax2 + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + assert fig.gca(polar=True) is ax2 ax3 = fig.add_subplot(122) assert fig.gca() is ax3 # the final request for a polar axes will end up creating one # with a spec of 111. - with pytest.warns(UserWarning): - # Changing the projection will throw a warning - assert fig.gca(polar=True) is not ax3 - assert fig.gca(polar=True) is not ax2 - assert fig.gca().get_subplotspec().get_geometry() == (1, 1, 0, 0) + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'), \ + pytest.raises( + ValueError, match=r'arguments passed to gca\(\) did not match'): + # Changing the projection will raise an exception + fig.gca(polar=True) fig.sca(ax1) - assert fig.gca(projection='rectilinear') is ax1 + with pytest.warns( + MatplotlibDeprecationWarning, + match=r'Calling gca\(\) with keyword arguments is deprecated'): + assert fig.gca(projection='rectilinear') is ax1 assert fig.gca() is ax1 From 41b93d895d8a22cf53a7276618214d3f20fc1870 Mon Sep 17 00:00:00 2001 From: Leo Singer Date: Wed, 2 Dec 2020 08:34:31 -0500 Subject: [PATCH 2/2] WIP --- lib/matplotlib/figure.py | 137 +++++++++++++++------------- lib/matplotlib/tests/test_axes.py | 4 +- lib/matplotlib/tests/test_figure.py | 4 +- 3 files changed, 77 insertions(+), 68 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 7c2d723e30b1..9e7ea15db0dc 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -56,22 +56,40 @@ class _AxesStack(cbook.Stack): Specialization of `.Stack`, to handle all tracking of `~.axes.Axes` in a `.Figure`. - This stack stores ``key, axes`` pairs, where **key** is a hash of the args - and kwargs used in generating the Axes. + This stack stores ``key, (ind, axes)`` pairs, where: + + * **key** is a hash of the args used in generating the Axes. + * **ind** is a serial index tracking the order in which Axes were added. AxesStack is a callable; calling it returns the current Axes. The `current_key_axes` method returns the current key and associated Axes. """ + def __init__(self): + super().__init__() + self._ind = 0 + def as_list(self): """ Return a list of the Axes instances that have been added to the figure. """ - return [a for k, a in self._elements] + ia_list = [a for k, a in self._elements] + ia_list.sort() + return [a for i, a in ia_list] + + def get(self, key): + """ + Return the Axes instance that was added with *key*. + If it is not present, return *None*. + """ + item = dict(self._elements).get(key) + if item is None: + return None + return item[1] def _entry_from_axes(self, e): - k = {a: k for k, a in self._elements}[e] - return (k, e) + ind, k = {a: (ind, k) for k, (ind, a) in self._elements}[e] + return (k, (ind, e)) def remove(self, a): """Remove the Axes from the stack.""" @@ -94,7 +112,18 @@ def add(self, key, a): # All the error checking may be unnecessary; but this method # is called so seldom that the overhead is negligible. cbook._check_isinstance(Axes, a=a) - return super().push((key, a)) + + a_existing = self.get(key) + if a_existing is not None: + super().remove((key, a_existing)) + cbook._warn_external( + "key {!r} already existed; Axes is being replaced".format(key)) + # I don't think the above should ever happen. + + if a in self: + return None + self._ind += 1 + return super().push((key, (self._ind, a))) def current_key_axes(self): """ @@ -102,10 +131,14 @@ def current_key_axes(self): If no Axes exists on the stack, then returns ``(None, None)``. """ - return super().__call__() or (None, None) + if not len(self._elements): + return self._default, self._default + else: + key, (index, axes) = self._elements[self._pos] + return key, axes def __call__(self): - ka = self.current_key_axes()[1] + return self.current_key_axes()[1] def __contains__(self, a): return a in self.as_list() @@ -643,11 +676,11 @@ def add_axes(self, *args, **kwargs): args = (kwargs.pop('rect'), ) if isinstance(args[0], Axes): - key = self._make_key(*args, **kwargs) a = args[0] if a.get_figure() is not self: raise ValueError( "The Axes must have been created in the present figure") + key = self._make_key(*args) else: rect = args[0] if not np.isfinite(rect).all(): @@ -656,6 +689,13 @@ def add_axes(self, *args, **kwargs): projection_class, kwargs, key = \ self._process_projection_requirements(*args, **kwargs) + # check that an axes of this type doesn't already exist, if it + # does, set it as active and return it + ax = self._axstack.get(key) + if isinstance(ax, projection_class): + self.sca(ax) + return ax + # create the new axes using the axes class given a = projection_class(self, rect, **kwargs) return self._add_axes_internal(key, a) @@ -787,7 +827,7 @@ def add_subplot(self, *args, **kwargs): "the present figure") # make a key for the subplot (which includes the axes object id # in the hash) - key = self._make_key(*args, **kwargs) + key = self._make_key(*args) else: if not args: @@ -800,6 +840,19 @@ def add_subplot(self, *args, **kwargs): args = tuple(map(int, str(args[0]))) projection_class, kwargs, key = \ self._process_projection_requirements(*args, **kwargs) + ax = self._axstack.get(key) # search axes with this key in stack + if ax is not None: + if isinstance(ax, projection_class): + # the axes already existed, so set it as active & return + self.sca(ax) + return ax + else: + # Undocumented convenience behavior: + # subplot(111); subplot(111, projection='polar') + # will replace the first with the second. + # Without this, add_subplot would be simpler and + # more similar to add_axes. + self._axstack.remove(ax) ax = subplot_class_factory(projection_class)(self, *args, **kwargs) return self._add_axes_internal(key, ax) @@ -1531,43 +1584,19 @@ def gca(self, **kwargs): cbook.warn_deprecated( "3.4", message="Calling gca() with keyword arguments is deprecated. " - "In a future version, gca() will take no keyword arguments. " - "The gca() function should only be used to get the current " - "axes, or if no axes exist, create new axes with default " - "keyword arguments. To create a new axes with non-default " - "arguments, use plt.axes() or plt.subplot().") + "gca() no longer checks whether the keyword arguments match " + "those with which the current axes were created. In a future " + "version, gca() will take no keyword arguments. The gca() " + "function should only be used to get the current axes, or if " + "no axes exist, create new axes with default keyword " + "arguments. To create a new axes with non-default arguments, " + "use plt.axes() or plt.subplot().") ckey, cax = self._axstack.current_key_axes() # if there exists an axes on the stack see if it matches # the desired axes configuration if cax is not None: - - # if no kwargs are given just return the current axes - # this is a convenience for gca() on axes such as polar etc. - if not kwargs: - return cax - - # if the user has specified particular projection detail - # then build up a key which can represent this - else: - projection_class, _, key = \ - self._process_projection_requirements(**kwargs) - - # let the returned axes have any gridspec by removing it from - # the key - ckey = ckey[1:] - key = key[1:] - - # if the cax matches this key then return the axes, otherwise - # continue and a new axes will be created - if key == ckey and isinstance(cax, projection_class): - return cax - else: - raise ValueError( - "The arguments passed to gca() did not match the " - "arguments with which the current axes were " - "originally created. To create new axes, use " - "axes() or subplot().") + return cax # no axes found, so create one which spans the figure return self.add_subplot(1, 1, 1, **kwargs) @@ -1643,28 +1672,12 @@ def _process_projection_requirements( # Make the key without projection kwargs, this is used as a unique # lookup for axes instances - key = self._make_key(*args, **kwargs) + key = self._make_key(*args) return projection_class, kwargs, key - def _make_key(self, *args, **kwargs): - """Make a hashable key out of args and kwargs.""" - - def fixitems(items): - # items may have arrays and lists in them, so convert them - # to tuples for the key - ret = [] - for k, v in items: - # some objects can define __getitem__ without being - # iterable and in those cases the conversion to tuples - # will fail. So instead of using the np.iterable(v) function - # we simply try and convert to a tuple, and proceed if not. - try: - v = tuple(v) - except Exception: - pass - ret.append((k, v)) - return tuple(ret) + def _make_key(self, *args): + """Make a hashable key out of args.""" def fixlist(args): ret = [] @@ -1674,7 +1687,7 @@ def fixlist(args): ret.append(a) return tuple(ret) - key = fixlist(args), fixitems(kwargs.items()) + key = fixlist(args) return key def get_default_bbox_extra_artists(self): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index e284cff6717e..8d835d705200 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2408,9 +2408,7 @@ def _as_mpl_axes(self): # try getting the axes given a different polar projection with pytest.warns( MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments is deprecated'), \ - pytest.raises( - ValueError, match=r'arguments passed to gca\(\) did not match'): + match=r'Calling gca\(\) with keyword arguments is deprecated'): ax_via_gca = plt.gca(projection=prj2) # try getting the axes given an == (not is) polar projection with pytest.warns( diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 15a20b1bd08b..ee796049c6d8 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -182,9 +182,7 @@ def test_gca(): # with a spec of 111. with pytest.warns( MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments is deprecated'), \ - pytest.raises( - ValueError, match=r'arguments passed to gca\(\) did not match'): + match=r'Calling gca\(\) with keyword arguments is deprecated'): # Changing the projection will raise an exception fig.gca(polar=True) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy