##// END OF EJS Templates
Merge pull request #4504 from takluyver/inputtransformer-syntaxerror...
Fernando Perez -
r14990:07072a49 merge
parent child Browse files
Show More
@@ -0,0 +1,6 b''
1 * :class:`IPython.core.inputsplitter.IPythonInputSplitter` no longer has a method
2 ``source_raw_reset()``, but gains :meth:`~IPython.core.inputsplitter.IPythonInputSplitter.raw_reset`
3 instead. Use of ``source_raw_reset`` can be replaced with::
4
5 raw = isp.source_raw
6 transformed = isp.source_reset()
@@ -0,0 +1,4 b''
1 * Input transformers (see :doc:`/config/inputtransforms`) may now raise
2 :exc:`SyntaxError` if they determine that input is invalid. The input
3 transformation machinery in IPython will handle displaying the exception to
4 the user and resetting state.
@@ -501,8 +501,14 b' class IPythonInputSplitter(InputSplitter):'
501 501 self.source_raw = ''
502 502 self.transformer_accumulating = False
503 503 self.within_python_line = False
504
504 505 for t in self.transforms:
506 try:
505 507 t.reset()
508 except SyntaxError:
509 # Nothing that calls reset() expects to handle transformer
510 # errors
511 pass
506 512
507 513 def flush_transformers(self):
508 514 def _flush(transform, out):
@@ -519,18 +525,19 b' class IPythonInputSplitter(InputSplitter):'
519 525 if out is not None:
520 526 self._store(out)
521 527
522 def source_raw_reset(self):
523 """Return input and raw source and perform a full reset.
528 def raw_reset(self):
529 """Return raw input only and perform a full reset.
524 530 """
525 self.flush_transformers()
526 out = self.source
527 out_r = self.source_raw
531 out = self.source_raw
528 532 self.reset()
529 return out, out_r
533 return out
530 534
531 535 def source_reset(self):
536 try:
532 537 self.flush_transformers()
533 return super(IPythonInputSplitter, self).source_reset()
538 return self.source
539 finally:
540 self.reset()
534 541
535 542 def push_accepts_more(self):
536 543 if self.transformer_accumulating:
@@ -542,8 +549,12 b' class IPythonInputSplitter(InputSplitter):'
542 549 """Process and translate a cell of input.
543 550 """
544 551 self.reset()
552 try:
545 553 self.push(cell)
546 return self.source_reset()
554 self.flush_transformers()
555 return self.source
556 finally:
557 self.reset()
547 558
548 559 def push(self, lines):
549 560 """Push one or more lines of IPython input.
@@ -52,6 +52,9 b' class InputTransformer(with_metaclass(abc.ABCMeta, object)):'
52 52 input or None if the transformer is waiting for more input.
53 53
54 54 Must be overridden by subclasses.
55
56 Implementations may raise ``SyntaxError`` if the input is invalid. No
57 other exceptions may be raised.
55 58 """
56 59 pass
57 60
@@ -2649,28 +2649,28 b' class InteractiveShell(SingletonConfigurable):'
2649 2649 if silent:
2650 2650 store_history = False
2651 2651
2652 self.input_transformer_manager.push(raw_cell)
2653 cell = self.input_transformer_manager.source_reset()
2654
2655 # Our own compiler remembers the __future__ environment. If we want to
2656 # run code with a separate __future__ environment, use the default
2657 # compiler
2658 compiler = self.compile if shell_futures else CachingCompiler()
2659
2660 with self.builtin_trap:
2661 prefilter_failed = False
2652 # If any of our input transformation (input_transformer_manager or
2653 # prefilter_manager) raises an exception, we store it in this variable
2654 # so that we can display the error after logging the input and storing
2655 # it in the history.
2656 preprocessing_exc_tuple = None
2657 try:
2658 # Static input transformations
2659 cell = self.input_transformer_manager.transform_cell(raw_cell)
2660 except SyntaxError:
2661 preprocessing_exc_tuple = sys.exc_info()
2662 cell = raw_cell # cell has to exist so it can be stored/logged
2663 else:
2662 2664 if len(cell.splitlines()) == 1:
2665 # Dynamic transformations - only applied for single line commands
2666 with self.builtin_trap:
2663 2667 try:
2664 2668 # use prefilter_lines to handle trailing newlines
2665 2669 # restore trailing newline for ast.parse
2666 2670 cell = self.prefilter_manager.prefilter_lines(cell) + '\n'
2667 except AliasError as e:
2668 error(e)
2669 prefilter_failed = True
2670 2671 except Exception:
2671 2672 # don't allow prefilter errors to crash IPython
2672 self.showtraceback()
2673 prefilter_failed = True
2673 preprocessing_exc_tuple = sys.exc_info()
2674 2674
2675 2675 # Store raw and processed history
2676 2676 if store_history:
@@ -2679,11 +2679,23 b' class InteractiveShell(SingletonConfigurable):'
2679 2679 if not silent:
2680 2680 self.logger.log(cell, raw_cell)
2681 2681
2682 if not prefilter_failed:
2683 # don't run if prefilter failed
2682 # Display the exception if input processing failed.
2683 if preprocessing_exc_tuple is not None:
2684 self.showtraceback(preprocessing_exc_tuple)
2685 if store_history:
2686 self.execution_count += 1
2687 return
2688
2689 # Our own compiler remembers the __future__ environment. If we want to
2690 # run code with a separate __future__ environment, use the default
2691 # compiler
2692 compiler = self.compile if shell_futures else CachingCompiler()
2693
2694 with self.builtin_trap:
2684 2695 cell_name = self.compile.cache(cell, self.execution_count)
2685 2696
2686 2697 with self.display_trap:
2698 # Compile to bytecode
2687 2699 try:
2688 2700 code_ast = compiler.ast_parse(cell, filename=cell_name)
2689 2701 except IndentationError:
@@ -2698,8 +2710,10 b' class InteractiveShell(SingletonConfigurable):'
2698 2710 self.execution_count += 1
2699 2711 return None
2700 2712
2713 # Apply AST transformations
2701 2714 code_ast = self.transform_ast(code_ast)
2702 2715
2716 # Execute the user code
2703 2717 interactivity = "none" if silent else self.ast_node_interactivity
2704 2718 self.run_ast_nodes(code_ast.body, cell_name,
2705 2719 interactivity=interactivity, compiler=compiler)
@@ -412,7 +412,8 b' class IPythonInputTestCase(InputSplitterTestCase):'
412 412 continue
413 413
414 414 isp.push(raw+'\n')
415 out, out_raw = isp.source_raw_reset()
415 out_raw = isp.source_raw
416 out = isp.source_reset()
416 417 self.assertEqual(out.rstrip(), out_t,
417 418 tt.pair_fail_msg.format("inputsplitter",raw, out_t, out))
418 419 self.assertEqual(out_raw.rstrip(), raw.rstrip())
@@ -431,7 +432,8 b' class IPythonInputTestCase(InputSplitterTestCase):'
431 432 isp.push(lraw)
432 433 raw_parts.append(lraw)
433 434
434 out, out_raw = isp.source_raw_reset()
435 out_raw = isp.source_raw
436 out = isp.source_reset()
435 437 out_t = '\n'.join(out_t_parts).rstrip()
436 438 raw = '\n'.join(raw_parts).rstrip()
437 439 self.assertEqual(out.rstrip(), out_t)
@@ -498,7 +500,8 b" if __name__ == '__main__':"
498 500 # Here we just return input so we can use it in a test suite, but a
499 501 # real interpreter would instead send it for execution somewhere.
500 502 #src = isp.source; raise EOFError # dbg
501 src, raw = isp.source_raw_reset()
503 raw = isp.source_raw
504 src = isp.source_reset()
502 505 print('Input source was:\n', src)
503 506 print('Raw source was:\n', raw)
504 507 except EOFError:
@@ -545,9 +548,7 b' class CellMagicsCommon(object):'
545 548
546 549 def test_whole_cell(self):
547 550 src = "%%cellm line\nbody\n"
548 sp = self.sp
549 sp.push(src)
550 out = sp.source_reset()
551 out = self.sp.transform_cell(src)
551 552 ref = u"get_ipython().run_cell_magic({u}'cellm', {u}'line', {u}'body')\n"
552 553 nt.assert_equal(out, py3compat.u_format(ref))
553 554
@@ -33,6 +33,7 b' from os.path import join'
33 33 import nose.tools as nt
34 34
35 35 # Our own
36 from IPython.core.inputtransformer import InputTransformer
36 37 from IPython.testing.decorators import skipif, skip_win32, onlyif_unicode_paths
37 38 from IPython.testing import tools as tt
38 39 from IPython.utils import io
@@ -674,4 +675,41 b' def test_user_expression():'
674 675
675 676
676 677
678 class TestSyntaxErrorTransformer(unittest.TestCase):
679 """Check that SyntaxError raised by an input transformer is handled by run_cell()"""
680
681 class SyntaxErrorTransformer(InputTransformer):
682
683 def push(self, line):
684 pos = line.find('syntaxerror')
685 if pos >= 0:
686 e = SyntaxError('input contains "syntaxerror"')
687 e.text = line
688 e.offset = pos + 1
689 raise e
690 return line
691
692 def reset(self):
693 pass
694
695 def setUp(self):
696 self.transformer = TestSyntaxErrorTransformer.SyntaxErrorTransformer()
697 ip.input_splitter.python_line_transforms.append(self.transformer)
698 ip.input_transformer_manager.python_line_transforms.append(self.transformer)
699
700 def tearDown(self):
701 ip.input_splitter.python_line_transforms.remove(self.transformer)
702 ip.input_transformer_manager.python_line_transforms.remove(self.transformer)
703
704 def test_syntaxerror_input_transformer(self):
705 with tt.AssertPrints('1234'):
706 ip.run_cell('1234')
707 with tt.AssertPrints('SyntaxError: invalid syntax'):
708 ip.run_cell('1 2 3') # plain python syntax error
709 with tt.AssertPrints('SyntaxError: input contains "syntaxerror"'):
710 ip.run_cell('2345 # syntaxerror') # input transformer syntax error
711 with tt.AssertPrints('3456'):
712 ip.run_cell('3456')
713
714
677 715
@@ -224,7 +224,10 b' class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):'
224 224 'interactive' is True; otherwise, it is False.
225 225 """
226 226 self._input_splitter.reset()
227 try:
227 228 complete = self._input_splitter.push(source)
229 except SyntaxError:
230 return True
228 231 if interactive:
229 232 complete = not self._input_splitter.push_accepts_more()
230 233 return complete
@@ -343,7 +343,7 b' class EmbeddedSphinxShell(object):'
343 343 splitter.push(line)
344 344 more = splitter.push_accepts_more()
345 345 if not more:
346 source_raw = splitter.source_raw_reset()[1]
346 source_raw = splitter.raw_reset()
347 347 self.IP.run_cell(source_raw, store_history=store_history)
348 348 finally:
349 349 sys.stdout = stdout
@@ -464,7 +464,7 b' class ZMQTerminalInteractiveShell(TerminalInteractiveShell):'
464 464 #double-guard against keyboardinterrupts during kbdint handling
465 465 try:
466 466 self.write('\nKeyboardInterrupt\n')
467 source_raw = self.input_splitter.source_raw_reset()[1]
467 source_raw = self.input_splitter.raw_reset()
468 468 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
469 469 more = False
470 470 except KeyboardInterrupt:
@@ -486,13 +486,18 b' class ZMQTerminalInteractiveShell(TerminalInteractiveShell):'
486 486 # asynchronously by signal handlers, for example.
487 487 self.showtraceback()
488 488 else:
489 try:
489 490 self.input_splitter.push(line)
490 491 more = self.input_splitter.push_accepts_more()
492 except SyntaxError:
493 # Run the code directly - run_cell takes care of displaying
494 # the exception.
495 more = False
491 496 if (self.SyntaxTB.last_syntax_error and
492 497 self.autoedit_syntax):
493 498 self.edit_syntax_error()
494 499 if not more:
495 source_raw = self.input_splitter.source_raw_reset()[1]
500 source_raw = self.input_splitter.raw_reset()
496 501 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
497 502 self.run_cell(source_raw)
498 503
@@ -529,7 +529,7 b' class TerminalInteractiveShell(InteractiveShell):'
529 529 #double-guard against keyboardinterrupts during kbdint handling
530 530 try:
531 531 self.write('\nKeyboardInterrupt\n')
532 source_raw = self.input_splitter.source_raw_reset()[1]
532 source_raw = self.input_splitter.raw_reset()
533 533 hlen_b4_cell = \
534 534 self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
535 535 more = False
@@ -552,13 +552,18 b' class TerminalInteractiveShell(InteractiveShell):'
552 552 # asynchronously by signal handlers, for example.
553 553 self.showtraceback()
554 554 else:
555 try:
555 556 self.input_splitter.push(line)
556 557 more = self.input_splitter.push_accepts_more()
558 except SyntaxError:
559 # Run the code directly - run_cell takes care of displaying
560 # the exception.
561 more = False
557 562 if (self.SyntaxTB.last_syntax_error and
558 563 self.autoedit_syntax):
559 564 self.edit_syntax_error()
560 565 if not more:
561 source_raw = self.input_splitter.source_raw_reset()[1]
566 source_raw = self.input_splitter.raw_reset()
562 567 self.run_cell(source_raw, store_history=True)
563 568 hlen_b4_cell = \
564 569 self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
@@ -17,12 +17,68 b' Authors'
17 17 #-----------------------------------------------------------------------------
18 18 # stdlib
19 19 import sys
20 import types
20 21 import unittest
21 22
23 from IPython.core.inputtransformer import InputTransformer
22 24 from IPython.testing.decorators import skipif
23 25 from IPython.utils import py3compat
24 26 from IPython.testing import tools as tt
25 27
28 # Decorator for interaction loop tests -----------------------------------------
29
30 class mock_input_helper(object):
31 """Machinery for tests of the main interact loop.
32
33 Used by the mock_input decorator.
34 """
35 def __init__(self, testgen):
36 self.testgen = testgen
37 self.exception = None
38 self.ip = get_ipython()
39
40 def __enter__(self):
41 self.orig_raw_input = self.ip.raw_input
42 self.ip.raw_input = self.fake_input
43 return self
44
45 def __exit__(self, etype, value, tb):
46 self.ip.raw_input = self.orig_raw_input
47
48 def fake_input(self, prompt):
49 try:
50 return next(self.testgen)
51 except StopIteration:
52 self.ip.exit_now = True
53 return u''
54 except:
55 self.exception = sys.exc_info()
56 self.ip.exit_now = True
57 return u''
58
59 def mock_input(testfunc):
60 """Decorator for tests of the main interact loop.
61
62 Write the test as a generator, yield-ing the input strings, which IPython
63 will see as if they were typed in at the prompt.
64 """
65 def test_method(self):
66 testgen = testfunc(self)
67 with mock_input_helper(testgen) as mih:
68 mih.ip.interact(display_banner=False)
69
70 if mih.exception is not None:
71 # Re-raise captured exception
72 etype, value, tb = mih.exception
73 import traceback
74 traceback.print_tb(tb, file=sys.stdout)
75 del tb # Avoid reference loop
76 raise value
77
78 return test_method
79
80 # Test classes -----------------------------------------------------------------
81
26 82 class InteractiveShellTestCase(unittest.TestCase):
27 83 def rl_hist_entries(self, rl, n):
28 84 """Get last n readline history entries as a list"""
@@ -171,6 +227,42 b' class InteractiveShellTestCase(unittest.TestCase):'
171 227 expected = [ py3compat.unicode_to_str(e, enc) for e in expected ]
172 228 self.assertEqual(hist, expected)
173 229
230 @mock_input
231 def test_inputtransformer_syntaxerror(self):
232 ip = get_ipython()
233 transformer = SyntaxErrorTransformer()
234 ip.input_splitter.python_line_transforms.append(transformer)
235 ip.input_transformer_manager.python_line_transforms.append(transformer)
236
237 try:
238 #raise Exception
239 with tt.AssertPrints('4', suppress=False):
240 yield u'print(2*2)'
241
242 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
243 yield u'print(2345) # syntaxerror'
244
245 with tt.AssertPrints('16', suppress=False):
246 yield u'print(4*4)'
247
248 finally:
249 ip.input_splitter.python_line_transforms.remove(transformer)
250 ip.input_transformer_manager.python_line_transforms.remove(transformer)
251
252
253 class SyntaxErrorTransformer(InputTransformer):
254 def push(self, line):
255 pos = line.find('syntaxerror')
256 if pos >= 0:
257 e = SyntaxError('input contains "syntaxerror"')
258 e.text = line
259 e.offset = pos + 1
260 raise e
261 return line
262
263 def reset(self):
264 pass
265
174 266 class TerminalMagicsTestCase(unittest.TestCase):
175 267 def test_paste_magics_message(self):
176 268 """Test that an IndentationError while using paste magics doesn't
@@ -44,6 +44,11 b' class IOStream:'
44 44 for meth in filter(clone, dir(stream)):
45 45 setattr(self, meth, getattr(stream, meth))
46 46
47 def __repr__(self):
48 cls = self.__class__
49 tpl = '{mod}.{cls}({args})'
50 return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)
51
47 52 def write(self,data):
48 53 try:
49 54 self._swrite(data)
@@ -46,6 +46,14 b' it gets added to both, e.g.::'
46 46 ip.input_splitter.logical_line_transforms.append(my_transformer())
47 47 ip.input_transformer_manager.logical_line_transforms.append(my_transformer())
48 48
49 These transformers may raise :exc:`SyntaxError` if the input code is invalid, but
50 in most cases it is clearer to pass unrecognised code through unmodified and let
51 Python's own parser decide whether it is valid.
52
53 .. versionchanged:: 2.0
54
55 Added the option to raise :exc:`SyntaxError`.
56
49 57 Stateless transformations
50 58 -------------------------
51 59
General Comments 0
You need to be logged in to leave comments. Login now