##// END OF EJS Templates
Start adding code for checking when input is complete
Thomas Kluyver -
Show More
@@ -1,15 +1,18 b''
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(r'^[ \t]+', lines[0])
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 b' def show_linewise_tokens(s: str):'
373
376
374 class TransformerManager:
377 class TransformerManager:
375 def __init__(self):
378 def __init__(self):
376 self.line_transforms = [
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 b' 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 b' 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