Repository: PyCQA/pyflakes
Pull request #457 (2019-08-03 19:44:26)

Add support for PEP 572 assignment expressions


Patch:
diff --git a/NEWS.rst b/NEWS.rst
index 993a545..c6cd960 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,4 +1,5 @@
 2.1.1 (2019-02-28)
+
 - Fix reported line number for type comment errors
 - Fix typing.overload check to only check imported names
 
diff --git a/pyflakes/api.py b/pyflakes/api.py
index 6226e5d..50e8ad8 100644
--- a/pyflakes/api.py
+++ b/pyflakes/api.py
@@ -14,7 +14,7 @@ from pyflakes import reporter as modReporter
 
 __all__ = ['check', 'checkPath', 'checkRecursive', 'iterSourceCode', 'main']
 
-PYTHON_SHEBANG_REGEX = re.compile(br'^#!.*\bpython[23w]?\b\s*$')
+PYTHON_SHEBANG_REGEX = re.compile(br'^#!.*\bpython([23](\.\d+)?|w)?[dmu]?\s')
 
 
 def check(codeString, filename, reporter=None):
@@ -118,8 +118,7 @@ def isPythonFile(filename):
     except IOError:
         return False
 
-    first_line = text.splitlines()[0]
-    return PYTHON_SHEBANG_REGEX.match(first_line)
+    return PYTHON_SHEBANG_REGEX.match(text)
 
 
 def iterSourceCode(paths):
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index f9ae0aa..3ed398d 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -76,11 +76,13 @@ else:
     FOR_TYPES = (ast.For,)
     LOOP_TYPES = (ast.While, ast.For)
 
-# https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L102-L104
+# 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/55420396/ast27/Parser/tokenizer.c#L1400
-TYPE_IGNORE_RE = re.compile(TYPE_COMMENT_RE.pattern + r'ignore\s*(#|$)')
-# https://github.com/python/typed_ast/blob/55420396/ast27/Grammar/Grammar#L147
+# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413
+ASCII_NON_ALNUM = ''.join([chr(i) for i in range(128) if not chr(i).isalnum()])
+TYPE_IGNORE_RE = re.compile(
+    TYPE_COMMENT_RE.pattern + r'ignore([{}]|$)'.format(ASCII_NON_ALNUM))
+# https://github.com/python/typed_ast/blob/1.4.0/ast27/Grammar/Grammar#L147
 TYPE_FUNC_RE = re.compile(r'^(\(.*?\))\s*->\s*(.*)$')
 
 
@@ -1327,7 +1329,7 @@ class Checker(object):
     # "expr" type nodes
     BOOLOP = UNARYOP = IFEXP = SET = \
         REPR = ATTRIBUTE = SUBSCRIPT = \
-        STARRED = NAMECONSTANT = handleChildren
+        STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren
 
     def _handle_string_dot_format(self, node):
         try:


Test patch:
diff --git a/pyflakes/test/test_api.py b/pyflakes/test/test_api.py
index ee205f9..93b3886 100644
--- a/pyflakes/test/test_api.py
+++ b/pyflakes/test/test_api.py
@@ -214,9 +214,35 @@ class TestIterSourceCode(TestCase):
         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]))
+            sorted([python, python2, python3, pythonw, python3args, python2u,
+                    python3d, python38m, python27]))
 
     def test_multipleDirectories(self):
         """
diff --git a/pyflakes/test/test_checker.py b/pyflakes/test/test_checker.py
index f47588d..b527572 100644
--- a/pyflakes/test/test_checker.py
+++ b/pyflakes/test/test_checker.py
@@ -154,7 +154,7 @@ class CollectTypeCommentsTests(TestCase):
 
     def test_type_comment_starts_with_word_ignore(self):
         ret = self._collect('x = 1 # type: ignore[T]')
-        self.assertSetEqual(ret, {(ast.Assign, ('# type: ignore[T]',))})
+        self.assertSetEqual(ret, set())
 
     def test_last_node_wins(self):
         """
diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py
index 775fdde..df2f790 100644
--- a/pyflakes/test/test_other.py
+++ b/pyflakes/test/test_other.py
@@ -1754,6 +1754,15 @@ class TestUnusedAssignment(TestCase):
         print(f'\x7b4*baz\N{RIGHT CURLY BRACKET}')
         ''')
 
+    @skipIf(version_info < (3, 8), 'new in Python 3.8')
+    def test_assign_expr(self):
+        """Test PEP 572 assignment expressions are treated as usage / write."""
+        self.flakes('''
+        from foo import y
+        print(x := y)
+        print(x)
+        ''')
+
 
 class TestStringFormatting(TestCase):
 
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index 9c34dcf..bebdef7 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -343,6 +343,27 @@ class TestTypeAnnotations(TestCase):
         # 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("""