diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 12c1206..b155744 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3657,7 +3657,7 @@ class InteractiveShell(SingletonConfigurable): from IPython.core import pylabtools as pt gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select) - if gui != 'inline': + if gui != None: # If we have our first gui selection, store it if self.pylab_gui_select is None: self.pylab_gui_select = gui diff --git a/IPython/core/magics/pylab.py b/IPython/core/magics/pylab.py index 2a69453..d9bdd17 100644 --- a/IPython/core/magics/pylab.py +++ b/IPython/core/magics/pylab.py @@ -18,7 +18,6 @@ from IPython.core import magic_arguments from IPython.core.magic import Magics, magics_class, line_magic from IPython.testing.skipdoctest import skip_doctest from warnings import warn -from IPython.core.pylabtools import backends #----------------------------------------------------------------------------- # Magic implementation classes @@ -26,11 +25,11 @@ from IPython.core.pylabtools import backends magic_gui_arg = magic_arguments.argument( 'gui', nargs='?', - help="""Name of the matplotlib backend to use %s. + help="""Name of the matplotlib backend to use such as 'qt' or 'widget'. If given, the corresponding matplotlib backend is used, otherwise it will be matplotlib's default (which you can set in your matplotlib config file). - """ % str(tuple(sorted(backends.keys()))) + """ ) @@ -93,7 +92,13 @@ class PylabMagics(Magics): """ args = magic_arguments.parse_argstring(self.matplotlib, line) if args.list: - backends_list = list(backends.keys()) + from IPython.core.pylabtools import _matplotlib_manages_backends + if _matplotlib_manages_backends(): + from matplotlib.backends.registry import backend_registry + backends_list = backend_registry.list_all() + else: + from IPython.core.pylabtools import backends + backends_list = list(backends.keys()) print("Available matplotlib backends: %s" % backends_list) else: gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui) diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index e5715a9..e08ce5c 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -12,9 +12,12 @@ import warnings from IPython.core.display import _pngxy from IPython.utils.decorators import flag_calls -# If user specifies a GUI, that dictates the backend, otherwise we read the -# user's mpl default from the mpl rc structure -backends = { + +# Matplotlib backend resolution functionality moved from IPython to Matplotlib +# in IPython 8.23 and Matplotlib 3.9. Need to keep `backends` and `backend2gui` +# here for earlier Matplotlib and for external backend libraries such as +# mplcairo that might rely upon it. +_deprecated_backends = { "tk": "TkAgg", "gtk": "GTKAgg", "gtk3": "GTK3Agg", @@ -41,29 +44,38 @@ backends = { # GUI support to activate based on the desired matplotlib backend. For the # most part it's just a reverse of the above dict, but we also need to add a # few others that map to the same GUI manually: -backend2gui = dict(zip(backends.values(), backends.keys())) +_deprecated_backend2gui = dict(zip(_deprecated_backends.values(), _deprecated_backends.keys())) # In the reverse mapping, there are a few extra valid matplotlib backends that # map to the same GUI support -backend2gui["GTK"] = backend2gui["GTKCairo"] = "gtk" -backend2gui["GTK3Cairo"] = "gtk3" -backend2gui["GTK4Cairo"] = "gtk4" -backend2gui["WX"] = "wx" -backend2gui["CocoaAgg"] = "osx" +_deprecated_backend2gui["GTK"] = _deprecated_backend2gui["GTKCairo"] = "gtk" +_deprecated_backend2gui["GTK3Cairo"] = "gtk3" +_deprecated_backend2gui["GTK4Cairo"] = "gtk4" +_deprecated_backend2gui["WX"] = "wx" +_deprecated_backend2gui["CocoaAgg"] = "osx" # There needs to be a hysteresis here as the new QtAgg Matplotlib backend # supports either Qt5 or Qt6 and the IPython qt event loop support Qt4, Qt5, # and Qt6. -backend2gui["QtAgg"] = "qt" -backend2gui["Qt4Agg"] = "qt4" -backend2gui["Qt5Agg"] = "qt5" +_deprecated_backend2gui["QtAgg"] = "qt" +_deprecated_backend2gui["Qt4Agg"] = "qt4" +_deprecated_backend2gui["Qt5Agg"] = "qt5" # And some backends that don't need GUI integration -del backend2gui["nbAgg"] -del backend2gui["agg"] -del backend2gui["svg"] -del backend2gui["pdf"] -del backend2gui["ps"] -del backend2gui["module://matplotlib_inline.backend_inline"] -del backend2gui["module://ipympl.backend_nbagg"] +del _deprecated_backend2gui["nbAgg"] +del _deprecated_backend2gui["agg"] +del _deprecated_backend2gui["svg"] +del _deprecated_backend2gui["pdf"] +del _deprecated_backend2gui["ps"] +del _deprecated_backend2gui["module://matplotlib_inline.backend_inline"] +del _deprecated_backend2gui["module://ipympl.backend_nbagg"] + + +# Deprecated attributes backends and backend2gui mostly following PEP 562. +def __getattr__(name): + if name in ("backends", "backend2gui"): + warnings.warn(f"{name} is deprecated", DeprecationWarning) + return globals()[f"_deprecated_{name}"] + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + #----------------------------------------------------------------------------- # Matplotlib utilities @@ -267,7 +279,7 @@ def select_figure_formats(shell, formats, **kwargs): [ f.pop(Figure, None) for f in shell.display_formatter.formatters.values() ] mplbackend = matplotlib.get_backend().lower() - if mplbackend == 'nbagg' or mplbackend == 'module://ipympl.backend_nbagg': + if mplbackend in ('nbagg', 'ipympl', 'widget', 'module://ipympl.backend_nbagg'): formatter = shell.display_formatter.ipython_display_formatter formatter.for_type(Figure, _reshow_nbagg_figure) @@ -318,9 +330,23 @@ def find_gui_and_backend(gui=None, gui_select=None): """ import matplotlib + if _matplotlib_manages_backends(): + backend_registry = matplotlib.backends.registry.backend_registry + + # gui argument may be a gui event loop or may be a backend name. + if gui in ("auto", None): + backend = matplotlib.rcParamsOrig['backend'] + backend, gui = backend_registry.resolve_backend(backend) + else: + backend, gui = backend_registry.resolve_gui_or_backend(gui) - has_unified_qt_backend = getattr(matplotlib, "__version_info__", (0, 0)) >= (3, 5) + return gui, backend + # Fallback to previous behaviour (Matplotlib < 3.9) + mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0)) + has_unified_qt_backend = mpl_version_info >= (3, 5) + + from IPython.core.pylabtools import backends backends_ = dict(backends) if not has_unified_qt_backend: backends_["qt"] = "qt5agg" @@ -338,6 +364,7 @@ def find_gui_and_backend(gui=None, gui_select=None): backend = matplotlib.rcParamsOrig['backend'] # In this case, we need to find what the appropriate gui selection call # should be for IPython, so we can activate inputhook accordingly + from IPython.core.pylabtools import backend2gui gui = backend2gui.get(backend, None) # If we have already had a gui active, we need it and inline are the @@ -346,6 +373,10 @@ def find_gui_and_backend(gui=None, gui_select=None): gui = gui_select backend = backends_[gui] + # Since IPython 8.23.0 use None for no gui event loop rather than "inline". + if gui == "inline": + gui = None + return gui, backend @@ -431,3 +462,9 @@ def configure_inline_support(shell, backend): ) configure_inline_support_orig(shell, backend) + + +def _matplotlib_manages_backends(): + import matplotlib + mpl_version_info = getattr(matplotlib, "__version_info__", (0, 0)) + return mpl_version_info >= (3, 9) diff --git a/IPython/core/shellapp.py b/IPython/core/shellapp.py index 29325a0..1b19b7e 100644 --- a/IPython/core/shellapp.py +++ b/IPython/core/shellapp.py @@ -31,8 +31,7 @@ from IPython.terminal import pt_inputhooks gui_keys = tuple(sorted(pt_inputhooks.backends) + sorted(pt_inputhooks.aliases)) -backend_keys = sorted(pylabtools.backends.keys()) -backend_keys.insert(0, 'auto') +backend_keys = [] shell_flags = {} diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index a06ad48..114a653 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -199,7 +199,7 @@ class TestPylabSwitch(object): assert s.pylab_gui_select == "qt" gui, backend = s.enable_matplotlib("inline") - assert gui == "inline" + assert gui is None assert s.pylab_gui_select == "qt" gui, backend = s.enable_matplotlib("qt") @@ -207,7 +207,7 @@ class TestPylabSwitch(object): assert s.pylab_gui_select == "qt" gui, backend = s.enable_matplotlib("inline") - assert gui == "inline" + assert gui is None assert s.pylab_gui_select == "qt" gui, backend = s.enable_matplotlib() @@ -217,11 +217,11 @@ class TestPylabSwitch(object): def test_inline(self): s = self.Shell() gui, backend = s.enable_matplotlib("inline") - assert gui == "inline" + assert gui is None assert s.pylab_gui_select == None gui, backend = s.enable_matplotlib("inline") - assert gui == "inline" + assert gui is None assert s.pylab_gui_select == None gui, backend = s.enable_matplotlib("qt") @@ -233,14 +233,14 @@ class TestPylabSwitch(object): ip = self.Shell() gui, backend = ip.enable_matplotlib("inline") - assert gui == "inline" + assert gui is None fmts = {'png'} active_mimes = {_fmt_mime_map[fmt] for fmt in fmts} pt.select_figure_formats(ip, fmts) gui, backend = ip.enable_matplotlib("inline") - assert gui == "inline" + assert gui is None for mime, f in ip.display_formatter.formatters.items(): if mime in active_mimes: @@ -254,7 +254,7 @@ class TestPylabSwitch(object): assert gui == "qt" assert s.pylab_gui_select == "qt" - gui, backend = s.enable_matplotlib("gtk") + gui, backend = s.enable_matplotlib("gtk3") assert gui == "qt" assert s.pylab_gui_select == "qt" diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index fcb816e..4b930e4 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -966,7 +966,7 @@ class TerminalInteractiveShell(InteractiveShell): if self._inputhook is not None and gui is None: self.active_eventloop = self._inputhook = None - if gui and (gui not in {"inline", "webagg"}): + if gui and (gui not in {None, "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) else: