Annotation of OpenXM/src/jupyter/kernel.py, Revision 1.2
1.1 takayama 1: # coding: utf-8
1.2 ! takayama 2: # $OpenXM: OpenXM/src/jupyter/kernel.py,v 1.1 2019/05/27 07:07:43 takayama Exp $
1.1 takayama 3: from __future__ import print_function
4:
5: import codecs
6: import glob
7: import json
8: import logging
9: import os
10: import re
11: import shutil
12: import subprocess
13: import sys
14: import tempfile
15: import uuid
16: from xml.dom import minidom
17:
18: from metakernel import MetaKernel, ProcessMetaKernel, REPLWrapper, u
19: from metakernel.pexpect import which
20: from IPython.display import Image, SVG
21:
22: from . import __version__
23:
24:
25: STDIN_PROMPT = '__stdin_prompt>'
26: STDIN_PROMPT_REGEX = re.compile(r'\A.+?%s|debug> ' % STDIN_PROMPT)
27: HELP_LINKS = [
28: {
29: 'text': "Risa/Asir",
30: 'url': "http://www.openxm.org",
31: },
32: {
33: 'text': "Asir Kernel",
34: 'url': "http://www.openxm.org",
35: },
36:
37: ] + MetaKernel.help_links
38:
39:
40: def get_kernel_json():
41: """Get the kernel json for the kernel.
42: """
43: here = os.path.dirname(__file__)
44: default_json_file = os.path.join(here, 'kernel.json')
45: json_file = os.environ.get('ASIR_KERNEL_JSON', default_json_file)
46: with open(json_file) as fid:
47: data = json.load(fid)
48: data['argv'][0] = sys.executable
49: return data
50:
51:
52: class OctaveKernel(ProcessMetaKernel):
53: implementation = 'Asir Kernel'
54: implementation_version = __version__,
55: language = 'asir'
56: help_links = HELP_LINKS
57: kernel_json = get_kernel_json()
58:
59: _octave_engine = None
60: _language_version = None
61:
62: @property
63: def language_version(self):
64: if self._language_version:
65: return self._language_version
66: ver = self.octave_engine.eval('version', silent=True)
67: ver = self._language_version = ver.split()[-1]
68: return ver
69:
70: @property
71: def language_info(self):
72: return {'mimetype': 'text/x-octave',
73: 'name': 'c',
74: 'file_extension': '.rr',
75: 'version': self.language_version,
76: 'help_links': HELP_LINKS}
77:
78: @property
79: def banner(self):
80: msg = 'Asir Kernel v%s running Risa/Asir v%s'
81: return msg % (__version__, self.language_version)
82:
83: @property
84: def octave_engine(self):
85: if self._octave_engine:
86: return self._octave_engine
87: self._octave_engine = OctaveEngine(plot_settings=self.plot_settings,
88: error_handler=self.Error,
89: stdin_handler=self.raw_input,
90: stream_handler=self.Print,
91: logger=self.log)
92: return self._octave_engine
93:
94: def makeWrapper(self):
95: """Start an Octave process and return a :class:`REPLWrapper` object.
96: """
97: return self.octave_engine.repl
98:
99: def do_execute_direct(self, code, silent=False):
100: if code.strip() in ['quit', 'quit()', 'exit', 'exit()']:
101: self._octave_engine = None
102: self.do_shutdown(True)
103: return
104: # f = open('tmptmp.txt','a');f.write(str(code));f.close() #####
105: # self._octave_engine.logger.debug(str(code)) #####
1.2 ! takayama 106: val = ProcessMetaKernel.do_execute_direct(self, code+';;', silent=silent)
1.1 takayama 107: if not silent:
108: try:
109: plot_dir = self.octave_engine.make_figures()
110: except Exception as e:
111: self.Error(e)
112: return val
113: if plot_dir:
114: for image in self.octave_engine.extract_figures(plot_dir, True):
115: self.Display(image)
116: return val
117:
118: def get_kernel_help_on(self, info, level=0, none_on_fail=False):
119: obj = info.get('help_obj', '')
120: if not obj or len(obj.split()) > 1:
121: if none_on_fail:
122: return None
123: else:
124: return ""
125: return self.octave_engine.eval('help %s' % obj, silent=True)
126:
127: def Print(self, *args, **kwargs):
128: # Ignore standalone input hook displays.
129: out = []
130: for arg in args:
131: if arg.strip() == STDIN_PROMPT:
132: return
133: if arg.strip().startswith(STDIN_PROMPT):
134: arg = arg.replace(STDIN_PROMPT, '')
135: out.append(arg)
136: super(OctaveKernel, self).Print(*out, **kwargs)
137:
138: def raw_input(self, text):
139: # Remove the stdin prompt to restore the original prompt.
140: text = text.replace(STDIN_PROMPT, '')
141: return super(OctaveKernel, self).raw_input(text)
142:
143: def get_completions(self, info):
144: """
145: Get completions from kernel based on info dict.
146: """
147: cmd = 'completion_matches("%s")' % info['obj']
148: val = self.octave_engine.eval(cmd, silent=True)
149: return val and val.splitlines() or []
150:
151: def handle_plot_settings(self):
152: """Handle the current plot settings"""
153: self.octave_engine.plot_settings = self.plot_settings
154:
155:
156: class OctaveEngine(object):
157:
158: def __init__(self, error_handler=None, stream_handler=None,
159: stdin_handler=None, plot_settings=None,
160: logger=None):
161: self.logger = logger
162: self.executable = self._get_executable()
163: self.repl = self._create_repl()
164: self.error_handler = error_handler
165: self.stream_handler = stream_handler
166: self.stdin_handler = stdin_handler
167: self._startup(plot_settings)
168:
169: @property
170: def plot_settings(self):
171: return None
172: return self._plot_settings
173:
174: @plot_settings.setter
175: def plot_settings(self, settings):
176: settings = settings or dict(backend='inline')
177: return None
178: self._plot_settings = settings
179:
180: # Remove "None" keys so we can use setdefault below.
181: keys = ['format', 'backend', 'width', 'height', 'resolution',
182: 'backend', 'name']
183: for key in keys:
184: if key in settings and settings.get(key, None) is None:
185: del settings[key]
186:
187: if sys.platform == 'darwin':
188: settings.setdefault('format', 'svg')
189: else:
190: settings.setdefault('format', 'png')
191:
192: settings.setdefault('backend', 'inline')
193: settings.setdefault('width', -1)
194: settings.setdefault('height', -1)
195: settings.setdefault('resolution', 0)
196: settings.setdefault('name', 'Figure')
197:
198: cmds = []
199: if settings['backend'] == 'inline':
200: cmds.append("set(0, 'defaultfigurevisible', 'off');")
201: else:
202: cmds.append("set(0, 'defaultfigurevisible', 'on');")
203: cmds.append("graphics_toolkit('%s');" % settings['backend'])
204: self.eval('\n'.join(cmds))
205:
206: def eval(self, code, timeout=None, silent=False):
207: """Evaluate code using the engine.
208: """
209: stream_handler = None if silent else self.stream_handler
210: if self.logger:
211: # self.logger.setLevel(logging.DEBUG) ####
212: self.logger.debug('Asir eval:')
213: self.logger.debug(code)
214: try:
215: resp = self.repl.run_command(code.rstrip(),
216: timeout=timeout,
217: stream_handler=stream_handler,
218: stdin_handler=self.stdin_handler)
219: resp = resp.replace(STDIN_PROMPT, '')
220: if self.logger and resp:
221: self.logger.debug(resp)
222: return resp
223: except KeyboardInterrupt:
224: return self._interrupt(True)
225: except Exception as e:
226: if self.error_handler:
227: self.error_handler(e)
228: else:
229: raise e
230:
231: def make_figures(self, plot_dir=None):
232: """Create figures for the current figures.
233:
234: Parameters
235: ----------
236: plot_dir: str, optional
237: The directory in which to create the plots.
238:
239: Returns
240: -------
241: out: str
242: The plot directory containing the files.
243: """
244: return None
245: settings = self.plot_settings
246: if settings['backend'] != 'inline':
247: self.eval('drawnow("expose");')
248: if not plot_dir:
249: return
250: fmt = settings['format']
251: res = settings['resolution']
252: wid = settings['width']
253: hgt = settings['height']
254: name = settings['name']
255: plot_dir = plot_dir or tempfile.mkdtemp()
256: plot_dir = plot_dir.replace(os.path.sep, '/')
257:
258: # Do not overwrite any existing plot files.
259: spec = os.path.join(plot_dir, '%s*' % name)
260: start = len(glob.glob(spec))
261:
262: make_figs = '_make_figures("%s", "%s", "%s", %d, %d, %d, %d)'
263: make_figs = make_figs % (plot_dir, fmt, name, wid, hgt, res, start)
264: resp = self.eval(make_figs, silent=True)
265: msg = 'Inline plot failed, consider trying another graphics toolkit\n'
266: if resp and 'error:' in resp:
267: resp = msg + resp
268: if self.error_handler:
269: self.error_handler(resp)
270: else:
271: raise Exception(resp)
272: return plot_dir
273:
274: def extract_figures(self, plot_dir, remove=False):
275: """Get a list of IPython Image objects for the created figures.
276:
277: Parameters
278: ----------
279: plot_dir: str
280: The directory in which to create the plots.
281: remove: bool, optional.
282: Whether to remove the plot directory after saving.
283: """
284: images = []
285: spec = os.path.join(plot_dir, '%s*' % self.plot_settings['name'])
286: for fname in reversed(glob.glob(spec)):
287: filename = os.path.join(plot_dir, fname)
288: try:
289: if fname.lower().endswith('.svg'):
290: im = self._handle_svg(filename)
291: else:
292: im = Image(filename)
293: images.append(im)
294: except Exception as e:
295: if self.error_handler:
296: self.error_handler(e)
297: else:
298: raise e
299: if remove:
300: shutil.rmtree(plot_dir, True)
301: return images
302:
303: def _startup(self, plot_settings):
304: return None
305:
306: def _handle_svg(self, filename):
307: """
308: Handle special considerations for SVG images.
309: """
310: # Gnuplot can create invalid characters in SVG files.
311: with codecs.open(filename, 'r', encoding='utf-8',
312: errors='replace') as fid:
313: data = fid.read()
314: im = SVG(data=data)
315: try:
316: im.data = self._fix_svg_size(im.data)
317: except Exception:
318: pass
319: return im
320:
321: def _fix_svg_size(self, data):
322: """GnuPlot SVGs do not have height/width attributes. Set
323: these to be the same as the viewBox, so that the browser
324: scales the image correctly.
325: """
326: # Minidom does not support parseUnicode, so it must be decoded
327: # to accept unicode characters
328: parsed = minidom.parseString(data.encode('utf-8'))
329: (svg,) = parsed.getElementsByTagName('svg')
330:
331: viewbox = svg.getAttribute('viewBox').split(' ')
332: width, height = viewbox[2:]
333: width, height = int(width), int(height)
334:
335: # Handle overrides in case they were not encoded.
336: settings = self.plot_settings
337: if settings['width'] != -1:
338: if settings['height'] == -1:
339: height = height * settings['width'] / width
340: width = settings['width']
341: if settings['height'] != -1:
342: if settings['width'] == -1:
343: width = width * settings['height'] / height
344: height = settings['height']
345:
346: svg.setAttribute('width', '%dpx' % width)
347: svg.setAttribute('height', '%dpx' % height)
348: return svg.toxml()
349:
350: def _create_repl(self):
351: cmd = self.executable
352: if 'asir-cli' not in cmd:
353: version_cmd = [self.executable, '--version']
354: version = subprocess.check_output(version_cmd).decode('utf-8')
355: if 'version 4' in version:
356: cmd += ' --no-gui'
357: # Interactive mode prevents crashing on Windows on syntax errors.
358: # Delay sourcing the "~/.octaverc" file in case it displays a pager.
359: cmd += ' --interactive --quiet --no-init-file '
360:
361: # Add cli options provided by the user.
362: cmd += os.environ.get('OCTAVE_CLI_OPTIONS', '')
363:
364: orig_prompt = u('PEXPECT_PROMPT>')
365: change_prompt = u("base_prompt('{0}')")
366:
367: repl = REPLWrapper(cmd, orig_prompt, change_prompt,
368: stdin_prompt_regex=STDIN_PROMPT_REGEX)
369: if os.name == 'nt':
370: repl.child.crlf = '\n'
371: repl.interrupt = self._interrupt
372: # Remove the default 50ms delay before sending lines.
373: repl.child.delaybeforesend = None
374: return repl
375:
376: def _interrupt(self, silent=False):
377: if (os.name == 'nt'):
378: msg = '** Warning: Cannot interrupt Octave on Windows'
379: if self.stream_handler:
380: self.stream_handler(msg)
381: elif self.logger:
382: self.logger.warn(msg)
383: return self._interrupt_expect(silent)
384: return REPLWrapper.interrupt(self.repl)
385:
386: def _interrupt_expect(self, silent):
387: repl = self.repl
388: child = repl.child
389: expects = [repl.prompt_regex, child.linesep]
390: expected = uuid.uuid4().hex
391: repl.sendline('disp("%s");' % expected)
392: if repl.prompt_emit_cmd:
393: repl.sendline(repl.prompt_emit_cmd)
394: lines = []
395: while True:
396: # Prevent a keyboard interrupt from breaking this up.
397: while True:
398: try:
399: pos = child.expect(expects)
400: break
401: except KeyboardInterrupt:
402: pass
403: if pos == 1: # End of line received
404: line = child.before
405: if silent:
406: lines.append(line)
407: else:
408: self.stream_handler(line)
409: else:
410: line = child.before
411: if line.strip() == expected:
412: break
413: if len(line) != 0:
414: # prompt received, but partial line precedes it
415: if silent:
416: lines.append(line)
417: else:
418: self.stream_handler(line)
419: return '\n'.join(lines)
420:
421: def _get_executable(self):
422: """Find the best octave executable.
423: """
424: executable = os.environ.get('ASIR_EXECUTABLE', None)
425: if not executable or not which(executable):
426: if which('asir-cli'):
427: executable = 'asir-cli'
428: else:
429: msg = ('asir-cli Executable not found, please add to path or set'
430: '"ASIR_EXECUTABLE" environment variable')
431: raise OSError(msg)
432: executable = executable.replace(os.path.sep, '/')
433: return executable
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>