Show More
@@ -1,15 +1,18 | |||||
|
1 | from codeop import compile_command | |||
1 | import re |
|
2 | import re | |
2 | from typing import List, Tuple |
|
3 | from typing import List, Tuple | |
3 | from IPython.utils import tokenize2 |
|
4 | from IPython.utils import tokenize2 | |
4 | from IPython.utils.tokenutil import generate_tokens |
|
5 | from IPython.utils.tokenutil import generate_tokens | |
5 |
|
6 | |||
|
7 | _indent_re = re.compile(r'^[ \t]+') | |||
|
8 | ||||
6 | def leading_indent(lines): |
|
9 | def leading_indent(lines): | |
7 | """Remove leading indentation. |
|
10 | """Remove leading indentation. | |
8 |
|
11 | |||
9 | If the first line starts with a spaces or tabs, the same whitespace will be |
|
12 | If the first line starts with a spaces or tabs, the same whitespace will be | |
10 | removed from each following line. |
|
13 | removed from each following line. | |
11 | """ |
|
14 | """ | |
12 |
m = re.match( |
|
15 | m = _indent_re.match(lines[0]) | |
13 | if not m: |
|
16 | if not m: | |
14 | return lines |
|
17 | return lines | |
15 | space = m.group(0) |
|
18 | space = m.group(0) | |
@@ -373,10 +376,12 def show_linewise_tokens(s: str): | |||||
373 |
|
376 | |||
374 | class TransformerManager: |
|
377 | class TransformerManager: | |
375 | def __init__(self): |
|
378 | def __init__(self): | |
376 |
self. |
|
379 | self.cleanup_transforms = [ | |
377 | leading_indent, |
|
380 | leading_indent, | |
378 | classic_prompt, |
|
381 | classic_prompt, | |
379 | ipython_prompt, |
|
382 | ipython_prompt, | |
|
383 | ] | |||
|
384 | self.line_transforms = [ | |||
380 | cell_magic, |
|
385 | cell_magic, | |
381 | ] |
|
386 | ] | |
382 | self.token_transformers = [ |
|
387 | self.token_transformers = [ | |
@@ -424,9 +429,97 class TransformerManager: | |||||
424 | if not cell.endswith('\n'): |
|
429 | if not cell.endswith('\n'): | |
425 | cell += '\n' # Ensure every line has a newline |
|
430 | cell += '\n' # Ensure every line has a newline | |
426 | lines = cell.splitlines(keepends=True) |
|
431 | lines = cell.splitlines(keepends=True) | |
427 | for transform in self.line_transforms: |
|
432 | for transform in self.cleanup_transforms + self.line_transforms: | |
428 | #print(transform, lines) |
|
433 | #print(transform, lines) | |
429 | lines = transform(lines) |
|
434 | lines = transform(lines) | |
430 |
|
435 | |||
431 | lines = self.do_token_transforms(lines) |
|
436 | lines = self.do_token_transforms(lines) | |
432 | return ''.join(lines) |
|
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 | tf = ipt2.HelpEnd((1, 0), (2, 8)) |
|
178 | tf = ipt2.HelpEnd((1, 0), (2, 8)) | |
179 | nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2]) |
|
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