##// END OF EJS Templates
Merge pull request #1362 from minrk/truncate_history_log...
Min RK -
r6062:53013e0d merge
parent child Browse files
Show More
@@ -1,559 +1,559 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 23 transform_ipy_prompt
24 24 from IPython.utils.traitlets import Bool, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101 _transform_prompt = staticmethod(transform_ipy_prompt)
102 102
103 103 # IPythonWidget protected class variables.
104 104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 105 _payload_source_edit = zmq_shell_source + '.edit_magic'
106 106 _payload_source_exit = zmq_shell_source + '.ask_exit'
107 107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
108 108 _payload_source_page = 'IPython.zmq.page.page'
109 109 _retrying_history_request = False
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'object' interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def __init__(self, *args, **kw):
116 116 super(IPythonWidget, self).__init__(*args, **kw)
117 117
118 118 # IPythonWidget protected variables.
119 119 self._payload_handlers = {
120 120 self._payload_source_edit : self._handle_payload_edit,
121 121 self._payload_source_exit : self._handle_payload_exit,
122 122 self._payload_source_page : self._handle_payload_page,
123 123 self._payload_source_next_input : self._handle_payload_next_input }
124 124 self._previous_prompt_obj = None
125 125 self._keep_kernel_on_exit = None
126 126
127 127 # Initialize widget styling.
128 128 if self.style_sheet:
129 129 self._style_sheet_changed()
130 130 self._syntax_style_changed()
131 131 else:
132 132 self.set_default_style()
133 133
134 134 #---------------------------------------------------------------------------
135 135 # 'BaseFrontendMixin' abstract interface
136 136 #---------------------------------------------------------------------------
137 137
138 138 def _handle_complete_reply(self, rep):
139 139 """ Reimplemented to support IPython's improved completion machinery.
140 140 """
141 141 self.log.debug("complete: %s", rep.get('content', ''))
142 142 cursor = self._get_cursor()
143 143 info = self._request_info.get('complete')
144 144 if info and info.id == rep['parent_header']['msg_id'] and \
145 145 info.pos == cursor.position():
146 146 matches = rep['content']['matches']
147 147 text = rep['content']['matched_text']
148 148 offset = len(text)
149 149
150 150 # Clean up matches with period and path separators if the matched
151 151 # text has not been transformed. This is done by truncating all
152 152 # but the last component and then suitably decreasing the offset
153 153 # between the current cursor position and the start of completion.
154 154 if len(matches) > 1 and matches[0][:offset] == text:
155 155 parts = re.split(r'[./\\]', text)
156 156 sep_count = len(parts) - 1
157 157 if sep_count:
158 158 chop_length = sum(map(len, parts[:sep_count])) + sep_count
159 159 matches = [ match[chop_length:] for match in matches ]
160 160 offset -= chop_length
161 161
162 162 # Move the cursor to the start of the match and complete.
163 163 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
164 164 self._complete_with_items(cursor, matches)
165 165
166 166 def _handle_execute_reply(self, msg):
167 167 """ Reimplemented to support prompt requests.
168 168 """
169 169 msg_id = msg['parent_header'].get('msg_id')
170 170 info = self._request_info['execute'].get(msg_id)
171 171 if info and info.kind == 'prompt':
172 172 number = msg['content']['execution_count'] + 1
173 173 self._show_interpreter_prompt(number)
174 174 self._request_info['execute'].pop(msg_id)
175 175 else:
176 176 super(IPythonWidget, self)._handle_execute_reply(msg)
177 177
178 178 def _handle_history_reply(self, msg):
179 179 """ Implemented to handle history tail replies, which are only supported
180 180 by the IPython kernel.
181 181 """
182 self.log.debug("history: %s", msg.get('content', ''))
183 182 content = msg['content']
184 183 if 'history' not in content:
185 184 self.log.error("History request failed: %r"%content)
186 185 if content.get('status', '') == 'aborted' and \
187 186 not self._retrying_history_request:
188 187 # a *different* action caused this request to be aborted, so
189 188 # we should try again.
190 189 self.log.error("Retrying aborted history request")
191 190 # prevent multiple retries of aborted requests:
192 191 self._retrying_history_request = True
193 192 # wait out the kernel's queue flush, which is currently timed at 0.1s
194 193 time.sleep(0.25)
195 194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
196 195 else:
197 196 self._retrying_history_request = False
198 197 return
199 198 # reset retry flag
200 199 self._retrying_history_request = False
201 200 history_items = content['history']
201 self.log.debug("Received history reply with %i entries", len(history_items))
202 202 items = []
203 203 last_cell = u""
204 204 for _, _, cell in history_items:
205 205 cell = cell.rstrip()
206 206 if cell != last_cell:
207 207 items.append(cell)
208 208 last_cell = cell
209 209 self._set_history(items)
210 210
211 211 def _handle_pyout(self, msg):
212 212 """ Reimplemented for IPython-style "display hook".
213 213 """
214 214 self.log.debug("pyout: %s", msg.get('content', ''))
215 215 if not self._hidden and self._is_from_this_session(msg):
216 216 content = msg['content']
217 217 prompt_number = content['execution_count']
218 218 data = content['data']
219 219 if data.has_key('text/html'):
220 220 self._append_plain_text(self.output_sep, True)
221 221 self._append_html(self._make_out_prompt(prompt_number), True)
222 222 html = data['text/html']
223 223 self._append_plain_text('\n', True)
224 224 self._append_html(html + self.output_sep2, True)
225 225 elif data.has_key('text/plain'):
226 226 self._append_plain_text(self.output_sep, True)
227 227 self._append_html(self._make_out_prompt(prompt_number), True)
228 228 text = data['text/plain']
229 229 # If the repr is multiline, make sure we start on a new line,
230 230 # so that its lines are aligned.
231 231 if "\n" in text and not self.output_sep.endswith("\n"):
232 232 self._append_plain_text('\n', True)
233 233 self._append_plain_text(text + self.output_sep2, True)
234 234
235 235 def _handle_display_data(self, msg):
236 236 """ The base handler for the ``display_data`` message.
237 237 """
238 238 self.log.debug("display: %s", msg.get('content', ''))
239 239 # For now, we don't display data from other frontends, but we
240 240 # eventually will as this allows all frontends to monitor the display
241 241 # data. But we need to figure out how to handle this in the GUI.
242 242 if not self._hidden and self._is_from_this_session(msg):
243 243 source = msg['content']['source']
244 244 data = msg['content']['data']
245 245 metadata = msg['content']['metadata']
246 246 # In the regular IPythonWidget, we simply print the plain text
247 247 # representation.
248 248 if data.has_key('text/html'):
249 249 html = data['text/html']
250 250 self._append_html(html, True)
251 251 elif data.has_key('text/plain'):
252 252 text = data['text/plain']
253 253 self._append_plain_text(text, True)
254 254 # This newline seems to be needed for text and html output.
255 255 self._append_plain_text(u'\n', True)
256 256
257 257 def _started_channels(self):
258 258 """ Reimplemented to make a history request.
259 259 """
260 260 super(IPythonWidget, self)._started_channels()
261 261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
262 262 n=1000)
263 263 #---------------------------------------------------------------------------
264 264 # 'ConsoleWidget' public interface
265 265 #---------------------------------------------------------------------------
266 266
267 267 #---------------------------------------------------------------------------
268 268 # 'FrontendWidget' public interface
269 269 #---------------------------------------------------------------------------
270 270
271 271 def execute_file(self, path, hidden=False):
272 272 """ Reimplemented to use the 'run' magic.
273 273 """
274 274 # Use forward slashes on Windows to avoid escaping each separator.
275 275 if sys.platform == 'win32':
276 276 path = os.path.normpath(path).replace('\\', '/')
277 277
278 278 # Perhaps we should not be using %run directly, but while we
279 279 # are, it is necessary to quote or escape filenames containing spaces
280 280 # or quotes.
281 281
282 282 # In earlier code here, to minimize escaping, we sometimes quoted the
283 283 # filename with single quotes. But to do this, this code must be
284 284 # platform-aware, because run uses shlex rather than python string
285 285 # parsing, so that:
286 286 # * In Win: single quotes can be used in the filename without quoting,
287 287 # and we cannot use single quotes to quote the filename.
288 288 # * In *nix: we can escape double quotes in a double quoted filename,
289 289 # but can't escape single quotes in a single quoted filename.
290 290
291 291 # So to keep this code non-platform-specific and simple, we now only
292 292 # use double quotes to quote filenames, and escape when needed:
293 293 if ' ' in path or "'" in path or '"' in path:
294 294 path = '"%s"' % path.replace('"', '\\"')
295 295 self.execute('%%run %s' % path, hidden=hidden)
296 296
297 297 #---------------------------------------------------------------------------
298 298 # 'FrontendWidget' protected interface
299 299 #---------------------------------------------------------------------------
300 300
301 301 def _complete(self):
302 302 """ Reimplemented to support IPython's improved completion machinery.
303 303 """
304 304 # We let the kernel split the input line, so we *always* send an empty
305 305 # text field. Readline-based frontends do get a real text field which
306 306 # they can use.
307 307 text = ''
308 308
309 309 # Send the completion request to the kernel
310 310 msg_id = self.kernel_manager.shell_channel.complete(
311 311 text, # text
312 312 self._get_input_buffer_cursor_line(), # line
313 313 self._get_input_buffer_cursor_column(), # cursor_pos
314 314 self.input_buffer) # block
315 315 pos = self._get_cursor().position()
316 316 info = self._CompletionRequest(msg_id, pos)
317 317 self._request_info['complete'] = info
318 318
319 319 def _process_execute_error(self, msg):
320 320 """ Reimplemented for IPython-style traceback formatting.
321 321 """
322 322 content = msg['content']
323 323 traceback = '\n'.join(content['traceback']) + '\n'
324 324 if False:
325 325 # FIXME: For now, tracebacks come as plain text, so we can't use
326 326 # the html renderer yet. Once we refactor ultratb to produce
327 327 # properly styled tracebacks, this branch should be the default
328 328 traceback = traceback.replace(' ', '&nbsp;')
329 329 traceback = traceback.replace('\n', '<br/>')
330 330
331 331 ename = content['ename']
332 332 ename_styled = '<span class="error">%s</span>' % ename
333 333 traceback = traceback.replace(ename, ename_styled)
334 334
335 335 self._append_html(traceback)
336 336 else:
337 337 # This is the fallback for now, using plain text with ansi escapes
338 338 self._append_plain_text(traceback)
339 339
340 340 def _process_execute_payload(self, item):
341 341 """ Reimplemented to dispatch payloads to handler methods.
342 342 """
343 343 handler = self._payload_handlers.get(item['source'])
344 344 if handler is None:
345 345 # We have no handler for this type of payload, simply ignore it
346 346 return False
347 347 else:
348 348 handler(item)
349 349 return True
350 350
351 351 def _show_interpreter_prompt(self, number=None):
352 352 """ Reimplemented for IPython-style prompts.
353 353 """
354 354 # If a number was not specified, make a prompt number request.
355 355 if number is None:
356 356 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
357 357 info = self._ExecutionRequest(msg_id, 'prompt')
358 358 self._request_info['execute'][msg_id] = info
359 359 return
360 360
361 361 # Show a new prompt and save information about it so that it can be
362 362 # updated later if the prompt number turns out to be wrong.
363 363 self._prompt_sep = self.input_sep
364 364 self._show_prompt(self._make_in_prompt(number), html=True)
365 365 block = self._control.document().lastBlock()
366 366 length = len(self._prompt)
367 367 self._previous_prompt_obj = self._PromptBlock(block, length, number)
368 368
369 369 # Update continuation prompt to reflect (possibly) new prompt length.
370 370 self._set_continuation_prompt(
371 371 self._make_continuation_prompt(self._prompt), html=True)
372 372
373 373 def _show_interpreter_prompt_for_reply(self, msg):
374 374 """ Reimplemented for IPython-style prompts.
375 375 """
376 376 # Update the old prompt number if necessary.
377 377 content = msg['content']
378 378 # abort replies do not have any keys:
379 379 if content['status'] == 'aborted':
380 380 if self._previous_prompt_obj:
381 381 previous_prompt_number = self._previous_prompt_obj.number
382 382 else:
383 383 previous_prompt_number = 0
384 384 else:
385 385 previous_prompt_number = content['execution_count']
386 386 if self._previous_prompt_obj and \
387 387 self._previous_prompt_obj.number != previous_prompt_number:
388 388 block = self._previous_prompt_obj.block
389 389
390 390 # Make sure the prompt block has not been erased.
391 391 if block.isValid() and block.text():
392 392
393 393 # Remove the old prompt and insert a new prompt.
394 394 cursor = QtGui.QTextCursor(block)
395 395 cursor.movePosition(QtGui.QTextCursor.Right,
396 396 QtGui.QTextCursor.KeepAnchor,
397 397 self._previous_prompt_obj.length)
398 398 prompt = self._make_in_prompt(previous_prompt_number)
399 399 self._prompt = self._insert_html_fetching_plain_text(
400 400 cursor, prompt)
401 401
402 402 # When the HTML is inserted, Qt blows away the syntax
403 403 # highlighting for the line, so we need to rehighlight it.
404 404 self._highlighter.rehighlightBlock(cursor.block())
405 405
406 406 self._previous_prompt_obj = None
407 407
408 408 # Show a new prompt with the kernel's estimated prompt number.
409 409 self._show_interpreter_prompt(previous_prompt_number + 1)
410 410
411 411 #---------------------------------------------------------------------------
412 412 # 'IPythonWidget' interface
413 413 #---------------------------------------------------------------------------
414 414
415 415 def set_default_style(self, colors='lightbg'):
416 416 """ Sets the widget style to the class defaults.
417 417
418 418 Parameters:
419 419 -----------
420 420 colors : str, optional (default lightbg)
421 421 Whether to use the default IPython light background or dark
422 422 background or B&W style.
423 423 """
424 424 colors = colors.lower()
425 425 if colors=='lightbg':
426 426 self.style_sheet = styles.default_light_style_sheet
427 427 self.syntax_style = styles.default_light_syntax_style
428 428 elif colors=='linux':
429 429 self.style_sheet = styles.default_dark_style_sheet
430 430 self.syntax_style = styles.default_dark_syntax_style
431 431 elif colors=='nocolor':
432 432 self.style_sheet = styles.default_bw_style_sheet
433 433 self.syntax_style = styles.default_bw_syntax_style
434 434 else:
435 435 raise KeyError("No such color scheme: %s"%colors)
436 436
437 437 #---------------------------------------------------------------------------
438 438 # 'IPythonWidget' protected interface
439 439 #---------------------------------------------------------------------------
440 440
441 441 def _edit(self, filename, line=None):
442 442 """ Opens a Python script for editing.
443 443
444 444 Parameters:
445 445 -----------
446 446 filename : str
447 447 A path to a local system file.
448 448
449 449 line : int, optional
450 450 A line of interest in the file.
451 451 """
452 452 if self.custom_edit:
453 453 self.custom_edit_requested.emit(filename, line)
454 454 elif not self.editor:
455 455 self._append_plain_text('No default editor available.\n'
456 456 'Specify a GUI text editor in the `IPythonWidget.editor` '
457 457 'configurable to enable the %edit magic')
458 458 else:
459 459 try:
460 460 filename = '"%s"' % filename
461 461 if line and self.editor_line:
462 462 command = self.editor_line.format(filename=filename,
463 463 line=line)
464 464 else:
465 465 try:
466 466 command = self.editor.format()
467 467 except KeyError:
468 468 command = self.editor.format(filename=filename)
469 469 else:
470 470 command += ' ' + filename
471 471 except KeyError:
472 472 self._append_plain_text('Invalid editor command.\n')
473 473 else:
474 474 try:
475 475 Popen(command, shell=True)
476 476 except OSError:
477 477 msg = 'Opening editor with command "%s" failed.\n'
478 478 self._append_plain_text(msg % command)
479 479
480 480 def _make_in_prompt(self, number):
481 481 """ Given a prompt number, returns an HTML In prompt.
482 482 """
483 483 try:
484 484 body = self.in_prompt % number
485 485 except TypeError:
486 486 # allow in_prompt to leave out number, e.g. '>>> '
487 487 body = self.in_prompt
488 488 return '<span class="in-prompt">%s</span>' % body
489 489
490 490 def _make_continuation_prompt(self, prompt):
491 491 """ Given a plain text version of an In prompt, returns an HTML
492 492 continuation prompt.
493 493 """
494 494 end_chars = '...: '
495 495 space_count = len(prompt.lstrip('\n')) - len(end_chars)
496 496 body = '&nbsp;' * space_count + end_chars
497 497 return '<span class="in-prompt">%s</span>' % body
498 498
499 499 def _make_out_prompt(self, number):
500 500 """ Given a prompt number, returns an HTML Out prompt.
501 501 """
502 502 body = self.out_prompt % number
503 503 return '<span class="out-prompt">%s</span>' % body
504 504
505 505 #------ Payload handlers --------------------------------------------------
506 506
507 507 # Payload handlers with a generic interface: each takes the opaque payload
508 508 # dict, unpacks it and calls the underlying functions with the necessary
509 509 # arguments.
510 510
511 511 def _handle_payload_edit(self, item):
512 512 self._edit(item['filename'], item['line_number'])
513 513
514 514 def _handle_payload_exit(self, item):
515 515 self._keep_kernel_on_exit = item['keepkernel']
516 516 self.exit_requested.emit(self)
517 517
518 518 def _handle_payload_next_input(self, item):
519 519 self.input_buffer = dedent(item['text'].rstrip())
520 520
521 521 def _handle_payload_page(self, item):
522 522 # Since the plain text widget supports only a very small subset of HTML
523 523 # and we have no control over the HTML source, we only page HTML
524 524 # payloads in the rich text widget.
525 525 if item['html'] and self.kind == 'rich':
526 526 self._page(item['html'], html=True)
527 527 else:
528 528 self._page(item['text'], html=False)
529 529
530 530 #------ Trait change handlers --------------------------------------------
531 531
532 532 def _style_sheet_changed(self):
533 533 """ Set the style sheets of the underlying widgets.
534 534 """
535 535 self.setStyleSheet(self.style_sheet)
536 536 self._control.document().setDefaultStyleSheet(self.style_sheet)
537 537 if self._page_control:
538 538 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
539 539
540 540 bg_color = self._control.palette().window().color()
541 541 self._ansi_processor.set_background_color(bg_color)
542 542
543 543
544 544 def _syntax_style_changed(self):
545 545 """ Set the style for the syntax highlighter.
546 546 """
547 547 if self._highlighter is None:
548 548 # ignore premature calls
549 549 return
550 550 if self.syntax_style:
551 551 self._highlighter.set_style(self.syntax_style)
552 552 else:
553 553 self._highlighter.set_style_sheet(self.style_sheet)
554 554
555 555 #------ Trait default initializers -----------------------------------------
556 556
557 557 def _banner_default(self):
558 558 from IPython.core.usage import default_gui_banner
559 559 return default_gui_banner
@@ -1,651 +1,652 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 7 call set_parent on all the PUB objects with the message about to be executed.
8 8 * Implement random port and security key logic.
9 9 * Implement control messages.
10 10 * Implement event loop and poll version.
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import print_function
17 17
18 18 # Standard library imports.
19 19 import __builtin__
20 20 import atexit
21 21 import sys
22 22 import time
23 23 import traceback
24 24 import logging
25 25 from signal import (
26 26 signal, default_int_handler, SIGINT, SIG_IGN
27 27 )
28 28 # System library imports.
29 29 import zmq
30 30
31 31 # Local imports.
32 32 from IPython.core import pylabtools
33 33 from IPython.config.configurable import Configurable
34 34 from IPython.config.application import boolean_flag, catch_config_error
35 35 from IPython.core.application import ProfileDir
36 36 from IPython.core.error import StdinNotImplementedError
37 37 from IPython.core.shellapp import (
38 38 InteractiveShellApp, shell_flags, shell_aliases
39 39 )
40 40 from IPython.utils import io
41 41 from IPython.utils import py3compat
42 42 from IPython.utils.jsonutil import json_clean
43 43 from IPython.utils.traitlets import (
44 44 Any, Instance, Float, Dict, CaselessStrEnum
45 45 )
46 46
47 47 from entry_point import base_launch_kernel
48 48 from kernelapp import KernelApp, kernel_flags, kernel_aliases
49 49 from session import Session, Message
50 50 from zmqshell import ZMQInteractiveShell
51 51
52 52
53 53 #-----------------------------------------------------------------------------
54 54 # Main kernel class
55 55 #-----------------------------------------------------------------------------
56 56
57 57 class Kernel(Configurable):
58 58
59 59 #---------------------------------------------------------------------------
60 60 # Kernel interface
61 61 #---------------------------------------------------------------------------
62 62
63 63 # attribute to override with a GUI
64 64 eventloop = Any(None)
65 65
66 66 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
67 67 session = Instance(Session)
68 68 profile_dir = Instance('IPython.core.profiledir.ProfileDir')
69 69 shell_socket = Instance('zmq.Socket')
70 70 iopub_socket = Instance('zmq.Socket')
71 71 stdin_socket = Instance('zmq.Socket')
72 72 log = Instance(logging.Logger)
73 73
74 74 # Private interface
75 75
76 76 # Time to sleep after flushing the stdout/err buffers in each execute
77 77 # cycle. While this introduces a hard limit on the minimal latency of the
78 78 # execute cycle, it helps prevent output synchronization problems for
79 79 # clients.
80 80 # Units are in seconds. The minimum zmq latency on local host is probably
81 81 # ~150 microseconds, set this to 500us for now. We may need to increase it
82 82 # a little if it's not enough after more interactive testing.
83 83 _execute_sleep = Float(0.0005, config=True)
84 84
85 85 # Frequency of the kernel's event loop.
86 86 # Units are in seconds, kernel subclasses for GUI toolkits may need to
87 87 # adapt to milliseconds.
88 88 _poll_interval = Float(0.05, config=True)
89 89
90 90 # If the shutdown was requested over the network, we leave here the
91 91 # necessary reply message so it can be sent by our registered atexit
92 92 # handler. This ensures that the reply is only sent to clients truly at
93 93 # the end of our shutdown process (which happens after the underlying
94 94 # IPython shell's own shutdown).
95 95 _shutdown_message = None
96 96
97 97 # This is a dict of port number that the kernel is listening on. It is set
98 98 # by record_ports and used by connect_request.
99 99 _recorded_ports = Dict()
100 100
101 101
102 102
103 103 def __init__(self, **kwargs):
104 104 super(Kernel, self).__init__(**kwargs)
105 105
106 106 # Before we even start up the shell, register *first* our exit handlers
107 107 # so they come before the shell's
108 108 atexit.register(self._at_shutdown)
109 109
110 110 # Initialize the InteractiveShell subclass
111 111 self.shell = ZMQInteractiveShell.instance(config=self.config,
112 112 profile_dir = self.profile_dir,
113 113 )
114 114 self.shell.displayhook.session = self.session
115 115 self.shell.displayhook.pub_socket = self.iopub_socket
116 116 self.shell.display_pub.session = self.session
117 117 self.shell.display_pub.pub_socket = self.iopub_socket
118 118
119 119 # TMP - hack while developing
120 120 self.shell._reply_content = None
121 121
122 122 # Build dict of handlers for message types
123 123 msg_types = [ 'execute_request', 'complete_request',
124 124 'object_info_request', 'history_request',
125 125 'connect_request', 'shutdown_request']
126 126 self.handlers = {}
127 127 for msg_type in msg_types:
128 128 self.handlers[msg_type] = getattr(self, msg_type)
129 129
130 130 def do_one_iteration(self):
131 131 """Do one iteration of the kernel's evaluation loop.
132 132 """
133 133 try:
134 134 ident,msg = self.session.recv(self.shell_socket, zmq.NOBLOCK)
135 135 except Exception:
136 136 self.log.warn("Invalid Message:", exc_info=True)
137 137 return
138 138 if msg is None:
139 139 return
140 140
141 141 msg_type = msg['header']['msg_type']
142 142
143 143 # This assert will raise in versions of zeromq 2.0.7 and lesser.
144 144 # We now require 2.0.8 or above, so we can uncomment for safety.
145 145 # print(ident,msg, file=sys.__stdout__)
146 146 assert ident is not None, "Missing message part."
147 147
148 148 # Print some info about this message and leave a '--->' marker, so it's
149 149 # easier to trace visually the message chain when debugging. Each
150 150 # handler prints its message at the end.
151 151 self.log.debug('\n*** MESSAGE TYPE:'+str(msg_type)+'***')
152 152 self.log.debug(' Content: '+str(msg['content'])+'\n --->\n ')
153 153
154 154 # Find and call actual handler for message
155 155 handler = self.handlers.get(msg_type, None)
156 156 if handler is None:
157 157 self.log.error("UNKNOWN MESSAGE TYPE:" +str(msg))
158 158 else:
159 159 handler(ident, msg)
160 160
161 161 # Check whether we should exit, in case the incoming message set the
162 162 # exit flag on
163 163 if self.shell.exit_now:
164 164 self.log.debug('\nExiting IPython kernel...')
165 165 # We do a normal, clean exit, which allows any actions registered
166 166 # via atexit (such as history saving) to take place.
167 167 sys.exit(0)
168 168
169 169
170 170 def start(self):
171 171 """ Start the kernel main loop.
172 172 """
173 173 # a KeyboardInterrupt (SIGINT) can occur on any python statement, so
174 174 # let's ignore (SIG_IGN) them until we're in a place to handle them properly
175 175 signal(SIGINT,SIG_IGN)
176 176 poller = zmq.Poller()
177 177 poller.register(self.shell_socket, zmq.POLLIN)
178 178 # loop while self.eventloop has not been overridden
179 179 while self.eventloop is None:
180 180 try:
181 181 # scale by extra factor of 10, because there is no
182 182 # reason for this to be anything less than ~ 0.1s
183 183 # since it is a real poller and will respond
184 184 # to events immediately
185 185
186 186 # double nested try/except, to properly catch KeyboardInterrupt
187 187 # due to pyzmq Issue #130
188 188 try:
189 189 poller.poll(10*1000*self._poll_interval)
190 190 # restore raising of KeyboardInterrupt
191 191 signal(SIGINT, default_int_handler)
192 192 self.do_one_iteration()
193 193 except:
194 194 raise
195 195 finally:
196 196 # prevent raising of KeyboardInterrupt
197 197 signal(SIGINT,SIG_IGN)
198 198 except KeyboardInterrupt:
199 199 # Ctrl-C shouldn't crash the kernel
200 200 io.raw_print("KeyboardInterrupt caught in kernel")
201 201 # stop ignoring sigint, now that we are out of our own loop,
202 202 # we don't want to prevent future code from handling it
203 203 signal(SIGINT, default_int_handler)
204 204 if self.eventloop is not None:
205 205 try:
206 206 self.eventloop(self)
207 207 except KeyboardInterrupt:
208 208 # Ctrl-C shouldn't crash the kernel
209 209 io.raw_print("KeyboardInterrupt caught in kernel")
210 210
211 211
212 212 def record_ports(self, ports):
213 213 """Record the ports that this kernel is using.
214 214
215 215 The creator of the Kernel instance must call this methods if they
216 216 want the :meth:`connect_request` method to return the port numbers.
217 217 """
218 218 self._recorded_ports = ports
219 219
220 220 #---------------------------------------------------------------------------
221 221 # Kernel request handlers
222 222 #---------------------------------------------------------------------------
223 223
224 224 def _publish_pyin(self, code, parent):
225 225 """Publish the code request on the pyin stream."""
226 226
227 227 self.session.send(self.iopub_socket, u'pyin', {u'code':code},
228 228 parent=parent)
229 229
230 230 def execute_request(self, ident, parent):
231 231
232 232 self.session.send(self.iopub_socket,
233 233 u'status',
234 234 {u'execution_state':u'busy'},
235 235 parent=parent )
236 236
237 237 try:
238 238 content = parent[u'content']
239 239 code = content[u'code']
240 240 silent = content[u'silent']
241 241 except:
242 242 self.log.error("Got bad msg: ")
243 243 self.log.error(str(Message(parent)))
244 244 return
245 245
246 246 shell = self.shell # we'll need this a lot here
247 247
248 248 # Replace raw_input. Note that is not sufficient to replace
249 249 # raw_input in the user namespace.
250 250 if content.get('allow_stdin', False):
251 251 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
252 252 else:
253 253 raw_input = lambda prompt='' : self._no_raw_input()
254 254
255 255 if py3compat.PY3:
256 256 __builtin__.input = raw_input
257 257 else:
258 258 __builtin__.raw_input = raw_input
259 259
260 260 # Set the parent message of the display hook and out streams.
261 261 shell.displayhook.set_parent(parent)
262 262 shell.display_pub.set_parent(parent)
263 263 sys.stdout.set_parent(parent)
264 264 sys.stderr.set_parent(parent)
265 265
266 266 # Re-broadcast our input for the benefit of listening clients, and
267 267 # start computing output
268 268 if not silent:
269 269 self._publish_pyin(code, parent)
270 270
271 271 reply_content = {}
272 272 try:
273 273 if silent:
274 274 # run_code uses 'exec' mode, so no displayhook will fire, and it
275 275 # doesn't call logging or history manipulations. Print
276 276 # statements in that code will obviously still execute.
277 277 shell.run_code(code)
278 278 else:
279 279 # FIXME: the shell calls the exception handler itself.
280 280 shell.run_cell(code, store_history=True)
281 281 except:
282 282 status = u'error'
283 283 # FIXME: this code right now isn't being used yet by default,
284 284 # because the run_cell() call above directly fires off exception
285 285 # reporting. This code, therefore, is only active in the scenario
286 286 # where runlines itself has an unhandled exception. We need to
287 287 # uniformize this, for all exception construction to come from a
288 288 # single location in the codbase.
289 289 etype, evalue, tb = sys.exc_info()
290 290 tb_list = traceback.format_exception(etype, evalue, tb)
291 291 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
292 292 else:
293 293 status = u'ok'
294 294
295 295 reply_content[u'status'] = status
296 296
297 297 # Return the execution counter so clients can display prompts
298 298 reply_content['execution_count'] = shell.execution_count -1
299 299
300 300 # FIXME - fish exception info out of shell, possibly left there by
301 301 # runlines. We'll need to clean up this logic later.
302 302 if shell._reply_content is not None:
303 303 reply_content.update(shell._reply_content)
304 304 # reset after use
305 305 shell._reply_content = None
306 306
307 307 # At this point, we can tell whether the main code execution succeeded
308 308 # or not. If it did, we proceed to evaluate user_variables/expressions
309 309 if reply_content['status'] == 'ok':
310 310 reply_content[u'user_variables'] = \
311 311 shell.user_variables(content[u'user_variables'])
312 312 reply_content[u'user_expressions'] = \
313 313 shell.user_expressions(content[u'user_expressions'])
314 314 else:
315 315 # If there was an error, don't even try to compute variables or
316 316 # expressions
317 317 reply_content[u'user_variables'] = {}
318 318 reply_content[u'user_expressions'] = {}
319 319
320 320 # Payloads should be retrieved regardless of outcome, so we can both
321 321 # recover partial output (that could have been generated early in a
322 322 # block, before an error) and clear the payload system always.
323 323 reply_content[u'payload'] = shell.payload_manager.read_payload()
324 324 # Be agressive about clearing the payload because we don't want
325 325 # it to sit in memory until the next execute_request comes in.
326 326 shell.payload_manager.clear_payload()
327 327
328 328 # Flush output before sending the reply.
329 329 sys.stdout.flush()
330 330 sys.stderr.flush()
331 331 # FIXME: on rare occasions, the flush doesn't seem to make it to the
332 332 # clients... This seems to mitigate the problem, but we definitely need
333 333 # to better understand what's going on.
334 334 if self._execute_sleep:
335 335 time.sleep(self._execute_sleep)
336 336
337 337 # Send the reply.
338 338 reply_content = json_clean(reply_content)
339 339 reply_msg = self.session.send(self.shell_socket, u'execute_reply',
340 340 reply_content, parent, ident=ident)
341 341 self.log.debug(str(reply_msg))
342 342
343 343 if reply_msg['content']['status'] == u'error':
344 344 self._abort_queue()
345 345
346 346 self.session.send(self.iopub_socket,
347 347 u'status',
348 348 {u'execution_state':u'idle'},
349 349 parent=parent )
350 350
351 351 def complete_request(self, ident, parent):
352 352 txt, matches = self._complete(parent)
353 353 matches = {'matches' : matches,
354 354 'matched_text' : txt,
355 355 'status' : 'ok'}
356 356 matches = json_clean(matches)
357 357 completion_msg = self.session.send(self.shell_socket, 'complete_reply',
358 358 matches, parent, ident)
359 359 self.log.debug(str(completion_msg))
360 360
361 361 def object_info_request(self, ident, parent):
362 362 object_info = self.shell.object_inspect(parent['content']['oname'])
363 363 # Before we send this object over, we scrub it for JSON usage
364 364 oinfo = json_clean(object_info)
365 365 msg = self.session.send(self.shell_socket, 'object_info_reply',
366 366 oinfo, parent, ident)
367 367 self.log.debug(msg)
368 368
369 369 def history_request(self, ident, parent):
370 370 # We need to pull these out, as passing **kwargs doesn't work with
371 371 # unicode keys before Python 2.6.5.
372 372 hist_access_type = parent['content']['hist_access_type']
373 373 raw = parent['content']['raw']
374 374 output = parent['content']['output']
375 375 if hist_access_type == 'tail':
376 376 n = parent['content']['n']
377 377 hist = self.shell.history_manager.get_tail(n, raw=raw, output=output,
378 378 include_latest=True)
379 379
380 380 elif hist_access_type == 'range':
381 381 session = parent['content']['session']
382 382 start = parent['content']['start']
383 383 stop = parent['content']['stop']
384 384 hist = self.shell.history_manager.get_range(session, start, stop,
385 385 raw=raw, output=output)
386 386
387 387 elif hist_access_type == 'search':
388 388 pattern = parent['content']['pattern']
389 389 hist = self.shell.history_manager.search(pattern, raw=raw,
390 390 output=output)
391 391
392 392 else:
393 393 hist = []
394 content = {'history' : list(hist)}
394 hist = list(hist)
395 content = {'history' : hist}
395 396 content = json_clean(content)
396 397 msg = self.session.send(self.shell_socket, 'history_reply',
397 398 content, parent, ident)
398 self.log.debug(str(msg))
399 self.log.debug("Sending history reply with %i entries", len(hist))
399 400
400 401 def connect_request(self, ident, parent):
401 402 if self._recorded_ports is not None:
402 403 content = self._recorded_ports.copy()
403 404 else:
404 405 content = {}
405 406 msg = self.session.send(self.shell_socket, 'connect_reply',
406 407 content, parent, ident)
407 408 self.log.debug(msg)
408 409
409 410 def shutdown_request(self, ident, parent):
410 411 self.shell.exit_now = True
411 412 self._shutdown_message = self.session.msg(u'shutdown_reply',
412 413 parent['content'], parent)
413 414 sys.exit(0)
414 415
415 416 #---------------------------------------------------------------------------
416 417 # Protected interface
417 418 #---------------------------------------------------------------------------
418 419
419 420 def _abort_queue(self):
420 421 while True:
421 422 try:
422 423 ident,msg = self.session.recv(self.shell_socket, zmq.NOBLOCK)
423 424 except Exception:
424 425 self.log.warn("Invalid Message:", exc_info=True)
425 426 continue
426 427 if msg is None:
427 428 break
428 429 else:
429 430 assert ident is not None, \
430 431 "Unexpected missing message part."
431 432
432 433 self.log.debug("Aborting:\n"+str(Message(msg)))
433 434 msg_type = msg['header']['msg_type']
434 435 reply_type = msg_type.split('_')[0] + '_reply'
435 436 reply_msg = self.session.send(self.shell_socket, reply_type,
436 437 {'status' : 'aborted'}, msg, ident=ident)
437 438 self.log.debug(reply_msg)
438 439 # We need to wait a bit for requests to come in. This can probably
439 440 # be set shorter for true asynchronous clients.
440 441 time.sleep(0.1)
441 442
442 443 def _no_raw_input(self):
443 444 """Raise StdinNotImplentedError if active frontend doesn't support
444 445 stdin."""
445 446 raise StdinNotImplementedError("raw_input was called, but this "
446 447 "frontend does not support stdin.")
447 448
448 449 def _raw_input(self, prompt, ident, parent):
449 450 # Flush output before making the request.
450 451 sys.stderr.flush()
451 452 sys.stdout.flush()
452 453
453 454 # Send the input request.
454 455 content = json_clean(dict(prompt=prompt))
455 456 self.session.send(self.stdin_socket, u'input_request', content, parent,
456 457 ident=ident)
457 458
458 459 # Await a response.
459 460 while True:
460 461 try:
461 462 ident, reply = self.session.recv(self.stdin_socket, 0)
462 463 except Exception:
463 464 self.log.warn("Invalid Message:", exc_info=True)
464 465 else:
465 466 break
466 467 try:
467 468 value = reply['content']['value']
468 469 except:
469 470 self.log.error("Got bad raw_input reply: ")
470 471 self.log.error(str(Message(parent)))
471 472 value = ''
472 473 if value == '\x04':
473 474 # EOF
474 475 raise EOFError
475 476 return value
476 477
477 478 def _complete(self, msg):
478 479 c = msg['content']
479 480 try:
480 481 cpos = int(c['cursor_pos'])
481 482 except:
482 483 # If we don't get something that we can convert to an integer, at
483 484 # least attempt the completion guessing the cursor is at the end of
484 485 # the text, if there's any, and otherwise of the line
485 486 cpos = len(c['text'])
486 487 if cpos==0:
487 488 cpos = len(c['line'])
488 489 return self.shell.complete(c['text'], c['line'], cpos)
489 490
490 491 def _object_info(self, context):
491 492 symbol, leftover = self._symbol_from_context(context)
492 493 if symbol is not None and not leftover:
493 494 doc = getattr(symbol, '__doc__', '')
494 495 else:
495 496 doc = ''
496 497 object_info = dict(docstring = doc)
497 498 return object_info
498 499
499 500 def _symbol_from_context(self, context):
500 501 if not context:
501 502 return None, context
502 503
503 504 base_symbol_string = context[0]
504 505 symbol = self.shell.user_ns.get(base_symbol_string, None)
505 506 if symbol is None:
506 507 symbol = __builtin__.__dict__.get(base_symbol_string, None)
507 508 if symbol is None:
508 509 return None, context
509 510
510 511 context = context[1:]
511 512 for i, name in enumerate(context):
512 513 new_symbol = getattr(symbol, name, None)
513 514 if new_symbol is None:
514 515 return symbol, context[i:]
515 516 else:
516 517 symbol = new_symbol
517 518
518 519 return symbol, []
519 520
520 521 def _at_shutdown(self):
521 522 """Actions taken at shutdown by the kernel, called by python's atexit.
522 523 """
523 524 # io.rprint("Kernel at_shutdown") # dbg
524 525 if self._shutdown_message is not None:
525 526 self.session.send(self.shell_socket, self._shutdown_message)
526 527 self.session.send(self.iopub_socket, self._shutdown_message)
527 528 self.log.debug(str(self._shutdown_message))
528 529 # A very short sleep to give zmq time to flush its message buffers
529 530 # before Python truly shuts down.
530 531 time.sleep(0.01)
531 532
532 533 #-----------------------------------------------------------------------------
533 534 # Aliases and Flags for the IPKernelApp
534 535 #-----------------------------------------------------------------------------
535 536
536 537 flags = dict(kernel_flags)
537 538 flags.update(shell_flags)
538 539
539 540 addflag = lambda *args: flags.update(boolean_flag(*args))
540 541
541 542 flags['pylab'] = (
542 543 {'IPKernelApp' : {'pylab' : 'auto'}},
543 544 """Pre-load matplotlib and numpy for interactive use with
544 545 the default matplotlib backend."""
545 546 )
546 547
547 548 aliases = dict(kernel_aliases)
548 549 aliases.update(shell_aliases)
549 550
550 551 # it's possible we don't want short aliases for *all* of these:
551 552 aliases.update(dict(
552 553 pylab='IPKernelApp.pylab',
553 554 ))
554 555
555 556 #-----------------------------------------------------------------------------
556 557 # The IPKernelApp class
557 558 #-----------------------------------------------------------------------------
558 559
559 560 class IPKernelApp(KernelApp, InteractiveShellApp):
560 561 name = 'ipkernel'
561 562
562 563 aliases = Dict(aliases)
563 564 flags = Dict(flags)
564 565 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
565 566 # configurables
566 567 pylab = CaselessStrEnum(['tk', 'qt', 'wx', 'gtk', 'osx', 'inline', 'auto'],
567 568 config=True,
568 569 help="""Pre-load matplotlib and numpy for interactive use,
569 570 selecting a particular matplotlib backend and loop integration.
570 571 """
571 572 )
572 573
573 574 @catch_config_error
574 575 def initialize(self, argv=None):
575 576 super(IPKernelApp, self).initialize(argv)
576 577 self.init_shell()
577 578 self.init_extensions()
578 579 self.init_code()
579 580
580 581 def init_kernel(self):
581 582
582 583 kernel = Kernel(config=self.config, session=self.session,
583 584 shell_socket=self.shell_socket,
584 585 iopub_socket=self.iopub_socket,
585 586 stdin_socket=self.stdin_socket,
586 587 log=self.log,
587 588 profile_dir=self.profile_dir,
588 589 )
589 590 self.kernel = kernel
590 591 kernel.record_ports(self.ports)
591 592 shell = kernel.shell
592 593 if self.pylab:
593 594 try:
594 595 gui, backend = pylabtools.find_gui_and_backend(self.pylab)
595 596 shell.enable_pylab(gui, import_all=self.pylab_import_all)
596 597 except Exception:
597 598 self.log.error("Pylab initialization failed", exc_info=True)
598 599 # print exception straight to stdout, because normally
599 600 # _showtraceback associates the reply with an execution,
600 601 # which means frontends will never draw it, as this exception
601 602 # is not associated with any execute request.
602 603
603 604 # replace pyerr-sending traceback with stdout
604 605 _showtraceback = shell._showtraceback
605 606 def print_tb(etype, evalue, stb):
606 607 print ("Error initializing pylab, pylab mode will not "
607 608 "be active", file=io.stderr)
608 609 print (shell.InteractiveTB.stb2text(stb), file=io.stdout)
609 610 shell._showtraceback = print_tb
610 611
611 612 # send the traceback over stdout
612 613 shell.showtraceback(tb_offset=0)
613 614
614 615 # restore proper _showtraceback method
615 616 shell._showtraceback = _showtraceback
616 617
617 618
618 619 def init_shell(self):
619 620 self.shell = self.kernel.shell
620 621 self.shell.configurables.append(self)
621 622
622 623
623 624 #-----------------------------------------------------------------------------
624 625 # Kernel main and launch functions
625 626 #-----------------------------------------------------------------------------
626 627
627 628 def launch_kernel(*args, **kwargs):
628 629 """Launches a localhost IPython kernel, binding to the specified ports.
629 630
630 631 This function simply calls entry_point.base_launch_kernel with the right
631 632 first command to start an ipkernel. See base_launch_kernel for arguments.
632 633
633 634 Returns
634 635 -------
635 636 A tuple of form:
636 637 (kernel_process, shell_port, iopub_port, stdin_port, hb_port)
637 638 where kernel_process is a Popen object and the ports are integers.
638 639 """
639 640 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
640 641 *args, **kwargs)
641 642
642 643
643 644 def main():
644 645 """Run an IPKernel as an application"""
645 646 app = IPKernelApp.instance()
646 647 app.initialize()
647 648 app.start()
648 649
649 650
650 651 if __name__ == '__main__':
651 652 main()
General Comments 0
You need to be logged in to leave comments. Login now