|
"""Event loop integration for the ZeroMQ-based kernels.""" |
|
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
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 |
|
|
|
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""" |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
def _schedule_exit(delay): |
|
"""schedule fall back to main loop in [delay] seconds""" |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
kernel.app._in_event_loop = True |
|
|
|
|
|
el = kernel.app.qt_event_loop |
|
el.exec() if hasattr(el, "exec") else el.exec_() |
|
kernel.app._in_event_loop = False |
|
|
|
|
|
|
|
loop_qt5 = loop_qt |
|
|
|
|
|
|
|
@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 |
|
|
|
|
|
poll_interval = int(1000 * kernel._poll_interval) |
|
|
|
def wake(): |
|
"""wake from wx""" |
|
if kernel.shell_stream.flush(limit=1): |
|
kernel.app.ExitMainLoop() |
|
return |
|
|
|
|
|
|
|
class TimerFrame(wx.Frame): |
|
def __init__(self, func): |
|
wx.Frame.__init__(self, None, -1) |
|
self.timer = wx.Timer(self) |
|
|
|
self.timer.Start(poll_interval) |
|
self.Bind(wx.EVT_TIMER, self.on_timer) |
|
self.func = func |
|
|
|
def on_timer(self, event): |
|
self.func() |
|
|
|
|
|
|
|
class IPWxApp(wx.App): |
|
def OnInit(self): |
|
self.frame = TimerFrame(wake) |
|
self.frame.Show(False) |
|
return True |
|
|
|
|
|
|
|
if not (getattr(kernel, "app", None) and isinstance(kernel.app, wx.App)): |
|
kernel.app = IPWxApp(redirect=False) |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
if hasattr(app, "createfilehandler"): |
|
|
|
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() |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
kernel.app_wrapper = BasicAppWrapper(app) |
|
app.tk.createfilehandler( |
|
kernel.shell_stream.getsockopt(zmq.FD), READABLE, process_stream_events |
|
) |
|
|
|
app.after(0, process_stream_events) |
|
|
|
app.mainloop() |
|
|
|
else: |
|
import asyncio |
|
|
|
import nest_asyncio |
|
|
|
nest_asyncio.apply() |
|
|
|
doi = kernel.do_one_iteration |
|
|
|
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() |
|
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""" |
|
|
|
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: |
|
|
|
|
|
try: |
|
|
|
sys.excepthook = handle_int |
|
mainloop(kernel._poll_interval) |
|
if kernel.shell_stream.flush(limit=1): |
|
|
|
return |
|
except BaseException: |
|
raise |
|
except KeyboardInterrupt: |
|
|
|
print("KeyboardInterrupt caught in kernel", file=sys.__stdout__) |
|
finally: |
|
|
|
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() |
|
|
|
if loop.is_running(): |
|
return |
|
|
|
if loop.is_closed(): |
|
|
|
loop = asyncio.new_event_loop() |
|
asyncio.set_event_loop(loop) |
|
loop._should_close = False |
|
|
|
|
|
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: |
|
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 |
|
loop.stop() |
|
|
|
if loop.is_running(): |
|
close_loop() |
|
|
|
elif not loop.is_closed(): |
|
loop.run_until_complete(close_loop) |
|
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 |
|
|
|
os.environ["QT_API"] = "pyqt5" |
|
except ImportError: |
|
try: |
|
import PySide2 |
|
|
|
os.environ["QT_API"] = "pyside2" |
|
except ImportError: |
|
os.environ["QT_API"] = "pyqt5" |
|
elif gui == "qt6": |
|
try: |
|
import PyQt6 |
|
|
|
os.environ["QT_API"] = "pyqt6" |
|
except ImportError: |
|
try: |
|
import PySide6 |
|
|
|
os.environ["QT_API"] = "pyside6" |
|
except ImportError: |
|
os.environ["QT_API"] = "pyqt6" |
|
elif gui == "qt": |
|
|
|
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 |
|
|
|
|
|
try: |
|
pass |
|
except Exception as e: |
|
|
|
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"): |
|
|
|
|
|
return |
|
|
|
set_qt_api_env_from_gui(gui) |
|
|
|
|
|
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: |
|
|
|
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"): |
|
|
|
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 |
|
): |
|
msg = "Cannot activate multiple GUI eventloops" |
|
raise RuntimeError(msg) |
|
kernel.eventloop = loop |
|
|
|
|
|
|