##// END OF EJS Templates
expose shell channel methods at the client level
MinRK -
Show More
@@ -1,772 +1,772
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7 import uuid
8 8
9 9 # System library imports
10 10 from pygments.lexers import PythonLexer
11 11 from IPython.external import qt
12 12 from IPython.external.qt import QtCore, QtGui
13 13
14 14 # Local imports
15 15 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
16 16 from IPython.core.inputtransformer import classic_prompt
17 17 from IPython.core.oinspect import call_tip
18 18 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
19 19 from IPython.utils.traitlets import Bool, Instance, Unicode
20 20 from bracket_matcher import BracketMatcher
21 21 from call_tip_widget import CallTipWidget
22 22 from completion_lexer import CompletionLexer
23 23 from history_console_widget import HistoryConsoleWidget
24 24 from pygments_highlighter import PygmentsHighlighter
25 25
26 26
27 27 class FrontendHighlighter(PygmentsHighlighter):
28 28 """ A PygmentsHighlighter that understands and ignores prompts.
29 29 """
30 30
31 31 def __init__(self, frontend):
32 32 super(FrontendHighlighter, self).__init__(frontend._control.document())
33 33 self._current_offset = 0
34 34 self._frontend = frontend
35 35 self.highlighting_on = False
36 36
37 37 def highlightBlock(self, string):
38 38 """ Highlight a block of text. Reimplemented to highlight selectively.
39 39 """
40 40 if not self.highlighting_on:
41 41 return
42 42
43 43 # The input to this function is a unicode string that may contain
44 44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
45 45 # the string as plain text so we can compare it.
46 46 current_block = self.currentBlock()
47 47 string = self._frontend._get_block_plain_text(current_block)
48 48
49 49 # Decide whether to check for the regular or continuation prompt.
50 50 if current_block.contains(self._frontend._prompt_pos):
51 51 prompt = self._frontend._prompt
52 52 else:
53 53 prompt = self._frontend._continuation_prompt
54 54
55 55 # Only highlight if we can identify a prompt, but make sure not to
56 56 # highlight the prompt.
57 57 if string.startswith(prompt):
58 58 self._current_offset = len(prompt)
59 59 string = string[len(prompt):]
60 60 super(FrontendHighlighter, self).highlightBlock(string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 super(FrontendHighlighter, self).setFormat(start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # The text to show when the kernel is (re)started.
82 82 banner = Unicode()
83 83
84 84 # An option and corresponding signal for overriding the default kernel
85 85 # interrupt behavior.
86 86 custom_interrupt = Bool(False)
87 87 custom_interrupt_requested = QtCore.Signal()
88 88
89 89 # An option and corresponding signals for overriding the default kernel
90 90 # restart behavior.
91 91 custom_restart = Bool(False)
92 92 custom_restart_kernel_died = QtCore.Signal(float)
93 93 custom_restart_requested = QtCore.Signal()
94 94
95 95 # Whether to automatically show calltips on open-parentheses.
96 96 enable_calltips = Bool(True, config=True,
97 97 help="Whether to draw information calltips on open-parentheses.")
98 98
99 99 clear_on_kernel_restart = Bool(True, config=True,
100 100 help="Whether to clear the console when the kernel is restarted")
101 101
102 102 confirm_restart = Bool(True, config=True,
103 103 help="Whether to ask for user confirmation when restarting kernel")
104 104
105 105 # Emitted when a user visible 'execute_request' has been submitted to the
106 106 # kernel from the FrontendWidget. Contains the code to be executed.
107 107 executing = QtCore.Signal(object)
108 108
109 109 # Emitted when a user-visible 'execute_reply' has been received from the
110 110 # kernel and processed by the FrontendWidget. Contains the response message.
111 111 executed = QtCore.Signal(object)
112 112
113 113 # Emitted when an exit request has been received from the kernel.
114 114 exit_requested = QtCore.Signal(object)
115 115
116 116 # Protected class variables.
117 117 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
118 118 logical_line_transforms=[],
119 119 python_line_transforms=[],
120 120 )
121 121 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
122 122 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
123 123 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
124 124 _input_splitter_class = InputSplitter
125 125 _local_kernel = False
126 126 _highlighter = Instance(FrontendHighlighter)
127 127
128 128 #---------------------------------------------------------------------------
129 129 # 'object' interface
130 130 #---------------------------------------------------------------------------
131 131
132 132 def __init__(self, *args, **kw):
133 133 super(FrontendWidget, self).__init__(*args, **kw)
134 134 # FIXME: remove this when PySide min version is updated past 1.0.7
135 135 # forcefully disable calltips if PySide is < 1.0.7, because they crash
136 136 if qt.QT_API == qt.QT_API_PYSIDE:
137 137 import PySide
138 138 if PySide.__version_info__ < (1,0,7):
139 139 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
140 140 self.enable_calltips = False
141 141
142 142 # FrontendWidget protected variables.
143 143 self._bracket_matcher = BracketMatcher(self._control)
144 144 self._call_tip_widget = CallTipWidget(self._control)
145 145 self._completion_lexer = CompletionLexer(PythonLexer())
146 146 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
147 147 self._hidden = False
148 148 self._highlighter = FrontendHighlighter(self)
149 149 self._input_splitter = self._input_splitter_class()
150 150 self._kernel_manager = None
151 151 self._kernel_client = None
152 152 self._request_info = {}
153 153 self._request_info['execute'] = {};
154 154 self._callback_dict = {}
155 155
156 156 # Configure the ConsoleWidget.
157 157 self.tab_width = 4
158 158 self._set_continuation_prompt('... ')
159 159
160 160 # Configure the CallTipWidget.
161 161 self._call_tip_widget.setFont(self.font)
162 162 self.font_changed.connect(self._call_tip_widget.setFont)
163 163
164 164 # Configure actions.
165 165 action = self._copy_raw_action
166 166 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
167 167 action.setEnabled(False)
168 168 action.setShortcut(QtGui.QKeySequence(key))
169 169 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
170 170 action.triggered.connect(self.copy_raw)
171 171 self.copy_available.connect(action.setEnabled)
172 172 self.addAction(action)
173 173
174 174 # Connect signal handlers.
175 175 document = self._control.document()
176 176 document.contentsChange.connect(self._document_contents_change)
177 177
178 178 # Set flag for whether we are connected via localhost.
179 179 self._local_kernel = kw.get('local_kernel',
180 180 FrontendWidget._local_kernel)
181 181
182 182 #---------------------------------------------------------------------------
183 183 # 'ConsoleWidget' public interface
184 184 #---------------------------------------------------------------------------
185 185
186 186 def copy(self):
187 187 """ Copy the currently selected text to the clipboard, removing prompts.
188 188 """
189 189 if self._page_control is not None and self._page_control.hasFocus():
190 190 self._page_control.copy()
191 191 elif self._control.hasFocus():
192 192 text = self._control.textCursor().selection().toPlainText()
193 193 if text:
194 194 text = self._prompt_transformer.transform_cell(text)
195 195 QtGui.QApplication.clipboard().setText(text)
196 196 else:
197 197 self.log.debug("frontend widget : unknown copy target")
198 198
199 199 #---------------------------------------------------------------------------
200 200 # 'ConsoleWidget' abstract interface
201 201 #---------------------------------------------------------------------------
202 202
203 203 def _is_complete(self, source, interactive):
204 204 """ Returns whether 'source' can be completely processed and a new
205 205 prompt created. When triggered by an Enter/Return key press,
206 206 'interactive' is True; otherwise, it is False.
207 207 """
208 208 self._input_splitter.reset()
209 209 complete = self._input_splitter.push(source)
210 210 if interactive:
211 211 complete = not self._input_splitter.push_accepts_more()
212 212 return complete
213 213
214 214 def _execute(self, source, hidden):
215 215 """ Execute 'source'. If 'hidden', do not show any output.
216 216
217 217 See parent class :meth:`execute` docstring for full details.
218 218 """
219 msg_id = self.kernel_client.shell_channel.execute(source, hidden)
219 msg_id = self.kernel_client.execute(source, hidden)
220 220 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
221 221 self._hidden = hidden
222 222 if not hidden:
223 223 self.executing.emit(source)
224 224
225 225 def _prompt_started_hook(self):
226 226 """ Called immediately after a new prompt is displayed.
227 227 """
228 228 if not self._reading:
229 229 self._highlighter.highlighting_on = True
230 230
231 231 def _prompt_finished_hook(self):
232 232 """ Called immediately after a prompt is finished, i.e. when some input
233 233 will be processed and a new prompt displayed.
234 234 """
235 235 # Flush all state from the input splitter so the next round of
236 236 # reading input starts with a clean buffer.
237 237 self._input_splitter.reset()
238 238
239 239 if not self._reading:
240 240 self._highlighter.highlighting_on = False
241 241
242 242 def _tab_pressed(self):
243 243 """ Called when the tab key is pressed. Returns whether to continue
244 244 processing the event.
245 245 """
246 246 # Perform tab completion if:
247 247 # 1) The cursor is in the input buffer.
248 248 # 2) There is a non-whitespace character before the cursor.
249 249 text = self._get_input_buffer_cursor_line()
250 250 if text is None:
251 251 return False
252 252 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
253 253 if complete:
254 254 self._complete()
255 255 return not complete
256 256
257 257 #---------------------------------------------------------------------------
258 258 # 'ConsoleWidget' protected interface
259 259 #---------------------------------------------------------------------------
260 260
261 261 def _context_menu_make(self, pos):
262 262 """ Reimplemented to add an action for raw copy.
263 263 """
264 264 menu = super(FrontendWidget, self)._context_menu_make(pos)
265 265 for before_action in menu.actions():
266 266 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
267 267 QtGui.QKeySequence.ExactMatch:
268 268 menu.insertAction(before_action, self._copy_raw_action)
269 269 break
270 270 return menu
271 271
272 272 def request_interrupt_kernel(self):
273 273 if self._executing:
274 274 self.interrupt_kernel()
275 275
276 276 def request_restart_kernel(self):
277 277 message = 'Are you sure you want to restart the kernel?'
278 278 self.restart_kernel(message, now=False)
279 279
280 280 def _event_filter_console_keypress(self, event):
281 281 """ Reimplemented for execution interruption and smart backspace.
282 282 """
283 283 key = event.key()
284 284 if self._control_key_down(event.modifiers(), include_command=False):
285 285
286 286 if key == QtCore.Qt.Key_C and self._executing:
287 287 self.request_interrupt_kernel()
288 288 return True
289 289
290 290 elif key == QtCore.Qt.Key_Period:
291 291 self.request_restart_kernel()
292 292 return True
293 293
294 294 elif not event.modifiers() & QtCore.Qt.AltModifier:
295 295
296 296 # Smart backspace: remove four characters in one backspace if:
297 297 # 1) everything left of the cursor is whitespace
298 298 # 2) the four characters immediately left of the cursor are spaces
299 299 if key == QtCore.Qt.Key_Backspace:
300 300 col = self._get_input_buffer_cursor_column()
301 301 cursor = self._control.textCursor()
302 302 if col > 3 and not cursor.hasSelection():
303 303 text = self._get_input_buffer_cursor_line()[:col]
304 304 if text.endswith(' ') and not text.strip():
305 305 cursor.movePosition(QtGui.QTextCursor.Left,
306 306 QtGui.QTextCursor.KeepAnchor, 4)
307 307 cursor.removeSelectedText()
308 308 return True
309 309
310 310 return super(FrontendWidget, self)._event_filter_console_keypress(event)
311 311
312 312 def _insert_continuation_prompt(self, cursor):
313 313 """ Reimplemented for auto-indentation.
314 314 """
315 315 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
316 316 cursor.insertText(' ' * self._input_splitter.indent_spaces)
317 317
318 318 #---------------------------------------------------------------------------
319 319 # 'BaseFrontendMixin' abstract interface
320 320 #---------------------------------------------------------------------------
321 321
322 322 def _handle_complete_reply(self, rep):
323 323 """ Handle replies for tab completion.
324 324 """
325 325 self.log.debug("complete: %s", rep.get('content', ''))
326 326 cursor = self._get_cursor()
327 327 info = self._request_info.get('complete')
328 328 if info and info.id == rep['parent_header']['msg_id'] and \
329 329 info.pos == cursor.position():
330 330 text = '.'.join(self._get_context())
331 331 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
332 332 self._complete_with_items(cursor, rep['content']['matches'])
333 333
334 334 def _silent_exec_callback(self, expr, callback):
335 335 """Silently execute `expr` in the kernel and call `callback` with reply
336 336
337 337 the `expr` is evaluated silently in the kernel (without) output in
338 338 the frontend. Call `callback` with the
339 339 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
340 340
341 341 Parameters
342 342 ----------
343 343 expr : string
344 344 valid string to be executed by the kernel.
345 345 callback : function
346 346 function accepting one argument, as a string. The string will be
347 347 the `repr` of the result of evaluating `expr`
348 348
349 349 The `callback` is called with the `repr()` of the result of `expr` as
350 350 first argument. To get the object, do `eval()` on the passed value.
351 351
352 352 See Also
353 353 --------
354 354 _handle_exec_callback : private method, deal with calling callback with reply
355 355
356 356 """
357 357
358 358 # generate uuid, which would be used as an indication of whether or
359 359 # not the unique request originated from here (can use msg id ?)
360 360 local_uuid = str(uuid.uuid1())
361 msg_id = self.kernel_client.shell_channel.execute('',
361 msg_id = self.kernel_client.execute('',
362 362 silent=True, user_expressions={ local_uuid:expr })
363 363 self._callback_dict[local_uuid] = callback
364 364 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
365 365
366 366 def _handle_exec_callback(self, msg):
367 367 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
368 368
369 369 Parameters
370 370 ----------
371 371 msg : raw message send by the kernel containing an `user_expressions`
372 372 and having a 'silent_exec_callback' kind.
373 373
374 374 Notes
375 375 -----
376 376 This function will look for a `callback` associated with the
377 377 corresponding message id. Association has been made by
378 378 `_silent_exec_callback`. `callback` is then called with the `repr()`
379 379 of the value of corresponding `user_expressions` as argument.
380 380 `callback` is then removed from the known list so that any message
381 381 coming again with the same id won't trigger it.
382 382
383 383 """
384 384
385 385 user_exp = msg['content'].get('user_expressions')
386 386 if not user_exp:
387 387 return
388 388 for expression in user_exp:
389 389 if expression in self._callback_dict:
390 390 self._callback_dict.pop(expression)(user_exp[expression])
391 391
392 392 def _handle_execute_reply(self, msg):
393 393 """ Handles replies for code execution.
394 394 """
395 395 self.log.debug("execute: %s", msg.get('content', ''))
396 396 msg_id = msg['parent_header']['msg_id']
397 397 info = self._request_info['execute'].get(msg_id)
398 398 # unset reading flag, because if execute finished, raw_input can't
399 399 # still be pending.
400 400 self._reading = False
401 401 if info and info.kind == 'user' and not self._hidden:
402 402 # Make sure that all output from the SUB channel has been processed
403 403 # before writing a new prompt.
404 404 self.kernel_client.iopub_channel.flush()
405 405
406 406 # Reset the ANSI style information to prevent bad text in stdout
407 407 # from messing up our colors. We're not a true terminal so we're
408 408 # allowed to do this.
409 409 if self.ansi_codes:
410 410 self._ansi_processor.reset_sgr()
411 411
412 412 content = msg['content']
413 413 status = content['status']
414 414 if status == 'ok':
415 415 self._process_execute_ok(msg)
416 416 elif status == 'error':
417 417 self._process_execute_error(msg)
418 418 elif status == 'aborted':
419 419 self._process_execute_abort(msg)
420 420
421 421 self._show_interpreter_prompt_for_reply(msg)
422 422 self.executed.emit(msg)
423 423 self._request_info['execute'].pop(msg_id)
424 424 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
425 425 self._handle_exec_callback(msg)
426 426 self._request_info['execute'].pop(msg_id)
427 427 else:
428 428 super(FrontendWidget, self)._handle_execute_reply(msg)
429 429
430 430 def _handle_input_request(self, msg):
431 431 """ Handle requests for raw_input.
432 432 """
433 433 self.log.debug("input: %s", msg.get('content', ''))
434 434 if self._hidden:
435 435 raise RuntimeError('Request for raw input during hidden execution.')
436 436
437 437 # Make sure that all output from the SUB channel has been processed
438 438 # before entering readline mode.
439 439 self.kernel_client.iopub_channel.flush()
440 440
441 441 def callback(line):
442 442 self.kernel_client.stdin_channel.input(line)
443 443 if self._reading:
444 444 self.log.debug("Got second input request, assuming first was interrupted.")
445 445 self._reading = False
446 446 self._readline(msg['content']['prompt'], callback=callback)
447 447
448 448 def _handle_kernel_died(self, since_last_heartbeat):
449 449 """ Handle the kernel's death by asking if the user wants to restart.
450 450 """
451 451 self.log.debug("kernel died: %s", since_last_heartbeat)
452 452 if self.custom_restart:
453 453 self.custom_restart_kernel_died.emit(since_last_heartbeat)
454 454 else:
455 455 message = 'The kernel heartbeat has been inactive for %.2f ' \
456 456 'seconds. Do you want to restart the kernel? You may ' \
457 457 'first want to check the network connection.' % \
458 458 since_last_heartbeat
459 459 self.restart_kernel(message, now=True)
460 460
461 461 def _handle_object_info_reply(self, rep):
462 462 """ Handle replies for call tips.
463 463 """
464 464 self.log.debug("oinfo: %s", rep.get('content', ''))
465 465 cursor = self._get_cursor()
466 466 info = self._request_info.get('call_tip')
467 467 if info and info.id == rep['parent_header']['msg_id'] and \
468 468 info.pos == cursor.position():
469 469 # Get the information for a call tip. For now we format the call
470 470 # line as string, later we can pass False to format_call and
471 471 # syntax-highlight it ourselves for nicer formatting in the
472 472 # calltip.
473 473 content = rep['content']
474 474 # if this is from pykernel, 'docstring' will be the only key
475 475 if content.get('ismagic', False):
476 476 # Don't generate a call-tip for magics. Ideally, we should
477 477 # generate a tooltip, but not on ( like we do for actual
478 478 # callables.
479 479 call_info, doc = None, None
480 480 else:
481 481 call_info, doc = call_tip(content, format_call=True)
482 482 if call_info or doc:
483 483 self._call_tip_widget.show_call_info(call_info, doc)
484 484
485 485 def _handle_pyout(self, msg):
486 486 """ Handle display hook output.
487 487 """
488 488 self.log.debug("pyout: %s", msg.get('content', ''))
489 489 if not self._hidden and self._is_from_this_session(msg):
490 490 text = msg['content']['data']
491 491 self._append_plain_text(text + '\n', before_prompt=True)
492 492
493 493 def _handle_stream(self, msg):
494 494 """ Handle stdout, stderr, and stdin.
495 495 """
496 496 self.log.debug("stream: %s", msg.get('content', ''))
497 497 if not self._hidden and self._is_from_this_session(msg):
498 498 # Most consoles treat tabs as being 8 space characters. Convert tabs
499 499 # to spaces so that output looks as expected regardless of this
500 500 # widget's tab width.
501 501 text = msg['content']['data'].expandtabs(8)
502 502
503 503 self._append_plain_text(text, before_prompt=True)
504 504 self._control.moveCursor(QtGui.QTextCursor.End)
505 505
506 506 def _handle_shutdown_reply(self, msg):
507 507 """ Handle shutdown signal, only if from other console.
508 508 """
509 509 self.log.debug("shutdown: %s", msg.get('content', ''))
510 510 if not self._hidden and not self._is_from_this_session(msg):
511 511 if self._local_kernel:
512 512 if not msg['content']['restart']:
513 513 self.exit_requested.emit(self)
514 514 else:
515 515 # we just got notified of a restart!
516 516 time.sleep(0.25) # wait 1/4 sec to reset
517 517 # lest the request for a new prompt
518 518 # goes to the old kernel
519 519 self.reset()
520 520 else: # remote kernel, prompt on Kernel shutdown/reset
521 521 title = self.window().windowTitle()
522 522 if not msg['content']['restart']:
523 523 reply = QtGui.QMessageBox.question(self, title,
524 524 "Kernel has been shutdown permanently. "
525 525 "Close the Console?",
526 526 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
527 527 if reply == QtGui.QMessageBox.Yes:
528 528 self.exit_requested.emit(self)
529 529 else:
530 530 # XXX: remove message box in favor of using the
531 531 # clear_on_kernel_restart setting?
532 532 reply = QtGui.QMessageBox.question(self, title,
533 533 "Kernel has been reset. Clear the Console?",
534 534 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
535 535 if reply == QtGui.QMessageBox.Yes:
536 536 time.sleep(0.25) # wait 1/4 sec to reset
537 537 # lest the request for a new prompt
538 538 # goes to the old kernel
539 539 self.reset()
540 540
541 541 def _started_channels(self):
542 542 """ Called when the KernelManager channels have started listening or
543 543 when the frontend is assigned an already listening KernelManager.
544 544 """
545 545 self.reset(clear=True)
546 546
547 547 #---------------------------------------------------------------------------
548 548 # 'FrontendWidget' public interface
549 549 #---------------------------------------------------------------------------
550 550
551 551 def copy_raw(self):
552 552 """ Copy the currently selected text to the clipboard without attempting
553 553 to remove prompts or otherwise alter the text.
554 554 """
555 555 self._control.copy()
556 556
557 557 def execute_file(self, path, hidden=False):
558 558 """ Attempts to execute file with 'path'. If 'hidden', no output is
559 559 shown.
560 560 """
561 561 self.execute('execfile(%r)' % path, hidden=hidden)
562 562
563 563 def interrupt_kernel(self):
564 564 """ Attempts to interrupt the running kernel.
565 565
566 566 Also unsets _reading flag, to avoid runtime errors
567 567 if raw_input is called again.
568 568 """
569 569 if self.custom_interrupt:
570 570 self._reading = False
571 571 self.custom_interrupt_requested.emit()
572 572 elif self.kernel_manager:
573 573 self._reading = False
574 574 self.kernel_manager.interrupt_kernel()
575 575 else:
576 576 self._append_plain_text('Kernel process is either remote or '
577 577 'unspecified. Cannot interrupt.\n')
578 578
579 579 def reset(self, clear=False):
580 580 """ Resets the widget to its initial state if ``clear`` parameter or
581 581 ``clear_on_kernel_restart`` configuration setting is True, otherwise
582 582 prints a visual indication of the fact that the kernel restarted, but
583 583 does not clear the traces from previous usage of the kernel before it
584 584 was restarted. With ``clear=True``, it is similar to ``%clear``, but
585 585 also re-writes the banner and aborts execution if necessary.
586 586 """
587 587 if self._executing:
588 588 self._executing = False
589 589 self._request_info['execute'] = {}
590 590 self._reading = False
591 591 self._highlighter.highlighting_on = False
592 592
593 593 if self.clear_on_kernel_restart or clear:
594 594 self._control.clear()
595 595 self._append_plain_text(self.banner)
596 596 else:
597 597 self._append_plain_text("# restarting kernel...")
598 598 self._append_html("<hr><br>")
599 599 # XXX: Reprinting the full banner may be too much, but once #1680 is
600 600 # addressed, that will mitigate it.
601 601 #self._append_plain_text(self.banner)
602 602 # update output marker for stdout/stderr, so that startup
603 603 # messages appear after banner:
604 604 self._append_before_prompt_pos = self._get_cursor().position()
605 605 self._show_interpreter_prompt()
606 606
607 607 def restart_kernel(self, message, now=False):
608 608 """ Attempts to restart the running kernel.
609 609 """
610 610 # FIXME: now should be configurable via a checkbox in the dialog. Right
611 611 # now at least the heartbeat path sets it to True and the manual restart
612 612 # to False. But those should just be the pre-selected states of a
613 613 # checkbox that the user could override if so desired. But I don't know
614 614 # enough Qt to go implementing the checkbox now.
615 615
616 616 if self.custom_restart:
617 617 self.custom_restart_requested.emit()
618 618
619 619 elif self.kernel_manager:
620 620 # Pause the heart beat channel to prevent further warnings.
621 621 self.kernel_client.hb_channel.pause()
622 622
623 623 # Prompt the user to restart the kernel. Un-pause the heartbeat if
624 624 # they decline. (If they accept, the heartbeat will be un-paused
625 625 # automatically when the kernel is restarted.)
626 626 if self.confirm_restart:
627 627 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
628 628 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
629 629 message, buttons)
630 630 do_restart = result == QtGui.QMessageBox.Yes
631 631 else:
632 632 # confirm_restart is False, so we don't need to ask user
633 633 # anything, just do the restart
634 634 do_restart = True
635 635 if do_restart:
636 636 try:
637 637 self.kernel_manager.restart_kernel(now=now)
638 638 except RuntimeError:
639 639 self._append_plain_text('Kernel started externally. '
640 640 'Cannot restart.\n',
641 641 before_prompt=True
642 642 )
643 643 else:
644 644 self.reset()
645 645 else:
646 646 self.kernel_client.hb_channel.unpause()
647 647
648 648 else:
649 649 self._append_plain_text('Kernel process is either remote or '
650 650 'unspecified. Cannot restart.\n',
651 651 before_prompt=True
652 652 )
653 653
654 654 #---------------------------------------------------------------------------
655 655 # 'FrontendWidget' protected interface
656 656 #---------------------------------------------------------------------------
657 657
658 658 def _call_tip(self):
659 659 """ Shows a call tip, if appropriate, at the current cursor location.
660 660 """
661 661 # Decide if it makes sense to show a call tip
662 662 if not self.enable_calltips:
663 663 return False
664 664 cursor = self._get_cursor()
665 665 cursor.movePosition(QtGui.QTextCursor.Left)
666 666 if cursor.document().characterAt(cursor.position()) != '(':
667 667 return False
668 668 context = self._get_context(cursor)
669 669 if not context:
670 670 return False
671 671
672 672 # Send the metadata request to the kernel
673 673 name = '.'.join(context)
674 msg_id = self.kernel_client.shell_channel.object_info(name)
674 msg_id = self.kernel_client.object_info(name)
675 675 pos = self._get_cursor().position()
676 676 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
677 677 return True
678 678
679 679 def _complete(self):
680 680 """ Performs completion at the current cursor location.
681 681 """
682 682 context = self._get_context()
683 683 if context:
684 684 # Send the completion request to the kernel
685 msg_id = self.kernel_client.shell_channel.complete(
685 msg_id = self.kernel_client.complete(
686 686 '.'.join(context), # text
687 687 self._get_input_buffer_cursor_line(), # line
688 688 self._get_input_buffer_cursor_column(), # cursor_pos
689 689 self.input_buffer) # block
690 690 pos = self._get_cursor().position()
691 691 info = self._CompletionRequest(msg_id, pos)
692 692 self._request_info['complete'] = info
693 693
694 694 def _get_context(self, cursor=None):
695 695 """ Gets the context for the specified cursor (or the current cursor
696 696 if none is specified).
697 697 """
698 698 if cursor is None:
699 699 cursor = self._get_cursor()
700 700 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
701 701 QtGui.QTextCursor.KeepAnchor)
702 702 text = cursor.selection().toPlainText()
703 703 return self._completion_lexer.get_context(text)
704 704
705 705 def _process_execute_abort(self, msg):
706 706 """ Process a reply for an aborted execution request.
707 707 """
708 708 self._append_plain_text("ERROR: execution aborted\n")
709 709
710 710 def _process_execute_error(self, msg):
711 711 """ Process a reply for an execution request that resulted in an error.
712 712 """
713 713 content = msg['content']
714 714 # If a SystemExit is passed along, this means exit() was called - also
715 715 # all the ipython %exit magic syntax of '-k' to be used to keep
716 716 # the kernel running
717 717 if content['ename']=='SystemExit':
718 718 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
719 719 self._keep_kernel_on_exit = keepkernel
720 720 self.exit_requested.emit(self)
721 721 else:
722 722 traceback = ''.join(content['traceback'])
723 723 self._append_plain_text(traceback)
724 724
725 725 def _process_execute_ok(self, msg):
726 726 """ Process a reply for a successful execution request.
727 727 """
728 728 payload = msg['content']['payload']
729 729 for item in payload:
730 730 if not self._process_execute_payload(item):
731 731 warning = 'Warning: received unknown payload of type %s'
732 732 print(warning % repr(item['source']))
733 733
734 734 def _process_execute_payload(self, item):
735 735 """ Process a single payload item from the list of payload items in an
736 736 execution reply. Returns whether the payload was handled.
737 737 """
738 738 # The basic FrontendWidget doesn't handle payloads, as they are a
739 739 # mechanism for going beyond the standard Python interpreter model.
740 740 return False
741 741
742 742 def _show_interpreter_prompt(self):
743 743 """ Shows a prompt for the interpreter.
744 744 """
745 745 self._show_prompt('>>> ')
746 746
747 747 def _show_interpreter_prompt_for_reply(self, msg):
748 748 """ Shows a prompt for the interpreter given an 'execute_reply' message.
749 749 """
750 750 self._show_interpreter_prompt()
751 751
752 752 #------ Signal handlers ----------------------------------------------------
753 753
754 754 def _document_contents_change(self, position, removed, added):
755 755 """ Called whenever the document's content changes. Display a call tip
756 756 if appropriate.
757 757 """
758 758 # Calculate where the cursor should be *after* the change:
759 759 position += added
760 760
761 761 document = self._control.document()
762 762 if position == self._get_cursor().position():
763 763 self._call_tip()
764 764
765 765 #------ Trait default initializers -----------------------------------------
766 766
767 767 def _banner_default(self):
768 768 """ Returns the standard Python banner.
769 769 """
770 770 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
771 771 '"license" for more information.'
772 772 return banner % (sys.version, sys.platform)
@@ -1,638 +1,648
1 1 """Base classes to manage a Client's interaction with a running kernel
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2013 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from __future__ import absolute_import
16 16
17 17 # Standard library imports
18 18 import atexit
19 19 import errno
20 20 from threading import Thread
21 21 import time
22 22
23 23 import zmq
24 24 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
25 25 # during garbage collection of threads at exit:
26 26 from zmq import ZMQError
27 27 from zmq.eventloop import ioloop, zmqstream
28 28
29 29 # Local imports
30 30 from .channelabc import (
31 31 ShellChannelABC, IOPubChannelABC,
32 32 HBChannelABC, StdInChannelABC,
33 33 )
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Constants and exceptions
37 37 #-----------------------------------------------------------------------------
38 38
39 39 class InvalidPortNumber(Exception):
40 40 pass
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # Utility functions
44 44 #-----------------------------------------------------------------------------
45 45
46 46 # some utilities to validate message structure, these might get moved elsewhere
47 47 # if they prove to have more generic utility
48 48
49 49 def validate_string_list(lst):
50 50 """Validate that the input is a list of strings.
51 51
52 52 Raises ValueError if not."""
53 53 if not isinstance(lst, list):
54 54 raise ValueError('input %r must be a list' % lst)
55 55 for x in lst:
56 56 if not isinstance(x, basestring):
57 57 raise ValueError('element %r in list must be a string' % x)
58 58
59 59
60 60 def validate_string_dict(dct):
61 61 """Validate that the input is a dict with string keys and values.
62 62
63 63 Raises ValueError if not."""
64 64 for k,v in dct.iteritems():
65 65 if not isinstance(k, basestring):
66 66 raise ValueError('key %r in dict must be a string' % k)
67 67 if not isinstance(v, basestring):
68 68 raise ValueError('value %r in dict must be a string' % v)
69 69
70 70
71 71 #-----------------------------------------------------------------------------
72 72 # ZMQ Socket Channel classes
73 73 #-----------------------------------------------------------------------------
74 74
75 75 class ZMQSocketChannel(Thread):
76 76 """The base class for the channels that use ZMQ sockets."""
77 77 context = None
78 78 session = None
79 79 socket = None
80 80 ioloop = None
81 81 stream = None
82 82 _address = None
83 83 _exiting = False
84 proxy_methods = []
84 85
85 86 def __init__(self, context, session, address):
86 87 """Create a channel.
87 88
88 89 Parameters
89 90 ----------
90 91 context : :class:`zmq.Context`
91 92 The ZMQ context to use.
92 93 session : :class:`session.Session`
93 94 The session to use.
94 95 address : zmq url
95 96 Standard (ip, port) tuple that the kernel is listening on.
96 97 """
97 98 super(ZMQSocketChannel, self).__init__()
98 99 self.daemon = True
99 100
100 101 self.context = context
101 102 self.session = session
102 103 if isinstance(address, tuple):
103 104 if address[1] == 0:
104 105 message = 'The port number for a channel cannot be 0.'
105 106 raise InvalidPortNumber(message)
106 107 address = "tcp://%s:%i" % address
107 108 self._address = address
108 109 atexit.register(self._notice_exit)
109 110
110 111 def _notice_exit(self):
111 112 self._exiting = True
112 113
113 114 def _run_loop(self):
114 115 """Run my loop, ignoring EINTR events in the poller"""
115 116 while True:
116 117 try:
117 118 self.ioloop.start()
118 119 except ZMQError as e:
119 120 if e.errno == errno.EINTR:
120 121 continue
121 122 else:
122 123 raise
123 124 except Exception:
124 125 if self._exiting:
125 126 break
126 127 else:
127 128 raise
128 129 else:
129 130 break
130 131
131 132 def stop(self):
132 133 """Stop the channel's event loop and join its thread.
133 134
134 135 This calls :method:`Thread.join` and returns when the thread
135 136 terminates. :class:`RuntimeError` will be raised if
136 137 :method:`self.start` is called again.
137 138 """
138 139 self.join()
139 140
140 141 @property
141 142 def address(self):
142 143 """Get the channel's address as a zmq url string.
143 144
144 145 These URLS have the form: 'tcp://127.0.0.1:5555'.
145 146 """
146 147 return self._address
147 148
148 149 def _queue_send(self, msg):
149 150 """Queue a message to be sent from the IOLoop's thread.
150 151
151 152 Parameters
152 153 ----------
153 154 msg : message to send
154 155
155 156 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
156 157 thread control of the action.
157 158 """
158 159 def thread_send():
159 160 self.session.send(self.stream, msg)
160 161 self.ioloop.add_callback(thread_send)
161 162
162 163 def _handle_recv(self, msg):
163 164 """Callback for stream.on_recv.
164 165
165 166 Unpacks message, and calls handlers with it.
166 167 """
167 168 ident,smsg = self.session.feed_identities(msg)
168 169 self.call_handlers(self.session.unserialize(smsg))
169 170
170 171
171 172
172 173 class ShellChannel(ZMQSocketChannel):
173 174 """The shell channel for issuing request/replies to the kernel."""
174 175
175 176 command_queue = None
176 177 # flag for whether execute requests should be allowed to call raw_input:
177 178 allow_stdin = True
179 proxy_methods = [
180 'execute',
181 'complete',
182 'object_info',
183 'history',
184 'kernel_info',
185 'shutdown',
186 ]
178 187
179 188 def __init__(self, context, session, address):
180 189 super(ShellChannel, self).__init__(context, session, address)
181 190 self.ioloop = ioloop.IOLoop()
182 191
183 192 def run(self):
184 193 """The thread's main activity. Call start() instead."""
185 194 self.socket = self.context.socket(zmq.DEALER)
186 195 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
187 196 self.socket.connect(self.address)
188 197 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
189 198 self.stream.on_recv(self._handle_recv)
190 199 self._run_loop()
191 200 try:
192 201 self.socket.close()
193 202 except:
194 203 pass
195 204
196 205 def stop(self):
197 206 """Stop the channel's event loop and join its thread."""
198 207 self.ioloop.stop()
199 208 super(ShellChannel, self).stop()
200 209
201 210 def call_handlers(self, msg):
202 211 """This method is called in the ioloop thread when a message arrives.
203 212
204 213 Subclasses should override this method to handle incoming messages.
205 214 It is important to remember that this method is called in the thread
206 215 so that some logic must be done to ensure that the application level
207 216 handlers are called in the application thread.
208 217 """
209 218 raise NotImplementedError('call_handlers must be defined in a subclass.')
210 219
211 220 def execute(self, code, silent=False, store_history=True,
212 221 user_variables=None, user_expressions=None, allow_stdin=None):
213 222 """Execute code in the kernel.
214 223
215 224 Parameters
216 225 ----------
217 226 code : str
218 227 A string of Python code.
219 228
220 229 silent : bool, optional (default False)
221 230 If set, the kernel will execute the code as quietly possible, and
222 231 will force store_history to be False.
223 232
224 233 store_history : bool, optional (default True)
225 234 If set, the kernel will store command history. This is forced
226 235 to be False if silent is True.
227 236
228 237 user_variables : list, optional
229 238 A list of variable names to pull from the user's namespace. They
230 239 will come back as a dict with these names as keys and their
231 240 :func:`repr` as values.
232 241
233 242 user_expressions : dict, optional
234 243 A dict mapping names to expressions to be evaluated in the user's
235 244 dict. The expression values are returned as strings formatted using
236 245 :func:`repr`.
237 246
238 247 allow_stdin : bool, optional (default self.allow_stdin)
239 248 Flag for whether the kernel can send stdin requests to frontends.
240 249
241 250 Some frontends (e.g. the Notebook) do not support stdin requests.
242 251 If raw_input is called from code executed from such a frontend, a
243 252 StdinNotImplementedError will be raised.
244 253
245 254 Returns
246 255 -------
247 256 The msg_id of the message sent.
248 257 """
249 258 if user_variables is None:
250 259 user_variables = []
251 260 if user_expressions is None:
252 261 user_expressions = {}
253 262 if allow_stdin is None:
254 263 allow_stdin = self.allow_stdin
255 264
256 265
257 266 # Don't waste network traffic if inputs are invalid
258 267 if not isinstance(code, basestring):
259 268 raise ValueError('code %r must be a string' % code)
260 269 validate_string_list(user_variables)
261 270 validate_string_dict(user_expressions)
262 271
263 272 # Create class for content/msg creation. Related to, but possibly
264 273 # not in Session.
265 274 content = dict(code=code, silent=silent, store_history=store_history,
266 275 user_variables=user_variables,
267 276 user_expressions=user_expressions,
268 277 allow_stdin=allow_stdin,
269 278 )
270 279 msg = self.session.msg('execute_request', content)
271 280 self._queue_send(msg)
272 281 return msg['header']['msg_id']
273 282
274 283 def complete(self, text, line, cursor_pos, block=None):
275 284 """Tab complete text in the kernel's namespace.
276 285
277 286 Parameters
278 287 ----------
279 288 text : str
280 289 The text to complete.
281 290 line : str
282 291 The full line of text that is the surrounding context for the
283 292 text to complete.
284 293 cursor_pos : int
285 294 The position of the cursor in the line where the completion was
286 295 requested.
287 296 block : str, optional
288 297 The full block of code in which the completion is being requested.
289 298
290 299 Returns
291 300 -------
292 301 The msg_id of the message sent.
293 302 """
294 303 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
295 304 msg = self.session.msg('complete_request', content)
296 305 self._queue_send(msg)
297 306 return msg['header']['msg_id']
298 307
299 308 def object_info(self, oname, detail_level=0):
300 309 """Get metadata information about an object in the kernel's namespace.
301 310
302 311 Parameters
303 312 ----------
304 313 oname : str
305 314 A string specifying the object name.
306 315 detail_level : int, optional
307 316 The level of detail for the introspection (0-2)
308 317
309 318 Returns
310 319 -------
311 320 The msg_id of the message sent.
312 321 """
313 322 content = dict(oname=oname, detail_level=detail_level)
314 323 msg = self.session.msg('object_info_request', content)
315 324 self._queue_send(msg)
316 325 return msg['header']['msg_id']
317 326
318 327 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
319 328 """Get entries from the kernel's history list.
320 329
321 330 Parameters
322 331 ----------
323 332 raw : bool
324 333 If True, return the raw input.
325 334 output : bool
326 335 If True, then return the output as well.
327 336 hist_access_type : str
328 337 'range' (fill in session, start and stop params), 'tail' (fill in n)
329 338 or 'search' (fill in pattern param).
330 339
331 340 session : int
332 341 For a range request, the session from which to get lines. Session
333 342 numbers are positive integers; negative ones count back from the
334 343 current session.
335 344 start : int
336 345 The first line number of a history range.
337 346 stop : int
338 347 The final (excluded) line number of a history range.
339 348
340 349 n : int
341 350 The number of lines of history to get for a tail request.
342 351
343 352 pattern : str
344 353 The glob-syntax pattern for a search request.
345 354
346 355 Returns
347 356 -------
348 357 The msg_id of the message sent.
349 358 """
350 359 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
351 360 **kwargs)
352 361 msg = self.session.msg('history_request', content)
353 362 self._queue_send(msg)
354 363 return msg['header']['msg_id']
355 364
356 365 def kernel_info(self):
357 366 """Request kernel info."""
358 367 msg = self.session.msg('kernel_info_request')
359 368 self._queue_send(msg)
360 369 return msg['header']['msg_id']
361 370
362 371 def shutdown(self, restart=False):
363 372 """Request an immediate kernel shutdown.
364 373
365 374 Upon receipt of the (empty) reply, client code can safely assume that
366 375 the kernel has shut down and it's safe to forcefully terminate it if
367 376 it's still alive.
368 377
369 378 The kernel will send the reply via a function registered with Python's
370 379 atexit module, ensuring it's truly done as the kernel is done with all
371 380 normal operation.
372 381 """
373 382 # Send quit message to kernel. Once we implement kernel-side setattr,
374 383 # this should probably be done that way, but for now this will do.
375 384 msg = self.session.msg('shutdown_request', {'restart':restart})
376 385 self._queue_send(msg)
377 386 return msg['header']['msg_id']
378 387
379 388
380 389
381 390 class IOPubChannel(ZMQSocketChannel):
382 391 """The iopub channel which listens for messages that the kernel publishes.
383 392
384 393 This channel is where all output is published to frontends.
385 394 """
386 395
387 396 def __init__(self, context, session, address):
388 397 super(IOPubChannel, self).__init__(context, session, address)
389 398 self.ioloop = ioloop.IOLoop()
390 399
391 400 def run(self):
392 401 """The thread's main activity. Call start() instead."""
393 402 self.socket = self.context.socket(zmq.SUB)
394 403 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
395 404 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
396 405 self.socket.connect(self.address)
397 406 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
398 407 self.stream.on_recv(self._handle_recv)
399 408 self._run_loop()
400 409 try:
401 410 self.socket.close()
402 411 except:
403 412 pass
404 413
405 414 def stop(self):
406 415 """Stop the channel's event loop and join its thread."""
407 416 self.ioloop.stop()
408 417 super(IOPubChannel, self).stop()
409 418
410 419 def call_handlers(self, msg):
411 420 """This method is called in the ioloop thread when a message arrives.
412 421
413 422 Subclasses should override this method to handle incoming messages.
414 423 It is important to remember that this method is called in the thread
415 424 so that some logic must be done to ensure that the application leve
416 425 handlers are called in the application thread.
417 426 """
418 427 raise NotImplementedError('call_handlers must be defined in a subclass.')
419 428
420 429 def flush(self, timeout=1.0):
421 430 """Immediately processes all pending messages on the iopub channel.
422 431
423 432 Callers should use this method to ensure that :method:`call_handlers`
424 433 has been called for all messages that have been received on the
425 434 0MQ SUB socket of this channel.
426 435
427 436 This method is thread safe.
428 437
429 438 Parameters
430 439 ----------
431 440 timeout : float, optional
432 441 The maximum amount of time to spend flushing, in seconds. The
433 442 default is one second.
434 443 """
435 444 # We do the IOLoop callback process twice to ensure that the IOLoop
436 445 # gets to perform at least one full poll.
437 446 stop_time = time.time() + timeout
438 447 for i in xrange(2):
439 448 self._flushed = False
440 449 self.ioloop.add_callback(self._flush)
441 450 while not self._flushed and time.time() < stop_time:
442 451 time.sleep(0.01)
443 452
444 453 def _flush(self):
445 454 """Callback for :method:`self.flush`."""
446 455 self.stream.flush()
447 456 self._flushed = True
448 457
449 458
450 459 class StdInChannel(ZMQSocketChannel):
451 460 """The stdin channel to handle raw_input requests that the kernel makes."""
452 461
453 462 msg_queue = None
463 proxy_methods = ['input']
454 464
455 465 def __init__(self, context, session, address):
456 466 super(StdInChannel, self).__init__(context, session, address)
457 467 self.ioloop = ioloop.IOLoop()
458 468
459 469 def run(self):
460 470 """The thread's main activity. Call start() instead."""
461 471 self.socket = self.context.socket(zmq.DEALER)
462 472 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
463 473 self.socket.connect(self.address)
464 474 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
465 475 self.stream.on_recv(self._handle_recv)
466 476 self._run_loop()
467 477 try:
468 478 self.socket.close()
469 479 except:
470 480 pass
471 481
472 482 def stop(self):
473 483 """Stop the channel's event loop and join its thread."""
474 484 self.ioloop.stop()
475 485 super(StdInChannel, self).stop()
476 486
477 487 def call_handlers(self, msg):
478 488 """This method is called in the ioloop thread when a message arrives.
479 489
480 490 Subclasses should override this method to handle incoming messages.
481 491 It is important to remember that this method is called in the thread
482 492 so that some logic must be done to ensure that the application leve
483 493 handlers are called in the application thread.
484 494 """
485 495 raise NotImplementedError('call_handlers must be defined in a subclass.')
486 496
487 497 def input(self, string):
488 498 """Send a string of raw input to the kernel."""
489 499 content = dict(value=string)
490 500 msg = self.session.msg('input_reply', content)
491 501 self._queue_send(msg)
492 502
493 503
494 504 class HBChannel(ZMQSocketChannel):
495 505 """The heartbeat channel which monitors the kernel heartbeat.
496 506
497 507 Note that the heartbeat channel is paused by default. As long as you start
498 508 this channel, the kernel manager will ensure that it is paused and un-paused
499 509 as appropriate.
500 510 """
501 511
502 512 time_to_dead = 3.0
503 513 socket = None
504 514 poller = None
505 515 _running = None
506 516 _pause = None
507 517 _beating = None
508 518
509 519 def __init__(self, context, session, address):
510 520 super(HBChannel, self).__init__(context, session, address)
511 521 self._running = False
512 522 self._pause =True
513 523 self.poller = zmq.Poller()
514 524
515 525 def _create_socket(self):
516 526 if self.socket is not None:
517 527 # close previous socket, before opening a new one
518 528 self.poller.unregister(self.socket)
519 529 self.socket.close()
520 530 self.socket = self.context.socket(zmq.REQ)
521 531 self.socket.setsockopt(zmq.LINGER, 0)
522 532 self.socket.connect(self.address)
523 533
524 534 self.poller.register(self.socket, zmq.POLLIN)
525 535
526 536 def _poll(self, start_time):
527 537 """poll for heartbeat replies until we reach self.time_to_dead.
528 538
529 539 Ignores interrupts, and returns the result of poll(), which
530 540 will be an empty list if no messages arrived before the timeout,
531 541 or the event tuple if there is a message to receive.
532 542 """
533 543
534 544 until_dead = self.time_to_dead - (time.time() - start_time)
535 545 # ensure poll at least once
536 546 until_dead = max(until_dead, 1e-3)
537 547 events = []
538 548 while True:
539 549 try:
540 550 events = self.poller.poll(1000 * until_dead)
541 551 except ZMQError as e:
542 552 if e.errno == errno.EINTR:
543 553 # ignore interrupts during heartbeat
544 554 # this may never actually happen
545 555 until_dead = self.time_to_dead - (time.time() - start_time)
546 556 until_dead = max(until_dead, 1e-3)
547 557 pass
548 558 else:
549 559 raise
550 560 except Exception:
551 561 if self._exiting:
552 562 break
553 563 else:
554 564 raise
555 565 else:
556 566 break
557 567 return events
558 568
559 569 def run(self):
560 570 """The thread's main activity. Call start() instead."""
561 571 self._create_socket()
562 572 self._running = True
563 573 self._beating = True
564 574
565 575 while self._running:
566 576 if self._pause:
567 577 # just sleep, and skip the rest of the loop
568 578 time.sleep(self.time_to_dead)
569 579 continue
570 580
571 581 since_last_heartbeat = 0.0
572 582 # io.rprint('Ping from HB channel') # dbg
573 583 # no need to catch EFSM here, because the previous event was
574 584 # either a recv or connect, which cannot be followed by EFSM
575 585 self.socket.send(b'ping')
576 586 request_time = time.time()
577 587 ready = self._poll(request_time)
578 588 if ready:
579 589 self._beating = True
580 590 # the poll above guarantees we have something to recv
581 591 self.socket.recv()
582 592 # sleep the remainder of the cycle
583 593 remainder = self.time_to_dead - (time.time() - request_time)
584 594 if remainder > 0:
585 595 time.sleep(remainder)
586 596 continue
587 597 else:
588 598 # nothing was received within the time limit, signal heart failure
589 599 self._beating = False
590 600 since_last_heartbeat = time.time() - request_time
591 601 self.call_handlers(since_last_heartbeat)
592 602 # and close/reopen the socket, because the REQ/REP cycle has been broken
593 603 self._create_socket()
594 604 continue
595 605 try:
596 606 self.socket.close()
597 607 except:
598 608 pass
599 609
600 610 def pause(self):
601 611 """Pause the heartbeat."""
602 612 self._pause = True
603 613
604 614 def unpause(self):
605 615 """Unpause the heartbeat."""
606 616 self._pause = False
607 617
608 618 def is_beating(self):
609 619 """Is the heartbeat running and responsive (and not paused)."""
610 620 if self.is_alive() and not self._pause and self._beating:
611 621 return True
612 622 else:
613 623 return False
614 624
615 625 def stop(self):
616 626 """Stop the channel's event loop and join its thread."""
617 627 self._running = False
618 628 super(HBChannel, self).stop()
619 629
620 630 def call_handlers(self, since_last_heartbeat):
621 631 """This method is called in the ioloop thread when a message arrives.
622 632
623 633 Subclasses should override this method to handle incoming messages.
624 634 It is important to remember that this method is called in the thread
625 635 so that some logic must be done to ensure that the application level
626 636 handlers are called in the application thread.
627 637 """
628 638 raise NotImplementedError('call_handlers must be defined in a subclass.')
629 639
630 640
631 641 #---------------------------------------------------------------------#-----------------------------------------------------------------------------
632 642 # ABC Registration
633 643 #-----------------------------------------------------------------------------
634 644
635 645 ShellChannelABC.register(ShellChannel)
636 646 IOPubChannelABC.register(IOPubChannel)
637 647 HBChannelABC.register(HBChannel)
638 648 StdInChannelABC.register(StdInChannel)
@@ -1,182 +1,216
1 1 """Base class to manage the interaction with a running kernel
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2013 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 from __future__ import absolute_import
16 16
17 17 import zmq
18 18
19 19 # Local imports
20 20 from IPython.config.configurable import LoggingConfigurable
21 21 from IPython.utils.traitlets import (
22 22 Any, Instance, Type,
23 23 )
24 24
25 25 from .zmq.session import Session
26 26 from .channels import (
27 27 ShellChannel, IOPubChannel,
28 28 HBChannel, StdInChannel,
29 29 )
30 30 from .clientabc import KernelClientABC
31 31 from .connect import ConnectionFileMixin
32 32
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Main kernel client class
36 36 #-----------------------------------------------------------------------------
37 37
38 38 class KernelClient(LoggingConfigurable, ConnectionFileMixin):
39 39 """Communicates with a single kernel on any host via zmq channels.
40 40
41 41 There are four channels associated with each kernel:
42 42
43 43 * shell: for request/reply calls to the kernel.
44 44 * iopub: for the kernel to publish results to frontends.
45 45 * hb: for monitoring the kernel's heartbeat.
46 46 * stdin: for frontends to reply to raw_input calls in the kernel.
47 47
48 48 """
49 49
50 50 # The PyZMQ Context to use for communication with the kernel.
51 51 context = Instance(zmq.Context)
52 52 def _context_default(self):
53 53 return zmq.Context.instance()
54 54
55 55 # The Session to use for communication with the kernel.
56 56 session = Instance(Session)
57 57 def _session_default(self):
58 58 return Session(config=self.config)
59 59
60 60 # The classes to use for the various channels
61 61 shell_channel_class = Type(ShellChannel)
62 62 iopub_channel_class = Type(IOPubChannel)
63 63 stdin_channel_class = Type(StdInChannel)
64 64 hb_channel_class = Type(HBChannel)
65 65
66 66 # Protected traits
67 67 _shell_channel = Any
68 68 _iopub_channel = Any
69 69 _stdin_channel = Any
70 70 _hb_channel = Any
71 71
72 # def __init__(self, *args, **kwargs):
73 # super(KernelClient, self).__init__(*args, **kwargs)
74 # # setup channel proxy methods, e.g.
75 # # Client.execute => shell_channel.execute
76 # for channel in ['shell', 'iopub', 'stdin', 'hb']:
77 # cls = getattr(self, '%s_channel_class' % channel)
78 # for method in cls.proxy_methods:
79 # setattr(self, method, self._proxy_method(channel, method))
80 #
81 #--------------------------------------------------------------------------
82 # Channel proxy methods
83 #--------------------------------------------------------------------------
84
85 def _get_msg(channel, *args, **kwargs):
86 return channel.get_msg(*args, **kwargs)
87
88 def get_shell_msg(self, *args, **kwargs):
89 """Get a message from the shell channel"""
90 return self.shell_channel.get_msg(*args, **kwargs)
91
92 def get_iopub_msg(self, *args, **kwargs):
93 """Get a message from the iopub channel"""
94 return self.iopub_channel.get_msg(*args, **kwargs)
95
96 def get_stdin_msg(self, *args, **kwargs):
97 """Get a message from the stdin channel"""
98 return self.stdin_channel.get_msg(*args, **kwargs)
99
72 100 #--------------------------------------------------------------------------
73 101 # Channel management methods
74 102 #--------------------------------------------------------------------------
75 103
76 104 def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
77 105 """Starts the channels for this kernel.
78 106
79 107 This will create the channels if they do not exist and then start
80 108 them (their activity runs in a thread). If port numbers of 0 are
81 109 being used (random ports) then you must first call
82 110 :method:`start_kernel`. If the channels have been stopped and you
83 111 call this, :class:`RuntimeError` will be raised.
84 112 """
85 113 if shell:
86 114 self.shell_channel.start()
115 for method in self.shell_channel.proxy_methods:
116 setattr(self, method, getattr(self.shell_channel, method))
87 117 if iopub:
88 118 self.iopub_channel.start()
119 for method in self.iopub_channel.proxy_methods:
120 setattr(self, method, getattr(self.iopub_channel, method))
89 121 if stdin:
90 122 self.stdin_channel.start()
123 for method in self.stdin_channel.proxy_methods:
124 setattr(self, method, getattr(self.stdin_channel, method))
91 125 self.shell_channel.allow_stdin = True
92 126 else:
93 127 self.shell_channel.allow_stdin = False
94 128 if hb:
95 129 self.hb_channel.start()
96 130
97 131 def stop_channels(self):
98 132 """Stops all the running channels for this kernel.
99 133
100 134 This stops their event loops and joins their threads.
101 135 """
102 136 if self.shell_channel.is_alive():
103 137 self.shell_channel.stop()
104 138 if self.iopub_channel.is_alive():
105 139 self.iopub_channel.stop()
106 140 if self.stdin_channel.is_alive():
107 141 self.stdin_channel.stop()
108 142 if self.hb_channel.is_alive():
109 143 self.hb_channel.stop()
110 144
111 145 @property
112 146 def channels_running(self):
113 147 """Are any of the channels created and running?"""
114 148 return (self.shell_channel.is_alive() or self.iopub_channel.is_alive() or
115 149 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
116 150
117 151 def _make_url(self, port):
118 152 """Make a zmq url with a port.
119 153
120 154 There are two cases that this handles:
121 155
122 156 * tcp: tcp://ip:port
123 157 * ipc: ipc://ip-port
124 158 """
125 159 if self.transport == 'tcp':
126 160 return "tcp://%s:%i" % (self.ip, port)
127 161 else:
128 162 return "%s://%s-%s" % (self.transport, self.ip, port)
129 163
130 164 @property
131 165 def shell_channel(self):
132 166 """Get the shell channel object for this kernel."""
133 167 if self._shell_channel is None:
134 168 self._shell_channel = self.shell_channel_class(
135 169 self.context, self.session, self._make_url(self.shell_port)
136 170 )
137 171 return self._shell_channel
138 172
139 173 @property
140 174 def iopub_channel(self):
141 175 """Get the iopub channel object for this kernel."""
142 176 if self._iopub_channel is None:
143 177 self._iopub_channel = self.iopub_channel_class(
144 178 self.context, self.session, self._make_url(self.iopub_port)
145 179 )
146 180 return self._iopub_channel
147 181
148 182 @property
149 183 def stdin_channel(self):
150 184 """Get the stdin channel object for this kernel."""
151 185 if self._stdin_channel is None:
152 186 self._stdin_channel = self.stdin_channel_class(
153 187 self.context, self.session, self._make_url(self.stdin_port)
154 188 )
155 189 return self._stdin_channel
156 190
157 191 @property
158 192 def hb_channel(self):
159 193 """Get the hb channel object for this kernel."""
160 194 if self._hb_channel is None:
161 195 self._hb_channel = self.hb_channel_class(
162 196 self.context, self.session, self._make_url(self.hb_port)
163 197 )
164 198 return self._hb_channel
165 199
166 200 def is_alive(self):
167 201 """Is the kernel process still running?"""
168 202 if self._hb_channel is not None:
169 203 # We didn't start the kernel with this KernelManager so we
170 204 # use the heartbeat.
171 205 return self._hb_channel.is_beating()
172 206 else:
173 207 # no heartbeat and not local, we can't tell if it's running,
174 208 # so naively return True
175 209 return True
176 210
177 211
178 212 #-----------------------------------------------------------------------------
179 213 # ABC Registration
180 214 #-----------------------------------------------------------------------------
181 215
182 216 KernelClientABC.register(KernelClient)
@@ -1,502 +1,484
1 1 """Test suite for our zeromq-based messaging specification.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010-2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 import re
11 11 import sys
12 12 import time
13 13 from subprocess import PIPE
14 14 from Queue import Empty
15 15
16 16 import nose.tools as nt
17 17
18 18 from IPython.kernel import KernelManager, BlockingKernelClient
19 19
20 20
21 21 from IPython.testing import decorators as dec
22 22 from IPython.utils import io
23 23 from IPython.utils.traitlets import (
24 24 HasTraits, TraitError, Bool, Unicode, Dict, Integer, List, Enum, Any,
25 25 )
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Global setup and utilities
29 29 #-----------------------------------------------------------------------------
30 30
31 31 def setup():
32 32 global KM, KC
33 33 KM = KernelManager()
34 34 KM.start_kernel(stdout=PIPE, stderr=PIPE)
35 35 KC = BlockingKernelClient(connection_file=KM.connection_file)
36 36 KC.load_connection_file()
37 37 KC.start_channels()
38 38
39 39 # wait for kernel to be ready
40 40 KC.shell_channel.execute("pass")
41 41 KC.shell_channel.get_msg(block=True, timeout=5)
42 42 flush_channels()
43 43
44 44
45 45 def teardown():
46 46 KC.stop_channels()
47 47 KM.shutdown_kernel()
48 48
49 49
50 50 def flush_channels(kc=None):
51 51 """flush any messages waiting on the queue"""
52 52 if kc is None:
53 53 kc = KC
54 54 for channel in (kc.shell_channel, kc.iopub_channel):
55 55 while True:
56 56 try:
57 57 msg = channel.get_msg(block=True, timeout=0.1)
58 58 except Empty:
59 59 break
60 60 else:
61 61 list(validate_message(msg))
62 62
63 63
64 64 def execute(code='', kc=None, **kwargs):
65 65 """wrapper for doing common steps for validating an execution request"""
66 if kc is None:
67 kc = KC
68 shell = kc.shell_channel
69 sub = kc.iopub_channel
70
71 msg_id = shell.execute(code=code, **kwargs)
72 reply = shell.get_msg(timeout=2)
66 msg_id = KC.execute(code=code, **kwargs)
67 reply = KC.get_shell_msg(timeout=2)
73 68 list(validate_message(reply, 'execute_reply', msg_id))
74 busy = sub.get_msg(timeout=2)
69 busy = KC.get_iopub_msg(timeout=2)
75 70 list(validate_message(busy, 'status', msg_id))
76 71 nt.assert_equal(busy['content']['execution_state'], 'busy')
77 72
78 73 if not kwargs.get('silent'):
79 pyin = sub.get_msg(timeout=2)
74 pyin = KC.get_iopub_msg(timeout=2)
80 75 list(validate_message(pyin, 'pyin', msg_id))
81 76 nt.assert_equal(pyin['content']['code'], code)
82 77
83 78 return msg_id, reply['content']
84 79
85 80 #-----------------------------------------------------------------------------
86 81 # MSG Spec References
87 82 #-----------------------------------------------------------------------------
88 83
89 84
90 85 class Reference(HasTraits):
91 86
92 87 """
93 88 Base class for message spec specification testing.
94 89
95 90 This class is the core of the message specification test. The
96 91 idea is that child classes implement trait attributes for each
97 92 message keys, so that message keys can be tested against these
98 93 traits using :meth:`check` method.
99 94
100 95 """
101 96
102 97 def check(self, d):
103 98 """validate a dict against our traits"""
104 99 for key in self.trait_names():
105 100 yield nt.assert_true(key in d, "Missing key: %r, should be found in %s" % (key, d))
106 101 # FIXME: always allow None, probably not a good idea
107 102 if d[key] is None:
108 103 continue
109 104 try:
110 105 setattr(self, key, d[key])
111 106 except TraitError as e:
112 107 yield nt.assert_true(False, str(e))
113 108
114 109
115 110 class RMessage(Reference):
116 111 msg_id = Unicode()
117 112 msg_type = Unicode()
118 113 header = Dict()
119 114 parent_header = Dict()
120 115 content = Dict()
121 116
122 117 class RHeader(Reference):
123 118 msg_id = Unicode()
124 119 msg_type = Unicode()
125 120 session = Unicode()
126 121 username = Unicode()
127 122
128 123 class RContent(Reference):
129 124 status = Enum((u'ok', u'error'))
130 125
131 126
132 127 class ExecuteReply(Reference):
133 128 execution_count = Integer()
134 129 status = Enum((u'ok', u'error'))
135 130
136 131 def check(self, d):
137 132 for tst in Reference.check(self, d):
138 133 yield tst
139 134 if d['status'] == 'ok':
140 135 for tst in ExecuteReplyOkay().check(d):
141 136 yield tst
142 137 elif d['status'] == 'error':
143 138 for tst in ExecuteReplyError().check(d):
144 139 yield tst
145 140
146 141
147 142 class ExecuteReplyOkay(Reference):
148 143 payload = List(Dict)
149 144 user_variables = Dict()
150 145 user_expressions = Dict()
151 146
152 147
153 148 class ExecuteReplyError(Reference):
154 149 ename = Unicode()
155 150 evalue = Unicode()
156 151 traceback = List(Unicode)
157 152
158 153
159 154 class OInfoReply(Reference):
160 155 name = Unicode()
161 156 found = Bool()
162 157 ismagic = Bool()
163 158 isalias = Bool()
164 159 namespace = Enum((u'builtin', u'magics', u'alias', u'Interactive'))
165 160 type_name = Unicode()
166 161 string_form = Unicode()
167 162 base_class = Unicode()
168 163 length = Integer()
169 164 file = Unicode()
170 165 definition = Unicode()
171 166 argspec = Dict()
172 167 init_definition = Unicode()
173 168 docstring = Unicode()
174 169 init_docstring = Unicode()
175 170 class_docstring = Unicode()
176 171 call_def = Unicode()
177 172 call_docstring = Unicode()
178 173 source = Unicode()
179 174
180 175 def check(self, d):
181 176 for tst in Reference.check(self, d):
182 177 yield tst
183 178 if d['argspec'] is not None:
184 179 for tst in ArgSpec().check(d['argspec']):
185 180 yield tst
186 181
187 182
188 183 class ArgSpec(Reference):
189 184 args = List(Unicode)
190 185 varargs = Unicode()
191 186 varkw = Unicode()
192 187 defaults = List()
193 188
194 189
195 190 class Status(Reference):
196 191 execution_state = Enum((u'busy', u'idle'))
197 192
198 193
199 194 class CompleteReply(Reference):
200 195 matches = List(Unicode)
201 196
202 197
203 198 def Version(num, trait=Integer):
204 199 return List(trait, default_value=[0] * num, minlen=num, maxlen=num)
205 200
206 201
207 202 class KernelInfoReply(Reference):
208 203
209 204 protocol_version = Version(2)
210 205 ipython_version = Version(4, Any)
211 206 language_version = Version(3)
212 207 language = Unicode()
213 208
214 209 def _ipython_version_changed(self, name, old, new):
215 210 for v in new:
216 211 nt.assert_true(
217 212 isinstance(v, int) or isinstance(v, basestring),
218 213 'expected int or string as version component, got {0!r}'
219 214 .format(v))
220 215
221 216
222 217 # IOPub messages
223 218
224 219 class PyIn(Reference):
225 220 code = Unicode()
226 221 execution_count = Integer()
227 222
228 223
229 224 PyErr = ExecuteReplyError
230 225
231 226
232 227 class Stream(Reference):
233 228 name = Enum((u'stdout', u'stderr'))
234 229 data = Unicode()
235 230
236 231
237 232 mime_pat = re.compile(r'\w+/\w+')
238 233
239 234 class DisplayData(Reference):
240 235 source = Unicode()
241 236 metadata = Dict()
242 237 data = Dict()
243 238 def _data_changed(self, name, old, new):
244 239 for k,v in new.iteritems():
245 240 nt.assert_true(mime_pat.match(k))
246 241 nt.assert_true(isinstance(v, basestring), "expected string data, got %r" % v)
247 242
248 243
249 244 class PyOut(Reference):
250 245 execution_count = Integer()
251 246 data = Dict()
252 247 def _data_changed(self, name, old, new):
253 248 for k,v in new.iteritems():
254 249 nt.assert_true(mime_pat.match(k))
255 250 nt.assert_true(isinstance(v, basestring), "expected string data, got %r" % v)
256 251
257 252
258 253 references = {
259 254 'execute_reply' : ExecuteReply(),
260 255 'object_info_reply' : OInfoReply(),
261 256 'status' : Status(),
262 257 'complete_reply' : CompleteReply(),
263 258 'kernel_info_reply': KernelInfoReply(),
264 259 'pyin' : PyIn(),
265 260 'pyout' : PyOut(),
266 261 'pyerr' : PyErr(),
267 262 'stream' : Stream(),
268 263 'display_data' : DisplayData(),
269 264 }
270 265 """
271 266 Specifications of `content` part of the reply messages.
272 267 """
273 268
274 269
275 270 def validate_message(msg, msg_type=None, parent=None):
276 271 """validate a message
277 272
278 273 This is a generator, and must be iterated through to actually
279 274 trigger each test.
280 275
281 276 If msg_type and/or parent are given, the msg_type and/or parent msg_id
282 277 are compared with the given values.
283 278 """
284 279 RMessage().check(msg)
285 280 if msg_type:
286 281 yield nt.assert_equal(msg['msg_type'], msg_type)
287 282 if parent:
288 283 yield nt.assert_equal(msg['parent_header']['msg_id'], parent)
289 284 content = msg['content']
290 285 ref = references[msg['msg_type']]
291 286 for tst in ref.check(content):
292 287 yield tst
293 288
294 289
295 290 #-----------------------------------------------------------------------------
296 291 # Tests
297 292 #-----------------------------------------------------------------------------
298 293
299 294 # Shell channel
300 295
301 296 @dec.parametric
302 297 def test_execute():
303 298 flush_channels()
304 299
305 shell = KC.shell_channel
306 msg_id = shell.execute(code='x=1')
307 reply = shell.get_msg(timeout=2)
300 msg_id = KC.execute(code='x=1')
301 reply = KC.get_shell_msg(timeout=2)
308 302 for tst in validate_message(reply, 'execute_reply', msg_id):
309 303 yield tst
310 304
311 305
312 306 @dec.parametric
313 307 def test_execute_silent():
314 308 flush_channels()
315 309 msg_id, reply = execute(code='x=1', silent=True)
316 310
317 311 # flush status=idle
318 312 status = KC.iopub_channel.get_msg(timeout=2)
319 313 for tst in validate_message(status, 'status', msg_id):
320 314 yield tst
321 315 nt.assert_equal(status['content']['execution_state'], 'idle')
322 316
323 317 yield nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
324 318 count = reply['execution_count']
325 319
326 320 msg_id, reply = execute(code='x=2', silent=True)
327 321
328 322 # flush status=idle
329 323 status = KC.iopub_channel.get_msg(timeout=2)
330 324 for tst in validate_message(status, 'status', msg_id):
331 325 yield tst
332 326 yield nt.assert_equal(status['content']['execution_state'], 'idle')
333 327
334 328 yield nt.assert_raises(Empty, KC.iopub_channel.get_msg, timeout=0.1)
335 329 count_2 = reply['execution_count']
336 330 yield nt.assert_equal(count_2, count)
337 331
338 332
339 333 @dec.parametric
340 334 def test_execute_error():
341 335 flush_channels()
342 336
343 337 msg_id, reply = execute(code='1/0')
344 338 yield nt.assert_equal(reply['status'], 'error')
345 339 yield nt.assert_equal(reply['ename'], 'ZeroDivisionError')
346 340
347 341 pyerr = KC.iopub_channel.get_msg(timeout=2)
348 342 for tst in validate_message(pyerr, 'pyerr', msg_id):
349 343 yield tst
350 344
351 345
352 346 def test_execute_inc():
353 347 """execute request should increment execution_count"""
354 348 flush_channels()
355 349
356 350 msg_id, reply = execute(code='x=1')
357 351 count = reply['execution_count']
358 352
359 353 flush_channels()
360 354
361 355 msg_id, reply = execute(code='x=2')
362 356 count_2 = reply['execution_count']
363 357 nt.assert_equal(count_2, count+1)
364 358
365 359
366 360 def test_user_variables():
367 361 flush_channels()
368 362
369 363 msg_id, reply = execute(code='x=1', user_variables=['x'])
370 364 user_variables = reply['user_variables']
371 365 nt.assert_equal(user_variables, {u'x' : u'1'})
372 366
373 367
374 368 def test_user_expressions():
375 369 flush_channels()
376 370
377 371 msg_id, reply = execute(code='x=1', user_expressions=dict(foo='x+1'))
378 372 user_expressions = reply['user_expressions']
379 373 nt.assert_equal(user_expressions, {u'foo' : u'2'})
380 374
381 375
382 376 @dec.parametric
383 377 def test_oinfo():
384 378 flush_channels()
385 379
386 shell = KC.shell_channel
387
388 msg_id = shell.object_info('a')
389 reply = shell.get_msg(timeout=2)
380 msg_id = KC.object_info('a')
381 reply = KC.get_shell_msg(timeout=2)
390 382 for tst in validate_message(reply, 'object_info_reply', msg_id):
391 383 yield tst
392 384
393 385
394 386 @dec.parametric
395 387 def test_oinfo_found():
396 388 flush_channels()
397 389
398 shell = KC.shell_channel
399
400 390 msg_id, reply = execute(code='a=5')
401 391
402 msg_id = shell.object_info('a')
403 reply = shell.get_msg(timeout=2)
392 msg_id = KC.object_info('a')
393 reply = KC.get_shell_msg(timeout=2)
404 394 for tst in validate_message(reply, 'object_info_reply', msg_id):
405 395 yield tst
406 396 content = reply['content']
407 397 yield nt.assert_true(content['found'])
408 398 argspec = content['argspec']
409 399 yield nt.assert_true(argspec is None, "didn't expect argspec dict, got %r" % argspec)
410 400
411 401
412 402 @dec.parametric
413 403 def test_oinfo_detail():
414 404 flush_channels()
415 405
416 shell = KC.shell_channel
417
418 406 msg_id, reply = execute(code='ip=get_ipython()')
419 407
420 msg_id = shell.object_info('ip.object_inspect', detail_level=2)
421 reply = shell.get_msg(timeout=2)
408 msg_id = KC.object_info('ip.object_inspect', detail_level=2)
409 reply = KC.get_shell_msg(timeout=2)
422 410 for tst in validate_message(reply, 'object_info_reply', msg_id):
423 411 yield tst
424 412 content = reply['content']
425 413 yield nt.assert_true(content['found'])
426 414 argspec = content['argspec']
427 415 yield nt.assert_true(isinstance(argspec, dict), "expected non-empty argspec dict, got %r" % argspec)
428 416 yield nt.assert_equal(argspec['defaults'], [0])
429 417
430 418
431 419 @dec.parametric
432 420 def test_oinfo_not_found():
433 421 flush_channels()
434 422
435 shell = KC.shell_channel
436
437 msg_id = shell.object_info('dne')
438 reply = shell.get_msg(timeout=2)
423 msg_id = KC.object_info('dne')
424 reply = KC.get_shell_msg(timeout=2)
439 425 for tst in validate_message(reply, 'object_info_reply', msg_id):
440 426 yield tst
441 427 content = reply['content']
442 428 yield nt.assert_false(content['found'])
443 429
444 430
445 431 @dec.parametric
446 432 def test_complete():
447 433 flush_channels()
448 434
449 shell = KC.shell_channel
450
451 435 msg_id, reply = execute(code="alpha = albert = 5")
452 436
453 msg_id = shell.complete('al', 'al', 2)
454 reply = shell.get_msg(timeout=2)
437 msg_id = KC.complete('al', 'al', 2)
438 reply = KC.get_shell_msg(timeout=2)
455 439 for tst in validate_message(reply, 'complete_reply', msg_id):
456 440 yield tst
457 441 matches = reply['content']['matches']
458 442 for name in ('alpha', 'albert'):
459 443 yield nt.assert_true(name in matches, "Missing match: %r" % name)
460 444
461 445
462 446 @dec.parametric
463 447 def test_kernel_info_request():
464 448 flush_channels()
465 449
466 shell = KC.shell_channel
467
468 msg_id = shell.kernel_info()
469 reply = shell.get_msg(timeout=2)
450 msg_id = KC.kernel_info()
451 reply = KC.get_shell_msg(timeout=2)
470 452 for tst in validate_message(reply, 'kernel_info_reply', msg_id):
471 453 yield tst
472 454
473 455
474 456 # IOPub channel
475 457
476 458
477 459 @dec.parametric
478 460 def test_stream():
479 461 flush_channels()
480 462
481 463 msg_id, reply = execute("print('hi')")
482 464
483 465 stdout = KC.iopub_channel.get_msg(timeout=2)
484 466 for tst in validate_message(stdout, 'stream', msg_id):
485 467 yield tst
486 468 content = stdout['content']
487 469 yield nt.assert_equal(content['name'], u'stdout')
488 470 yield nt.assert_equal(content['data'], u'hi\n')
489 471
490 472
491 473 @dec.parametric
492 474 def test_display_data():
493 475 flush_channels()
494 476
495 477 msg_id, reply = execute("from IPython.core.display import display; display(1)")
496 478
497 479 display = KC.iopub_channel.get_msg(timeout=2)
498 480 for tst in validate_message(display, 'display_data', parent=msg_id):
499 481 yield tst
500 482 data = display['content']['data']
501 483 yield nt.assert_equal(data['text/plain'], u'1')
502 484
@@ -1,193 +1,188
1 1 """test IPython.embed_kernel()"""
2 2
3 3 #-------------------------------------------------------------------------------
4 4 # Copyright (C) 2012 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING, distributed as part of this software.
8 8 #-------------------------------------------------------------------------------
9 9
10 10 #-------------------------------------------------------------------------------
11 11 # Imports
12 12 #-------------------------------------------------------------------------------
13 13
14 14 import os
15 15 import shutil
16 16 import sys
17 17 import tempfile
18 18 import time
19 19
20 20 from contextlib import contextmanager
21 21 from subprocess import Popen, PIPE
22 22
23 23 import nose.tools as nt
24 24
25 25 from IPython.kernel import BlockingKernelClient
26 26 from IPython.utils import path, py3compat
27 27
28 28 #-------------------------------------------------------------------------------
29 29 # Tests
30 30 #-------------------------------------------------------------------------------
31 31
32 32 def setup():
33 33 """setup temporary IPYTHONDIR for tests"""
34 34 global IPYTHONDIR
35 35 global env
36 36 global save_get_ipython_dir
37 37
38 38 IPYTHONDIR = tempfile.mkdtemp()
39 39
40 40 env = os.environ.copy()
41 41 env["IPYTHONDIR"] = IPYTHONDIR
42 42
43 43 save_get_ipython_dir = path.get_ipython_dir
44 44 path.get_ipython_dir = lambda : IPYTHONDIR
45 45
46 46
47 47 def teardown():
48 48 path.get_ipython_dir = save_get_ipython_dir
49 49
50 50 try:
51 51 shutil.rmtree(IPYTHONDIR)
52 52 except (OSError, IOError):
53 53 # no such file
54 54 pass
55 55
56 56
57 57 @contextmanager
58 58 def setup_kernel(cmd):
59 59 """start an embedded kernel in a subprocess, and wait for it to be ready
60 60
61 61 Returns
62 62 -------
63 63 kernel_manager: connected KernelManager instance
64 64 """
65 65 kernel = Popen([sys.executable, '-c', cmd], stdout=PIPE, stderr=PIPE, env=env)
66 66 connection_file = os.path.join(IPYTHONDIR,
67 67 'profile_default',
68 68 'security',
69 69 'kernel-%i.json' % kernel.pid
70 70 )
71 71 # wait for connection file to exist, timeout after 5s
72 72 tic = time.time()
73 73 while not os.path.exists(connection_file) and kernel.poll() is None and time.time() < tic + 10:
74 74 time.sleep(0.1)
75 75
76 76 if kernel.poll() is not None:
77 77 o,e = kernel.communicate()
78 78 e = py3compat.cast_unicode(e)
79 79 raise IOError("Kernel failed to start:\n%s" % e)
80 80
81 81 if not os.path.exists(connection_file):
82 82 if kernel.poll() is None:
83 83 kernel.terminate()
84 84 raise IOError("Connection file %r never arrived" % connection_file)
85 85
86 86 client = BlockingKernelClient(connection_file=connection_file)
87 87 client.load_connection_file()
88 88 client.start_channels()
89 89
90 90 try:
91 91 yield client
92 92 finally:
93 93 client.stop_channels()
94 94 kernel.terminate()
95 95
96 96 def test_embed_kernel_basic():
97 97 """IPython.embed_kernel() is basically functional"""
98 98 cmd = '\n'.join([
99 99 'from IPython import embed_kernel',
100 100 'def go():',
101 101 ' a=5',
102 102 ' b="hi there"',
103 103 ' embed_kernel()',
104 104 'go()',
105 105 '',
106 106 ])
107 107
108 108 with setup_kernel(cmd) as client:
109 shell = client.shell_channel
110
111 109 # oinfo a (int)
112 msg_id = shell.object_info('a')
113 msg = shell.get_msg(block=True, timeout=2)
110 msg_id = client.object_info('a')
111 msg = client.get_shell_msg(block=True, timeout=2)
114 112 content = msg['content']
115 113 nt.assert_true(content['found'])
116 114
117 msg_id = shell.execute("c=a*2")
118 msg = shell.get_msg(block=True, timeout=2)
115 msg_id = client.execute("c=a*2")
116 msg = client.get_shell_msg(block=True, timeout=2)
119 117 content = msg['content']
120 118 nt.assert_equal(content['status'], u'ok')
121 119
122 120 # oinfo c (should be 10)
123 msg_id = shell.object_info('c')
124 msg = shell.get_msg(block=True, timeout=2)
121 msg_id = client.object_info('c')
122 msg = client.get_shell_msg(block=True, timeout=2)
125 123 content = msg['content']
126 124 nt.assert_true(content['found'])
127 125 nt.assert_equal(content['string_form'], u'10')
128 126
129 127 def test_embed_kernel_namespace():
130 128 """IPython.embed_kernel() inherits calling namespace"""
131 129 cmd = '\n'.join([
132 130 'from IPython import embed_kernel',
133 131 'def go():',
134 132 ' a=5',
135 133 ' b="hi there"',
136 134 ' embed_kernel()',
137 135 'go()',
138 136 '',
139 137 ])
140 138
141 139 with setup_kernel(cmd) as client:
142 shell = client.shell_channel
143
144 140 # oinfo a (int)
145 msg_id = shell.object_info('a')
146 msg = shell.get_msg(block=True, timeout=2)
141 msg_id = client.object_info('a')
142 msg = client.get_shell_msg(block=True, timeout=2)
147 143 content = msg['content']
148 144 nt.assert_true(content['found'])
149 145 nt.assert_equal(content['string_form'], u'5')
150 146
151 147 # oinfo b (str)
152 msg_id = shell.object_info('b')
153 msg = shell.get_msg(block=True, timeout=2)
148 msg_id = client.object_info('b')
149 msg = client.get_shell_msg(block=True, timeout=2)
154 150 content = msg['content']
155 151 nt.assert_true(content['found'])
156 152 nt.assert_equal(content['string_form'], u'hi there')
157 153
158 154 # oinfo c (undefined)
159 msg_id = shell.object_info('c')
160 msg = shell.get_msg(block=True, timeout=2)
155 msg_id = client.object_info('c')
156 msg = client.get_shell_msg(block=True, timeout=2)
161 157 content = msg['content']
162 158 nt.assert_false(content['found'])
163 159
164 160 def test_embed_kernel_reentrant():
165 161 """IPython.embed_kernel() can be called multiple times"""
166 162 cmd = '\n'.join([
167 163 'from IPython import embed_kernel',
168 164 'count = 0',
169 165 'def go():',
170 166 ' global count',
171 167 ' embed_kernel()',
172 168 ' count = count + 1',
173 169 '',
174 170 'while True:'
175 171 ' go()',
176 172 '',
177 173 ])
178 174
179 175 with setup_kernel(cmd) as client:
180 shell = client.shell_channel
181 176 for i in range(5):
182 msg_id = shell.object_info('count')
183 msg = shell.get_msg(block=True, timeout=2)
177 msg_id = client.object_info('count')
178 msg = client.get_shell_msg(block=True, timeout=2)
184 179 content = msg['content']
185 180 nt.assert_true(content['found'])
186 181 nt.assert_equal(content['string_form'], unicode(i))
187 182
188 183 # exit from embed_kernel
189 shell.execute("get_ipython().exit_now = True")
190 msg = shell.get_msg(block=True, timeout=2)
184 client.execute("get_ipython().exit_now = True")
185 msg = client.get_shell_msg(block=True, timeout=2)
191 186 time.sleep(0.2)
192 187
193 188
General Comments 0
You need to be logged in to leave comments. Login now