##// END OF EJS Templates
Use messaging methods on kernel client instead of channel objects
Thomas Kluyver -
Show More
@@ -1,82 +1,82 b''
1 1 # Copyright (c) IPython Development Team.
2 2 # Distributed under the terms of the Modified BSD License.
3 3
4 4 from __future__ import print_function
5 5
6 6 import sys
7 7 import unittest
8 8
9 9 from IPython.kernel.inprocess.blocking import BlockingInProcessKernelClient
10 10 from IPython.kernel.inprocess.manager import InProcessKernelManager
11 11 from IPython.kernel.inprocess.ipkernel import InProcessKernel
12 12 from IPython.testing.decorators import skipif_not_matplotlib
13 13 from IPython.utils.io import capture_output
14 14 from IPython.utils import py3compat
15 15
16 16 if py3compat.PY3:
17 17 from io import StringIO
18 18 else:
19 19 from StringIO import StringIO
20 20
21 21
22 22 class InProcessKernelTestCase(unittest.TestCase):
23 23
24 24 def setUp(self):
25 25 self.km = InProcessKernelManager()
26 26 self.km.start_kernel()
27 27 self.kc = BlockingInProcessKernelClient(kernel=self.km.kernel)
28 28 self.kc.start_channels()
29 29
30 30 @skipif_not_matplotlib
31 31 def test_pylab(self):
32 32 """Does %pylab work in the in-process kernel?"""
33 33 kc = self.kc
34 34 kc.execute('%pylab')
35 35 msg = get_stream_message(kc)
36 36 self.assertIn('matplotlib', msg['content']['text'])
37 37
38 38 def test_raw_input(self):
39 39 """ Does the in-process kernel handle raw_input correctly?
40 40 """
41 41 io = StringIO('foobar\n')
42 42 sys_stdin = sys.stdin
43 43 sys.stdin = io
44 44 try:
45 45 if py3compat.PY3:
46 46 self.kc.execute('x = input()')
47 47 else:
48 48 self.kc.execute('x = raw_input()')
49 49 finally:
50 50 sys.stdin = sys_stdin
51 51 self.assertEqual(self.km.kernel.shell.user_ns.get('x'), 'foobar')
52 52
53 53 def test_stdout(self):
54 54 """ Does the in-process kernel correctly capture IO?
55 55 """
56 56 kernel = InProcessKernel()
57 57
58 58 with capture_output() as io:
59 59 kernel.shell.run_cell('print("foo")')
60 60 self.assertEqual(io.stdout, 'foo\n')
61 61
62 62 kc = BlockingInProcessKernelClient(kernel=kernel)
63 63 kernel.frontends.append(kc)
64 kc.shell_channel.execute('print("bar")')
64 kc.execute('print("bar")')
65 65 msg = get_stream_message(kc)
66 66 self.assertEqual(msg['content']['text'], 'bar\n')
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Utility functions
70 70 #-----------------------------------------------------------------------------
71 71
72 72 def get_stream_message(kernel_client, timeout=5):
73 73 """ Gets a single stream message synchronously from the sub channel.
74 74 """
75 75 while True:
76 76 msg = kernel_client.get_iopub_msg(timeout=timeout)
77 77 if msg['header']['msg_type'] == 'stream':
78 78 return msg
79 79
80 80
81 81 if __name__ == '__main__':
82 82 unittest.main()
@@ -1,808 +1,808 b''
1 1 """Frontend widget for the Qt Console"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 from collections import namedtuple
9 9 import sys
10 10 import uuid
11 11
12 12 from IPython.external import qt
13 13 from IPython.external.qt import QtCore, QtGui
14 14 from IPython.utils import py3compat
15 15 from IPython.utils.importstring import import_item
16 16
17 17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
18 18 from IPython.core.inputtransformer import classic_prompt
19 19 from IPython.core.oinspect import call_tip
20 20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
21 21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
22 22 from .bracket_matcher import BracketMatcher
23 23 from .call_tip_widget import CallTipWidget
24 24 from .history_console_widget import HistoryConsoleWidget
25 25 from .pygments_highlighter import PygmentsHighlighter
26 26
27 27
28 28 class FrontendHighlighter(PygmentsHighlighter):
29 29 """ A PygmentsHighlighter that understands and ignores prompts.
30 30 """
31 31
32 32 def __init__(self, frontend, lexer=None):
33 33 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
34 34 self._current_offset = 0
35 35 self._frontend = frontend
36 36 self.highlighting_on = False
37 37
38 38 def highlightBlock(self, string):
39 39 """ Highlight a block of text. Reimplemented to highlight selectively.
40 40 """
41 41 if not self.highlighting_on:
42 42 return
43 43
44 44 # The input to this function is a unicode string that may contain
45 45 # paragraph break characters, non-breaking spaces, etc. Here we acquire
46 46 # the string as plain text so we can compare it.
47 47 current_block = self.currentBlock()
48 48 string = self._frontend._get_block_plain_text(current_block)
49 49
50 50 # Decide whether to check for the regular or continuation prompt.
51 51 if current_block.contains(self._frontend._prompt_pos):
52 52 prompt = self._frontend._prompt
53 53 else:
54 54 prompt = self._frontend._continuation_prompt
55 55
56 56 # Only highlight if we can identify a prompt, but make sure not to
57 57 # highlight the prompt.
58 58 if string.startswith(prompt):
59 59 self._current_offset = len(prompt)
60 60 string = string[len(prompt):]
61 61 super(FrontendHighlighter, self).highlightBlock(string)
62 62
63 63 def rehighlightBlock(self, block):
64 64 """ Reimplemented to temporarily enable highlighting if disabled.
65 65 """
66 66 old = self.highlighting_on
67 67 self.highlighting_on = True
68 68 super(FrontendHighlighter, self).rehighlightBlock(block)
69 69 self.highlighting_on = old
70 70
71 71 def setFormat(self, start, count, format):
72 72 """ Reimplemented to highlight selectively.
73 73 """
74 74 start += self._current_offset
75 75 super(FrontendHighlighter, self).setFormat(start, count, format)
76 76
77 77
78 78 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
79 79 """ A Qt frontend for a generic Python kernel.
80 80 """
81 81
82 82 # The text to show when the kernel is (re)started.
83 83 banner = Unicode(config=True)
84 84 kernel_banner = Unicode()
85 85
86 86 # An option and corresponding signal for overriding the default kernel
87 87 # interrupt behavior.
88 88 custom_interrupt = Bool(False)
89 89 custom_interrupt_requested = QtCore.Signal()
90 90
91 91 # An option and corresponding signals for overriding the default kernel
92 92 # restart behavior.
93 93 custom_restart = Bool(False)
94 94 custom_restart_kernel_died = QtCore.Signal(float)
95 95 custom_restart_requested = QtCore.Signal()
96 96
97 97 # Whether to automatically show calltips on open-parentheses.
98 98 enable_calltips = Bool(True, config=True,
99 99 help="Whether to draw information calltips on open-parentheses.")
100 100
101 101 clear_on_kernel_restart = Bool(True, config=True,
102 102 help="Whether to clear the console when the kernel is restarted")
103 103
104 104 confirm_restart = Bool(True, config=True,
105 105 help="Whether to ask for user confirmation when restarting kernel")
106 106
107 107 lexer_class = DottedObjectName(config=True,
108 108 help="The pygments lexer class to use."
109 109 )
110 110 def _lexer_class_changed(self, name, old, new):
111 111 lexer_class = import_item(new)
112 112 self.lexer = lexer_class()
113 113
114 114 def _lexer_class_default(self):
115 115 if py3compat.PY3:
116 116 return 'pygments.lexers.Python3Lexer'
117 117 else:
118 118 return 'pygments.lexers.PythonLexer'
119 119
120 120 lexer = Any()
121 121 def _lexer_default(self):
122 122 lexer_class = import_item(self.lexer_class)
123 123 return lexer_class()
124 124
125 125 # Emitted when a user visible 'execute_request' has been submitted to the
126 126 # kernel from the FrontendWidget. Contains the code to be executed.
127 127 executing = QtCore.Signal(object)
128 128
129 129 # Emitted when a user-visible 'execute_reply' has been received from the
130 130 # kernel and processed by the FrontendWidget. Contains the response message.
131 131 executed = QtCore.Signal(object)
132 132
133 133 # Emitted when an exit request has been received from the kernel.
134 134 exit_requested = QtCore.Signal(object)
135 135
136 136 # Protected class variables.
137 137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 138 logical_line_transforms=[],
139 139 python_line_transforms=[],
140 140 )
141 141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 144 _input_splitter_class = InputSplitter
145 145 _local_kernel = False
146 146 _highlighter = Instance(FrontendHighlighter)
147 147
148 148 #---------------------------------------------------------------------------
149 149 # 'object' interface
150 150 #---------------------------------------------------------------------------
151 151
152 152 def __init__(self, *args, **kw):
153 153 super(FrontendWidget, self).__init__(*args, **kw)
154 154 # FIXME: remove this when PySide min version is updated past 1.0.7
155 155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 156 if qt.QT_API == qt.QT_API_PYSIDE:
157 157 import PySide
158 158 if PySide.__version_info__ < (1,0,7):
159 159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 160 self.enable_calltips = False
161 161
162 162 # FrontendWidget protected variables.
163 163 self._bracket_matcher = BracketMatcher(self._control)
164 164 self._call_tip_widget = CallTipWidget(self._control)
165 165 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
166 166 self._hidden = False
167 167 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
168 168 self._input_splitter = self._input_splitter_class()
169 169 self._kernel_manager = None
170 170 self._kernel_client = None
171 171 self._request_info = {}
172 172 self._request_info['execute'] = {};
173 173 self._callback_dict = {}
174 174
175 175 # Configure the ConsoleWidget.
176 176 self.tab_width = 4
177 177 self._set_continuation_prompt('... ')
178 178
179 179 # Configure the CallTipWidget.
180 180 self._call_tip_widget.setFont(self.font)
181 181 self.font_changed.connect(self._call_tip_widget.setFont)
182 182
183 183 # Configure actions.
184 184 action = self._copy_raw_action
185 185 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
186 186 action.setEnabled(False)
187 187 action.setShortcut(QtGui.QKeySequence(key))
188 188 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
189 189 action.triggered.connect(self.copy_raw)
190 190 self.copy_available.connect(action.setEnabled)
191 191 self.addAction(action)
192 192
193 193 # Connect signal handlers.
194 194 document = self._control.document()
195 195 document.contentsChange.connect(self._document_contents_change)
196 196
197 197 # Set flag for whether we are connected via localhost.
198 198 self._local_kernel = kw.get('local_kernel',
199 199 FrontendWidget._local_kernel)
200 200
201 201 # Whether or not a clear_output call is pending new output.
202 202 self._pending_clearoutput = False
203 203
204 204 #---------------------------------------------------------------------------
205 205 # 'ConsoleWidget' public interface
206 206 #---------------------------------------------------------------------------
207 207
208 208 def copy(self):
209 209 """ Copy the currently selected text to the clipboard, removing prompts.
210 210 """
211 211 if self._page_control is not None and self._page_control.hasFocus():
212 212 self._page_control.copy()
213 213 elif self._control.hasFocus():
214 214 text = self._control.textCursor().selection().toPlainText()
215 215 if text:
216 216 was_newline = text[-1] == '\n'
217 217 text = self._prompt_transformer.transform_cell(text)
218 218 if not was_newline: # user doesn't need newline
219 219 text = text[:-1]
220 220 QtGui.QApplication.clipboard().setText(text)
221 221 else:
222 222 self.log.debug("frontend widget : unknown copy target")
223 223
224 224 #---------------------------------------------------------------------------
225 225 # 'ConsoleWidget' abstract interface
226 226 #---------------------------------------------------------------------------
227 227
228 228 def _is_complete(self, source, interactive):
229 229 """ Returns whether 'source' can be completely processed and a new
230 230 prompt created. When triggered by an Enter/Return key press,
231 231 'interactive' is True; otherwise, it is False.
232 232 """
233 233 self._input_splitter.reset()
234 234 try:
235 235 complete = self._input_splitter.push(source)
236 236 except SyntaxError:
237 237 return True
238 238 if interactive:
239 239 complete = not self._input_splitter.push_accepts_more()
240 240 return complete
241 241
242 242 def _execute(self, source, hidden):
243 243 """ Execute 'source'. If 'hidden', do not show any output.
244 244
245 245 See parent class :meth:`execute` docstring for full details.
246 246 """
247 247 msg_id = self.kernel_client.execute(source, hidden)
248 248 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
249 249 self._hidden = hidden
250 250 if not hidden:
251 251 self.executing.emit(source)
252 252
253 253 def _prompt_started_hook(self):
254 254 """ Called immediately after a new prompt is displayed.
255 255 """
256 256 if not self._reading:
257 257 self._highlighter.highlighting_on = True
258 258
259 259 def _prompt_finished_hook(self):
260 260 """ Called immediately after a prompt is finished, i.e. when some input
261 261 will be processed and a new prompt displayed.
262 262 """
263 263 # Flush all state from the input splitter so the next round of
264 264 # reading input starts with a clean buffer.
265 265 self._input_splitter.reset()
266 266
267 267 if not self._reading:
268 268 self._highlighter.highlighting_on = False
269 269
270 270 def _tab_pressed(self):
271 271 """ Called when the tab key is pressed. Returns whether to continue
272 272 processing the event.
273 273 """
274 274 # Perform tab completion if:
275 275 # 1) The cursor is in the input buffer.
276 276 # 2) There is a non-whitespace character before the cursor.
277 277 text = self._get_input_buffer_cursor_line()
278 278 if text is None:
279 279 return False
280 280 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
281 281 if complete:
282 282 self._complete()
283 283 return not complete
284 284
285 285 #---------------------------------------------------------------------------
286 286 # 'ConsoleWidget' protected interface
287 287 #---------------------------------------------------------------------------
288 288
289 289 def _context_menu_make(self, pos):
290 290 """ Reimplemented to add an action for raw copy.
291 291 """
292 292 menu = super(FrontendWidget, self)._context_menu_make(pos)
293 293 for before_action in menu.actions():
294 294 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
295 295 QtGui.QKeySequence.ExactMatch:
296 296 menu.insertAction(before_action, self._copy_raw_action)
297 297 break
298 298 return menu
299 299
300 300 def request_interrupt_kernel(self):
301 301 if self._executing:
302 302 self.interrupt_kernel()
303 303
304 304 def request_restart_kernel(self):
305 305 message = 'Are you sure you want to restart the kernel?'
306 306 self.restart_kernel(message, now=False)
307 307
308 308 def _event_filter_console_keypress(self, event):
309 309 """ Reimplemented for execution interruption and smart backspace.
310 310 """
311 311 key = event.key()
312 312 if self._control_key_down(event.modifiers(), include_command=False):
313 313
314 314 if key == QtCore.Qt.Key_C and self._executing:
315 315 self.request_interrupt_kernel()
316 316 return True
317 317
318 318 elif key == QtCore.Qt.Key_Period:
319 319 self.request_restart_kernel()
320 320 return True
321 321
322 322 elif not event.modifiers() & QtCore.Qt.AltModifier:
323 323
324 324 # Smart backspace: remove four characters in one backspace if:
325 325 # 1) everything left of the cursor is whitespace
326 326 # 2) the four characters immediately left of the cursor are spaces
327 327 if key == QtCore.Qt.Key_Backspace:
328 328 col = self._get_input_buffer_cursor_column()
329 329 cursor = self._control.textCursor()
330 330 if col > 3 and not cursor.hasSelection():
331 331 text = self._get_input_buffer_cursor_line()[:col]
332 332 if text.endswith(' ') and not text.strip():
333 333 cursor.movePosition(QtGui.QTextCursor.Left,
334 334 QtGui.QTextCursor.KeepAnchor, 4)
335 335 cursor.removeSelectedText()
336 336 return True
337 337
338 338 return super(FrontendWidget, self)._event_filter_console_keypress(event)
339 339
340 340 def _insert_continuation_prompt(self, cursor):
341 341 """ Reimplemented for auto-indentation.
342 342 """
343 343 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
344 344 cursor.insertText(' ' * self._input_splitter.indent_spaces)
345 345
346 346 #---------------------------------------------------------------------------
347 347 # 'BaseFrontendMixin' abstract interface
348 348 #---------------------------------------------------------------------------
349 349 def _handle_clear_output(self, msg):
350 350 """Handle clear output messages."""
351 351 if include_output(msg):
352 352 wait = msg['content'].get('wait', True)
353 353 if wait:
354 354 self._pending_clearoutput = True
355 355 else:
356 356 self.clear_output()
357 357
358 358 def _silent_exec_callback(self, expr, callback):
359 359 """Silently execute `expr` in the kernel and call `callback` with reply
360 360
361 361 the `expr` is evaluated silently in the kernel (without) output in
362 362 the frontend. Call `callback` with the
363 363 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
364 364
365 365 Parameters
366 366 ----------
367 367 expr : string
368 368 valid string to be executed by the kernel.
369 369 callback : function
370 370 function accepting one argument, as a string. The string will be
371 371 the `repr` of the result of evaluating `expr`
372 372
373 373 The `callback` is called with the `repr()` of the result of `expr` as
374 374 first argument. To get the object, do `eval()` on the passed value.
375 375
376 376 See Also
377 377 --------
378 378 _handle_exec_callback : private method, deal with calling callback with reply
379 379
380 380 """
381 381
382 382 # generate uuid, which would be used as an indication of whether or
383 383 # not the unique request originated from here (can use msg id ?)
384 384 local_uuid = str(uuid.uuid1())
385 385 msg_id = self.kernel_client.execute('',
386 386 silent=True, user_expressions={ local_uuid:expr })
387 387 self._callback_dict[local_uuid] = callback
388 388 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
389 389
390 390 def _handle_exec_callback(self, msg):
391 391 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
392 392
393 393 Parameters
394 394 ----------
395 395 msg : raw message send by the kernel containing an `user_expressions`
396 396 and having a 'silent_exec_callback' kind.
397 397
398 398 Notes
399 399 -----
400 400 This function will look for a `callback` associated with the
401 401 corresponding message id. Association has been made by
402 402 `_silent_exec_callback`. `callback` is then called with the `repr()`
403 403 of the value of corresponding `user_expressions` as argument.
404 404 `callback` is then removed from the known list so that any message
405 405 coming again with the same id won't trigger it.
406 406
407 407 """
408 408
409 409 user_exp = msg['content'].get('user_expressions')
410 410 if not user_exp:
411 411 return
412 412 for expression in user_exp:
413 413 if expression in self._callback_dict:
414 414 self._callback_dict.pop(expression)(user_exp[expression])
415 415
416 416 def _handle_execute_reply(self, msg):
417 417 """ Handles replies for code execution.
418 418 """
419 419 self.log.debug("execute: %s", msg.get('content', ''))
420 420 msg_id = msg['parent_header']['msg_id']
421 421 info = self._request_info['execute'].get(msg_id)
422 422 # unset reading flag, because if execute finished, raw_input can't
423 423 # still be pending.
424 424 self._reading = False
425 425 if info and info.kind == 'user' and not self._hidden:
426 426 # Make sure that all output from the SUB channel has been processed
427 427 # before writing a new prompt.
428 428 self.kernel_client.iopub_channel.flush()
429 429
430 430 # Reset the ANSI style information to prevent bad text in stdout
431 431 # from messing up our colors. We're not a true terminal so we're
432 432 # allowed to do this.
433 433 if self.ansi_codes:
434 434 self._ansi_processor.reset_sgr()
435 435
436 436 content = msg['content']
437 437 status = content['status']
438 438 if status == 'ok':
439 439 self._process_execute_ok(msg)
440 440 elif status == 'error':
441 441 self._process_execute_error(msg)
442 442 elif status == 'aborted':
443 443 self._process_execute_abort(msg)
444 444
445 445 self._show_interpreter_prompt_for_reply(msg)
446 446 self.executed.emit(msg)
447 447 self._request_info['execute'].pop(msg_id)
448 448 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
449 449 self._handle_exec_callback(msg)
450 450 self._request_info['execute'].pop(msg_id)
451 451 else:
452 452 super(FrontendWidget, self)._handle_execute_reply(msg)
453 453
454 454 def _handle_input_request(self, msg):
455 455 """ Handle requests for raw_input.
456 456 """
457 457 self.log.debug("input: %s", msg.get('content', ''))
458 458 if self._hidden:
459 459 raise RuntimeError('Request for raw input during hidden execution.')
460 460
461 461 # Make sure that all output from the SUB channel has been processed
462 462 # before entering readline mode.
463 463 self.kernel_client.iopub_channel.flush()
464 464
465 465 def callback(line):
466 self.kernel_client.stdin_channel.input(line)
466 self.kernel_client.input(line)
467 467 if self._reading:
468 468 self.log.debug("Got second input request, assuming first was interrupted.")
469 469 self._reading = False
470 470 self._readline(msg['content']['prompt'], callback=callback)
471 471
472 472 def _kernel_restarted_message(self, died=True):
473 473 msg = "Kernel died, restarting" if died else "Kernel restarting"
474 474 self._append_html("<br>%s<hr><br>" % msg,
475 475 before_prompt=False
476 476 )
477 477
478 478 def _handle_kernel_died(self, since_last_heartbeat):
479 479 """Handle the kernel's death (if we do not own the kernel).
480 480 """
481 481 self.log.warn("kernel died: %s", since_last_heartbeat)
482 482 if self.custom_restart:
483 483 self.custom_restart_kernel_died.emit(since_last_heartbeat)
484 484 else:
485 485 self._kernel_restarted_message(died=True)
486 486 self.reset()
487 487
488 488 def _handle_kernel_restarted(self, died=True):
489 489 """Notice that the autorestarter restarted the kernel.
490 490
491 491 There's nothing to do but show a message.
492 492 """
493 493 self.log.warn("kernel restarted")
494 494 self._kernel_restarted_message(died=died)
495 495 self.reset()
496 496
497 497 def _handle_inspect_reply(self, rep):
498 498 """Handle replies for call tips."""
499 499 self.log.debug("oinfo: %s", rep.get('content', ''))
500 500 cursor = self._get_cursor()
501 501 info = self._request_info.get('call_tip')
502 502 if info and info.id == rep['parent_header']['msg_id'] and \
503 503 info.pos == cursor.position():
504 504 content = rep['content']
505 505 if content.get('status') == 'ok' and content.get('found', False):
506 506 self._call_tip_widget.show_inspect_data(content)
507 507
508 508 def _handle_execute_result(self, msg):
509 509 """ Handle display hook output.
510 510 """
511 511 self.log.debug("execute_result: %s", msg.get('content', ''))
512 512 if self.include_output(msg):
513 513 self.flush_clearoutput()
514 514 text = msg['content']['data']
515 515 self._append_plain_text(text + '\n', before_prompt=True)
516 516
517 517 def _handle_stream(self, msg):
518 518 """ Handle stdout, stderr, and stdin.
519 519 """
520 520 self.log.debug("stream: %s", msg.get('content', ''))
521 521 if self.include_output(msg):
522 522 self.flush_clearoutput()
523 523 self.append_stream(msg['content']['text'])
524 524
525 525 def _handle_shutdown_reply(self, msg):
526 526 """ Handle shutdown signal, only if from other console.
527 527 """
528 528 self.log.info("shutdown: %s", msg.get('content', ''))
529 529 restart = msg.get('content', {}).get('restart', False)
530 530 if not self._hidden and not self.from_here(msg):
531 531 # got shutdown reply, request came from session other than ours
532 532 if restart:
533 533 # someone restarted the kernel, handle it
534 534 self._handle_kernel_restarted(died=False)
535 535 else:
536 536 # kernel was shutdown permanently
537 537 # this triggers exit_requested if the kernel was local,
538 538 # and a dialog if the kernel was remote,
539 539 # so we don't suddenly clear the qtconsole without asking.
540 540 if self._local_kernel:
541 541 self.exit_requested.emit(self)
542 542 else:
543 543 title = self.window().windowTitle()
544 544 reply = QtGui.QMessageBox.question(self, title,
545 545 "Kernel has been shutdown permanently. "
546 546 "Close the Console?",
547 547 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
548 548 if reply == QtGui.QMessageBox.Yes:
549 549 self.exit_requested.emit(self)
550 550
551 551 def _handle_status(self, msg):
552 552 """Handle status message"""
553 553 # This is where a busy/idle indicator would be triggered,
554 554 # when we make one.
555 555 state = msg['content'].get('execution_state', '')
556 556 if state == 'starting':
557 557 # kernel started while we were running
558 558 if self._executing:
559 559 self._handle_kernel_restarted(died=True)
560 560 elif state == 'idle':
561 561 pass
562 562 elif state == 'busy':
563 563 pass
564 564
565 565 def _started_channels(self):
566 566 """ Called when the KernelManager channels have started listening or
567 567 when the frontend is assigned an already listening KernelManager.
568 568 """
569 569 self.reset(clear=True)
570 570
571 571 #---------------------------------------------------------------------------
572 572 # 'FrontendWidget' public interface
573 573 #---------------------------------------------------------------------------
574 574
575 575 def copy_raw(self):
576 576 """ Copy the currently selected text to the clipboard without attempting
577 577 to remove prompts or otherwise alter the text.
578 578 """
579 579 self._control.copy()
580 580
581 581 def execute_file(self, path, hidden=False):
582 582 """ Attempts to execute file with 'path'. If 'hidden', no output is
583 583 shown.
584 584 """
585 585 self.execute('execfile(%r)' % path, hidden=hidden)
586 586
587 587 def interrupt_kernel(self):
588 588 """ Attempts to interrupt the running kernel.
589 589
590 590 Also unsets _reading flag, to avoid runtime errors
591 591 if raw_input is called again.
592 592 """
593 593 if self.custom_interrupt:
594 594 self._reading = False
595 595 self.custom_interrupt_requested.emit()
596 596 elif self.kernel_manager:
597 597 self._reading = False
598 598 self.kernel_manager.interrupt_kernel()
599 599 else:
600 600 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
601 601
602 602 def reset(self, clear=False):
603 603 """ Resets the widget to its initial state if ``clear`` parameter
604 604 is True, otherwise
605 605 prints a visual indication of the fact that the kernel restarted, but
606 606 does not clear the traces from previous usage of the kernel before it
607 607 was restarted. With ``clear=True``, it is similar to ``%clear``, but
608 608 also re-writes the banner and aborts execution if necessary.
609 609 """
610 610 if self._executing:
611 611 self._executing = False
612 612 self._request_info['execute'] = {}
613 613 self._reading = False
614 614 self._highlighter.highlighting_on = False
615 615
616 616 if clear:
617 617 self._control.clear()
618 618 if self._display_banner:
619 619 self._append_plain_text(self.banner)
620 620 if self.kernel_banner:
621 621 self._append_plain_text(self.kernel_banner)
622 622
623 623 # update output marker for stdout/stderr, so that startup
624 624 # messages appear after banner:
625 625 self._append_before_prompt_pos = self._get_cursor().position()
626 626 self._show_interpreter_prompt()
627 627
628 628 def restart_kernel(self, message, now=False):
629 629 """ Attempts to restart the running kernel.
630 630 """
631 631 # FIXME: now should be configurable via a checkbox in the dialog. Right
632 632 # now at least the heartbeat path sets it to True and the manual restart
633 633 # to False. But those should just be the pre-selected states of a
634 634 # checkbox that the user could override if so desired. But I don't know
635 635 # enough Qt to go implementing the checkbox now.
636 636
637 637 if self.custom_restart:
638 638 self.custom_restart_requested.emit()
639 639 return
640 640
641 641 if self.kernel_manager:
642 642 # Pause the heart beat channel to prevent further warnings.
643 643 self.kernel_client.hb_channel.pause()
644 644
645 645 # Prompt the user to restart the kernel. Un-pause the heartbeat if
646 646 # they decline. (If they accept, the heartbeat will be un-paused
647 647 # automatically when the kernel is restarted.)
648 648 if self.confirm_restart:
649 649 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
650 650 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
651 651 message, buttons)
652 652 do_restart = result == QtGui.QMessageBox.Yes
653 653 else:
654 654 # confirm_restart is False, so we don't need to ask user
655 655 # anything, just do the restart
656 656 do_restart = True
657 657 if do_restart:
658 658 try:
659 659 self.kernel_manager.restart_kernel(now=now)
660 660 except RuntimeError as e:
661 661 self._append_plain_text(
662 662 'Error restarting kernel: %s\n' % e,
663 663 before_prompt=True
664 664 )
665 665 else:
666 666 self._append_html("<br>Restarting kernel...\n<hr><br>",
667 667 before_prompt=True,
668 668 )
669 669 else:
670 670 self.kernel_client.hb_channel.unpause()
671 671
672 672 else:
673 673 self._append_plain_text(
674 674 'Cannot restart a Kernel I did not start\n',
675 675 before_prompt=True
676 676 )
677 677
678 678 def append_stream(self, text):
679 679 """Appends text to the output stream."""
680 680 # Most consoles treat tabs as being 8 space characters. Convert tabs
681 681 # to spaces so that output looks as expected regardless of this
682 682 # widget's tab width.
683 683 text = text.expandtabs(8)
684 684 self._append_plain_text(text, before_prompt=True)
685 685 self._control.moveCursor(QtGui.QTextCursor.End)
686 686
687 687 def flush_clearoutput(self):
688 688 """If a clearoutput is pending, execute it."""
689 689 if self._pending_clearoutput:
690 690 self._pending_clearoutput = False
691 691 self.clear_output()
692 692
693 693 def clear_output(self):
694 694 """Clears the current line of output."""
695 695 cursor = self._control.textCursor()
696 696 cursor.beginEditBlock()
697 697 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
698 698 cursor.insertText('')
699 699 cursor.endEditBlock()
700 700
701 701 #---------------------------------------------------------------------------
702 702 # 'FrontendWidget' protected interface
703 703 #---------------------------------------------------------------------------
704 704
705 705 def _auto_call_tip(self):
706 706 """Trigger call tip automatically on open parenthesis
707 707
708 708 Call tips can be requested explcitly with `_call_tip`.
709 709 """
710 710 cursor = self._get_cursor()
711 711 cursor.movePosition(QtGui.QTextCursor.Left)
712 712 if cursor.document().characterAt(cursor.position()) == '(':
713 713 # trigger auto call tip on open paren
714 714 self._call_tip()
715 715
716 716 def _call_tip(self):
717 717 """Shows a call tip, if appropriate, at the current cursor location."""
718 718 # Decide if it makes sense to show a call tip
719 719 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
720 720 return False
721 721 cursor_pos = self._get_input_buffer_cursor_pos()
722 722 code = self.input_buffer
723 723 # Send the metadata request to the kernel
724 724 msg_id = self.kernel_client.inspect(code, cursor_pos)
725 725 pos = self._get_cursor().position()
726 726 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
727 727 return True
728 728
729 729 def _complete(self):
730 730 """ Performs completion at the current cursor location.
731 731 """
732 732 # Send the completion request to the kernel
733 733 msg_id = self.kernel_client.complete(
734 734 code=self.input_buffer,
735 735 cursor_pos=self._get_input_buffer_cursor_pos(),
736 736 )
737 737 pos = self._get_cursor().position()
738 738 info = self._CompletionRequest(msg_id, pos)
739 739 self._request_info['complete'] = info
740 740
741 741 def _process_execute_abort(self, msg):
742 742 """ Process a reply for an aborted execution request.
743 743 """
744 744 self._append_plain_text("ERROR: execution aborted\n")
745 745
746 746 def _process_execute_error(self, msg):
747 747 """ Process a reply for an execution request that resulted in an error.
748 748 """
749 749 content = msg['content']
750 750 # If a SystemExit is passed along, this means exit() was called - also
751 751 # all the ipython %exit magic syntax of '-k' to be used to keep
752 752 # the kernel running
753 753 if content['ename']=='SystemExit':
754 754 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
755 755 self._keep_kernel_on_exit = keepkernel
756 756 self.exit_requested.emit(self)
757 757 else:
758 758 traceback = ''.join(content['traceback'])
759 759 self._append_plain_text(traceback)
760 760
761 761 def _process_execute_ok(self, msg):
762 762 """ Process a reply for a successful execution request.
763 763 """
764 764 payload = msg['content']['payload']
765 765 for item in payload:
766 766 if not self._process_execute_payload(item):
767 767 warning = 'Warning: received unknown payload of type %s'
768 768 print(warning % repr(item['source']))
769 769
770 770 def _process_execute_payload(self, item):
771 771 """ Process a single payload item from the list of payload items in an
772 772 execution reply. Returns whether the payload was handled.
773 773 """
774 774 # The basic FrontendWidget doesn't handle payloads, as they are a
775 775 # mechanism for going beyond the standard Python interpreter model.
776 776 return False
777 777
778 778 def _show_interpreter_prompt(self):
779 779 """ Shows a prompt for the interpreter.
780 780 """
781 781 self._show_prompt('>>> ')
782 782
783 783 def _show_interpreter_prompt_for_reply(self, msg):
784 784 """ Shows a prompt for the interpreter given an 'execute_reply' message.
785 785 """
786 786 self._show_interpreter_prompt()
787 787
788 788 #------ Signal handlers ----------------------------------------------------
789 789
790 790 def _document_contents_change(self, position, removed, added):
791 791 """ Called whenever the document's content changes. Display a call tip
792 792 if appropriate.
793 793 """
794 794 # Calculate where the cursor should be *after* the change:
795 795 position += added
796 796
797 797 document = self._control.document()
798 798 if position == self._get_cursor().position():
799 799 self._auto_call_tip()
800 800
801 801 #------ Trait default initializers -----------------------------------------
802 802
803 803 def _banner_default(self):
804 804 """ Returns the standard Python banner.
805 805 """
806 806 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
807 807 '"license" for more information.'
808 808 return banner % (sys.version, sys.platform)
@@ -1,305 +1,305 b''
1 1 # System library imports
2 2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from IPython.utils.py3compat import unicode_type
6 6 from IPython.utils.traitlets import Bool
7 7 from .console_widget import ConsoleWidget
8 8
9 9
10 10 class HistoryConsoleWidget(ConsoleWidget):
11 11 """ A ConsoleWidget that keeps a history of the commands that have been
12 12 executed and provides a readline-esque interface to this history.
13 13 """
14 14
15 15 #------ Configuration ------------------------------------------------------
16 16
17 17 # If enabled, the input buffer will become "locked" to history movement when
18 18 # an edit is made to a multi-line input buffer. To override the lock, use
19 19 # Shift in conjunction with the standard history cycling keys.
20 20 history_lock = Bool(False, config=True)
21 21
22 22 #---------------------------------------------------------------------------
23 23 # 'object' interface
24 24 #---------------------------------------------------------------------------
25 25
26 26 def __init__(self, *args, **kw):
27 27 super(HistoryConsoleWidget, self).__init__(*args, **kw)
28 28
29 29 # HistoryConsoleWidget protected variables.
30 30 self._history = []
31 31 self._history_edits = {}
32 32 self._history_index = 0
33 33 self._history_prefix = ''
34 34
35 35 #---------------------------------------------------------------------------
36 36 # 'ConsoleWidget' public interface
37 37 #---------------------------------------------------------------------------
38 38
39 39 def execute(self, source=None, hidden=False, interactive=False):
40 40 """ Reimplemented to the store history.
41 41 """
42 42 if not hidden:
43 43 history = self.input_buffer if source is None else source
44 44
45 45 executed = super(HistoryConsoleWidget, self).execute(
46 46 source, hidden, interactive)
47 47
48 48 if executed and not hidden:
49 49 # Save the command unless it was an empty string or was identical
50 50 # to the previous command.
51 51 history = history.rstrip()
52 52 if history and (not self._history or self._history[-1] != history):
53 53 self._history.append(history)
54 54
55 55 # Emulate readline: reset all history edits.
56 56 self._history_edits = {}
57 57
58 58 # Move the history index to the most recent item.
59 59 self._history_index = len(self._history)
60 60
61 61 return executed
62 62
63 63 #---------------------------------------------------------------------------
64 64 # 'ConsoleWidget' abstract interface
65 65 #---------------------------------------------------------------------------
66 66
67 67 def _up_pressed(self, shift_modifier):
68 68 """ Called when the up key is pressed. Returns whether to continue
69 69 processing the event.
70 70 """
71 71 prompt_cursor = self._get_prompt_cursor()
72 72 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
73 73 # Bail out if we're locked.
74 74 if self._history_locked() and not shift_modifier:
75 75 return False
76 76
77 77 # Set a search prefix based on the cursor position.
78 78 col = self._get_input_buffer_cursor_column()
79 79 input_buffer = self.input_buffer
80 80 # use the *shortest* of the cursor column and the history prefix
81 81 # to determine if the prefix has changed
82 82 n = min(col, len(self._history_prefix))
83 83
84 84 # prefix changed, restart search from the beginning
85 85 if (self._history_prefix[:n] != input_buffer[:n]):
86 86 self._history_index = len(self._history)
87 87
88 88 # the only time we shouldn't set the history prefix
89 89 # to the line up to the cursor is if we are already
90 90 # in a simple scroll (no prefix),
91 91 # and the cursor is at the end of the first line
92 92
93 93 # check if we are at the end of the first line
94 94 c = self._get_cursor()
95 95 current_pos = c.position()
96 96 c.movePosition(QtGui.QTextCursor.EndOfLine)
97 97 at_eol = (c.position() == current_pos)
98 98
99 99 if self._history_index == len(self._history) or \
100 100 not (self._history_prefix == '' and at_eol) or \
101 101 not (self._get_edited_history(self._history_index)[:col] == input_buffer[:col]):
102 102 self._history_prefix = input_buffer[:col]
103 103
104 104 # Perform the search.
105 105 self.history_previous(self._history_prefix,
106 106 as_prefix=not shift_modifier)
107 107
108 108 # Go to the first line of the prompt for seemless history scrolling.
109 109 # Emulate readline: keep the cursor position fixed for a prefix
110 110 # search.
111 111 cursor = self._get_prompt_cursor()
112 112 if self._history_prefix:
113 113 cursor.movePosition(QtGui.QTextCursor.Right,
114 114 n=len(self._history_prefix))
115 115 else:
116 116 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
117 117 self._set_cursor(cursor)
118 118
119 119 return False
120 120
121 121 return True
122 122
123 123 def _down_pressed(self, shift_modifier):
124 124 """ Called when the down key is pressed. Returns whether to continue
125 125 processing the event.
126 126 """
127 127 end_cursor = self._get_end_cursor()
128 128 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
129 129 # Bail out if we're locked.
130 130 if self._history_locked() and not shift_modifier:
131 131 return False
132 132
133 133 # Perform the search.
134 134 replaced = self.history_next(self._history_prefix,
135 135 as_prefix=not shift_modifier)
136 136
137 137 # Emulate readline: keep the cursor position fixed for a prefix
138 138 # search. (We don't need to move the cursor to the end of the buffer
139 139 # in the other case because this happens automatically when the
140 140 # input buffer is set.)
141 141 if self._history_prefix and replaced:
142 142 cursor = self._get_prompt_cursor()
143 143 cursor.movePosition(QtGui.QTextCursor.Right,
144 144 n=len(self._history_prefix))
145 145 self._set_cursor(cursor)
146 146
147 147 return False
148 148
149 149 return True
150 150
151 151 #---------------------------------------------------------------------------
152 152 # 'HistoryConsoleWidget' public interface
153 153 #---------------------------------------------------------------------------
154 154
155 155 def history_previous(self, substring='', as_prefix=True):
156 156 """ If possible, set the input buffer to a previous history item.
157 157
158 158 Parameters
159 159 ----------
160 160 substring : str, optional
161 161 If specified, search for an item with this substring.
162 162 as_prefix : bool, optional
163 163 If True, the substring must match at the beginning (default).
164 164
165 165 Returns
166 166 -------
167 167 Whether the input buffer was changed.
168 168 """
169 169 index = self._history_index
170 170 replace = False
171 171 while index > 0:
172 172 index -= 1
173 173 history = self._get_edited_history(index)
174 174 if (as_prefix and history.startswith(substring)) \
175 175 or (not as_prefix and substring in history):
176 176 replace = True
177 177 break
178 178
179 179 if replace:
180 180 self._store_edits()
181 181 self._history_index = index
182 182 self.input_buffer = history
183 183
184 184 return replace
185 185
186 186 def history_next(self, substring='', as_prefix=True):
187 187 """ If possible, set the input buffer to a subsequent history item.
188 188
189 189 Parameters
190 190 ----------
191 191 substring : str, optional
192 192 If specified, search for an item with this substring.
193 193 as_prefix : bool, optional
194 194 If True, the substring must match at the beginning (default).
195 195
196 196 Returns
197 197 -------
198 198 Whether the input buffer was changed.
199 199 """
200 200 index = self._history_index
201 201 replace = False
202 202 while index < len(self._history):
203 203 index += 1
204 204 history = self._get_edited_history(index)
205 205 if (as_prefix and history.startswith(substring)) \
206 206 or (not as_prefix and substring in history):
207 207 replace = True
208 208 break
209 209
210 210 if replace:
211 211 self._store_edits()
212 212 self._history_index = index
213 213 self.input_buffer = history
214 214
215 215 return replace
216 216
217 217 def history_tail(self, n=10):
218 218 """ Get the local history list.
219 219
220 220 Parameters
221 221 ----------
222 222 n : int
223 223 The (maximum) number of history items to get.
224 224 """
225 225 return self._history[-n:]
226 226
227 227 def _request_update_session_history_length(self):
228 msg_id = self.kernel_client.shell_channel.execute('',
228 msg_id = self.kernel_client.execute('',
229 229 silent=True,
230 230 user_expressions={
231 231 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
232 232 }
233 233 )
234 234 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'save_magic')
235 235
236 236 def _handle_execute_reply(self, msg):
237 237 """ Handles replies for code execution, here only session history length
238 238 """
239 239 msg_id = msg['parent_header']['msg_id']
240 240 info = self._request_info['execute'].pop(msg_id,None)
241 241 if info and info.kind == 'save_magic' and not self._hidden:
242 242 content = msg['content']
243 243 status = content['status']
244 244 if status == 'ok':
245 245 self._max_session_history = int(
246 246 content['user_expressions']['hlen']['data']['text/plain']
247 247 )
248 248
249 249 def save_magic(self):
250 250 # update the session history length
251 251 self._request_update_session_history_length()
252 252
253 253 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
254 254 "Enter A filename",
255 255 filter='Python File (*.py);; All files (*.*)'
256 256 )
257 257
258 258 # let's the user search/type for a file name, while the history length
259 259 # is fetched
260 260
261 261 if file_name:
262 262 hist_range, ok = QtGui.QInputDialog.getText(self,
263 263 'Please enter an interval of command to save',
264 264 'Saving commands:',
265 265 text=str('1-'+str(self._max_session_history))
266 266 )
267 267 if ok:
268 268 self.execute("%save"+" "+file_name+" "+str(hist_range))
269 269
270 270 #---------------------------------------------------------------------------
271 271 # 'HistoryConsoleWidget' protected interface
272 272 #---------------------------------------------------------------------------
273 273
274 274 def _history_locked(self):
275 275 """ Returns whether history movement is locked.
276 276 """
277 277 return (self.history_lock and
278 278 (self._get_edited_history(self._history_index) !=
279 279 self.input_buffer) and
280 280 (self._get_prompt_cursor().blockNumber() !=
281 281 self._get_end_cursor().blockNumber()))
282 282
283 283 def _get_edited_history(self, index):
284 284 """ Retrieves a history item, possibly with temporary edits.
285 285 """
286 286 if index in self._history_edits:
287 287 return self._history_edits[index]
288 288 elif index == len(self._history):
289 289 return unicode_type()
290 290 return self._history[index]
291 291
292 292 def _set_history(self, history):
293 293 """ Replace the current history with a sequence of history items.
294 294 """
295 295 self._history = list(history)
296 296 self._history_edits = {}
297 297 self._history_index = len(self._history)
298 298
299 299 def _store_edits(self):
300 300 """ If there are edits to the current input buffer, store them.
301 301 """
302 302 current = self.input_buffer
303 303 if self._history_index == len(self._history) or \
304 304 self._history[self._history_index] != current:
305 305 self._history_edits[self._history_index] = current
@@ -1,599 +1,598 b''
1 1 """A FrontendWidget that emulates the interface of the console IPython.
2 2
3 3 This supports the additional functionality provided by the IPython kernel.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 from collections import namedtuple
10 10 import os.path
11 11 import re
12 12 from subprocess import Popen
13 13 import sys
14 14 import time
15 15 from textwrap import dedent
16 16
17 17 from IPython.external.qt import QtCore, QtGui
18 18
19 19 from IPython.core.inputsplitter import IPythonInputSplitter
20 20 from IPython.core.release import version
21 21 from IPython.core.inputtransformer import ipy_prompt
22 22 from IPython.utils.traitlets import Bool, Unicode
23 23 from .frontend_widget import FrontendWidget
24 24 from . import styles
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Constants
28 28 #-----------------------------------------------------------------------------
29 29
30 30 # Default strings to build and display input and output prompts (and separators
31 31 # in between)
32 32 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
33 33 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
34 34 default_input_sep = '\n'
35 35 default_output_sep = ''
36 36 default_output_sep2 = ''
37 37
38 38 # Base path for most payload sources.
39 39 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
40 40
41 41 if sys.platform.startswith('win'):
42 42 default_editor = 'notepad'
43 43 else:
44 44 default_editor = ''
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # IPythonWidget class
48 48 #-----------------------------------------------------------------------------
49 49
50 50 class IPythonWidget(FrontendWidget):
51 51 """ A FrontendWidget for an IPython kernel.
52 52 """
53 53
54 54 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
55 55 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
56 56 # settings.
57 57 custom_edit = Bool(False)
58 58 custom_edit_requested = QtCore.Signal(object, object)
59 59
60 60 editor = Unicode(default_editor, config=True,
61 61 help="""
62 62 A command for invoking a system text editor. If the string contains a
63 63 {filename} format specifier, it will be used. Otherwise, the filename
64 64 will be appended to the end the command.
65 65 """)
66 66
67 67 editor_line = Unicode(config=True,
68 68 help="""
69 69 The editor command to use when a specific line number is requested. The
70 70 string should contain two format specifiers: {line} and {filename}. If
71 71 this parameter is not specified, the line number option to the %edit
72 72 magic will be ignored.
73 73 """)
74 74
75 75 style_sheet = Unicode(config=True,
76 76 help="""
77 77 A CSS stylesheet. The stylesheet can contain classes for:
78 78 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
79 79 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
80 80 3. IPython: .error, .in-prompt, .out-prompt, etc
81 81 """)
82 82
83 83 syntax_style = Unicode(config=True,
84 84 help="""
85 85 If not empty, use this Pygments style for syntax highlighting.
86 86 Otherwise, the style sheet is queried for Pygments style
87 87 information.
88 88 """)
89 89
90 90 # Prompts.
91 91 in_prompt = Unicode(default_in_prompt, config=True)
92 92 out_prompt = Unicode(default_out_prompt, config=True)
93 93 input_sep = Unicode(default_input_sep, config=True)
94 94 output_sep = Unicode(default_output_sep, config=True)
95 95 output_sep2 = Unicode(default_output_sep2, config=True)
96 96
97 97 # FrontendWidget protected class variables.
98 98 _input_splitter_class = IPythonInputSplitter
99 99 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
100 100 logical_line_transforms=[],
101 101 python_line_transforms=[],
102 102 )
103 103
104 104 # IPythonWidget protected class variables.
105 105 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
106 106 _payload_source_edit = 'edit'
107 107 _payload_source_exit = 'ask_exit'
108 108 _payload_source_next_input = 'set_next_input'
109 109 _payload_source_page = 'page'
110 110 _retrying_history_request = False
111 111 _starting = False
112 112
113 113 #---------------------------------------------------------------------------
114 114 # 'object' interface
115 115 #---------------------------------------------------------------------------
116 116
117 117 def __init__(self, *args, **kw):
118 118 super(IPythonWidget, self).__init__(*args, **kw)
119 119
120 120 # IPythonWidget protected variables.
121 121 self._payload_handlers = {
122 122 self._payload_source_edit : self._handle_payload_edit,
123 123 self._payload_source_exit : self._handle_payload_exit,
124 124 self._payload_source_page : self._handle_payload_page,
125 125 self._payload_source_next_input : self._handle_payload_next_input }
126 126 self._previous_prompt_obj = None
127 127 self._keep_kernel_on_exit = None
128 128
129 129 # Initialize widget styling.
130 130 if self.style_sheet:
131 131 self._style_sheet_changed()
132 132 self._syntax_style_changed()
133 133 else:
134 134 self.set_default_style()
135 135
136 136 self._guiref_loaded = False
137 137
138 138 #---------------------------------------------------------------------------
139 139 # 'BaseFrontendMixin' abstract interface
140 140 #---------------------------------------------------------------------------
141 141 def _handle_complete_reply(self, rep):
142 142 """ Reimplemented to support IPython's improved completion machinery.
143 143 """
144 144 self.log.debug("complete: %s", rep.get('content', ''))
145 145 cursor = self._get_cursor()
146 146 info = self._request_info.get('complete')
147 147 if info and info.id == rep['parent_header']['msg_id'] and \
148 148 info.pos == cursor.position():
149 149 content = rep['content']
150 150 matches = content['matches']
151 151 start = content['cursor_start']
152 152 end = content['cursor_end']
153 153
154 154 start = max(start, 0)
155 155 end = max(end, start)
156 156
157 157 # Move the control's cursor to the desired end point
158 158 cursor_pos = self._get_input_buffer_cursor_pos()
159 159 if end < cursor_pos:
160 160 cursor.movePosition(QtGui.QTextCursor.Left,
161 161 n=(cursor_pos - end))
162 162 elif end > cursor_pos:
163 163 cursor.movePosition(QtGui.QTextCursor.Right,
164 164 n=(end - cursor_pos))
165 165 # This line actually applies the move to control's cursor
166 166 self._control.setTextCursor(cursor)
167 167
168 168 offset = end - start
169 169 # Move the local cursor object to the start of the match and
170 170 # complete.
171 171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
172 172 self._complete_with_items(cursor, matches)
173 173
174 174 def _handle_execute_reply(self, msg):
175 175 """ Reimplemented to support prompt requests.
176 176 """
177 177 msg_id = msg['parent_header'].get('msg_id')
178 178 info = self._request_info['execute'].get(msg_id)
179 179 if info and info.kind == 'prompt':
180 180 content = msg['content']
181 181 if content['status'] == 'aborted':
182 182 self._show_interpreter_prompt()
183 183 else:
184 184 number = content['execution_count'] + 1
185 185 self._show_interpreter_prompt(number)
186 186 self._request_info['execute'].pop(msg_id)
187 187 else:
188 188 super(IPythonWidget, self)._handle_execute_reply(msg)
189 189
190 190 def _handle_history_reply(self, msg):
191 191 """ Implemented to handle history tail replies, which are only supported
192 192 by the IPython kernel.
193 193 """
194 194 content = msg['content']
195 195 if 'history' not in content:
196 196 self.log.error("History request failed: %r"%content)
197 197 if content.get('status', '') == 'aborted' and \
198 198 not self._retrying_history_request:
199 199 # a *different* action caused this request to be aborted, so
200 200 # we should try again.
201 201 self.log.error("Retrying aborted history request")
202 202 # prevent multiple retries of aborted requests:
203 203 self._retrying_history_request = True
204 204 # wait out the kernel's queue flush, which is currently timed at 0.1s
205 205 time.sleep(0.25)
206 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
206 self.kernel_client.history(hist_access_type='tail',n=1000)
207 207 else:
208 208 self._retrying_history_request = False
209 209 return
210 210 # reset retry flag
211 211 self._retrying_history_request = False
212 212 history_items = content['history']
213 213 self.log.debug("Received history reply with %i entries", len(history_items))
214 214 items = []
215 215 last_cell = u""
216 216 for _, _, cell in history_items:
217 217 cell = cell.rstrip()
218 218 if cell != last_cell:
219 219 items.append(cell)
220 220 last_cell = cell
221 221 self._set_history(items)
222 222
223 223 def _insert_other_input(self, cursor, content):
224 224 """Insert function for input from other frontends"""
225 225 cursor.beginEditBlock()
226 226 start = cursor.position()
227 227 n = content.get('execution_count', 0)
228 228 cursor.insertText('\n')
229 229 self._insert_html(cursor, self._make_in_prompt(n))
230 230 cursor.insertText(content['code'])
231 231 self._highlighter.rehighlightBlock(cursor.block())
232 232 cursor.endEditBlock()
233 233
234 234 def _handle_execute_input(self, msg):
235 235 """Handle an execute_input message"""
236 236 self.log.debug("execute_input: %s", msg.get('content', ''))
237 237 if self.include_output(msg):
238 238 self._append_custom(self._insert_other_input, msg['content'], before_prompt=True)
239 239
240 240
241 241 def _handle_execute_result(self, msg):
242 242 """ Reimplemented for IPython-style "display hook".
243 243 """
244 244 self.log.debug("execute_result: %s", msg.get('content', ''))
245 245 if self.include_output(msg):
246 246 self.flush_clearoutput()
247 247 content = msg['content']
248 248 prompt_number = content.get('execution_count', 0)
249 249 data = content['data']
250 250 if 'text/plain' in data:
251 251 self._append_plain_text(self.output_sep, True)
252 252 self._append_html(self._make_out_prompt(prompt_number), True)
253 253 text = data['text/plain']
254 254 # If the repr is multiline, make sure we start on a new line,
255 255 # so that its lines are aligned.
256 256 if "\n" in text and not self.output_sep.endswith("\n"):
257 257 self._append_plain_text('\n', True)
258 258 self._append_plain_text(text + self.output_sep2, True)
259 259
260 260 def _handle_display_data(self, msg):
261 261 """ The base handler for the ``display_data`` message.
262 262 """
263 263 self.log.debug("display: %s", msg.get('content', ''))
264 264 # For now, we don't display data from other frontends, but we
265 265 # eventually will as this allows all frontends to monitor the display
266 266 # data. But we need to figure out how to handle this in the GUI.
267 267 if self.include_output(msg):
268 268 self.flush_clearoutput()
269 269 data = msg['content']['data']
270 270 metadata = msg['content']['metadata']
271 271 # In the regular IPythonWidget, we simply print the plain text
272 272 # representation.
273 273 if 'text/plain' in data:
274 274 text = data['text/plain']
275 275 self._append_plain_text(text, True)
276 276 # This newline seems to be needed for text and html output.
277 277 self._append_plain_text(u'\n', True)
278 278
279 279 def _handle_kernel_info_reply(self, rep):
280 280 """Handle kernel info replies."""
281 281 content = rep['content']
282 282 if not self._guiref_loaded:
283 283 if content.get('language') == 'python':
284 284 self._load_guiref_magic()
285 285 self._guiref_loaded = True
286 286
287 287 self.kernel_banner = content.get('banner', '')
288 288 if self._starting:
289 289 # finish handling started channels
290 290 self._starting = False
291 291 super(IPythonWidget, self)._started_channels()
292 292
293 293 def _started_channels(self):
294 294 """Reimplemented to make a history request and load %guiref."""
295 295 self._starting = True
296 296 # The reply will trigger %guiref load provided language=='python'
297 297 self.kernel_client.kernel_info()
298 298
299 self.kernel_client.shell_channel.history(hist_access_type='tail',
300 n=1000)
299 self.kernel_client.history(hist_access_type='tail', n=1000)
301 300
302 301 def _load_guiref_magic(self):
303 302 """Load %guiref magic."""
304 self.kernel_client.shell_channel.execute('\n'.join([
303 self.kernel_client.execute('\n'.join([
305 304 "try:",
306 305 " _usage",
307 306 "except:",
308 307 " from IPython.core import usage as _usage",
309 308 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
310 309 " del _usage",
311 310 ]), silent=True)
312 311
313 312 #---------------------------------------------------------------------------
314 313 # 'ConsoleWidget' public interface
315 314 #---------------------------------------------------------------------------
316 315
317 316 #---------------------------------------------------------------------------
318 317 # 'FrontendWidget' public interface
319 318 #---------------------------------------------------------------------------
320 319
321 320 def execute_file(self, path, hidden=False):
322 321 """ Reimplemented to use the 'run' magic.
323 322 """
324 323 # Use forward slashes on Windows to avoid escaping each separator.
325 324 if sys.platform == 'win32':
326 325 path = os.path.normpath(path).replace('\\', '/')
327 326
328 327 # Perhaps we should not be using %run directly, but while we
329 328 # are, it is necessary to quote or escape filenames containing spaces
330 329 # or quotes.
331 330
332 331 # In earlier code here, to minimize escaping, we sometimes quoted the
333 332 # filename with single quotes. But to do this, this code must be
334 333 # platform-aware, because run uses shlex rather than python string
335 334 # parsing, so that:
336 335 # * In Win: single quotes can be used in the filename without quoting,
337 336 # and we cannot use single quotes to quote the filename.
338 337 # * In *nix: we can escape double quotes in a double quoted filename,
339 338 # but can't escape single quotes in a single quoted filename.
340 339
341 340 # So to keep this code non-platform-specific and simple, we now only
342 341 # use double quotes to quote filenames, and escape when needed:
343 342 if ' ' in path or "'" in path or '"' in path:
344 343 path = '"%s"' % path.replace('"', '\\"')
345 344 self.execute('%%run %s' % path, hidden=hidden)
346 345
347 346 #---------------------------------------------------------------------------
348 347 # 'FrontendWidget' protected interface
349 348 #---------------------------------------------------------------------------
350 349
351 350 def _process_execute_error(self, msg):
352 351 """ Reimplemented for IPython-style traceback formatting.
353 352 """
354 353 content = msg['content']
355 354 traceback = '\n'.join(content['traceback']) + '\n'
356 355 if False:
357 356 # FIXME: For now, tracebacks come as plain text, so we can't use
358 357 # the html renderer yet. Once we refactor ultratb to produce
359 358 # properly styled tracebacks, this branch should be the default
360 359 traceback = traceback.replace(' ', '&nbsp;')
361 360 traceback = traceback.replace('\n', '<br/>')
362 361
363 362 ename = content['ename']
364 363 ename_styled = '<span class="error">%s</span>' % ename
365 364 traceback = traceback.replace(ename, ename_styled)
366 365
367 366 self._append_html(traceback)
368 367 else:
369 368 # This is the fallback for now, using plain text with ansi escapes
370 369 self._append_plain_text(traceback)
371 370
372 371 def _process_execute_payload(self, item):
373 372 """ Reimplemented to dispatch payloads to handler methods.
374 373 """
375 374 handler = self._payload_handlers.get(item['source'])
376 375 if handler is None:
377 376 # We have no handler for this type of payload, simply ignore it
378 377 return False
379 378 else:
380 379 handler(item)
381 380 return True
382 381
383 382 def _show_interpreter_prompt(self, number=None):
384 383 """ Reimplemented for IPython-style prompts.
385 384 """
386 385 # If a number was not specified, make a prompt number request.
387 386 if number is None:
388 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
387 msg_id = self.kernel_client.execute('', silent=True)
389 388 info = self._ExecutionRequest(msg_id, 'prompt')
390 389 self._request_info['execute'][msg_id] = info
391 390 return
392 391
393 392 # Show a new prompt and save information about it so that it can be
394 393 # updated later if the prompt number turns out to be wrong.
395 394 self._prompt_sep = self.input_sep
396 395 self._show_prompt(self._make_in_prompt(number), html=True)
397 396 block = self._control.document().lastBlock()
398 397 length = len(self._prompt)
399 398 self._previous_prompt_obj = self._PromptBlock(block, length, number)
400 399
401 400 # Update continuation prompt to reflect (possibly) new prompt length.
402 401 self._set_continuation_prompt(
403 402 self._make_continuation_prompt(self._prompt), html=True)
404 403
405 404 def _show_interpreter_prompt_for_reply(self, msg):
406 405 """ Reimplemented for IPython-style prompts.
407 406 """
408 407 # Update the old prompt number if necessary.
409 408 content = msg['content']
410 409 # abort replies do not have any keys:
411 410 if content['status'] == 'aborted':
412 411 if self._previous_prompt_obj:
413 412 previous_prompt_number = self._previous_prompt_obj.number
414 413 else:
415 414 previous_prompt_number = 0
416 415 else:
417 416 previous_prompt_number = content['execution_count']
418 417 if self._previous_prompt_obj and \
419 418 self._previous_prompt_obj.number != previous_prompt_number:
420 419 block = self._previous_prompt_obj.block
421 420
422 421 # Make sure the prompt block has not been erased.
423 422 if block.isValid() and block.text():
424 423
425 424 # Remove the old prompt and insert a new prompt.
426 425 cursor = QtGui.QTextCursor(block)
427 426 cursor.movePosition(QtGui.QTextCursor.Right,
428 427 QtGui.QTextCursor.KeepAnchor,
429 428 self._previous_prompt_obj.length)
430 429 prompt = self._make_in_prompt(previous_prompt_number)
431 430 self._prompt = self._insert_html_fetching_plain_text(
432 431 cursor, prompt)
433 432
434 433 # When the HTML is inserted, Qt blows away the syntax
435 434 # highlighting for the line, so we need to rehighlight it.
436 435 self._highlighter.rehighlightBlock(cursor.block())
437 436
438 437 self._previous_prompt_obj = None
439 438
440 439 # Show a new prompt with the kernel's estimated prompt number.
441 440 self._show_interpreter_prompt(previous_prompt_number + 1)
442 441
443 442 #---------------------------------------------------------------------------
444 443 # 'IPythonWidget' interface
445 444 #---------------------------------------------------------------------------
446 445
447 446 def set_default_style(self, colors='lightbg'):
448 447 """ Sets the widget style to the class defaults.
449 448
450 449 Parameters
451 450 ----------
452 451 colors : str, optional (default lightbg)
453 452 Whether to use the default IPython light background or dark
454 453 background or B&W style.
455 454 """
456 455 colors = colors.lower()
457 456 if colors=='lightbg':
458 457 self.style_sheet = styles.default_light_style_sheet
459 458 self.syntax_style = styles.default_light_syntax_style
460 459 elif colors=='linux':
461 460 self.style_sheet = styles.default_dark_style_sheet
462 461 self.syntax_style = styles.default_dark_syntax_style
463 462 elif colors=='nocolor':
464 463 self.style_sheet = styles.default_bw_style_sheet
465 464 self.syntax_style = styles.default_bw_syntax_style
466 465 else:
467 466 raise KeyError("No such color scheme: %s"%colors)
468 467
469 468 #---------------------------------------------------------------------------
470 469 # 'IPythonWidget' protected interface
471 470 #---------------------------------------------------------------------------
472 471
473 472 def _edit(self, filename, line=None):
474 473 """ Opens a Python script for editing.
475 474
476 475 Parameters
477 476 ----------
478 477 filename : str
479 478 A path to a local system file.
480 479
481 480 line : int, optional
482 481 A line of interest in the file.
483 482 """
484 483 if self.custom_edit:
485 484 self.custom_edit_requested.emit(filename, line)
486 485 elif not self.editor:
487 486 self._append_plain_text('No default editor available.\n'
488 487 'Specify a GUI text editor in the `IPythonWidget.editor` '
489 488 'configurable to enable the %edit magic')
490 489 else:
491 490 try:
492 491 filename = '"%s"' % filename
493 492 if line and self.editor_line:
494 493 command = self.editor_line.format(filename=filename,
495 494 line=line)
496 495 else:
497 496 try:
498 497 command = self.editor.format()
499 498 except KeyError:
500 499 command = self.editor.format(filename=filename)
501 500 else:
502 501 command += ' ' + filename
503 502 except KeyError:
504 503 self._append_plain_text('Invalid editor command.\n')
505 504 else:
506 505 try:
507 506 Popen(command, shell=True)
508 507 except OSError:
509 508 msg = 'Opening editor with command "%s" failed.\n'
510 509 self._append_plain_text(msg % command)
511 510
512 511 def _make_in_prompt(self, number):
513 512 """ Given a prompt number, returns an HTML In prompt.
514 513 """
515 514 try:
516 515 body = self.in_prompt % number
517 516 except TypeError:
518 517 # allow in_prompt to leave out number, e.g. '>>> '
519 518 from xml.sax.saxutils import escape
520 519 body = escape(self.in_prompt)
521 520 return '<span class="in-prompt">%s</span>' % body
522 521
523 522 def _make_continuation_prompt(self, prompt):
524 523 """ Given a plain text version of an In prompt, returns an HTML
525 524 continuation prompt.
526 525 """
527 526 end_chars = '...: '
528 527 space_count = len(prompt.lstrip('\n')) - len(end_chars)
529 528 body = '&nbsp;' * space_count + end_chars
530 529 return '<span class="in-prompt">%s</span>' % body
531 530
532 531 def _make_out_prompt(self, number):
533 532 """ Given a prompt number, returns an HTML Out prompt.
534 533 """
535 534 try:
536 535 body = self.out_prompt % number
537 536 except TypeError:
538 537 # allow out_prompt to leave out number, e.g. '<<< '
539 538 from xml.sax.saxutils import escape
540 539 body = escape(self.out_prompt)
541 540 return '<span class="out-prompt">%s</span>' % body
542 541
543 542 #------ Payload handlers --------------------------------------------------
544 543
545 544 # Payload handlers with a generic interface: each takes the opaque payload
546 545 # dict, unpacks it and calls the underlying functions with the necessary
547 546 # arguments.
548 547
549 548 def _handle_payload_edit(self, item):
550 549 self._edit(item['filename'], item['line_number'])
551 550
552 551 def _handle_payload_exit(self, item):
553 552 self._keep_kernel_on_exit = item['keepkernel']
554 553 self.exit_requested.emit(self)
555 554
556 555 def _handle_payload_next_input(self, item):
557 556 self.input_buffer = item['text']
558 557
559 558 def _handle_payload_page(self, item):
560 559 # Since the plain text widget supports only a very small subset of HTML
561 560 # and we have no control over the HTML source, we only page HTML
562 561 # payloads in the rich text widget.
563 562 data = item['data']
564 563 if 'text/html' in data and self.kind == 'rich':
565 564 self._page(data['text/html'], html=True)
566 565 else:
567 566 self._page(data['text/plain'], html=False)
568 567
569 568 #------ Trait change handlers --------------------------------------------
570 569
571 570 def _style_sheet_changed(self):
572 571 """ Set the style sheets of the underlying widgets.
573 572 """
574 573 self.setStyleSheet(self.style_sheet)
575 574 if self._control is not None:
576 575 self._control.document().setDefaultStyleSheet(self.style_sheet)
577 576 bg_color = self._control.palette().window().color()
578 577 self._ansi_processor.set_background_color(bg_color)
579 578
580 579 if self._page_control is not None:
581 580 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
582 581
583 582
584 583
585 584 def _syntax_style_changed(self):
586 585 """ Set the style for the syntax highlighter.
587 586 """
588 587 if self._highlighter is None:
589 588 # ignore premature calls
590 589 return
591 590 if self.syntax_style:
592 591 self._highlighter.set_style(self.syntax_style)
593 592 else:
594 593 self._highlighter.set_style_sheet(self.style_sheet)
595 594
596 595 #------ Trait default initializers -----------------------------------------
597 596
598 597 def _banner_default(self):
599 598 return "IPython QtConsole {version}\n".format(version=version)
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Adapt readline completer interface to make ZMQ request."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 try:
8 8 from queue import Empty # Py 3
9 9 except ImportError:
10 10 from Queue import Empty # Py 2
11 11
12 12 from IPython.config import Configurable
13 13 from IPython.core.completer import IPCompleter
14 14 from IPython.utils.traitlets import Float
15 15 import IPython.utils.rlineimpl as readline
16 16
17 17 class ZMQCompleter(IPCompleter):
18 18 """Client-side completion machinery.
19 19
20 20 How it works: self.complete will be called multiple times, with
21 21 state=0,1,2,... When state=0 it should compute ALL the completion matches,
22 22 and then return them for each value of state."""
23 23
24 24 timeout = Float(5.0, config=True, help='timeout before completion abort')
25 25
26 26 def __init__(self, shell, client, config=None):
27 27 super(ZMQCompleter,self).__init__(config=config)
28 28
29 29 self.shell = shell
30 30 self.client = client
31 31 self.matches = []
32 32
33 33 def complete_request(self, text):
34 34 line = readline.get_line_buffer()
35 35 cursor_pos = readline.get_endidx()
36 36
37 37 # send completion request to kernel
38 38 # Give the kernel up to 0.5s to respond
39 msg_id = self.client.shell_channel.complete(
39 msg_id = self.client.complete(
40 40 code=line,
41 41 cursor_pos=cursor_pos,
42 42 )
43 43
44 44 msg = self.client.shell_channel.get_msg(timeout=self.timeout)
45 45 if msg['parent_header']['msg_id'] == msg_id:
46 46 return msg["content"]["matches"]
47 47 return []
48 48
49 49 def rlcomplete(self, text, state):
50 50 if state == 0:
51 51 try:
52 52 self.matches = self.complete_request(text)
53 53 except Empty:
54 54 #print('WARNING: Kernel timeout on tab completion.')
55 55 pass
56 56
57 57 try:
58 58 return self.matches[state]
59 59 except IndexError:
60 60 return None
61 61
62 62 def complete(self, text, line, cursor_pos=None):
63 63 return self.rlcomplete(text, 0)
@@ -1,578 +1,578 b''
1 1 # -*- coding: utf-8 -*-
2 2 """terminal client to the IPython kernel"""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import bdb
11 11 import signal
12 12 import os
13 13 import sys
14 14 import time
15 15 import subprocess
16 16 from getpass import getpass
17 17 from io import BytesIO
18 18
19 19 try:
20 20 from queue import Empty # Py 3
21 21 except ImportError:
22 22 from Queue import Empty # Py 2
23 23
24 24 from IPython.core import page
25 25 from IPython.core import release
26 26 from IPython.terminal.console.zmqhistory import ZMQHistoryManager
27 27 from IPython.utils.warn import warn, error
28 28 from IPython.utils import io
29 29 from IPython.utils.py3compat import string_types, input
30 30 from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float, Bool
31 31 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
32 32
33 33 from IPython.terminal.interactiveshell import TerminalInteractiveShell
34 34 from IPython.terminal.console.completer import ZMQCompleter
35 35
36 36 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
37 37 """A subclass of TerminalInteractiveShell that uses the 0MQ kernel"""
38 38 _executing = False
39 39 _execution_state = Unicode('')
40 40 _pending_clearoutput = False
41 41 kernel_banner = Unicode('')
42 42 kernel_timeout = Float(60, config=True,
43 43 help="""Timeout for giving up on a kernel (in seconds).
44 44
45 45 On first connect and restart, the console tests whether the
46 46 kernel is running and responsive by sending kernel_info_requests.
47 47 This sets the timeout in seconds for how long the kernel can take
48 48 before being presumed dead.
49 49 """
50 50 )
51 51
52 52 image_handler = Enum(('PIL', 'stream', 'tempfile', 'callable'),
53 53 config=True, help=
54 54 """
55 55 Handler for image type output. This is useful, for example,
56 56 when connecting to the kernel in which pylab inline backend is
57 57 activated. There are four handlers defined. 'PIL': Use
58 58 Python Imaging Library to popup image; 'stream': Use an
59 59 external program to show the image. Image will be fed into
60 60 the STDIN of the program. You will need to configure
61 61 `stream_image_handler`; 'tempfile': Use an external program to
62 62 show the image. Image will be saved in a temporally file and
63 63 the program is called with the temporally file. You will need
64 64 to configure `tempfile_image_handler`; 'callable': You can set
65 65 any Python callable which is called with the image data. You
66 66 will need to configure `callable_image_handler`.
67 67 """
68 68 )
69 69
70 70 stream_image_handler = List(config=True, help=
71 71 """
72 72 Command to invoke an image viewer program when you are using
73 73 'stream' image handler. This option is a list of string where
74 74 the first element is the command itself and reminders are the
75 75 options for the command. Raw image data is given as STDIN to
76 76 the program.
77 77 """
78 78 )
79 79
80 80 tempfile_image_handler = List(config=True, help=
81 81 """
82 82 Command to invoke an image viewer program when you are using
83 83 'tempfile' image handler. This option is a list of string
84 84 where the first element is the command itself and reminders
85 85 are the options for the command. You can use {file} and
86 86 {format} in the string to represent the location of the
87 87 generated image file and image format.
88 88 """
89 89 )
90 90
91 91 callable_image_handler = Any(config=True, help=
92 92 """
93 93 Callable object called via 'callable' image handler with one
94 94 argument, `data`, which is `msg["content"]["data"]` where
95 95 `msg` is the message from iopub channel. For exmaple, you can
96 96 find base64 encoded PNG data as `data['image/png']`.
97 97 """
98 98 )
99 99
100 100 mime_preference = List(
101 101 default_value=['image/png', 'image/jpeg', 'image/svg+xml'],
102 102 config=True, allow_none=False, help=
103 103 """
104 104 Preferred object representation MIME type in order. First
105 105 matched MIME type will be used.
106 106 """
107 107 )
108 108
109 109 manager = Instance('IPython.kernel.KernelManager')
110 110 client = Instance('IPython.kernel.KernelClient')
111 111 def _client_changed(self, name, old, new):
112 112 self.session_id = new.session.session
113 113 session_id = Unicode()
114 114
115 115 def init_completer(self):
116 116 """Initialize the completion machinery.
117 117
118 118 This creates completion machinery that can be used by client code,
119 119 either interactively in-process (typically triggered by the readline
120 120 library), programmatically (such as in test suites) or out-of-process
121 121 (typically over the network by remote frontends).
122 122 """
123 123 from IPython.core.completerlib import (module_completer,
124 124 magic_run_completer, cd_completer)
125 125
126 126 self.Completer = ZMQCompleter(self, self.client, config=self.config)
127 127
128 128
129 129 self.set_hook('complete_command', module_completer, str_key = 'import')
130 130 self.set_hook('complete_command', module_completer, str_key = 'from')
131 131 self.set_hook('complete_command', magic_run_completer, str_key = '%run')
132 132 self.set_hook('complete_command', cd_completer, str_key = '%cd')
133 133
134 134 # Only configure readline if we truly are using readline. IPython can
135 135 # do tab-completion over the network, in GUIs, etc, where readline
136 136 # itself may be absent
137 137 if self.has_readline:
138 138 self.set_readline_completer()
139 139
140 140 def run_cell(self, cell, store_history=True):
141 141 """Run a complete IPython cell.
142 142
143 143 Parameters
144 144 ----------
145 145 cell : str
146 146 The code (including IPython code such as %magic functions) to run.
147 147 store_history : bool
148 148 If True, the raw and translated cell will be stored in IPython's
149 149 history. For user code calling back into IPython's machinery, this
150 150 should be set to False.
151 151 """
152 152 if (not cell) or cell.isspace():
153 153 # pressing enter flushes any pending display
154 154 self.handle_iopub()
155 155 return
156 156
157 157 # flush stale replies, which could have been ignored, due to missed heartbeats
158 158 while self.client.shell_channel.msg_ready():
159 159 self.client.shell_channel.get_msg()
160 # shell_channel.execute takes 'hidden', which is the inverse of store_hist
161 msg_id = self.client.shell_channel.execute(cell, not store_history)
160 # execute takes 'hidden', which is the inverse of store_hist
161 msg_id = self.client.execute(cell, not store_history)
162 162
163 163 # first thing is wait for any side effects (output, stdin, etc.)
164 164 self._executing = True
165 165 self._execution_state = "busy"
166 166 while self._execution_state != 'idle' and self.client.is_alive():
167 167 try:
168 168 self.handle_input_request(msg_id, timeout=0.05)
169 169 except Empty:
170 170 # display intermediate print statements, etc.
171 171 self.handle_iopub(msg_id)
172 172
173 173 # after all of that is done, wait for the execute reply
174 174 while self.client.is_alive():
175 175 try:
176 176 self.handle_execute_reply(msg_id, timeout=0.05)
177 177 except Empty:
178 178 pass
179 179 else:
180 180 break
181 181 self._executing = False
182 182
183 183 #-----------------
184 184 # message handlers
185 185 #-----------------
186 186
187 187 def handle_execute_reply(self, msg_id, timeout=None):
188 188 msg = self.client.shell_channel.get_msg(block=False, timeout=timeout)
189 189 if msg["parent_header"].get("msg_id", None) == msg_id:
190 190
191 191 self.handle_iopub(msg_id)
192 192
193 193 content = msg["content"]
194 194 status = content['status']
195 195
196 196 if status == 'aborted':
197 197 self.write('Aborted\n')
198 198 return
199 199 elif status == 'ok':
200 200 # handle payloads
201 201 for item in content["payload"]:
202 202 source = item['source']
203 203 if source == 'page':
204 204 page.page(item['data']['text/plain'])
205 205 elif source == 'set_next_input':
206 206 self.set_next_input(item['text'])
207 207 elif source == 'ask_exit':
208 208 self.ask_exit()
209 209
210 210 elif status == 'error':
211 211 for frame in content["traceback"]:
212 212 print(frame, file=io.stderr)
213 213
214 214 self.execution_count = int(content["execution_count"] + 1)
215 215
216 216 include_other_output = Bool(False, config=True,
217 217 help="""Whether to include output from clients
218 218 other than this one sharing the same kernel.
219 219
220 220 Outputs are not displayed until enter is pressed.
221 221 """
222 222 )
223 223 other_output_prefix = Unicode("[remote] ", config=True,
224 224 help="""Prefix to add to outputs coming from clients other than this one.
225 225
226 226 Only relevant if include_other_output is True.
227 227 """
228 228 )
229 229
230 230 def from_here(self, msg):
231 231 """Return whether a message is from this session"""
232 232 return msg['parent_header'].get("session", self.session_id) == self.session_id
233 233
234 234 def include_output(self, msg):
235 235 """Return whether we should include a given output message"""
236 236 from_here = self.from_here(msg)
237 237 if msg['msg_type'] == 'execute_input':
238 238 # only echo inputs not from here
239 239 return self.include_other_output and not from_here
240 240
241 241 if self.include_other_output:
242 242 return True
243 243 else:
244 244 return from_here
245 245
246 246 def handle_iopub(self, msg_id=''):
247 247 """Process messages on the IOPub channel
248 248
249 249 This method consumes and processes messages on the IOPub channel,
250 250 such as stdout, stderr, execute_result and status.
251 251
252 252 It only displays output that is caused by this session.
253 253 """
254 254 while self.client.iopub_channel.msg_ready():
255 255 sub_msg = self.client.iopub_channel.get_msg()
256 256 msg_type = sub_msg['header']['msg_type']
257 257 parent = sub_msg["parent_header"]
258 258
259 259 if self.include_output(sub_msg):
260 260 if msg_type == 'status':
261 261 self._execution_state = sub_msg["content"]["execution_state"]
262 262 elif msg_type == 'stream':
263 263 if sub_msg["content"]["name"] == "stdout":
264 264 if self._pending_clearoutput:
265 265 print("\r", file=io.stdout, end="")
266 266 self._pending_clearoutput = False
267 267 print(sub_msg["content"]["text"], file=io.stdout, end="")
268 268 io.stdout.flush()
269 269 elif sub_msg["content"]["name"] == "stderr":
270 270 if self._pending_clearoutput:
271 271 print("\r", file=io.stderr, end="")
272 272 self._pending_clearoutput = False
273 273 print(sub_msg["content"]["text"], file=io.stderr, end="")
274 274 io.stderr.flush()
275 275
276 276 elif msg_type == 'execute_result':
277 277 if self._pending_clearoutput:
278 278 print("\r", file=io.stdout, end="")
279 279 self._pending_clearoutput = False
280 280 self.execution_count = int(sub_msg["content"]["execution_count"])
281 281 if not self.from_here(sub_msg):
282 282 sys.stdout.write(self.other_output_prefix)
283 283 format_dict = sub_msg["content"]["data"]
284 284 self.handle_rich_data(format_dict)
285 285
286 286 # taken from DisplayHook.__call__:
287 287 hook = self.displayhook
288 288 hook.start_displayhook()
289 289 hook.write_output_prompt()
290 290 hook.write_format_data(format_dict)
291 291 hook.log_output(format_dict)
292 292 hook.finish_displayhook()
293 293
294 294 elif msg_type == 'display_data':
295 295 data = sub_msg["content"]["data"]
296 296 handled = self.handle_rich_data(data)
297 297 if not handled:
298 298 if not self.from_here(sub_msg):
299 299 sys.stdout.write(self.other_output_prefix)
300 300 # if it was an image, we handled it by now
301 301 if 'text/plain' in data:
302 302 print(data['text/plain'])
303 303
304 304 elif msg_type == 'execute_input':
305 305 content = sub_msg['content']
306 306 self.execution_count = content['execution_count']
307 307 if not self.from_here(sub_msg):
308 308 sys.stdout.write(self.other_output_prefix)
309 309 sys.stdout.write(self.prompt_manager.render('in'))
310 310 sys.stdout.write(content['code'])
311 311
312 312 elif msg_type == 'clear_output':
313 313 if sub_msg["content"]["wait"]:
314 314 self._pending_clearoutput = True
315 315 else:
316 316 print("\r", file=io.stdout, end="")
317 317
318 318 _imagemime = {
319 319 'image/png': 'png',
320 320 'image/jpeg': 'jpeg',
321 321 'image/svg+xml': 'svg',
322 322 }
323 323
324 324 def handle_rich_data(self, data):
325 325 for mime in self.mime_preference:
326 326 if mime in data and mime in self._imagemime:
327 327 self.handle_image(data, mime)
328 328 return True
329 329
330 330 def handle_image(self, data, mime):
331 331 handler = getattr(
332 332 self, 'handle_image_{0}'.format(self.image_handler), None)
333 333 if handler:
334 334 handler(data, mime)
335 335
336 336 def handle_image_PIL(self, data, mime):
337 337 if mime not in ('image/png', 'image/jpeg'):
338 338 return
339 339 import PIL.Image
340 340 raw = base64.decodestring(data[mime].encode('ascii'))
341 341 img = PIL.Image.open(BytesIO(raw))
342 342 img.show()
343 343
344 344 def handle_image_stream(self, data, mime):
345 345 raw = base64.decodestring(data[mime].encode('ascii'))
346 346 imageformat = self._imagemime[mime]
347 347 fmt = dict(format=imageformat)
348 348 args = [s.format(**fmt) for s in self.stream_image_handler]
349 349 with open(os.devnull, 'w') as devnull:
350 350 proc = subprocess.Popen(
351 351 args, stdin=subprocess.PIPE,
352 352 stdout=devnull, stderr=devnull)
353 353 proc.communicate(raw)
354 354
355 355 def handle_image_tempfile(self, data, mime):
356 356 raw = base64.decodestring(data[mime].encode('ascii'))
357 357 imageformat = self._imagemime[mime]
358 358 filename = 'tmp.{0}'.format(imageformat)
359 359 with NamedFileInTemporaryDirectory(filename) as f, \
360 360 open(os.devnull, 'w') as devnull:
361 361 f.write(raw)
362 362 f.flush()
363 363 fmt = dict(file=f.name, format=imageformat)
364 364 args = [s.format(**fmt) for s in self.tempfile_image_handler]
365 365 subprocess.call(args, stdout=devnull, stderr=devnull)
366 366
367 367 def handle_image_callable(self, data, mime):
368 368 self.callable_image_handler(data)
369 369
370 370 def handle_input_request(self, msg_id, timeout=0.1):
371 371 """ Method to capture raw_input
372 372 """
373 373 req = self.client.stdin_channel.get_msg(timeout=timeout)
374 374 # in case any iopub came while we were waiting:
375 375 self.handle_iopub(msg_id)
376 376 if msg_id == req["parent_header"].get("msg_id"):
377 377 # wrap SIGINT handler
378 378 real_handler = signal.getsignal(signal.SIGINT)
379 379 def double_int(sig,frame):
380 380 # call real handler (forwards sigint to kernel),
381 381 # then raise local interrupt, stopping local raw_input
382 382 real_handler(sig,frame)
383 383 raise KeyboardInterrupt
384 384 signal.signal(signal.SIGINT, double_int)
385 385 content = req['content']
386 386 read = getpass if content.get('password', False) else input
387 387 try:
388 388 raw_data = read(content["prompt"])
389 389 except EOFError:
390 390 # turn EOFError into EOF character
391 391 raw_data = '\x04'
392 392 except KeyboardInterrupt:
393 393 sys.stdout.write('\n')
394 394 return
395 395 finally:
396 396 # restore SIGINT handler
397 397 signal.signal(signal.SIGINT, real_handler)
398 398
399 399 # only send stdin reply if there *was not* another request
400 400 # or execution finished while we were reading.
401 401 if not (self.client.stdin_channel.msg_ready() or self.client.shell_channel.msg_ready()):
402 self.client.stdin_channel.input(raw_data)
402 self.client.input(raw_data)
403 403
404 404 def mainloop(self, display_banner=False):
405 405 while True:
406 406 try:
407 407 self.interact(display_banner=display_banner)
408 408 #self.interact_with_readline()
409 409 # XXX for testing of a readline-decoupled repl loop, call
410 410 # interact_with_readline above
411 411 break
412 412 except KeyboardInterrupt:
413 413 # this should not be necessary, but KeyboardInterrupt
414 414 # handling seems rather unpredictable...
415 415 self.write("\nKeyboardInterrupt in interact()\n")
416 416
417 self.client.shell_channel.shutdown()
417 self.client.shutdown()
418 418
419 419 def _banner1_default(self):
420 420 return "IPython Console {version}\n".format(version=release.version)
421 421
422 422 def compute_banner(self):
423 423 super(ZMQTerminalInteractiveShell, self).compute_banner()
424 424 if self.client and not self.kernel_banner:
425 425 msg_id = self.client.kernel_info()
426 426 while True:
427 427 try:
428 428 reply = self.client.get_shell_msg(timeout=1)
429 429 except Empty:
430 430 break
431 431 else:
432 432 if reply['parent_header'].get('msg_id') == msg_id:
433 433 self.kernel_banner = reply['content'].get('banner', '')
434 434 break
435 435 self.banner += self.kernel_banner
436 436
437 437 def wait_for_kernel(self, timeout=None):
438 438 """method to wait for a kernel to be ready"""
439 439 tic = time.time()
440 440 self.client.hb_channel.unpause()
441 441 while True:
442 442 msg_id = self.client.kernel_info()
443 443 reply = None
444 444 while True:
445 445 try:
446 446 reply = self.client.get_shell_msg(timeout=1)
447 447 except Empty:
448 448 break
449 449 else:
450 450 if reply['parent_header'].get('msg_id') == msg_id:
451 451 return True
452 452 if timeout is not None \
453 453 and (time.time() - tic) > timeout \
454 454 and not self.client.hb_channel.is_beating():
455 455 # heart failed
456 456 return False
457 457 return True
458 458
459 459 def interact(self, display_banner=None):
460 460 """Closely emulate the interactive Python console."""
461 461
462 462 # batch run -> do not interact
463 463 if self.exit_now:
464 464 return
465 465
466 466 if display_banner is None:
467 467 display_banner = self.display_banner
468 468
469 469 if isinstance(display_banner, string_types):
470 470 self.show_banner(display_banner)
471 471 elif display_banner:
472 472 self.show_banner()
473 473
474 474 more = False
475 475
476 476 # run a non-empty no-op, so that we don't get a prompt until
477 477 # we know the kernel is ready. This keeps the connection
478 478 # message above the first prompt.
479 479 if not self.wait_for_kernel(self.kernel_timeout):
480 480 error("Kernel did not respond\n")
481 481 return
482 482
483 483 if self.has_readline:
484 484 self.readline_startup_hook(self.pre_readline)
485 485 hlen_b4_cell = self.readline.get_current_history_length()
486 486 else:
487 487 hlen_b4_cell = 0
488 488 # exit_now is set by a call to %Exit or %Quit, through the
489 489 # ask_exit callback.
490 490
491 491 while not self.exit_now:
492 492 if not self.client.is_alive():
493 493 # kernel died, prompt for action or exit
494 494
495 495 action = "restart" if self.manager else "wait for restart"
496 496 ans = self.ask_yes_no("kernel died, %s ([y]/n)?" % action, default='y')
497 497 if ans:
498 498 if self.manager:
499 499 self.manager.restart_kernel(True)
500 500 self.wait_for_kernel(self.kernel_timeout)
501 501 else:
502 502 self.exit_now = True
503 503 continue
504 504 try:
505 505 # protect prompt block from KeyboardInterrupt
506 506 # when sitting on ctrl-C
507 507 self.hooks.pre_prompt_hook()
508 508 if more:
509 509 try:
510 510 prompt = self.prompt_manager.render('in2')
511 511 except Exception:
512 512 self.showtraceback()
513 513 if self.autoindent:
514 514 self.rl_do_indent = True
515 515
516 516 else:
517 517 try:
518 518 prompt = self.separate_in + self.prompt_manager.render('in')
519 519 except Exception:
520 520 self.showtraceback()
521 521
522 522 line = self.raw_input(prompt)
523 523 if self.exit_now:
524 524 # quick exit on sys.std[in|out] close
525 525 break
526 526 if self.autoindent:
527 527 self.rl_do_indent = False
528 528
529 529 except KeyboardInterrupt:
530 530 #double-guard against keyboardinterrupts during kbdint handling
531 531 try:
532 532 self.write('\n' + self.get_exception_only())
533 533 source_raw = self.input_splitter.raw_reset()
534 534 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
535 535 more = False
536 536 except KeyboardInterrupt:
537 537 pass
538 538 except EOFError:
539 539 if self.autoindent:
540 540 self.rl_do_indent = False
541 541 if self.has_readline:
542 542 self.readline_startup_hook(None)
543 543 self.write('\n')
544 544 self.exit()
545 545 except bdb.BdbQuit:
546 546 warn('The Python debugger has exited with a BdbQuit exception.\n'
547 547 'Because of how pdb handles the stack, it is impossible\n'
548 548 'for IPython to properly format this particular exception.\n'
549 549 'IPython will resume normal operation.')
550 550 except:
551 551 # exceptions here are VERY RARE, but they can be triggered
552 552 # asynchronously by signal handlers, for example.
553 553 self.showtraceback()
554 554 else:
555 555 try:
556 556 self.input_splitter.push(line)
557 557 more = self.input_splitter.push_accepts_more()
558 558 except SyntaxError:
559 559 # Run the code directly - run_cell takes care of displaying
560 560 # the exception.
561 561 more = False
562 562 if (self.SyntaxTB.last_syntax_error and
563 563 self.autoedit_syntax):
564 564 self.edit_syntax_error()
565 565 if not more:
566 566 source_raw = self.input_splitter.raw_reset()
567 567 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
568 568 self.run_cell(source_raw)
569 569
570 570
571 571 # Turn off the exit flag, so the mainloop can be restarted if desired
572 572 self.exit_now = False
573 573
574 574 def init_history(self):
575 575 """Sets up the command history. """
576 576 self.history_manager = ZMQHistoryManager(client=self.client)
577 577 self.configurables.append(self.history_manager)
578 578
General Comments 0
You need to be logged in to leave comments. Login now