##// END OF EJS Templates
Convert "osx" gui framework to/from "macosx" in Matplotlib (#14420)...
M Bussonnier -
r28753:85bb5302 merge
parent child Browse files
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
@@ -1,354 +1,352 b''
1 1 """Tests for pylab tools module.
2 2 """
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7
8 8 from binascii import a2b_base64
9 9 from io import BytesIO
10 10
11 11 import pytest
12 12
13 13 matplotlib = pytest.importorskip("matplotlib")
14 14 matplotlib.use('Agg')
15 15 from matplotlib.figure import Figure
16 16
17 17 from matplotlib import pyplot as plt
18 18 from matplotlib_inline import backend_inline
19 19 import numpy as np
20 20
21 21 from IPython.core.getipython import get_ipython
22 22 from IPython.core.interactiveshell import InteractiveShell
23 23 from IPython.core.display import _PNG, _JPEG
24 24 from .. import pylabtools as pt
25 25
26 26 from IPython.testing import decorators as dec
27 27
28 28
29 29 def test_figure_to_svg():
30 30 # simple empty-figure test
31 31 fig = plt.figure()
32 32 assert pt.print_figure(fig, "svg") is None
33 33
34 34 plt.close('all')
35 35
36 36 # simple check for at least svg-looking output
37 37 fig = plt.figure()
38 38 ax = fig.add_subplot(1,1,1)
39 39 ax.plot([1,2,3])
40 40 plt.draw()
41 41 svg = pt.print_figure(fig, "svg")[:100].lower()
42 42 assert "doctype svg" in svg
43 43
44 44
45 45 def _check_pil_jpeg_bytes():
46 46 """Skip if PIL can't write JPEGs to BytesIO objects"""
47 47 # PIL's JPEG plugin can't write to BytesIO objects
48 48 # Pillow fixes this
49 49 from PIL import Image
50 50 buf = BytesIO()
51 51 img = Image.new("RGB", (4,4))
52 52 try:
53 53 img.save(buf, 'jpeg')
54 54 except Exception as e:
55 55 ename = e.__class__.__name__
56 56 raise pytest.skip("PIL can't write JPEG to BytesIO: %s: %s" % (ename, e)) from e
57 57
58 58 @dec.skip_without("PIL.Image")
59 59 def test_figure_to_jpeg():
60 60 _check_pil_jpeg_bytes()
61 61 # simple check for at least jpeg-looking output
62 62 fig = plt.figure()
63 63 ax = fig.add_subplot(1,1,1)
64 64 ax.plot([1,2,3])
65 65 plt.draw()
66 66 jpeg = pt.print_figure(fig, 'jpeg', pil_kwargs={'optimize': 50})[:100].lower()
67 67 assert jpeg.startswith(_JPEG)
68 68
69 69 def test_retina_figure():
70 70 # simple empty-figure test
71 71 fig = plt.figure()
72 72 assert pt.retina_figure(fig) == None
73 73 plt.close('all')
74 74
75 75 fig = plt.figure()
76 76 ax = fig.add_subplot(1,1,1)
77 77 ax.plot([1,2,3])
78 78 plt.draw()
79 79 png, md = pt.retina_figure(fig)
80 80 assert png.startswith(_PNG)
81 81 assert "width" in md
82 82 assert "height" in md
83 83
84 84
85 85 _fmt_mime_map = {
86 86 'png': 'image/png',
87 87 'jpeg': 'image/jpeg',
88 88 'pdf': 'application/pdf',
89 89 'retina': 'image/png',
90 90 'svg': 'image/svg+xml',
91 91 }
92 92
93 93 def test_select_figure_formats_str():
94 94 ip = get_ipython()
95 95 for fmt, active_mime in _fmt_mime_map.items():
96 96 pt.select_figure_formats(ip, fmt)
97 97 for mime, f in ip.display_formatter.formatters.items():
98 98 if mime == active_mime:
99 99 assert Figure in f
100 100 else:
101 101 assert Figure not in f
102 102
103 103 def test_select_figure_formats_kwargs():
104 104 ip = get_ipython()
105 105 kwargs = dict(bbox_inches="tight")
106 106 pt.select_figure_formats(ip, "png", **kwargs)
107 107 formatter = ip.display_formatter.formatters["image/png"]
108 108 f = formatter.lookup_by_type(Figure)
109 109 cell = f.keywords
110 110 expected = kwargs
111 111 expected["base64"] = True
112 112 expected["fmt"] = "png"
113 113 assert cell == expected
114 114
115 115 # check that the formatter doesn't raise
116 116 fig = plt.figure()
117 117 ax = fig.add_subplot(1,1,1)
118 118 ax.plot([1,2,3])
119 119 plt.draw()
120 120 formatter.enabled = True
121 121 png = formatter(fig)
122 122 assert isinstance(png, str)
123 123 png_bytes = a2b_base64(png)
124 124 assert png_bytes.startswith(_PNG)
125 125
126 126 def test_select_figure_formats_set():
127 127 ip = get_ipython()
128 128 for fmts in [
129 129 {'png', 'svg'},
130 130 ['png'],
131 131 ('jpeg', 'pdf', 'retina'),
132 132 {'svg'},
133 133 ]:
134 134 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
135 135 pt.select_figure_formats(ip, fmts)
136 136 for mime, f in ip.display_formatter.formatters.items():
137 137 if mime in active_mimes:
138 138 assert Figure in f
139 139 else:
140 140 assert Figure not in f
141 141
142 142 def test_select_figure_formats_bad():
143 143 ip = get_ipython()
144 144 with pytest.raises(ValueError):
145 145 pt.select_figure_formats(ip, 'foo')
146 146 with pytest.raises(ValueError):
147 147 pt.select_figure_formats(ip, {'png', 'foo'})
148 148 with pytest.raises(ValueError):
149 149 pt.select_figure_formats(ip, ['retina', 'pdf', 'bar', 'bad'])
150 150
151 151 def test_import_pylab():
152 152 ns = {}
153 153 pt.import_pylab(ns, import_all=False)
154 154 assert "plt" in ns
155 155 assert ns["np"] == np
156 156
157 157
158 158 class TestPylabSwitch(object):
159 159 class Shell(InteractiveShell):
160 160 def init_history(self):
161 161 """Sets up the command history, and starts regular autosaves."""
162 162 self.config.HistoryManager.hist_file = ":memory:"
163 163 super().init_history()
164 164
165 165 def enable_gui(self, gui):
166 166 pass
167 167
168 168 def setup_method(self):
169 169 import matplotlib
170 170 def act_mpl(backend):
171 171 matplotlib.rcParams['backend'] = backend
172 172
173 173 # Save rcParams since they get modified
174 174 self._saved_rcParams = matplotlib.rcParams
175 175 self._saved_rcParamsOrig = matplotlib.rcParamsOrig
176 176 matplotlib.rcParams = dict(backend="QtAgg")
177 177 matplotlib.rcParamsOrig = dict(backend="QtAgg")
178 178
179 179 # Mock out functions
180 180 self._save_am = pt.activate_matplotlib
181 181 pt.activate_matplotlib = act_mpl
182 182 self._save_ip = pt.import_pylab
183 183 pt.import_pylab = lambda *a,**kw:None
184 184 self._save_cis = backend_inline.configure_inline_support
185 185 backend_inline.configure_inline_support = lambda *a, **kw: None
186 186
187 187 def teardown_method(self):
188 188 pt.activate_matplotlib = self._save_am
189 189 pt.import_pylab = self._save_ip
190 190 backend_inline.configure_inline_support = self._save_cis
191 191 import matplotlib
192 192 matplotlib.rcParams = self._saved_rcParams
193 193 matplotlib.rcParamsOrig = self._saved_rcParamsOrig
194 194
195 195 def test_qt(self):
196 196 s = self.Shell()
197 197 gui, backend = s.enable_matplotlib(None)
198 198 assert gui == "qt"
199 199 assert s.pylab_gui_select == "qt"
200 200
201 201 gui, backend = s.enable_matplotlib("inline")
202 202 assert gui is None
203 203 assert s.pylab_gui_select == "qt"
204 204
205 205 gui, backend = s.enable_matplotlib("qt")
206 206 assert gui == "qt"
207 207 assert s.pylab_gui_select == "qt"
208 208
209 209 gui, backend = s.enable_matplotlib("inline")
210 210 assert gui is None
211 211 assert s.pylab_gui_select == "qt"
212 212
213 213 gui, backend = s.enable_matplotlib()
214 214 assert gui == "qt"
215 215 assert s.pylab_gui_select == "qt"
216 216
217 217 def test_inline(self):
218 218 s = self.Shell()
219 219 gui, backend = s.enable_matplotlib("inline")
220 220 assert gui is None
221 221 assert s.pylab_gui_select == None
222 222
223 223 gui, backend = s.enable_matplotlib("inline")
224 224 assert gui is None
225 225 assert s.pylab_gui_select == None
226 226
227 227 gui, backend = s.enable_matplotlib("qt")
228 228 assert gui == "qt"
229 229 assert s.pylab_gui_select == "qt"
230 230
231 231 def test_inline_twice(self):
232 232 "Using '%matplotlib inline' twice should not reset formatters"
233 233
234 234 ip = self.Shell()
235 235 gui, backend = ip.enable_matplotlib("inline")
236 236 assert gui is None
237 237
238 238 fmts = {'png'}
239 239 active_mimes = {_fmt_mime_map[fmt] for fmt in fmts}
240 240 pt.select_figure_formats(ip, fmts)
241 241
242 242 gui, backend = ip.enable_matplotlib("inline")
243 243 assert gui is None
244 244
245 245 for mime, f in ip.display_formatter.formatters.items():
246 246 if mime in active_mimes:
247 247 assert Figure in f
248 248 else:
249 249 assert Figure not in f
250 250
251 251 def test_qt_gtk(self):
252 252 s = self.Shell()
253 253 gui, backend = s.enable_matplotlib("qt")
254 254 assert gui == "qt"
255 255 assert s.pylab_gui_select == "qt"
256 256
257 257 gui, backend = s.enable_matplotlib("gtk3")
258 258 assert gui == "qt"
259 259 assert s.pylab_gui_select == "qt"
260 260
261 261
262 262 def test_no_gui_backends():
263 263 for k in ['agg', 'svg', 'pdf', 'ps']:
264 264 assert k not in pt.backend2gui
265 265
266 266
267 267 def test_figure_no_canvas():
268 268 fig = Figure()
269 269 fig.canvas = None
270 270 pt.print_figure(fig)
271 271
272 272
273 273 @pytest.mark.parametrize(
274 274 "name, expected_gui, expected_backend",
275 275 [
276 276 # name is gui
277 277 ("gtk3", "gtk3", "gtk3agg"),
278 278 ("gtk4", "gtk4", "gtk4agg"),
279 ("headless", "headless", "agg"),
279 ("headless", None, "agg"),
280 280 ("osx", "osx", "macosx"),
281 281 ("qt", "qt", "qtagg"),
282 282 ("qt5", "qt5", "qt5agg"),
283 ("qt6", "qt6", "qt6agg"),
283 ("qt6", "qt6", "qtagg"),
284 284 ("tk", "tk", "tkagg"),
285 285 ("wx", "wx", "wxagg"),
286 286 # name is backend
287 287 ("agg", None, "agg"),
288 288 ("cairo", None, "cairo"),
289 289 ("pdf", None, "pdf"),
290 290 ("ps", None, "ps"),
291 291 ("svg", None, "svg"),
292 292 ("template", None, "template"),
293 293 ("gtk3agg", "gtk3", "gtk3agg"),
294 294 ("gtk3cairo", "gtk3", "gtk3cairo"),
295 295 ("gtk4agg", "gtk4", "gtk4agg"),
296 296 ("gtk4cairo", "gtk4", "gtk4cairo"),
297 297 ("macosx", "osx", "macosx"),
298 298 ("nbagg", "nbagg", "nbagg"),
299 299 ("notebook", "nbagg", "notebook"),
300 300 ("qtagg", "qt", "qtagg"),
301 301 ("qtcairo", "qt", "qtcairo"),
302 302 ("qt5agg", "qt5", "qt5agg"),
303 303 ("qt5cairo", "qt5", "qt5cairo"),
304 ("qt6agg", "qt", "qt6agg"),
305 ("qt6cairo", "qt", "qt6cairo"),
306 304 ("tkagg", "tk", "tkagg"),
307 305 ("tkcairo", "tk", "tkcairo"),
308 306 ("webagg", "webagg", "webagg"),
309 307 ("wxagg", "wx", "wxagg"),
310 308 ("wxcairo", "wx", "wxcairo"),
311 309 ],
312 310 )
313 311 def test_backend_builtin(name, expected_gui, expected_backend):
314 312 # Test correct identification of Matplotlib built-in backends without importing and using them,
315 313 # otherwise we would need to ensure all the complex dependencies such as windowing toolkits are
316 314 # installed.
317 315
318 316 mpl_manages_backends = pt._matplotlib_manages_backends()
319 317 if not mpl_manages_backends:
320 318 # Backends not supported before _matplotlib_manages_backends or supported
321 319 # but with different expected_gui or expected_backend.
322 320 if (
323 321 name.endswith("agg")
324 322 or name.endswith("cairo")
325 323 or name in ("headless", "macosx", "pdf", "ps", "svg", "template")
326 324 ):
327 325 pytest.skip()
328 326 elif name == "qt6":
329 327 expected_backend = "qtagg"
330 328 elif name == "notebook":
331 329 expected_backend, expected_gui = expected_gui, expected_backend
332 330
333 331 gui, backend = pt.find_gui_and_backend(name)
334 332 if not mpl_manages_backends:
335 333 gui = gui.lower() if gui else None
336 334 backend = backend.lower() if backend else None
337 335 assert gui == expected_gui
338 336 assert backend == expected_backend
339 337
340 338
341 339 def test_backend_entry_point():
342 340 gui, backend = pt.find_gui_and_backend("inline")
343 341 assert gui is None
344 342 expected_backend = (
345 343 "inline"
346 344 if pt._matplotlib_manages_backends()
347 345 else "module://matplotlib_inline.backend_inline"
348 346 )
349 347 assert backend == expected_backend
350 348
351 349
352 350 def test_backend_unknown():
353 351 with pytest.raises(RuntimeError if pt._matplotlib_manages_backends() else KeyError):
354 352 pt.find_gui_and_backend("name-does-not-exist")
General Comments 0
You need to be logged in to leave comments. Login now