Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
MypyFile,
OverloadedFuncDef,
SymbolTable,
TypeInfo,
)
from mypy.options import OPTIONS_AFFECTING_CACHE_NO_PLATFORM
from mypy.partially_defined import PossiblyUndefinedVariableVisitor
Expand Down Expand Up @@ -3483,17 +3484,20 @@ def finish_passes(self) -> None:
if options.export_types:
manager.all_types.update(self.type_map())

# Possible sources of indirect dependencies:
# * Symbols not directly imported in this module but accessed via an attribute
# or via a re-export (vast majority of these recorded in semantic analysis).
# * For each expression type we need to record definitions of type components
# since "meaning" of the type may be updated when definitions are updated.
# * For mypyc-compiled modules only: modules defining MRO ancestors of
# classes defined here, since the generated C embeds each ancestor's
# method/attribute layout.
indirect_refs = self.tree.module_refs | self.type_checker().module_refs
if self.options.mypyc:
indirect_refs |= self.compiled_class_ancestor_refs()
# We should always patch indirect dependencies, even in full (non-incremental) builds,
# because the cache still may be written, and it must be correct.
self.patch_indirect_dependencies(
# Two possible sources of indirect dependencies:
# * Symbols not directly imported in this module but accessed via an attribute
# or via a re-export (vast majority of these recorded in semantic analysis).
# * For each expression type we need to record definitions of type components
# since "meaning" of the type may be updated when definitions are updated.
self.tree.module_refs | self.type_checker().module_refs,
set(self.type_map().values()),
)
self.patch_indirect_dependencies(indirect_refs, set(self.type_map().values()))

if self.options.dump_inference_stats:
dump_type_stats(
Expand Down Expand Up @@ -3535,6 +3539,27 @@ def patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -
self.add_dependency(dep)
self.priorities[dep] = PRI_INDIRECT

def compiled_class_ancestor_refs(self) -> set[str]:
"""Modules defining MRO ancestors of classes defined in this module.

Only used for mypyc-compiled modules: the generated C for a class
(vtable arrays, getter/setter tables, object struct) references
every inherited method/attribute of every ancestor, including
ancestors defined in modules this module does not import directly.
Recording them as indirect dependencies makes an ancestor's
interface change re-trigger type checking (and hence C regeneration)
of this module. Only top-level classes are scanned: mypyc rejects
nested class definitions.
"""
assert self.tree is not None
mods: set[str] = set()
for sym in self.tree.names.values():
node = sym.node
if isinstance(node, TypeInfo) and node.module_name == self.id:
for ancestor in node.mro[1:]:
mods.add(ancestor.module_name)
return mods

def compute_fine_grained_deps(self) -> dict[str, set[str]]:
assert self.tree is not None
if self.id in ("builtins", "typing", "types", "sys", "_typeshed"):
Expand Down
104 changes: 104 additions & 0 deletions mypyc/test-data/run-multimodule.test
Original file line number Diff line number Diff line change
Expand Up @@ -1832,6 +1832,110 @@ def _force_recompile() -> int:
from native import test
test()

[case testIncrementalCrossGroupInheritedMethodRemoved]
# Regression: under separate=True, a module whose only content is a subclass
# definition (`class Leaf(Mid): pass`) produces no expression types, so it
# recorded no dependency on the modules defining its transitive bases. When a
# Base method two hops up the inheritance chain was removed, staleness
# propagation stopped at other_mid (its own interface is unchanged) and
# other_leaf stayed fresh: its stale generated C still referenced the method
# in Leaf's vtable via exports_other_base.CPyDef_..., which no longer exists
# in the regenerated export table, and the build failed.
# compiled_class_ancestor_refs must record the MRO ancestors' modules as
# indirect deps so an ancestor's interface change recompiles the defining
# module too. (removed_method is declared after existing_method so the
# surviving call site's vtable slot is unaffected.)
import other_leaf

def test() -> int:
c = other_leaf.Leaf()
return c.existing_method(5)

[file other_base.py]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

def removed_method(self, x: int) -> int:
return x + 100

[file other_base.py.2]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

[file other_mid.py]
from other_base import Base

class Mid(Base):
def mid_method(self, x: int) -> int:
return x + 2

[file other_leaf.py]
from other_mid import Mid

class Leaf(Mid):
pass

[file driver.py]
from native import test
print(test())

[out]
6
[out2]
6

[case testIncrementalCrossGroupInheritedMethodRemovedNonEmptyLeaf]
# Same regression as testIncrementalCrossGroupInheritedMethodRemoved, but the
# subclass body is NOT empty: a class constant and a method that never uses
# self. Neither produces an expression whose type reaches Leaf's MRO, so the
# pre-existing dependency sources still record nothing for other_leaf, while
# its generated vtable references the ancestors regardless of body shape.
# Guards against narrowing compiled_class_ancestor_refs to pass-only bodies.
import other_leaf

def test() -> int:
return other_leaf.Leaf().existing_method(5)

[file other_base.py]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

def removed_method(self, x: int) -> int:
return x + 100

[file other_base.py.2]
class Base:
def existing_method(self, x: int) -> int:
return x + 1

[file other_mid.py]
from other_base import Base

class Mid(Base):
def mid_method(self, x: int) -> int:
return x + 2

[file other_leaf.py]
from other_mid import Mid

class Leaf(Mid):
FLAG = True

def foo(self) -> str:
return "foo"

[file driver.py]
from native import test
print(test())

[out]
6
[out2]
6

[case testCrossModuleInheritedAttrDefaultsSameGroup]
# separate: [(["native.py"], "grp1"), (["other_a.py", "other_b.py"], "grp2")]
# Regression: with the subclass (other_a) and base (other_b) in the same
Expand Down
Loading