Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 15d11ef

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 b6a6414 commit 15d11ef

File tree

7 files changed

+88
-104
lines changed

7 files changed

+88
-104
lines changed
Lines changed: 2 additions & 0 deletions
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

Lines changed: 4 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import functools
2-
3-
from matplotlib import _api, docstring
1+
from matplotlib import _api, cbook, docstring
42
import matplotlib.artist as martist
53
from matplotlib.axes._axes import Axes
64
from matplotlib.gridspec import GridSpec, SubplotSpec
@@ -37,15 +35,6 @@ def __init__(self, fig, *args, **kwargs):
3735
# This will also update the axes position.
3836
self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args))
3937

40-
def __reduce__(self):
41-
# get the first axes class which does not inherit from a subplotbase
42-
axes_class = next(
43-
c for c in type(self).__mro__
44-
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
45-
return (_picklable_subplot_class_constructor,
46-
(axes_class,),
47-
self.__getstate__())
48-
4938
@_api.deprecated(
5039
"3.4", alternative="get_subplotspec",
5140
addendum="(get_subplotspec returns a SubplotSpec instance.)")
@@ -170,59 +159,10 @@ def _make_twin_axes(self, *args, **kwargs):
170159
return twin
171160

172161

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

214-
215-
def _picklable_subplot_class_constructor(axes_class):
216-
"""
217-
Stub factory that returns an empty instance of the appropriate subplot
218-
class when called with an axes class. This is purely to allow pickling of
219-
Axes and Subplots.
220-
"""
221-
subplot_class = subplot_class_factory(axes_class)
222-
return subplot_class.__new__(subplot_class)
223-
224-
225166
docstring.interpd.update(Axes_kwdoc=martist.kwdoc(Axes))
226-
docstring.dedent_interpd(Axes.__init__)
227-
228167
docstring.interpd.update(Subplot_kwdoc=martist.kwdoc(Axes))
168+
docstring.dedent_interpd(Axes.__init__)

lib/matplotlib/cbook/__init__.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,3 +2191,68 @@ def _unikey_or_keysym_to_mplkey(unikey, keysym):
21912191
"next": "pagedown", # Used by tk.
21922192
}.get(key, key)
21932193
return key
2194+
2195+
2196+
@functools.lru_cache(None)
2197+
def _make_class_factory(
2198+
mixin_class, fmt, attr_name=None, *, _default_base_class=None):
2199+
"""
2200+
Return a function that creates picklable classes inheriting from a mixin.
2201+
2202+
After ::
2203+
2204+
factory = _make_class_factory(FooMixin, fmt, attr_name)
2205+
FooAxes = factory(Axes)
2206+
2207+
``Foo`` is a class that inherits from ``FooMixin`` and ``Axes`` and **is
2208+
picklable** (picklability is what differentiates this from a plain call to
2209+
`type`). Its ``__name__`` is set to ``fmt.format(Axes.__name__)`` and the
2210+
base class is stored in the ``attr_name`` attribute, if not None.
2211+
2212+
Moreover, the return value of ``factory`` is memoized: calls with the same
2213+
``Axes`` class always return the same subclass.
2214+
"""
2215+
2216+
@functools.lru_cache(None)
2217+
def class_factory(axes_class):
2218+
# The parameter is named "axes_class" for backcompat but is really just
2219+
# a base class; no axes semantics are used.
2220+
base_class = axes_class
2221+
# _default_base_class should go away once the deprecation elapses.
2222+
if base_class is None and _default_base_class is not None:
2223+
warn_deprecated(
2224+
"3.5", message="Support for passing None to class factories "
2225+
"is deprecated since %(since)s and will be removed "
2226+
"%(removal)s; explicitly pass the default {} class instead."
2227+
.format(_default_base_class.__name__))
2228+
return class_factory(_default_base_class)
2229+
2230+
class subcls(mixin_class, base_class):
2231+
# Better approximation than __module__ = "matplotlib.cbook".
2232+
__module__ = mixin_class.__module__
2233+
2234+
def __reduce__(self):
2235+
return (_picklable_class_constructor,
2236+
(mixin_class, fmt, attr_name,
2237+
_default_base_class, base_class),
2238+
self.__getstate__())
2239+
2240+
subcls.__name__ = subcls.__qualname__ = fmt.format(base_class.__name__)
2241+
if attr_name is not None:
2242+
setattr(subcls, attr_name, base_class)
2243+
return subcls
2244+
2245+
return class_factory
2246+
2247+
2248+
def _picklable_class_constructor(
2249+
base_cls, fmt, attr_name, _default_base_class, base_class):
2250+
"""Internal helper for _make_class_factory."""
2251+
# Explicitly passing _default_base_class or not matters wrt. lru_cache.
2252+
if _default_base_class is None:
2253+
factory = _make_class_factory(base_cls, fmt, attr_name)
2254+
else:
2255+
factory = _make_class_factory(
2256+
base_cls, fmt, attr_name, _default_base_class=_default_base_class)
2257+
cls = factory(base_class)
2258+
return cls.__new__(cls)

lib/matplotlib/tests/test_axes.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6324,21 +6324,6 @@ def test_spines_properbbox_after_zoom():
63246324
np.testing.assert_allclose(bb.get_points(), bb2.get_points(), rtol=1e-6)
63256325

63266326

6327-
def test_cartopy_backcompat():
6328-
6329-
class Dummy(matplotlib.axes.Axes):
6330-
...
6331-
6332-
class DummySubplot(matplotlib.axes.SubplotBase, Dummy):
6333-
_axes_class = Dummy
6334-
6335-
matplotlib.axes._subplots._subplot_classes[Dummy] = DummySubplot
6336-
6337-
FactoryDummySubplot = matplotlib.axes.subplot_class_factory(Dummy)
6338-
6339-
assert DummySubplot is FactoryDummySubplot
6340-
6341-
63426327
def test_gettightbbox_ignore_nan():
63436328
fig, ax = plt.subplots()
63446329
remove_ticks_and_titles(fig)

lib/matplotlib/tests/test_pickle.py

Lines changed: 6 additions & 0 deletions
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

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import functools
22

3-
from matplotlib import _api
3+
from matplotlib import _api, cbook
44
import matplotlib.artist as martist
55
import matplotlib.image as mimage
66
import matplotlib.transforms as mtransforms
@@ -94,12 +94,8 @@ def apply_aspect(self, position=None):
9494
# end of aux_transform support
9595

9696

97-
@functools.lru_cache(None)
98-
def parasite_axes_class_factory(axes_class):
99-
return type("%sParasite" % axes_class.__name__,
100-
(ParasiteAxesBase, axes_class), {})
101-
102-
97+
parasite_axes_class_factory = cbook._make_class_factory(
98+
ParasiteAxesBase, "{}Parasite")
10399
ParasiteAxes = parasite_axes_class_factory(Axes)
104100

105101

@@ -283,7 +279,7 @@ def _add_twin_axes(self, axes_class, **kwargs):
283279
*kwargs* are forwarded to the parasite axes constructor.
284280
"""
285281
if axes_class is None:
286-
axes_class = self._get_base_axes()
282+
axes_class = self._base_axes_class
287283
ax = parasite_axes_class_factory(axes_class)(self, **kwargs)
288284
self.parasites.append(ax)
289285
ax._remove_method = self._remove_any_twin
@@ -310,11 +306,10 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
310306
return Bbox.union([b for b in bbs if b.width != 0 or b.height != 0])
311307

312308

313-
@functools.lru_cache(None)
314-
def host_axes_class_factory(axes_class):
315-
return type("%sHostAxes" % axes_class.__name__,
316-
(HostAxesBase, axes_class),
317-
{'_get_base_axes': lambda self: axes_class})
309+
host_axes_class_factory = cbook._make_class_factory(
310+
HostAxesBase, "{}HostAxes", "_base_axes_class")
311+
HostAxes = host_axes_class_factory(Axes)
312+
SubplotHost = subplot_class_factory(HostAxes)
318313

319314

320315
def host_subplot_class_factory(axes_class):
@@ -323,10 +318,6 @@ def host_subplot_class_factory(axes_class):
323318
return subplot_host_class
324319

325320

326-
HostAxes = host_axes_class_factory(Axes)
327-
SubplotHost = subplot_class_factory(HostAxes)
328-
329-
330321
def host_axes(*args, axes_class=Axes, figure=None, **kwargs):
331322
"""
332323
Create axes that can act as a hosts to parasitic axes.

lib/mpl_toolkits/axisartist/floating_axes.py

Lines changed: 3 additions & 8 deletions
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
@@ -352,12 +351,8 @@ def adjust_axes_lim(self):
352351
self.set_ylim(ymin-dy, ymax+dy)
353352

354353

355-
@functools.lru_cache(None)
356-
def floatingaxes_class_factory(axes_class):
357-
return type("Floating %s" % axes_class.__name__,
358-
(FloatingAxesBase, axes_class), {})
359-
360-
354+
floatingaxes_class_factory = cbook._make_class_factory(
355+
FloatingAxesBase, "Floating {}")
361356
FloatingAxes = floatingaxes_class_factory(
362357
host_axes_class_factory(axislines.Axes))
363358
FloatingSubplot = maxes.subplot_class_factory(FloatingAxes)

0 commit comments

Comments
 (0)