Skip to content

Python: .get()/.update() on a typed project-class instance get no edge (dropped as builtins) #2583

Closed

Description

@inth3shadows

Summary

In Python, a call to a project method named like a builtin collection method (get, update, items, pop, …) is dropped as a builtin unless the receiver variable's name, capitalized, is the name of a project class. This happens even when the receiver's type is known: reg = SystemPackRegistry(); reg.get("y") gets no edge, while the same code with the methods renamed, or with the variable named systemPackRegistry, resolves at 0.9.

Reproduction (verified)

On main at 8fe3156, built from source, Linux (WSL2), Node 22.23.2, WASM extractor (CODEGRAPH_KERNEL=0). Two files:

# loader.py
class SystemPackRegistry:
    def get(self, pack_id):
        return pack_id

    def update(self):
        return None


def load_registry() -> SystemPackRegistry:
    return SystemPackRegistry()
# test_packs.py
from loader import SystemPackRegistry, load_registry


def test_pack():
    registry = load_registry()
    return registry.get("x")      # line 6


def test_ctor():
    reg = SystemPackRegistry()
    reg.update()                  # line 11
    return reg.get("y")           # line 12

codegraph init -i, then the calls / imports / instantiates edges:

load_registry -instantiates-> SystemPackRegistry [loader.py:1] @10 exact-match/0.9
test_packs.py -imports-> SystemPackRegistry [loader.py:1] @1 import/0.9
test_packs.py -imports-> load_registry [loader.py:9] @1 import/0.9
test_pack -calls-> load_registry [loader.py:9] @5 import/0.9
test_ctor -instantiates-> SystemPackRegistry [loader.py:1] @10 import/0.9

There is no edge for lines 6, 11 or 12.

Expected: test_ctor -calls-> SystemPackRegistry::update (line 11) and -> SystemPackRegistry::get (line 12), and test_pack -calls-> SystemPackRegistry::get (line 6), as in the controls below.

Controls, same build:

Change Line 6 Line 11 Line 12
as above none none none
methods renamed get→lookup, update→refresh SystemPackRegistry::lookup instance-method/0.7 SystemPackRegistry::refresh instance-method/0.9 SystemPackRegistry::lookup instance-method/0.9
reg renamed systemPackRegistry none SystemPackRegistry::update instance-method/0.9 SystemPackRegistry::get instance-method/0.9

So the resolver can type both receivers, but the builtin filter runs first and decides from the variable's name alone.

Likely cause (inferred from reading the code)

isBuiltInOrExternal runs before any resolution strategy. For a dotted Python ref whose method is in PYTHON_BUILT_IN_METHODS (get, update, items, pop, read, write, close, …), it keeps the ref only if the capitalized receiver (reg → Reg) is a project class name, or the receiver is an imported project module (#1681). Otherwise it returns true, and the ref is discarded before matchMethodCall can infer the receiver's type from reg = SystemPackRegistry() or -> SystemPackRegistry.

One option is to leave these refs to matchMethodCall when the receiver has a local declaration that the existing receiver-type inference can type, and drop them only when it can't.

Related

A build of the inth3shadows fork that carries open PRs #1704 and #1926 gives the same output.

Not checked

  • Other names in the set (items, pop, read, close, …); only get and update were run.
  • self.<attr>.get() and parameters annotated with the class (def f(reg: SystemPackRegistry)).
  • The native kernel extractor (only CODEGRAPH_KERNEL=0 was run).
  • Windows and macOS.

Activity

  1. inth3shadows commented on Oct 15, 2026

    @inth3shadows
    ContributorAuthor

    I tried the fix suggested above, on main at c29caf2. The fix kept a builtin-named call when the receiver's type can be read and that class has the method. It does link the constructor, annotated-parameter and pytest-fixture cases: on one real project it added 124 correct edges, such as log = ReceiptLog(…); log.append(…). But the receiver typing it relies on is not safe for names like get, update, close and append, so I'm not opening a PR. These are the shapes that got a new wrong edge. Each was verified by indexing on the branch and on main, and main gives no edge for all of them:

    • The type's import is ignored. from queue import Queue; q = Queue(); q.get() links a project class named Queue. The same happens with from requests import Session, from collections import Counter and from io import StringIO. It also happens with two project classes of the same name: from b import Handler reached a.py's Handler. An inherited method is found by its bare base-class name too.
    • A later rebinding is not seen. reg = Store() then reg = {} (or = json.load(f), an if/else, for reg in …, with open(p) as reg, a comprehension variable) is still typed Store.
    • A chained constructor is typed as the class. x = Foo().data or x = Foo().build(), and a fixture return Config().settings, are typed Foo or Config.
    • Module-level code scans into function bodies. A module-level reg.get() picks up a reg = Store() written inside an earlier function.
    • Unions take the first type. reg: Store | dict with isinstance(reg, dict) is typed Store.

    In the shapes above, every one already links wrongly on main when the method has a non-builtin name. The builtin-method filter is what hides it for get/update/…. So lifting the filter for typed receivers needs receiver typing that respects imports and rebinding first.

    Not fixed by the attempt either: registry = load_registry() with def load_registry() -> SystemPackRegistry (line 6 in the report). The resolver doesn't read Python return annotations today. The renamed-method control linked through the unique-method-name fallback, not through typing.

  2. colbymchenry commented on Oct 23, 2026

    @colbymchenry
    Owner

    Thanks for the careful report, and for trying a fix first: the false-positive shapes from your follow-up became the negative tests.

    #2821 is merged to main. It links builtin-named calls like reg.get() and reg.update() when the function says what the receiver holds:

    • a constructor or a typed factory as the whole value, so registry = load_registry() with -> SystemPackRegistry links too;
    • or a parameter annotation.

    The receiver is typed only when every binding of the name in its function agrees, and the class is the one the file's imports name. So from queue import Queue, a later reg = {} or json.load(...), Foo().data(), a union, and module-level code still link nothing.

    It ships in the next release.

  3. added a commit that references this issue on Oct 23, 2026
    d5fc1fb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions