Skip to content

Commit e32d7f9

Browse files
authored
Merge pull request #323 from minrk/run-async
async support
2 parents 2c686f1 + 2b10aa1 commit e32d7f9

14 files changed

+534
-175
lines changed

.travis.yml

+43-19
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
11
language: python
22
python:
3-
- "nightly"
4-
- "3.7-dev"
5-
- 3.6
6-
- 3.5
7-
- 3.4
8-
- 2.7
3+
- "nightly"
4+
- "3.7-dev"
5+
- 3.6
6+
- 3.5
7+
- 3.4
98
sudo: false
109
install:
11-
- |
12-
pip install --upgrade setuptools pip
13-
pip install --pre .
14-
pip install ipykernel[test] codecov
15-
- |
16-
if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" || "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
17-
pip install matplotlib
10+
- |
11+
# pip install
12+
pip install --upgrade setuptools pip
13+
pip install --pre .
14+
pip install ipykernel[test] codecov
15+
- |
16+
# install matplotlib
17+
if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then
18+
pip install matplotlib curio trio
19+
fi
20+
- |
21+
# pin tornado
22+
if [[ ! -z "$TORNADO" ]]; then
23+
pip install tornado=="$TORNADO"
24+
fi
25+
- |
26+
# pin IPython
27+
if [[ ! -z "$IPYTHON" ]]; then
28+
if [[ "$IPYTHON" == "master" ]]; then
29+
SPEC=git+https://github.com/ipython/ipython#egg=ipython
30+
else
31+
SPEC="ipython==$IPYTHON"
1832
fi
19-
- pip freeze
33+
pip install --upgrade --pre "$SPEC"
34+
fi
35+
- pip freeze
2036
script:
21-
- jupyter kernelspec list
22-
- pytest --cov ipykernel --durations 10 -v ipykernel
37+
- jupyter kernelspec list
38+
- pytest --cov ipykernel --durations 10 -v ipykernel
2339
after_success:
24-
- codecov
40+
- codecov
2541
matrix:
26-
allow_failures:
27-
- python: "nightly"
42+
include:
43+
- python: 3.5
44+
env:
45+
- TORNADO="4.5.*"
46+
- IPYTHON=master
47+
- python: 3.6
48+
env:
49+
- IPYTHON=master
50+
allow_failures:
51+
- python: "nightly"

appveyor.yml

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ clone_depth: 1
66
environment:
77

88
matrix:
9-
- python: "C:/Python27-x64"
10-
- python: "C:/Python27"
119
- python: "C:/Python36-x64"
1210
- python: "C:/Python36"
1311

docs/changelog.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Changes in IPython kernel
196196

197197
- Publish all IO in a thread, via :class:`IOPubThread`.
198198
This solves the problem of requiring :meth:`sys.stdout.flush` to be called in the notebook to produce output promptly during long-running cells.
199-
- Remove refrences to outdated IPython guiref in kernel banner.
199+
- Remove references to outdated IPython guiref in kernel banner.
200200
- Patch faulthandler to use ``sys.__stderr__`` instead of forwarded ``sys.stderr``,
201201
which has no fileno when forwarded.
202202
- Deprecate some vestiges of the Big Split:

ipykernel/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
version_info = (4, 9, 0)
1+
version_info = (5, 0, 0, 'dev')
22
__version__ = '.'.join(map(str, version_info))
33

44
kernel_protocol_version_info = (5, 1)

ipykernel/connect.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def get_connection_file(app=None):
4040
def find_connection_file(filename='kernel-*.json', profile=None):
4141
"""DEPRECATED: find a connection file, and return its absolute path.
4242
43-
THIS FUNCION IS DEPRECATED. Use juptyer_client.find_connection_file instead.
43+
THIS FUNCTION IS DEPRECATED. Use juptyer_client.find_connection_file instead.
4444
4545
Parameters
4646
----------

ipykernel/eventloops.py

+84-46
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright (c) IPython Development Team.
55
# Distributed under the terms of the Modified BSD License.
66

7+
from functools import partial
78
import os
89
import sys
910
import platform
@@ -14,13 +15,15 @@
1415
from traitlets.config.application import Application
1516
from IPython.utils import io
1617

18+
1719
def _use_appnope():
1820
"""Should we use appnope for dealing with OS X app nap?
1921
2022
Checks if we are on OS X 10.9 or greater.
2123
"""
2224
return sys.platform == 'darwin' and V(platform.mac_ver()[0]) >= V('10.9')
2325

26+
2427
def _notify_stream_qt(kernel, stream):
2528

2629
from IPython.external.qt_for_kernel import QtCore
@@ -34,9 +37,14 @@ def context():
3437
yield
3538

3639
def process_stream_events():
37-
while stream.getsockopt(zmq.EVENTS) & zmq.POLLIN:
38-
with context():
39-
kernel.do_one_iteration()
40+
"""fall back to main loop when there's a socket event"""
41+
# call flush to ensure that the stream doesn't lose events
42+
# due to our consuming of the edge-triggered FD
43+
# flush returns the number of events consumed.
44+
# if there were any, wake it up
45+
if stream.flush(limit=1):
46+
notifier.setEnabled(False)
47+
kernel.app.quit()
4048

4149
fd = stream.getsockopt(zmq.FD)
4250
notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app)
@@ -88,6 +96,7 @@ def exit_decorator(exit_func):
8896
to register a function to be called on exit
8997
"""
9098
func.exit_hook = exit_func
99+
return exit_func
91100

92101
func.exit = exit_decorator
93102
return func
@@ -122,20 +131,17 @@ def loop_qt4(kernel):
122131
_loop_qt(kernel.app)
123132

124133

125-
@loop_qt4.exit
126-
def loop_qt4_exit(kernel):
127-
kernel.app.exit()
128-
129-
130134
@register_integration('qt', 'qt5')
131135
def loop_qt5(kernel):
132136
"""Start a kernel with PyQt5 event loop integration."""
133137
os.environ['QT_API'] = 'pyqt5'
134138
return loop_qt4(kernel)
135139

136140

141+
# exit and watch are the same for qt 4 and 5
142+
@loop_qt4.exit
137143
@loop_qt5.exit
138-
def loop_qt5_exit(kernel):
144+
def loop_qt_exit(kernel):
139145
kernel.app.exit()
140146

141147

@@ -163,9 +169,15 @@ def loop_wx(kernel):
163169
from appnope import nope
164170
nope()
165171

166-
doi = kernel.do_one_iteration
167172
# Wx uses milliseconds
168-
poll_interval = int(1000*kernel._poll_interval)
173+
poll_interval = int(1000 * kernel._poll_interval)
174+
175+
def wake():
176+
"""wake from wx"""
177+
for stream in kernel.shell_streams:
178+
if stream.flush(limit=1):
179+
kernel.app.ExitMainLoop()
180+
return
169181

170182
# We have to put the wx.Timer in a wx.Frame for it to fire properly.
171183
# We make the Frame hidden when we create it in the main app below.
@@ -182,16 +194,20 @@ def on_timer(self, event):
182194
self.func()
183195

184196
# We need a custom wx.App to create our Frame subclass that has the
185-
# wx.Timer to drive the ZMQ event loop.
197+
# wx.Timer to defer back to the tornado event loop.
186198
class IPWxApp(wx.App):
187199
def OnInit(self):
188-
self.frame = TimerFrame(doi)
200+
self.frame = TimerFrame(wake)
189201
self.frame.Show(False)
190202
return True
191203

192204
# The redirect=False here makes sure that wx doesn't replace
193205
# sys.stdout/stderr with its own classes.
194-
kernel.app = IPWxApp(redirect=False)
206+
if not (
207+
getattr(kernel, 'app', None)
208+
and isinstance(kernel.app, wx.App)
209+
):
210+
kernel.app = IPWxApp(redirect=False)
195211

196212
# The import of wx on Linux sets the handler for signal.SIGINT
197213
# to 0. This is a bug in wx or gtk. We fix by just setting it
@@ -213,35 +229,31 @@ def loop_wx_exit(kernel):
213229
def loop_tk(kernel):
214230
"""Start a kernel with the Tk event loop."""
215231

216-
try:
217-
from tkinter import Tk # Py 3
218-
except ImportError:
219-
from Tkinter import Tk # Py 2
220-
doi = kernel.do_one_iteration
221-
# Tk uses milliseconds
222-
poll_interval = int(1000*kernel._poll_interval)
223-
# For Tkinter, we create a Tk object and call its withdraw method.
224-
class Timer(object):
225-
def __init__(self, func):
226-
self.app = Tk()
227-
self.app.withdraw()
228-
self.func = func
232+
from tkinter import Tk, READABLE
229233

230-
def on_timer(self):
231-
self.func()
232-
self.app.after(poll_interval, self.on_timer)
234+
def process_stream_events(stream, *a, **kw):
235+
"""fall back to main loop when there's a socket event"""
236+
if stream.flush(limit=1):
237+
app.tk.deletefilehandler(stream.getsockopt(zmq.FD))
238+
app.quit()
233239

234-
def start(self):
235-
self.on_timer() # Call it once to get things going.
236-
self.app.mainloop()
240+
# For Tkinter, we create a Tk object and call its withdraw method.
241+
kernel.app = app = Tk()
242+
kernel.app.withdraw()
243+
for stream in kernel.shell_streams:
244+
notifier = partial(process_stream_events, stream)
245+
# seems to be needed for tk
246+
notifier.__name__ = 'notifier'
247+
app.tk.createfilehandler(stream.getsockopt(zmq.FD), READABLE, notifier)
248+
# schedule initial call after start
249+
app.after(0, notifier)
237250

238-
kernel.timer = Timer(doi)
239-
kernel.timer.start()
251+
app.mainloop()
240252

241253

242254
@loop_tk.exit
243255
def loop_tk_exit(kernel):
244-
kernel.timer.app.destroy()
256+
kernel.app.destroy()
245257

246258

247259
@register_integration('gtk')
@@ -299,8 +311,10 @@ def handle_int(etype, value, tb):
299311
# don't let interrupts during mainloop invoke crash_handler:
300312
sys.excepthook = handle_int
301313
mainloop(kernel._poll_interval)
302-
sys.excepthook = real_excepthook
303-
kernel.do_one_iteration()
314+
for stream in kernel.shell_streams:
315+
if stream.flush(limit=1):
316+
# events to process, return control to kernel
317+
return
304318
except:
305319
raise
306320
except KeyboardInterrupt:
@@ -326,11 +340,24 @@ def loop_asyncio(kernel):
326340
if loop.is_running():
327341
return
328342

329-
def kernel_handler():
330-
loop.call_soon(kernel.do_one_iteration)
331-
loop.call_later(kernel._poll_interval, kernel_handler)
343+
if loop.is_closed():
344+
# main loop is closed, create a new one
345+
loop = asyncio.new_event_loop()
346+
asyncio.set_event_loop(loop)
347+
loop._should_close = False
348+
349+
# pause eventloop when there's an event on a zmq socket
350+
def process_stream_events(stream):
351+
"""fall back to main loop when there's a socket event"""
352+
if stream.flush(limit=1):
353+
loop.stop()
354+
355+
for stream in kernel.shell_streams:
356+
fd = stream.getsockopt(zmq.FD)
357+
notifier = partial(process_stream_events, stream)
358+
loop.add_reader(fd, notifier)
359+
loop.call_soon(notifier)
332360

333-
loop.call_soon(kernel_handler)
334361
while True:
335362
error = None
336363
try:
@@ -339,9 +366,8 @@ def kernel_handler():
339366
continue
340367
except Exception as e:
341368
error = e
342-
if hasattr(loop, 'shutdown_asyncgens'):
343-
loop.run_until_complete(loop.shutdown_asyncgens())
344-
loop.close()
369+
if loop._should_close:
370+
loop.close()
345371
if error is not None:
346372
raise error
347373
break
@@ -352,8 +378,20 @@ def loop_asyncio_exit(kernel):
352378
"""Exit hook for asyncio"""
353379
import asyncio
354380
loop = asyncio.get_event_loop()
381+
382+
@asyncio.coroutine
383+
def close_loop():
384+
if hasattr(loop, 'shutdown_asyncgens'):
385+
yield from loop.shutdown_asyncgens()
386+
loop._should_close = True
387+
loop.stop()
388+
355389
if loop.is_running():
356-
loop.call_soon(loop.stop)
390+
close_loop()
391+
392+
elif not loop.is_closed():
393+
loop.run_until_complete(close_loop)
394+
loop.close()
357395

358396

359397
def enable_gui(gui, kernel=None):

0 commit comments

Comments
 (0)