diff --git a/IPython/core/display.py b/IPython/core/display.py index 8c043a1..e99a4fa 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -126,13 +126,13 @@ def display(*objs, **kwargs): else: continue if raw: - publish_display_data('display', obj, metadata) + publish_display_data(data=obj, metadata=metadata) else: format_dict, md_dict = format(obj, include=include, exclude=exclude) if metadata: # kwarg-specified metadata gets precedence _merge(md_dict, metadata) - publish_display_data('display', format_dict, md_dict) + publish_display_data(data=format_dict, metadata=md_dict) def display_pretty(*objs, **kwargs): diff --git a/IPython/core/displaypub.py b/IPython/core/displaypub.py index dedd9e9..99c4a61 100644 --- a/IPython/core/displaypub.py +++ b/IPython/core/displaypub.py @@ -10,22 +10,10 @@ There are two components of the display system: This module defines the logic display publishing. The display publisher uses the ``display_data`` message type that is defined in the IPython messaging spec. - -Authors: - -* Brian Granger """ -#----------------------------------------------------------------------------- -# Copyright (C) 2008-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. from __future__ import print_function @@ -45,29 +33,24 @@ class DisplayPublisher(Configurable): be accessed there. """ - def _validate_data(self, source, data, metadata=None): + def _validate_data(self, data, metadata=None): """Validate the display data. Parameters ---------- - source : str - The fully dotted name of the callable that created the data, like - :func:`foo.bar.my_formatter`. data : dict The formata data dictionary. metadata : dict Any metadata for the data. """ - if not isinstance(source, string_types): - raise TypeError('source must be a str, got: %r' % source) if not isinstance(data, dict): raise TypeError('data must be a dict, got: %r' % data) if metadata is not None: if not isinstance(metadata, dict): raise TypeError('metadata must be a dict, got: %r' % data) - def publish(self, source, data, metadata=None): + def publish(self, data, metadata=None, source=None): """Publish data and metadata to all frontends. See the ``display_data`` message in the messaging documentation for @@ -87,9 +70,6 @@ class DisplayPublisher(Configurable): Parameters ---------- - source : str - A string that give the function or method that created the data, - such as 'IPython.core.page'. data : dict A dictionary having keys that are valid MIME types (like 'text/plain' or 'image/svg+xml') and values that are the data for @@ -104,6 +84,8 @@ class DisplayPublisher(Configurable): the data. Metadata specific to each mime-type can be specified in the metadata dict with the same mime-type keys as the data itself. + source : str, deprecated + Unused. """ # The default is to simply write the plain text data using io.stdout. @@ -122,8 +104,8 @@ class CapturingDisplayPublisher(DisplayPublisher): """A DisplayPublisher that stores""" outputs = List() - def publish(self, source, data, metadata=None): - self.outputs.append((source, data, metadata)) + def publish(self, data, metadata=None, source=None): + self.outputs.append((data, metadata)) def clear_output(self, wait=False): super(CapturingDisplayPublisher, self).clear_output(wait) @@ -132,7 +114,7 @@ class CapturingDisplayPublisher(DisplayPublisher): del self.outputs[:] -def publish_display_data(source, data, metadata=None): +def publish_display_data(data, metadata=None, source=None): """Publish data and metadata to all frontends. See the ``display_data`` message in the messaging documentation for @@ -152,9 +134,6 @@ def publish_display_data(source, data, metadata=None): Parameters ---------- - source : str - A string that give the function or method that created the data, - such as 'IPython.core.page'. data : dict A dictionary having keys that are valid MIME types (like 'text/plain' or 'image/svg+xml') and values that are the data for @@ -168,12 +147,13 @@ def publish_display_data(source, data, metadata=None): arbitrary key, value pairs that frontends can use to interpret the data. mime-type keys matching those in data can be used to specify metadata about particular representations. + source : str, deprecated + Unused. """ from IPython.core.interactiveshell import InteractiveShell InteractiveShell.instance().display_pub.publish( - source, - data, - metadata + data=data, + metadata=metadata, ) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 4ab5569..fc710f2 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -10,12 +10,7 @@ # the file COPYING, distributed as part of this software. #----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - -from __future__ import absolute_import -from __future__ import print_function +from __future__ import absolute_import, print_function import __future__ import abc @@ -57,6 +52,7 @@ from IPython.core.payload import PayloadManager from IPython.core.prefilter import PrefilterManager from IPython.core.profiledir import ProfileDir from IPython.core.prompts import PromptManager +from IPython.core.usage import default_banner from IPython.lib.latextools import LaTeXTool from IPython.testing.skipdoctest import skip_doctest from IPython.utils import PyColorize @@ -233,6 +229,16 @@ class InteractiveShell(SingletonConfigurable): Enable magic commands to be called without the leading %. """ ) + + banner = Unicode('') + + banner1 = Unicode(default_banner, config=True, + help="""The part of the banner to be printed before the profile""" + ) + banner2 = Unicode('', config=True, + help="""The part of the banner to be printed after the profile""" + ) + cache_size = Integer(1000, config=True, help= """ Set the size of the output cache. The default is 1000, you can @@ -773,6 +779,24 @@ class InteractiveShell(SingletonConfigurable): sys.modules[self._orig_sys_modules_main_name] = self._orig_sys_modules_main_mod #------------------------------------------------------------------------- + # Things related to the banner + #------------------------------------------------------------------------- + + @property + def banner(self): + banner = self.banner1 + if self.profile and self.profile != 'default': + banner += '\nIPython profile: %s\n' % self.profile + if self.banner2: + banner += '\n' + self.banner2 + return banner + + def show_banner(self, banner=None): + if banner is None: + banner = self.banner + self.write(banner) + + #------------------------------------------------------------------------- # Things related to hooks #------------------------------------------------------------------------- @@ -1502,6 +1526,7 @@ class InteractiveShell(SingletonConfigurable): return 'not found' # so callers can take other action def object_inspect(self, oname, detail_level=0): + """Get object info about oname""" with self.builtin_trap: info = self._object_find(oname) if info.found: @@ -1511,6 +1536,17 @@ class InteractiveShell(SingletonConfigurable): else: return oinspect.object_info(name=oname, found=False) + def object_inspect_text(self, oname, detail_level=0): + """Get object info as formatted text""" + with self.builtin_trap: + info = self._object_find(oname) + if info.found: + return self.inspector._format_info(info.obj, oname, info=info, + detail_level=detail_level + ) + else: + raise KeyError(oname) + #------------------------------------------------------------------------- # Things related to history management #------------------------------------------------------------------------- @@ -2395,7 +2431,7 @@ class InteractiveShell(SingletonConfigurable): def _user_obj_error(self): """return simple exception dict - for use in user_variables / expressions + for use in user_expressions """ etype, evalue, tb = self._get_exc_info() @@ -2413,7 +2449,7 @@ class InteractiveShell(SingletonConfigurable): def _format_user_obj(self, obj): """format a user object to display dict - for use in user_expressions / variables + for use in user_expressions """ data, md = self.display_formatter.format(obj) @@ -2424,30 +2460,6 @@ class InteractiveShell(SingletonConfigurable): } return value - def user_variables(self, names): - """Get a list of variable names from the user's namespace. - - Parameters - ---------- - names : list of strings - A list of names of variables to be read from the user namespace. - - Returns - ------- - A dict, keyed by the input names and with the rich mime-type repr(s) of each value. - Each element will be a sub-dict of the same form as a display_data message. - """ - out = {} - user_ns = self.user_ns - - for varname in names: - try: - value = self._format_user_obj(user_ns[varname]) - except: - value = self._user_obj_error() - out[varname] = value - return out - def user_expressions(self, expressions): """Evaluate a dict of expressions in the user's namespace. diff --git a/IPython/core/oinspect.py b/IPython/core/oinspect.py index 6353856..253fc26 100644 --- a/IPython/core/oinspect.py +++ b/IPython/core/oinspect.py @@ -7,12 +7,9 @@ Similar in spirit to the inspect module, but all calls take a name argument to reference the name under which an object is being read. """ -#***************************************************************************** -# Copyright (C) 2001-2004 Fernando Perez <fperez@colorado.edu> -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#***************************************************************************** +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function __all__ = ['Inspector','InspectColors'] @@ -532,21 +529,9 @@ class Inspector: ("Init docstring", "init_docstring"), ("Call def", "call_def"), ("Call docstring", "call_docstring")] - - def pinfo(self,obj,oname='',formatter=None,info=None,detail_level=0): - """Show detailed information about an object. - - Optional arguments: - - - oname: name of the variable pointing to the object. - - - formatter: special formatter for docstrings (see pdoc) - - - info: a structure with some information fields which may have been - precomputed already. - - - detail_level: if set to 1, more information is given. - """ + + def _format_info(self, obj, oname='', formatter=None, info=None, detail_level=0): + """Format an info dict as text""" info = self.info(obj, oname=oname, formatter=formatter, info=info, detail_level=detail_level) displayfields = [] @@ -590,11 +575,30 @@ class Inspector: # Info for objects: else: add_fields(self.pinfo_fields_obj) - - # Finally send to printer/pager: + if displayfields: - page.page(self._format_fields(displayfields)) + return self._format_fields(displayfields) + else: + return u'' + + def pinfo(self, obj, oname='', formatter=None, info=None, detail_level=0): + """Show detailed information about an object. + + Optional arguments: + + - oname: name of the variable pointing to the object. + - formatter: special formatter for docstrings (see pdoc) + + - info: a structure with some information fields which may have been + precomputed already. + + - detail_level: if set to 1, more information is given. + """ + text = self._format_info(obj, oname, formatter, info, detail_level) + if text: + page.page(text) + def info(self, obj, oname='', formatter=None, info=None, detail_level=0): """Compute a dict with detailed information about an object. diff --git a/IPython/core/page.py b/IPython/core/page.py index 23fedb5..518b6f9 100644 --- a/IPython/core/page.py +++ b/IPython/core/page.py @@ -133,7 +133,10 @@ def _detect_screen_size(screen_lines_def): #screen_cols,'columns.' # dbg def page(strng, start=0, screen_lines=0, pager_cmd=None): - """Print a string, piping through a pager after a certain length. + """Display a string, piping through a pager after a certain length. + + strng can be a mime-bundle dict, supplying multiple representations, + keyed by mime-type. The screen_lines parameter specifies the number of *usable* lines of your terminal screen (total lines minus lines you need to reserve to show other @@ -152,6 +155,10 @@ def page(strng, start=0, screen_lines=0, pager_cmd=None): If no system pager works, the string is sent through a 'dumb pager' written in python, very simplistic. """ + + # for compatibility with mime-bundle form: + if isinstance(strng, dict): + strng = strng['text/plain'] # Some routines may auto-compute start offsets incorrectly and pass a # negative value. Offset to 0 for robustness. diff --git a/IPython/core/payloadpage.py b/IPython/core/payloadpage.py index b1d07a6..682fd9b 100644 --- a/IPython/core/payloadpage.py +++ b/IPython/core/payloadpage.py @@ -1,41 +1,16 @@ # encoding: utf-8 -""" -A payload based version of page. +"""A payload based version of page.""" -Authors: +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -* Brian Granger -* Fernando Perez -""" - -#----------------------------------------------------------------------------- -# Copyright (C) 2008-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - -# Third-party -try: - from docutils.core import publish_string -except ImportError: - # html paging won't be available, but we don't raise any errors. It's a - # purely optional feature. - pass - -# Our own -from IPython.core.interactiveshell import InteractiveShell +from IPython.core.getipython import get_ipython #----------------------------------------------------------------------------- # Classes and functions #----------------------------------------------------------------------------- -def page(strng, start=0, screen_lines=0, pager_cmd=None, - html=None, auto_html=False): +def page(strng, start=0, screen_lines=0, pager_cmd=None): """Print a string, piping through a pager. This version ignores the screen_lines and pager_cmd arguments and uses @@ -43,49 +18,28 @@ def page(strng, start=0, screen_lines=0, pager_cmd=None, Parameters ---------- - strng : str - Text to page. + strng : str or mime-dict + Text to page, or a mime-type keyed dict of already formatted data. start : int Starting line at which to place the display. - - html : str, optional - If given, an html string to send as well. - - auto_html : bool, optional - If true, the input string is assumed to be valid reStructuredText and is - converted to HTML with docutils. Note that if docutils is not found, - this option is silently ignored. - - Notes - ----- - - Only one of the ``html`` and ``auto_html`` options can be given, not - both. """ # Some routines may auto-compute start offsets incorrectly and pass a # negative value. Offset to 0 for robustness. start = max(0, start) - shell = InteractiveShell.instance() - - if auto_html: - try: - # These defaults ensure user configuration variables for docutils - # are not loaded, only our config is used here. - defaults = {'file_insertion_enabled': 0, - 'raw_enabled': 0, - '_disable_config': 1} - html = publish_string(strng, writer_name='html', - settings_overrides=defaults) - except: - pass - + shell = get_ipython() + + if isinstance(strng, dict): + data = strng + else: + data = {'text/plain' : strng} payload = dict( source='page', + data=data, text=strng, - html=html, - start_line_number=start + start=start, + screen_lines=screen_lines, ) shell.payload_manager.write_payload(payload) diff --git a/IPython/core/release.py b/IPython/core/release.py index e51a93c..822b940 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -40,7 +40,8 @@ version = __version__ # backwards compatibility name version_info = (_version_major, _version_minor, _version_patch, _version_extra) # Change this when incrementing the kernel protocol version -kernel_protocol_version_info = (4, 1) +kernel_protocol_version_info = (5, 0) +kernel_protocol_version = "%i.%i" % kernel_protocol_version_info description = "IPython: Productive Interactive Computing" diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index b01dc6d..f5ceb03 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -4,22 +4,11 @@ Historically the main classes in interactiveshell have been under-tested. This module should grow as many single-method tests as possible to trap many of the recurring bugs we seem to encounter with high-level interaction. - -Authors -------- -* Fernando Perez """ -#----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- -# stdlib +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + import ast import os import signal @@ -33,10 +22,8 @@ except ImportError: import mock from os.path import join -# third-party import nose.tools as nt -# Our own from IPython.core.inputtransformer import InputTransformer from IPython.testing.decorators import skipif, skip_win32, onlyif_unicode_paths from IPython.testing import tools as tt @@ -645,7 +632,7 @@ def test_user_variables(): ip.user_ns['dummy'] = d = DummyRepr() keys = set(['dummy', 'doesnotexist']) - r = ip.user_variables(keys) + r = ip.user_expressions({ key:key for key in keys}) nt.assert_equal(keys, set(r.keys())) dummy = r['dummy'] @@ -660,7 +647,7 @@ def test_user_variables(): dne = r['doesnotexist'] nt.assert_equal(dne['status'], 'error') - nt.assert_equal(dne['ename'], 'KeyError') + nt.assert_equal(dne['ename'], 'NameError') # back to text only ip.display_formatter.active_types = ['text/plain'] diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index b8aba88..f12f9d4 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -1,13 +1,10 @@ -//---------------------------------------------------------------------------- -// Copyright (C) 2008-2012 The IPython Development Team -// -// Distributed under the terms of the BSD License. The full license is in -// the file COPYING, distributed as part of this software. -//---------------------------------------------------------------------------- +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. //============================================================================ // Utilities //============================================================================ + IPython.namespace('IPython.utils'); IPython.utils = (function (IPython) { @@ -430,7 +427,7 @@ IPython.utils = (function (IPython) { var escape_html = function (text) { // escape text to HTML return $("<div/>").text(text).html(); - } + }; var get_body_data = function(key) { @@ -439,8 +436,40 @@ IPython.utils = (function (IPython) { // until we are building an actual request return decodeURIComponent($('body').data(key)); }; - - + + var to_absolute_cursor_pos = function (cm, cursor) { + // get the absolute cursor position from CodeMirror's col, ch + if (!cursor) { + cursor = cm.getCursor(); + } + var cursor_pos = cursor.ch; + for (var i = 0; i < cursor.line; i++) { + cursor_pos += cm.getLine(i).length + 1; + } + return cursor_pos; + }; + + var from_absolute_cursor_pos = function (cm, cursor_pos) { + // turn absolute cursor postion into CodeMirror col, ch cursor + var i, line; + var offset = 0; + for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) { + if (offset + line.length < cursor_pos) { + offset += line.length + 1; + } else { + return { + line : i, + ch : cursor_pos - offset, + }; + } + } + // reached end, return endpoint + return { + ch : line.length - 1, + line : i - 1, + }; + }; + // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript var browser = (function() { if (typeof navigator === 'undefined') { @@ -449,7 +478,7 @@ IPython.utils = (function (IPython) { } var N= navigator.appName, ua= navigator.userAgent, tem; var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i); - if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1]; + if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1]; M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?']; return M; })(); @@ -465,13 +494,13 @@ IPython.utils = (function (IPython) { if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS"; if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX"; if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux"; - return OSName + return OSName; })(); var is_or_has = function (a, b) { // Is b a child of a or a itself? return a.has(b).length !==0 || a.is(b); - } + }; var is_focused = function (e) { // Is element e, or one of its children focused? @@ -486,7 +515,7 @@ IPython.utils = (function (IPython) { } else { return false; } - } + }; var log_ajax_error = function (jqXHR, status, error) { // log ajax failures with informative messages @@ -499,7 +528,7 @@ IPython.utils = (function (IPython) { } console.log(msg); }; - + return { regex_split : regex_split, uuid : uuid, @@ -515,6 +544,8 @@ IPython.utils = (function (IPython) { splitext : splitext, escape_html : escape_html, always_new : always_new, + to_absolute_cursor_pos : to_absolute_cursor_pos, + from_absolute_cursor_pos : from_absolute_cursor_pos, browser : browser, platform: platform, is_or_has : is_or_has, diff --git a/IPython/html/static/notebook/js/completer.js b/IPython/html/static/notebook/js/completer.js index df940b3..bddf732 100644 --- a/IPython/html/static/notebook/js/completer.js +++ b/IPython/html/static/notebook/js/completer.js @@ -1,21 +1,26 @@ -// function completer. +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +// Completer // -// completer should be a class that takes an cell instance +// Completer is be a class that takes a cell instance. + var IPython = (function (IPython) { // that will prevent us from misspelling "use strict"; // easier key mapping var keycodes = IPython.keyboard.keycodes; + var utils = IPython.utils; - function prepend_n_prc(str, n) { + var prepend_n_prc = function(str, n) { for( var i =0 ; i< n ; i++){ str = '%'+str ; } return str; }; - function _existing_completion(item, completion_array){ + var _existing_completion = function(item, completion_array){ for( var i=0; i < completion_array.length; i++) { if (completion_array[i].trim().substr(-item.length) == item) { return true; @@ -143,13 +148,17 @@ var IPython = (function (IPython) { // one kernel completion came back, finish_completing will be called with the results // we fork here and directly call finish completing if kernel is busy + var cursor_pos = utils.to_absolute_cursor_pos(this.editor, cur); if (this.skip_kernel_completion) { - this.finish_completing({ - 'matches': [], - matched_text: "" - }); + this.finish_completing({ content: { + matches: [], + cursor_start: cursor_pos, + cursor_end: cursor_pos, + }}); } else { - this.cell.kernel.complete(line, cur.ch, $.proxy(this.finish_completing, this)); + this.cell.kernel.complete(this.editor.getValue(), cursor_pos, + $.proxy(this.finish_completing, this) + ); } }; @@ -157,7 +166,8 @@ var IPython = (function (IPython) { // let's build a function that wrap all that stuff into what is needed // for the new completer: var content = msg.content; - var matched_text = content.matched_text; + var start = content.cursor_start; + var end = content.cursor_end; var matches = content.matches; var cur = this.editor.getCursor(); @@ -165,7 +175,8 @@ var IPython = (function (IPython) { var filtered_results = []; //remove results from context completion //that are already in kernel completion - for (var i=0; i < results.length; i++) { + var i; + for (i=0; i < results.length; i++) { if (!_existing_completion(results[i].str, matches)) { filtered_results.push(results[i]); } @@ -174,18 +185,12 @@ var IPython = (function (IPython) { // append the introspection result, in order, at at the beginning of // the table and compute the replacement range from current cursor // positon and matched_text length. - for (var i = matches.length - 1; i >= 0; --i) { + for (i = matches.length - 1; i >= 0; --i) { filtered_results.unshift({ str: matches[i], type: "introspection", - from: { - line: cur.line, - ch: cur.ch - matched_text.length - }, - to: { - line: cur.line, - ch: cur.ch - } + from: utils.from_absolute_cursor_pos(this.editor, start), + to: utils.from_absolute_cursor_pos(this.editor, end) }); } @@ -249,8 +254,9 @@ var IPython = (function (IPython) { // After everything is on the page, compute the postion. // We put it above the code if it is too close to the bottom of the page. - cur.ch = cur.ch-matched_text.length; - var pos = this.editor.cursorCoords(cur); + var pos = this.editor.cursorCoords( + utils.from_absolute_cursor_pos(this.editor, start) + ); var left = pos.left-3; var top; var cheight = this.complete.height(); diff --git a/IPython/html/static/notebook/js/outputarea.js b/IPython/html/static/notebook/js/outputarea.js index 27a4ca9..337e07f 100644 --- a/IPython/html/static/notebook/js/outputarea.js +++ b/IPython/html/static/notebook/js/outputarea.js @@ -1,9 +1,5 @@ -//---------------------------------------------------------------------------- -// Copyright (C) 2008 The IPython Development Team -// -// Distributed under the terms of the BSD License. The full license is in -// the file COPYING, distributed as part of this software. -//---------------------------------------------------------------------------- +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. //============================================================================ // OutputArea @@ -221,15 +217,18 @@ var IPython = (function (IPython) { json = content.data; json.output_type = msg_type; json.metadata = content.metadata; - } else if (msg_type === "pyout") { + } else if (msg_type === "execute_result") { json = content.data; json.output_type = msg_type; json.metadata = content.metadata; json.prompt_number = content.execution_count; - } else if (msg_type === "pyerr") { + } else if (msg_type === "error") { json.ename = content.ename; json.evalue = content.evalue; json.traceback = content.traceback; + } else { + console.log("unhandled output message", msg); + return; } this.append_output(json); }; @@ -283,10 +282,10 @@ var IPython = (function (IPython) { needs_height_reset = true; } - if (json.output_type === 'pyout') { - this.append_pyout(json); - } else if (json.output_type === 'pyerr') { - this.append_pyerr(json); + if (json.output_type === 'execute_result') { + this.append_execute_result(json); + } else if (json.output_type === 'error') { + this.append_error(json); } else if (json.output_type === 'stream') { this.append_stream(json); } @@ -406,7 +405,7 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_pyout = function (json) { + OutputArea.prototype.append_execute_result = function (json) { var n = json.prompt_number || ' '; var toinsert = this.create_output_area(); if (this.prompt_area) { @@ -414,7 +413,7 @@ var IPython = (function (IPython) { } var inserted = this.append_mime_type(json, toinsert); if (inserted) { - inserted.addClass('output_pyout'); + inserted.addClass('output_result'); } this._safe_append(toinsert); // If we just output latex, typeset it. @@ -426,7 +425,7 @@ var IPython = (function (IPython) { }; - OutputArea.prototype.append_pyerr = function (json) { + OutputArea.prototype.append_error = function (json) { var tb = json.traceback; if (tb !== undefined && tb.length > 0) { var s = ''; @@ -438,7 +437,7 @@ var IPython = (function (IPython) { var toinsert = this.create_output_area(); var append_text = OutputArea.append_map['text/plain']; if (append_text) { - append_text.apply(this, [s, {}, toinsert]).addClass('output_pyerr'); + append_text.apply(this, [s, {}, toinsert]).addClass('output_error'); } this._safe_append(toinsert); } @@ -746,6 +745,8 @@ var IPython = (function (IPython) { // disable any other raw_inputs, if they are left around $("div.output_subarea.raw_input_container").remove(); + var input_type = content.password ? 'password' : 'text'; + area.append( $("<div/>") .addClass("box-flex1 output_subarea raw_input_container") @@ -757,7 +758,7 @@ var IPython = (function (IPython) { .append( $("<input/>") .addClass("raw_input") - .attr('type', 'text') + .attr('type', input_type) .attr("size", 47) .keydown(function (event, ui) { // make sure we submit on enter, @@ -786,10 +787,15 @@ var IPython = (function (IPython) { var theprompt = container.find("span.raw_input_prompt"); var theinput = container.find("input.raw_input"); var value = theinput.val(); + var echo = value; + // don't echo if it's a password + if (theinput.attr('type') == 'password') { + echo = '········'; + } var content = { output_type : 'stream', name : 'stdout', - text : theprompt.text() + value + '\n' + text : theprompt.text() + echo + '\n' } // remove form container container.parent().remove(); @@ -850,11 +856,26 @@ var IPython = (function (IPython) { for (var i=0; i<len; i++) { data = outputs[i]; var msg_type = data.output_type; - if (msg_type === "display_data" || msg_type === "pyout") { + if (msg_type == "pyout") { + // pyout message has been renamed to execute_result, + // but the nbformat has not been updated, + // so transform back to pyout for json. + msg_type = data.output_type = "execute_result"; + } else if (msg_type == "pyerr") { + // pyerr message has been renamed to error, + // but the nbformat has not been updated, + // so transform back to pyerr for json. + msg_type = data.output_type = "error"; + } + if (msg_type === "display_data" || msg_type === "execute_result") { // convert short keys to mime keys // TODO: remove mapping of short keys when we update to nbformat 4 data = this.rename_keys(data, OutputArea.mime_map_r); data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r); + // msg spec JSON is an object, nbformat v3 JSON is a JSON string + if (data["application/json"] !== undefined && typeof data["application/json"] === 'string') { + data["application/json"] = JSON.parse(data["application/json"]); + } } this.append_output(data); @@ -869,10 +890,25 @@ var IPython = (function (IPython) { for (var i=0; i<len; i++) { data = this.outputs[i]; var msg_type = data.output_type; - if (msg_type === "display_data" || msg_type === "pyout") { + if (msg_type === "display_data" || msg_type === "execute_result") { // convert mime keys to short keys data = this.rename_keys(data, OutputArea.mime_map); data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map); + // msg spec JSON is an object, nbformat v3 JSON is a JSON string + if (data.json !== undefined && typeof data.json !== 'string') { + data.json = JSON.stringify(data.json); + } + } + if (msg_type == "execute_result") { + // pyout message has been renamed to execute_result, + // but the nbformat has not been updated, + // so transform back to pyout for json. + data.output_type = "pyout"; + } else if (msg_type == "error") { + // pyerr message has been renamed to error, + // but the nbformat has not been updated, + // so transform back to pyerr for json. + data.output_type = "pyerr"; } outputs[i] = data; } diff --git a/IPython/html/static/notebook/js/pager.js b/IPython/html/static/notebook/js/pager.js index aac9bfb..dd4ead8 100644 --- a/IPython/html/static/notebook/js/pager.js +++ b/IPython/html/static/notebook/js/pager.js @@ -1,9 +1,5 @@ -//---------------------------------------------------------------------------- -// Copyright (C) 2008-2011 The IPython Development Team -// -// Distributed under the terms of the BSD License. The full license is in -// the file COPYING, distributed as part of this software. -//---------------------------------------------------------------------------- +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. //============================================================================ // Pager @@ -81,12 +77,18 @@ var IPython = (function (IPython) { var that = this; this.pager_element.bind('collapse_pager', function (event, extrap) { - var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast'; + var time = 'fast'; + if (extrap && extrap.duration) { + time = extrap.duration; + } that.pager_element.hide(time); }); this.pager_element.bind('expand_pager', function (event, extrap) { - var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast'; + var time = 'fast'; + if (extrap && extrap.duration) { + time = extrap.duration; + } that.pager_element.show(time); }); @@ -103,12 +105,13 @@ var IPython = (function (IPython) { that.toggle(); }); - $([IPython.events]).on('open_with_text.Pager', function (event, data) { - if (data.text.trim() !== '') { + $([IPython.events]).on('open_with_text.Pager', function (event, payload) { + // FIXME: support other mime types + if (payload.data['text/plain'] && payload.data['text/plain'] !== "") { that.clear(); that.expand(); - that.append_text(data.text); - }; + that.append_text(payload.data['text/plain']); + } }); }; @@ -117,7 +120,7 @@ var IPython = (function (IPython) { if (this.expanded === true) { this.expanded = false; this.pager_element.add($('div#notebook')).trigger('collapse_pager', extrap); - }; + } }; @@ -125,7 +128,7 @@ var IPython = (function (IPython) { if (this.expanded !== true) { this.expanded = true; this.pager_element.add($('div#notebook')).trigger('expand_pager', extrap); - }; + } }; @@ -134,7 +137,7 @@ var IPython = (function (IPython) { this.collapse(); } else { this.expand(); - }; + } }; @@ -160,8 +163,7 @@ var IPython = (function (IPython) { pager_body.append(this.pager_element.clone().children()); w.document.close(); this.collapse(); - - } + }; Pager.prototype.append_text = function (text) { // The only user content injected with this HTML call is escaped by diff --git a/IPython/html/static/notebook/js/tooltip.js b/IPython/html/static/notebook/js/tooltip.js index 7c55d6f..76d1ec0 100644 --- a/IPython/html/static/notebook/js/tooltip.js +++ b/IPython/html/static/notebook/js/tooltip.js @@ -1,9 +1,6 @@ -//---------------------------------------------------------------------------- -// Copyright (C) 2008-2011 The IPython Development Team -// -// Distributed under the terms of the BSD License. The full license is in -// the file COPYING, distributed as part of this software. -//---------------------------------------------------------------------------- +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + //============================================================================ // Tooltip //============================================================================ @@ -105,8 +102,8 @@ var IPython = (function (IPython) { this.tooltip.append(this.text); // function that will be called if you press tab 1, 2, 3... times in a row - this.tabs_functions = [function (cell, text) { - that._request_tooltip(cell, text); + this.tabs_functions = [function (cell, text, cursor) { + that._request_tooltip(cell, text, cursor); }, function () { that.expand(); }, function () { @@ -131,13 +128,10 @@ var IPython = (function (IPython) { Tooltip.prototype.showInPager = function (cell) { // reexecute last call in pager by appending ? to show back in pager var that = this; - var callbacks = {'shell' : { - 'payload' : { - 'page' : $.proxy(cell._open_with_pager, cell) - } - } - }; - cell.kernel.execute(that.name + '?', callbacks, {'silent': false, 'store_history': true}); + var payload = {}; + payload.text = that._reply.content.data['text/plain']; + + $([IPython.events]).trigger('open_with_text.Pager', payload); this.remove_and_cancel_tooltip(); }; @@ -222,10 +216,9 @@ var IPython = (function (IPython) { return Tooltip.last_token_re.exec(line); }; - Tooltip.prototype._request_tooltip = function (cell, line) { + Tooltip.prototype._request_tooltip = function (cell, text, cursor_pos) { var callbacks = $.proxy(this._show, this); - var oir_token = this.extract_oir_token(line); - var msg_id = cell.kernel.object_info(oir_token, callbacks); + var msg_id = cell.kernel.inspect(text, cursor_pos, callbacks); }; // make an imediate completion request @@ -236,10 +229,8 @@ var IPython = (function (IPython) { this.cancel_pending(); var editor = cell.code_mirror; var cursor = editor.getCursor(); - var text = editor.getRange({ - line: cursor.line, - ch: 0 - }, cursor).trim(); + var cursor_pos = utils.to_absolute_cursor_pos(editor, cursor); + var text = cell.get_text(); this._hide_if_no_docstring = hide_if_no_docstring; @@ -260,17 +251,12 @@ var IPython = (function (IPython) { this.reset_tabs_function (cell, text); } - // don't do anything if line beggin with '(' or is empty - if (text === "" || text === "(") { - return; - } - - this.tabs_functions[this._consecutive_counter](cell, text); + this.tabs_functions[this._consecutive_counter](cell, text, cursor_pos); // then if we are at the end of list function, reset if (this._consecutive_counter == this.tabs_functions.length) { - this.reset_tabs_function (cell, text); - } + this.reset_tabs_function (cell, text, cursor); + } return; }; @@ -302,6 +288,7 @@ var IPython = (function (IPython) { Tooltip.prototype._show = function (reply) { // move the bubble if it is not hidden // otherwise fade it + this._reply = reply; var content = reply.content; if (!content.found) { // object not found, nothing to show @@ -338,43 +325,14 @@ var IPython = (function (IPython) { this.arrow.animate({ 'left': posarrowleft + 'px' }); - - // build docstring - var defstring = content.call_def; - if (!defstring) { - defstring = content.init_definition; - } - if (!defstring) { - defstring = content.definition; - } - - var docstring = content.call_docstring; - if (!docstring) { - docstring = content.init_docstring; - } - if (!docstring) { - docstring = content.docstring; - } - - if (!docstring) { - // For reals this time, no docstring - if (this._hide_if_no_docstring) { - return; - } else { - docstring = "<empty docstring>"; - } - } - + this._hidden = false; this.tooltip.fadeIn('fast'); this.text.children().remove(); - + + // This should support rich data types, but only text/plain for now // Any HTML within the docstring is escaped by the fixConsole() method. - var pre = $('<pre/>').html(utils.fixConsole(docstring)); - if (defstring) { - var defstring_html = $('<pre/>').html(utils.fixConsole(defstring)); - this.text.append(defstring_html); - } + var pre = $('<pre/>').html(utils.fixConsole(content.data['text/plain'])); this.text.append(pre); // keep scroll top to be sure to always see the first line this.text.scrollTop(0); diff --git a/IPython/html/static/notebook/less/outputarea.less b/IPython/html/static/notebook/less/outputarea.less index 8bb5852..d003ef9 100644 --- a/IPython/html/static/notebook/less/outputarea.less +++ b/IPython/html/static/notebook/less/outputarea.less @@ -107,7 +107,7 @@ div.output_text { line-height: @code_line_height; } -/* stdout/stderr are 'text' as well as 'stream', but pyout/pyerr are *not* streams */ +/* stdout/stderr are 'text' as well as 'stream', but execute_result/error are *not* streams */ div.output_stream { } diff --git a/IPython/html/static/services/kernels/js/kernel.js b/IPython/html/static/services/kernels/js/kernel.js index 88ea978..8149f01 100644 --- a/IPython/html/static/services/kernels/js/kernel.js +++ b/IPython/html/static/services/kernels/js/kernel.js @@ -57,7 +57,8 @@ var IPython = (function (IPython) { msg_id : utils.uuid(), username : this.username, session : this.session_id, - msg_type : msg_type + msg_type : msg_type, + version : "5.0" }, metadata : metadata || {}, content : content, @@ -76,13 +77,13 @@ var IPython = (function (IPython) { // Initialize the iopub handlers Kernel.prototype.init_iopub_handlers = function () { - var output_types = ['stream', 'display_data', 'pyout', 'pyerr']; + var output_msg_types = ['stream', 'display_data', 'execute_result', 'error']; this._iopub_handlers = {}; this.register_iopub_handler('status', $.proxy(this._handle_status_message, this)); this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this)); - for (var i=0; i < output_types.length; i++) { - this.register_iopub_handler(output_types[i], $.proxy(this._handle_output_message, this)); + for (var i=0; i < output_msg_types.length; i++) { + this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this)); } }; @@ -246,7 +247,7 @@ var IPython = (function (IPython) { * Get kernel info * * @param callback {function} - * @method object_info + * @method kernel_info * * When calling this method, pass a callback function that expects one argument. * The callback will be passed the complete `kernel_info_reply` message documented @@ -263,28 +264,27 @@ var IPython = (function (IPython) { /** * Get info on an object * - * @param objname {string} + * @param code {string} + * @param cursor_pos {integer} * @param callback {function} - * @method object_info + * @method inspect * * When calling this method, pass a callback function that expects one argument. - * The callback will be passed the complete `object_info_reply` message documented + * The callback will be passed the complete `inspect_reply` message documented * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information) */ - Kernel.prototype.object_info = function (objname, callback) { + Kernel.prototype.inspect = function (code, cursor_pos, callback) { var callbacks; if (callback) { callbacks = { shell : { reply : callback } }; } - if (typeof(objname) !== null && objname !== null) { - var content = { - oname : objname.toString(), - detail_level : 0, - }; - return this.send_shell_message("object_info_request", content, callbacks); - } - return; + var content = { + code : code, + cursor_pos : cursor_pos, + detail_level : 0, + }; + return this.send_shell_message("inspect_request", content, callbacks); }; /** @@ -302,7 +302,6 @@ var IPython = (function (IPython) { * @param {object} [options] * @param [options.silent=false] {Boolean} * @param [options.user_expressions=empty_dict] {Dict} - * @param [options.user_variables=empty_list] {List od Strings} * @param [options.allow_stdin=false] {Boolean} true|false * * @example @@ -312,7 +311,6 @@ var IPython = (function (IPython) { * * options = { * silent : true, - * user_variables : [], * user_expressions : {}, * allow_stdin : false * } @@ -342,7 +340,6 @@ var IPython = (function (IPython) { code : code, silent : true, store_history : false, - user_variables : [], user_expressions : {}, allow_stdin : false }; @@ -363,21 +360,19 @@ var IPython = (function (IPython) { * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete) * * @method complete - * @param line {integer} + * @param code {string} * @param cursor_pos {integer} * @param callback {function} * */ - Kernel.prototype.complete = function (line, cursor_pos, callback) { + Kernel.prototype.complete = function (code, cursor_pos, callback) { var callbacks; if (callback) { callbacks = { shell : { reply : callback } }; } var content = { - text : '', - line : line, - block : null, - cursor_pos : cursor_pos + code : code, + cursor_pos : cursor_pos, }; return this.send_shell_message("complete_request", content, callbacks); }; @@ -573,7 +568,7 @@ var IPython = (function (IPython) { }; - // handle an output message (pyout, display_data, etc.) + // handle an output message (execute_result, display_data, etc.) Kernel.prototype._handle_output_message = function (msg) { var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id); if (!callbacks || !callbacks.iopub) { diff --git a/IPython/html/tests/notebook/display_image.js b/IPython/html/tests/notebook/display_image.js index 86b9319..e04a252 100644 --- a/IPython/html/tests/notebook/display_image.js +++ b/IPython/html/tests/notebook/display_image.js @@ -14,10 +14,9 @@ b64_image_data = { casper.notebook_test(function () { - // this.printLog(); this.test_img_shape = function(fmt, retina) { this.thenEvaluate(function (b64data, retina) { - IPython.notebook.get_cell(0).clear_output(); + IPython.notebook.insert_cell_at_index(0, "code"); var cell = IPython.notebook.get_cell(0); cell.set_text([ "import base64", diff --git a/IPython/html/tests/notebook/roundtrip.js b/IPython/html/tests/notebook/roundtrip.js index a9319b9..38ef881 100644 --- a/IPython/html/tests/notebook/roundtrip.js +++ b/IPython/html/tests/notebook/roundtrip.js @@ -33,7 +33,7 @@ function assert_has(short_name, json, result, result2) { } // helper function for checkout that the first two cells have a particular -// output_type (either 'pyout' or 'display_data'), and checks the to/fromJSON +// output_type (either 'execute_result' or 'display_data'), and checks the to/fromJSON // for a set of mimetype keys, using their short names ('javascript', 'text', // 'png', etc). function check_output_area(output_type, keys) { @@ -109,7 +109,7 @@ casper.notebook_test(function () { }); this.then(function () { - check_output_area.apply(this, ['pyout', ['text', 'json']]); + check_output_area.apply(this, ['execute_result', ['text', 'json']]); }); this.then(function() { @@ -127,7 +127,7 @@ casper.notebook_test(function () { }); this.then(function ( ) { - check_output_area.apply(this, ['pyout', ['text', 'latex']]); + check_output_area.apply(this, ['execute_result', ['text', 'latex']]); }); this.then(function() { @@ -145,7 +145,7 @@ casper.notebook_test(function () { }); this.then(function ( ) { - check_output_area.apply(this, ['pyout', ['text', 'html']]); + check_output_area.apply(this, ['execute_result', ['text', 'html']]); }); this.then(function() { @@ -165,7 +165,7 @@ casper.notebook_test(function () { this.thenEvaluate(function() { IPython.notebook.save_notebook(); }); this.then(function ( ) { - check_output_area.apply(this, ['pyout', ['text', 'png']]); + check_output_area.apply(this, ['execute_result', ['text', 'png']]); }); this.then(function() { @@ -184,7 +184,7 @@ casper.notebook_test(function () { }); this.then(function ( ) { - check_output_area.apply(this, ['pyout', ['text', 'jpeg']]); + check_output_area.apply(this, ['execute_result', ['text', 'jpeg']]); }); this.then(function() { @@ -202,7 +202,7 @@ casper.notebook_test(function () { }); this.then(function ( ) { - check_output_area.apply(this, ['pyout', ['text', 'svg']]); + check_output_area.apply(this, ['execute_result', ['text', 'svg']]); }); this.then(function() { @@ -238,7 +238,7 @@ casper.notebook_test(function () { 'display_data custom mimetype ' + long_name); var result = this.get_output_cell(0, 1); this.test.assertTrue(result.hasOwnProperty(long_name), - 'pyout custom mimetype ' + long_name); + 'execute_result custom mimetype ' + long_name); }); diff --git a/IPython/html/tests/notebook/safe_append_output.js b/IPython/html/tests/notebook/safe_append_output.js index 1217740..7604d7b 100644 --- a/IPython/html/tests/notebook/safe_append_output.js +++ b/IPython/html/tests/notebook/safe_append_output.js @@ -14,7 +14,7 @@ casper.notebook_test(function () { this.evaluate(function () { var cell = IPython.notebook.get_cell(0); cell.set_text( "dp = get_ipython().display_pub\n" + - "dp.publish('test', {'text/plain' : '5', 'image/png' : 5})" + "dp.publish({'text/plain' : '5', 'image/png' : 5})" ); cell.execute(); }); diff --git a/IPython/kernel/channels.py b/IPython/kernel/channels.py index 3b64865..4d875ba 100644 --- a/IPython/kernel/channels.py +++ b/IPython/kernel/channels.py @@ -1,20 +1,10 @@ -"""Base classes to manage a Client's interaction with a running kernel -""" +"""Base classes to manage a Client's interaction with a running kernel""" -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. from __future__ import absolute_import -# Standard library imports import atexit import errno from threading import Thread @@ -196,7 +186,7 @@ class ShellChannel(ZMQSocketChannel): proxy_methods = [ 'execute', 'complete', - 'object_info', + 'inspect', 'history', 'kernel_info', 'shutdown', @@ -227,7 +217,7 @@ class ShellChannel(ZMQSocketChannel): raise NotImplementedError('call_handlers must be defined in a subclass.') def execute(self, code, silent=False, store_history=True, - user_variables=None, user_expressions=None, allow_stdin=None): + user_expressions=None, allow_stdin=None): """Execute code in the kernel. Parameters @@ -243,11 +233,6 @@ class ShellChannel(ZMQSocketChannel): If set, the kernel will store command history. This is forced to be False if silent is True. - user_variables : list, optional - A list of variable names to pull from the user's namespace. They - will come back as a dict with these names as keys and their - :func:`repr` as values. - user_expressions : dict, optional A dict mapping names to expressions to be evaluated in the user's dict. The expression values are returned as strings formatted using @@ -264,8 +249,6 @@ class ShellChannel(ZMQSocketChannel): ------- The msg_id of the message sent. """ - if user_variables is None: - user_variables = [] if user_expressions is None: user_expressions = {} if allow_stdin is None: @@ -275,13 +258,11 @@ class ShellChannel(ZMQSocketChannel): # Don't waste network traffic if inputs are invalid if not isinstance(code, string_types): raise ValueError('code %r must be a string' % code) - validate_string_list(user_variables) validate_string_dict(user_expressions) # Create class for content/msg creation. Related to, but possibly # not in Session. content = dict(code=code, silent=silent, store_history=store_history, - user_variables=user_variables, user_expressions=user_expressions, allow_stdin=allow_stdin, ) @@ -289,38 +270,42 @@ class ShellChannel(ZMQSocketChannel): self._queue_send(msg) return msg['header']['msg_id'] - def complete(self, text, line, cursor_pos, block=None): + def complete(self, code, cursor_pos=None): """Tab complete text in the kernel's namespace. Parameters ---------- - text : str - The text to complete. - line : str - The full line of text that is the surrounding context for the - text to complete. - cursor_pos : int - The position of the cursor in the line where the completion was - requested. - block : str, optional - The full block of code in which the completion is being requested. + code : str + The context in which completion is requested. + Can be anything between a variable name and an entire cell. + cursor_pos : int, optional + The position of the cursor in the block of code where the completion was requested. + Default: ``len(code)`` Returns ------- The msg_id of the message sent. """ - content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos) + if cursor_pos is None: + cursor_pos = len(code) + content = dict(code=code, cursor_pos=cursor_pos) msg = self.session.msg('complete_request', content) self._queue_send(msg) return msg['header']['msg_id'] - def object_info(self, oname, detail_level=0): + def inspect(self, code, cursor_pos=None, detail_level=0): """Get metadata information about an object in the kernel's namespace. + It is up to the kernel to determine the appropriate object to inspect. + Parameters ---------- - oname : str - A string specifying the object name. + code : str + The context in which info is requested. + Can be anything between a variable name and an entire cell. + cursor_pos : int, optional + The position of the cursor in the block of code where the info was requested. + Default: ``len(code)`` detail_level : int, optional The level of detail for the introspection (0-2) @@ -328,8 +313,12 @@ class ShellChannel(ZMQSocketChannel): ------- The msg_id of the message sent. """ - content = dict(oname=oname, detail_level=detail_level) - msg = self.session.msg('object_info_request', content) + if cursor_pos is None: + cursor_pos = len(code) + content = dict(code=code, cursor_pos=cursor_pos, + detail_level=detail_level, + ) + msg = self.session.msg('inspect_request', content) self._queue_send(msg) return msg['header']['msg_id'] diff --git a/IPython/kernel/channelsabc.py b/IPython/kernel/channelsabc.py index 43da3ef..dadf612 100644 --- a/IPython/kernel/channelsabc.py +++ b/IPython/kernel/channelsabc.py @@ -1,11 +1,7 @@ """Abstract base classes for kernel client channels""" -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import abc @@ -42,7 +38,7 @@ class ShellChannelABC(ChannelABC): @abc.abstractmethod def execute(self, code, silent=False, store_history=True, - user_variables=None, user_expressions=None, allow_stdin=None): + user_expressions=None, allow_stdin=None): pass @abc.abstractmethod @@ -50,7 +46,7 @@ class ShellChannelABC(ChannelABC): pass @abc.abstractmethod - def object_info(self, oname, detail_level=0): + def inspect(self, oname, detail_level=0): pass @abc.abstractmethod diff --git a/IPython/kernel/inprocess/channels.py b/IPython/kernel/inprocess/channels.py index 8ff95b5..25eb1a0 100644 --- a/IPython/kernel/inprocess/channels.py +++ b/IPython/kernel/inprocess/channels.py @@ -1,23 +1,13 @@ -""" A kernel client for in-process kernels. """ +"""A kernel client for in-process kernels.""" -#----------------------------------------------------------------------------- -# Copyright (C) 2012 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -# IPython imports from IPython.kernel.channelsabc import ( ShellChannelABC, IOPubChannelABC, HBChannelABC, StdInChannelABC, ) -# Local imports from .socket import DummySocket #----------------------------------------------------------------------------- @@ -83,7 +73,7 @@ class InProcessShellChannel(InProcessChannel): proxy_methods = [ 'execute', 'complete', - 'object_info', + 'inspect', 'history', 'shutdown', 'kernel_info', @@ -94,26 +84,31 @@ class InProcessShellChannel(InProcessChannel): #-------------------------------------------------------------------------- def execute(self, code, silent=False, store_history=True, - user_variables=[], user_expressions={}, allow_stdin=None): + user_expressions={}, allow_stdin=None): if allow_stdin is None: allow_stdin = self.allow_stdin content = dict(code=code, silent=silent, store_history=store_history, - user_variables=user_variables, user_expressions=user_expressions, allow_stdin=allow_stdin) msg = self.client.session.msg('execute_request', content) self._dispatch_to_kernel(msg) return msg['header']['msg_id'] - def complete(self, text, line, cursor_pos, block=None): - content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos) + def complete(self, code, cursor_pos=None): + if cursor_pos is None: + cursor_pos = len(code) + content = dict(code=code, cursor_pos=cursor_pos) msg = self.client.session.msg('complete_request', content) self._dispatch_to_kernel(msg) return msg['header']['msg_id'] - def object_info(self, oname, detail_level=0): - content = dict(oname=oname, detail_level=detail_level) - msg = self.client.session.msg('object_info_request', content) + def inspect(self, code, cursor_pos=None, detail_level=0): + if cursor_pos is None: + cursor_pos = len(code) + content = dict(code=code, cursor_pos=cursor_pos, + detail_level=detail_level, + ) + msg = self.client.session.msg('inspect_request', content) self._dispatch_to_kernel(msg) return msg['header']['msg_id'] diff --git a/IPython/kernel/inprocess/ipkernel.py b/IPython/kernel/inprocess/ipkernel.py index 0b2bbae..2b18e01 100644 --- a/IPython/kernel/inprocess/ipkernel.py +++ b/IPython/kernel/inprocess/ipkernel.py @@ -1,22 +1,12 @@ """An in-process kernel""" -#----------------------------------------------------------------------------- -# Copyright (C) 2012 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -# Standard library imports from contextlib import contextmanager import logging import sys -# Local imports from IPython.core.interactiveshell import InteractiveShellABC from IPython.utils.jsonutil import json_clean from IPython.utils.traitlets import Any, Enum, Instance, List, Type @@ -83,14 +73,14 @@ class InProcessKernel(Kernel): """ The in-process kernel doesn't abort requests. """ pass - def _raw_input(self, prompt, ident, parent): + def _input_request(self, prompt, ident, parent, password=False): # Flush output before making the request. self.raw_input_str = None sys.stderr.flush() sys.stdout.flush() # Send the input request. - content = json_clean(dict(prompt=prompt)) + content = json_clean(dict(prompt=prompt, password=password)) msg = self.session.msg(u'input_request', content, parent) for frontend in self.frontends: if frontend.session.session == parent['header']['session']: diff --git a/IPython/kernel/inprocess/tests/test_kernelmanager.py b/IPython/kernel/inprocess/tests/test_kernelmanager.py index ec07d07..f44a651 100644 --- a/IPython/kernel/inprocess/tests/test_kernelmanager.py +++ b/IPython/kernel/inprocess/tests/test_kernelmanager.py @@ -1,19 +1,10 @@ -#------------------------------------------------------------------------------- -# Copyright (C) 2012 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#------------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- from __future__ import print_function -# Standard library imports import unittest -# Local imports from IPython.kernel.inprocess.blocking import BlockingInProcessKernelClient from IPython.kernel.inprocess.manager import InProcessKernelManager @@ -71,13 +62,13 @@ class InProcessKernelManagerTestCase(unittest.TestCase): kc = BlockingInProcessKernelClient(kernel=km.kernel) kc.start_channels() km.kernel.shell.push({'my_bar': 0, 'my_baz': 1}) - kc.complete('my_ba', 'my_ba', 5) + kc.complete('my_ba', 5) msg = kc.get_shell_msg() self.assertEqual(msg['header']['msg_type'], 'complete_reply') self.assertEqual(sorted(msg['content']['matches']), ['my_bar', 'my_baz']) - def test_object_info(self): + def test_inspect(self): """ Does requesting object information from an in-process kernel work? """ km = InProcessKernelManager() @@ -85,11 +76,13 @@ class InProcessKernelManagerTestCase(unittest.TestCase): kc = BlockingInProcessKernelClient(kernel=km.kernel) kc.start_channels() km.kernel.shell.user_ns['foo'] = 1 - kc.object_info('foo') + kc.inspect('foo') msg = kc.get_shell_msg() - self.assertEquals(msg['header']['msg_type'], 'object_info_reply') - self.assertEquals(msg['content']['name'], 'foo') - self.assertEquals(msg['content']['type_name'], 'int') + self.assertEqual(msg['header']['msg_type'], 'inspect_reply') + content = msg['content'] + assert content['found'] + text = content['data']['text/plain'] + self.assertIn('int', text) def test_history(self): """ Does requesting history from an in-process kernel work? diff --git a/IPython/kernel/managerabc.py b/IPython/kernel/managerabc.py index aca8745..12248d7 100644 --- a/IPython/kernel/managerabc.py +++ b/IPython/kernel/managerabc.py @@ -1,11 +1,7 @@ """Abstract base class for kernel managers.""" -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import abc @@ -24,54 +20,6 @@ class KernelManagerABC(with_metaclass(abc.ABCMeta, object)): def kernel(self): pass - @abc.abstractproperty - def shell_channel_class(self): - pass - - @abc.abstractproperty - def iopub_channel_class(self): - pass - - @abc.abstractproperty - def hb_channel_class(self): - pass - - @abc.abstractproperty - def stdin_channel_class(self): - pass - - #-------------------------------------------------------------------------- - # Channel management methods - #-------------------------------------------------------------------------- - - @abc.abstractmethod - def start_channels(self, shell=True, iopub=True, stdin=True, hb=True): - pass - - @abc.abstractmethod - def stop_channels(self): - pass - - @abc.abstractproperty - def channels_running(self): - pass - - @abc.abstractproperty - def shell_channel(self): - pass - - @abc.abstractproperty - def iopub_channel(self): - pass - - @abc.abstractproperty - def stdin_channel(self): - pass - - @abc.abstractproperty - def hb_channel(self): - pass - #-------------------------------------------------------------------------- # Kernel management #-------------------------------------------------------------------------- diff --git a/IPython/kernel/tests/test_message_spec.py b/IPython/kernel/tests/test_message_spec.py index 443181b..e1080e9 100644 --- a/IPython/kernel/tests/test_message_spec.py +++ b/IPython/kernel/tests/test_message_spec.py @@ -1,13 +1,10 @@ -"""Test suite for our zeromq-based message specification. -""" -#----------------------------------------------------------------------------- -# Copyright (C) 2010 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING.txt, distributed as part of this software. -#----------------------------------------------------------------------------- +"""Test suite for our zeromq-based message specification.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import re +from distutils.version import LooseVersion as V from subprocess import PIPE try: from queue import Empty # Py 3 @@ -60,7 +57,21 @@ class Reference(HasTraits): try: setattr(self, key, d[key]) except TraitError as e: - nt.assert_true(False, str(e)) + assert False, str(e) + + +class Version(Unicode): + def __init__(self, *args, **kwargs): + self.min = kwargs.pop('min', None) + self.max = kwargs.pop('max', None) + kwargs['default_value'] = self.min + super(Version, self).__init__(*args, **kwargs) + + def validate(self, obj, value): + if self.min and V(value) < V(self.min): + raise TraitError("bad version: %s < %s" % (value, self.min)) + if self.max and (V(value) > V(self.max)): + raise TraitError("bad version: %s > %s" % (value, self.max)) class RMessage(Reference): @@ -69,16 +80,31 @@ class RMessage(Reference): header = Dict() parent_header = Dict() content = Dict() + + def check(self, d): + super(RMessage, self).check(d) + RHeader().check(self.header) + if self.parent_header: + RHeader().check(self.parent_header) class RHeader(Reference): msg_id = Unicode() msg_type = Unicode() session = Unicode() username = Unicode() + version = Version(min='5.0') -class RContent(Reference): - status = Enum((u'ok', u'error')) +mime_pat = re.compile(r'^[\w\-\+\.]+/[\w\-\+\.]+$') + +class MimeBundle(Reference): + metadata = Dict() + data = Dict() + def _data_changed(self, name, old, new): + for k,v in iteritems(new): + assert mime_pat.match(k) + nt.assert_is_instance(v, string_types) +# shell replies class ExecuteReply(Reference): execution_count = Integer() @@ -94,7 +120,6 @@ class ExecuteReply(Reference): class ExecuteReplyOkay(Reference): payload = List(Dict) - user_variables = Dict() user_expressions = Dict() @@ -104,31 +129,8 @@ class ExecuteReplyError(Reference): traceback = List(Unicode) -class OInfoReply(Reference): - name = Unicode() +class InspectReply(MimeBundle): found = Bool() - ismagic = Bool() - isalias = Bool() - namespace = Enum((u'builtin', u'magics', u'alias', u'Interactive')) - type_name = Unicode() - string_form = Unicode() - base_class = Unicode() - length = Integer() - file = Unicode() - definition = Unicode() - argspec = Dict() - init_definition = Unicode() - docstring = Unicode() - init_docstring = Unicode() - class_docstring = Unicode() - call_def = Unicode() - call_docstring = Unicode() - source = Unicode() - - def check(self, d): - Reference.check(self, d) - if d['argspec'] is not None: - ArgSpec().check(d['argspec']) class ArgSpec(Reference): @@ -144,33 +146,28 @@ class Status(Reference): class CompleteReply(Reference): matches = List(Unicode) - - -def Version(num, trait=Integer): - return List(trait, default_value=[0] * num, minlen=num, maxlen=num) + cursor_start = Integer() + cursor_end = Integer() + status = Unicode() class KernelInfoReply(Reference): - - protocol_version = Version(2) - ipython_version = Version(4, Any) - language_version = Version(3) - language = Unicode() - - def _ipython_version_changed(self, name, old, new): - for v in new: - assert isinstance(v, int) or isinstance(v, string_types), \ - 'expected int or string as version component, got {0!r}'.format(v) + protocol_version = Version(min='5.0') + implementation = Unicode('ipython') + implementation_version = Version(min='2.1') + language_version = Version(min='2.7') + language = Unicode('python') + banner = Unicode() # IOPub messages -class PyIn(Reference): +class ExecuteInput(Reference): code = Unicode() execution_count = Integer() -PyErr = ExecuteReplyError +Error = ExecuteReplyError class Stream(Reference): @@ -178,38 +175,26 @@ class Stream(Reference): data = Unicode() -mime_pat = re.compile(r'\w+/\w+') +class DisplayData(MimeBundle): + pass -class DisplayData(Reference): - source = Unicode() - metadata = Dict() - data = Dict() - def _data_changed(self, name, old, new): - for k,v in iteritems(new): - assert mime_pat.match(k) - nt.assert_is_instance(v, string_types) - -class PyOut(Reference): +class ExecuteResult(MimeBundle): execution_count = Integer() - data = Dict() - def _data_changed(self, name, old, new): - for k,v in iteritems(new): - assert mime_pat.match(k) - nt.assert_is_instance(v, string_types) references = { 'execute_reply' : ExecuteReply(), - 'object_info_reply' : OInfoReply(), + 'inspect_reply' : InspectReply(), 'status' : Status(), 'complete_reply' : CompleteReply(), 'kernel_info_reply': KernelInfoReply(), - 'pyin' : PyIn(), - 'pyout' : PyOut(), - 'pyerr' : PyErr(), + 'execute_input' : ExecuteInput(), + 'execute_result' : ExecuteResult(), + 'error' : Error(), 'stream' : Stream(), 'display_data' : DisplayData(), + 'header' : RHeader(), } """ Specifications of `content` part of the reply messages. @@ -280,8 +265,8 @@ def test_execute_error(): nt.assert_equal(reply['status'], 'error') nt.assert_equal(reply['ename'], 'ZeroDivisionError') - pyerr = KC.iopub_channel.get_msg(timeout=TIMEOUT) - validate_message(pyerr, 'pyerr', msg_id) + error = KC.iopub_channel.get_msg(timeout=TIMEOUT) + validate_message(error, 'error', msg_id) def test_execute_inc(): @@ -298,28 +283,6 @@ def test_execute_inc(): nt.assert_equal(count_2, count+1) -def test_user_variables(): - flush_channels() - - msg_id, reply = execute(code='x=1', user_variables=['x']) - user_variables = reply['user_variables'] - nt.assert_equal(user_variables, {u'x': { - u'status': u'ok', - u'data': {u'text/plain': u'1'}, - u'metadata': {}, - }}) - - -def test_user_variables_fail(): - flush_channels() - - msg_id, reply = execute(code='x=1', user_variables=['nosuchname']) - user_variables = reply['user_variables'] - foo = user_variables['nosuchname'] - nt.assert_equal(foo['status'], 'error') - nt.assert_equal(foo['ename'], 'KeyError') - - def test_user_expressions(): flush_channels() @@ -345,9 +308,9 @@ def test_user_expressions_fail(): def test_oinfo(): flush_channels() - msg_id = KC.object_info('a') + msg_id = KC.inspect('a') reply = KC.get_shell_msg(timeout=TIMEOUT) - validate_message(reply, 'object_info_reply', msg_id) + validate_message(reply, 'inspect_reply', msg_id) def test_oinfo_found(): @@ -355,13 +318,14 @@ def test_oinfo_found(): msg_id, reply = execute(code='a=5') - msg_id = KC.object_info('a') + msg_id = KC.inspect('a') reply = KC.get_shell_msg(timeout=TIMEOUT) - validate_message(reply, 'object_info_reply', msg_id) + validate_message(reply, 'inspect_reply', msg_id) content = reply['content'] assert content['found'] - argspec = content['argspec'] - nt.assert_is(argspec, None) + text = content['data']['text/plain'] + nt.assert_in('Type:', text) + nt.assert_in('Docstring:', text) def test_oinfo_detail(): @@ -369,22 +333,22 @@ def test_oinfo_detail(): msg_id, reply = execute(code='ip=get_ipython()') - msg_id = KC.object_info('ip.object_inspect', detail_level=2) + msg_id = KC.inspect('ip.object_inspect', cursor_pos=10, detail_level=1) reply = KC.get_shell_msg(timeout=TIMEOUT) - validate_message(reply, 'object_info_reply', msg_id) + validate_message(reply, 'inspect_reply', msg_id) content = reply['content'] assert content['found'] - argspec = content['argspec'] - nt.assert_is_instance(argspec, dict, "expected non-empty argspec dict, got %r" % argspec) - nt.assert_equal(argspec['defaults'], [0]) + text = content['data']['text/plain'] + nt.assert_in('Definition:', text) + nt.assert_in('Source:', text) def test_oinfo_not_found(): flush_channels() - msg_id = KC.object_info('dne') + msg_id = KC.inspect('dne') reply = KC.get_shell_msg(timeout=TIMEOUT) - validate_message(reply, 'object_info_reply', msg_id) + validate_message(reply, 'inspect_reply', msg_id) content = reply['content'] nt.assert_false(content['found']) @@ -394,7 +358,7 @@ def test_complete(): msg_id, reply = execute(code="alpha = albert = 5") - msg_id = KC.complete('al', 'al', 2) + msg_id = KC.complete('al', 2) reply = KC.get_shell_msg(timeout=TIMEOUT) validate_message(reply, 'complete_reply', msg_id) matches = reply['content']['matches'] @@ -430,7 +394,6 @@ def test_stream(): stdout = KC.iopub_channel.get_msg(timeout=TIMEOUT) validate_message(stdout, 'stream', msg_id) content = stdout['content'] - nt.assert_equal(content['name'], u'stdout') nt.assert_equal(content['data'], u'hi\n') diff --git a/IPython/kernel/tests/utils.py b/IPython/kernel/tests/utils.py index 1f257ad..3ba288d 100644 --- a/IPython/kernel/tests/utils.py +++ b/IPython/kernel/tests/utils.py @@ -1,15 +1,7 @@ """utilities for testing IPython kernels""" -#------------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#------------------------------------------------------------------------------- - -#------------------------------------------------------------------------------- -# Imports -#------------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import atexit @@ -84,9 +76,9 @@ def execute(code='', kc=None, **kwargs): nt.assert_equal(busy['content']['execution_state'], 'busy') if not kwargs.get('silent'): - pyin = kc.get_iopub_msg(timeout=TIMEOUT) - validate_message(pyin, 'pyin', msg_id) - nt.assert_equal(pyin['content']['code'], code) + execute_input = kc.get_iopub_msg(timeout=TIMEOUT) + validate_message(execute_input, 'execute_input', msg_id) + nt.assert_equal(execute_input['content']['code'], code) return msg_id, reply['content'] diff --git a/IPython/kernel/zmq/displayhook.py b/IPython/kernel/zmq/displayhook.py index 756cb40..27d91d5 100644 --- a/IPython/kernel/zmq/displayhook.py +++ b/IPython/kernel/zmq/displayhook.py @@ -1,5 +1,8 @@ -"""Replacements for sys.displayhook that publish over ZMQ. -""" +"""Replacements for sys.displayhook that publish over ZMQ.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + import sys from IPython.core.displayhook import DisplayHook @@ -12,7 +15,7 @@ from .session import extract_header, Session class ZMQDisplayHook(object): """A simple displayhook that publishes the object's repr over a ZeroMQ socket.""" - topic=b'pyout' + topic=b'execute_result' def __init__(self, session, pub_socket): self.session = session @@ -26,7 +29,7 @@ class ZMQDisplayHook(object): builtin_mod._ = obj sys.stdout.flush() sys.stderr.flush() - msg = self.session.send(self.pub_socket, u'pyout', {u'data':repr(obj)}, + msg = self.session.send(self.pub_socket, u'execute_result', {u'data':repr(obj)}, parent=self.parent_header, ident=self.topic) def set_parent(self, parent): @@ -48,7 +51,7 @@ class ZMQShellDisplayHook(DisplayHook): self.parent_header = extract_header(parent) def start_displayhook(self): - self.msg = self.session.msg(u'pyout', {}, parent=self.parent_header) + self.msg = self.session.msg(u'execute_result', {}, parent=self.parent_header) def write_output_prompt(self): """Write the output prompt.""" diff --git a/IPython/kernel/zmq/ipkernel.py b/IPython/kernel/zmq/ipkernel.py index 4fb257c..09ce934 100755 --- a/IPython/kernel/zmq/ipkernel.py +++ b/IPython/kernel/zmq/ipkernel.py @@ -1,12 +1,11 @@ -#!/usr/bin/env python """An interactive kernel that talks to frontends over 0MQ.""" -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function -# Standard library imports +import getpass import sys import time import traceback @@ -18,18 +17,17 @@ from signal import ( signal, default_int_handler, SIGINT ) -# System library imports import zmq from zmq.eventloop import ioloop from zmq.eventloop.zmqstream import ZMQStream -# Local imports from IPython.config.configurable import Configurable from IPython.core.error import StdinNotImplementedError from IPython.core import release from IPython.utils import py3compat from IPython.utils.py3compat import builtin_mod, unicode_type, string_types from IPython.utils.jsonutil import json_clean +from IPython.utils.tokenutil import token_at_cursor from IPython.utils.traitlets import ( Any, Instance, Float, Dict, List, Set, Integer, Unicode, Type, Bool, @@ -44,9 +42,9 @@ from .zmqshell import ZMQInteractiveShell # Main kernel class #----------------------------------------------------------------------------- -protocol_version = list(release.kernel_protocol_version_info) -ipython_version = list(release.version_info) -language_version = list(sys.version_info[:3]) +protocol_version = release.kernel_protocol_version +ipython_version = release.version +language_version = sys.version.split()[0] class Kernel(Configurable): @@ -100,6 +98,10 @@ class Kernel(Configurable): """ ) + # track associations with current request + _allow_stdin = Bool(False) + _parent_header = Dict() + _parent_ident = Any(b'') # Time to sleep after flushing the stdout/err buffers in each execute # cycle. While this introduces a hard limit on the minimal latency of the # execute cycle, it helps prevent output synchronization problems for @@ -146,7 +148,7 @@ class Kernel(Configurable): ) self.shell.displayhook.session = self.session self.shell.displayhook.pub_socket = self.iopub_socket - self.shell.displayhook.topic = self._topic('pyout') + self.shell.displayhook.topic = self._topic('execute_result') self.shell.display_pub.session = self.session self.shell.display_pub.pub_socket = self.iopub_socket self.shell.data_pub.session = self.session @@ -157,7 +159,7 @@ class Kernel(Configurable): # Build dict of handlers for message types msg_types = [ 'execute_request', 'complete_request', - 'object_info_request', 'history_request', + 'inspect_request', 'history_request', 'kernel_info_request', 'connect_request', 'shutdown_request', 'apply_request', @@ -241,6 +243,7 @@ class Kernel(Configurable): else: # ensure default_int_handler during handler call sig = signal(SIGINT, default_int_handler) + self.log.debug("%s: %s", msg_type, msg) try: handler(stream, idents, msg) except Exception: @@ -320,12 +323,12 @@ class Kernel(Configurable): new_md.update(other) return new_md - def _publish_pyin(self, code, parent, execution_count): - """Publish the code request on the pyin stream.""" + def _publish_execute_input(self, code, parent, execution_count): + """Publish the code request on the iopub stream.""" - self.session.send(self.iopub_socket, u'pyin', + self.session.send(self.iopub_socket, u'execute_input', {u'code':code, u'execution_count': execution_count}, - parent=parent, ident=self._topic('pyin') + parent=parent, ident=self._topic('execute_input') ) def _publish_status(self, status, parent=None): @@ -336,8 +339,48 @@ class Kernel(Configurable): parent=parent, ident=self._topic('status'), ) + + def _forward_input(self, allow_stdin=False): + """Forward raw_input and getpass to the current frontend. - + via input_request + """ + self._allow_stdin = allow_stdin + + if py3compat.PY3: + self._sys_raw_input = builtin_mod.input + builtin_mod.input = self.raw_input + else: + self._sys_raw_input = builtin_mod.raw_input + self._sys_eval_input = builtin_mod.input + builtin_mod.raw_input = self.raw_input + builtin_mod.input = lambda prompt='': eval(self.raw_input(prompt)) + self._save_getpass = getpass.getpass + getpass.getpass = self.getpass + + def _restore_input(self): + """Restore raw_input, getpass""" + if py3compat.PY3: + builtin_mod.input = self._sys_raw_input + else: + builtin_mod.raw_input = self._sys_raw_input + builtin_mod.input = self._sys_eval_input + + getpass.getpass = self._save_getpass + + def set_parent(self, ident, parent): + """Set the current parent_header + + Side effects (IOPub messages) and replies are associated with + the request that caused them via the parent_header. + + The parent identity is used to route input_request messages + on the stdin channel. + """ + self._parent_ident = ident + self._parent_header = parent + self.shell.set_parent(parent) + def execute_request(self, stream, ident, parent): """handle an execute_request""" @@ -354,33 +397,17 @@ class Kernel(Configurable): return md = self._make_metadata(parent['metadata']) - + shell = self.shell # we'll need this a lot here - - # Replace raw_input. Note that is not sufficient to replace - # raw_input in the user namespace. - if content.get('allow_stdin', False): - raw_input = lambda prompt='': self._raw_input(prompt, ident, parent) - input = lambda prompt='': eval(raw_input(prompt)) - else: - raw_input = input = lambda prompt='' : self._no_raw_input() - - if py3compat.PY3: - self._sys_raw_input = builtin_mod.input - builtin_mod.input = raw_input - else: - self._sys_raw_input = builtin_mod.raw_input - self._sys_eval_input = builtin_mod.input - builtin_mod.raw_input = raw_input - builtin_mod.input = input - + + self._forward_input(content.get('allow_stdin', False)) # Set the parent message of the display hook and out streams. - shell.set_parent(parent) - + self.set_parent(ident, parent) + # Re-broadcast our input for the benefit of listening clients, and # start computing output if not silent: - self._publish_pyin(code, parent, shell.execution_count) + self._publish_execute_input(code, parent, shell.execution_count) reply_content = {} # FIXME: the shell calls the exception handler itself. @@ -401,12 +428,7 @@ class Kernel(Configurable): else: status = u'ok' finally: - # Restore raw_input. - if py3compat.PY3: - builtin_mod.input = self._sys_raw_input - else: - builtin_mod.raw_input = self._sys_raw_input - builtin_mod.input = self._sys_eval_input + self._restore_input() reply_content[u'status'] = status @@ -427,16 +449,12 @@ class Kernel(Configurable): # At this point, we can tell whether the main code execution succeeded - # or not. If it did, we proceed to evaluate user_variables/expressions + # or not. If it did, we proceed to evaluate user_expressions if reply_content['status'] == 'ok': - reply_content[u'user_variables'] = \ - shell.user_variables(content.get(u'user_variables', [])) reply_content[u'user_expressions'] = \ shell.user_expressions(content.get(u'user_expressions', {})) else: - # If there was an error, don't even try to compute variables or - # expressions - reply_content[u'user_variables'] = {} + # If there was an error, don't even try to compute expressions reply_content[u'user_expressions'] = {} # Payloads should be retrieved regardless of outcome, so we can both @@ -476,24 +494,41 @@ class Kernel(Configurable): self._publish_status(u'idle', parent) def complete_request(self, stream, ident, parent): - txt, matches = self._complete(parent) + content = parent['content'] + code = content['code'] + cursor_pos = content['cursor_pos'] + + txt, matches = self.shell.complete('', code, cursor_pos) matches = {'matches' : matches, - 'matched_text' : txt, + 'cursor_end' : cursor_pos, + 'cursor_start' : cursor_pos - len(txt), + 'metadata' : {}, 'status' : 'ok'} matches = json_clean(matches) completion_msg = self.session.send(stream, 'complete_reply', matches, parent, ident) self.log.debug("%s", completion_msg) - def object_info_request(self, stream, ident, parent): + def inspect_request(self, stream, ident, parent): content = parent['content'] - object_info = self.shell.object_inspect(content['oname'], - detail_level = content.get('detail_level', 0) - ) + + name = token_at_cursor(content['code'], content['cursor_pos']) + info = self.shell.object_inspect(name) + + reply_content = {'status' : 'ok'} + reply_content['data'] = data = {} + reply_content['metadata'] = {} + reply_content['found'] = info['found'] + if info['found']: + info_text = self.shell.object_inspect_text( + name, + detail_level=content.get('detail_level', 0), + ) + reply_content['data']['text/plain'] = info_text # Before we send this object over, we scrub it for JSON usage - oinfo = json_clean(object_info) - msg = self.session.send(stream, 'object_info_reply', - oinfo, parent, ident) + reply_content = json_clean(reply_content) + msg = self.session.send(stream, 'inspect_reply', + reply_content, parent, ident) self.log.debug("%s", msg) def history_request(self, stream, ident, parent): @@ -542,9 +577,11 @@ class Kernel(Configurable): def kernel_info_request(self, stream, ident, parent): vinfo = { 'protocol_version': protocol_version, - 'ipython_version': ipython_version, + 'implementation': 'ipython', + 'implementation_version': ipython_version, 'language_version': language_version, 'language': 'python', + 'banner': self.shell.banner, } msg = self.session.send(stream, 'kernel_info_reply', vinfo, parent, ident) @@ -584,9 +621,6 @@ class Kernel(Configurable): shell = self.shell shell.set_parent(parent) - # pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent) - # self.iopub_socket.send(pyin_msg) - # self.session.send(self.iopub_socket, u'pyin', {u'code':code},parent=parent) md = self._make_metadata(parent['metadata']) try: working = shell.user_ns @@ -631,8 +665,8 @@ class Kernel(Configurable): # reset after use shell._reply_content = None - self.session.send(self.iopub_socket, u'pyerr', reply_content, parent=parent, - ident=self._topic('pyerr')) + self.session.send(self.iopub_socket, u'error', reply_content, parent=parent, + ident=self._topic('error')) self.log.info("Exception in apply request:\n%s", '\n'.join(reply_content['traceback'])) result_buf = [] @@ -734,8 +768,42 @@ class Kernel(Configurable): stdin.""" raise StdinNotImplementedError("raw_input was called, but this " "frontend does not support stdin.") + + def getpass(self, prompt=''): + """Forward getpass to frontends - def _raw_input(self, prompt, ident, parent): + Raises + ------ + StdinNotImplentedError if active frontend doesn't support stdin. + """ + if not self._allow_stdin: + raise StdinNotImplementedError( + "getpass was called, but this frontend does not support input requests." + ) + return self._input_request(prompt, + self._parent_ident, + self._parent_header, + password=True, + ) + + def raw_input(self, prompt=''): + """Forward raw_input to frontends + + Raises + ------ + StdinNotImplentedError if active frontend doesn't support stdin. + """ + if not self._allow_stdin: + raise StdinNotImplementedError( + "raw_input was called, but this frontend does not support input requests." + ) + return self._input_request(prompt, + self._parent_ident, + self._parent_header, + password=False, + ) + + def _input_request(self, prompt, ident, parent, password=False): # Flush output before making the request. sys.stderr.flush() sys.stdout.flush() @@ -750,7 +818,7 @@ class Kernel(Configurable): raise # Send the input request. - content = json_clean(dict(prompt=prompt)) + content = json_clean(dict(prompt=prompt, password=password)) self.session.send(self.stdin_socket, u'input_request', content, parent, ident=ident) @@ -768,27 +836,13 @@ class Kernel(Configurable): try: value = py3compat.unicode_to_str(reply['content']['value']) except: - self.log.error("Got bad raw_input reply: ") - self.log.error("%s", parent) + self.log.error("Bad input_reply: %s", parent) value = '' if value == '\x04': # EOF raise EOFError return value - def _complete(self, msg): - c = msg['content'] - try: - cpos = int(c['cursor_pos']) - except: - # If we don't get something that we can convert to an integer, at - # least attempt the completion guessing the cursor is at the end of - # the text, if there's any, and otherwise of the line - cpos = len(c['text']) - if cpos==0: - cpos = len(c['line']) - return self.shell.complete(c['text'], c['line'], cpos) - def _at_shutdown(self): """Actions taken at shutdown by the kernel, called by python's atexit. """ diff --git a/IPython/kernel/zmq/kernelapp.py b/IPython/kernel/zmq/kernelapp.py index ece61e8..24e265b 100644 --- a/IPython/kernel/zmq/kernelapp.py +++ b/IPython/kernel/zmq/kernelapp.py @@ -1,26 +1,19 @@ -"""An Application for launching a kernel -""" +"""An Application for launching a kernel""" + # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- - from __future__ import print_function -# Standard library imports import atexit import os import sys import signal -# System library imports import zmq from zmq.eventloop import ioloop from zmq.eventloop.zmqstream import ZMQStream -# IPython imports from IPython.core.ultratb import FormattedTB from IPython.core.application import ( BaseIPythonApplication, base_flags, base_aliases, catch_config_error @@ -353,7 +346,7 @@ class IPKernelApp(BaseIPythonApplication, InteractiveShellApp, shell = self.shell _showtraceback = shell._showtraceback try: - # replace pyerr-sending traceback with stderr + # replace error-sending traceback with stderr def print_tb(etype, evalue, stb): print ("GUI event loop or pylab initialization failed", file=io.stderr) diff --git a/IPython/kernel/zmq/session.py b/IPython/kernel/zmq/session.py index e0ccec1..6f3e9f9 100644 --- a/IPython/kernel/zmq/session.py +++ b/IPython/kernel/zmq/session.py @@ -32,6 +32,7 @@ from zmq.utils import jsonapi from zmq.eventloop.ioloop import IOLoop from zmq.eventloop.zmqstream import ZMQStream +from IPython.core.release import kernel_protocol_version from IPython.config.configurable import Configurable, LoggingConfigurable from IPython.utils import io from IPython.utils.importstring import import_item @@ -182,6 +183,7 @@ class Message(object): def msg_header(msg_id, msg_type, username, session): date = datetime.now() + version = kernel_protocol_version return locals() def extract_header(msg_or_header): diff --git a/IPython/kernel/zmq/tests/test_embed_kernel.py b/IPython/kernel/zmq/tests/test_embed_kernel.py index 4449431..45f543f 100644 --- a/IPython/kernel/zmq/tests/test_embed_kernel.py +++ b/IPython/kernel/zmq/tests/test_embed_kernel.py @@ -113,7 +113,7 @@ def test_embed_kernel_basic(): with setup_kernel(cmd) as client: # oinfo a (int) - msg_id = client.object_info('a') + msg_id = client.inspect('a') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_true(content['found']) @@ -124,11 +124,12 @@ def test_embed_kernel_basic(): nt.assert_equal(content['status'], u'ok') # oinfo c (should be 10) - msg_id = client.object_info('c') + msg_id = client.inspect('c') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_true(content['found']) - nt.assert_equal(content['string_form'], u'10') + text = content['data']['text/plain'] + nt.assert_in('10', text) def test_embed_kernel_namespace(): """IPython.embed_kernel() inherits calling namespace""" @@ -144,21 +145,23 @@ def test_embed_kernel_namespace(): with setup_kernel(cmd) as client: # oinfo a (int) - msg_id = client.object_info('a') + msg_id = client.inspect('a') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_true(content['found']) - nt.assert_equal(content['string_form'], u'5') + text = content['data']['text/plain'] + nt.assert_in(u'5', text) # oinfo b (str) - msg_id = client.object_info('b') + msg_id = client.inspect('b') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_true(content['found']) - nt.assert_equal(content['string_form'], u'hi there') + text = content['data']['text/plain'] + nt.assert_in(u'hi there', text) # oinfo c (undefined) - msg_id = client.object_info('c') + msg_id = client.inspect('c') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_false(content['found']) @@ -180,11 +183,12 @@ def test_embed_kernel_reentrant(): with setup_kernel(cmd) as client: for i in range(5): - msg_id = client.object_info('count') + msg_id = client.inspect('count') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_true(content['found']) - nt.assert_equal(content['string_form'], unicode_type(i)) + text = content['data']['text/plain'] + nt.assert_in(unicode_type(i), text) # exit from embed_kernel client.execute("get_ipython().exit_now = True") diff --git a/IPython/kernel/zmq/tests/test_start_kernel.py b/IPython/kernel/zmq/tests/test_start_kernel.py index a31c1f3..3ac53dd 100644 --- a/IPython/kernel/zmq/tests/test_start_kernel.py +++ b/IPython/kernel/zmq/tests/test_start_kernel.py @@ -10,22 +10,24 @@ def test_ipython_start_kernel_userns(): 'start_kernel(user_ns=ns)') with setup_kernel(cmd) as client: - msg_id = client.object_info('tre') + msg_id = client.inspect('tre') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] assert content['found'] - nt.assert_equal(content['string_form'], u'123') + text = content['data']['text/plain'] + nt.assert_in(u'123', text) # user_module should be an instance of DummyMod msg_id = client.execute("usermod = get_ipython().user_module") msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_equal(content['status'], u'ok') - msg_id = client.object_info('usermod') + msg_id = client.inspect('usermod') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] assert content['found'] - nt.assert_in('DummyMod', content['string_form']) + text = content['data']['text/plain'] + nt.assert_in(u'DummyMod', text) def test_ipython_start_kernel_no_userns(): # Issue #4188 - user_ns should be passed to shell as None, not {} @@ -38,8 +40,9 @@ def test_ipython_start_kernel_no_userns(): msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] nt.assert_equal(content['status'], u'ok') - msg_id = client.object_info('usermod') + msg_id = client.inspect('usermod') msg = client.get_shell_msg(block=True, timeout=TIMEOUT) content = msg['content'] assert content['found'] - nt.assert_not_in('DummyMod', content['string_form']) + text = content['data']['text/plain'] + nt.assert_not_in(u'DummyMod', text) diff --git a/IPython/kernel/zmq/zmqshell.py b/IPython/kernel/zmq/zmqshell.py index 357b6e1..cd1326d 100644 --- a/IPython/kernel/zmq/zmqshell.py +++ b/IPython/kernel/zmq/zmqshell.py @@ -10,20 +10,18 @@ implementation that doesn't rely on so much monkeypatching. But this lets us maintain a fully working IPython as we develop the new machinery. This should thus be thought of as scaffolding. """ -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function -# Stdlib import os import sys import time -# System library imports from zmq.eventloop import ioloop -# Our own from IPython.core.interactiveshell import ( InteractiveShell, InteractiveShellABC ) @@ -34,6 +32,7 @@ from IPython.core.error import UsageError from IPython.core.magics import MacroToEdit, CodeMagics from IPython.core.magic import magics_class, line_magic, Magics from IPython.core.payloadpage import install_payload_page +from IPython.core.usage import default_gui_banner from IPython.display import display, Javascript from IPython.kernel.inprocess.socket import SocketABC from IPython.kernel import ( @@ -74,13 +73,12 @@ class ZMQDisplayPublisher(DisplayPublisher): sys.stdout.flush() sys.stderr.flush() - def publish(self, source, data, metadata=None): + def publish(self, data, metadata=None, source=None): self._flush_streams() if metadata is None: metadata = {} - self._validate_data(source, data, metadata) + self._validate_data(data, metadata) content = {} - content['source'] = source content['data'] = encode_images(data) content['metadata'] = metadata self.session.send( @@ -424,6 +422,9 @@ class ZMQInteractiveShell(InteractiveShell): data_pub_class = Type(ZMQDataPublisher) kernel = Any() parent_header = Any() + + def _banner1_default(self): + return default_gui_banner # Override the traitlet in the parent class, because there's no point using # readline for the kernel. Can be removed when the readline code is moved @@ -512,9 +513,9 @@ class ZMQInteractiveShell(InteractiveShell): # to pick up topic = None if dh.topic: - topic = dh.topic.replace(b'pyout', b'pyerr') + topic = dh.topic.replace(b'execute_result', b'error') - exc_msg = dh.session.send(dh.pub_socket, u'pyerr', json_clean(exc_content), dh.parent_header, ident=topic) + exc_msg = dh.session.send(dh.pub_socket, u'error', json_clean(exc_content), dh.parent_header, ident=topic) # FIXME - Hack: store exception info in shell object. Right now, the # caller is reading this info after the fact, we need to fix this logic diff --git a/IPython/parallel/client/asyncresult.py b/IPython/parallel/client/asyncresult.py index 8320ede..a8111b4 100644 --- a/IPython/parallel/client/asyncresult.py +++ b/IPython/parallel/client/asyncresult.py @@ -1,19 +1,7 @@ -"""AsyncResult objects for the client +"""AsyncResult objects for the client""" -Authors: - -* MinRK -""" -#----------------------------------------------------------------------------- -# Copyright (C) 2010-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. from __future__ import print_function @@ -436,7 +424,7 @@ class AsyncResult(object): for output in self.outputs: self._republish_displaypub(output, self.engine_id) - if self.pyout is not None: + if self.execute_result is not None: display(self.get()) def _wait_for_outputs(self, timeout=-1): @@ -495,15 +483,15 @@ class AsyncResult(object): stdouts = self.stdout stderrs = self.stderr - pyouts = self.pyout + execute_results = self.execute_result output_lists = self.outputs results = self.get() targets = self.engine_id if groupby == "engine": - for eid,stdout,stderr,outputs,r,pyout in zip( - targets, stdouts, stderrs, output_lists, results, pyouts + for eid,stdout,stderr,outputs,r,execute_result in zip( + targets, stdouts, stderrs, output_lists, results, execute_results ): self._display_stream(stdout, '[stdout:%i] ' % eid) self._display_stream(stderr, '[stderr:%i] ' % eid, file=sys.stderr) @@ -514,13 +502,13 @@ class AsyncResult(object): # displaypub is meaningless outside IPython return - if outputs or pyout is not None: + if outputs or execute_result is not None: _raw_text('[output:%i]' % eid) for output in outputs: self._republish_displaypub(output, eid) - if pyout is not None: + if execute_result is not None: display(r) elif groupby in ('type', 'order'): @@ -555,9 +543,9 @@ class AsyncResult(object): for output in outputs: self._republish_displaypub(output, eid) - # finally, add pyout: - for eid,r,pyout in zip(targets, results, pyouts): - if pyout is not None: + # finally, add execute_result: + for eid,r,execute_result in zip(targets, results, execute_results): + if execute_result is not None: display(r) else: diff --git a/IPython/parallel/client/client.py b/IPython/parallel/client/client.py index 25ad7c7..fe6203d 100644 --- a/IPython/parallel/client/client.py +++ b/IPython/parallel/client/client.py @@ -1,20 +1,9 @@ -"""A semi-synchronous Client for the ZMQ cluster +"""A semi-synchronous Client for IPython parallel""" -Authors: +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -* MinRK -""" from __future__ import print_function -#----------------------------------------------------------------------------- -# Copyright (C) 2010-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- import os import json @@ -29,7 +18,6 @@ from pprint import pprint pjoin = os.path.join import zmq -# from zmq.eventloop import ioloop, zmqstream from IPython.config.configurable import MultipleInstanceError from IPython.core.application import BaseIPythonApplication @@ -84,25 +72,25 @@ class ExecuteReply(RichOutput): @property def source(self): - pyout = self.metadata['pyout'] - if pyout: - return pyout.get('source', '') + execute_result = self.metadata['execute_result'] + if execute_result: + return execute_result.get('source', '') @property def data(self): - pyout = self.metadata['pyout'] - if pyout: - return pyout.get('data', {}) + execute_result = self.metadata['execute_result'] + if execute_result: + return execute_result.get('data', {}) @property def _metadata(self): - pyout = self.metadata['pyout'] - if pyout: - return pyout.get('metadata', {}) + execute_result = self.metadata['execute_result'] + if execute_result: + return execute_result.get('metadata', {}) def display(self): from IPython.display import publish_display_data - publish_display_data(self.source, self.data, self.metadata) + publish_display_data(self.data, self.metadata) def _repr_mime_(self, mime): if mime not in self.data: @@ -122,16 +110,16 @@ class ExecuteReply(RichOutput): return self.metadata[key] def __repr__(self): - pyout = self.metadata['pyout'] or {'data':{}} - text_out = pyout['data'].get('text/plain', '') + execute_result = self.metadata['execute_result'] or {'data':{}} + text_out = execute_result['data'].get('text/plain', '') if len(text_out) > 32: text_out = text_out[:29] + '...' return "<ExecuteReply[%i]: %s>" % (self.execution_count, text_out) def _repr_pretty_(self, p, cycle): - pyout = self.metadata['pyout'] or {'data':{}} - text_out = pyout['data'].get('text/plain', '') + execute_result = self.metadata['execute_result'] or {'data':{}} + text_out = execute_result['data'].get('text/plain', '') if not text_out: return @@ -181,9 +169,9 @@ class Metadata(dict): 'after' : None, 'status' : None, - 'pyin' : None, - 'pyout' : None, - 'pyerr' : None, + 'execute_input' : None, + 'execute_result' : None, + 'error' : None, 'stdout' : '', 'stderr' : '', 'outputs' : [], @@ -881,14 +869,14 @@ class Client(HasTraits): name = content['name'] s = md[name] or '' md[name] = s + content['data'] - elif msg_type == 'pyerr': - md.update({'pyerr' : self._unwrap_exception(content)}) - elif msg_type == 'pyin': - md.update({'pyin' : content['code']}) + elif msg_type == 'error': + md.update({'error' : self._unwrap_exception(content)}) + elif msg_type == 'execute_input': + md.update({'execute_input' : content['code']}) elif msg_type == 'display_data': md['outputs'].append(content) - elif msg_type == 'pyout': - md['pyout'] = content + elif msg_type == 'execute_result': + md['execute_result'] = content elif msg_type == 'data_message': data, remainder = serialize.unserialize_object(msg['buffers']) md['data'].update(data) @@ -1297,7 +1285,7 @@ class Client(HasTraits): if not isinstance(metadata, dict): raise TypeError("metadata must be dict, not %s" % type(metadata)) - content = dict(code=code, silent=bool(silent), user_variables=[], user_expressions={}) + content = dict(code=code, silent=bool(silent), user_expressions={}) msg = self.session.send(socket, "execute_request", content=content, ident=ident, diff --git a/IPython/parallel/controller/hub.py b/IPython/parallel/controller/hub.py index ee89133..0032320 100644 --- a/IPython/parallel/controller/hub.py +++ b/IPython/parallel/controller/hub.py @@ -1,21 +1,12 @@ """The IPython Controller Hub with 0MQ + This is the master object that handles connections from engines and clients, and monitors traffic through the various queues. - -Authors: - -* Min RK """ -#----------------------------------------------------------------------------- -# Copyright (C) 2010-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function import json @@ -75,9 +66,9 @@ def empty_record(): 'result_content' : None, 'result_buffers' : None, 'queue' : None, - 'pyin' : None, - 'pyout': None, - 'pyerr': None, + 'execute_input' : None, + 'execute_result': None, + 'error': None, 'stdout': '', 'stderr': '', } @@ -103,9 +94,9 @@ def init_record(msg): 'result_content' : None, 'result_buffers' : None, 'queue' : None, - 'pyin' : None, - 'pyout': None, - 'pyerr': None, + 'execute_input' : None, + 'execute_result': None, + 'error': None, 'stdout': '', 'stderr': '', } @@ -874,11 +865,11 @@ class Hub(SessionFactory): s = rec[name] or '' d[name] = s + content['data'] - elif msg_type == 'pyerr': - d['pyerr'] = content - elif msg_type == 'pyin': - d['pyin'] = content['code'] - elif msg_type in ('display_data', 'pyout'): + elif msg_type == 'error': + d['error'] = content + elif msg_type == 'execute_input': + d['execute_input'] = content['code'] + elif msg_type in ('display_data', 'execute_result'): d[msg_type] = content elif msg_type == 'status': pass @@ -1325,7 +1316,7 @@ class Hub(SessionFactory): def _extract_record(self, rec): """decompose a TaskRecord dict into subsection of reply for get_result""" io_dict = {} - for key in ('pyin', 'pyout', 'pyerr', 'stdout', 'stderr'): + for key in ('execute_input', 'execute_result', 'error', 'stdout', 'stderr'): io_dict[key] = rec[key] content = { 'header': rec['header'], diff --git a/IPython/parallel/controller/sqlitedb.py b/IPython/parallel/controller/sqlitedb.py index 2187af1..eb25b79 100644 --- a/IPython/parallel/controller/sqlitedb.py +++ b/IPython/parallel/controller/sqlitedb.py @@ -1,15 +1,7 @@ -"""A TaskRecord backend using sqlite3 +"""A TaskRecord backend using sqlite3""" -Authors: - -* Min RK -""" -#----------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. import json import os @@ -128,9 +120,9 @@ class SQLiteDB(BaseDB): 'result_content' , 'result_buffers' , 'queue' , - 'pyin' , - 'pyout', - 'pyerr', + 'execute_input' , + 'execute_result', + 'error', 'stdout', 'stderr', ]) @@ -152,9 +144,9 @@ class SQLiteDB(BaseDB): 'result_content' : 'dict text', 'result_buffers' : 'bufs blob', 'queue' : 'text', - 'pyin' : 'text', - 'pyout' : 'text', - 'pyerr' : 'text', + 'execute_input' : 'text', + 'execute_result' : 'text', + 'error' : 'text', 'stdout' : 'text', 'stderr' : 'text', }) @@ -263,9 +255,9 @@ class SQLiteDB(BaseDB): result_content dict text, result_buffers bufs blob, queue text, - pyin text, - pyout text, - pyerr text, + execute_input text, + execute_result text, + error text, stdout text, stderr text) """%self.table) diff --git a/IPython/parallel/engine/engine.py b/IPython/parallel/engine/engine.py index e60f2bf..5cd289d 100644 --- a/IPython/parallel/engine/engine.py +++ b/IPython/parallel/engine/engine.py @@ -1,17 +1,10 @@ """A simple engine that talks to a controller over 0MQ. it handles registration, etc. and launches a kernel connected to the Controller's Schedulers. - -Authors: - -* Min RK """ -#----------------------------------------------------------------------------- -# Copyright (C) 2010-2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. from __future__ import print_function @@ -228,7 +221,7 @@ class EngineFactory(RegistrationFactory): sys.stderr.topic = cast_bytes('engine.%i.stderr' % self.id) if self.display_hook_factory: sys.displayhook = self.display_hook_factory(self.session, iopub_socket) - sys.displayhook.topic = cast_bytes('engine.%i.pyout' % self.id) + sys.displayhook.topic = cast_bytes('engine.%i.execute_result' % self.id) self.kernel = Kernel(parent=self, int_id=self.id, ident=self.ident, session=self.session, control_stream=control_stream, shell_streams=shell_streams, iopub_socket=iopub_socket, diff --git a/IPython/parallel/tests/test_client.py b/IPython/parallel/tests/test_client.py index ae4fe63..e19da91 100644 --- a/IPython/parallel/tests/test_client.py +++ b/IPython/parallel/tests/test_client.py @@ -1,20 +1,7 @@ -"""Tests for parallel client.py +"""Tests for parallel client.py""" -Authors: - -* Min RK -""" - -#------------------------------------------------------------------------------- -# Copyright (C) 2011 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#------------------------------------------------------------------------------- - -#------------------------------------------------------------------------------- -# Imports -#------------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. from __future__ import division @@ -177,7 +164,7 @@ class TestClient(ClusterTestCase): time.sleep(.25) ahr = self.client.get_result(ar.msg_ids[0]) self.assertTrue(isinstance(ahr, AsyncHubResult)) - self.assertEqual(ahr.get().pyout, ar.get().pyout) + self.assertEqual(ahr.get().execute_result, ar.get().execute_result) ar2 = self.client.get_result(ar.msg_ids[0]) self.assertFalse(isinstance(ar2, AsyncHubResult)) c.close() diff --git a/IPython/parallel/tests/test_view.py b/IPython/parallel/tests/test_view.py index 44a3aa0..2bf499b 100644 --- a/IPython/parallel/tests/test_view.py +++ b/IPython/parallel/tests/test_view.py @@ -537,7 +537,7 @@ class TestView(ClusterTestCase): ar = e0.execute("5", silent=False) er = ar.get() self.assertEqual(str(er), "<ExecuteReply[%i]: 5>" % er.execution_count) - self.assertEqual(er.pyout['data']['text/plain'], '5') + self.assertEqual(er.execute_result['data']['text/plain'], '5') def test_execute_reply_rich(self): e0 = self.client[self.client.ids[0]] @@ -558,21 +558,21 @@ class TestView(ClusterTestCase): er = ar.get() self.assertEqual(er.stdout.strip(), '5') - def test_execute_pyout(self): - """execute triggers pyout with silent=False""" + def test_execute_result(self): + """execute triggers execute_result with silent=False""" view = self.client[:] ar = view.execute("5", silent=False, block=True) expected = [{'text/plain' : '5'}] * len(view) - mimes = [ out['data'] for out in ar.pyout ] + mimes = [ out['data'] for out in ar.execute_result ] self.assertEqual(mimes, expected) def test_execute_silent(self): - """execute does not trigger pyout with silent=True""" + """execute does not trigger execute_result with silent=True""" view = self.client[:] ar = view.execute("5", block=True) expected = [None] * len(view) - self.assertEqual(ar.pyout, expected) + self.assertEqual(ar.execute_result, expected) def test_execute_magic(self): """execute accepts IPython commands""" diff --git a/IPython/qt/console/call_tip_widget.py b/IPython/qt/console/call_tip_widget.py index 4cea749..378a138 100644 --- a/IPython/qt/console/call_tip_widget.py +++ b/IPython/qt/console/call_tip_widget.py @@ -1,5 +1,6 @@ # Standard library imports import re +import textwrap from unicodedata import category # System library imports @@ -122,21 +123,15 @@ class CallTipWidget(QtGui.QLabel): # 'CallTipWidget' interface #-------------------------------------------------------------------------- - def show_call_info(self, call_line=None, doc=None, maxlines=20): - """ Attempts to show the specified call line and docstring at the - current cursor location. The docstring is possibly truncated for - length. - """ - if doc: - match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc) - if match: - doc = doc[:match.end()] + '\n[Documentation continues...]' - else: - doc = '' + def show_inspect_data(self, content, maxlines=20): + """Show inspection data as a tooltip""" + data = content.get('data', {}) + text = data.get('text/plain', '') + match = re.match("(?:[^\n]*\n){%i}" % maxlines, text) + if match: + text = text[:match.end()] + '\n[Documentation continues...]' - if call_line: - doc = '\n\n'.join([call_line, doc]) - return self.show_tip(self._format_tooltip(doc)) + return self.show_tip(self._format_tooltip(text)) def show_tip(self, tip): """ Attempts to show the specified tip at the current cursor location. @@ -247,8 +242,8 @@ class CallTipWidget(QtGui.QLabel): QtGui.qApp.topLevelAt(QtGui.QCursor.pos()) != self): self._hide_timer.start(300, self) - def _format_tooltip(self,doc): - import textwrap + def _format_tooltip(self, doc): + doc = re.sub(r'\033\[(\d|;)+?m', '', doc) # make sure a long argument list does not make # the first row overflow the width of the actual tip body diff --git a/IPython/qt/console/console_widget.py b/IPython/qt/console/console_widget.py index 38078c3..bf85310 100644 --- a/IPython/qt/console/console_widget.py +++ b/IPython/qt/console/console_widget.py @@ -1584,7 +1584,16 @@ class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui. cursor = self._control.textCursor() text = self._get_block_plain_text(cursor.block()) return text[len(prompt):] - + + def _get_input_buffer_cursor_pos(self): + """Return the cursor position within the input buffer.""" + cursor = self._control.textCursor() + cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) + input_buffer = cursor.selection().toPlainText() + + # Don't count continuation prompts + return len(input_buffer.replace('\n' + self._continuation_prompt, '\n')) + def _get_input_buffer_cursor_prompt(self): """ Returns the (plain text) prompt for line of the input buffer that contains the cursor, or None if there is no such line. diff --git a/IPython/qt/console/frontend_widget.py b/IPython/qt/console/frontend_widget.py index a69ad70..e67c377 100644 --- a/IPython/qt/console/frontend_widget.py +++ b/IPython/qt/console/frontend_widget.py @@ -1,17 +1,19 @@ +"""Frontend widget for the Qt Console""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function -# Standard library imports from collections import namedtuple import sys import uuid -# System library imports from IPython.external import qt from IPython.external.qt import QtCore, QtGui from IPython.utils import py3compat from IPython.utils.importstring import import_item -# Local imports from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter from IPython.core.inputtransformer import classic_prompt from IPython.core.oinspect import call_tip @@ -80,6 +82,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): # The text to show when the kernel is (re)started. banner = Unicode(config=True) + kernel_banner = Unicode() # An option and corresponding signal for overriding the default kernel # interrupt behavior. @@ -502,34 +505,21 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): self._kernel_restarted_message(died=died) self.reset() - def _handle_object_info_reply(self, rep): - """ Handle replies for call tips. - """ + def _handle_inspect_reply(self, rep): + """Handle replies for call tips.""" self.log.debug("oinfo: %s", rep.get('content', '')) cursor = self._get_cursor() info = self._request_info.get('call_tip') if info and info.id == rep['parent_header']['msg_id'] and \ info.pos == cursor.position(): - # Get the information for a call tip. For now we format the call - # line as string, later we can pass False to format_call and - # syntax-highlight it ourselves for nicer formatting in the - # calltip. content = rep['content'] - # if this is from pykernel, 'docstring' will be the only key - if content.get('ismagic', False): - # Don't generate a call-tip for magics. Ideally, we should - # generate a tooltip, but not on ( like we do for actual - # callables. - call_info, doc = None, None - else: - call_info, doc = call_tip(content, format_call=True) - if call_info or doc: - self._call_tip_widget.show_call_info(call_info, doc) + if content.get('status') == 'ok': + self._call_tip_widget.show_inspect_data(content) - def _handle_pyout(self, msg): + def _handle_execute_result(self, msg): """ Handle display hook output. """ - self.log.debug("pyout: %s", msg.get('content', '')) + self.log.debug("execute_result: %s", msg.get('content', '')) if not self._hidden and self._is_from_this_session(msg): self.flush_clearoutput() text = msg['content']['data'] @@ -637,6 +627,9 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): if clear: self._control.clear() self._append_plain_text(self.banner) + if self.kernel_banner: + self._append_plain_text(self.kernel_banner) + # update output marker for stdout/stderr, so that startup # messages appear after banner: self._append_before_prompt_pos = self._get_cursor().position() @@ -725,17 +718,10 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): # Decide if it makes sense to show a call tip if not self.enable_calltips: return False - cursor = self._get_cursor() - cursor.movePosition(QtGui.QTextCursor.Left) - if cursor.document().characterAt(cursor.position()) != '(': - return False - context = self._get_context(cursor) - if not context: - return False - + cursor_pos = self._get_input_buffer_cursor_pos() + code = self.input_buffer # Send the metadata request to the kernel - name = '.'.join(context) - msg_id = self.kernel_client.object_info(name) + msg_id = self.kernel_client.inspect(code, cursor_pos) pos = self._get_cursor().position() self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos) return True @@ -747,10 +733,9 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): if context: # Send the completion request to the kernel msg_id = self.kernel_client.complete( - '.'.join(context), # text - self._get_input_buffer_cursor_line(), # line - self._get_input_buffer_cursor_column(), # cursor_pos - self.input_buffer) # block + code=self.input_buffer, + cursor_pos=self._get_input_buffer_cursor_pos(), + ) pos = self._get_cursor().position() info = self._CompletionRequest(msg_id, pos) self._request_info['complete'] = info diff --git a/IPython/qt/console/ipython_widget.py b/IPython/qt/console/ipython_widget.py index d99a783..1a0a757 100644 --- a/IPython/qt/console/ipython_widget.py +++ b/IPython/qt/console/ipython_widget.py @@ -3,11 +3,9 @@ This supports the additional functionality provided by the IPython kernel. """ -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -# Standard library imports from collections import namedtuple import os.path import re @@ -16,11 +14,10 @@ import sys import time from textwrap import dedent -# System library imports from IPython.external.qt import QtCore, QtGui -# Local imports from IPython.core.inputsplitter import IPythonInputSplitter +from IPython.core.release import version from IPython.core.inputtransformer import ipy_prompt from IPython.utils.traitlets import Bool, Unicode from .frontend_widget import FrontendWidget @@ -111,6 +108,7 @@ class IPythonWidget(FrontendWidget): _payload_source_next_input = 'set_next_input' _payload_source_page = 'page' _retrying_history_request = False + _starting = False #--------------------------------------------------------------------------- # 'object' interface @@ -148,22 +146,12 @@ class IPythonWidget(FrontendWidget): info = self._request_info.get('complete') if info and info.id == rep['parent_header']['msg_id'] and \ info.pos == cursor.position(): - matches = rep['content']['matches'] - text = rep['content']['matched_text'] - offset = len(text) - - # Clean up matches with period and path separators if the matched - # text has not been transformed. This is done by truncating all - # but the last component and then suitably decreasing the offset - # between the current cursor position and the start of completion. - if len(matches) > 1 and matches[0][:offset] == text: - parts = re.split(r'[./\\]', text) - sep_count = len(parts) - 1 - if sep_count: - chop_length = sum(map(len, parts[:sep_count])) + sep_count - matches = [ match[chop_length:] for match in matches ] - offset -= chop_length - + content = rep['content'] + matches = content['matches'] + start = content['cursor_start'] + end = content['cursor_end'] + + offset = end - start # Move the cursor to the start of the match and complete. cursor.movePosition(QtGui.QTextCursor.Left, n=offset) self._complete_with_items(cursor, matches) @@ -217,10 +205,10 @@ class IPythonWidget(FrontendWidget): last_cell = cell self._set_history(items) - def _handle_pyout(self, msg): + def _handle_execute_result(self, msg): """ Reimplemented for IPython-style "display hook". """ - self.log.debug("pyout: %s", msg.get('content', '')) + self.log.debug("execute_result: %s", msg.get('content', '')) if not self._hidden and self._is_from_this_session(msg): self.flush_clearoutput() content = msg['content'] @@ -257,31 +245,28 @@ class IPythonWidget(FrontendWidget): self._append_plain_text(u'\n', True) def _handle_kernel_info_reply(self, rep): - """ Handle kernel info replies. - """ + """Handle kernel info replies.""" + content = rep['content'] if not self._guiref_loaded: - if rep['content'].get('language') == 'python': + if content.get('language') == 'python': self._load_guiref_magic() self._guiref_loaded = True + + self.kernel_banner = content.get('banner', '') + if self._starting: + # finish handling started channels + self._starting = False + super(IPythonWidget, self)._started_channels() def _started_channels(self): """Reimplemented to make a history request and load %guiref.""" - super(IPythonWidget, self)._started_channels() - + self._starting = True # The reply will trigger %guiref load provided language=='python' self.kernel_client.kernel_info() self.kernel_client.shell_channel.history(hist_access_type='tail', n=1000) - def _started_kernel(self): - """Load %guiref when the kernel starts (if channels are also started). - - Principally triggered by kernel restart. - """ - if self.kernel_client.shell_channel is not None: - self._load_guiref_magic() - def _load_guiref_magic(self): """Load %guiref magic.""" self.kernel_client.shell_channel.execute('\n'.join([ @@ -331,24 +316,6 @@ class IPythonWidget(FrontendWidget): # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- - def _complete(self): - """ Reimplemented to support IPython's improved completion machinery. - """ - # We let the kernel split the input line, so we *always* send an empty - # text field. Readline-based frontends do get a real text field which - # they can use. - text = '' - - # Send the completion request to the kernel - msg_id = self.kernel_client.shell_channel.complete( - text, # text - self._get_input_buffer_cursor_line(), # line - self._get_input_buffer_cursor_column(), # cursor_pos - self.input_buffer) # block - pos = self._get_cursor().position() - info = self._CompletionRequest(msg_id, pos) - self._request_info['complete'] = info - def _process_execute_error(self, msg): """ Reimplemented for IPython-style traceback formatting. """ @@ -555,10 +522,11 @@ class IPythonWidget(FrontendWidget): # Since the plain text widget supports only a very small subset of HTML # and we have no control over the HTML source, we only page HTML # payloads in the rich text widget. - if item['html'] and self.kind == 'rich': - self._page(item['html'], html=True) + data = item['data'] + if 'text/html' in data and self.kind == 'rich': + self._page(data['text/html'], html=True) else: - self._page(item['text'], html=False) + self._page(data['text/plain'], html=False) #------ Trait change handlers -------------------------------------------- @@ -590,5 +558,4 @@ class IPythonWidget(FrontendWidget): #------ Trait default initializers ----------------------------------------- def _banner_default(self): - from IPython.core.usage import default_gui_banner - return default_gui_banner + return "IPython QtConsole {version}\n".format(version=version) diff --git a/IPython/qt/console/rich_ipython_widget.py b/IPython/qt/console/rich_ipython_widget.py index fd0737d..67e8563 100644 --- a/IPython/qt/console/rich_ipython_widget.py +++ b/IPython/qt/console/rich_ipython_widget.py @@ -98,12 +98,12 @@ class RichIPythonWidget(IPythonWidget): Shared code for some the following if statement """ - self.log.debug("pyout: %s", msg.get('content', '')) + self.log.debug("execute_result: %s", msg.get('content', '')) self._append_plain_text(self.output_sep, True) self._append_html(self._make_out_prompt(prompt_number), True) self._append_plain_text('\n', True) - def _handle_pyout(self, msg): + def _handle_execute_result(self, msg): """ Overridden to handle rich data types, like SVG. """ if not self._hidden and self._is_from_this_session(msg): @@ -128,7 +128,7 @@ class RichIPythonWidget(IPythonWidget): self._append_html(self.output_sep2, True) else: # Default back to the plain text representation. - return super(RichIPythonWidget, self)._handle_pyout(msg) + return super(RichIPythonWidget, self)._handle_execute_result(msg) def _handle_display_data(self, msg): """ Overridden to handle rich data types, like SVG. diff --git a/IPython/qt/kernel_mixins.py b/IPython/qt/kernel_mixins.py index 0f3a09d..667aa0e 100644 --- a/IPython/qt/kernel_mixins.py +++ b/IPython/qt/kernel_mixins.py @@ -1,10 +1,10 @@ -""" Defines a KernelManager that provides signals and slots. -""" +"""Defines a KernelManager that provides signals and slots.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -# System library imports. from IPython.external.qt import QtCore -# IPython imports. from IPython.utils.traitlets import HasTraits, Type from .util import MetaQObjectHasTraits, SuperQObject @@ -57,7 +57,7 @@ class QtShellChannelMixin(ChannelQObject): # Emitted when a reply has been received for the corresponding request type. execute_reply = QtCore.Signal(object) complete_reply = QtCore.Signal(object) - object_info_reply = QtCore.Signal(object) + inspect_reply = QtCore.Signal(object) history_reply = QtCore.Signal(object) #--------------------------------------------------------------------------- @@ -85,14 +85,14 @@ class QtIOPubChannelMixin(ChannelQObject): # Emitted when a message of type 'stream' is received. stream_received = QtCore.Signal(object) - # Emitted when a message of type 'pyin' is received. - pyin_received = QtCore.Signal(object) + # Emitted when a message of type 'execute_input' is received. + execute_input_received = QtCore.Signal(object) - # Emitted when a message of type 'pyout' is received. - pyout_received = QtCore.Signal(object) + # Emitted when a message of type 'execute_result' is received. + execute_result_received = QtCore.Signal(object) - # Emitted when a message of type 'pyerr' is received. - pyerr_received = QtCore.Signal(object) + # Emitted when a message of type 'error' is received. + error_received = QtCore.Signal(object) # Emitted when a message of type 'display_data' is received display_data_received = QtCore.Signal(object) diff --git a/IPython/terminal/console/completer.py b/IPython/terminal/console/completer.py index 294d347..a85b6e5 100644 --- a/IPython/terminal/console/completer.py +++ b/IPython/terminal/console/completer.py @@ -1,6 +1,9 @@ -"""Adapt readline completer interface to make ZMQ request. -""" # -*- coding: utf-8 -*- +"""Adapt readline completer interface to make ZMQ request.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + try: from queue import Empty # Py 3 except ImportError: @@ -27,14 +30,16 @@ class ZMQCompleter(IPCompleter): self.client = client self.matches = [] - def complete_request(self,text): + def complete_request(self, text): line = readline.get_line_buffer() cursor_pos = readline.get_endidx() # send completion request to kernel # Give the kernel up to 0.5s to respond - msg_id = self.client.shell_channel.complete(text=text, line=line, - cursor_pos=cursor_pos) + msg_id = self.client.shell_channel.complete( + code=line, + cursor_pos=cursor_pos, + ) msg = self.client.shell_channel.get_msg(timeout=self.timeout) if msg['parent_header']['msg_id'] == msg_id: diff --git a/IPython/terminal/console/interactiveshell.py b/IPython/terminal/console/interactiveshell.py index dc897bf..1a298f8 100644 --- a/IPython/terminal/console/interactiveshell.py +++ b/IPython/terminal/console/interactiveshell.py @@ -1,27 +1,20 @@ # -*- coding: utf-8 -*- -"""terminal client to the IPython kernel - -""" -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- - -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +"""terminal client to the IPython kernel""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + from __future__ import print_function +import base64 import bdb import signal import os import sys import time import subprocess +from getpass import getpass from io import BytesIO -import base64 try: from queue import Empty # Py 3 @@ -29,6 +22,7 @@ except ImportError: from Queue import Empty # Py 2 from IPython.core import page +from IPython.core import release from IPython.utils.warn import warn, error from IPython.utils import io from IPython.utils.py3compat import string_types, input @@ -44,6 +38,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): _executing = False _execution_state = Unicode('') _pending_clearoutput = False + kernel_banner = Unicode('') kernel_timeout = Float(60, config=True, help="""Timeout for giving up on a kernel (in seconds). @@ -179,7 +174,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): self._execution_state = "busy" while self._execution_state != 'idle' and self.client.is_alive(): try: - self.handle_stdin_request(msg_id, timeout=0.05) + self.handle_input_request(msg_id, timeout=0.05) except Empty: # display intermediate print statements, etc. self.handle_iopub(msg_id) @@ -211,11 +206,13 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): self.write('Aborted\n') return elif status == 'ok': - # print execution payloads as well: + # handle payloads for item in content["payload"]: - text = item.get('text', None) - if text: - page.page(text) + source = item['source'] + if source == 'page': + page.page(item['data']['text/plain']) + elif source == 'set_next_input': + self.set_next_input(item['text']) elif status == 'error': for frame in content["traceback"]: @@ -228,7 +225,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): """Process messages on the IOPub channel This method consumes and processes messages on the IOPub channel, - such as stdout, stderr, pyout and status. + such as stdout, stderr, execute_result and status. It only displays output that is caused by this session. """ @@ -254,7 +251,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): print(sub_msg["content"]["data"], file=io.stderr, end="") io.stderr.flush() - elif msg_type == 'pyout': + elif msg_type == 'execute_result': if self._pending_clearoutput: print("\r", file=io.stdout, end="") self._pending_clearoutput = False @@ -335,13 +332,13 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): def handle_image_callable(self, data, mime): self.callable_image_handler(data) - def handle_stdin_request(self, msg_id, timeout=0.1): + def handle_input_request(self, msg_id, timeout=0.1): """ Method to capture raw_input """ - msg_rep = self.client.stdin_channel.get_msg(timeout=timeout) + req = self.client.stdin_channel.get_msg(timeout=timeout) # in case any iopub came while we were waiting: self.handle_iopub(msg_id) - if msg_id == msg_rep["parent_header"].get("msg_id"): + if msg_id == req["parent_header"].get("msg_id"): # wrap SIGINT handler real_handler = signal.getsignal(signal.SIGINT) def double_int(sig,frame): @@ -350,9 +347,10 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): real_handler(sig,frame) raise KeyboardInterrupt signal.signal(signal.SIGINT, double_int) - + content = req['content'] + read = getpass if content.get('password', False) else input try: - raw_data = input(msg_rep["content"]["prompt"]) + raw_data = read(content["prompt"]) except EOFError: # turn EOFError into EOF character raw_data = '\x04' @@ -372,7 +370,7 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): while True: try: self.interact(display_banner=display_banner) - #self.interact_with_readline() + #self.interact_with_readline() # XXX for testing of a readline-decoupled repl loop, call # interact_with_readline above break @@ -381,6 +379,24 @@ class ZMQTerminalInteractiveShell(TerminalInteractiveShell): # handling seems rather unpredictable... self.write("\nKeyboardInterrupt in interact()\n") + def _banner1_default(self): + return "IPython Console {version}\n".format(version=release.version) + + def compute_banner(self): + super(ZMQTerminalInteractiveShell, self).compute_banner() + if self.client and not self.kernel_banner: + msg_id = self.client.kernel_info() + while True: + try: + reply = self.client.get_shell_msg(timeout=1) + except Empty: + break + else: + if reply['parent_header'].get('msg_id') == msg_id: + self.kernel_banner = reply['content'].get('banner', '') + break + self.banner += self.kernel_banner + def wait_for_kernel(self, timeout=None): """method to wait for a kernel to be ready""" tic = time.time() diff --git a/IPython/terminal/embed.py b/IPython/terminal/embed.py index a37d226..ebda2e9 100644 --- a/IPython/terminal/embed.py +++ b/IPython/terminal/embed.py @@ -75,24 +75,17 @@ class InteractiveShellEmbed(TerminalInteractiveShell): # Like the base class display_banner is not configurable, but here it # is True by default. display_banner = CBool(True) + exit_msg = Unicode() + - def __init__(self, config=None, ipython_dir=None, user_ns=None, - user_module=None, custom_exceptions=((),None), - usage=None, banner1=None, banner2=None, - display_banner=None, exit_msg=u'', user_global_ns=None): + def __init__(self, **kw): + - if user_global_ns is not None: + if kw.get('user_global_ns', None) is not None: warnings.warn("user_global_ns has been replaced by user_module. The\ parameter will be ignored.", DeprecationWarning) - super(InteractiveShellEmbed,self).__init__( - config=config, ipython_dir=ipython_dir, user_ns=user_ns, - user_module=user_module, custom_exceptions=custom_exceptions, - usage=usage, banner1=banner1, banner2=banner2, - display_banner=display_banner - ) - - self.exit_msg = exit_msg + super(InteractiveShellEmbed,self).__init__(**kw) # don't use the ipython crash handler so that user exceptions aren't # trapped diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index d0e8d38..ff02a3f 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -20,7 +20,7 @@ import os import sys from IPython.core.error import TryNext, UsageError -from IPython.core.usage import interactive_usage, default_banner +from IPython.core.usage import interactive_usage from IPython.core.inputsplitter import IPythonInputSplitter from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC from IPython.core.magic import Magics, magics_class, line_magic @@ -258,13 +258,6 @@ class TerminalInteractiveShell(InteractiveShell): autoedit_syntax = CBool(False, config=True, help="auto editing of files with syntax errors.") - banner = Unicode('') - banner1 = Unicode(default_banner, config=True, - help="""The part of the banner to be printed before the profile""" - ) - banner2 = Unicode('', config=True, - help="""The part of the banner to be printed after the profile""" - ) confirm_exit = CBool(True, config=True, help=""" Set to confirm when you try to exit IPython with an EOF (Control-D @@ -274,8 +267,7 @@ class TerminalInteractiveShell(InteractiveShell): # This display_banner only controls whether or not self.show_banner() # is called when mainloop/interact are called. The default is False # because for the terminal based application, the banner behavior - # is controlled by Global.display_banner, which IPythonApp looks at - # to determine if *it* should call show_banner() by hand or not. + # is controlled by the application. display_banner = CBool(False) # This isn't configurable! embedded = CBool(False) embedded_active = CBool(False) @@ -300,6 +292,7 @@ class TerminalInteractiveShell(InteractiveShell): term_title = CBool(False, config=True, help="Enable auto setting the terminal title." ) + usage = Unicode(interactive_usage) # This `using_paste_magics` is used to detect whether the code is being # executed via paste magics functions @@ -317,23 +310,7 @@ class TerminalInteractiveShell(InteractiveShell): except ValueError as e: raise UsageError("%s" % e) - def __init__(self, config=None, ipython_dir=None, profile_dir=None, - user_ns=None, user_module=None, custom_exceptions=((),None), - usage=None, banner1=None, banner2=None, display_banner=None, - **kwargs): - - super(TerminalInteractiveShell, self).__init__( - config=config, ipython_dir=ipython_dir, profile_dir=profile_dir, user_ns=user_ns, - user_module=user_module, custom_exceptions=custom_exceptions, - **kwargs - ) - # use os.system instead of utils.process.system by default, - # because piped system doesn't make sense in the Terminal: - self.system = self.system_raw - - self.init_term_title() - self.init_usage(usage) - self.init_banner(banner1, banner2, display_banner) + system = InteractiveShell.system_raw #------------------------------------------------------------------------- # Overrides of init stages @@ -356,6 +333,9 @@ class TerminalInteractiveShell(InteractiveShell): num_lines_bot = self.separate_in.count('\n')+1 return self.screen_length - num_lines_bot + def _term_title_changed(self, name, new_value): + self.init_term_title() + def init_term_title(self): # Enable or disable the terminal title. if self.term_title: @@ -386,46 +366,6 @@ class TerminalInteractiveShell(InteractiveShell): self.alias_manager.soft_define_alias(name, cmd) #------------------------------------------------------------------------- - # Things related to the banner and usage - #------------------------------------------------------------------------- - - def _banner1_changed(self): - self.compute_banner() - - def _banner2_changed(self): - self.compute_banner() - - def _term_title_changed(self, name, new_value): - self.init_term_title() - - def init_banner(self, banner1, banner2, display_banner): - if banner1 is not None: - self.banner1 = banner1 - if banner2 is not None: - self.banner2 = banner2 - if display_banner is not None: - self.display_banner = display_banner - self.compute_banner() - - def show_banner(self, banner=None): - if banner is None: - banner = self.banner - self.write(banner) - - def compute_banner(self): - self.banner = self.banner1 - if self.profile and self.profile != 'default': - self.banner += '\nIPython profile: %s\n' % self.profile - if self.banner2: - self.banner += '\n' + self.banner2 - - def init_usage(self, usage=None): - if usage is None: - self.usage = interactive_usage - else: - self.usage = usage - - #------------------------------------------------------------------------- # Mainloop and code execution logic #------------------------------------------------------------------------- diff --git a/IPython/utils/capture.py b/IPython/utils/capture.py index 01ad4e8..1731bf2 100644 --- a/IPython/utils/capture.py +++ b/IPython/utils/capture.py @@ -1,19 +1,10 @@ # encoding: utf-8 -""" -IO capturing utilities. -""" +"""IO capturing utilities.""" -#----------------------------------------------------------------------------- -# Copyright (C) 2013 The IPython Development Team -# -# Distributed under the terms of the BSD License. The full license is in -# the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- -from __future__ import print_function, absolute_import +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. -#----------------------------------------------------------------------------- -# Imports -#----------------------------------------------------------------------------- +from __future__ import print_function, absolute_import import sys @@ -30,14 +21,13 @@ else: class RichOutput(object): - def __init__(self, source="", data=None, metadata=None): - self.source = source + def __init__(self, data=None, metadata=None): self.data = data or {} self.metadata = metadata or {} def display(self): from IPython.display import publish_display_data - publish_display_data(self.source, self.data, self.metadata) + publish_display_data(data=self.data, metadata=self.metadata) def _repr_mime_(self, mime): if mime not in self.data: @@ -118,7 +108,7 @@ class CapturedIO(object): for o in c.outputs: display(o) """ - return [ RichOutput(s, d, md) for s, d, md in self._outputs ] + return [ RichOutput(d, md) for d, md in self._outputs ] def show(self): """write my output to sys.stdout/err as appropriate""" @@ -126,8 +116,8 @@ class CapturedIO(object): sys.stderr.write(self.stderr) sys.stdout.flush() sys.stderr.flush() - for source, data, metadata in self._outputs: - RichOutput(source, data, metadata).display() + for data, metadata in self._outputs: + RichOutput(data, metadata).display() __call__ = show diff --git a/IPython/utils/tests/test_capture.py b/IPython/utils/tests/test_capture.py index f3305d5..30345d7 100644 --- a/IPython/utils/tests/test_capture.py +++ b/IPython/utils/tests/test_capture.py @@ -78,8 +78,7 @@ def test_rich_output(): """test RichOutput basics""" data = basic_data metadata = basic_metadata - rich = capture.RichOutput(source="test", data=data, metadata=metadata) - yield nt.assert_equal, rich.source, "test" + rich = capture.RichOutput(data=data, metadata=metadata) yield nt.assert_equal, rich._repr_html_(), data['text/html'] yield nt.assert_equal, rich._repr_png_(), (data['image/png'], metadata['image/png']) yield nt.assert_equal, rich._repr_latex_(), None @@ -89,7 +88,7 @@ def test_rich_output(): def test_rich_output_no_metadata(): """test RichOutput with no metadata""" data = full_data - rich = capture.RichOutput(source="test", data=data) + rich = capture.RichOutput(data=data) for method, mime in _mime_map.items(): yield nt.assert_equal, getattr(rich, method)(), data[mime] @@ -97,7 +96,7 @@ def test_rich_output_metadata(): """test RichOutput with metadata""" data = full_data metadata = full_metadata - rich = capture.RichOutput(source="test", data=data, metadata=metadata) + rich = capture.RichOutput(data=data, metadata=metadata) for method, mime in _mime_map.items(): yield nt.assert_equal, getattr(rich, method)(), (data[mime], metadata[mime]) diff --git a/IPython/utils/tests/test_tokenutil.py b/IPython/utils/tests/test_tokenutil.py new file mode 100644 index 0000000..4d7084a --- /dev/null +++ b/IPython/utils/tests/test_tokenutil.py @@ -0,0 +1,63 @@ +"""Tests for tokenutil""" +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +import nose.tools as nt + +from IPython.utils.tokenutil import token_at_cursor + +def expect_token(expected, cell, cursor_pos): + token = token_at_cursor(cell, cursor_pos) + offset = 0 + for line in cell.splitlines(): + if offset + len(line) >= cursor_pos: + break + else: + offset += len(line) + column = cursor_pos - offset + line_with_cursor = '%s|%s' % (line[:column], line[column:]) + line + nt.assert_equal(token, expected, + "Excpected %r, got %r in: %s" % ( + expected, token, line_with_cursor) + ) + +def test_simple(): + cell = "foo" + for i in range(len(cell)): + expect_token("foo", cell, i) + +def test_function(): + cell = "foo(a=5, b='10')" + expected = 'foo' + for i in (6,7,8,10,11,12): + expect_token("foo", cell, i) + +def test_multiline(): + cell = '\n'.join([ + 'a = 5', + 'b = hello("string", there)' + ]) + expected = 'hello' + start = cell.index(expected) + for i in range(start, start + len(expected)): + expect_token(expected, cell, i) + expected = 'there' + start = cell.index(expected) + for i in range(start, start + len(expected)): + expect_token(expected, cell, i) + +def test_attrs(): + cell = "foo(a=obj.attr.subattr)" + expected = 'obj' + idx = cell.find('obj') + for i in range(idx, idx + 3): + expect_token(expected, cell, i) + idx = idx + 4 + expected = 'obj.attr' + for i in range(idx, idx + 4): + expect_token(expected, cell, i) + idx = idx + 5 + expected = 'obj.attr.subattr' + for i in range(idx, len(cell)): + expect_token(expected, cell, i) diff --git a/IPython/utils/tokenutil.py b/IPython/utils/tokenutil.py new file mode 100644 index 0000000..9a14e04 --- /dev/null +++ b/IPython/utils/tokenutil.py @@ -0,0 +1,78 @@ +"""Token-related utilities""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import absolute_import, print_function + +from collections import namedtuple +from io import StringIO +from keyword import iskeyword + +from . import tokenize2 +from .py3compat import cast_unicode_py2 + +Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line']) + +def generate_tokens(readline): + """wrap generate_tokens to catch EOF errors""" + try: + for token in tokenize2.generate_tokens(readline): + yield token + except tokenize2.TokenError: + # catch EOF error + return + +def token_at_cursor(cell, cursor_pos=0): + """Get the token at a given cursor + + Used for introspection. + + Parameters + ---------- + + cell : unicode + A block of Python code + cursor_pos : int + The location of the cursor in the block where the token should be found + """ + cell = cast_unicode_py2(cell) + names = [] + tokens = [] + offset = 0 + for tup in generate_tokens(StringIO(cell).readline): + + tok = Token(*tup) + + # token, text, start, end, line = tup + start_col = tok.start[1] + end_col = tok.end[1] + if offset + start_col > cursor_pos: + # current token starts after the cursor, + # don't consume it + break + + if tok.token == tokenize2.NAME and not iskeyword(tok.text): + if names and tokens and tokens[-1].token == tokenize2.OP and tokens[-1].text == '.': + names[-1] = "%s.%s" % (names[-1], tok.text) + else: + names.append(tok.text) + elif tok.token == tokenize2.OP: + if tok.text == '=' and names: + # don't inspect the lhs of an assignment + names.pop(-1) + + if offset + end_col > cursor_pos: + # we found the cursor, stop reading + break + + tokens.append(tok) + if tok.token == tokenize2.NEWLINE: + offset += len(tok.line) + + if names: + return names[-1] + else: + return '' + + diff --git a/docs/source/_static/default.css b/docs/source/_static/default.css index 38544c6..a976035 100644 --- a/docs/source/_static/default.css +++ b/docs/source/_static/default.css @@ -519,3 +519,16 @@ ul.keywordmatches li.goodmatch a { div.figure { text-align: center; } + +div.versionchanged { + margin-left: 30px; + margin-right: 30px; +} + +span.versionmodified { + font-style: italic; +} + +pre { + white-space: pre-wrap; +} \ No newline at end of file diff --git a/docs/source/development/execution.rst b/docs/source/development/execution.rst new file mode 100644 index 0000000..8350c04 --- /dev/null +++ b/docs/source/development/execution.rst @@ -0,0 +1,64 @@ +.. _execution_semantics: + +Execution semantics in the IPython kernel +========================================= + +The execution of use code consists of the following phases: + +1. Fire the ``pre_execute`` event. +2. Fire the ``pre_run_cell`` event unless silent is True. +3. Execute the ``code`` field, see below for details. +4. If execution succeeds, expressions in ``user_expressions`` are computed. + This ensures that any error in the expressions don't affect the main code execution. +5. Fire the post_execute event. + +.. seealso:: + + :doc:`config/callbacks` + + +To understand how the ``code`` field is executed, one must know that Python +code can be compiled in one of three modes (controlled by the ``mode`` argument +to the :func:`compile` builtin): + +*single* + Valid for a single interactive statement (though the source can contain + multiple lines, such as a for loop). When compiled in this mode, the + generated bytecode contains special instructions that trigger the calling of + :func:`sys.displayhook` for any expression in the block that returns a value. + This means that a single statement can actually produce multiple calls to + :func:`sys.displayhook`, if for example it contains a loop where each + iteration computes an unassigned expression would generate 10 calls:: + + for i in range(10): + i**2 + +*exec* + An arbitrary amount of source code, this is how modules are compiled. + :func:`sys.displayhook` is *never* implicitly called. + +*eval* + A single expression that returns a value. :func:`sys.displayhook` is *never* + implicitly called. + + +The ``code`` field is split into individual blocks each of which is valid for +execution in 'single' mode, and then: + +- If there is only a single block: it is executed in 'single' mode. + +- If there is more than one block: + + * if the last one is a single line long, run all but the last in 'exec' mode + and the very last one in 'single' mode. This makes it easy to type simple + expressions at the end to see computed values. + + * if the last one is no more than two lines long, run all but the last in + 'exec' mode and the very last one in 'single' mode. This makes it easy to + type simple expressions at the end to see computed values. - otherwise + (last one is also multiline), run all in 'exec' mode + + * otherwise (last one is also multiline), run all in 'exec' mode as a single + unit. + + diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index 7ae2096..f8c9386 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -20,6 +20,7 @@ on the IPython GitHub wiki. :maxdepth: 1 messaging + execution parallel_messages parallel_connections lexer diff --git a/docs/source/development/messaging.rst b/docs/source/development/messaging.rst index 25007c3..6c145c4 100644 --- a/docs/source/development/messaging.rst +++ b/docs/source/development/messaging.rst @@ -9,7 +9,7 @@ Versioning ========== The IPython message specification is versioned independently of IPython. -The current version of the specification is 4.1. +The current version of the specification is 5.0. Introduction @@ -38,22 +38,13 @@ The basic design is explained in the following diagram: A single kernel can be simultaneously connected to one or more frontends. The kernel has three sockets that serve the following functions: -1. stdin: this ROUTER socket is connected to all frontends, and it allows - the kernel to request input from the active frontend when :func:`raw_input` is called. - The frontend that executed the code has a DEALER socket that acts as a 'virtual keyboard' - for the kernel while this communication is happening (illustrated in the - figure by the black outline around the central keyboard). In practice, - frontends may display such kernel requests using a special input widget or - otherwise indicating that the user is to type input for the kernel instead - of normal commands in the frontend. - -2. Shell: this single ROUTER socket allows multiple incoming connections from +1. Shell: this single ROUTER socket allows multiple incoming connections from frontends, and this is the socket where requests for code execution, object information, prompts, etc. are made to the kernel by any frontend. The communication on this socket is a sequence of request/reply actions from each frontend and the kernel. -3. IOPub: this socket is the 'broadcast channel' where the kernel publishes all +2. IOPub: this socket is the 'broadcast channel' where the kernel publishes all side effects (stdout, stderr, etc.) as well as the requests coming from any client over the shell socket and its own requests on the stdin socket. There are a number of actions in Python which generate side effects: :func:`print` @@ -64,11 +55,23 @@ kernel has three sockets that serve the following functions: about communications taking place with one client over the shell channel to be made available to all clients in a uniform manner. +3. stdin: this ROUTER socket is connected to all frontends, and it allows + the kernel to request input from the active frontend when :func:`raw_input` is called. + The frontend that executed the code has a DEALER socket that acts as a 'virtual keyboard' + for the kernel while this communication is happening (illustrated in the + figure by the black outline around the central keyboard). In practice, + frontends may display such kernel requests using a special input widget or + otherwise indicating that the user is to type input for the kernel instead + of normal commands in the frontend. + All messages are tagged with enough information (details below) for clients to know which messages come from their own interaction with the kernel and which ones are from other clients, so they can display each type appropriately. +4. Control: This channel is identical to Shell, but operates on a separate socket, + to allow important messages to avoid queueing behind execution requests (e.g. shutdown or abort). + The actual format of the messages allowed on each of these channels is specified below. Messages are dicts of dicts with string keys and values that are reasonably representable in JSON. Our current implementation uses JSON @@ -103,6 +106,8 @@ A message is defined by the following four-dictionary structure:: 'session' : uuid, # All recognized message type strings are listed below. 'msg_type' : str, + # the message protocol version + 'version' : '5.0', }, # In a chain of messages, the header from the parent is copied so that @@ -117,6 +122,10 @@ A message is defined by the following four-dictionary structure:: 'content' : dict, } +.. versionchanged:: 5.0 + + ``version`` key added to the header. + The Wire Protocol ================= @@ -151,14 +160,14 @@ The front of the message is the ZeroMQ routing prefix, which can be zero or more socket identities. This is every piece of the message prior to the delimiter key ``<IDS|MSG>``. In the case of IOPub, there should be just one prefix component, -which is the topic for IOPub subscribers, e.g. ``pyout``, ``display_data``. +which is the topic for IOPub subscribers, e.g. ``execute_result``, ``display_data``. .. note:: In most cases, the IOPub topics are irrelevant and completely ignored, because frontends just subscribe to all topics. The convention used in the IPython kernel is to use the msg_type as the topic, - and possibly extra information about the message, e.g. ``pyout`` or ``stream.stdout`` + and possibly extra information about the message, e.g. ``execute_result`` or ``stream.stdout`` After the delimiter is the `HMAC`_ signature of the message, used for authentication. If authentication is disabled, this should be an empty string. @@ -248,13 +257,11 @@ Message type: ``execute_request``:: 'code' : str, # A boolean flag which, if True, signals the kernel to execute - # this code as quietly as possible. This means that the kernel - # will compile the code with 'exec' instead of 'single' (so - # sys.displayhook will not fire), forces store_history to be False, + # this code as quietly as possible. + # silent=True forces store_history to be False, # and will *not*: - # - broadcast exceptions on the PUB socket - # - do any logging - # + # - broadcast output on the IOPUB channel + # - have an execute_result # The default is False. 'silent' : bool, @@ -263,156 +270,67 @@ Message type: ``execute_request``:: # is forced to be False. 'store_history' : bool, - # A list of variable names from the user's namespace to be retrieved. - # What returns is a rich representation of each variable (dict keyed by name). + # A dict mapping names to expressions to be evaluated in the + # user's dict. The rich display-data representation of each will be evaluated after execution. # See the display_data content for the structure of the representation data. - 'user_variables' : list, - - # Similarly, a dict mapping names to expressions to be evaluated in the - # user's dict. 'user_expressions' : dict, - # Some frontends (e.g. the Notebook) do not support stdin requests. If - # raw_input is called from code executed from such a frontend, a - # StdinNotImplementedError will be raised. + # Some frontends do not support stdin requests. + # If raw_input is called from code executed from such a frontend, + # a StdinNotImplementedError will be raised. 'allow_stdin' : True, - } -The ``code`` field contains a single string (possibly multiline). The kernel -is responsible for splitting this into one or more independent execution blocks -and deciding whether to compile these in 'single' or 'exec' mode (see below for -detailed execution semantics). +.. versionchanged:: 5.0 + + ``user_variables`` removed, because it is redundant with user_expressions. -The ``user_`` fields deserve a detailed explanation. In the past, IPython had +The ``code`` field contains a single string (possibly multiline) to be executed. + +The ``user_expressions`` field deserves a detailed explanation. In the past, IPython had the notion of a prompt string that allowed arbitrary code to be evaluated, and this was put to good use by many in creating prompts that displayed system status, path information, and even more esoteric uses like remote instrument status acquired over the network. But now that IPython has a clean separation between the kernel and the clients, the kernel has no prompt knowledge; prompts -are a frontend-side feature, and it should be even possible for different +are a frontend feature, and it should be even possible for different frontends to display different prompts while interacting with the same kernel. +``user_expressions`` can be used to retrieve this information. -The kernel now provides the ability to retrieve data from the user's namespace -after the execution of the main ``code``, thanks to two fields in the -``execute_request`` message: - -- ``user_variables``: If only variables from the user's namespace are needed, a - list of variable names can be passed and a dict with these names as keys and - their :func:`repr()` as values will be returned. +Any error in evaluating any expression in ``user_expressions`` will result in +only that key containing a standard error message, of the form:: -- ``user_expressions``: For more complex expressions that require function - evaluations, a dict can be provided with string keys and arbitrary python - expressions as values. The return message will contain also a dict with the - same keys and the :func:`repr()` of the evaluated expressions as value. - -With this information, frontends can display any status information they wish -in the form that best suits each frontend (a status line, a popup, inline for a -terminal, etc). + { + 'status' : 'error', + 'ename' : 'NameError', + 'evalue' : 'foo', + 'traceback' : ... + } .. Note:: In order to obtain the current execution counter for the purposes of - displaying input prompts, frontends simply make an execution request with an + displaying input prompts, frontends may make an execution request with an empty code string and ``silent=True``. -Execution semantics -~~~~~~~~~~~~~~~~~~~ - -When the silent flag is false, the execution of use code consists of the -following phases (in silent mode, only the ``code`` field is executed): - -1. Run the ``pre_runcode_hook``. - -2. Execute the ``code`` field, see below for details. - -3. If #2 succeeds, compute ``user_variables`` and ``user_expressions`` are - computed. This ensures that any error in the latter don't harm the main - code execution. - -4. Call any method registered with :meth:`register_post_execute`. - -.. warning:: - - The API for running code before/after the main code block is likely to - change soon. Both the ``pre_runcode_hook`` and the - :meth:`register_post_execute` are susceptible to modification, as we find a - consistent model for both. - -To understand how the ``code`` field is executed, one must know that Python -code can be compiled in one of three modes (controlled by the ``mode`` argument -to the :func:`compile` builtin): - -*single* - Valid for a single interactive statement (though the source can contain - multiple lines, such as a for loop). When compiled in this mode, the - generated bytecode contains special instructions that trigger the calling of - :func:`sys.displayhook` for any expression in the block that returns a value. - This means that a single statement can actually produce multiple calls to - :func:`sys.displayhook`, if for example it contains a loop where each - iteration computes an unassigned expression would generate 10 calls:: - - for i in range(10): - i**2 - -*exec* - An arbitrary amount of source code, this is how modules are compiled. - :func:`sys.displayhook` is *never* implicitly called. - -*eval* - A single expression that returns a value. :func:`sys.displayhook` is *never* - implicitly called. - - -The ``code`` field is split into individual blocks each of which is valid for -execution in 'single' mode, and then: - -- If there is only a single block: it is executed in 'single' mode. - -- If there is more than one block: - - * if the last one is a single line long, run all but the last in 'exec' mode - and the very last one in 'single' mode. This makes it easy to type simple - expressions at the end to see computed values. - - * if the last one is no more than two lines long, run all but the last in - 'exec' mode and the very last one in 'single' mode. This makes it easy to - type simple expressions at the end to see computed values. - otherwise - (last one is also multiline), run all in 'exec' mode - - * otherwise (last one is also multiline), run all in 'exec' mode as a single - unit. - -Any error in retrieving the ``user_variables`` or evaluating the -``user_expressions`` will result in a simple error message in the return fields -of the form:: - - [ERROR] ExceptionType: Exception message - -The user can simply send the same variable name or expression for evaluation to -see a regular traceback. - -Errors in any registered post_execute functions are also reported similarly, -and the failing function is removed from the post_execution set so that it does -not continue triggering failures. - Upon completion of the execution request, the kernel *always* sends a reply, with a status code indicating what happened and additional data depending on the outcome. See :ref:`below <execution_results>` for the possible return codes and associated data. +.. seealso:: + + :ref:`execution_semantics` .. _execution_counter: -Execution counter (old prompt number) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Execution counter (prompt number) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The kernel has a single, monotonically increasing counter of all execution -requests that are made with ``store_history=True``. This counter is used to populate -the ``In[n]``, ``Out[n]`` and ``_n`` variables, so clients will likely want to -display it in some form to the user, which will typically (but not necessarily) -be done in the prompts. The value of this counter will be returned as the -``execution_count`` field of all ``execute_reply`` and ``pyin`` messages. +The kernel should have a single, monotonically increasing counter of all execution +requests that are made with ``store_history=True``. This counter is used to populate +the ``In[n]`` and ``Out[n]`` prompts. The value of this counter will be returned as the +``execution_count`` field of all ``execute_reply`` and ``execute_input`` messages. .. _execution_results: @@ -444,15 +362,18 @@ When status is 'ok', the following extra fields are present:: # which is a string classifying the payload (e.g. 'pager'). 'payload' : list(dict), - # Results for the user_variables and user_expressions. - 'user_variables' : dict, + # Results for the user_expressions. 'user_expressions' : dict, } +.. versionchanged:: 5.0 + + ``user_variables`` is removed, use user_expressions instead. + .. admonition:: Execution payloads The notion of an 'execution payload' is different from a return value of a - given set of code, which normally is just displayed on the pyout stream + given set of code, which normally is just displayed on the execute_result stream through the PUB socket. The idea of a payload is to allow special types of code, typically magics, to populate a data container in the IPython kernel that will be shipped back to the caller via this channel. The kernel @@ -489,145 +410,82 @@ When status is 'abort', there are for now no additional data fields. This happens when the kernel was interrupted by a signal. -Object information ------------------- +Introspection +------------- -One of IPython's most used capabilities is the introspection of Python objects -in the user's namespace, typically invoked via the ``?`` and ``??`` characters -(which in reality are shorthands for the ``%pinfo`` magic). This is used often -enough that it warrants an explicit message type, especially because frontends -may want to get object information in response to user keystrokes (like Tab or -F1) besides from the user explicitly typing code like ``x??``. +Code can be inspected to show useful information to the user. +It is up to the Kernel to decide what information should be displayed, and its formatting. -Message type: ``object_info_request``:: +Message type: ``inspect_request``:: content = { - # The (possibly dotted) name of the object to be searched in all - # relevant namespaces - 'oname' : str, + # The code context in which introspection is requested + # this may be up to an entire multiline cell. + 'code' : str, + + # The cursor position within 'code' (in unicode characters) where inspection is requested + 'cursor_pos' : int, - # The level of detail desired. The default (0) is equivalent to typing + # The level of detail desired. In IPython, the default (0) is equivalent to typing # 'x?' at the prompt, 1 is equivalent to 'x??'. - 'detail_level' : int, + # The difference is up to kernels, but in IPython level 1 includes the source code + # if available. + 'detail_level' : 0 or 1, } -The returned information will be a dictionary with keys very similar to the -field names that IPython prints at the terminal. - -Message type: ``object_info_reply``:: +.. versionchanged:: 5.0 + + ``object_info_request`` renamed to ``inspect_request``. + +.. versionchanged:: 5.0 + + ``name`` key replaced with ``code`` and ``cursor_pos``, + moving the lexing responsibility to the kernel. + +The reply is a mime-bundle, like a `display_data`_ message, +which should be a formatted representation of information about the context. +In the notebook, this is used to show tooltips over function calls, etc. + +Message type: ``inspect_reply``:: content = { - # The name the object was requested under - 'name' : str, - - # Boolean flag indicating whether the named object was found or not. If - # it's false, all other fields will be empty. - 'found' : bool, - - # Flags for magics and system aliases - 'ismagic' : bool, - 'isalias' : bool, - - # The name of the namespace where the object was found ('builtin', - # 'magics', 'alias', 'interactive', etc.) - 'namespace' : str, - - # The type name will be type.__name__ for normal Python objects, but it - # can also be a string like 'Magic function' or 'System alias' - 'type_name' : str, - - # The string form of the object, possibly truncated for length if - # detail_level is 0 - 'string_form' : str, - - # For objects with a __class__ attribute this will be set - 'base_class' : str, - - # For objects with a __len__ attribute this will be set - 'length' : int, - - # If the object is a function, class or method whose file we can find, - # we give its full path - 'file' : str, - - # For pure Python callable objects, we can reconstruct the object - # definition line which provides its call signature. For convenience this - # is returned as a single 'definition' field, but below the raw parts that - # compose it are also returned as the argspec field. - 'definition' : str, - - # The individual parts that together form the definition string. Clients - # with rich display capabilities may use this to provide a richer and more - # precise representation of the definition line (e.g. by highlighting - # arguments based on the user's cursor position). For non-callable - # objects, this field is empty. - 'argspec' : { # The names of all the arguments - args : list, - # The name of the varargs (*args), if any - varargs : str, - # The name of the varkw (**kw), if any - varkw : str, - # The values (as strings) of all default arguments. Note - # that these must be matched *in reverse* with the 'args' - # list above, since the first positional args have no default - # value at all. - defaults : list, - }, - - # For instances, provide the constructor signature (the definition of - # the __init__ method): - 'init_definition' : str, - - # Docstrings: for any object (function, method, module, package) with a - # docstring, we show it. But in addition, we may provide additional - # docstrings. For example, for instances we will show the constructor - # and class docstrings as well, if available. - 'docstring' : str, - - # For instances, provide the constructor and class docstrings - 'init_docstring' : str, - 'class_docstring' : str, - - # If it's a callable object whose call method has a separate docstring and - # definition line: - 'call_def' : str, - 'call_docstring' : str, - - # If detail_level was 1, we also try to find the source code that - # defines the object, if possible. The string 'None' will indicate - # that no source was found. - 'source' : str, + # 'ok' if the request succeeded or 'error', with error information as in all other replies. + 'status' : 'ok', + + # data can be empty if nothing is found + 'data' : dict, + 'metadata' : dict, } - -Complete --------- +.. versionchanged:: 5.0 + + ``object_info_reply`` renamed to ``inspect_reply``. + +.. versionchanged:: 5.0 + + Reply is changed from structured data to a mime bundle, allowing formatting decisions to be made by the kernel. + +Completion +---------- Message type: ``complete_request``:: content = { - # The text to be completed, such as 'a.is' - # this may be an empty string if the frontend does not do any lexing, - # in which case the kernel must figure out the completion - # based on 'line' and 'cursor_pos'. - 'text' : str, - - # The full line, such as 'print a.is'. This allows completers to - # make decisions that may require information about more than just the - # current word. - 'line' : str, - - # The entire block of text where the line is. This may be useful in the - # case of multiline completions where more context may be needed. Note: if - # in practice this field proves unnecessary, remove it to lighten the - # messages. - - 'block' : str or null/None, - - # The position of the cursor where the user hit 'TAB' on the line. + # The code context in which completion is requested + # this may be up to an entire multiline cell, such as + # 'foo = a.isal' + 'code' : str, + + # The cursor position within 'code' (in unicode characters) where completion is requested 'cursor_pos' : int, } +.. versionchanged:: 5.0 + + ``line``, ``block``, and ``text`` keys are removed in favor of a single ``code`` for context. + Lexing is up to the kernel. + + Message type: ``complete_reply``:: content = { @@ -635,11 +493,13 @@ Message type: ``complete_reply``:: # ['a.isalnum', 'a.isalpha'] for the above example. 'matches' : list, - # the substring of the matched text - # this is typically the common prefix of the matches, - # and the text that is already in the block that would be replaced by the full completion. - # This would be 'a.is' in the above example. - 'matched_text' : str, + # The range of text that should be replaced by the above matches when a completion is accepted. + # typically cursor_end is the same as cursor_pos in the request. + 'cursor_start' : int, + 'cursor_end' : int, + + # Information that frontend plugins might use for extra display information about completions. + 'metadata' : dict, # status should be 'ok' unless an exception was raised during the request, # in which case it should be 'error', along with the usual error message content @@ -647,7 +507,12 @@ Message type: ``complete_reply``:: 'status' : 'ok' } - +.. versionchanged:: 5.0 + + - ``matched_text`` is removed in favor of ``cursor_start`` and ``cursor_end``. + - ``metadata`` is added for extended information. + + History ------- @@ -743,30 +608,48 @@ Message type: ``kernel_info_request``:: Message type: ``kernel_info_reply``:: content = { - # Version of messaging protocol (mandatory). + # Version of messaging protocol. # The first integer indicates major version. It is incremented when # there is any backward incompatible change. # The second integer indicates minor version. It is incremented when # there is any backward compatible change. - 'protocol_version': [int, int], + 'protocol_version': 'X.Y.Z', - # IPython version number (optional). - # Non-python kernel backend may not have this version number. - # The last component is an extra field, which may be 'dev' or - # 'rc1' in development version. It is an empty string for - # released version. - 'ipython_version': [int, int, int, str], + # The kernel implementation name + # (e.g. 'ipython' for the IPython kernel) + 'implementation': str, - # Language version number (mandatory). - # It is Python version number (e.g., [2, 7, 3]) for the kernel - # included in IPython. - 'language_version': [int, ...], + # Implementation version number. + # The version number of the kernel's implementation + # (e.g. IPython.__version__ for the IPython kernel) + 'implementation_version': 'X.Y.Z', - # Programming language in which kernel is implemented (mandatory). + # Programming language in which kernel is implemented. # Kernel included in IPython returns 'python'. 'language': str, + + # Language version number. + # It is Python version number (e.g., '2.7.3') for the kernel + # included in IPython. + 'language_version': 'X.Y.Z', + + # A banner of information about the kernel, + # which may be desplayed in console environments. + 'banner' : str, } +.. versionchanged:: 5.0 + + Versions changed from lists of integers to strings. + +.. versionchanged:: 5.0 + + ``ipython_version`` is removed. + +.. versionchanged:: 5.0 + + ``implementation``, ``implementation_version``, and ``banner`` keys are added. + Kernel shutdown --------------- @@ -834,6 +717,14 @@ frontend to decide which to use and how. A single message should contain all possible representations of the same information. Each representation should be a JSON'able data structure, and should be a valid MIME type. +Some questions remain about this design: + +* Do we use this message type for execute_result/displayhook? Probably not, because + the displayhook also has to handle the Out prompt display. On the other hand + we could put that information into the metadata section. + +.. _display_data: + Message type: ``display_data``:: content = { @@ -861,7 +752,7 @@ with a reasonably unique name to avoid conflicts. The only metadata keys currently defined in IPython are the width and height of images:: - 'metadata' : { + metadata = { 'image/png' : { 'width': 640, 'height': 480 @@ -869,6 +760,12 @@ of images:: } +.. versionchanged:: 5.0 + + `application/json` data should be unpacked JSON data, + not double-serialized as a JSON string. + + Raw Data Publication -------------------- @@ -888,11 +785,11 @@ Message type: ``data_pub``:: content = { # the keys of the data dict, after it has been unserialized - keys = ['a', 'b'] + 'keys' : ['a', 'b'] } # the namespace dict will be serialized in the message buffers, # which will have a length of at least one - buffers = ['pdict', ...] + buffers = [b'pdict', ...] The interpretation of a sequence of data_pub messages for a given parent request should be @@ -906,15 +803,15 @@ to update a single namespace with subsequent results. of which the Client can then publish *representations* via ``display_data`` to various frontends. -Python inputs -------------- +Code inputs +----------- To let all frontends know what code is being executed at any given time, these messages contain a re-broadcast of the ``code`` portion of an :ref:`execute_request <execute>`, along with the :ref:`execution_count <execution_counter>`. -Message type: ``pyin``:: +Message type: ``execute_input``:: content = { 'code' : str, # Source code to be executed, one or more lines @@ -925,32 +822,25 @@ Message type: ``pyin``:: 'execution_count' : int } -Python outputs --------------- +.. versionchanged:: 5.0 + + ``pyin`` is renamed to ``execute_input``. + + +Execution results +----------------- -When Python produces output from code that has been compiled in with the -'single' flag to :func:`compile`, any expression that produces a value (such as -``1+1``) is passed to ``sys.displayhook``, which is a callable that can do with -this value whatever it wants. The default behavior of ``sys.displayhook`` in -the Python interactive prompt is to print to ``sys.stdout`` the :func:`repr` of -the value as long as it is not ``None`` (which isn't printed at all). In our -case, the kernel instantiates as ``sys.displayhook`` an object which has -similar behavior, but which instead of printing to stdout, broadcasts these -values as ``pyout`` messages for clients to display appropriately. - -IPython's displayhook can handle multiple simultaneous formats depending on its -configuration. The default pretty-printed repr text is always given with the -``data`` entry in this message. Any other formats are provided in the -``extra_formats`` list. Frontends are free to display any or all of these -according to its capabilities. ``extra_formats`` list contains 3-tuples of an ID -string, a type string, and the data. The ID is unique to the formatter -implementation that created the data. Frontends will typically ignore the ID -unless if it has requested a particular formatter. The type string tells the -frontend how to interpret the data. It is often, but not always a MIME type. -Frontends should ignore types that it does not understand. The data itself is +Results of an execution are published as an ``execute_result``. +These are identical to `display_data`_ messages, with the addition of an ``execution_count`` key. + +Results can have multiple simultaneous formats depending on its +configuration. A plain text representation should always be provided +in the ``text/plain`` mime-type. Frontends are free to display any or all of these +according to its capabilities. +Frontends should ignore mime-types they do not understand. The data itself is any JSON object and depends on the format. It is often, but not always a string. -Message type: ``pyout``:: +Message type: ``execute_result``:: content = { @@ -958,26 +848,30 @@ Message type: ``pyout``:: # display it, since IPython automatically creates variables called _N # (for prompt N). 'execution_count' : int, - + # data and metadata are identical to a display_data message. # the object being displayed is that passed to the display hook, # i.e. the *result* of the execution. 'data' : dict, 'metadata' : dict, } - -Python errors -------------- + +Execution errors +---------------- When an error occurs during code execution -Message type: ``pyerr``:: +Message type: ``error``:: content = { # Similar content to the execute_reply messages for the 'error' case, # except the 'status' field is omitted. } +.. versionchanged:: 5.0 + + ``pyerr`` renamed to ``error`` + Kernel status ------------- @@ -1009,8 +903,8 @@ Message type: ``clear_output``:: .. versionchanged:: 4.1 - 'stdout', 'stderr', and 'display' boolean keys for selective clearing are removed, - and 'wait' is added. + ``stdout``, ``stderr``, and ``display`` boolean keys for selective clearing are removed, + and ``wait`` is added. The selective clearing keys are ignored in v4 and the default behavior remains the same, so v4 clear_output messages will be safely handled by a v4.1 frontend. @@ -1028,12 +922,25 @@ the ``raw_input(prompt)`` call. Message type: ``input_request``:: - content = { 'prompt' : str } + content = { + # the text to show at the prompt + 'prompt' : str, + # Is the request for a password? + # If so, the frontend shouldn't echo input. + 'password' : bool + } Message type: ``input_reply``:: content = { 'value' : str } - + + +When ``password`` is True, the frontend should not echo the input as it is entered. + +.. versionchanged:: 5.0 + + ``password`` key added. + .. note:: The stdin socket of the client is required to have the same zmq IDENTITY @@ -1053,34 +960,13 @@ Message type: ``input_reply``:: transported over the zmq connection), raw ``stdin`` isn't expected to be available. - + Heartbeat for kernels ===================== -Initially we had considered using messages like those above over ZMQ for a -kernel 'heartbeat' (a way to detect quickly and reliably whether a kernel is -alive at all, even if it may be busy executing user code). But this has the -problem that if the kernel is locked inside extension code, it wouldn't execute -the python heartbeat code. But it turns out that we can implement a basic -heartbeat with pure ZMQ, without using any Python messaging at all. - -The monitor sends out a single zmq message (right now, it is a str of the -monitor's lifetime in seconds), and gets the same message right back, prefixed -with the zmq identity of the DEALER socket in the heartbeat process. This can be -a uuid, or even a full message, but there doesn't seem to be a need for packing -up a message when the sender and receiver are the exact same Python object. - -The model is this:: +Clients send ping messages on a REQ socket, which are echoed right back +from the Kernel's REP socket. These are simple bytestrings, not full JSON messages described above. - monitor.send(str(self.lifetime)) # '1.2345678910' - -and the monitor receives some number of messages of the form:: - - ['uuid-abcd-dead-beef', '1.2345678910'] - -where the first part is the zmq.IDENTITY of the heart's DEALER on the engine, and -the rest is the message sent by the monitor. No Python code ever has any -access to the message between the monitor's send, and the monitor's recv. Custom Messages =============== @@ -1156,15 +1042,11 @@ handlers should set the parent header and publish status busy / idle, just like an execute request. -ToDo -==== +To Do +===== Missing things include: * Important: finish thinking through the payload concept and API. -* Important: ensure that we have a good solution for magics like %edit. It's - likely that with the payload concept we can build a full solution, but not - 100% clear yet. - .. include:: ../links.txt