summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Kreft <sk-@users.noreply.github.com>2020-09-28 17:44:21 -0300
committerGitHub <noreply@github.com>2020-09-28 13:44:21 -0700
commitc23a81037d4f68067c4c987985d177ec7664de59 (patch)
treea298fa01477a8b766c818295848d9fc90d7bb6d7
parent5fc37cbda5bf4e5afcb64c45caa62062979256b4 (diff)
downloadpyflakes-c23a81037d4f68067c4c987985d177ec7664de59.tar.gz
fix: don't return syntax error for Annotated's metadata (#580)
* fix: don't return syntax error for Annotated's metadata PEP 593 https://www.python.org/dev/peps/pep-0593/ introduced the type Annotated, which allows to add metadata to types. By the definition only the first argument is required to be a proper type, all the other arguments are dependent on the consumer and can be other types or even literals. This PR fixes #574. * fix whitespace * fix tests and formatting * PR comments from asottile * fix _in_annotation and remove _in_typing_annotated field * fix test name * get rid of _exit_annotation in favor of _entger_annotation(AnnotationState.None) * handle Annotated in python3.9+ Co-authored-by: Anthony Sottile <asottile@umich.edu>
-rw-r--r--pyflakes/checker.py44
-rw-r--r--pyflakes/test/test_type_annotations.py36
2 files changed, 71 insertions, 9 deletions
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():
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
@@ -543,6 +543,42 @@ class TestTypeAnnotations(TestCase):
""")
@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("""