Bug Report
Under incremental mypyc compilation with separate=True, a module whose classes inherit from ancestors defined several modules up the chain can be left stale when a distant ancestor's method set changes. Mypy's incremental system only recompiles a module when a recorded dependency changes, and a dependency edge to an ancestor's module is created in cases such as an explicit import or a checked expression whose type references that ancestor. A bare subclass definition produces neither, so the transitive ancestors never enter the module's dependency graph.
For type checking this isn't a problem: a change in a distant ancestor doesn't affect the subclass's interface. But mypyc's generated C embeds the full layout of every ancestor into the subclass, including a vtable slot for each inherited method and struct offsets for inherited attributes. So the generated C depends on ancestors that the dependency graph doesn't capture.
Originally surfaced in a sqlglot CI build (tobymao/sqlglot#7875): https://github.com/tobymao/sqlglot/actions/runs/29492925074/job/87603095733
To Reproduce
# a.py
class A:
def foo1(self) -> int:
return 1
def foo2(self) -> int: # <- removed in the second build
return 2
# b.py
from a import A
class B(A):
def other(self) -> str:
return "b"
# c.py
from b import B
class C(B):
pass # <- records no dependency on a.py
Expected Behavior
The incremental rebuild regenerates c.py's C as well (its vtable references every inherited method of its ancestors) and the build succeeds.
Actual Behavior
python3 setup.py build_ext --inplace # first build, populates .mypy_cache — succeeds
now delete foo2 from a.py
python3 setup.py build_ext --inplace # incremental rebuild
build/__native_c.c:66:34: error: no member named 'CPyDef_a___A___foo2' in
'struct export_table_a'; did you mean 'CPyDef_a___A___foo1'?
1 error generated.
error: command '/usr/bin/clang' failed with exit code 1
Your Environment
- Mypy version used: reproduced on a fork based on the release-1.20 branch
(fork version 2.1.0.post8); the relevant dependency-recording code in
State.finish_passes / patch_indirect_dependencies is unchanged on master
- Mypy command-line flags: none — mypy is invoked by mypyc via
mypycify(["a.py", "b.py", "c.py"], separate=True)
- Mypy configuration options from
mypy.ini (and other config files): none
- Python version used: 3.12.9
I will link a PR for this soon.
Bug Report
Under incremental mypyc compilation with
separate=True, a module whose classes inherit from ancestors defined several modules up the chain can be left stale when a distant ancestor's method set changes. Mypy's incremental system only recompiles a module when a recorded dependency changes, and a dependency edge to an ancestor's module is created in cases such as an explicit import or a checked expression whose type references that ancestor. A bare subclass definition produces neither, so the transitive ancestors never enter the module's dependency graph.For type checking this isn't a problem: a change in a distant ancestor doesn't affect the subclass's interface. But mypyc's generated C embeds the full layout of every ancestor into the subclass, including a vtable slot for each inherited method and struct offsets for inherited attributes. So the generated C depends on ancestors that the dependency graph doesn't capture.
Originally surfaced in a sqlglot CI build (tobymao/sqlglot#7875): https://github.com/tobymao/sqlglot/actions/runs/29492925074/job/87603095733
To Reproduce
Expected Behavior
The incremental rebuild regenerates c.py's C as well (its vtable references every inherited method of its ancestors) and the build succeeds.
Actual Behavior
Your Environment
(fork version 2.1.0.post8); the relevant dependency-recording code in
State.finish_passes/patch_indirect_dependenciesis unchanged on mastermypycify(["a.py", "b.py", "c.py"], separate=True)mypy.ini(and other config files): noneI will link a PR for this soon.