Skip to content

Commit c23a810

Browse files
sk-asottile
andauthored
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 <[email protected]>
1 parent 5fc37cb commit c23a810

File tree

2 files changed

+71
-9
lines changed

2 files changed

+71
-9
lines changed

pyflakes/checker.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,13 @@ def _is_const_non_singleton(node): # type: (ast.AST) -> bool
128128
return _is_constant(node) and not _is_singleton(node)
129129

130130

131+
def _is_name_or_attr(node, name): # type: (ast.Ast, str) -> bool
132+
return (
133+
(isinstance(node, ast.Name) and node.id == name) or
134+
(isinstance(node, ast.Attribute) and node.attr == name)
135+
)
136+
137+
131138
# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L102-L104
132139
TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*')
133140
# https://github.com/python/typed_ast/blob/1.4.0/ast27/Parser/tokenizer.c#L1408-L1413
@@ -1497,20 +1504,39 @@ def ignore(self, node):
14971504
STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren
14981505

14991506
def SUBSCRIPT(self, node):
1500-
if (
1501-
(
1502-
isinstance(node.value, ast.Name) and
1503-
node.value.id == 'Literal'
1504-
) or (
1505-
isinstance(node.value, ast.Attribute) and
1506-
node.value.attr == 'Literal'
1507-
)
1508-
):
1507+
if _is_name_or_attr(node.value, 'Literal'):
15091508
orig, self._in_typing_literal = self._in_typing_literal, True
15101509
try:
15111510
self.handleChildren(node)
15121511
finally:
15131512
self._in_typing_literal = orig
1513+
elif _is_name_or_attr(node.value, 'Annotated'):
1514+
self.handleNode(node.value, node)
1515+
1516+
# py39+
1517+
if isinstance(node.slice, ast.Tuple):
1518+
slice_tuple = node.slice
1519+
# <py39
1520+
elif (
1521+
isinstance(node.slice, ast.Index) and
1522+
isinstance(node.slice.value, ast.Tuple)
1523+
):
1524+
slice_tuple = node.slice.value
1525+
else:
1526+
slice_tuple = None
1527+
1528+
# not a multi-arg `Annotated`
1529+
if slice_tuple is None or len(slice_tuple.elts) < 2:
1530+
self.handleNode(node.slice, node)
1531+
else:
1532+
# the first argument is the type
1533+
self.handleNode(slice_tuple.elts[0], node)
1534+
# the rest of the arguments are not
1535+
with self._enter_annotation(AnnotationState.NONE):
1536+
for arg in slice_tuple.elts[1:]:
1537+
self.handleNode(arg, node)
1538+
1539+
self.handleNode(node.ctx, node)
15141540
else:
15151541
if _is_any_typing_member(node.value, self.scopeStack):
15161542
with self._enter_annotation():

pyflakes/test/test_type_annotations.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,42 @@ def f(x: Literal['some string']) -> None:
542542
return None
543543
""")
544544

545+
@skipIf(version_info < (3,), 'new in Python 3')
546+
def test_annotated_type_typing_missing_forward_type(self):
547+
self.flakes("""
548+
from typing import Annotated
549+
550+
def f(x: Annotated['integer']) -> None:
551+
return None
552+
""", m.UndefinedName)
553+
554+
@skipIf(version_info < (3,), 'new in Python 3')
555+
def test_annotated_type_typing_missing_forward_type_multiple_args(self):
556+
self.flakes("""
557+
from typing import Annotated
558+
559+
def f(x: Annotated['integer', 1]) -> None:
560+
return None
561+
""", m.UndefinedName)
562+
563+
@skipIf(version_info < (3,), 'new in Python 3')
564+
def test_annotated_type_typing_with_string_args(self):
565+
self.flakes("""
566+
from typing import Annotated
567+
568+
def f(x: Annotated[int, '> 0']) -> None:
569+
return None
570+
""")
571+
572+
@skipIf(version_info < (3,), 'new in Python 3')
573+
def test_annotated_type_typing_with_string_args_in_union(self):
574+
self.flakes("""
575+
from typing import Annotated, Union
576+
577+
def f(x: Union[Annotated['int', '>0'], 'integer']) -> None:
578+
return None
579+
""", m.UndefinedName)
580+
545581
@skipIf(version_info < (3,), 'new in Python 3')
546582
def test_literal_type_some_other_module(self):
547583
"""err on the side of false-negatives for types named Literal"""

0 commit comments

Comments
 (0)