Merge lp:~jml/divmod.org/pyflakes-reporter into lp:divmod.org

Proposed by Jonathan Lange
Status: Superseded
Proposed branch: lp:~jml/divmod.org/pyflakes-reporter
Merge into: lp:divmod.org
Diff against target: 770 lines (+526/-91)
3 files modified
Pyflakes/bin/pyflakes (+0/-1)
Pyflakes/pyflakes/scripts/pyflakes.py (+130/-28)
Pyflakes/pyflakes/test/test_script.py (+396/-62)
To merge this branch: bzr merge lp:~jml/divmod.org/pyflakes-reporter
Reviewer Review Type Date Requested Status
Divmod-dev Pending
Review via email: mp+112336@code.launchpad.net

This proposal has been superseded by a proposal from 2012-07-08.

Description of the change

A sketch for a reporter in pyflakes. This moves pyflakes toward having an
interface that can be called from Python to gather errors without requiring
stderr and stdout to be captured. It also separates the format of output
from the means of checking.

I was inspired to do this while trying to add a test to my own projects to
guarantee that it is pyflakes-clean. I didn't want to do stdout/err trapping,
and thought that something like this would be useful.

I'm not really proposing that this branch be merged as-is. Clearly, it lacks
unit tests for the change.

However, I do want to know whether a change along these lines would be considered
for inclusion. If so, I'll spend the energy to toughen it up.

Thanks,
jml

To post a comment you must log in.
Revision history for this message
Glyph Lefkowitz (glyph) wrote :

For what it's worth, seems like a good idea to me.

Tests, please, though.

Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

I, too, like this idea. I'd love to see it merged.

Revision history for this message
Jonathan Lange (jml) wrote :

On 27 June 2012 19:51, Glyph Lefkowitz <email address hidden> wrote:
> For what it's worth, seems like a good idea to me.
>

Cool, thanks.

> Tests, please, though.

Sure. I'll probably throw this away & start something again TDD. This
seemed the shortest way of describing the change I wished to make.

jml

lp:~jml/divmod.org/pyflakes-reporter updated
2697. By Jonathan Lange

Bazaar for the first time ever ate my changes. This commit collapses the history
for:
  - refactoring the tests
    - new assertion method
    - withStderr thingy taking *args rather than lambda
    - wrapper around creating temporary file with content
  - new reporter class with tests
    - actually using it in tree
    - adding a bunch of tests.

2698. By Jonathan Lange

Add ioError.

2699. By Jonathan Lange

Pass reporter all the way down.

2700. By Jonathan Lange

Start to factor the tests so they assert against the data in the reporter
rather than the format of the results.

2701. By Jonathan Lange

More tests using reporter.

2702. By Jonathan Lange

Handle multiple lines in the reporter.
Restore the multi-line test to chheck output, just in case.

2703. By Jonathan Lange

Add flake reporting.

2704. By Jonathan Lange

Report flakes from the reporter too.

2705. By Jonathan Lange

Whitespace

2706. By Jonathan Lange

Initial thing to iterate of source code.

2707. By Jonathan Lange

Make iterSourceCode take many paths.

2708. By Jonathan Lange

Now we have iterSourceCode instead.

2709. By Jonathan Lange

Split out the bit that checks recursively.

2710. By Jonathan Lange

A swag of integration tests.

2711. By Jonathan Lange

Do it propertly.

2712. By Jonathan Lange

0. Move Reporter to new module, pyflakes.reporter.

2713. By Jonathan Lange

3. Use FilePath rather than os.path (in tests)

2714. By Jonathan Lange

4. Add a bunch of docstrings.

2715. By Jonathan Lange

6 (partial). Rename 'popenPyflakes' to 'runPyflakes'.

2716. By Jonathan Lange

6 (partial). Contain the poison by returning returncode, out, err from runPyflakes6 (partial). Contain the poison by returning returncode, out, err from runPyflakes6 (partial). Contain the poison by returning returncode, out, err from runPyflakes6 (partial). Contain the poison by returning returncode, out, err from runPyflakes6 (partial). Contain the poison by returning returncode, out, err from runPyflakes6 (partial). Contain the poison by returning returncode, out, err from runPyflakes

2717. By Jonathan Lange

6 (partial). Change the integration tests to expect a Deferred from runPyflakes.

2718. By Jonathan Lange

6 (partial). Use Twisted to spawn the process, not subprocess.

2719. By Jonathan Lange

Change the test assertions to more naturally follow getProcessOutputAndValue's return

2720. By Jonathan Lange

Remove unnecessary parens

2721. By Jonathan Lange

Explain what's going on.

2722. By Jonathan Lange

New method for errors.

2723. By Jonathan Lange

Collapse problemDecodingSource and ioError into the one method.

2724. By Jonathan Lange

Document that we expect text, not bytes.

2725. By Jonathan Lange

Better ptype

2726. By Jonathan Lange

Pass unicode

2727. By Jonathan Lange

Correct coding convention

2728. By Jonathan Lange

Use absolute_import.

2729. By Jonathan Lange

Duplicate _EverythingGetter, rather than import it.

2730. By Jonathan Lange

Wrap types in C{}

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Pyflakes/bin/pyflakes'
2--- Pyflakes/bin/pyflakes 2008-08-28 14:33:07 +0000
3+++ Pyflakes/bin/pyflakes 2012-07-08 13:52:19 +0000
4@@ -1,4 +1,3 @@
5 #!/usr/bin/python
6-
7 from pyflakes.scripts.pyflakes import main
8 main()
9
10=== modified file 'Pyflakes/pyflakes/scripts/pyflakes.py'
11--- Pyflakes/pyflakes/scripts/pyflakes.py 2010-04-13 14:53:04 +0000
12+++ Pyflakes/pyflakes/scripts/pyflakes.py 2012-07-08 13:52:19 +0000
13@@ -9,7 +9,80 @@
14
15 checker = __import__('pyflakes.checker').checker
16
17-def check(codeString, filename):
18+
19+class Reporter(object):
20+ """
21+ Formats the results of pyflakes checks to users.
22+ """
23+
24+ def __init__(self, warningStream, errorStream):
25+ """
26+ Construct a L{Reporter}.
27+
28+ @param warningStream: A file-like object where warnings will be
29+ written to. C{sys.stdout} is a good value.
30+ @param errorStream: A file-like object where error output will be
31+ written to. C{sys.stderr} is a good value.
32+ """
33+ self._stdout = warningStream
34+ self._stderr = errorStream
35+
36+
37+ def ioError(self, filename, msg):
38+ """
39+ There was an C{IOError} while reading C{filename}.
40+ """
41+ self._stderr.write("%s: %s\n" % (filename, msg.args[1]))
42+
43+
44+ def problemDecodingSource(self, filename):
45+ """
46+ There was a problem decoding the source code in C{filename}.
47+ """
48+ self._stderr.write(filename)
49+ self._stderr.write(': problem decoding source\n')
50+
51+
52+ def syntaxError(self, filename, msg, lineno, offset, text):
53+ """
54+ There was a syntax errror in C{filename}.
55+
56+ @param filename: The path to the file with the syntax error.
57+ @param msg: An explanation of the syntax error.
58+ @param lineno: The line number where the syntax error occurred.
59+ @param offset: The column on which the syntax error occurred.
60+ @param text: The source code containing the syntax error.
61+ """
62+ line = text.splitlines()[-1]
63+ if offset is not None:
64+ offset = offset - (len(text) - len(line))
65+ self._stderr.write('%s:%d: %s\n' % (filename, lineno, msg))
66+ self._stderr.write(line)
67+ self._stderr.write('\n')
68+ if offset is not None:
69+ self._stderr.write(" " * (offset + 1) + "^\n")
70+
71+
72+ def flake(self, message):
73+ """
74+ pyflakes found something wrong with the code.
75+
76+ @param: A L{pyflakes.messages.Message}.
77+ """
78+ self._stdout.write(str(message))
79+ self._stdout.write('\n')
80+
81+
82+
83+def _makeDefaultReporter():
84+ """
85+ Make a reporter that can be used when no reporter is specified.
86+ """
87+ return Reporter(sys.stdout, sys.stderr)
88+
89+
90+
91+def check(codeString, filename, reporter=None):
92 """
93 Check the Python source given by C{codeString} for flakes.
94
95@@ -20,9 +93,14 @@
96 errors.
97 @type filename: C{str}
98
99+ @param reporter: A L{Reporter} instance, where errors and warnings will be
100+ reported.
101+
102 @return: The number of warnings emitted.
103 @rtype: C{int}
104 """
105+ if reporter is None:
106+ reporter = _makeDefaultReporter()
107 # First, compile into an AST and handle syntax errors.
108 try:
109 tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST)
110@@ -36,55 +114,79 @@
111 # Avoid using msg, since for the only known case, it contains a
112 # bogus message that claims the encoding the file declared was
113 # unknown.
114- print >> sys.stderr, "%s: problem decoding source" % (filename, )
115+ reporter.problemDecodingSource(filename)
116 else:
117- line = text.splitlines()[-1]
118-
119- if offset is not None:
120- offset = offset - (len(text) - len(line))
121-
122- print >> sys.stderr, '%s:%d: %s' % (filename, lineno, msg)
123- print >> sys.stderr, line
124-
125- if offset is not None:
126- print >> sys.stderr, " " * offset, "^"
127-
128+ reporter.syntaxError(filename, msg, lineno, offset, text)
129 return 1
130 else:
131 # Okay, it's syntactically valid. Now check it.
132 w = checker.Checker(tree, filename)
133 w.messages.sort(lambda a, b: cmp(a.lineno, b.lineno))
134 for warning in w.messages:
135- print warning
136+ reporter.flake(warning)
137 return len(w.messages)
138
139
140-def checkPath(filename):
141+def checkPath(filename, reporter=None):
142 """
143 Check the given path, printing out any warnings detected.
144
145+ @param reporter: A L{Reporter} instance, where errors and warnings will be
146+ reported.
147+
148 @return: the number of warnings printed
149 """
150+ if reporter is None:
151+ reporter = _makeDefaultReporter()
152 try:
153- return check(file(filename, 'U').read() + '\n', filename)
154+ return check(file(filename, 'U').read() + '\n', filename, reporter)
155 except IOError, msg:
156- print >> sys.stderr, "%s: %s" % (filename, msg.args[1])
157+ reporter.ioError(filename, msg)
158 return 1
159
160
161+
162+def iterSourceCode(paths):
163+ """
164+ Iterate over all Python source files in C{paths}.
165+
166+ @param paths: A list of paths. Directories will be recursed into and
167+ any .py files found will be yielded. Any non-directories will be
168+ yielded as-is.
169+ """
170+ for path in paths:
171+ if os.path.isdir(path):
172+ for dirpath, dirnames, filenames in os.walk(path):
173+ for filename in filenames:
174+ if filename.endswith('.py'):
175+ yield os.path.join(dirpath, filename)
176+ else:
177+ yield path
178+
179+
180+
181+def checkRecursive(paths, reporter):
182+ """
183+ Recursively check all source files in C{paths}.
184+
185+ @param paths: A list of paths to Python source files and directories
186+ containing Python source files.
187+ @param reporter: A L{Reporter} where all of the warnings and errors
188+ will be reported to.
189+ @return: The number of warnings found.
190+ """
191+ warnings = 0
192+ for sourcePath in iterSourceCode(paths):
193+ warnings += checkPath(sourcePath, reporter)
194+ return warnings
195+
196+
197+
198 def main():
199- warnings = 0
200 args = sys.argv[1:]
201+ reporter = _makeDefaultReporter()
202 if args:
203- for arg in args:
204- if os.path.isdir(arg):
205- for dirpath, dirnames, filenames in os.walk(arg):
206- for filename in filenames:
207- if filename.endswith('.py'):
208- warnings += checkPath(os.path.join(dirpath, filename))
209- else:
210- warnings += checkPath(arg)
211+ warnings = checkRecursive(args, reporter)
212 else:
213- warnings += check(sys.stdin.read(), '<stdin>')
214-
215+ warnings = check(sys.stdin.read(), '<stdin>', reporter)
216 raise SystemExit(warnings > 0)
217
218=== modified file 'Pyflakes/pyflakes/test/test_script.py'
219--- Pyflakes/pyflakes/test/test_script.py 2009-06-17 20:58:48 +0000
220+++ Pyflakes/pyflakes/test/test_script.py 2012-07-08 13:52:19 +0000
221@@ -1,51 +1,293 @@
222-
223 """
224 Tests for L{pyflakes.scripts.pyflakes}.
225 """
226
227+import os
228+import subprocess
229 import sys
230 from StringIO import StringIO
231
232 from twisted.python.filepath import FilePath
233 from twisted.trial.unittest import TestCase
234
235-from pyflakes.scripts.pyflakes import checkPath
236-
237-def withStderrTo(stderr, f):
238+from pyflakes.messages import UnusedImport
239+from pyflakes.scripts.pyflakes import (
240+ checkPath,
241+ checkRecursive,
242+ iterSourceCode,
243+ Reporter,
244+ )
245+
246+
247+def withStderrTo(stderr, f, *args, **kwargs):
248 """
249 Call C{f} with C{sys.stderr} redirected to C{stderr}.
250 """
251 (outer, sys.stderr) = (sys.stderr, stderr)
252 try:
253- return f()
254+ return f(*args, **kwargs)
255 finally:
256 sys.stderr = outer
257
258
259
260+class LoggingReporter(object):
261+
262+ def __init__(self, log):
263+ self.log = log
264+
265+
266+ def flake(self, message):
267+ self.log.append(('flake', str(message)))
268+
269+
270+ def ioError(self, filename, exception):
271+ self.log.append(('ioError', filename, exception.args[1]))
272+
273+
274+ def problemDecodingSource(self, filename):
275+ self.log.append(('problemDecodingSource', filename))
276+
277+
278+ def syntaxError(self, filename, msg, lineno, offset, line):
279+ self.log.append(('syntaxError', filename, msg, lineno, offset, line))
280+
281+
282+
283+class TestIterSourceCode(TestCase):
284+ """
285+ Tests for L{iterSourceCode}.
286+ """
287+
288+ def test_emptyDirectory(self):
289+ """
290+ There are no Python files in an empty directory.
291+ """
292+ tempdir = FilePath(self.mktemp())
293+ tempdir.createDirectory()
294+ self.assertEqual(list(iterSourceCode([tempdir.path])), [])
295+
296+
297+ def test_singleFile(self):
298+ """
299+ If the directory contains one Python file, C{iterSourceCode} will find
300+ it.
301+ """
302+ tempdir = FilePath(self.mktemp())
303+ tempdir.createDirectory()
304+ tempdir.child('foo.py').touch()
305+ self.assertEqual(
306+ list(iterSourceCode([tempdir.path])),
307+ [os.path.join(tempdir.path, 'foo.py')])
308+
309+
310+ def test_onlyPythonSource(self):
311+ """
312+ Files that are not Python source files are not included.
313+ """
314+ tempdir = FilePath(self.mktemp())
315+ tempdir.createDirectory()
316+ tempdir.child('foo.pyc').touch()
317+ self.assertEqual(list(iterSourceCode([tempdir.path])), [])
318+
319+
320+ def test_recurses(self):
321+ """
322+ If the Python files are hidden deep down in child directories, we will
323+ find them.
324+ """
325+ tempdir = FilePath(self.mktemp())
326+ tempdir.createDirectory()
327+ tempdir.child('foo').createDirectory()
328+ tempdir.child('foo').child('a.py').touch()
329+ tempdir.child('bar').createDirectory()
330+ tempdir.child('bar').child('b.py').touch()
331+ tempdir.child('c.py').touch()
332+ self.assertEqual(
333+ sorted(iterSourceCode([tempdir.path])),
334+ sorted([os.path.join(tempdir.path, 'foo/a.py'),
335+ os.path.join(tempdir.path, 'bar/b.py'),
336+ os.path.join(tempdir.path, 'c.py')]))
337+
338+
339+ def test_multipleDirectories(self):
340+ """
341+ L{iterSourceCode} can be given multiple directories. It will recurse
342+ into each of them.
343+ """
344+ tempdir = FilePath(self.mktemp())
345+ tempdir.createDirectory()
346+ foo = tempdir.child('foo')
347+ foo.createDirectory()
348+ foo.child('a.py').touch()
349+ bar = tempdir.child('bar')
350+ bar.createDirectory()
351+ bar.child('b.py').touch()
352+ self.assertEqual(
353+ sorted(iterSourceCode([foo.path, bar.path])),
354+ sorted([os.path.join(foo.path, 'a.py'),
355+ os.path.join(bar.path, 'b.py')]))
356+
357+
358+ def test_explicitFiles(self):
359+ """
360+ If one of the paths given to L{iterSourceCode} is not a directory but
361+ a file, it will include that in its output.
362+ """
363+ tempfile = FilePath(self.mktemp())
364+ tempfile.touch()
365+ self.assertEqual(list(iterSourceCode([tempfile.path])),
366+ [tempfile.path])
367+
368+
369+
370+class TestReporter(TestCase):
371+ """
372+ Tests for L{Reporter}.
373+ """
374+
375+ def test_problemDecodingSource(self):
376+ """
377+ C{problemDecodingSource} reports that there was a problem decoding the
378+ source to the error stream. It includes the filename that it couldn't
379+ decode.
380+ """
381+ err = StringIO()
382+ reporter = Reporter(None, err)
383+ reporter.problemDecodingSource('foo.py')
384+ self.assertEquals("foo.py: problem decoding source\n", err.getvalue())
385+
386+
387+ def test_syntaxError(self):
388+ """
389+ C{syntaxError} reports that there was a syntax error in the source
390+ file. It reports to the error stream and includes the filename, line
391+ number, error message, actual line of source and a caret pointing to
392+ where the error is.
393+ """
394+ err = StringIO()
395+ reporter = Reporter(None, err)
396+ reporter.syntaxError('foo.py', 'a problem', 3, 4, 'bad line of source')
397+ self.assertEquals(
398+ ("foo.py:3: a problem\n"
399+ "bad line of source\n"
400+ " ^\n"),
401+ err.getvalue())
402+
403+
404+ def test_syntaxErrorNoOffset(self):
405+ """
406+ C{syntaxError} doesn't include a caret pointing to the error if
407+ C{offset} is passed as C{None}.
408+ """
409+ err = StringIO()
410+ reporter = Reporter(None, err)
411+ reporter.syntaxError('foo.py', 'a problem', 3, None,
412+ 'bad line of source')
413+ self.assertEquals(
414+ ("foo.py:3: a problem\n"
415+ "bad line of source\n"),
416+ err.getvalue())
417+
418+
419+ def test_multiLineSyntaxError(self):
420+ """
421+ If there's a multi-line syntax error, then we only report the last
422+ line. The offset is adjusted so that it is relative to the start of
423+ the last line.
424+ """
425+ err = StringIO()
426+ lines = [
427+ 'bad line of source',
428+ 'more bad lines of source',
429+ ]
430+ reporter = Reporter(None, err)
431+ reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 5,
432+ '\n'.join(lines))
433+ self.assertEquals(
434+ ("foo.py:3: a problem\n" +
435+ lines[-1] + "\n" +
436+ " ^\n"),
437+ err.getvalue())
438+
439+
440+ def test_ioError(self):
441+ """
442+ C{ioError} reports an error reading a source file. It only includes
443+ the human-readable bit of the error message, and excludes the errno.
444+ """
445+ err = StringIO()
446+ reporter = Reporter(None, err)
447+ exception = IOError(42, 'bar')
448+ try:
449+ raise exception
450+ except IOError, e:
451+ pass
452+ reporter.ioError('source.py', e)
453+ self.assertEquals('source.py: bar\n', err.getvalue())
454+
455+
456+ def test_flake(self):
457+ out = StringIO()
458+ reporter = Reporter(out, None)
459+ message = UnusedImport('foo.py', 42, 'bar')
460+ reporter.flake(message)
461+ self.assertEquals(out.getvalue(), "%s\n" % (message,))
462+
463+
464+
465 class CheckTests(TestCase):
466 """
467 Tests for L{check} and L{checkPath} which check a file for flakes.
468 """
469+
470+ def makeTempFile(self, content):
471+ """
472+ Make a temporary file containing C{content} and return a path to it.
473+ """
474+ path = FilePath(self.mktemp())
475+ path.setContent(content)
476+ return path.path
477+
478+
479+ def assertHasErrors(self, path, errorList):
480+ """
481+ Assert that C{path} causes errors.
482+
483+ @param path: A path to a file to check.
484+ @param errorList: A list of errors expected to be printed to stderr.
485+ """
486+ err = StringIO()
487+ count = withStderrTo(err, checkPath, path)
488+ self.assertEquals(
489+ (count, err.getvalue()), (len(errorList), ''.join(errorList)))
490+
491+
492+ def getErrors(self, path):
493+ log = []
494+ reporter = LoggingReporter(log)
495+ count = checkPath(path, reporter)
496+ return count, log
497+
498+
499 def test_missingTrailingNewline(self):
500 """
501 Source which doesn't end with a newline shouldn't cause any
502 exception to be raised nor an error indicator to be returned by
503 L{check}.
504 """
505- fName = self.mktemp()
506- FilePath(fName).setContent("def foo():\n\tpass\n\t")
507- self.assertFalse(checkPath(fName))
508+ fName = self.makeTempFile("def foo():\n\tpass\n\t")
509+ self.assertHasErrors(fName, [])
510
511
512 def test_checkPathNonExisting(self):
513 """
514 L{checkPath} handles non-existing files.
515 """
516- err = StringIO()
517- count = withStderrTo(err, lambda: checkPath('extremo'))
518- self.assertEquals(err.getvalue(), 'extremo: No such file or directory\n')
519+ count, errors = self.getErrors('extremo')
520 self.assertEquals(count, 1)
521+ self.assertEquals(
522+ errors, [('ioError', 'extremo', 'No such file or directory')])
523
524
525 def test_multilineSyntaxError(self):
526@@ -72,19 +314,14 @@
527 exc = self.assertRaises(SyntaxError, evaluate, source)
528 self.assertTrue(exc.text.count('\n') > 1)
529
530- sourcePath = FilePath(self.mktemp())
531- sourcePath.setContent(source)
532- err = StringIO()
533- count = withStderrTo(err, lambda: checkPath(sourcePath.path))
534- self.assertEqual(count, 1)
535-
536- self.assertEqual(
537- err.getvalue(),
538- """\
539+ sourcePath = self.makeTempFile(source)
540+ self.assertHasErrors(
541+ sourcePath, ["""\
542 %s:8: invalid syntax
543 '''quux'''
544 ^
545-""" % (sourcePath.path,))
546+"""
547+ % (sourcePath,)])
548
549
550 def test_eofSyntaxError(self):
551@@ -92,19 +329,14 @@
552 The error reported for source files which end prematurely causing a
553 syntax error reflects the cause for the syntax error.
554 """
555- source = "def foo("
556- sourcePath = FilePath(self.mktemp())
557- sourcePath.setContent(source)
558- err = StringIO()
559- count = withStderrTo(err, lambda: checkPath(sourcePath.path))
560- self.assertEqual(count, 1)
561- self.assertEqual(
562- err.getvalue(),
563- """\
564+ sourcePath = self.makeTempFile("def foo(")
565+ self.assertHasErrors(
566+ sourcePath,
567+ ["""\
568 %s:1: unexpected EOF while parsing
569 def foo(
570 ^
571-""" % (sourcePath.path,))
572+""" % (sourcePath,)])
573
574
575 def test_nonDefaultFollowsDefaultSyntaxError(self):
576@@ -117,17 +349,13 @@
577 def foo(bar=baz, bax):
578 pass
579 """
580- sourcePath = FilePath(self.mktemp())
581- sourcePath.setContent(source)
582- err = StringIO()
583- count = withStderrTo(err, lambda: checkPath(sourcePath.path))
584- self.assertEqual(count, 1)
585- self.assertEqual(
586- err.getvalue(),
587- """\
588+ sourcePath = self.makeTempFile(source)
589+ self.assertHasErrors(
590+ sourcePath,
591+ ["""\
592 %s:1: non-default argument follows default argument
593 def foo(bar=baz, bax):
594-""" % (sourcePath.path,))
595+""" % (sourcePath,)])
596
597
598 def test_nonKeywordAfterKeywordSyntaxError(self):
599@@ -139,32 +367,39 @@
600 source = """\
601 foo(bar=baz, bax)
602 """
603- sourcePath = FilePath(self.mktemp())
604- sourcePath.setContent(source)
605- err = StringIO()
606- count = withStderrTo(err, lambda: checkPath(sourcePath.path))
607- self.assertEqual(count, 1)
608- self.assertEqual(
609- err.getvalue(),
610- """\
611+ sourcePath = self.makeTempFile(source)
612+ self.assertHasErrors(
613+ sourcePath,
614+ ["""\
615 %s:1: non-keyword arg after keyword arg
616 foo(bar=baz, bax)
617-""" % (sourcePath.path,))
618+""" % (sourcePath,)])
619
620
621 def test_permissionDenied(self):
622 """
623- If the a source file is not readable, this is reported on standard
624+ If the source file is not readable, this is reported on standard
625 error.
626 """
627 sourcePath = FilePath(self.mktemp())
628 sourcePath.setContent('')
629 sourcePath.chmod(0)
630- err = StringIO()
631- count = withStderrTo(err, lambda: checkPath(sourcePath.path))
632- self.assertEquals(count, 1)
633- self.assertEquals(
634- err.getvalue(), "%s: Permission denied\n" % (sourcePath.path,))
635+ count, errors = self.getErrors(sourcePath.path)
636+ self.assertEquals(count, 1)
637+ self.assertEquals(
638+ errors, [('ioError', sourcePath.path, "Permission denied")])
639+
640+
641+ def test_pyflakesWarning(self):
642+ """
643+ If the source file has a pyflakes warning, this is reported as a
644+ 'flake'.
645+ """
646+ sourcePath = self.makeTempFile("import foo")
647+ count, errors = self.getErrors(sourcePath)
648+ self.assertEquals(count, 1)
649+ self.assertEquals(
650+ errors, [('flake', str(UnusedImport(sourcePath, 1, 'foo')))])
651
652
653 def test_misencodedFile(self):
654@@ -176,10 +411,109 @@
655 # coding: ascii
656 x = "\N{SNOWMAN}"
657 """.encode('utf-8')
658- sourcePath = FilePath(self.mktemp())
659- sourcePath.setContent(source)
660- err = StringIO()
661- count = withStderrTo(err, lambda: checkPath(sourcePath.path))
662- self.assertEquals(count, 1)
663- self.assertEquals(
664- err.getvalue(), "%s: problem decoding source\n" % (sourcePath.path,))
665+ sourcePath = self.makeTempFile(source)
666+ self.assertHasErrors(
667+ sourcePath, ["%s: problem decoding source\n" % (sourcePath,)])
668+
669+
670+ def test_checkRecursive(self):
671+ """
672+ L{checkRecursive} descends into each directory, finding Python files
673+ and reporting problems.
674+ """
675+ tempdir = FilePath(self.mktemp())
676+ tempdir.createDirectory()
677+ tempdir.child('foo').createDirectory()
678+ file1 = tempdir.child('foo').child('bar.py')
679+ file1.setContent("import baz\n")
680+ file2 = tempdir.child('baz.py')
681+ file2.setContent("import contraband")
682+ log = []
683+ reporter = LoggingReporter(log)
684+ warnings = checkRecursive([tempdir.path], reporter)
685+ self.assertEqual(warnings, 2)
686+ self.assertEqual(
687+ sorted(log),
688+ sorted([('flake', str(UnusedImport(file1.path, 1, 'baz'))),
689+ ('flake',
690+ str(UnusedImport(file2.path, 1, 'contraband')))]))
691+
692+
693+
694+class IntegrationTests(TestCase):
695+ """
696+ Tests of the pyflakes script that actually spawn the script.
697+ """
698+
699+ def getPyflakesBinary(self):
700+ """
701+ Return the path to the pyflakes binary.
702+ """
703+ import pyflakes
704+ return os.path.join(
705+ os.path.dirname(os.path.dirname(pyflakes.__file__)),
706+ 'bin', 'pyflakes')
707+
708+
709+ def popenPyflakes(self, *args, **kwargs):
710+ """
711+ Launch a subprocess running C{pyflakes}.
712+ """
713+ env = dict(os.environ)
714+ env['PYTHONPATH'] = os.pathsep.join(sys.path)
715+ p = subprocess.Popen(
716+ [sys.executable, self.getPyflakesBinary()] + list(args),
717+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
718+ env=env, **kwargs)
719+ return p
720+
721+
722+ def test_goodFile(self):
723+ """
724+ When a Python source file is all good, the return code is zero and no
725+ messages are printed to either stdout or stderr.
726+ """
727+ tempfile = FilePath(self.mktemp())
728+ tempfile.touch()
729+ p = self.popenPyflakes(tempfile.path)
730+ out, err = p.communicate()
731+ self.assertEqual((0, '', ''), (p.returncode, out, err))
732+
733+
734+ def test_fileWithFlakes(self):
735+ """
736+ When a Python source file has warnings, the return code is non-zero
737+ and the warnings are printed to stdout.
738+ """
739+ tempfile = FilePath(self.mktemp())
740+ tempfile.setContent("import contraband\n")
741+ p = self.popenPyflakes(tempfile.path)
742+ out, err = p.communicate()
743+ self.assertEqual(
744+ (1, "%s\n" % UnusedImport(tempfile.path, 1, 'contraband'), ''),
745+ (p.returncode, out, err))
746+
747+
748+ def test_errors(self):
749+ """
750+ When pyflakes finds errors with the files it's given, (if they don't
751+ exist, say), then the return code is non-zero and the errors are
752+ printed to stderr.
753+ """
754+ tempfile = FilePath(self.mktemp())
755+ p = self.popenPyflakes(tempfile.path)
756+ out, err = p.communicate()
757+ self.assertEqual(
758+ (1, '', '%s: No such file or directory\n' % (tempfile.path,)),
759+ (p.returncode, out, err))
760+
761+
762+ def test_readFromStdin(self):
763+ """
764+ If no arguments are passed to C{pyflakes} then it reads from stdin.
765+ """
766+ p = self.popenPyflakes(stdin=subprocess.PIPE)
767+ out, err = p.communicate('import contraband')
768+ self.assertEqual(
769+ (1, "%s\n" % UnusedImport('<stdin>', 1, 'contraband'), ''),
770+ (p.returncode, out, err))

Subscribers

People subscribed via source and target branches

to all changes: