Skip to content

Commit 3c39bd0

Browse files
committed
Fix crash with forward reference in TypedDict
Move join to happen after semantic analysis to avoid crash due to incomplete MRO in TypeInfo. The implementation uses a new semantic analysis 'fixup' phase. Fixes #3319.
1 parent b073eaa commit 3c39bd0

File tree

4 files changed

+56
-5
lines changed

4 files changed

+56
-5
lines changed

mypy/build.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from os.path import dirname, basename
2222

2323
from typing import (AbstractSet, Dict, Iterable, Iterator, List,
24-
NamedTuple, Optional, Set, Tuple, Union)
24+
NamedTuple, Optional, Set, Tuple, Union, Callable)
2525
# Can't use TYPE_CHECKING because it's not in the Python 3.5.1 stdlib
2626
MYPY = False
2727
if MYPY:
@@ -1601,15 +1601,21 @@ def parse_file(self) -> None:
16011601
self.check_blockers()
16021602

16031603
def semantic_analysis(self) -> None:
1604+
fixups = [] # type: List[Callable[[], None]]
16041605
with self.wrap_context():
1605-
self.manager.semantic_analyzer.visit_file(self.tree, self.xpath, self.options)
1606+
self.manager.semantic_analyzer.visit_file(self.tree, self.xpath, self.options, fixups)
1607+
self.fixups = fixups
16061608

16071609
def semantic_analysis_pass_three(self) -> None:
16081610
with self.wrap_context():
16091611
self.manager.semantic_analyzer_pass3.visit_file(self.tree, self.xpath, self.options)
16101612
if self.options.dump_type_stats:
16111613
dump_type_stats(self.tree, self.xpath)
16121614

1615+
def semantic_analysis_fixups(self) -> None:
1616+
for fixup in self.fixups:
1617+
fixup()
1618+
16131619
def type_check_first_pass(self) -> None:
16141620
manager = self.manager
16151621
if self.options.semantic_analysis_only:
@@ -2043,6 +2049,8 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
20432049
graph[id].semantic_analysis_pass_three()
20442050
for id in fresh:
20452051
graph[id].calculate_mros()
2052+
for id in stale:
2053+
graph[id].semantic_analysis_fixups()
20462054
for id in stale:
20472055
graph[id].type_check_first_pass()
20482056
more = True

mypy/semanal.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -257,13 +257,20 @@ def __init__(self,
257257
self.postponed_functions_stack = []
258258
self.all_exports = set() # type: Set[str]
259259

260-
def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None:
260+
def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
261+
fixups: List[Callable[[], None]]) -> None:
262+
"""Run semantic analysis phase 2 over a file.
263+
264+
Add callbacks by mutating the fixups list argument. They will be called
265+
after all semantic analysis phases but before type checking.
266+
"""
261267
self.options = options
262268
self.errors.set_file(fnam, file_node.fullname())
263269
self.cur_mod_node = file_node
264270
self.cur_mod_id = file_node.fullname()
265271
self.is_stub_file = fnam.lower().endswith('.pyi')
266272
self.globals = file_node.names
273+
self.fixups = fixups
267274

268275
if 'builtins' in self.modules:
269276
self.globals['__builtins__'] = SymbolTableNode(
@@ -290,6 +297,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None:
290297
g.module_public = False
291298

292299
del self.options
300+
del self.fixups
293301

294302
def refresh_partial(self, node: Union[MypyFile, FuncItem]) -> None:
295303
"""Refresh a stale target in fine-grained incremental mode."""
@@ -2366,11 +2374,19 @@ def fail_typeddict_arg(self, message: str,
23662374

23672375
def build_typeddict_typeinfo(self, name: str, items: List[str],
23682376
types: List[Type]) -> TypeInfo:
2369-
mapping_value_type = join.join_type_list(types)
23702377
fallback = (self.named_type_or_none('typing.Mapping',
2371-
[self.str_type(), mapping_value_type])
2378+
[self.str_type(), self.object_type()])
23722379
or self.object_type())
23732380

2381+
def fixup() -> None:
2382+
mapping_value_type = join.join_type_list(types)
2383+
fallback.args[1] = mapping_value_type
2384+
2385+
# We can't calculate the complete fallback type until after semantic
2386+
# analysis, since otherwise MROs might be incomplete. Postpone a fixup
2387+
# function that patches the fallback.
2388+
self.fixups.append(fixup)
2389+
23742390
info = self.basic_new_typeinfo(name, fallback)
23752391
info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), fallback)
23762392

mypy/server/update.py

+1
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ def build_incremental_step(manager: BuildManager,
147147
# TODO: state.fix_suppressed_dependencies()?
148148
state.semantic_analysis()
149149
state.semantic_analysis_pass_three()
150+
# TODO: state.semantic_analysis_fixups()
150151
state.type_check_first_pass()
151152
# TODO: state.type_check_second_pass()?
152153
state.finish_passes()

test-data/unit/check-typeddict.test

+26
Original file line numberDiff line numberDiff line change
@@ -813,3 +813,29 @@ p = TaggedPoint(type='2d', x=42, y=1337)
813813
p.get('x', 1 + 'y') # E: Unsupported operand types for + ("int" and "str")
814814
[builtins fixtures/dict.pyi]
815815
[typing fixtures/typing-full.pyi]
816+
817+
818+
-- Special cases
819+
820+
[case testForwardReferenceInTypedDict]
821+
from typing import Mapping
822+
from mypy_extensions import TypedDict
823+
X = TypedDict('X', {'b': 'B', 'c': 'C'})
824+
class B: pass
825+
class C(B): pass
826+
x: X
827+
reveal_type(x) # E: Revealed type is 'TypedDict(b=__main__.B, c=__main__.C, _fallback=__main__.X)'
828+
m1: Mapping[str, B] = x
829+
m2: Mapping[str, C] = x # E: Incompatible types in assignment (expression has type "X", variable has type Mapping[str, C])
830+
[builtins fixtures/dict.pyi]
831+
832+
[case testForwardReferenceToTypedDictInTypedDict]
833+
from typing import Mapping
834+
from mypy_extensions import TypedDict
835+
# Forward references don't quite work yet
836+
X = TypedDict('X', {'a': 'A'}) # E: Invalid type "__main__.A"
837+
A = TypedDict('A', {'b': int})
838+
x: X
839+
reveal_type(x) # E: Revealed type is 'TypedDict(a=TypedDict(b=builtins.int, _fallback=__main__.A), _fallback=__main__.X)'
840+
reveal_type(x['a']['b']) # E: Revealed type is 'builtins.int'
841+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)