Skip to content

Commit 5c9e84c

Browse files
committed
Store mros in cache files in order to fix a really pernicious bug
1 parent a58dd5e commit 5c9e84c

File tree

4 files changed

+43
-41
lines changed

4 files changed

+43
-41
lines changed

mypy/build.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from mypy.util import DecodeError
4848
from mypy.report import Reports
4949
from mypy import moduleinfo
50-
from mypy.fixup import fixup_module_pass_one, fixup_module_pass_two
50+
from mypy.fixup import fixup_module
5151
from mypy.nodes import Expression
5252
from mypy.options import Options
5353
from mypy.parse import parse
@@ -1704,13 +1704,9 @@ def fix_cross_refs(self) -> None:
17041704
assert self.tree is not None, "Internal error: method must be called on parsed file only"
17051705
# We need to set quick_and_dirty when doing a fine grained
17061706
# cache load because we need to gracefully handle missing modules.
1707-
fixup_module_pass_one(self.tree, self.manager.modules,
1708-
self.manager.options.quick_and_dirty or
1709-
self.options.use_fine_grained_cache)
1710-
1711-
def calculate_mros(self) -> None:
1712-
assert self.tree is not None, "Internal error: method must be called on parsed file only"
1713-
fixup_module_pass_two(self.tree, self.manager.modules)
1707+
fixup_module(self.tree, self.manager.modules,
1708+
self.manager.options.quick_and_dirty or
1709+
self.options.use_fine_grained_cache)
17141710

17151711
def patch_dependency_parents(self) -> None:
17161712
"""
@@ -2600,8 +2596,6 @@ def process_fresh_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
26002596
graph[id].load_tree()
26012597
for id in scc:
26022598
graph[id].fix_cross_refs()
2603-
for id in scc:
2604-
graph[id].calculate_mros()
26052599
for id in scc:
26062600
graph[id].patch_dependency_parents()
26072601

@@ -2635,8 +2629,6 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
26352629
graph[id].semantic_analysis()
26362630
for id in stale:
26372631
graph[id].semantic_analysis_pass_three()
2638-
for id in fresh:
2639-
graph[id].calculate_mros()
26402632
for id in stale:
26412633
graph[id].semantic_analysis_apply_patches()
26422634
for id in stale:

mypy/fixup.py

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,12 @@
1919
# N.B: we do a quick_and_dirty fixup in both quick_and_dirty mode and
2020
# when fixing up a fine-grained incremental cache load (since there may
2121
# be cross-refs into deleted modules)
22-
def fixup_module_pass_one(tree: MypyFile, modules: Dict[str, MypyFile],
23-
quick_and_dirty: bool) -> None:
22+
def fixup_module(tree: MypyFile, modules: Dict[str, MypyFile],
23+
quick_and_dirty: bool) -> None:
2424
node_fixer = NodeFixer(modules, quick_and_dirty)
2525
node_fixer.visit_symbol_table(tree.names)
2626

2727

28-
def fixup_module_pass_two(tree: MypyFile, modules: Dict[str, MypyFile]) -> None:
29-
compute_all_mros(tree.names, modules)
30-
31-
32-
def compute_all_mros(symtab: SymbolTable, modules: Dict[str, MypyFile]) -> None:
33-
for key, value in symtab.items():
34-
if value.kind in (LDEF, MDEF, GDEF) and isinstance(value.node, TypeInfo):
35-
info = value.node
36-
info.calculate_mro()
37-
assert info.mro, "No MRO calculated for %s" % (info.fullname(),)
38-
compute_all_mros(info.names, modules)
39-
40-
4128
# TODO: Fix up .info when deserializing, i.e. much earlier.
4229
class NodeFixer(NodeVisitor[None]):
4330
current_info = None # type: Optional[TypeInfo]
@@ -69,6 +56,10 @@ def visit_type_info(self, info: TypeInfo) -> None:
6956
info.declared_metaclass.accept(self.type_fixer)
7057
if info.metaclass_type:
7158
info.metaclass_type.accept(self.type_fixer)
59+
if info._mro_refs:
60+
info.mro = [lookup_qualified_typeinfo(self.modules, name, self.quick_and_dirty)
61+
for name in info._mro_refs]
62+
info._mro_refs = None
7263
finally:
7364
self.current_info = save_info
7465

@@ -162,18 +153,12 @@ def visit_instance(self, inst: Instance) -> None:
162153
if type_ref is None:
163154
return # We've already been here.
164155
del inst.type_ref
165-
node = lookup_qualified(self.modules, type_ref, self.quick_and_dirty)
166-
if isinstance(node, TypeInfo):
167-
inst.type = node
168-
# TODO: Is this needed or redundant?
169-
# Also fix up the bases, just in case.
170-
for base in inst.type.bases:
171-
if base.type is NOT_READY:
172-
base.accept(self)
173-
else:
174-
# Looks like a missing TypeInfo in quick mode, put something there
175-
assert self.quick_and_dirty, "Should never get here in normal mode"
176-
inst.type = stale_info(self.modules)
156+
inst.type = lookup_qualified_typeinfo(self.modules, type_ref, self.quick_and_dirty)
157+
# TODO: Is this needed or redundant?
158+
# Also fix up the bases, just in case.
159+
for base in inst.type.bases:
160+
if base.type is NOT_READY:
161+
base.accept(self)
177162
for a in inst.args:
178163
a.accept(self)
179164

@@ -251,6 +236,17 @@ def visit_type_type(self, t: TypeType) -> None:
251236
t.item.accept(self)
252237

253238

239+
def lookup_qualified_typeinfo(modules: Dict[str, MypyFile], name: str,
240+
quick_and_dirty: bool) -> TypeInfo:
241+
node = lookup_qualified(modules, name, quick_and_dirty)
242+
if isinstance(node, TypeInfo):
243+
return node
244+
else:
245+
# Looks like a missing TypeInfo in quick mode, put something there
246+
assert quick_and_dirty, "Should never get here in normal mode"
247+
return stale_info(modules)
248+
249+
254250
def lookup_qualified(modules: Dict[str, MypyFile], name: str,
255251
quick_and_dirty: bool) -> Optional[SymbolNode]:
256252
stnode = lookup_qualified_stnode(modules, name, quick_and_dirty)

mypy/nodes.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1960,6 +1960,9 @@ class is generic then it will be a type constructor of higher kind.
19601960
# Method Resolution Order: the order of looking up attributes. The first
19611961
# value always to refers to this class.
19621962
mro = None # type: List[TypeInfo]
1963+
# Used to stash the names of the mro classes temporarily between
1964+
# deserialization and fixup. See deserialize() for why.
1965+
_mro_refs = None # type: Optional[List[str]]
19631966

19641967
declared_metaclass = None # type: Optional[mypy.types.Instance]
19651968
metaclass_type = None # type: Optional[mypy.types.Instance]
@@ -2276,6 +2279,7 @@ def serialize(self) -> JsonDict:
22762279
'protocol_members': self.protocol_members,
22772280
'type_vars': self.type_vars,
22782281
'bases': [b.serialize() for b in self.bases],
2282+
'mro': [c.fullname() for c in self.mro],
22792283
'_promote': None if self._promote is None else self._promote.serialize(),
22802284
'declared_metaclass': (None if self.declared_metaclass is None
22812285
else self.declared_metaclass.serialize()),
@@ -2307,7 +2311,17 @@ def deserialize(cls, data: JsonDict) -> 'TypeInfo':
23072311
else mypy.types.Instance.deserialize(data['declared_metaclass']))
23082312
ti.metaclass_type = (None if data['metaclass_type'] is None
23092313
else mypy.types.Instance.deserialize(data['metaclass_type']))
2310-
# NOTE: ti.mro will be set in the fixup phase.
2314+
# NOTE: ti.mro will be set in the fixup phase based on these
2315+
# names. The reason we need to store the mro instead of just
2316+
# recomputing it from base classes has to do with a subtle
2317+
# point about fine-grained incremental: the cache files might
2318+
# not be loaded until after a class in the mro has changed its
2319+
# bases, which causes the mro to change. If we recomputed our
2320+
# mro, we would compute the *new* mro, which leaves us with no
2321+
# way to detact that the mro has changed! Thus we need to make
2322+
# sure to load the original mro so that once the class is
2323+
# rechecked, it can tell that the mro has changed.
2324+
ti._mro_refs = data['mro']
23112325
ti.tuple_type = (None if data['tuple_type'] is None
23122326
else mypy.types.TupleType.deserialize(data['tuple_type']))
23132327
ti.typeddict_type = (None if data['typeddict_type'] is None

test-data/unit/fine-grained.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5504,7 +5504,7 @@ from typing import overload
55045504
==
55055505
main:3: error: Module has no attribute "f"
55065506

5507-
[case testOverloadsUpdatedTypeRecheckImplementation-skip]
5507+
[case testOverloadsUpdatedTypeRecheckImplementation]
55085508
from typing import overload
55095509
import mod
55105510
class Outer:

0 commit comments

Comments
 (0)