Skip to content

fix(python-lsp): look attributes up along the C3 method resolution order - #2697

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

DeusData merged 3 commits into
mainfrom
fix/py-attr-c3-mro

Conversation

@DeusData

Copy link
Copy Markdown
Owner

What

#2696 makes super() follow Python's C3 method resolution order. Ordinary attribute lookup had the same
defect: it walked the bases one at a time, each through its whole ancestry. So on a diamond, self.m() or
obj.m() reached the shared base before a later base that overrides it:

class Fixtures:
    def store_event(self): ...

class APITestCase(Fixtures): ...

class SnubaTestCase(Fixtures):
    def store_event(self): ...          # overrides the shared base

class MyEventsTest(APITestCase, SnubaTestCase):
    def test_query(self):
        self.store_event()   # MRO: MyEventsTest, APITestCase, SnubaTestCase, Fixtures

(sentry's real hierarchy has more levels; it has this shape.)

py_lookup_attribute now asks the class's own members first and then follows the class's C3 order, the
same order super() uses. A hierarchy without a C3 order keeps the depth-first walk.

Each class is linearized once per file. The orders live in a per-file, arena-allocated open-addressing
cache keyed by class QN, which also replaces #2696's per-lookup cache. Linearizing a class merges its
bases' cached orders, so no lookup re-linearizes a deep hierarchy. The registry is complete before the walk
starts, so an entry never goes stale.

Stacked on #2696, whose branch (fix/py-super-c3-mro) is this PR's base. No index-format change.

Evidence

#2696's head 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,102 1,355,099 1,418 1,421
TESTS 511,519 511,493 1,103 1,129
CALL_REFERENCE 26,918 26,918 1 1

Every other edge type is identical, apart from SEMANTICALLY_RELATED, which is derived from the CALLS
neighbourhoods. 1,417 of the removed CALLS are retargets on the same call line. The other 4 merge into an
edge the caller already had to the new target on another line. The TESTS changes follow from the CALLS.
No strategy is new: the edges stay lsp_method / lsp_operator_dunder.

Every retarget moves from the shared base that the depth-first walk reached through the first base, to
the class the C3 order puts first:

  • sentry (1,275, in 7 target pairs): Fixtures.store_event becomes SnubaTestCase.store_event (761), and
    Fixtures.create_project becomes SnubaTestCase.create_project (485). Fixtures is the common base of
    both test-case branches, so it follows SnubaTestCase in every test class's MRO. The rest are
    SharedSnubaMixin.store_event, the metrics query builders' run_query, and
    DemoSafePermission.determine_access.
  • scrapy (53): CrawlerRunnerBase.crawl becomes AsyncCrawlerRunner.crawl.
  • sqlalchemy (30): the engine.interfaces.Dialect stubs become DefaultDialect, and
    ExternallyTraversible._copy_internals becomes HasCopyInternals._copy_internals.
  • sympy (26): Basic becomes Expr / AssocOp (doit, count_ops, could_extract_minus_sign), and
    Basic.simplify becomes BooleanFunction.simplify.
  • home-assistant (15): FlowHandler.async_show_form / async_create_entry become ConfigFlow's.
  • wagtail (8), transformers (6), matplotlib (2), pandas (2): the same pattern.

Indexing time per pass is unchanged within machine noise. The repeats that ran slower were slower in
extraction as well, which this change does not touch.

Tests

The radarr diamond seen from an ordinary attribute: self.write_state() in
RadarrCalendarEntity and e.write_state() on a RadarrCalendarEntity look the
method up along the class's C3 order, which reaches CalendarEntity before
the shared base Entity. Single inheritance as the control; sequential and
parallel.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Ordinary attribute lookup walked the bases one at a time, each through its
whole ancestry, so on a diamond self.m() and e.m() reached the shared base
before a later base that overrides it. It now follows the class's C3 order,
the order super() uses, after the class's own members; a hierarchy without
a C3 order keeps the depth-first walk.

Each class is linearized once per file: the orders live in a per-file,
arena-allocated open-addressing cache keyed by class QN, which also replaces
the per-lookup cache of the super() resolver. The registry is complete
before the walk starts, so an entry never goes stale; keys and the class
entry of an order are arena copies. Linearizing a class costs one merge of
its bases' cached orders, so no lookup re-linearizes a deep hierarchy.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData changed the base branch from fix/py-super-c3-mro to main October 19, 2026 00:55
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit 27c3303 into main Oct 19, 2026
28 checks passed
DeusData added a commit that referenced this pull request Oct 19, 2026
py_lsp.c: #2697 moved py_lookup_attribute below the C3 code (own members, C3 order, then the depth-first walk) and declares it up front; the trivial definition this branch still carried next to its module-member lookup is dropped, the module-member block is kept, and the C3 section takes main's header.

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