##// END OF EJS Templates
* Updated prompt request code to support the new silent execution mechanism....
epatters -
Show More
@@ -1,466 +1,479 b''
1 1 # Standard library imports
2 from collections import namedtuple
2 3 import signal
3 4 import sys
4 5
5 6 # System library imports
6 7 from pygments.lexers import PythonLexer
7 8 from PyQt4 import QtCore, QtGui
8 9
9 10 # Local imports
10 11 from IPython.core.inputsplitter import InputSplitter
11 12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 13 from IPython.utils.traitlets import Bool
13 14 from bracket_matcher import BracketMatcher
14 15 from call_tip_widget import CallTipWidget
15 16 from completion_lexer import CompletionLexer
16 17 from console_widget import HistoryConsoleWidget
17 18 from pygments_highlighter import PygmentsHighlighter
18 19
19 20
20 21 class FrontendHighlighter(PygmentsHighlighter):
21 22 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 23 prompts.
23 24 """
24 25
25 26 def __init__(self, frontend):
26 27 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 28 self._current_offset = 0
28 29 self._frontend = frontend
29 30 self.highlighting_on = False
30 31
31 32 def highlightBlock(self, qstring):
32 33 """ Highlight a block of text. Reimplemented to highlight selectively.
33 34 """
34 35 if not self.highlighting_on:
35 36 return
36 37
37 38 # The input to this function is unicode string that may contain
38 39 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 40 # the string as plain text so we can compare it.
40 41 current_block = self.currentBlock()
41 42 string = self._frontend._get_block_plain_text(current_block)
42 43
43 44 # Decide whether to check for the regular or continuation prompt.
44 45 if current_block.contains(self._frontend._prompt_pos):
45 46 prompt = self._frontend._prompt
46 47 else:
47 48 prompt = self._frontend._continuation_prompt
48 49
49 50 # Don't highlight the part of the string that contains the prompt.
50 51 if string.startswith(prompt):
51 52 self._current_offset = len(prompt)
52 53 qstring.remove(0, len(prompt))
53 54 else:
54 55 self._current_offset = 0
55 56
56 57 PygmentsHighlighter.highlightBlock(self, qstring)
57 58
58 59 def rehighlightBlock(self, block):
59 60 """ Reimplemented to temporarily enable highlighting if disabled.
60 61 """
61 62 old = self.highlighting_on
62 63 self.highlighting_on = True
63 64 super(FrontendHighlighter, self).rehighlightBlock(block)
64 65 self.highlighting_on = old
65 66
66 67 def setFormat(self, start, count, format):
67 68 """ Reimplemented to highlight selectively.
68 69 """
69 70 start += self._current_offset
70 71 PygmentsHighlighter.setFormat(self, start, count, format)
71 72
72 73
73 74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 75 """ A Qt frontend for a generic Python kernel.
75 76 """
76 77
77 78 # An option and corresponding signal for overriding the default kernel
78 79 # interrupt behavior.
79 80 custom_interrupt = Bool(False)
80 81 custom_interrupt_requested = QtCore.pyqtSignal()
81 82
82 83 # An option and corresponding signals for overriding the default kernel
83 84 # restart behavior.
84 85 custom_restart = Bool(False)
85 86 custom_restart_kernel_died = QtCore.pyqtSignal(float)
86 87 custom_restart_requested = QtCore.pyqtSignal()
87 88
88 89 # Emitted when an 'execute_reply' has been received from the kernel and
89 90 # processed by the FrontendWidget.
90 91 executed = QtCore.pyqtSignal(object)
91 92
92 93 # Protected class variables.
94 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
95 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
96 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
93 97 _input_splitter_class = InputSplitter
94 98
95 99 #---------------------------------------------------------------------------
96 100 # 'object' interface
97 101 #---------------------------------------------------------------------------
98 102
99 103 def __init__(self, *args, **kw):
100 104 super(FrontendWidget, self).__init__(*args, **kw)
101 105
102 106 # FrontendWidget protected variables.
103 107 self._bracket_matcher = BracketMatcher(self._control)
104 108 self._call_tip_widget = CallTipWidget(self._control)
105 109 self._completion_lexer = CompletionLexer(PythonLexer())
106 110 self._hidden = False
107 111 self._highlighter = FrontendHighlighter(self)
108 112 self._input_splitter = self._input_splitter_class(input_mode='block')
109 113 self._kernel_manager = None
110 114 self._possible_kernel_restart = False
115 self._request_info = {}
111 116
112 117 # Configure the ConsoleWidget.
113 118 self.tab_width = 4
114 119 self._set_continuation_prompt('... ')
115 120
116 121 # Connect signal handlers.
117 122 document = self._control.document()
118 123 document.contentsChange.connect(self._document_contents_change)
119 124
120 125 #---------------------------------------------------------------------------
121 126 # 'ConsoleWidget' abstract interface
122 127 #---------------------------------------------------------------------------
123 128
124 129 def _is_complete(self, source, interactive):
125 130 """ Returns whether 'source' can be completely processed and a new
126 131 prompt created. When triggered by an Enter/Return key press,
127 132 'interactive' is True; otherwise, it is False.
128 133 """
129 134 complete = self._input_splitter.push(source.expandtabs(4))
130 135 if interactive:
131 136 complete = not self._input_splitter.push_accepts_more()
132 137 return complete
133 138
134 139 def _execute(self, source, hidden, user_variables=None,
135 140 user_expressions=None):
136 141 """ Execute 'source'. If 'hidden', do not show any output.
137 142
138 143 See parent class :meth:`execute` docstring for full details.
139 144 """
140 145 # tmp code for testing, disable in real use with 'if 0'. Only delete
141 146 # this code once we have automated tests for these fields.
142 147 if 0:
143 148 user_variables = ['x', 'y', 'z']
144 149 user_expressions = {'sum' : '1+1',
145 150 'bad syntax' : 'klsdafj kasd f',
146 151 'bad call' : 'range("hi")',
147 152 'time' : 'time.time()',
148 153 }
149 154 # /end tmp code
150 155
151 156 # FIXME - user_variables/expressions are not visible in API above us.
152 self.kernel_manager.xreq_channel.execute(source, hidden,
153 user_variables,
154 user_expressions)
157 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden,
158 user_variables,
159 user_expressions)
160 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
155 161 self._hidden = hidden
156 162
157 163 def _prompt_started_hook(self):
158 164 """ Called immediately after a new prompt is displayed.
159 165 """
160 166 if not self._reading:
161 167 self._highlighter.highlighting_on = True
162 168
163 169 def _prompt_finished_hook(self):
164 170 """ Called immediately after a prompt is finished, i.e. when some input
165 171 will be processed and a new prompt displayed.
166 172 """
167 173 if not self._reading:
168 174 self._highlighter.highlighting_on = False
169 175
170 176 def _tab_pressed(self):
171 177 """ Called when the tab key is pressed. Returns whether to continue
172 178 processing the event.
173 179 """
174 180 # Perform tab completion if:
175 181 # 1) The cursor is in the input buffer.
176 182 # 2) There is a non-whitespace character before the cursor.
177 183 text = self._get_input_buffer_cursor_line()
178 184 if text is None:
179 185 return False
180 186 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
181 187 if complete:
182 188 self._complete()
183 189 return not complete
184 190
185 191 #---------------------------------------------------------------------------
186 192 # 'ConsoleWidget' protected interface
187 193 #---------------------------------------------------------------------------
188 194
189 195 def _event_filter_console_keypress(self, event):
190 196 """ Reimplemented to allow execution interruption.
191 197 """
192 198 key = event.key()
193 199 if self._control_key_down(event.modifiers()):
194 200 if key == QtCore.Qt.Key_C and self._executing:
195 201 self.interrupt_kernel()
196 202 return True
197 203 elif key == QtCore.Qt.Key_Period:
198 204 message = 'Are you sure you want to restart the kernel?'
199 205 self.restart_kernel(message)
200 206 return True
201 207 return super(FrontendWidget, self)._event_filter_console_keypress(event)
202 208
203 209 def _insert_continuation_prompt(self, cursor):
204 210 """ Reimplemented for auto-indentation.
205 211 """
206 212 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
207 213 spaces = self._input_splitter.indent_spaces
208 214 cursor.insertText('\t' * (spaces / self.tab_width))
209 215 cursor.insertText(' ' * (spaces % self.tab_width))
210 216
211 217 #---------------------------------------------------------------------------
212 218 # 'BaseFrontendMixin' abstract interface
213 219 #---------------------------------------------------------------------------
214 220
215 221 def _handle_complete_reply(self, rep):
216 222 """ Handle replies for tab completion.
217 223 """
218 224 cursor = self._get_cursor()
219 if rep['parent_header']['msg_id'] == self._complete_id and \
220 cursor.position() == self._complete_pos:
225 info = self._request_info.get('complete')
226 if info and info.id == rep['parent_header']['msg_id'] and \
227 info.pos == cursor.position():
221 228 text = '.'.join(self._get_context())
222 229 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
223 230 self._complete_with_items(cursor, rep['content']['matches'])
224 231
225 232 def _handle_execute_reply(self, msg):
226 233 """ Handles replies for code execution.
227 234 """
228 if not self._hidden:
235 info = self._request_info.get('execute')
236 if info and info.id == msg['parent_header']['msg_id'] and \
237 not self._hidden:
229 238 # Make sure that all output from the SUB channel has been processed
230 239 # before writing a new prompt.
231 240 self.kernel_manager.sub_channel.flush()
232 241
233 242 content = msg['content']
234 243 status = content['status']
235 244 if status == 'ok':
236 245 self._process_execute_ok(msg)
237 246 elif status == 'error':
238 247 self._process_execute_error(msg)
239 248 elif status == 'abort':
240 249 self._process_execute_abort(msg)
241 250
242 251 self._show_interpreter_prompt_for_reply(msg)
243 252 self.executed.emit(msg)
244 253
245 254 def _handle_input_request(self, msg):
246 255 """ Handle requests for raw_input.
247 256 """
248 257 if self._hidden:
249 258 raise RuntimeError('Request for raw input during hidden execution.')
250 259
251 260 # Make sure that all output from the SUB channel has been processed
252 261 # before entering readline mode.
253 262 self.kernel_manager.sub_channel.flush()
254 263
255 264 def callback(line):
256 265 self.kernel_manager.rep_channel.input(line)
257 266 self._readline(msg['content']['prompt'], callback=callback)
258 267
259 268 def _handle_kernel_died(self, since_last_heartbeat):
260 269 """ Handle the kernel's death by asking if the user wants to restart.
261 270 """
262 271 message = 'The kernel heartbeat has been inactive for %.2f ' \
263 272 'seconds. Do you want to restart the kernel? You may ' \
264 273 'first want to check the network connection.' % \
265 274 since_last_heartbeat
266 275 if self.custom_restart:
267 276 self.custom_restart_kernel_died.emit(since_last_heartbeat)
268 277 else:
269 278 self.restart_kernel(message)
270 279
271 280 def _handle_object_info_reply(self, rep):
272 281 """ Handle replies for call tips.
273 282 """
274 283 cursor = self._get_cursor()
275 if rep['parent_header']['msg_id'] == self._call_tip_id and \
276 cursor.position() == self._call_tip_pos:
284 info = self._request_info.get('call_tip')
285 if info and info.id == rep['parent_header']['msg_id'] and \
286 info.pos == cursor.position():
277 287 doc = rep['content']['docstring']
278 288 if doc:
279 289 self._call_tip_widget.show_docstring(doc)
280 290
281 291 def _handle_pyout(self, msg):
282 292 """ Handle display hook output.
283 293 """
284 294 if not self._hidden and self._is_from_this_session(msg):
285 295 self._append_plain_text(msg['content']['data'] + '\n')
286 296
287 297 def _handle_stream(self, msg):
288 298 """ Handle stdout, stderr, and stdin.
289 299 """
290 300 if not self._hidden and self._is_from_this_session(msg):
291 301 self._append_plain_text(msg['content']['data'])
292 302 self._control.moveCursor(QtGui.QTextCursor.End)
293 303
294 304 def _started_channels(self):
295 305 """ Called when the KernelManager channels have started listening or
296 306 when the frontend is assigned an already listening KernelManager.
297 307 """
298 308 self._control.clear()
299 309 self._append_plain_text(self._get_banner())
300 310 self._show_interpreter_prompt()
301 311
302 312 def _stopped_channels(self):
303 313 """ Called when the KernelManager channels have stopped listening or
304 314 when a listening KernelManager is removed from the frontend.
305 315 """
306 316 self._executing = self._reading = False
307 317 self._highlighter.highlighting_on = False
308 318
309 319 #---------------------------------------------------------------------------
310 320 # 'FrontendWidget' interface
311 321 #---------------------------------------------------------------------------
312 322
313 323 def execute_file(self, path, hidden=False):
314 324 """ Attempts to execute file with 'path'. If 'hidden', no output is
315 325 shown.
316 326 """
317 327 self.execute('execfile("%s")' % path, hidden=hidden)
318 328
319 329 def interrupt_kernel(self):
320 330 """ Attempts to interrupt the running kernel.
321 331 """
322 332 if self.custom_interrupt:
323 333 self.custom_interrupt_requested.emit()
324 334 elif self.kernel_manager.has_kernel:
325 335 self.kernel_manager.signal_kernel(signal.SIGINT)
326 336 else:
327 337 self._append_plain_text('Kernel process is either remote or '
328 338 'unspecified. Cannot interrupt.\n')
329 339
330 340 def restart_kernel(self, message):
331 341 """ Attempts to restart the running kernel.
332 342 """
333 343 # We want to make sure that if this dialog is already happening, that
334 344 # other signals don't trigger it again. This can happen when the
335 345 # kernel_died heartbeat signal is emitted and the user is slow to
336 346 # respond to the dialog.
337 347 if not self._possible_kernel_restart:
338 348 if self.custom_restart:
339 349 self.custom_restart_requested.emit()
340 350 elif self.kernel_manager.has_kernel:
341 351 # Setting this to True will prevent this logic from happening
342 352 # again until the current pass is completed.
343 353 self._possible_kernel_restart = True
344 354 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
345 355 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
346 356 message, buttons)
347 357 if result == QtGui.QMessageBox.Yes:
348 358 try:
349 359 self.kernel_manager.restart_kernel()
350 360 except RuntimeError:
351 361 message = 'Kernel started externally. Cannot restart.\n'
352 362 self._append_plain_text(message)
353 363 else:
354 364 self._stopped_channels()
355 365 self._append_plain_text('Kernel restarting...\n')
356 366 self._show_interpreter_prompt()
357 367 # This might need to be moved to another location?
358 368 self._possible_kernel_restart = False
359 369 else:
360 370 self._append_plain_text('Kernel process is either remote or '
361 371 'unspecified. Cannot restart.\n')
362 372
363 373 #---------------------------------------------------------------------------
364 374 # 'FrontendWidget' protected interface
365 375 #---------------------------------------------------------------------------
366 376
367 377 def _call_tip(self):
368 378 """ Shows a call tip, if appropriate, at the current cursor location.
369 379 """
370 380 # Decide if it makes sense to show a call tip
371 381 cursor = self._get_cursor()
372 382 cursor.movePosition(QtGui.QTextCursor.Left)
373 383 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
374 384 return False
375 385 context = self._get_context(cursor)
376 386 if not context:
377 387 return False
378 388
379 389 # Send the metadata request to the kernel
380 390 name = '.'.join(context)
381 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
382 self._call_tip_pos = self._get_cursor().position()
391 msg_id = self.kernel_manager.xreq_channel.object_info(name)
392 pos = self._get_cursor().position()
393 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
383 394 return True
384 395
385 396 def _complete(self):
386 397 """ Performs completion at the current cursor location.
387 398 """
388 399 context = self._get_context()
389 400 if context:
390 401 # Send the completion request to the kernel
391 self._complete_id = self.kernel_manager.xreq_channel.complete(
402 msg_id = self.kernel_manager.xreq_channel.complete(
392 403 '.'.join(context), # text
393 404 self._get_input_buffer_cursor_line(), # line
394 405 self._get_input_buffer_cursor_column(), # cursor_pos
395 406 self.input_buffer) # block
396 self._complete_pos = self._get_cursor().position()
407 pos = self._get_cursor().position()
408 info = self._CompletionRequest(msg_id, pos)
409 self._request_info['complete'] = info
397 410
398 411 def _get_banner(self):
399 412 """ Gets a banner to display at the beginning of a session.
400 413 """
401 414 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
402 415 '"license" for more information.'
403 416 return banner % (sys.version, sys.platform)
404 417
405 418 def _get_context(self, cursor=None):
406 419 """ Gets the context for the specified cursor (or the current cursor
407 420 if none is specified).
408 421 """
409 422 if cursor is None:
410 423 cursor = self._get_cursor()
411 424 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
412 425 QtGui.QTextCursor.KeepAnchor)
413 426 text = str(cursor.selection().toPlainText())
414 427 return self._completion_lexer.get_context(text)
415 428
416 429 def _process_execute_abort(self, msg):
417 430 """ Process a reply for an aborted execution request.
418 431 """
419 432 self._append_plain_text("ERROR: execution aborted\n")
420 433
421 434 def _process_execute_error(self, msg):
422 435 """ Process a reply for an execution request that resulted in an error.
423 436 """
424 437 content = msg['content']
425 438 traceback = ''.join(content['traceback'])
426 439 self._append_plain_text(traceback)
427 440
428 441 def _process_execute_ok(self, msg):
429 442 """ Process a reply for a successful execution equest.
430 443 """
431 444 payload = msg['content']['payload']
432 445 for item in payload:
433 446 if not self._process_execute_payload(item):
434 447 warning = 'Received unknown payload of type %s\n'
435 448 self._append_plain_text(warning % repr(item['source']))
436 449
437 450 def _process_execute_payload(self, item):
438 451 """ Process a single payload item from the list of payload items in an
439 452 execution reply. Returns whether the payload was handled.
440 453 """
441 454 # The basic FrontendWidget doesn't handle payloads, as they are a
442 455 # mechanism for going beyond the standard Python interpreter model.
443 456 return False
444 457
445 458 def _show_interpreter_prompt(self):
446 459 """ Shows a prompt for the interpreter.
447 460 """
448 461 self._show_prompt('>>> ')
449 462
450 463 def _show_interpreter_prompt_for_reply(self, msg):
451 464 """ Shows a prompt for the interpreter given an 'execute_reply' message.
452 465 """
453 466 self._show_interpreter_prompt()
454 467
455 468 #------ Signal handlers ----------------------------------------------------
456 469
457 470 def _document_contents_change(self, position, removed, added):
458 471 """ Called whenever the document's content changes. Display a call tip
459 472 if appropriate.
460 473 """
461 474 # Calculate where the cursor should be *after* the change:
462 475 position += added
463 476
464 477 document = self._control.document()
465 478 if position == self._get_cursor().position():
466 479 self._call_tip()
@@ -1,408 +1,414 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 # Standard library imports
14 14 from collections import namedtuple
15 15 import re
16 16 from subprocess import Popen
17 17
18 18 # System library imports
19 19 from PyQt4 import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter
23 23 from IPython.core.usage import default_banner
24 from IPython.utils import io
25 24 from IPython.utils.traitlets import Bool, Str
26 25 from frontend_widget import FrontendWidget
27 26
28 27 #-----------------------------------------------------------------------------
29 28 # Constants
30 29 #-----------------------------------------------------------------------------
31 30
32 31 # The default light style sheet: black text on a white background.
33 32 default_light_style_sheet = '''
34 33 .error { color: red; }
35 34 .in-prompt { color: navy; }
36 35 .in-prompt-number { font-weight: bold; }
37 36 .out-prompt { color: darkred; }
38 37 .out-prompt-number { font-weight: bold; }
39 38 '''
40 39 default_light_syntax_style = 'default'
41 40
42 41 # The default dark style sheet: white text on a black background.
43 42 default_dark_style_sheet = '''
44 43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
45 44 QFrame { border: 1px solid grey; }
46 45 .error { color: red; }
47 46 .in-prompt { color: lime; }
48 47 .in-prompt-number { color: lime; font-weight: bold; }
49 48 .out-prompt { color: red; }
50 49 .out-prompt-number { color: red; font-weight: bold; }
51 50 '''
52 51 default_dark_syntax_style = 'monokai'
53 52
54 53 # Default strings to build and display input and output prompts (and separators
55 54 # in between)
56 55 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
57 56 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
58 57 default_input_sep = '\n'
59 58 default_output_sep = ''
60 59 default_output_sep2 = ''
61 60
62 61 #-----------------------------------------------------------------------------
63 62 # IPythonWidget class
64 63 #-----------------------------------------------------------------------------
65 64
66 65 class IPythonWidget(FrontendWidget):
67 66 """ A FrontendWidget for an IPython kernel.
68 67 """
69 68
70 69 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
71 70 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
72 71 # settings.
73 72 custom_edit = Bool(False)
74 73 custom_edit_requested = QtCore.pyqtSignal(object, object)
75 74
76 75 # A command for invoking a system text editor. If the string contains a
77 76 # {filename} format specifier, it will be used. Otherwise, the filename will
78 77 # be appended to the end the command.
79 78 editor = Str('default', config=True)
80 79
81 80 # The editor command to use when a specific line number is requested. The
82 81 # string should contain two format specifiers: {line} and {filename}. If
83 82 # this parameter is not specified, the line number option to the %edit magic
84 83 # will be ignored.
85 84 editor_line = Str(config=True)
86 85
87 86 # A CSS stylesheet. The stylesheet can contain classes for:
88 87 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
89 88 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
90 89 # 3. IPython: .error, .in-prompt, .out-prompt, etc
91 90 style_sheet = Str(config=True)
92 91
93 92 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
94 93 # the style sheet is queried for Pygments style information.
95 94 syntax_style = Str(config=True)
96 95
97 96 # Prompts.
98 97 in_prompt = Str(default_in_prompt, config=True)
99 98 out_prompt = Str(default_out_prompt, config=True)
100 99 input_sep = Str(default_input_sep, config=True)
101 100 output_sep = Str(default_output_sep, config=True)
102 101 output_sep2 = Str(default_output_sep2, config=True)
103 102
104 103 # FrontendWidget protected class variables.
105 104 _input_splitter_class = IPythonInputSplitter
106 105
107 106 # IPythonWidget protected class variables.
108 107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
109 108 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
110 109 _payload_source_page = 'IPython.zmq.page.page'
111 110
112 111 #---------------------------------------------------------------------------
113 112 # 'object' interface
114 113 #---------------------------------------------------------------------------
115 114
116 115 def __init__(self, *args, **kw):
117 116 super(IPythonWidget, self).__init__(*args, **kw)
118 117
119 118 # IPythonWidget protected variables.
120 119 self._previous_prompt_obj = None
121 120
122 121 # Initialize widget styling.
123 122 if self.style_sheet:
124 123 self._style_sheet_changed()
125 124 self._syntax_style_changed()
126 125 else:
127 126 self.set_default_style()
128 127
129 128 #---------------------------------------------------------------------------
130 129 # 'BaseFrontendMixin' abstract interface
131 130 #---------------------------------------------------------------------------
132 131
133 132 def _handle_complete_reply(self, rep):
134 133 """ Reimplemented to support IPython's improved completion machinery.
135 134 """
136 135 cursor = self._get_cursor()
137 if rep['parent_header']['msg_id'] == self._complete_id and \
138 cursor.position() == self._complete_pos:
136 info = self._request_info.get('complete')
137 if info and info.id == rep['parent_header']['msg_id'] and \
138 info.pos == cursor.position():
139 139 matches = rep['content']['matches']
140 140 text = rep['content']['matched_text']
141 141
142 142 # Clean up matches with '.'s and path separators.
143 143 parts = re.split(r'[./\\]', text)
144 144 sep_count = len(parts) - 1
145 145 if sep_count:
146 146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
147 147 matches = [ match[chop_length:] for match in matches ]
148 148 text = text[chop_length:]
149 149
150 150 # Move the cursor to the start of the match and complete.
151 151 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
152 152 self._complete_with_items(cursor, matches)
153 153
154 def _handle_execute_reply(self, msg):
155 """ Reimplemented to support prompt requests.
156 """
157 info = self._request_info.get('execute')
158 if info and info.id == msg['parent_header']['msg_id']:
159 if info.kind == 'prompt':
160 number = msg['content']['execution_count'] + 1
161 self._show_interpreter_prompt(number)
162 else:
163 super(IPythonWidget, self)._handle_execute_reply(msg)
164
154 165 def _handle_history_reply(self, msg):
155 166 """ Implemented to handle history replies, which are only supported by
156 167 the IPython kernel.
157 168 """
158 169 history_dict = msg['content']['history']
159 170 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
160 171 self._set_history(items)
161 172
162 def _handle_prompt_reply(self, msg):
163 """ Implemented to handle prompt number replies, which are only
164 supported by the IPython kernel.
165 """
166 self._show_interpreter_prompt(msg['content']['execution_count'])
167
168 173 def _handle_pyout(self, msg):
169 174 """ Reimplemented for IPython-style "display hook".
170 175 """
171 176 if not self._hidden and self._is_from_this_session(msg):
172 177 content = msg['content']
173 178 prompt_number = content['execution_count']
174 179 self._append_plain_text(self.output_sep)
175 180 self._append_html(self._make_out_prompt(prompt_number))
176 181 self._append_plain_text(content['data']+self.output_sep2)
177 182
178 183 def _started_channels(self):
179 184 """ Reimplemented to make a history request.
180 185 """
181 186 super(IPythonWidget, self)._started_channels()
182 187 # FIXME: Disabled until history requests are properly implemented.
183 188 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
184 189
185 190 #---------------------------------------------------------------------------
186 191 # 'FrontendWidget' interface
187 192 #---------------------------------------------------------------------------
188 193
189 194 def execute_file(self, path, hidden=False):
190 195 """ Reimplemented to use the 'run' magic.
191 196 """
192 197 self.execute('%%run %s' % path, hidden=hidden)
193 198
194 199 #---------------------------------------------------------------------------
195 200 # 'FrontendWidget' protected interface
196 201 #---------------------------------------------------------------------------
197 202
198 203 def _complete(self):
199 204 """ Reimplemented to support IPython's improved completion machinery.
200 205 """
201 206 # We let the kernel split the input line, so we *always* send an empty
202 207 # text field. Readline-based frontends do get a real text field which
203 208 # they can use.
204 209 text = ''
205 210
206 211 # Send the completion request to the kernel
207 self._complete_id = self.kernel_manager.xreq_channel.complete(
212 msg_id = self.kernel_manager.xreq_channel.complete(
208 213 text, # text
209 214 self._get_input_buffer_cursor_line(), # line
210 215 self._get_input_buffer_cursor_column(), # cursor_pos
211 216 self.input_buffer) # block
212 self._complete_pos = self._get_cursor().position()
217 pos = self._get_cursor().position()
218 info = self._CompletionRequest(msg_id, pos)
219 self._request_info['complete'] = info
213 220
214 221 def _get_banner(self):
215 222 """ Reimplemented to return IPython's default banner.
216 223 """
217 224 return default_banner + '\n'
218 225
219 226 def _process_execute_error(self, msg):
220 227 """ Reimplemented for IPython-style traceback formatting.
221 228 """
222 229 content = msg['content']
223 230 traceback = '\n'.join(content['traceback']) + '\n'
224 231 if False:
225 232 # FIXME: For now, tracebacks come as plain text, so we can't use
226 233 # the html renderer yet. Once we refactor ultratb to produce
227 234 # properly styled tracebacks, this branch should be the default
228 235 traceback = traceback.replace(' ', '&nbsp;')
229 236 traceback = traceback.replace('\n', '<br/>')
230 237
231 238 ename = content['ename']
232 239 ename_styled = '<span class="error">%s</span>' % ename
233 240 traceback = traceback.replace(ename, ename_styled)
234 241
235 242 self._append_html(traceback)
236 243 else:
237 244 # This is the fallback for now, using plain text with ansi escapes
238 245 self._append_plain_text(traceback)
239 246
240 247 def _process_execute_payload(self, item):
241 248 """ Reimplemented to handle %edit and paging payloads.
242 249 """
243 250 if item['source'] == self._payload_source_edit:
244 251 self._edit(item['filename'], item['line_number'])
245 252 return True
246 253 elif item['source'] == self._payload_source_page:
247 254 self._page(item['data'])
248 255 return True
249 256 else:
250 257 return False
251 258
252 259 def _show_interpreter_prompt(self, number=None):
253 260 """ Reimplemented for IPython-style prompts.
254 261 """
255 262 # If a number was not specified, make a prompt number request.
256 263 if number is None:
257 # FIXME - fperez: this should be a silent code request
258 number = 1
259 ##self.kernel_manager.xreq_channel.prompt()
260 ##return
264 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
265 info = self._ExecutionRequest(msg_id, 'prompt')
266 self._request_info['execute'] = info
267 return
261 268
262 269 # Show a new prompt and save information about it so that it can be
263 270 # updated later if the prompt number turns out to be wrong.
264 271 self._prompt_sep = self.input_sep
265 272 self._show_prompt(self._make_in_prompt(number), html=True)
266 273 block = self._control.document().lastBlock()
267 274 length = len(self._prompt)
268 275 self._previous_prompt_obj = self._PromptBlock(block, length, number)
269 276
270 277 # Update continuation prompt to reflect (possibly) new prompt length.
271 278 self._set_continuation_prompt(
272 279 self._make_continuation_prompt(self._prompt), html=True)
273 280
274 281 def _show_interpreter_prompt_for_reply(self, msg):
275 282 """ Reimplemented for IPython-style prompts.
276 283 """
277 284 # Update the old prompt number if necessary.
278 285 content = msg['content']
279 ##io.rprint('_show_interpreter_prompt_for_reply\n', content) # dbg
280 286 previous_prompt_number = content['execution_count']
281 287 if self._previous_prompt_obj and \
282 288 self._previous_prompt_obj.number != previous_prompt_number:
283 289 block = self._previous_prompt_obj.block
284 290
285 291 # Make sure the prompt block has not been erased.
286 292 if block.isValid() and not block.text().isEmpty():
287 293
288 294 # Remove the old prompt and insert a new prompt.
289 295 cursor = QtGui.QTextCursor(block)
290 296 cursor.movePosition(QtGui.QTextCursor.Right,
291 297 QtGui.QTextCursor.KeepAnchor,
292 298 self._previous_prompt_obj.length)
293 299 prompt = self._make_in_prompt(previous_prompt_number)
294 300 self._prompt = self._insert_html_fetching_plain_text(
295 301 cursor, prompt)
296 302
297 303 # When the HTML is inserted, Qt blows away the syntax
298 304 # highlighting for the line, so we need to rehighlight it.
299 305 self._highlighter.rehighlightBlock(cursor.block())
300 306
301 307 self._previous_prompt_obj = None
302 308
303 309 # Show a new prompt with the kernel's estimated prompt number.
304 310 self._show_interpreter_prompt(previous_prompt_number+1)
305 311
306 312 #---------------------------------------------------------------------------
307 313 # 'IPythonWidget' interface
308 314 #---------------------------------------------------------------------------
309 315
310 316 def set_default_style(self, lightbg=True):
311 317 """ Sets the widget style to the class defaults.
312 318
313 319 Parameters:
314 320 -----------
315 321 lightbg : bool, optional (default True)
316 322 Whether to use the default IPython light background or dark
317 323 background style.
318 324 """
319 325 if lightbg:
320 326 self.style_sheet = default_light_style_sheet
321 327 self.syntax_style = default_light_syntax_style
322 328 else:
323 329 self.style_sheet = default_dark_style_sheet
324 330 self.syntax_style = default_dark_syntax_style
325 331
326 332 #---------------------------------------------------------------------------
327 333 # 'IPythonWidget' protected interface
328 334 #---------------------------------------------------------------------------
329 335
330 336 def _edit(self, filename, line=None):
331 337 """ Opens a Python script for editing.
332 338
333 339 Parameters:
334 340 -----------
335 341 filename : str
336 342 A path to a local system file.
337 343
338 344 line : int, optional
339 345 A line of interest in the file.
340 346 """
341 347 if self.custom_edit:
342 348 self.custom_edit_requested.emit(filename, line)
343 349 elif self.editor == 'default':
344 350 self._append_plain_text('No default editor available.\n')
345 351 else:
346 352 try:
347 353 filename = '"%s"' % filename
348 354 if line and self.editor_line:
349 355 command = self.editor_line.format(filename=filename,
350 356 line=line)
351 357 else:
352 358 try:
353 359 command = self.editor.format()
354 360 except KeyError:
355 361 command = self.editor.format(filename=filename)
356 362 else:
357 363 command += ' ' + filename
358 364 except KeyError:
359 365 self._append_plain_text('Invalid editor command.\n')
360 366 else:
361 367 try:
362 368 Popen(command, shell=True)
363 369 except OSError:
364 370 msg = 'Opening editor with command "%s" failed.\n'
365 371 self._append_plain_text(msg % command)
366 372
367 373 def _make_in_prompt(self, number):
368 374 """ Given a prompt number, returns an HTML In prompt.
369 375 """
370 376 body = self.in_prompt % number
371 377 return '<span class="in-prompt">%s</span>' % body
372 378
373 379 def _make_continuation_prompt(self, prompt):
374 380 """ Given a plain text version of an In prompt, returns an HTML
375 381 continuation prompt.
376 382 """
377 383 end_chars = '...: '
378 384 space_count = len(prompt.lstrip('\n')) - len(end_chars)
379 385 body = '&nbsp;' * space_count + end_chars
380 386 return '<span class="in-prompt">%s</span>' % body
381 387
382 388 def _make_out_prompt(self, number):
383 389 """ Given a prompt number, returns an HTML Out prompt.
384 390 """
385 391 body = self.out_prompt % number
386 392 return '<span class="out-prompt">%s</span>' % body
387 393
388 394 #------ Trait change handlers ---------------------------------------------
389 395
390 396 def _style_sheet_changed(self):
391 397 """ Set the style sheets of the underlying widgets.
392 398 """
393 399 self.setStyleSheet(self.style_sheet)
394 400 self._control.document().setDefaultStyleSheet(self.style_sheet)
395 401 if self._page_control:
396 402 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
397 403
398 404 bg_color = self._control.palette().background().color()
399 405 self._ansi_processor.set_background_color(bg_color)
400 406
401 407 def _syntax_style_changed(self):
402 408 """ Set the style for the syntax highlighter.
403 409 """
404 410 if self.syntax_style:
405 411 self._highlighter.set_style(self.syntax_style)
406 412 else:
407 413 self._highlighter.set_style_sheet(self.style_sheet)
408 414
@@ -1,786 +1,785 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 import io
33 33 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
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 # Utility functions
47 47 #-----------------------------------------------------------------------------
48 48
49 49 # some utilities to validate message structure, these might get moved elsewhere
50 50 # if they prove to have more generic utility
51 51
52 52 def validate_string_list(lst):
53 53 """Validate that the input is a list of strings.
54 54
55 55 Raises ValueError if not."""
56 56 if not isinstance(lst, list):
57 57 raise ValueError('input %r must be a list' % lst)
58 58 for x in lst:
59 59 if not isinstance(x, basestring):
60 60 raise ValueError('element %r in list must be a string' % x)
61 61
62 62
63 63 def validate_string_dict(dct):
64 64 """Validate that the input is a dict with string keys and values.
65 65
66 66 Raises ValueError if not."""
67 67 for k,v in dct.iteritems():
68 68 if not isinstance(k, basestring):
69 69 raise ValueError('key %r in dict must be a string' % k)
70 70 if not isinstance(v, basestring):
71 71 raise ValueError('value %r in dict must be a string' % v)
72 72
73 73
74 74 #-----------------------------------------------------------------------------
75 75 # ZMQ Socket Channel classes
76 76 #-----------------------------------------------------------------------------
77 77
78 78 class ZmqSocketChannel(Thread):
79 79 """The base class for the channels that use ZMQ sockets.
80 80 """
81 81 context = None
82 82 session = None
83 83 socket = None
84 84 ioloop = None
85 85 iostate = None
86 86 _address = None
87 87
88 88 def __init__(self, context, session, address):
89 89 """Create a channel
90 90
91 91 Parameters
92 92 ----------
93 93 context : :class:`zmq.Context`
94 94 The ZMQ context to use.
95 95 session : :class:`session.Session`
96 96 The session to use.
97 97 address : tuple
98 98 Standard (ip, port) tuple that the kernel is listening on.
99 99 """
100 100 super(ZmqSocketChannel, self).__init__()
101 101 self.daemon = True
102 102
103 103 self.context = context
104 104 self.session = session
105 105 if address[1] == 0:
106 106 message = 'The port number for a channel cannot be 0.'
107 107 raise InvalidPortNumber(message)
108 108 self._address = address
109 109
110 110 def stop(self):
111 111 """Stop the channel's activity.
112 112
113 113 This calls :method:`Thread.join` and returns when the thread
114 114 terminates. :class:`RuntimeError` will be raised if
115 115 :method:`self.start` is called again.
116 116 """
117 117 self.join()
118 118
119 119 @property
120 120 def address(self):
121 121 """Get the channel's address as an (ip, port) tuple.
122 122
123 123 By the default, the address is (localhost, 0), where 0 means a random
124 124 port.
125 125 """
126 126 return self._address
127 127
128 128 def add_io_state(self, state):
129 129 """Add IO state to the eventloop.
130 130
131 131 Parameters
132 132 ----------
133 133 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
134 134 The IO state flag to set.
135 135
136 136 This is thread safe as it uses the thread safe IOLoop.add_callback.
137 137 """
138 138 def add_io_state_callback():
139 139 if not self.iostate & state:
140 140 self.iostate = self.iostate | state
141 141 self.ioloop.update_handler(self.socket, self.iostate)
142 142 self.ioloop.add_callback(add_io_state_callback)
143 143
144 144 def drop_io_state(self, state):
145 145 """Drop IO state from the eventloop.
146 146
147 147 Parameters
148 148 ----------
149 149 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
150 150 The IO state flag to set.
151 151
152 152 This is thread safe as it uses the thread safe IOLoop.add_callback.
153 153 """
154 154 def drop_io_state_callback():
155 155 if self.iostate & state:
156 156 self.iostate = self.iostate & (~state)
157 157 self.ioloop.update_handler(self.socket, self.iostate)
158 158 self.ioloop.add_callback(drop_io_state_callback)
159 159
160 160
161 161 class XReqSocketChannel(ZmqSocketChannel):
162 162 """The XREQ channel for issues request/replies to the kernel.
163 163 """
164 164
165 165 command_queue = None
166 166
167 167 def __init__(self, context, session, address):
168 168 self.command_queue = Queue()
169 169 super(XReqSocketChannel, self).__init__(context, session, address)
170 170
171 171 def run(self):
172 172 """The thread's main activity. Call start() instead."""
173 173 self.socket = self.context.socket(zmq.XREQ)
174 174 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
175 175 self.socket.connect('tcp://%s:%i' % self.address)
176 176 self.ioloop = ioloop.IOLoop()
177 177 self.iostate = POLLERR|POLLIN
178 178 self.ioloop.add_handler(self.socket, self._handle_events,
179 179 self.iostate)
180 180 self.ioloop.start()
181 181
182 182 def stop(self):
183 183 self.ioloop.stop()
184 184 super(XReqSocketChannel, self).stop()
185 185
186 186 def call_handlers(self, msg):
187 187 """This method is called in the ioloop thread when a message arrives.
188 188
189 189 Subclasses should override this method to handle incoming messages.
190 190 It is important to remember that this method is called in the thread
191 191 so that some logic must be done to ensure that the application leve
192 192 handlers are called in the application thread.
193 193 """
194 194 raise NotImplementedError('call_handlers must be defined in a subclass.')
195 195
196 196 def execute(self, code, silent=False,
197 197 user_variables=None, user_expressions=None):
198 198 """Execute code in the kernel.
199 199
200 200 Parameters
201 201 ----------
202 202 code : str
203 203 A string of Python code.
204 204
205 205 silent : bool, optional (default False)
206 206 If set, the kernel will execute the code as quietly possible.
207 207
208 208 user_variables : list, optional
209
210 209 A list of variable names to pull from the user's namespace. They
211 210 will come back as a dict with these names as keys and their
212 211 :func:`repr` as values.
213 212
214 213 user_expressions : dict, optional
215 214 A dict with string keys and to pull from the user's
216 215 namespace. They will come back as a dict with these names as keys
217 216 and their :func:`repr` as values.
218 217
219 218 Returns
220 219 -------
221 220 The msg_id of the message sent.
222 221 """
223 222 if user_variables is None:
224 223 user_variables = []
225 224 if user_expressions is None:
226 225 user_expressions = {}
227 226
228 227 # Don't waste network traffic if inputs are invalid
229 228 if not isinstance(code, basestring):
230 229 raise ValueError('code %r must be a string' % code)
231 230 validate_string_list(user_variables)
232 231 validate_string_dict(user_expressions)
233 232
234 233 # Create class for content/msg creation. Related to, but possibly
235 234 # not in Session.
236 235 content = dict(code=code, silent=silent,
237 236 user_variables=user_variables,
238 237 user_expressions=user_expressions)
239 238 msg = self.session.msg('execute_request', content)
240 239 self._queue_request(msg)
241 240 return msg['header']['msg_id']
242 241
243 242 def complete(self, text, line, cursor_pos, block=None):
244 243 """Tab complete text in the kernel's namespace.
245 244
246 245 Parameters
247 246 ----------
248 247 text : str
249 248 The text to complete.
250 249 line : str
251 250 The full line of text that is the surrounding context for the
252 251 text to complete.
253 252 cursor_pos : int
254 253 The position of the cursor in the line where the completion was
255 254 requested.
256 255 block : str, optional
257 256 The full block of code in which the completion is being requested.
258 257
259 258 Returns
260 259 -------
261 260 The msg_id of the message sent.
262 261 """
263 262 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
264 263 msg = self.session.msg('complete_request', content)
265 264 self._queue_request(msg)
266 265 return msg['header']['msg_id']
267 266
268 267 def object_info(self, oname):
269 268 """Get metadata information about an object.
270 269
271 270 Parameters
272 271 ----------
273 272 oname : str
274 273 A string specifying the object name.
275 274
276 275 Returns
277 276 -------
278 277 The msg_id of the message sent.
279 278 """
280 279 content = dict(oname=oname)
281 280 msg = self.session.msg('object_info_request', content)
282 281 self._queue_request(msg)
283 282 return msg['header']['msg_id']
284 283
285 284 def history(self, index=None, raw=False, output=True):
286 285 """Get the history list.
287 286
288 287 Parameters
289 288 ----------
290 289 index : n or (n1, n2) or None
291 290 If n, then the last entries. If a tuple, then all in
292 291 range(n1, n2). If None, then all entries. Raises IndexError if
293 292 the format of index is incorrect.
294 293 raw : bool
295 294 If True, return the raw input.
296 295 output : bool
297 296 If True, then return the output as well.
298 297
299 298 Returns
300 299 -------
301 300 The msg_id of the message sent.
302 301 """
303 302 content = dict(index=index, raw=raw, output=output)
304 303 msg = self.session.msg('history_request', content)
305 304 self._queue_request(msg)
306 305 return msg['header']['msg_id']
307 306
308 307 def _handle_events(self, socket, events):
309 308 if events & POLLERR:
310 309 self._handle_err()
311 310 if events & POLLOUT:
312 311 self._handle_send()
313 312 if events & POLLIN:
314 313 self._handle_recv()
315 314
316 315 def _handle_recv(self):
317 316 msg = self.socket.recv_json()
318 317 self.call_handlers(msg)
319 318
320 319 def _handle_send(self):
321 320 try:
322 321 msg = self.command_queue.get(False)
323 322 except Empty:
324 323 pass
325 324 else:
326 325 self.socket.send_json(msg)
327 326 if self.command_queue.empty():
328 327 self.drop_io_state(POLLOUT)
329 328
330 329 def _handle_err(self):
331 330 # We don't want to let this go silently, so eventually we should log.
332 331 raise zmq.ZMQError()
333 332
334 333 def _queue_request(self, msg):
335 334 self.command_queue.put(msg)
336 335 self.add_io_state(POLLOUT)
337 336
338 337
339 338 class SubSocketChannel(ZmqSocketChannel):
340 339 """The SUB channel which listens for messages that the kernel publishes.
341 340 """
342 341
343 342 def __init__(self, context, session, address):
344 343 super(SubSocketChannel, self).__init__(context, session, address)
345 344
346 345 def run(self):
347 346 """The thread's main activity. Call start() instead."""
348 347 self.socket = self.context.socket(zmq.SUB)
349 348 self.socket.setsockopt(zmq.SUBSCRIBE,'')
350 349 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
351 350 self.socket.connect('tcp://%s:%i' % self.address)
352 351 self.ioloop = ioloop.IOLoop()
353 352 self.iostate = POLLIN|POLLERR
354 353 self.ioloop.add_handler(self.socket, self._handle_events,
355 354 self.iostate)
356 355 self.ioloop.start()
357 356
358 357 def stop(self):
359 358 self.ioloop.stop()
360 359 super(SubSocketChannel, self).stop()
361 360
362 361 def call_handlers(self, msg):
363 362 """This method is called in the ioloop thread when a message arrives.
364 363
365 364 Subclasses should override this method to handle incoming messages.
366 365 It is important to remember that this method is called in the thread
367 366 so that some logic must be done to ensure that the application leve
368 367 handlers are called in the application thread.
369 368 """
370 369 raise NotImplementedError('call_handlers must be defined in a subclass.')
371 370
372 371 def flush(self, timeout=1.0):
373 372 """Immediately processes all pending messages on the SUB channel.
374 373
375 374 Callers should use this method to ensure that :method:`call_handlers`
376 375 has been called for all messages that have been received on the
377 376 0MQ SUB socket of this channel.
378 377
379 378 This method is thread safe.
380 379
381 380 Parameters
382 381 ----------
383 382 timeout : float, optional
384 383 The maximum amount of time to spend flushing, in seconds. The
385 384 default is one second.
386 385 """
387 386 # We do the IOLoop callback process twice to ensure that the IOLoop
388 387 # gets to perform at least one full poll.
389 388 stop_time = time.time() + timeout
390 389 for i in xrange(2):
391 390 self._flushed = False
392 391 self.ioloop.add_callback(self._flush)
393 392 while not self._flushed and time.time() < stop_time:
394 393 time.sleep(0.01)
395 394
396 395 def _handle_events(self, socket, events):
397 396 # Turn on and off POLLOUT depending on if we have made a request
398 397 if events & POLLERR:
399 398 self._handle_err()
400 399 if events & POLLIN:
401 400 self._handle_recv()
402 401
403 402 def _handle_err(self):
404 403 # We don't want to let this go silently, so eventually we should log.
405 404 raise zmq.ZMQError()
406 405
407 406 def _handle_recv(self):
408 407 # Get all of the messages we can
409 408 while True:
410 409 try:
411 410 msg = self.socket.recv_json(zmq.NOBLOCK)
412 411 except zmq.ZMQError:
413 412 # Check the errno?
414 413 # Will this trigger POLLERR?
415 414 break
416 415 else:
417 416 self.call_handlers(msg)
418 417
419 418 def _flush(self):
420 419 """Callback for :method:`self.flush`."""
421 420 self._flushed = True
422 421
423 422
424 423 class RepSocketChannel(ZmqSocketChannel):
425 424 """A reply channel to handle raw_input requests that the kernel makes."""
426 425
427 426 msg_queue = None
428 427
429 428 def __init__(self, context, session, address):
430 429 self.msg_queue = Queue()
431 430 super(RepSocketChannel, self).__init__(context, session, address)
432 431
433 432 def run(self):
434 433 """The thread's main activity. Call start() instead."""
435 434 self.socket = self.context.socket(zmq.XREQ)
436 435 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
437 436 self.socket.connect('tcp://%s:%i' % self.address)
438 437 self.ioloop = ioloop.IOLoop()
439 438 self.iostate = POLLERR|POLLIN
440 439 self.ioloop.add_handler(self.socket, self._handle_events,
441 440 self.iostate)
442 441 self.ioloop.start()
443 442
444 443 def stop(self):
445 444 self.ioloop.stop()
446 445 super(RepSocketChannel, self).stop()
447 446
448 447 def call_handlers(self, msg):
449 448 """This method is called in the ioloop thread when a message arrives.
450 449
451 450 Subclasses should override this method to handle incoming messages.
452 451 It is important to remember that this method is called in the thread
453 452 so that some logic must be done to ensure that the application leve
454 453 handlers are called in the application thread.
455 454 """
456 455 raise NotImplementedError('call_handlers must be defined in a subclass.')
457 456
458 457 def input(self, string):
459 458 """Send a string of raw input to the kernel."""
460 459 content = dict(value=string)
461 460 msg = self.session.msg('input_reply', content)
462 461 self._queue_reply(msg)
463 462
464 463 def _handle_events(self, socket, events):
465 464 if events & POLLERR:
466 465 self._handle_err()
467 466 if events & POLLOUT:
468 467 self._handle_send()
469 468 if events & POLLIN:
470 469 self._handle_recv()
471 470
472 471 def _handle_recv(self):
473 472 msg = self.socket.recv_json()
474 473 self.call_handlers(msg)
475 474
476 475 def _handle_send(self):
477 476 try:
478 477 msg = self.msg_queue.get(False)
479 478 except Empty:
480 479 pass
481 480 else:
482 481 self.socket.send_json(msg)
483 482 if self.msg_queue.empty():
484 483 self.drop_io_state(POLLOUT)
485 484
486 485 def _handle_err(self):
487 486 # We don't want to let this go silently, so eventually we should log.
488 487 raise zmq.ZMQError()
489 488
490 489 def _queue_reply(self, msg):
491 490 self.msg_queue.put(msg)
492 491 self.add_io_state(POLLOUT)
493 492
494 493
495 494 class HBSocketChannel(ZmqSocketChannel):
496 495 """The heartbeat channel which monitors the kernel heartbeat."""
497 496
498 497 time_to_dead = 3.0
499 498 socket = None
500 499 poller = None
501 500
502 501 def __init__(self, context, session, address):
503 502 super(HBSocketChannel, self).__init__(context, session, address)
504 503 self._running = False
505 504
506 505 def _create_socket(self):
507 506 self.socket = self.context.socket(zmq.REQ)
508 507 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
509 508 self.socket.connect('tcp://%s:%i' % self.address)
510 509 self.poller = zmq.Poller()
511 510 self.poller.register(self.socket, zmq.POLLIN)
512 511
513 512 def run(self):
514 513 """The thread's main activity. Call start() instead."""
515 514 self._create_socket()
516 515 self._running = True
517 516 # Wait 2 seconds for the kernel to come up and the sockets to auto
518 517 # connect. If we don't we will see the kernel as dead. Also, before
519 518 # the sockets are connected, the poller.poll line below is returning
520 519 # too fast. This avoids that because the polling doesn't start until
521 520 # after the sockets are connected.
522 521 time.sleep(2.0)
523 522 while self._running:
524 523 since_last_heartbeat = 0.0
525 524 request_time = time.time()
526 525 try:
527 526 #io.rprint('Ping from HB channel') # dbg
528 527 self.socket.send_json('ping')
529 528 except zmq.ZMQError, e:
530 529 #io.rprint('*** HB Error:', e) # dbg
531 530 if e.errno == zmq.EFSM:
532 531 #io.rprint('sleep...', self.time_to_dead) # dbg
533 532 time.sleep(self.time_to_dead)
534 533 self._create_socket()
535 534 else:
536 535 raise
537 536 else:
538 537 while True:
539 538 try:
540 539 self.socket.recv_json(zmq.NOBLOCK)
541 540 except zmq.ZMQError, e:
542 541 #io.rprint('*** HB Error 2:', e) # dbg
543 542 if e.errno == zmq.EAGAIN:
544 543 before_poll = time.time()
545 544 until_dead = self.time_to_dead - (before_poll -
546 545 request_time)
547 546
548 547 # When the return value of poll() is an empty list,
549 548 # that is when things have gone wrong (zeromq bug).
550 549 # As long as it is not an empty list, poll is
551 550 # working correctly even if it returns quickly.
552 551 # Note: poll timeout is in milliseconds.
553 552 self.poller.poll(1000*until_dead)
554 553
555 554 since_last_heartbeat = time.time() - request_time
556 555 if since_last_heartbeat > self.time_to_dead:
557 556 self.call_handlers(since_last_heartbeat)
558 557 break
559 558 else:
560 559 # FIXME: We should probably log this instead.
561 560 raise
562 561 else:
563 562 until_dead = self.time_to_dead - (time.time() -
564 563 request_time)
565 564 if until_dead > 0.0:
566 565 #io.rprint('sleep...', self.time_to_dead) # dbg
567 566 time.sleep(until_dead)
568 567 break
569 568
570 569 def stop(self):
571 570 self._running = False
572 571 super(HBSocketChannel, self).stop()
573 572
574 573 def call_handlers(self, since_last_heartbeat):
575 574 """This method is called in the ioloop thread when a message arrives.
576 575
577 576 Subclasses should override this method to handle incoming messages.
578 577 It is important to remember that this method is called in the thread
579 578 so that some logic must be done to ensure that the application leve
580 579 handlers are called in the application thread.
581 580 """
582 581 raise NotImplementedError('call_handlers must be defined in a subclass.')
583 582
584 583
585 584 #-----------------------------------------------------------------------------
586 585 # Main kernel manager class
587 586 #-----------------------------------------------------------------------------
588 587
589 588 class KernelManager(HasTraits):
590 589 """ Manages a kernel for a frontend.
591 590
592 591 The SUB channel is for the frontend to receive messages published by the
593 592 kernel.
594 593
595 594 The REQ channel is for the frontend to make requests of the kernel.
596 595
597 596 The REP channel is for the kernel to request stdin (raw_input) from the
598 597 frontend.
599 598 """
600 599 # The PyZMQ Context to use for communication with the kernel.
601 600 context = Instance(zmq.Context,(),{})
602 601
603 602 # The Session to use for communication with the kernel.
604 603 session = Instance(Session,(),{})
605 604
606 605 # The kernel process with which the KernelManager is communicating.
607 606 kernel = Instance(Popen)
608 607
609 608 # The addresses for the communication channels.
610 609 xreq_address = TCPAddress((LOCALHOST, 0))
611 610 sub_address = TCPAddress((LOCALHOST, 0))
612 611 rep_address = TCPAddress((LOCALHOST, 0))
613 612 hb_address = TCPAddress((LOCALHOST, 0))
614 613
615 614 # The classes to use for the various channels.
616 615 xreq_channel_class = Type(XReqSocketChannel)
617 616 sub_channel_class = Type(SubSocketChannel)
618 617 rep_channel_class = Type(RepSocketChannel)
619 618 hb_channel_class = Type(HBSocketChannel)
620 619
621 620 # Protected traits.
622 621 _launch_args = Any
623 622 _xreq_channel = Any
624 623 _sub_channel = Any
625 624 _rep_channel = Any
626 625 _hb_channel = Any
627 626
628 627 #--------------------------------------------------------------------------
629 628 # Channel management methods:
630 629 #--------------------------------------------------------------------------
631 630
632 631 def start_channels(self):
633 632 """Starts the channels for this kernel.
634 633
635 634 This will create the channels if they do not exist and then start
636 635 them. If port numbers of 0 are being used (random ports) then you
637 636 must first call :method:`start_kernel`. If the channels have been
638 637 stopped and you call this, :class:`RuntimeError` will be raised.
639 638 """
640 639 self.xreq_channel.start()
641 640 self.sub_channel.start()
642 641 self.rep_channel.start()
643 642 self.hb_channel.start()
644 643
645 644 def stop_channels(self):
646 645 """Stops the channels for this kernel.
647 646
648 647 This stops the channels by joining their threads. If the channels
649 648 were not started, :class:`RuntimeError` will be raised.
650 649 """
651 650 self.xreq_channel.stop()
652 651 self.sub_channel.stop()
653 652 self.rep_channel.stop()
654 653 self.hb_channel.stop()
655 654
656 655 @property
657 656 def channels_running(self):
658 657 """Are all of the channels created and running?"""
659 658 return self.xreq_channel.is_alive() \
660 659 and self.sub_channel.is_alive() \
661 660 and self.rep_channel.is_alive() \
662 661 and self.hb_channel.is_alive()
663 662
664 663 #--------------------------------------------------------------------------
665 664 # Kernel process management methods:
666 665 #--------------------------------------------------------------------------
667 666
668 667 def start_kernel(self, **kw):
669 668 """Starts a kernel process and configures the manager to use it.
670 669
671 670 If random ports (port=0) are being used, this method must be called
672 671 before the channels are created.
673 672
674 673 Parameters:
675 674 -----------
676 675 ipython : bool, optional (default True)
677 676 Whether to use an IPython kernel instead of a plain Python kernel.
678 677 """
679 678 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
680 679 self.rep_address, self.hb_address
681 680 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
682 681 rep[0] != LOCALHOST or hb[0] != LOCALHOST:
683 682 raise RuntimeError("Can only launch a kernel on localhost."
684 683 "Make sure that the '*_address' attributes are "
685 684 "configured properly.")
686 685
687 686 self._launch_args = kw.copy()
688 687 if kw.pop('ipython', True):
689 688 from ipkernel import launch_kernel as launch
690 689 else:
691 690 from pykernel import launch_kernel as launch
692 691 self.kernel, xrep, pub, req, hb = launch(
693 692 xrep_port=xreq[1], pub_port=sub[1],
694 693 req_port=rep[1], hb_port=hb[1], **kw)
695 694 self.xreq_address = (LOCALHOST, xrep)
696 695 self.sub_address = (LOCALHOST, pub)
697 696 self.rep_address = (LOCALHOST, req)
698 697 self.hb_address = (LOCALHOST, hb)
699 698
700 699 def restart_kernel(self):
701 700 """Restarts a kernel with the same arguments that were used to launch
702 701 it. If the old kernel was launched with random ports, the same ports
703 702 will be used for the new kernel.
704 703 """
705 704 if self._launch_args is None:
706 705 raise RuntimeError("Cannot restart the kernel. "
707 706 "No previous call to 'start_kernel'.")
708 707 else:
709 708 if self.has_kernel:
710 709 self.kill_kernel()
711 710 self.start_kernel(**self._launch_args)
712 711
713 712 @property
714 713 def has_kernel(self):
715 714 """Returns whether a kernel process has been specified for the kernel
716 715 manager.
717 716 """
718 717 return self.kernel is not None
719 718
720 719 def kill_kernel(self):
721 720 """ Kill the running kernel. """
722 721 if self.kernel is not None:
723 722 self.kernel.kill()
724 723 self.kernel = None
725 724 else:
726 725 raise RuntimeError("Cannot kill kernel. No kernel is running!")
727 726
728 727 def signal_kernel(self, signum):
729 728 """ Sends a signal to the kernel. """
730 729 if self.kernel is not None:
731 730 self.kernel.send_signal(signum)
732 731 else:
733 732 raise RuntimeError("Cannot signal kernel. No kernel is running!")
734 733
735 734 @property
736 735 def is_alive(self):
737 736 """Is the kernel process still running?"""
738 737 if self.kernel is not None:
739 738 if self.kernel.poll() is None:
740 739 return True
741 740 else:
742 741 return False
743 742 else:
744 743 # We didn't start the kernel with this KernelManager so we don't
745 744 # know if it is running. We should use a heartbeat for this case.
746 745 return True
747 746
748 747 #--------------------------------------------------------------------------
749 748 # Channels used for communication with the kernel:
750 749 #--------------------------------------------------------------------------
751 750
752 751 @property
753 752 def xreq_channel(self):
754 753 """Get the REQ socket channel object to make requests of the kernel."""
755 754 if self._xreq_channel is None:
756 755 self._xreq_channel = self.xreq_channel_class(self.context,
757 756 self.session,
758 757 self.xreq_address)
759 758 return self._xreq_channel
760 759
761 760 @property
762 761 def sub_channel(self):
763 762 """Get the SUB socket channel object."""
764 763 if self._sub_channel is None:
765 764 self._sub_channel = self.sub_channel_class(self.context,
766 765 self.session,
767 766 self.sub_address)
768 767 return self._sub_channel
769 768
770 769 @property
771 770 def rep_channel(self):
772 771 """Get the REP socket channel object to handle stdin (raw_input)."""
773 772 if self._rep_channel is None:
774 773 self._rep_channel = self.rep_channel_class(self.context,
775 774 self.session,
776 775 self.rep_address)
777 776 return self._rep_channel
778 777
779 778 @property
780 779 def hb_channel(self):
781 780 """Get the REP socket channel object to handle stdin (raw_input)."""
782 781 if self._hb_channel is None:
783 782 self._hb_channel = self.hb_channel_class(self.context,
784 783 self.session,
785 784 self.hb_address)
786 785 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now