Show More
@@ -1,15 +1,18 | |||
|
1 | from codeop import compile_command | |
|
1 | 2 | import re |
|
2 | 3 | from typing import List, Tuple |
|
3 | 4 | from IPython.utils import tokenize2 |
|
4 | 5 | from IPython.utils.tokenutil import generate_tokens |
|
5 | 6 | |
|
7 | _indent_re = re.compile(r'^[ \t]+') | |
|
8 | ||
|
6 | 9 | def leading_indent(lines): |
|
7 | 10 | """Remove leading indentation. |
|
8 | 11 | |
|
9 | 12 | If the first line starts with a spaces or tabs, the same whitespace will be |
|
10 | 13 | removed from each following line. |
|
11 | 14 | """ |
|
12 |
m = re.match( |
|
|
15 | m = _indent_re.match(lines[0]) | |
|
13 | 16 | if not m: |
|
14 | 17 | return lines |
|
15 | 18 | space = m.group(0) |
@@ -373,10 +376,12 def show_linewise_tokens(s: str): | |||
|
373 | 376 | |
|
374 | 377 | class TransformerManager: |
|
375 | 378 | def __init__(self): |
|
376 |
self. |
|
|
379 | self.cleanup_transforms = [ | |
|
377 | 380 | leading_indent, |
|
378 | 381 | classic_prompt, |
|
379 | 382 | ipython_prompt, |
|
383 | ] | |
|
384 | self.line_transforms = [ | |
|
380 | 385 | cell_magic, |
|
381 | 386 | ] |
|
382 | 387 | self.token_transformers = [ |
@@ -424,9 +429,97 class TransformerManager: | |||
|
424 | 429 | if not cell.endswith('\n'): |
|
425 | 430 | cell += '\n' # Ensure every line has a newline |
|
426 | 431 | lines = cell.splitlines(keepends=True) |
|
427 | for transform in self.line_transforms: | |
|
432 | for transform in self.cleanup_transforms + self.line_transforms: | |
|
428 | 433 | #print(transform, lines) |
|
429 | 434 | lines = transform(lines) |
|
430 | 435 | |
|
431 | 436 | lines = self.do_token_transforms(lines) |
|
432 | 437 | return ''.join(lines) |
|
438 | ||
|
439 | def check_complete(self, cell: str): | |
|
440 | """Return whether a block of code is ready to execute, or should be continued | |
|
441 | ||
|
442 | Parameters | |
|
443 | ---------- | |
|
444 | source : string | |
|
445 | Python input code, which can be multiline. | |
|
446 | ||
|
447 | Returns | |
|
448 | ------- | |
|
449 | status : str | |
|
450 | One of 'complete', 'incomplete', or 'invalid' if source is not a | |
|
451 | prefix of valid code. | |
|
452 | indent_spaces : int or None | |
|
453 | The number of spaces by which to indent the next line of code. If | |
|
454 | status is not 'incomplete', this is None. | |
|
455 | """ | |
|
456 | if not cell.endswith('\n'): | |
|
457 | cell += '\n' # Ensure every line has a newline | |
|
458 | lines = cell.splitlines(keepends=True) | |
|
459 | if cell.rstrip().endswith('\\'): | |
|
460 | # Explicit backslash continuation | |
|
461 | return 'incomplete', find_last_indent(lines) | |
|
462 | ||
|
463 | try: | |
|
464 | for transform in self.cleanup_transforms: | |
|
465 | lines = transform(lines) | |
|
466 | except SyntaxError: | |
|
467 | return 'invalid', None | |
|
468 | ||
|
469 | if lines[0].startswith('%%'): | |
|
470 | # Special case for cell magics - completion marked by blank line | |
|
471 | if lines[-1].strip(): | |
|
472 | return 'incomplete', find_last_indent(lines) | |
|
473 | else: | |
|
474 | return 'complete', None | |
|
475 | ||
|
476 | try: | |
|
477 | for transform in self.line_transforms: | |
|
478 | lines = transform(lines) | |
|
479 | lines = self.do_token_transforms(lines) | |
|
480 | except SyntaxError: | |
|
481 | return 'invalid', None | |
|
482 | ||
|
483 | tokens_by_line = make_tokens_by_line(lines) | |
|
484 | if tokens_by_line[-1][-1].type != tokenize2.ENDMARKER: | |
|
485 | # We're in a multiline string or expression | |
|
486 | return 'incomplete', find_last_indent(lines) | |
|
487 | ||
|
488 | # Find the last token on the previous line that's not NEWLINE or COMMENT | |
|
489 | toks_last_line = tokens_by_line[-2] | |
|
490 | ix = len(toks_last_line) - 1 | |
|
491 | while ix >= 0 and toks_last_line[ix].type in {tokenize2.NEWLINE, | |
|
492 | tokenize2.COMMENT}: | |
|
493 | ix -= 1 | |
|
494 | ||
|
495 | if toks_last_line[ix].string == ':': | |
|
496 | # The last line starts a block (e.g. 'if foo:') | |
|
497 | ix = 0 | |
|
498 | while toks_last_line[ix].type in {tokenize2.INDENT, tokenize2.DEDENT}: | |
|
499 | ix += 1 | |
|
500 | indent = toks_last_line[ix].start[1] | |
|
501 | return 'incomplete', indent + 4 | |
|
502 | ||
|
503 | # If there's a blank line at the end, assume we're ready to execute. | |
|
504 | if not lines[-1].strip(): | |
|
505 | return 'complete', None | |
|
506 | ||
|
507 | # At this point, our checks think the code is complete (or invalid). | |
|
508 | # We'll use codeop.compile_command to check this with the real parser. | |
|
509 | ||
|
510 | try: | |
|
511 | res = compile_command(''.join(lines), symbol='exec') | |
|
512 | except (SyntaxError, OverflowError, ValueError, TypeError, | |
|
513 | MemoryError, SyntaxWarning): | |
|
514 | return 'invalid', None | |
|
515 | else: | |
|
516 | if res is None: | |
|
517 | return 'incomplete', find_last_indent(lines) | |
|
518 | return 'complete', None | |
|
519 | ||
|
520 | ||
|
521 | def find_last_indent(lines): | |
|
522 | m = _indent_re.match(lines[-1]) | |
|
523 | if not m: | |
|
524 | return 0 | |
|
525 | return len(m.group(0).replace('\t', ' '*4)) |
@@ -177,3 +177,12 def test_transform_help(): | |||
|
177 | 177 | |
|
178 | 178 | tf = ipt2.HelpEnd((1, 0), (2, 8)) |
|
179 | 179 | nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2]) |
|
180 | ||
|
181 | def test_check_complete(): | |
|
182 | tm = ipt2.TransformerManager() | |
|
183 | nt.assert_equal(tm.check_complete("a = 1"), ('complete', None)) | |
|
184 | nt.assert_equal(tm.check_complete("for a in range(5):"), ('incomplete', 4)) | |
|
185 | nt.assert_equal(tm.check_complete("raise = 2"), ('invalid', None)) | |
|
186 | nt.assert_equal(tm.check_complete("a = [1,\n2,"), ('incomplete', 0)) | |
|
187 | nt.assert_equal(tm.check_complete("a = '''\n hi"), ('incomplete', 3)) | |
|
188 | nt.assert_equal(tm.check_complete("def a():\n x=1\n global x"), ('invalid', None)) |
General Comments 0
You need to be logged in to leave comments.
Login now