Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Picklable figures #1020

Closed
wants to merge 5 commits into from

7 participants

@pelson
Collaborator

I have been working on the ability to pickle figures (as mentioned in #323).

I have done quite a lot of testing of this (pyplot figures, non-pyplot figures, polar axes, contours, pcolormeshes etc.) but it is inevitable that there will be some types of plots that simply do not work at this stage (I would like to know about those).

There was a little (backwards compatible) shuffle in the way pyplot creates its figures which will need applying to all backends, and some unit tests will need adding, but other than that, I think this is in a form where it is worth getting other developers eyes on the code.

Any questions, please ask.

@pelson
Collaborator

Update: I have just tried pickling a Basemap instance and associated figure and it has all gone swimmingly. This could be a very valuable time saver for creation of high resolution maps for a fixed location, from a batch job for instance (@jswhit will probably be most interested by this).

@jswhit
Collaborator

Basemap instances were designed to be picklable, so that high-res instances that take a long time to create could be saved. The basemap hires.py example shows how much of a speed up you can get from this.

@pelson
Collaborator

The basemap hires.py example shows how much of a speed up you can get from this.

I did not know that. Thanks Jeff. Please ignore my eureka moment which has already been handled by Basemap :-) .

@jswhit
Collaborator

Glad to know someone finds it useful - I don't think anyone but me has ever used that feature.

@pelson pelson commented on the diff
lib/matplotlib/backends/backend_tkagg.py
((5 lines not shown))
FigureClass = kwargs.pop('FigureClass', Figure)
figure = FigureClass(*args, **kwargs)
+ return new_figure_manager_given_figure(num, figure)
+
+
+def new_figure_manager_given_figure(num, figure):
@pelson Collaborator
pelson added a note

Did anyone have any better ways of doing this rather than splitting up of new_figure_manager (which I still need to do for all of the backends)? It seems like a pattern that would pop up frequently if one were producing Figures without the plt.figure function, so I am surprised I couldn't find anything to "attach" a pre-created figure to the Gcf (and let it take care of making the manager for me).

It seems that new_figure_manager (and the corresponding new_figure_manager_given_figure) is nearly identical in all the backends. Wouldn't it be better to extend the __init__() of FigureManager, so that it may be inherited?

@pelson Collaborator
pelson added a note

@akhmerov: Your suggestion sounds great, but sadly I don't agree that they are nearly identical in all the backends (I looked at qt4agg, wxagg and tkagg after reading your comment). That is not to say your inheritance suggestion does not deserve merit, it would be nicer to have new_figure_manager be a class method on a FigureManager class, but I'm not sure the benefit outweighs the cost of breaking backwards compatibility (it would be possible to put an alias to the class method in place of the current function, but that would mean there were two ways of doing the same thing...).

@pelson: It is not entirely obvious that the backends qt4agg, wxagg and tkagg are so different. qt4 differs from tk by just classes it calls. In wx the difference is that there's a FigureFrame object, which has to be passed to a figure manager. Now __init__ of FigureFrame does what your new_figure_manager_given_figure does, and it also creates a figure manager. However at the same time, both __init__ of FigureManagerWx and FigureFrameWx require almost identical information, so it doesn't seem so crazy if e.g. FigureManagerWx initialized FigureFrameWx. This would also make sense given that FigureManager is the central object in other backends. Overall: I realize that for keeping backwards compatibility one would need to use an alias, and that it is not immediately obvious how to implement the proper behaviour, however it seems that the current state of backends is a mixture of OO code with weird naming scheme (why does one need backends.tkagg.FigureManagerTkAgg instead of backends.tkagg.FigureManager?), and copy-paste-modify non-OO code. This is a pain to interface to: for example IPython needed to hack into the code, to implement their own backend. I think that temporary existence of two ways of doing the same thing is a relatively low cost for having one of these ways clean.

@pelson Collaborator
pelson added a note

I can see you have a real passion to sort this out @akhmerov :-) . Would you be willing to submit a pull request against my branch (or even mpl/master) with some of your suggested changes and we can talk it through in-line? I do not propose you modify all backends at this point, perhaps just the 3 we have discussed.

Yeah, indeed I have a real passion, only limited time. Is there any time after which it would become too late? Is it ok if it takes me around a month?

@pelson Collaborator
pelson added a note

Is it ok if it takes me around a month?

To base it on this branch that would be too long. There is a freeze planned for the 20th of August and I would like to get this branch in in the next week or so.

Having said this, the change I am proposing here is additive and not a major rework, so I am comfortable in making this change even with the knowledge that a better (larger) rework is on the cards in the future.

@akhmerov
akhmerov added a note

Ok, I guess then I'll just do this independently and probably after the freeze.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@WeatherGod
Collaborator

I suspect that if I follow the polar examples, I could get the mplot3d stuff to work with this feature?

@pelson
Collaborator

I suspect that if I follow the polar examples, I could get the mplot3d stuff to work with this feature?

Yeah. I was hoping to get some feedback on what I have done, before adding the testing machinery (and the necessary debugging from the pickle library if you come across something which doesn't pickle). Given there has been little comment, I will take that as a good thing and will look to adding tests and updating all of the backends soon-ish.

lib/matplotlib/axes.py
@@ -172,6 +172,15 @@ def __init__(self, axes, command='plot'):
self.command = command
self.set_color_cycle()
+ def __getinitargs__(self):
+ # note: __getinitargs__ only works for old-style classes
+ # means that the color cycle will be lost.
+ return (self.axes, self.command)
@mdboom Owner
mdboom added a note

Shouldn't we be moving to new style classes anyway? Any harm in updating the class to new style (and then using __getstate__ instead?)

@pelson Collaborator
pelson added a note

If your happy for me to do that in this PR, then I will.

@mdboom Owner
mdboom added a note

Yes, let's do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mdboom
Owner

Looks good. The nested class thing is a complication with pickling that I wasn't aware of. I'm a bit concerned that those writing custom projections need to understand that complexity. Maybe it's better to not use nested classes for this (and update the "custom projection" docs accordingly?) I presume it's possible to have non-nested classes, but keep a reference to the class within the projection class, so as to not break backward compatibility, i.e.:

class CustomTransformExtern:
   pass

class CustomProjection:
   CustomTransform = CustomTransformExtern
@pelson
Collaborator

I presume it's possible to have non-nested classes, but keep a reference to the class within the projection class, so as to not break backward compatibility

Good idea. I didn't do it originally primarily for the backwards compatibility issue (plus size of review was a factor). Do you want me to do that in this PR on in a follow on one?

@mdboom
Owner

I don't have a strong preference as to whether the nested class thing is resolved here or in a follow on -- but it would nice to not have to explain the peculiarities of pickles to those writing custom projections -- it's nicer to have everything "just work" in the obvious way.

@mdboom
Owner

We should have a regression test for this. Something that

  1. Creates a figure with some complex combinations of artists, axes etc.
  2. Pickles it to an in-memory string
  3. Unpickles it from memory
  4. Outputs the figure and compares to some baseline_images
@dmcdougall
Collaborator

On a high level, what changes need to occur for other backends to support figure pickling?

Great effort, by the way @pelson. This is something I've wanted for a while.

@pelson
Collaborator

Figure pickling does not involve any backend code. The key is that one must re-attach a figure to a backend, hence my addition of a new_figure_manager_given_figure function. It is my intention to go through this in the next 24 hours, so that might give you an indication of the work needed.

@mdboom
Owner

One other comment (and I know you're still working on this) -- it would be nice to have an example that shows how to create, pickle and restore a figure.

lib/matplotlib/cbook.py
@@ -266,6 +266,13 @@ def __init__(self, *args):
self._cid = 0
self._func_cid_map = {}
+ def __getstate__(self):
+ return {}
@pelson Collaborator
pelson added a note

Worthy of a comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/ticker.py
((35 lines not shown))
+class TickHelper(object):
+ axis = None
@pelson Collaborator
pelson added a note

This line looks suspicious. I don't think it should be there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pelson pelson commented on the diff
lib/matplotlib/transforms.py
@@ -91,6 +91,17 @@ def __init__(self):
# computed for the first time.
self._invalid = 1
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ # turn the weakkey dictionary into a normal dictionary
+ d['_parents'] = dict(self._parents.iteritems())
+ return d
+
+ def __setstate__(self, data_dict):
+ self.__dict__ = data_dict
+ # turn the normal dictionary back into a WeakKeyDict
+ self._parents = WeakKeyDictionary(self._parents)
@pelson Collaborator
pelson added a note

This will conflict with my transform limit PR. Need to watch out for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pelson
Collaborator

Ok. I'm making some good progress with this. I think the test coverage is good. Its highlighted some issues with some pretty fundamental artists (colorbars, legends and text (only the latter currently working)).

I want to make a couple more changes re the nested transforms in polar, but other than that, I would be happy to have this merged and subsequently fix other issues (colorbar and legend pickling for instance).

Is that a suitable plan of attack?

@mdboom
Owner

I think that's a fine plan of attack if that means that colorbar and legend pickling don't work but otherwise things that used to work are not broken (which I assume is the case, just asking for clarity).

@WeatherGod
Collaborator

I would be happy with marking this as an experimental feature.

@pelson
Collaborator

@WeatherGod: Agreed. I have updated the whats new to reflect this.

@pelson
Collaborator

That last commit was a bit noisy due to the moving of the nested classes. It was a copy and paste job, and I have verified all the tests still pass.

@pelson
Collaborator

Ok. I am comfortable with this PR now. I was quite surprised how easy it was to apply the changes to all of the backends, and how similar that code was throughout (as @akhmerov says: there is definitely a lot of room for improvement there).

I will look into the colorbar / legend support in the next few days hopefully and submit those as bug fixes (whether we decide to merge them is another matter).

@tkf tkf referenced this pull request in ipython/ipython
Open

Request: pickle-based pylab backend for ZMQ shell #2322

@mdboom
Owner

@pelson: I think this is a great feature. I'm trying to suss out the benefits of having it go in incomplete (without the colorbar and legend support) vs. waiting for it to be complete. I think I'd prefer it to be complete for the release. Do you think colorbar and legend support is a lot of additional work?

@efiring
Owner

@mdboom, I agree--this is a major new feature, but I don't think it should be in a release without colorbar and legend support. Releasing it that way would be just asking for a bunch of pointless email traffic and user confusion. Better to reserve it for 1.3 if necessary.

@dmcdougall dmcdougall commented on the diff
lib/matplotlib/figure.py
((4 lines not shown))
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ # the axobservers cannot currently be pickled.
+ # Additionally, the canvas cannot currently be pickled, but this has
+ # the benefit of meaning that a figure can be detached from one canvas,
+ # and re-attached to another.
+ for attr_to_pop in ('_axobservers', 'show', 'canvas', '_cachedRenderer') :
+ state.pop(attr_to_pop, None)
+
+ # add version information to the state
+ state['__mpl_version__'] = _mpl_version
+
+ # check to see if the figure has a manager and whether it is registered
+ # with pyplot
+ if self.canvas is not None and self.canvas.manager is not None:
+ manager = self.canvas.manager
@dmcdougall Collaborator

Will you need to add a manager to each backend's constructor method? It works well in pyplot, but using the object-oriented interface

import pickle as p
from matplotlib.backends.backend_pdf import FigureCanvasPdf as fc
from matplotlib.figure import Figure

fig = Figure()
can = fc(fig)
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3], [1, 2, 3])
fig.savefig('plot.pdf')

fout = open('pickled_fig.pkl', 'wb')
p.dump(fig, fout)
fout.close()

throws an error: AttributeError: 'FigureCanvasPdf' object has no attribute 'manager'

Knowing the manager at construct-time means probing for the current backend, which fits in nicely with #1125.

@dmcdougall Collaborator

Er, I think I meant the Figure's constructor method, not the backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dmcdougall
Collaborator

This is AWESOME, by the way.

I just ran a little example using pyplot. In one script I pickled a figure using the PDF backend. In another script I unpickled it and added something else to the axes and it worked.

:D

@pelson
Collaborator

Firstly, thanks @dmcdougall and others for trying this out, its helpful to get all of your feedback.

@efiring and @mdboom: It is inevitable that there will be features missing whenever this is first released. I have considered adding a format in the @image_comparison decorator which would allow me to get further test coverage quickly and easily, but I haven't even investigated if this is feasible. My personal feeling is that I would rather see frequent, smaller changes to incrementally improve coverage/fix bugs, rather than a single monolithic PR, but either way works. The colorbar and legend support is very likely to be achievable by 1.2 RC1 (assuming that it is actually possible, which I have no evidence to the contrary) and I am happy to put aside some time to investigate and implement the necessary changes.

Because this is a completely new feature without any backwards incompatible changes, I would be comfortable slipping this in to the first RC with a week or so to spare, if anybody is uncomfortable then the alternative is to accept that this feature will not be available until v1.3. I make that to be a deadline of merging this before the 10th of September, else it will not get shipped just yet.

Agreed?

@efiring
Owner

@pelson, I agree that with something like this, it can be good to get in the core, which you have now, and then use smaller PRs to add coverage. My main concern is getting into a situation where users expect everything to work, when common components don't work. If we did not have a release coming up, this would not be a problem.

We could make an exception to the "feature freeze", and merge (with or without a rebase) what you have now. Then if colorbar and legend support can be added by rc1, and if everything looks OK, the feature can be added to "What's New". If not, it could be left in place, but with a note in the FAQ saying "pickle support is an underway project, but don't expect it to work until 1.3." "What's New" could also include a statement that internal changes have been made to lay the foundation, but it is not ready for use.

@pelson
Collaborator

I'm going to spend some time on this in the next day or so, so hopefully that should clarify the situation, and then we can make a decision whether to put this in v1.2.x or not.

@mdboom
Owner

@pelson: Sounds good to me. Thanks again for your work on this. I know the IPython guys are very much looking forward to this.

@pelson
Collaborator

I have addressed @dmcdougall 's valuable feedback in the new PR (and added a test for his code).

This PR is now obsolete

@pelson pelson closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
Showing with 826 additions and 279 deletions.
  1. +11 −0 doc/users/whats_new.rst
  2. +1 −0  lib/matplotlib/__init__.py
  3. +2 −1  lib/matplotlib/_pylab_helpers.py
  4. +8 −1 lib/matplotlib/artist.py
  5. +47 −7 lib/matplotlib/axes.py
  6. +0 −1  lib/matplotlib/axis.py
  7. +1 −1  lib/matplotlib/backends/__init__.py
  8. +8 −2 lib/matplotlib/backends/backend_agg.py
  9. +9 −2 lib/matplotlib/backends/backend_cairo.py
  10. +10 −1 lib/matplotlib/backends/backend_cocoaagg.py
  11. +8 −1 lib/matplotlib/backends/backend_emf.py
  12. +7 −0 lib/matplotlib/backends/backend_fltkagg.py
  13. +8 −4 lib/matplotlib/backends/backend_gdk.py
  14. +8 −3 lib/matplotlib/backends/backend_gtk.py
  15. +8 −1 lib/matplotlib/backends/backend_gtk3agg.py
  16. +8 −1 lib/matplotlib/backends/backend_gtk3cairo.py
  17. +10 −1 lib/matplotlib/backends/backend_gtkagg.py
  18. +8 −1 lib/matplotlib/backends/backend_gtkcairo.py
  19. +12 −1 lib/matplotlib/backends/backend_macosx.py
  20. +8 −1 lib/matplotlib/backends/backend_pdf.py
  21. +8 −1 lib/matplotlib/backends/backend_ps.py
  22. +10 −3 lib/matplotlib/backends/backend_qt.py
  23. +10 −3 lib/matplotlib/backends/backend_qt4.py
  24. +9 −1 lib/matplotlib/backends/backend_qt4agg.py
  25. +10 −2 lib/matplotlib/backends/backend_qtagg.py
  26. +9 −1 lib/matplotlib/backends/backend_svg.py
  27. +10 −2 lib/matplotlib/backends/backend_template.py
  28. +8 −1 lib/matplotlib/backends/backend_tkagg.py
  29. +9 −0 lib/matplotlib/backends/backend_wx.py
  30. +8 −2 lib/matplotlib/backends/backend_wxagg.py
  31. +40 −2 lib/matplotlib/cbook.py
  32. +12 −7 lib/matplotlib/colorbar.py
  33. +7 −0 lib/matplotlib/contour.py
  34. +64 −1 lib/matplotlib/figure.py
  35. +10 −0 lib/matplotlib/markers.py
  36. +28 −20 lib/matplotlib/patches.py
  37. +190 −174 lib/matplotlib/projections/polar.py
  38. +8 −11 lib/matplotlib/pyplot.py
  39. BIN  lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png
  40. +157 −0 lib/matplotlib/tests/test_pickle.py
  41. +18 −17 lib/matplotlib/ticker.py
  42. +19 −1 lib/matplotlib/transforms.py
View
11 doc/users/whats_new.rst
@@ -70,6 +70,17 @@ minimum and maximum colorbar extensions.
plt.show()
+Figures are picklable
+---------------------
+
+Philip Elson added an experimental feature to make figures picklable
+for quick and easy short-term storage of plots. Pickle files
+are not designed for long term storage, are unsupported when restoring a pickle
+saved in another matplotlib version and are insecure when restoring a pickle
+from an untrusted source. Having said this, they are useful for short term
+storage for later modification inside matplotlib.
+
+
Set default bounding box in matplotlibrc
------------------------------------------
View
1  lib/matplotlib/__init__.py
@@ -1068,6 +1068,7 @@ def tk_window_focus():
'matplotlib.tests.test_mathtext',
'matplotlib.tests.test_mlab',
'matplotlib.tests.test_patches',
+ 'matplotlib.tests.test_pickle',
'matplotlib.tests.test_rcparams',
'matplotlib.tests.test_simplification',
'matplotlib.tests.test_spines',
View
3  lib/matplotlib/_pylab_helpers.py
@@ -14,7 +14,7 @@ def error_msg(msg):
class Gcf(object):
"""
- Manage a set of integer-numbered figures.
+ Singleton to manage a set of integer-numbered figures.
This class is never instantiated; it consists of two class
attributes (a list and a dictionary), and a set of static
@@ -131,6 +131,7 @@ def set_active(manager):
if m != manager: Gcf._activeQue.append(m)
Gcf._activeQue.append(manager)
Gcf.figs[manager.num] = manager
+
atexit.register(Gcf.destroy_all)
View
9 lib/matplotlib/artist.py
@@ -104,6 +104,13 @@ def __init__(self):
self.y_isdata = True # with y
self._snap = None
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ # remove the unpicklable remove method, this will get re-added on load
+ # (by the axes) if the artist lives on an axes.
+ d['_remove_method'] = None
+ return d
+
def remove(self):
"""
Remove the artist from the figure if possible. The effect
@@ -123,7 +130,7 @@ def remove(self):
# the _remove_method attribute directly. This would be a protected
# attribute if Python supported that sort of thing. The callback
# has one parameter, which is the child to be removed.
- if self._remove_method != None:
+ if self._remove_method is not None:
self._remove_method(self)
else:
raise NotImplementedError('cannot remove artist')
View
54 lib/matplotlib/axes.py
@@ -154,9 +154,8 @@ def set_default_color_cycle(clist):
DeprecationWarning)
-class _process_plot_var_args:
+class _process_plot_var_args(object):
"""
-
Process variable length arguments to the plot command, so that
plot commands like the following are supported::
@@ -172,6 +171,14 @@ def __init__(self, axes, command='plot'):
self.command = command
self.set_color_cycle()
+ def __getstate__(self):
+ # note: it is not possible to pickle a itertools.cycle instance
+ return {'axes': self.axes, 'command': self.command}
+
+ def __setstate__(self, state):
+ self.__dict__ = state.copy()
+ self.set_color_cycle()
+
def set_color_cycle(self, clist=None):
if clist is None:
clist = rcParams['axes.color_cycle']
@@ -332,7 +339,7 @@ def _grab_next_args(self, *args, **kwargs):
for seg in self._plot_args(remaining[:isplit], kwargs):
yield seg
remaining=remaining[isplit:]
-
+
class Axes(martist.Artist):
"""
@@ -352,9 +359,10 @@ class Axes(martist.Artist):
_shared_x_axes = cbook.Grouper()
_shared_y_axes = cbook.Grouper()
-
+
def __str__(self):
return "Axes(%g,%g;%gx%g)" % tuple(self._position.bounds)
+
def __init__(self, fig, rect,
axisbg = None, # defaults to rc axes.facecolor
frameon = True,
@@ -489,6 +497,15 @@ def __init__(self, fig, rect,
self._ycid = self.yaxis.callbacks.connect('units finalize',
self.relim)
+ def __setstate__(self, state):
+ self.__dict__ = state
+ # put the _remove_method back on all artists contained within the axes
+ for container_name in ['lines', 'collections', 'tables', 'patches',
+ 'texts', 'images']:
+ container = getattr(self, container_name)
+ for artist in container:
+ artist._remove_method = container.remove
+
def get_window_extent(self, *args, **kwargs):
"""
get the axes bounding box in display space; *args* and
@@ -1599,13 +1616,13 @@ def _process_unit_info(self, xdata=None, ydata=None, kwargs=None):
if xdata is not None:
# we only need to update if there is nothing set yet.
if not self.xaxis.have_units():
- self.xaxis.update_units(xdata)
+ self.xaxis.update_units(xdata)
#print '\tset from xdata', self.xaxis.units
if ydata is not None:
# we only need to update if there is nothing set yet.
if not self.yaxis.have_units():
- self.yaxis.update_units(ydata)
+ self.yaxis.update_units(ydata)
#print '\tset from ydata', self.yaxis.units
# process kwargs 2nd since these will override default units
@@ -8770,7 +8787,15 @@ def __init__(self, fig, *args, **kwargs):
# _axes_class is set in the subplot_class_factory
self._axes_class.__init__(self, fig, self.figbox, **kwargs)
-
+ def __reduce__(self):
+ # get the first axes class which does not inherit from a subplotbase
+ axes_class = filter(lambda klass: (issubclass(klass, Axes) and
+ not issubclass(klass, SubplotBase)),
+ self.__class__.mro())[0]
+ r = [_PicklableSubplotClassConstructor(),
+ (axes_class,),
+ self.__getstate__()]
+ return tuple(r)
def get_geometry(self):
"""get the subplot geometry, eg 2,2,3"""
@@ -8852,6 +8877,21 @@ def subplot_class_factory(axes_class=None):
# This is provided for backward compatibility
Subplot = subplot_class_factory()
+
+class _PicklableSubplotClassConstructor(object):
+ """
+ This stub class exists to return the appropriate subplot
+ class when __call__-ed with an axes class. This is purely to
+ allow Pickling of Axes and Subplots."""
+ def __call__(self, axes_class):
+ # create a dummy object instance
+ subplot_instance = _PicklableSubplotClassConstructor()
+ subplot_class = subplot_class_factory(axes_class)
+ # update the class to the desired subplot class
+ subplot_instance.__class__ = subplot_class
+ return subplot_instance
+
+
docstring.interpd.update(Axes=martist.kwdoc(Axes))
docstring.interpd.update(Subplot=martist.kwdoc(Axes))
View
1  lib/matplotlib/axis.py
@@ -595,7 +595,6 @@ class Ticker:
formatter = None
-
class Axis(artist.Artist):
"""
View
2  lib/matplotlib/backends/__init__.py
@@ -52,6 +52,6 @@ def do_nothing(*args, **kwargs): pass
matplotlib.verbose.report('backend %s version %s' % (backend,backend_version))
- return new_figure_manager, draw_if_interactive, show
+ return backend_mod, new_figure_manager, draw_if_interactive, show
View
10 lib/matplotlib/backends/backend_agg.py
@@ -385,7 +385,6 @@ def post_processing(image, dpi):
image)
-
def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
@@ -396,7 +395,14 @@ def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasAgg(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasAgg(figure)
manager = FigureManagerBase(canvas, num)
return manager
View
11 lib/matplotlib/backends/backend_cairo.py
@@ -397,10 +397,17 @@ def new_figure_manager(num, *args, **kwargs): # called by backends/__init__.py
"""
Create a new figure manager instance
"""
- if _debug: print('%s.%s()' % (self.__class__.__name__, _fn_name()))
+ if _debug: print('%s.%s()' % (_fn_name()))
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasCairo(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasCairo(figure)
manager = FigureManagerBase(canvas, num)
return manager
View
11 lib/matplotlib/backends/backend_cocoaagg.py
@@ -35,12 +35,21 @@
mplBundle = NSBundle.bundleWithPath_(os.path.dirname(__file__))
+
def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass( *args, **kwargs )
- canvas = FigureCanvasCocoaAgg(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasCocoaAgg(figure)
return FigureManagerCocoaAgg(canvas, num)
+
## Below is the original show() function:
#def show():
# for manager in Gcf.get_all_fig_managers():
View
9 lib/matplotlib/backends/backend_emf.py
@@ -688,7 +688,14 @@ def new_figure_manager(num, *args, **kwargs):
# main-level app (egg backend_gtk, backend_gtkagg) for pylab
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasEMF(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasEMF(figure)
manager = FigureManagerEMF(canvas, num)
return manager
View
7 lib/matplotlib/backends/backend_fltkagg.py
@@ -78,6 +78,13 @@ def new_figure_manager(num, *args, **kwargs):
"""
FigureClass = kwargs.pop('FigureClass', Figure)
figure = FigureClass(*args, **kwargs)
+ return new_figure_manager_given_figure(num, figure)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
window = Fltk.Fl_Double_Window(10,10,30,30)
canvas = FigureCanvasFltkAgg(figure)
window.end()
View
12 lib/matplotlib/backends/backend_gdk.py
@@ -422,11 +422,15 @@ def new_figure_manager(num, *args, **kwargs):
"""
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasGDK(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasGDK(figure)
manager = FigureManagerBase(canvas, num)
- # equals:
- #manager = FigureManagerBase (FigureCanvasGDK (Figure(*args, **kwargs),
- # num)
return manager
View
11 lib/matplotlib/backends/backend_gtk.py
@@ -90,10 +90,15 @@ def new_figure_manager(num, *args, **kwargs):
"""
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasGTK(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasGTK(figure)
manager = FigureManagerGTK(canvas, num)
- # equals:
- #manager = FigureManagerGTK(FigureCanvasGTK(Figure(*args, **kwargs), num)
return manager
View
9 lib/matplotlib/backends/backend_gtk3agg.py
@@ -78,7 +78,14 @@ def new_figure_manager(num, *args, **kwargs):
"""
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasGTK3Agg(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasGTK3Agg(figure)
manager = FigureManagerGTK3Agg(canvas, num)
return manager
View
9 lib/matplotlib/backends/backend_gtk3cairo.py
@@ -45,7 +45,14 @@ def new_figure_manager(num, *args, **kwargs):
"""
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasGTK3Cairo(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasGTK3Cairo(figure)
manager = FigureManagerGTK3Cairo(canvas, num)
return manager
View
11 lib/matplotlib/backends/backend_gtkagg.py
@@ -33,6 +33,7 @@ def _get_toolbar(self, canvas):
toolbar = None
return toolbar
+
def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
@@ -40,10 +41,18 @@ def new_figure_manager(num, *args, **kwargs):
if DEBUG: print('backend_gtkagg.new_figure_manager')
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasGTKAgg(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasGTKAgg(figure)
return FigureManagerGTKAgg(canvas, num)
if DEBUG: print('backend_gtkagg.new_figure_manager done')
+
class FigureCanvasGTKAgg(FigureCanvasGTK, FigureCanvasAgg):
filetypes = FigureCanvasGTK.filetypes.copy()
filetypes.update(FigureCanvasAgg.filetypes)
View
9 lib/matplotlib/backends/backend_gtkcairo.py
@@ -26,7 +26,14 @@ def new_figure_manager(num, *args, **kwargs):
if _debug: print('backend_gtkcairo.%s()' % fn_name())
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasGTKCairo(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasGTKCairo(figure)
return FigureManagerGTK(canvas, num)
View
13 lib/matplotlib/backends/backend_macosx.py
@@ -233,9 +233,20 @@ def new_figure_manager(num, *args, **kwargs):
"""
if not _macosx.verify_main_display():
import warnings
- warnings.warn("Python is not installed as a framework. The MacOSX backend may not work correctly if Python is not installed as a framework. Please see the Python documentation for more information on installing Python as a framework on Mac OS X")
+ warnings.warn("Python is not installed as a framework. The MacOSX "
+ "backend may not work correctly if Python is not "
+ "installed as a framework. Please see the Python "
+ "documentation for more information on installing "
+ "Python as a framework on Mac OS X")
FigureClass = kwargs.pop('FigureClass', Figure)
figure = FigureClass(*args, **kwargs)
+ return new_figure_manager_given_figure(num, figure)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
canvas = FigureCanvasMac(figure)
manager = FigureManagerMac(canvas, num)
return manager
View
9 lib/matplotlib/backends/backend_pdf.py
@@ -2171,7 +2171,14 @@ def new_figure_manager(num, *args, **kwargs):
# main-level app (egg backend_gtk, backend_gtkagg) for pylab
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasPdf(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasPdf(figure)
manager = FigureManagerPdf(canvas, num)
return manager
View
9 lib/matplotlib/backends/backend_ps.py
@@ -944,7 +944,14 @@ def shouldstroke(self):
def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasPS(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasPS(figure)
manager = FigureManagerPS(canvas, num)
return manager
View
13 lib/matplotlib/backends/backend_qt.py
@@ -69,9 +69,16 @@ def new_figure_manager( num, *args, **kwargs ):
Create a new figure manager instance
"""
FigureClass = kwargs.pop('FigureClass', Figure)
- thisFig = FigureClass( *args, **kwargs )
- canvas = FigureCanvasQT( thisFig )
- manager = FigureManagerQT( canvas, num )
+ thisFig = FigureClass(*args, **kwargs)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasQT(figure)
+ manager = FigureManagerQT(canvas, num)
return manager
View
13 lib/matplotlib/backends/backend_qt4.py
@@ -72,9 +72,16 @@ def new_figure_manager( num, *args, **kwargs ):
"""
Create a new figure manager instance
"""
- thisFig = Figure( *args, **kwargs )
- canvas = FigureCanvasQT( thisFig )
- manager = FigureManagerQT( canvas, num )
+ thisFig = Figure(*args, **kwargs)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasQT(figure)
+ manager = FigureManagerQT(canvas, num)
return manager
View
10 lib/matplotlib/backends/backend_qt4agg.py
@@ -23,9 +23,17 @@ def new_figure_manager( num, *args, **kwargs ):
if DEBUG: print('backend_qtagg.new_figure_manager')
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass( *args, **kwargs )
- canvas = FigureCanvasQTAgg( thisFig )
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasQTAgg(figure)
return FigureManagerQT( canvas, num )
+
class NavigationToolbar2QTAgg(NavigationToolbar2QT):
def _get_canvas(self, fig):
return FigureCanvasQTAgg(fig)
View
12 lib/matplotlib/backends/backend_qtagg.py
@@ -23,8 +23,16 @@ def new_figure_manager( num, *args, **kwargs ):
if DEBUG: print('backend_qtagg.new_figure_manager')
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass( *args, **kwargs )
- canvas = FigureCanvasQTAgg( thisFig )
- return FigureManagerQTAgg( canvas, num )
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasQTAgg(figure)
+ return FigureManagerQTAgg(canvas, num)
+
class NavigationToolbar2QTAgg(NavigationToolbar2QT):
def _get_canvas(self, fig):
View
10 lib/matplotlib/backends/backend_svg.py
@@ -1151,10 +1151,18 @@ class FigureManagerSVG(FigureManagerBase):
def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasSVG(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasSVG(figure)
manager = FigureManagerSVG(canvas, num)
return manager
+
svgProlog = u"""\
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
View
12 lib/matplotlib/backends/backend_template.py
@@ -184,13 +184,21 @@ def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
"""
- # if a main-level app must be created, this is the usual place to
+ # if a main-level app must be created, this (and
+ # new_figure_manager_given_figure) is the usual place to
# do it -- see backend_wx, backend_wxagg and backend_tkagg for
# examples. Not all GUIs require explicit instantiation of a
# main-level app (egg backend_gtk, backend_gtkagg) for pylab
FigureClass = kwargs.pop('FigureClass', Figure)
thisFig = FigureClass(*args, **kwargs)
- canvas = FigureCanvasTemplate(thisFig)
+ return new_figure_manager_given_figure(num, thisFig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ canvas = FigureCanvasTemplate(figure)
manager = FigureManagerTemplate(canvas, num)
return manager
View
9 lib/matplotlib/backends/backend_tkagg.py
@@ -74,9 +74,16 @@ def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
"""
- _focus = windowing.FocusManager()
FigureClass = kwargs.pop('FigureClass', Figure)
figure = FigureClass(*args, **kwargs)
+ return new_figure_manager_given_figure(num, figure)
+
+
+def new_figure_manager_given_figure(num, figure):
@pelson Collaborator
pelson added a note

Did anyone have any better ways of doing this rather than splitting up of new_figure_manager (which I still need to do for all of the backends)? It seems like a pattern that would pop up frequently if one were producing Figures without the plt.figure function, so I am surprised I couldn't find anything to "attach" a pre-created figure to the Gcf (and let it take care of making the manager for me).

It seems that new_figure_manager (and the corresponding new_figure_manager_given_figure) is nearly identical in all the backends. Wouldn't it be better to extend the __init__() of FigureManager, so that it may be inherited?

@pelson Collaborator
pelson added a note

@akhmerov: Your suggestion sounds great, but sadly I don't agree that they are nearly identical in all the backends (I looked at qt4agg, wxagg and tkagg after reading your comment). That is not to say your inheritance suggestion does not deserve merit, it would be nicer to have new_figure_manager be a class method on a FigureManager class, but I'm not sure the benefit outweighs the cost of breaking backwards compatibility (it would be possible to put an alias to the class method in place of the current function, but that would mean there were two ways of doing the same thing...).

@pelson: It is not entirely obvious that the backends qt4agg, wxagg and tkagg are so different. qt4 differs from tk by just classes it calls. In wx the difference is that there's a FigureFrame object, which has to be passed to a figure manager. Now __init__ of FigureFrame does what your new_figure_manager_given_figure does, and it also creates a figure manager. However at the same time, both __init__ of FigureManagerWx and FigureFrameWx require almost identical information, so it doesn't seem so crazy if e.g. FigureManagerWx initialized FigureFrameWx. This would also make sense given that FigureManager is the central object in other backends. Overall: I realize that for keeping backwards compatibility one would need to use an alias, and that it is not immediately obvious how to implement the proper behaviour, however it seems that the current state of backends is a mixture of OO code with weird naming scheme (why does one need backends.tkagg.FigureManagerTkAgg instead of backends.tkagg.FigureManager?), and copy-paste-modify non-OO code. This is a pain to interface to: for example IPython needed to hack into the code, to implement their own backend. I think that temporary existence of two ways of doing the same thing is a relatively low cost for having one of these ways clean.

@pelson Collaborator
pelson added a note

I can see you have a real passion to sort this out @akhmerov :-) . Would you be willing to submit a pull request against my branch (or even mpl/master) with some of your suggested changes and we can talk it through in-line? I do not propose you modify all backends at this point, perhaps just the 3 we have discussed.

Yeah, indeed I have a real passion, only limited time. Is there any time after which it would become too late? Is it ok if it takes me around a month?

@pelson Collaborator
pelson added a note

Is it ok if it takes me around a month?

To base it on this branch that would be too long. There is a freeze planned for the 20th of August and I would like to get this branch in in the next week or so.

Having said this, the change I am proposing here is additive and not a major rework, so I am comfortable in making this change even with the knowledge that a better (larger) rework is on the cards in the future.

@akhmerov
akhmerov added a note

Ok, I guess then I'll just do this independently and probably after the freeze.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ _focus = windowing.FocusManager()
window = Tk.Tk()
if Tk.TkVersion >= 8.5:
View
9 lib/matplotlib/backends/backend_wx.py
@@ -1456,6 +1456,14 @@ def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
fig = FigureClass(*args, **kwargs)
+ return new_figure_manager_given_figure(num, fig)
+
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ fig = figure
frame = FigureFrameWx(num, fig)
figmgr = frame.get_figure_manager()
if matplotlib.is_interactive():
@@ -1463,6 +1471,7 @@ def new_figure_manager(num, *args, **kwargs):
return figmgr
+
class FigureFrameWx(wx.Frame):
def __init__(self, num, fig):
# On non-Windows platform, explicitly set the position - fix
View
10 lib/matplotlib/backends/backend_wxagg.py
@@ -121,14 +121,20 @@ def new_figure_manager(num, *args, **kwargs):
FigureClass = kwargs.pop('FigureClass', Figure)
fig = FigureClass(*args, **kwargs)
- frame = FigureFrameWxAgg(num, fig)
+
+ return new_figure_manager_given_figure(num, fig)
+
+def new_figure_manager_given_figure(num, figure):
+ """
+ Create a new figure manager instance for the given figure.
+ """
+ frame = FigureFrameWxAgg(num, figure)
figmgr = frame.get_figure_manager()
if matplotlib.is_interactive():
figmgr.frame.Show()
return figmgr
-
#
# agg/wxPython image conversion functions (wxPython >= 2.8)
#
View
42 lib/matplotlib/cbook.py
@@ -266,6 +266,15 @@ def __init__(self, *args):
self._cid = 0
self._func_cid_map = {}
+ def __getstate__(self):
+ # We cannot currently pickle the callables in the registry, so
+ # return an empty dictionary.
+ return {}
+
+ def __setstate__(self, state):
+ # re-initialise an empty callback registry
+ self.__init__()
+
def connect(self, s, func):
"""
register *func* to be called when a signal *s* is generated
@@ -375,7 +384,7 @@ class silent_list(list):
"""
override repr when returning a list of matplotlib artists to
prevent long, meaningless output. This is meant to be used for a
- homogeneous list of a give type
+ homogeneous list of a given type
"""
def __init__(self, type, seq=None):
self.type = type
@@ -385,7 +394,15 @@ def __repr__(self):
return '<a list of %d %s objects>' % (len(self), self.type)
def __str__(self):
- return '<a list of %d %s objects>' % (len(self), self.type)
+ return repr(self)
+
+ def __getstate__(self):
+ # store a dictionary of this SilentList's state
+ return {'type': self.type, 'seq': self[:]}
+
+ def __setstate__(self, state):
+ self.type = state['type']
+ self.extend(state['seq'])
def strip_math(s):
'remove latex formatting from mathtext'
@@ -1879,6 +1896,27 @@ def is_math_text(s):
return even_dollars
+
+class _NestedClassGetter(object):
+ # recipe from http://stackoverflow.com/a/11493777/741316
+ """
+ When called with the containing class as the first argument,
+ and the name of the nested class as the second argument,
+ returns an instance of the nested class.
+ """
+ def __call__(self, containing_class, class_name):
+ nested_class = getattr(containing_class, class_name)
+
+ # make an instance of a simple object (this one will do), for which we
+ # can change the __class__ later on.
+ nested_instance = _NestedClassGetter()
+
+ # set the class of the instance, the __init__ will never be called on
+ # the class but the original state will be set later on by pickle.
+ nested_instance.__class__ = nested_class
+ return nested_instance
+
+
# Numpy > 1.6.x deprecates putmask in favor of the new copyto.
# So long as we support versions 1.6.x and less, we need the
# following local version of putmask. We choose to make a
View
19 lib/matplotlib/colorbar.py
@@ -185,6 +185,12 @@
docstring.interpd.update(colorbar_doc=colorbar_doc)
+def _set_ticks_on_axis_warn(*args, **kw):
+ # a top level function which gets put in at the axes'
+ # set_xticks set_yticks by _patch_ax
+ warnings.warn("Use the colorbar set_ticks() method instead.")
+
+
class ColorbarBase(cm.ScalarMappable):
'''
Draw a colorbar in an existing axes.
@@ -277,7 +283,7 @@ def __init__(self, ax, cmap=None,
# The rest is in a method so we can recalculate when clim changes.
self.config_axis()
self.draw_all()
-
+
def _extend_lower(self):
"""Returns whether the lower limit is open ended."""
return self.extend in ('both', 'min')
@@ -285,13 +291,12 @@ def _extend_lower(self):
def _extend_upper(self):
"""Returns whether the uper limit is open ended."""
return self.extend in ('both', 'max')
-
+
def _patch_ax(self):
- def _warn(*args, **kw):
- warnings.warn("Use the colorbar set_ticks() method instead.")
-
- self.ax.set_xticks = _warn
- self.ax.set_yticks = _warn
+ # bind some methods to the axes to warn users
+ # against using those methods.
+ self.ax.set_xticks = _set_ticks_on_axis_warn
+ self.ax.set_yticks = _set_ticks_on_axis_warn
def draw_all(self):
'''
View
7 lib/matplotlib/contour.py
@@ -847,6 +847,13 @@ def __init__(self, ax, *args, **kwargs):
self.collections.append(col)
self.changed() # set the colors
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ # the C object Cntr cannot currently be pickled. This isn't a big issue
+ # as it is not actually used once the contour has been calculated
+ state['Cntr'] = None
+ return state
+
def legend_elements(self, variable_name='x', str_format=str):
"""
Return a list of artist and labels suitable for passing through
View
65 lib/matplotlib/figure.py
@@ -33,6 +33,7 @@
import matplotlib.cbook as cbook
from matplotlib import docstring
+from matplotlib import __version__ as _mpl_version
from operator import itemgetter
import os.path
@@ -1131,11 +1132,73 @@ def _gci(self):
return im
return None
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ # the axobservers cannot currently be pickled.
+ # Additionally, the canvas cannot currently be pickled, but this has
+ # the benefit of meaning that a figure can be detached from one canvas,
+ # and re-attached to another.
+ for attr_to_pop in ('_axobservers', 'show', 'canvas', '_cachedRenderer') :
+ state.pop(attr_to_pop, None)
+
+ # add version information to the state
+ state['__mpl_version__'] = _mpl_version
+
+ # check to see if the figure has a manager and whether it is registered
+ # with pyplot
+ if self.canvas is not None and self.canvas.manager is not None:
+ manager = self.canvas.manager
@dmcdougall Collaborator

Will you need to add a manager to each backend's constructor method? It works well in pyplot, but using the object-oriented interface

import pickle as p
from matplotlib.backends.backend_pdf import FigureCanvasPdf as fc
from matplotlib.figure import Figure

fig = Figure()
can = fc(fig)
ax = fig.add_subplot(1, 1, 1)
ax.plot([1, 2, 3], [1, 2, 3])
fig.savefig('plot.pdf')

fout = open('pickled_fig.pkl', 'wb')
p.dump(fig, fout)
fout.close()

throws an error: AttributeError: 'FigureCanvasPdf' object has no attribute 'manager'

Knowing the manager at construct-time means probing for the current backend, which fits in nicely with #1125.

@dmcdougall Collaborator

Er, I think I meant the Figure's constructor method, not the backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ import matplotlib._pylab_helpers
+ if manager in matplotlib._pylab_helpers.Gcf.figs.viewvalues():
+ state['_restore_to_pylab'] = True
+ return state
+
+ def __setstate__(self, state):
+ version = state.pop('__mpl_version__')
+ restore_to_pylab = state.pop('_restore_to_pylab', False)
+
+ if version != _mpl_version:
+ import warnings
+ warnings.warn("This figure was saved with matplotlib version %s "
+ "and is unlikely to function correctly." %
+ (version, ))
+
+ self.__dict__ = state
+
+ # re-initialise some of the unstored state information
+ self._axobservers = []
+ self.canvas = None
+
+ if restore_to_pylab:
+ # lazy import to avoid circularity
+ import matplotlib.pyplot as plt
+ import matplotlib._pylab_helpers as pylab_helpers
+ allnums = plt.get_fignums()
+ num = max(allnums) + 1 if allnums else 1
+ mgr = plt._backend_mod.new_figure_manager_given_figure(num, self)
+
+ # XXX The following is a copy and paste from pyplot. Consider
+ # factoring to pylab_helpers
+
+ if self.get_label():
+ mgr.set_window_title(self.get_label())
+
+ # make this figure current on button press event
+ def make_active(event):
+ pylab_helpers.Gcf.set_active(mgr)
+
+ mgr._cidgcf = mgr.canvas.mpl_connect('button_press_event',
+ make_active)
+
+ pylab_helpers.Gcf.set_active(mgr)
+ self.number = num
+
+ plt.draw_if_interactive()
+
def add_axobserver(self, func):
'whenever the axes state change, ``func(self)`` will be called'
self._axobservers.append(func)
-
def savefig(self, *args, **kwargs):
"""
Save the current figure.
View
10 lib/matplotlib/markers.py
@@ -113,6 +113,16 @@ def __init__(self, marker=None, fillstyle='full'):
self.set_marker(marker)
self.set_fillstyle(fillstyle)
+ def __getstate__(self):
+ d = self.__dict__.copy()
+ d.pop('_marker_function')
+ return d
+
+ def __setstate__(self, statedict):
+ self.__dict__ = statedict
+ self.set_marker(self._marker)
+ self._recache()
+
def _recache(self):
self._path = Path(np.empty((0,2)))
self._transform = IdentityTransform()
View
48 lib/matplotlib/patches.py
@@ -1617,7 +1617,6 @@ def pprint_styles(klass):
"""
return _pprint_styles(klass._style_list)
-
@classmethod
def register(klass, name, style):
"""
@@ -1687,9 +1686,6 @@ def __init__(self):
"""
super(BoxStyle._Base, self).__init__()
-
-
-
def transmute(self, x0, y0, width, height, mutation_size):
"""
The transmute method is a very core of the
@@ -1701,8 +1697,6 @@ def transmute(self, x0, y0, width, height, mutation_size):
"""
raise NotImplementedError('Derived must override')
-
-
def __call__(self, x0, y0, width, height, mutation_size,
aspect_ratio=1.):
"""
@@ -1728,7 +1722,15 @@ def __call__(self, x0, y0, width, height, mutation_size,
else:
return self.transmute(x0, y0, width, height, mutation_size)
-
+ def __reduce__(self):
+ # because we have decided to nest thes classes, we need to
+ # add some more information to allow instance pickling.
+ import matplotlib.cbook as cbook
+ return (cbook._NestedClassGetter(),
+ (BoxStyle, self.__class__.__name__),
+ self.__dict__
+ )
+
class Square(_Base):
"""
@@ -2296,9 +2298,6 @@ def get_bbox(self):
return transforms.Bbox.from_bounds(self._x, self._y, self._width, self._height)
-
-
-
from matplotlib.bezier import split_bezier_intersecting_with_closedpath
from matplotlib.bezier import get_intersection, inside_circle, get_parallels
from matplotlib.bezier import make_wedged_bezier2
@@ -2359,7 +2358,7 @@ class _Base(object):
points. This base class defines a __call__ method, and few
helper methods.
"""
-
+
class SimpleEvent:
def __init__(self, xy):
self.x, self.y = xy
@@ -2401,7 +2400,6 @@ def insideB(xy_display):
return path
-
def _shrink(self, path, shrinkA, shrinkB):
"""
Shrink the path by fixed size (in points) with shrinkA and shrinkB
@@ -2441,6 +2439,15 @@ def __call__(self, posA, posB,
shrinked_path = self._shrink(clipped_path, shrinkA, shrinkB)
return shrinked_path
+
+ def __reduce__(self):
+ # because we have decided to nest thes classes, we need to
+ # add some more information to allow instance pickling.
+ import matplotlib.cbook as cbook
+ return (cbook._NestedClassGetter(),
+ (ConnectionStyle, self.__class__.__name__),
+ self.__dict__
+ )
class Arc3(_Base):
@@ -2771,7 +2778,6 @@ def connect(self, posA, posB):
{"AvailableConnectorstyles": _pprint_styles(_style_list)}
-
class ArrowStyle(_Style):
"""
:class:`ArrowStyle` is a container class which defines several
@@ -2867,8 +2873,6 @@ class and must be overriden in the subclasses. It receives
raise NotImplementedError('Derived must override')
-
-
def __call__(self, path, mutation_size, linewidth,
aspect_ratio=1.):
"""
@@ -2901,7 +2905,15 @@ def __call__(self, path, mutation_size, linewidth,
return path_mutated, fillable
else:
return self.transmute(path, mutation_size, linewidth)
-
+
+ def __reduce__(self):
+ # because we have decided to nest thes classes, we need to
+ # add some more information to allow instance pickling.
+ import matplotlib.cbook as cbook
+ return (cbook._NestedClassGetter(),
+ (ArrowStyle, self.__class__.__name__),
+ self.__dict__
+ )
class _Curve(_Base):
@@ -3048,7 +3060,6 @@ def __init__(self):
_style_list["-"] = Curve
-
class CurveA(_Curve):
"""
An arrow with a head at its begin point.
@@ -3087,7 +3098,6 @@ def __init__(self, head_length=.4, head_width=.2):
beginarrow=False, endarrow=True,
head_length=head_length, head_width=head_width )
- #_style_list["->"] = CurveB
_style_list["->"] = CurveB
@@ -3109,11 +3119,9 @@ def __init__(self, head_length=.4, head_width=.2):
beginarrow=True, endarrow=True,
head_length=head_length, head_width=head_width )
- #_style_list["<->"] = CurveAB
_style_list["<->"] = CurveAB
-
class CurveFilledA(_Curve):
"""
An arrow with filled triangle head at the begin.
View
364 lib/matplotlib/projections/polar.py
@@ -18,206 +18,211 @@
ScaledTranslation, blended_transform_factory, BboxTransformToMaxOnly
import matplotlib.spines as mspines
-class PolarAxes(Axes):
- """
- A polar graph projection, where the input dimensions are *theta*, *r*.
- Theta starts pointing east and goes anti-clockwise.
+class PolarTransform(Transform):
"""
- name = 'polar'
-
- class PolarTransform(Transform):
- """
- The base polar transform. This handles projection *theta* and
- *r* into Cartesian coordinate space *x* and *y*, but does not
- perform the ultimate affine transformation into the correct
- position.
- """
- input_dims = 2
- output_dims = 2
- is_separable = False
-
- def __init__(self, axis=None, use_rmin=True):
- Transform.__init__(self)
- self._axis = axis
- self._use_rmin = use_rmin
-
- def transform(self, tr):
- xy = np.empty(tr.shape, np.float_)
- if self._axis is not None:
- if self._use_rmin:
- rmin = self._axis.viewLim.ymin
- else:
- rmin = 0
- theta_offset = self._axis.get_theta_offset()
- theta_direction = self._axis.get_theta_direction()
+ The base polar transform. This handles projection *theta* and
+ *r* into Cartesian coordinate space *x* and *y*, but does not
+ perform the ultimate affine transformation into the correct
+ position.
+ """
+ input_dims = 2
+ output_dims = 2
+ is_separable = False
+
+ def __init__(self, axis=None, use_rmin=True):
+ Transform.__init__(self)
+ self._axis = axis
+ self._use_rmin = use_rmin
+
+ def transform(self, tr):
+ xy = np.empty(tr.shape, np.float_)
+ if self._axis is not None:
+ if self._use_rmin:
+ rmin = self._axis.viewLim.ymin
else:
rmin = 0
- theta_offset = 0
- theta_direction = 1
-
- t = tr[:, 0:1]
- r = tr[:, 1:2]
- x = xy[:, 0:1]
- y = xy[:, 1:2]
-
- t *= theta_direction
- t += theta_offset
-
- if rmin != 0:
- r = r - rmin
- mask = r < 0
- x[:] = np.where(mask, np.nan, r * np.cos(t))
- y[:] = np.where(mask, np.nan, r * np.sin(t))
- else:
- x[:] = r * np.cos(t)
- y[:] = r * np.sin(t)
+ theta_offset = self._axis.get_theta_offset()
+ theta_direction = self._axis.get_theta_direction()
+ else:
+ rmin = 0
+ theta_offset = 0
+ theta_direction = 1
+
+ t = tr[:, 0:1]
+ r = tr[:, 1:2]
+ x = xy[:, 0:1]
+ y = xy[:, 1:2]
+
+ t *= theta_direction
+ t += theta_offset
+
+ if rmin != 0:
+ r = r - rmin
+ mask = r < 0
+ x[:] = np.where(mask, np.nan, r * np.cos(t))
+ y[:] = np.where(mask, np.nan, r * np.sin(t))
+ else:
+ x[:] = r * np.cos(t)
+ y[:] = r * np.sin(t)
- return xy
- transform.__doc__ = Transform.transform.__doc__
+ return xy
+ transform.__doc__ = Transform.transform.__doc__
- transform_non_affine = transform
- transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
+ transform_non_affine = transform
+ transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__
- def transform_path(self, path):
- vertices = path.vertices
- if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]:
- return Path(self.transform(vertices), path.codes)
- ipath = path.interpolated(path._interpolation_steps)
- return Path(self.transform(ipath.vertices), ipath.codes)
- transform_path.__doc__ = Transform.transform_path.__doc__
+ def transform_path(self, path):
+ vertices = path.vertices
+ if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]:
+ return Path(self.transform(vertices), path.codes)
+ ipath = path.interpolated(path._interpolation_steps)
+ return Path(self.transform(ipath.vertices), ipath.codes)
+ transform_path.__doc__ = Transform.transform_path.__doc__
- transform_path_non_affine = transform_path
- transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
+ transform_path_non_affine = transform_path
+ transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__
- def inverted(self):
- return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin)
- inverted.__doc__ = Transform.inverted.__doc__
+ def inverted(self):
+ return PolarAxes.InvertedPolarTransform(self._axis, self._use_rmin)
+ inverted.__doc__ = Transform.inverted.__doc__
- class PolarAffine(Affine2DBase):
- """
- The affine part of the polar projection. Scales the output so
- that maximum radius rests on the edge of the axes circle.
- """
- def __init__(self, scale_transform, limits):
- """
- *limits* is the view limit of the data. The only part of
- its bounds that is used is ymax (for the radius maximum).
- The theta range is always fixed to (0, 2pi).
- """
- Affine2DBase.__init__(self)
- self._scale_transform = scale_transform
- self._limits = limits
- self.set_children(scale_transform, limits)
- self._mtx = None
-
- def get_matrix(self):
- if self._invalid:
- limits_scaled = self._limits.transformed(self._scale_transform)
- yscale = limits_scaled.ymax - limits_scaled.ymin
- affine = Affine2D() \
- .scale(0.5 / yscale) \
- .translate(0.5, 0.5)
- self._mtx = affine.get_matrix()
- self._inverted = None
- self._invalid = 0
- return self._mtx
- get_matrix.__doc__ = Affine2DBase.get_matrix.__doc__
-
- class InvertedPolarTransform(Transform):
- """
- The inverse of the polar transform, mapping Cartesian
- coordinate space *x* and *y* back to *theta* and *r*.
- """
- input_dims = 2
- output_dims = 2
- is_separable = False
-
- def __init__(self, axis=None, use_rmin=True):
- Transform.__init__(self)
- self._axis = axis
- self._use_rmin = use_rmin
-
- def transform(self, xy):
- if self._axis is not None:
- if self._use_rmin:
- rmin = self._axis.viewLim.ymin
- else:
- rmin = 0
- theta_offset = self._axis.get_theta_offset()
- theta_direction = self._axis.get_theta_direction()
+
+class PolarAffine(Affine2DBase):
+ """
+ The affine part of the polar projection. Scales the output so
+ that maximum radius rests on the edge of the axes circle.
+ """
+ def __init__(self, scale_transform, limits):
+ """
+ *limits* is the view limit of the data. The only part of
+ its bounds that is used is ymax (for the radius maximum).
+ The theta range is always fixed to (0, 2pi).
+ """
+ Affine2DBase.__init__(self)
+ self._scale_transform = scale_transform
+ self._limits = limits
+ self.set_children(scale_transform, limits)
+ self._mtx = None
+
+ def get_matrix(self):
+ if self._invalid:
+ limits_scaled = self._limits.transformed(self._scale_transform)
+ yscale = limits_scaled.ymax - limits_scaled.ymin
+ affine = Affine2D() \
+ .scale(0.5 / yscale) \
+ .translate(0.5, 0.5)
+ self._mtx = affine.get_matrix()
+ self._inverted = None
+ self._invalid = 0
+ return self._mtx
+ get_matrix.__doc__ = Affine2DBase.get_matrix.__doc__
+
+
+class InvertedPolarTransform(Transform):
+ """
+ The inverse of the polar transform, mapping Cartesian
+ coordinate space *x* and *y* back to *theta* and *r*.
+ """
+ input_dims = 2
+ output_dims = 2
+ is_separable = False
+
+ def __init__(self, axis=None, use_rmin=True):
+ Transform.__init__(self)
+ self._axis = axis
+ self._use_rmin = use_rmin
+
+ def transform(self, xy):
+ if self._axis is not None:
+ if self._use_rmin:
+ rmin = self._axis.viewLim.ymin
else:
rmin = 0
- theta_offset = 0
- theta_direction = 1
+ theta_offset = self._axis.get_theta_offset()
+ theta_direction = self._axis.get_theta_direction()
+ else:
+ rmin = 0
+ theta_offset = 0
+ theta_direction = 1
- x = xy[:, 0:1]
- y = xy[:, 1:]
- r = np.sqrt(x*x + y*y)
- theta = np.arccos(x / r)
- theta = np.where(y < 0, 2 * np.pi - theta, theta)
+ x = xy[:, 0:1]
+ y = xy[:, 1:]
+ r = np.sqrt(x*x + y*y)
+ theta = np.arccos(x / r)
+ theta = np.where(y < 0, 2 * np.pi - theta, theta)
- theta -= theta_offset
- theta *= theta_direction
+ theta -= theta_offset
+ theta *= theta_direction
- r += rmin
+ r += rmin
- return np.concatenate((theta, r), 1)
- transform.__doc__ = Transform.transform.__doc__
+ return np.concatenate((theta, r), 1)
+ transform.__doc__ = Transform.transform.__doc__
- def inverted(self):
- return PolarAxes.PolarTransform(self._axis, self._use_rmin)
- inverted.__doc__ = Transform.inverted.__doc__
+ def inverted(self):
+ return PolarAxes.PolarTransform(self._axis, self._use_rmin)
+ inverted.__doc__ = Transform.inverted.__doc__
- class ThetaFormatter(Formatter):
- """
- Used to format the *theta* tick labels. Converts the native
- unit of radians into degrees and adds a degree symbol.
- """
- def __call__(self, x, pos=None):
- # \u00b0 : degree symbol
- if rcParams['text.usetex'] and not rcParams['text.latex.unicode']:
- return r"$%0.0f^\circ$" % ((x / np.pi) * 180.0)
- else:
- # we use unicode, rather than mathtext with \circ, so
- # that it will work correctly with any arbitrary font
- # (assuming it has a degree sign), whereas $5\circ$
- # will only work correctly with one of the supported
- # math fonts (Computer Modern and STIX)
- return u"%0.0f\u00b0" % ((x / np.pi) * 180.0)
-
- class RadialLocator(Locator):
- """
- Used to locate radius ticks.
- Ensures that all ticks are strictly positive. For all other
- tasks, it delegates to the base
- :class:`~matplotlib.ticker.Locator` (which may be different
- depending on the scale of the *r*-axis.
- """
- def __init__(self, base):
- self.base = base
+class ThetaFormatter(Formatter):
+ """
+ Used to format the *theta* tick labels. Converts the native
+ unit of radians into degrees and adds a degree symbol.
+ """
+ def __call__(self, x, pos=None):
+ # \u00b0 : degree symbol
+ if rcParams['text.usetex'] and not rcParams['text.latex.unicode']:
+ return r"$%0.0f^\circ$" % ((x / np.pi) * 180.0)
+ else:
+ # we use unicode, rather than mathtext with \circ, so
+ # that it will work correctly with any arbitrary font
+ # (assuming it has a degree sign), whereas $5\circ$
+ # will only work correctly with one of the supported
+ # math fonts (Computer Modern and STIX)
+ return u"%0.0f\u00b0" % ((x / np.pi) * 180.0)
+
+
+class RadialLocator(Locator):
+ """
+ Used to locate radius ticks.
+
+ Ensures that all ticks are strictly positive. For all other
+ tasks, it delegates to the base
+ :class:`~matplotlib.ticker.Locator` (which may be different
+ depending on the scale of the *r*-axis.
+ """
+ def __init__(self, base):
+ self.base = base
- def __call__(self):
- ticks = self.base()
- return [x for x in ticks if x > 0]
+ def __call__(self):
+ ticks = self.base()
+ return [x for x in ticks if x > 0]
- def autoscale(self):
- return self.base.autoscale()
+ def autoscale(self):
+ return self.base.autoscale()
- def pan(self, numsteps):
- return self.base.pan(numsteps)
+ def pan(self, numsteps):
+ return self.base.pan(numsteps)
- def zoom(self, direction):
- return self.base.zoom(direction)
+ def zoom(self, direction):
+ return self.base.zoom(direction)
- def refresh(self):
- return self.base.refresh()
+ def refresh(self):
+ return self.base.refresh()
- def view_limits(self, vmin, vmax):
- vmin, vmax = self.base.view_limits(vmin, vmax)
- return 0, vmax
+ def view_limits(self, vmin, vmax):
+ vmin, vmax = self.base.view_limits(vmin, vmax)
+ return 0, vmax
+
+class PolarAxes(Axes):
+ """
+ A polar graph projection, where the input dimensions are *theta*, *r*.
+
+ Theta starts pointing east and goes anti-clockwise.
+ """
+ name = 'polar'
def __init__(self, *args, **kwargs):
"""
@@ -653,6 +658,17 @@ def drag_pan(self, button, key, x, y):
scale = r / startr
self.set_rmax(p.rmax / scale)
+
+# to keep things all self contained, we can put aliases to the Polar classes
+# defined above. This isn't strictly necessary, but it makes some of the
+# code more readable (and provides a backwards compatible Polar API)
+PolarAxes.PolarTransform = PolarTransform
+PolarAxes.PolarAffine = PolarAffine
+PolarAxes.InvertedPolarTransform = InvertedPolarTransform
+PolarAxes.ThetaFormatter = ThetaFormatter
+PolarAxes.RadialLocator = RadialLocator
+
+
# These are a couple of aborted attempts to project a polar plot using
# cubic bezier curves.
View
19 lib/matplotlib/pyplot.py
@@ -94,7 +94,7 @@ def _backend_selection():
## Global ##
from matplotlib.backends import pylab_setup
-new_figure_manager, draw_if_interactive, _show = pylab_setup()
+_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup()
@docstring.copy_dedent(Artist.findobj)
def findobj(o=None, match=None):
@@ -102,6 +102,7 @@ def findobj(o=None, match=None):
o = gcf()
return o.findobj(match)
+
def switch_backend(newbackend):
"""
Switch the default backend. This feature is **experimental**, and
@@ -115,10 +116,10 @@ def switch_backend(newbackend):
Calling this command will close all open windows.
"""
close('all')
- global new_figure_manager, draw_if_interactive, _show
+ global _backend_mod, new_figure_manager, draw_if_interactive, _show
matplotlib.use(newbackend, warn=False, force=True)
from matplotlib.backends import pylab_setup
- new_figure_manager, draw_if_interactive, _show = pylab_setup()
+ _backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup()
def show(*args, **kw):
@@ -312,22 +313,17 @@ class that will be passed on to :meth:`new_figure_manager` in the
if edgecolor is None : edgecolor = rcParams['figure.edgecolor']
allnums = get_fignums()
+ next_num = max(allnums) + 1 if allnums else 1
figLabel = ''
if num is None:
- if allnums:
- num = max(allnums) + 1
- else:
- num = 1
+ num = next_num
elif is_string_like(num):
figLabel = num
allLabels = get_figlabels()
if figLabel not in allLabels:
if figLabel == 'all':
warnings.warn("close('all') closes all existing figures")
- if len(allLabels):
- num = max(allnums) + 1
- else:
- num = 1
+ num = next_num
else:
inum = allLabels.index(figLabel)
num = allnums[inum]
@@ -363,6 +359,7 @@ def make_active(event):
draw_if_interactive()
return figManager.canvas.figure
+
def gcf():
"Return a reference to the current figure."
View
BIN  lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
157 lib/matplotlib/tests/test_pickle.py
@@ -0,0 +1,157 @@
+from __future__ import print_function
+
+import numpy as np
+
+import matplotlib
+matplotlib.use('tkagg')
+
+from matplotlib.testing.decorators import cleanup, image_comparison
+import matplotlib.pyplot as plt
+
+from nose.tools import assert_equal, assert_not_equal
+
+# cpickle is faster, pickle gives better exceptions
+import cPickle as pickle
+import pickle
+
+from cStringIO import StringIO
+
+
+def recursive_pickle(obj, nested_info='top level object', memo=None):
+ """
+ Pickle the object's attributes recursively, storing a memo of the object
+ which have already been pickled.
+
+ If any pickling issues occur, a pickle.Pickle error will be raised with details.
+
+ This is not a completely general purpose routine, but will be useful for
+ debugging some pickle issues. HINT: cPickle is less verbose than Pickle
+
+
+ """
+ if memo is None:
+ memo = {}
+
+ if id(obj) in memo:
+ return
+
+ # put this object in the memo
+ memo[id(obj)] = obj
+
+ # start by pickling all of the object's attributes/contents
+
+ if isinstance(obj, list):
+ for i, item in enumerate(obj):
+ recursive_pickle(item, memo=memo, nested_info='list item #%s in (%s)' % (i, nested_info))
+ else:
+ if isinstance(obj, dict):
+ state = obj
+ elif hasattr(obj, '__getstate__'):
+ state = obj.__getstate__()
+ if not isinstance(state, dict):
+ state = {}
+ elif hasattr(obj, '__dict__'):
+ state = obj.__dict__
+ else:
+ state = {}
+
+ for key, value in state.iteritems():
+ recursive_pickle(value, memo=memo, nested_info='attribute "%s" in (%s)' % (key, nested_info))
+
+# print(id(obj), type(obj), nested_info)
+
+ # finally, try picking the object itself
+ try:
+ pickle.dump(obj, StringIO())#, pickle.HIGHEST_PROTOCOL)
+ except (pickle.PickleError, AssertionError), err:
+ print(pickle.PickleError('Pickling failed with nested info: [(%s) %s].'
+ '\nException: %s' % (type(obj),
+ nested_info,
+ err)))
+ # re-raise the exception for full traceback
+ raise
+
+
+@cleanup
+def test_simple():
+ fig = plt.figure()
+ # un-comment to debug
+ recursive_pickle(fig, 'figure')
+ pickle.dump(fig, StringIO(), pickle.HIGHEST_PROTOCOL)
+
+ ax = plt.subplot(121)
+# recursive_pickle(ax, 'ax')
+ pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL)
+
+ ax = plt.axes(projection='polar')
+# recursive_pickle(ax, 'ax')
+ pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL)
+
+# ax = plt.subplot(121, projection='hammer')
+# recursive_pickle(ax, 'figure')
+# pickle.dump(ax, StringIO(), pickle.HIGHEST_PROTOCOL)
+
+
+@image_comparison(baseline_images=['multi_pickle'],
+ extensions=['png'])
+def test_complete():
+ fig = plt.figure('Figure with a label?')
+
+ plt.suptitle('Can you fit any more in a figure?')
+
+ # make some arbitrary data
+ x, y = np.arange(8), np.arange(10)
+ data = u = v = np.linspace(0, 10, 80).reshape(10, 8)
+ v = np.sin(v * -0.6)
+
+ plt.subplot(3,3,1)
+ plt.plot(range(10))
+
+ plt.subplot(3, 3, 2)
+ plt.contourf(data, hatches=['//', 'ooo'])
+# plt.colorbar() # sadly, colorbar is currently failing. This might be an easy fix once
+ # its been identified what the problem is. (lambda functions in colorbar)
+
+ plt.subplot(3, 3, 3)
+ plt.pcolormesh(data)
+# cb = plt.colorbar()
+
+ plt.subplot(3, 3, 4)
+ plt.imshow(data)
+
+ plt.subplot(3, 3, 5)
+ plt.pcolor(data)
+
+ plt.subplot(3, 3, 6)
+ plt.streamplot(x, y, u, v)
+
+ plt.subplot(3, 3, 7)
+ plt.quiver(x, y, u, v)
+
+ plt.subplot(3, 3, 8)
+ plt.scatter(x, x**2, label='$x^2$')
+# plt.legend()
+
+ plt.subplot(3, 3, 9)
+ plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4)
+
+
+ result_fh = StringIO()
+# recursive_pickle(fig, 'figure')