Skip to content

Commit 94b4cfd

Browse files
JukkaLgvanrossum
authored andcommitted
Fix crash with forward reference in TypedDict (#3560)
Move join to happen after semantic analysis to avoid crash due to incomplete MRO in TypeInfo. The implementation uses a new semantic analysis 'patch' phase. Fixes #3319. Fixes #2489. Fixes #3316.
1 parent 100f978 commit 94b4cfd

File tree

4 files changed

+70
-5
lines changed

4 files changed

+70
-5
lines changed

mypy/build.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from os.path import dirname, basename
2323

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

16531653
def semantic_analysis(self) -> None:
1654+
patches = [] # type: List[Callable[[], None]]
16541655
with self.wrap_context():
1655-
self.manager.semantic_analyzer.visit_file(self.tree, self.xpath, self.options)
1656+
self.manager.semantic_analyzer.visit_file(self.tree, self.xpath, self.options, patches)
1657+
self.patches = patches
16561658

16571659
def semantic_analysis_pass_three(self) -> None:
16581660
with self.wrap_context():
16591661
self.manager.semantic_analyzer_pass3.visit_file(self.tree, self.xpath, self.options)
16601662
if self.options.dump_type_stats:
16611663
dump_type_stats(self.tree, self.xpath)
16621664

1665+
def semantic_analysis_apply_patches(self) -> None:
1666+
for patch_func in self.patches:
1667+
patch_func()
1668+
16631669
def type_check_first_pass(self) -> None:
16641670
manager = self.manager
16651671
if self.options.semantic_analysis_only:
@@ -2095,6 +2101,8 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
20952101
graph[id].semantic_analysis_pass_three()
20962102
for id in fresh:
20972103
graph[id].calculate_mros()
2104+
for id in stale:
2105+
graph[id].semantic_analysis_apply_patches()
20982106
for id in stale:
20992107
graph[id].type_check_first_pass()
21002108
more = True

mypy/semanal.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,20 @@ def __init__(self,
261261
self.all_exports = set() # type: Set[str]
262262
self.plugin = plugin
263263

264-
def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None:
264+
def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
265+
patches: List[Callable[[], None]]) -> None:
266+
"""Run semantic analysis phase 2 over a file.
267+
268+
Add callbacks by mutating the patches list argument. They will be called
269+
after all semantic analysis phases but before type checking.
270+
"""
265271
self.options = options
266272
self.errors.set_file(fnam, file_node.fullname())
267273
self.cur_mod_node = file_node
268274
self.cur_mod_id = file_node.fullname()
269275
self.is_stub_file = fnam.lower().endswith('.pyi')
270276
self.globals = file_node.names
277+
self.patches = patches
271278

272279
if 'builtins' in self.modules:
273280
self.globals['__builtins__'] = SymbolTableNode(
@@ -294,6 +301,7 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options) -> None:
294301
g.module_public = False
295302

296303
del self.options
304+
del self.patches
297305

298306
def refresh_partial(self, node: Union[MypyFile, FuncItem]) -> None:
299307
"""Refresh a stale target in fine-grained incremental mode."""
@@ -2373,11 +2381,19 @@ def fail_typeddict_arg(self, message: str,
23732381

23742382
def build_typeddict_typeinfo(self, name: str, items: List[str],
23752383
types: List[Type]) -> TypeInfo:
2376-
mapping_value_type = join.join_type_list(types)
23772384
fallback = (self.named_type_or_none('typing.Mapping',
2378-
[self.str_type(), mapping_value_type])
2385+
[self.str_type(), self.object_type()])
23792386
or self.object_type())
23802387

2388+
def patch() -> None:
2389+
# Calculate the correct value type for the fallback Mapping.
2390+
fallback.args[1] = join.join_type_list(types)
2391+
2392+
# We can't calculate the complete fallback type until after semantic
2393+
# analysis, since otherwise MROs might be incomplete. Postpone a callback
2394+
# function that patches the fallback.
2395+
self.patches.append(patch)
2396+
23812397
info = self.basic_new_typeinfo(name, fallback)
23822398
info.typeddict_type = TypedDictType(OrderedDict(zip(items, types)), fallback)
23832399

mypy/server/update.py

Lines changed: 1 addition & 0 deletions
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_apply_patches()
150151
state.type_check_first_pass()
151152
# TODO: state.type_check_second_pass()?
152153
state.finish_passes()

test-data/unit/check-typeddict.test

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,3 +813,43 @@ 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 testForwardReferenceInClassTypedDict]
833+
from typing import Mapping
834+
from mypy_extensions import TypedDict
835+
class X(TypedDict):
836+
b: 'B'
837+
c: 'C'
838+
class B: pass
839+
class C(B): pass
840+
x: X
841+
reveal_type(x) # E: Revealed type is 'TypedDict(b=__main__.B, c=__main__.C, _fallback=__main__.X)'
842+
m1: Mapping[str, B] = x
843+
m2: Mapping[str, C] = x # E: Incompatible types in assignment (expression has type "X", variable has type Mapping[str, C])
844+
[builtins fixtures/dict.pyi]
845+
846+
[case testForwardReferenceToTypedDictInTypedDict]
847+
from typing import Mapping
848+
from mypy_extensions import TypedDict
849+
# Forward references don't quite work yet
850+
X = TypedDict('X', {'a': 'A'}) # E: Invalid type "__main__.A"
851+
A = TypedDict('A', {'b': int})
852+
x: X
853+
reveal_type(x) # E: Revealed type is 'TypedDict(a=TypedDict(b=builtins.int, _fallback=__main__.A), _fallback=__main__.X)'
854+
reveal_type(x['a']['b']) # E: Revealed type is 'builtins.int'
855+
[builtins fixtures/dict.pyi]

0 commit comments

Comments
 (0)