From a0a04ca113fdc0e54ae9a4bf8ae53aeaeb53e643 2013-12-24 21:01:24 From: MinRK Date: 2013-12-24 21:01:24 Subject: [PATCH] Backport PR #4218: Fix display of SyntaxError when .py file is modified As described on the mailing list, the line displayed with a SyntaxError can be incorrect, when editing and rerunning a file, due to stale caching. The line appears to be retrieved from the cache when the SyntaxError is created, which we can't influence, but we can replace it before we display the error, which is what this does. --- diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index fab2bb2..1d7ae51 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -108,8 +108,33 @@ class IndentationErrorTest(unittest.TestCase): with tt.AssertPrints("zoon()", suppress=False): ip.magic('run %s' % fname) +se_file_1 = """1 +2 +7/ +""" + +se_file_2 = """7/ +""" + class SyntaxErrorTest(unittest.TestCase): def test_syntaxerror_without_lineno(self): with tt.AssertNotPrints("TypeError"): with tt.AssertPrints("line unknown"): ip.run_cell("raise SyntaxError()") + + def test_changing_py_file(self): + with TemporaryDirectory() as td: + fname = os.path.join(td, "foo.py") + with open(fname, 'w') as f: + f.write(se_file_1) + + with tt.AssertPrints(["7/", "SyntaxError"]): + ip.magic("run " + fname) + + # Modify the file + with open(fname, 'w') as f: + f.write(se_file_2) + + # The SyntaxError should point to the correct line + with tt.AssertPrints(["7/", "SyntaxError"]): + ip.magic("run " + fname) diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 4261339..846d1a1 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -1193,6 +1193,20 @@ class SyntaxTB(ListTB): self.last_syntax_error = value ListTB.__call__(self,etype,value,elist) + def structured_traceback(self, etype, value, elist, tb_offset=None, + context=5): + # If the source file has been edited, the line in the syntax error can + # be wrong (retrieved from an outdated cache). This replaces it with + # the current value. + if isinstance(value.filename, py3compat.string_types) \ + and isinstance(value.lineno, int): + linecache.checkcache(value.filename) + newtext = ulinecache.getline(value.filename, value.lineno) + if newtext: + value.text = newtext + return super(SyntaxTB, self).structured_traceback(etype, value, elist, + tb_offset=tb_offset, context=context) + def clear_err_state(self): """Return the current error state and clear it""" e = self.last_syntax_error diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py index 0bd3f7a..4732e1c 100644 --- a/IPython/testing/tools.py +++ b/IPython/testing/tools.py @@ -18,7 +18,6 @@ from __future__ import absolute_import # Imports #----------------------------------------------------------------------------- -import inspect import os import re import sys @@ -350,6 +349,8 @@ class AssertPrints(object): """ def __init__(self, s, channel='stdout', suppress=True): self.s = s + if isinstance(self.s, py3compat.string_types): + self.s = [self.s] self.channel = channel self.suppress = suppress @@ -363,7 +364,8 @@ class AssertPrints(object): self.tee.flush() setattr(sys, self.channel, self.orig_stream) printed = self.buffer.getvalue() - assert self.s in printed, notprinted_msg.format(self.s, self.channel, printed) + for s in self.s: + assert s in printed, notprinted_msg.format(s, self.channel, printed) return False printed_msg = """Found {0!r} in printed output (on {1}): @@ -380,7 +382,8 @@ class AssertNotPrints(AssertPrints): self.tee.flush() setattr(sys, self.channel, self.orig_stream) printed = self.buffer.getvalue() - assert self.s not in printed, printed_msg.format(self.s, self.channel, printed) + for s in self.s: + assert s not in printed, printed_msg.format(s, self.channel, printed) return False @contextmanager