[BACK]Return to kernel.py CVS log [TXT][DIR] Up to [local] / OpenXM / src / jupyter

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>