You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

374 lines
11 KiB

4 years ago
  1. # encoding: utf-8
  2. """Event loop integration for the ZeroMQ-based kernels."""
  3. # Copyright (c) IPython Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. import os
  6. import sys
  7. import platform
  8. import zmq
  9. from distutils.version import LooseVersion as V
  10. from traitlets.config.application import Application
  11. from IPython.utils import io
  12. def _use_appnope():
  13. """Should we use appnope for dealing with OS X app nap?
  14. Checks if we are on OS X 10.9 or greater.
  15. """
  16. return sys.platform == 'darwin' and V(platform.mac_ver()[0]) >= V('10.9')
  17. def _notify_stream_qt(kernel, stream):
  18. from IPython.external.qt_for_kernel import QtCore
  19. if _use_appnope() and kernel._darwin_app_nap:
  20. from appnope import nope_scope as context
  21. else:
  22. from contextlib import contextmanager
  23. @contextmanager
  24. def context():
  25. yield
  26. def process_stream_events():
  27. while stream.getsockopt(zmq.EVENTS) & zmq.POLLIN:
  28. with context():
  29. kernel.do_one_iteration()
  30. fd = stream.getsockopt(zmq.FD)
  31. notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app)
  32. notifier.activated.connect(process_stream_events)
  33. # there may already be unprocessed events waiting.
  34. # these events will not wake zmq's edge-triggered FD
  35. # since edge-triggered notification only occurs on new i/o activity.
  36. # process all the waiting events immediately
  37. # so we start in a clean state ensuring that any new i/o events will notify.
  38. # schedule first call on the eventloop as soon as it's running,
  39. # so we don't block here processing events
  40. timer = QtCore.QTimer(kernel.app)
  41. timer.setSingleShot(True)
  42. timer.timeout.connect(process_stream_events)
  43. timer.start(0)
  44. # mapping of keys to loop functions
  45. loop_map = {
  46. 'inline': None,
  47. 'nbagg': None,
  48. 'notebook': None,
  49. 'ipympl': None,
  50. 'widget': None,
  51. None: None,
  52. }
  53. def register_integration(*toolkitnames):
  54. """Decorator to register an event loop to integrate with the IPython kernel
  55. The decorator takes names to register the event loop as for the %gui magic.
  56. You can provide alternative names for the same toolkit.
  57. The decorated function should take a single argument, the IPython kernel
  58. instance, arrange for the event loop to call ``kernel.do_one_iteration()``
  59. at least every ``kernel._poll_interval`` seconds, and start the event loop.
  60. :mod:`ipykernel.eventloops` provides and registers such functions
  61. for a few common event loops.
  62. """
  63. def decorator(func):
  64. for name in toolkitnames:
  65. loop_map[name] = func
  66. func.exit_hook = lambda kernel: None
  67. def exit_decorator(exit_func):
  68. """@func.exit is now a decorator
  69. to register a function to be called on exit
  70. """
  71. func.exit_hook = exit_func
  72. func.exit = exit_decorator
  73. return func
  74. return decorator
  75. def _loop_qt(app):
  76. """Inner-loop for running the Qt eventloop
  77. Pulled from guisupport.start_event_loop in IPython < 5.2,
  78. since IPython 5.2 only checks `get_ipython().active_eventloop` is defined,
  79. rather than if the eventloop is actually running.
  80. """
  81. app._in_event_loop = True
  82. app.exec_()
  83. app._in_event_loop = False
  84. @register_integration('qt4')
  85. def loop_qt4(kernel):
  86. """Start a kernel with PyQt4 event loop integration."""
  87. from IPython.lib.guisupport import get_app_qt4
  88. kernel.app = get_app_qt4([" "])
  89. kernel.app.setQuitOnLastWindowClosed(False)
  90. for s in kernel.shell_streams:
  91. _notify_stream_qt(kernel, s)
  92. _loop_qt(kernel.app)
  93. @loop_qt4.exit
  94. def loop_qt4_exit(kernel):
  95. kernel.app.exit()
  96. @register_integration('qt', 'qt5')
  97. def loop_qt5(kernel):
  98. """Start a kernel with PyQt5 event loop integration."""
  99. os.environ['QT_API'] = 'pyqt5'
  100. return loop_qt4(kernel)
  101. @loop_qt5.exit
  102. def loop_qt5_exit(kernel):
  103. kernel.app.exit()
  104. def _loop_wx(app):
  105. """Inner-loop for running the Wx eventloop
  106. Pulled from guisupport.start_event_loop in IPython < 5.2,
  107. since IPython 5.2 only checks `get_ipython().active_eventloop` is defined,
  108. rather than if the eventloop is actually running.
  109. """
  110. app._in_event_loop = True
  111. app.MainLoop()
  112. app._in_event_loop = False
  113. @register_integration('wx')
  114. def loop_wx(kernel):
  115. """Start a kernel with wx event loop support."""
  116. import wx
  117. if _use_appnope() and kernel._darwin_app_nap:
  118. # we don't hook up App Nap contexts for Wx,
  119. # just disable it outright.
  120. from appnope import nope
  121. nope()
  122. doi = kernel.do_one_iteration
  123. # Wx uses milliseconds
  124. poll_interval = int(1000*kernel._poll_interval)
  125. # We have to put the wx.Timer in a wx.Frame for it to fire properly.
  126. # We make the Frame hidden when we create it in the main app below.
  127. class TimerFrame(wx.Frame):
  128. def __init__(self, func):
  129. wx.Frame.__init__(self, None, -1)
  130. self.timer = wx.Timer(self)
  131. # Units for the timer are in milliseconds
  132. self.timer.Start(poll_interval)
  133. self.Bind(wx.EVT_TIMER, self.on_timer)
  134. self.func = func
  135. def on_timer(self, event):
  136. self.func()
  137. # We need a custom wx.App to create our Frame subclass that has the
  138. # wx.Timer to drive the ZMQ event loop.
  139. class IPWxApp(wx.App):
  140. def OnInit(self):
  141. self.frame = TimerFrame(doi)
  142. self.frame.Show(False)
  143. return True
  144. # The redirect=False here makes sure that wx doesn't replace
  145. # sys.stdout/stderr with its own classes.
  146. kernel.app = IPWxApp(redirect=False)
  147. # The import of wx on Linux sets the handler for signal.SIGINT
  148. # to 0. This is a bug in wx or gtk. We fix by just setting it
  149. # back to the Python default.
  150. import signal
  151. if not callable(signal.getsignal(signal.SIGINT)):
  152. signal.signal(signal.SIGINT, signal.default_int_handler)
  153. _loop_wx(kernel.app)
  154. @loop_wx.exit
  155. def loop_wx_exit(kernel):
  156. import wx
  157. wx.Exit()
  158. @register_integration('tk')
  159. def loop_tk(kernel):
  160. """Start a kernel with the Tk event loop."""
  161. try:
  162. from tkinter import Tk # Py 3
  163. except ImportError:
  164. from Tkinter import Tk # Py 2
  165. doi = kernel.do_one_iteration
  166. # Tk uses milliseconds
  167. poll_interval = int(1000*kernel._poll_interval)
  168. # For Tkinter, we create a Tk object and call its withdraw method.
  169. class Timer(object):
  170. def __init__(self, func):
  171. self.app = Tk()
  172. self.app.withdraw()
  173. self.func = func
  174. def on_timer(self):
  175. self.func()
  176. self.app.after(poll_interval, self.on_timer)
  177. def start(self):
  178. self.on_timer() # Call it once to get things going.
  179. self.app.mainloop()
  180. kernel.timer = Timer(doi)
  181. kernel.timer.start()
  182. @loop_tk.exit
  183. def loop_tk_exit(kernel):
  184. kernel.timer.app.destroy()
  185. @register_integration('gtk')
  186. def loop_gtk(kernel):
  187. """Start the kernel, coordinating with the GTK event loop"""
  188. from .gui.gtkembed import GTKEmbed
  189. gtk_kernel = GTKEmbed(kernel)
  190. gtk_kernel.start()
  191. kernel._gtk = gtk_kernel
  192. @loop_gtk.exit
  193. def loop_gtk_exit(kernel):
  194. kernel._gtk.stop()
  195. @register_integration('gtk3')
  196. def loop_gtk3(kernel):
  197. """Start the kernel, coordinating with the GTK event loop"""
  198. from .gui.gtk3embed import GTKEmbed
  199. gtk_kernel = GTKEmbed(kernel)
  200. gtk_kernel.start()
  201. kernel._gtk = gtk_kernel
  202. @loop_gtk3.exit
  203. def loop_gtk3_exit(kernel):
  204. kernel._gtk.stop()
  205. @register_integration('osx')
  206. def loop_cocoa(kernel):
  207. """Start the kernel, coordinating with the Cocoa CFRunLoop event loop
  208. via the matplotlib MacOSX backend.
  209. """
  210. from ._eventloop_macos import mainloop, stop
  211. real_excepthook = sys.excepthook
  212. def handle_int(etype, value, tb):
  213. """don't let KeyboardInterrupts look like crashes"""
  214. # wake the eventloop when we get a signal
  215. stop()
  216. if etype is KeyboardInterrupt:
  217. io.raw_print("KeyboardInterrupt caught in CFRunLoop")
  218. else:
  219. real_excepthook(etype, value, tb)
  220. while not kernel.shell.exit_now:
  221. try:
  222. # double nested try/except, to properly catch KeyboardInterrupt
  223. # due to pyzmq Issue #130
  224. try:
  225. # don't let interrupts during mainloop invoke crash_handler:
  226. sys.excepthook = handle_int
  227. mainloop(kernel._poll_interval)
  228. sys.excepthook = real_excepthook
  229. kernel.do_one_iteration()
  230. except:
  231. raise
  232. except KeyboardInterrupt:
  233. # Ctrl-C shouldn't crash the kernel
  234. io.raw_print("KeyboardInterrupt caught in kernel")
  235. finally:
  236. # ensure excepthook is restored
  237. sys.excepthook = real_excepthook
  238. @loop_cocoa.exit
  239. def loop_cocoa_exit(kernel):
  240. from ._eventloop_macos import stop
  241. stop()
  242. @register_integration('asyncio')
  243. def loop_asyncio(kernel):
  244. '''Start a kernel with asyncio event loop support.'''
  245. import asyncio
  246. loop = asyncio.get_event_loop()
  247. # loop is already running (e.g. tornado 5), nothing left to do
  248. if loop.is_running():
  249. return
  250. def kernel_handler():
  251. loop.call_soon(kernel.do_one_iteration)
  252. loop.call_later(kernel._poll_interval, kernel_handler)
  253. loop.call_soon(kernel_handler)
  254. while True:
  255. error = None
  256. try:
  257. loop.run_forever()
  258. except KeyboardInterrupt:
  259. continue
  260. except Exception as e:
  261. error = e
  262. if hasattr(loop, 'shutdown_asyncgens'):
  263. loop.run_until_complete(loop.shutdown_asyncgens())
  264. loop.close()
  265. if error is not None:
  266. raise error
  267. break
  268. @loop_asyncio.exit
  269. def loop_asyncio_exit(kernel):
  270. """Exit hook for asyncio"""
  271. import asyncio
  272. loop = asyncio.get_event_loop()
  273. if loop.is_running():
  274. loop.call_soon(loop.stop)
  275. def enable_gui(gui, kernel=None):
  276. """Enable integration with a given GUI"""
  277. if gui not in loop_map:
  278. e = "Invalid GUI request %r, valid ones are:%s" % (gui, loop_map.keys())
  279. raise ValueError(e)
  280. if kernel is None:
  281. if Application.initialized():
  282. kernel = getattr(Application.instance(), 'kernel', None)
  283. if kernel is None:
  284. raise RuntimeError("You didn't specify a kernel,"
  285. " and no IPython Application with a kernel appears to be running."
  286. )
  287. loop = loop_map[gui]
  288. if loop and kernel.eventloop is not None and kernel.eventloop is not loop:
  289. raise RuntimeError("Cannot activate multiple GUI eventloops")
  290. kernel.eventloop = loop