You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			608 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			608 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Python
		
	
"""Event loop integration for the ZeroMQ-based kernels."""
 | 
						|
 | 
						|
# Copyright (c) IPython Development Team.
 | 
						|
# Distributed under the terms of the Modified BSD License.
 | 
						|
 | 
						|
import os
 | 
						|
import platform
 | 
						|
import sys
 | 
						|
from functools import partial
 | 
						|
 | 
						|
import zmq
 | 
						|
from packaging.version import Version as V
 | 
						|
from traitlets.config.application import Application
 | 
						|
 | 
						|
 | 
						|
def _use_appnope():
 | 
						|
    """Should we use appnope for dealing with OS X app nap?
 | 
						|
 | 
						|
    Checks if we are on OS X 10.9 or greater.
 | 
						|
    """
 | 
						|
    return sys.platform == "darwin" and V(platform.mac_ver()[0]) >= V("10.9")
 | 
						|
 | 
						|
 | 
						|
# mapping of keys to loop functions
 | 
						|
loop_map = {
 | 
						|
    "inline": None,
 | 
						|
    "nbagg": None,
 | 
						|
    "webagg": None,
 | 
						|
    "notebook": None,
 | 
						|
    "ipympl": None,
 | 
						|
    "widget": None,
 | 
						|
    None: None,
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
def register_integration(*toolkitnames):
 | 
						|
    """Decorator to register an event loop to integrate with the IPython kernel
 | 
						|
 | 
						|
    The decorator takes names to register the event loop as for the %gui magic.
 | 
						|
    You can provide alternative names for the same toolkit.
 | 
						|
 | 
						|
    The decorated function should take a single argument, the IPython kernel
 | 
						|
    instance, arrange for the event loop to call ``kernel.do_one_iteration()``
 | 
						|
    at least every ``kernel._poll_interval`` seconds, and start the event loop.
 | 
						|
 | 
						|
    :mod:`ipykernel.eventloops` provides and registers such functions
 | 
						|
    for a few common event loops.
 | 
						|
    """
 | 
						|
 | 
						|
    def decorator(func):
 | 
						|
        """Integration registration decorator."""
 | 
						|
        for name in toolkitnames:
 | 
						|
            loop_map[name] = func
 | 
						|
 | 
						|
        func.exit_hook = lambda kernel: None  # noqa: ARG005
 | 
						|
 | 
						|
        def exit_decorator(exit_func):
 | 
						|
            """@func.exit is now a decorator
 | 
						|
 | 
						|
            to register a function to be called on exit
 | 
						|
            """
 | 
						|
            func.exit_hook = exit_func
 | 
						|
            return exit_func
 | 
						|
 | 
						|
        func.exit = exit_decorator
 | 
						|
        return func
 | 
						|
 | 
						|
    return decorator
 | 
						|
 | 
						|
 | 
						|
def _notify_stream_qt(kernel):
 | 
						|
    import operator
 | 
						|
    from functools import lru_cache
 | 
						|
 | 
						|
    from IPython.external.qt_for_kernel import QtCore
 | 
						|
 | 
						|
    try:
 | 
						|
        from IPython.external.qt_for_kernel import enum_helper
 | 
						|
    except ImportError:
 | 
						|
 | 
						|
        @lru_cache(None)
 | 
						|
        def enum_helper(name):
 | 
						|
            return operator.attrgetter(name.rpartition(".")[0])(sys.modules[QtCore.__package__])
 | 
						|
 | 
						|
    def exit_loop():
 | 
						|
        """fall back to main loop"""
 | 
						|
        kernel._qt_notifier.setEnabled(False)
 | 
						|
        kernel.app.qt_event_loop.quit()
 | 
						|
 | 
						|
    def process_stream_events():
 | 
						|
        """fall back to main loop when there's a socket event"""
 | 
						|
        # call flush to ensure that the stream doesn't lose events
 | 
						|
        # due to our consuming of the edge-triggered FD
 | 
						|
        # flush returns the number of events consumed.
 | 
						|
        # if there were any, wake it up
 | 
						|
        if kernel.shell_stream.flush(limit=1):
 | 
						|
            exit_loop()
 | 
						|
 | 
						|
    if not hasattr(kernel, "_qt_notifier"):
 | 
						|
        fd = kernel.shell_stream.getsockopt(zmq.FD)
 | 
						|
        kernel._qt_notifier = QtCore.QSocketNotifier(
 | 
						|
            fd, enum_helper("QtCore.QSocketNotifier.Type").Read, kernel.app.qt_event_loop
 | 
						|
        )
 | 
						|
        kernel._qt_notifier.activated.connect(process_stream_events)
 | 
						|
    else:
 | 
						|
        kernel._qt_notifier.setEnabled(True)
 | 
						|
 | 
						|
    # allow for scheduling exits from the loop in case a timeout needs to
 | 
						|
    # be set from the kernel level
 | 
						|
    def _schedule_exit(delay):
 | 
						|
        """schedule fall back to main loop in [delay] seconds"""
 | 
						|
        # The signatures of QtCore.QTimer.singleShot are inconsistent between PySide and PyQt
 | 
						|
        # if setting the TimerType, so we create a timer explicitly and store it
 | 
						|
        # to avoid a memory leak.
 | 
						|
        # PreciseTimer is needed so we exit after _at least_ the specified delay, not within 5% of it
 | 
						|
        if not hasattr(kernel, "_qt_timer"):
 | 
						|
            kernel._qt_timer = QtCore.QTimer(kernel.app)
 | 
						|
            kernel._qt_timer.setSingleShot(True)
 | 
						|
            kernel._qt_timer.setTimerType(enum_helper("QtCore.Qt.TimerType").PreciseTimer)
 | 
						|
            kernel._qt_timer.timeout.connect(exit_loop)
 | 
						|
        kernel._qt_timer.start(int(1000 * delay))
 | 
						|
 | 
						|
    loop_qt._schedule_exit = _schedule_exit
 | 
						|
 | 
						|
    # there may already be unprocessed events waiting.
 | 
						|
    # these events will not wake zmq's edge-triggered FD
 | 
						|
    # since edge-triggered notification only occurs on new i/o activity.
 | 
						|
    # process all the waiting events immediately
 | 
						|
    # so we start in a clean state ensuring that any new i/o events will notify.
 | 
						|
    # schedule first call on the eventloop as soon as it's running,
 | 
						|
    # so we don't block here processing events
 | 
						|
    QtCore.QTimer.singleShot(0, process_stream_events)
 | 
						|
 | 
						|
 | 
						|
@register_integration("qt", "qt5", "qt6")
 | 
						|
def loop_qt(kernel):
 | 
						|
    """Event loop for all supported versions of Qt."""
 | 
						|
    _notify_stream_qt(kernel)  # install hook to stop event loop.
 | 
						|
 | 
						|
    # Start the event loop.
 | 
						|
    kernel.app._in_event_loop = True
 | 
						|
 | 
						|
    # `exec` blocks until there's ZMQ activity.
 | 
						|
    el = kernel.app.qt_event_loop  # for brevity
 | 
						|
    el.exec() if hasattr(el, "exec") else el.exec_()
 | 
						|
    kernel.app._in_event_loop = False
 | 
						|
 | 
						|
 | 
						|
# NOTE: To be removed in version 7
 | 
						|
loop_qt5 = loop_qt
 | 
						|
 | 
						|
 | 
						|
# exit and watch are the same for qt 4 and 5
 | 
						|
@loop_qt.exit
 | 
						|
def loop_qt_exit(kernel):
 | 
						|
    kernel.app.exit()
 | 
						|
 | 
						|
 | 
						|
def _loop_wx(app):
 | 
						|
    """Inner-loop for running the Wx eventloop
 | 
						|
 | 
						|
    Pulled from guisupport.start_event_loop in IPython < 5.2,
 | 
						|
    since IPython 5.2 only checks `get_ipython().active_eventloop` is defined,
 | 
						|
    rather than if the eventloop is actually running.
 | 
						|
    """
 | 
						|
    app._in_event_loop = True
 | 
						|
    app.MainLoop()
 | 
						|
    app._in_event_loop = False
 | 
						|
 | 
						|
 | 
						|
@register_integration("wx")
 | 
						|
def loop_wx(kernel):
 | 
						|
    """Start a kernel with wx event loop support."""
 | 
						|
 | 
						|
    import wx
 | 
						|
 | 
						|
    # Wx uses milliseconds
 | 
						|
    poll_interval = int(1000 * kernel._poll_interval)
 | 
						|
 | 
						|
    def wake():
 | 
						|
        """wake from wx"""
 | 
						|
        if kernel.shell_stream.flush(limit=1):
 | 
						|
            kernel.app.ExitMainLoop()
 | 
						|
            return
 | 
						|
 | 
						|
    # We have to put the wx.Timer in a wx.Frame for it to fire properly.
 | 
						|
    # We make the Frame hidden when we create it in the main app below.
 | 
						|
    class TimerFrame(wx.Frame):  # type:ignore[misc]
 | 
						|
        def __init__(self, func):
 | 
						|
            wx.Frame.__init__(self, None, -1)
 | 
						|
            self.timer = wx.Timer(self)
 | 
						|
            # Units for the timer are in milliseconds
 | 
						|
            self.timer.Start(poll_interval)
 | 
						|
            self.Bind(wx.EVT_TIMER, self.on_timer)
 | 
						|
            self.func = func
 | 
						|
 | 
						|
        def on_timer(self, event):
 | 
						|
            self.func()
 | 
						|
 | 
						|
    # We need a custom wx.App to create our Frame subclass that has the
 | 
						|
    # wx.Timer to defer back to the tornado event loop.
 | 
						|
    class IPWxApp(wx.App):  # type:ignore[misc]
 | 
						|
        def OnInit(self):
 | 
						|
            self.frame = TimerFrame(wake)
 | 
						|
            self.frame.Show(False)
 | 
						|
            return True
 | 
						|
 | 
						|
    # The redirect=False here makes sure that wx doesn't replace
 | 
						|
    # sys.stdout/stderr with its own classes.
 | 
						|
    if not (getattr(kernel, "app", None) and isinstance(kernel.app, wx.App)):
 | 
						|
        kernel.app = IPWxApp(redirect=False)
 | 
						|
 | 
						|
    # The import of wx on Linux sets the handler for signal.SIGINT
 | 
						|
    # to 0.  This is a bug in wx or gtk.  We fix by just setting it
 | 
						|
    # back to the Python default.
 | 
						|
    import signal
 | 
						|
 | 
						|
    if not callable(signal.getsignal(signal.SIGINT)):
 | 
						|
        signal.signal(signal.SIGINT, signal.default_int_handler)
 | 
						|
 | 
						|
    _loop_wx(kernel.app)
 | 
						|
 | 
						|
 | 
						|
@loop_wx.exit
 | 
						|
def loop_wx_exit(kernel):
 | 
						|
    """Exit the wx loop."""
 | 
						|
    import wx
 | 
						|
 | 
						|
    wx.Exit()
 | 
						|
 | 
						|
 | 
						|
@register_integration("tk")
 | 
						|
def loop_tk(kernel):
 | 
						|
    """Start a kernel with the Tk event loop."""
 | 
						|
 | 
						|
    from tkinter import READABLE, Tk
 | 
						|
 | 
						|
    app = Tk()
 | 
						|
    # Capability detection:
 | 
						|
    # per https://docs.python.org/3/library/tkinter.html#file-handlers
 | 
						|
    # file handlers are not available on Windows
 | 
						|
    if hasattr(app, "createfilehandler"):
 | 
						|
        # A basic wrapper for structural similarity with the Windows version
 | 
						|
        class BasicAppWrapper:
 | 
						|
            def __init__(self, app):
 | 
						|
                self.app = app
 | 
						|
                self.app.withdraw()
 | 
						|
 | 
						|
        def exit_loop():
 | 
						|
            """fall back to main loop"""
 | 
						|
            app.tk.deletefilehandler(kernel.shell_stream.getsockopt(zmq.FD))
 | 
						|
            app.quit()
 | 
						|
            app.destroy()
 | 
						|
            del kernel.app_wrapper
 | 
						|
 | 
						|
        def process_stream_events(*a, **kw):
 | 
						|
            """fall back to main loop when there's a socket event"""
 | 
						|
            if kernel.shell_stream.flush(limit=1):
 | 
						|
                exit_loop()
 | 
						|
 | 
						|
        # allow for scheduling exits from the loop in case a timeout needs to
 | 
						|
        # be set from the kernel level
 | 
						|
        def _schedule_exit(delay):
 | 
						|
            """schedule fall back to main loop in [delay] seconds"""
 | 
						|
            app.after(int(1000 * delay), exit_loop)
 | 
						|
 | 
						|
        loop_tk._schedule_exit = _schedule_exit
 | 
						|
 | 
						|
        # For Tkinter, we create a Tk object and call its withdraw method.
 | 
						|
        kernel.app_wrapper = BasicAppWrapper(app)
 | 
						|
        app.tk.createfilehandler(
 | 
						|
            kernel.shell_stream.getsockopt(zmq.FD), READABLE, process_stream_events
 | 
						|
        )
 | 
						|
        # schedule initial call after start
 | 
						|
        app.after(0, process_stream_events)
 | 
						|
 | 
						|
        app.mainloop()
 | 
						|
 | 
						|
    else:
 | 
						|
        import asyncio
 | 
						|
 | 
						|
        import nest_asyncio
 | 
						|
 | 
						|
        nest_asyncio.apply()
 | 
						|
 | 
						|
        doi = kernel.do_one_iteration
 | 
						|
        # Tk uses milliseconds
 | 
						|
        poll_interval = int(1000 * kernel._poll_interval)
 | 
						|
 | 
						|
        class TimedAppWrapper:
 | 
						|
            def __init__(self, app, func):
 | 
						|
                self.app = app
 | 
						|
                self.app.withdraw()
 | 
						|
                self.func = func
 | 
						|
 | 
						|
            def on_timer(self):
 | 
						|
                loop = asyncio.get_event_loop()
 | 
						|
                try:
 | 
						|
                    loop.run_until_complete(self.func())
 | 
						|
                except Exception:
 | 
						|
                    kernel.log.exception("Error in message handler")
 | 
						|
                self.app.after(poll_interval, self.on_timer)
 | 
						|
 | 
						|
            def start(self):
 | 
						|
                self.on_timer()  # Call it once to get things going.
 | 
						|
                self.app.mainloop()
 | 
						|
 | 
						|
        kernel.app_wrapper = TimedAppWrapper(app, doi)
 | 
						|
        kernel.app_wrapper.start()
 | 
						|
 | 
						|
 | 
						|
@loop_tk.exit
 | 
						|
def loop_tk_exit(kernel):
 | 
						|
    """Exit the tk loop."""
 | 
						|
    try:
 | 
						|
        kernel.app_wrapper.app.destroy()
 | 
						|
        del kernel.app_wrapper
 | 
						|
    except (RuntimeError, AttributeError):
 | 
						|
        pass
 | 
						|
 | 
						|
 | 
						|
@register_integration("gtk")
 | 
						|
def loop_gtk(kernel):
 | 
						|
    """Start the kernel, coordinating with the GTK event loop"""
 | 
						|
    from .gui.gtkembed import GTKEmbed
 | 
						|
 | 
						|
    gtk_kernel = GTKEmbed(kernel)
 | 
						|
    gtk_kernel.start()
 | 
						|
    kernel._gtk = gtk_kernel
 | 
						|
 | 
						|
 | 
						|
@loop_gtk.exit
 | 
						|
def loop_gtk_exit(kernel):
 | 
						|
    """Exit the gtk loop."""
 | 
						|
    kernel._gtk.stop()
 | 
						|
 | 
						|
 | 
						|
@register_integration("gtk3")
 | 
						|
def loop_gtk3(kernel):
 | 
						|
    """Start the kernel, coordinating with the GTK event loop"""
 | 
						|
    from .gui.gtk3embed import GTKEmbed
 | 
						|
 | 
						|
    gtk_kernel = GTKEmbed(kernel)
 | 
						|
    gtk_kernel.start()
 | 
						|
    kernel._gtk = gtk_kernel
 | 
						|
 | 
						|
 | 
						|
@loop_gtk3.exit
 | 
						|
def loop_gtk3_exit(kernel):
 | 
						|
    """Exit the gtk3 loop."""
 | 
						|
    kernel._gtk.stop()
 | 
						|
 | 
						|
 | 
						|
@register_integration("osx")
 | 
						|
def loop_cocoa(kernel):
 | 
						|
    """Start the kernel, coordinating with the Cocoa CFRunLoop event loop
 | 
						|
    via the matplotlib MacOSX backend.
 | 
						|
    """
 | 
						|
    from ._eventloop_macos import mainloop, stop
 | 
						|
 | 
						|
    real_excepthook = sys.excepthook
 | 
						|
 | 
						|
    def handle_int(etype, value, tb):
 | 
						|
        """don't let KeyboardInterrupts look like crashes"""
 | 
						|
        # wake the eventloop when we get a signal
 | 
						|
        stop()
 | 
						|
        if etype is KeyboardInterrupt:
 | 
						|
            print("KeyboardInterrupt caught in CFRunLoop", file=sys.__stdout__)
 | 
						|
        else:
 | 
						|
            real_excepthook(etype, value, tb)
 | 
						|
 | 
						|
    while not kernel.shell.exit_now:
 | 
						|
        try:
 | 
						|
            # double nested try/except, to properly catch KeyboardInterrupt
 | 
						|
            # due to pyzmq Issue #130
 | 
						|
            try:
 | 
						|
                # don't let interrupts during mainloop invoke crash_handler:
 | 
						|
                sys.excepthook = handle_int
 | 
						|
                mainloop(kernel._poll_interval)
 | 
						|
                if kernel.shell_stream.flush(limit=1):
 | 
						|
                    # events to process, return control to kernel
 | 
						|
                    return
 | 
						|
            except BaseException:
 | 
						|
                raise
 | 
						|
        except KeyboardInterrupt:
 | 
						|
            # Ctrl-C shouldn't crash the kernel
 | 
						|
            print("KeyboardInterrupt caught in kernel", file=sys.__stdout__)
 | 
						|
        finally:
 | 
						|
            # ensure excepthook is restored
 | 
						|
            sys.excepthook = real_excepthook
 | 
						|
 | 
						|
 | 
						|
@loop_cocoa.exit
 | 
						|
def loop_cocoa_exit(kernel):
 | 
						|
    """Exit the cocoa loop."""
 | 
						|
    from ._eventloop_macos import stop
 | 
						|
 | 
						|
    stop()
 | 
						|
 | 
						|
 | 
						|
@register_integration("asyncio")
 | 
						|
def loop_asyncio(kernel):
 | 
						|
    """Start a kernel with asyncio event loop support."""
 | 
						|
    import asyncio
 | 
						|
 | 
						|
    loop = asyncio.get_event_loop()
 | 
						|
    # loop is already running (e.g. tornado 5), nothing left to do
 | 
						|
    if loop.is_running():
 | 
						|
        return
 | 
						|
 | 
						|
    if loop.is_closed():
 | 
						|
        # main loop is closed, create a new one
 | 
						|
        loop = asyncio.new_event_loop()
 | 
						|
        asyncio.set_event_loop(loop)
 | 
						|
    loop._should_close = False  # type:ignore[attr-defined]
 | 
						|
 | 
						|
    # pause eventloop when there's an event on a zmq socket
 | 
						|
    def process_stream_events(stream):
 | 
						|
        """fall back to main loop when there's a socket event"""
 | 
						|
        if stream.flush(limit=1):
 | 
						|
            loop.stop()
 | 
						|
 | 
						|
    notifier = partial(process_stream_events, kernel.shell_stream)
 | 
						|
    loop.add_reader(kernel.shell_stream.getsockopt(zmq.FD), notifier)
 | 
						|
    loop.call_soon(notifier)
 | 
						|
 | 
						|
    while True:
 | 
						|
        error = None
 | 
						|
        try:
 | 
						|
            loop.run_forever()
 | 
						|
        except KeyboardInterrupt:
 | 
						|
            continue
 | 
						|
        except Exception as e:
 | 
						|
            error = e
 | 
						|
        if loop._should_close:  # type:ignore[attr-defined]
 | 
						|
            loop.close()
 | 
						|
        if error is not None:
 | 
						|
            raise error
 | 
						|
        break
 | 
						|
 | 
						|
 | 
						|
@loop_asyncio.exit
 | 
						|
def loop_asyncio_exit(kernel):
 | 
						|
    """Exit hook for asyncio"""
 | 
						|
    import asyncio
 | 
						|
 | 
						|
    loop = asyncio.get_event_loop()
 | 
						|
 | 
						|
    async def close_loop():
 | 
						|
        if hasattr(loop, "shutdown_asyncgens"):
 | 
						|
            yield loop.shutdown_asyncgens()
 | 
						|
        loop._should_close = True  # type:ignore[attr-defined]
 | 
						|
        loop.stop()
 | 
						|
 | 
						|
    if loop.is_running():
 | 
						|
        close_loop()
 | 
						|
 | 
						|
    elif not loop.is_closed():
 | 
						|
        loop.run_until_complete(close_loop)  # type:ignore[arg-type]
 | 
						|
        loop.close()
 | 
						|
 | 
						|
 | 
						|
def set_qt_api_env_from_gui(gui):
 | 
						|
    """
 | 
						|
    Sets the QT_API environment variable by trying to import PyQtx or PySidex.
 | 
						|
 | 
						|
    The user can generically request `qt` or a specific Qt version, e.g. `qt6`.
 | 
						|
    For a generic Qt request, we let the mechanism in IPython choose the best
 | 
						|
    available version by leaving the `QT_API` environment variable blank.
 | 
						|
 | 
						|
    For specific versions, we check to see whether the PyQt or PySide
 | 
						|
    implementations are present and set `QT_API` accordingly to indicate to
 | 
						|
    IPython which version we want. If neither implementation is present, we
 | 
						|
    leave the environment variable set so IPython will generate a helpful error
 | 
						|
    message.
 | 
						|
 | 
						|
    Notes
 | 
						|
    -----
 | 
						|
    - If the environment variable is already set, it will be used unchanged,
 | 
						|
      regardless of what the user requested.
 | 
						|
    """
 | 
						|
    qt_api = os.environ.get("QT_API", None)
 | 
						|
 | 
						|
    from IPython.external.qt_loaders import (
 | 
						|
        QT_API_PYQT5,
 | 
						|
        QT_API_PYQT6,
 | 
						|
        QT_API_PYSIDE2,
 | 
						|
        QT_API_PYSIDE6,
 | 
						|
        loaded_api,
 | 
						|
    )
 | 
						|
 | 
						|
    loaded = loaded_api()
 | 
						|
 | 
						|
    qt_env2gui = {
 | 
						|
        QT_API_PYSIDE2: "qt5",
 | 
						|
        QT_API_PYQT5: "qt5",
 | 
						|
        QT_API_PYSIDE6: "qt6",
 | 
						|
        QT_API_PYQT6: "qt6",
 | 
						|
    }
 | 
						|
    if loaded is not None and gui != "qt" and qt_env2gui[loaded] != gui:
 | 
						|
        print(f"Cannot switch Qt versions for this session; you must use {qt_env2gui[loaded]}.")
 | 
						|
        return
 | 
						|
 | 
						|
    if qt_api is not None and gui != "qt":
 | 
						|
        if qt_env2gui[qt_api] != gui:
 | 
						|
            print(
 | 
						|
                f'Request for "{gui}" will be ignored because `QT_API` '
 | 
						|
                f'environment variable is set to "{qt_api}"'
 | 
						|
            )
 | 
						|
            return
 | 
						|
    else:
 | 
						|
        if gui == "qt5":
 | 
						|
            try:
 | 
						|
                import PyQt5  # noqa: F401
 | 
						|
 | 
						|
                os.environ["QT_API"] = "pyqt5"
 | 
						|
            except ImportError:
 | 
						|
                try:
 | 
						|
                    import PySide2  # noqa: F401
 | 
						|
 | 
						|
                    os.environ["QT_API"] = "pyside2"
 | 
						|
                except ImportError:
 | 
						|
                    os.environ["QT_API"] = "pyqt5"
 | 
						|
        elif gui == "qt6":
 | 
						|
            try:
 | 
						|
                import PyQt6  # noqa: F401
 | 
						|
 | 
						|
                os.environ["QT_API"] = "pyqt6"
 | 
						|
            except ImportError:
 | 
						|
                try:
 | 
						|
                    import PySide6  # noqa: F401
 | 
						|
 | 
						|
                    os.environ["QT_API"] = "pyside6"
 | 
						|
                except ImportError:
 | 
						|
                    os.environ["QT_API"] = "pyqt6"
 | 
						|
        elif gui == "qt":
 | 
						|
            # Don't set QT_API; let IPython logic choose the version.
 | 
						|
            if "QT_API" in os.environ:
 | 
						|
                del os.environ["QT_API"]
 | 
						|
        else:
 | 
						|
            print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
 | 
						|
            return
 | 
						|
 | 
						|
    # Do the actual import now that the environment variable is set to make sure it works.
 | 
						|
    try:
 | 
						|
        pass
 | 
						|
    except Exception as e:
 | 
						|
        # Clear the environment variable for the next attempt.
 | 
						|
        if "QT_API" in os.environ:
 | 
						|
            del os.environ["QT_API"]
 | 
						|
            print(f"QT_API couldn't be set due to error {e}")
 | 
						|
        return
 | 
						|
 | 
						|
 | 
						|
def make_qt_app_for_kernel(gui, kernel):
 | 
						|
    """Sets the `QT_API` environment variable if it isn't already set."""
 | 
						|
    if hasattr(kernel, "app"):
 | 
						|
        # Kernel is already running a Qt event loop, so there's no need to
 | 
						|
        # create another app for it.
 | 
						|
        return
 | 
						|
 | 
						|
    set_qt_api_env_from_gui(gui)
 | 
						|
 | 
						|
    # This import is guaranteed to work now:
 | 
						|
    from IPython.external.qt_for_kernel import QtCore
 | 
						|
    from IPython.lib.guisupport import get_app_qt4
 | 
						|
 | 
						|
    kernel.app = get_app_qt4([" "])
 | 
						|
    kernel.app.qt_event_loop = QtCore.QEventLoop(kernel.app)
 | 
						|
 | 
						|
 | 
						|
def enable_gui(gui, kernel=None):
 | 
						|
    """Enable integration with a given GUI"""
 | 
						|
    if gui not in loop_map:
 | 
						|
        e = f"Invalid GUI request {gui!r}, valid ones are:{loop_map.keys()}"
 | 
						|
        raise ValueError(e)
 | 
						|
    if kernel is None:
 | 
						|
        if Application.initialized():
 | 
						|
            kernel = getattr(Application.instance(), "kernel", None)
 | 
						|
        if kernel is None:
 | 
						|
            msg = (
 | 
						|
                "You didn't specify a kernel,"
 | 
						|
                " and no IPython Application with a kernel appears to be running."
 | 
						|
            )
 | 
						|
            raise RuntimeError(msg)
 | 
						|
    if gui is None:
 | 
						|
        # User wants to turn off integration; clear any evidence if Qt was the last one.
 | 
						|
        if hasattr(kernel, "app"):
 | 
						|
            delattr(kernel, "app")
 | 
						|
        if hasattr(kernel, "_qt_notifier"):
 | 
						|
            delattr(kernel, "_qt_notifier")
 | 
						|
        if hasattr(kernel, "_qt_timer"):
 | 
						|
            delattr(kernel, "_qt_timer")
 | 
						|
    else:
 | 
						|
        if gui.startswith("qt"):
 | 
						|
            # Prepare the kernel here so any exceptions are displayed in the client.
 | 
						|
            make_qt_app_for_kernel(gui, kernel)
 | 
						|
 | 
						|
    loop = loop_map[gui]
 | 
						|
    if (
 | 
						|
        loop and kernel.eventloop is not None and kernel.eventloop is not loop  # type:ignore[unreachable]
 | 
						|
    ):
 | 
						|
        msg = "Cannot activate multiple GUI eventloops"  # type:ignore[unreachable]
 | 
						|
        raise RuntimeError(msg)
 | 
						|
    kernel.eventloop = loop
 | 
						|
    # We set `eventloop`; the function the user chose is executed in `Kernel.enter_eventloop`, thus
 | 
						|
    # any exceptions raised during the event loop will not be shown in the client.
 |