2
Tests for L{pyflakes.scripts.pyflakes}.
11
from pyflakes.messages import UnusedImport
12
from pyflakes.reporter import Reporter
13
from pyflakes.api import (
19
from pyflakes.test.harness import TestCase, skipIf
21
if sys.version_info < (3,):
22
from cStringIO import StringIO
24
from io import StringIO
30
except AttributeError:
33
ERROR_HAS_COL_NUM = ERROR_HAS_LAST_LINE = sys.version_info >= (3, 2) or PYPY
36
def withStderrTo(stderr, f, *args, **kwargs):
38
Call C{f} with C{sys.stderr} redirected to C{stderr}.
40
(outer, sys.stderr) = (sys.stderr, stderr)
42
return f(*args, **kwargs)
51
def __init__(self, lineno, col_offset=0):
53
self.col_offset = col_offset
56
class SysStreamCapturing(object):
58
"""Replaces sys.stdin, sys.stdout and sys.stderr with StringIO objects."""
60
def __init__(self, stdin):
61
self._stdin = StringIO(stdin or '')
64
self._orig_stdin = sys.stdin
65
self._orig_stdout = sys.stdout
66
self._orig_stderr = sys.stderr
68
sys.stdin = self._stdin
69
sys.stdout = self._stdout_stringio = StringIO()
70
sys.stderr = self._stderr_stringio = StringIO()
74
def __exit__(self, *args):
75
self.output = self._stdout_stringio.getvalue()
76
self.error = self._stderr_stringio.getvalue()
78
sys.stdin = self._orig_stdin
79
sys.stdout = self._orig_stdout
80
sys.stderr = self._orig_stderr
83
class LoggingReporter(object):
85
Implementation of Reporter that just appends any error to a list.
88
def __init__(self, log):
90
Construct a C{LoggingReporter}.
92
@param log: A list to append log messages to.
96
def flake(self, message):
97
self.log.append(('flake', str(message)))
99
def unexpectedError(self, filename, message):
100
self.log.append(('unexpectedError', filename, message))
102
def syntaxError(self, filename, msg, lineno, offset, line):
103
self.log.append(('syntaxError', filename, msg, lineno, offset, line))
106
class TestIterSourceCode(TestCase):
108
Tests for L{iterSourceCode}.
112
self.tempdir = tempfile.mkdtemp()
115
shutil.rmtree(self.tempdir)
117
def makeEmptyFile(self, *parts):
119
fpath = os.path.join(self.tempdir, *parts)
120
fd = open(fpath, 'a')
124
def test_emptyDirectory(self):
126
There are no Python files in an empty directory.
128
self.assertEqual(list(iterSourceCode([self.tempdir])), [])
130
def test_singleFile(self):
132
If the directory contains one Python file, C{iterSourceCode} will find
135
childpath = self.makeEmptyFile('foo.py')
136
self.assertEqual(list(iterSourceCode([self.tempdir])), [childpath])
138
def test_onlyPythonSource(self):
140
Files that are not Python source files are not included.
142
self.makeEmptyFile('foo.pyc')
143
self.assertEqual(list(iterSourceCode([self.tempdir])), [])
145
def test_recurses(self):
147
If the Python files are hidden deep down in child directories, we will
150
os.mkdir(os.path.join(self.tempdir, 'foo'))
151
apath = self.makeEmptyFile('foo', 'a.py')
152
os.mkdir(os.path.join(self.tempdir, 'bar'))
153
bpath = self.makeEmptyFile('bar', 'b.py')
154
cpath = self.makeEmptyFile('c.py')
156
sorted(iterSourceCode([self.tempdir])),
157
sorted([apath, bpath, cpath]))
159
def test_multipleDirectories(self):
161
L{iterSourceCode} can be given multiple directories. It will recurse
164
foopath = os.path.join(self.tempdir, 'foo')
165
barpath = os.path.join(self.tempdir, 'bar')
167
apath = self.makeEmptyFile('foo', 'a.py')
169
bpath = self.makeEmptyFile('bar', 'b.py')
171
sorted(iterSourceCode([foopath, barpath])),
172
sorted([apath, bpath]))
174
def test_explicitFiles(self):
176
If one of the paths given to L{iterSourceCode} is not a directory but
177
a file, it will include that in its output.
179
epath = self.makeEmptyFile('e.py')
180
self.assertEqual(list(iterSourceCode([epath])),
184
class TestReporter(TestCase):
186
Tests for L{Reporter}.
189
def test_syntaxError(self):
191
C{syntaxError} reports that there was a syntax error in the source
192
file. It reports to the error stream and includes the filename, line
193
number, error message, actual line of source and a caret pointing to
197
reporter = Reporter(None, err)
198
reporter.syntaxError('foo.py', 'a problem', 3, 7, 'bad line of source')
200
("foo.py:3:8: a problem\n"
201
"bad line of source\n"
205
def test_syntaxErrorNoOffset(self):
207
C{syntaxError} doesn't include a caret pointing to the error if
208
C{offset} is passed as C{None}.
211
reporter = Reporter(None, err)
212
reporter.syntaxError('foo.py', 'a problem', 3, None,
213
'bad line of source')
215
("foo.py:3: a problem\n"
216
"bad line of source\n"),
219
def test_multiLineSyntaxError(self):
221
If there's a multi-line syntax error, then we only report the last
222
line. The offset is adjusted so that it is relative to the start of
227
'bad line of source',
228
'more bad lines of source',
230
reporter = Reporter(None, err)
231
reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7,
234
("foo.py:3:7: a problem\n" +
239
def test_unexpectedError(self):
241
C{unexpectedError} reports an error processing a source file.
244
reporter = Reporter(None, err)
245
reporter.unexpectedError('source.py', 'error message')
246
self.assertEqual('source.py: error message\n', err.getvalue())
248
def test_flake(self):
250
C{flake} reports a code warning from Pyflakes. It is exactly the
251
str() of a L{pyflakes.messages.Message}.
254
reporter = Reporter(out, None)
255
message = UnusedImport('foo.py', Node(42), 'bar')
256
reporter.flake(message)
257
self.assertEqual(out.getvalue(), "%s\n" % (message,))
260
class CheckTests(TestCase):
262
Tests for L{check} and L{checkPath} which check a file for flakes.
265
def makeTempFile(self, content):
267
Make a temporary file containing C{content} and return a path to it.
269
_, fpath = tempfile.mkstemp()
270
if not hasattr(content, 'decode'):
271
content = content.encode('ascii')
272
fd = open(fpath, 'wb')
277
def assertHasErrors(self, path, errorList):
279
Assert that C{path} causes errors.
281
@param path: A path to a file to check.
282
@param errorList: A list of errors expected to be printed to stderr.
285
count = withStderrTo(err, checkPath, path)
287
(count, err.getvalue()), (len(errorList), ''.join(errorList)))
289
def getErrors(self, path):
291
Get any warnings or errors reported by pyflakes for the file at C{path}.
293
@param path: The path to a Python file on disk that pyflakes will check.
294
@return: C{(count, log)}, where C{count} is the number of warnings or
295
errors generated, and log is a list of those warnings, presented
296
as structured data. See L{LoggingReporter} for more details.
299
reporter = LoggingReporter(log)
300
count = checkPath(path, reporter)
303
def test_legacyScript(self):
304
from pyflakes.scripts import pyflakes as script_pyflakes
305
self.assertIs(script_pyflakes.checkPath, checkPath)
307
def test_missingTrailingNewline(self):
309
Source which doesn't end with a newline shouldn't cause any
310
exception to be raised nor an error indicator to be returned by
313
fName = self.makeTempFile("def foo():\n\tpass\n\t")
314
self.assertHasErrors(fName, [])
316
def test_checkPathNonExisting(self):
318
L{checkPath} handles non-existing files.
320
count, errors = self.getErrors('extremo')
321
self.assertEqual(count, 1)
324
[('unexpectedError', 'extremo', 'No such file or directory')])
326
def test_multilineSyntaxError(self):
328
Source which includes a syntax error which results in the raised
329
L{SyntaxError.text} containing multiple lines of source are reported
330
with only the last line of that source.
343
# Sanity check - SyntaxError.text should be multiple lines, if it
344
# isn't, something this test was unprepared for has happened.
345
def evaluate(source):
350
e = sys.exc_info()[1]
352
self.assertTrue(e.text.count('\n') > 1)
356
sourcePath = self.makeTempFile(source)
359
message = 'EOF while scanning triple-quoted string literal'
361
message = 'invalid syntax'
363
self.assertHasErrors(
369
""" % (sourcePath, message)])
371
def test_eofSyntaxError(self):
373
The error reported for source files which end prematurely causing a
374
syntax error reflects the cause for the syntax error.
376
sourcePath = self.makeTempFile("def foo(")
379
%s:1:7: parenthesis is never closed
385
%s:1:9: unexpected EOF while parsing
390
self.assertHasErrors(
394
def test_eofSyntaxErrorWithTab(self):
396
The error reported for source files which end prematurely causing a
397
syntax error reflects the cause for the syntax error.
399
sourcePath = self.makeTempFile("if True:\n\tfoo =")
400
column = 5 if PYPY else 7
401
last_line = '\t ^' if PYPY else '\t ^'
403
self.assertHasErrors(
406
%s:2:%s: invalid syntax
409
""" % (sourcePath, column, last_line)])
411
def test_nonDefaultFollowsDefaultSyntaxError(self):
413
Source which has a non-default argument following a default argument
414
should include the line number of the syntax error. However these
415
exceptions do not include an offset.
418
def foo(bar=baz, bax):
421
sourcePath = self.makeTempFile(source)
422
last_line = ' ^\n' if ERROR_HAS_LAST_LINE else ''
423
column = '8:' if ERROR_HAS_COL_NUM else ''
424
self.assertHasErrors(
427
%s:1:%s non-default argument follows default argument
428
def foo(bar=baz, bax):
429
%s""" % (sourcePath, column, last_line)])
431
def test_nonKeywordAfterKeywordSyntaxError(self):
433
Source which has a non-keyword argument after a keyword argument should
434
include the line number of the syntax error. However these exceptions
435
do not include an offset.
440
sourcePath = self.makeTempFile(source)
441
last_line = ' ^\n' if ERROR_HAS_LAST_LINE else ''
442
column = '13:' if ERROR_HAS_COL_NUM or PYPY else ''
444
if sys.version_info >= (3, 5):
445
message = 'positional argument follows keyword argument'
447
message = 'non-keyword arg after keyword arg'
449
self.assertHasErrors(
454
%s""" % (sourcePath, column, message, last_line)])
456
def test_invalidEscape(self):
458
The invalid escape syntax raises ValueError in Python 2
460
ver = sys.version_info
461
# ValueError: invalid \x escape
462
sourcePath = self.makeTempFile(r"foo = '\xyz'")
464
decoding_error = "%s: problem decoding source\n" % (sourcePath,)
467
decoding_error = """\
468
%s:1:6: %s: ('unicodeescape', b'\\\\xyz', 0, 2, 'truncated \\\\xXX escape')
471
""" % (sourcePath, 'UnicodeDecodeError')
473
last_line = ' ^\n' if ERROR_HAS_LAST_LINE else ''
474
# Column has been "fixed" since 3.2.4 and 3.3.1
475
col = 1 if ver >= (3, 3, 1) or ((3, 2, 4) <= ver < (3, 3)) else 2
476
decoding_error = """\
477
%s:1:7: (unicode error) 'unicodeescape' codec can't decode bytes \
478
in position 0-%d: truncated \\xXX escape
480
%s""" % (sourcePath, col, last_line)
481
self.assertHasErrors(
482
sourcePath, [decoding_error])
484
@skipIf(sys.platform == 'win32', 'unsupported on Windows')
485
def test_permissionDenied(self):
487
If the source file is not readable, this is reported on standard
490
sourcePath = self.makeTempFile('')
491
os.chmod(sourcePath, 0)
492
count, errors = self.getErrors(sourcePath)
493
self.assertEqual(count, 1)
496
[('unexpectedError', sourcePath, "Permission denied")])
498
def test_pyflakesWarning(self):
500
If the source file has a pyflakes warning, this is reported as a
503
sourcePath = self.makeTempFile("import foo")
504
count, errors = self.getErrors(sourcePath)
505
self.assertEqual(count, 1)
507
errors, [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))])
509
def test_encodedFileUTF8(self):
511
If source file declares the correct encoding, no error is reported.
513
SNOWMAN = unichr(0x2603)
517
""" % SNOWMAN).encode('utf-8')
518
sourcePath = self.makeTempFile(source)
519
self.assertHasErrors(sourcePath, [])
521
def test_CRLFLineEndings(self):
523
Source files with Windows CR LF line endings are parsed successfully.
525
sourcePath = self.makeTempFile("x = 42\r\n")
526
self.assertHasErrors(sourcePath, [])
528
def test_misencodedFileUTF8(self):
530
If a source file contains bytes which cannot be decoded, this is
533
SNOWMAN = unichr(0x2603)
537
""" % SNOWMAN).encode('utf-8')
538
sourcePath = self.makeTempFile(source)
540
if PYPY and sys.version_info < (3, ):
541
message = ('\'ascii\' codec can\'t decode byte 0xe2 '
542
'in position 21: ordinal not in range(128)')
546
^\n""" % (sourcePath, message)
549
message = 'problem decoding source'
550
result = "%s: problem decoding source\n" % (sourcePath,)
552
self.assertHasErrors(
553
sourcePath, [result])
555
def test_misencodedFileUTF16(self):
557
If a source file contains bytes which cannot be decoded, this is
560
SNOWMAN = unichr(0x2603)
564
""" % SNOWMAN).encode('utf-16')
565
sourcePath = self.makeTempFile(source)
566
self.assertHasErrors(
567
sourcePath, ["%s: problem decoding source\n" % (sourcePath,)])
569
def test_checkRecursive(self):
571
L{checkRecursive} descends into each directory, finding Python files
572
and reporting problems.
574
tempdir = tempfile.mkdtemp()
575
os.mkdir(os.path.join(tempdir, 'foo'))
576
file1 = os.path.join(tempdir, 'foo', 'bar.py')
577
fd = open(file1, 'wb')
578
fd.write("import baz\n".encode('ascii'))
580
file2 = os.path.join(tempdir, 'baz.py')
581
fd = open(file2, 'wb')
582
fd.write("import contraband".encode('ascii'))
585
reporter = LoggingReporter(log)
586
warnings = checkRecursive([tempdir], reporter)
587
self.assertEqual(warnings, 2)
590
sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))),
592
str(UnusedImport(file2, Node(1), 'contraband')))]))
595
class IntegrationTests(TestCase):
597
Tests of the pyflakes script that actually spawn the script.
601
self.tempdir = tempfile.mkdtemp()
602
self.tempfilepath = os.path.join(self.tempdir, 'temp')
605
shutil.rmtree(self.tempdir)
607
def getPyflakesBinary(self):
609
Return the path to the pyflakes binary.
612
package_dir = os.path.dirname(pyflakes.__file__)
613
return os.path.join(package_dir, '..', 'bin', 'pyflakes')
615
def runPyflakes(self, paths, stdin=None):
617
Launch a subprocess running C{pyflakes}.
619
@param paths: Command-line arguments to pass to pyflakes.
620
@param stdin: Text to use as stdin.
621
@return: C{(returncode, stdout, stderr)} of the completed pyflakes
624
env = dict(os.environ)
625
env['PYTHONPATH'] = os.pathsep.join(sys.path)
626
command = [sys.executable, self.getPyflakesBinary()]
627
command.extend(paths)
629
p = subprocess.Popen(command, env=env, stdin=subprocess.PIPE,
630
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
631
(stdout, stderr) = p.communicate(stdin.encode('ascii'))
633
p = subprocess.Popen(command, env=env,
634
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
635
(stdout, stderr) = p.communicate()
637
if sys.version_info >= (3,):
638
stdout = stdout.decode('utf-8')
639
stderr = stderr.decode('utf-8')
640
return (stdout, stderr, rv)
642
def test_goodFile(self):
644
When a Python source file is all good, the return code is zero and no
645
messages are printed to either stdout or stderr.
647
fd = open(self.tempfilepath, 'a')
649
d = self.runPyflakes([self.tempfilepath])
650
self.assertEqual(d, ('', '', 0))
652
def test_fileWithFlakes(self):
654
When a Python source file has warnings, the return code is non-zero
655
and the warnings are printed to stdout.
657
fd = open(self.tempfilepath, 'wb')
658
fd.write("import contraband\n".encode('ascii'))
660
d = self.runPyflakes([self.tempfilepath])
661
expected = UnusedImport(self.tempfilepath, Node(1), 'contraband')
662
self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1))
664
def test_errors(self):
666
When pyflakes finds errors with the files it's given, (if they don't
667
exist, say), then the return code is non-zero and the errors are
670
d = self.runPyflakes([self.tempfilepath])
671
error_msg = '%s: No such file or directory%s' % (self.tempfilepath,
673
self.assertEqual(d, ('', error_msg, 1))
675
def test_readFromStdin(self):
677
If no arguments are passed to C{pyflakes} then it reads from stdin.
679
d = self.runPyflakes([], stdin='import contraband')
680
expected = UnusedImport('<stdin>', Node(1), 'contraband')
681
self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1))
684
class TestMain(IntegrationTests):
686
Tests of the pyflakes main function.
689
def runPyflakes(self, paths, stdin=None):
691
with SysStreamCapturing(stdin) as capture:
693
except SystemExit as e:
694
return (capture.output, capture.error, e.code)
696
raise RuntimeError('SystemExit not raised')