Skip to content

Commit c9708a1

Browse files
authored
Fix false positive with partially quoted annotations (#479)
1 parent 4ed1ba4 commit c9708a1

File tree

2 files changed

+144
-19
lines changed

2 files changed

+144
-19
lines changed

pyflakes/checker.py

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -618,40 +618,55 @@ def getNodeName(node):
618618
return node.name
619619

620620

621-
def is_typing_overload(value, scope_stack):
622-
def name_is_typing_overload(name): # type: (str) -> bool
621+
def _is_typing(node, typing_attr, scope_stack):
622+
def _bare_name_is_attr(name):
623+
expected_typing_names = {
624+
'typing.{}'.format(typing_attr),
625+
'typing_extensions.{}'.format(typing_attr),
626+
}
623627
for scope in reversed(scope_stack):
624628
if name in scope:
625629
return (
626630
isinstance(scope[name], ImportationFrom) and
627-
scope[name].fullName in (
628-
'typing.overload', 'typing_extensions.overload',
629-
)
631+
scope[name].fullName in expected_typing_names
630632
)
631633

632634
return False
633635

634-
def is_typing_overload_decorator(node):
635-
return (
636-
(
637-
isinstance(node, ast.Name) and name_is_typing_overload(node.id)
638-
) or (
639-
isinstance(node, ast.Attribute) and
640-
isinstance(node.value, ast.Name) and
641-
node.value.id == 'typing' and
642-
node.attr == 'overload'
643-
)
636+
return (
637+
(
638+
isinstance(node, ast.Name) and
639+
_bare_name_is_attr(node.id)
640+
) or (
641+
isinstance(node, ast.Attribute) and
642+
isinstance(node.value, ast.Name) and
643+
node.value.id in {'typing', 'typing_extensions'} and
644+
node.attr == typing_attr
644645
)
646+
)
645647

648+
649+
def is_typing_overload(value, scope_stack):
646650
return (
647651
isinstance(value.source, FUNCTION_TYPES) and
648652
any(
649-
is_typing_overload_decorator(dec)
653+
_is_typing(dec, 'overload', scope_stack)
650654
for dec in value.source.decorator_list
651655
)
652656
)
653657

654658

659+
def in_annotation(func):
660+
@functools.wraps(func)
661+
def in_annotation_func(self, *args, **kwargs):
662+
orig, self._in_annotation = self._in_annotation, True
663+
try:
664+
return func(self, *args, **kwargs)
665+
finally:
666+
self._in_annotation = orig
667+
return in_annotation_func
668+
669+
655670
def make_tokens(code):
656671
# PY3: tokenize.tokenize requires readline of bytes
657672
if not isinstance(code, bytes):
@@ -738,6 +753,9 @@ class Checker(object):
738753
nodeDepth = 0
739754
offset = None
740755
traceTree = False
756+
_in_annotation = False
757+
_in_typing_literal = False
758+
_in_deferred = False
741759

742760
builtIns = set(builtin_vars).union(_MAGIC_GLOBALS)
743761
_customBuiltIns = os.environ.get('PYFLAKES_BUILTINS')
@@ -769,6 +787,7 @@ def __init__(self, tree, filename='(none)', builtins=None,
769787
for builtin in self.builtIns:
770788
self.addBinding(None, Builtin(builtin))
771789
self.handleChildren(tree)
790+
self._in_deferred = True
772791
self.runDeferred(self._deferredFunctions)
773792
# Set _deferredFunctions to None so that deferFunction will fail
774793
# noisily if called after we've run through the deferred functions.
@@ -1299,6 +1318,7 @@ def handleDoctests(self, node):
12991318
self.popScope()
13001319
self.scopeStack = saved_stack
13011320

1321+
@in_annotation
13021322
def handleStringAnnotation(self, s, node, ref_lineno, ref_col_offset, err):
13031323
try:
13041324
tree = ast.parse(s)
@@ -1322,6 +1342,7 @@ def handleStringAnnotation(self, s, node, ref_lineno, ref_col_offset, err):
13221342

13231343
self.handleNode(parsed_annotation, node)
13241344

1345+
@in_annotation
13251346
def handleAnnotation(self, annotation, node):
13261347
if isinstance(annotation, ast.Str):
13271348
# Defer handling forward annotation.
@@ -1334,7 +1355,8 @@ def handleAnnotation(self, annotation, node):
13341355
messages.ForwardAnnotationSyntaxError,
13351356
))
13361357
elif self.annotationsFutureEnabled:
1337-
self.deferFunction(lambda: self.handleNode(annotation, node))
1358+
fn = in_annotation(Checker.handleNode)
1359+
self.deferFunction(lambda: fn(self, annotation, node))
13381360
else:
13391361
self.handleNode(annotation, node)
13401362

@@ -1350,9 +1372,19 @@ def ignore(self, node):
13501372

13511373
# "expr" type nodes
13521374
BOOLOP = UNARYOP = IFEXP = SET = \
1353-
REPR = ATTRIBUTE = SUBSCRIPT = \
1375+
REPR = ATTRIBUTE = \
13541376
STARRED = NAMECONSTANT = NAMEDEXPR = handleChildren
13551377

1378+
def SUBSCRIPT(self, node):
1379+
if _is_typing(node.value, 'Literal', self.scopeStack):
1380+
orig, self._in_typing_literal = self._in_typing_literal, True
1381+
try:
1382+
self.handleChildren(node)
1383+
finally:
1384+
self._in_typing_literal = orig
1385+
else:
1386+
self.handleChildren(node)
1387+
13561388
def _handle_string_dot_format(self, node):
13571389
try:
13581390
placeholders = tuple(parse_format_string(node.func.value.s))
@@ -1593,7 +1625,27 @@ def BINOP(self, node):
15931625
self._handle_percent_format(node)
15941626
self.handleChildren(node)
15951627

1596-
NUM = STR = BYTES = ELLIPSIS = CONSTANT = ignore
1628+
def STR(self, node):
1629+
if self._in_annotation and not self._in_typing_literal:
1630+
fn = functools.partial(
1631+
self.handleStringAnnotation,
1632+
node.s,
1633+
node,
1634+
node.lineno,
1635+
node.col_offset,
1636+
messages.ForwardAnnotationSyntaxError,
1637+
)
1638+
if self._in_deferred:
1639+
fn()
1640+
else:
1641+
self.deferFunction(fn)
1642+
1643+
if PY38_PLUS:
1644+
def CONSTANT(self, node):
1645+
if isinstance(node.value, str):
1646+
return self.STR(node)
1647+
else:
1648+
NUM = BYTES = ELLIPSIS = CONSTANT = ignore
15971649

15981650
# "slice" type nodes
15991651
SLICE = EXTSLICE = INDEX = handleChildren

pyflakes/test/test_type_annotations.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def g(s):
4242
def test_typingExtensionsOverload(self):
4343
"""Allow intentional redefinitions via @typing_extensions.overload"""
4444
self.flakes("""
45+
import typing_extensions
4546
from typing_extensions import overload
4647
4748
@overload
@@ -54,6 +55,17 @@ def f(s): # type: (int) -> int
5455
5556
def f(s):
5657
return s
58+
59+
@typing_extensions.overload
60+
def g(s): # type: (None) -> None
61+
pass
62+
63+
@typing_extensions.overload
64+
def g(s): # type: (int) -> int
65+
pass
66+
67+
def g(s):
68+
return s
5769
""")
5870

5971
@skipIf(version_info < (3, 5), 'new in Python 3.5')
@@ -426,3 +438,64 @@ def test_positional_only_argument_annotations(self):
426438
427439
def f(c: C, /): ...
428440
""")
441+
442+
@skipIf(version_info < (3,), 'new in Python 3')
443+
def test_partially_quoted_type_annotation(self):
444+
self.flakes("""
445+
from queue import Queue
446+
from typing import Optional
447+
448+
def f() -> Optional['Queue[str]']:
449+
return None
450+
""")
451+
452+
@skipIf(version_info < (3,), 'new in Python 3')
453+
def test_literal_type_typing(self):
454+
self.flakes("""
455+
from typing import Literal
456+
457+
def f(x: Literal['some string']) -> None:
458+
return None
459+
""")
460+
461+
@skipIf(version_info < (3,), 'new in Python 3')
462+
def test_literal_type_typing_extensions(self):
463+
self.flakes("""
464+
from typing_extensions import Literal
465+
466+
def f(x: Literal['some string']) -> None:
467+
return None
468+
""")
469+
470+
@skipIf(version_info < (3,), 'new in Python 3')
471+
def test_literal_union_type_typing(self):
472+
self.flakes("""
473+
from typing import Literal
474+
475+
def f(x: Literal['some string', 'foo bar']) -> None:
476+
return None
477+
""")
478+
479+
@skipIf(version_info < (3,), 'new in Python 3')
480+
def test_deferred_twice_annotation(self):
481+
self.flakes("""
482+
from queue import Queue
483+
from typing import Optional
484+
485+
486+
def f() -> "Optional['Queue[str]']":
487+
return None
488+
""")
489+
490+
@skipIf(version_info < (3, 7), 'new in Python 3.7')
491+
def test_partial_string_annotations_with_future_annotations(self):
492+
self.flakes("""
493+
from __future__ import annotations
494+
495+
from queue import Queue
496+
from typing import Optional
497+
498+
499+
def f() -> Optional['Queue[str]']:
500+
return None
501+
""")

0 commit comments

Comments
 (0)