eventloops.py
277 lines
| 8.8 KiB
| text/x-python
|
PythonLexer
Fernando Perez
|
r5472 | # encoding: utf-8 | ||
MinRK
|
r18083 | """Event loop integration for the ZeroMQ-based kernels.""" | ||
Fernando Perez
|
r5472 | |||
MinRK
|
r18083 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
Fernando Perez
|
r5472 | |||
MinRK
|
r18083 | import os | ||
Fernando Perez
|
r5472 | import sys | ||
import zmq | ||||
MinRK
|
r6880 | from IPython.config.application import Application | ||
Fernando Perez
|
r5472 | from IPython.utils import io | ||
MinRK
|
r6880 | |||
MinRK
|
r13583 | def _on_os_x_10_9(): | ||
import platform | ||||
from distutils.version import LooseVersion as V | ||||
return sys.platform == 'darwin' and V(platform.mac_ver()[0]) >= V('10.9') | ||||
def _notify_stream_qt(kernel, stream): | ||||
from IPython.external.qt_for_kernel import QtCore | ||||
if _on_os_x_10_9() and kernel._darwin_app_nap: | ||||
from IPython.external.appnope import nope_scope as context | ||||
else: | ||||
Puneeth Chaganti
|
r13752 | from IPython.core.interactiveshell import NoOpContext as context | ||
MinRK
|
r13583 | def process_stream_events(): | ||
while stream.getsockopt(zmq.EVENTS) & zmq.POLLIN: | ||||
with context(): | ||||
kernel.do_one_iteration() | ||||
fd = stream.getsockopt(zmq.FD) | ||||
notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app) | ||||
notifier.activated.connect(process_stream_events) | ||||
Thomas Kluyver
|
r17892 | # mapping of keys to loop functions | ||
loop_map = { | ||||
'inline': None, | ||||
MinRK
|
r18091 | 'nbagg': None, | ||
Thomas Kluyver
|
r17892 | 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:`IPython.kernel.zmq.eventloops` provides and registers such functions | ||||
for a few common event loops. | ||||
""" | ||||
def decorator(func): | ||||
for name in toolkitnames: | ||||
loop_map[name] = func | ||||
return func | ||||
return decorator | ||||
@register_integration('qt', 'qt4') | ||||
Fernando Perez
|
r5472 | def loop_qt4(kernel): | ||
"""Start a kernel with PyQt4 event loop integration.""" | ||||
from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4 | ||||
kernel.app = get_app_qt4([" "]) | ||||
kernel.app.setQuitOnLastWindowClosed(False) | ||||
MinRK
|
r13583 | |||
for s in kernel.shell_streams: | ||||
_notify_stream_qt(kernel, s) | ||||
Fernando Perez
|
r5472 | start_event_loop_qt4(kernel.app) | ||
MinRK
|
r18083 | @register_integration('qt5') | ||
def loop_qt5(kernel): | ||||
"""Start a kernel with PyQt5 event loop integration.""" | ||||
os.environ['QT_API'] = 'pyqt5' | ||||
return loop_qt4(kernel) | ||||
Fernando Perez
|
r5472 | |||
Thomas Kluyver
|
r17892 | @register_integration('wx') | ||
Fernando Perez
|
r5472 | def loop_wx(kernel): | ||
"""Start a kernel with wx event loop support.""" | ||||
import wx | ||||
from IPython.lib.guisupport import start_event_loop_wx | ||||
MinRK
|
r13583 | |||
if _on_os_x_10_9() and kernel._darwin_app_nap: | ||||
# we don't hook up App Nap contexts for Wx, | ||||
# just disable it outright. | ||||
from IPython.external.appnope import nope | ||||
nope() | ||||
Fernando Perez
|
r5472 | |||
doi = kernel.do_one_iteration | ||||
# Wx uses milliseconds | ||||
poll_interval = int(1000*kernel._poll_interval) | ||||
# 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): | ||||
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 drive the ZMQ event loop. | ||||
class IPWxApp(wx.App): | ||||
def OnInit(self): | ||||
self.frame = TimerFrame(doi) | ||||
self.frame.Show(False) | ||||
return True | ||||
# The redirect=False here makes sure that wx doesn't replace | ||||
# sys.stdout/stderr with its own classes. | ||||
kernel.app = IPWxApp(redirect=False) | ||||
Pankaj Pandey
|
r6434 | |||
# 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) | ||||
Fernando Perez
|
r5472 | start_event_loop_wx(kernel.app) | ||
Thomas Kluyver
|
r17892 | @register_integration('tk') | ||
Fernando Perez
|
r5472 | def loop_tk(kernel): | ||
"""Start a kernel with the Tk event loop.""" | ||||
Thomas Kluyver
|
r13354 | try: | ||
from tkinter import Tk # Py 3 | ||||
except ImportError: | ||||
from Tkinter import Tk # Py 2 | ||||
Fernando Perez
|
r5472 | doi = kernel.do_one_iteration | ||
# Tk uses milliseconds | ||||
poll_interval = int(1000*kernel._poll_interval) | ||||
# For Tkinter, we create a Tk object and call its withdraw method. | ||||
class Timer(object): | ||||
def __init__(self, func): | ||||
Thomas Kluyver
|
r13354 | self.app = Tk() | ||
Fernando Perez
|
r5472 | self.app.withdraw() | ||
self.func = func | ||||
def on_timer(self): | ||||
self.func() | ||||
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.timer = Timer(doi) | ||||
kernel.timer.start() | ||||
Thomas Kluyver
|
r17892 | @register_integration('gtk') | ||
Fernando Perez
|
r5472 | 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() | ||||
Thomas Kluyver
|
r17892 | @register_integration('gtk3') | ||
John Stowers
|
r17404 | 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() | ||||
Thomas Kluyver
|
r17892 | @register_integration('osx') | ||
Fernando Perez
|
r5472 | def loop_cocoa(kernel): | ||
"""Start the kernel, coordinating with the Cocoa CFRunLoop event loop | ||||
via the matplotlib MacOSX backend. | ||||
""" | ||||
import matplotlib | ||||
if matplotlib.__version__ < '1.1.0': | ||||
kernel.log.warn( | ||||
"MacOSX backend in matplotlib %s doesn't have a Timer, " | ||||
"falling back on Tk for CFRunLoop integration. Note that " | ||||
"even this won't work if Tk is linked against X11 instead of " | ||||
"Cocoa (e.g. EPD). To use the MacOSX backend in the kernel, " | ||||
"you must use matplotlib >= 1.1.0, or a native libtk." | ||||
) | ||||
return loop_tk(kernel) | ||||
from matplotlib.backends.backend_macosx import TimerMac, show | ||||
# scale interval for sec->ms | ||||
poll_interval = int(1000*kernel._poll_interval) | ||||
real_excepthook = sys.excepthook | ||||
def handle_int(etype, value, tb): | ||||
"""don't let KeyboardInterrupts look like crashes""" | ||||
if etype is KeyboardInterrupt: | ||||
io.raw_print("KeyboardInterrupt caught in CFRunLoop") | ||||
else: | ||||
real_excepthook(etype, value, tb) | ||||
# add doi() as a Timer to the CFRunLoop | ||||
def doi(): | ||||
# restore excepthook during IPython code | ||||
sys.excepthook = real_excepthook | ||||
kernel.do_one_iteration() | ||||
# and back: | ||||
sys.excepthook = handle_int | ||||
t = TimerMac(poll_interval) | ||||
t.add_callback(doi) | ||||
t.start() | ||||
# but still need a Poller for when there are no active windows, | ||||
# during which time mainloop() returns immediately | ||||
poller = zmq.Poller() | ||||
MinRK
|
r6790 | if kernel.control_stream: | ||
poller.register(kernel.control_stream.socket, zmq.POLLIN) | ||||
for stream in kernel.shell_streams: | ||||
poller.register(stream.socket, zmq.POLLIN) | ||||
Fernando Perez
|
r5472 | |||
while True: | ||||
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 | ||||
show.mainloop() | ||||
sys.excepthook = real_excepthook | ||||
# use poller if mainloop returned (no windows) | ||||
# scale by extra factor of 10, since it's a real poll | ||||
poller.poll(10*poll_interval) | ||||
kernel.do_one_iteration() | ||||
except: | ||||
raise | ||||
except KeyboardInterrupt: | ||||
# Ctrl-C shouldn't crash the kernel | ||||
io.raw_print("KeyboardInterrupt caught in kernel") | ||||
finally: | ||||
# ensure excepthook is restored | ||||
sys.excepthook = real_excepthook | ||||
def enable_gui(gui, kernel=None): | ||||
"""Enable integration with a given GUI""" | ||||
if gui not in loop_map: | ||||
MinRK
|
r11319 | e = "Invalid GUI request %r, valid ones are:%s" % (gui, loop_map.keys()) | ||
raise ValueError(e) | ||||
MinRK
|
r6880 | if kernel is None: | ||
if Application.initialized(): | ||||
kernel = getattr(Application.instance(), 'kernel', None) | ||||
if kernel is None: | ||||
raise RuntimeError("You didn't specify a kernel," | ||||
" and no IPython Application with a kernel appears to be running." | ||||
) | ||||
Fernando Perez
|
r5472 | loop = loop_map[gui] | ||
Ryan May
|
r7938 | if loop and kernel.eventloop is not None and kernel.eventloop is not loop: | ||
MinRK
|
r15232 | raise RuntimeError("Cannot activate multiple GUI eventloops") | ||
Fernando Perez
|
r5472 | kernel.eventloop = loop | ||