Skip to content

Templatize class factories. #19033

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/api/next_api_changes/removals/19033-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
The private ``matplotlib.axes._subplots._subplot_classes`` dict has been removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Support for passing ``None`` to ``subplot_class_factory`` has been removed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Explicitly pass in the base `~matplotlib.axes.Axes` class instead.
93 changes: 32 additions & 61 deletions doc/missing-references.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"doc/users/prev_whats_new/whats_new_3.1.0.rst:335"
],
"cbar_axes": [
"lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid.__init__:45",
"lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:45",
"lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:45"
],
Expand Down Expand Up @@ -79,39 +78,35 @@
],
"matplotlib.axes.Axes.lines": [
"doc/tutorials/intermediate/artists.rst:100",
"doc/tutorials/intermediate/artists.rst:440"
"doc/tutorials/intermediate/artists.rst:442"
],
"matplotlib.axes.Axes.patch": [
"doc/api/prev_api_changes/api_changes_0.98.x.rst:89",
"doc/tutorials/intermediate/artists.rst:186",
"doc/tutorials/intermediate/artists.rst:424"
"doc/tutorials/intermediate/artists.rst:187",
"doc/tutorials/intermediate/artists.rst:426"
],
"matplotlib.axes.Axes.patches": [
"doc/tutorials/intermediate/artists.rst:463"
"doc/tutorials/intermediate/artists.rst:465"
],
"matplotlib.axes.Axes.transAxes": [
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows.__init__:8",
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows:8"
],
"matplotlib.axes.Axes.transData": [
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox.__init__:11",
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox:11",
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredEllipse.__init__:8",
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredEllipse:8",
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar.__init__:8",
"lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar:8"
],
"matplotlib.axes.Axes.viewLim": [
"doc/api/prev_api_changes/api_changes_0.99.x.rst:23"
],
"matplotlib.axes.Axes.xaxis": [
"doc/tutorials/intermediate/artists.rst:608"
"doc/tutorials/intermediate/artists.rst:610"
],
"matplotlib.axes.Axes.yaxis": [
"doc/tutorials/intermediate/artists.rst:608"
"doc/tutorials/intermediate/artists.rst:610"
],
"matplotlib.axis.Axis.label": [
"doc/tutorials/intermediate/artists.rst:655"
"doc/tutorials/intermediate/artists.rst:657"
],
"matplotlib.cm.ScalarMappable.callbacksSM": [
"doc/api/prev_api_changes/api_changes_0.98.0.rst:10"
Expand All @@ -124,11 +119,11 @@
],
"matplotlib.figure.Figure.patch": [
"doc/api/prev_api_changes/api_changes_0.98.x.rst:89",
"doc/tutorials/intermediate/artists.rst:186",
"doc/tutorials/intermediate/artists.rst:319"
"doc/tutorials/intermediate/artists.rst:187",
"doc/tutorials/intermediate/artists.rst:320"
],
"matplotlib.figure.Figure.transFigure": [
"doc/tutorials/intermediate/artists.rst:368"
"doc/tutorials/intermediate/artists.rst:369"
],
"max": [
"lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.p1:4"
Expand Down Expand Up @@ -256,15 +251,6 @@
"doc/tutorials/intermediate/artists.rst:44",
"doc/tutorials/intermediate/artists.rst:67"
],
"matplotlib.axes._axes.Axes": [
"doc/api/artist_api.rst:189",
"doc/api/axes_api.rst:609",
"lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.GeoAxes:1",
"lib/matplotlib/projections/polar.py:docstring of matplotlib.projections.polar.PolarAxes:1",
"lib/mpl_toolkits/axes_grid1/mpl_axes.py:docstring of mpl_toolkits.axes_grid1.mpl_axes.Axes:1",
"lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.Axes:1",
"lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D:1"
],
"matplotlib.axes._base._AxesBase": [
"doc/api/artist_api.rst:189",
"doc/api/axes_api.rst:609",
Expand Down Expand Up @@ -428,16 +414,6 @@
"lib/matplotlib/transforms.py:docstring of matplotlib.transforms.BlendedAffine2D:1",
"lib/matplotlib/transforms.py:docstring of matplotlib.transforms.BlendedGenericTransform:1"
],
"matplotlib.tri.trifinder.TriFinder": [
"lib/matplotlib/tri/trifinder.py:docstring of matplotlib.tri.trifinder.TrapezoidMapTriFinder:1"
],
"matplotlib.tri.triinterpolate.TriInterpolator": [
"lib/matplotlib/tri/triinterpolate.py:docstring of matplotlib.tri.triinterpolate.CubicTriInterpolator:1",
"lib/matplotlib/tri/triinterpolate.py:docstring of matplotlib.tri.triinterpolate.LinearTriInterpolator:1"
],
"matplotlib.tri.trirefine.TriRefiner": [
"lib/matplotlib/tri/trirefine.py:docstring of matplotlib.tri.trirefine.UniformTriRefiner:1"
],
"matplotlib.widgets._SelectorWidget": [
"lib/matplotlib/widgets.py:docstring of matplotlib.widgets.LassoSelector:1",
"lib/matplotlib/widgets.py:docstring of matplotlib.widgets.PolygonSelector:1",
Expand Down Expand Up @@ -485,7 +461,7 @@
"lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.AxisArtistHelper.Fixed:1",
"lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.AxisArtistHelper.Floating:1"
],
"mpl_toolkits.axisartist.floating_axes.Floating AxesHostAxes": [
"mpl_toolkits.axisartist.floating_axes.FloatingAxesHostAxes": [
"<unknown>:1",
"doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:31:<autosummary>:1"
],
Expand All @@ -498,11 +474,11 @@
},
"py:data": {
"matplotlib.axes.Axes.transAxes": [
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:219",
"lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:220",
"lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:179",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:220",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:219"
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:222",
"lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:223",
"lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:182",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:223",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:222"
]
},
"py:func": {
Expand Down Expand Up @@ -541,15 +517,13 @@
],
"_iter_collection": [
"lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.RendererBase.draw_path_collection:12",
"lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.draw_path_collection:12",
"lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_path_collection:12",
"lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_path_collection:12",
"lib/matplotlib/backends/backend_svg.py:docstring of matplotlib.backends.backend_svg.RendererSVG.draw_path_collection:12",
"lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.PathEffectRenderer.draw_path_collection:12"
],
"_iter_collection_raw_paths": [
"lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.RendererBase.draw_path_collection:12",
"lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.draw_path_collection:12",
"lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_path_collection:12",
"lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_path_collection:12",
"lib/matplotlib/backends/backend_svg.py:docstring of matplotlib.backends.backend_svg.RendererSVG.draw_path_collection:12",
Expand All @@ -574,24 +548,22 @@
"lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Affine2DBase:13"
],
"matplotlib.collections._CollectionWithSizes.set_sizes": [
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:172",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:82",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:112",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:112",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:168",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:165",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:202",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:172",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:82",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:112",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:112",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:168",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:165",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:202",
"lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs.__init__:176",
"lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:206",
"lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver.__init__:206",
"lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:239"
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:171",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:81",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:111",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:111",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:167",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:164",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:201",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:171",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:81",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:111",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:111",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:167",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:164",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:201",
"lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:205",
"lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:238"
],
"matplotlib.dates.DateFormatter.__call__": [
"doc/users/prev_whats_new/whats_new_1.5.rst:497"
Expand Down Expand Up @@ -727,7 +699,6 @@
"doc/users/event_handling.rst:179"
],
"Size.from_any": [
"lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid.__init__:57",
"lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:57",
"lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:57"
],
Expand Down
64 changes: 3 additions & 61 deletions lib/matplotlib/axes/_subplots.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import functools

from matplotlib import _api
from matplotlib import _api, cbook
from matplotlib.axes._axes import Axes
from matplotlib.gridspec import GridSpec, SubplotSpec

Expand Down Expand Up @@ -36,15 +34,6 @@ def __init__(self, fig, *args, **kwargs):
# This will also update the axes position.
self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args))

def __reduce__(self):
# get the first axes class which does not inherit from a subplotbase
axes_class = next(
c for c in type(self).__mro__
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
return (_picklable_subplot_class_constructor,
(axes_class,),
self.__getstate__())

@_api.deprecated(
"3.4", alternative="get_subplotspec",
addendum="(get_subplotspec returns a SubplotSpec instance.)")
Expand Down Expand Up @@ -169,53 +158,6 @@ def _make_twin_axes(self, *args, **kwargs):
return twin


# this here to support cartopy which was using a private part of the
# API to register their Axes subclasses.

# In 3.1 this should be changed to a dict subclass that warns on use
# In 3.3 to a dict subclass that raises a useful exception on use
# In 3.4 should be removed

# The slow timeline is to give cartopy enough time to get several
# release out before we break them.
_subplot_classes = {}


@functools.lru_cache(None)
def subplot_class_factory(axes_class=None):
"""
Make a new class that inherits from `.SubplotBase` and the
given axes_class (which is assumed to be a subclass of `.axes.Axes`).
This is perhaps a little bit roundabout to make a new class on
the fly like this, but it means that a new Subplot class does
not have to be created for every type of Axes.
"""
if axes_class is None:
_api.warn_deprecated(
"3.3", message="Support for passing None to subplot_class_factory "
"is deprecated since %(since)s; explicitly pass the default Axes "
"class instead. This will become an error %(removal)s.")
axes_class = Axes
try:
# Avoid creating two different instances of GeoAxesSubplot...
# Only a temporary backcompat fix. This should be removed in
# 3.4
return next(cls for cls in SubplotBase.__subclasses__()
if cls.__bases__ == (SubplotBase, axes_class))
except StopIteration:
return type("%sSubplot" % axes_class.__name__,
(SubplotBase, axes_class),
{'_axes_class': axes_class})


subplot_class_factory = cbook._make_class_factory(
SubplotBase, "{}Subplot", "_axes_class")
Subplot = subplot_class_factory(Axes) # Provided for backward compatibility.


def _picklable_subplot_class_constructor(axes_class):
"""
Stub factory that returns an empty instance of the appropriate subplot
class when called with an axes class. This is purely to allow pickling of
Axes and Subplots.
"""
subplot_class = subplot_class_factory(axes_class)
return subplot_class.__new__(subplot_class)
50 changes: 50 additions & 0 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2188,3 +2188,53 @@ def _unikey_or_keysym_to_mplkey(unikey, keysym):
"next": "pagedown", # Used by tk.
}.get(key, key)
return key


@functools.lru_cache(None)
def _make_class_factory(mixin_class, fmt, attr_name=None):
"""
Return a function that creates picklable classes inheriting from a mixin.

After ::

factory = _make_class_factory(FooMixin, fmt, attr_name)
FooAxes = factory(Axes)

``Foo`` is a class that inherits from ``FooMixin`` and ``Axes`` and **is
picklable** (picklability is what differentiates this from a plain call to
`type`). Its ``__name__`` is set to ``fmt.format(Axes.__name__)`` and the
base class is stored in the ``attr_name`` attribute, if not None.

Moreover, the return value of ``factory`` is memoized: calls with the same
``Axes`` class always return the same subclass.
"""

@functools.lru_cache(None)
def class_factory(axes_class):
# The parameter is named "axes_class" for backcompat but is really just
# a base class; no axes semantics are used.
base_class = axes_class

class subcls(mixin_class, base_class):
# Better approximation than __module__ = "matplotlib.cbook".
__module__ = mixin_class.__module__

def __reduce__(self):
return (_picklable_class_constructor,
(mixin_class, fmt, attr_name, base_class),
self.__getstate__())

subcls.__name__ = subcls.__qualname__ = fmt.format(base_class.__name__)
if attr_name is not None:
setattr(subcls, attr_name, base_class)
return subcls

class_factory.__module__ = mixin_class.__module__
return class_factory


def _picklable_class_constructor(mixin_class, fmt, attr_name, base_class):
"""Internal helper for _make_class_factory."""
factory = _make_class_factory(mixin_class, fmt, attr_name)
cls = factory(base_class)
return cls.__new__(cls)
15 changes: 0 additions & 15 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6340,21 +6340,6 @@ def test_spines_properbbox_after_zoom():
np.testing.assert_allclose(bb.get_points(), bb2.get_points(), rtol=1e-6)


def test_cartopy_backcompat():

class Dummy(matplotlib.axes.Axes):
...

class DummySubplot(matplotlib.axes.SubplotBase, Dummy):
_axes_class = Dummy

matplotlib.axes._subplots._subplot_classes[Dummy] = DummySubplot

FactoryDummySubplot = matplotlib.axes.subplot_class_factory(Dummy)

assert DummySubplot is FactoryDummySubplot


def test_gettightbbox_ignore_nan():
fig, ax = plt.subplots()
remove_ticks_and_titles(fig)
Expand Down
6 changes: 6 additions & 0 deletions lib/matplotlib/tests/test_pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import matplotlib.figure as mfigure
from mpl_toolkits.axes_grid1 import parasite_axes


def test_simple():
Expand Down Expand Up @@ -212,3 +213,8 @@ def test_unpickle_canvas():
out.seek(0)
fig2 = pickle.load(out)
assert fig2.canvas is not None


def test_mpl_toolkits():
ax = parasite_axes.host_axes([0, 0, 1, 1])
assert type(pickle.loads(pickle.dumps(ax))) == parasite_axes.HostAxes
Loading