Skip to content

Commit d1cb381

Browse files
Use jupyter_client's AsyncKernelManager (#191)
* Use jupyter_client's AsyncKernelManager * Rename MappingKernelManage to AsyncMappingKernelManage, convert gen.coroutine/yield to async/await, remove run_blocking * Fix Windows subprocess handle issue * Restrict Windows to python>=3.7 * Fix GH actions matrix exclusion * Make AsyncMappingKernelManager a subclass of MappingKernelManager for configuration back-compatibility * Make AsyncKernelManager an opt-in * Pin jupyter_core and jupyter_client a bit higher * Remove async from MappingKernelManager.shutdown_kernel * Hard-code super() in MappingKernelManager and AsyncMappingKernelManager * Add argv fixture to enable MappingKernelManager and AsyncMappingKernelManager * Rewrite ensure_async to not await already awaited coroutines * Add async shutdown_kernel to AsyncMappingKernelManager, keep MappingKernelManager.shutdown_kernel blocking * Add restart kwarg to shutdown_kernel * Add log message when starting (async) kernel manager * Bump jupyter_client 6.1.1 * Rename super attribute to pinned_superclass * Prevent using AsyncMappingKernelManager on python<=3.5 (at run-time and in tests) * Ignore last_activity and execution_state when comparing sessions * Replace newsession with new_session * Fix Python version check * Fix skipping of tests * GatewayKernelManager inherits from MappingKernelManager to keep python3.5 compatibility * Added back removal of kernelmanager.AsyncMappingKernelManager * Don't test absence of AsyncMultiKernelManager
1 parent 3a75ada commit d1cb381

File tree

23 files changed

+449
-364
lines changed

23 files changed

+449
-364
lines changed

.github/workflows/main.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Jupyter Server Tests
22
on:
33
push:
4-
branches:
4+
branches:
55
- master
66
pull_request:
77
branches: '*'
@@ -13,6 +13,11 @@ jobs:
1313
matrix:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
1515
python-version: [ '3.5', '3.6', '3.7', '3.8' ]
16+
exclude:
17+
- os: windows-latest
18+
python-version: '3.5'
19+
- os: windows-latest
20+
python-version: '3.6'
1621
steps:
1722
- name: Checkout
1823
uses: actions/checkout@v1

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ To install the latest release locally, make sure you have
2626

2727
$ pip install jupyter_server
2828

29+
Jupyter Server currently supports the following Python versions:
30+
31+
Platform | Python
32+
--- | ---
33+
Linux | >=3.5
34+
OSX | >=3.5
35+
Windows | >=3.7
36+
2937
### Versioning and Branches
3038

3139
If Jupyter Server is a dependency of your project/application, it is important that you pin it to a version that works for your application. Currently, Jupyter Server only has minor and patch versions. Different minor versions likely include API-changes while patch versions do not change API.

docs/source/extending/bundler_extensions.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,19 @@ respond in any manner. For example, it may read additional query parameters
8686
from the request, issue a redirect to another site, run a local process (e.g.,
8787
`nbconvert`), make a HTTP request to another service, etc.
8888

89-
The caller of the `bundle` function is `@tornado.gen.coroutine` decorated and
90-
wraps its call with `torando.gen.maybe_future`. This behavior means you may
89+
The caller of the `bundle` function is `async` and wraps its call with
90+
`jupyter_server.utils.ensure_async`. This behavior means you may
9191
handle the web request synchronously, as in the example above, or
92-
asynchronously using `@tornado.gen.coroutine` and `yield`, as in the example
92+
asynchronously using `async` and `await`, as in the example
9393
below.
9494

9595
.. code:: python
9696
97-
from tornado import gen
97+
import asyncio
9898
99-
@gen.coroutine
100-
def bundle(handler, model):
99+
async def bundle(handler, model):
101100
# simulate a long running IO op (e.g., deploying to a remote host)
102-
yield gen.sleep(10)
101+
await asyncio.sleep(10)
103102
104103
# now respond
105104
handler.finish('I spent 10 seconds bundling {}!'.format(model['path']))

jupyter_server/base/zmqhandlers.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
import tornado
1111

1212
from urllib.parse import urlparse
13-
from tornado import gen, ioloop, web
13+
from tornado import ioloop, web
1414
from tornado.websocket import WebSocketHandler
1515

1616
from jupyter_client.session import Session
1717
from jupyter_client.jsonutil import date_default, extract_dates
1818
from ipython_genutils.py3compat import cast_unicode
1919

2020
from .handlers import JupyterHandler
21-
from jupyter_server.utils import maybe_future
2221

2322

2423
def serialize_binary_message(msg):
@@ -90,15 +89,15 @@ class WebSocketMixin(object):
9089
last_ping = 0
9190
last_pong = 0
9291
stream = None
93-
92+
9493
@property
9594
def ping_interval(self):
9695
"""The interval for websocket keep-alive pings.
97-
96+
9897
Set ws_ping_interval = 0 to disable pings.
9998
"""
10099
return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
101-
100+
102101
@property
103102
def ping_timeout(self):
104103
"""If no ping is received in this many milliseconds,
@@ -111,7 +110,7 @@ def ping_timeout(self):
111110

112111
def check_origin(self, origin=None):
113112
"""Check Origin == Host or Access-Control-Allow-Origin.
114-
113+
115114
Tornado >= 4 calls this method automatically, raising 403 if it returns False.
116115
"""
117116

@@ -122,18 +121,18 @@ def check_origin(self, origin=None):
122121
host = self.request.headers.get("Host")
123122
if origin is None:
124123
origin = self.get_origin()
125-
124+
126125
# If no origin or host header is provided, assume from script
127126
if origin is None or host is None:
128127
return True
129-
128+
130129
origin = origin.lower()
131130
origin_host = urlparse(origin).netloc
132-
131+
133132
# OK if origin matches host
134133
if origin_host == host:
135134
return True
136-
135+
137136
# Check CORS headers
138137
if self.allow_origin:
139138
allow = self.allow_origin == origin
@@ -190,7 +189,7 @@ def on_pong(self, data):
190189

191190

192191
class ZMQStreamHandler(WebSocketMixin, WebSocketHandler):
193-
192+
194193
if tornado.version_info < (4,1):
195194
"""Backport send_error from tornado 4.1 to 4.0"""
196195
def send_error(self, *args, **kwargs):
@@ -203,17 +202,17 @@ def send_error(self, *args, **kwargs):
203202
# we can close the connection more gracefully.
204203
self.stream.close()
205204

206-
205+
207206
def _reserialize_reply(self, msg_or_list, channel=None):
208207
"""Reserialize a reply message using JSON.
209208
210209
msg_or_list can be an already-deserialized msg dict or the zmq buffer list.
211210
If it is the zmq list, it will be deserialized with self.session.
212-
211+
213212
This takes the msg list from the ZMQ socket and serializes the result for the websocket.
214213
This method should be used by self._on_zmq_reply to build messages that can
215214
be sent back to the browser.
216-
215+
217216
"""
218217
if isinstance(msg_or_list, dict):
219218
# already unpacked
@@ -271,14 +270,13 @@ def pre_get(self):
271270
else:
272271
self.log.warning("No session ID specified")
273272

274-
@gen.coroutine
275-
def get(self, *args, **kwargs):
273+
async def get(self, *args, **kwargs):
276274
# pre_get can be a coroutine in subclasses
277275
# assign and yield in two step to avoid tornado 3 issues
278276
res = self.pre_get()
279-
yield maybe_future(res)
277+
await res
280278
res = super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
281-
yield maybe_future(res)
279+
await res
282280

283281
def initialize(self):
284282
self.log.debug("Initializing websocket connection %s", self.request.path)

jupyter_server/files/handlers.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
import mimetypes
77
import json
88
from base64 import decodebytes
9-
from tornado import gen, web
9+
from tornado import web
1010
from jupyter_server.base.handlers import JupyterHandler
11-
from jupyter_server.utils import maybe_future
1211

1312

1413
class FilesHandler(JupyterHandler):
@@ -32,8 +31,7 @@ def head(self, path):
3231
self.get(path, include_body=False)
3332

3433
@web.authenticated
35-
@gen.coroutine
36-
def get(self, path, include_body=True):
34+
async def get(self, path, include_body=True):
3735
cm = self.contents_manager
3836

3937
if cm.is_hidden(path) and not cm.allow_hidden:
@@ -45,9 +43,9 @@ def get(self, path, include_body=True):
4543
_, name = path.rsplit('/', 1)
4644
else:
4745
name = path
48-
49-
model = yield maybe_future(cm.get(path, type='file', content=include_body))
50-
46+
47+
model = await cm.get(path, type='file', content=include_body)
48+
5149
if self.get_argument("download", False):
5250
self.set_attachment_header(name)
5351

@@ -78,4 +76,3 @@ def get(self, path, include_body=True):
7876

7977

8078
default_handlers = []
81-

jupyter_server/gateway/handlers.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ..base.handlers import APIHandler, JupyterHandler
99
from ..utils import url_path_join
1010

11-
from tornado import gen, web
11+
from tornado import web
1212
from tornado.concurrent import Future
1313
from tornado.ioloop import IOLoop, PeriodicCallback
1414
from tornado.websocket import WebSocketHandler, websocket_connect
@@ -61,11 +61,10 @@ def initialize(self):
6161
self.session = Session(config=self.config)
6262
self.gateway = GatewayWebSocketClient(gateway_url=GatewayClient.instance().url)
6363

64-
@gen.coroutine
65-
def get(self, kernel_id, *args, **kwargs):
64+
async def get(self, kernel_id, *args, **kwargs):
6665
self.authenticate()
6766
self.kernel_id = cast_unicode(kernel_id, 'ascii')
68-
yield super(WebSocketChannelsHandler, self).get(kernel_id=kernel_id, *args, **kwargs)
67+
await super(WebSocketChannelsHandler, self).get(kernel_id=kernel_id, *args, **kwargs)
6968

7069
def send_ping(self):
7170
if self.ws_connection is None and self.ping_callback is not None:
@@ -132,8 +131,7 @@ def __init__(self, **kwargs):
132131
self.ws_future = Future()
133132
self.disconnected = False
134133

135-
@gen.coroutine
136-
def _connect(self, kernel_id):
134+
async def _connect(self, kernel_id):
137135
# websocket is initialized before connection
138136
self.ws = None
139137
self.kernel_id = kernel_id
@@ -168,14 +166,13 @@ def _disconnect(self):
168166
self.ws_future.cancel()
169167
self.log.debug("_disconnect: future cancelled, disconnected: {}".format(self.disconnected))
170168

171-
@gen.coroutine
172-
def _read_messages(self, callback):
169+
async def _read_messages(self, callback):
173170
"""Read messages from gateway server."""
174171
while self.ws is not None:
175172
message = None
176173
if not self.disconnected:
177174
try:
178-
message = yield self.ws.read_message()
175+
message = await self.ws.read_message()
179176
except Exception as e:
180177
self.log.error("Exception reading message from websocket: {}".format(e)) # , exc_info=True)
181178
if message is None:
@@ -229,10 +226,9 @@ class GatewayResourceHandler(APIHandler):
229226
"""Retrieves resources for specific kernelspec definitions from kernel/enterprise gateway."""
230227

231228
@web.authenticated
232-
@gen.coroutine
233-
def get(self, kernel_name, path, include_body=True):
229+
async def get(self, kernel_name, path, include_body=True):
234230
ksm = self.kernel_spec_manager
235-
kernel_spec_res = yield ksm.get_kernel_spec_resource(kernel_name, path)
231+
kernel_spec_res = await ksm.get_kernel_spec_resource(kernel_name, path)
236232
if kernel_spec_res is None:
237233
self.log.warning("Kernelspec resource '{}' for '{}' not found. Gateway may not support"
238234
" resource serving.".format(path, kernel_name))

0 commit comments

Comments
 (0)