Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle code guarded by 'if TYPE_CHECKING' correctly #530

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,17 @@ def _add_to_names(container):

class Scope(dict):
importStarred = False # set to True when import * is found
# Special key for checking whether a binding is defined only for type checking.
TYPE_CHECKING_ONLY = object()

def __init__(self):
super(Scope, self).__init__(self)
self[self.TYPE_CHECKING_ONLY] = collections.defaultdict(bool)

def items(self):
for key, val in super(Scope, self).items():
if key != self.TYPE_CHECKING_ONLY:
yield key, val

def __repr__(self):
scope_cls = self.__class__.__name__
Expand Down Expand Up @@ -829,6 +840,7 @@ class Checker(object):
_in_annotation = False
_in_typing_literal = False
_in_deferred = False
_in_type_checking = False

builtIns = set(builtin_vars).union(_MAGIC_GLOBALS)
_customBuiltIns = os.environ.get('PYFLAKES_BUILTINS')
Expand Down Expand Up @@ -1099,6 +1111,7 @@ def addBinding(self, node, value):
# then assume the rebound name is used as a global or within a loop
value.used = self.scope[value.name].used

self.scope[Scope.TYPE_CHECKING_ONLY][value.name] = self._in_type_checking
self.scope[value.name] = value

def _unknown_handler(self, node):
Expand Down Expand Up @@ -1165,6 +1178,11 @@ def handleNodeLoad(self, node):
scope[n.fullName].used = (self.scope, node)
except KeyError:
pass
if (self.scope[Scope.TYPE_CHECKING_ONLY][name]
and not self._in_annotation):
# Only defined during type-checking; this does not count. Real code
# (not an annotation) using this binding will not work.
continue
except KeyError:
pass
else:
Expand Down Expand Up @@ -1833,7 +1851,12 @@ def DICT(self, node):
def IF(self, node):
if isinstance(node.test, ast.Tuple) and node.test.elts != []:
self.report(messages.IfTuple, node)

prev = self._in_type_checking
if _is_typing(node.test, 'TYPE_CHECKING', self.scopeStack):
self._in_type_checking = True
self.handleChildren(node)
self._in_type_checking = prev

IFEXP = IF

Expand Down
29 changes: 29 additions & 0 deletions pyflakes/test/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,35 @@ def test_futureImportStar(self):
from __future__ import *
''', m.FutureFeatureNotDefined)

def test_ignoresTypingImports(self):
"""Ignores imports within 'if TYPE_CHECKING' checking normal code."""
self.flakes('''
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from a import b
b()
''', m.UndefinedName)

def test_ignoresTypingClassDefinitino(self):
"""Ignores definitions within 'if TYPE_CHECKING' checking normal code."""
self.flakes('''
from typing import TYPE_CHECKING
if TYPE_CHECKING:
class T:
...
t = T()
''', m.UndefinedName)

def test_usesTypingImportsForAnnotations(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, the new tests are using snake_case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""Uses imports within 'if TYPE_CHECKING' checking annotations."""
self.flakes('''
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from a import b
def f() -> "b":
pass
''')


class TestSpecialAll(TestCase):
"""
Expand Down