Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/smc_sagews/smc_sagews/sage_jupyter.py
Views: 286
1
"""
2
sage_jupyter.py
3
4
Spawn and send commands to jupyter kernels.
5
6
AUTHORS:
7
- Hal Snyder (main author)
8
- William Stein
9
- Harald Schilly
10
"""
11
12
#########################################################################################
13
# Copyright (C) 2016, SageMath, Inc. #
14
# #
15
# Distributed under the terms of the GNU General Public License (GPL), version 2+ #
16
# #
17
# http://www.gnu.org/licenses/ #
18
#########################################################################################
19
20
from __future__ import absolute_import
21
import os
22
import string
23
import textwrap
24
import six
25
26
salvus = None # set externally
27
28
# jupyter kernel
29
30
31
class JUPYTER(object):
32
def __call__(self, kernel_name, **kwargs):
33
if kernel_name.startswith('sage'):
34
raise ValueError(
35
"You may not run Sage kernels from a Sage worksheet.\nInstead use the sage_select command in a Terminal to\nswitch to a different version of Sage, then restart your project."
36
)
37
return _jkmagic(kernel_name, **kwargs)
38
39
def available_kernels(self):
40
'''
41
Returns the list of available Jupyter kernels.
42
'''
43
v = os.popen("jupyter kernelspec list").readlines()
44
return ''.join(x for x in v if not x.strip().startswith('sage'))
45
46
def _get_doc(self):
47
ds0 = textwrap.dedent(r"""\
48
Use the jupyter command to use any Jupyter kernel that you have installed using from your CoCalc worksheet
49
50
| py3 = jupyter("python3")
51
52
After that, begin a sagews cell with %py3 to send statements to the Python3
53
kernel that you just created:
54
55
| %py3
56
| print(42)
57
58
You can even draw graphics.
59
60
| %py3
61
| import numpy as np; import pylab as plt
62
| x = np.linspace(0, 3*np.pi, 500)
63
| plt.plot(x, np.sin(x**2))
64
| plt.show()
65
66
You can set the default mode for all cells in the worksheet. After putting the following
67
in a cell, click the "restart" button, and you have an anaconda worksheet.
68
69
| %auto
70
| anaconda5 = jupyter('anaconda5')
71
| %default_mode anaconda5
72
73
Each call to jupyter creates its own Jupyter kernel. So you can have more than
74
one instance of the same kernel type in the same worksheet session.
75
76
| p1 = jupyter('python3')
77
| p2 = jupyter('python3')
78
| p1('a = 5')
79
| p2('a = 10')
80
| p1('print(a)') # prints 5
81
| p2('print(a)') # prints 10
82
83
For details on supported features and known issues, see the SMC Wiki page:
84
https://github.com/sagemathinc/cocalc/wiki/sagejupyter
85
""")
86
# print("calling JUPYTER._get_doc()")
87
kspec = self.available_kernels()
88
ks2 = kspec.replace("kernels:\n ", "kernels:\n\n|")
89
return ds0 + ks2
90
91
__doc__ = property(_get_doc)
92
93
94
jupyter = JUPYTER()
95
96
97
def _jkmagic(kernel_name, **kwargs):
98
r"""
99
Called when user issues `my_kernel = jupyter("kernel_name")` from a cell.
100
These are not intended to be called directly by user.
101
102
Start a jupyter kernel and create a sagews function for it. See docstring for class JUPYTER above.
103
Based on http://jupyter-client.readthedocs.io/en/latest/api/index.html
104
105
INPUT:
106
107
- ``kernel_name`` -- name of kernel as it appears in output of `jupyter kernelspec list`
108
109
"""
110
# CRITICAL: We import these here rather than at module scope, since they can take nearly a second
111
# of CPU time to import.
112
import jupyter_client # TIMING: takes a bit of time
113
from ansi2html import Ansi2HTMLConverter # TIMING: this is surprisingly bad.
114
from six.moves.queue import Empty # TIMING: cheap
115
import base64, tempfile, sys, re # TIMING: cheap
116
117
import warnings
118
import sage.misc.latex
119
with warnings.catch_warnings():
120
warnings.simplefilter("ignore", DeprecationWarning)
121
km, kc = jupyter_client.manager.start_new_kernel(
122
kernel_name=kernel_name)
123
import atexit
124
atexit.register(km.shutdown_kernel)
125
atexit.register(kc.hb_channel.close)
126
127
# inline: no header or style tags, useful for full == False
128
# linkify: little gimmik, translates URLs to anchor tags
129
conv = Ansi2HTMLConverter(inline=True, linkify=True)
130
131
def hout(s, block=True, scroll=False, error=False):
132
r"""
133
wrapper for ansi conversion before displaying output
134
135
INPUT:
136
137
- ``s`` - string to display in output of sagews cell
138
139
- ``block`` - set false to prevent newlines between output segments
140
141
- ``scroll`` - set true to put output into scrolling div
142
143
- ``error`` - set true to send text output to stderr
144
"""
145
# `full = False` or else cell output is huge
146
if "\x1b[" in s:
147
# use html output if ansi control code found in string
148
h = conv.convert(s, full=False)
149
if block:
150
h2 = '<pre style="font-family:monospace;">' + h + '</pre>'
151
else:
152
h2 = '<pre style="display:inline-block;margin-right:-1ch;font-family:monospace;">' + h + '</pre>'
153
if scroll:
154
h2 = '<div style="max-height:320px;width:80%;overflow:auto;">' + h2 + '</div>'
155
salvus.html(h2)
156
else:
157
if error:
158
sys.stderr.write(s)
159
sys.stderr.flush()
160
else:
161
sys.stdout.write(s)
162
sys.stdout.flush()
163
164
def run_code(code=None, **kwargs):
165
def p(*args):
166
from smc_sagews.sage_server import log
167
if run_code.debug:
168
log("kernel {}: {}".format(kernel_name,
169
' '.join(str(a) for a in args)))
170
171
if kwargs.get('get_kernel_client', False):
172
return kc
173
174
if kwargs.get('get_kernel_manager', False):
175
return km
176
177
if kwargs.get('get_kernel_name', False):
178
return kernel_name
179
180
if code is None:
181
return
182
183
# execute the code
184
msg_id = kc.execute(code)
185
186
# get responses
187
shell = kc.shell_channel
188
iopub = kc.iopub_channel
189
stdinj = kc.stdin_channel
190
191
# buffering for %capture because we don't know whether output is stdout or stderr
192
# until shell execute_reply message is received with status 'ok' or 'error'
193
capture_mode = not hasattr(sys.stdout._f, 'im_func')
194
195
# handle iopub messages
196
while True:
197
try:
198
msg = iopub.get_msg()
199
msg_type = msg['msg_type']
200
content = msg['content']
201
202
except Empty:
203
# shouldn't happen
204
p("iopub channel empty")
205
break
206
207
p('iopub', msg_type, str(content)[:300])
208
209
if msg['parent_header'].get('msg_id') != msg_id:
210
p('*** non-matching parent header')
211
continue
212
213
if msg_type == 'status' and content['execution_state'] == 'idle':
214
break
215
216
def display_mime(msg_data):
217
'''
218
jupyter server does send data dictionaries, that do contain mime-type:data mappings
219
depending on the type, handle them in the salvus API
220
'''
221
# sometimes output is sent in several formats
222
# 1. if there is an image format, prefer that
223
# 2. elif default text or image mode is available, prefer that
224
# 3. else choose first matching format in modes list
225
from smc_sagews.sage_salvus import show
226
227
def show_plot(data, suffix):
228
r"""
229
If an html style is defined for this kernel, use it.
230
Otherwise use salvus.file().
231
"""
232
suffix = '.' + suffix
233
fname = tempfile.mkstemp(suffix=suffix)[1]
234
fmode = 'wb' if six.PY3 else 'w'
235
with open(fname, fmode) as fo:
236
fo.write(data)
237
238
if run_code.smc_image_scaling is None:
239
salvus.file(fname)
240
else:
241
img_src = salvus.file(fname, show=False)
242
# The max-width is because this smc-image-scaling is very difficult
243
# to deal with when using React to render this on the share server,
244
# and not scaling down is really ugly. When the width gets set
245
# as normal in a notebook, this won't impact anything, but when
246
# it is displayed on share server (where width is not set) at least
247
# it won't look like total crap. See https://github.com/sagemathinc/cocalc/issues/4421
248
htms = '<img src="{0}" smc-image-scaling="{1}" style="max-width:840px"/>'.format(
249
img_src, run_code.smc_image_scaling)
250
salvus.html(htms)
251
os.unlink(fname)
252
253
mkeys = list(msg_data.keys())
254
imgmodes = ['image/svg+xml', 'image/png', 'image/jpeg']
255
txtmodes = [
256
'text/html', 'text/plain', 'text/latex', 'text/markdown'
257
]
258
if any('image' in k for k in mkeys):
259
dfim = run_code.default_image_fmt
260
#print('default_image_fmt %s'%dfim)
261
dispmode = next((m for m in mkeys if dfim in m), None)
262
if dispmode is None:
263
dispmode = next(m for m in imgmodes if m in mkeys)
264
#print('dispmode is %s'%dispmode)
265
# https://en.wikipedia.org/wiki/Data_scheme#Examples
266
# <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEU
267
# <img src='data:image/svg+xml;utf8,<svg ... > ... </svg>'>
268
if dispmode == 'image/svg+xml':
269
data = msg_data[dispmode]
270
show_plot(data, 'svg')
271
elif dispmode == 'image/png':
272
data = base64.standard_b64decode(msg_data[dispmode])
273
show_plot(data, 'png')
274
elif dispmode == 'image/jpeg':
275
data = base64.standard_b64decode(msg_data[dispmode])
276
show_plot(data, 'jpg')
277
return
278
elif any('text' in k for k in mkeys):
279
dftm = run_code.default_text_fmt
280
if capture_mode:
281
dftm = 'plain'
282
dispmode = next((m for m in mkeys if dftm in m), None)
283
if dispmode is None:
284
dispmode = next(m for m in txtmodes if m in mkeys)
285
if dispmode == 'text/plain':
286
p('text/plain', msg_data[dispmode])
287
# override if plain text is object marker for latex output
288
if re.match(r'<IPython.core.display.\w+ object>',
289
msg_data[dispmode]):
290
p("overriding plain -> latex")
291
show(msg_data['text/latex'])
292
else:
293
txt = re.sub(r"^\[\d+\] ", "", msg_data[dispmode])
294
hout(txt)
295
elif dispmode == 'text/html':
296
salvus.html(msg_data[dispmode])
297
elif dispmode == 'text/latex':
298
p('text/latex', msg_data[dispmode])
299
sage.misc.latex.latex.eval(msg_data[dispmode])
300
elif dispmode == 'text/markdown':
301
salvus.md(msg_data[dispmode])
302
return
303
304
# reminder of iopub loop is switch on value of msg_type
305
306
if msg_type == 'execute_input':
307
# the following is a cheat to avoid forking a separate thread to listen on stdin channel
308
# most of the time, ignore "execute_input" message type
309
# but if code calls python3 input(), wait for message on stdin channel
310
if 'code' in content:
311
ccode = content['code']
312
if kernel_name.startswith(
313
('python', 'anaconda', 'octave')) and re.match(
314
r'^[^#]*\W?input\(', ccode):
315
# FIXME input() will be ignored if it's aliased to another name
316
p('iopub input call: ', ccode)
317
try:
318
# do nothing if no messsage on stdin channel within 0.5 sec
319
imsg = stdinj.get_msg(timeout=0.5)
320
imsg_type = imsg['msg_type']
321
icontent = imsg['content']
322
p('stdin', imsg_type, str(icontent)[:300])
323
# kernel is now blocked waiting for input
324
if imsg_type == 'input_request':
325
prompt = '' if icontent[
326
'password'] else icontent['prompt']
327
value = salvus.raw_input(prompt=prompt)
328
xcontent = dict(value=value)
329
xmsg = kc.session.msg('input_reply', xcontent)
330
p('sending input_reply', xcontent)
331
stdinj.send(xmsg)
332
except:
333
pass
334
elif kernel_name == 'octave' and re.search(
335
r"\s*pause\s*([#;\n].*)?$", ccode, re.M):
336
# FIXME "1+2\npause\n3+4" pauses before executing any code
337
# would need block parser here
338
p('iopub octave pause: ', ccode)
339
try:
340
# do nothing if no messsage on stdin channel within 0.5 sec
341
imsg = stdinj.get_msg(timeout=0.5)
342
imsg_type = imsg['msg_type']
343
icontent = imsg['content']
344
p('stdin', imsg_type, str(icontent)[:300])
345
# kernel is now blocked waiting for input
346
if imsg_type == 'input_request':
347
prompt = "Paused, enter any value to continue"
348
value = salvus.raw_input(prompt=prompt)
349
xcontent = dict(value=value)
350
xmsg = kc.session.msg('input_reply', xcontent)
351
p('sending input_reply', xcontent)
352
stdinj.send(xmsg)
353
except:
354
pass
355
elif msg_type == 'execute_result':
356
if not 'data' in content:
357
continue
358
p('execute_result data keys: ', list(content['data'].keys()))
359
display_mime(content['data'])
360
361
elif msg_type == 'display_data':
362
if 'data' in content:
363
display_mime(content['data'])
364
365
elif msg_type == 'status':
366
if content['execution_state'] == 'idle':
367
# when idle, kernel has executed all input
368
break
369
370
elif msg_type == 'clear_output':
371
salvus.clear()
372
373
elif msg_type == 'stream':
374
if 'text' in content:
375
# bash kernel uses stream messages with output in 'text' field
376
# might be ANSI color-coded
377
if 'name' in content and content['name'] == 'stderr':
378
hout(content['text'], error=True)
379
else:
380
hout(content['text'], block=False)
381
382
elif msg_type == 'error':
383
# XXX look for ename and evalue too?
384
if 'traceback' in content:
385
tr = content['traceback']
386
if isinstance(tr, list):
387
for tr in content['traceback']:
388
hout(tr + '\n', error=True)
389
else:
390
hout(tr, error=True)
391
392
# handle shell messages
393
while True:
394
try:
395
msg = shell.get_msg(timeout=0.2)
396
msg_type = msg['msg_type']
397
content = msg['content']
398
except Empty:
399
# shouldn't happen
400
p("shell channel empty")
401
break
402
if msg['parent_header'].get('msg_id') == msg_id:
403
p('shell', msg_type, len(str(content)), str(content)[:300])
404
if msg_type == 'execute_reply':
405
if content['status'] == 'ok':
406
if 'payload' in content:
407
payload = content['payload']
408
if len(payload) > 0:
409
if 'data' in payload[0]:
410
data = payload[0]['data']
411
if 'text/plain' in data:
412
text = data['text/plain']
413
hout(text, scroll=True)
414
break
415
else:
416
# not our reply
417
continue
418
return
419
420
# 'html', 'plain', 'latex', 'markdown' - support depends on jupyter kernel
421
run_code.default_text_fmt = 'html'
422
423
# 'svg', 'png', 'jpeg' - support depends on jupyter kernel
424
run_code.default_image_fmt = 'png'
425
426
# set to floating point fraction e.g. 0.5
427
run_code.smc_image_scaling = None
428
429
# set True to record jupyter messages to sage_server log
430
run_code.debug = False
431
432
# allow `anaconda.jupyter_kernel.kernel_name` etc.
433
run_code.kernel_name = kernel_name
434
435
return run_code
436
437