Skip to content

Commit 5874148

Browse files
authored
Merge pull request #835 from blink1073/synchronous_managers
2 parents 432be9d + 097fa1e commit 5874148

20 files changed

+814
-274
lines changed

.github/workflows/downstream.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ concurrency:
1212
jobs:
1313
ipykernel:
1414
runs-on: ubuntu-latest
15-
timeout-minutes: 10
15+
timeout-minutes: 15
1616
steps:
1717
- uses: actions/checkout@v2
1818
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
@@ -23,7 +23,7 @@ jobs:
2323

2424
nbclient:
2525
runs-on: ubuntu-latest
26-
timeout-minutes: 10
26+
timeout-minutes: 15
2727
steps:
2828
- uses: actions/checkout@v2
2929
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
@@ -34,7 +34,7 @@ jobs:
3434

3535
nbconvert:
3636
runs-on: ubuntu-latest
37-
timeout-minutes: 10
37+
timeout-minutes: 15
3838
steps:
3939
- uses: actions/checkout@v2
4040
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
@@ -45,7 +45,7 @@ jobs:
4545

4646
jupyter_server:
4747
runs-on: ubuntu-latest
48-
timeout-minutes: 10
48+
timeout-minutes: 15
4949
steps:
5050
- uses: actions/checkout@v2
5151
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

.github/workflows/main.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ jobs:
7676
- name: Run the tests on pypy and windows
7777
if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(matrix.os, 'windows') }}
7878
run: |
79-
python -m pytest -vv || python -m pytest -vv --lf
79+
# Ignore warnings on Windows and PyPI
80+
python -m pytest -vv -W ignore || python -m pytest -vv -W ignore --lf
8081
8182
- name: Code coverage
8283
run: codecov

jupyter_client/asynchronous/client.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
"""Implements an async kernel client"""
22
# Copyright (c) Jupyter Development Team.
33
# Distributed under the terms of the Modified BSD License.
4+
import asyncio
5+
6+
import zmq.asyncio
7+
from traitlets import Instance
48
from traitlets import Type
59

10+
from jupyter_client.channels import AsyncZMQSocketChannel
611
from jupyter_client.channels import HBChannel
7-
from jupyter_client.channels import ZMQSocketChannel
812
from jupyter_client.client import KernelClient
913
from jupyter_client.client import reqrep
1014

@@ -14,9 +18,11 @@ def _(self, *args, **kwargs):
1418
reply = kwargs.pop("reply", False)
1519
timeout = kwargs.pop("timeout", None)
1620
msg_id = meth(self, *args, **kwargs)
21+
fut: asyncio.Future = asyncio.Future()
22+
fut.set_result(msg_id)
1723
if not reply:
18-
return msg_id
19-
return self._async_recv_reply(msg_id, timeout=timeout, channel=channel)
24+
return fut
25+
return self._recv_reply(msg_id, timeout=timeout, channel=channel)
2026

2127
return _
2228

@@ -28,6 +34,12 @@ class AsyncKernelClient(KernelClient):
2834
raising :exc:`queue.Empty` if no message arrives within ``timeout`` seconds.
2935
"""
3036

37+
context = Instance(zmq.asyncio.Context)
38+
39+
def _context_default(self) -> zmq.asyncio.Context:
40+
self._created_context = True
41+
return zmq.asyncio.Context()
42+
3143
# --------------------------------------------------------------------------
3244
# Channel proxy methods
3345
# --------------------------------------------------------------------------
@@ -40,18 +52,19 @@ class AsyncKernelClient(KernelClient):
4052
wait_for_ready = KernelClient._async_wait_for_ready
4153

4254
# The classes to use for the various channels
43-
shell_channel_class = Type(ZMQSocketChannel)
44-
iopub_channel_class = Type(ZMQSocketChannel)
45-
stdin_channel_class = Type(ZMQSocketChannel)
55+
shell_channel_class = Type(AsyncZMQSocketChannel)
56+
iopub_channel_class = Type(AsyncZMQSocketChannel)
57+
stdin_channel_class = Type(AsyncZMQSocketChannel)
4658
hb_channel_class = Type(HBChannel)
47-
control_channel_class = Type(ZMQSocketChannel)
59+
control_channel_class = Type(AsyncZMQSocketChannel)
4860

4961
_recv_reply = KernelClient._async_recv_reply
5062

5163
# replies come on the shell channel
5264
execute = reqrep(wrapped, KernelClient.execute)
5365
history = reqrep(wrapped, KernelClient.history)
5466
complete = reqrep(wrapped, KernelClient.complete)
67+
is_complete = reqrep(wrapped, KernelClient.is_complete)
5568
inspect = reqrep(wrapped, KernelClient.inspect)
5669
kernel_info = reqrep(wrapped, KernelClient.kernel_info)
5770
comm_info = reqrep(wrapped, KernelClient.comm_info)

jupyter_client/blocking/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def _(self, *args, **kwargs):
2020
msg_id = meth(self, *args, **kwargs)
2121
if not reply:
2222
return msg_id
23-
return run_sync(self._async_recv_reply)(msg_id, timeout=timeout, channel=channel)
23+
return self._recv_reply(msg_id, timeout=timeout, channel=channel)
2424

2525
return _
2626

jupyter_client/channels.py

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .channelsabc import HBChannelABC
1515
from .session import Session
1616
from jupyter_client import protocol_version_info
17+
from jupyter_client.utils import ensure_async
1718

1819
# import ZMQError in top-level namespace, to avoid ugly attribute-error messages
1920
# during garbage collection of threads at exit
@@ -49,15 +50,15 @@ class HBChannel(Thread):
4950

5051
def __init__(
5152
self,
52-
context: t.Optional[zmq.asyncio.Context] = None,
53+
context: t.Optional[zmq.Context] = None,
5354
session: t.Optional[Session] = None,
5455
address: t.Union[t.Tuple[str, int], str] = "",
5556
):
5657
"""Create the heartbeat monitor thread.
5758
5859
Parameters
5960
----------
60-
context : :class:`zmq.asyncio.Context`
61+
context : :class:`zmq.Context`
6162
The ZMQ context to use.
6263
session : :class:`session.Session`
6364
The session to use.
@@ -106,12 +107,6 @@ def _create_socket(self) -> None:
106107

107108
self.poller.register(self.socket, zmq.POLLIN)
108109

109-
def run(self) -> None:
110-
loop = asyncio.new_event_loop()
111-
asyncio.set_event_loop(loop)
112-
loop.run_until_complete(self._async_run())
113-
loop.close()
114-
115110
async def _async_run(self) -> None:
116111
"""The thread's main activity. Call start() instead."""
117112
self._create_socket()
@@ -127,16 +122,16 @@ async def _async_run(self) -> None:
127122

128123
since_last_heartbeat = 0.0
129124
# no need to catch EFSM here, because the previous event was
130-
# either a recv or connect, which cannot be followed by EFSM
131-
await self.socket.send(b"ping")
125+
# either a recv or connect, which cannot be followed by EFSM)
126+
await ensure_async(self.socket.send(b"ping"))
132127
request_time = time.time()
133128
# Wait until timeout
134129
self._exit.wait(self.time_to_dead)
135130
# poll(0) means return immediately (see http://api.zeromq.org/2-1:zmq-poll)
136131
self._beating = bool(self.poller.poll(0))
137132
if self._beating:
138133
# the poll above guarantees we have something to recv
139-
await self.socket.recv()
134+
await ensure_async(self.socket.recv())
140135
continue
141136
elif self._running:
142137
# nothing was received within the time limit, signal heart failure
@@ -146,6 +141,12 @@ async def _async_run(self) -> None:
146141
self._create_socket()
147142
continue
148143

144+
def run(self) -> None:
145+
loop = asyncio.new_event_loop()
146+
asyncio.set_event_loop(loop)
147+
loop.run_until_complete(self._async_run())
148+
loop.close()
149+
149150
def pause(self) -> None:
150151
"""Pause the heartbeat."""
151152
self._pause = True
@@ -191,14 +192,14 @@ def call_handlers(self, since_last_heartbeat: float) -> None:
191192

192193

193194
class ZMQSocketChannel(object):
194-
"""A ZMQ socket in an async API"""
195+
"""A ZMQ socket wrapper"""
195196

196-
def __init__(self, socket: zmq.asyncio.Socket, session: Session, loop: t.Any = None) -> None:
197+
def __init__(self, socket: zmq.Socket, session: Session, loop: t.Any = None) -> None:
197198
"""Create a channel.
198199
199200
Parameters
200201
----------
201-
socket : :class:`zmq.asyncio.Socket`
202+
socket : :class:`zmq.Socket`
202203
The ZMQ socket to use.
203204
session : :class:`session.Session`
204205
The session to use.
@@ -207,42 +208,41 @@ def __init__(self, socket: zmq.asyncio.Socket, session: Session, loop: t.Any = N
207208
"""
208209
super().__init__()
209210

210-
self.socket: t.Optional[zmq.asyncio.Socket] = socket
211+
self.socket: t.Optional[zmq.Socket] = socket
211212
self.session = session
212213

213-
async def _recv(self, **kwargs: t.Any) -> t.Dict[str, t.Any]:
214+
def _recv(self, **kwargs: t.Any) -> t.Dict[str, t.Any]:
214215
assert self.socket is not None
215-
msg = await self.socket.recv_multipart(**kwargs)
216+
msg = self.socket.recv_multipart(**kwargs)
216217
ident, smsg = self.session.feed_identities(msg)
217218
return self.session.deserialize(smsg)
218219

219-
async def get_msg(self, timeout: t.Optional[float] = None) -> t.Dict[str, t.Any]:
220+
def get_msg(self, timeout: t.Optional[float] = None) -> t.Dict[str, t.Any]:
220221
"""Gets a message if there is one that is ready."""
221222
assert self.socket is not None
222223
if timeout is not None:
223224
timeout *= 1000 # seconds to ms
224-
ready = await self.socket.poll(timeout)
225-
225+
ready = self.socket.poll(timeout)
226226
if ready:
227-
res = await self._recv()
227+
res = self._recv()
228228
return res
229229
else:
230230
raise Empty
231231

232-
async def get_msgs(self) -> t.List[t.Dict[str, t.Any]]:
232+
def get_msgs(self) -> t.List[t.Dict[str, t.Any]]:
233233
"""Get all messages that are currently ready."""
234234
msgs = []
235235
while True:
236236
try:
237-
msgs.append(await self.get_msg())
237+
msgs.append(self.get_msg())
238238
except Empty:
239239
break
240240
return msgs
241241

242-
async def msg_ready(self) -> bool:
242+
def msg_ready(self) -> bool:
243243
"""Is there a message that has been received?"""
244244
assert self.socket is not None
245-
return bool(await self.socket.poll(timeout=0))
245+
return bool(self.socket.poll(timeout=0))
246246

247247
def close(self) -> None:
248248
if self.socket is not None:
@@ -264,3 +264,60 @@ def send(self, msg: t.Dict[str, t.Any]) -> None:
264264

265265
def start(self) -> None:
266266
pass
267+
268+
269+
class AsyncZMQSocketChannel(ZMQSocketChannel):
270+
"""A ZMQ socket in an async API"""
271+
272+
socket: zmq.asyncio.Socket
273+
274+
def __init__(self, socket: zmq.asyncio.Socket, session: Session, loop: t.Any = None) -> None:
275+
"""Create a channel.
276+
277+
Parameters
278+
----------
279+
socket : :class:`zmq.asyncio.Socket`
280+
The ZMQ socket to use.
281+
session : :class:`session.Session`
282+
The session to use.
283+
loop
284+
Unused here, for other implementations
285+
"""
286+
if not isinstance(socket, zmq.asyncio.Socket):
287+
raise ValueError('Socket must be asyncio')
288+
super().__init__(socket, session)
289+
290+
async def _recv(self, **kwargs: t.Any) -> t.Dict[str, t.Any]: # type:ignore[override]
291+
assert self.socket is not None
292+
msg = await self.socket.recv_multipart(**kwargs)
293+
_, smsg = self.session.feed_identities(msg)
294+
return self.session.deserialize(smsg)
295+
296+
async def get_msg( # type:ignore[override]
297+
self, timeout: t.Optional[float] = None
298+
) -> t.Dict[str, t.Any]:
299+
"""Gets a message if there is one that is ready."""
300+
assert self.socket is not None
301+
if timeout is not None:
302+
timeout *= 1000 # seconds to ms
303+
ready = await self.socket.poll(timeout)
304+
if ready:
305+
res = await self._recv()
306+
return res
307+
else:
308+
raise Empty
309+
310+
async def get_msgs(self) -> t.List[t.Dict[str, t.Any]]: # type:ignore[override]
311+
"""Get all messages that are currently ready."""
312+
msgs = []
313+
while True:
314+
try:
315+
msgs.append(await self.get_msg())
316+
except Empty:
317+
break
318+
return msgs
319+
320+
async def msg_ready(self) -> bool: # type:ignore[override]
321+
"""Is there a message that has been received?"""
322+
assert self.socket is not None
323+
return bool(await self.socket.poll(timeout=0))

0 commit comments

Comments
 (0)