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