Skip to content

Commit fdfa893

Browse files
committed
Templatize class factories.
This makes mpl_toolkits axes classes picklable (see test_pickle) by generalizing the machinery of _picklable_subplot_class_constructor, which would otherwise have had to be reimplemented for each class factory.
1 parent 5960b5a commit fdfa893

File tree

7 files changed

+71
-118
lines changed

7 files changed

+71
-118
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The private ``matplotlib.axes._subplots._subplot_classes`` dict has been removed
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

lib/matplotlib/axes/_subplots.py

+2-60
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import functools
21
import uuid
32

43
from matplotlib import _api, cbook, docstring
@@ -38,15 +37,6 @@ def __init__(self, fig, *args, **kwargs):
3837
# This will also update the axes position.
3938
self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args))
4039

41-
def __reduce__(self):
42-
# get the first axes class which does not inherit from a subplotbase
43-
axes_class = next(
44-
c for c in type(self).__mro__
45-
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
46-
return (_picklable_subplot_class_constructor,
47-
(axes_class,),
48-
self.__getstate__())
49-
5040
@_api.deprecated(
5141
"3.4", alternative="get_subplotspec",
5242
addendum="(get_subplotspec returns a SubplotSpec instance.)")
@@ -168,58 +158,10 @@ def _make_twin_axes(self, *args, **kwargs):
168158
return twin
169159

170160

171-
# this here to support cartopy which was using a private part of the
172-
# API to register their Axes subclasses.
173-
174-
# In 3.1 this should be changed to a dict subclass that warns on use
175-
# In 3.3 to a dict subclass that raises a useful exception on use
176-
# In 3.4 should be removed
177-
178-
# The slow timeline is to give cartopy enough time to get several
179-
# release out before we break them.
180-
_subplot_classes = {}
181-
182-
183-
@functools.lru_cache(None)
184-
def subplot_class_factory(axes_class=None):
185-
"""
186-
Make a new class that inherits from `.SubplotBase` and the
187-
given axes_class (which is assumed to be a subclass of `.axes.Axes`).
188-
This is perhaps a little bit roundabout to make a new class on
189-
the fly like this, but it means that a new Subplot class does
190-
not have to be created for every type of Axes.
191-
"""
192-
if axes_class is None:
193-
cbook.warn_deprecated(
194-
"3.3", message="Support for passing None to subplot_class_factory "
195-
"is deprecated since %(since)s; explicitly pass the default Axes "
196-
"class instead. This will become an error %(removal)s.")
197-
axes_class = Axes
198-
try:
199-
# Avoid creating two different instances of GeoAxesSubplot...
200-
# Only a temporary backcompat fix. This should be removed in
201-
# 3.4
202-
return next(cls for cls in SubplotBase.__subclasses__()
203-
if cls.__bases__ == (SubplotBase, axes_class))
204-
except StopIteration:
205-
return type("%sSubplot" % axes_class.__name__,
206-
(SubplotBase, axes_class),
207-
{'_axes_class': axes_class})
208-
209-
161+
subplot_class_factory = cbook._make_class_factory(
162+
SubplotBase, "{}Subplot", "_axes_class", default_axes_class=Axes)
210163
Subplot = subplot_class_factory(Axes) # Provided for backward compatibility.
211164

212-
213-
def _picklable_subplot_class_constructor(axes_class):
214-
"""
215-
Stub factory that returns an empty instance of the appropriate subplot
216-
class when called with an axes class. This is purely to allow pickling of
217-
Axes and Subplots.
218-
"""
219-
subplot_class = subplot_class_factory(axes_class)
220-
return subplot_class.__new__(subplot_class)
221-
222-
223165
docstring.interpd.update(Axes=martist.kwdoc(Axes))
224166
docstring.dedent_interpd(Axes.__init__)
225167

lib/matplotlib/cbook/__init__.py

+51
Original file line numberDiff line numberDiff line change
@@ -2322,3 +2322,54 @@ def _unikey_or_keysym_to_mplkey(unikey, keysym):
23222322
"next": "pagedown", # Used by tk.
23232323
}.get(key, key)
23242324
return key
2325+
2326+
2327+
@functools.lru_cache(None)
2328+
def _make_class_factory(
2329+
mixin_class, fmt, axes_attr=None, *, default_axes_class=None):
2330+
"""
2331+
Return a function that creates picklable classes inheriting from a mixin.
2332+
2333+
After ::
2334+
2335+
factory = _make_class_factory(FooMixin, fmt, axes_attr)
2336+
FooAxes = factory(Axes)
2337+
2338+
``Foo`` is a class that inherits from ``FooMixin`` and ``Axes`` and **is
2339+
picklable** (picklability is what differentiates this from a plain call to
2340+
`type`). Its ``__name__`` is set to ``fmt.format(Axes.__name__)`` and the
2341+
base class is stored in the ``axes_attr`` attribute, if not None.
2342+
"""
2343+
2344+
@functools.lru_cache(None)
2345+
def class_factory(axes_class):
2346+
# default_axes_class should go away once the deprecation elapses.
2347+
if axes_class is None and default_axes_class is not None:
2348+
warn_deprecated(
2349+
"3.3", message="Support for passing None to class factories "
2350+
"is deprecated since %(since)s and will be removed "
2351+
"%(removal)s; explicitly pass the default Axes class instead.")
2352+
return class_factory(default_axes_class)
2353+
d = {"__reduce__":
2354+
lambda self: (_picklable_class_constructor,
2355+
(mixin_class, fmt, axes_attr,
2356+
default_axes_class, axes_class,),
2357+
self.__getstate__())}
2358+
if axes_attr is not None:
2359+
d[axes_attr] = axes_class
2360+
cls = type(
2361+
fmt.format(axes_class.__name__), (mixin_class, axes_class), d)
2362+
# Better in first approximation than __module__ = "matplotlib.cbook"...
2363+
cls.__module__ = mixin_class.__module__
2364+
return cls
2365+
2366+
return class_factory
2367+
2368+
2369+
def _picklable_class_constructor(
2370+
base_cls, fmt, axes_attr, default_axes_class, axes_class):
2371+
"""Internal helper for _make_class_factory."""
2372+
cls = _make_class_factory(
2373+
base_cls, fmt, axes_attr,
2374+
default_axes_class=default_axes_class)(axes_class)
2375+
return cls.__new__(cls)

lib/matplotlib/tests/test_axes.py

-15
Original file line numberDiff line numberDiff line change
@@ -6142,21 +6142,6 @@ def test_spines_properbbox_after_zoom():
61426142
np.testing.assert_allclose(bb.get_points(), bb2.get_points(), rtol=1e-6)
61436143

61446144

6145-
def test_cartopy_backcompat():
6146-
6147-
class Dummy(matplotlib.axes.Axes):
6148-
...
6149-
6150-
class DummySubplot(matplotlib.axes.SubplotBase, Dummy):
6151-
_axes_class = Dummy
6152-
6153-
matplotlib.axes._subplots._subplot_classes[Dummy] = DummySubplot
6154-
6155-
FactoryDummySubplot = matplotlib.axes.subplot_class_factory(Dummy)
6156-
6157-
assert DummySubplot is FactoryDummySubplot
6158-
6159-
61606145
def test_gettightbbox_ignore_nan():
61616146
fig, ax = plt.subplots()
61626147
remove_ticks_and_titles(fig)

lib/matplotlib/tests/test_pickle.py

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import matplotlib.pyplot as plt
1111
import matplotlib.transforms as mtransforms
1212
import matplotlib.figure as mfigure
13+
from mpl_toolkits.axes_grid1 import parasite_axes
1314

1415

1516
def test_simple():
@@ -212,3 +213,8 @@ def test_unpickle_canvas():
212213
out.seek(0)
213214
fig2 = pickle.load(out)
214215
assert fig2.canvas is not None
216+
217+
218+
def test_mpl_toolkits():
219+
ax = parasite_axes.host_axes([0, 0, 1, 1])
220+
assert type(pickle.loads(pickle.dumps(ax))) == parasite_axes.HostAxes

lib/mpl_toolkits/axes_grid1/parasite_axes.py

+7-34
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,8 @@ def apply_aspect(self, position=None):
8484
# end of aux_transform support
8585

8686

87-
@functools.lru_cache(None)
88-
def parasite_axes_class_factory(axes_class=None):
89-
if axes_class is None:
90-
cbook.warn_deprecated(
91-
"3.3", message="Support for passing None to "
92-
"parasite_axes_class_factory is deprecated since %(since)s and "
93-
"will be removed %(removal)s; explicitly pass the default Axes "
94-
"class instead.")
95-
axes_class = Axes
96-
97-
return type("%sParasite" % axes_class.__name__,
98-
(ParasiteAxesBase, axes_class), {})
99-
100-
87+
parasite_axes_class_factory = cbook._make_class_factory(
88+
ParasiteAxesBase, "{}Parasite", default_axes_class=Axes)
10189
ParasiteAxes = parasite_axes_class_factory(Axes)
10290

10391

@@ -290,7 +278,7 @@ def _add_twin_axes(self, axes_class, **kwargs):
290278
*kwargs* are forwarded to the parasite axes constructor.
291279
"""
292280
if axes_class is None:
293-
axes_class = self._get_base_axes()
281+
axes_class = self._base_axes_class
294282
ax = parasite_axes_class_factory(axes_class)(self, **kwargs)
295283
self.parasites.append(ax)
296284
ax._remove_method = self._remove_any_twin
@@ -317,21 +305,10 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
317305
return Bbox.union([b for b in bbs if b.width != 0 or b.height != 0])
318306

319307

320-
@functools.lru_cache(None)
321-
def host_axes_class_factory(axes_class=None):
322-
if axes_class is None:
323-
cbook.warn_deprecated(
324-
"3.3", message="Support for passing None to host_axes_class is "
325-
"deprecated since %(since)s and will be removed %(removed)s; "
326-
"explicitly pass the default Axes class instead.")
327-
axes_class = Axes
328-
329-
def _get_base_axes(self):
330-
return axes_class
331-
332-
return type("%sHostAxes" % axes_class.__name__,
333-
(HostAxesBase, axes_class),
334-
{'_get_base_axes': _get_base_axes})
308+
host_axes_class_factory = cbook._make_class_factory(
309+
HostAxesBase, "{}HostAxes", "_base_axes_class", default_axes_class=Axes)
310+
HostAxes = host_axes_class_factory(Axes)
311+
SubplotHost = subplot_class_factory(HostAxes)
335312

336313

337314
def host_subplot_class_factory(axes_class):
@@ -340,10 +317,6 @@ def host_subplot_class_factory(axes_class):
340317
return subplot_host_class
341318

342319

343-
HostAxes = host_axes_class_factory(Axes)
344-
SubplotHost = subplot_class_factory(HostAxes)
345-
346-
347320
def host_axes(*args, axes_class=Axes, figure=None, **kwargs):
348321
"""
349322
Create axes that can act as a hosts to parasitic axes.

lib/mpl_toolkits/axisartist/floating_axes.py

+3-9
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
# TODO :
66
# see if tick_iterator method can be simplified by reusing the parent method.
77

8-
import functools
9-
108
import numpy as np
119

10+
from matplotlib import cbook
1211
import matplotlib.patches as mpatches
1312
from matplotlib.path import Path
1413
import matplotlib.axes as maxes
@@ -360,13 +359,8 @@ def adjust_axes_lim(self):
360359
self.set_ylim(ymin-dy, ymax+dy)
361360

362361

363-
@functools.lru_cache(None)
364-
def floatingaxes_class_factory(axes_class):
365-
return type("Floating %s" % axes_class.__name__,
366-
(FloatingAxesBase, axes_class),
367-
{'_axes_class_floating': axes_class})
368-
369-
362+
floatingaxes_class_factory = cbook._make_class_factory(
363+
FloatingAxesBase, "Floating {}", "_axes_class_floating")
370364
FloatingAxes = floatingaxes_class_factory(
371365
host_axes_class_factory(axislines.Axes))
372366
FloatingSubplot = maxes.subplot_class_factory(FloatingAxes)

0 commit comments

Comments
 (0)