##// END OF EJS Templates
be conservative about kernel_info implementation...
MinRK -
Show More
@@ -1,598 +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 #-----------------------------------------------------------------------------
7 7 # Imports
8 8 #-----------------------------------------------------------------------------
9 9
10 10 # Standard library imports
11 11 from collections import namedtuple
12 12 import os.path
13 13 import re
14 14 from subprocess import Popen
15 15 import sys
16 16 import time
17 17 from textwrap import dedent
18 18
19 19 # System library imports
20 20 from IPython.external.qt import QtCore, QtGui
21 21
22 22 # Local imports
23 23 from IPython.core.inputsplitter import IPythonInputSplitter
24 24 from IPython.core.inputtransformer import ipy_prompt
25 25 from IPython.utils.traitlets import Bool, Unicode
26 26 from .frontend_widget import FrontendWidget
27 27 from . import styles
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Constants
31 31 #-----------------------------------------------------------------------------
32 32
33 33 # Default strings to build and display input and output prompts (and separators
34 34 # in between)
35 35 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
36 36 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
37 37 default_input_sep = '\n'
38 38 default_output_sep = ''
39 39 default_output_sep2 = ''
40 40
41 41 # Base path for most payload sources.
42 42 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
43 43
44 44 if sys.platform.startswith('win'):
45 45 default_editor = 'notepad'
46 46 else:
47 47 default_editor = ''
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # IPythonWidget class
51 51 #-----------------------------------------------------------------------------
52 52
53 53 class IPythonWidget(FrontendWidget):
54 54 """ A FrontendWidget for an IPython kernel.
55 55 """
56 56
57 57 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
58 58 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
59 59 # settings.
60 60 custom_edit = Bool(False)
61 61 custom_edit_requested = QtCore.Signal(object, object)
62 62
63 63 editor = Unicode(default_editor, config=True,
64 64 help="""
65 65 A command for invoking a system text editor. If the string contains a
66 66 {filename} format specifier, it will be used. Otherwise, the filename
67 67 will be appended to the end the command.
68 68 """)
69 69
70 70 editor_line = Unicode(config=True,
71 71 help="""
72 72 The editor command to use when a specific line number is requested. The
73 73 string should contain two format specifiers: {line} and {filename}. If
74 74 this parameter is not specified, the line number option to the %edit
75 75 magic will be ignored.
76 76 """)
77 77
78 78 style_sheet = Unicode(config=True,
79 79 help="""
80 80 A CSS stylesheet. The stylesheet can contain classes for:
81 81 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
82 82 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
83 83 3. IPython: .error, .in-prompt, .out-prompt, etc
84 84 """)
85 85
86 86 syntax_style = Unicode(config=True,
87 87 help="""
88 88 If not empty, use this Pygments style for syntax highlighting.
89 89 Otherwise, the style sheet is queried for Pygments style
90 90 information.
91 91 """)
92 92
93 93 # Prompts.
94 94 in_prompt = Unicode(default_in_prompt, config=True)
95 95 out_prompt = Unicode(default_out_prompt, config=True)
96 96 input_sep = Unicode(default_input_sep, config=True)
97 97 output_sep = Unicode(default_output_sep, config=True)
98 98 output_sep2 = Unicode(default_output_sep2, config=True)
99 99
100 100 # FrontendWidget protected class variables.
101 101 _input_splitter_class = IPythonInputSplitter
102 102 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
103 103 logical_line_transforms=[],
104 104 python_line_transforms=[],
105 105 )
106 106
107 107 # IPythonWidget protected class variables.
108 108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
109 109 _payload_source_edit = 'edit_magic'
110 110 _payload_source_exit = 'ask_exit'
111 111 _payload_source_next_input = 'set_next_input'
112 112 _payload_source_page = 'page'
113 113 _retrying_history_request = False
114 114
115 115 #---------------------------------------------------------------------------
116 116 # 'object' interface
117 117 #---------------------------------------------------------------------------
118 118
119 119 def __init__(self, *args, **kw):
120 120 super(IPythonWidget, self).__init__(*args, **kw)
121 121
122 122 # IPythonWidget protected variables.
123 123 self._payload_handlers = {
124 124 self._payload_source_edit : self._handle_payload_edit,
125 125 self._payload_source_exit : self._handle_payload_exit,
126 126 self._payload_source_page : self._handle_payload_page,
127 127 self._payload_source_next_input : self._handle_payload_next_input }
128 128 self._previous_prompt_obj = None
129 129 self._keep_kernel_on_exit = None
130 130
131 131 # Initialize widget styling.
132 132 if self.style_sheet:
133 133 self._style_sheet_changed()
134 134 self._syntax_style_changed()
135 135 else:
136 136 self.set_default_style()
137 137
138 138 self._guiref_loaded = False
139 139
140 140 #---------------------------------------------------------------------------
141 141 # 'BaseFrontendMixin' abstract interface
142 142 #---------------------------------------------------------------------------
143 143
144 144 def _handle_complete_reply(self, rep):
145 145 """ Reimplemented to support IPython's improved completion machinery.
146 146 """
147 147 self.log.debug("complete: %s", rep.get('content', ''))
148 148 cursor = self._get_cursor()
149 149 info = self._request_info.get('complete')
150 150 if info and info.id == rep['parent_header']['msg_id'] and \
151 151 info.pos == cursor.position():
152 152 matches = rep['content']['matches']
153 153 text = rep['content']['matched_text']
154 154 offset = len(text)
155 155
156 156 # Clean up matches with period and path separators if the matched
157 157 # text has not been transformed. This is done by truncating all
158 158 # but the last component and then suitably decreasing the offset
159 159 # between the current cursor position and the start of completion.
160 160 if len(matches) > 1 and matches[0][:offset] == text:
161 161 parts = re.split(r'[./\\]', text)
162 162 sep_count = len(parts) - 1
163 163 if sep_count:
164 164 chop_length = sum(map(len, parts[:sep_count])) + sep_count
165 165 matches = [ match[chop_length:] for match in matches ]
166 166 offset -= chop_length
167 167
168 168 # Move the cursor to the start of the match and complete.
169 169 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
170 170 self._complete_with_items(cursor, matches)
171 171
172 172 def _handle_execute_reply(self, msg):
173 173 """ Reimplemented to support prompt requests.
174 174 """
175 175 msg_id = msg['parent_header'].get('msg_id')
176 176 info = self._request_info['execute'].get(msg_id)
177 177 if info and info.kind == 'prompt':
178 178 number = msg['content']['execution_count'] + 1
179 179 self._show_interpreter_prompt(number)
180 180 self._request_info['execute'].pop(msg_id)
181 181 else:
182 182 super(IPythonWidget, self)._handle_execute_reply(msg)
183 183
184 184 def _handle_history_reply(self, msg):
185 185 """ Implemented to handle history tail replies, which are only supported
186 186 by the IPython kernel.
187 187 """
188 188 content = msg['content']
189 189 if 'history' not in content:
190 190 self.log.error("History request failed: %r"%content)
191 191 if content.get('status', '') == 'aborted' and \
192 192 not self._retrying_history_request:
193 193 # a *different* action caused this request to be aborted, so
194 194 # we should try again.
195 195 self.log.error("Retrying aborted history request")
196 196 # prevent multiple retries of aborted requests:
197 197 self._retrying_history_request = True
198 198 # wait out the kernel's queue flush, which is currently timed at 0.1s
199 199 time.sleep(0.25)
200 200 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
201 201 else:
202 202 self._retrying_history_request = False
203 203 return
204 204 # reset retry flag
205 205 self._retrying_history_request = False
206 206 history_items = content['history']
207 207 self.log.debug("Received history reply with %i entries", len(history_items))
208 208 items = []
209 209 last_cell = u""
210 210 for _, _, cell in history_items:
211 211 cell = cell.rstrip()
212 212 if cell != last_cell:
213 213 items.append(cell)
214 214 last_cell = cell
215 215 self._set_history(items)
216 216
217 217 def _handle_pyout(self, msg):
218 218 """ Reimplemented for IPython-style "display hook".
219 219 """
220 220 self.log.debug("pyout: %s", msg.get('content', ''))
221 221 if not self._hidden and self._is_from_this_session(msg):
222 222 content = msg['content']
223 223 prompt_number = content.get('execution_count', 0)
224 224 data = content['data']
225 225 if 'text/html' in data:
226 226 self._append_plain_text(self.output_sep, True)
227 227 self._append_html(self._make_out_prompt(prompt_number), True)
228 228 html = data['text/html']
229 229 self._append_plain_text('\n', True)
230 230 self._append_html(html + self.output_sep2, True)
231 231 elif 'text/plain' in data:
232 232 self._append_plain_text(self.output_sep, True)
233 233 self._append_html(self._make_out_prompt(prompt_number), True)
234 234 text = data['text/plain']
235 235 # If the repr is multiline, make sure we start on a new line,
236 236 # so that its lines are aligned.
237 237 if "\n" in text and not self.output_sep.endswith("\n"):
238 238 self._append_plain_text('\n', True)
239 239 self._append_plain_text(text + self.output_sep2, True)
240 240
241 241 def _handle_display_data(self, msg):
242 242 """ The base handler for the ``display_data`` message.
243 243 """
244 244 self.log.debug("display: %s", msg.get('content', ''))
245 245 # For now, we don't display data from other frontends, but we
246 246 # eventually will as this allows all frontends to monitor the display
247 247 # data. But we need to figure out how to handle this in the GUI.
248 248 if not self._hidden and self._is_from_this_session(msg):
249 249 source = msg['content']['source']
250 250 data = msg['content']['data']
251 251 metadata = msg['content']['metadata']
252 252 # In the regular IPythonWidget, we simply print the plain text
253 253 # representation.
254 254 if 'text/html' in data:
255 255 html = data['text/html']
256 256 self._append_html(html, True)
257 257 elif 'text/plain' in data:
258 258 text = data['text/plain']
259 259 self._append_plain_text(text, True)
260 260 # This newline seems to be needed for text and html output.
261 261 self._append_plain_text(u'\n', True)
262 262
263 263 def _handle_kernel_info_reply(self, rep):
264 264 """ Handle kernel info replies.
265 265 """
266 266 if not self._guiref_loaded:
267 if rep['content']['language'] == 'python':
267 if rep['content'].get('language') == 'python':
268 268 self._load_guiref_magic()
269 269 self._guiref_loaded = True
270 270
271 271 def _started_channels(self):
272 272 """Reimplemented to make a history request and load %guiref."""
273 273 super(IPythonWidget, self)._started_channels()
274 274
275 275 # The reply will trigger %guiref load provided language=='python'
276 276 self.kernel_client.kernel_info()
277 277
278 278 self.kernel_client.shell_channel.history(hist_access_type='tail',
279 279 n=1000)
280 280
281 281 def _started_kernel(self):
282 282 """Load %guiref when the kernel starts (if channels are also started).
283 283
284 284 Principally triggered by kernel restart.
285 285 """
286 286 if self.kernel_client.shell_channel is not None:
287 287 self._load_guiref_magic()
288 288
289 289 def _load_guiref_magic(self):
290 290 """Load %guiref magic."""
291 291 self.kernel_client.shell_channel.execute('\n'.join([
292 292 "try:",
293 293 " _usage",
294 294 "except:",
295 295 " from IPython.core import usage as _usage",
296 296 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
297 297 " del _usage",
298 298 ]), silent=True)
299 299
300 300 #---------------------------------------------------------------------------
301 301 # 'ConsoleWidget' public interface
302 302 #---------------------------------------------------------------------------
303 303
304 304 #---------------------------------------------------------------------------
305 305 # 'FrontendWidget' public interface
306 306 #---------------------------------------------------------------------------
307 307
308 308 def execute_file(self, path, hidden=False):
309 309 """ Reimplemented to use the 'run' magic.
310 310 """
311 311 # Use forward slashes on Windows to avoid escaping each separator.
312 312 if sys.platform == 'win32':
313 313 path = os.path.normpath(path).replace('\\', '/')
314 314
315 315 # Perhaps we should not be using %run directly, but while we
316 316 # are, it is necessary to quote or escape filenames containing spaces
317 317 # or quotes.
318 318
319 319 # In earlier code here, to minimize escaping, we sometimes quoted the
320 320 # filename with single quotes. But to do this, this code must be
321 321 # platform-aware, because run uses shlex rather than python string
322 322 # parsing, so that:
323 323 # * In Win: single quotes can be used in the filename without quoting,
324 324 # and we cannot use single quotes to quote the filename.
325 325 # * In *nix: we can escape double quotes in a double quoted filename,
326 326 # but can't escape single quotes in a single quoted filename.
327 327
328 328 # So to keep this code non-platform-specific and simple, we now only
329 329 # use double quotes to quote filenames, and escape when needed:
330 330 if ' ' in path or "'" in path or '"' in path:
331 331 path = '"%s"' % path.replace('"', '\\"')
332 332 self.execute('%%run %s' % path, hidden=hidden)
333 333
334 334 #---------------------------------------------------------------------------
335 335 # 'FrontendWidget' protected interface
336 336 #---------------------------------------------------------------------------
337 337
338 338 def _complete(self):
339 339 """ Reimplemented to support IPython's improved completion machinery.
340 340 """
341 341 # We let the kernel split the input line, so we *always* send an empty
342 342 # text field. Readline-based frontends do get a real text field which
343 343 # they can use.
344 344 text = ''
345 345
346 346 # Send the completion request to the kernel
347 347 msg_id = self.kernel_client.shell_channel.complete(
348 348 text, # text
349 349 self._get_input_buffer_cursor_line(), # line
350 350 self._get_input_buffer_cursor_column(), # cursor_pos
351 351 self.input_buffer) # block
352 352 pos = self._get_cursor().position()
353 353 info = self._CompletionRequest(msg_id, pos)
354 354 self._request_info['complete'] = info
355 355
356 356 def _process_execute_error(self, msg):
357 357 """ Reimplemented for IPython-style traceback formatting.
358 358 """
359 359 content = msg['content']
360 360 traceback = '\n'.join(content['traceback']) + '\n'
361 361 if False:
362 362 # FIXME: For now, tracebacks come as plain text, so we can't use
363 363 # the html renderer yet. Once we refactor ultratb to produce
364 364 # properly styled tracebacks, this branch should be the default
365 365 traceback = traceback.replace(' ', '&nbsp;')
366 366 traceback = traceback.replace('\n', '<br/>')
367 367
368 368 ename = content['ename']
369 369 ename_styled = '<span class="error">%s</span>' % ename
370 370 traceback = traceback.replace(ename, ename_styled)
371 371
372 372 self._append_html(traceback)
373 373 else:
374 374 # This is the fallback for now, using plain text with ansi escapes
375 375 self._append_plain_text(traceback)
376 376
377 377 def _process_execute_payload(self, item):
378 378 """ Reimplemented to dispatch payloads to handler methods.
379 379 """
380 380 handler = self._payload_handlers.get(item['source'])
381 381 if handler is None:
382 382 # We have no handler for this type of payload, simply ignore it
383 383 return False
384 384 else:
385 385 handler(item)
386 386 return True
387 387
388 388 def _show_interpreter_prompt(self, number=None):
389 389 """ Reimplemented for IPython-style prompts.
390 390 """
391 391 # If a number was not specified, make a prompt number request.
392 392 if number is None:
393 393 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
394 394 info = self._ExecutionRequest(msg_id, 'prompt')
395 395 self._request_info['execute'][msg_id] = info
396 396 return
397 397
398 398 # Show a new prompt and save information about it so that it can be
399 399 # updated later if the prompt number turns out to be wrong.
400 400 self._prompt_sep = self.input_sep
401 401 self._show_prompt(self._make_in_prompt(number), html=True)
402 402 block = self._control.document().lastBlock()
403 403 length = len(self._prompt)
404 404 self._previous_prompt_obj = self._PromptBlock(block, length, number)
405 405
406 406 # Update continuation prompt to reflect (possibly) new prompt length.
407 407 self._set_continuation_prompt(
408 408 self._make_continuation_prompt(self._prompt), html=True)
409 409
410 410 def _show_interpreter_prompt_for_reply(self, msg):
411 411 """ Reimplemented for IPython-style prompts.
412 412 """
413 413 # Update the old prompt number if necessary.
414 414 content = msg['content']
415 415 # abort replies do not have any keys:
416 416 if content['status'] == 'aborted':
417 417 if self._previous_prompt_obj:
418 418 previous_prompt_number = self._previous_prompt_obj.number
419 419 else:
420 420 previous_prompt_number = 0
421 421 else:
422 422 previous_prompt_number = content['execution_count']
423 423 if self._previous_prompt_obj and \
424 424 self._previous_prompt_obj.number != previous_prompt_number:
425 425 block = self._previous_prompt_obj.block
426 426
427 427 # Make sure the prompt block has not been erased.
428 428 if block.isValid() and block.text():
429 429
430 430 # Remove the old prompt and insert a new prompt.
431 431 cursor = QtGui.QTextCursor(block)
432 432 cursor.movePosition(QtGui.QTextCursor.Right,
433 433 QtGui.QTextCursor.KeepAnchor,
434 434 self._previous_prompt_obj.length)
435 435 prompt = self._make_in_prompt(previous_prompt_number)
436 436 self._prompt = self._insert_html_fetching_plain_text(
437 437 cursor, prompt)
438 438
439 439 # When the HTML is inserted, Qt blows away the syntax
440 440 # highlighting for the line, so we need to rehighlight it.
441 441 self._highlighter.rehighlightBlock(cursor.block())
442 442
443 443 self._previous_prompt_obj = None
444 444
445 445 # Show a new prompt with the kernel's estimated prompt number.
446 446 self._show_interpreter_prompt(previous_prompt_number + 1)
447 447
448 448 #---------------------------------------------------------------------------
449 449 # 'IPythonWidget' interface
450 450 #---------------------------------------------------------------------------
451 451
452 452 def set_default_style(self, colors='lightbg'):
453 453 """ Sets the widget style to the class defaults.
454 454
455 455 Parameters
456 456 ----------
457 457 colors : str, optional (default lightbg)
458 458 Whether to use the default IPython light background or dark
459 459 background or B&W style.
460 460 """
461 461 colors = colors.lower()
462 462 if colors=='lightbg':
463 463 self.style_sheet = styles.default_light_style_sheet
464 464 self.syntax_style = styles.default_light_syntax_style
465 465 elif colors=='linux':
466 466 self.style_sheet = styles.default_dark_style_sheet
467 467 self.syntax_style = styles.default_dark_syntax_style
468 468 elif colors=='nocolor':
469 469 self.style_sheet = styles.default_bw_style_sheet
470 470 self.syntax_style = styles.default_bw_syntax_style
471 471 else:
472 472 raise KeyError("No such color scheme: %s"%colors)
473 473
474 474 #---------------------------------------------------------------------------
475 475 # 'IPythonWidget' protected interface
476 476 #---------------------------------------------------------------------------
477 477
478 478 def _edit(self, filename, line=None):
479 479 """ Opens a Python script for editing.
480 480
481 481 Parameters
482 482 ----------
483 483 filename : str
484 484 A path to a local system file.
485 485
486 486 line : int, optional
487 487 A line of interest in the file.
488 488 """
489 489 if self.custom_edit:
490 490 self.custom_edit_requested.emit(filename, line)
491 491 elif not self.editor:
492 492 self._append_plain_text('No default editor available.\n'
493 493 'Specify a GUI text editor in the `IPythonWidget.editor` '
494 494 'configurable to enable the %edit magic')
495 495 else:
496 496 try:
497 497 filename = '"%s"' % filename
498 498 if line and self.editor_line:
499 499 command = self.editor_line.format(filename=filename,
500 500 line=line)
501 501 else:
502 502 try:
503 503 command = self.editor.format()
504 504 except KeyError:
505 505 command = self.editor.format(filename=filename)
506 506 else:
507 507 command += ' ' + filename
508 508 except KeyError:
509 509 self._append_plain_text('Invalid editor command.\n')
510 510 else:
511 511 try:
512 512 Popen(command, shell=True)
513 513 except OSError:
514 514 msg = 'Opening editor with command "%s" failed.\n'
515 515 self._append_plain_text(msg % command)
516 516
517 517 def _make_in_prompt(self, number):
518 518 """ Given a prompt number, returns an HTML In prompt.
519 519 """
520 520 try:
521 521 body = self.in_prompt % number
522 522 except TypeError:
523 523 # allow in_prompt to leave out number, e.g. '>>> '
524 524 body = self.in_prompt
525 525 return '<span class="in-prompt">%s</span>' % body
526 526
527 527 def _make_continuation_prompt(self, prompt):
528 528 """ Given a plain text version of an In prompt, returns an HTML
529 529 continuation prompt.
530 530 """
531 531 end_chars = '...: '
532 532 space_count = len(prompt.lstrip('\n')) - len(end_chars)
533 533 body = '&nbsp;' * space_count + end_chars
534 534 return '<span class="in-prompt">%s</span>' % body
535 535
536 536 def _make_out_prompt(self, number):
537 537 """ Given a prompt number, returns an HTML Out prompt.
538 538 """
539 539 body = self.out_prompt % number
540 540 return '<span class="out-prompt">%s</span>' % body
541 541
542 542 #------ Payload handlers --------------------------------------------------
543 543
544 544 # Payload handlers with a generic interface: each takes the opaque payload
545 545 # dict, unpacks it and calls the underlying functions with the necessary
546 546 # arguments.
547 547
548 548 def _handle_payload_edit(self, item):
549 549 self._edit(item['filename'], item['line_number'])
550 550
551 551 def _handle_payload_exit(self, item):
552 552 self._keep_kernel_on_exit = item['keepkernel']
553 553 self.exit_requested.emit(self)
554 554
555 555 def _handle_payload_next_input(self, item):
556 556 self.input_buffer = item['text']
557 557
558 558 def _handle_payload_page(self, item):
559 559 # Since the plain text widget supports only a very small subset of HTML
560 560 # and we have no control over the HTML source, we only page HTML
561 561 # payloads in the rich text widget.
562 562 if item['html'] and self.kind == 'rich':
563 563 self._page(item['html'], html=True)
564 564 else:
565 565 self._page(item['text'], html=False)
566 566
567 567 #------ Trait change handlers --------------------------------------------
568 568
569 569 def _style_sheet_changed(self):
570 570 """ Set the style sheets of the underlying widgets.
571 571 """
572 572 self.setStyleSheet(self.style_sheet)
573 573 if self._control is not None:
574 574 self._control.document().setDefaultStyleSheet(self.style_sheet)
575 575 bg_color = self._control.palette().window().color()
576 576 self._ansi_processor.set_background_color(bg_color)
577 577
578 578 if self._page_control is not None:
579 579 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
580 580
581 581
582 582
583 583 def _syntax_style_changed(self):
584 584 """ Set the style for the syntax highlighter.
585 585 """
586 586 if self._highlighter is None:
587 587 # ignore premature calls
588 588 return
589 589 if self.syntax_style:
590 590 self._highlighter.set_style(self.syntax_style)
591 591 else:
592 592 self._highlighter.set_style_sheet(self.style_sheet)
593 593
594 594 #------ Trait default initializers -----------------------------------------
595 595
596 596 def _banner_default(self):
597 597 from IPython.core.usage import default_gui_banner
598 598 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now