|
|
- # encoding: utf-8
- """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 sys
- import platform
-
- import zmq
-
- from distutils.version import LooseVersion as V
- from traitlets.config.application import Application
- from IPython.utils import io
-
- 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')
-
- def _notify_stream_qt(kernel, stream):
-
- from IPython.external.qt_for_kernel import QtCore
-
- if _use_appnope() and kernel._darwin_app_nap:
- from appnope import nope_scope as context
- else:
- from contextlib import contextmanager
- @contextmanager
- def context():
- yield
-
- 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)
- # 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
- timer = QtCore.QTimer(kernel.app)
- timer.setSingleShot(True)
- timer.timeout.connect(process_stream_events)
- timer.start(0)
-
- # mapping of keys to loop functions
- loop_map = {
- 'inline': None,
- 'nbagg': 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):
- 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
-
- func.exit = exit_decorator
- return func
-
- return decorator
-
-
- def _loop_qt(app):
- """Inner-loop for running the Qt 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.exec_()
- app._in_event_loop = False
-
-
- @register_integration('qt4')
- def loop_qt4(kernel):
- """Start a kernel with PyQt4 event loop integration."""
-
- from IPython.lib.guisupport import get_app_qt4
-
- kernel.app = get_app_qt4([" "])
- kernel.app.setQuitOnLastWindowClosed(False)
-
- for s in kernel.shell_streams:
- _notify_stream_qt(kernel, s)
-
- _loop_qt(kernel.app)
-
-
- @loop_qt4.exit
- def loop_qt4_exit(kernel):
- kernel.app.exit()
-
-
- @register_integration('qt', 'qt5')
- def loop_qt5(kernel):
- """Start a kernel with PyQt5 event loop integration."""
- os.environ['QT_API'] = 'pyqt5'
- return loop_qt4(kernel)
-
-
- @loop_qt5.exit
- def loop_qt5_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
-
- if _use_appnope() and kernel._darwin_app_nap:
- # we don't hook up App Nap contexts for Wx,
- # just disable it outright.
- from appnope import nope
- nope()
-
- 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)
-
- # 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):
- import wx
- wx.Exit()
-
-
- @register_integration('tk')
- def loop_tk(kernel):
- """Start a kernel with the Tk event loop."""
-
- try:
- from tkinter import Tk # Py 3
- except ImportError:
- from Tkinter import Tk # Py 2
- 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):
- self.app = Tk()
- 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()
-
-
- @loop_tk.exit
- def loop_tk_exit(kernel):
- kernel.timer.app.destroy()
-
-
- @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):
- 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):
- 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:
- io.raw_print("KeyboardInterrupt caught in CFRunLoop")
- 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)
- sys.excepthook = real_excepthook
- 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
-
-
- @loop_cocoa.exit
- def loop_cocoa_exit(kernel):
- 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
-
- def kernel_handler():
- loop.call_soon(kernel.do_one_iteration)
- loop.call_later(kernel._poll_interval, kernel_handler)
-
- loop.call_soon(kernel_handler)
- while True:
- error = None
- try:
- loop.run_forever()
- except KeyboardInterrupt:
- continue
- except Exception as e:
- error = e
- if hasattr(loop, 'shutdown_asyncgens'):
- loop.run_until_complete(loop.shutdown_asyncgens())
- 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()
- if loop.is_running():
- loop.call_soon(loop.stop)
-
-
- def enable_gui(gui, kernel=None):
- """Enable integration with a given GUI"""
- if gui not in loop_map:
- e = "Invalid GUI request %r, valid ones are:%s" % (gui, loop_map.keys())
- raise ValueError(e)
- 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."
- )
- loop = loop_map[gui]
- if loop and kernel.eventloop is not None and kernel.eventloop is not loop:
- raise RuntimeError("Cannot activate multiple GUI eventloops")
- kernel.eventloop = loop
|