Skip to content

Commit 6d79e6e

Browse files
committed
Fix interaction with unpickled 3d plots.
In order to reset the mouse interaction callbacks on unpickled 3d plots, the callback registry really needs to be on the Figure object rather than the Canvas, because the canvas doesn't exist yet when the 3d axes is being unpickled (it is only set on the figure at the very end of unpickling). So move the callback registry to the figure (with a proxy property on the canvas). Then, add a private mechanism to pickle select callbacks, and combine everything together. Test with e.g. ``` import matplotlib.pyplot as plt import pickle fig = plt.figure() fig.add_subplot(111, projection='3d') p = pickle.dumps(fig) plt.close("all") pickle.loads(p) plt.show() ```
1 parent 74d6145 commit 6d79e6e

File tree

4 files changed

+49
-15
lines changed

4 files changed

+49
-15
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,8 +1718,6 @@ def __init__(self, figure):
17181718
figure.set_canvas(self)
17191719
self.figure = figure
17201720
self.manager = None
1721-
# a dictionary from event name to a dictionary that maps cid->func
1722-
self.callbacks = cbook.CallbackRegistry()
17231721
self.widgetlock = widgets.LockDraw()
17241722
self._button = None # the button pressed
17251723
self._key = None # the key pressed
@@ -1730,6 +1728,10 @@ def __init__(self, figure):
17301728
self.toolbar = None # NavigationToolbar2 will set me
17311729
self._is_idle_drawing = False
17321730

1731+
@property
1732+
def callbacks(self):
1733+
return self.figure._canvas_callbacks
1734+
17331735
@classmethod
17341736
@functools.lru_cache()
17351737
def _fix_ipython_backend2gui(cls):

lib/matplotlib/cbook/__init__.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ def __hash__(self):
104104
return hash(self._obj)
105105

106106

107+
def _weak_or_strong_ref(func, callback):
108+
"""
109+
Return a `WeakMethod` wrapping *func* if possible, else a `_StrongRef`.
110+
"""
111+
try:
112+
return weakref.WeakMethod(func, callback)
113+
except TypeError:
114+
return _StrongRef(func)
115+
116+
107117
class CallbackRegistry:
108118
"""
109119
Handle registering and disconnecting for a set of signals and callbacks:
@@ -163,21 +173,37 @@ def __init__(self, exception_handler=_exception_printer):
163173
self.callbacks = {}
164174
self._cid_gen = itertools.count()
165175
self._func_cid_map = {}
176+
# A hidden variable that marks cids that need to be pickled.
177+
self._pickled_cids = set()
166178

167179
def __getstate__(self):
168-
# In general, callbacks may not be pickled, so we just drop them.
169-
return {**vars(self), "callbacks": {}, "_func_cid_map": {}}
180+
return {
181+
**vars(self),
182+
# In general, callbacks may not be pickled, so we just drop them,
183+
# unless directed otherwise by self._pickled_cids.
184+
"callbacks": {s: {cid: proxy() for cid, proxy in d.items()
185+
if cid in self._pickled_cids}
186+
for s, d in self.callbacks.items()},
187+
# It is simpler to reconstruct this from callbacks in __setstate__.
188+
"_func_cid_map": None,
189+
}
190+
191+
def __setstate__(self, state):
192+
vars(self).update(state)
193+
self.callbacks = {
194+
s: {cid: _weak_or_strong_ref(func, self._remove_proxy)
195+
for cid, func in d.items()}
196+
for s, d in self.callbacks.items()}
197+
self._func_cid_map = {
198+
s: {proxy: cid for cid, proxy in d.items()}
199+
for s, d in self.callbacks.items()}
170200

171201
def connect(self, s, func):
172202
"""Register *func* to be called when signal *s* is generated."""
173203
self._func_cid_map.setdefault(s, {})
174-
try:
175-
proxy = weakref.WeakMethod(func, self._remove_proxy)
176-
except TypeError:
177-
proxy = _StrongRef(func)
204+
proxy = _weak_or_strong_ref(func, self._remove_proxy)
178205
if proxy in self._func_cid_map[s]:
179206
return self._func_cid_map[s][proxy]
180-
181207
cid = next(self._cid_gen)
182208
self._func_cid_map[s][proxy] = cid
183209
self.callbacks.setdefault(s, {})

lib/matplotlib/figure.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2280,6 +2280,10 @@ def __init__(self,
22802280
super().__init__()
22812281

22822282
self.callbacks = cbook.CallbackRegistry()
2283+
# Callbacks traditionally associated with the canvas (and exposed with
2284+
# a proxy property), but that actually need to be on the figure for
2285+
# pickling.
2286+
self._canvas_callbacks = cbook.CallbackRegistry()
22832287

22842288
if figsize is None:
22852289
figsize = mpl.rcParams['figure.figsize']

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,14 @@ def __init__(
118118
self._zcid = None
119119

120120
self.mouse_init()
121-
self.figure.canvas.mpl_connect(
122-
'motion_notify_event', self._on_move),
123-
self.figure.canvas.mpl_connect(
124-
'button_press_event', self._button_press),
125-
self.figure.canvas.mpl_connect(
126-
'button_release_event', self._button_release),
121+
self.figure.canvas.callbacks._pickled_cids.update({
122+
self.figure.canvas.mpl_connect(
123+
'motion_notify_event', self._on_move),
124+
self.figure.canvas.mpl_connect(
125+
'button_press_event', self._button_press),
126+
self.figure.canvas.mpl_connect(
127+
'button_release_event', self._button_release),
128+
})
127129
self.set_top_view()
128130

129131
self.patch.set_linewidth(0)

0 commit comments

Comments
 (0)