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