Skip to content

Commit fd2bc4c

Browse files
committed
add BlockingKernelClient.run
Captures output, returns reply
1 parent 40ed846 commit fd2bc4c

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed

jupyter_client/blocking/client.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,31 @@
55
# Copyright (c) Jupyter Development Team.
66
# Distributed under the terms of the Modified BSD License.
77

8+
from __future__ import print_function
9+
810
try:
911
from queue import Empty # Python 3
1012
except ImportError:
1113
from Queue import Empty # Python 2
14+
import sys
1215
import time
1316

17+
try:
18+
monotonic = time.monotonic
19+
except NameError: # py2
20+
monotonic = time.clock # close enough
21+
1422
from traitlets import Type
1523
from jupyter_client.channels import HBChannel
1624
from jupyter_client.client import KernelClient
1725
from .channels import ZMQSocketChannel
1826

27+
try:
28+
TimeoutError
29+
except NameError:
30+
# py2
31+
TimeoutError = RuntimeError
32+
1933

2034
class BlockingKernelClient(KernelClient):
2135
"""A BlockingKernelClient """
@@ -74,3 +88,111 @@ def wait_for_ready(self, timeout=None):
7488
iopub_channel_class = Type(ZMQSocketChannel)
7589
stdin_channel_class = Type(ZMQSocketChannel)
7690
hb_channel_class = Type(HBChannel)
91+
92+
def run(self, code, silent=False, store_history=True,
93+
user_expressions=None, stop_on_error=True,
94+
timeout=None,
95+
):
96+
"""Run code in the kernel, redisplaying output.
97+
98+
Wraps a call to `.execute`, capturing and redisplaying any output produced.
99+
The execute_reply is returned.
100+
101+
Parameters
102+
----------
103+
code : str
104+
A string of code in the kernel's language.
105+
106+
silent : bool, optional (default False)
107+
If set, the kernel will execute the code as quietly possible, and
108+
will force store_history to be False.
109+
110+
store_history : bool, optional (default True)
111+
If set, the kernel will store command history. This is forced
112+
to be False if silent is True.
113+
114+
user_expressions : dict, optional
115+
A dict mapping names to expressions to be evaluated in the user's
116+
dict. The expression values are returned as strings formatted using
117+
:func:`repr`.
118+
119+
stop_on_error: bool, optional (default True)
120+
Flag whether to abort the execution queue, if an exception is encountered.
121+
timeout: int or None (default None)
122+
Timeout (in seconds) to wait for output. If None, wait forever.
123+
124+
Returns
125+
-------
126+
reply: dict
127+
The execute_reply message.
128+
"""
129+
if not self.iopub_channel.is_alive():
130+
raise RuntimeError("IOPub channel must be running to receive output")
131+
msg_id = self.execute(code,
132+
silent=silent,
133+
store_history=store_history,
134+
user_expressions=user_expressions,
135+
stop_on_error=stop_on_error,
136+
)
137+
138+
if 'IPython' in sys.modules:
139+
from IPython import get_ipython
140+
ip = get_ipython()
141+
in_kernel = getattr(ip, 'kernel', False)
142+
if in_kernel:
143+
socket = ip.display_pub.pub_socket
144+
session = ip.display_pub.session
145+
parent_header = ip.display_pub.parent_header
146+
else:
147+
in_kernel = False
148+
149+
# set deadline based on timeout
150+
start = monotonic()
151+
if timeout is not None:
152+
deadline = monotonic() + timeout
153+
154+
# wait for output and redisplay it
155+
while True:
156+
if timeout is not None:
157+
timeout = max(0, deadline - monotonic())
158+
try:
159+
msg = self.get_iopub_msg(timeout=timeout)
160+
except Empty:
161+
raise TimeoutError("Timeout waiting for IPython output")
162+
163+
if msg['parent_header'].get('msg_id') != msg_id:
164+
# not from my request
165+
continue
166+
msg_type = msg['header']['msg_type']
167+
content = msg['content']
168+
if msg_type == 'status':
169+
if content['execution_state'] == 'idle':
170+
# idle means output is done
171+
break
172+
elif msg_type == 'stream':
173+
stream = getattr(sys, content['name'])
174+
stream.write(content['text'])
175+
elif msg_type in ('display_data', 'execute_result', 'error'):
176+
if in_kernel:
177+
session.send(socket, msg_type, content, parent=parent_header)
178+
else:
179+
if msg_type == 'error':
180+
print('\n'.join(content['traceback']), file=sys.stderr)
181+
else:
182+
sys.stdout.write(content['data'].get('text/plain', ''))
183+
else:
184+
pass
185+
186+
# output is done, get the reply
187+
while True:
188+
if timeout is not None:
189+
timeout = max(0, deadline - monotonic())
190+
try:
191+
reply = self.get_shell_msg(timeout=timeout)
192+
except Empty:
193+
raise TimeoutError("Timeout waiting for reply")
194+
if reply['parent_header'].get('msg_id') != msg_id:
195+
# not my reply, someone may have forgotten to retrieve theirs
196+
continue
197+
return reply
198+

jupyter_client/tests/test_client.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Tests for the KernelClient"""
2+
3+
# Copyright (c) Jupyter Development Team.
4+
# Distributed under the terms of the Modified BSD License.
5+
6+
7+
import os
8+
pjoin = os.path.join
9+
from unittest import TestCase
10+
11+
from nose import SkipTest
12+
13+
from jupyter_client.kernelspec import KernelSpecManager, NoSuchKernel, NATIVE_KERNEL_NAME
14+
from ..manager import start_new_kernel
15+
from .utils import test_env
16+
17+
from IPython.utils.capture import capture_output
18+
19+
TIMEOUT = 30
20+
21+
class TestKernelClient(TestCase):
22+
def setUp(self):
23+
self.env_patch = test_env()
24+
self.env_patch.start()
25+
26+
def tearDown(self):
27+
self.env_patch.stop()
28+
29+
def test_run(self):
30+
try:
31+
KernelSpecManager().get_kernel_spec(NATIVE_KERNEL_NAME)
32+
except NoSuchKernel:
33+
raise SkipTest()
34+
km, kc = start_new_kernel(kernel_name=NATIVE_KERNEL_NAME)
35+
self.addCleanup(kc.stop_channels)
36+
self.addCleanup(km.shutdown_kernel)
37+
38+
with capture_output() as io:
39+
reply = kc.run("print('hello')", timeout=TIMEOUT)
40+
assert 'hello' in io.stdout
41+
assert reply['content']['status'] == 'ok'

0 commit comments

Comments
 (0)