##// END OF EJS Templates
Move Matplotlib backend resolution to Matplotlib
Ian Thomas -
Show More
@@ -3657,7 +3657,7 b' class InteractiveShell(SingletonConfigurable):'
3657 3657 from IPython.core import pylabtools as pt
3658 3658 gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select)
3659 3659
3660 if gui != 'inline':
3660 if gui != None:
3661 3661 # If we have our first gui selection, store it
3662 3662 if self.pylab_gui_select is None:
3663 3663 self.pylab_gui_select = gui
@@ -18,7 +18,6 b' from IPython.core import magic_arguments'
18 18 from IPython.core.magic import Magics, magics_class, line_magic
19 19 from IPython.testing.skipdoctest import skip_doctest
20 20 from warnings import warn
21 from IPython.core.pylabtools import backends
22 21
23 22 #-----------------------------------------------------------------------------
24 23 # Magic implementation classes
@@ -26,11 +25,11 b' from IPython.core.pylabtools import backends'
26 25
27 26 magic_gui_arg = magic_arguments.argument(
28 27 'gui', nargs='?',
29 help="""Name of the matplotlib backend to use %s.
28 help="""Name of the matplotlib backend to use such as 'qt' or 'widget'.
30 29 If given, the corresponding matplotlib backend is used,
31 30 otherwise it will be matplotlib's default
32 31 (which you can set in your matplotlib config file).
33 """ % str(tuple(sorted(backends.keys())))
32 """
34 33 )
35 34
36 35
@@ -93,7 +92,13 b' class PylabMagics(Magics):'
93 92 """
94 93 args = magic_arguments.parse_argstring(self.matplotlib, line)
95 94 if args.list:
96 backends_list = list(backends.keys())
95 from IPython.core.pylabtools import _matplotlib_manages_backends
96 if _matplotlib_manages_backends():
97 from matplotlib.backends.registry import backend_registry
98 backends_list = backend_registry.list_all()
99 else:
100 from IPython.core.pylabtools import backends
101 backends_list = list(backends.keys())
97 102 print("Available matplotlib backends: %s" % backends_list)
98 103 else:
99 104 gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui)
@@ -12,9 +12,12 b' import warnings'
12 12 from IPython.core.display import _pngxy
13 13 from IPython.utils.decorators import flag_calls
14 14
15 # If user specifies a GUI, that dictates the backend, otherwise we read the
16 # user's mpl default from the mpl rc structure
17 backends = {
15
16 # Matplotlib backend resolution functionality moved from IPython to Matplotlib
17 # in IPython 8.23 and Matplotlib 3.9. Need to keep `backends` and `backend2gui`
18 # here for earlier Matplotlib and for external backend libraries such as
19 # mplcairo that might rely upon it.
20 _deprecated_backends = {
18 21 "tk": "TkAgg",
19 22 "gtk": "GTKAgg",
20 23 "gtk3": "GTK3Agg",
@@ -41,29 +44,38 b' backends = {'
41 44 # GUI support to activate based on the desired matplotlib backend. For the
42 45 # most part it's just a reverse of the above dict, but we also need to add a
43 46 # few others that map to the same GUI manually:
44 backend2gui = dict(zip(backends.values(), backends.keys()))
47 _deprecated_backend2gui = dict(zip(_deprecated_backends.values(), _deprecated_backends.keys()))
45 48 # In the reverse mapping, there are a few extra valid matplotlib backends that
46 49 # map to the same GUI support
47 backend2gui["GTK"] = backend2gui["GTKCairo"] = "gtk"
48 backend2gui["GTK3Cairo"] = "gtk3"
49 backend2gui["GTK4Cairo"] = "gtk4"
50 backend2gui["WX"] = "wx"
51 backend2gui["CocoaAgg"] = "osx"
50 _deprecated_backend2gui["GTK"] = _deprecated_backend2gui["GTKCairo"] = "gtk"
51 _deprecated_backend2gui["GTK3Cairo"] = "gtk3"
52 _deprecated_backend2gui["GTK4Cairo"] = "gtk4"
53 _deprecated_backend2gui["WX"] = "wx"
54 _deprecated_backend2gui["CocoaAgg"] = "osx"
52 55 # There needs to be a hysteresis here as the new QtAgg Matplotlib backend
53 56 # supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5,
54 57 # and Qt6.
55 backend2gui["QtAgg"] = "qt"
56 backend2gui["Qt4Agg"] = "qt4"
57 backend2gui["Qt5Agg"] = "qt5"
58 _deprecated_backend2gui["QtAgg"] = "qt"
59 _deprecated_backend2gui["Qt4Agg"] = "qt4"
60 _deprecated_backend2gui["Qt5Agg"] = "qt5"
58 61
59 62 # And some backends that don't need GUI integration
60 del backend2gui["nbAgg"]
61 del backend2gui["agg"]
62 del backend2gui["svg"]
63 del backend2gui["pdf"]
64 del backend2gui["ps"]
65 del backend2gui["module://matplotlib_inline.backend_inline"]
66 del backend2gui["module://ipympl.backend_nbagg"]
63 del _deprecated_backend2gui["nbAgg"]
64 del _deprecated_backend2gui["agg"]
65 del _deprecated_backend2gui["svg"]
66 del _deprecated_backend2gui["pdf"]
67 del _deprecated_backend2gui["ps"]
68 del _deprecated_backend2gui["module://matplotlib_inline.backend_inline"]
69 del _deprecated_backend2gui["module://ipympl.backend_nbagg"]
70
71
72 # Deprecated attributes backends and backend2gui mostly following PEP 562.
73 def __getattr__(name):
74 if name in ("backends", "backend2gui"):
75 warnings.warn(f"{name} is deprecated", DeprecationWarning)
76 return globals()[f"_deprecated_{name}"]
77 raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
78
67 79
68 80 #-----------------------------------------------------------------------------
69 81 # Matplotlib utilities
@@ -267,7 +279,7 b' def select_figure_formats(shell, formats, **kwargs):'
267 279
268 280 [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ]
269 281 mplbackend = matplotlib.get_backend().lower()
270 if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg':
282 if mplbackend in ('nbagg', 'ipympl', 'widget', 'module://ipympl.backend_nbagg'):
271 283 formatter = shell.display_formatter.ipython_display_formatter
272 284 formatter.for_type(Figure, _reshow_nbagg_figure)
273 285
@@ -318,9 +330,23 b' def find_gui_and_backend(gui=None, gui_select=None):'
318 330 """
319 331
320 332 import matplotlib
333 if _matplotlib_manages_backends():
334 backend_registry = matplotlib.backends.registry.backend_registry
335
336 # gui argument may be a gui event loop or may be a backend name.
337 if gui in ("auto", None):
338 backend = matplotlib.rcParamsOrig['backend']
339 backend, gui = backend_registry.resolve_backend(backend)
340 else:
341 backend, gui = backend_registry.resolve_gui_or_backend(gui)
321 342
322 has_unified_qt_backend = getattr(matplotlib, "__version_info__", (0, 0)) >= (3, 5)
343 return gui, backend
323 344
345 # Fallback to previous behaviour (Matplotlib < 3.9)
346 mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0))
347 has_unified_qt_backend = mpl_version_info >= (3, 5)
348
349 from IPython.core.pylabtools import backends
324 350 backends_ = dict(backends)
325 351 if not has_unified_qt_backend:
326 352 backends_["qt"] = "qt5agg"
@@ -338,6 +364,7 b' def find_gui_and_backend(gui=None, gui_select=None):'
338 364 backend = matplotlib.rcParamsOrig['backend']
339 365 # In this case, we need to find what the appropriate gui selection call
340 366 # should be for IPython, so we can activate inputhook accordingly
367 from IPython.core.pylabtools import backend2gui
341 368 gui = backend2gui.get(backend, None)
342 369
343 370 # If we have already had a gui active, we need it and inline are the
@@ -346,6 +373,10 b' def find_gui_and_backend(gui=None, gui_select=None):'
346 373 gui = gui_select
347 374 backend = backends_[gui]
348 375
376 # Since IPython 8.23.0 use None for no gui event loop rather than "inline".
377 if gui == "inline":
378 gui = None
379
349 380 return gui, backend
350 381
351 382
@@ -431,3 +462,9 b' def configure_inline_support(shell, backend):'
431 462 )
432 463
433 464 configure_inline_support_orig(shell, backend)
465
466
467 def _matplotlib_manages_backends():
468 import matplotlib
469 mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0))
470 return mpl_version_info >= (3, 9)
@@ -31,8 +31,7 b' from IPython.terminal import pt_inputhooks'
31 31
32 32 gui_keys = tuple(sorted(pt_inputhooks.backends) + sorted(pt_inputhooks.aliases))
33 33
34 backend_keys = sorted(pylabtools.backends.keys())
35 backend_keys.insert(0, 'auto')
34 backend_keys = []
36 35
37 36 shell_flags = {}
38 37
@@ -199,7 +199,7 b' class TestPylabSwitch(object):'
199 199 assert s.pylab_gui_select == "qt"
200 200
201 201 gui, backend = s.enable_matplotlib("inline")
202 assert gui == "inline"
202 assert gui is None
203 203 assert s.pylab_gui_select == "qt"
204 204
205 205 gui, backend = s.enable_matplotlib("qt")
@@ -207,7 +207,7 b' class TestPylabSwitch(object):'
207 207 assert s.pylab_gui_select == "qt"
208 208
209 209 gui, backend = s.enable_matplotlib("inline")
210 assert gui == "inline"
210 assert gui is None
211 211 assert s.pylab_gui_select == "qt"
212 212
213 213 gui, backend = s.enable_matplotlib()
@@ -217,11 +217,11 b' class TestPylabSwitch(object):'
217 217 def test_inline(self):
218 218 s = self.Shell()
219 219 gui, backend = s.enable_matplotlib("inline")
220 assert gui == "inline"
220 assert gui is None
221 221 assert s.pylab_gui_select == None
222 222
223 223 gui, backend = s.enable_matplotlib("inline")
224 assert gui == "inline"
224 assert gui is None
225 225 assert s.pylab_gui_select == None
226 226
227 227 gui, backend = s.enable_matplotlib("qt")
@@ -233,14 +233,14 b' class TestPylabSwitch(object):'
233 233
234 234 ip = self.Shell()
235 235 gui, backend = ip.enable_matplotlib("inline")
236 assert gui == "inline"
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 assert gui == "inline"
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:
@@ -254,7 +254,7 b' class TestPylabSwitch(object):'
254 254 assert gui == "qt"
255 255 assert s.pylab_gui_select == "qt"
256 256
257 gui, backend = s.enable_matplotlib("gtk")
257 gui, backend = s.enable_matplotlib("gtk3")
258 258 assert gui == "qt"
259 259 assert s.pylab_gui_select == "qt"
260 260
@@ -966,7 +966,7 b' class TerminalInteractiveShell(InteractiveShell):'
966 966 if self._inputhook is not None and gui is None:
967 967 self.active_eventloop = self._inputhook = None
968 968
969 if gui and (gui not in {"inline", "webagg"}):
969 if gui and (gui not in {None, "webagg"}):
970 970 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
971 971 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
972 972 else:
General Comments 0
You need to be logged in to leave comments. Login now