From fa87fa74f4ac01c1979b58a8927a0fa01ac289d3 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 23 Nov 2014 00:31:32 -0500 Subject: [PATCH 1/4] TST : tweak test_add_collection This is needed because this test tries to add artists from one axes to another which is not really supported. Not 100% that this is fully equivalent to the current test. There is a call to `add_collection` in scatter, but the test might be defeated by some of the other logic in scatter. --- lib/matplotlib/tests/test_collections.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index e4e709ffc0f0..043ec4c25ad7 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -407,14 +407,12 @@ def test_null_collection_datalim(): def test_add_collection(): # Test if data limits are unchanged by adding an empty collection. # Github issue #1490, pull #1497. - ax = plt.axes() plt.figure() - ax2 = plt.axes() - coll = ax2.scatter([0, 1], [0, 1]) + ax = plt.axes() + coll = ax.scatter([0, 1], [0, 1]) ax.add_collection(coll) bounds = ax.dataLim.bounds - coll = ax2.scatter([], []) - ax.add_collection(coll) + coll = ax.scatter([], []) assert_equal(ax.dataLim.bounds, bounds) From 394447fe991cca79768b6cf520f99eb9d745deea Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 23 Nov 2014 22:46:11 -0500 Subject: [PATCH 2/4] MNT : fix `__init__` order in AnchoredSizeLocator Trying to set the axes property before calling up the mro to the base class (eventually Artist) `__init__` which means that the `_axes` attribute is not yet attached to the instance object. - removed setting of `self.axes`, this is taken care of in the baseclass - moved rest of sub-class specific attributes to after call up mro stack --- lib/mpl_toolkits/axes_grid1/inset_locator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index feb4a0316e8c..8daa198f7f0c 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -58,20 +58,21 @@ def __call__(self, ax, renderer): return bb + from . import axes_size as Size class AnchoredSizeLocator(AnchoredLocatorBase): def __init__(self, bbox_to_anchor, x_size, y_size, loc, borderpad=0.5, bbox_transform=None): - self.axes = None - self.x_size = Size.from_any(x_size) - self.y_size = Size.from_any(y_size) super(AnchoredSizeLocator, self).__init__(bbox_to_anchor, None, loc, borderpad=borderpad, bbox_transform=bbox_transform) + self.x_size = Size.from_any(x_size) + self.y_size = Size.from_any(y_size) + def get_extent(self, renderer): x, y, w, h = self.get_bbox_to_anchor().bounds From 69decec0297f5693303297800e5e05252b4663f9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 26 Nov 2014 21:57:13 -0500 Subject: [PATCH 3/4] MNT : tweak how internals of Axes.plot work The actual processing and artist creation in Axes.plot happens through the `__call__` method on a _process_plot_var_args instance hanging off the Axes object. The _process_plot_var_args knows what axes it is hanging off of and passed that into the Line2D artists as a kwarg at creation time, which sets the axes of the Line2D. The axes of the Line2D is set again during the `Axes.add_line` method. For the most part this is fine, if un-needed, because the axes is the same objects both time it is set. However when using the mpl_toolkit, Axes objects get wrapped in not-strictly-subclassed objects and the Axes object that the _process_plot_var_args object knows about is not the Axes object that add_line is called on which now results in an exception as it is moving artists between axes. --- lib/matplotlib/axes/_base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index eff223f92bb4..5f4035a8faa0 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -237,7 +237,6 @@ def _makeline(self, x, y, kw, kwargs): # (can't use setdefault because it always evaluates # its second argument) seg = mlines.Line2D(x, y, - axes=self.axes, **kw ) self.set_lineprops(seg, **kwargs) From c137a7186852d01980af8c53c3025788f677fab6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 23 Nov 2014 00:37:28 -0500 Subject: [PATCH 4/4] API : property-ify Artist.axes - deprecate Artist.{get,set}_axes - add axes property - Raise exception when trying to move artists between axes. --- .../api_changes/2014-12-12_axes_property.rst | 16 +++++++ lib/matplotlib/artist.py | 44 ++++++++++++++++--- lib/matplotlib/axes/_base.py | 7 +-- lib/matplotlib/figure.py | 6 ++- lib/matplotlib/legend.py | 6 ++- lib/matplotlib/lines.py | 8 ++-- 6 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 doc/api/api_changes/2014-12-12_axes_property.rst diff --git a/doc/api/api_changes/2014-12-12_axes_property.rst b/doc/api/api_changes/2014-12-12_axes_property.rst new file mode 100644 index 000000000000..3e98a5cbd4a5 --- /dev/null +++ b/doc/api/api_changes/2014-12-12_axes_property.rst @@ -0,0 +1,16 @@ +Prevent moving artists between Axes, Property-ify Artist.axes, deprecate Artist.{get,set}_axes +`````````````````````````````````````````````````````````````````````````````````````````````` + +The reason this was done was to prevent adding an Artist that is +already associated with an Axes to be moved/added to a different Axes. +This was never supported as it causes havoc with the transform stack. +The apparent support for this (as it did not raise an exception) was +the source of multiple bug reports and questions on SO. + +For almost all use-cases, the assignment of the axes to an artist should be +taken care of by the axes as part of the ``Axes.add_*`` method, hence the +deprecation {get,set}_axes. + +Removing the ``set_axes`` method will also remove the 'axes' line from +the ACCEPTS kwarg tables (assuming that the removal date gets here +before that gets overhauled). diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index af9155c40dd6..7fb492367f1d 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -8,6 +8,7 @@ import inspect import matplotlib import matplotlib.cbook as cbook +from matplotlib.cbook import mplDeprecation from matplotlib import docstring, rcParams from .transforms import (Bbox, IdentityTransform, TransformedBbox, TransformedPath, Transform) @@ -77,6 +78,7 @@ class Artist(object): zorder = 0 def __init__(self): + self._axes = None self.figure = None self._transform = None @@ -175,17 +177,43 @@ def set_axes(self, axes): Set the :class:`~matplotlib.axes.Axes` instance in which the artist resides, if any. + This has been deprecated in mpl 1.5, please use the + axes property. Will be removed in 1.7 or 2.0. + ACCEPTS: an :class:`~matplotlib.axes.Axes` instance """ + warnings.warn(_get_axes_msg, mplDeprecation, stacklevel=1) self.axes = axes def get_axes(self): """ Return the :class:`~matplotlib.axes.Axes` instance the artist - resides in, or *None* + resides in, or *None*. + + This has been deprecated in mpl 1.5, please use the + axes property. Will be removed in 1.7 or 2.0. """ + warnings.warn(_get_axes_msg, mplDeprecation, stacklevel=1) return self.axes + @property + def axes(self): + """ + The :class:`~matplotlib.axes.Axes` instance the artist + resides in, or *None*. + """ + return self._axes + + @axes.setter + def axes(self, new_axes): + if self._axes is not None and new_axes != self._axes: + raise ValueError("Can not reset the axes. You are " + "probably trying to re-use an artist " + "in more than one Axes which is not " + "supported") + self._axes = new_axes + return new_axes + def get_window_extent(self, renderer): """ Get the axes bounding box in display space. @@ -751,10 +779,13 @@ def update(self, props): changed = False for k, v in six.iteritems(props): - func = getattr(self, 'set_' + k, None) - if func is None or not six.callable(func): - raise AttributeError('Unknown property %s' % k) - func(v) + if k in ['axes']: + setattr(self, k, v) + else: + func = getattr(self, 'set_' + k, None) + if func is None or not six.callable(func): + raise AttributeError('Unknown property %s' % k) + func(v) changed = True self.eventson = store if changed: @@ -1328,3 +1359,6 @@ def kwdoc(a): return '\n'.join(ArtistInspector(a).pprint_setters(leadingspace=2)) docstring.interpd.update(Artist=kwdoc(Artist)) + +_get_axes_msg = """This has been deprecated in mpl 1.5, please use the +axes property. A removal date has not been set.""" diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 5f4035a8faa0..03a5e4ed07e4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -392,7 +392,8 @@ def __init__(self, fig, rect, else: self._position = mtransforms.Bbox.from_bounds(*rect) self._originalPosition = self._position.frozen() - self.set_axes(self) + # self.set_axes(self) + self.axes = self self.set_aspect('auto') self._adjustable = 'box' self.set_anchor('C') @@ -774,7 +775,7 @@ def _set_artist_props(self, a): if not a.is_transform_set(): a.set_transform(self.transData) - a.set_axes(self) + a.axes = self def _gen_axes_patch(self): """ @@ -1431,7 +1432,7 @@ def add_artist(self, a): Returns the artist. """ - a.set_axes(self) + a.axes = self self.artists.append(a) self._set_artist_props(a) a.set_clip_path(self.patch) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 34bfc30c503e..85ad1e6ba48a 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -298,7 +298,11 @@ def __init__(self, Defaults to rc ``figure.autolayout``. """ Artist.__init__(self) - + # remove the non-figure artist _axes property + # as it makes no sense for a figure to be _in_ an axes + # this is used by the property methods in the artist base class + # which are over-ridden in this class + del self._axes self.callbacks = cbook.CallbackRegistry() if figsize is None: diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index e5c0fc47a05a..e0536f5bfd44 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -304,7 +304,7 @@ def __init__(self, parent, handles, labels, if isinstance(parent, Axes): self.isaxes = True - self.set_axes(parent) + self.axes = parent self.set_figure(parent.figure) elif isinstance(parent, Figure): self.isaxes = False @@ -391,7 +391,9 @@ def _set_artist_props(self, a): """ a.set_figure(self.figure) if self.isaxes: - a.set_axes(self.axes) + # a.set_axes(self.axes) + a.axes = self.axes + a.set_transform(self.get_transform()) def _set_loc(self, loc): diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 87bb99896f7d..dd9be576f1b9 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -536,15 +536,17 @@ def get_window_extent(self, renderer): bbox = bbox.padded(ms) return bbox - def set_axes(self, ax): - Artist.set_axes(self, ax) + @Artist.axes.setter + def axes(self, ax): + # call the set method from the base-class property + Artist.axes.fset(self, ax) + # connect unit-related callbacks if ax.xaxis is not None: self._xcid = ax.xaxis.callbacks.connect('units', self.recache_always) if ax.yaxis is not None: self._ycid = ax.yaxis.callbacks.connect('units', self.recache_always) - set_axes.__doc__ = Artist.set_axes.__doc__ def set_data(self, *args): """