This commit is contained in:
Waylon Walker 2022-03-31 20:20:07 -05:00
commit 38355d2442
No known key found for this signature in database
GPG key ID: 66E2BF2B4190EFE4
9083 changed files with 1225834 additions and 0 deletions

View file

@ -0,0 +1,72 @@
import ast
import textwrap
import unittest
from pyflakes import checker
__all__ = ['TestCase', 'skip', 'skipIf']
skip = unittest.skip
skipIf = unittest.skipIf
class TestCase(unittest.TestCase):
withDoctest = False
def flakes(self, input, *expectedOutputs, **kw):
tree = ast.parse(textwrap.dedent(input))
file_tokens = checker.make_tokens(textwrap.dedent(input))
if kw.get('is_segment'):
tree = tree.body[0]
kw.pop('is_segment')
w = checker.Checker(
tree, file_tokens=file_tokens, withDoctest=self.withDoctest, **kw
)
outputs = [type(o) for o in w.messages]
expectedOutputs = list(expectedOutputs)
outputs.sort(key=lambda t: t.__name__)
expectedOutputs.sort(key=lambda t: t.__name__)
self.assertEqual(outputs, expectedOutputs, '''\
for input:
%s
expected outputs:
%r
but got:
%s''' % (input, expectedOutputs, '\n'.join([str(o) for o in w.messages])))
return w
if not hasattr(unittest.TestCase, 'assertIs'):
def assertIs(self, expr1, expr2, msg=None):
if expr1 is not expr2:
self.fail(msg or '%r is not %r' % (expr1, expr2))
if not hasattr(unittest.TestCase, 'assertIsInstance'):
def assertIsInstance(self, obj, cls, msg=None):
"""Same as self.assertTrue(isinstance(obj, cls))."""
if not isinstance(obj, cls):
self.fail(msg or '%r is not an instance of %r' % (obj, cls))
if not hasattr(unittest.TestCase, 'assertNotIsInstance'):
def assertNotIsInstance(self, obj, cls, msg=None):
"""Same as self.assertFalse(isinstance(obj, cls))."""
if isinstance(obj, cls):
self.fail(msg or '%r is an instance of %r' % (obj, cls))
if not hasattr(unittest.TestCase, 'assertIn'):
def assertIn(self, member, container, msg=None):
"""Just like self.assertTrue(a in b)."""
if member not in container:
self.fail(msg or '%r not found in %r' % (member, container))
if not hasattr(unittest.TestCase, 'assertNotIn'):
def assertNotIn(self, member, container, msg=None):
"""Just like self.assertTrue(a not in b)."""
if member in container:
self.fail(msg or
'%r unexpectedly found in %r' % (member, container))

View file

@ -0,0 +1,845 @@
"""
Tests for L{pyflakes.scripts.pyflakes}.
"""
import contextlib
import os
import sys
import shutil
import subprocess
import tempfile
from pyflakes.messages import UnusedImport
from pyflakes.reporter import Reporter
from pyflakes.api import (
main,
checkPath,
checkRecursive,
iterSourceCode,
)
from pyflakes.test.harness import TestCase, skipIf
if sys.version_info < (3,):
from cStringIO import StringIO
else:
from io import StringIO
unichr = chr
try:
sys.pypy_version_info
PYPY = True
except AttributeError:
PYPY = False
try:
WindowsError
WIN = True
except NameError:
WIN = False
ERROR_HAS_COL_NUM = ERROR_HAS_LAST_LINE = sys.version_info >= (3, 2) or PYPY
def withStderrTo(stderr, f, *args, **kwargs):
"""
Call C{f} with C{sys.stderr} redirected to C{stderr}.
"""
(outer, sys.stderr) = (sys.stderr, stderr)
try:
return f(*args, **kwargs)
finally:
sys.stderr = outer
class Node(object):
"""
Mock an AST node.
"""
def __init__(self, lineno, col_offset=0):
self.lineno = lineno
self.col_offset = col_offset
class SysStreamCapturing(object):
"""
Context manager capturing sys.stdin, sys.stdout and sys.stderr.
The file handles are replaced with a StringIO object.
On environments that support it, the StringIO object uses newlines
set to os.linesep. Otherwise newlines are converted from \\n to
os.linesep during __exit__.
"""
def _create_StringIO(self, buffer=None):
# Python 3 has a newline argument
try:
return StringIO(buffer, newline=os.linesep)
except TypeError:
self._newline = True
# Python 2 creates an input only stream when buffer is not None
if buffer is None:
return StringIO()
else:
return StringIO(buffer)
def __init__(self, stdin):
self._newline = False
self._stdin = self._create_StringIO(stdin or '')
def __enter__(self):
self._orig_stdin = sys.stdin
self._orig_stdout = sys.stdout
self._orig_stderr = sys.stderr
sys.stdin = self._stdin
sys.stdout = self._stdout_stringio = self._create_StringIO()
sys.stderr = self._stderr_stringio = self._create_StringIO()
return self
def __exit__(self, *args):
self.output = self._stdout_stringio.getvalue()
self.error = self._stderr_stringio.getvalue()
if self._newline and os.linesep != '\n':
self.output = self.output.replace('\n', os.linesep)
self.error = self.error.replace('\n', os.linesep)
sys.stdin = self._orig_stdin
sys.stdout = self._orig_stdout
sys.stderr = self._orig_stderr
class LoggingReporter(object):
"""
Implementation of Reporter that just appends any error to a list.
"""
def __init__(self, log):
"""
Construct a C{LoggingReporter}.
@param log: A list to append log messages to.
"""
self.log = log
def flake(self, message):
self.log.append(('flake', str(message)))
def unexpectedError(self, filename, message):
self.log.append(('unexpectedError', filename, message))
def syntaxError(self, filename, msg, lineno, offset, line):
self.log.append(('syntaxError', filename, msg, lineno, offset, line))
class TestIterSourceCode(TestCase):
"""
Tests for L{iterSourceCode}.
"""
def setUp(self):
self.tempdir = tempfile.mkdtemp()
def tearDown(self):
shutil.rmtree(self.tempdir)
def makeEmptyFile(self, *parts):
assert parts
fpath = os.path.join(self.tempdir, *parts)
open(fpath, 'a').close()
return fpath
def test_emptyDirectory(self):
"""
There are no Python files in an empty directory.
"""
self.assertEqual(list(iterSourceCode([self.tempdir])), [])
def test_singleFile(self):
"""
If the directory contains one Python file, C{iterSourceCode} will find
it.
"""
childpath = self.makeEmptyFile('foo.py')
self.assertEqual(list(iterSourceCode([self.tempdir])), [childpath])
def test_onlyPythonSource(self):
"""
Files that are not Python source files are not included.
"""
self.makeEmptyFile('foo.pyc')
self.assertEqual(list(iterSourceCode([self.tempdir])), [])
def test_recurses(self):
"""
If the Python files are hidden deep down in child directories, we will
find them.
"""
os.mkdir(os.path.join(self.tempdir, 'foo'))
apath = self.makeEmptyFile('foo', 'a.py')
self.makeEmptyFile('foo', 'a.py~')
os.mkdir(os.path.join(self.tempdir, 'bar'))
bpath = self.makeEmptyFile('bar', 'b.py')
cpath = self.makeEmptyFile('c.py')
self.assertEqual(
sorted(iterSourceCode([self.tempdir])),
sorted([apath, bpath, cpath]))
def test_shebang(self):
"""
Find Python files that don't end with `.py`, but contain a Python
shebang.
"""
python = os.path.join(self.tempdir, 'a')
with open(python, 'w') as fd:
fd.write('#!/usr/bin/env python\n')
self.makeEmptyFile('b')
with open(os.path.join(self.tempdir, 'c'), 'w') as fd:
fd.write('hello\nworld\n')
python2 = os.path.join(self.tempdir, 'd')
with open(python2, 'w') as fd:
fd.write('#!/usr/bin/env python2\n')
python3 = os.path.join(self.tempdir, 'e')
with open(python3, 'w') as fd:
fd.write('#!/usr/bin/env python3\n')
pythonw = os.path.join(self.tempdir, 'f')
with open(pythonw, 'w') as fd:
fd.write('#!/usr/bin/env pythonw\n')
python3args = os.path.join(self.tempdir, 'g')
with open(python3args, 'w') as fd:
fd.write('#!/usr/bin/python3 -u\n')
python2u = os.path.join(self.tempdir, 'h')
with open(python2u, 'w') as fd:
fd.write('#!/usr/bin/python2u\n')
python3d = os.path.join(self.tempdir, 'i')
with open(python3d, 'w') as fd:
fd.write('#!/usr/local/bin/python3d\n')
python38m = os.path.join(self.tempdir, 'j')
with open(python38m, 'w') as fd:
fd.write('#! /usr/bin/env python3.8m\n')
python27 = os.path.join(self.tempdir, 'k')
with open(python27, 'w') as fd:
fd.write('#!/usr/bin/python2.7 \n')
# Should NOT be treated as Python source
notfirst = os.path.join(self.tempdir, 'l')
with open(notfirst, 'w') as fd:
fd.write('#!/bin/sh\n#!/usr/bin/python\n')
self.assertEqual(
sorted(iterSourceCode([self.tempdir])),
sorted([python, python2, python3, pythonw, python3args, python2u,
python3d, python38m, python27]))
def test_multipleDirectories(self):
"""
L{iterSourceCode} can be given multiple directories. It will recurse
into each of them.
"""
foopath = os.path.join(self.tempdir, 'foo')
barpath = os.path.join(self.tempdir, 'bar')
os.mkdir(foopath)
apath = self.makeEmptyFile('foo', 'a.py')
os.mkdir(barpath)
bpath = self.makeEmptyFile('bar', 'b.py')
self.assertEqual(
sorted(iterSourceCode([foopath, barpath])),
sorted([apath, bpath]))
def test_explicitFiles(self):
"""
If one of the paths given to L{iterSourceCode} is not a directory but
a file, it will include that in its output.
"""
epath = self.makeEmptyFile('e.py')
self.assertEqual(list(iterSourceCode([epath])),
[epath])
class TestReporter(TestCase):
"""
Tests for L{Reporter}.
"""
def test_syntaxError(self):
"""
C{syntaxError} reports that there was a syntax error in the source
file. It reports to the error stream and includes the filename, line
number, error message, actual line of source and a caret pointing to
where the error is.
"""
err = StringIO()
reporter = Reporter(None, err)
reporter.syntaxError('foo.py', 'a problem', 3,
8 if sys.version_info >= (3, 8) else 7,
'bad line of source')
self.assertEqual(
("foo.py:3:8: a problem\n"
"bad line of source\n"
" ^\n"),
err.getvalue())
def test_syntaxErrorNoOffset(self):
"""
C{syntaxError} doesn't include a caret pointing to the error if
C{offset} is passed as C{None}.
"""
err = StringIO()
reporter = Reporter(None, err)
reporter.syntaxError('foo.py', 'a problem', 3, None,
'bad line of source')
self.assertEqual(
("foo.py:3: a problem\n"
"bad line of source\n"),
err.getvalue())
def test_multiLineSyntaxError(self):
"""
If there's a multi-line syntax error, then we only report the last
line. The offset is adjusted so that it is relative to the start of
the last line.
"""
err = StringIO()
lines = [
'bad line of source',
'more bad lines of source',
]
reporter = Reporter(None, err)
reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7,
'\n'.join(lines))
column = 25 if sys.version_info >= (3, 8) else 7
self.assertEqual(
("foo.py:3:%d: a problem\n" % column +
lines[-1] + "\n" +
" " * (column - 1) + "^\n"),
err.getvalue())
def test_unexpectedError(self):
"""
C{unexpectedError} reports an error processing a source file.
"""
err = StringIO()
reporter = Reporter(None, err)
reporter.unexpectedError('source.py', 'error message')
self.assertEqual('source.py: error message\n', err.getvalue())
def test_flake(self):
"""
C{flake} reports a code warning from Pyflakes. It is exactly the
str() of a L{pyflakes.messages.Message}.
"""
out = StringIO()
reporter = Reporter(out, None)
message = UnusedImport('foo.py', Node(42), 'bar')
reporter.flake(message)
self.assertEqual(out.getvalue(), "%s\n" % (message,))
class CheckTests(TestCase):
"""
Tests for L{check} and L{checkPath} which check a file for flakes.
"""
@contextlib.contextmanager
def makeTempFile(self, content):
"""
Make a temporary file containing C{content} and return a path to it.
"""
fd, name = tempfile.mkstemp()
try:
with os.fdopen(fd, 'wb') as f:
if not hasattr(content, 'decode'):
content = content.encode('ascii')
f.write(content)
yield name
finally:
os.remove(name)
def assertHasErrors(self, path, errorList):
"""
Assert that C{path} causes errors.
@param path: A path to a file to check.
@param errorList: A list of errors expected to be printed to stderr.
"""
err = StringIO()
count = withStderrTo(err, checkPath, path)
self.assertEqual(
(count, err.getvalue()), (len(errorList), ''.join(errorList)))
def getErrors(self, path):
"""
Get any warnings or errors reported by pyflakes for the file at C{path}.
@param path: The path to a Python file on disk that pyflakes will check.
@return: C{(count, log)}, where C{count} is the number of warnings or
errors generated, and log is a list of those warnings, presented
as structured data. See L{LoggingReporter} for more details.
"""
log = []
reporter = LoggingReporter(log)
count = checkPath(path, reporter)
return count, log
def test_legacyScript(self):
from pyflakes.scripts import pyflakes as script_pyflakes
self.assertIs(script_pyflakes.checkPath, checkPath)
def test_missingTrailingNewline(self):
"""
Source which doesn't end with a newline shouldn't cause any
exception to be raised nor an error indicator to be returned by
L{check}.
"""
with self.makeTempFile("def foo():\n\tpass\n\t") as fName:
self.assertHasErrors(fName, [])
def test_checkPathNonExisting(self):
"""
L{checkPath} handles non-existing files.
"""
count, errors = self.getErrors('extremo')
self.assertEqual(count, 1)
self.assertEqual(
errors,
[('unexpectedError', 'extremo', 'No such file or directory')])
def test_multilineSyntaxError(self):
"""
Source which includes a syntax error which results in the raised
L{SyntaxError.text} containing multiple lines of source are reported
with only the last line of that source.
"""
source = """\
def foo():
'''
def bar():
pass
def baz():
'''quux'''
"""
# Sanity check - SyntaxError.text should be multiple lines, if it
# isn't, something this test was unprepared for has happened.
def evaluate(source):
exec(source)
try:
evaluate(source)
except SyntaxError:
e = sys.exc_info()[1]
if not PYPY and sys.version_info < (3, 10):
self.assertTrue(e.text.count('\n') > 1)
else:
self.fail()
with self.makeTempFile(source) as sourcePath:
if PYPY:
message = 'end of file (EOF) while scanning triple-quoted string literal'
elif sys.version_info >= (3, 10):
message = 'unterminated triple-quoted string literal (detected at line 8)' # noqa: E501
else:
message = 'invalid syntax'
if sys.version_info >= (3, 10):
column = 12
elif sys.version_info >= (3, 8):
column = 8
else:
column = 11
self.assertHasErrors(
sourcePath,
["""\
%s:8:%d: %s
'''quux'''
%s^
""" % (sourcePath, column, message, ' ' * (column - 1))])
def test_eofSyntaxError(self):
"""
The error reported for source files which end prematurely causing a
syntax error reflects the cause for the syntax error.
"""
with self.makeTempFile("def foo(") as sourcePath:
if PYPY:
msg = 'parenthesis is never closed'
elif sys.version_info >= (3, 10):
msg = "'(' was never closed"
else:
msg = 'unexpected EOF while parsing'
if PYPY:
column = 7
elif sys.version_info >= (3, 10):
column = 8
else:
column = 9
spaces = ' ' * (column - 1)
expected = '{}:1:{}: {}\ndef foo(\n{}^\n'.format(
sourcePath, column, msg, spaces
)
self.assertHasErrors(sourcePath, [expected])
def test_eofSyntaxErrorWithTab(self):
"""
The error reported for source files which end prematurely causing a
syntax error reflects the cause for the syntax error.
"""
with self.makeTempFile("if True:\n\tfoo =") as sourcePath:
column = 6 if PYPY else 7
last_line = '\t ^' if PYPY else '\t ^'
self.assertHasErrors(
sourcePath,
["""\
%s:2:%s: invalid syntax
\tfoo =
%s
""" % (sourcePath, column, last_line)])
def test_nonDefaultFollowsDefaultSyntaxError(self):
"""
Source which has a non-default argument following a default argument
should include the line number of the syntax error. However these
exceptions do not include an offset.
"""
source = """\
def foo(bar=baz, bax):
pass
"""
with self.makeTempFile(source) as sourcePath:
if ERROR_HAS_LAST_LINE:
if PYPY:
column = 7
elif sys.version_info >= (3, 10):
column = 18
elif sys.version_info >= (3, 9):
column = 21
elif sys.version_info >= (3, 8):
column = 9
else:
column = 8
last_line = ' ' * (column - 1) + '^\n'
columnstr = '%d:' % column
else:
last_line = columnstr = ''
self.assertHasErrors(
sourcePath,
["""\
%s:1:%s non-default argument follows default argument
def foo(bar=baz, bax):
%s""" % (sourcePath, columnstr, last_line)])
def test_nonKeywordAfterKeywordSyntaxError(self):
"""
Source which has a non-keyword argument after a keyword argument should
include the line number of the syntax error. However these exceptions
do not include an offset.
"""
source = """\
foo(bar=baz, bax)
"""
with self.makeTempFile(source) as sourcePath:
if ERROR_HAS_LAST_LINE:
if PYPY:
column = 12
elif sys.version_info >= (3, 9):
column = 17
elif sys.version_info >= (3, 8):
column = 14
else:
column = 13
last_line = ' ' * (column - 1) + '^\n'
columnstr = '%d:' % column
else:
last_line = columnstr = ''
if sys.version_info >= (3, 5):
message = 'positional argument follows keyword argument'
else:
message = 'non-keyword arg after keyword arg'
self.assertHasErrors(
sourcePath,
["""\
%s:1:%s %s
foo(bar=baz, bax)
%s""" % (sourcePath, columnstr, message, last_line)])
def test_invalidEscape(self):
"""
The invalid escape syntax raises ValueError in Python 2
"""
ver = sys.version_info
# ValueError: invalid \x escape
with self.makeTempFile(r"foo = '\xyz'") as sourcePath:
if ver < (3,):
decoding_error = "%s: problem decoding source\n" % (sourcePath,)
else:
position_end = 1
if PYPY:
column = 5
elif ver >= (3, 9):
column = 13
else:
column = 7
# Column has been "fixed" since 3.2.4 and 3.3.1
if ver < (3, 2, 4) or ver[:3] == (3, 3, 0):
position_end = 2
if ERROR_HAS_LAST_LINE:
last_line = '%s^\n' % (' ' * (column - 1))
else:
last_line = ''
decoding_error = """\
%s:1:%d: (unicode error) 'unicodeescape' codec can't decode bytes \
in position 0-%d: truncated \\xXX escape
foo = '\\xyz'
%s""" % (sourcePath, column, position_end, last_line)
self.assertHasErrors(
sourcePath, [decoding_error])
@skipIf(sys.platform == 'win32', 'unsupported on Windows')
def test_permissionDenied(self):
"""
If the source file is not readable, this is reported on standard
error.
"""
if os.getuid() == 0:
self.skipTest('root user can access all files regardless of '
'permissions')
with self.makeTempFile('') as sourcePath:
os.chmod(sourcePath, 0)
count, errors = self.getErrors(sourcePath)
self.assertEqual(count, 1)
self.assertEqual(
errors,
[('unexpectedError', sourcePath, "Permission denied")])
def test_pyflakesWarning(self):
"""
If the source file has a pyflakes warning, this is reported as a
'flake'.
"""
with self.makeTempFile("import foo") as sourcePath:
count, errors = self.getErrors(sourcePath)
self.assertEqual(count, 1)
self.assertEqual(
errors, [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))])
def test_encodedFileUTF8(self):
"""
If source file declares the correct encoding, no error is reported.
"""
SNOWMAN = unichr(0x2603)
source = ("""\
# coding: utf-8
x = "%s"
""" % SNOWMAN).encode('utf-8')
with self.makeTempFile(source) as sourcePath:
self.assertHasErrors(sourcePath, [])
def test_CRLFLineEndings(self):
"""
Source files with Windows CR LF line endings are parsed successfully.
"""
with self.makeTempFile("x = 42\r\n") as sourcePath:
self.assertHasErrors(sourcePath, [])
def test_misencodedFileUTF8(self):
"""
If a source file contains bytes which cannot be decoded, this is
reported on stderr.
"""
SNOWMAN = unichr(0x2603)
source = ("""\
# coding: ascii
x = "%s"
""" % SNOWMAN).encode('utf-8')
with self.makeTempFile(source) as sourcePath:
if PYPY and sys.version_info < (3, ):
message = ('\'ascii\' codec can\'t decode byte 0xe2 '
'in position 21: ordinal not in range(128)')
result = """\
%s:0:0: %s
x = "\xe2\x98\x83"
^\n""" % (sourcePath, message)
else:
message = 'problem decoding source'
result = "%s: problem decoding source\n" % (sourcePath,)
self.assertHasErrors(
sourcePath, [result])
def test_misencodedFileUTF16(self):
"""
If a source file contains bytes which cannot be decoded, this is
reported on stderr.
"""
SNOWMAN = unichr(0x2603)
source = ("""\
# coding: ascii
x = "%s"
""" % SNOWMAN).encode('utf-16')
with self.makeTempFile(source) as sourcePath:
self.assertHasErrors(
sourcePath, ["%s: problem decoding source\n" % (sourcePath,)])
def test_checkRecursive(self):
"""
L{checkRecursive} descends into each directory, finding Python files
and reporting problems.
"""
tempdir = tempfile.mkdtemp()
try:
os.mkdir(os.path.join(tempdir, 'foo'))
file1 = os.path.join(tempdir, 'foo', 'bar.py')
with open(file1, 'wb') as fd:
fd.write("import baz\n".encode('ascii'))
file2 = os.path.join(tempdir, 'baz.py')
with open(file2, 'wb') as fd:
fd.write("import contraband".encode('ascii'))
log = []
reporter = LoggingReporter(log)
warnings = checkRecursive([tempdir], reporter)
self.assertEqual(warnings, 2)
self.assertEqual(
sorted(log),
sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))),
('flake',
str(UnusedImport(file2, Node(1), 'contraband')))]))
finally:
shutil.rmtree(tempdir)
class IntegrationTests(TestCase):
"""
Tests of the pyflakes script that actually spawn the script.
"""
def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.tempfilepath = os.path.join(self.tempdir, 'temp')
def tearDown(self):
shutil.rmtree(self.tempdir)
def getPyflakesBinary(self):
"""
Return the path to the pyflakes binary.
"""
import pyflakes
package_dir = os.path.dirname(pyflakes.__file__)
return os.path.join(package_dir, '..', 'bin', 'pyflakes')
def runPyflakes(self, paths, stdin=None):
"""
Launch a subprocess running C{pyflakes}.
@param paths: Command-line arguments to pass to pyflakes.
@param stdin: Text to use as stdin.
@return: C{(returncode, stdout, stderr)} of the completed pyflakes
process.
"""
env = dict(os.environ)
env['PYTHONPATH'] = os.pathsep.join(sys.path)
command = [sys.executable, self.getPyflakesBinary()]
command.extend(paths)
if stdin:
p = subprocess.Popen(command, env=env, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate(stdin.encode('ascii'))
else:
p = subprocess.Popen(command, env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = p.communicate()
rv = p.wait()
if sys.version_info >= (3,):
stdout = stdout.decode('utf-8')
stderr = stderr.decode('utf-8')
return (stdout, stderr, rv)
def test_goodFile(self):
"""
When a Python source file is all good, the return code is zero and no
messages are printed to either stdout or stderr.
"""
open(self.tempfilepath, 'a').close()
d = self.runPyflakes([self.tempfilepath])
self.assertEqual(d, ('', '', 0))
def test_fileWithFlakes(self):
"""
When a Python source file has warnings, the return code is non-zero
and the warnings are printed to stdout.
"""
with open(self.tempfilepath, 'wb') as fd:
fd.write("import contraband\n".encode('ascii'))
d = self.runPyflakes([self.tempfilepath])
expected = UnusedImport(self.tempfilepath, Node(1), 'contraband')
self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1))
def test_errors_io(self):
"""
When pyflakes finds errors with the files it's given, (if they don't
exist, say), then the return code is non-zero and the errors are
printed to stderr.
"""
d = self.runPyflakes([self.tempfilepath])
error_msg = '%s: No such file or directory%s' % (self.tempfilepath,
os.linesep)
self.assertEqual(d, ('', error_msg, 1))
def test_errors_syntax(self):
"""
When pyflakes finds errors with the files it's given, (if they don't
exist, say), then the return code is non-zero and the errors are
printed to stderr.
"""
with open(self.tempfilepath, 'wb') as fd:
fd.write("import".encode('ascii'))
d = self.runPyflakes([self.tempfilepath])
error_msg = '{0}:1:{2}: invalid syntax{1}import{1} {3}^{1}'.format(
self.tempfilepath, os.linesep, 6 if PYPY else 7, '' if PYPY else ' ')
self.assertEqual(d, ('', error_msg, 1))
def test_readFromStdin(self):
"""
If no arguments are passed to C{pyflakes} then it reads from stdin.
"""
d = self.runPyflakes([], stdin='import contraband')
expected = UnusedImport('<stdin>', Node(1), 'contraband')
self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1))
class TestMain(IntegrationTests):
"""
Tests of the pyflakes main function.
"""
def runPyflakes(self, paths, stdin=None):
try:
with SysStreamCapturing(stdin) as capture:
main(args=paths)
except SystemExit as e:
self.assertIsInstance(e.code, bool)
rv = int(e.code)
return (capture.output, capture.error, rv)
else:
raise RuntimeError('SystemExit not raised')

View file

@ -0,0 +1,41 @@
"""
Tests for detecting redefinition of builtins.
"""
from sys import version_info
from pyflakes import messages as m
from pyflakes.test.harness import TestCase, skipIf
class TestBuiltins(TestCase):
def test_builtin_unbound_local(self):
self.flakes('''
def foo():
a = range(1, 10)
range = a
return range
foo()
print(range)
''', m.UndefinedLocal)
def test_global_shadowing_builtin(self):
self.flakes('''
def f():
global range
range = None
print(range)
f()
''')
@skipIf(version_info >= (3,), 'not an UnboundLocalError in Python 3')
def test_builtin_in_comprehension(self):
self.flakes('''
def f():
[range for range in range(1, 10)]
f()
''', m.UndefinedLocal)

View file

@ -0,0 +1,186 @@
import ast
import sys
from pyflakes import checker
from pyflakes.test.harness import TestCase, skipIf
class TypeableVisitorTests(TestCase):
"""
Tests of L{_TypeableVisitor}
"""
@staticmethod
def _run_visitor(s):
"""
Run L{_TypeableVisitor} on the parsed source and return the visitor.
"""
tree = ast.parse(s)
visitor = checker._TypeableVisitor()
visitor.visit(tree)
return visitor
def test_node_types(self):
"""
Test that the typeable node types are collected
"""
visitor = self._run_visitor(
"""\
x = 1 # assignment
for x in range(1): pass # for loop
def f(): pass # function definition
with a as b: pass # with statement
"""
)
self.assertEqual(visitor.typeable_lines, [1, 2, 3, 4])
self.assertIsInstance(visitor.typeable_nodes[1], ast.Assign)
self.assertIsInstance(visitor.typeable_nodes[2], ast.For)
self.assertIsInstance(visitor.typeable_nodes[3], ast.FunctionDef)
self.assertIsInstance(visitor.typeable_nodes[4], ast.With)
def test_visitor_recurses(self):
"""
Test the common pitfall of missing `generic_visit` in visitors by
ensuring that nested nodes are reported
"""
visitor = self._run_visitor(
"""\
def f():
x = 1
"""
)
self.assertEqual(visitor.typeable_lines, [1, 2])
self.assertIsInstance(visitor.typeable_nodes[1], ast.FunctionDef)
self.assertIsInstance(visitor.typeable_nodes[2], ast.Assign)
@skipIf(sys.version_info < (3, 5), 'async syntax introduced in py35')
def test_py35_node_types(self):
"""
Test that the PEP 492 node types are collected
"""
visitor = self._run_visitor(
"""\
async def f(): # async def
async for x in y: pass # async for
async with a as b: pass # async with
"""
)
self.assertEqual(visitor.typeable_lines, [1, 2, 3])
self.assertIsInstance(visitor.typeable_nodes[1], ast.AsyncFunctionDef)
self.assertIsInstance(visitor.typeable_nodes[2], ast.AsyncFor)
self.assertIsInstance(visitor.typeable_nodes[3], ast.AsyncWith)
def test_last_node_wins(self):
"""
Test that when two typeable nodes are present on a line, the last
typeable one wins.
"""
visitor = self._run_visitor('x = 1; y = 1')
# detected both assignable nodes
self.assertEqual(visitor.typeable_lines, [1, 1])
# but the assignment to `y` wins
self.assertEqual(visitor.typeable_nodes[1].targets[0].id, 'y')
class CollectTypeCommentsTests(TestCase):
"""
Tests of L{_collect_type_comments}
"""
@staticmethod
def _collect(s):
"""
Run L{_collect_type_comments} on the parsed source and return the
mapping from nodes to comments. The return value is converted to
a set: {(node_type, tuple of comments), ...}
"""
tree = ast.parse(s)
tokens = checker.make_tokens(s)
ret = checker._collect_type_comments(tree, tokens)
return {(type(k), tuple(s for _, s in v)) for k, v in ret.items()}
def test_bytes(self):
"""
Test that the function works for binary source
"""
ret = self._collect(b'x = 1 # type: int')
self.assertSetEqual(ret, {(ast.Assign, ('# type: int',))})
def test_text(self):
"""
Test that the function works for text source
"""
ret = self._collect(u'x = 1 # type: int')
self.assertEqual(ret, {(ast.Assign, ('# type: int',))})
def test_non_type_comment_ignored(self):
"""
Test that a non-type comment is ignored
"""
ret = self._collect('x = 1 # noqa')
self.assertSetEqual(ret, set())
def test_type_comment_before_typeable(self):
"""
Test that a type comment before something typeable is ignored.
"""
ret = self._collect('# type: int\nx = 1')
self.assertSetEqual(ret, set())
def test_type_ignore_comment_ignored(self):
"""
Test that `# type: ignore` comments are not collected.
"""
ret = self._collect('x = 1 # type: ignore')
self.assertSetEqual(ret, set())
def test_type_ignore_with_other_things_ignored(self):
"""
Test that `# type: ignore` comments with more content are also not
collected.
"""
ret = self._collect('x = 1 # type: ignore # noqa')
self.assertSetEqual(ret, set())
ret = self._collect('x = 1 #type:ignore#noqa')
self.assertSetEqual(ret, set())
def test_type_comment_with_extra_still_collected(self):
ret = self._collect('x = 1 # type: int # noqa')
self.assertSetEqual(ret, {(ast.Assign, ('# type: int # noqa',))})
def test_type_comment_without_whitespace(self):
ret = self._collect('x = 1 #type:int')
self.assertSetEqual(ret, {(ast.Assign, ('#type:int',))})
def test_type_comment_starts_with_word_ignore(self):
ret = self._collect('x = 1 # type: ignore[T]')
self.assertSetEqual(ret, set())
def test_last_node_wins(self):
"""
Test that when two typeable nodes are present on a line, the last
typeable one wins.
"""
ret = self._collect('def f(): x = 1 # type: int')
self.assertSetEqual(ret, {(ast.Assign, ('# type: int',))})
def test_function_def_assigned_comments(self):
"""
Test that type comments for function arguments are all attributed to
the function definition.
"""
ret = self._collect(
"""\
def f(
a, # type: int
b, # type: str
):
# type: (...) -> None
pass
"""
)
expected = {(
ast.FunctionDef,
('# type: int', '# type: str', '# type: (...) -> None'),
)}
self.assertSetEqual(ret, expected)

View file

@ -0,0 +1,132 @@
from sys import version_info
from pyflakes import messages as m
from pyflakes.checker import (FunctionScope, ClassScope, ModuleScope,
Argument, FunctionDefinition, Assignment)
from pyflakes.test.harness import TestCase, skipIf
class TestCodeSegments(TestCase):
"""
Tests for segments of a module
"""
def test_function_segment(self):
self.flakes('''
def foo():
def bar():
pass
''', is_segment=True)
self.flakes('''
def foo():
def bar():
x = 0
''', m.UnusedVariable, is_segment=True)
def test_class_segment(self):
self.flakes('''
class Foo:
class Bar:
pass
''', is_segment=True)
self.flakes('''
class Foo:
def bar():
x = 0
''', m.UnusedVariable, is_segment=True)
def test_scope_class(self):
checker = self.flakes('''
class Foo:
x = 0
def bar(a, b=1, *d, **e):
pass
''', is_segment=True)
scopes = checker.deadScopes
module_scopes = [
scope for scope in scopes if scope.__class__ is ModuleScope]
class_scopes = [
scope for scope in scopes if scope.__class__ is ClassScope]
function_scopes = [
scope for scope in scopes if scope.__class__ is FunctionScope]
# Ensure module scope is not present because we are analysing
# the inner contents of Foo
self.assertEqual(len(module_scopes), 0)
self.assertEqual(len(class_scopes), 1)
self.assertEqual(len(function_scopes), 1)
class_scope = class_scopes[0]
function_scope = function_scopes[0]
self.assertIsInstance(class_scope, ClassScope)
self.assertIsInstance(function_scope, FunctionScope)
self.assertIn('x', class_scope)
self.assertIn('bar', class_scope)
self.assertIn('a', function_scope)
self.assertIn('b', function_scope)
self.assertIn('d', function_scope)
self.assertIn('e', function_scope)
self.assertIsInstance(class_scope['bar'], FunctionDefinition)
self.assertIsInstance(class_scope['x'], Assignment)
self.assertIsInstance(function_scope['a'], Argument)
self.assertIsInstance(function_scope['b'], Argument)
self.assertIsInstance(function_scope['d'], Argument)
self.assertIsInstance(function_scope['e'], Argument)
def test_scope_function(self):
checker = self.flakes('''
def foo(a, b=1, *d, **e):
def bar(f, g=1, *h, **i):
pass
''', is_segment=True)
scopes = checker.deadScopes
module_scopes = [
scope for scope in scopes if scope.__class__ is ModuleScope]
function_scopes = [
scope for scope in scopes if scope.__class__ is FunctionScope]
# Ensure module scope is not present because we are analysing
# the inner contents of foo
self.assertEqual(len(module_scopes), 0)
self.assertEqual(len(function_scopes), 2)
function_scope_foo = function_scopes[1]
function_scope_bar = function_scopes[0]
self.assertIsInstance(function_scope_foo, FunctionScope)
self.assertIsInstance(function_scope_bar, FunctionScope)
self.assertIn('a', function_scope_foo)
self.assertIn('b', function_scope_foo)
self.assertIn('d', function_scope_foo)
self.assertIn('e', function_scope_foo)
self.assertIn('bar', function_scope_foo)
self.assertIn('f', function_scope_bar)
self.assertIn('g', function_scope_bar)
self.assertIn('h', function_scope_bar)
self.assertIn('i', function_scope_bar)
self.assertIsInstance(function_scope_foo['bar'], FunctionDefinition)
self.assertIsInstance(function_scope_foo['a'], Argument)
self.assertIsInstance(function_scope_foo['b'], Argument)
self.assertIsInstance(function_scope_foo['d'], Argument)
self.assertIsInstance(function_scope_foo['e'], Argument)
self.assertIsInstance(function_scope_bar['f'], Argument)
self.assertIsInstance(function_scope_bar['g'], Argument)
self.assertIsInstance(function_scope_bar['h'], Argument)
self.assertIsInstance(function_scope_bar['i'], Argument)
@skipIf(version_info < (3, 5), 'new in Python 3.5')
def test_scope_async_function(self):
self.flakes('async def foo(): pass', is_segment=True)

View file

@ -0,0 +1,213 @@
"""
Tests for dict duplicate keys Pyflakes behavior.
"""
from sys import version_info
from pyflakes import messages as m
from pyflakes.test.harness import TestCase, skipIf
class Test(TestCase):
def test_duplicate_keys(self):
self.flakes(
"{'yes': 1, 'yes': 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
@skipIf(version_info < (3,),
"bytes and strings with same 'value' are not equal in python3")
def test_duplicate_keys_bytes_vs_unicode_py3(self):
self.flakes("{b'a': 1, u'a': 2}")
@skipIf(version_info < (3,),
"bytes and strings with same 'value' are not equal in python3")
def test_duplicate_values_bytes_vs_unicode_py3(self):
self.flakes(
"{1: b'a', 1: u'a'}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
@skipIf(version_info >= (3,),
"bytes and strings with same 'value' are equal in python2")
def test_duplicate_keys_bytes_vs_unicode_py2(self):
self.flakes(
"{b'a': 1, u'a': 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
@skipIf(version_info >= (3,),
"bytes and strings with same 'value' are equal in python2")
def test_duplicate_values_bytes_vs_unicode_py2(self):
self.flakes("{1: b'a', 1: u'a'}")
def test_multiple_duplicate_keys(self):
self.flakes(
"{'yes': 1, 'yes': 2, 'no': 2, 'no': 3}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_in_function(self):
self.flakes(
'''
def f(thing):
pass
f({'yes': 1, 'yes': 2})
''',
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_in_lambda(self):
self.flakes(
"lambda x: {(0,1): 1, (0,1): 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_tuples(self):
self.flakes(
"{(0,1): 1, (0,1): 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_tuples_int_and_float(self):
self.flakes(
"{(0,1): 1, (0,1.0): 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_ints(self):
self.flakes(
"{1: 1, 1: 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_bools(self):
self.flakes(
"{True: 1, True: 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_bools_false(self):
# Needed to ensure 2.x correctly coerces these from variables
self.flakes(
"{False: 1, False: 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_keys_none(self):
self.flakes(
"{None: 1, None: 2}",
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_variable_keys(self):
self.flakes(
'''
a = 1
{a: 1, a: 2}
''',
m.MultiValueRepeatedKeyVariable,
m.MultiValueRepeatedKeyVariable,
)
def test_duplicate_variable_values(self):
self.flakes(
'''
a = 1
b = 2
{1: a, 1: b}
''',
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_variable_values_same_value(self):
# Current behaviour is not to look up variable values. This is to
# confirm that.
self.flakes(
'''
a = 1
b = 1
{1: a, 1: b}
''',
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_duplicate_key_float_and_int(self):
"""
These do look like different values, but when it comes to their use as
keys, they compare as equal and so are actually duplicates.
The literal dict {1: 1, 1.0: 1} actually becomes {1.0: 1}.
"""
self.flakes(
'''
{1: 1, 1.0: 2}
''',
m.MultiValueRepeatedKeyLiteral,
m.MultiValueRepeatedKeyLiteral,
)
def test_no_duplicate_key_error_same_value(self):
self.flakes('''
{'yes': 1, 'yes': 1}
''')
def test_no_duplicate_key_errors(self):
self.flakes('''
{'yes': 1, 'no': 2}
''')
def test_no_duplicate_keys_tuples_same_first_element(self):
self.flakes("{(0,1): 1, (0,2): 1}")
def test_no_duplicate_key_errors_func_call(self):
self.flakes('''
def test(thing):
pass
test({True: 1, None: 2, False: 1})
''')
def test_no_duplicate_key_errors_bool_or_none(self):
self.flakes("{True: 1, None: 2, False: 1}")
def test_no_duplicate_key_errors_ints(self):
self.flakes('''
{1: 1, 2: 1}
''')
def test_no_duplicate_key_errors_vars(self):
self.flakes('''
test = 'yes'
rest = 'yes'
{test: 1, rest: 2}
''')
def test_no_duplicate_key_errors_tuples(self):
self.flakes('''
{(0,1): 1, (0,2): 1}
''')
def test_no_duplicate_key_errors_instance_attributes(self):
self.flakes('''
class Test():
pass
f = Test()
f.a = 1
{f.a: 1, f.a: 1}
''')

View file

@ -0,0 +1,465 @@
import sys
import textwrap
from pyflakes import messages as m
from pyflakes.checker import (
DoctestScope,
FunctionScope,
ModuleScope,
)
from pyflakes.test.test_other import Test as TestOther
from pyflakes.test.test_imports import Test as TestImports
from pyflakes.test.test_undefined_names import Test as TestUndefinedNames
from pyflakes.test.harness import TestCase, skip
try:
sys.pypy_version_info
PYPY = True
except AttributeError:
PYPY = False
class _DoctestMixin(object):
withDoctest = True
def doctestify(self, input):
lines = []
for line in textwrap.dedent(input).splitlines():
if line.strip() == '':
pass
elif (line.startswith(' ') or
line.startswith('except:') or
line.startswith('except ') or
line.startswith('finally:') or
line.startswith('else:') or
line.startswith('elif ') or
(lines and lines[-1].startswith(('>>> @', '... @')))):
line = "... %s" % line
else:
line = ">>> %s" % line
lines.append(line)
doctestificator = textwrap.dedent('''\
def doctest_something():
"""
%s
"""
''')
return doctestificator % "\n ".join(lines)
def flakes(self, input, *args, **kw):
return super(_DoctestMixin, self).flakes(self.doctestify(input), *args, **kw)
class Test(TestCase):
withDoctest = True
def test_scope_class(self):
"""Check that a doctest is given a DoctestScope."""
checker = self.flakes("""
m = None
def doctest_stuff():
'''
>>> d = doctest_stuff()
'''
f = m
return f
""")
scopes = checker.deadScopes
module_scopes = [
scope for scope in scopes if scope.__class__ is ModuleScope]
doctest_scopes = [
scope for scope in scopes if scope.__class__ is DoctestScope]
function_scopes = [
scope for scope in scopes if scope.__class__ is FunctionScope]
self.assertEqual(len(module_scopes), 1)
self.assertEqual(len(doctest_scopes), 1)
module_scope = module_scopes[0]
doctest_scope = doctest_scopes[0]
self.assertIsInstance(doctest_scope, DoctestScope)
self.assertIsInstance(doctest_scope, ModuleScope)
self.assertNotIsInstance(doctest_scope, FunctionScope)
self.assertNotIsInstance(module_scope, DoctestScope)
self.assertIn('m', module_scope)
self.assertIn('doctest_stuff', module_scope)
self.assertIn('d', doctest_scope)
self.assertEqual(len(function_scopes), 1)
self.assertIn('f', function_scopes[0])
def test_nested_doctest_ignored(self):
"""Check that nested doctests are ignored."""
checker = self.flakes("""
m = None
def doctest_stuff():
'''
>>> def function_in_doctest():
... \"\"\"
... >>> ignored_undefined_name
... \"\"\"
... df = m
... return df
...
>>> function_in_doctest()
'''
f = m
return f
""")
scopes = checker.deadScopes
module_scopes = [
scope for scope in scopes if scope.__class__ is ModuleScope]
doctest_scopes = [
scope for scope in scopes if scope.__class__ is DoctestScope]
function_scopes = [
scope for scope in scopes if scope.__class__ is FunctionScope]
self.assertEqual(len(module_scopes), 1)
self.assertEqual(len(doctest_scopes), 1)
module_scope = module_scopes[0]
doctest_scope = doctest_scopes[0]
self.assertIn('m', module_scope)
self.assertIn('doctest_stuff', module_scope)
self.assertIn('function_in_doctest', doctest_scope)
self.assertEqual(len(function_scopes), 2)
self.assertIn('f', function_scopes[0])
self.assertIn('df', function_scopes[1])
def test_global_module_scope_pollution(self):
"""Check that global in doctest does not pollute module scope."""
checker = self.flakes("""
def doctest_stuff():
'''
>>> def function_in_doctest():
... global m
... m = 50
... df = 10
... m = df
...
>>> function_in_doctest()
'''
f = 10
return f
""")
scopes = checker.deadScopes
module_scopes = [
scope for scope in scopes if scope.__class__ is ModuleScope]
doctest_scopes = [
scope for scope in scopes if scope.__class__ is DoctestScope]
function_scopes = [
scope for scope in scopes if scope.__class__ is FunctionScope]
self.assertEqual(len(module_scopes), 1)
self.assertEqual(len(doctest_scopes), 1)
module_scope = module_scopes[0]
doctest_scope = doctest_scopes[0]
self.assertIn('doctest_stuff', module_scope)
self.assertIn('function_in_doctest', doctest_scope)
self.assertEqual(len(function_scopes), 2)
self.assertIn('f', function_scopes[0])
self.assertIn('df', function_scopes[1])
self.assertIn('m', function_scopes[1])
self.assertNotIn('m', module_scope)
def test_global_undefined(self):
self.flakes("""
global m
def doctest_stuff():
'''
>>> m
'''
""", m.UndefinedName)
def test_nested_class(self):
"""Doctest within nested class are processed."""
self.flakes("""
class C:
class D:
'''
>>> m
'''
def doctest_stuff(self):
'''
>>> m
'''
return 1
""", m.UndefinedName, m.UndefinedName)
def test_ignore_nested_function(self):
"""Doctest module does not process doctest in nested functions."""
# 'syntax error' would cause a SyntaxError if the doctest was processed.
# However doctest does not find doctest in nested functions
# (https://bugs.python.org/issue1650090). If nested functions were
# processed, this use of m should cause UndefinedName, and the
# name inner_function should probably exist in the doctest scope.
self.flakes("""
def doctest_stuff():
def inner_function():
'''
>>> syntax error
>>> inner_function()
1
>>> m
'''
return 1
m = inner_function()
return m
""")
def test_inaccessible_scope_class(self):
"""Doctest may not access class scope."""
self.flakes("""
class C:
def doctest_stuff(self):
'''
>>> m
'''
return 1
m = 1
""", m.UndefinedName)
def test_importBeforeDoctest(self):
self.flakes("""
import foo
def doctest_stuff():
'''
>>> foo
'''
""")
@skip("todo")
def test_importBeforeAndInDoctest(self):
self.flakes('''
import foo
def doctest_stuff():
"""
>>> import foo
>>> foo
"""
foo
''', m.RedefinedWhileUnused)
def test_importInDoctestAndAfter(self):
self.flakes('''
def doctest_stuff():
"""
>>> import foo
>>> foo
"""
import foo
foo()
''')
def test_offsetInDoctests(self):
exc = self.flakes('''
def doctest_stuff():
"""
>>> x # line 5
"""
''', m.UndefinedName).messages[0]
self.assertEqual(exc.lineno, 5)
self.assertEqual(exc.col, 12)
def test_offsetInLambdasInDoctests(self):
exc = self.flakes('''
def doctest_stuff():
"""
>>> lambda: x # line 5
"""
''', m.UndefinedName).messages[0]
self.assertEqual(exc.lineno, 5)
self.assertEqual(exc.col, 20)
def test_offsetAfterDoctests(self):
exc = self.flakes('''
def doctest_stuff():
"""
>>> x = 5
"""
x
''', m.UndefinedName).messages[0]
self.assertEqual(exc.lineno, 8)
self.assertEqual(exc.col, 0)
def test_syntaxErrorInDoctest(self):
exceptions = self.flakes(
'''
def doctest_stuff():
"""
>>> from # line 4
>>> fortytwo = 42
>>> except Exception:
"""
''',
m.DoctestSyntaxError,
m.DoctestSyntaxError,
m.DoctestSyntaxError).messages
exc = exceptions[0]
self.assertEqual(exc.lineno, 4)
if PYPY:
self.assertEqual(exc.col, 27)
elif sys.version_info >= (3, 8):
self.assertEqual(exc.col, 18)
else:
self.assertEqual(exc.col, 26)
# PyPy error column offset is 0,
# for the second and third line of the doctest
# i.e. at the beginning of the line
exc = exceptions[1]
self.assertEqual(exc.lineno, 5)
if PYPY:
self.assertEqual(exc.col, 14)
else:
self.assertEqual(exc.col, 16)
exc = exceptions[2]
self.assertEqual(exc.lineno, 6)
if PYPY:
self.assertEqual(exc.col, 14)
elif sys.version_info >= (3, 8):
self.assertEqual(exc.col, 13)
else:
self.assertEqual(exc.col, 18)
def test_indentationErrorInDoctest(self):
exc = self.flakes('''
def doctest_stuff():
"""
>>> if True:
... pass
"""
''', m.DoctestSyntaxError).messages[0]
self.assertEqual(exc.lineno, 5)
if PYPY:
self.assertEqual(exc.col, 14)
elif sys.version_info >= (3, 8):
self.assertEqual(exc.col, 13)
else:
self.assertEqual(exc.col, 16)
def test_offsetWithMultiLineArgs(self):
(exc1, exc2) = self.flakes(
'''
def doctest_stuff(arg1,
arg2,
arg3):
"""
>>> assert
>>> this
"""
''',
m.DoctestSyntaxError,
m.UndefinedName).messages
self.assertEqual(exc1.lineno, 6)
if PYPY:
self.assertEqual(exc1.col, 20)
else:
self.assertEqual(exc1.col, 19)
self.assertEqual(exc2.lineno, 7)
self.assertEqual(exc2.col, 12)
def test_doctestCanReferToFunction(self):
self.flakes("""
def foo():
'''
>>> foo
'''
""")
def test_doctestCanReferToClass(self):
self.flakes("""
class Foo():
'''
>>> Foo
'''
def bar(self):
'''
>>> Foo
'''
""")
def test_noOffsetSyntaxErrorInDoctest(self):
exceptions = self.flakes(
'''
def buildurl(base, *args, **kwargs):
"""
>>> buildurl('/blah.php', ('a', '&'), ('b', '=')
'/blah.php?a=%26&b=%3D'
>>> buildurl('/blah.php', a='&', 'b'='=')
'/blah.php?b=%3D&a=%26'
"""
pass
''',
m.DoctestSyntaxError,
m.DoctestSyntaxError).messages
exc = exceptions[0]
self.assertEqual(exc.lineno, 4)
exc = exceptions[1]
self.assertEqual(exc.lineno, 6)
def test_singleUnderscoreInDoctest(self):
self.flakes('''
def func():
"""A docstring
>>> func()
1
>>> _
1
"""
return 1
''')
def test_globalUnderscoreInDoctest(self):
self.flakes("""
from gettext import ugettext as _
def doctest_stuff():
'''
>>> pass
'''
""", m.UnusedImport)
class TestOther(_DoctestMixin, TestOther):
"""Run TestOther with each test wrapped in a doctest."""
class TestImports(_DoctestMixin, TestImports):
"""Run TestImports with each test wrapped in a doctest."""
class TestUndefinedNames(_DoctestMixin, TestUndefinedNames):
"""Run TestUndefinedNames with each test wrapped in a doctest."""

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,222 @@
from pyflakes.messages import IsLiteral
from pyflakes.test.harness import TestCase
class Test(TestCase):
def test_is_str(self):
self.flakes("""
x = 'foo'
if x is 'foo':
pass
""", IsLiteral)
def test_is_bytes(self):
self.flakes("""
x = b'foo'
if x is b'foo':
pass
""", IsLiteral)
def test_is_unicode(self):
self.flakes("""
x = u'foo'
if x is u'foo':
pass
""", IsLiteral)
def test_is_int(self):
self.flakes("""
x = 10
if x is 10:
pass
""", IsLiteral)
def test_is_true(self):
self.flakes("""
x = True
if x is True:
pass
""")
def test_is_false(self):
self.flakes("""
x = False
if x is False:
pass
""")
def test_is_not_str(self):
self.flakes("""
x = 'foo'
if x is not 'foo':
pass
""", IsLiteral)
def test_is_not_bytes(self):
self.flakes("""
x = b'foo'
if x is not b'foo':
pass
""", IsLiteral)
def test_is_not_unicode(self):
self.flakes("""
x = u'foo'
if x is not u'foo':
pass
""", IsLiteral)
def test_is_not_int(self):
self.flakes("""
x = 10
if x is not 10:
pass
""", IsLiteral)
def test_is_not_true(self):
self.flakes("""
x = True
if x is not True:
pass
""")
def test_is_not_false(self):
self.flakes("""
x = False
if x is not False:
pass
""")
def test_left_is_str(self):
self.flakes("""
x = 'foo'
if 'foo' is x:
pass
""", IsLiteral)
def test_left_is_bytes(self):
self.flakes("""
x = b'foo'
if b'foo' is x:
pass
""", IsLiteral)
def test_left_is_unicode(self):
self.flakes("""
x = u'foo'
if u'foo' is x:
pass
""", IsLiteral)
def test_left_is_int(self):
self.flakes("""
x = 10
if 10 is x:
pass
""", IsLiteral)
def test_left_is_true(self):
self.flakes("""
x = True
if True is x:
pass
""")
def test_left_is_false(self):
self.flakes("""
x = False
if False is x:
pass
""")
def test_left_is_not_str(self):
self.flakes("""
x = 'foo'
if 'foo' is not x:
pass
""", IsLiteral)
def test_left_is_not_bytes(self):
self.flakes("""
x = b'foo'
if b'foo' is not x:
pass
""", IsLiteral)
def test_left_is_not_unicode(self):
self.flakes("""
x = u'foo'
if u'foo' is not x:
pass
""", IsLiteral)
def test_left_is_not_int(self):
self.flakes("""
x = 10
if 10 is not x:
pass
""", IsLiteral)
def test_left_is_not_true(self):
self.flakes("""
x = True
if True is not x:
pass
""")
def test_left_is_not_false(self):
self.flakes("""
x = False
if False is not x:
pass
""")
def test_chained_operators_is_true(self):
self.flakes("""
x = 5
if x is True < 4:
pass
""")
def test_chained_operators_is_str(self):
self.flakes("""
x = 5
if x is 'foo' < 4:
pass
""", IsLiteral)
def test_chained_operators_is_true_end(self):
self.flakes("""
x = 5
if 4 < x is True:
pass
""")
def test_chained_operators_is_str_end(self):
self.flakes("""
x = 5
if 4 < x is 'foo':
pass
""", IsLiteral)
def test_is_tuple_constant(self):
self.flakes('''\
x = 5
if x is ():
pass
''', IsLiteral)
def test_is_tuple_constant_containing_constants(self):
self.flakes('''\
x = 5
if x is (1, '2', True, (1.5, ())):
pass
''', IsLiteral)
def test_is_tuple_containing_variables_ok(self):
# a bit nonsensical, but does not trigger a SyntaxWarning
self.flakes('''\
x = 5
if x is (x,):
pass
''')

View file

@ -0,0 +1,83 @@
from sys import version_info
from pyflakes.test.harness import TestCase, skipIf
@skipIf(version_info < (3, 10), "Python >= 3.10 only")
class TestMatch(TestCase):
def test_match_bindings(self):
self.flakes('''
def f():
x = 1
match x:
case 1 as y:
print(f'matched as {y}')
''')
self.flakes('''
def f():
x = [1, 2, 3]
match x:
case [1, y, 3]:
print(f'matched {y}')
''')
self.flakes('''
def f():
x = {'foo': 1}
match x:
case {'foo': y}:
print(f'matched {y}')
''')
def test_match_pattern_matched_class(self):
self.flakes('''
from a import B
match 1:
case B(x=1) as y:
print(f'matched {y}')
''')
self.flakes('''
from a import B
match 1:
case B(a, x=z) as y:
print(f'matched {y} {a} {z}')
''')
def test_match_placeholder(self):
self.flakes('''
def f():
match 1:
case _:
print('catchall!')
''')
def test_match_singleton(self):
self.flakes('''
match 1:
case True:
print('true')
''')
def test_match_or_pattern(self):
self.flakes('''
match 1:
case 1 | 2:
print('one or two')
''')
def test_match_star(self):
self.flakes('''
x = [1, 2, 3]
match x:
case [1, *y]:
print(f'captured: {y}')
''')
def test_match_double_star(self):
self.flakes('''
x = {'foo': 'bar', 'baz': 'womp'}
match x:
case {'foo': k1, **rest}:
print(f'{k1=} {rest=}')
''')

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
from sys import version_info
from pyflakes import messages as m
from pyflakes.test.harness import TestCase, skipIf
class Test(TestCase):
@skipIf(version_info >= (3, 3), 'new in Python 3.3')
def test_return(self):
self.flakes('''
class a:
def b():
for x in a.c:
if x:
yield x
return a
''', m.ReturnWithArgsInsideGenerator)
@skipIf(version_info >= (3, 3), 'new in Python 3.3')
def test_returnNone(self):
self.flakes('''
def a():
yield 12
return None
''', m.ReturnWithArgsInsideGenerator)
@skipIf(version_info >= (3, 3), 'new in Python 3.3')
def test_returnYieldExpression(self):
self.flakes('''
def a():
b = yield a
return b
''', m.ReturnWithArgsInsideGenerator)

View file

@ -0,0 +1,763 @@
"""
Tests for behaviour related to type annotations.
"""
from sys import version_info
from pyflakes import messages as m
from pyflakes.test.harness import TestCase, skipIf
class TestTypeAnnotations(TestCase):
def test_typingOverload(self):
"""Allow intentional redefinitions via @typing.overload"""
self.flakes("""
import typing
from typing import overload
@overload
def f(s): # type: (None) -> None
pass
@overload
def f(s): # type: (int) -> int
pass
def f(s):
return s
@typing.overload
def g(s): # type: (None) -> None
pass
@typing.overload
def g(s): # type: (int) -> int
pass
def g(s):
return s
""")
def test_typingExtensionsOverload(self):
"""Allow intentional redefinitions via @typing_extensions.overload"""
self.flakes("""
import typing_extensions
from typing_extensions import overload
@overload
def f(s): # type: (None) -> None
pass
@overload
def f(s): # type: (int) -> int
pass
def f(s):
return s
@typing_extensions.overload
def g(s): # type: (None) -> None
pass
@typing_extensions.overload
def g(s): # type: (int) -> int
pass
def g(s):
return s
""")
@skipIf(version_info < (3, 5), 'new in Python 3.5')
def test_typingOverloadAsync(self):
"""Allow intentional redefinitions via @typing.overload (async)"""
self.flakes("""
from typing import overload
@overload
async def f(s): # type: (None) -> None
pass
@overload
async def f(s): # type: (int) -> int
pass
async def f(s):
return s
""")
def test_overload_with_multiple_decorators(self):
self.flakes("""
from typing import overload
dec = lambda f: f
@dec
@overload
def f(x): # type: (int) -> int
pass
@dec
@overload
def f(x): # type: (str) -> str
pass
@dec
def f(x): return x
""")
def test_overload_in_class(self):
self.flakes("""
from typing import overload
class C:
@overload
def f(self, x): # type: (int) -> int
pass
@overload
def f(self, x): # type: (str) -> str
pass
def f(self, x): return x
""")
def test_aliased_import(self):
"""Detect when typing is imported as another name"""
self.flakes("""
import typing as t
@t.overload
def f(s): # type: (None) -> None
pass
@t.overload
def f(s): # type: (int) -> int
pass
def f(s):
return s
""")
def test_not_a_typing_overload(self):
"""regression test for @typing.overload detection bug in 2.1.0"""
self.flakes("""
def foo(x):
return x
@foo
def bar():
pass
def bar():
pass
""", m.RedefinedWhileUnused)
@skipIf(version_info < (3, 6), 'new in Python 3.6')
def test_variable_annotations(self):
self.flakes('''
name: str
age: int
''')
self.flakes('''
name: str = 'Bob'
age: int = 18
''')
self.flakes('''
class C:
name: str
age: int
''')
self.flakes('''
class C:
name: str = 'Bob'
age: int = 18
''')
self.flakes('''
def f():
name: str
age: int
''')
self.flakes('''
def f():
name: str = 'Bob'
age: int = 18
foo: not_a_real_type = None
''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable, m.UndefinedName)
self.flakes('''
def f():
name: str
print(name)
''', m.UndefinedName)
self.flakes('''
from typing import Any
def f():
a: Any
''')
self.flakes('''
foo: not_a_real_type
''', m.UndefinedName)
self.flakes('''
foo: not_a_real_type = None
''', m.UndefinedName)
self.flakes('''
class C:
foo: not_a_real_type
''', m.UndefinedName)
self.flakes('''
class C:
foo: not_a_real_type = None
''', m.UndefinedName)
self.flakes('''
def f():
class C:
foo: not_a_real_type
''', m.UndefinedName)
self.flakes('''
def f():
class C:
foo: not_a_real_type = None
''', m.UndefinedName)
self.flakes('''
from foo import Bar
bar: Bar
''')
self.flakes('''
from foo import Bar
bar: 'Bar'
''')
self.flakes('''
import foo
bar: foo.Bar
''')
self.flakes('''
import foo
bar: 'foo.Bar'
''')
self.flakes('''
from foo import Bar
def f(bar: Bar): pass
''')
self.flakes('''
from foo import Bar
def f(bar: 'Bar'): pass
''')
self.flakes('''
from foo import Bar
def f(bar) -> Bar: return bar
''')
self.flakes('''
from foo import Bar
def f(bar) -> 'Bar': return bar
''')
self.flakes('''
bar: 'Bar'
''', m.UndefinedName)
self.flakes('''
bar: 'foo.Bar'
''', m.UndefinedName)
self.flakes('''
from foo import Bar
bar: str
''', m.UnusedImport)
self.flakes('''
from foo import Bar
def f(bar: str): pass
''', m.UnusedImport)
self.flakes('''
def f(a: A) -> A: pass
class A: pass
''', m.UndefinedName, m.UndefinedName)
self.flakes('''
def f(a: 'A') -> 'A': return a
class A: pass
''')
self.flakes('''
a: A
class A: pass
''', m.UndefinedName)
self.flakes('''
a: 'A'
class A: pass
''')
self.flakes('''
T: object
def f(t: T): pass
''', m.UndefinedName)
self.flakes('''
T: object
def g(t: 'T'): pass
''')
self.flakes('''
a: 'A B'
''', m.ForwardAnnotationSyntaxError)
self.flakes('''
a: 'A; B'
''', m.ForwardAnnotationSyntaxError)
self.flakes('''
a: '1 + 2'
''')
self.flakes('''
a: 'a: "A"'
''', m.ForwardAnnotationSyntaxError)
@skipIf(version_info < (3, 6), 'new in Python 3.6')
def test_annotating_an_import(self):
self.flakes('''
from a import b, c
b: c
print(b)
''')
@skipIf(version_info < (3, 6), 'new in Python 3.6')
def test_unused_annotation(self):
# Unused annotations are fine in module and class scope
self.flakes('''
x: int
class Cls:
y: int
''')
# TODO: this should print a UnusedVariable message
self.flakes('''
def f():
x: int
''')
# This should only print one UnusedVariable message
self.flakes('''
def f():
x: int
x = 3
''', m.UnusedVariable)
@skipIf(version_info < (3, 5), 'new in Python 3.5')
def test_annotated_async_def(self):
self.flakes('''
class c: pass
async def func(c: c) -> None: pass
''')
@skipIf(version_info < (3, 7), 'new in Python 3.7')
def test_postponed_annotations(self):
self.flakes('''
from __future__ import annotations
def f(a: A) -> A: pass
class A:
b: B
class B: pass
''')
self.flakes('''
from __future__ import annotations
def f(a: A) -> A: pass
class A:
b: Undefined
class B: pass
''', m.UndefinedName)
self.flakes('''
from __future__ import annotations
T: object
def f(t: T): pass
def g(t: 'T'): pass
''')
@skipIf(version_info < (3, 6), 'new in Python 3.6')
def test_type_annotation_clobbers_all(self):
self.flakes('''\
from typing import TYPE_CHECKING, List
from y import z
if not TYPE_CHECKING:
__all__ = ("z",)
else:
__all__: List[str]
''')
def test_typeCommentsMarkImportsAsUsed(self):
self.flakes("""
from mod import A, B, C, D, E, F, G
def f(
a, # type: A
):
# type: (...) -> B
for b in a: # type: C
with b as c: # type: D
d = c.x # type: E
return d
def g(x): # type: (F) -> G
return x.y
""")
def test_typeCommentsFullSignature(self):
self.flakes("""
from mod import A, B, C, D
def f(a, b):
# type: (A, B[C]) -> D
return a + b
""")
def test_typeCommentsStarArgs(self):
self.flakes("""
from mod import A, B, C, D
def f(a, *b, **c):
# type: (A, *B, **C) -> D
return a + b
""")
def test_typeCommentsFullSignatureWithDocstring(self):
self.flakes('''
from mod import A, B, C, D
def f(a, b):
# type: (A, B[C]) -> D
"""do the thing!"""
return a + b
''')
def test_typeCommentsAdditionalComment(self):
self.flakes("""
from mod import F
x = 1 # type: F # noqa
""")
def test_typeCommentsNoWhitespaceAnnotation(self):
self.flakes("""
from mod import F
x = 1 #type:F
""")
def test_typeCommentsInvalidDoesNotMarkAsUsed(self):
self.flakes("""
from mod import F
# type: F
""", m.UnusedImport)
def test_typeCommentsSyntaxError(self):
self.flakes("""
def f(x): # type: (F[) -> None
pass
""", m.CommentAnnotationSyntaxError)
def test_typeCommentsSyntaxErrorCorrectLine(self):
checker = self.flakes("""\
x = 1
# type: definitely not a PEP 484 comment
""", m.CommentAnnotationSyntaxError)
self.assertEqual(checker.messages[0].lineno, 2)
def test_typeCommentsAssignedToPreviousNode(self):
# This test demonstrates an issue in the implementation which
# associates the type comment with a node above it, however the type
# comment isn't valid according to mypy. If an improved approach
# which can detect these "invalid" type comments is implemented, this
# test should be removed / improved to assert that new check.
self.flakes("""
from mod import F
x = 1
# type: F
""")
def test_typeIgnore(self):
self.flakes("""
a = 0 # type: ignore
b = 0 # type: ignore[excuse]
c = 0 # type: ignore=excuse
d = 0 # type: ignore [excuse]
e = 0 # type: ignore whatever
""")
def test_typeIgnoreBogus(self):
self.flakes("""
x = 1 # type: ignored
""", m.UndefinedName)
def test_typeIgnoreBogusUnicode(self):
error = (m.CommentAnnotationSyntaxError if version_info < (3,)
else m.UndefinedName)
self.flakes("""
x = 2 # type: ignore\xc3
""", error)
@skipIf(version_info < (3,), 'new in Python 3')
def test_return_annotation_is_class_scope_variable(self):
self.flakes("""
from typing import TypeVar
class Test:
Y = TypeVar('Y')
def t(self, x: Y) -> Y:
return x
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_return_annotation_is_function_body_variable(self):
self.flakes("""
class Test:
def t(self) -> Y:
Y = 2
return Y
""", m.UndefinedName)
@skipIf(version_info < (3, 8), 'new in Python 3.8')
def test_positional_only_argument_annotations(self):
self.flakes("""
from x import C
def f(c: C, /): ...
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_partially_quoted_type_annotation(self):
self.flakes("""
from queue import Queue
from typing import Optional
def f() -> Optional['Queue[str]']:
return None
""")
def test_partially_quoted_type_assignment(self):
self.flakes("""
from queue import Queue
from typing import Optional
MaybeQueue = Optional['Queue[str]']
""")
def test_nested_partially_quoted_type_assignment(self):
self.flakes("""
from queue import Queue
from typing import Callable
Func = Callable[['Queue[str]'], None]
""")
def test_quoted_type_cast(self):
self.flakes("""
from typing import cast, Optional
maybe_int = cast('Optional[int]', 42)
""")
def test_type_cast_literal_str_to_str(self):
# Checks that our handling of quoted type annotations in the first
# argument to `cast` doesn't cause issues when (only) the _second_
# argument is a literal str which looks a bit like a type annotation.
self.flakes("""
from typing import cast
a_string = cast(str, 'Optional[int]')
""")
def test_quoted_type_cast_renamed_import(self):
self.flakes("""
from typing import cast as tsac, Optional as Maybe
maybe_int = tsac('Maybe[int]', 42)
""")
def test_quoted_TypeVar_constraints(self):
self.flakes("""
from typing import TypeVar, Optional
T = TypeVar('T', 'str', 'Optional[int]', bytes)
""")
def test_quoted_TypeVar_bound(self):
self.flakes("""
from typing import TypeVar, Optional, List
T = TypeVar('T', bound='Optional[int]')
S = TypeVar('S', int, bound='List[int]')
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_literal_type_typing(self):
self.flakes("""
from typing import Literal
def f(x: Literal['some string']) -> None:
return None
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_literal_type_typing_extensions(self):
self.flakes("""
from typing_extensions import Literal
def f(x: Literal['some string']) -> None:
return None
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_annotated_type_typing_missing_forward_type(self):
self.flakes("""
from typing import Annotated
def f(x: Annotated['integer']) -> None:
return None
""", m.UndefinedName)
@skipIf(version_info < (3,), 'new in Python 3')
def test_annotated_type_typing_missing_forward_type_multiple_args(self):
self.flakes("""
from typing import Annotated
def f(x: Annotated['integer', 1]) -> None:
return None
""", m.UndefinedName)
@skipIf(version_info < (3,), 'new in Python 3')
def test_annotated_type_typing_with_string_args(self):
self.flakes("""
from typing import Annotated
def f(x: Annotated[int, '> 0']) -> None:
return None
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_annotated_type_typing_with_string_args_in_union(self):
self.flakes("""
from typing import Annotated, Union
def f(x: Union[Annotated['int', '>0'], 'integer']) -> None:
return None
""", m.UndefinedName)
@skipIf(version_info < (3,), 'new in Python 3')
def test_literal_type_some_other_module(self):
"""err on the side of false-negatives for types named Literal"""
self.flakes("""
from my_module import compat
from my_module.compat import Literal
def f(x: compat.Literal['some string']) -> None:
return None
def g(x: Literal['some string']) -> None:
return None
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_literal_union_type_typing(self):
self.flakes("""
from typing import Literal
def f(x: Literal['some string', 'foo bar']) -> None:
return None
""")
@skipIf(version_info < (3,), 'new in Python 3')
def test_deferred_twice_annotation(self):
self.flakes("""
from queue import Queue
from typing import Optional
def f() -> "Optional['Queue[str]']":
return None
""")
@skipIf(version_info < (3, 7), 'new in Python 3.7')
def test_partial_string_annotations_with_future_annotations(self):
self.flakes("""
from __future__ import annotations
from queue import Queue
from typing import Optional
def f() -> Optional['Queue[str]']:
return None
""")
def test_idomiatic_typing_guards(self):
# typing.TYPE_CHECKING: python3.5.3+
self.flakes("""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from t import T
def f(): # type: () -> T
pass
""")
# False: the old, more-compatible approach
self.flakes("""
if False:
from t import T
def f(): # type: () -> T
pass
""")
# some choose to assign a constant and do it that way
self.flakes("""
MYPY = False
if MYPY:
from t import T
def f(): # type: () -> T
pass
""")
def test_typing_guard_for_protocol(self):
self.flakes("""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Protocol
else:
Protocol = object
class C(Protocol):
def f(): # type: () -> int
pass
""")
def test_typednames_correct_forward_ref(self):
self.flakes("""
from typing import TypedDict, List, NamedTuple
List[TypedDict("x", {})]
List[TypedDict("x", x=int)]
List[NamedTuple("a", a=int)]
List[NamedTuple("a", [("a", int)])]
""")
self.flakes("""
from typing import TypedDict, List, NamedTuple, TypeVar
List[TypedDict("x", {"x": "Y"})]
List[TypedDict("x", x="Y")]
List[NamedTuple("a", [("a", "Y")])]
List[NamedTuple("a", a="Y")]
List[TypedDict("x", {"x": List["a"]})]
List[TypeVar("A", bound="C")]
List[TypeVar("A", List["C"])]
""", *[m.UndefinedName]*7)
self.flakes("""
from typing import NamedTuple, TypeVar, cast
from t import A, B, C, D, E
NamedTuple("A", [("a", A["C"])])
TypeVar("A", bound=A["B"])
TypeVar("A", A["D"])
cast(A["E"], [])
""")
@skipIf(version_info < (3, 6), 'new in Python 3.6')
def test_namedtypes_classes(self):
self.flakes("""
from typing import TypedDict, NamedTuple
class X(TypedDict):
y: TypedDict("z", {"zz":int})
class Y(NamedTuple):
y: NamedTuple("v", [("vv", int)])
""")

View file

@ -0,0 +1,871 @@
import ast
from sys import version_info
from pyflakes import messages as m, checker
from pyflakes.test.harness import TestCase, skipIf, skip
class Test(TestCase):
def test_undefined(self):
self.flakes('bar', m.UndefinedName)
def test_definedInListComp(self):
self.flakes('[a for a in range(10) if a]')
@skipIf(version_info < (3,),
'in Python 2 list comprehensions execute in the same scope')
def test_undefinedInListComp(self):
self.flakes('''
[a for a in range(10)]
a
''',
m.UndefinedName)
@skipIf(version_info < (3,),
'in Python 2 exception names stay bound after the except: block')
def test_undefinedExceptionName(self):
"""Exception names can't be used after the except: block.
The exc variable is unused inside the exception handler."""
self.flakes('''
try:
raise ValueError('ve')
except ValueError as exc:
pass
exc
''', m.UndefinedName, m.UnusedVariable)
def test_namesDeclaredInExceptBlocks(self):
"""Locals declared in except: blocks can be used after the block.
This shows the example in test_undefinedExceptionName is
different."""
self.flakes('''
try:
raise ValueError('ve')
except ValueError as exc:
e = exc
e
''')
@skip('error reporting disabled due to false positives below')
def test_undefinedExceptionNameObscuringLocalVariable(self):
"""Exception names obscure locals, can't be used after.
Last line will raise UnboundLocalError on Python 3 after exiting
the except: block. Note next two examples for false positives to
watch out for."""
self.flakes('''
exc = 'Original value'
try:
raise ValueError('ve')
except ValueError as exc:
pass
exc
''',
m.UndefinedName)
@skipIf(version_info < (3,),
'in Python 2 exception names stay bound after the except: block')
def test_undefinedExceptionNameObscuringLocalVariable2(self):
"""Exception names are unbound after the `except:` block.
Last line will raise UnboundLocalError on Python 3 but would print out
've' on Python 2. The exc variable is unused inside the exception
handler."""
self.flakes('''
try:
raise ValueError('ve')
except ValueError as exc:
pass
print(exc)
exc = 'Original value'
''', m.UndefinedName, m.UnusedVariable)
def test_undefinedExceptionNameObscuringLocalVariableFalsePositive1(self):
"""Exception names obscure locals, can't be used after. Unless.
Last line will never raise UnboundLocalError because it's only
entered if no exception was raised."""
# The exc variable is unused inside the exception handler.
expected = [] if version_info < (3,) else [m.UnusedVariable]
self.flakes('''
exc = 'Original value'
try:
raise ValueError('ve')
except ValueError as exc:
print('exception logged')
raise
exc
''', *expected)
def test_delExceptionInExcept(self):
"""The exception name can be deleted in the except: block."""
self.flakes('''
try:
pass
except Exception as exc:
del exc
''')
def test_undefinedExceptionNameObscuringLocalVariableFalsePositive2(self):
"""Exception names obscure locals, can't be used after. Unless.
Last line will never raise UnboundLocalError because `error` is
only falsy if the `except:` block has not been entered."""
# The exc variable is unused inside the exception handler.
expected = [] if version_info < (3,) else [m.UnusedVariable]
self.flakes('''
exc = 'Original value'
error = None
try:
raise ValueError('ve')
except ValueError as exc:
error = 'exception logged'
if error:
print(error)
else:
exc
''', *expected)
@skip('error reporting disabled due to false positives below')
def test_undefinedExceptionNameObscuringGlobalVariable(self):
"""Exception names obscure globals, can't be used after.
Last line will raise UnboundLocalError on both Python 2 and
Python 3 because the existence of that exception name creates
a local scope placeholder for it, obscuring any globals, etc."""
self.flakes('''
exc = 'Original value'
def func():
try:
pass # nothing is raised
except ValueError as exc:
pass # block never entered, exc stays unbound
exc
''',
m.UndefinedLocal)
@skip('error reporting disabled due to false positives below')
def test_undefinedExceptionNameObscuringGlobalVariable2(self):
"""Exception names obscure globals, can't be used after.
Last line will raise NameError on Python 3 because the name is
locally unbound after the `except:` block, even if it's
nonlocal. We should issue an error in this case because code
only working correctly if an exception isn't raised, is invalid.
Unless it's explicitly silenced, see false positives below."""
self.flakes('''
exc = 'Original value'
def func():
global exc
try:
raise ValueError('ve')
except ValueError as exc:
pass # block never entered, exc stays unbound
exc
''',
m.UndefinedLocal)
def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive1(self):
"""Exception names obscure globals, can't be used after. Unless.
Last line will never raise NameError because it's only entered
if no exception was raised."""
# The exc variable is unused inside the exception handler.
expected = [] if version_info < (3,) else [m.UnusedVariable]
self.flakes('''
exc = 'Original value'
def func():
global exc
try:
raise ValueError('ve')
except ValueError as exc:
print('exception logged')
raise
exc
''', *expected)
def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive2(self):
"""Exception names obscure globals, can't be used after. Unless.
Last line will never raise NameError because `error` is only
falsy if the `except:` block has not been entered."""
# The exc variable is unused inside the exception handler.
expected = [] if version_info < (3,) else [m.UnusedVariable]
self.flakes('''
exc = 'Original value'
def func():
global exc
error = None
try:
raise ValueError('ve')
except ValueError as exc:
error = 'exception logged'
if error:
print(error)
else:
exc
''', *expected)
def test_functionsNeedGlobalScope(self):
self.flakes('''
class a:
def b():
fu
fu = 1
''')
def test_builtins(self):
self.flakes('range(10)')
def test_builtinWindowsError(self):
"""
C{WindowsError} is sometimes a builtin name, so no warning is emitted
for using it.
"""
self.flakes('WindowsError')
@skipIf(version_info < (3, 6), 'new feature in 3.6')
def test_moduleAnnotations(self):
"""
Use of the C{__annotations__} in module scope should not emit
an undefined name warning when version is greater than or equal to 3.6.
"""
self.flakes('__annotations__')
def test_magicGlobalsFile(self):
"""
Use of the C{__file__} magic global should not emit an undefined name
warning.
"""
self.flakes('__file__')
def test_magicGlobalsBuiltins(self):
"""
Use of the C{__builtins__} magic global should not emit an undefined
name warning.
"""
self.flakes('__builtins__')
def test_magicGlobalsName(self):
"""
Use of the C{__name__} magic global should not emit an undefined name
warning.
"""
self.flakes('__name__')
def test_magicGlobalsPath(self):
"""
Use of the C{__path__} magic global should not emit an undefined name
warning, if you refer to it from a file called __init__.py.
"""
self.flakes('__path__', m.UndefinedName)
self.flakes('__path__', filename='package/__init__.py')
def test_magicModuleInClassScope(self):
"""
Use of the C{__module__} magic builtin should not emit an undefined
name warning if used in class scope.
"""
self.flakes('__module__', m.UndefinedName)
self.flakes('''
class Foo:
__module__
''')
self.flakes('''
class Foo:
def bar(self):
__module__
''', m.UndefinedName)
@skipIf(version_info < (3, 3), "Python >= 3.3 only")
def test_magicQualnameInClassScope(self):
"""
Use of the C{__qualname__} magic builtin should not emit an undefined
name warning if used in class scope.
"""
self.flakes('__qualname__', m.UndefinedName)
self.flakes('''
class Foo:
__qualname__
''')
self.flakes('''
class Foo:
def bar(self):
__qualname__
''', m.UndefinedName)
def test_globalImportStar(self):
"""Can't find undefined names with import *."""
self.flakes('from fu import *; bar',
m.ImportStarUsed, m.ImportStarUsage)
@skipIf(version_info >= (3,), 'obsolete syntax')
def test_localImportStar(self):
"""
A local import * still allows undefined names to be found
in upper scopes.
"""
self.flakes('''
def a():
from fu import *
bar
''', m.ImportStarUsed, m.UndefinedName, m.UnusedImport)
@skipIf(version_info >= (3,), 'obsolete syntax')
def test_unpackedParameter(self):
"""Unpacked function parameters create bindings."""
self.flakes('''
def a((bar, baz)):
bar; baz
''')
def test_definedByGlobal(self):
"""
"global" can make an otherwise undefined name in another function
defined.
"""
self.flakes('''
def a(): global fu; fu = 1
def b(): fu
''')
self.flakes('''
def c(): bar
def b(): global bar; bar = 1
''')
def test_definedByGlobalMultipleNames(self):
"""
"global" can accept multiple names.
"""
self.flakes('''
def a(): global fu, bar; fu = 1; bar = 2
def b(): fu; bar
''')
def test_globalInGlobalScope(self):
"""
A global statement in the global scope is ignored.
"""
self.flakes('''
global x
def foo():
print(x)
''', m.UndefinedName)
def test_global_reset_name_only(self):
"""A global statement does not prevent other names being undefined."""
# Only different undefined names are reported.
# See following test that fails where the same name is used.
self.flakes('''
def f1():
s
def f2():
global m
''', m.UndefinedName)
@skip("todo")
def test_unused_global(self):
"""An unused global statement does not define the name."""
self.flakes('''
def f1():
m
def f2():
global m
''', m.UndefinedName)
def test_del(self):
"""Del deletes bindings."""
self.flakes('a = 1; del a; a', m.UndefinedName)
def test_delGlobal(self):
"""Del a global binding from a function."""
self.flakes('''
a = 1
def f():
global a
del a
a
''')
def test_delUndefined(self):
"""Del an undefined name."""
self.flakes('del a', m.UndefinedName)
def test_delConditional(self):
"""
Ignores conditional bindings deletion.
"""
self.flakes('''
context = None
test = True
if False:
del(test)
assert(test)
''')
def test_delConditionalNested(self):
"""
Ignored conditional bindings deletion even if they are nested in other
blocks.
"""
self.flakes('''
context = None
test = True
if False:
with context():
del(test)
assert(test)
''')
def test_delWhile(self):
"""
Ignore bindings deletion if called inside the body of a while
statement.
"""
self.flakes('''
def test():
foo = 'bar'
while False:
del foo
assert(foo)
''')
def test_delWhileTestUsage(self):
"""
Ignore bindings deletion if called inside the body of a while
statement and name is used inside while's test part.
"""
self.flakes('''
def _worker():
o = True
while o is not True:
del o
o = False
''')
def test_delWhileNested(self):
"""
Ignore bindings deletions if node is part of while's test, even when
del is in a nested block.
"""
self.flakes('''
context = None
def _worker():
o = True
while o is not True:
while True:
with context():
del o
o = False
''')
def test_globalFromNestedScope(self):
"""Global names are available from nested scopes."""
self.flakes('''
a = 1
def b():
def c():
a
''')
def test_laterRedefinedGlobalFromNestedScope(self):
"""
Test that referencing a local name that shadows a global, before it is
defined, generates a warning.
"""
self.flakes('''
a = 1
def fun():
a
a = 2
return a
''', m.UndefinedLocal)
def test_laterRedefinedGlobalFromNestedScope2(self):
"""
Test that referencing a local name in a nested scope that shadows a
global declared in an enclosing scope, before it is defined, generates
a warning.
"""
self.flakes('''
a = 1
def fun():
global a
def fun2():
a
a = 2
return a
''', m.UndefinedLocal)
def test_intermediateClassScopeIgnored(self):
"""
If a name defined in an enclosing scope is shadowed by a local variable
and the name is used locally before it is bound, an unbound local
warning is emitted, even if there is a class scope between the enclosing
scope and the local scope.
"""
self.flakes('''
def f():
x = 1
class g:
def h(self):
a = x
x = None
print(x, a)
print(x)
''', m.UndefinedLocal)
def test_doubleNestingReportsClosestName(self):
"""
Test that referencing a local name in a nested scope that shadows a
variable declared in two different outer scopes before it is defined
in the innermost scope generates an UnboundLocal warning which
refers to the nearest shadowed name.
"""
exc = self.flakes('''
def a():
x = 1
def b():
x = 2 # line 5
def c():
x
x = 3
return x
return x
return x
''', m.UndefinedLocal).messages[0]
# _DoctestMixin.flakes adds two lines preceding the code above.
expected_line_num = 7 if self.withDoctest else 5
self.assertEqual(exc.message_args, ('x', expected_line_num))
def test_laterRedefinedGlobalFromNestedScope3(self):
"""
Test that referencing a local name in a nested scope that shadows a
global, before it is defined, generates a warning.
"""
self.flakes('''
def fun():
a = 1
def fun2():
a
a = 1
return a
return a
''', m.UndefinedLocal)
def test_undefinedAugmentedAssignment(self):
self.flakes(
'''
def f(seq):
a = 0
seq[a] += 1
seq[b] /= 2
c[0] *= 2
a -= 3
d += 4
e[any] = 5
''',
m.UndefinedName, # b
m.UndefinedName, # c
m.UndefinedName, m.UnusedVariable, # d
m.UndefinedName, # e
)
def test_nestedClass(self):
"""Nested classes can access enclosing scope."""
self.flakes('''
def f(foo):
class C:
bar = foo
def f(self):
return foo
return C()
f(123).f()
''')
def test_badNestedClass(self):
"""Free variables in nested classes must bind at class creation."""
self.flakes('''
def f():
class C:
bar = foo
foo = 456
return foo
f()
''', m.UndefinedName)
def test_definedAsStarArgs(self):
"""Star and double-star arg names are defined."""
self.flakes('''
def f(a, *b, **c):
print(a, b, c)
''')
@skipIf(version_info < (3,), 'new in Python 3')
def test_definedAsStarUnpack(self):
"""Star names in unpack are defined."""
self.flakes('''
a, *b = range(10)
print(a, b)
''')
self.flakes('''
*a, b = range(10)
print(a, b)
''')
self.flakes('''
a, *b, c = range(10)
print(a, b, c)
''')
@skipIf(version_info < (3,), 'new in Python 3')
def test_usedAsStarUnpack(self):
"""
Star names in unpack are used if RHS is not a tuple/list literal.
"""
self.flakes('''
def f():
a, *b = range(10)
''')
self.flakes('''
def f():
(*a, b) = range(10)
''')
self.flakes('''
def f():
[a, *b, c] = range(10)
''')
@skipIf(version_info < (3,), 'new in Python 3')
def test_unusedAsStarUnpack(self):
"""
Star names in unpack are unused if RHS is a tuple/list literal.
"""
self.flakes('''
def f():
a, *b = any, all, 4, 2, 'un'
''', m.UnusedVariable, m.UnusedVariable)
self.flakes('''
def f():
(*a, b) = [bool, int, float, complex]
''', m.UnusedVariable, m.UnusedVariable)
self.flakes('''
def f():
[a, *b, c] = 9, 8, 7, 6, 5, 4
''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable)
@skipIf(version_info < (3,), 'new in Python 3')
def test_keywordOnlyArgs(self):
"""Keyword-only arg names are defined."""
self.flakes('''
def f(*, a, b=None):
print(a, b)
''')
self.flakes('''
import default_b
def f(*, a, b=default_b):
print(a, b)
''')
@skipIf(version_info < (3,), 'new in Python 3')
def test_keywordOnlyArgsUndefined(self):
"""Typo in kwonly name."""
self.flakes('''
def f(*, a, b=default_c):
print(a, b)
''', m.UndefinedName)
@skipIf(version_info < (3,), 'new in Python 3')
def test_annotationUndefined(self):
"""Undefined annotations."""
self.flakes('''
from abc import note1, note2, note3, note4, note5
def func(a: note1, *args: note2,
b: note3=12, **kw: note4) -> note5: pass
''')
self.flakes('''
def func():
d = e = 42
def func(a: {1, d}) -> (lambda c: e): pass
''')
@skipIf(version_info < (3,), 'new in Python 3')
def test_metaClassUndefined(self):
self.flakes('''
from abc import ABCMeta
class A(metaclass=ABCMeta): pass
''')
def test_definedInGenExp(self):
"""
Using the loop variable of a generator expression results in no
warnings.
"""
self.flakes('(a for a in [1, 2, 3] if a)')
self.flakes('(b for b in (a for a in [1, 2, 3] if a) if b)')
def test_undefinedInGenExpNested(self):
"""
The loop variables of generator expressions nested together are
not defined in the other generator.
"""
self.flakes('(b for b in (a for a in [1, 2, 3] if b) if b)',
m.UndefinedName)
self.flakes('(b for b in (a for a in [1, 2, 3] if a) if a)',
m.UndefinedName)
def test_undefinedWithErrorHandler(self):
"""
Some compatibility code checks explicitly for NameError.
It should not trigger warnings.
"""
self.flakes('''
try:
socket_map
except NameError:
socket_map = {}
''')
self.flakes('''
try:
_memoryview.contiguous
except (NameError, AttributeError):
raise RuntimeError("Python >= 3.3 is required")
''')
# If NameError is not explicitly handled, generate a warning
self.flakes('''
try:
socket_map
except:
socket_map = {}
''', m.UndefinedName)
self.flakes('''
try:
socket_map
except Exception:
socket_map = {}
''', m.UndefinedName)
def test_definedInClass(self):
"""
Defined name for generator expressions and dict/set comprehension.
"""
self.flakes('''
class A:
T = range(10)
Z = (x for x in T)
L = [x for x in T]
B = dict((i, str(i)) for i in T)
''')
self.flakes('''
class A:
T = range(10)
X = {x for x in T}
Y = {x:x for x in T}
''')
def test_definedInClassNested(self):
"""Defined name for nested generator expressions in a class."""
self.flakes('''
class A:
T = range(10)
Z = (x for x in (a for a in T))
''')
def test_undefinedInLoop(self):
"""
The loop variable is defined after the expression is computed.
"""
self.flakes('''
for i in range(i):
print(i)
''', m.UndefinedName)
self.flakes('''
[42 for i in range(i)]
''', m.UndefinedName)
self.flakes('''
(42 for i in range(i))
''', m.UndefinedName)
def test_definedFromLambdaInDictionaryComprehension(self):
"""
Defined name referenced from a lambda function within a dict/set
comprehension.
"""
self.flakes('''
{lambda: id(x) for x in range(10)}
''')
def test_definedFromLambdaInGenerator(self):
"""
Defined name referenced from a lambda function within a generator
expression.
"""
self.flakes('''
any(lambda: id(x) for x in range(10))
''')
def test_undefinedFromLambdaInDictionaryComprehension(self):
"""
Undefined name referenced from a lambda function within a dict/set
comprehension.
"""
self.flakes('''
{lambda: id(y) for x in range(10)}
''', m.UndefinedName)
def test_undefinedFromLambdaInComprehension(self):
"""
Undefined name referenced from a lambda function within a generator
expression.
"""
self.flakes('''
any(lambda: id(y) for x in range(10))
''', m.UndefinedName)
def test_dunderClass(self):
"""
`__class__` is defined in class scope under Python 3, but is not
in Python 2.
"""
code = '''
class Test(object):
def __init__(self):
print(__class__.__name__)
self.x = 1
t = Test()
'''
if version_info < (3,):
self.flakes(code, m.UndefinedName)
else:
self.flakes(code)
class NameTests(TestCase):
"""
Tests for some extra cases of name handling.
"""
def test_impossibleContext(self):
"""
A Name node with an unrecognized context results in a RuntimeError being
raised.
"""
tree = ast.parse("x = 10")
file_tokens = checker.make_tokens("x = 10")
# Make it into something unrecognizable.
tree.body[0].targets[0].ctx = object()
self.assertRaises(RuntimeError, checker.Checker, tree, file_tokens=file_tokens)