Path: blob/master/src/smc_sagews/smc_sagews/sage_jupyter.py
Views: 286
"""1sage_jupyter.py23Spawn and send commands to jupyter kernels.45AUTHORS:6- Hal Snyder (main author)7- William Stein8- Harald Schilly9"""1011#########################################################################################12# Copyright (C) 2016, SageMath, Inc. #13# #14# Distributed under the terms of the GNU General Public License (GPL), version 2+ #15# #16# http://www.gnu.org/licenses/ #17#########################################################################################1819from __future__ import absolute_import20import os21import string22import textwrap23import six2425salvus = None # set externally2627# jupyter kernel282930class JUPYTER(object):31def __call__(self, kernel_name, **kwargs):32if kernel_name.startswith('sage'):33raise ValueError(34"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."35)36return _jkmagic(kernel_name, **kwargs)3738def available_kernels(self):39'''40Returns the list of available Jupyter kernels.41'''42v = os.popen("jupyter kernelspec list").readlines()43return ''.join(x for x in v if not x.strip().startswith('sage'))4445def _get_doc(self):46ds0 = textwrap.dedent(r"""\47Use the jupyter command to use any Jupyter kernel that you have installed using from your CoCalc worksheet4849| py3 = jupyter("python3")5051After that, begin a sagews cell with %py3 to send statements to the Python352kernel that you just created:5354| %py355| print(42)5657You can even draw graphics.5859| %py360| import numpy as np; import pylab as plt61| x = np.linspace(0, 3*np.pi, 500)62| plt.plot(x, np.sin(x**2))63| plt.show()6465You can set the default mode for all cells in the worksheet. After putting the following66in a cell, click the "restart" button, and you have an anaconda worksheet.6768| %auto69| anaconda5 = jupyter('anaconda5')70| %default_mode anaconda57172Each call to jupyter creates its own Jupyter kernel. So you can have more than73one instance of the same kernel type in the same worksheet session.7475| p1 = jupyter('python3')76| p2 = jupyter('python3')77| p1('a = 5')78| p2('a = 10')79| p1('print(a)') # prints 580| p2('print(a)') # prints 108182For details on supported features and known issues, see the SMC Wiki page:83https://github.com/sagemathinc/cocalc/wiki/sagejupyter84""")85# print("calling JUPYTER._get_doc()")86kspec = self.available_kernels()87ks2 = kspec.replace("kernels:\n ", "kernels:\n\n|")88return ds0 + ks28990__doc__ = property(_get_doc)919293jupyter = JUPYTER()949596def _jkmagic(kernel_name, **kwargs):97r"""98Called when user issues `my_kernel = jupyter("kernel_name")` from a cell.99These are not intended to be called directly by user.100101Start a jupyter kernel and create a sagews function for it. See docstring for class JUPYTER above.102Based on http://jupyter-client.readthedocs.io/en/latest/api/index.html103104INPUT:105106- ``kernel_name`` -- name of kernel as it appears in output of `jupyter kernelspec list`107108"""109# CRITICAL: We import these here rather than at module scope, since they can take nearly a second110# of CPU time to import.111import jupyter_client # TIMING: takes a bit of time112from ansi2html import Ansi2HTMLConverter # TIMING: this is surprisingly bad.113from six.moves.queue import Empty # TIMING: cheap114import base64, tempfile, sys, re # TIMING: cheap115116import warnings117import sage.misc.latex118with warnings.catch_warnings():119warnings.simplefilter("ignore", DeprecationWarning)120km, kc = jupyter_client.manager.start_new_kernel(121kernel_name=kernel_name)122import atexit123atexit.register(km.shutdown_kernel)124atexit.register(kc.hb_channel.close)125126# inline: no header or style tags, useful for full == False127# linkify: little gimmik, translates URLs to anchor tags128conv = Ansi2HTMLConverter(inline=True, linkify=True)129130def hout(s, block=True, scroll=False, error=False):131r"""132wrapper for ansi conversion before displaying output133134INPUT:135136- ``s`` - string to display in output of sagews cell137138- ``block`` - set false to prevent newlines between output segments139140- ``scroll`` - set true to put output into scrolling div141142- ``error`` - set true to send text output to stderr143"""144# `full = False` or else cell output is huge145if "\x1b[" in s:146# use html output if ansi control code found in string147h = conv.convert(s, full=False)148if block:149h2 = '<pre style="font-family:monospace;">' + h + '</pre>'150else:151h2 = '<pre style="display:inline-block;margin-right:-1ch;font-family:monospace;">' + h + '</pre>'152if scroll:153h2 = '<div style="max-height:320px;width:80%;overflow:auto;">' + h2 + '</div>'154salvus.html(h2)155else:156if error:157sys.stderr.write(s)158sys.stderr.flush()159else:160sys.stdout.write(s)161sys.stdout.flush()162163def run_code(code=None, **kwargs):164def p(*args):165from smc_sagews.sage_server import log166if run_code.debug:167log("kernel {}: {}".format(kernel_name,168' '.join(str(a) for a in args)))169170if kwargs.get('get_kernel_client', False):171return kc172173if kwargs.get('get_kernel_manager', False):174return km175176if kwargs.get('get_kernel_name', False):177return kernel_name178179if code is None:180return181182# execute the code183msg_id = kc.execute(code)184185# get responses186shell = kc.shell_channel187iopub = kc.iopub_channel188stdinj = kc.stdin_channel189190# buffering for %capture because we don't know whether output is stdout or stderr191# until shell execute_reply message is received with status 'ok' or 'error'192capture_mode = not hasattr(sys.stdout._f, 'im_func')193194# handle iopub messages195while True:196try:197msg = iopub.get_msg()198msg_type = msg['msg_type']199content = msg['content']200201except Empty:202# shouldn't happen203p("iopub channel empty")204break205206p('iopub', msg_type, str(content)[:300])207208if msg['parent_header'].get('msg_id') != msg_id:209p('*** non-matching parent header')210continue211212if msg_type == 'status' and content['execution_state'] == 'idle':213break214215def display_mime(msg_data):216'''217jupyter server does send data dictionaries, that do contain mime-type:data mappings218depending on the type, handle them in the salvus API219'''220# sometimes output is sent in several formats221# 1. if there is an image format, prefer that222# 2. elif default text or image mode is available, prefer that223# 3. else choose first matching format in modes list224from smc_sagews.sage_salvus import show225226def show_plot(data, suffix):227r"""228If an html style is defined for this kernel, use it.229Otherwise use salvus.file().230"""231suffix = '.' + suffix232fname = tempfile.mkstemp(suffix=suffix)[1]233fmode = 'wb' if six.PY3 else 'w'234with open(fname, fmode) as fo:235fo.write(data)236237if run_code.smc_image_scaling is None:238salvus.file(fname)239else:240img_src = salvus.file(fname, show=False)241# The max-width is because this smc-image-scaling is very difficult242# to deal with when using React to render this on the share server,243# and not scaling down is really ugly. When the width gets set244# as normal in a notebook, this won't impact anything, but when245# it is displayed on share server (where width is not set) at least246# it won't look like total crap. See https://github.com/sagemathinc/cocalc/issues/4421247htms = '<img src="{0}" smc-image-scaling="{1}" style="max-width:840px"/>'.format(248img_src, run_code.smc_image_scaling)249salvus.html(htms)250os.unlink(fname)251252mkeys = list(msg_data.keys())253imgmodes = ['image/svg+xml', 'image/png', 'image/jpeg']254txtmodes = [255'text/html', 'text/plain', 'text/latex', 'text/markdown'256]257if any('image' in k for k in mkeys):258dfim = run_code.default_image_fmt259#print('default_image_fmt %s'%dfim)260dispmode = next((m for m in mkeys if dfim in m), None)261if dispmode is None:262dispmode = next(m for m in imgmodes if m in mkeys)263#print('dispmode is %s'%dispmode)264# https://en.wikipedia.org/wiki/Data_scheme#Examples265# <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEU266# <img src='data:image/svg+xml;utf8,<svg ... > ... </svg>'>267if dispmode == 'image/svg+xml':268data = msg_data[dispmode]269show_plot(data, 'svg')270elif dispmode == 'image/png':271data = base64.standard_b64decode(msg_data[dispmode])272show_plot(data, 'png')273elif dispmode == 'image/jpeg':274data = base64.standard_b64decode(msg_data[dispmode])275show_plot(data, 'jpg')276return277elif any('text' in k for k in mkeys):278dftm = run_code.default_text_fmt279if capture_mode:280dftm = 'plain'281dispmode = next((m for m in mkeys if dftm in m), None)282if dispmode is None:283dispmode = next(m for m in txtmodes if m in mkeys)284if dispmode == 'text/plain':285p('text/plain', msg_data[dispmode])286# override if plain text is object marker for latex output287if re.match(r'<IPython.core.display.\w+ object>',288msg_data[dispmode]):289p("overriding plain -> latex")290show(msg_data['text/latex'])291else:292txt = re.sub(r"^\[\d+\] ", "", msg_data[dispmode])293hout(txt)294elif dispmode == 'text/html':295salvus.html(msg_data[dispmode])296elif dispmode == 'text/latex':297p('text/latex', msg_data[dispmode])298sage.misc.latex.latex.eval(msg_data[dispmode])299elif dispmode == 'text/markdown':300salvus.md(msg_data[dispmode])301return302303# reminder of iopub loop is switch on value of msg_type304305if msg_type == 'execute_input':306# the following is a cheat to avoid forking a separate thread to listen on stdin channel307# most of the time, ignore "execute_input" message type308# but if code calls python3 input(), wait for message on stdin channel309if 'code' in content:310ccode = content['code']311if kernel_name.startswith(312('python', 'anaconda', 'octave')) and re.match(313r'^[^#]*\W?input\(', ccode):314# FIXME input() will be ignored if it's aliased to another name315p('iopub input call: ', ccode)316try:317# do nothing if no messsage on stdin channel within 0.5 sec318imsg = stdinj.get_msg(timeout=0.5)319imsg_type = imsg['msg_type']320icontent = imsg['content']321p('stdin', imsg_type, str(icontent)[:300])322# kernel is now blocked waiting for input323if imsg_type == 'input_request':324prompt = '' if icontent[325'password'] else icontent['prompt']326value = salvus.raw_input(prompt=prompt)327xcontent = dict(value=value)328xmsg = kc.session.msg('input_reply', xcontent)329p('sending input_reply', xcontent)330stdinj.send(xmsg)331except:332pass333elif kernel_name == 'octave' and re.search(334r"\s*pause\s*([#;\n].*)?$", ccode, re.M):335# FIXME "1+2\npause\n3+4" pauses before executing any code336# would need block parser here337p('iopub octave pause: ', ccode)338try:339# do nothing if no messsage on stdin channel within 0.5 sec340imsg = stdinj.get_msg(timeout=0.5)341imsg_type = imsg['msg_type']342icontent = imsg['content']343p('stdin', imsg_type, str(icontent)[:300])344# kernel is now blocked waiting for input345if imsg_type == 'input_request':346prompt = "Paused, enter any value to continue"347value = salvus.raw_input(prompt=prompt)348xcontent = dict(value=value)349xmsg = kc.session.msg('input_reply', xcontent)350p('sending input_reply', xcontent)351stdinj.send(xmsg)352except:353pass354elif msg_type == 'execute_result':355if not 'data' in content:356continue357p('execute_result data keys: ', list(content['data'].keys()))358display_mime(content['data'])359360elif msg_type == 'display_data':361if 'data' in content:362display_mime(content['data'])363364elif msg_type == 'status':365if content['execution_state'] == 'idle':366# when idle, kernel has executed all input367break368369elif msg_type == 'clear_output':370salvus.clear()371372elif msg_type == 'stream':373if 'text' in content:374# bash kernel uses stream messages with output in 'text' field375# might be ANSI color-coded376if 'name' in content and content['name'] == 'stderr':377hout(content['text'], error=True)378else:379hout(content['text'], block=False)380381elif msg_type == 'error':382# XXX look for ename and evalue too?383if 'traceback' in content:384tr = content['traceback']385if isinstance(tr, list):386for tr in content['traceback']:387hout(tr + '\n', error=True)388else:389hout(tr, error=True)390391# handle shell messages392while True:393try:394msg = shell.get_msg(timeout=0.2)395msg_type = msg['msg_type']396content = msg['content']397except Empty:398# shouldn't happen399p("shell channel empty")400break401if msg['parent_header'].get('msg_id') == msg_id:402p('shell', msg_type, len(str(content)), str(content)[:300])403if msg_type == 'execute_reply':404if content['status'] == 'ok':405if 'payload' in content:406payload = content['payload']407if len(payload) > 0:408if 'data' in payload[0]:409data = payload[0]['data']410if 'text/plain' in data:411text = data['text/plain']412hout(text, scroll=True)413break414else:415# not our reply416continue417return418419# 'html', 'plain', 'latex', 'markdown' - support depends on jupyter kernel420run_code.default_text_fmt = 'html'421422# 'svg', 'png', 'jpeg' - support depends on jupyter kernel423run_code.default_image_fmt = 'png'424425# set to floating point fraction e.g. 0.5426run_code.smc_image_scaling = None427428# set True to record jupyter messages to sage_server log429run_code.debug = False430431# allow `anaconda.jupyter_kernel.kernel_name` etc.432run_code.kernel_name = kernel_name433434return run_code435436437