Skip to content

Commit 792bcdc

Browse files
authored
Update imported Vars in the third pass (#5635)
Fixes #5275 Fixes #4498 Fixes #4442 This is a simple _band-aid_ fix for `Invalid type` in import cycles where type aliases, named tuples, or typed dicts appear. Note that this is a partial fix that only fixes the `Invalid type` error when a type alias etc. appears in type context. This doesn't fix errors (e.g. `Cannot determine type of X`) that may appear if the type alias etc. appear in runtime context. The motivation for partial fix is two-fold: * The error often appears in stub files (especially for large libraries/frameworks) where we have more import cycles, but no runtime context at all. * Ideally we should refactor semantic analysis to have deferred nodes, and process them in smaller passes when there is more info (like we do in type checking phase). But this is _much_ harder since this requires a large refactoring. Also an alternative fix of updating node of every `NameExpr` and `MemberExpr` in third pass is costly from performance point of view, and still nontrivial.
1 parent 1bc1047 commit 792bcdc

File tree

5 files changed

+186
-0
lines changed

5 files changed

+186
-0
lines changed

mypy/semanal_pass3.py

+43
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,55 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
6767
self.sem.globals = file_node.names
6868
with experiments.strict_optional_set(options.strict_optional):
6969
self.scope.enter_file(file_node.fullname())
70+
self.update_imported_vars()
7071
self.accept(file_node)
7172
self.analyze_symbol_table(file_node.names)
7273
self.scope.leave()
7374
del self.cur_mod_node
7475
self.patches = []
7576

77+
def update_imported_vars(self) -> None:
78+
"""Update nodes for imported names, if they got updated from Var to TypeInfo or TypeAlias.
79+
80+
This is a simple _band-aid_ fix for "Invalid type" error in import cycles where type
81+
aliases, named tuples, or typed dicts appear. The root cause is that during first pass
82+
definitions like:
83+
84+
A = List[int]
85+
86+
are seen by mypy as variables, because it doesn't know yet that `List` refers to a type.
87+
In the second pass, such `Var` is replaced with a `TypeAlias`. But in import cycle,
88+
import of `A` will still refer to the old `Var` node. Therefore we need to update it.
89+
90+
Note that this is a partial fix that only fixes the "Invalid type" error when a type alias
91+
etc. appears in type context. This doesn't fix errors (e.g. "Cannot determine type of A")
92+
that may appear if the type alias etc. appear in runtime context.
93+
94+
The motivation for partial fix is two-fold:
95+
* The "Invalid type" error often appears in stub files (especially for large
96+
libraries/frameworks) where we have more import cycles, but no runtime
97+
context at all.
98+
* Ideally we should refactor semantic analysis to have deferred nodes, and process
99+
them in smaller passes when there is more info (like we do in type checking phase).
100+
But this is _much_ harder since this requires a large refactoring. Also an alternative
101+
fix of updating node of every `NameExpr` and `MemberExpr` in third pass is costly
102+
from performance point of view, and still nontrivial.
103+
"""
104+
for sym in self.cur_mod_node.names.values():
105+
if sym and isinstance(sym.node, Var):
106+
fullname = sym.node.fullname()
107+
if '.' not in fullname:
108+
continue
109+
mod_name, _, name = fullname.rpartition('.')
110+
if mod_name not in self.sem.modules:
111+
continue
112+
if mod_name != self.sem.cur_mod_id: # imported
113+
new_sym = self.sem.modules[mod_name].names.get(name)
114+
if new_sym and isinstance(new_sym.node, (TypeInfo, TypeAlias)):
115+
# This Var was replaced with a class (like named tuple)
116+
# or alias, update this.
117+
sym.node = new_sym.node
118+
76119
def refresh_partial(self, node: Union[MypyFile, FuncDef, OverloadedFuncDef],
77120
patches: List[Tuple[int, Callable[[], None]]]) -> None:
78121
"""Refresh a stale target in fine-grained incremental mode."""

test-data/unit/check-incremental.test

+44
Original file line numberDiff line numberDiff line change
@@ -5123,6 +5123,50 @@ def outer() -> None:
51235123
[out]
51245124
[out2]
51255125

5126+
[case testRecursiveAliasImported]
5127+
import a
5128+
[file a.py]
5129+
import lib
5130+
x: int
5131+
[file a.py.2]
5132+
import lib
5133+
x: lib.A
5134+
reveal_type(x)
5135+
[file lib.pyi]
5136+
from typing import List
5137+
from other import B
5138+
A = List[B] # type: ignore
5139+
[file other.pyi]
5140+
from typing import List
5141+
from lib import A
5142+
B = List[A]
5143+
[builtins fixtures/list.pyi]
5144+
[out]
5145+
[out2]
5146+
tmp/a.py:3: error: Revealed type is 'builtins.list[builtins.list[builtins.list[Any]]]'
5147+
5148+
[case testRecursiveNamedTupleTypedDict]
5149+
import a
5150+
[file a.py]
5151+
import lib
5152+
x: int
5153+
[file a.py.2]
5154+
import lib
5155+
x: lib.A
5156+
reveal_type(x.x['x'])
5157+
[file lib.pyi]
5158+
from typing import NamedTuple
5159+
from other import B
5160+
A = NamedTuple('A', [('x', B)]) # type: ignore
5161+
[file other.pyi]
5162+
from mypy_extensions import TypedDict
5163+
from lib import A
5164+
B = TypedDict('B', {'x': A})
5165+
[builtins fixtures/dict.pyi]
5166+
[out]
5167+
[out2]
5168+
tmp/a.py:3: error: Revealed type is 'Tuple[TypedDict('other.B', {'x': Any}), fallback=lib.A]'
5169+
51265170
[case testFollowImportSkipNotInvalidatedOnPresent]
51275171
# flags: --follow-imports=skip
51285172
# cmd: mypy -m main

test-data/unit/check-namedtuple.test

+22
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,28 @@ my_eval(A([B(1), B(2)])) # OK
691691
[builtins fixtures/isinstancelist.pyi]
692692
[out]
693693

694+
[case testNamedTupleImportCycle]
695+
import b
696+
[file a.py]
697+
class C:
698+
pass
699+
700+
from b import tp
701+
x: tp
702+
reveal_type(x.x) # E: Revealed type is 'builtins.int'
703+
704+
# Unfortunately runtime part doesn't work yet, see docstring in SemanticAnalyzerPass3.update_imported_vars()
705+
reveal_type(tp) # E: Revealed type is 'Any' \
706+
# E: Cannot determine type of 'tp'
707+
tp('x') # E: Cannot determine type of 'tp'
708+
709+
[file b.py]
710+
from a import C
711+
from typing import NamedTuple
712+
713+
tp = NamedTuple('tp', [('x', int)])
714+
[out]
715+
694716
[case testSubclassOfRecursiveNamedTuple]
695717
from typing import List, NamedTuple
696718

test-data/unit/check-type-aliases.test

+54
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,60 @@ reveal_type(D().meth(1)) # E: Revealed type is 'Union[__main__.D*, builtins.int
427427
[builtins fixtures/classmethod.pyi]
428428
[out]
429429

430+
[case testAliasInImportCycle]
431+
# cmd: mypy -m t t2
432+
[file t.py]
433+
MYPY = False
434+
if MYPY:
435+
from t2 import A
436+
x: A
437+
[file t2.py]
438+
import t
439+
from typing import Callable
440+
A = Callable[[], None]
441+
[builtins fixtures/bool.pyi]
442+
[out]
443+
444+
[case testAliasInImportCycle2]
445+
import a
446+
[file a.pyi]
447+
from b import Parameter
448+
449+
class _ParamType:
450+
p: Parameter
451+
452+
_ConvertibleType = _ParamType
453+
454+
def convert_type(ty: _ConvertibleType):
455+
...
456+
457+
[file b.pyi]
458+
from a import _ConvertibleType
459+
460+
class Parameter:
461+
type: _ConvertibleType
462+
[out]
463+
464+
[case testAliasInImportCycle3]
465+
# cmd: mypy -m t t2
466+
[file t.py]
467+
MYPY = False
468+
if MYPY:
469+
from t2 import A
470+
x: A
471+
reveal_type(x) # E: Revealed type is 't2.D'
472+
473+
# Unfortunately runtime part doesn't work yet, see docstring in SemanticAnalyzerPass3.update_imported_vars()
474+
reveal_type(A) # E: Revealed type is 'Any' \
475+
# E: Cannot determine type of 'A'
476+
A() # E: Cannot determine type of 'A'
477+
[file t2.py]
478+
import t
479+
class D: pass
480+
A = D
481+
[builtins fixtures/bool.pyi]
482+
[out]
483+
430484
[case testFlexibleAlias1]
431485
from typing import TypeVar, List, Tuple
432486
from mypy_extensions import FlexibleAlias

test-data/unit/check-typeddict.test

+23
Original file line numberDiff line numberDiff line change
@@ -1390,3 +1390,26 @@ def f(x: a.N) -> None:
13901390
[out]
13911391
tmp/b.py:4: error: Revealed type is 'TypedDict('a.N', {'a': builtins.str})'
13921392
tmp/b.py:5: error: Revealed type is 'builtins.str'
1393+
1394+
[case testTypedDictImportCycle]
1395+
import b
1396+
[file a.py]
1397+
class C:
1398+
pass
1399+
1400+
from b import tp
1401+
x: tp
1402+
reveal_type(x['x']) # E: Revealed type is 'builtins.int'
1403+
1404+
# Unfortunately runtime part doesn't work yet, see docstring in SemanticAnalyzerPass3.update_imported_vars()
1405+
reveal_type(tp) # E: Revealed type is 'Any' \
1406+
# E: Cannot determine type of 'tp'
1407+
tp('x') # E: Cannot determine type of 'tp'
1408+
1409+
[file b.py]
1410+
from a import C
1411+
from mypy_extensions import TypedDict
1412+
1413+
tp = TypedDict('tp', {'x': int})
1414+
[builtins fixtures/dict.pyi]
1415+
[out]

0 commit comments

Comments
 (0)