##// END OF EJS Templates
Backport PR #13179: Fix qtagg eventloop
Matthias Bussonnier -
Show More
@@ -1,417 +1,423 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Pylab (matplotlib) support utilities."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from io import BytesIO
8 8 from binascii import b2a_base64
9 9 from functools import partial
10 10 import warnings
11 11
12 12 from IPython.core.display import _pngxy
13 13 from IPython.utils.decorators import flag_calls
14 14
15 15 # If user specifies a GUI, that dictates the backend, otherwise we read the
16 16 # user's mpl default from the mpl rc structure
17 17 backends = {
18 18 "tk": "TkAgg",
19 19 "gtk": "GTKAgg",
20 20 "gtk3": "GTK3Agg",
21 21 "gtk4": "GTK4Agg",
22 22 "wx": "WXAgg",
23 23 "qt4": "Qt4Agg",
24 24 "qt5": "Qt5Agg",
25 25 "qt6": "QtAgg",
26 26 "qt": "Qt5Agg",
27 27 "osx": "MacOSX",
28 28 "nbagg": "nbAgg",
29 29 "notebook": "nbAgg",
30 30 "agg": "agg",
31 31 "svg": "svg",
32 32 "pdf": "pdf",
33 33 "ps": "ps",
34 34 "inline": "module://matplotlib_inline.backend_inline",
35 35 "ipympl": "module://ipympl.backend_nbagg",
36 36 "widget": "module://ipympl.backend_nbagg",
37 37 }
38 38
39 39 # We also need a reverse backends2guis mapping that will properly choose which
40 40 # GUI support to activate based on the desired matplotlib backend. For the
41 41 # most part it's just a reverse of the above dict, but we also need to add a
42 42 # few others that map to the same GUI manually:
43 43 backend2gui = dict(zip(backends.values(), backends.keys()))
44 # Our tests expect backend2gui to just return 'qt'
45 backend2gui['Qt4Agg'] = 'qt'
46 44 # In the reverse mapping, there are a few extra valid matplotlib backends that
47 45 # map to the same GUI support
48 46 backend2gui["GTK"] = backend2gui["GTKCairo"] = "gtk"
49 47 backend2gui["GTK3Cairo"] = "gtk3"
50 48 backend2gui["GTK4Cairo"] = "gtk4"
51 49 backend2gui["WX"] = "wx"
52 50 backend2gui["CocoaAgg"] = "osx"
51 # There needs to be a hysteresis here as the new QtAgg Matplotlib backend
52 # supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
53 # and Qt6.
54 backend2gui["QtAgg"] = "qt"
55 backend2gui["Qt4Agg"] = "qt"
56 backend2gui["Qt5Agg"] = "qt"
57
53 58 # And some backends that don't need GUI integration
54 59 del backend2gui["nbAgg"]
55 60 del backend2gui["agg"]
56 61 del backend2gui["svg"]
57 62 del backend2gui["pdf"]
58 63 del backend2gui["ps"]
59 64 del backend2gui["module://matplotlib_inline.backend_inline"]
65 del backend2gui["module://ipympl.backend_nbagg"]
60 66
61 67 #-----------------------------------------------------------------------------
62 68 # Matplotlib utilities
63 69 #-----------------------------------------------------------------------------
64 70
65 71
66 72 def getfigs(*fig_nums):
67 73 """Get a list of matplotlib figures by figure numbers.
68 74
69 75 If no arguments are given, all available figures are returned. If the
70 76 argument list contains references to invalid figures, a warning is printed
71 77 but the function continues pasting further figures.
72 78
73 79 Parameters
74 80 ----------
75 81 figs : tuple
76 82 A tuple of ints giving the figure numbers of the figures to return.
77 83 """
78 84 from matplotlib._pylab_helpers import Gcf
79 85 if not fig_nums:
80 86 fig_managers = Gcf.get_all_fig_managers()
81 87 return [fm.canvas.figure for fm in fig_managers]
82 88 else:
83 89 figs = []
84 90 for num in fig_nums:
85 91 f = Gcf.figs.get(num)
86 92 if f is None:
87 93 print('Warning: figure %s not available.' % num)
88 94 else:
89 95 figs.append(f.canvas.figure)
90 96 return figs
91 97
92 98
93 99 def figsize(sizex, sizey):
94 100 """Set the default figure size to be [sizex, sizey].
95 101
96 102 This is just an easy to remember, convenience wrapper that sets::
97 103
98 104 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
99 105 """
100 106 import matplotlib
101 107 matplotlib.rcParams['figure.figsize'] = [sizex, sizey]
102 108
103 109
104 110 def print_figure(fig, fmt="png", bbox_inches="tight", base64=False, **kwargs):
105 111 """Print a figure to an image, and return the resulting file data
106 112
107 113 Returned data will be bytes unless ``fmt='svg'``,
108 114 in which case it will be unicode.
109 115
110 116 Any keyword args are passed to fig.canvas.print_figure,
111 117 such as ``quality`` or ``bbox_inches``.
112 118
113 119 If `base64` is True, return base64-encoded str instead of raw bytes
114 120 for binary-encoded image formats
115 121
116 122 .. versionadded: 7.29
117 123 base64 argument
118 124 """
119 125 # When there's an empty figure, we shouldn't return anything, otherwise we
120 126 # get big blank areas in the qt console.
121 127 if not fig.axes and not fig.lines:
122 128 return
123 129
124 130 dpi = fig.dpi
125 131 if fmt == 'retina':
126 132 dpi = dpi * 2
127 133 fmt = 'png'
128 134
129 135 # build keyword args
130 136 kw = {
131 137 "format":fmt,
132 138 "facecolor":fig.get_facecolor(),
133 139 "edgecolor":fig.get_edgecolor(),
134 140 "dpi":dpi,
135 141 "bbox_inches":bbox_inches,
136 142 }
137 143 # **kwargs get higher priority
138 144 kw.update(kwargs)
139 145
140 146 bytes_io = BytesIO()
141 147 if fig.canvas is None:
142 148 from matplotlib.backend_bases import FigureCanvasBase
143 149 FigureCanvasBase(fig)
144 150
145 151 fig.canvas.print_figure(bytes_io, **kw)
146 152 data = bytes_io.getvalue()
147 153 if fmt == 'svg':
148 154 data = data.decode('utf-8')
149 155 elif base64:
150 156 data = b2a_base64(data).decode("ascii")
151 157 return data
152 158
153 159 def retina_figure(fig, base64=False, **kwargs):
154 160 """format a figure as a pixel-doubled (retina) PNG
155 161
156 162 If `base64` is True, return base64-encoded str instead of raw bytes
157 163 for binary-encoded image formats
158 164
159 165 .. versionadded: 7.29
160 166 base64 argument
161 167 """
162 168 pngdata = print_figure(fig, fmt="retina", base64=False, **kwargs)
163 169 # Make sure that retina_figure acts just like print_figure and returns
164 170 # None when the figure is empty.
165 171 if pngdata is None:
166 172 return
167 173 w, h = _pngxy(pngdata)
168 174 metadata = {"width": w//2, "height":h//2}
169 175 if base64:
170 176 pngdata = b2a_base64(pngdata).decode("ascii")
171 177 return pngdata, metadata
172 178
173 179
174 180 # We need a little factory function here to create the closure where
175 181 # safe_execfile can live.
176 182 def mpl_runner(safe_execfile):
177 183 """Factory to return a matplotlib-enabled runner for %run.
178 184
179 185 Parameters
180 186 ----------
181 187 safe_execfile : function
182 188 This must be a function with the same interface as the
183 189 :meth:`safe_execfile` method of IPython.
184 190
185 191 Returns
186 192 -------
187 193 A function suitable for use as the ``runner`` argument of the %run magic
188 194 function.
189 195 """
190 196
191 197 def mpl_execfile(fname,*where,**kw):
192 198 """matplotlib-aware wrapper around safe_execfile.
193 199
194 200 Its interface is identical to that of the :func:`execfile` builtin.
195 201
196 202 This is ultimately a call to execfile(), but wrapped in safeties to
197 203 properly handle interactive rendering."""
198 204
199 205 import matplotlib
200 206 import matplotlib.pyplot as plt
201 207
202 208 #print '*** Matplotlib runner ***' # dbg
203 209 # turn off rendering until end of script
204 210 is_interactive = matplotlib.rcParams['interactive']
205 211 matplotlib.interactive(False)
206 212 safe_execfile(fname,*where,**kw)
207 213 matplotlib.interactive(is_interactive)
208 214 # make rendering call now, if the user tried to do it
209 215 if plt.draw_if_interactive.called:
210 216 plt.draw()
211 217 plt.draw_if_interactive.called = False
212 218
213 219 # re-draw everything that is stale
214 220 try:
215 221 da = plt.draw_all
216 222 except AttributeError:
217 223 pass
218 224 else:
219 225 da()
220 226
221 227 return mpl_execfile
222 228
223 229
224 230 def _reshow_nbagg_figure(fig):
225 231 """reshow an nbagg figure"""
226 232 try:
227 233 reshow = fig.canvas.manager.reshow
228 234 except AttributeError:
229 235 raise NotImplementedError()
230 236 else:
231 237 reshow()
232 238
233 239
234 240 def select_figure_formats(shell, formats, **kwargs):
235 241 """Select figure formats for the inline backend.
236 242
237 243 Parameters
238 244 ==========
239 245 shell : InteractiveShell
240 246 The main IPython instance.
241 247 formats : str or set
242 248 One or a set of figure formats to enable: 'png', 'retina', 'jpeg', 'svg', 'pdf'.
243 249 **kwargs : any
244 250 Extra keyword arguments to be passed to fig.canvas.print_figure.
245 251 """
246 252 import matplotlib
247 253 from matplotlib.figure import Figure
248 254
249 255 svg_formatter = shell.display_formatter.formatters['image/svg+xml']
250 256 png_formatter = shell.display_formatter.formatters['image/png']
251 257 jpg_formatter = shell.display_formatter.formatters['image/jpeg']
252 258 pdf_formatter = shell.display_formatter.formatters['application/pdf']
253 259
254 260 if isinstance(formats, str):
255 261 formats = {formats}
256 262 # cast in case of list / tuple
257 263 formats = set(formats)
258 264
259 265 [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
260 266 mplbackend = matplotlib.get_backend().lower()
261 267 if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg':
262 268 formatter = shell.display_formatter.ipython_display_formatter
263 269 formatter.for_type(Figure, _reshow_nbagg_figure)
264 270
265 271 supported = {'png', 'png2x', 'retina', 'jpg', 'jpeg', 'svg', 'pdf'}
266 272 bad = formats.difference(supported)
267 273 if bad:
268 274 bs = "%s" % ','.join([repr(f) for f in bad])
269 275 gs = "%s" % ','.join([repr(f) for f in supported])
270 276 raise ValueError("supported formats are: %s not %s" % (gs, bs))
271 277
272 278 if "png" in formats:
273 279 png_formatter.for_type(
274 280 Figure, partial(print_figure, fmt="png", base64=True, **kwargs)
275 281 )
276 282 if "retina" in formats or "png2x" in formats:
277 283 png_formatter.for_type(Figure, partial(retina_figure, base64=True, **kwargs))
278 284 if "jpg" in formats or "jpeg" in formats:
279 285 jpg_formatter.for_type(
280 286 Figure, partial(print_figure, fmt="jpg", base64=True, **kwargs)
281 287 )
282 288 if "svg" in formats:
283 289 svg_formatter.for_type(Figure, partial(print_figure, fmt="svg", **kwargs))
284 290 if "pdf" in formats:
285 291 pdf_formatter.for_type(
286 292 Figure, partial(print_figure, fmt="pdf", base64=True, **kwargs)
287 293 )
288 294
289 295 #-----------------------------------------------------------------------------
290 296 # Code for initializing matplotlib and importing pylab
291 297 #-----------------------------------------------------------------------------
292 298
293 299
294 300 def find_gui_and_backend(gui=None, gui_select=None):
295 301 """Given a gui string return the gui and mpl backend.
296 302
297 303 Parameters
298 304 ----------
299 305 gui : str
300 306 Can be one of ('tk','gtk','wx','qt','qt4','inline','agg').
301 307 gui_select : str
302 308 Can be one of ('tk','gtk','wx','qt','qt4','inline').
303 309 This is any gui already selected by the shell.
304 310
305 311 Returns
306 312 -------
307 313 A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg',
308 314 'WXAgg','Qt4Agg','module://matplotlib_inline.backend_inline','agg').
309 315 """
310 316
311 317 import matplotlib
312 318
313 319 if gui and gui != 'auto':
314 320 # select backend based on requested gui
315 321 backend = backends[gui]
316 322 if gui == 'agg':
317 323 gui = None
318 324 else:
319 325 # We need to read the backend from the original data structure, *not*
320 326 # from mpl.rcParams, since a prior invocation of %matplotlib may have
321 327 # overwritten that.
322 328 # WARNING: this assumes matplotlib 1.1 or newer!!
323 329 backend = matplotlib.rcParamsOrig['backend']
324 330 # In this case, we need to find what the appropriate gui selection call
325 331 # should be for IPython, so we can activate inputhook accordingly
326 332 gui = backend2gui.get(backend, None)
327 333
328 334 # If we have already had a gui active, we need it and inline are the
329 335 # ones allowed.
330 336 if gui_select and gui != gui_select:
331 337 gui = gui_select
332 338 backend = backends[gui]
333 339
334 340 return gui, backend
335 341
336 342
337 343 def activate_matplotlib(backend):
338 344 """Activate the given backend and set interactive to True."""
339 345
340 346 import matplotlib
341 347 matplotlib.interactive(True)
342 348
343 349 # Matplotlib had a bug where even switch_backend could not force
344 350 # the rcParam to update. This needs to be set *before* the module
345 351 # magic of switch_backend().
346 352 matplotlib.rcParams['backend'] = backend
347 353
348 354 # Due to circular imports, pyplot may be only partially initialised
349 355 # when this function runs.
350 356 # So avoid needing matplotlib attribute-lookup to access pyplot.
351 357 from matplotlib import pyplot as plt
352 358
353 359 plt.switch_backend(backend)
354 360
355 361 plt.show._needmain = False
356 362 # We need to detect at runtime whether show() is called by the user.
357 363 # For this, we wrap it into a decorator which adds a 'called' flag.
358 364 plt.draw_if_interactive = flag_calls(plt.draw_if_interactive)
359 365
360 366
361 367 def import_pylab(user_ns, import_all=True):
362 368 """Populate the namespace with pylab-related values.
363 369
364 370 Imports matplotlib, pylab, numpy, and everything from pylab and numpy.
365 371
366 372 Also imports a few names from IPython (figsize, display, getfigs)
367 373
368 374 """
369 375
370 376 # Import numpy as np/pyplot as plt are conventions we're trying to
371 377 # somewhat standardize on. Making them available to users by default
372 378 # will greatly help this.
373 379 s = ("import numpy\n"
374 380 "import matplotlib\n"
375 381 "from matplotlib import pylab, mlab, pyplot\n"
376 382 "np = numpy\n"
377 383 "plt = pyplot\n"
378 384 )
379 385 exec(s, user_ns)
380 386
381 387 if import_all:
382 388 s = ("from matplotlib.pylab import *\n"
383 389 "from numpy import *\n")
384 390 exec(s, user_ns)
385 391
386 392 # IPython symbols to add
387 393 user_ns['figsize'] = figsize
388 394 from IPython.core.display import display
389 395 # Add display and getfigs to the user's namespace
390 396 user_ns['display'] = display
391 397 user_ns['getfigs'] = getfigs
392 398
393 399
394 400 def configure_inline_support(shell, backend):
395 401 """
396 402 .. deprecated: 7.23
397 403
398 404 use `matplotlib_inline.backend_inline.configure_inline_support()`
399 405
400 406 Configure an IPython shell object for matplotlib use.
401 407
402 408 Parameters
403 409 ----------
404 410 shell : InteractiveShell instance
405 411
406 412 backend : matplotlib backend
407 413 """
408 414 warnings.warn(
409 415 "`configure_inline_support` is deprecated since IPython 7.23, directly "
410 416 "use `matplotlib_inline.backend_inline.configure_inline_support()`",
411 417 DeprecationWarning,
412 418 stacklevel=2,
413 419 )
414 420
415 421 from matplotlib_inline.backend_inline import configure_inline_support as configure_inline_support_orig
416 422
417 423 configure_inline_support_orig(shell, backend)
General Comments 0
You need to be logged in to leave comments. Login now