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