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.
"""