##// END OF EJS Templates
* Moved KernelManager attribute management code in FrontendWidget into a mixin class usable in any Qt frontend. Registering handlers for message types is now trivial....
epatters -
Show More
@@ -0,0 +1,85 b''
1 """ Defines a convenient mix-in class for implementing Qt frontends.
2 """
3
4 class BaseFrontendMixin(object):
5 """ A mix-in class for implementing Qt frontends.
6
7 To handle messages of a particular type, frontends need only define an
8 appropriate handler method. For example, to handle 'stream' messaged, define
9 a '_handle_stream(msg)' method.
10 """
11
12 #---------------------------------------------------------------------------
13 # 'BaseFrontendMixin' concrete interface
14 #---------------------------------------------------------------------------
15
16 def _get_kernel_manager(self):
17 """ Returns the current kernel manager.
18 """
19 return self._kernel_manager
20
21 def _set_kernel_manager(self, kernel_manager):
22 """ Disconnect from the current kernel manager (if any) and set a new
23 kernel manager.
24 """
25 # Disconnect the old kernel manager, if necessary.
26 old_manager = self._kernel_manager
27 if old_manager is not None:
28 old_manager.started_channels.disconnect(self._started_channels)
29 old_manager.stopped_channels.disconnect(self._stopped_channels)
30
31 # Disconnect the old kernel manager's channels.
32 old_manager.sub_channel.message_received.disconnect(self._dispatch)
33 old_manager.xreq_channel.message_received.disconnect(self._dispatch)
34 old_manager.rep_channel.message_received.connect(self._dispatch)
35
36 # Handle the case where the old kernel manager is still listening.
37 if old_manager.channels_running:
38 self._stopped_channels()
39
40 # Set the new kernel manager.
41 self._kernel_manager = kernel_manager
42 if kernel_manager is None:
43 return
44
45 # Connect the new kernel manager.
46 kernel_manager.started_channels.connect(self._started_channels)
47 kernel_manager.stopped_channels.connect(self._stopped_channels)
48
49 # Connect the new kernel manager's channels.
50 kernel_manager.sub_channel.message_received.connect(self._dispatch)
51 kernel_manager.xreq_channel.message_received.connect(self._dispatch)
52 kernel_manager.rep_channel.message_received.connect(self._dispatch)
53
54 # Handle the case where the kernel manager started channels before
55 # we connected.
56 if kernel_manager.channels_running:
57 self._started_channels()
58
59 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
60
61 #---------------------------------------------------------------------------
62 # 'BaseFrontendMixin' abstract interface
63 #---------------------------------------------------------------------------
64
65 def _started_channels(self):
66 """ Called when the KernelManager channels have started listening or
67 when the frontend is assigned an already listening KernelManager.
68 """
69
70 def _stopped_channels(self):
71 """ Called when the KernelManager channels have stopped listening or
72 when a listening KernelManager is removed from the frontend.
73 """
74
75 #---------------------------------------------------------------------------
76 # Private interface
77 #---------------------------------------------------------------------------
78
79 def _dispatch(self, msg):
80 """ Call the frontend handler associated with
81 """
82 msg_type = msg['msg_type']
83 handler = getattr(self, '_handle_' + msg_type, None)
84 if handler:
85 handler(msg)
@@ -9,6 +9,7 b' import zmq'
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 13 from call_tip_widget import CallTipWidget
13 14 from completion_lexer import CompletionLexer
14 15 from console_widget import HistoryConsoleWidget
@@ -60,7 +61,7 b' class FrontendHighlighter(PygmentsHighlighter):'
60 61 PygmentsHighlighter.setFormat(self, start, count, format)
61 62
62 63
63 class FrontendWidget(HistoryConsoleWidget):
64 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
64 65 """ A Qt frontend for a generic Python kernel.
65 66 """
66 67
@@ -146,70 +147,96 b' class FrontendWidget(HistoryConsoleWidget):'
146 147 return not self._complete()
147 148
148 149 #---------------------------------------------------------------------------
149 # 'FrontendWidget' interface
150 # 'BaseFrontendMixin' abstract interface
150 151 #---------------------------------------------------------------------------
151 152
152 def execute_file(self, path, hidden=False):
153 """ Attempts to execute file with 'path'. If 'hidden', no output is
154 shown.
153 def _handle_complete_reply(self, rep):
154 """ Handle replies for tab completion.
155 155 """
156 self.execute('execfile("%s")' % path, hidden=hidden)
156 cursor = self._get_cursor()
157 if rep['parent_header']['msg_id'] == self._complete_id and \
158 cursor.position() == self._complete_pos:
159 text = '.'.join(self._get_context())
160 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
161 self._complete_with_items(cursor, rep['content']['matches'])
157 162
158 def _get_kernel_manager(self):
159 """ Returns the current kernel manager.
163 def _handle_execute_reply(self, msg):
164 """ Handles replies for code execution.
160 165 """
161 return self._kernel_manager
166 if not self._hidden:
167 # Make sure that all output from the SUB channel has been processed
168 # before writing a new prompt.
169 self.kernel_manager.sub_channel.flush()
170
171 content = msg['content']
172 status = content['status']
173 if status == 'ok':
174 self._process_execute_ok(msg)
175 elif status == 'error':
176 self._process_execute_error(msg)
177 elif status == 'abort':
178 self._process_execute_abort(msg)
179
180 self._hidden = True
181 self._show_interpreter_prompt()
182 self.executed.emit(msg)
162 183
163 def _set_kernel_manager(self, kernel_manager):
164 """ Disconnect from the current kernel manager (if any) and set a new
165 kernel manager.
184 def _handle_input_request(self, msg):
185 """ Handle requests for raw_input.
166 186 """
167 # Disconnect the old kernel manager, if necessary.
168 if self._kernel_manager is not None:
169 self._kernel_manager.started_channels.disconnect(
170 self._started_channels)
171 self._kernel_manager.stopped_channels.disconnect(
172 self._stopped_channels)
173
174 # Disconnect the old kernel manager's channels.
175 sub = self._kernel_manager.sub_channel
176 xreq = self._kernel_manager.xreq_channel
177 rep = self._kernel_manager.rep_channel
178 sub.message_received.disconnect(self._handle_sub)
179 xreq.execute_reply.disconnect(self._handle_execute_reply)
180 xreq.complete_reply.disconnect(self._handle_complete_reply)
181 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
182 rep.input_requested.disconnect(self._handle_req)
183
184 # Handle the case where the old kernel manager is still listening.
185 if self._kernel_manager.channels_running:
186 self._stopped_channels()
187
188 # Set the new kernel manager.
189 self._kernel_manager = kernel_manager
190 if kernel_manager is None:
191 return
187 # Make sure that all output from the SUB channel has been processed
188 # before entering readline mode.
189 self.kernel_manager.sub_channel.flush()
192 190
193 # Connect the new kernel manager.
194 kernel_manager.started_channels.connect(self._started_channels)
195 kernel_manager.stopped_channels.connect(self._stopped_channels)
191 def callback(line):
192 self.kernel_manager.rep_channel.input(line)
193 self._readline(msg['content']['prompt'], callback=callback)
196 194
197 # Connect the new kernel manager's channels.
198 sub = kernel_manager.sub_channel
199 xreq = kernel_manager.xreq_channel
200 rep = kernel_manager.rep_channel
201 sub.message_received.connect(self._handle_sub)
202 xreq.execute_reply.connect(self._handle_execute_reply)
203 xreq.complete_reply.connect(self._handle_complete_reply)
204 xreq.object_info_reply.connect(self._handle_object_info_reply)
205 rep.input_requested.connect(self._handle_req)
195 def _handle_object_info_reply(self, rep):
196 """ Handle replies for call tips.
197 """
198 cursor = self._get_cursor()
199 if rep['parent_header']['msg_id'] == self._call_tip_id and \
200 cursor.position() == self._call_tip_pos:
201 doc = rep['content']['docstring']
202 if doc:
203 self._call_tip_widget.show_docstring(doc)
206 204
207 # Handle the case where the kernel manager started channels before
208 # we connected.
209 if kernel_manager.channels_running:
210 self._started_channels()
205 def _handle_pyout(self, msg):
206 """ Handle display hook output.
207 """
208 self._append_plain_text(msg['content']['data'] + '\n')
209
210 def _handle_stream(self, msg):
211 """ Handle stdout, stderr, and stdin.
212 """
213 self._append_plain_text(msg['content']['data'])
214 self._control.moveCursor(QtGui.QTextCursor.End)
211 215
212 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
216 def _started_channels(self):
217 """ Called when the KernelManager channels have started listening or
218 when the frontend is assigned an already listening KernelManager.
219 """
220 self._reset()
221 self._append_plain_text(self._get_banner())
222 self._show_interpreter_prompt()
223
224 def _stopped_channels(self):
225 """ Called when the KernelManager channels have stopped listening or
226 when a listening KernelManager is removed from the frontend.
227 """
228 # FIXME: Print a message here?
229 pass
230
231 #---------------------------------------------------------------------------
232 # 'FrontendWidget' interface
233 #---------------------------------------------------------------------------
234
235 def execute_file(self, path, hidden=False):
236 """ Attempts to execute file with 'path'. If 'hidden', no output is
237 shown.
238 """
239 self.execute('execfile("%s")' % path, hidden=hidden)
213 240
214 241 #---------------------------------------------------------------------------
215 242 # 'FrontendWidget' protected interface
@@ -275,26 +302,32 b' class FrontendWidget(HistoryConsoleWidget):'
275 302 self._append_plain_text('Kernel process is either remote or '
276 303 'unspecified. Cannot interrupt.\n')
277 304
278 def _show_interpreter_prompt(self):
279 """ Shows a prompt for the interpreter.
305 def _process_execute_abort(self, msg):
306 """ Process a reply for an aborted execution request.
280 307 """
281 self._show_prompt('>>> ')
308 self._append_plain_text("ERROR: execution aborted\n")
282 309
283 #------ Signal handlers ----------------------------------------------------
284
285 def _started_channels(self):
286 """ Called when the kernel manager has started listening.
310 def _process_execute_error(self, msg):
311 """ Process a reply for an execution request that resulted in an error.
287 312 """
288 self._reset()
289 self._append_plain_text(self._get_banner())
290 self._show_interpreter_prompt()
313 content = msg['content']
314 traceback = ''.join(content['traceback'])
315 self._append_plain_text(traceback)
291 316
292 def _stopped_channels(self):
293 """ Called when the kernel manager has stopped listening.
317 def _process_execute_ok(self, msg):
318 """ Process a reply for a successful execution equest.
294 319 """
295 # FIXME: Print a message here?
320 # The basic FrontendWidget doesn't handle payloads, as they are a
321 # mechanism for going beyond the standard Python interpreter model.
296 322 pass
297 323
324 def _show_interpreter_prompt(self):
325 """ Shows a prompt for the interpreter.
326 """
327 self._show_prompt('>>> ')
328
329 #------ Signal handlers ----------------------------------------------------
330
298 331 def _document_contents_change(self, position, removed, added):
299 332 """ Called whenever the document's content changes. Display a call tip
300 333 if appropriate.
@@ -305,72 +338,3 b' class FrontendWidget(HistoryConsoleWidget):'
305 338 document = self._control.document()
306 339 if position == self._get_cursor().position():
307 340 self._call_tip()
308
309 def _handle_req(self, req):
310 # Make sure that all output from the SUB channel has been processed
311 # before entering readline mode.
312 self.kernel_manager.sub_channel.flush()
313
314 def callback(line):
315 self.kernel_manager.rep_channel.input(line)
316 self._readline(req['content']['prompt'], callback=callback)
317
318 def _handle_sub(self, omsg):
319 if self._hidden:
320 return
321 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
322 if handler is not None:
323 handler(omsg)
324
325 def _handle_pyout(self, omsg):
326 self._append_plain_text(omsg['content']['data'] + '\n')
327
328 def _handle_stream(self, omsg):
329 self._append_plain_text(omsg['content']['data'])
330 self._control.moveCursor(QtGui.QTextCursor.End)
331
332 def _handle_execute_reply(self, reply):
333 if self._hidden:
334 return
335
336 # Make sure that all output from the SUB channel has been processed
337 # before writing a new prompt.
338 self.kernel_manager.sub_channel.flush()
339
340 content = reply['content']
341 status = content['status']
342 if status == 'ok':
343 self._handle_execute_payload(content['payload'])
344 elif status == 'error':
345 self._handle_execute_error(reply)
346 elif status == 'aborted':
347 text = "ERROR: ABORTED\n"
348 self._append_plain_text(text)
349
350 self._hidden = True
351 self._show_interpreter_prompt()
352 self.executed.emit(reply)
353
354 def _handle_execute_error(self, reply):
355 content = reply['content']
356 traceback = ''.join(content['traceback'])
357 self._append_plain_text(traceback)
358
359 def _handle_execute_payload(self, payload):
360 pass
361
362 def _handle_complete_reply(self, rep):
363 cursor = self._get_cursor()
364 if rep['parent_header']['msg_id'] == self._complete_id and \
365 cursor.position() == self._complete_pos:
366 text = '.'.join(self._get_context())
367 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
368 self._complete_with_items(cursor, rep['content']['matches'])
369
370 def _handle_object_info_reply(self, rep):
371 cursor = self._get_cursor()
372 if rep['parent_header']['msg_id'] == self._call_tip_id and \
373 cursor.position() == self._call_tip_pos:
374 doc = rep['content']['docstring']
375 if doc:
376 self._call_tip_widget.show_docstring(doc)
@@ -49,6 +49,18 b' class IPythonWidget(FrontendWidget):'
49 49 self.reset_styling()
50 50
51 51 #---------------------------------------------------------------------------
52 # 'BaseFrontendMixin' abstract interface
53 #---------------------------------------------------------------------------
54
55 def _handle_pyout(self, msg):
56 """ Reimplemented for IPython-style "display hook".
57 """
58 self._append_html(self._make_out_prompt(self._prompt_count))
59 self._save_prompt_block()
60
61 self._append_plain_text(msg['content']['data'] + '\n')
62
63 #---------------------------------------------------------------------------
52 64 # 'FrontendWidget' interface
53 65 #---------------------------------------------------------------------------
54 66
@@ -66,6 +78,21 b' class IPythonWidget(FrontendWidget):'
66 78 """
67 79 return default_banner
68 80
81 def _process_execute_error(self, msg):
82 """ Reimplemented for IPython-style traceback formatting.
83 """
84 content = msg['content']
85 traceback_lines = content['traceback'][:]
86 traceback = ''.join(traceback_lines)
87 traceback = traceback.replace(' ', ' ')
88 traceback = traceback.replace('\n', '<br/>')
89
90 ename = content['ename']
91 ename_styled = '<span class="error">%s</span>' % ename
92 traceback = traceback.replace(ename, ename_styled)
93
94 self._append_html(traceback)
95
69 96 def _show_interpreter_prompt(self):
70 97 """ Reimplemented for IPython-style prompts.
71 98 """
@@ -93,31 +120,6 b' class IPythonWidget(FrontendWidget):'
93 120 self._set_continuation_prompt(
94 121 self._make_continuation_prompt(self._prompt), html=True)
95 122
96 #------ Signal handlers ----------------------------------------------------
97
98 def _handle_execute_error(self, reply):
99 """ Reimplemented for IPython-style traceback formatting.
100 """
101 content = reply['content']
102 traceback_lines = content['traceback'][:]
103 traceback = ''.join(traceback_lines)
104 traceback = traceback.replace(' ', '&nbsp;')
105 traceback = traceback.replace('\n', '<br/>')
106
107 ename = content['ename']
108 ename_styled = '<span class="error">%s</span>' % ename
109 traceback = traceback.replace(ename, ename_styled)
110
111 self._append_html(traceback)
112
113 def _handle_pyout(self, omsg):
114 """ Reimplemented for IPython-style "display hook".
115 """
116 self._append_html(self._make_out_prompt(self._prompt_count))
117 self._save_prompt_block()
118
119 self._append_plain_text(omsg['content']['data'] + '\n')
120
121 123 #---------------------------------------------------------------------------
122 124 # 'IPythonWidget' interface
123 125 #---------------------------------------------------------------------------
@@ -55,9 +55,10 b' class RichIPythonWidget(IPythonWidget):'
55 55 # 'FrontendWidget' protected interface
56 56 #---------------------------------------------------------------------------
57 57
58 def _handle_execute_payload(self, payload):
59 """ Reimplemented to handle pylab plot payloads.
58 def _process_execute_ok(self, msg):
59 """ Reimplemented to handle matplotlib plot payloads.
60 60 """
61 payload = msg['content']['payload']
61 62 plot_payload = payload.get('plot', None)
62 63 if plot_payload and plot_payload['format'] == 'svg':
63 64 svg = plot_payload['data']
@@ -73,7 +74,7 b' class RichIPythonWidget(IPythonWidget):'
73 74 cursor.insertImage(format)
74 75 cursor.insertBlock()
75 76 else:
76 super(RichIPythonWidget, self)._handle_execute_payload(payload)
77 super(RichIPythonWidget, self)._process_execute_ok(msg)
77 78
78 79 #---------------------------------------------------------------------------
79 80 # 'RichIPythonWidget' protected interface
@@ -26,11 +26,21 b' class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):'
26 26 # Emitted when any message is received.
27 27 message_received = QtCore.pyqtSignal(object)
28 28
29 # Emitted when a message of type 'pyout' or 'stdout' is received.
30 output_received = QtCore.pyqtSignal(object)
29 # Emitted when a message of type 'stream' is received.
30 stream_received = QtCore.pyqtSignal(object)
31 31
32 # Emitted when a message of type 'pyerr' or 'stderr' is received.
33 error_received = QtCore.pyqtSignal(object)
32 # Emitted when a message of type 'pyin' is received.
33 pyin_received = QtCore.pyqtSignal(object)
34
35 # Emitted when a message of type 'pyout' is received.
36 pyout_received = QtCore.pyqtSignal(object)
37
38 # Emitted when a message of type 'pyerr' is received.
39 pyerr_received = QtCore.pyqtSignal(object)
40
41 # Emitted when a crash report message is received from the kernel's
42 # last-resort sys.excepthook.
43 crash_received = QtCore.pyqtSignal(object)
34 44
35 45 #---------------------------------------------------------------------------
36 46 # 'object' interface
@@ -54,10 +64,11 b' class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):'
54 64
55 65 # Emit signals for specialized message types.
56 66 msg_type = msg['msg_type']
57 if msg_type in ('pyout', 'stdout'):
58 self.output_received.emit(msg)
59 elif msg_type in ('pyerr', 'stderr'):
60 self.error_received.emit(msg)
67 signal = getattr(self, msg_type + '_received', None)
68 if signal:
69 signal.emit(msg)
70 elif msg_type in ('stdout', 'stderr'):
71 self.stream_received.emit(msg)
61 72
62 73 def flush(self):
63 74 """ Reimplemented to ensure that signals are dispatched immediately.
@@ -136,6 +147,7 b' class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):'
136 147 if msg_type == 'input_request':
137 148 self.input_requested.emit(msg)
138 149
150
139 151 class QtKernelManager(KernelManager, QtCore.QObject):
140 152 """ A KernelManager that provides signals and slots.
141 153 """
General Comments 0
You need to be logged in to leave comments. Login now