Repository: PyCQA/pyflakes

Issue:
syntax error in forward annotation when using Annotated
When using [`Annotated`](https://www.python.org/dev/peps/pep-0593/) with string annotations the following error is raised:

```
./pyflakes.py:3:16 syntax error in forward annotation '>1'
```

Triggering code:

```
from typing import Annotated

Annotated[int, '>1']
```

Discussion:
sk-: First pass at fixing #574, this is my first contribution to `pyflakes` so let me know if any changes are required.

Note also that the failing test in Appveyor is because it failed to install `pypy`.

```
(New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.1.1-win32.zip', "$env:appveyor_build_folder\pypy2.7-v7.1.1-win32.zip")
Exception calling "DownloadFile" with "2" argument(s): "The remote server returned an error: (404) Not Found."
At line:1 char:1
+ (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/p ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : WebException
 
Command executed with exception: Exception calling "DownloadFile" with "2" argument(s): "The remote server returned an error: (404) Not Found."
```
asottile: if you rebase, appveyor should be fixed
sk-: Thanks @asottile for the quick review.
asottile: this has a conflict with #535 -- ideally I'd like to merge the other one first (since it was written first) and then update this PR -- thoughts?
sigmavirus24: @asottile that makes sense. The other one was already hit with a different merge conflict. Have you been holding off on merging that one for a reason?
asottile: @sigmavirus24 iirc pyflakes has 2-signoff so I was going to wait for a second review (though, given inactivity maybe we should go to 1-signoff?)
sigmavirus24: > iirc pyflakes has 2-signoff

If it does, it's not configured that way with the settings. I don't remember the requirement being 2 approvals, but I'll go review that one and see if I can approve it.
asottile: @sk-  let me know how that looks!
sk-: @asottile Looks great, specially the extraction of the function `_is_name_or_attr`.

Patch:
diff --git a/pyflakes/checker.py b/pyflakes/checker.py
index 16c783f..a6439d6 100644
--- a/pyflakes/checker.py
+++ b/pyflakes/checker.py
@@ -128,6 +128,13 @@ def _is_const_non_singleton(node):  # type: (ast.AST) -> bool
     return _is_constant(node) and not _is_singleton(node)
 
 
+def _is_name_or_attr(node, name):  # type: (ast.Ast, str) -> bool
+    return (
+        (isinstance(node, ast.Name) and node.id == name) or
+        (isinstance(node, ast.Attribute) and node.attr == name)
+    )
+
+
 # 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
@@ -1497,20 +1504,39 @@ class Checker(object):
         STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren
 
     def SUBSCRIPT(self, node):
-        if (
-                (
-                    isinstance(node.value, ast.Name) and
-                    node.value.id == 'Literal'
-                ) or (
-                    isinstance(node.value, ast.Attribute) and
-                    node.value.attr == 'Literal'
-                )
-        ):
+        if _is_name_or_attr(node.value, 'Literal'):
             orig, self._in_typing_literal = self._in_typing_literal, True
             try:
                 self.handleChildren(node)
             finally:
                 self._in_typing_literal = orig
+        elif _is_name_or_attr(node.value, 'Annotated'):
+            self.handleNode(node.value, node)
+
+            # py39+
+            if isinstance(node.slice, ast.Tuple):
+                slice_tuple = node.slice
+            # <py39
+            elif (
+                    isinstance(node.slice, ast.Index) and
+                    isinstance(node.slice.value, ast.Tuple)
+            ):
+                slice_tuple = node.slice.value
+            else:
+                slice_tuple = None
+
+            # not a multi-arg `Annotated`
+            if slice_tuple is None or len(slice_tuple.elts) < 2:
+                self.handleNode(node.slice, node)
+            else:
+                # the first argument is the type
+                self.handleNode(slice_tuple.elts[0], node)
+                # the rest of the arguments are not
+                with self._enter_annotation(AnnotationState.NONE):
+                    for arg in slice_tuple.elts[1:]:
+                        self.handleNode(arg, node)
+
+            self.handleNode(node.ctx, node)
         else:
             if _is_any_typing_member(node.value, self.scopeStack):
                 with self._enter_annotation():


Test patch:
diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py
index eff222b..d5a3e08 100644
--- a/pyflakes/test/test_type_annotations.py
+++ b/pyflakes/test/test_type_annotations.py
@@ -542,6 +542,42 @@ class TestTypeAnnotations(TestCase):
             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"""