]>
crepu.dev Git - config.git/blob - djavu-asus/elpy/rpc-venv/lib/python3.11/site-packages/pyflakes/test/test_api.py
2 Tests for L{pyflakes.scripts.pyflakes}.
13 from pyflakes
.checker
import PYPY
14 from pyflakes
.messages
import UnusedImport
15 from pyflakes
.reporter
import Reporter
16 from pyflakes
.api
import (
23 from pyflakes
.test
.harness
import TestCase
, skipIf
26 def withStderrTo(stderr
, f
, *args
, **kwargs
):
28 Call C{f} with C{sys.stderr} redirected to C{stderr}.
30 (outer
, sys
.stderr
) = (sys
.stderr
, stderr
)
32 return f(*args
, **kwargs
)
41 def __init__(self
, lineno
, col_offset
=0):
43 self
.col_offset
= col_offset
46 class SysStreamCapturing
:
47 """Context manager capturing sys.stdin, sys.stdout and sys.stderr.
49 The file handles are replaced with a StringIO object.
52 def __init__(self
, stdin
):
53 self
._stdin
= io
.StringIO(stdin
or '', newline
=os
.linesep
)
56 self
._orig
_stdin
= sys
.stdin
57 self
._orig
_stdout
= sys
.stdout
58 self
._orig
_stderr
= sys
.stderr
60 sys
.stdin
= self
._stdin
61 sys
.stdout
= self
._stdout
_stringio
= io
.StringIO(newline
=os
.linesep
)
62 sys
.stderr
= self
._stderr
_stringio
= io
.StringIO(newline
=os
.linesep
)
66 def __exit__(self
, *args
):
67 self
.output
= self
._stdout
_stringio
.getvalue()
68 self
.error
= self
._stderr
_stringio
.getvalue()
70 sys
.stdin
= self
._orig
_stdin
71 sys
.stdout
= self
._orig
_stdout
72 sys
.stderr
= self
._orig
_stderr
75 class LoggingReporter
:
77 Implementation of Reporter that just appends any error to a list.
80 def __init__(self
, log
):
82 Construct a C{LoggingReporter}.
84 @param log: A list to append log messages to.
88 def flake(self
, message
):
89 self
.log
.append(('flake', str(message
)))
91 def unexpectedError(self
, filename
, message
):
92 self
.log
.append(('unexpectedError', filename
, message
))
94 def syntaxError(self
, filename
, msg
, lineno
, offset
, line
):
95 self
.log
.append(('syntaxError', filename
, msg
, lineno
, offset
, line
))
98 class TestIterSourceCode(TestCase
):
100 Tests for L{iterSourceCode}.
104 self
.tempdir
= tempfile
.mkdtemp()
107 shutil
.rmtree(self
.tempdir
)
109 def makeEmptyFile(self
, *parts
):
111 fpath
= os
.path
.join(self
.tempdir
, *parts
)
112 open(fpath
, 'a').close()
115 def test_emptyDirectory(self
):
117 There are no Python files in an empty directory.
119 self
.assertEqual(list(iterSourceCode([self
.tempdir
])), [])
121 def test_singleFile(self
):
123 If the directory contains one Python file, C{iterSourceCode} will find
126 childpath
= self
.makeEmptyFile('foo.py')
127 self
.assertEqual(list(iterSourceCode([self
.tempdir
])), [childpath
])
129 def test_onlyPythonSource(self
):
131 Files that are not Python source files are not included.
133 self
.makeEmptyFile('foo.pyc')
134 self
.assertEqual(list(iterSourceCode([self
.tempdir
])), [])
136 def test_recurses(self
):
138 If the Python files are hidden deep down in child directories, we will
141 os
.mkdir(os
.path
.join(self
.tempdir
, 'foo'))
142 apath
= self
.makeEmptyFile('foo', 'a.py')
143 self
.makeEmptyFile('foo', 'a.py~')
144 os
.mkdir(os
.path
.join(self
.tempdir
, 'bar'))
145 bpath
= self
.makeEmptyFile('bar', 'b.py')
146 cpath
= self
.makeEmptyFile('c.py')
148 sorted(iterSourceCode([self
.tempdir
])),
149 sorted([apath
, bpath
, cpath
]))
151 def test_shebang(self
):
153 Find Python files that don't end with `.py`, but contain a Python
156 python
= os
.path
.join(self
.tempdir
, 'a')
157 with
open(python
, 'w') as fd
:
158 fd
.write('#!/usr/bin/env python\n')
160 self
.makeEmptyFile('b')
162 with
open(os
.path
.join(self
.tempdir
, 'c'), 'w') as fd
:
163 fd
.write('hello\nworld\n')
165 python3
= os
.path
.join(self
.tempdir
, 'e')
166 with
open(python3
, 'w') as fd
:
167 fd
.write('#!/usr/bin/env python3\n')
169 pythonw
= os
.path
.join(self
.tempdir
, 'f')
170 with
open(pythonw
, 'w') as fd
:
171 fd
.write('#!/usr/bin/env pythonw\n')
173 python3args
= os
.path
.join(self
.tempdir
, 'g')
174 with
open(python3args
, 'w') as fd
:
175 fd
.write('#!/usr/bin/python3 -u\n')
177 python3d
= os
.path
.join(self
.tempdir
, 'i')
178 with
open(python3d
, 'w') as fd
:
179 fd
.write('#!/usr/local/bin/python3d\n')
181 python38m
= os
.path
.join(self
.tempdir
, 'j')
182 with
open(python38m
, 'w') as fd
:
183 fd
.write('#! /usr/bin/env python3.8m\n')
185 # Should NOT be treated as Python source
186 notfirst
= os
.path
.join(self
.tempdir
, 'l')
187 with
open(notfirst
, 'w') as fd
:
188 fd
.write('#!/bin/sh\n#!/usr/bin/python\n')
191 sorted(iterSourceCode([self
.tempdir
])),
193 python
, python3
, pythonw
, python3args
, python3d
,
197 def test_multipleDirectories(self
):
199 L{iterSourceCode} can be given multiple directories. It will recurse
202 foopath
= os
.path
.join(self
.tempdir
, 'foo')
203 barpath
= os
.path
.join(self
.tempdir
, 'bar')
205 apath
= self
.makeEmptyFile('foo', 'a.py')
207 bpath
= self
.makeEmptyFile('bar', 'b.py')
209 sorted(iterSourceCode([foopath
, barpath
])),
210 sorted([apath
, bpath
]))
212 def test_explicitFiles(self
):
214 If one of the paths given to L{iterSourceCode} is not a directory but
215 a file, it will include that in its output.
217 epath
= self
.makeEmptyFile('e.py')
218 self
.assertEqual(list(iterSourceCode([epath
])),
222 class TestReporter(TestCase
):
224 Tests for L{Reporter}.
227 def test_syntaxError(self
):
229 C{syntaxError} reports that there was a syntax error in the source
230 file. It reports to the error stream and includes the filename, line
231 number, error message, actual line of source and a caret pointing to
235 reporter
= Reporter(None, err
)
236 reporter
.syntaxError('foo.py', 'a problem', 3, 8, 'bad line of source')
238 ("foo.py:3:8: a problem\n"
239 "bad line of source\n"
243 def test_syntaxErrorNoOffset(self
):
245 C{syntaxError} doesn't include a caret pointing to the error if
246 C{offset} is passed as C{None}.
249 reporter
= Reporter(None, err
)
250 reporter
.syntaxError('foo.py', 'a problem', 3, None,
251 'bad line of source')
253 ("foo.py:3: a problem\n"
254 "bad line of source\n"),
257 def test_syntaxErrorNoText(self
):
259 C{syntaxError} doesn't include text or nonsensical offsets if C{text} is C{None}.
261 This typically happens when reporting syntax errors from stdin.
264 reporter
= Reporter(None, err
)
265 reporter
.syntaxError('<stdin>', 'a problem', 0, 0, None)
266 self
.assertEqual(("<stdin>:1:1: a problem\n"), err
.getvalue())
268 def test_multiLineSyntaxError(self
):
270 If there's a multi-line syntax error, then we only report the last
271 line. The offset is adjusted so that it is relative to the start of
276 'bad line of source',
277 'more bad lines of source',
279 reporter
= Reporter(None, err
)
280 reporter
.syntaxError('foo.py', 'a problem', 3, len(lines
[0]) + 7,
283 ("foo.py:3:25: a problem\n" +
288 def test_unexpectedError(self
):
290 C{unexpectedError} reports an error processing a source file.
293 reporter
= Reporter(None, err
)
294 reporter
.unexpectedError('source.py', 'error message')
295 self
.assertEqual('source.py: error message\n', err
.getvalue())
297 def test_flake(self
):
299 C{flake} reports a code warning from Pyflakes. It is exactly the
300 str() of a L{pyflakes.messages.Message}.
303 reporter
= Reporter(out
, None)
304 message
= UnusedImport('foo.py', Node(42), 'bar')
305 reporter
.flake(message
)
306 self
.assertEqual(out
.getvalue(), f
"{message}\n")
309 class CheckTests(TestCase
):
311 Tests for L{check} and L{checkPath} which check a file for flakes.
314 @contextlib.contextmanager
315 def makeTempFile(self
, content
):
317 Make a temporary file containing C{content} and return a path to it.
319 fd
, name
= tempfile
.mkstemp()
321 with os
.fdopen(fd
, 'wb') as f
:
322 if not hasattr(content
, 'decode'):
323 content
= content
.encode('ascii')
329 def assertHasErrors(self
, path
, errorList
):
331 Assert that C{path} causes errors.
333 @param path: A path to a file to check.
334 @param errorList: A list of errors expected to be printed to stderr.
337 count
= withStderrTo(err
, checkPath
, path
)
339 (count
, err
.getvalue()), (len(errorList
), ''.join(errorList
)))
341 def getErrors(self
, path
):
343 Get any warnings or errors reported by pyflakes for the file at C{path}.
345 @param path: The path to a Python file on disk that pyflakes will check.
346 @return: C{(count, log)}, where C{count} is the number of warnings or
347 errors generated, and log is a list of those warnings, presented
348 as structured data. See L{LoggingReporter} for more details.
351 reporter
= LoggingReporter(log
)
352 count
= checkPath(path
, reporter
)
355 def test_legacyScript(self
):
356 from pyflakes
.scripts
import pyflakes
as script_pyflakes
357 self
.assertIs(script_pyflakes
.checkPath
, checkPath
)
359 def test_missingTrailingNewline(self
):
361 Source which doesn't end with a newline shouldn't cause any
362 exception to be raised nor an error indicator to be returned by
365 with self
.makeTempFile("def foo():\n\tpass\n\t") as fName
:
366 self
.assertHasErrors(fName
, [])
368 def test_checkPathNonExisting(self
):
370 L{checkPath} handles non-existing files.
372 count
, errors
= self
.getErrors('extremo')
373 self
.assertEqual(count
, 1)
376 [('unexpectedError', 'extremo', 'No such file or directory')])
378 def test_multilineSyntaxError(self
):
380 Source which includes a syntax error which results in the raised
381 L{SyntaxError.text} containing multiple lines of source are reported
382 with only the last line of that source.
395 # Sanity check - SyntaxError.text should be multiple lines, if it
396 # isn't, something this test was unprepared for has happened.
397 def evaluate(source
):
401 except SyntaxError as e
:
402 if not PYPY
and sys
.version_info
< (3, 10):
403 self
.assertTrue(e
.text
.count('\n') > 1)
407 with self
.makeTempFile(source
) as sourcePath
:
409 message
= 'end of file (EOF) while scanning triple-quoted string literal'
410 elif sys
.version_info
>= (3, 10):
411 message
= 'unterminated triple-quoted string literal (detected at line 8)' # noqa: E501
413 message
= 'invalid syntax'
415 if PYPY
or sys
.version_info
>= (3, 10):
419 self
.assertHasErrors(
425 """ % (sourcePath
, column
, message
, ' ' * (column
- 1))])
427 def test_eofSyntaxError(self
):
429 The error reported for source files which end prematurely causing a
430 syntax error reflects the cause for the syntax error.
432 with self
.makeTempFile("def foo(") as sourcePath
:
434 msg
= 'parenthesis is never closed'
435 elif sys
.version_info
>= (3, 10):
436 msg
= "'(' was never closed"
438 msg
= 'unexpected EOF while parsing'
440 if PYPY
or sys
.version_info
>= (3, 10):
445 spaces
= ' ' * (column
- 1)
446 expected
= '{}:1:{}: {}\ndef foo(\n{}^\n'.format(
447 sourcePath
, column
, msg
, spaces
450 self
.assertHasErrors(sourcePath
, [expected
])
452 def test_eofSyntaxErrorWithTab(self
):
454 The error reported for source files which end prematurely causing a
455 syntax error reflects the cause for the syntax error.
457 with self
.makeTempFile("if True:\n\tfoo =") as sourcePath
:
458 self
.assertHasErrors(
461 {sourcePath}:2:7: invalid syntax
466 def test_nonDefaultFollowsDefaultSyntaxError(self
):
468 Source which has a non-default argument following a default argument
469 should include the line number of the syntax error. However these
470 exceptions do not include an offset.
473 def foo(bar=baz, bax):
476 with self
.makeTempFile(source
) as sourcePath
:
477 if sys
.version_info
>= (3, 12):
478 msg
= 'parameter without a default follows parameter with a default' # noqa: E501
480 msg
= 'non-default argument follows default argument'
482 if PYPY
and sys
.version_info
>= (3, 9):
486 elif sys
.version_info
>= (3, 10):
488 elif sys
.version_info
>= (3, 9):
492 last_line
= ' ' * (column
- 1) + '^\n'
493 self
.assertHasErrors(
496 {sourcePath}:1:{column}: {msg}
497 def foo(bar=baz, bax):
501 def test_nonKeywordAfterKeywordSyntaxError(self
):
503 Source which has a non-keyword argument after a keyword argument should
504 include the line number of the syntax error. However these exceptions
505 do not include an offset.
510 with self
.makeTempFile(source
) as sourcePath
:
511 if sys
.version_info
>= (3, 9):
517 last_line
= ' ' * (column
- 1) + '^\n'
518 columnstr
= '%d:' % column
520 message
= 'positional argument follows keyword argument'
522 self
.assertHasErrors(
527 {}""".format(sourcePath
, columnstr
, message
, last_line
)])
529 def test_invalidEscape(self
):
531 The invalid escape syntax raises ValueError in Python 2
533 # ValueError: invalid \x escape
534 with self
.makeTempFile(r
"foo = '\xyz'") as sourcePath
:
536 if PYPY
and sys
.version_info
>= (3, 9):
540 elif (3, 9) <= sys
.version_info
< (3, 12):
545 last_line
= '%s^\n' % (' ' * (column
- 1))
547 decoding_error
= """\
548 %s:1:%d: (unicode error) 'unicodeescape' codec can't decode bytes \
549 in position 0-%d: truncated \\xXX escape
551 %s""" % (sourcePath
, column
, position_end
, last_line
)
553 self
.assertHasErrors(
554 sourcePath
, [decoding_error
])
556 @skipIf(sys
.platform
== 'win32', 'unsupported on Windows')
557 def test_permissionDenied(self
):
559 If the source file is not readable, this is reported on standard
563 self
.skipTest('root user can access all files regardless of '
565 with self
.makeTempFile('') as sourcePath
:
566 os
.chmod(sourcePath
, 0)
567 count
, errors
= self
.getErrors(sourcePath
)
568 self
.assertEqual(count
, 1)
571 [('unexpectedError', sourcePath
, "Permission denied")])
573 def test_pyflakesWarning(self
):
575 If the source file has a pyflakes warning, this is reported as a
578 with self
.makeTempFile("import foo") as sourcePath
:
579 count
, errors
= self
.getErrors(sourcePath
)
580 self
.assertEqual(count
, 1)
582 errors
, [('flake', str(UnusedImport(sourcePath
, Node(1), 'foo')))])
584 def test_encodedFileUTF8(self
):
586 If source file declares the correct encoding, no error is reported.
588 SNOWMAN
= chr(0x2603)
592 """ % SNOWMAN
).encode('utf-8')
593 with self
.makeTempFile(source
) as sourcePath
:
594 self
.assertHasErrors(sourcePath
, [])
596 def test_CRLFLineEndings(self
):
598 Source files with Windows CR LF line endings are parsed successfully.
600 with self
.makeTempFile("x = 42\r\n") as sourcePath
:
601 self
.assertHasErrors(sourcePath
, [])
603 def test_misencodedFileUTF8(self
):
605 If a source file contains bytes which cannot be decoded, this is
608 SNOWMAN
= chr(0x2603)
612 """ % SNOWMAN
).encode('utf-8')
613 with self
.makeTempFile(source
) as sourcePath
:
614 self
.assertHasErrors(
616 [f
"{sourcePath}:1:1: 'ascii' codec can't decode byte 0xe2 in position 21: ordinal not in range(128)\n"]) # noqa: E501
618 def test_misencodedFileUTF16(self
):
620 If a source file contains bytes which cannot be decoded, this is
623 SNOWMAN
= chr(0x2603)
627 """ % SNOWMAN
).encode('utf-16')
628 with self
.makeTempFile(source
) as sourcePath
:
629 if sys
.version_info
< (3, 11, 4):
630 expected
= f
"{sourcePath}: problem decoding source\n"
632 expected
= f
"{sourcePath}:1: source code string cannot contain null bytes\n" # noqa: E501
634 self
.assertHasErrors(sourcePath
, [expected
])
636 def test_checkRecursive(self
):
638 L{checkRecursive} descends into each directory, finding Python files
639 and reporting problems.
641 tempdir
= tempfile
.mkdtemp()
643 os
.mkdir(os
.path
.join(tempdir
, 'foo'))
644 file1
= os
.path
.join(tempdir
, 'foo', 'bar.py')
645 with
open(file1
, 'wb') as fd
:
646 fd
.write(b
"import baz\n")
647 file2
= os
.path
.join(tempdir
, 'baz.py')
648 with
open(file2
, 'wb') as fd
:
649 fd
.write(b
"import contraband")
651 reporter
= LoggingReporter(log
)
652 warnings
= checkRecursive([tempdir
], reporter
)
653 self
.assertEqual(warnings
, 2)
656 sorted([('flake', str(UnusedImport(file1
, Node(1), 'baz'))),
658 str(UnusedImport(file2
, Node(1), 'contraband')))]))
660 shutil
.rmtree(tempdir
)
662 def test_stdinReportsErrors(self
):
664 L{check} reports syntax errors from stdin
666 source
= "max(1 for i in range(10), key=lambda x: x+1)\n"
668 count
= withStderrTo(err
, check
, source
, "<stdin>")
669 self
.assertEqual(count
, 1)
670 errlines
= err
.getvalue().split("\n")[:-1]
672 if sys
.version_info
>= (3, 9):
674 "<stdin>:1:5: Generator expression must be parenthesized",
675 "max(1 for i in range(10), key=lambda x: x+1)",
680 "<stdin>:1:4: Generator expression must be parenthesized if not sole argument", # noqa: E501
681 "max(1 for i in range(10), key=lambda x: x+1)",
686 "<stdin>:1:5: Generator expression must be parenthesized",
689 self
.assertEqual(errlines
, expected_error
)
692 class IntegrationTests(TestCase
):
694 Tests of the pyflakes script that actually spawn the script.
697 self
.tempdir
= tempfile
.mkdtemp()
698 self
.tempfilepath
= os
.path
.join(self
.tempdir
, 'temp')
701 shutil
.rmtree(self
.tempdir
)
703 def getPyflakesBinary(self
):
705 Return the path to the pyflakes binary.
708 package_dir
= os
.path
.dirname(pyflakes
.__file
__)
709 return os
.path
.join(package_dir
, '..', 'bin', 'pyflakes')
711 def runPyflakes(self
, paths
, stdin
=None):
713 Launch a subprocess running C{pyflakes}.
715 @param paths: Command-line arguments to pass to pyflakes.
716 @param stdin: Text to use as stdin.
717 @return: C{(returncode, stdout, stderr)} of the completed pyflakes
720 env
= dict(os
.environ
)
721 env
['PYTHONPATH'] = os
.pathsep
.join(sys
.path
)
722 command
= [sys
.executable
, self
.getPyflakesBinary()]
723 command
.extend(paths
)
725 p
= subprocess
.Popen(command
, env
=env
, stdin
=subprocess
.PIPE
,
726 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
727 (stdout
, stderr
) = p
.communicate(stdin
.encode('ascii'))
729 p
= subprocess
.Popen(command
, env
=env
,
730 stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
)
731 (stdout
, stderr
) = p
.communicate()
733 stdout
= stdout
.decode('utf-8')
734 stderr
= stderr
.decode('utf-8')
735 return (stdout
, stderr
, rv
)
737 def test_goodFile(self
):
739 When a Python source file is all good, the return code is zero and no
740 messages are printed to either stdout or stderr.
742 open(self
.tempfilepath
, 'a').close()
743 d
= self
.runPyflakes([self
.tempfilepath
])
744 self
.assertEqual(d
, ('', '', 0))
746 def test_fileWithFlakes(self
):
748 When a Python source file has warnings, the return code is non-zero
749 and the warnings are printed to stdout.
751 with
open(self
.tempfilepath
, 'wb') as fd
:
752 fd
.write(b
"import contraband\n")
753 d
= self
.runPyflakes([self
.tempfilepath
])
754 expected
= UnusedImport(self
.tempfilepath
, Node(1), 'contraband')
755 self
.assertEqual(d
, (f
"{expected}{os.linesep}", '', 1))
757 def test_errors_io(self
):
759 When pyflakes finds errors with the files it's given, (if they don't
760 exist, say), then the return code is non-zero and the errors are
763 d
= self
.runPyflakes([self
.tempfilepath
])
764 error_msg
= '{}: No such file or directory{}'.format(self
.tempfilepath
,
766 self
.assertEqual(d
, ('', error_msg
, 1))
768 def test_errors_syntax(self
):
770 When pyflakes finds errors with the files it's given, (if they don't
771 exist, say), then the return code is non-zero and the errors are
774 with
open(self
.tempfilepath
, 'wb') as fd
:
776 d
= self
.runPyflakes([self
.tempfilepath
])
777 error_msg
= '{0}:1:7: invalid syntax{1}import{1} ^{1}'.format(
778 self
.tempfilepath
, os
.linesep
)
779 self
.assertEqual(d
, ('', error_msg
, 1))
781 def test_readFromStdin(self
):
783 If no arguments are passed to C{pyflakes} then it reads from stdin.
785 d
= self
.runPyflakes([], stdin
='import contraband')
786 expected
= UnusedImport('<stdin>', Node(1), 'contraband')
787 self
.assertEqual(d
, (f
"{expected}{os.linesep}", '', 1))
790 class TestMain(IntegrationTests
):
792 Tests of the pyflakes main function.
794 def runPyflakes(self
, paths
, stdin
=None):
796 with
SysStreamCapturing(stdin
) as capture
:
798 except SystemExit as e
:
799 self
.assertIsInstance(e
.code
, bool)
801 return (capture
.output
, capture
.error
, rv
)
803 raise RuntimeError('SystemExit not raised')