Skip to content

Commit fc2f577

Browse files
author
Guido van Rossum
committed
Add a new, lower priority for imports inside "if MYPY"
1 parent 68c6e96 commit fc2f577

File tree

3 files changed

+67
-20
lines changed

3 files changed

+67
-20
lines changed

mypy/build.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from typing import (AbstractSet, Dict, Iterable, Iterator, List,
2525
NamedTuple, Optional, Set, Tuple, Union)
2626

27-
from mypy.nodes import (MypyFile, Import, ImportFrom, ImportAll)
27+
from mypy.types import Type
28+
from mypy.nodes import (MypyFile, Node, ImportBase, Import, ImportFrom, ImportAll)
2829
from mypy.semanal import FirstPass, SemanticAnalyzer, ThirdPass
2930
from mypy.checker import TypeChecker
3031
from mypy.indirection import TypeIndirectionVisitor
@@ -307,9 +308,22 @@ def default_lib_path(data_dir: str, pyversion: Tuple[int, int]) -> List[str]:
307308
PRI_MED = 10 # top-level "import X"
308309
PRI_LOW = 20 # either form inside a function
309310
PRI_INDIRECT = 30 # an indirect dependency
311+
PRI_MYPY = 40 # inside "if MYPY" or "if typing.TYPE_CHECKING"
310312
PRI_ALL = 99 # include all priorities
311313

312314

315+
def import_priority(imp: ImportBase, toplevel_priority: int) -> int:
316+
"""Compute import priority from an import node."""
317+
if imp.is_mypy_only:
318+
# Inside "if MYPY" or "if typing.TYPE_CHECKING"
319+
return PRI_MYPY
320+
if not imp.is_top_level:
321+
# Inside a function
322+
return PRI_LOW
323+
# A regular import; priority determined by argument.
324+
return toplevel_priority
325+
326+
313327
# TODO: Get rid of all_types. It's not used except for one log message.
314328
# Maybe we could instead publish a map from module ID to its type_map.
315329
class BuildManager:
@@ -395,20 +409,21 @@ def correct_rel_imp(imp: Union[ImportFrom, ImportAll]) -> str:
395409
for imp in file.imports:
396410
if not imp.is_unreachable:
397411
if isinstance(imp, Import):
398-
pri = PRI_MED if imp.is_top_level else PRI_LOW
412+
pri = import_priority(imp, PRI_MED)
413+
ancestor_pri = import_priority(imp, PRI_LOW)
399414
for id, _ in imp.ids:
400415
ancestor_parts = id.split(".")[:-1]
401416
ancestors = []
402417
for part in ancestor_parts:
403418
ancestors.append(part)
404-
res.append((PRI_LOW, ".".join(ancestors), imp.line))
419+
res.append((ancestor_pri, ".".join(ancestors), imp.line))
405420
res.append((pri, id, imp.line))
406421
elif isinstance(imp, ImportFrom):
407422
cur_id = correct_rel_imp(imp)
408423
pos = len(res)
409424
all_are_submodules = True
410425
# Also add any imported names that are submodules.
411-
pri = PRI_MED if imp.is_top_level else PRI_LOW
426+
pri = import_priority(imp, PRI_MED)
412427
for name, __ in imp.names:
413428
sub_id = cur_id + '.' + name
414429
if self.is_module(sub_id):
@@ -421,10 +436,10 @@ def correct_rel_imp(imp: Union[ImportFrom, ImportAll]) -> str:
421436
# cur_id is also a dependency, and we should
422437
# insert it *before* any submodules.
423438
if not all_are_submodules:
424-
pri = PRI_HIGH if imp.is_top_level else PRI_LOW
439+
pri = import_priority(imp, PRI_HIGH)
425440
res.insert(pos, ((pri, cur_id, imp.line)))
426441
elif isinstance(imp, ImportAll):
427-
pri = PRI_HIGH if imp.is_top_level else PRI_LOW
442+
pri = import_priority(imp, PRI_HIGH)
428443
res.append((pri, correct_rel_imp(imp), imp.line))
429444

430445
return res

mypy/nodes.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,11 @@ def deserialize(cls, data: JsonDict) -> 'MypyFile':
285285

286286
class ImportBase(Statement):
287287
"""Base class for all import statements."""
288-
is_unreachable = False
289-
is_top_level = False # Set by semanal.FirstPass
288+
289+
is_unreachable = False # Set by semanal.FirstPass if inside `if False` etc.
290+
is_top_level = False # Ditto if outside any class or def
291+
is_mypy_only = False # Ditto if inside `if TYPE_CHECKING` or `if MYPY`
292+
290293
# If an import replaces existing definitions, we construct dummy assignment
291294
# statements that assign the imported names to the names in the current scope,
292295
# for type checking purposes. Example:

mypy/semanal.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,20 @@
8484
T = TypeVar('T')
8585

8686

87-
# Inferred value of an expression.
88-
ALWAYS_TRUE = 0
89-
ALWAYS_FALSE = 1
90-
TRUTH_VALUE_UNKNOWN = 2
87+
# Inferred truth value of an expression.
88+
ALWAYS_TRUE = 1
89+
MYPY_TRUE = 2 # True in mypy, False at runtime
90+
ALWAYS_FALSE = 3
91+
MYPY_FALSE = 4 # False in mypy, True at runtime
92+
TRUTH_VALUE_UNKNOWN = 5
93+
94+
inverted_truth_mapping = {
95+
ALWAYS_TRUE: ALWAYS_FALSE,
96+
ALWAYS_FALSE: ALWAYS_TRUE,
97+
TRUTH_VALUE_UNKNOWN: TRUTH_VALUE_UNKNOWN,
98+
MYPY_TRUE: MYPY_FALSE,
99+
MYPY_FALSE: MYPY_TRUE,
100+
}
91101

92102
# Map from obsolete name to the current spelling.
93103
obsolete_name_mapping = {
@@ -3082,12 +3092,16 @@ def infer_reachability_of_if_statement(s: IfStmt,
30823092
platform: str) -> None:
30833093
for i in range(len(s.expr)):
30843094
result = infer_if_condition_value(s.expr[i], pyversion, platform)
3085-
if result == ALWAYS_FALSE:
3095+
if result in (ALWAYS_FALSE, MYPY_FALSE):
30863096
# The condition is always false, so we skip the if/elif body.
30873097
mark_block_unreachable(s.body[i])
3088-
elif result == ALWAYS_TRUE:
3098+
elif result in (ALWAYS_TRUE, MYPY_TRUE):
30893099
# This condition is always true, so all of the remaining
30903100
# elif/else bodies will never be executed.
3101+
if result == MYPY_TRUE:
3102+
# This condition is false at runtime; this will affect
3103+
# import priorities.
3104+
mark_block_mypy_only(s.body[i])
30913105
for body in s.body[i + 1:]:
30923106
mark_block_unreachable(body)
30933107
if s.else_body:
@@ -3099,7 +3113,8 @@ def infer_if_condition_value(expr: Expression, pyversion: Tuple[int, int], platf
30993113
"""Infer whether if condition is always true/false.
31003114
31013115
Return ALWAYS_TRUE if always true, ALWAYS_FALSE if always false,
3102-
and TRUTH_VALUE_UNKNOWN otherwise.
3116+
MYPY_TRUE if true under mypy and false at runtime, MYPY_FALSE if
3117+
false under mypy and true at runtime, else TRUTH_VALUE_UNKNOWN.
31033118
"""
31043119
name = ''
31053120
negated = False
@@ -3123,12 +3138,9 @@ def infer_if_condition_value(expr: Expression, pyversion: Tuple[int, int], platf
31233138
elif name == 'PY3':
31243139
result = ALWAYS_TRUE if pyversion[0] == 3 else ALWAYS_FALSE
31253140
elif name == 'MYPY' or name == 'TYPE_CHECKING':
3126-
result = ALWAYS_TRUE
3141+
result = MYPY_TRUE
31273142
if negated:
3128-
if result == ALWAYS_TRUE:
3129-
result = ALWAYS_FALSE
3130-
elif result == ALWAYS_FALSE:
3131-
result = ALWAYS_TRUE
3143+
result = inverted_truth_mapping[result]
31323144
return result
31333145

31343146

@@ -3303,6 +3315,23 @@ def visit_import_all(self, node: ImportAll) -> None:
33033315
node.is_unreachable = True
33043316

33053317

3318+
def mark_block_mypy_only(block: Block) -> None:
3319+
block.accept(MarkImportsMypyOnlyVisitor())
3320+
3321+
3322+
class MarkImportsMypyOnlyVisitor(TraverserVisitor):
3323+
"""Visitor that sets is_mypy_only (which affects priority)."""
3324+
3325+
def visit_import(self, node: Import) -> None:
3326+
node.is_mypy_only = True
3327+
3328+
def visit_import_from(self, node: ImportFrom) -> None:
3329+
node.is_mypy_only = True
3330+
3331+
def visit_import_all(self, node: ImportAll) -> None:
3332+
node.is_mypy_only = True
3333+
3334+
33063335
def is_identity_signature(sig: Type) -> bool:
33073336
"""Is type a callable of form T -> T (where T is a type variable)?"""
33083337
if isinstance(sig, CallableType) and sig.arg_kinds == [ARG_POS]:

0 commit comments

Comments
 (0)