Skip to content

Commit d875b02

Browse files
authored
fix syntax error reporting from stdin (#357) (#716)
In b73a3d1, there was an assumption that text is None only if there was an encoding error with the file. However this was the case for all pythons before 3.9 when reading code from stdin. This takes care to correctly report as much context as possible, so errors aren't silently dropped with the unhelpful "problem decoding source" message.
1 parent 44ef321 commit d875b02

File tree

3 files changed

+70
-16
lines changed

3 files changed

+70
-16
lines changed

pyflakes/api.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,7 @@ def check(codeString, filename, reporter=None):
5555
text = None
5656
offset -= 1
5757

58-
# If there's an encoding problem with the file, the text is None.
59-
if text is None:
60-
# Avoid using msg, since for the only known case, it contains a
61-
# bogus message that claims the encoding the file declared was
62-
# unknown.
63-
reporter.unexpectedError(filename, 'problem decoding source')
64-
else:
65-
reporter.syntaxError(filename, msg, lineno, offset, text)
58+
reporter.syntaxError(filename, msg, lineno, offset, text)
6659
return 1
6760
except Exception:
6861
reporter.unexpectedError(filename, 'problem decoding source')

pyflakes/reporter.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,30 @@ def syntaxError(self, filename, msg, lineno, offset, text):
5151
@param text: The source code containing the syntax error.
5252
@ptype text: C{unicode}
5353
"""
54-
line = text.splitlines()[-1]
54+
if text is None:
55+
line = None
56+
else:
57+
line = text.splitlines()[-1]
58+
59+
# lineno might be 0 if the error came from stdin
60+
lineno = max(lineno, 1)
61+
5562
if offset is not None:
56-
if sys.version_info < (3, 8):
63+
if sys.version_info < (3, 8) and text is not None:
5764
offset = offset - (len(text) - len(line)) + 1
65+
# some versions of python emit an offset of -1 for certain encoding errors
66+
offset = max(offset, 1)
5867
self._stderr.write('%s:%d:%d: %s\n' %
5968
(filename, lineno, offset, msg))
6069
else:
6170
self._stderr.write('%s:%d: %s\n' % (filename, lineno, msg))
62-
self._stderr.write(line)
63-
self._stderr.write('\n')
64-
if offset is not None:
65-
self._stderr.write(re.sub(r'\S', ' ', line[:offset - 1]) +
66-
"^\n")
71+
72+
if line is not None:
73+
self._stderr.write(line)
74+
self._stderr.write('\n')
75+
if offset is not None:
76+
self._stderr.write(re.sub(r'\S', ' ', line[:offset - 1]) +
77+
"^\n")
6778

6879
def flake(self, message):
6980
"""

pyflakes/test/test_api.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from pyflakes.reporter import Reporter
1616
from pyflakes.api import (
1717
main,
18+
check,
1819
checkPath,
1920
checkRecursive,
2021
iterSourceCode,
@@ -255,6 +256,17 @@ def test_syntaxErrorNoOffset(self):
255256
"bad line of source\n"),
256257
err.getvalue())
257258

259+
def test_syntaxErrorNoText(self):
260+
"""
261+
C{syntaxError} doesn't include text or nonsensical offsets if C{text} is C{None}.
262+
263+
This typically happens when reporting syntax errors from stdin.
264+
"""
265+
err = io.StringIO()
266+
reporter = Reporter(None, err)
267+
reporter.syntaxError('<stdin>', 'a problem', 0, 0, None)
268+
self.assertEqual(("<stdin>:1:1: a problem\n"), err.getvalue())
269+
258270
def test_multiLineSyntaxError(self):
259271
"""
260272
If there's a multi-line syntax error, then we only report the last
@@ -606,7 +618,8 @@ def test_misencodedFileUTF8(self):
606618
""" % SNOWMAN).encode('utf-8')
607619
with self.makeTempFile(source) as sourcePath:
608620
self.assertHasErrors(
609-
sourcePath, [f"{sourcePath}: problem decoding source\n"])
621+
sourcePath,
622+
[f"{sourcePath}:1:1: 'ascii' codec can't decode byte 0xe2 in position 21: ordinal not in range(128)\n"]) # noqa: E501
610623

611624
def test_misencodedFileUTF16(self):
612625
"""
@@ -648,6 +661,43 @@ def test_checkRecursive(self):
648661
finally:
649662
shutil.rmtree(tempdir)
650663

664+
def test_stdinReportsErrors(self):
665+
"""
666+
L{check} reports syntax errors from stdin
667+
"""
668+
source = "max(1 for i in range(10), key=lambda x: x+1)\n"
669+
err = io.StringIO()
670+
count = withStderrTo(err, check, source, "<stdin>")
671+
self.assertEqual(count, 1)
672+
errlines = err.getvalue().split("\n")[:-1]
673+
674+
if PYPY:
675+
expected_error = [
676+
"<stdin>:1:3: Generator expression must be parenthesized if not sole argument", # noqa: E501
677+
"max(1 for i in range(10), key=lambda x: x+1)",
678+
" ^",
679+
]
680+
elif sys.version_info >= (3, 9):
681+
expected_error = [
682+
"<stdin>:1:5: Generator expression must be parenthesized",
683+
"max(1 for i in range(10), key=lambda x: x+1)",
684+
" ^",
685+
]
686+
elif sys.version_info >= (3, 8):
687+
expected_error = [
688+
"<stdin>:1:5: Generator expression must be parenthesized",
689+
]
690+
elif sys.version_info >= (3, 7):
691+
expected_error = [
692+
"<stdin>:1:4: Generator expression must be parenthesized",
693+
]
694+
elif sys.version_info >= (3, 6):
695+
expected_error = [
696+
"<stdin>:1:4: Generator expression must be parenthesized if not sole argument", # noqa: E501
697+
]
698+
699+
self.assertEqual(errlines, expected_error)
700+
651701

652702
class IntegrationTests(TestCase):
653703
"""

0 commit comments

Comments
 (0)