Annotation of OpenXM/src/jupyter/kernel.py, Revision 1.1
1.1 ! takayama 1: # coding: utf-8
! 2: # $OpenXM$
! 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)) #####
! 106: val = ProcessMetaKernel.do_execute_direct(self, code, silent=silent)
! 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>