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..9e7ea15db0dc 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -58,7 +58,7 @@ class _AxesStack(cbook.Stack): This stack stores ``key, (ind, axes)`` pairs, where: - * **key** is a hash of the args and kwargs used in generating the Axes. + * **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. @@ -85,14 +85,6 @@ def get(self, key): 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] def _entry_from_axes(self, e): @@ -114,18 +106,12 @@ 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: @@ -689,19 +675,12 @@ 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): 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(): @@ -848,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: @@ -1601,36 +1580,23 @@ def gca(self, **kwargs): %(Axes)s """ + if kwargs: + cbook.warn_deprecated( + "3.4", + message="Calling gca() with keyword arguments is deprecated. " + "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: - cbook._warn_external('Requested projection is different ' - 'from current axis projection, ' - 'creating new axis with requested ' - 'projection.') + return cax # no axes found, so create one which spans the figure return self.add_subplot(1, 1, 1, **kwargs) @@ -1706,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 = [] @@ -1737,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/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..8d835d705200 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -2387,28 +2387,34 @@ 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'): 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..ee796049c6d8 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,42 @@ 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'): + # 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