Path: blob/master/build/pkgs/ipython/patches/0001-Allow-InputTransformers-to-raise-SyntaxErrors.patch
8815 views
From 23df1eb24e9d217788dd89f19b4f1c49d8ce9adf Mon Sep 17 00:00:00 20011From: Volker Braun <[email protected]>2Date: Wed, 21 Aug 2013 22:03:39 +01003Subject: [PATCH] Allow InputTransformers to raise SyntaxErrors.45---6IPython/core/inputsplitter.py | 33 ++-7IPython/core/inputtransformer.py | 3 +8IPython/core/interactiveshell.py | 308 +++++++++++----------9IPython/core/tests/test_inputsplitter.py | 23 +-10IPython/core/tests/test_interactiveshell.py | 160 +++++++----11IPython/qt/console/frontend_widget.py | 5 +-12IPython/sphinxext/ipython_directive.py | 2 +-13IPython/terminal/console/interactiveshell.py | 13 +-14IPython/terminal/interactiveshell.py | 13 +-15IPython/terminal/tests/test_interactivshell.py | 92 ++++++16IPython/utils/io.py | 5 +17docs/source/config/inputtransforms.rst | 8 +18.../pr/incompat-inputsplitter-source-raw-reset.rst | 6 +19.../whatsnew/pr/inputtransformer-syntaxerrors.rst | 4 +2014 files changed, 435 insertions(+), 240 deletions(-)21create mode 100644 docs/source/whatsnew/pr/incompat-inputsplitter-source-raw-reset.rst22create mode 100644 docs/source/whatsnew/pr/inputtransformer-syntaxerrors.rst2324diff --git a/IPython/core/inputsplitter.py b/IPython/core/inputsplitter.py25index 97c199d..6885490 10064426--- a/IPython/core/inputsplitter.py27+++ b/IPython/core/inputsplitter.py28@@ -535,8 +535,14 @@ def reset(self):29self.source_raw = ''30self.transformer_accumulating = False31self.within_python_line = False32+33for t in self.transforms:34- t.reset()35+ try:36+ t.reset()37+ except SyntaxError:38+ # Nothing that calls reset() expects to handle transformer39+ # errors40+ pass4142def flush_transformers(self):43def _flush(transform, out):44@@ -553,18 +559,19 @@ def _flush(transform, out):45if out is not None:46self._store(out)4748- def source_raw_reset(self):49- """Return input and raw source and perform a full reset.50+ def raw_reset(self):51+ """Return raw input only and perform a full reset.52"""53- self.flush_transformers()54- out = self.source55- out_r = self.source_raw56+ out = self.source_raw57self.reset()58- return out, out_r59+ return out6061def source_reset(self):62- self.flush_transformers()63- return super(IPythonInputSplitter, self).source_reset()64+ try:65+ self.flush_transformers()66+ return self.source67+ finally:68+ self.reset()6970def push_accepts_more(self):71if self.transformer_accumulating:72@@ -576,8 +583,12 @@ def transform_cell(self, cell):73"""Process and translate a cell of input.74"""75self.reset()76- self.push(cell)77- return self.source_reset()78+ try:79+ self.push(cell)80+ self.flush_transformers()81+ return self.source82+ finally:83+ self.reset()8485def push(self, lines):86"""Push one or more lines of IPython input.87diff --git a/IPython/core/inputtransformer.py b/IPython/core/inputtransformer.py88index 83edf48..eef71a4 10064489--- a/IPython/core/inputtransformer.py90+++ b/IPython/core/inputtransformer.py91@@ -43,6 +43,9 @@ def push(self, line):92input or None if the transformer is waiting for more input.9394Must be overridden by subclasses.95+96+ Implementations may raise ``SyntaxError`` if the input is invalid. No97+ other exceptions may be raised.98"""99pass100101diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py102index 2f96ac6..daa9a3c 100644103--- a/IPython/core/interactiveshell.py104+++ b/IPython/core/interactiveshell.py105@@ -2600,12 +2600,46 @@ def run_cell(self, raw_cell, store_history=False, silent=False, shell_futures=Tr106"""107if (not raw_cell) or raw_cell.isspace():108return109-110+111if silent:112store_history = False113114- self.input_transformer_manager.push(raw_cell)115- cell = self.input_transformer_manager.source_reset()116+ # If any of our input transformation (input_transformer_manager or117+ # prefilter_manager) raises an exception, we store it in this variable118+ # so that we can display the error after logging the input and storing119+ # it in the history.120+ preprocessing_exc_tuple = None121+ try:122+ # Static input transformations123+ cell = self.input_transformer_manager.transform_cell(raw_cell)124+ except SyntaxError:125+ preprocessing_exc_tuple = sys.exc_info()126+ cell = raw_cell # cell has to exist so it can be stored/logged127+ else:128+ if len(cell.splitlines()) == 1:129+ # Dynamic transformations - only applied for single line commands130+ with self.builtin_trap:131+ try:132+ # use prefilter_lines to handle trailing newlines133+ # restore trailing newline for ast.parse134+ cell = self.prefilter_manager.prefilter_lines(cell) + '\n'135+ except Exception:136+ # don't allow prefilter errors to crash IPython137+ preprocessing_exc_tuple = sys.exc_info()138+139+ # Store raw and processed history140+ if store_history:141+ self.history_manager.store_inputs(self.execution_count,142+ cell, raw_cell)143+ if not silent:144+ self.logger.log(cell, raw_cell)145+146+ # Display the exception if input processing failed.147+ if preprocessing_exc_tuple is not None:148+ self.showtraceback(preprocessing_exc_tuple)149+ if store_history:150+ self.execution_count += 1151+ return152153# Our own compiler remembers the __future__ environment. If we want to154# run code with a separate __future__ environment, use the default155@@ -2613,73 +2647,53 @@ def run_cell(self, raw_cell, store_history=False, silent=False, shell_futures=Tr156compiler = self.compile if shell_futures else CachingCompiler()157158with self.builtin_trap:159- prefilter_failed = False160- if len(cell.splitlines()) == 1:161- try:162- # use prefilter_lines to handle trailing newlines163- # restore trailing newline for ast.parse164- cell = self.prefilter_manager.prefilter_lines(cell) + '\n'165- except AliasError as e:166- error(e)167- prefilter_failed = True168- except Exception:169- # don't allow prefilter errors to crash IPython170- self.showtraceback()171- prefilter_failed = True172-173- # Store raw and processed history174- if store_history:175- self.history_manager.store_inputs(self.execution_count,176- cell, raw_cell)177- if not silent:178- self.logger.log(cell, raw_cell)179-180- if not prefilter_failed:181- # don't run if prefilter failed182- cell_name = self.compile.cache(cell, self.execution_count)183+ cell_name = self.compile.cache(cell, self.execution_count)184185- with self.display_trap:186+ with self.display_trap:187+ # Compile to bytecode188+ try:189+ code_ast = compiler.ast_parse(cell, filename=cell_name)190+ except IndentationError:191+ self.showindentationerror()192+ if store_history:193+ self.execution_count += 1194+ return None195+ except (OverflowError, SyntaxError, ValueError, TypeError,196+ MemoryError):197+ self.showsyntaxerror()198+ if store_history:199+ self.execution_count += 1200+ return None201+202+ # Apply AST transformations203+ code_ast = self.transform_ast(code_ast)204+205+ # Execute the user code206+ interactivity = "none" if silent else self.ast_node_interactivity207+ self.run_ast_nodes(code_ast.body, cell_name,208+ interactivity=interactivity, compiler=compiler)209+210+ # Execute any registered post-execution functions.211+ # unless we are silent212+ post_exec = [] if silent else self._post_execute.iteritems()213+214+ for func, status in post_exec:215+ if self.disable_failing_post_execute and not status:216+ continue217try:218- code_ast = compiler.ast_parse(cell, filename=cell_name)219- except IndentationError:220- self.showindentationerror()221- if store_history:222- self.execution_count += 1223- return None224- except (OverflowError, SyntaxError, ValueError, TypeError,225- MemoryError):226- self.showsyntaxerror()227- if store_history:228- self.execution_count += 1229- return None230-231- code_ast = self.transform_ast(code_ast)232-233- interactivity = "none" if silent else self.ast_node_interactivity234- self.run_ast_nodes(code_ast.body, cell_name,235- interactivity=interactivity, compiler=compiler)236-237- # Execute any registered post-execution functions.238- # unless we are silent239- post_exec = [] if silent else self._post_execute.iteritems()240-241- for func, status in post_exec:242- if self.disable_failing_post_execute and not status:243- continue244- try:245- func()246- except KeyboardInterrupt:247- print("\nKeyboardInterrupt", file=io.stderr)248- except Exception:249- # register as failing:250- self._post_execute[func] = False251- self.showtraceback()252- print('\n'.join([253- "post-execution function %r produced an error." % func,254- "If this problem persists, you can disable failing post-exec functions with:",255- "",256- " get_ipython().disable_failing_post_execute = True"257- ]), file=io.stderr)258+ func()259+ except KeyboardInterrupt:260+ print("\nKeyboardInterrupt", file=io.stderr)261+ except Exception:262+ # register as failing:263+ self._post_execute[func] = False264+ self.showtraceback()265+ print('\n'.join([266+ "post-execution function %r produced an error." % func,267+ "If this problem persists, you can disable failing post-exec functions with:",268+ "",269+ " get_ipython().disable_failing_post_execute = True"270+ ]), file=io.stderr)271272if store_history:273# Write output to the database. Does nothing unless274diff --git a/IPython/core/tests/test_inputsplitter.py b/IPython/core/tests/test_inputsplitter.py275index 9508979..196384d 100644276--- a/IPython/core/tests/test_inputsplitter.py277+++ b/IPython/core/tests/test_inputsplitter.py278@@ -410,7 +410,8 @@ def test_syntax(self):279continue280281isp.push(raw+'\n')282- out, out_raw = isp.source_raw_reset()283+ out_raw = isp.source_raw284+ out = isp.source_reset()285self.assertEqual(out.rstrip(), out_t,286tt.pair_fail_msg.format("inputsplitter",raw, out_t, out))287self.assertEqual(out_raw.rstrip(), raw.rstrip())288@@ -424,12 +425,13 @@ def test_syntax_multiline(self):289for lraw, out_t_part in line_pairs:290if out_t_part is not None:291out_t_parts.append(out_t_part)292-293+294if lraw is not None:295isp.push(lraw)296raw_parts.append(lraw)297298- out, out_raw = isp.source_raw_reset()299+ out_raw = isp.source_raw300+ out = isp.source_reset()301out_t = '\n'.join(out_t_parts).rstrip()302raw = '\n'.join(raw_parts).rstrip()303self.assertEqual(out.rstrip(), out_t)304@@ -496,7 +498,8 @@ def test_cellmagic_preempt(self):305# Here we just return input so we can use it in a test suite, but a306# real interpreter would instead send it for execution somewhere.307#src = isp.source; raise EOFError # dbg308- src, raw = isp.source_raw_reset()309+ raw = isp.source_raw310+ src = isp.source_reset()311print 'Input source was:\n', src312print 'Raw source was:\n', raw313except EOFError:314@@ -543,12 +546,10 @@ class CellMagicsCommon(object):315316def test_whole_cell(self):317src = "%%cellm line\nbody\n"318- sp = self.sp319- sp.push(src)320- out = sp.source_reset()321+ out = self.sp.transform_cell(src)322ref = u"get_ipython().run_cell_magic({u}'cellm', {u}'line', {u}'body')\n"323nt.assert_equal(out, py3compat.u_format(ref))324-325+326def test_cellmagic_help(self):327self.sp.push('%%cellm?')328nt.assert_false(self.sp.push_accepts_more())329diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py330index 5176bd1..9e8a000 100644331--- a/IPython/core/tests/test_interactiveshell.py332+++ b/IPython/core/tests/test_interactiveshell.py333@@ -33,6 +33,7 @@334import nose.tools as nt335336# Our own337+from IPython.core.inputtransformer import InputTransformer338from IPython.testing.decorators import skipif, onlyif_unicode_paths339from IPython.testing import tools as tt340from IPython.utils import io341@@ -637,16 +638,53 @@ def test_user_expression():342data = a['data']343metadata = a['metadata']344nt.assert_equal(data.get('text/plain'), '3')345-346+347b = r['b']348nt.assert_equal(b['status'], 'error')349nt.assert_equal(b['ename'], 'ZeroDivisionError')350-351+352# back to text only353ip.display_formatter.active_types = ['text/plain']354-355356357358359360+class TestSyntaxErrorTransformer(unittest.TestCase):361+ """Check that SyntaxError raised by an input transformer is handled by run_cell()"""362+363+ class SyntaxErrorTransformer(InputTransformer):364+365+ def push(self, line):366+ pos = line.find('syntaxerror')367+ if pos >= 0:368+ e = SyntaxError('input contains "syntaxerror"')369+ e.text = line370+ e.offset = pos + 1371+ raise e372+ return line373+374+ def reset(self):375+ pass376+377+ def setUp(self):378+ self.transformer = TestSyntaxErrorTransformer.SyntaxErrorTransformer()379+ ip.input_splitter.python_line_transforms.append(self.transformer)380+ ip.input_transformer_manager.python_line_transforms.append(self.transformer)381+382+ def tearDown(self):383+ ip.input_splitter.python_line_transforms.remove(self.transformer)384+ ip.input_transformer_manager.python_line_transforms.remove(self.transformer)385+386+ def test_syntaxerror_input_transformer(self):387+ with tt.AssertPrints('1234'):388+ ip.run_cell('1234')389+ with tt.AssertPrints('SyntaxError: invalid syntax'):390+ ip.run_cell('1 2 3') # plain python syntax error391+ with tt.AssertPrints('SyntaxError: input contains "syntaxerror"'):392+ ip.run_cell('2345 # syntaxerror') # input transformer syntax error393+ with tt.AssertPrints('3456'):394+ ip.run_cell('3456')395+396+397+398diff --git a/IPython/qt/console/frontend_widget.py b/IPython/qt/console/frontend_widget.py399index 524cf76..0dc598b 100644400--- a/IPython/qt/console/frontend_widget.py401+++ b/IPython/qt/console/frontend_widget.py402@@ -205,7 +205,10 @@ def _is_complete(self, source, interactive):403'interactive' is True; otherwise, it is False.404"""405self._input_splitter.reset()406- complete = self._input_splitter.push(source)407+ try:408+ complete = self._input_splitter.push(source)409+ except SyntaxError:410+ return True411if interactive:412complete = not self._input_splitter.push_accepts_more()413return complete414diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py415index c253448..7105603 100644416--- a/IPython/sphinxext/ipython_directive.py417+++ b/IPython/sphinxext/ipython_directive.py418@@ -252,7 +252,7 @@ def process_input_line(self, line, store_history=True):419splitter.push(line)420more = splitter.push_accepts_more()421if not more:422- source_raw = splitter.source_raw_reset()[1]423+ source_raw = splitter.raw_reset()424self.IP.run_cell(source_raw, store_history=store_history)425finally:426sys.stdout = stdout427diff --git a/IPython/terminal/console/interactiveshell.py b/IPython/terminal/console/interactiveshell.py428index bd135cf..286dc6d 100644429--- a/IPython/terminal/console/interactiveshell.py430+++ b/IPython/terminal/console/interactiveshell.py431@@ -456,7 +456,7 @@ def interact(self, display_banner=None):432#double-guard against keyboardinterrupts during kbdint handling433try:434self.write('\nKeyboardInterrupt\n')435- source_raw = self.input_splitter.source_raw_reset()[1]436+ source_raw = self.input_splitter.raw_reset()437hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)438more = False439except KeyboardInterrupt:440@@ -478,13 +478,18 @@ def interact(self, display_banner=None):441# asynchronously by signal handlers, for example.442self.showtraceback()443else:444- self.input_splitter.push(line)445- more = self.input_splitter.push_accepts_more()446+ try:447+ self.input_splitter.push(line)448+ more = self.input_splitter.push_accepts_more()449+ except SyntaxError:450+ # Run the code directly - run_cell takes care of displaying451+ # the exception.452+ more = False453if (self.SyntaxTB.last_syntax_error and454self.autoedit_syntax):455self.edit_syntax_error()456if not more:457- source_raw = self.input_splitter.source_raw_reset()[1]458+ source_raw = self.input_splitter.raw_reset()459hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)460self.run_cell(source_raw)461462diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py463index 32a6c17..cd00e68 100644464--- a/IPython/terminal/interactiveshell.py465+++ b/IPython/terminal/interactiveshell.py466@@ -522,7 +522,7 @@ def interact(self, display_banner=None):467#double-guard against keyboardinterrupts during kbdint handling468try:469self.write('\nKeyboardInterrupt\n')470- source_raw = self.input_splitter.source_raw_reset()[1]471+ source_raw = self.input_splitter.raw_reset()472hlen_b4_cell = \473self._replace_rlhist_multiline(source_raw, hlen_b4_cell)474more = False475@@ -545,13 +545,18 @@ def interact(self, display_banner=None):476# asynchronously by signal handlers, for example.477self.showtraceback()478else:479- self.input_splitter.push(line)480- more = self.input_splitter.push_accepts_more()481+ try:482+ self.input_splitter.push(line)483+ more = self.input_splitter.push_accepts_more()484+ except SyntaxError:485+ # Run the code directly - run_cell takes care of displaying486+ # the exception.487+ more = False488if (self.SyntaxTB.last_syntax_error and489self.autoedit_syntax):490self.edit_syntax_error()491if not more:492- source_raw = self.input_splitter.source_raw_reset()[1]493+ source_raw = self.input_splitter.raw_reset()494self.run_cell(source_raw, store_history=True)495hlen_b4_cell = \496self._replace_rlhist_multiline(source_raw, hlen_b4_cell)497diff --git a/IPython/terminal/tests/test_interactivshell.py b/IPython/terminal/tests/test_interactivshell.py498index 6ab4acb..9437aff 100644499--- a/IPython/terminal/tests/test_interactivshell.py500+++ b/IPython/terminal/tests/test_interactivshell.py501@@ -17,12 +17,68 @@502#-----------------------------------------------------------------------------503# stdlib504import sys505+import types506import unittest507508+from IPython.core.inputtransformer import InputTransformer509from IPython.testing.decorators import skipif510from IPython.utils import py3compat511from IPython.testing import tools as tt512513+# Decorator for interaction loop tests -----------------------------------------514+515+class mock_input_helper(object):516+ """Machinery for tests of the main interact loop.517+518+ Used by the mock_input decorator.519+ """520+ def __init__(self, testgen):521+ self.testgen = testgen522+ self.exception = None523+ self.ip = get_ipython()524+525+ def __enter__(self):526+ self.orig_raw_input = self.ip.raw_input527+ self.ip.raw_input = self.fake_input528+ return self529+530+ def __exit__(self, etype, value, tb):531+ self.ip.raw_input = self.orig_raw_input532+533+ def fake_input(self, prompt):534+ try:535+ return next(self.testgen)536+ except StopIteration:537+ self.ip.exit_now = True538+ return u''539+ except:540+ self.exception = sys.exc_info()541+ self.ip.exit_now = True542+ return u''543+544+def mock_input(testfunc):545+ """Decorator for tests of the main interact loop.546+547+ Write the test as a generator, yield-ing the input strings, which IPython548+ will see as if they were typed in at the prompt.549+ """550+ def test_method(self):551+ testgen = testfunc(self)552+ with mock_input_helper(testgen) as mih:553+ mih.ip.interact(display_banner=False)554+555+ if mih.exception is not None:556+ # Re-raise captured exception557+ etype, value, tb = mih.exception558+ import traceback559+ traceback.print_tb(tb, file=sys.stdout)560+ del tb # Avoid reference loop561+ raise value562+563+ return test_method564+565+# Test classes -----------------------------------------------------------------566+567class InteractiveShellTestCase(unittest.TestCase):568def rl_hist_entries(self, rl, n):569"""Get last n readline history entries as a list"""570@@ -171,6 +227,42 @@ def test_replace_multiline_hist_replaces_empty_line(self):571expected = [ py3compat.unicode_to_str(e, enc) for e in expected ]572self.assertEqual(hist, expected)573574+ @mock_input575+ def test_inputtransformer_syntaxerror(self):576+ ip = get_ipython()577+ transformer = SyntaxErrorTransformer()578+ ip.input_splitter.python_line_transforms.append(transformer)579+ ip.input_transformer_manager.python_line_transforms.append(transformer)580+581+ try:582+ #raise Exception583+ with tt.AssertPrints('4', suppress=False):584+ yield u'print(2*2)'585+586+ with tt.AssertPrints('SyntaxError: input contains', suppress=False):587+ yield u'print(2345) # syntaxerror'588+589+ with tt.AssertPrints('16', suppress=False):590+ yield u'print(4*4)'591+592+ finally:593+ ip.input_splitter.python_line_transforms.remove(transformer)594+ ip.input_transformer_manager.python_line_transforms.remove(transformer)595+596+597+class SyntaxErrorTransformer(InputTransformer):598+ def push(self, line):599+ pos = line.find('syntaxerror')600+ if pos >= 0:601+ e = SyntaxError('input contains "syntaxerror"')602+ e.text = line603+ e.offset = pos + 1604+ raise e605+ return line606+607+ def reset(self):608+ pass609+610class TerminalMagicsTestCase(unittest.TestCase):611def test_paste_magics_message(self):612"""Test that an IndentationError while using paste magics doesn't613diff --git a/IPython/utils/io.py b/IPython/utils/io.py614index 5cd2228..c86d2ae 100644615--- a/IPython/utils/io.py616+++ b/IPython/utils/io.py617@@ -43,6 +43,11 @@ def clone(meth):618for meth in filter(clone, dir(stream)):619setattr(self, meth, getattr(stream, meth))620621+ def __repr__(self):622+ cls = self.__class__623+ tpl = '{mod}.{cls}({args})'624+ return tpl.format(mod=cls.__module__, cls=cls.__name__, args=self.stream)625+626def write(self,data):627try:628self._swrite(data)629diff --git a/docs/source/config/inputtransforms.rst b/docs/source/config/inputtransforms.rst630index a28c13d..1f9347f 100644631--- a/docs/source/config/inputtransforms.rst632+++ b/docs/source/config/inputtransforms.rst633@@ -43,6 +43,14 @@ to tell when a block of input is complete, and634to transform complete cells. If you add a transformer, you should make sure that635it gets added to both.636637+These transformers may raise :exc:`SyntaxError` if the input code is invalid, but638+in most cases it is clearer to pass unrecognised code through unmodified and let639+Python's own parser decide whether it is valid.640+641+.. versionchanged:: 2.0642+643+ Added the option to raise :exc:`SyntaxError`.644+645Stateless transformations646-------------------------647648diff --git a/docs/source/whatsnew/pr/incompat-inputsplitter-source-raw-reset.rst b/docs/source/whatsnew/pr/incompat-inputsplitter-source-raw-reset.rst649new file mode 100644650index 0000000..7e9056f651--- /dev/null652+++ b/docs/source/whatsnew/pr/incompat-inputsplitter-source-raw-reset.rst653@@ -0,0 +1,6 @@654+* :class:`IPython.core.inputsplitter.IPythonInputSplitter` no longer has a method655+ ``source_raw_reset()``, but gains :meth:`~IPython.core.inputsplitter.IPythonInputSplitter.raw_reset`656+ instead. Use of ``source_raw_reset`` can be replaced with::657+658+ raw = isp.source_raw659+ transformed = isp.source_reset()660diff --git a/docs/source/whatsnew/pr/inputtransformer-syntaxerrors.rst b/docs/source/whatsnew/pr/inputtransformer-syntaxerrors.rst661new file mode 100644662index 0000000..74d3594663--- /dev/null664+++ b/docs/source/whatsnew/pr/inputtransformer-syntaxerrors.rst665@@ -0,0 +1,4 @@666+* Input transformers (see :doc:`/config/inputtransforms`) may now raise667+ :exc:`SyntaxError` if they determine that input is invalid. The input668+ transformation machinery in IPython will handle displaying the exception to669+ the user and resetting state.670--6711.8.5.3672673674675