Repository: PyCQA/pyflakes
Pull request #511 (2020-02-14 22:54:55)

Fix bug in async function scope checking
Previously attempting to check an async function scoped tree would immediately error because tuples are not callable.

The test added here is a direct copy of `test_scope_function` as I'm not sure what (if anything) should specifically be tested for async functions, though it serves to prove the fix. Happy to change this if there are other things we should be checking there.


Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index c89d6fb..f886c26 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -78,6 +78,51 @@ else:
     LOOP_TYPES = (ast.While, ast.For)
     FUNCTION_TYPES = (ast.FunctionDef,)
 
+
+if PY38_PLUS:
+    def _is_singleton(node):  # type: (ast.AST) -> bool
+        return (
+            isinstance(node, ast.Constant) and
+            isinstance(node.value, (bool, type(Ellipsis), type(None)))
+        )
+elif not PY2:
+    def _is_singleton(node):  # type: (ast.AST) -> bool
+        return isinstance(node, (ast.NameConstant, ast.Ellipsis))
+else:
+    def _is_singleton(node):  # type: (ast.AST) -> bool
+        return (
+            isinstance(node, ast.Name) and
+            node.id in {'True', 'False', 'Ellipsis', 'None'}
+        )
+
+
+def _is_tuple_constant(node):  # type: (ast.AST) -> bool
+    return (
+        isinstance(node, ast.Tuple) and
+        all(_is_constant(elt) for elt in node.elts)
+    )
+
+
+if PY38_PLUS:
+    def _is_constant(node):
+        return isinstance(node, ast.Constant) or _is_tuple_constant(node)
+else:
+    _const_tps = (ast.Str, ast.Num)
+    if not PY2:
+        _const_tps += (ast.Bytes,)
+
+    def _is_constant(node):
+        return (
+            isinstance(node, _const_tps) or
+            _is_singleton(node) or
+            _is_tuple_constant(node)
+        )
+
+
+def _is_const_non_singleton(node):  # type: (ast.AST) -> bool
+    return _is_constant(node) and not _is_singleton(node)
+
+
 # https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L102-L104
 TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*')
 # https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413
@@ -748,7 +793,7 @@ class Checker(object):
         ast.DictComp: GeneratorScope,
     }
     if PY35_PLUS:
-        _ast_node_scope[ast.AsyncFunctionDef] = FunctionScope,
+        _ast_node_scope[ast.AsyncFunctionDef] = FunctionScope
 
     nodeDepth = 0
     offset = None
@@ -2124,14 +2169,14 @@ class Checker(object):
             self.handleNode(node.value, node)
 
     def COMPARE(self, node):
-        literals = (ast.Str, ast.Num)
-        if not PY2:
-            literals += (ast.Bytes,)
-
         left = node.left
         for op, right in zip(node.ops, node.comparators):
-            if (isinstance(op, (ast.Is, ast.IsNot)) and
-                    (isinstance(left, literals) or isinstance(right, literals))):
+            if (
+                    isinstance(op, (ast.Is, ast.IsNot)) and (
+                        _is_const_non_singleton(left) or
+                        _is_const_non_singleton(right)
+                    )
+            ):
                 self.report(messages.IsLiteral, node)
             left = right
 
diff --git a/pyflakes/messages.py b/pyflakes/messages.py
index cf67cf8..e93a493 100644
--- a/pyflakes/messages.py
+++ b/pyflakes/messages.py
@@ -265,7 +265,7 @@ class InvalidPrintSyntax(Message):
 
 
 class IsLiteral(Message):
-    message = 'use ==/!= to compare str, bytes, and int literals'
+    message = 'use ==/!= to compare constant literals (str, bytes, int, float, tuple)'
 
 
 class FStringMissingPlaceholders(Message):


Test patch:
diff --git a/pyflakes/test/test_code_segment.py b/pyflakes/test/test_code_segment.py
index 5e53f8f..131a74d 100644
--- a/pyflakes/test/test_code_segment.py
+++ b/pyflakes/test/test_code_segment.py
@@ -1,7 +1,9 @@
+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
+from pyflakes.test.harness import TestCase, skipIf
 
 
 class TestCodeSegments(TestCase):
@@ -124,3 +126,7 @@ class TestCodeSegments(TestCase):
         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)
diff --git a/pyflakes/test/test_is_literal.py b/pyflakes/test/test_is_literal.py
index 9280cda..fbbb205 100644
--- a/pyflakes/test/test_is_literal.py
+++ b/pyflakes/test/test_is_literal.py
@@ -198,3 +198,25 @@ class Test(TestCase):
         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
+        ''')