diff --git a/IPython/core/history.py b/IPython/core/history.py index d24e880..c30af43 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -35,14 +35,14 @@ def magic_history(self, parameter_s = ''): for making documentation, and in conjunction with -o, for producing doctest-ready output. - -t: (default) print the 'translated' history, as IPython understands it. - IPython filters your input and converts it all into valid Python source - before executing it (things like magics or aliases are turned into - function calls, for example). With this option, you'll see the native - history instead of the user-entered version: '%cd /' will be seen as - '_ip.magic("%cd /")' instead of '%cd /'. + -r: (default) print the 'raw' history, i.e. the actual commands you typed. - -r: print the 'raw' history, i.e. the actual commands you typed. + -t: print the 'translated' history, as IPython understands it. IPython + filters your input and converts it all into valid Python source before + executing it (things like magics or aliases are turned into function + calls, for example). With this option, you'll see the native history + instead of the user-entered version: '%cd /' will be seen as + 'get_ipython().magic("%cd /")' instead of '%cd /'. -g: treat the arg as a pattern to grep for in (full) history. This includes the "shadow history" (almost all commands ever written). @@ -80,7 +80,8 @@ def magic_history(self, parameter_s = ''): elif 'r' in opts: input_hist = self.input_hist_raw else: - input_hist = self.input_hist + # Raw history is the default + input_hist = self.input_hist_raw default_length = 40 pattern = None diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index cd00bb4..ed7fd32 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -1828,7 +1828,6 @@ class InteractiveShell(Configurable, Magic): self.resetbuffer() lines = lines.splitlines() more = 0 - with nested(self.builtin_trap, self.display_trap): for line in lines: # skip blank lines so we don't mess up the prompt counter, but do @@ -1837,8 +1836,9 @@ class InteractiveShell(Configurable, Magic): if line or more: # push to raw history, so hist line numbers stay in sync - self.input_hist_raw.append("# " + line + "\n") - prefiltered = self.prefilter_manager.prefilter_lines(line,more) + self.input_hist_raw.append(line + '\n') + prefiltered = self.prefilter_manager.prefilter_lines(line, + more) more = self.push_line(prefiltered) # IPython's runsource returns None if there was an error # compiling the code. This allows us to stop processing right diff --git a/IPython/frontend/qt/console/bracket_matcher.py b/IPython/frontend/qt/console/bracket_matcher.py new file mode 100644 index 0000000..1b059ca --- /dev/null +++ b/IPython/frontend/qt/console/bracket_matcher.py @@ -0,0 +1,101 @@ +""" Provides bracket matching for Q[Plain]TextEdit widgets. +""" + +# System library imports +from PyQt4 import QtCore, QtGui + + +class BracketMatcher(QtCore.QObject): + """ Matches square brackets, braces, and parentheses based on cursor + position. + """ + + # Protected class variables. + _opening_map = { '(':')', '{':'}', '[':']' } + _closing_map = { ')':'(', '}':'{', ']':'[' } + + #-------------------------------------------------------------------------- + # 'QObject' interface + #-------------------------------------------------------------------------- + + def __init__(self, parent): + """ Create a call tip manager that is attached to the specified Qt + text edit widget. + """ + assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) + QtCore.QObject.__init__(self, parent) + + # The format to apply to matching brackets. + self.format = QtGui.QTextCharFormat() + self.format.setBackground(QtGui.QColor('silver')) + + parent.cursorPositionChanged.connect(self._cursor_position_changed) + + #-------------------------------------------------------------------------- + # Protected interface + #-------------------------------------------------------------------------- + + def _find_match(self, position): + """ Given a valid position in the text document, try to find the + position of the matching bracket. Returns -1 if unsuccessful. + """ + # Decide what character to search for and what direction to search in. + document = self.parent().document() + qchar = document.characterAt(position) + start_char = qchar.toAscii() + search_char = self._opening_map.get(start_char) + if search_char: + increment = 1 + else: + search_char = self._closing_map.get(start_char) + if search_char: + increment = -1 + else: + return -1 + + # Search for the character. + depth = 0 + while position >= 0 and position < document.characterCount(): + char = qchar.toAscii() + if char == start_char: + depth += 1 + elif char == search_char: + depth -= 1 + if depth == 0: + break + position += increment + qchar = document.characterAt(position) + else: + position = -1 + return position + + def _selection_for_character(self, position): + """ Convenience method for selecting a character. + """ + selection = QtGui.QTextEdit.ExtraSelection() + cursor = self.parent().textCursor() + cursor.setPosition(position) + cursor.movePosition(QtGui.QTextCursor.NextCharacter, + QtGui.QTextCursor.KeepAnchor) + selection.cursor = cursor + selection.format = self.format + return selection + + #------ Signal handlers ---------------------------------------------------- + + def _cursor_position_changed(self): + """ Updates the document formatting based on the new cursor position. + """ + # Clear out the old formatting. + text_edit = self.parent() + text_edit.setExtraSelections([]) + + # Attempt to match a bracket for the new cursor position. + cursor = text_edit.textCursor() + if not cursor.hasSelection(): + position = cursor.position() - 1 + match_position = self._find_match(position) + if match_position != -1: + extra_selections = [ self._selection_for_character(pos) + for pos in (position, match_position) ] + text_edit.setExtraSelections(extra_selections) diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index e8461bc..d2c4bb4 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -101,7 +101,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): QtCore.Qt.Key_N : QtCore.Qt.Key_Down, QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, } _shortcuts = set(_ctrl_down_remap.keys() + - [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ]) + [ QtCore.Qt.Key_C, QtCore.Qt.Key_V, QtCore.Qt.Key_O ]) #--------------------------------------------------------------------------- # 'QObject' interface @@ -164,18 +164,26 @@ class ConsoleWidget(Configurable, QtGui.QWidget): def eventFilter(self, obj, event): """ Reimplemented to ensure a console-like behavior in the underlying - text widget. + text widgets. """ - # Re-map keys for all filtered widgets. etype = event.type() - if etype == QtCore.QEvent.KeyPress and \ - self._control_key_down(event.modifiers()) and \ - event.key() in self._ctrl_down_remap: - new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, - self._ctrl_down_remap[event.key()], - QtCore.Qt.NoModifier) - QtGui.qApp.sendEvent(obj, new_event) - return True + if etype == QtCore.QEvent.KeyPress: + + # Re-map keys for all filtered widgets. + key = event.key() + if self._control_key_down(event.modifiers()) and \ + key in self._ctrl_down_remap: + new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, + self._ctrl_down_remap[key], + QtCore.Qt.NoModifier) + QtGui.qApp.sendEvent(obj, new_event) + return True + + elif obj == self._control: + return self._event_filter_console_keypress(event) + + elif obj == self._page_control: + return self._event_filter_page_keypress(event) # Override shortucts for all filtered widgets. Note that on Mac OS it is # always unnecessary to override shortcuts, hence the check below (users @@ -187,12 +195,6 @@ class ConsoleWidget(Configurable, QtGui.QWidget): event.accept() return False - elif etype == QtCore.QEvent.KeyPress: - if obj == self._control: - return self._event_filter_console_keypress(event) - elif obj == self._page_control: - return self._event_filter_page_keypress(event) - return super(ConsoleWidget, self).eventFilter(obj, event) #--------------------------------------------------------------------------- @@ -334,12 +336,11 @@ class ConsoleWidget(Configurable, QtGui.QWidget): else: # Do this inside an edit block so continuation prompts are # removed seamlessly via undo/redo. - cursor = self._control.textCursor() + cursor = self._get_end_cursor() cursor.beginEditBlock() - - self._append_plain_text('\n') - self._show_continuation_prompt() - + cursor.insertText('\n') + self._insert_continuation_prompt(cursor) + self._control.moveCursor(QtGui.QTextCursor.End) cursor.endEditBlock() return complete @@ -431,9 +432,8 @@ class ConsoleWidget(Configurable, QtGui.QWidget): """ Sets the font to the default fixed-width font for this platform. """ # FIXME: font family and size should be configurable by the user. - if sys.platform == 'win32': - # Fixme: we should test whether Consolas is available and use it + # FIXME: we should test whether Consolas is available and use it # first if it is. Consolas ships by default from Vista onwards, # it's *vastly* more readable and prettier than Courier, and is # often installed even on XP systems. So we should first check for @@ -632,13 +632,32 @@ class ConsoleWidget(Configurable, QtGui.QWidget): if self._in_buffer(position): cursor.movePosition(QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor) + if not cursor.hasSelection(): + # Line deletion (remove continuation prompt) + cursor.movePosition(QtGui.QTextCursor.NextBlock, + QtGui.QTextCursor.KeepAnchor) + cursor.movePosition(QtGui.QTextCursor.Right, + QtGui.QTextCursor.KeepAnchor, + len(self._continuation_prompt)) cursor.removeSelectedText() intercepted = True elif key == QtCore.Qt.Key_L: + # It would be better to simply move the prompt block to the top + # of the control viewport. QPlainTextEdit has a private method + # to do this (setTopBlock), but it cannot be duplicated here + # because it requires access to the QTextControl that underlies + # both QPlainTextEdit and QTextEdit. In short, this can only be + # achieved by appending newlines after the prompt, which is a + # gigantic hack and likely to cause other problems. self.clear() intercepted = True + elif key == QtCore.Qt.Key_O: + if self._page_control and self._page_control.isVisible(): + self._page_control.setFocus() + intercept = True + elif key == QtCore.Qt.Key_X: # FIXME: Instead of disabling cut completely, only allow it # when safe. @@ -669,16 +688,44 @@ class ConsoleWidget(Configurable, QtGui.QWidget): cursor.removeSelectedText() intercepted = True + elif key == QtCore.Qt.Key_Greater: + self._control.moveCursor(QtGui.QTextCursor.End) + intercepted = True + + elif key == QtCore.Qt.Key_Less: + self._control.setTextCursor(self._get_prompt_cursor()) + intercepted = True + else: if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): - if self._reading: - self._append_plain_text('\n') - self._reading = False - if self._reading_callback: - self._reading_callback() - elif not self._executing: - self.execute(interactive=True) intercepted = True + if self._in_buffer(position): + if self._reading: + self._append_plain_text('\n') + self._reading = False + if self._reading_callback: + self._reading_callback() + + # If there is only whitespace after the cursor, execute. + # Otherwise, split the line with a continuation prompt. + elif not self._executing: + cursor.movePosition(QtGui.QTextCursor.End, + QtGui.QTextCursor.KeepAnchor) + if cursor.selectedText().trimmed().isEmpty(): + self.execute(interactive=True) + else: + cursor.beginEditBlock() + cursor.setPosition(position) + cursor.insertText('\n') + self._insert_continuation_prompt(cursor) + + # Ensure that the whole input buffer is visible. + # FIXME: This will not be usable if the input buffer + # is taller than the console widget. + self._control.moveCursor(QtGui.QTextCursor.End) + self._control.setTextCursor(cursor) + + cursor.endEditBlock() elif key == QtCore.Qt.Key_Up: if self._reading or not self._up_pressed(): @@ -702,17 +749,20 @@ class ConsoleWidget(Configurable, QtGui.QWidget): intercepted = not self._in_buffer(position - 1) elif key == QtCore.Qt.Key_Home: - cursor.movePosition(QtGui.QTextCursor.StartOfBlock) start_line = cursor.blockNumber() if start_line == self._get_prompt_cursor().blockNumber(): start_pos = self._prompt_pos else: + cursor.movePosition(QtGui.QTextCursor.StartOfBlock, + QtGui.QTextCursor.KeepAnchor) start_pos = cursor.position() start_pos += len(self._continuation_prompt) + cursor.setPosition(position) if shift_down and self._in_buffer(position): - self._set_selection(position, start_pos) + cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor) else: - self._set_position(start_pos) + cursor.setPosition(start_pos) + self._set_cursor(cursor) intercepted = True elif key == QtCore.Qt.Key_Backspace: @@ -769,8 +819,24 @@ class ConsoleWidget(Configurable, QtGui.QWidget): interface. """ key = event.key() + ctrl_down = self._control_key_down(event.modifiers()) + alt_down = event.modifiers() & QtCore.Qt.AltModifier + + if ctrl_down: + if key == QtCore.Qt.Key_O: + self._control.setFocus() + intercept = True + + elif alt_down: + if key == QtCore.Qt.Key_Greater: + self._page_control.moveCursor(QtGui.QTextCursor.End) + intercepted = True + + elif key == QtCore.Qt.Key_Less: + self._page_control.moveCursor(QtGui.QTextCursor.Start) + intercepted = True - if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape): + elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape): if self._splitter: self._page_control.hide() else: @@ -779,7 +845,14 @@ class ConsoleWidget(Configurable, QtGui.QWidget): elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, - QtCore.Qt.Key_Down, + QtCore.Qt.Key_PageDown, + QtCore.Qt.NoModifier) + QtGui.qApp.sendEvent(self._page_control, new_event) + return True + + elif key == QtCore.Qt.Key_Backspace: + new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, + QtCore.Qt.Key_PageUp, QtCore.Qt.NoModifier) QtGui.qApp.sendEvent(self._page_control, new_event) return True @@ -964,6 +1037,15 @@ class ConsoleWidget(Configurable, QtGui.QWidget): cursor.setPosition(position) return cursor + def _insert_continuation_prompt(self, cursor): + """ Inserts new continuation prompt using the specified cursor. + """ + if self._continuation_prompt_html is None: + self._insert_plain_text(cursor, self._continuation_prompt) + else: + self._continuation_prompt = self._insert_html_fetching_plain_text( + cursor, self._continuation_prompt_html) + def _insert_html(self, cursor, html): """ Inserts HTML using the specified cursor in such a way that future formatting is unaffected. @@ -1184,18 +1266,6 @@ class ConsoleWidget(Configurable, QtGui.QWidget): """ self._control.setTextCursor(cursor) - def _set_position(self, position): - """ Convenience method to set the position of the cursor. - """ - cursor = self._control.textCursor() - cursor.setPosition(position) - self._control.setTextCursor(cursor) - - def _set_selection(self, start, end): - """ Convenience method to set the current selected text. - """ - self._control.setTextCursor(self._get_selection_cursor(start, end)) - def _show_context_menu(self, pos): """ Shows a context menu at the given QPoint (in widget coordinates). """ @@ -1259,15 +1329,6 @@ class ConsoleWidget(Configurable, QtGui.QWidget): self._prompt_pos = self._get_end_cursor().position() self._prompt_started() - def _show_continuation_prompt(self): - """ Writes a new continuation prompt at the end of the buffer. - """ - if self._continuation_prompt_html is None: - self._append_plain_text(self._continuation_prompt) - else: - self._continuation_prompt = self._append_html_fetching_plain_text( - self._continuation_prompt_html) - class HistoryConsoleWidget(ConsoleWidget): """ A ConsoleWidget that keeps a history of the commands that have been diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 658f861..31503a9 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -9,7 +9,8 @@ from PyQt4 import QtCore, QtGui # Local imports from IPython.core.inputsplitter import InputSplitter from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin -from IPython.utils.traitlets import Bool, Type +from IPython.utils.traitlets import Bool +from bracket_matcher import BracketMatcher from call_tip_widget import CallTipWidget from completion_lexer import CompletionLexer from console_widget import HistoryConsoleWidget @@ -88,8 +89,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): executed = QtCore.pyqtSignal(object) # Protected class variables. - _highlighter_class = Type(FrontendHighlighter) - _input_splitter_class = Type(InputSplitter) + _input_splitter_class = InputSplitter #--------------------------------------------------------------------------- # 'object' interface @@ -99,10 +99,11 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): super(FrontendWidget, self).__init__(*args, **kw) # FrontendWidget protected variables. + self._bracket_matcher = BracketMatcher(self._control) self._call_tip_widget = CallTipWidget(self._control) self._completion_lexer = CompletionLexer(PythonLexer()) self._hidden = False - self._highlighter = self._highlighter_class(self) + self._highlighter = FrontendHighlighter(self) self._input_splitter = self._input_splitter_class(input_mode='block') self._kernel_manager = None @@ -170,8 +171,8 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ Reimplemented to allow execution interruption. """ key = event.key() - if self._executing and self._control_key_down(event.modifiers()): - if key == QtCore.Qt.Key_C: + if self._control_key_down(event.modifiers()): + if key == QtCore.Qt.Key_C and self._executing: self._kernel_interrupt() return True elif key == QtCore.Qt.Key_Period: @@ -179,13 +180,13 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): return True return super(FrontendWidget, self)._event_filter_console_keypress(event) - def _show_continuation_prompt(self): + def _insert_continuation_prompt(self, cursor): """ Reimplemented for auto-indentation. """ - super(FrontendWidget, self)._show_continuation_prompt() + super(FrontendWidget, self)._insert_continuation_prompt(cursor) spaces = self._input_splitter.indent_spaces - self._append_plain_text('\t' * (spaces / self.tab_width)) - self._append_plain_text(' ' * (spaces % self.tab_width)) + cursor.insertText('\t' * (spaces / self.tab_width)) + cursor.insertText(' ' * (spaces % self.tab_width)) #--------------------------------------------------------------------------- # 'BaseFrontendMixin' abstract interface @@ -353,15 +354,20 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): if self.custom_restart: self.custom_restart_requested.emit() elif self.kernel_manager.has_kernel: - try: - self.kernel_manager.restart_kernel() - except RuntimeError: - message = 'Kernel started externally. Cannot restart.\n' - self._append_plain_text(message) - else: - self._stopped_channels() - self._append_plain_text('Kernel restarting...\n') - self._show_interpreter_prompt() + message = 'Are you sure you want to restart the kernel?' + buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No + result = QtGui.QMessageBox.question(self, 'Restart kernel?', + message, buttons) + if result == QtGui.QMessageBox.Yes: + try: + self.kernel_manager.restart_kernel() + except RuntimeError: + message = 'Kernel started externally. Cannot restart.\n' + self._append_plain_text(message) + else: + self._stopped_channels() + self._append_plain_text('Kernel restarting...\n') + self._show_interpreter_prompt() else: self._append_plain_text('Kernel process is either remote or ' 'unspecified. Cannot restart.\n') diff --git a/IPython/frontend/qt/console/pygments_highlighter.py b/IPython/frontend/qt/console/pygments_highlighter.py index adc4abc..c32079d 100644 --- a/IPython/frontend/qt/console/pygments_highlighter.py +++ b/IPython/frontend/qt/console/pygments_highlighter.py @@ -110,14 +110,12 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter): elif hasattr(self._lexer, '_saved_state_stack'): del self._lexer._saved_state_stack - index = 0 # Lex the text using Pygments + index = 0 for token, text in self._lexer.get_tokens(qstring): - l = len(text) - format = self._get_format(token) - if format is not None: - self.setFormat(index, l, format) - index += l + length = len(text) + self.setFormat(index, length, self._get_format(token)) + index += length if hasattr(self._lexer, '_saved_state_stack'): data = PygmentsBlockUserData( @@ -185,11 +183,9 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter): def _get_format_from_style(self, token, style): """ Returns a QTextCharFormat for token by reading a Pygments style. """ - result = None + result = QtGui.QTextCharFormat() for key, value in style.style_for_token(token).items(): if value: - if result is None: - result = QtGui.QTextCharFormat() if key == 'color': result.setForeground(self._get_brush(value)) elif key == 'bgcolor': diff --git a/IPython/zmq/entry_point.py b/IPython/zmq/entry_point.py index e8ffc4b..0ce3112 100644 --- a/IPython/zmq/entry_point.py +++ b/IPython/zmq/entry_point.py @@ -64,10 +64,8 @@ def make_kernel(namespace, kernel_factory, """ Creates a kernel. """ # Install minimal exception handling - color_scheme = 'LightBG' if sys.platform == 'darwin' else 'Linux' - sys.excepthook = FormattedTB( - mode='Verbose', color_scheme=color_scheme, ostream=sys.__stdout__ - ) + sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor', + ostream=sys.__stdout__) # Create a context, a session, and the kernel sockets. io.raw_print("Starting the kernel...") diff --git a/docs/Makefile b/docs/Makefile index 2a03b8d..dee26be 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -101,4 +101,4 @@ gitwash-update: cd source/development/gitwash && rename 's/.rst/.txt/' *.rst nightly: dist - rsync -avH --delete dist/ ipython:www/doc/nightly \ No newline at end of file + rsync -avH --delete dist/ ipython:www/doc/nightly diff --git a/docs/source/development/messaging.txt b/docs/source/development/messaging.txt index 7f9c609..82b6394 100644 --- a/docs/source/development/messaging.txt +++ b/docs/source/development/messaging.txt @@ -130,20 +130,9 @@ Messages on the XREP/XREQ socket Execute ------- -The execution request contains a single string, but this may be a multiline -string. The kernel is responsible for splitting this into possibly more than -one block and deciding whether to compile these in 'single' or 'exec' mode. -We're still sorting out this policy. The current inputsplitter is capable of -splitting the input for blocks that can all be run as 'single', but in the long -run it may prove cleaner to only use 'single' mode for truly single-line -inputs, and run all multiline input in 'exec' mode. This would preserve the -natural behavior of single-line inputs while allowing long cells to behave more -likea a script. This design will be refined as we complete the implementation. - -.. Note:: - - What today we call 'prompt requests' will be encoded in the - ``state_template`` field. +This message type is used by frontends to ask the kernel to execute code on +behalf of the user, in a namespace reserved to the user's variables (and thus +separate from the kernel's own internal code and variables). Message type: ``execute_request``:: @@ -162,25 +151,75 @@ Message type: ``execute_request``:: # The default is False. 'silent' : bool, - # An optional string to request arbitrary state information from the - # kernel. This string is evaluated via the itpl module, and it can - # therefore contain arbitrary code for execution. - - 'state_template' : str, + # A list of variable names from the user's namespace to be retrieved. What + # returns is a JSON string of the variable's repr(), not a python object. + 'user_variables' : list, + + # Similarly, a dict mapping names to expressions to be evaluated in the + # user's dict. + 'user_expressions' : dict, } +The ``code`` field contains a single string, but this may be a multiline +string. The kernel is responsible for splitting this into possibly more than +one block and deciding whether to compile these in 'single' or 'exec' mode. +We're still sorting out this policy. The current inputsplitter is capable of +splitting the input for blocks that can all be run as 'single', but in the long +run it may prove cleaner to only use 'single' mode for truly single-line +inputs, and run all multiline input in 'exec' mode. This would preserve the +natural behavior of single-line inputs while allowing long cells to behave more +likea a script. This design will be refined as we complete the implementation. + +The ``user_`` fields deserve 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 aqcuired over the network. But now that IPython has a clean separation +between the kernel and the clients, the notion of embedding 'prompt' +maninpulations into the kernel itself feels awkward. Prompts should be a +frontend-side feature, and it should be even possible for different frontends +to display different prompts while interacting with the same kernel. + +We have therefore abandoned the idea of a 'prompt string' to be evaluated by +the kernel, and instead provide the ability to retrieve from the user's +namespace information after the execution of the main ``code``, with two fields +of the execution request: + +- ``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. + +- ``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). + +.. Note:: + + In order to obtain the current execution counter for the purposes of + displaying input prompts, frontends simply make an execution request with an + empty code string and ``silent=True``. + Execution semantics - Upon execution of the ``code`` field, the kernel *always* sends a reply, - with a status code indicating what happened and additional data depending - on the outcome. + 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. + + The ``code`` field is executed first, and then the ``user_variables`` and + ``user_expressions`` are computed. This ensures that any error in the + latter don't harm the main code execution. - Any code in the ``state_template`` string is evaluated, but full exceptions - that may occur are *not* propagated back. If any error occurs during the - evaluation, the value of the string will simply be:: + 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 in : ExceptionType - Exception message] + [ERROR] ExceptionType: Exception message - The user can simply send the same code contained in the template for normal + The user can simply send the same variable name or expression for evaluation to see a regular traceback. Execution counter (old prompt number) @@ -280,7 +319,7 @@ Message type: ``getattr_request``:: content = { # The (possibly dotted) name of the attribute - 'name' : str + 'name' : str, } When a ``getattr_request`` fails, there are two possible error types: @@ -296,20 +335,20 @@ Message type: ``getattr_reply``:: content = { # One of ['ok', 'AttributeError', 'AccessError']. - 'status' : str + 'status' : str, # If status is 'ok', a JSON object. - 'value' : object + 'value' : object, } Message type: ``setattr_request``:: content = { # The (possibly dotted) name of the attribute - 'name' : str + 'name' : str, # A JSON-encoded object, that will be validated by the Traits # information in the kernel - 'value' : object + 'value' : object, } When a ``setattr_request`` fails, there are also two possible error types with @@ -319,7 +358,7 @@ Message type: ``setattr_reply``:: content = { # One of ['ok', 'AttributeError', 'AccessError']. - 'status' : str + 'status' : str, } @@ -391,13 +430,13 @@ Message type: ``object_info_reply``:: # The name of the varargs (*args), if any varargs : str, # The name of the varkw (**kw), if any - varkw : str + 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. - func_defaults : list - } + func_defaults : list, + }, # For instances, provide the constructor signature (the definition of # the __init__ method): @@ -487,6 +526,7 @@ Message type: ``history_reply``:: # respectively. 'history' : dict, } + Messages on the PUB/SUB socket ============================== @@ -539,10 +579,10 @@ Message type: ``pyout``:: # The data is typically the repr() of the object. 'data' : str, - # The prompt number for this execution is also provided so that clients - # can display it, since IPython automatically creates variables called - # _N (for prompt N). - 'prompt_number' : int, + # The counter for this execution is also provided so that clients can + # display it, since IPython automatically creates variables called _N (for + # prompt N). + 'execution_count' : int, } Python errors