From 889e008b4a45fae40919e5cea397fecfc9d47399 2011-06-21 22:53:55 From: Evan Patterson Date: 2011-06-21 22:53:55 Subject: [PATCH] Merge pull request #526 from epatters/append-before-prompt Handle asynchronous output in Qt console --- diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py index f343045..560afd5 100644 --- a/IPython/frontend/qt/console/console_widget.py +++ b/IPython/frontend/qt/console/console_widget.py @@ -65,13 +65,16 @@ class ConsoleWidget(Configurable, QtGui.QWidget): """ ) gui_completion = Bool(False, config=True, - help="Use a list widget instead of plain text output for tab completion." + help=""" + Use a list widget instead of plain text output for tab completion. + """ ) # NOTE: this value can only be specified during initialization. kind = Enum(['plain', 'rich'], default_value='plain', config=True, help=""" - The type of underlying text widget to use. Valid values are 'plain', which - specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit. + The type of underlying text widget to use. Valid values are 'plain', + which specifies a QPlainTextEdit, and 'rich', which specifies a + QTextEdit. """ ) # NOTE: this value can only be specified during initialization. @@ -84,7 +87,8 @@ class ConsoleWidget(Configurable, QtGui.QWidget): 'hsplit' : When paging is requested, the widget is split horizontally. The top pane contains the console, and the bottom pane contains the paged text. - 'vsplit' : Similar to 'hsplit', except that a vertical splitter used. + 'vsplit' : Similar to 'hsplit', except that a vertical splitter + used. 'custom' : No action is taken by the widget beyond emitting a 'custom_page_requested(str)' signal. 'none' : The text is written directly to the console. @@ -195,6 +199,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): # Initialize protected variables. Some variables contain useful state # information for subclasses; they should be considered read-only. + self._append_before_prompt_pos = 0 self._ansi_processor = QtAnsiCodeProcessor() self._completion_widget = CompletionWidget(self._control) self._continuation_prompt = '> ' @@ -507,7 +512,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): """ self._html_exporter.export() - def _get_input_buffer(self): + def _get_input_buffer(self, force=False): """ The text that the user has entered entered at the current prompt. If the console is currently executing, the text that is executing will @@ -515,7 +520,7 @@ class ConsoleWidget(Configurable, QtGui.QWidget): """ # If we're executing, the input buffer may not even exist anymore due to # the limit imposed by 'buffer_size'. Therefore, we store it. - if self._executing: + if self._executing and not force: return self._input_buffer_executing cursor = self._get_end_cursor() @@ -718,36 +723,47 @@ class ConsoleWidget(Configurable, QtGui.QWidget): # 'ConsoleWidget' protected interface #-------------------------------------------------------------------------- - def _append_html(self, html): - """ Appends html at the end of the console buffer. + def _append_custom(self, insert, input, before_prompt=False): + """ A low-level method for appending content to the end of the buffer. + + If 'before_prompt' is enabled, the content will be inserted before the + current prompt, if there is one. """ - cursor = self._get_end_cursor() - self._insert_html(cursor, html) + # Determine where to insert the content. + cursor = self._control.textCursor() + if before_prompt and not self._executing: + cursor.setPosition(self._append_before_prompt_pos) + else: + cursor.movePosition(QtGui.QTextCursor.End) + start_pos = cursor.position() - def _append_html_fetching_plain_text(self, html): - """ Appends 'html', then returns the plain text version of it. - """ - cursor = self._get_end_cursor() - return self._insert_html_fetching_plain_text(cursor, html) + # Perform the insertion. + result = insert(cursor, input) + + # Adjust the prompt position if we have inserted before it. This is safe + # because buffer truncation is disabled when not executing. + if before_prompt and not self._executing: + diff = cursor.position() - start_pos + self._append_before_prompt_pos += diff + self._prompt_pos += diff + + return result - def _append_plain_text(self, text): - """ Appends plain text at the end of the console buffer, processing - ANSI codes if enabled. + def _append_html(self, html, before_prompt=False): + """ Appends HTML at the end of the console buffer. """ - cursor = self._get_end_cursor() - self._insert_plain_text(cursor, text) + self._append_custom(self._insert_html, html, before_prompt) - def _append_plain_text_keeping_prompt(self, text): - """ Writes 'text' after the current prompt, then restores the old prompt - with its old input buffer. + def _append_html_fetching_plain_text(self, html, before_prompt=False): + """ Appends HTML, then returns the plain text version of it. """ - input_buffer = self.input_buffer - self._append_plain_text('\n') - self._prompt_finished() + return self._append_custom(self._insert_html_fetching_plain_text, + html, before_prompt) - self._append_plain_text(text) - self._show_prompt() - self.input_buffer = input_buffer + def _append_plain_text(self, text, before_prompt=False): + """ Appends plain text, processing ANSI codes if enabled. + """ + self._append_custom(self._insert_plain_text, text, before_prompt) def _cancel_text_completion(self): """ If text completion is progress, cancel it. @@ -1616,7 +1632,8 @@ class ConsoleWidget(Configurable, QtGui.QWidget): self._control.setReadOnly(False) self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True) - self._executing = False + if not self._reading: + self._executing = False self._prompt_started_hook() # If the input buffer has changed while executing, load it. @@ -1659,11 +1676,11 @@ class ConsoleWidget(Configurable, QtGui.QWidget): self._reading_callback = None while self._reading: QtCore.QCoreApplication.processEvents() - return self.input_buffer.rstrip('\n') + return self._get_input_buffer(force=True).rstrip('\n') else: self._reading_callback = lambda: \ - callback(self.input_buffer.rstrip('\n')) + callback(self._get_input_buffer(force=True).rstrip('\n')) def _set_continuation_prompt(self, prompt, html=False): """ Sets the continuation prompt. @@ -1716,14 +1733,16 @@ class ConsoleWidget(Configurable, QtGui.QWidget): If set, a new line will be written before showing the prompt if there is not already a newline at the end of the buffer. """ + # Save the current end position to support _append*(before_prompt=True). + cursor = self._get_end_cursor() + self._append_before_prompt_pos = cursor.position() + # Insert a preliminary newline, if necessary. - if newline: - cursor = self._get_end_cursor() - if cursor.position() > 0: - cursor.movePosition(QtGui.QTextCursor.Left, - QtGui.QTextCursor.KeepAnchor) - if cursor.selection().toPlainText() != '\n': - self._append_plain_text('\n') + if newline and cursor.position() > 0: + cursor.movePosition(QtGui.QTextCursor.Left, + QtGui.QTextCursor.KeepAnchor) + if cursor.selection().toPlainText() != '\n': + self._append_plain_text('\n') # Write the prompt. self._append_plain_text(self._prompt_sep) diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 9b6b6df..c21b584 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -22,8 +22,7 @@ from pygments_highlighter import PygmentsHighlighter class FrontendHighlighter(PygmentsHighlighter): - """ A PygmentsHighlighter that can be turned on and off and that ignores - prompts. + """ A PygmentsHighlighter that understands and ignores prompts. """ def __init__(self, frontend): @@ -50,14 +49,12 @@ class FrontendHighlighter(PygmentsHighlighter): else: prompt = self._frontend._continuation_prompt - # Don't highlight the part of the string that contains the prompt. + # Only highlight if we can identify a prompt, but make sure not to + # highlight the prompt. if string.startswith(prompt): self._current_offset = len(prompt) string = string[len(prompt):] - else: - self._current_offset = 0 - - PygmentsHighlighter.highlightBlock(self, string) + super(FrontendHighlighter, self).highlightBlock(string) def rehighlightBlock(self, block): """ Reimplemented to temporarily enable highlighting if disabled. @@ -71,7 +68,7 @@ class FrontendHighlighter(PygmentsHighlighter): """ Reimplemented to highlight selectively. """ start += self._current_offset - PygmentsHighlighter.setFormat(self, start, count, format) + super(FrontendHighlighter, self).setFormat(start, count, format) class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): @@ -372,14 +369,8 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ Handle display hook output. """ if not self._hidden and self._is_from_this_session(msg): - data = msg['content']['data'] - if isinstance(data, basestring): - # plaintext data from pure Python kernel - text = data - else: - # formatted output from DisplayFormatter (IPython kernel) - text = data.get('text/plain', '') - self._append_plain_text(text + '\n') + text = msg['content']['data'] + self._append_plain_text(text + '\n', before_prompt=True) def _handle_stream(self, msg): """ Handle stdout, stderr, and stdin. @@ -390,7 +381,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): # widget's tab width. text = msg['content']['data'].expandtabs(8) - self._append_plain_text(text) + self._append_plain_text(text, before_prompt=True) self._control.moveCursor(QtGui.QTextCursor.End) def _handle_shutdown_reply(self, msg): diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index a2c12b3..900396d 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -62,16 +62,16 @@ class IPythonWidget(FrontendWidget): editor = Unicode(default_editor, config=True, help=""" A command for invoking a system text editor. If the string contains a - {filename} format specifier, it will be used. Otherwise, the filename will - be appended to the end the command. + {filename} format specifier, it will be used. Otherwise, the filename + will be appended to the end the command. """) editor_line = Unicode(config=True, help=""" The editor command to use when a specific line number is requested. The string should contain two format specifiers: {line} and {filename}. If - this parameter is not specified, the line number option to the %edit magic - will be ignored. + this parameter is not specified, the line number option to the %edit + magic will be ignored. """) style_sheet = Unicode(config=True, @@ -85,8 +85,9 @@ class IPythonWidget(FrontendWidget): syntax_style = Str(config=True, help=""" - If not empty, use this Pygments style for syntax highlighting. Otherwise, - the style sheet is queried for Pygments style information. + If not empty, use this Pygments style for syntax highlighting. + Otherwise, the style sheet is queried for Pygments style + information. """) # Prompts. @@ -187,20 +188,20 @@ class IPythonWidget(FrontendWidget): prompt_number = content['execution_count'] data = content['data'] if data.has_key('text/html'): - self._append_plain_text(self.output_sep) - self._append_html(self._make_out_prompt(prompt_number)) + self._append_plain_text(self.output_sep, True) + self._append_html(self._make_out_prompt(prompt_number), True) html = data['text/html'] - self._append_plain_text('\n') - self._append_html(html + self.output_sep2) + self._append_plain_text('\n', True) + self._append_html(html + self.output_sep2, True) elif data.has_key('text/plain'): - self._append_plain_text(self.output_sep) - self._append_html(self._make_out_prompt(prompt_number)) + self._append_plain_text(self.output_sep, True) + self._append_html(self._make_out_prompt(prompt_number), True) text = data['text/plain'] # If the repr is multiline, make sure we start on a new line, # so that its lines are aligned. if "\n" in text and not self.output_sep.endswith("\n"): - self._append_plain_text('\n') - self._append_plain_text(text + self.output_sep2) + self._append_plain_text('\n', True) + self._append_plain_text(text + self.output_sep2, True) def _handle_display_data(self, msg): """ The base handler for the ``display_data`` message. @@ -216,19 +217,19 @@ class IPythonWidget(FrontendWidget): # representation. if data.has_key('text/html'): html = data['text/html'] - self._append_html(html) + self._append_html(html, True) elif data.has_key('text/plain'): text = data['text/plain'] - self._append_plain_text(text) + self._append_plain_text(text, True) # This newline seems to be needed for text and html output. - self._append_plain_text(u'\n') + self._append_plain_text(u'\n', True) def _started_channels(self): """ Reimplemented to make a history request. """ super(IPythonWidget, self)._started_channels() - self.kernel_manager.shell_channel.history(hist_access_type='tail', n=1000) - + self.kernel_manager.shell_channel.history(hist_access_type='tail', + n=1000) #--------------------------------------------------------------------------- # 'ConsoleWidget' public interface #--------------------------------------------------------------------------- @@ -413,8 +414,8 @@ class IPythonWidget(FrontendWidget): self.custom_edit_requested.emit(filename, line) elif not self.editor: self._append_plain_text('No default editor available.\n' - 'Specify a GUI text editor in the `IPythonWidget.editor` configurable\n' - 'to enable the %edit magic') + 'Specify a GUI text editor in the `IPythonWidget.editor` ' + 'configurable to enable the %edit magic') else: try: filename = '"%s"' % filename diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index d416b62..771c004 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -74,20 +74,17 @@ class RichIPythonWidget(IPythonWidget): prompt_number = content['execution_count'] data = content['data'] if data.has_key('image/svg+xml'): - self._append_plain_text(self.output_sep) - self._append_html(self._make_out_prompt(prompt_number)) - # TODO: try/except this call. - self._append_svg(data['image/svg+xml']) - self._append_html(self.output_sep2) + self._append_plain_text(self.output_sep, True) + self._append_html(self._make_out_prompt(prompt_number), True) + self._append_svg(data['image/svg+xml'], True) + self._append_html(self.output_sep2, True) elif data.has_key('image/png'): - self._append_plain_text(self.output_sep) - self._append_html(self._make_out_prompt(prompt_number)) + self._append_plain_text(self.output_sep, True) + self._append_html(self._make_out_prompt(prompt_number), True) # This helps the output to look nice. - self._append_plain_text('\n') - # TODO: try/except these calls - png = decodestring(data['image/png']) - self._append_png(png) - self._append_html(self.output_sep2) + self._append_plain_text('\n', True) + self._append_png(decodestring(data['image/png']), True) + self._append_html(self.output_sep2, True) else: # Default back to the plain text representation. return super(RichIPythonWidget, self)._handle_pyout(msg) @@ -103,14 +100,12 @@ class RichIPythonWidget(IPythonWidget): # FIXME: Is this the right ordering of things to try? if data.has_key('image/svg+xml'): svg = data['image/svg+xml'] - # TODO: try/except this call. - self._append_svg(svg) + self._append_svg(svg, True) elif data.has_key('image/png'): - # TODO: try/except these calls # PNG data is base64 encoded as it passes over the network # in a JSON structure so we decode it. png = decodestring(data['image/png']) - self._append_png(png) + self._append_png(png, True) else: # Default back to the plain text representation. return super(RichIPythonWidget, self)._handle_display_data(msg) @@ -119,35 +114,15 @@ class RichIPythonWidget(IPythonWidget): # 'RichIPythonWidget' protected interface #--------------------------------------------------------------------------- - def _append_svg(self, svg): - """ Append raw svg data to the widget. + def _append_png(self, png, before_prompt=False): + """ Append raw PNG data to the widget. """ - try: - image = svg_to_image(svg) - except ValueError: - self._append_plain_text('Received invalid plot data.') - else: - format = self._add_image(image) - self._name_to_svg_map[format.name()] = svg - cursor = self._get_end_cursor() - cursor.insertBlock() - cursor.insertImage(format) - cursor.insertBlock() + self._append_custom(self._insert_png, png, before_prompt) - def _append_png(self, png): - """ Append raw svg data to the widget. + def _append_svg(self, svg, before_prompt=False): + """ Append raw SVG data to the widget. """ - try: - image = QtGui.QImage() - image.loadFromData(png, 'PNG') - except ValueError: - self._append_plain_text('Received invalid plot data.') - else: - format = self._add_image(image) - cursor = self._get_end_cursor() - cursor.insertBlock() - cursor.insertImage(format) - cursor.insertBlock() + self._append_custom(self._insert_svg, svg, before_prompt) def _add_image(self, image): """ Adds the specified QImage to the document and returns a @@ -236,6 +211,34 @@ class RichIPythonWidget(IPythonWidget): else: return 'Unrecognized image format' + def _insert_png(self, cursor, png): + """ Insert raw PNG data into the widget. + """ + try: + image = QtGui.QImage() + image.loadFromData(png, 'PNG') + except ValueError: + self._insert_plain_text(cursor, 'Received invalid PNG data.') + else: + format = self._add_image(image) + cursor.insertBlock() + cursor.insertImage(format) + cursor.insertBlock() + + def _insert_svg(self, cursor, svg): + """ Insert raw SVG data into the widet. + """ + try: + image = svg_to_image(svg) + except ValueError: + self._insert_plain_text(cursor, 'Received invalid SVG data.') + else: + format = self._add_image(image) + self._name_to_svg_map[format.name()] = svg + cursor.insertBlock() + cursor.insertImage(format) + cursor.insertBlock() + def _save_image(self, name, format='PNG'): """ Shows a save dialog for the ImageResource with 'name'. """