##// END OF EJS Templates
qtconsole: Fix non-empty namespace at startup....
Bradley M. Froehle -
Show More
@@ -1,577 +1,581 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 182 content = msg['content']
183 183 if 'history' not in content:
184 184 self.log.error("History request failed: %r"%content)
185 185 if content.get('status', '') == 'aborted' and \
186 186 not self._retrying_history_request:
187 187 # a *different* action caused this request to be aborted, so
188 188 # we should try again.
189 189 self.log.error("Retrying aborted history request")
190 190 # prevent multiple retries of aborted requests:
191 191 self._retrying_history_request = True
192 192 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 193 time.sleep(0.25)
194 194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 195 else:
196 196 self._retrying_history_request = False
197 197 return
198 198 # reset retry flag
199 199 self._retrying_history_request = False
200 200 history_items = content['history']
201 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.get('execution_count', 0)
218 218 data = content['data']
219 219 if 'text/html' in data:
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 'text/plain' 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 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 'text/html' in data:
249 249 html = data['text/html']
250 250 self._append_html(html, True)
251 251 elif 'text/plain' in data:
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 and load %guiref."""
259 259 super(IPythonWidget, self)._started_channels()
260 260 self._load_guiref_magic()
261 261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
262 262 n=1000)
263 263
264 264 def _started_kernel(self):
265 265 """Load %guiref when the kernel starts (if channels are also started).
266 266
267 267 Principally triggered by kernel restart.
268 268 """
269 269 if self.kernel_manager.shell_channel is not None:
270 270 self._load_guiref_magic()
271 271
272 272 def _load_guiref_magic(self):
273 273 """Load %guiref magic."""
274 274 self.kernel_manager.shell_channel.execute('\n'.join([
275 "from IPython.core import usage",
276 "get_ipython().register_magic_function(usage.page_guiref, 'line', 'guiref')",
275 "try:",
276 " _usage",
277 "except:",
278 " from IPython.core import usage as _usage",
279 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
280 " del _usage",
277 281 ]), silent=True)
278 282
279 283 #---------------------------------------------------------------------------
280 284 # 'ConsoleWidget' public interface
281 285 #---------------------------------------------------------------------------
282 286
283 287 #---------------------------------------------------------------------------
284 288 # 'FrontendWidget' public interface
285 289 #---------------------------------------------------------------------------
286 290
287 291 def execute_file(self, path, hidden=False):
288 292 """ Reimplemented to use the 'run' magic.
289 293 """
290 294 # Use forward slashes on Windows to avoid escaping each separator.
291 295 if sys.platform == 'win32':
292 296 path = os.path.normpath(path).replace('\\', '/')
293 297
294 298 # Perhaps we should not be using %run directly, but while we
295 299 # are, it is necessary to quote or escape filenames containing spaces
296 300 # or quotes.
297 301
298 302 # In earlier code here, to minimize escaping, we sometimes quoted the
299 303 # filename with single quotes. But to do this, this code must be
300 304 # platform-aware, because run uses shlex rather than python string
301 305 # parsing, so that:
302 306 # * In Win: single quotes can be used in the filename without quoting,
303 307 # and we cannot use single quotes to quote the filename.
304 308 # * In *nix: we can escape double quotes in a double quoted filename,
305 309 # but can't escape single quotes in a single quoted filename.
306 310
307 311 # So to keep this code non-platform-specific and simple, we now only
308 312 # use double quotes to quote filenames, and escape when needed:
309 313 if ' ' in path or "'" in path or '"' in path:
310 314 path = '"%s"' % path.replace('"', '\\"')
311 315 self.execute('%%run %s' % path, hidden=hidden)
312 316
313 317 #---------------------------------------------------------------------------
314 318 # 'FrontendWidget' protected interface
315 319 #---------------------------------------------------------------------------
316 320
317 321 def _complete(self):
318 322 """ Reimplemented to support IPython's improved completion machinery.
319 323 """
320 324 # We let the kernel split the input line, so we *always* send an empty
321 325 # text field. Readline-based frontends do get a real text field which
322 326 # they can use.
323 327 text = ''
324 328
325 329 # Send the completion request to the kernel
326 330 msg_id = self.kernel_manager.shell_channel.complete(
327 331 text, # text
328 332 self._get_input_buffer_cursor_line(), # line
329 333 self._get_input_buffer_cursor_column(), # cursor_pos
330 334 self.input_buffer) # block
331 335 pos = self._get_cursor().position()
332 336 info = self._CompletionRequest(msg_id, pos)
333 337 self._request_info['complete'] = info
334 338
335 339 def _process_execute_error(self, msg):
336 340 """ Reimplemented for IPython-style traceback formatting.
337 341 """
338 342 content = msg['content']
339 343 traceback = '\n'.join(content['traceback']) + '\n'
340 344 if False:
341 345 # FIXME: For now, tracebacks come as plain text, so we can't use
342 346 # the html renderer yet. Once we refactor ultratb to produce
343 347 # properly styled tracebacks, this branch should be the default
344 348 traceback = traceback.replace(' ', '&nbsp;')
345 349 traceback = traceback.replace('\n', '<br/>')
346 350
347 351 ename = content['ename']
348 352 ename_styled = '<span class="error">%s</span>' % ename
349 353 traceback = traceback.replace(ename, ename_styled)
350 354
351 355 self._append_html(traceback)
352 356 else:
353 357 # This is the fallback for now, using plain text with ansi escapes
354 358 self._append_plain_text(traceback)
355 359
356 360 def _process_execute_payload(self, item):
357 361 """ Reimplemented to dispatch payloads to handler methods.
358 362 """
359 363 handler = self._payload_handlers.get(item['source'])
360 364 if handler is None:
361 365 # We have no handler for this type of payload, simply ignore it
362 366 return False
363 367 else:
364 368 handler(item)
365 369 return True
366 370
367 371 def _show_interpreter_prompt(self, number=None):
368 372 """ Reimplemented for IPython-style prompts.
369 373 """
370 374 # If a number was not specified, make a prompt number request.
371 375 if number is None:
372 376 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
373 377 info = self._ExecutionRequest(msg_id, 'prompt')
374 378 self._request_info['execute'][msg_id] = info
375 379 return
376 380
377 381 # Show a new prompt and save information about it so that it can be
378 382 # updated later if the prompt number turns out to be wrong.
379 383 self._prompt_sep = self.input_sep
380 384 self._show_prompt(self._make_in_prompt(number), html=True)
381 385 block = self._control.document().lastBlock()
382 386 length = len(self._prompt)
383 387 self._previous_prompt_obj = self._PromptBlock(block, length, number)
384 388
385 389 # Update continuation prompt to reflect (possibly) new prompt length.
386 390 self._set_continuation_prompt(
387 391 self._make_continuation_prompt(self._prompt), html=True)
388 392
389 393 def _show_interpreter_prompt_for_reply(self, msg):
390 394 """ Reimplemented for IPython-style prompts.
391 395 """
392 396 # Update the old prompt number if necessary.
393 397 content = msg['content']
394 398 # abort replies do not have any keys:
395 399 if content['status'] == 'aborted':
396 400 if self._previous_prompt_obj:
397 401 previous_prompt_number = self._previous_prompt_obj.number
398 402 else:
399 403 previous_prompt_number = 0
400 404 else:
401 405 previous_prompt_number = content['execution_count']
402 406 if self._previous_prompt_obj and \
403 407 self._previous_prompt_obj.number != previous_prompt_number:
404 408 block = self._previous_prompt_obj.block
405 409
406 410 # Make sure the prompt block has not been erased.
407 411 if block.isValid() and block.text():
408 412
409 413 # Remove the old prompt and insert a new prompt.
410 414 cursor = QtGui.QTextCursor(block)
411 415 cursor.movePosition(QtGui.QTextCursor.Right,
412 416 QtGui.QTextCursor.KeepAnchor,
413 417 self._previous_prompt_obj.length)
414 418 prompt = self._make_in_prompt(previous_prompt_number)
415 419 self._prompt = self._insert_html_fetching_plain_text(
416 420 cursor, prompt)
417 421
418 422 # When the HTML is inserted, Qt blows away the syntax
419 423 # highlighting for the line, so we need to rehighlight it.
420 424 self._highlighter.rehighlightBlock(cursor.block())
421 425
422 426 self._previous_prompt_obj = None
423 427
424 428 # Show a new prompt with the kernel's estimated prompt number.
425 429 self._show_interpreter_prompt(previous_prompt_number + 1)
426 430
427 431 #---------------------------------------------------------------------------
428 432 # 'IPythonWidget' interface
429 433 #---------------------------------------------------------------------------
430 434
431 435 def set_default_style(self, colors='lightbg'):
432 436 """ Sets the widget style to the class defaults.
433 437
434 438 Parameters:
435 439 -----------
436 440 colors : str, optional (default lightbg)
437 441 Whether to use the default IPython light background or dark
438 442 background or B&W style.
439 443 """
440 444 colors = colors.lower()
441 445 if colors=='lightbg':
442 446 self.style_sheet = styles.default_light_style_sheet
443 447 self.syntax_style = styles.default_light_syntax_style
444 448 elif colors=='linux':
445 449 self.style_sheet = styles.default_dark_style_sheet
446 450 self.syntax_style = styles.default_dark_syntax_style
447 451 elif colors=='nocolor':
448 452 self.style_sheet = styles.default_bw_style_sheet
449 453 self.syntax_style = styles.default_bw_syntax_style
450 454 else:
451 455 raise KeyError("No such color scheme: %s"%colors)
452 456
453 457 #---------------------------------------------------------------------------
454 458 # 'IPythonWidget' protected interface
455 459 #---------------------------------------------------------------------------
456 460
457 461 def _edit(self, filename, line=None):
458 462 """ Opens a Python script for editing.
459 463
460 464 Parameters:
461 465 -----------
462 466 filename : str
463 467 A path to a local system file.
464 468
465 469 line : int, optional
466 470 A line of interest in the file.
467 471 """
468 472 if self.custom_edit:
469 473 self.custom_edit_requested.emit(filename, line)
470 474 elif not self.editor:
471 475 self._append_plain_text('No default editor available.\n'
472 476 'Specify a GUI text editor in the `IPythonWidget.editor` '
473 477 'configurable to enable the %edit magic')
474 478 else:
475 479 try:
476 480 filename = '"%s"' % filename
477 481 if line and self.editor_line:
478 482 command = self.editor_line.format(filename=filename,
479 483 line=line)
480 484 else:
481 485 try:
482 486 command = self.editor.format()
483 487 except KeyError:
484 488 command = self.editor.format(filename=filename)
485 489 else:
486 490 command += ' ' + filename
487 491 except KeyError:
488 492 self._append_plain_text('Invalid editor command.\n')
489 493 else:
490 494 try:
491 495 Popen(command, shell=True)
492 496 except OSError:
493 497 msg = 'Opening editor with command "%s" failed.\n'
494 498 self._append_plain_text(msg % command)
495 499
496 500 def _make_in_prompt(self, number):
497 501 """ Given a prompt number, returns an HTML In prompt.
498 502 """
499 503 try:
500 504 body = self.in_prompt % number
501 505 except TypeError:
502 506 # allow in_prompt to leave out number, e.g. '>>> '
503 507 body = self.in_prompt
504 508 return '<span class="in-prompt">%s</span>' % body
505 509
506 510 def _make_continuation_prompt(self, prompt):
507 511 """ Given a plain text version of an In prompt, returns an HTML
508 512 continuation prompt.
509 513 """
510 514 end_chars = '...: '
511 515 space_count = len(prompt.lstrip('\n')) - len(end_chars)
512 516 body = '&nbsp;' * space_count + end_chars
513 517 return '<span class="in-prompt">%s</span>' % body
514 518
515 519 def _make_out_prompt(self, number):
516 520 """ Given a prompt number, returns an HTML Out prompt.
517 521 """
518 522 body = self.out_prompt % number
519 523 return '<span class="out-prompt">%s</span>' % body
520 524
521 525 #------ Payload handlers --------------------------------------------------
522 526
523 527 # Payload handlers with a generic interface: each takes the opaque payload
524 528 # dict, unpacks it and calls the underlying functions with the necessary
525 529 # arguments.
526 530
527 531 def _handle_payload_edit(self, item):
528 532 self._edit(item['filename'], item['line_number'])
529 533
530 534 def _handle_payload_exit(self, item):
531 535 self._keep_kernel_on_exit = item['keepkernel']
532 536 self.exit_requested.emit(self)
533 537
534 538 def _handle_payload_next_input(self, item):
535 539 self.input_buffer = dedent(item['text'].rstrip())
536 540
537 541 def _handle_payload_page(self, item):
538 542 # Since the plain text widget supports only a very small subset of HTML
539 543 # and we have no control over the HTML source, we only page HTML
540 544 # payloads in the rich text widget.
541 545 if item['html'] and self.kind == 'rich':
542 546 self._page(item['html'], html=True)
543 547 else:
544 548 self._page(item['text'], html=False)
545 549
546 550 #------ Trait change handlers --------------------------------------------
547 551
548 552 def _style_sheet_changed(self):
549 553 """ Set the style sheets of the underlying widgets.
550 554 """
551 555 self.setStyleSheet(self.style_sheet)
552 556 if self._control is not None:
553 557 self._control.document().setDefaultStyleSheet(self.style_sheet)
554 558 bg_color = self._control.palette().window().color()
555 559 self._ansi_processor.set_background_color(bg_color)
556 560
557 561 if self._page_control is not None:
558 562 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
559 563
560 564
561 565
562 566 def _syntax_style_changed(self):
563 567 """ Set the style for the syntax highlighter.
564 568 """
565 569 if self._highlighter is None:
566 570 # ignore premature calls
567 571 return
568 572 if self.syntax_style:
569 573 self._highlighter.set_style(self.syntax_style)
570 574 else:
571 575 self._highlighter.set_style_sheet(self.style_sheet)
572 576
573 577 #------ Trait default initializers -----------------------------------------
574 578
575 579 def _banner_default(self):
576 580 from IPython.core.usage import default_gui_banner
577 581 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now