diff options
author | Sebastian Kreft <sk-@users.noreply.github.com> | 2020-09-28 17:44:21 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-09-28 13:44:21 -0700 |
commit | c23a81037d4f68067c4c987985d177ec7664de59 (patch) | |
tree | a298fa01477a8b766c818295848d9fc90d7bb6d7 | |
parent | 5fc37cbda5bf4e5afcb64c45caa62062979256b4 (diff) | |
download | pyflakes-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.py | 44 | ||||
-rw-r--r-- | pyflakes/test/test_type_annotations.py | 36 |
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(""" |