-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy path__init__.py
363 lines (280 loc) · 12.2 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
"""
Prototype project for new Matplotlib GUI management.
The pyplot module current serves two critical, but unrelated functions:
1. provide a state-full implicit API that rhymes / was inspired by MATLAB
2. provide the management of interaction between Matplotlib and the GUI event
loop
This project is prototype for separating the second function from the first.
This will enable users to both only use the explicit API (nee OO interface) and
to have smooth integration with the GUI event loop as with pyplot.
"""
from collections import Counter
import functools
import logging
import warnings
import weakref
from matplotlib.backend_bases import FigureCanvasBase as _FigureCanvasBase
from ._figure import Figure # noqa: F401
from ._manage_interactive import ion, ioff, is_interactive # noqa: F401
from ._manage_backend import select_gui_toolkit # noqa: F401
from ._manage_backend import current_backend_module as _cbm
from ._promotion import promote_figure as promote_figure
from ._creation import figure, subplots, subplot_mosaic # noqa: F401
from ._version import get_versions
__version__ = get_versions()["version"]
del get_versions
_log = logging.getLogger(__name__)
def show(figs, *, block=None, timeout=0):
"""
Show the figures and maybe block.
Parameters
----------
figs : List[Figure]
The figures to show. If they do not currently have a GUI aware
canvas + manager attached they will be promoted.
block : bool, optional
Whether to wait for all figures to be closed before returning.
If `True` block and run the GUI main loop until all figure windows
are closed.
If `False` ensure that all figure windows are displayed and return
immediately. In this case, you are responsible for ensuring
that the event loop is running to have responsive figures.
Defaults to True in non-interactive mode and to False in interactive
mode (see `.is_interactive`).
"""
# TODO handle single figure
# call this to ensure a backend is indeed selected
backend = _cbm()
managers = []
for fig in figs:
if fig.canvas.manager is not None:
managers.append(fig.canvas.manager)
else:
managers.append(promote_figure(fig, num=None))
if block is None:
block = not is_interactive()
if block and len(managers):
if timeout == 0:
backend.show_managers(managers=managers, block=block)
elif len(managers):
manager, *_ = managers
manager.canvas.start_event_loop(timeout=timeout)
class FigureRegistry:
"""
A registry to wrap the creation of figures and track them.
This instance will keep a hard reference to created Figures to ensure
that they do not get garbage collected.
Parameters
----------
block : bool, optional
Whether to wait for all figures to be closed before returning from
show_all.
If `True` block and run the GUI main loop until all figure windows
are closed.
If `False` ensure that all figure windows are displayed and return
immediately. In this case, you are responsible for ensuring
that the event loop is running to have responsive figures.
Defaults to True in non-interactive mode and to False in interactive
mode (see `.is_interactive`).
timeout : float, optional
Default time to wait for all of the Figures to be closed if blocking.
If 0 block forever.
"""
def __init__(self, *, block=None, timeout=0, prefix="Figure "):
# settings stashed to set defaults on show
self._timeout = timeout
self._block = block
# the canonical location for storing the Figures this registry owns.
# any additional views must never include a figure that is not a key but
# may omit figures
self._fig_to_number = dict()
# Settings / state to control the default figure label
self._prefix = prefix
@property
def figures(self):
return tuple(self._fig_to_number)
def _register_fig(self, fig):
# if the user closes the figure by any other mechanism, drop our
# reference to it. This is important for getting a "pyplot" like user
# experience
def registry_cleanup(fig_wr):
fig = fig_wr()
if fig is not None:
if fig.canvas is not None:
fig.canvas.mpl_disconnect(cid)
self.close(fig)
fig_wr = weakref.ref(fig)
cid = fig.canvas.mpl_connect("close_event", lambda e: registry_cleanup(fig_wr))
# Make sure we give the figure a quasi-unique label. We will never set
# the same label twice, but will not over-ride any user label (but
# empty string) on a Figure so if they provide duplicate labels, change
# the labels under us, or provide a label that will be shadowed in the
# future it will be what it is.
fignum = max(self._fig_to_number.values(), default=-1) + 1
if fig.get_label() == "":
fig.set_label(f"{self._prefix}{fignum:d}")
self._fig_to_number[fig] = fignum
if is_interactive():
promote_figure(fig, num=fignum)
return fig
@property
def by_label(self):
"""
Return a dictionary of the current mapping labels -> figures.
If there are duplicate labels, newer figures will take precedence.
"""
mapping = {fig.get_label(): fig for fig in self.figures}
if len(mapping) != len(self.figures):
counts = Counter(fig.get_label() for fig in self.figures)
multiples = {k: v for k, v in counts.items() if v > 1}
warnings.warn(
(
f"There are repeated labels ({multiples!r}), but only the newest figure with that label can "
"be returned. "
),
stacklevel=2,
)
return mapping
@property
def by_number(self):
"""
Return a dictionary of the current mapping number -> figures.
"""
self._ensure_all_figures_promoted()
return {fig.canvas.manager.num: fig for fig in self.figures}
@functools.wraps(figure)
def figure(self, *args, **kwargs):
fig = figure(*args, **kwargs)
return self._register_fig(fig)
@functools.wraps(subplots)
def subplots(self, *args, **kwargs):
fig, axs = subplots(*args, **kwargs)
return self._register_fig(fig), axs
@functools.wraps(subplot_mosaic)
def subplot_mosaic(self, *args, **kwargs):
fig, axd = subplot_mosaic(*args, **kwargs)
return self._register_fig(fig), axd
def _ensure_all_figures_promoted(self):
for f in self.figures:
if f.canvas.manager is None:
promote_figure(f, num=self._fig_to_number[f])
def show_all(self, *, block=None, timeout=None):
"""
Show all of the Figures that the FigureRegistry knows about.
Parameters
----------
block : bool, optional
Whether to wait for all figures to be closed before returning from
show_all.
If `True` block and run the GUI main loop until all figure windows
are closed.
If `False` ensure that all figure windows are displayed and return
immediately. In this case, you are responsible for ensuring
that the event loop is running to have responsive figures.
Defaults to the value set on the Registry at init
timeout : float, optional
time to wait for all of the Figures to be closed if blocking.
If 0 block forever.
Defaults to the timeout set on the Registry at init
"""
if block is None:
block = self._block
if timeout is None:
timeout = self._timeout
self._ensure_all_figures_promoted()
show(self.figures, block=self._block, timeout=self._timeout)
# alias to easy pyplot compatibility
show = show_all
def close_all(self):
"""
Close all Figures know to this Registry.
This will do four things:
1. call the ``.destory()`` method on the manager
2. clears the Figure on the canvas instance
3. replace the canvas on each Figure with a new `~matplotlib.backend_bases.FigureCanvasBase` instance
4. drops its hard reference to the Figure
If the user still holds a reference to the Figure it can be revived by
passing it to `show`.
"""
for fig in list(self.figures):
self.close(fig)
def close(self, val):
"""
Close (meaning destroy the UI) and forget a managed Figure.
This will do two things:
- start the destruction process of an UI (the event loop may need to
run to complete this process and if the user is holding hard
references to any of the UI elements they may remain alive).
- Remove the `Figure` from this Registry.
We will no longer have any hard references to the Figure, but if
the user does the `Figure` (and its components) will not be garbage
collected. Due to the circular references in Matplotlib these
objects may not be collected until the full cyclic garbage collection
runs.
If the user still has a reference to the `Figure` they can re-show the
figure via `show`, but the `FigureRegistry` will not be aware of it.
Parameters
----------
val : 'all' or int or str or Figure
- The special case of 'all' closes all open Figures
- If any other string is passed, it is interpreted as a key in
`by_label` and that Figure is closed
- If an integer it is interpreted as a key in `by_number` and that
Figure is closed
- If it is a `Figure` instance, then that figure is closed
"""
if val == "all":
return self.close_all()
# or do we want to close _all_ of the figures with a given label / number?
if isinstance(val, str):
fig = self.by_label[val]
elif isinstance(val, int):
fig = self.by_number[val]
else:
fig = val
if fig not in self.figures:
raise ValueError(
"Trying to close a figure not associated with this Registry."
)
if fig.canvas.manager is not None:
fig.canvas.manager.destroy()
# disconnect figure from canvas
fig.canvas.figure = None
# disconnect canvas from figure
_FigureCanvasBase(figure=fig)
assert fig.canvas.manager is None
self._fig_to_number.pop(fig, None)
class FigureContext(FigureRegistry):
"""
Extends FigureRegistry to be used as a context manger.
All figures known to the Registry will be shown on exiting the context.
Parameters
----------
block : bool, optional
Whether to wait for all figures to be closed before returning from
show_all.
If `True` block and run the GUI main loop until all figure windows
are closed.
If `False` ensure that all figure windows are displayed and return
immediately. In this case, you are responsible for ensuring
that the event loop is running to have responsive figures.
Defaults to True in non-interactive mode and to False in interactive
mode (see `.is_interactive`).
timeout : float, optional
Default time to wait for all of the Figures to be closed if blocking.
If 0 block forever.
forgive_failure : bool, optional
If True, block to show the figure before letting the exception
propagate
"""
def __init__(self, *, forgive_failure=False, **kwargs):
super().__init__(**kwargs)
self._forgive_failure = forgive_failure
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_value is not None and not self._forgive_failure:
return
show(self.figures, block=self._block, timeout=self._timeout)
# from mpl_gui import * # is a langauge miss-feature
__all__ = []