From 0773317e70fe840c982a85bb93396e212de1be51 2010-08-16 18:44:33 From: epatters Date: 2010-08-16 18:44:33 Subject: [PATCH] * Moved KernelManager attribute management code in FrontendWidget into a mixin class usable in any Qt frontend. Registering handlers for message types is now trivial. * Made the Qt kernel manager signal interface more consistent with the revised message specification. --- diff --git a/IPython/frontend/qt/base_frontend_mixin.py b/IPython/frontend/qt/base_frontend_mixin.py new file mode 100644 index 0000000..3eaa759 --- /dev/null +++ b/IPython/frontend/qt/base_frontend_mixin.py @@ -0,0 +1,85 @@ +""" Defines a convenient mix-in class for implementing Qt frontends. +""" + +class BaseFrontendMixin(object): + """ A mix-in class for implementing Qt frontends. + + To handle messages of a particular type, frontends need only define an + appropriate handler method. For example, to handle 'stream' messaged, define + a '_handle_stream(msg)' method. + """ + + #--------------------------------------------------------------------------- + # 'BaseFrontendMixin' concrete interface + #--------------------------------------------------------------------------- + + def _get_kernel_manager(self): + """ Returns the current kernel manager. + """ + return self._kernel_manager + + def _set_kernel_manager(self, kernel_manager): + """ Disconnect from the current kernel manager (if any) and set a new + kernel manager. + """ + # Disconnect the old kernel manager, if necessary. + old_manager = self._kernel_manager + if old_manager is not None: + old_manager.started_channels.disconnect(self._started_channels) + old_manager.stopped_channels.disconnect(self._stopped_channels) + + # Disconnect the old kernel manager's channels. + old_manager.sub_channel.message_received.disconnect(self._dispatch) + old_manager.xreq_channel.message_received.disconnect(self._dispatch) + old_manager.rep_channel.message_received.connect(self._dispatch) + + # Handle the case where the old kernel manager is still listening. + if old_manager.channels_running: + self._stopped_channels() + + # Set the new kernel manager. + self._kernel_manager = kernel_manager + if kernel_manager is None: + return + + # Connect the new kernel manager. + kernel_manager.started_channels.connect(self._started_channels) + kernel_manager.stopped_channels.connect(self._stopped_channels) + + # Connect the new kernel manager's channels. + kernel_manager.sub_channel.message_received.connect(self._dispatch) + kernel_manager.xreq_channel.message_received.connect(self._dispatch) + kernel_manager.rep_channel.message_received.connect(self._dispatch) + + # Handle the case where the kernel manager started channels before + # we connected. + if kernel_manager.channels_running: + self._started_channels() + + kernel_manager = property(_get_kernel_manager, _set_kernel_manager) + + #--------------------------------------------------------------------------- + # 'BaseFrontendMixin' abstract interface + #--------------------------------------------------------------------------- + + def _started_channels(self): + """ Called when the KernelManager channels have started listening or + when the frontend is assigned an already listening KernelManager. + """ + + def _stopped_channels(self): + """ Called when the KernelManager channels have stopped listening or + when a listening KernelManager is removed from the frontend. + """ + + #--------------------------------------------------------------------------- + # Private interface + #--------------------------------------------------------------------------- + + def _dispatch(self, msg): + """ Call the frontend handler associated with + """ + msg_type = msg['msg_type'] + handler = getattr(self, '_handle_' + msg_type, None) + if handler: + handler(msg) diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index e16eb0b..7431e30 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -9,6 +9,7 @@ import zmq # Local imports from IPython.core.inputsplitter import InputSplitter +from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin from call_tip_widget import CallTipWidget from completion_lexer import CompletionLexer from console_widget import HistoryConsoleWidget @@ -60,7 +61,7 @@ class FrontendHighlighter(PygmentsHighlighter): PygmentsHighlighter.setFormat(self, start, count, format) -class FrontendWidget(HistoryConsoleWidget): +class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): """ A Qt frontend for a generic Python kernel. """ @@ -146,70 +147,96 @@ class FrontendWidget(HistoryConsoleWidget): return not self._complete() #--------------------------------------------------------------------------- - # 'FrontendWidget' interface + # 'BaseFrontendMixin' abstract interface #--------------------------------------------------------------------------- - def execute_file(self, path, hidden=False): - """ Attempts to execute file with 'path'. If 'hidden', no output is - shown. + def _handle_complete_reply(self, rep): + """ Handle replies for tab completion. """ - self.execute('execfile("%s")' % path, hidden=hidden) + cursor = self._get_cursor() + if rep['parent_header']['msg_id'] == self._complete_id and \ + cursor.position() == self._complete_pos: + text = '.'.join(self._get_context()) + cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) + self._complete_with_items(cursor, rep['content']['matches']) - def _get_kernel_manager(self): - """ Returns the current kernel manager. + def _handle_execute_reply(self, msg): + """ Handles replies for code execution. """ - return self._kernel_manager + if not self._hidden: + # Make sure that all output from the SUB channel has been processed + # before writing a new prompt. + self.kernel_manager.sub_channel.flush() + + content = msg['content'] + status = content['status'] + if status == 'ok': + self._process_execute_ok(msg) + elif status == 'error': + self._process_execute_error(msg) + elif status == 'abort': + self._process_execute_abort(msg) + + self._hidden = True + self._show_interpreter_prompt() + self.executed.emit(msg) + + def _handle_input_request(self, msg): + """ Handle requests for raw_input. + """ + # Make sure that all output from the SUB channel has been processed + # before entering readline mode. + self.kernel_manager.sub_channel.flush() + + def callback(line): + self.kernel_manager.rep_channel.input(line) + self._readline(msg['content']['prompt'], callback=callback) - def _set_kernel_manager(self, kernel_manager): - """ Disconnect from the current kernel manager (if any) and set a new - kernel manager. + def _handle_object_info_reply(self, rep): + """ Handle replies for call tips. """ - # Disconnect the old kernel manager, if necessary. - if self._kernel_manager is not None: - self._kernel_manager.started_channels.disconnect( - self._started_channels) - self._kernel_manager.stopped_channels.disconnect( - self._stopped_channels) - - # Disconnect the old kernel manager's channels. - sub = self._kernel_manager.sub_channel - xreq = self._kernel_manager.xreq_channel - rep = self._kernel_manager.rep_channel - sub.message_received.disconnect(self._handle_sub) - xreq.execute_reply.disconnect(self._handle_execute_reply) - xreq.complete_reply.disconnect(self._handle_complete_reply) - xreq.object_info_reply.disconnect(self._handle_object_info_reply) - rep.input_requested.disconnect(self._handle_req) - - # Handle the case where the old kernel manager is still listening. - if self._kernel_manager.channels_running: - self._stopped_channels() - - # Set the new kernel manager. - self._kernel_manager = kernel_manager - if kernel_manager is None: - return + cursor = self._get_cursor() + if rep['parent_header']['msg_id'] == self._call_tip_id and \ + cursor.position() == self._call_tip_pos: + doc = rep['content']['docstring'] + if doc: + self._call_tip_widget.show_docstring(doc) - # Connect the new kernel manager. - kernel_manager.started_channels.connect(self._started_channels) - kernel_manager.stopped_channels.connect(self._stopped_channels) - - # Connect the new kernel manager's channels. - sub = kernel_manager.sub_channel - xreq = kernel_manager.xreq_channel - rep = kernel_manager.rep_channel - sub.message_received.connect(self._handle_sub) - xreq.execute_reply.connect(self._handle_execute_reply) - xreq.complete_reply.connect(self._handle_complete_reply) - xreq.object_info_reply.connect(self._handle_object_info_reply) - rep.input_requested.connect(self._handle_req) - - # Handle the case where the kernel manager started channels before - # we connected. - if kernel_manager.channels_running: - self._started_channels() + def _handle_pyout(self, msg): + """ Handle display hook output. + """ + self._append_plain_text(msg['content']['data'] + '\n') - kernel_manager = property(_get_kernel_manager, _set_kernel_manager) + def _handle_stream(self, msg): + """ Handle stdout, stderr, and stdin. + """ + self._append_plain_text(msg['content']['data']) + self._control.moveCursor(QtGui.QTextCursor.End) + + def _started_channels(self): + """ Called when the KernelManager channels have started listening or + when the frontend is assigned an already listening KernelManager. + """ + self._reset() + self._append_plain_text(self._get_banner()) + self._show_interpreter_prompt() + + def _stopped_channels(self): + """ Called when the KernelManager channels have stopped listening or + when a listening KernelManager is removed from the frontend. + """ + # FIXME: Print a message here? + pass + + #--------------------------------------------------------------------------- + # 'FrontendWidget' interface + #--------------------------------------------------------------------------- + + def execute_file(self, path, hidden=False): + """ Attempts to execute file with 'path'. If 'hidden', no output is + shown. + """ + self.execute('execfile("%s")' % path, hidden=hidden) #--------------------------------------------------------------------------- # 'FrontendWidget' protected interface @@ -275,26 +302,32 @@ class FrontendWidget(HistoryConsoleWidget): self._append_plain_text('Kernel process is either remote or ' 'unspecified. Cannot interrupt.\n') - def _show_interpreter_prompt(self): - """ Shows a prompt for the interpreter. + def _process_execute_abort(self, msg): + """ Process a reply for an aborted execution request. """ - self._show_prompt('>>> ') - - #------ Signal handlers ---------------------------------------------------- + self._append_plain_text("ERROR: execution aborted\n") - def _started_channels(self): - """ Called when the kernel manager has started listening. + def _process_execute_error(self, msg): + """ Process a reply for an execution request that resulted in an error. """ - self._reset() - self._append_plain_text(self._get_banner()) - self._show_interpreter_prompt() + content = msg['content'] + traceback = ''.join(content['traceback']) + self._append_plain_text(traceback) - def _stopped_channels(self): - """ Called when the kernel manager has stopped listening. + def _process_execute_ok(self, msg): + """ Process a reply for a successful execution equest. """ - # FIXME: Print a message here? + # The basic FrontendWidget doesn't handle payloads, as they are a + # mechanism for going beyond the standard Python interpreter model. pass + def _show_interpreter_prompt(self): + """ Shows a prompt for the interpreter. + """ + self._show_prompt('>>> ') + + #------ Signal handlers ---------------------------------------------------- + def _document_contents_change(self, position, removed, added): """ Called whenever the document's content changes. Display a call tip if appropriate. @@ -305,72 +338,3 @@ class FrontendWidget(HistoryConsoleWidget): document = self._control.document() if position == self._get_cursor().position(): self._call_tip() - - def _handle_req(self, req): - # Make sure that all output from the SUB channel has been processed - # before entering readline mode. - self.kernel_manager.sub_channel.flush() - - def callback(line): - self.kernel_manager.rep_channel.input(line) - self._readline(req['content']['prompt'], callback=callback) - - def _handle_sub(self, omsg): - if self._hidden: - return - handler = getattr(self, '_handle_%s' % omsg['msg_type'], None) - if handler is not None: - handler(omsg) - - def _handle_pyout(self, omsg): - self._append_plain_text(omsg['content']['data'] + '\n') - - def _handle_stream(self, omsg): - self._append_plain_text(omsg['content']['data']) - self._control.moveCursor(QtGui.QTextCursor.End) - - def _handle_execute_reply(self, reply): - if self._hidden: - return - - # Make sure that all output from the SUB channel has been processed - # before writing a new prompt. - self.kernel_manager.sub_channel.flush() - - content = reply['content'] - status = content['status'] - if status == 'ok': - self._handle_execute_payload(content['payload']) - elif status == 'error': - self._handle_execute_error(reply) - elif status == 'aborted': - text = "ERROR: ABORTED\n" - self._append_plain_text(text) - - self._hidden = True - self._show_interpreter_prompt() - self.executed.emit(reply) - - def _handle_execute_error(self, reply): - content = reply['content'] - traceback = ''.join(content['traceback']) - self._append_plain_text(traceback) - - def _handle_execute_payload(self, payload): - pass - - def _handle_complete_reply(self, rep): - cursor = self._get_cursor() - if rep['parent_header']['msg_id'] == self._complete_id and \ - cursor.position() == self._complete_pos: - text = '.'.join(self._get_context()) - cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) - self._complete_with_items(cursor, rep['content']['matches']) - - def _handle_object_info_reply(self, rep): - cursor = self._get_cursor() - if rep['parent_header']['msg_id'] == self._call_tip_id and \ - cursor.position() == self._call_tip_pos: - doc = rep['content']['docstring'] - if doc: - self._call_tip_widget.show_docstring(doc) diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py index 127fc56..e938fe5 100644 --- a/IPython/frontend/qt/console/ipython_widget.py +++ b/IPython/frontend/qt/console/ipython_widget.py @@ -49,6 +49,18 @@ class IPythonWidget(FrontendWidget): self.reset_styling() #--------------------------------------------------------------------------- + # 'BaseFrontendMixin' abstract interface + #--------------------------------------------------------------------------- + + def _handle_pyout(self, msg): + """ Reimplemented for IPython-style "display hook". + """ + self._append_html(self._make_out_prompt(self._prompt_count)) + self._save_prompt_block() + + self._append_plain_text(msg['content']['data'] + '\n') + + #--------------------------------------------------------------------------- # 'FrontendWidget' interface #--------------------------------------------------------------------------- @@ -66,6 +78,21 @@ class IPythonWidget(FrontendWidget): """ return default_banner + def _process_execute_error(self, msg): + """ Reimplemented for IPython-style traceback formatting. + """ + content = msg['content'] + traceback_lines = content['traceback'][:] + traceback = ''.join(traceback_lines) + traceback = traceback.replace(' ', ' ') + traceback = traceback.replace('\n', '
') + + ename = content['ename'] + ename_styled = '%s' % ename + traceback = traceback.replace(ename, ename_styled) + + self._append_html(traceback) + def _show_interpreter_prompt(self): """ Reimplemented for IPython-style prompts. """ @@ -93,31 +120,6 @@ class IPythonWidget(FrontendWidget): self._set_continuation_prompt( self._make_continuation_prompt(self._prompt), html=True) - #------ Signal handlers ---------------------------------------------------- - - def _handle_execute_error(self, reply): - """ Reimplemented for IPython-style traceback formatting. - """ - content = reply['content'] - traceback_lines = content['traceback'][:] - traceback = ''.join(traceback_lines) - traceback = traceback.replace(' ', ' ') - traceback = traceback.replace('\n', '
') - - ename = content['ename'] - ename_styled = '%s' % ename - traceback = traceback.replace(ename, ename_styled) - - self._append_html(traceback) - - def _handle_pyout(self, omsg): - """ Reimplemented for IPython-style "display hook". - """ - self._append_html(self._make_out_prompt(self._prompt_count)) - self._save_prompt_block() - - self._append_plain_text(omsg['content']['data'] + '\n') - #--------------------------------------------------------------------------- # 'IPythonWidget' interface #--------------------------------------------------------------------------- diff --git a/IPython/frontend/qt/console/rich_ipython_widget.py b/IPython/frontend/qt/console/rich_ipython_widget.py index ff5a831..377b0f6 100644 --- a/IPython/frontend/qt/console/rich_ipython_widget.py +++ b/IPython/frontend/qt/console/rich_ipython_widget.py @@ -55,9 +55,10 @@ class RichIPythonWidget(IPythonWidget): # 'FrontendWidget' protected interface #--------------------------------------------------------------------------- - def _handle_execute_payload(self, payload): - """ Reimplemented to handle pylab plot payloads. + def _process_execute_ok(self, msg): + """ Reimplemented to handle matplotlib plot payloads. """ + payload = msg['content']['payload'] plot_payload = payload.get('plot', None) if plot_payload and plot_payload['format'] == 'svg': svg = plot_payload['data'] @@ -73,7 +74,7 @@ class RichIPythonWidget(IPythonWidget): cursor.insertImage(format) cursor.insertBlock() else: - super(RichIPythonWidget, self)._handle_execute_payload(payload) + super(RichIPythonWidget, self)._process_execute_ok(msg) #--------------------------------------------------------------------------- # 'RichIPythonWidget' protected interface diff --git a/IPython/frontend/qt/kernelmanager.py b/IPython/frontend/qt/kernelmanager.py index ee3ba64..f19e48e 100644 --- a/IPython/frontend/qt/kernelmanager.py +++ b/IPython/frontend/qt/kernelmanager.py @@ -26,11 +26,21 @@ class QtSubSocketChannel(SubSocketChannel, QtCore.QObject): # Emitted when any message is received. message_received = QtCore.pyqtSignal(object) - # Emitted when a message of type 'pyout' or 'stdout' is received. - output_received = QtCore.pyqtSignal(object) + # Emitted when a message of type 'stream' is received. + stream_received = QtCore.pyqtSignal(object) - # Emitted when a message of type 'pyerr' or 'stderr' is received. - error_received = QtCore.pyqtSignal(object) + # Emitted when a message of type 'pyin' is received. + pyin_received = QtCore.pyqtSignal(object) + + # Emitted when a message of type 'pyout' is received. + pyout_received = QtCore.pyqtSignal(object) + + # Emitted when a message of type 'pyerr' is received. + pyerr_received = QtCore.pyqtSignal(object) + + # Emitted when a crash report message is received from the kernel's + # last-resort sys.excepthook. + crash_received = QtCore.pyqtSignal(object) #--------------------------------------------------------------------------- # 'object' interface @@ -54,10 +64,11 @@ class QtSubSocketChannel(SubSocketChannel, QtCore.QObject): # Emit signals for specialized message types. msg_type = msg['msg_type'] - if msg_type in ('pyout', 'stdout'): - self.output_received.emit(msg) - elif msg_type in ('pyerr', 'stderr'): - self.error_received.emit(msg) + signal = getattr(self, msg_type + '_received', None) + if signal: + signal.emit(msg) + elif msg_type in ('stdout', 'stderr'): + self.stream_received.emit(msg) def flush(self): """ Reimplemented to ensure that signals are dispatched immediately. @@ -136,6 +147,7 @@ class QtRepSocketChannel(RepSocketChannel, QtCore.QObject): if msg_type == 'input_request': self.input_requested.emit(msg) + class QtKernelManager(KernelManager, QtCore.QObject): """ A KernelManager that provides signals and slots. """