Skip to content

fix(python-lsp): resolve super() through the C3 method resolution order - #2696

Merged
DeusData merged 2 commits into
mainfrom
fix/py-super-c3-mro
Oct 19, 2026
Merged

DeusData merged 2 commits into
mainfrom
fix/py-super-c3-mro

Conversation

@DeusData

Copy link
Copy Markdown
Owner

What

super() binds to the enclosing class's method resolution order, which is Python's C3 linearization. In
that order a common base follows every class that derives from it:

class RadarrCalendarEntity(RadarrEntity, CalendarEntity):
    # MRO: RadarrCalendarEntity, RadarrEntity, CoordinatorEntity, ..., CalendarEntity, Entity
    def _async_write_ha_state(self):
        super()._async_write_ha_state()   # CalendarEntity's override, not Entity's

The super() resolver walked the bases one at a time, each through its whole ancestry. So the first base
reached the shared Entity through CoordinatorEntity before CalendarEntity, which overrides it, was
ever looked at. For super().__init__() it consulted only the first base. When that base was a mixin
without a constructor, it synthesized <mixin>.__init__, a QN that matches no node, and the call was
dropped.

py_lsp.c now computes the C3 order. py_c3_linearize memoizes per lookup, is capped in depth and length,
and reports when a hierarchy has no C3 order. super() takes the first class after the enclosing class that
declares the member itself. A hierarchy without a C3 order keeps the previous base-by-base walk, and so does
the synthesized constructor of an undeclared __init__. Ordinary attribute lookup is unchanged.

No index-format change.

Evidence

Base main 1346ac4 vs this change, 26 Python repos (manifest v1), production builds, macOS arm64,
--repeat 2, every repeat byte-identical.

type base this PR +added -removed
CALLS 1,355,020 1,355,102 129 47

Every other edge type is identical, apart from SEMANTICALLY_RELATED, which is derived from the CALLS
neighbourhoods. All added edges are lsp_super (54) or lsp_super_init (75).

  • Retargeted (47, same call line, new target): the C3 target is a class nearer in the MRO than the shared
    base the old walk reached first. Examples:
    • scikit-learn HuberRegressor.__sklearn_tags__ reaches RegressorMixin, not BaseEstimator.
    • sqlalchemy MSDialect.do_savepoint and MSDialect_pyodbc.do_executemany reach DefaultDialect, not
      the engine.interfaces.Dialect stub.
    • home-assistant Airtouch5AC.async_added_to_hass reaches Airtouch5Entity, not helpers.entity.Entity.
    • matplotlib FigureCanvasQTCairo.draw reaches FigureCanvasQT.draw.
    • pandas StringArray.__setitem__ reaches NDArrayBackedExtensionArray.
    • mypy CallableType.__init__ reaches FunctionLike.__init__.
  • New (82, mostly lsp_super_init): super().__init__() in classes whose first base is a mixin without a
    constructor. Examples:
    • scikit-learn AdaBoostRegressor / AdaBoostClassifier reach BaseWeightBoosting.__init__.
    • django AdminUserCreationForm reaches BaseUserCreationForm.__init__, and GIS Scale reaches
      GeoFuncMixin.__init__.
    • sqlalchemy Update reaches ValuesBase.__init__.
  • The radarr case itself: on main it already resolves to CalendarEntity, but only because
    RadarrEntity's ancestry is unresolved. Once relative imports resolve (fix(python): from . import x names the importing package, not its parent #2680), main resolves it to
    Entity._async_write_ha_state, and with this change to CalendarEntity._async_write_ha_state (checked
    on home-assistant with both changes merged).

Tests

  • pipeline_python_super_follows_c3_mro{,_parallel}: the radarr shape across two files.
    super().write_state() and super().__init__() reach CalendarEntity, never Entity, and a
    single-inheritance class is the control. Sequential and parallel. RED without the fix (ASSERT(m_c3)).
  • Affected suites (complexity, extraction, registry, pipeline, cross_repo, py_lsp, py_lsp_stress,
    py_lsp_scale, py_lsp_bench, lsp_resolution_probe, edge_types_probe, convergence_probe,
    matrix_known_classes, parallel, incremental, integration) pass.

The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity,
CalendarEntity) where RadarrEntity reaches the common base Entity through
CoordinatorEntity and CalendarEntity overrides Entity. In C3 order a common
base comes after every class deriving from it, so super().write_state() and
super().__init__() reach CalendarEntity, not Entity through the first base's
ancestry. Single inheritance as the control; sequential and parallel.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
super() binds to the enclosing class's method resolution order, Python's C3
linearization, in which a common base follows every class deriving from it.
The super() resolver walked the bases one by one, each through its whole
ancestry, so with RadarrCalendarEntity(RadarrEntity, CalendarEntity) the
first base reached Entity through CoordinatorEntity before CalendarEntity,
which overrides it, was ever looked at.

py_c3_linearize computes the order (memoized per lookup, capped in depth and
length; -1 when the hierarchy has none), and super() takes the first class
after the enclosing one that declares the member. A hierarchy without a C3
order keeps the base-by-base walk, as does the synthesized __init__ of an
undeclared constructor.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit 5dc83ac into main Oct 19, 2026
29 checks passed
DeusData added a commit that referenced this pull request Oct 19, 2026
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
thugrock7 pushed a commit to thugrock7/codebase-memory-mcp that referenced this pull request Oct 19, 2026
…a#2680, DeusData#2690, DeusData#2695, DeusData#2696)

py_lsp.c: the module-member lookup of this branch and the C3 super() lookup of DeusData#2696 were added at the same place; both are kept.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant