2
Tests for L{pyflakes.scripts.pyflakes}.
11
from unittest import skipIf, TestCase
13
from pyflakes.messages import UnusedImport
14
from pyflakes.reporter import Reporter
15
from pyflakes.api import (
21
if sys.version_info < (3,):
22
from cStringIO import StringIO
24
from io import StringIO
28
def withStderrTo(stderr, f, *args, **kwargs):
30
Call C{f} with C{sys.stderr} redirected to C{stderr}.
32
(outer, sys.stderr) = (sys.stderr, stderr)
34
return f(*args, **kwargs)
43
def __init__(self, lineno, col_offset=0):
45
self.col_offset = col_offset
48
class LoggingReporter(object):
50
Implementation of Reporter that just appends any error to a list.
53
def __init__(self, log):
55
Construct a C{LoggingReporter}.
57
@param log: A list to append log messages to.
61
def flake(self, message):
62
self.log.append(('flake', str(message)))
64
def unexpectedError(self, filename, message):
65
self.log.append(('unexpectedError', filename, message))
67
def syntaxError(self, filename, msg, lineno, offset, line):
68
self.log.append(('syntaxError', filename, msg, lineno, offset, line))
71
class TestIterSourceCode(TestCase):
73
Tests for L{iterSourceCode}.
77
self.tempdir = tempfile.mkdtemp()
80
shutil.rmtree(self.tempdir)
82
def makeEmptyFile(self, *parts):
84
fpath = os.path.join(self.tempdir, *parts)
89
def test_emptyDirectory(self):
91
There are no Python files in an empty directory.
93
self.assertEqual(list(iterSourceCode([self.tempdir])), [])
95
def test_singleFile(self):
97
If the directory contains one Python file, C{iterSourceCode} will find
100
childpath = self.makeEmptyFile('foo.py')
101
self.assertEqual(list(iterSourceCode([self.tempdir])), [childpath])
103
def test_onlyPythonSource(self):
105
Files that are not Python source files are not included.
107
self.makeEmptyFile('foo.pyc')
108
self.assertEqual(list(iterSourceCode([self.tempdir])), [])
110
def test_recurses(self):
112
If the Python files are hidden deep down in child directories, we will
115
os.mkdir(os.path.join(self.tempdir, 'foo'))
116
apath = self.makeEmptyFile('foo', 'a.py')
117
os.mkdir(os.path.join(self.tempdir, 'bar'))
118
bpath = self.makeEmptyFile('bar', 'b.py')
119
cpath = self.makeEmptyFile('c.py')
121
sorted(iterSourceCode([self.tempdir])),
122
sorted([apath, bpath, cpath]))
124
def test_multipleDirectories(self):
126
L{iterSourceCode} can be given multiple directories. It will recurse
129
foopath = os.path.join(self.tempdir, 'foo')
130
barpath = os.path.join(self.tempdir, 'bar')
132
apath = self.makeEmptyFile('foo', 'a.py')
134
bpath = self.makeEmptyFile('bar', 'b.py')
136
sorted(iterSourceCode([foopath, barpath])),
137
sorted([apath, bpath]))
139
def test_explicitFiles(self):
141
If one of the paths given to L{iterSourceCode} is not a directory but
142
a file, it will include that in its output.
144
epath = self.makeEmptyFile('e.py')
145
self.assertEqual(list(iterSourceCode([epath])),
149
class TestReporter(TestCase):
151
Tests for L{Reporter}.
154
def test_syntaxError(self):
156
C{syntaxError} reports that there was a syntax error in the source
157
file. It reports to the error stream and includes the filename, line
158
number, error message, actual line of source and a caret pointing to
162
reporter = Reporter(None, err)
163
reporter.syntaxError('foo.py', 'a problem', 3, 4, 'bad line of source')
165
("foo.py:3: a problem\n"
166
"bad line of source\n"
170
def test_syntaxErrorNoOffset(self):
172
C{syntaxError} doesn't include a caret pointing to the error if
173
C{offset} is passed as C{None}.
176
reporter = Reporter(None, err)
177
reporter.syntaxError('foo.py', 'a problem', 3, None,
178
'bad line of source')
180
("foo.py:3: a problem\n"
181
"bad line of source\n"),
184
def test_multiLineSyntaxError(self):
186
If there's a multi-line syntax error, then we only report the last
187
line. The offset is adjusted so that it is relative to the start of
192
'bad line of source',
193
'more bad lines of source',
195
reporter = Reporter(None, err)
196
reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 5,
199
("foo.py:3: a problem\n" +
204
def test_unexpectedError(self):
206
C{unexpectedError} reports an error processing a source file.
209
reporter = Reporter(None, err)
210
reporter.unexpectedError('source.py', 'error message')
211
self.assertEquals('source.py: error message\n', err.getvalue())
213
def test_flake(self):
215
C{flake} reports a code warning from Pyflakes. It is exactly the
216
str() of a L{pyflakes.messages.Message}.
219
reporter = Reporter(out, None)
220
message = UnusedImport('foo.py', Node(42), 'bar')
221
reporter.flake(message)
222
self.assertEquals(out.getvalue(), "%s\n" % (message,))
225
class CheckTests(TestCase):
227
Tests for L{check} and L{checkPath} which check a file for flakes.
230
def makeTempFile(self, content):
232
Make a temporary file containing C{content} and return a path to it.
234
_, fpath = tempfile.mkstemp()
235
if not hasattr(content, 'decode'):
236
content = content.encode('ascii')
237
fd = open(fpath, 'wb')
242
def assertHasErrors(self, path, errorList):
244
Assert that C{path} causes errors.
246
@param path: A path to a file to check.
247
@param errorList: A list of errors expected to be printed to stderr.
250
count = withStderrTo(err, checkPath, path)
252
(count, err.getvalue()), (len(errorList), ''.join(errorList)))
254
def getErrors(self, path):
256
Get any warnings or errors reported by pyflakes for the file at C{path}.
258
@param path: The path to a Python file on disk that pyflakes will check.
259
@return: C{(count, log)}, where C{count} is the number of warnings or
260
errors generated, and log is a list of those warnings, presented
261
as structured data. See L{LoggingReporter} for more details.
264
reporter = LoggingReporter(log)
265
count = checkPath(path, reporter)
268
def test_legacyScript(self):
269
from pyflakes.scripts import pyflakes as script_pyflakes
270
self.assertIs(script_pyflakes.checkPath, checkPath)
272
def test_missingTrailingNewline(self):
274
Source which doesn't end with a newline shouldn't cause any
275
exception to be raised nor an error indicator to be returned by
278
fName = self.makeTempFile("def foo():\n\tpass\n\t")
279
self.assertHasErrors(fName, [])
281
def test_checkPathNonExisting(self):
283
L{checkPath} handles non-existing files.
285
count, errors = self.getErrors('extremo')
286
self.assertEquals(count, 1)
289
[('unexpectedError', 'extremo', 'No such file or directory')])
291
def test_multilineSyntaxError(self):
293
Source which includes a syntax error which results in the raised
294
L{SyntaxError.text} containing multiple lines of source are reported
295
with only the last line of that source.
308
# Sanity check - SyntaxError.text should be multiple lines, if it
309
# isn't, something this test was unprepared for has happened.
310
def evaluate(source):
315
e = sys.exc_info()[1]
316
self.assertTrue(e.text.count('\n') > 1)
320
sourcePath = self.makeTempFile(source)
321
self.assertHasErrors(
327
""" % (sourcePath,)])
329
def test_eofSyntaxError(self):
331
The error reported for source files which end prematurely causing a
332
syntax error reflects the cause for the syntax error.
334
sourcePath = self.makeTempFile("def foo(")
335
self.assertHasErrors(
338
%s:1: unexpected EOF while parsing
341
""" % (sourcePath,)])
343
def test_nonDefaultFollowsDefaultSyntaxError(self):
345
Source which has a non-default argument following a default argument
346
should include the line number of the syntax error. However these
347
exceptions do not include an offset.
350
def foo(bar=baz, bax):
353
sourcePath = self.makeTempFile(source)
354
last_line = ' ^\n' if sys.version_info >= (3, 2) else ''
355
self.assertHasErrors(
358
%s:1: non-default argument follows default argument
359
def foo(bar=baz, bax):
360
%s""" % (sourcePath, last_line)])
362
def test_nonKeywordAfterKeywordSyntaxError(self):
364
Source which has a non-keyword argument after a keyword argument should
365
include the line number of the syntax error. However these exceptions
366
do not include an offset.
371
sourcePath = self.makeTempFile(source)
372
last_line = ' ^\n' if sys.version_info >= (3, 2) else ''
373
self.assertHasErrors(
376
%s:1: non-keyword arg after keyword arg
378
%s""" % (sourcePath, last_line)])
380
def test_invalidEscape(self):
382
The invalid escape syntax raises ValueError in Python 2
384
ver = sys.version_info
385
# ValueError: invalid \x escape
386
sourcePath = self.makeTempFile(r"foo = '\xyz'")
388
decoding_error = "%s: problem decoding source\n" % (sourcePath,)
390
last_line = ' ^\n' if ver >= (3, 2) else ''
391
# Column has been "fixed" since 3.2.4 and 3.3.1
392
col = 1 if ver >= (3, 3, 1) or ((3, 2, 4) <= ver < (3, 3)) else 2
393
decoding_error = """\
394
%s:1: (unicode error) 'unicodeescape' codec can't decode bytes \
395
in position 0-%d: truncated \\xXX escape
397
%s""" % (sourcePath, col, last_line)
398
self.assertHasErrors(
399
sourcePath, [decoding_error])
401
def test_permissionDenied(self):
403
If the source file is not readable, this is reported on standard
406
sourcePath = self.makeTempFile('')
407
os.chmod(sourcePath, 0)
408
count, errors = self.getErrors(sourcePath)
409
self.assertEquals(count, 1)
412
[('unexpectedError', sourcePath, "Permission denied")])
414
def test_pyflakesWarning(self):
416
If the source file has a pyflakes warning, this is reported as a
419
sourcePath = self.makeTempFile("import foo")
420
count, errors = self.getErrors(sourcePath)
421
self.assertEquals(count, 1)
423
errors, [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))])
425
@skipIf(sys.version_info >= (3,), "not relevant")
426
def test_misencodedFileUTF8(self):
428
If a source file contains bytes which cannot be decoded, this is
431
SNOWMAN = unichr(0x2603)
435
""" % SNOWMAN).encode('utf-8')
436
sourcePath = self.makeTempFile(source)
437
self.assertHasErrors(
438
sourcePath, ["%s: problem decoding source\n" % (sourcePath,)])
440
def test_misencodedFileUTF16(self):
442
If a source file contains bytes which cannot be decoded, this is
445
SNOWMAN = unichr(0x2603)
449
""" % SNOWMAN).encode('utf-16')
450
sourcePath = self.makeTempFile(source)
451
self.assertHasErrors(
452
sourcePath, ["%s: problem decoding source\n" % (sourcePath,)])
454
def test_checkRecursive(self):
456
L{checkRecursive} descends into each directory, finding Python files
457
and reporting problems.
459
tempdir = tempfile.mkdtemp()
460
os.mkdir(os.path.join(tempdir, 'foo'))
461
file1 = os.path.join(tempdir, 'foo', 'bar.py')
462
fd = open(file1, 'wb')
463
fd.write("import baz\n".encode('ascii'))
465
file2 = os.path.join(tempdir, 'baz.py')
466
fd = open(file2, 'wb')
467
fd.write("import contraband".encode('ascii'))
470
reporter = LoggingReporter(log)
471
warnings = checkRecursive([tempdir], reporter)
472
self.assertEqual(warnings, 2)
475
sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))),
477
str(UnusedImport(file2, Node(1), 'contraband')))]))
480
class IntegrationTests(TestCase):
482
Tests of the pyflakes script that actually spawn the script.
486
self.tempdir = tempfile.mkdtemp()
487
self.tempfilepath = os.path.join(self.tempdir, 'temp')
490
shutil.rmtree(self.tempdir)
492
def getPyflakesBinary(self):
494
Return the path to the pyflakes binary.
497
package_dir = os.path.dirname(pyflakes.__file__)
498
return os.path.join(package_dir, '..', 'bin', 'pyflakes')
500
def runPyflakes(self, paths, stdin=None):
502
Launch a subprocess running C{pyflakes}.
504
@param args: Command-line arguments to pass to pyflakes.
505
@param kwargs: Options passed on to C{subprocess.Popen}.
506
@return: C{(returncode, stdout, stderr)} of the completed pyflakes
509
env = dict(os.environ)
510
env['PYTHONPATH'] = os.pathsep.join(sys.path)
511
command = [sys.executable, self.getPyflakesBinary()]
512
command.extend(paths)
514
p = subprocess.Popen(command, env=env, stdin=subprocess.PIPE,
515
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
516
(stdout, stderr) = p.communicate(stdin)
518
p = subprocess.Popen(command, env=env,
519
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
520
(stdout, stderr) = p.communicate()
522
if sys.version_info >= (3,):
523
stdout = stdout.decode('utf-8')
524
stderr = stderr.decode('utf-8')
525
return (stdout, stderr, rv)
527
def test_goodFile(self):
529
When a Python source file is all good, the return code is zero and no
530
messages are printed to either stdout or stderr.
532
fd = open(self.tempfilepath, 'a')
534
d = self.runPyflakes([self.tempfilepath])
535
self.assertEqual(d, ('', '', 0))
537
def test_fileWithFlakes(self):
539
When a Python source file has warnings, the return code is non-zero
540
and the warnings are printed to stdout.
542
fd = open(self.tempfilepath, 'wb')
543
fd.write("import contraband\n".encode('ascii'))
545
d = self.runPyflakes([self.tempfilepath])
546
expected = UnusedImport(self.tempfilepath, Node(1), 'contraband')
547
self.assertEqual(d, ("%s\n" % expected, '', 1))
549
def test_errors(self):
551
When pyflakes finds errors with the files it's given, (if they don't
552
exist, say), then the return code is non-zero and the errors are
555
d = self.runPyflakes([self.tempfilepath])
556
error_msg = '%s: No such file or directory\n' % (self.tempfilepath,)
557
self.assertEqual(d, ('', error_msg, 1))
559
def test_readFromStdin(self):
561
If no arguments are passed to C{pyflakes} then it reads from stdin.
563
d = self.runPyflakes([], stdin='import contraband'.encode('ascii'))
564
expected = UnusedImport('<stdin>', Node(1), 'contraband')
565
self.assertEqual(d, ("%s\n" % expected, '', 1))