##// END OF EJS Templates
Protect Qt console against SyntaxError from input transformers.
Thomas Kluyver -
Show More
@@ -1,661 +1,668 b''
1 1 """Analysis of text input into executable blocks.
2 2
3 3 The main class in this module, :class:`InputSplitter`, is designed to break
4 4 input from either interactive, line-by-line environments or block-based ones,
5 5 into standalone blocks that can be executed by Python as 'single' statements
6 6 (thus triggering sys.displayhook).
7 7
8 8 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
9 9 with full support for the extended IPython syntax (magics, system calls, etc).
10 10
11 11 For more details, see the class docstring below.
12 12
13 13 Syntax Transformations
14 14 ----------------------
15 15
16 16 One of the main jobs of the code in this file is to apply all syntax
17 17 transformations that make up 'the IPython language', i.e. magics, shell
18 18 escapes, etc. All transformations should be implemented as *fully stateless*
19 19 entities, that simply take one line as their input and return a line.
20 20 Internally for implementation purposes they may be a normal function or a
21 21 callable object, but the only input they receive will be a single line and they
22 22 should only return a line, without holding any data-dependent state between
23 23 calls.
24 24
25 25 As an example, the EscapedTransformer is a class so we can more clearly group
26 26 together the functionality of dispatching to individual functions based on the
27 27 starting escape character, but the only method for public use is its call
28 28 method.
29 29
30 30
31 31 ToDo
32 32 ----
33 33
34 34 - Should we make push() actually raise an exception once push_accepts_more()
35 35 returns False?
36 36
37 37 - Naming cleanups. The tr_* names aren't the most elegant, though now they are
38 38 at least just attributes of a class so not really very exposed.
39 39
40 40 - Think about the best way to support dynamic things: automagic, autocall,
41 41 macros, etc.
42 42
43 43 - Think of a better heuristic for the application of the transforms in
44 44 IPythonInputSplitter.push() than looking at the buffer ending in ':'. Idea:
45 45 track indentation change events (indent, dedent, nothing) and apply them only
46 46 if the indentation went up, but not otherwise.
47 47
48 48 - Think of the cleanest way for supporting user-specified transformations (the
49 49 user prefilters we had before).
50 50
51 51 Authors
52 52 -------
53 53
54 54 * Fernando Perez
55 55 * Brian Granger
56 56 """
57 57 #-----------------------------------------------------------------------------
58 58 # Copyright (C) 2010 The IPython Development Team
59 59 #
60 60 # Distributed under the terms of the BSD License. The full license is in
61 61 # the file COPYING, distributed as part of this software.
62 62 #-----------------------------------------------------------------------------
63 63
64 64 #-----------------------------------------------------------------------------
65 65 # Imports
66 66 #-----------------------------------------------------------------------------
67 67 # stdlib
68 68 import ast
69 69 import codeop
70 70 import re
71 71 import sys
72 72
73 73 # IPython modules
74 74 from IPython.utils.py3compat import cast_unicode
75 75 from IPython.core.inputtransformer import (leading_indent,
76 76 classic_prompt,
77 77 ipy_prompt,
78 78 strip_encoding_cookie,
79 79 cellmagic,
80 80 assemble_logical_lines,
81 81 help_end,
82 82 escaped_commands,
83 83 assign_from_magic,
84 84 assign_from_system,
85 85 assemble_python_lines,
86 86 )
87 87
88 88 # These are available in this module for backwards compatibility.
89 89 from IPython.core.inputtransformer import (ESC_SHELL, ESC_SH_CAP, ESC_HELP,
90 90 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,
91 91 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN, ESC_SEQUENCES)
92 92
93 93 #-----------------------------------------------------------------------------
94 94 # Utilities
95 95 #-----------------------------------------------------------------------------
96 96
97 97 # FIXME: These are general-purpose utilities that later can be moved to the
98 98 # general ward. Kept here for now because we're being very strict about test
99 99 # coverage with this code, and this lets us ensure that we keep 100% coverage
100 100 # while developing.
101 101
102 102 # compiled regexps for autoindent management
103 103 dedent_re = re.compile('|'.join([
104 104 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
105 105 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
106 106 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
107 107 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
108 108 r'^\s+pass\s*$', # pass (optionally followed by trailing spaces)
109 109 r'^\s+break\s*$', # break (optionally followed by trailing spaces)
110 110 r'^\s+continue\s*$', # continue (optionally followed by trailing spaces)
111 111 ]))
112 112 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
113 113
114 114 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
115 115 # before pure comments
116 116 comment_line_re = re.compile('^\s*\#')
117 117
118 118
119 119 def num_ini_spaces(s):
120 120 """Return the number of initial spaces in a string.
121 121
122 122 Note that tabs are counted as a single space. For now, we do *not* support
123 123 mixing of tabs and spaces in the user's input.
124 124
125 125 Parameters
126 126 ----------
127 127 s : string
128 128
129 129 Returns
130 130 -------
131 131 n : int
132 132 """
133 133
134 134 ini_spaces = ini_spaces_re.match(s)
135 135 if ini_spaces:
136 136 return ini_spaces.end()
137 137 else:
138 138 return 0
139 139
140 140 def last_blank(src):
141 141 """Determine if the input source ends in a blank.
142 142
143 143 A blank is either a newline or a line consisting of whitespace.
144 144
145 145 Parameters
146 146 ----------
147 147 src : string
148 148 A single or multiline string.
149 149 """
150 150 if not src: return False
151 151 ll = src.splitlines()[-1]
152 152 return (ll == '') or ll.isspace()
153 153
154 154
155 155 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
156 156 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
157 157
158 158 def last_two_blanks(src):
159 159 """Determine if the input source ends in two blanks.
160 160
161 161 A blank is either a newline or a line consisting of whitespace.
162 162
163 163 Parameters
164 164 ----------
165 165 src : string
166 166 A single or multiline string.
167 167 """
168 168 if not src: return False
169 169 # The logic here is tricky: I couldn't get a regexp to work and pass all
170 170 # the tests, so I took a different approach: split the source by lines,
171 171 # grab the last two and prepend '###\n' as a stand-in for whatever was in
172 172 # the body before the last two lines. Then, with that structure, it's
173 173 # possible to analyze with two regexps. Not the most elegant solution, but
174 174 # it works. If anyone tries to change this logic, make sure to validate
175 175 # the whole test suite first!
176 176 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
177 177 return (bool(last_two_blanks_re.match(new_src)) or
178 178 bool(last_two_blanks_re2.match(new_src)) )
179 179
180 180
181 181 def remove_comments(src):
182 182 """Remove all comments from input source.
183 183
184 184 Note: comments are NOT recognized inside of strings!
185 185
186 186 Parameters
187 187 ----------
188 188 src : string
189 189 A single or multiline input string.
190 190
191 191 Returns
192 192 -------
193 193 String with all Python comments removed.
194 194 """
195 195
196 196 return re.sub('#.*', '', src)
197 197
198 198
199 199 def get_input_encoding():
200 200 """Return the default standard input encoding.
201 201
202 202 If sys.stdin has no encoding, 'ascii' is returned."""
203 203 # There are strange environments for which sys.stdin.encoding is None. We
204 204 # ensure that a valid encoding is returned.
205 205 encoding = getattr(sys.stdin, 'encoding', None)
206 206 if encoding is None:
207 207 encoding = 'ascii'
208 208 return encoding
209 209
210 210 #-----------------------------------------------------------------------------
211 211 # Classes and functions for normal Python syntax handling
212 212 #-----------------------------------------------------------------------------
213 213
214 214 class InputSplitter(object):
215 215 """An object that can accumulate lines of Python source before execution.
216 216
217 217 This object is designed to be fed python source line-by-line, using
218 218 :meth:`push`. It will return on each push whether the currently pushed
219 219 code could be executed already. In addition, it provides a method called
220 220 :meth:`push_accepts_more` that can be used to query whether more input
221 221 can be pushed into a single interactive block.
222 222
223 223 This is a simple example of how an interactive terminal-based client can use
224 224 this tool::
225 225
226 226 isp = InputSplitter()
227 227 while isp.push_accepts_more():
228 228 indent = ' '*isp.indent_spaces
229 229 prompt = '>>> ' + indent
230 230 line = indent + raw_input(prompt)
231 231 isp.push(line)
232 232 print 'Input source was:\n', isp.source_reset(),
233 233 """
234 234 # Number of spaces of indentation computed from input that has been pushed
235 235 # so far. This is the attributes callers should query to get the current
236 236 # indentation level, in order to provide auto-indent facilities.
237 237 indent_spaces = 0
238 238 # String, indicating the default input encoding. It is computed by default
239 239 # at initialization time via get_input_encoding(), but it can be reset by a
240 240 # client with specific knowledge of the encoding.
241 241 encoding = ''
242 242 # String where the current full source input is stored, properly encoded.
243 243 # Reading this attribute is the normal way of querying the currently pushed
244 244 # source code, that has been properly encoded.
245 245 source = ''
246 246 # Code object corresponding to the current source. It is automatically
247 247 # synced to the source, so it can be queried at any time to obtain the code
248 248 # object; it will be None if the source doesn't compile to valid Python.
249 249 code = None
250 250
251 251 # Private attributes
252 252
253 253 # List with lines of input accumulated so far
254 254 _buffer = None
255 255 # Command compiler
256 256 _compile = None
257 257 # Mark when input has changed indentation all the way back to flush-left
258 258 _full_dedent = False
259 259 # Boolean indicating whether the current block is complete
260 260 _is_complete = None
261 261
262 262 def __init__(self):
263 263 """Create a new InputSplitter instance.
264 264 """
265 265 self._buffer = []
266 266 self._compile = codeop.CommandCompiler()
267 267 self.encoding = get_input_encoding()
268 268
269 269 def reset(self):
270 270 """Reset the input buffer and associated state."""
271 271 self.indent_spaces = 0
272 272 self._buffer[:] = []
273 273 self.source = ''
274 274 self.code = None
275 275 self._is_complete = False
276 276 self._full_dedent = False
277 277
278 278 def source_reset(self):
279 279 """Return the input source and perform a full reset.
280 280 """
281 281 out = self.source
282 282 self.reset()
283 283 return out
284 284
285 285 def push(self, lines):
286 286 """Push one or more lines of input.
287 287
288 288 This stores the given lines and returns a status code indicating
289 289 whether the code forms a complete Python block or not.
290 290
291 291 Any exceptions generated in compilation are swallowed, but if an
292 292 exception was produced, the method returns True.
293 293
294 294 Parameters
295 295 ----------
296 296 lines : string
297 297 One or more lines of Python input.
298 298
299 299 Returns
300 300 -------
301 301 is_complete : boolean
302 302 True if the current input source (the result of the current input
303 303 plus prior inputs) forms a complete Python execution block. Note that
304 304 this value is also stored as a private attribute (``_is_complete``), so it
305 305 can be queried at any time.
306 306 """
307 307 self._store(lines)
308 308 source = self.source
309 309
310 310 # Before calling _compile(), reset the code object to None so that if an
311 311 # exception is raised in compilation, we don't mislead by having
312 312 # inconsistent code/source attributes.
313 313 self.code, self._is_complete = None, None
314 314
315 315 # Honor termination lines properly
316 316 if source.endswith('\\\n'):
317 317 return False
318 318
319 319 self._update_indent(lines)
320 320 try:
321 321 self.code = self._compile(source, symbol="exec")
322 322 # Invalid syntax can produce any of a number of different errors from
323 323 # inside the compiler, so we have to catch them all. Syntax errors
324 324 # immediately produce a 'ready' block, so the invalid Python can be
325 325 # sent to the kernel for evaluation with possible ipython
326 326 # special-syntax conversion.
327 327 except (SyntaxError, OverflowError, ValueError, TypeError,
328 328 MemoryError):
329 329 self._is_complete = True
330 330 else:
331 331 # Compilation didn't produce any exceptions (though it may not have
332 332 # given a complete code object)
333 333 self._is_complete = self.code is not None
334 334
335 335 return self._is_complete
336 336
337 337 def push_accepts_more(self):
338 338 """Return whether a block of interactive input can accept more input.
339 339
340 340 This method is meant to be used by line-oriented frontends, who need to
341 341 guess whether a block is complete or not based solely on prior and
342 342 current input lines. The InputSplitter considers it has a complete
343 343 interactive block and will not accept more input when either:
344 344
345 345 * A SyntaxError is raised
346 346
347 347 * The code is complete and consists of a single line or a single
348 348 non-compound statement
349 349
350 350 * The code is complete and has a blank line at the end
351 351
352 352 If the current input produces a syntax error, this method immediately
353 353 returns False but does *not* raise the syntax error exception, as
354 354 typically clients will want to send invalid syntax to an execution
355 355 backend which might convert the invalid syntax into valid Python via
356 356 one of the dynamic IPython mechanisms.
357 357 """
358 358
359 359 # With incomplete input, unconditionally accept more
360 360 # A syntax error also sets _is_complete to True - see push()
361 361 if not self._is_complete:
362 362 #print("Not complete") # debug
363 363 return True
364 364
365 365 # The user can make any (complete) input execute by leaving a blank line
366 366 last_line = self.source.splitlines()[-1]
367 367 if (not last_line) or last_line.isspace():
368 368 #print("Blank line") # debug
369 369 return False
370 370
371 371 # If there's just a single line or AST node, and we're flush left, as is
372 372 # the case after a simple statement such as 'a=1', we want to execute it
373 373 # straight away.
374 374 if self.indent_spaces==0:
375 375 if len(self.source.splitlines()) <= 1:
376 376 return False
377 377
378 378 try:
379 379 code_ast = ast.parse(u''.join(self._buffer))
380 380 except Exception:
381 381 #print("Can't parse AST") # debug
382 382 return False
383 383 else:
384 384 if len(code_ast.body) == 1 and \
385 385 not hasattr(code_ast.body[0], 'body'):
386 386 #print("Simple statement") # debug
387 387 return False
388 388
389 389 # General fallback - accept more code
390 390 return True
391 391
392 392 #------------------------------------------------------------------------
393 393 # Private interface
394 394 #------------------------------------------------------------------------
395 395
396 396 def _find_indent(self, line):
397 397 """Compute the new indentation level for a single line.
398 398
399 399 Parameters
400 400 ----------
401 401 line : str
402 402 A single new line of non-whitespace, non-comment Python input.
403 403
404 404 Returns
405 405 -------
406 406 indent_spaces : int
407 407 New value for the indent level (it may be equal to self.indent_spaces
408 408 if indentation doesn't change.
409 409
410 410 full_dedent : boolean
411 411 Whether the new line causes a full flush-left dedent.
412 412 """
413 413 indent_spaces = self.indent_spaces
414 414 full_dedent = self._full_dedent
415 415
416 416 inisp = num_ini_spaces(line)
417 417 if inisp < indent_spaces:
418 418 indent_spaces = inisp
419 419 if indent_spaces <= 0:
420 420 #print 'Full dedent in text',self.source # dbg
421 421 full_dedent = True
422 422
423 423 if line.rstrip()[-1] == ':':
424 424 indent_spaces += 4
425 425 elif dedent_re.match(line):
426 426 indent_spaces -= 4
427 427 if indent_spaces <= 0:
428 428 full_dedent = True
429 429
430 430 # Safety
431 431 if indent_spaces < 0:
432 432 indent_spaces = 0
433 433 #print 'safety' # dbg
434 434
435 435 return indent_spaces, full_dedent
436 436
437 437 def _update_indent(self, lines):
438 438 for line in remove_comments(lines).splitlines():
439 439 if line and not line.isspace():
440 440 self.indent_spaces, self._full_dedent = self._find_indent(line)
441 441
442 442 def _store(self, lines, buffer=None, store='source'):
443 443 """Store one or more lines of input.
444 444
445 445 If input lines are not newline-terminated, a newline is automatically
446 446 appended."""
447 447
448 448 if buffer is None:
449 449 buffer = self._buffer
450 450
451 451 if lines.endswith('\n'):
452 452 buffer.append(lines)
453 453 else:
454 454 buffer.append(lines+'\n')
455 455 setattr(self, store, self._set_source(buffer))
456 456
457 457 def _set_source(self, buffer):
458 458 return u''.join(buffer)
459 459
460 460
461 461 class IPythonInputSplitter(InputSplitter):
462 462 """An input splitter that recognizes all of IPython's special syntax."""
463 463
464 464 # String with raw, untransformed input.
465 465 source_raw = ''
466 466
467 467 # Flag to track when a transformer has stored input that it hasn't given
468 468 # back yet.
469 469 transformer_accumulating = False
470 470
471 471 # Flag to track when assemble_python_lines has stored input that it hasn't
472 472 # given back yet.
473 473 within_python_line = False
474 474
475 475 # Private attributes
476 476
477 477 # List with lines of raw input accumulated so far.
478 478 _buffer_raw = None
479 479
480 480 def __init__(self, line_input_checker=True, physical_line_transforms=None,
481 481 logical_line_transforms=None, python_line_transforms=None):
482 482 super(IPythonInputSplitter, self).__init__()
483 483 self._buffer_raw = []
484 484 self._validate = True
485 485
486 486 if physical_line_transforms is not None:
487 487 self.physical_line_transforms = physical_line_transforms
488 488 else:
489 489 self.physical_line_transforms = [
490 490 leading_indent(),
491 491 classic_prompt(),
492 492 ipy_prompt(),
493 493 strip_encoding_cookie(),
494 494 cellmagic(end_on_blank_line=line_input_checker),
495 495 ]
496 496
497 497 self.assemble_logical_lines = assemble_logical_lines()
498 498 if logical_line_transforms is not None:
499 499 self.logical_line_transforms = logical_line_transforms
500 500 else:
501 501 self.logical_line_transforms = [
502 502 help_end(),
503 503 escaped_commands(),
504 504 assign_from_magic(),
505 505 assign_from_system(),
506 506 ]
507 507
508 508 self.assemble_python_lines = assemble_python_lines()
509 509 if python_line_transforms is not None:
510 510 self.python_line_transforms = python_line_transforms
511 511 else:
512 512 # We don't use any of these at present
513 513 self.python_line_transforms = []
514 514
515 515 @property
516 516 def transforms(self):
517 517 "Quick access to all transformers."
518 518 return self.physical_line_transforms + \
519 519 [self.assemble_logical_lines] + self.logical_line_transforms + \
520 520 [self.assemble_python_lines] + self.python_line_transforms
521 521
522 522 @property
523 523 def transforms_in_use(self):
524 524 """Transformers, excluding logical line transformers if we're in a
525 525 Python line."""
526 526 t = self.physical_line_transforms[:]
527 527 if not self.within_python_line:
528 528 t += [self.assemble_logical_lines] + self.logical_line_transforms
529 529 return t + [self.assemble_python_lines] + self.python_line_transforms
530 530
531 531 def reset(self):
532 532 """Reset the input buffer and associated state."""
533 533 super(IPythonInputSplitter, self).reset()
534 534 self._buffer_raw[:] = []
535 535 self.source_raw = ''
536 536 self.transformer_accumulating = False
537 537 self.within_python_line = False
538
539 last_exc = None
538 540 for t in self.transforms:
539 t.reset()
541 try:
542 t.reset()
543 except SyntaxError as e:
544 last_exc = e
545 if last_exc is not None:
546 raise last_exc
540 547
541 548 def flush_transformers(self):
542 549 def _flush(transform, out):
543 550 if out is not None:
544 551 tmp = transform.push(out)
545 552 return tmp or transform.reset() or None
546 553 else:
547 554 return transform.reset() or None
548 555
549 556 out = None
550 557 for t in self.transforms_in_use:
551 558 out = _flush(t, out)
552 559
553 560 if out is not None:
554 561 self._store(out)
555 562
556 563 def source_raw_reset(self):
557 564 """Return input and raw source and perform a full reset.
558 565 """
559 566 self.flush_transformers()
560 567 out = self.source
561 568 out_r = self.source_raw
562 569 self.reset()
563 570 return out, out_r
564 571
565 572 def source_reset(self):
566 573 self.flush_transformers()
567 574 return super(IPythonInputSplitter, self).source_reset()
568 575
569 576 def push_accepts_more(self):
570 577 if self.transformer_accumulating:
571 578 return True
572 579 else:
573 580 return super(IPythonInputSplitter, self).push_accepts_more()
574 581
575 582 def transform_cell(self, cell):
576 583 """Process and translate a cell of input.
577 584 """
578 585 self.reset()
579 586 self.push(cell)
580 587 return self.source_reset()
581 588
582 589 def push(self, lines):
583 590 """Push one or more lines of IPython input.
584 591
585 592 This stores the given lines and returns a status code indicating
586 593 whether the code forms a complete Python block or not, after processing
587 594 all input lines for special IPython syntax.
588 595
589 596 Any exceptions generated in compilation are swallowed, but if an
590 597 exception was produced, the method returns True.
591 598
592 599 Parameters
593 600 ----------
594 601 lines : string
595 602 One or more lines of Python input.
596 603
597 604 Returns
598 605 -------
599 606 is_complete : boolean
600 607 True if the current input source (the result of the current input
601 608 plus prior inputs) forms a complete Python execution block. Note that
602 609 this value is also stored as a private attribute (_is_complete), so it
603 610 can be queried at any time.
604 611 """
605 612
606 613 # We must ensure all input is pure unicode
607 614 lines = cast_unicode(lines, self.encoding)
608 615
609 616 # ''.splitlines() --> [], but we need to push the empty line to transformers
610 617 lines_list = lines.splitlines()
611 618 if not lines_list:
612 619 lines_list = ['']
613 620
614 621 # Store raw source before applying any transformations to it. Note
615 622 # that this must be done *after* the reset() call that would otherwise
616 623 # flush the buffer.
617 624 self._store(lines, self._buffer_raw, 'source_raw')
618 625
619 626 for line in lines_list:
620 627 out = self.push_line(line)
621 628
622 629 return out
623 630
624 631 def push_line(self, line):
625 632 buf = self._buffer
626 633
627 634 def _accumulating(dbg):
628 635 #print(dbg)
629 636 self.transformer_accumulating = True
630 637 return False
631 638
632 639 for transformer in self.physical_line_transforms:
633 640 line = transformer.push(line)
634 641 if line is None:
635 642 return _accumulating(transformer)
636 643
637 644 if not self.within_python_line:
638 645 line = self.assemble_logical_lines.push(line)
639 646 if line is None:
640 647 return _accumulating('acc logical line')
641 648
642 649 for transformer in self.logical_line_transforms:
643 650 line = transformer.push(line)
644 651 if line is None:
645 652 return _accumulating(transformer)
646 653
647 654 line = self.assemble_python_lines.push(line)
648 655 if line is None:
649 656 self.within_python_line = True
650 657 return _accumulating('acc python line')
651 658 else:
652 659 self.within_python_line = False
653 660
654 661 for transformer in self.python_line_transforms:
655 662 line = transformer.push(line)
656 663 if line is None:
657 664 return _accumulating(transformer)
658 665
659 666 #print("transformers clear") #debug
660 667 self.transformer_accumulating = False
661 668 return super(IPythonInputSplitter, self).push(line)
@@ -1,784 +1,793 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import uuid
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from IPython.external import qt
11 11 from IPython.external.qt import QtCore, QtGui
12 12
13 13 # Local imports
14 14 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
15 15 from IPython.core.inputtransformer import classic_prompt
16 16 from IPython.core.oinspect import call_tip
17 17 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
18 18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 19 from .bracket_matcher import BracketMatcher
20 20 from .call_tip_widget import CallTipWidget
21 21 from .completion_lexer import CompletionLexer
22 22 from .history_console_widget import HistoryConsoleWidget
23 23 from .pygments_highlighter import PygmentsHighlighter
24 24
25 25
26 26 class FrontendHighlighter(PygmentsHighlighter):
27 27 """ A PygmentsHighlighter that understands and ignores prompts.
28 28 """
29 29
30 30 def __init__(self, frontend):
31 31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 32 self._current_offset = 0
33 33 self._frontend = frontend
34 34 self.highlighting_on = False
35 35
36 36 def highlightBlock(self, string):
37 37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 38 """
39 39 if not self.highlighting_on:
40 40 return
41 41
42 42 # The input to this function is a unicode string that may contain
43 43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 44 # the string as plain text so we can compare it.
45 45 current_block = self.currentBlock()
46 46 string = self._frontend._get_block_plain_text(current_block)
47 47
48 48 # Decide whether to check for the regular or continuation prompt.
49 49 if current_block.contains(self._frontend._prompt_pos):
50 50 prompt = self._frontend._prompt
51 51 else:
52 52 prompt = self._frontend._continuation_prompt
53 53
54 54 # Only highlight if we can identify a prompt, but make sure not to
55 55 # highlight the prompt.
56 56 if string.startswith(prompt):
57 57 self._current_offset = len(prompt)
58 58 string = string[len(prompt):]
59 59 super(FrontendHighlighter, self).highlightBlock(string)
60 60
61 61 def rehighlightBlock(self, block):
62 62 """ Reimplemented to temporarily enable highlighting if disabled.
63 63 """
64 64 old = self.highlighting_on
65 65 self.highlighting_on = True
66 66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 67 self.highlighting_on = old
68 68
69 69 def setFormat(self, start, count, format):
70 70 """ Reimplemented to highlight selectively.
71 71 """
72 72 start += self._current_offset
73 73 super(FrontendHighlighter, self).setFormat(start, count, format)
74 74
75 75
76 76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 77 """ A Qt frontend for a generic Python kernel.
78 78 """
79 79
80 80 # The text to show when the kernel is (re)started.
81 81 banner = Unicode(config=True)
82 82
83 83 # An option and corresponding signal for overriding the default kernel
84 84 # interrupt behavior.
85 85 custom_interrupt = Bool(False)
86 86 custom_interrupt_requested = QtCore.Signal()
87 87
88 88 # An option and corresponding signals for overriding the default kernel
89 89 # restart behavior.
90 90 custom_restart = Bool(False)
91 91 custom_restart_kernel_died = QtCore.Signal(float)
92 92 custom_restart_requested = QtCore.Signal()
93 93
94 94 # Whether to automatically show calltips on open-parentheses.
95 95 enable_calltips = Bool(True, config=True,
96 96 help="Whether to draw information calltips on open-parentheses.")
97 97
98 98 clear_on_kernel_restart = Bool(True, config=True,
99 99 help="Whether to clear the console when the kernel is restarted")
100 100
101 101 confirm_restart = Bool(True, config=True,
102 102 help="Whether to ask for user confirmation when restarting kernel")
103 103
104 104 # Emitted when a user visible 'execute_request' has been submitted to the
105 105 # kernel from the FrontendWidget. Contains the code to be executed.
106 106 executing = QtCore.Signal(object)
107 107
108 108 # Emitted when a user-visible 'execute_reply' has been received from the
109 109 # kernel and processed by the FrontendWidget. Contains the response message.
110 110 executed = QtCore.Signal(object)
111 111
112 112 # Emitted when an exit request has been received from the kernel.
113 113 exit_requested = QtCore.Signal(object)
114 114
115 115 # Protected class variables.
116 116 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
117 117 logical_line_transforms=[],
118 118 python_line_transforms=[],
119 119 )
120 120 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
121 121 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
122 122 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
123 123 _input_splitter_class = InputSplitter
124 124 _local_kernel = False
125 125 _highlighter = Instance(FrontendHighlighter)
126 126
127 127 #---------------------------------------------------------------------------
128 128 # 'object' interface
129 129 #---------------------------------------------------------------------------
130 130
131 131 def __init__(self, *args, **kw):
132 132 super(FrontendWidget, self).__init__(*args, **kw)
133 133 # FIXME: remove this when PySide min version is updated past 1.0.7
134 134 # forcefully disable calltips if PySide is < 1.0.7, because they crash
135 135 if qt.QT_API == qt.QT_API_PYSIDE:
136 136 import PySide
137 137 if PySide.__version_info__ < (1,0,7):
138 138 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
139 139 self.enable_calltips = False
140 140
141 141 # FrontendWidget protected variables.
142 142 self._bracket_matcher = BracketMatcher(self._control)
143 143 self._call_tip_widget = CallTipWidget(self._control)
144 144 self._completion_lexer = CompletionLexer(PythonLexer())
145 145 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
146 146 self._hidden = False
147 147 self._highlighter = FrontendHighlighter(self)
148 148 self._input_splitter = self._input_splitter_class()
149 149 self._kernel_manager = None
150 150 self._kernel_client = None
151 151 self._request_info = {}
152 152 self._request_info['execute'] = {};
153 153 self._callback_dict = {}
154 154
155 155 # Configure the ConsoleWidget.
156 156 self.tab_width = 4
157 157 self._set_continuation_prompt('... ')
158 158
159 159 # Configure the CallTipWidget.
160 160 self._call_tip_widget.setFont(self.font)
161 161 self.font_changed.connect(self._call_tip_widget.setFont)
162 162
163 163 # Configure actions.
164 164 action = self._copy_raw_action
165 165 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
166 166 action.setEnabled(False)
167 167 action.setShortcut(QtGui.QKeySequence(key))
168 168 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
169 169 action.triggered.connect(self.copy_raw)
170 170 self.copy_available.connect(action.setEnabled)
171 171 self.addAction(action)
172 172
173 173 # Connect signal handlers.
174 174 document = self._control.document()
175 175 document.contentsChange.connect(self._document_contents_change)
176 176
177 177 # Set flag for whether we are connected via localhost.
178 178 self._local_kernel = kw.get('local_kernel',
179 179 FrontendWidget._local_kernel)
180 180
181 181 #---------------------------------------------------------------------------
182 182 # 'ConsoleWidget' public interface
183 183 #---------------------------------------------------------------------------
184 184
185 185 def copy(self):
186 186 """ Copy the currently selected text to the clipboard, removing prompts.
187 187 """
188 188 if self._page_control is not None and self._page_control.hasFocus():
189 189 self._page_control.copy()
190 190 elif self._control.hasFocus():
191 191 text = self._control.textCursor().selection().toPlainText()
192 192 if text:
193 193 text = self._prompt_transformer.transform_cell(text)
194 194 QtGui.QApplication.clipboard().setText(text)
195 195 else:
196 196 self.log.debug("frontend widget : unknown copy target")
197 197
198 198 #---------------------------------------------------------------------------
199 199 # 'ConsoleWidget' abstract interface
200 200 #---------------------------------------------------------------------------
201 201
202 202 def _is_complete(self, source, interactive):
203 203 """ Returns whether 'source' can be completely processed and a new
204 204 prompt created. When triggered by an Enter/Return key press,
205 205 'interactive' is True; otherwise, it is False.
206 206 """
207 self._input_splitter.reset()
208 complete = self._input_splitter.push(source)
207 try:
208 self._input_splitter.reset()
209 except SyntaxError:
210 pass
211 try:
212 complete = self._input_splitter.push(source)
213 except SyntaxError:
214 return True
209 215 if interactive:
210 216 complete = not self._input_splitter.push_accepts_more()
211 217 return complete
212 218
213 219 def _execute(self, source, hidden):
214 220 """ Execute 'source'. If 'hidden', do not show any output.
215 221
216 222 See parent class :meth:`execute` docstring for full details.
217 223 """
218 224 msg_id = self.kernel_client.execute(source, hidden)
219 225 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
220 226 self._hidden = hidden
221 227 if not hidden:
222 228 self.executing.emit(source)
223 229
224 230 def _prompt_started_hook(self):
225 231 """ Called immediately after a new prompt is displayed.
226 232 """
227 233 if not self._reading:
228 234 self._highlighter.highlighting_on = True
229 235
230 236 def _prompt_finished_hook(self):
231 237 """ Called immediately after a prompt is finished, i.e. when some input
232 238 will be processed and a new prompt displayed.
233 239 """
234 240 # Flush all state from the input splitter so the next round of
235 241 # reading input starts with a clean buffer.
236 self._input_splitter.reset()
242 try:
243 self._input_splitter.reset()
244 except SyntaxError:
245 pass
237 246
238 247 if not self._reading:
239 248 self._highlighter.highlighting_on = False
240 249
241 250 def _tab_pressed(self):
242 251 """ Called when the tab key is pressed. Returns whether to continue
243 252 processing the event.
244 253 """
245 254 # Perform tab completion if:
246 255 # 1) The cursor is in the input buffer.
247 256 # 2) There is a non-whitespace character before the cursor.
248 257 text = self._get_input_buffer_cursor_line()
249 258 if text is None:
250 259 return False
251 260 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
252 261 if complete:
253 262 self._complete()
254 263 return not complete
255 264
256 265 #---------------------------------------------------------------------------
257 266 # 'ConsoleWidget' protected interface
258 267 #---------------------------------------------------------------------------
259 268
260 269 def _context_menu_make(self, pos):
261 270 """ Reimplemented to add an action for raw copy.
262 271 """
263 272 menu = super(FrontendWidget, self)._context_menu_make(pos)
264 273 for before_action in menu.actions():
265 274 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
266 275 QtGui.QKeySequence.ExactMatch:
267 276 menu.insertAction(before_action, self._copy_raw_action)
268 277 break
269 278 return menu
270 279
271 280 def request_interrupt_kernel(self):
272 281 if self._executing:
273 282 self.interrupt_kernel()
274 283
275 284 def request_restart_kernel(self):
276 285 message = 'Are you sure you want to restart the kernel?'
277 286 self.restart_kernel(message, now=False)
278 287
279 288 def _event_filter_console_keypress(self, event):
280 289 """ Reimplemented for execution interruption and smart backspace.
281 290 """
282 291 key = event.key()
283 292 if self._control_key_down(event.modifiers(), include_command=False):
284 293
285 294 if key == QtCore.Qt.Key_C and self._executing:
286 295 self.request_interrupt_kernel()
287 296 return True
288 297
289 298 elif key == QtCore.Qt.Key_Period:
290 299 self.request_restart_kernel()
291 300 return True
292 301
293 302 elif not event.modifiers() & QtCore.Qt.AltModifier:
294 303
295 304 # Smart backspace: remove four characters in one backspace if:
296 305 # 1) everything left of the cursor is whitespace
297 306 # 2) the four characters immediately left of the cursor are spaces
298 307 if key == QtCore.Qt.Key_Backspace:
299 308 col = self._get_input_buffer_cursor_column()
300 309 cursor = self._control.textCursor()
301 310 if col > 3 and not cursor.hasSelection():
302 311 text = self._get_input_buffer_cursor_line()[:col]
303 312 if text.endswith(' ') and not text.strip():
304 313 cursor.movePosition(QtGui.QTextCursor.Left,
305 314 QtGui.QTextCursor.KeepAnchor, 4)
306 315 cursor.removeSelectedText()
307 316 return True
308 317
309 318 return super(FrontendWidget, self)._event_filter_console_keypress(event)
310 319
311 320 def _insert_continuation_prompt(self, cursor):
312 321 """ Reimplemented for auto-indentation.
313 322 """
314 323 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
315 324 cursor.insertText(' ' * self._input_splitter.indent_spaces)
316 325
317 326 #---------------------------------------------------------------------------
318 327 # 'BaseFrontendMixin' abstract interface
319 328 #---------------------------------------------------------------------------
320 329
321 330 def _handle_complete_reply(self, rep):
322 331 """ Handle replies for tab completion.
323 332 """
324 333 self.log.debug("complete: %s", rep.get('content', ''))
325 334 cursor = self._get_cursor()
326 335 info = self._request_info.get('complete')
327 336 if info and info.id == rep['parent_header']['msg_id'] and \
328 337 info.pos == cursor.position():
329 338 text = '.'.join(self._get_context())
330 339 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
331 340 self._complete_with_items(cursor, rep['content']['matches'])
332 341
333 342 def _silent_exec_callback(self, expr, callback):
334 343 """Silently execute `expr` in the kernel and call `callback` with reply
335 344
336 345 the `expr` is evaluated silently in the kernel (without) output in
337 346 the frontend. Call `callback` with the
338 347 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
339 348
340 349 Parameters
341 350 ----------
342 351 expr : string
343 352 valid string to be executed by the kernel.
344 353 callback : function
345 354 function accepting one argument, as a string. The string will be
346 355 the `repr` of the result of evaluating `expr`
347 356
348 357 The `callback` is called with the `repr()` of the result of `expr` as
349 358 first argument. To get the object, do `eval()` on the passed value.
350 359
351 360 See Also
352 361 --------
353 362 _handle_exec_callback : private method, deal with calling callback with reply
354 363
355 364 """
356 365
357 366 # generate uuid, which would be used as an indication of whether or
358 367 # not the unique request originated from here (can use msg id ?)
359 368 local_uuid = str(uuid.uuid1())
360 369 msg_id = self.kernel_client.execute('',
361 370 silent=True, user_expressions={ local_uuid:expr })
362 371 self._callback_dict[local_uuid] = callback
363 372 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
364 373
365 374 def _handle_exec_callback(self, msg):
366 375 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
367 376
368 377 Parameters
369 378 ----------
370 379 msg : raw message send by the kernel containing an `user_expressions`
371 380 and having a 'silent_exec_callback' kind.
372 381
373 382 Notes
374 383 -----
375 384 This function will look for a `callback` associated with the
376 385 corresponding message id. Association has been made by
377 386 `_silent_exec_callback`. `callback` is then called with the `repr()`
378 387 of the value of corresponding `user_expressions` as argument.
379 388 `callback` is then removed from the known list so that any message
380 389 coming again with the same id won't trigger it.
381 390
382 391 """
383 392
384 393 user_exp = msg['content'].get('user_expressions')
385 394 if not user_exp:
386 395 return
387 396 for expression in user_exp:
388 397 if expression in self._callback_dict:
389 398 self._callback_dict.pop(expression)(user_exp[expression])
390 399
391 400 def _handle_execute_reply(self, msg):
392 401 """ Handles replies for code execution.
393 402 """
394 403 self.log.debug("execute: %s", msg.get('content', ''))
395 404 msg_id = msg['parent_header']['msg_id']
396 405 info = self._request_info['execute'].get(msg_id)
397 406 # unset reading flag, because if execute finished, raw_input can't
398 407 # still be pending.
399 408 self._reading = False
400 409 if info and info.kind == 'user' and not self._hidden:
401 410 # Make sure that all output from the SUB channel has been processed
402 411 # before writing a new prompt.
403 412 self.kernel_client.iopub_channel.flush()
404 413
405 414 # Reset the ANSI style information to prevent bad text in stdout
406 415 # from messing up our colors. We're not a true terminal so we're
407 416 # allowed to do this.
408 417 if self.ansi_codes:
409 418 self._ansi_processor.reset_sgr()
410 419
411 420 content = msg['content']
412 421 status = content['status']
413 422 if status == 'ok':
414 423 self._process_execute_ok(msg)
415 424 elif status == 'error':
416 425 self._process_execute_error(msg)
417 426 elif status == 'aborted':
418 427 self._process_execute_abort(msg)
419 428
420 429 self._show_interpreter_prompt_for_reply(msg)
421 430 self.executed.emit(msg)
422 431 self._request_info['execute'].pop(msg_id)
423 432 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
424 433 self._handle_exec_callback(msg)
425 434 self._request_info['execute'].pop(msg_id)
426 435 else:
427 436 super(FrontendWidget, self)._handle_execute_reply(msg)
428 437
429 438 def _handle_input_request(self, msg):
430 439 """ Handle requests for raw_input.
431 440 """
432 441 self.log.debug("input: %s", msg.get('content', ''))
433 442 if self._hidden:
434 443 raise RuntimeError('Request for raw input during hidden execution.')
435 444
436 445 # Make sure that all output from the SUB channel has been processed
437 446 # before entering readline mode.
438 447 self.kernel_client.iopub_channel.flush()
439 448
440 449 def callback(line):
441 450 self.kernel_client.stdin_channel.input(line)
442 451 if self._reading:
443 452 self.log.debug("Got second input request, assuming first was interrupted.")
444 453 self._reading = False
445 454 self._readline(msg['content']['prompt'], callback=callback)
446 455
447 456 def _kernel_restarted_message(self, died=True):
448 457 msg = "Kernel died, restarting" if died else "Kernel restarting"
449 458 self._append_html("<br>%s<hr><br>" % msg,
450 459 before_prompt=False
451 460 )
452 461
453 462 def _handle_kernel_died(self, since_last_heartbeat):
454 463 """Handle the kernel's death (if we do not own the kernel).
455 464 """
456 465 self.log.warn("kernel died: %s", since_last_heartbeat)
457 466 if self.custom_restart:
458 467 self.custom_restart_kernel_died.emit(since_last_heartbeat)
459 468 else:
460 469 self._kernel_restarted_message(died=True)
461 470 self.reset()
462 471
463 472 def _handle_kernel_restarted(self, died=True):
464 473 """Notice that the autorestarter restarted the kernel.
465 474
466 475 There's nothing to do but show a message.
467 476 """
468 477 self.log.warn("kernel restarted")
469 478 self._kernel_restarted_message(died=died)
470 479 self.reset()
471 480
472 481 def _handle_object_info_reply(self, rep):
473 482 """ Handle replies for call tips.
474 483 """
475 484 self.log.debug("oinfo: %s", rep.get('content', ''))
476 485 cursor = self._get_cursor()
477 486 info = self._request_info.get('call_tip')
478 487 if info and info.id == rep['parent_header']['msg_id'] and \
479 488 info.pos == cursor.position():
480 489 # Get the information for a call tip. For now we format the call
481 490 # line as string, later we can pass False to format_call and
482 491 # syntax-highlight it ourselves for nicer formatting in the
483 492 # calltip.
484 493 content = rep['content']
485 494 # if this is from pykernel, 'docstring' will be the only key
486 495 if content.get('ismagic', False):
487 496 # Don't generate a call-tip for magics. Ideally, we should
488 497 # generate a tooltip, but not on ( like we do for actual
489 498 # callables.
490 499 call_info, doc = None, None
491 500 else:
492 501 call_info, doc = call_tip(content, format_call=True)
493 502 if call_info or doc:
494 503 self._call_tip_widget.show_call_info(call_info, doc)
495 504
496 505 def _handle_pyout(self, msg):
497 506 """ Handle display hook output.
498 507 """
499 508 self.log.debug("pyout: %s", msg.get('content', ''))
500 509 if not self._hidden and self._is_from_this_session(msg):
501 510 text = msg['content']['data']
502 511 self._append_plain_text(text + '\n', before_prompt=True)
503 512
504 513 def _handle_stream(self, msg):
505 514 """ Handle stdout, stderr, and stdin.
506 515 """
507 516 self.log.debug("stream: %s", msg.get('content', ''))
508 517 if not self._hidden and self._is_from_this_session(msg):
509 518 # Most consoles treat tabs as being 8 space characters. Convert tabs
510 519 # to spaces so that output looks as expected regardless of this
511 520 # widget's tab width.
512 521 text = msg['content']['data'].expandtabs(8)
513 522
514 523 self._append_plain_text(text, before_prompt=True)
515 524 self._control.moveCursor(QtGui.QTextCursor.End)
516 525
517 526 def _handle_shutdown_reply(self, msg):
518 527 """ Handle shutdown signal, only if from other console.
519 528 """
520 529 self.log.warn("shutdown: %s", msg.get('content', ''))
521 530 restart = msg.get('content', {}).get('restart', False)
522 531 if not self._hidden and not self._is_from_this_session(msg):
523 532 # got shutdown reply, request came from session other than ours
524 533 if restart:
525 534 # someone restarted the kernel, handle it
526 535 self._handle_kernel_restarted(died=False)
527 536 else:
528 537 # kernel was shutdown permanently
529 538 # this triggers exit_requested if the kernel was local,
530 539 # and a dialog if the kernel was remote,
531 540 # so we don't suddenly clear the qtconsole without asking.
532 541 if self._local_kernel:
533 542 self.exit_requested.emit(self)
534 543 else:
535 544 title = self.window().windowTitle()
536 545 reply = QtGui.QMessageBox.question(self, title,
537 546 "Kernel has been shutdown permanently. "
538 547 "Close the Console?",
539 548 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
540 549 if reply == QtGui.QMessageBox.Yes:
541 550 self.exit_requested.emit(self)
542 551
543 552 def _handle_status(self, msg):
544 553 """Handle status message"""
545 554 # This is where a busy/idle indicator would be triggered,
546 555 # when we make one.
547 556 state = msg['content'].get('execution_state', '')
548 557 if state == 'starting':
549 558 # kernel started while we were running
550 559 if self._executing:
551 560 self._handle_kernel_restarted(died=True)
552 561 elif state == 'idle':
553 562 pass
554 563 elif state == 'busy':
555 564 pass
556 565
557 566 def _started_channels(self):
558 567 """ Called when the KernelManager channels have started listening or
559 568 when the frontend is assigned an already listening KernelManager.
560 569 """
561 570 self.reset(clear=True)
562 571
563 572 #---------------------------------------------------------------------------
564 573 # 'FrontendWidget' public interface
565 574 #---------------------------------------------------------------------------
566 575
567 576 def copy_raw(self):
568 577 """ Copy the currently selected text to the clipboard without attempting
569 578 to remove prompts or otherwise alter the text.
570 579 """
571 580 self._control.copy()
572 581
573 582 def execute_file(self, path, hidden=False):
574 583 """ Attempts to execute file with 'path'. If 'hidden', no output is
575 584 shown.
576 585 """
577 586 self.execute('execfile(%r)' % path, hidden=hidden)
578 587
579 588 def interrupt_kernel(self):
580 589 """ Attempts to interrupt the running kernel.
581 590
582 591 Also unsets _reading flag, to avoid runtime errors
583 592 if raw_input is called again.
584 593 """
585 594 if self.custom_interrupt:
586 595 self._reading = False
587 596 self.custom_interrupt_requested.emit()
588 597 elif self.kernel_manager:
589 598 self._reading = False
590 599 self.kernel_manager.interrupt_kernel()
591 600 else:
592 601 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
593 602
594 603 def reset(self, clear=False):
595 604 """ Resets the widget to its initial state if ``clear`` parameter
596 605 is True, otherwise
597 606 prints a visual indication of the fact that the kernel restarted, but
598 607 does not clear the traces from previous usage of the kernel before it
599 608 was restarted. With ``clear=True``, it is similar to ``%clear``, but
600 609 also re-writes the banner and aborts execution if necessary.
601 610 """
602 611 if self._executing:
603 612 self._executing = False
604 613 self._request_info['execute'] = {}
605 614 self._reading = False
606 615 self._highlighter.highlighting_on = False
607 616
608 617 if clear:
609 618 self._control.clear()
610 619 self._append_plain_text(self.banner)
611 620 # update output marker for stdout/stderr, so that startup
612 621 # messages appear after banner:
613 622 self._append_before_prompt_pos = self._get_cursor().position()
614 623 self._show_interpreter_prompt()
615 624
616 625 def restart_kernel(self, message, now=False):
617 626 """ Attempts to restart the running kernel.
618 627 """
619 628 # FIXME: now should be configurable via a checkbox in the dialog. Right
620 629 # now at least the heartbeat path sets it to True and the manual restart
621 630 # to False. But those should just be the pre-selected states of a
622 631 # checkbox that the user could override if so desired. But I don't know
623 632 # enough Qt to go implementing the checkbox now.
624 633
625 634 if self.custom_restart:
626 635 self.custom_restart_requested.emit()
627 636 return
628 637
629 638 if self.kernel_manager:
630 639 # Pause the heart beat channel to prevent further warnings.
631 640 self.kernel_client.hb_channel.pause()
632 641
633 642 # Prompt the user to restart the kernel. Un-pause the heartbeat if
634 643 # they decline. (If they accept, the heartbeat will be un-paused
635 644 # automatically when the kernel is restarted.)
636 645 if self.confirm_restart:
637 646 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
638 647 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
639 648 message, buttons)
640 649 do_restart = result == QtGui.QMessageBox.Yes
641 650 else:
642 651 # confirm_restart is False, so we don't need to ask user
643 652 # anything, just do the restart
644 653 do_restart = True
645 654 if do_restart:
646 655 try:
647 656 self.kernel_manager.restart_kernel(now=now)
648 657 except RuntimeError as e:
649 658 self._append_plain_text(
650 659 'Error restarting kernel: %s\n' % e,
651 660 before_prompt=True
652 661 )
653 662 else:
654 663 self._append_html("<br>Restarting kernel...\n<hr><br>",
655 664 before_prompt=True,
656 665 )
657 666 else:
658 667 self.kernel_client.hb_channel.unpause()
659 668
660 669 else:
661 670 self._append_plain_text(
662 671 'Cannot restart a Kernel I did not start\n',
663 672 before_prompt=True
664 673 )
665 674
666 675 #---------------------------------------------------------------------------
667 676 # 'FrontendWidget' protected interface
668 677 #---------------------------------------------------------------------------
669 678
670 679 def _call_tip(self):
671 680 """ Shows a call tip, if appropriate, at the current cursor location.
672 681 """
673 682 # Decide if it makes sense to show a call tip
674 683 if not self.enable_calltips:
675 684 return False
676 685 cursor = self._get_cursor()
677 686 cursor.movePosition(QtGui.QTextCursor.Left)
678 687 if cursor.document().characterAt(cursor.position()) != '(':
679 688 return False
680 689 context = self._get_context(cursor)
681 690 if not context:
682 691 return False
683 692
684 693 # Send the metadata request to the kernel
685 694 name = '.'.join(context)
686 695 msg_id = self.kernel_client.object_info(name)
687 696 pos = self._get_cursor().position()
688 697 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
689 698 return True
690 699
691 700 def _complete(self):
692 701 """ Performs completion at the current cursor location.
693 702 """
694 703 context = self._get_context()
695 704 if context:
696 705 # Send the completion request to the kernel
697 706 msg_id = self.kernel_client.complete(
698 707 '.'.join(context), # text
699 708 self._get_input_buffer_cursor_line(), # line
700 709 self._get_input_buffer_cursor_column(), # cursor_pos
701 710 self.input_buffer) # block
702 711 pos = self._get_cursor().position()
703 712 info = self._CompletionRequest(msg_id, pos)
704 713 self._request_info['complete'] = info
705 714
706 715 def _get_context(self, cursor=None):
707 716 """ Gets the context for the specified cursor (or the current cursor
708 717 if none is specified).
709 718 """
710 719 if cursor is None:
711 720 cursor = self._get_cursor()
712 721 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
713 722 QtGui.QTextCursor.KeepAnchor)
714 723 text = cursor.selection().toPlainText()
715 724 return self._completion_lexer.get_context(text)
716 725
717 726 def _process_execute_abort(self, msg):
718 727 """ Process a reply for an aborted execution request.
719 728 """
720 729 self._append_plain_text("ERROR: execution aborted\n")
721 730
722 731 def _process_execute_error(self, msg):
723 732 """ Process a reply for an execution request that resulted in an error.
724 733 """
725 734 content = msg['content']
726 735 # If a SystemExit is passed along, this means exit() was called - also
727 736 # all the ipython %exit magic syntax of '-k' to be used to keep
728 737 # the kernel running
729 738 if content['ename']=='SystemExit':
730 739 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
731 740 self._keep_kernel_on_exit = keepkernel
732 741 self.exit_requested.emit(self)
733 742 else:
734 743 traceback = ''.join(content['traceback'])
735 744 self._append_plain_text(traceback)
736 745
737 746 def _process_execute_ok(self, msg):
738 747 """ Process a reply for a successful execution request.
739 748 """
740 749 payload = msg['content']['payload']
741 750 for item in payload:
742 751 if not self._process_execute_payload(item):
743 752 warning = 'Warning: received unknown payload of type %s'
744 753 print(warning % repr(item['source']))
745 754
746 755 def _process_execute_payload(self, item):
747 756 """ Process a single payload item from the list of payload items in an
748 757 execution reply. Returns whether the payload was handled.
749 758 """
750 759 # The basic FrontendWidget doesn't handle payloads, as they are a
751 760 # mechanism for going beyond the standard Python interpreter model.
752 761 return False
753 762
754 763 def _show_interpreter_prompt(self):
755 764 """ Shows a prompt for the interpreter.
756 765 """
757 766 self._show_prompt('>>> ')
758 767
759 768 def _show_interpreter_prompt_for_reply(self, msg):
760 769 """ Shows a prompt for the interpreter given an 'execute_reply' message.
761 770 """
762 771 self._show_interpreter_prompt()
763 772
764 773 #------ Signal handlers ----------------------------------------------------
765 774
766 775 def _document_contents_change(self, position, removed, added):
767 776 """ Called whenever the document's content changes. Display a call tip
768 777 if appropriate.
769 778 """
770 779 # Calculate where the cursor should be *after* the change:
771 780 position += added
772 781
773 782 document = self._control.document()
774 783 if position == self._get_cursor().position():
775 784 self._call_tip()
776 785
777 786 #------ Trait default initializers -----------------------------------------
778 787
779 788 def _banner_default(self):
780 789 """ Returns the standard Python banner.
781 790 """
782 791 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
783 792 '"license" for more information.'
784 793 return banner % (sys.version, sys.platform)
General Comments 0
You need to be logged in to leave comments. Login now