##// END OF EJS Templates
* The SVG payload matplotlib backend now works....
epatters -
Show More
@@ -0,0 +1,54 b''
1 """ A demo of Qt console-style IPython frontend.
2 """
3
4 # Systemm library imports
5 from PyQt4 import QtCore, QtGui
6
7 # Local imports
8 from IPython.external.argparse import ArgumentParser
9 from IPython.frontend.qt.kernelmanager import QtKernelManager
10 from ipython_widget import IPythonWidget
11 from rich_ipython_widget import RichIPythonWidget
12
13
14 def main():
15 """ Entry point for demo.
16 """
17 # Parse command line arguments.
18 parser = ArgumentParser()
19 parser.add_argument('--pylab', action='store_true',
20 help='start kernel with pylab enabled')
21 parser.add_argument('--rich', action='store_true',
22 help='use rich text frontend')
23 namespace = parser.parse_args()
24
25 # Don't let Qt or ZMQ swallow KeyboardInterupts.
26 import signal
27 signal.signal(signal.SIGINT, signal.SIG_DFL)
28
29 # Create a KernelManager and start a kernel.
30 kernel_manager = QtKernelManager()
31 if namespace.pylab:
32 if namespace.rich:
33 kernel_manager.start_kernel(pylab='payload-svg')
34 else:
35 kernel_manager.start_kernel(pylab='qt4')
36 else:
37 kernel_manager.start_kernel()
38 kernel_manager.start_channels()
39
40 # Launch the application.
41 app = QtGui.QApplication([])
42 if namespace.rich:
43 widget = RichIPythonWidget()
44 else:
45 widget = IPythonWidget()
46 widget.kernel_manager = kernel_manager
47 widget.setWindowTitle('Python')
48 widget.resize(640, 480)
49 widget.show()
50 app.exec_()
51
52
53 if __name__ == '__main__':
54 main()
@@ -0,0 +1,43 b''
1 # System library imports
2 from PyQt4 import QtCore, QtGui
3
4 # Local imports
5 from IPython.frontend.qt.util import image_from_svg
6 from ipython_widget import IPythonWidget
7
8
9 class RichIPythonWidget(IPythonWidget):
10 """ An IPythonWidget that supports rich text, including lists, images, and
11 tables. Note that raw performance will be reduced compared to the plain
12 text version.
13 """
14
15 #---------------------------------------------------------------------------
16 # 'QObject' interface
17 #---------------------------------------------------------------------------
18
19 def __init__(self, parent=None):
20 """ Create a RichIPythonWidget.
21 """
22 super(RichIPythonWidget, self).__init__(kind='rich', parent=parent)
23
24 #---------------------------------------------------------------------------
25 # 'FrontendWidget' interface
26 #---------------------------------------------------------------------------
27
28 def _handle_execute_payload(self, payload):
29 """ Reimplemented to handle pylab plot payloads.
30 """
31 super(RichIPythonWidget, self)._handle_execute_payload(payload)
32
33 plot_payload = payload.get('plot', None)
34 if plot_payload and plot_payload['format'] == 'svg':
35 try:
36 image = image_from_svg(plot_payload['data'])
37 except ValueError:
38 self._append_plain_text('Received invalid plot data.')
39 else:
40 cursor = self._get_end_cursor()
41 cursor.insertBlock()
42 cursor.insertImage(image)
43 cursor.insertBlock()
@@ -1,165 +1,163 b''
1 1 # Standard library imports
2 2 import re
3 3 from textwrap import dedent
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8
9 9 class CallTipWidget(QtGui.QLabel):
10 10 """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
11 11 """
12 12
13 13 #--------------------------------------------------------------------------
14 14 # 'QObject' interface
15 15 #--------------------------------------------------------------------------
16 16
17 17 def __init__(self, parent):
18 18 """ Create a call tip manager that is attached to the specified Qt
19 19 text edit widget.
20 20 """
21 21 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
22 22 QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)
23 23
24 24 self.setFont(parent.document().defaultFont())
25 25 self.setForegroundRole(QtGui.QPalette.ToolTipText)
26 26 self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
27 27 self.setPalette(QtGui.QToolTip.palette())
28 28
29 29 self.setAlignment(QtCore.Qt.AlignLeft)
30 30 self.setIndent(1)
31 31 self.setFrameStyle(QtGui.QFrame.NoFrame)
32 32 self.setMargin(1 + self.style().pixelMetric(
33 33 QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
34 34 self.setWindowOpacity(self.style().styleHint(
35 35 QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
36 36
37 37 def eventFilter(self, obj, event):
38 38 """ Reimplemented to hide on certain key presses and on parent focus
39 39 changes.
40 40 """
41 41 if obj == self.parent():
42 42 etype = event.type()
43 43 if (etype == QtCore.QEvent.KeyPress and
44 44 event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
45 45 QtCore.Qt.Key_Escape)):
46 46 self.hide()
47 47 elif etype == QtCore.QEvent.FocusOut:
48 48 self.hide()
49 49
50 50 return QtGui.QLabel.eventFilter(self, obj, event)
51 51
52 52 #--------------------------------------------------------------------------
53 53 # 'QWidget' interface
54 54 #--------------------------------------------------------------------------
55 55
56 56 def hideEvent(self, event):
57 57 """ Reimplemented to disconnect signal handlers and event filter.
58 58 """
59 59 QtGui.QLabel.hideEvent(self, event)
60 60 parent = self.parent()
61 61 parent.cursorPositionChanged.disconnect(self._cursor_position_changed)
62 62 parent.removeEventFilter(self)
63 63
64 64 def paintEvent(self, event):
65 65 """ Reimplemented to paint the background panel.
66 66 """
67 67 painter = QtGui.QStylePainter(self)
68 68 option = QtGui.QStyleOptionFrame()
69 69 option.init(self)
70 70 painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
71 71 painter.end()
72 72
73 73 QtGui.QLabel.paintEvent(self, event)
74 74
75 75 def showEvent(self, event):
76 76 """ Reimplemented to connect signal handlers and event filter.
77 77 """
78 78 QtGui.QLabel.showEvent(self, event)
79 79 parent = self.parent()
80 80 parent.cursorPositionChanged.connect(self._cursor_position_changed)
81 81 parent.installEventFilter(self)
82 82
83 83 #--------------------------------------------------------------------------
84 84 # 'CallTipWidget' interface
85 85 #--------------------------------------------------------------------------
86 86
87 87 def show_docstring(self, doc, maxlines=20):
88 88 """ Attempts to show the specified docstring at the current cursor
89 89 location. The docstring is dedented and possibly truncated for
90 90 length.
91 91 """
92 92 doc = dedent(doc.rstrip()).lstrip()
93 93 match = re.match("(?:[^\n]*\n){%i}" % maxlines, doc)
94 94 if match:
95 95 doc = doc[:match.end()] + '\n[Documentation continues...]'
96 96 return self.show_tip(doc)
97 97
98 98 def show_tip(self, tip):
99 99 """ Attempts to show the specified tip at the current cursor location.
100 100 """
101 101 text_edit = self.parent()
102 102 document = text_edit.document()
103 103 cursor = text_edit.textCursor()
104 104 search_pos = cursor.position() - 1
105 105 self._start_position, _ = self._find_parenthesis(search_pos,
106 106 forward=False)
107 107 if self._start_position == -1:
108 108 return False
109 109
110 110 point = text_edit.cursorRect(cursor).bottomRight()
111 111 point = text_edit.mapToGlobal(point)
112 112 self.move(point)
113 113 self.setText(tip)
114 if self.isVisible():
115 114 self.resize(self.sizeHint())
116 else:
117 115 self.show()
118 116 return True
119 117
120 118 #--------------------------------------------------------------------------
121 119 # Protected interface
122 120 #--------------------------------------------------------------------------
123 121
124 122 def _find_parenthesis(self, position, forward=True):
125 123 """ If 'forward' is True (resp. False), proceed forwards
126 124 (resp. backwards) through the line that contains 'position' until an
127 125 unmatched closing (resp. opening) parenthesis is found. Returns a
128 126 tuple containing the position of this parenthesis (or -1 if it is
129 127 not found) and the number commas (at depth 0) found along the way.
130 128 """
131 129 commas = depth = 0
132 130 document = self.parent().document()
133 131 qchar = document.characterAt(position)
134 132 while (position > 0 and qchar.isPrint() and
135 133 # Need to check explicitly for line/paragraph separators:
136 134 qchar.unicode() not in (0x2028, 0x2029)):
137 135 char = qchar.toAscii()
138 136 if char == ',' and depth == 0:
139 137 commas += 1
140 138 elif char == ')':
141 139 if forward and depth == 0:
142 140 break
143 141 depth += 1
144 142 elif char == '(':
145 143 if not forward and depth == 0:
146 144 break
147 145 depth -= 1
148 146 position += 1 if forward else -1
149 147 qchar = document.characterAt(position)
150 148 else:
151 149 position = -1
152 150 return position, commas
153 151
154 152 #------ Signal handlers ----------------------------------------------------
155 153
156 154 def _cursor_position_changed(self):
157 155 """ Updates the tip based on user cursor movement.
158 156 """
159 157 cursor = self.parent().textCursor()
160 158 if cursor.position() <= self._start_position:
161 159 self.hide()
162 160 else:
163 161 position, commas = self._find_parenthesis(self._start_position + 1)
164 162 if position != -1:
165 163 self.hide()
@@ -1,369 +1,376 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from call_tip_widget import CallTipWidget
13 13 from completion_lexer import CompletionLexer
14 14 from console_widget import HistoryConsoleWidget
15 15 from pygments_highlighter import PygmentsHighlighter
16 16
17 17
18 18 class FrontendHighlighter(PygmentsHighlighter):
19 19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 20 prompts.
21 21 """
22 22
23 23 def __init__(self, frontend):
24 24 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
28 28
29 29 def highlightBlock(self, qstring):
30 30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 31 """
32 32 if not self.highlighting_on:
33 33 return
34 34
35 35 # The input to this function is unicode string that may contain
36 36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 37 # the string as plain text so we can compare it.
38 38 current_block = self.currentBlock()
39 39 string = self._frontend._get_block_plain_text(current_block)
40 40
41 41 # Decide whether to check for the regular or continuation prompt.
42 42 if current_block.contains(self._frontend._prompt_pos):
43 43 prompt = self._frontend._prompt
44 44 else:
45 45 prompt = self._frontend._continuation_prompt
46 46
47 47 # Don't highlight the part of the string that contains the prompt.
48 48 if string.startswith(prompt):
49 49 self._current_offset = len(prompt)
50 50 qstring.remove(0, len(prompt))
51 51 else:
52 52 self._current_offset = 0
53 53
54 54 PygmentsHighlighter.highlightBlock(self, qstring)
55 55
56 56 def setFormat(self, start, count, format):
57 57 """ Reimplemented to highlight selectively.
58 58 """
59 59 start += self._current_offset
60 60 PygmentsHighlighter.setFormat(self, start, count, format)
61 61
62 62
63 63 class FrontendWidget(HistoryConsoleWidget):
64 64 """ A Qt frontend for a generic Python kernel.
65 65 """
66 66
67 67 # Emitted when an 'execute_reply' is received from the kernel.
68 68 executed = QtCore.pyqtSignal(object)
69 69
70 70 #---------------------------------------------------------------------------
71 71 # 'object' interface
72 72 #---------------------------------------------------------------------------
73 73
74 74 def __init__(self, *args, **kw):
75 75 super(FrontendWidget, self).__init__(*args, **kw)
76 76
77 77 # FrontendWidget protected variables.
78 78 self._call_tip_widget = CallTipWidget(self._control)
79 79 self._completion_lexer = CompletionLexer(PythonLexer())
80 80 self._hidden = True
81 81 self._highlighter = FrontendHighlighter(self)
82 82 self._input_splitter = InputSplitter(input_mode='replace')
83 83 self._kernel_manager = None
84 84
85 85 # Configure the ConsoleWidget.
86 86 self.tab_width = 4
87 87 self._set_continuation_prompt('... ')
88 88
89 89 # Connect signal handlers.
90 90 document = self._control.document()
91 91 document.contentsChange.connect(self._document_contents_change)
92 92
93 93 #---------------------------------------------------------------------------
94 94 # 'ConsoleWidget' abstract interface
95 95 #---------------------------------------------------------------------------
96 96
97 97 def _is_complete(self, source, interactive):
98 98 """ Returns whether 'source' can be completely processed and a new
99 99 prompt created. When triggered by an Enter/Return key press,
100 100 'interactive' is True; otherwise, it is False.
101 101 """
102 102 complete = self._input_splitter.push(source.expandtabs(4))
103 103 if interactive:
104 104 complete = not self._input_splitter.push_accepts_more()
105 105 return complete
106 106
107 107 def _execute(self, source, hidden):
108 108 """ Execute 'source'. If 'hidden', do not show any output.
109 109 """
110 110 self.kernel_manager.xreq_channel.execute(source)
111 111 self._hidden = hidden
112 112
113 113 def _execute_interrupt(self):
114 114 """ Attempts to stop execution. Returns whether this method has an
115 115 implementation.
116 116 """
117 117 self._interrupt_kernel()
118 118 return True
119 119
120 120 def _prompt_started_hook(self):
121 121 """ Called immediately after a new prompt is displayed.
122 122 """
123 123 if not self._reading:
124 124 self._highlighter.highlighting_on = True
125 125
126 126 # Auto-indent if this is a continuation prompt.
127 127 if self._get_prompt_cursor().blockNumber() != \
128 128 self._get_end_cursor().blockNumber():
129 129 spaces = self._input_splitter.indent_spaces
130 130 self._append_plain_text('\t' * (spaces / self.tab_width))
131 131 self._append_plain_text(' ' * (spaces % self.tab_width))
132 132
133 133 def _prompt_finished_hook(self):
134 134 """ Called immediately after a prompt is finished, i.e. when some input
135 135 will be processed and a new prompt displayed.
136 136 """
137 137 if not self._reading:
138 138 self._highlighter.highlighting_on = False
139 139
140 140 def _tab_pressed(self):
141 141 """ Called when the tab key is pressed. Returns whether to continue
142 142 processing the event.
143 143 """
144 144 self._keep_cursor_in_buffer()
145 145 cursor = self._get_cursor()
146 146 return not self._complete()
147 147
148 148 #---------------------------------------------------------------------------
149 149 # 'FrontendWidget' interface
150 150 #---------------------------------------------------------------------------
151 151
152 152 def execute_file(self, path, hidden=False):
153 153 """ Attempts to execute file with 'path'. If 'hidden', no output is
154 154 shown.
155 155 """
156 156 self.execute('execfile("%s")' % path, hidden=hidden)
157 157
158 158 def _get_kernel_manager(self):
159 159 """ Returns the current kernel manager.
160 160 """
161 161 return self._kernel_manager
162 162
163 163 def _set_kernel_manager(self, kernel_manager):
164 164 """ Disconnect from the current kernel manager (if any) and set a new
165 165 kernel manager.
166 166 """
167 167 # Disconnect the old kernel manager, if necessary.
168 168 if self._kernel_manager is not None:
169 169 self._kernel_manager.started_channels.disconnect(
170 170 self._started_channels)
171 171 self._kernel_manager.stopped_channels.disconnect(
172 172 self._stopped_channels)
173 173
174 174 # Disconnect the old kernel manager's channels.
175 175 sub = self._kernel_manager.sub_channel
176 176 xreq = self._kernel_manager.xreq_channel
177 177 rep = self._kernel_manager.rep_channel
178 178 sub.message_received.disconnect(self._handle_sub)
179 179 xreq.execute_reply.disconnect(self._handle_execute_reply)
180 180 xreq.complete_reply.disconnect(self._handle_complete_reply)
181 181 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
182 182 rep.input_requested.disconnect(self._handle_req)
183 183
184 184 # Handle the case where the old kernel manager is still listening.
185 185 if self._kernel_manager.channels_running:
186 186 self._stopped_channels()
187 187
188 188 # Set the new kernel manager.
189 189 self._kernel_manager = kernel_manager
190 190 if kernel_manager is None:
191 191 return
192 192
193 193 # Connect the new kernel manager.
194 194 kernel_manager.started_channels.connect(self._started_channels)
195 195 kernel_manager.stopped_channels.connect(self._stopped_channels)
196 196
197 197 # Connect the new kernel manager's channels.
198 198 sub = kernel_manager.sub_channel
199 199 xreq = kernel_manager.xreq_channel
200 200 rep = kernel_manager.rep_channel
201 201 sub.message_received.connect(self._handle_sub)
202 202 xreq.execute_reply.connect(self._handle_execute_reply)
203 203 xreq.complete_reply.connect(self._handle_complete_reply)
204 204 xreq.object_info_reply.connect(self._handle_object_info_reply)
205 205 rep.input_requested.connect(self._handle_req)
206 206
207 207 # Handle the case where the kernel manager started channels before
208 208 # we connected.
209 209 if kernel_manager.channels_running:
210 210 self._started_channels()
211 211
212 212 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
213 213
214 214 #---------------------------------------------------------------------------
215 215 # 'FrontendWidget' protected interface
216 216 #---------------------------------------------------------------------------
217 217
218 218 def _call_tip(self):
219 219 """ Shows a call tip, if appropriate, at the current cursor location.
220 220 """
221 221 # Decide if it makes sense to show a call tip
222 222 cursor = self._get_cursor()
223 223 cursor.movePosition(QtGui.QTextCursor.Left)
224 224 document = self._control.document()
225 225 if document.characterAt(cursor.position()).toAscii() != '(':
226 226 return False
227 227 context = self._get_context(cursor)
228 228 if not context:
229 229 return False
230 230
231 231 # Send the metadata request to the kernel
232 232 name = '.'.join(context)
233 233 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
234 234 self._call_tip_pos = self._get_cursor().position()
235 235 return True
236 236
237 237 def _complete(self):
238 238 """ Performs completion at the current cursor location.
239 239 """
240 240 # Decide if it makes sense to do completion
241 241 context = self._get_context()
242 242 if not context:
243 243 return False
244 244
245 245 # Send the completion request to the kernel
246 246 text = '.'.join(context)
247 247 self._complete_id = self.kernel_manager.xreq_channel.complete(
248 248 text, self.input_buffer_cursor_line, self.input_buffer)
249 249 self._complete_pos = self._get_cursor().position()
250 250 return True
251 251
252 252 def _get_banner(self):
253 253 """ Gets a banner to display at the beginning of a session.
254 254 """
255 255 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
256 256 '"license" for more information.'
257 257 return banner % (sys.version, sys.platform)
258 258
259 259 def _get_context(self, cursor=None):
260 260 """ Gets the context at the current cursor location.
261 261 """
262 262 if cursor is None:
263 263 cursor = self._get_cursor()
264 264 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
265 265 QtGui.QTextCursor.KeepAnchor)
266 266 text = str(cursor.selection().toPlainText())
267 267 return self._completion_lexer.get_context(text)
268 268
269 269 def _interrupt_kernel(self):
270 270 """ Attempts to the interrupt the kernel.
271 271 """
272 272 if self.kernel_manager.has_kernel:
273 273 self.kernel_manager.signal_kernel(signal.SIGINT)
274 274 else:
275 275 self._append_plain_text('Kernel process is either remote or '
276 276 'unspecified. Cannot interrupt.\n')
277 277
278 278 def _show_interpreter_prompt(self):
279 279 """ Shows a prompt for the interpreter.
280 280 """
281 281 self._show_prompt('>>> ')
282 282
283 283 #------ Signal handlers ----------------------------------------------------
284 284
285 285 def _started_channels(self):
286 286 """ Called when the kernel manager has started listening.
287 287 """
288 288 self._reset()
289 289 self._append_plain_text(self._get_banner())
290 290 self._show_interpreter_prompt()
291 291
292 292 def _stopped_channels(self):
293 293 """ Called when the kernel manager has stopped listening.
294 294 """
295 295 # FIXME: Print a message here?
296 296 pass
297 297
298 298 def _document_contents_change(self, position, removed, added):
299 299 """ Called whenever the document's content changes. Display a call tip
300 300 if appropriate.
301 301 """
302 302 # Calculate where the cursor should be *after* the change:
303 303 position += added
304 304
305 305 document = self._control.document()
306 306 if position == self._get_cursor().position():
307 307 self._call_tip()
308 308
309 309 def _handle_req(self, req):
310 310 # Make sure that all output from the SUB channel has been processed
311 311 # before entering readline mode.
312 312 self.kernel_manager.sub_channel.flush()
313 313
314 314 def callback(line):
315 315 self.kernel_manager.rep_channel.input(line)
316 316 self._readline(req['content']['prompt'], callback=callback)
317 317
318 318 def _handle_sub(self, omsg):
319 319 if self._hidden:
320 320 return
321 321 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
322 322 if handler is not None:
323 323 handler(omsg)
324 324
325 325 def _handle_pyout(self, omsg):
326 326 self._append_plain_text(omsg['content']['data'] + '\n')
327 327
328 328 def _handle_stream(self, omsg):
329 329 self._append_plain_text(omsg['content']['data'])
330 330 self._control.moveCursor(QtGui.QTextCursor.End)
331 331
332 332 def _handle_execute_reply(self, reply):
333 333 if self._hidden:
334 334 return
335 335
336 336 # Make sure that all output from the SUB channel has been processed
337 337 # before writing a new prompt.
338 338 self.kernel_manager.sub_channel.flush()
339 339
340 status = reply['content']['status']
341 if status == 'error':
340 content = reply['content']
341 status = content['status']
342 if status == 'ok':
343 self._handle_execute_payload(content['payload'])
344 elif status == 'error':
342 345 self._handle_execute_error(reply)
343 346 elif status == 'aborted':
344 347 text = "ERROR: ABORTED\n"
345 348 self._append_plain_text(text)
349
346 350 self._hidden = True
347 351 self._show_interpreter_prompt()
348 352 self.executed.emit(reply)
349 353
350 354 def _handle_execute_error(self, reply):
351 355 content = reply['content']
352 356 traceback = ''.join(content['traceback'])
353 357 self._append_plain_text(traceback)
354 358
359 def _handle_execute_payload(self, payload):
360 pass
361
355 362 def _handle_complete_reply(self, rep):
356 363 cursor = self._get_cursor()
357 364 if rep['parent_header']['msg_id'] == self._complete_id and \
358 365 cursor.position() == self._complete_pos:
359 366 text = '.'.join(self._get_context())
360 367 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
361 368 self._complete_with_items(cursor, rep['content']['matches'])
362 369
363 370 def _handle_object_info_reply(self, rep):
364 371 cursor = self._get_cursor()
365 372 if rep['parent_header']['msg_id'] == self._call_tip_id and \
366 373 cursor.position() == self._call_tip_pos:
367 374 doc = rep['content']['docstring']
368 375 if doc:
369 376 self._call_tip_widget.show_docstring(doc)
@@ -1,206 +1,184 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4 # Local imports
5 5 from IPython.core.usage import default_banner
6 6 from frontend_widget import FrontendWidget
7 7
8 8
9 9 class IPythonWidget(FrontendWidget):
10 10 """ A FrontendWidget for an IPython kernel.
11 11 """
12 12
13 13 # The default stylesheet: black text on a white background.
14 14 default_stylesheet = """
15 15 .error { color: red; }
16 16 .in-prompt { color: navy; }
17 17 .in-prompt-number { font-weight: bold; }
18 18 .out-prompt { color: darkred; }
19 19 .out-prompt-number { font-weight: bold; }
20 20 """
21 21
22 22 # A dark stylesheet: white text on a black background.
23 23 dark_stylesheet = """
24 24 QPlainTextEdit { background-color: black; color: white }
25 25 QFrame { border: 1px solid grey; }
26 26 .error { color: red; }
27 27 .in-prompt { color: lime; }
28 28 .in-prompt-number { color: lime; font-weight: bold; }
29 29 .out-prompt { color: red; }
30 30 .out-prompt-number { color: red; font-weight: bold; }
31 31 """
32 32
33 33 # Default prompts.
34 34 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
35 35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 super(IPythonWidget, self).__init__(*args, **kw)
43 43
44 44 # Initialize protected variables.
45 45 self._previous_prompt_blocks = []
46 46 self._prompt_count = 0
47 47
48 48 # Set a default stylesheet.
49 49 self.reset_styling()
50 50
51 51 #---------------------------------------------------------------------------
52 52 # 'FrontendWidget' interface
53 53 #---------------------------------------------------------------------------
54 54
55 55 def execute_file(self, path, hidden=False):
56 56 """ Reimplemented to use the 'run' magic.
57 57 """
58 58 self.execute('run %s' % path, hidden=hidden)
59 59
60 60 #---------------------------------------------------------------------------
61 61 # 'FrontendWidget' protected interface
62 62 #---------------------------------------------------------------------------
63 63
64 64 def _get_banner(self):
65 65 """ Reimplemented to return IPython's default banner.
66 66 """
67 67 return default_banner
68 68
69 69 def _show_interpreter_prompt(self):
70 70 """ Reimplemented for IPython-style prompts.
71 71 """
72 72 # Update old prompt numbers if necessary.
73 73 previous_prompt_number = self._prompt_count
74 74 if previous_prompt_number != self._prompt_count:
75 75 for i, (block, length) in enumerate(self._previous_prompt_blocks):
76 76 if block.isValid():
77 77 cursor = QtGui.QTextCursor(block)
78 78 cursor.movePosition(QtGui.QTextCursor.Right,
79 79 QtGui.QTextCursor.KeepAnchor, length-1)
80 80 if i == 0:
81 81 prompt = self._make_in_prompt(previous_prompt_number)
82 82 else:
83 83 prompt = self._make_out_prompt(previous_prompt_number)
84 84 self._insert_html(cursor, prompt)
85 85 self._previous_prompt_blocks = []
86 86
87 87 # Show a new prompt.
88 88 self._prompt_count += 1
89 89 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
90 90 self._save_prompt_block()
91 91
92 92 # Update continuation prompt to reflect (possibly) new prompt length.
93 93 self._set_continuation_prompt(
94 94 self._make_continuation_prompt(self._prompt), html=True)
95 95
96 96 #------ Signal handlers ----------------------------------------------------
97 97
98 98 def _handle_execute_error(self, reply):
99 99 """ Reimplemented for IPython-style traceback formatting.
100 100 """
101 101 content = reply['content']
102 102 traceback_lines = content['traceback'][:]
103 103 traceback = ''.join(traceback_lines)
104 104 traceback = traceback.replace(' ', '&nbsp;')
105 105 traceback = traceback.replace('\n', '<br/>')
106 106
107 107 ename = content['ename']
108 108 ename_styled = '<span class="error">%s</span>' % ename
109 109 traceback = traceback.replace(ename, ename_styled)
110 110
111 111 self._append_html(traceback)
112 112
113 113 def _handle_pyout(self, omsg):
114 114 """ Reimplemented for IPython-style "display hook".
115 115 """
116 116 self._append_html(self._make_out_prompt(self._prompt_count))
117 117 self._save_prompt_block()
118 118
119 119 self._append_plain_text(omsg['content']['data'] + '\n')
120 120
121 121 #---------------------------------------------------------------------------
122 122 # 'IPythonWidget' interface
123 123 #---------------------------------------------------------------------------
124 124
125 125 def reset_styling(self):
126 126 """ Restores the default IPythonWidget styling.
127 127 """
128 128 self.set_styling(self.default_stylesheet, syntax_style='default')
129 129 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
130 130
131 131 def set_styling(self, stylesheet, syntax_style=None):
132 132 """ Sets the IPythonWidget styling.
133 133
134 134 Parameters:
135 135 -----------
136 136 stylesheet : str
137 137 A CSS stylesheet. The stylesheet can contain classes for:
138 138 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
139 139 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
140 140 3. IPython: .error, .in-prompt, .out-prompt, etc.
141 141
142 142 syntax_style : str or None [default None]
143 143 If specified, use the Pygments style with given name. Otherwise,
144 144 the stylesheet is queried for Pygments style information.
145 145 """
146 146 self.setStyleSheet(stylesheet)
147 147 self._control.document().setDefaultStyleSheet(stylesheet)
148 148
149 149 if syntax_style is None:
150 150 self._highlighter.set_style_sheet(stylesheet)
151 151 else:
152 152 self._highlighter.set_style(syntax_style)
153 153
154 154 #---------------------------------------------------------------------------
155 155 # 'IPythonWidget' protected interface
156 156 #---------------------------------------------------------------------------
157 157
158 158 def _make_in_prompt(self, number):
159 159 """ Given a prompt number, returns an HTML In prompt.
160 160 """
161 161 body = self.in_prompt % number
162 162 return '<span class="in-prompt">%s</span>' % body
163 163
164 164 def _make_continuation_prompt(self, prompt):
165 165 """ Given a plain text version of an In prompt, returns an HTML
166 166 continuation prompt.
167 167 """
168 168 end_chars = '...: '
169 169 space_count = len(prompt.lstrip('\n')) - len(end_chars)
170 170 body = '&nbsp;' * space_count + end_chars
171 171 return '<span class="in-prompt">%s</span>' % body
172 172
173 173 def _make_out_prompt(self, number):
174 174 """ Given a prompt number, returns an HTML Out prompt.
175 175 """
176 176 body = self.out_prompt % number
177 177 return '<span class="out-prompt">%s</span>' % body
178 178
179 179 def _save_prompt_block(self):
180 180 """ Assuming a prompt has just been written at the end of the buffer,
181 181 store the QTextBlock that contains it and its length.
182 182 """
183 183 block = self._control.document().lastBlock()
184 184 self._previous_prompt_blocks.append((block, block.length()))
185
186
187 if __name__ == '__main__':
188 from IPython.frontend.qt.kernelmanager import QtKernelManager
189
190 # Don't let Qt or ZMQ swallow KeyboardInterupts.
191 import signal
192 signal.signal(signal.SIGINT, signal.SIG_DFL)
193
194 # Create a KernelManager.
195 kernel_manager = QtKernelManager()
196 kernel_manager.start_kernel()
197 kernel_manager.start_channels()
198
199 # Launch the application.
200 app = QtGui.QApplication([])
201 widget = IPythonWidget()
202 widget.kernel_manager = kernel_manager
203 widget.setWindowTitle('Python')
204 widget.resize(640, 480)
205 widget.show()
206 app.exec_()
@@ -1,184 +1,180 b''
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from PyQt4 import QtCore
6 6 import zmq
7 7
8 8 # IPython imports.
9 9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
10 10 XReqSocketChannel, RepSocketChannel
11 11 from util import MetaQObjectHasTraits
12 12
13 13 # When doing multiple inheritance from QtCore.QObject and other classes
14 14 # the calling of the parent __init__'s is a subtle issue:
15 15 # * QtCore.QObject does not call super so you can't use super and put
16 16 # QObject first in the inheritance list.
17 17 # * QtCore.QObject.__init__ takes 1 argument, the parent. So if you are going
18 18 # to use super, any class that comes before QObject must pass it something
19 19 # reasonable.
20 20 # In summary, I don't think using super in these situations will work.
21 21 # Instead we will need to call the __init__ methods of both parents
22 22 # by hand. Not pretty, but it works.
23 23
24 24 class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
25 25
26 26 # Emitted when any message is received.
27 27 message_received = QtCore.pyqtSignal(object)
28 28
29 29 # Emitted when a message of type 'pyout' or 'stdout' is received.
30 30 output_received = QtCore.pyqtSignal(object)
31 31
32 32 # Emitted when a message of type 'pyerr' or 'stderr' is received.
33 33 error_received = QtCore.pyqtSignal(object)
34 34
35 35 #---------------------------------------------------------------------------
36 36 # 'object' interface
37 37 #---------------------------------------------------------------------------
38 38
39 39 def __init__(self, *args, **kw):
40 40 """ Reimplemented to ensure that QtCore.QObject is initialized first.
41 41 """
42 42 QtCore.QObject.__init__(self)
43 43 SubSocketChannel.__init__(self, *args, **kw)
44 44
45 45 #---------------------------------------------------------------------------
46 46 # 'SubSocketChannel' interface
47 47 #---------------------------------------------------------------------------
48 48
49 49 def call_handlers(self, msg):
50 50 """ Reimplemented to emit signals instead of making callbacks.
51 51 """
52 52 # Emit the generic signal.
53 53 self.message_received.emit(msg)
54 54
55 55 # Emit signals for specialized message types.
56 56 msg_type = msg['msg_type']
57 57 if msg_type in ('pyout', 'stdout'):
58 58 self.output_received.emit(msg)
59 59 elif msg_type in ('pyerr', 'stderr'):
60 60 self.error_received.emit(msg)
61 61
62 62 def flush(self):
63 63 """ Reimplemented to ensure that signals are dispatched immediately.
64 64 """
65 65 super(QtSubSocketChannel, self).flush()
66 66 QtCore.QCoreApplication.instance().processEvents()
67 67
68 68
69 69 class QtXReqSocketChannel(XReqSocketChannel, QtCore.QObject):
70 70
71 71 # Emitted when any message is received.
72 72 message_received = QtCore.pyqtSignal(object)
73 73
74 74 # Emitted when a reply has been received for the corresponding request type.
75 75 execute_reply = QtCore.pyqtSignal(object)
76 76 complete_reply = QtCore.pyqtSignal(object)
77 77 object_info_reply = QtCore.pyqtSignal(object)
78 78
79 79 #---------------------------------------------------------------------------
80 80 # 'object' interface
81 81 #---------------------------------------------------------------------------
82 82
83 83 def __init__(self, *args, **kw):
84 84 """ Reimplemented to ensure that QtCore.QObject is initialized first.
85 85 """
86 86 QtCore.QObject.__init__(self)
87 87 XReqSocketChannel.__init__(self, *args, **kw)
88 88
89 89 #---------------------------------------------------------------------------
90 90 # 'XReqSocketChannel' interface
91 91 #---------------------------------------------------------------------------
92 92
93 93 def call_handlers(self, msg):
94 94 """ Reimplemented to emit signals instead of making callbacks.
95 95 """
96 96 # Emit the generic signal.
97 97 self.message_received.emit(msg)
98 98
99 99 # Emit signals for specialized message types.
100 100 msg_type = msg['msg_type']
101 101 signal = getattr(self, msg_type, None)
102 102 if signal:
103 103 signal.emit(msg)
104 104
105 105
106 106 class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):
107 107
108 108 # Emitted when any message is received.
109 109 message_received = QtCore.pyqtSignal(object)
110 110
111 111 # Emitted when an input request is received.
112 112 input_requested = QtCore.pyqtSignal(object)
113 113
114 114 #---------------------------------------------------------------------------
115 115 # 'object' interface
116 116 #---------------------------------------------------------------------------
117 117
118 118 def __init__(self, *args, **kw):
119 119 """ Reimplemented to ensure that QtCore.QObject is initialized first.
120 120 """
121 121 QtCore.QObject.__init__(self)
122 122 RepSocketChannel.__init__(self, *args, **kw)
123 123
124 124 #---------------------------------------------------------------------------
125 125 # 'RepSocketChannel' interface
126 126 #---------------------------------------------------------------------------
127 127
128 128 def call_handlers(self, msg):
129 129 """ Reimplemented to emit signals instead of making callbacks.
130 130 """
131 131 # Emit the generic signal.
132 132 self.message_received.emit(msg)
133 133
134 134 # Emit signals for specialized message types.
135 135 msg_type = msg['msg_type']
136 136 if msg_type == 'input_request':
137 137 self.input_requested.emit(msg)
138 138
139 139 class QtKernelManager(KernelManager, QtCore.QObject):
140 140 """ A KernelManager that provides signals and slots.
141 141 """
142 142
143 143 __metaclass__ = MetaQObjectHasTraits
144 144
145 145 # Emitted when the kernel manager has started listening.
146 146 started_channels = QtCore.pyqtSignal()
147 147
148 148 # Emitted when the kernel manager has stopped listening.
149 149 stopped_channels = QtCore.pyqtSignal()
150 150
151 151 # Use Qt-specific channel classes that emit signals.
152 152 sub_channel_class = QtSubSocketChannel
153 153 xreq_channel_class = QtXReqSocketChannel
154 154 rep_channel_class = QtRepSocketChannel
155 155
156 def __init__(self, *args, **kw):
157 QtCore.QObject.__init__(self)
158 KernelManager.__init__(self, *args, **kw)
159
160 156 #---------------------------------------------------------------------------
161 157 # 'object' interface
162 158 #---------------------------------------------------------------------------
163 159
164 160 def __init__(self, *args, **kw):
165 161 """ Reimplemented to ensure that QtCore.QObject is initialized first.
166 162 """
167 163 QtCore.QObject.__init__(self)
168 164 KernelManager.__init__(self, *args, **kw)
169 165
170 166 #---------------------------------------------------------------------------
171 167 # 'KernelManager' interface
172 168 #---------------------------------------------------------------------------
173 169
174 170 def start_channels(self):
175 171 """ Reimplemented to emit signal.
176 172 """
177 173 super(QtKernelManager, self).start_channels()
178 174 self.started_channels.emit()
179 175
180 176 def stop_channels(self):
181 177 """ Reimplemented to emit signal.
182 178 """
183 179 super(QtKernelManager, self).stop_channels()
184 180 self.stopped_channels.emit()
@@ -1,586 +1,599 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Finish implementing `raw_input`.
7 7 * Implement `set_parent` logic. Right before doing exec, the Kernel should
8 8 call set_parent on all the PUB objects with the message about to be executed.
9 9 * Implement random port and security key logic.
10 10 * Implement control messages.
11 11 * Implement event loop and poll version.
12 12 """
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17
18 18 # Standard library imports.
19 19 import __builtin__
20 20 from code import CommandCompiler
21 21 from cStringIO import StringIO
22 22 import os
23 23 import sys
24 24 from threading import Thread
25 25 import time
26 26 import traceback
27 27
28 28 # System library imports.
29 29 import zmq
30 30
31 31 # Local imports.
32 32 from IPython.external.argparse import ArgumentParser
33 33 from session import Session, Message, extract_header
34 34 from completer import KernelCompleter
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Kernel and stream classes
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class OutStream(object):
41 41 """A file like object that publishes the stream to a 0MQ PUB socket."""
42 42
43 43 # The time interval between automatic flushes, in seconds.
44 44 flush_interval = 0.05
45 45
46 46 def __init__(self, session, pub_socket, name):
47 47 self.session = session
48 48 self.pub_socket = pub_socket
49 49 self.name = name
50 50 self.parent_header = {}
51 51 self._new_buffer()
52 52
53 53 def set_parent(self, parent):
54 54 self.parent_header = extract_header(parent)
55 55
56 56 def close(self):
57 57 self.pub_socket = None
58 58
59 59 def flush(self):
60 60 if self.pub_socket is None:
61 61 raise ValueError(u'I/O operation on closed file')
62 62 else:
63 63 data = self._buffer.getvalue()
64 64 if data:
65 65 content = {u'name':self.name, u'data':data}
66 66 msg = self.session.msg(u'stream', content=content,
67 67 parent=self.parent_header)
68 68 print>>sys.__stdout__, Message(msg)
69 69 self.pub_socket.send_json(msg)
70 70
71 71 self._buffer.close()
72 72 self._new_buffer()
73 73
74 74 def isatty(self):
75 75 return False
76 76
77 77 def next(self):
78 78 raise IOError('Read not supported on a write only stream.')
79 79
80 80 def read(self, size=-1):
81 81 raise IOError('Read not supported on a write only stream.')
82 82
83 83 def readline(self, size=-1):
84 84 raise IOError('Read not supported on a write only stream.')
85 85
86 86 def write(self, string):
87 87 if self.pub_socket is None:
88 88 raise ValueError('I/O operation on closed file')
89 89 else:
90 90 self._buffer.write(string)
91 91 current_time = time.time()
92 92 if self._start <= 0:
93 93 self._start = current_time
94 94 elif current_time - self._start > self.flush_interval:
95 95 self.flush()
96 96
97 97 def writelines(self, sequence):
98 98 if self.pub_socket is None:
99 99 raise ValueError('I/O operation on closed file')
100 100 else:
101 101 for string in sequence:
102 102 self.write(string)
103 103
104 104 def _new_buffer(self):
105 105 self._buffer = StringIO()
106 106 self._start = -1
107 107
108 108
109 109 class DisplayHook(object):
110 110
111 111 def __init__(self, session, pub_socket):
112 112 self.session = session
113 113 self.pub_socket = pub_socket
114 114 self.parent_header = {}
115 115
116 116 def __call__(self, obj):
117 117 if obj is not None:
118 118 __builtin__._ = obj
119 119 msg = self.session.msg(u'pyout', {u'data':repr(obj)},
120 120 parent=self.parent_header)
121 121 self.pub_socket.send_json(msg)
122 122
123 123 def set_parent(self, parent):
124 124 self.parent_header = extract_header(parent)
125 125
126 126
127 127 class Kernel(object):
128 128
129 129 # The global kernel instance.
130 130 _kernel = None
131 131
132 132 # Maps user-friendly backend names to matplotlib backend identifiers.
133 133 _pylab_map = { 'tk': 'TkAgg',
134 134 'gtk': 'GTKAgg',
135 135 'wx': 'WXAgg',
136 136 'qt': 'Qt4Agg', # qt3 not supported
137 137 'qt4': 'Qt4Agg',
138 138 'payload-svg' : \
139 139 'module://IPython.zmq.pylab.backend_payload_svg' }
140 140
141 141 #---------------------------------------------------------------------------
142 142 # Kernel interface
143 143 #---------------------------------------------------------------------------
144 144
145 145 def __init__(self, session, reply_socket, pub_socket, req_socket):
146 146 self.session = session
147 147 self.reply_socket = reply_socket
148 148 self.pub_socket = pub_socket
149 149 self.req_socket = req_socket
150 150 self.user_ns = {}
151 151 self.history = []
152 152 self.compiler = CommandCompiler()
153 153 self.completer = KernelCompleter(self.user_ns)
154 154
155 155 # Protected variables.
156 156 self._exec_payload = {}
157 157
158 158 # Build dict of handlers for message types
159 159 msg_types = [ 'execute_request', 'complete_request',
160 160 'object_info_request' ]
161 161 self.handlers = {}
162 162 for msg_type in msg_types:
163 163 self.handlers[msg_type] = getattr(self, msg_type)
164 164
165 165 def add_exec_payload(self, key, value):
166 166 """ Adds a key/value pair to the execute payload.
167 167 """
168 168 self._exec_payload[key] = value
169 169
170 170 def activate_pylab(self, backend=None, import_all=True):
171 171 """ Activates pylab in this kernel's namespace.
172 172
173 173 Parameters:
174 174 -----------
175 175 backend : str, optional
176 176 A valid backend name.
177 177
178 178 import_all : bool, optional
179 179 If true, an 'import *' is done from numpy and pylab.
180 180 """
181 181 # FIXME: This is adapted from IPython.lib.pylabtools.pylab_activate.
182 182 # Common funtionality should be refactored.
183 183
184 import matplotlib
185
186 184 # We must set the desired backend before importing pylab.
185 import matplotlib
187 186 if backend:
188 matplotlib.use(self._pylab_map[backend])
189
190 # This must be imported last in the matplotlib series, after
191 # backend/interactivity choices have been made.
192 import matplotlib.pylab as pylab
187 backend_id = self._pylab_map[backend]
188 if backend_id.startswith('module://'):
189 # Work around bug in matplotlib: matplotlib.use converts the
190 # backend_id to lowercase even if a module name is specified!
191 matplotlib.rcParams['backend'] = backend_id
192 else:
193 matplotlib.use(backend_id)
193 194
194 195 # Import numpy as np/pyplot as plt are conventions we're trying to
195 196 # somewhat standardize on. Making them available to users by default
196 197 # will greatly help this.
197 198 exec ("import numpy\n"
198 199 "import matplotlib\n"
199 200 "from matplotlib import pylab, mlab, pyplot\n"
200 201 "np = numpy\n"
201 202 "plt = pyplot\n"
202 203 ) in self.user_ns
203 204
204 205 if import_all:
205 206 exec("from matplotlib.pylab import *\n"
206 207 "from numpy import *\n") in self.user_ns
207 208
208 209 matplotlib.interactive(True)
209 210
210 211 @classmethod
211 212 def get_kernel(cls):
212 213 """ Return the global kernel instance or raise a RuntimeError if it does
213 214 not exist.
214 215 """
215 216 if cls._kernel is None:
216 217 raise RuntimeError("Kernel not started!")
217 218 else:
218 219 return cls._kernel
219 220
220 221 def start(self):
221 222 """ Start the kernel main loop.
222 223 """
223 224 # Set the global kernel instance.
224 225 Kernel._kernel = self
225 226
226 227 while True:
227 228 ident = self.reply_socket.recv()
228 229 assert self.reply_socket.rcvmore(), "Missing message part."
229 230 msg = self.reply_socket.recv_json()
230 231 omsg = Message(msg)
231 232 print>>sys.__stdout__
232 233 print>>sys.__stdout__, omsg
233 234 handler = self.handlers.get(omsg.msg_type, None)
234 235 if handler is None:
235 236 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
236 237 else:
237 238 handler(ident, omsg)
238 239
239 240 #---------------------------------------------------------------------------
240 241 # Kernel request handlers
241 242 #---------------------------------------------------------------------------
242 243
243 244 def execute_request(self, ident, parent):
244 245 try:
245 246 code = parent[u'content'][u'code']
246 247 except:
247 248 print>>sys.__stderr__, "Got bad msg: "
248 249 print>>sys.__stderr__, Message(parent)
249 250 return
250 251 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
251 252 self.pub_socket.send_json(pyin_msg)
252 253
253 254 # Clear the execute payload from the last request.
254 255 self._exec_payload = {}
255 256
256 257 try:
257 258 comp_code = self.compiler(code, '<zmq-kernel>')
258 259
259 260 # Replace raw_input. Note that is not sufficient to replace
260 261 # raw_input in the user namespace.
261 262 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
262 263 __builtin__.raw_input = raw_input
263 264
264 265 # Configure the display hook.
265 266 sys.displayhook.set_parent(parent)
266 267
267 268 exec comp_code in self.user_ns, self.user_ns
268 269 except:
269 270 result = u'error'
270 271 etype, evalue, tb = sys.exc_info()
271 272 tb = traceback.format_exception(etype, evalue, tb)
272 273 exc_content = {
273 274 u'status' : u'error',
274 275 u'traceback' : tb,
275 276 u'ename' : unicode(etype.__name__),
276 277 u'evalue' : unicode(evalue)
277 278 }
278 279 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
279 280 self.pub_socket.send_json(exc_msg)
280 281 reply_content = exc_content
281 282 else:
282 283 reply_content = { 'status' : 'ok', 'payload' : self._exec_payload }
283 284
284 285 # Flush output before sending the reply.
285 286 sys.stderr.flush()
286 287 sys.stdout.flush()
287 288
288 289 # Send the reply.
289 290 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
290 291 print>>sys.__stdout__, Message(reply_msg)
291 292 self.reply_socket.send(ident, zmq.SNDMORE)
292 293 self.reply_socket.send_json(reply_msg)
293 294 if reply_msg['content']['status'] == u'error':
294 295 self._abort_queue()
295 296
296 297 def complete_request(self, ident, parent):
297 298 comp = self.completer.complete(parent.content.line, parent.content.text)
298 299 matches = {'matches' : comp, 'status' : 'ok'}
299 300 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
300 301 matches, parent, ident)
301 302 print >> sys.__stdout__, completion_msg
302 303
303 304 def object_info_request(self, ident, parent):
304 305 context = parent['content']['oname'].split('.')
305 306 object_info = self._object_info(context)
306 307 msg = self.session.send(self.reply_socket, 'object_info_reply',
307 308 object_info, parent, ident)
308 309 print >> sys.__stdout__, msg
309 310
310 311 #---------------------------------------------------------------------------
311 312 # Protected interface
312 313 #---------------------------------------------------------------------------
313 314
314 315 def _abort_queue(self):
315 316 while True:
316 317 try:
317 318 ident = self.reply_socket.recv(zmq.NOBLOCK)
318 319 except zmq.ZMQError, e:
319 320 if e.errno == zmq.EAGAIN:
320 321 break
321 322 else:
322 323 assert self.reply_socket.rcvmore(), "Missing message part."
323 324 msg = self.reply_socket.recv_json()
324 325 print>>sys.__stdout__, "Aborting:"
325 326 print>>sys.__stdout__, Message(msg)
326 327 msg_type = msg['msg_type']
327 328 reply_type = msg_type.split('_')[0] + '_reply'
328 329 reply_msg = self.session.msg(reply_type, {'status':'aborted'}, msg)
329 330 print>>sys.__stdout__, Message(reply_msg)
330 331 self.reply_socket.send(ident,zmq.SNDMORE)
331 332 self.reply_socket.send_json(reply_msg)
332 333 # We need to wait a bit for requests to come in. This can probably
333 334 # be set shorter for true asynchronous clients.
334 335 time.sleep(0.1)
335 336
336 337 def _raw_input(self, prompt, ident, parent):
337 338 # Flush output before making the request.
338 339 sys.stderr.flush()
339 340 sys.stdout.flush()
340 341
341 342 # Send the input request.
342 343 content = dict(prompt=prompt)
343 344 msg = self.session.msg(u'input_request', content, parent)
344 345 self.req_socket.send_json(msg)
345 346
346 347 # Await a response.
347 348 reply = self.req_socket.recv_json()
348 349 try:
349 350 value = reply['content']['value']
350 351 except:
351 352 print>>sys.__stderr__, "Got bad raw_input reply: "
352 353 print>>sys.__stderr__, Message(parent)
353 354 value = ''
354 355 return value
355 356
356 357 def _object_info(self, context):
357 358 symbol, leftover = self._symbol_from_context(context)
358 359 if symbol is not None and not leftover:
359 360 doc = getattr(symbol, '__doc__', '')
360 361 else:
361 362 doc = ''
362 363 object_info = dict(docstring = doc)
363 364 return object_info
364 365
365 366 def _symbol_from_context(self, context):
366 367 if not context:
367 368 return None, context
368 369
369 370 base_symbol_string = context[0]
370 371 symbol = self.user_ns.get(base_symbol_string, None)
371 372 if symbol is None:
372 373 symbol = __builtin__.__dict__.get(base_symbol_string, None)
373 374 if symbol is None:
374 375 return None, context
375 376
376 377 context = context[1:]
377 378 for i, name in enumerate(context):
378 379 new_symbol = getattr(symbol, name, None)
379 380 if new_symbol is None:
380 381 return symbol, context[i:]
381 382 else:
382 383 symbol = new_symbol
383 384
384 385 return symbol, []
385 386
386 387 #-----------------------------------------------------------------------------
387 388 # Kernel main and launch functions
388 389 #-----------------------------------------------------------------------------
389 390
390 391 class ExitPollerUnix(Thread):
391 392 """ A Unix-specific daemon thread that terminates the program immediately
392 393 when the parent process no longer exists.
393 394 """
394 395
395 396 def __init__(self):
396 397 super(ExitPollerUnix, self).__init__()
397 398 self.daemon = True
398 399
399 400 def run(self):
400 401 # We cannot use os.waitpid because it works only for child processes.
401 402 from errno import EINTR
402 403 while True:
403 404 try:
404 405 if os.getppid() == 1:
405 406 os._exit(1)
406 407 time.sleep(1.0)
407 408 except OSError, e:
408 409 if e.errno == EINTR:
409 410 continue
410 411 raise
411 412
412 413 class ExitPollerWindows(Thread):
413 414 """ A Windows-specific daemon thread that terminates the program immediately
414 415 when a Win32 handle is signaled.
415 416 """
416 417
417 418 def __init__(self, handle):
418 419 super(ExitPollerWindows, self).__init__()
419 420 self.daemon = True
420 421 self.handle = handle
421 422
422 423 def run(self):
423 424 from _subprocess import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
424 425 result = WaitForSingleObject(self.handle, INFINITE)
425 426 if result == WAIT_OBJECT_0:
426 427 os._exit(1)
427 428
428 429
429 430 def bind_port(socket, ip, port):
430 431 """ Binds the specified ZMQ socket. If the port is zero, a random port is
431 432 chosen. Returns the port that was bound.
432 433 """
433 434 connection = 'tcp://%s' % ip
434 435 if port <= 0:
435 436 port = socket.bind_to_random_port(connection)
436 437 else:
437 438 connection += ':%i' % port
438 439 socket.bind(connection)
439 440 return port
440 441
441 442
442 443 def main():
443 444 """ Main entry point for launching a kernel.
444 445 """
445 446 # Parse command line arguments.
446 447 parser = ArgumentParser()
447 448 parser.add_argument('--ip', type=str, default='127.0.0.1',
448 449 help='set the kernel\'s IP address [default: local]')
449 450 parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
450 451 help='set the XREP channel port [default: random]')
451 452 parser.add_argument('--pub', type=int, metavar='PORT', default=0,
452 453 help='set the PUB channel port [default: random]')
453 454 parser.add_argument('--req', type=int, metavar='PORT', default=0,
454 455 help='set the REQ channel port [default: random]')
455 456 if sys.platform == 'win32':
456 457 parser.add_argument('--parent', type=int, metavar='HANDLE',
457 458 default=0, help='kill this process if the process '
458 459 'with HANDLE dies')
459 460 else:
460 461 parser.add_argument('--parent', action='store_true',
461 462 help='kill this process if its parent dies')
462 463 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
463 464 const='auto', help = \
464 465 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
465 466 given, the GUI backend is matplotlib's, otherwise use one of: \
466 467 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
467 468
468 469 namespace = parser.parse_args()
469 470
470 471 # Create a context, a session, and the kernel sockets.
471 472 print >>sys.__stdout__, "Starting the kernel..."
472 473 context = zmq.Context()
473 474 session = Session(username=u'kernel')
474 475
475 476 reply_socket = context.socket(zmq.XREP)
476 477 xrep_port = bind_port(reply_socket, namespace.ip, namespace.xrep)
477 478 print >>sys.__stdout__, "XREP Channel on port", xrep_port
478 479
479 480 pub_socket = context.socket(zmq.PUB)
480 481 pub_port = bind_port(pub_socket, namespace.ip, namespace.pub)
481 482 print >>sys.__stdout__, "PUB Channel on port", pub_port
482 483
483 484 req_socket = context.socket(zmq.XREQ)
484 485 req_port = bind_port(req_socket, namespace.ip, namespace.req)
485 486 print >>sys.__stdout__, "REQ Channel on port", req_port
486 487
487 488 # Create the kernel.
488 489 kernel = Kernel(session, reply_socket, pub_socket, req_socket)
489 490
490 491 # Set up pylab, if necessary.
491 492 if namespace.pylab:
492 493 if namespace.pylab == 'auto':
493 494 kernel.activate_pylab()
494 495 else:
495 496 kernel.activate_pylab(namespace.pylab)
496 497
497 498 # Redirect input streams and set a display hook.
498 499 sys.stdout = OutStream(session, pub_socket, u'stdout')
499 500 sys.stderr = OutStream(session, pub_socket, u'stderr')
500 501 sys.displayhook = DisplayHook(session, pub_socket)
501 502
502 503 # Configure this kernel/process to die on parent termination, if necessary.
503 504 if namespace.parent:
504 505 if sys.platform == 'win32':
505 506 poller = ExitPollerWindows(namespace.parent)
506 507 else:
507 508 poller = ExitPollerUnix()
508 509 poller.start()
509 510
510 511 # Start the kernel mainloop.
511 512 kernel.start()
512 513
513 514
514 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
515 def launch_kernel(xrep_port=0, pub_port=0, req_port=0,
516 pylab=False, independent=False):
515 517 """ Launches a localhost kernel, binding to the specified ports.
516 518
517 519 Parameters
518 520 ----------
519 521 xrep_port : int, optional
520 522 The port to use for XREP channel.
521 523
522 524 pub_port : int, optional
523 525 The port to use for the SUB channel.
524 526
525 527 req_port : int, optional
526 528 The port to use for the REQ (raw input) channel.
527 529
530 pylab : bool or string, optional (default False)
531 If not False, the kernel will be launched with pylab enabled. If a
532 string is passed, matplotlib will use the specified backend. Otherwise,
533 matplotlib's default backend will be used.
534
528 535 independent : bool, optional (default False)
529 536 If set, the kernel process is guaranteed to survive if this process
530 537 dies. If not set, an effort is made to ensure that the kernel is killed
531 538 when this process dies. Note that in this case it is still good practice
532 539 to kill kernels manually before exiting.
533 540
534 541 Returns
535 542 -------
536 543 A tuple of form:
537 544 (kernel_process, xrep_port, pub_port, req_port)
538 545 where kernel_process is a Popen object and the ports are integers.
539 546 """
540 547 import socket
541 548 from subprocess import Popen
542 549
543 550 # Find open ports as necessary.
544 551 ports = []
545 552 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + int(req_port <= 0)
546 553 for i in xrange(ports_needed):
547 554 sock = socket.socket()
548 555 sock.bind(('', 0))
549 556 ports.append(sock)
550 557 for i, sock in enumerate(ports):
551 558 port = sock.getsockname()[1]
552 559 sock.close()
553 560 ports[i] = port
554 561 if xrep_port <= 0:
555 562 xrep_port = ports.pop(0)
556 563 if pub_port <= 0:
557 564 pub_port = ports.pop(0)
558 565 if req_port <= 0:
559 566 req_port = ports.pop(0)
560 567
561 # Spawn a kernel.
568 # Build the kernel launch command.
562 569 command = 'from IPython.zmq.kernel import main; main()'
563 570 arguments = [ sys.executable, '-c', command, '--xrep', str(xrep_port),
564 571 '--pub', str(pub_port), '--req', str(req_port) ]
572 if pylab:
573 arguments.append('--pylab')
574 if isinstance(pylab, basestring):
575 arguments.append(pylab)
576
577 # Spawn a kernel.
565 578 if independent:
566 579 if sys.platform == 'win32':
567 580 proc = Popen(['start', '/b'] + arguments, shell=True)
568 581 else:
569 582 proc = Popen(arguments, preexec_fn=lambda: os.setsid())
570 583 else:
571 584 if sys.platform == 'win32':
572 585 from _subprocess import DuplicateHandle, GetCurrentProcess, \
573 586 DUPLICATE_SAME_ACCESS
574 587 pid = GetCurrentProcess()
575 588 handle = DuplicateHandle(pid, pid, pid, 0,
576 589 True, # Inheritable by new processes.
577 590 DUPLICATE_SAME_ACCESS)
578 591 proc = Popen(arguments + ['--parent', str(int(handle))])
579 592 else:
580 593 proc = Popen(arguments + ['--parent'])
581 594
582 595 return proc, xrep_port, pub_port, req_port
583 596
584 597
585 598 if __name__ == '__main__':
586 599 main()
@@ -1,571 +1,575 b''
1 1 """Base classes to manage the interaction with a running kernel.
2 2
3 3 Todo
4 4 ====
5 5
6 6 * Create logger to handle debugging and console messages.
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2008-2010 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # Standard library imports.
21 21 from Queue import Queue, Empty
22 22 from subprocess import Popen
23 23 from threading import Thread
24 24 import time
25 25
26 26 # System library imports.
27 27 import zmq
28 28 from zmq import POLLIN, POLLOUT, POLLERR
29 29 from zmq.eventloop import ioloop
30 30
31 31 # Local imports.
32 32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 33 from kernel import launch_kernel
34 34 from session import Session
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Constants and exceptions
38 38 #-----------------------------------------------------------------------------
39 39
40 40 LOCALHOST = '127.0.0.1'
41 41
42 42 class InvalidPortNumber(Exception):
43 43 pass
44 44
45 45 #-----------------------------------------------------------------------------
46 46 # ZMQ Socket Channel classes
47 47 #-----------------------------------------------------------------------------
48 48
49 49 class ZmqSocketChannel(Thread):
50 50 """The base class for the channels that use ZMQ sockets.
51 51 """
52 52 context = None
53 53 session = None
54 54 socket = None
55 55 ioloop = None
56 56 iostate = None
57 57 _address = None
58 58
59 59 def __init__(self, context, session, address):
60 60 """Create a channel
61 61
62 62 Parameters
63 63 ----------
64 64 context : :class:`zmq.Context`
65 65 The ZMQ context to use.
66 66 session : :class:`session.Session`
67 67 The session to use.
68 68 address : tuple
69 69 Standard (ip, port) tuple that the kernel is listening on.
70 70 """
71 71 super(ZmqSocketChannel, self).__init__()
72 72 self.daemon = True
73 73
74 74 self.context = context
75 75 self.session = session
76 76 if address[1] == 0:
77 77 message = 'The port number for a channel cannot be 0.'
78 78 raise InvalidPortNumber(message)
79 79 self._address = address
80 80
81 81 def stop(self):
82 82 """Stop the channel's activity.
83 83
84 84 This calls :method:`Thread.join` and returns when the thread
85 85 terminates. :class:`RuntimeError` will be raised if
86 86 :method:`self.start` is called again.
87 87 """
88 88 self.join()
89 89
90 90 @property
91 91 def address(self):
92 92 """Get the channel's address as an (ip, port) tuple.
93 93
94 94 By the default, the address is (localhost, 0), where 0 means a random
95 95 port.
96 96 """
97 97 return self._address
98 98
99 99 def add_io_state(self, state):
100 100 """Add IO state to the eventloop.
101 101
102 102 Parameters
103 103 ----------
104 104 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
105 105 The IO state flag to set.
106 106
107 107 This is thread safe as it uses the thread safe IOLoop.add_callback.
108 108 """
109 109 def add_io_state_callback():
110 110 if not self.iostate & state:
111 111 self.iostate = self.iostate | state
112 112 self.ioloop.update_handler(self.socket, self.iostate)
113 113 self.ioloop.add_callback(add_io_state_callback)
114 114
115 115 def drop_io_state(self, state):
116 116 """Drop IO state from the eventloop.
117 117
118 118 Parameters
119 119 ----------
120 120 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
121 121 The IO state flag to set.
122 122
123 123 This is thread safe as it uses the thread safe IOLoop.add_callback.
124 124 """
125 125 def drop_io_state_callback():
126 126 if self.iostate & state:
127 127 self.iostate = self.iostate & (~state)
128 128 self.ioloop.update_handler(self.socket, self.iostate)
129 129 self.ioloop.add_callback(drop_io_state_callback)
130 130
131 131
132 132 class XReqSocketChannel(ZmqSocketChannel):
133 133 """The XREQ channel for issues request/replies to the kernel.
134 134 """
135 135
136 136 command_queue = None
137 137
138 138 def __init__(self, context, session, address):
139 139 self.command_queue = Queue()
140 140 super(XReqSocketChannel, self).__init__(context, session, address)
141 141
142 142 def run(self):
143 143 """The thread's main activity. Call start() instead."""
144 144 self.socket = self.context.socket(zmq.XREQ)
145 145 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
146 146 self.socket.connect('tcp://%s:%i' % self.address)
147 147 self.ioloop = ioloop.IOLoop()
148 148 self.iostate = POLLERR|POLLIN
149 149 self.ioloop.add_handler(self.socket, self._handle_events,
150 150 self.iostate)
151 151 self.ioloop.start()
152 152
153 153 def stop(self):
154 154 self.ioloop.stop()
155 155 super(XReqSocketChannel, self).stop()
156 156
157 157 def call_handlers(self, msg):
158 158 """This method is called in the ioloop thread when a message arrives.
159 159
160 160 Subclasses should override this method to handle incoming messages.
161 161 It is important to remember that this method is called in the thread
162 162 so that some logic must be done to ensure that the application leve
163 163 handlers are called in the application thread.
164 164 """
165 165 raise NotImplementedError('call_handlers must be defined in a subclass.')
166 166
167 167 def execute(self, code):
168 168 """Execute code in the kernel.
169 169
170 170 Parameters
171 171 ----------
172 172 code : str
173 173 A string of Python code.
174 174
175 175 Returns
176 176 -------
177 177 The msg_id of the message sent.
178 178 """
179 179 # Create class for content/msg creation. Related to, but possibly
180 180 # not in Session.
181 181 content = dict(code=code)
182 182 msg = self.session.msg('execute_request', content)
183 183 self._queue_request(msg)
184 184 return msg['header']['msg_id']
185 185
186 186 def complete(self, text, line, block=None):
187 187 """Tab complete text, line, block in the kernel's namespace.
188 188
189 189 Parameters
190 190 ----------
191 191 text : str
192 192 The text to complete.
193 193 line : str
194 194 The full line of text that is the surrounding context for the
195 195 text to complete.
196 196 block : str
197 197 The full block of code in which the completion is being requested.
198 198
199 199 Returns
200 200 -------
201 201 The msg_id of the message sent.
202 202 """
203 203 content = dict(text=text, line=line)
204 204 msg = self.session.msg('complete_request', content)
205 205 self._queue_request(msg)
206 206 return msg['header']['msg_id']
207 207
208 208 def object_info(self, oname):
209 209 """Get metadata information about an object.
210 210
211 211 Parameters
212 212 ----------
213 213 oname : str
214 214 A string specifying the object name.
215 215
216 216 Returns
217 217 -------
218 218 The msg_id of the message sent.
219 219 """
220 220 content = dict(oname=oname)
221 221 msg = self.session.msg('object_info_request', content)
222 222 self._queue_request(msg)
223 223 return msg['header']['msg_id']
224 224
225 225 def _handle_events(self, socket, events):
226 226 if events & POLLERR:
227 227 self._handle_err()
228 228 if events & POLLOUT:
229 229 self._handle_send()
230 230 if events & POLLIN:
231 231 self._handle_recv()
232 232
233 233 def _handle_recv(self):
234 234 msg = self.socket.recv_json()
235 235 self.call_handlers(msg)
236 236
237 237 def _handle_send(self):
238 238 try:
239 239 msg = self.command_queue.get(False)
240 240 except Empty:
241 241 pass
242 242 else:
243 243 self.socket.send_json(msg)
244 244 if self.command_queue.empty():
245 245 self.drop_io_state(POLLOUT)
246 246
247 247 def _handle_err(self):
248 248 # We don't want to let this go silently, so eventually we should log.
249 249 raise zmq.ZMQError()
250 250
251 251 def _queue_request(self, msg):
252 252 self.command_queue.put(msg)
253 253 self.add_io_state(POLLOUT)
254 254
255 255
256 256 class SubSocketChannel(ZmqSocketChannel):
257 257 """The SUB channel which listens for messages that the kernel publishes.
258 258 """
259 259
260 260 def __init__(self, context, session, address):
261 261 super(SubSocketChannel, self).__init__(context, session, address)
262 262
263 263 def run(self):
264 264 """The thread's main activity. Call start() instead."""
265 265 self.socket = self.context.socket(zmq.SUB)
266 266 self.socket.setsockopt(zmq.SUBSCRIBE,'')
267 267 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
268 268 self.socket.connect('tcp://%s:%i' % self.address)
269 269 self.ioloop = ioloop.IOLoop()
270 270 self.iostate = POLLIN|POLLERR
271 271 self.ioloop.add_handler(self.socket, self._handle_events,
272 272 self.iostate)
273 273 self.ioloop.start()
274 274
275 275 def stop(self):
276 276 self.ioloop.stop()
277 277 super(SubSocketChannel, self).stop()
278 278
279 279 def call_handlers(self, msg):
280 280 """This method is called in the ioloop thread when a message arrives.
281 281
282 282 Subclasses should override this method to handle incoming messages.
283 283 It is important to remember that this method is called in the thread
284 284 so that some logic must be done to ensure that the application leve
285 285 handlers are called in the application thread.
286 286 """
287 287 raise NotImplementedError('call_handlers must be defined in a subclass.')
288 288
289 289 def flush(self, timeout=1.0):
290 290 """Immediately processes all pending messages on the SUB channel.
291 291
292 292 Callers should use this method to ensure that :method:`call_handlers`
293 293 has been called for all messages that have been received on the
294 294 0MQ SUB socket of this channel.
295 295
296 296 This method is thread safe.
297 297
298 298 Parameters
299 299 ----------
300 300 timeout : float, optional
301 301 The maximum amount of time to spend flushing, in seconds. The
302 302 default is one second.
303 303 """
304 304 # We do the IOLoop callback process twice to ensure that the IOLoop
305 305 # gets to perform at least one full poll.
306 306 stop_time = time.time() + timeout
307 307 for i in xrange(2):
308 308 self._flushed = False
309 309 self.ioloop.add_callback(self._flush)
310 310 while not self._flushed and time.time() < stop_time:
311 311 time.sleep(0.01)
312 312
313 313 def _handle_events(self, socket, events):
314 314 # Turn on and off POLLOUT depending on if we have made a request
315 315 if events & POLLERR:
316 316 self._handle_err()
317 317 if events & POLLIN:
318 318 self._handle_recv()
319 319
320 320 def _handle_err(self):
321 321 # We don't want to let this go silently, so eventually we should log.
322 322 raise zmq.ZMQError()
323 323
324 324 def _handle_recv(self):
325 325 # Get all of the messages we can
326 326 while True:
327 327 try:
328 328 msg = self.socket.recv_json(zmq.NOBLOCK)
329 329 except zmq.ZMQError:
330 330 # Check the errno?
331 331 # Will this trigger POLLERR?
332 332 break
333 333 else:
334 334 self.call_handlers(msg)
335 335
336 336 def _flush(self):
337 337 """Callback for :method:`self.flush`."""
338 338 self._flushed = True
339 339
340 340
341 341 class RepSocketChannel(ZmqSocketChannel):
342 342 """A reply channel to handle raw_input requests that the kernel makes."""
343 343
344 344 msg_queue = None
345 345
346 346 def __init__(self, context, session, address):
347 347 self.msg_queue = Queue()
348 348 super(RepSocketChannel, self).__init__(context, session, address)
349 349
350 350 def run(self):
351 351 """The thread's main activity. Call start() instead."""
352 352 self.socket = self.context.socket(zmq.XREQ)
353 353 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
354 354 self.socket.connect('tcp://%s:%i' % self.address)
355 355 self.ioloop = ioloop.IOLoop()
356 356 self.iostate = POLLERR|POLLIN
357 357 self.ioloop.add_handler(self.socket, self._handle_events,
358 358 self.iostate)
359 359 self.ioloop.start()
360 360
361 361 def stop(self):
362 362 self.ioloop.stop()
363 363 super(RepSocketChannel, self).stop()
364 364
365 365 def call_handlers(self, msg):
366 366 """This method is called in the ioloop thread when a message arrives.
367 367
368 368 Subclasses should override this method to handle incoming messages.
369 369 It is important to remember that this method is called in the thread
370 370 so that some logic must be done to ensure that the application leve
371 371 handlers are called in the application thread.
372 372 """
373 373 raise NotImplementedError('call_handlers must be defined in a subclass.')
374 374
375 375 def input(self, string):
376 376 """Send a string of raw input to the kernel."""
377 377 content = dict(value=string)
378 378 msg = self.session.msg('input_reply', content)
379 379 self._queue_reply(msg)
380 380
381 381 def _handle_events(self, socket, events):
382 382 if events & POLLERR:
383 383 self._handle_err()
384 384 if events & POLLOUT:
385 385 self._handle_send()
386 386 if events & POLLIN:
387 387 self._handle_recv()
388 388
389 389 def _handle_recv(self):
390 390 msg = self.socket.recv_json()
391 391 self.call_handlers(msg)
392 392
393 393 def _handle_send(self):
394 394 try:
395 395 msg = self.msg_queue.get(False)
396 396 except Empty:
397 397 pass
398 398 else:
399 399 self.socket.send_json(msg)
400 400 if self.msg_queue.empty():
401 401 self.drop_io_state(POLLOUT)
402 402
403 403 def _handle_err(self):
404 404 # We don't want to let this go silently, so eventually we should log.
405 405 raise zmq.ZMQError()
406 406
407 407 def _queue_reply(self, msg):
408 408 self.msg_queue.put(msg)
409 409 self.add_io_state(POLLOUT)
410 410
411 411
412 412 #-----------------------------------------------------------------------------
413 413 # Main kernel manager class
414 414 #-----------------------------------------------------------------------------
415 415
416 416 class KernelManager(HasTraits):
417 417 """ Manages a kernel for a frontend.
418 418
419 419 The SUB channel is for the frontend to receive messages published by the
420 420 kernel.
421 421
422 422 The REQ channel is for the frontend to make requests of the kernel.
423 423
424 424 The REP channel is for the kernel to request stdin (raw_input) from the
425 425 frontend.
426 426 """
427 427 # The PyZMQ Context to use for communication with the kernel.
428 428 context = Instance(zmq.Context,(),{})
429 429
430 430 # The Session to use for communication with the kernel.
431 431 session = Instance(Session,(),{})
432 432
433 433 # The kernel process with which the KernelManager is communicating.
434 434 kernel = Instance(Popen)
435 435
436 # The addresses for the communication channels.
437 xreq_address = TCPAddress((LOCALHOST, 0))
438 sub_address = TCPAddress((LOCALHOST, 0))
439 rep_address = TCPAddress((LOCALHOST, 0))
440
436 441 # The classes to use for the various channels.
437 442 xreq_channel_class = Type(XReqSocketChannel)
438 443 sub_channel_class = Type(SubSocketChannel)
439 444 rep_channel_class = Type(RepSocketChannel)
440 445
441 446 # Protected traits.
442 xreq_address = TCPAddress((LOCALHOST, 0))
443 sub_address = TCPAddress((LOCALHOST, 0))
444 rep_address = TCPAddress((LOCALHOST, 0))
445 447 _xreq_channel = Any
446 448 _sub_channel = Any
447 449 _rep_channel = Any
448 450
449 def __init__(self, **kwargs):
450 super(KernelManager, self).__init__(**kwargs)
451
452 451 #--------------------------------- -----------------------------------------
453 452 # Channel management methods:
454 453 #--------------------------------------------------------------------------
455 454
456 455 def start_channels(self):
457 456 """Starts the channels for this kernel.
458 457
459 458 This will create the channels if they do not exist and then start
460 459 them. If port numbers of 0 are being used (random ports) then you
461 460 must first call :method:`start_kernel`. If the channels have been
462 461 stopped and you call this, :class:`RuntimeError` will be raised.
463 462 """
464 463 self.xreq_channel.start()
465 464 self.sub_channel.start()
466 465 self.rep_channel.start()
467 466
468 467 def stop_channels(self):
469 468 """Stops the channels for this kernel.
470 469
471 470 This stops the channels by joining their threads. If the channels
472 471 were not started, :class:`RuntimeError` will be raised.
473 472 """
474 473 self.xreq_channel.stop()
475 474 self.sub_channel.stop()
476 475 self.rep_channel.stop()
477 476
478 477 @property
479 478 def channels_running(self):
480 479 """Are all of the channels created and running?"""
481 480 return self.xreq_channel.is_alive() \
482 481 and self.sub_channel.is_alive() \
483 482 and self.rep_channel.is_alive()
484 483
485 484 #--------------------------------------------------------------------------
486 485 # Kernel process management methods:
487 486 #--------------------------------------------------------------------------
488 487
489 def start_kernel(self):
488 def start_kernel(self, pylab=False):
490 489 """Starts a kernel process and configures the manager to use it.
491 490
492 491 If random ports (port=0) are being used, this method must be called
493 492 before the channels are created.
493
494 Parameters:
495 -----------
496 pylab : bool or string, optional (default False)
497 See IPython.zmq.kernel.launch_kernel for documentation.
494 498 """
495 499 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
496 500 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
497 501 raise RuntimeError("Can only launch a kernel on localhost."
498 502 "Make sure that the '*_address' attributes are "
499 503 "configured properly.")
500 504
501 505 self.kernel, xrep, pub, req = launch_kernel(
502 xrep_port=xreq[1], pub_port=sub[1], req_port=rep[1])
506 xrep_port=xreq[1], pub_port=sub[1], req_port=rep[1], pylab=pylab)
503 507 self.xreq_address = (LOCALHOST, xrep)
504 508 self.sub_address = (LOCALHOST, pub)
505 509 self.rep_address = (LOCALHOST, req)
506 510
507 511 @property
508 512 def has_kernel(self):
509 513 """Returns whether a kernel process has been specified for the kernel
510 514 manager.
511 515 """
512 516 return self.kernel is not None
513 517
514 518 def kill_kernel(self):
515 519 """ Kill the running kernel. """
516 520 if self.kernel is not None:
517 521 self.kernel.kill()
518 522 self.kernel = None
519 523 else:
520 524 raise RuntimeError("Cannot kill kernel. No kernel is running!")
521 525
522 526 def signal_kernel(self, signum):
523 527 """ Sends a signal to the kernel. """
524 528 if self.kernel is not None:
525 529 self.kernel.send_signal(signum)
526 530 else:
527 531 raise RuntimeError("Cannot signal kernel. No kernel is running!")
528 532
529 533 @property
530 534 def is_alive(self):
531 535 """Is the kernel process still running?"""
532 536 if self.kernel is not None:
533 537 if self.kernel.poll() is None:
534 538 return True
535 539 else:
536 540 return False
537 541 else:
538 542 # We didn't start the kernel with this KernelManager so we don't
539 543 # know if it is running. We should use a heartbeat for this case.
540 544 return True
541 545
542 546 #--------------------------------------------------------------------------
543 547 # Channels used for communication with the kernel:
544 548 #--------------------------------------------------------------------------
545 549
546 550 @property
547 551 def xreq_channel(self):
548 552 """Get the REQ socket channel object to make requests of the kernel."""
549 553 if self._xreq_channel is None:
550 554 self._xreq_channel = self.xreq_channel_class(self.context,
551 555 self.session,
552 556 self.xreq_address)
553 557 return self._xreq_channel
554 558
555 559 @property
556 560 def sub_channel(self):
557 561 """Get the SUB socket channel object."""
558 562 if self._sub_channel is None:
559 563 self._sub_channel = self.sub_channel_class(self.context,
560 564 self.session,
561 565 self.sub_address)
562 566 return self._sub_channel
563 567
564 568 @property
565 569 def rep_channel(self):
566 570 """Get the REP socket channel object to handle stdin (raw_input)."""
567 571 if self._rep_channel is None:
568 572 self._rep_channel = self.rep_channel_class(self.context,
569 573 self.session,
570 574 self.rep_address)
571 575 return self._rep_channel
@@ -1,25 +1,25 b''
1 1 # Standard library imports
2 2 from cStringIO import StringIO
3 3
4 4 # System library imports.
5 5 from matplotlib.backends.backend_svg import new_figure_manager
6 6 from matplotlib._pylab_helpers import Gcf
7 7
8 8 # Local imports.
9 9 from backend_payload import add_plot_payload
10 10
11 11
12 12 def show():
13 13 """ Deliver a SVG payload.
14 14 """
15 figure_manager = Gcf.get_actve()
15 figure_manager = Gcf.get_active()
16 16 if figure_manager is not None:
17 17 data = svg_from_canvas(figure_manager.canvas)
18 18 add_plot_payload('svg', data)
19 19
20 20 def svg_from_canvas(canvas):
21 21 """ Return a string containing the SVG representation of a FigureCanvasSvg.
22 22 """
23 23 string_io = StringIO()
24 24 canvas.print_svg(string_io)
25 25 return string_io.getvalue()
General Comments 0
You need to be logged in to leave comments. Login now