Skip to content

fix(python): from . import x names the importing package, not its parent - #2680

Merged
DeusData merged 3 commits into
mainfrom
fix/py-relative-package-import
Oct 19, 2026
Merged

DeusData merged 3 commits into
mainfrom
fix/py-relative-package-import

Conversation

@DeusData

@DeusData DeusData commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

What

A package-relative from-import (from . import x, from .. import x, from . import x as y) has a
module spelled only with dots. extract_imports.c joined that module to the imported name with another
., so from . import x was recorded as ..x, which names the PARENT package. The import resolver
missed the real binding and fell to the symbol-name fallback in pass_pkgmap.c, which picks the
lexicographically first same-named definition anywhere in the project. That node became the import-map
value, and the Python LSP's alias binding consumes the same value. Every call through such an import
therefore became a confident import_map / lsp_callable_alias edge into an unrelated function.

Example (home-assistant): homeassistant/components/webhook/trigger.py does from . import async_register and calls it. The edge went to homeassistant.components.accuweather.system_health .async_register, the first of 28 functions with that name. Test packages were hit hardest:
tests/components/<integration>/test_*.py importing setup_integration / init_integration from their
own package bound to tests.components.acaia.*.

The fix joins without the extra separator when the module already ends in .. from .m import f
(recorded .m.f) and absolute imports are unchanged.

Evidence

Base origin/main 9f85f10 vs this head, 26 Python repos (manifest v1), production builds, macOS arm64,
--repeat 2, every repeat byte-identical.

base this PR +added -removed
CALLS 1,326,684 1,326,857 7,913 7,740
IMPORTS 477,476 481,037 5,666 2,105
TESTS 505,546 504,644 43 945
USAGE 1,654,225 1,654,569 2,204 1,860
WRITES 534,629 534,619 346 356
INHERITS / OVERRIDE 68,723 / 79,984 68,733 / 80,045 39 / 70 29 / 9

8 of 26 repos are identical; home-assistant carries most of the change (CALLS +7,402/-7,316).
Every changed stratum was sampled (12 edges each) and read:

  • CALLS lsp_callable_alias +7,113/-5,829 and import_map +226/-1,394: every sampled removal pointed at
    the alphabetically first namesake (tests.components.acaia.*, ads.binary_sensor.setup_platform, a
    method of an unrelated class); the additions point at the importing package's own definition
    (11-12 of 12 correct; one plausible module alias).
  • CALLS same_module -19: module-attribute calls through from . import X as alias bound to a same-named
    function of the caller's module; replaced by the correct import_map edges.
  • IMPORTS: removals were the wrong namesakes (and from . import util as orm_util in sqlalchemy's orm
    bound to the parent package's util); additions name the right package member or module.
  • TESTS -945: test->production edges that existed only because a test's own helper was mis-bound to a
    production function of another integration.
  • USAGE / WRITES / DECORATES churn (mostly sqlalchemy, net flat): attribute reads and writes on untyped
    receivers are resolved by name guesses ranked by import reachability; with sqlalchemy's relative imports
    now resolving, those guesses re-rank. Neither side is a reliable target.
  • Recall oracle (stdlib ast over every corpus file, 26 repos; each expected caller/callee pair checked
    against the graph): bare calls through relative from-imports 23,217 / 30,405 correct (76.4%, 7,179
    wrong) -> 30,396 / 30,405 (99.97%, 0 wrong); aliased relative 89 / 93 -> 93 / 93; module-attribute calls
    +115 correct. Absolute from-imports, re-exports and same-file calls are unchanged (the fix touches only
    the relative forms).

Exposed regressions (17 edges plus derived OVERRIDE)

These are pre-existing approximations that the corrected imports now reach:

  • 16 INHERITS (sqlalchemy 15, matplotlib 1) plus their derived OVERRIDE edges: exception classes nested in
    AsyncAdapt_asyncpg_dbapi (class OperationalError(DatabaseError) names the SIBLING nested class) now
    resolve to lib.sqlalchemy.exc.DatabaseError. A bare base name is resolved through the registry's name
    strategies, which prefer an import-reachable candidate; the sibling won only while exc was not
    reachable. Closed by fix(python): bind a nested class base to an earlier sibling of its class body #2695, which resolves a nested class's bare base in its enclosing class body first
    (exact). Measured with this change: all 16 come back.
  • 1 super() call (home-assistant radarr): RadarrCalendarEntity(RadarrEntity, CalendarEntity)'s
    super()._async_write_ha_state() moved from CalendarEntity (C3 order) to Entity: the super()
    lookup walks each base's ancestry depth-first; resolving RadarrEntity's ancestry exposed it. Closed by
    fix(python-lsp): resolve super() through the C3 method resolution order #2696, which walks the C3 MRO.
  • Re-export suffix picks: @testing.combinations and sql.cast / sql.select / sql.and_ now land on a
    same-named method inside the correctly resolved package instead of the re-exported function (the
    package __init__ re-exports it). Closed by the re-export resolution work in a follow-up PR, which lists these
    as required cases.

Re-measured on current main (clean builds)

The numbers above were measured against main 9f85f10. This change merged with main ea5c1f7 was rebuilt
from a clean build directory and compared with a clean ea5c1f7 build on sqlalchemy and matplotlib
(--repeat 2, noise 0):

  • IMPORTS, INHERITS, OVERRIDE and DECORATES are identical to the figures above on both repos. matplotlib's
    USAGE, WRITES and TESTS are identical too.
  • sqlalchemy CALLS is +310 / -286 (was +296 / -273). USAGE is +1,358 / -1,280 (was +1,338 / -1,261) and
    WRITES +284 / -280 (was +281 / -277), TESTS +15 / -14 (was +15 / -15). matplotlib CALLS is +63 / -24
    (was +62 / -24).
  • The difference comes from fix(python): extract defs nested in a function body and resolve bare names lexically (#1548) #2686 (defs nested in functions), which landed in between. The same corrected
    calls now start at the nested def instead of the file node (collections._dict_decorators.setdefault
    instead of collections.py), and the newly extracted nested defs add a few more calls through the
    corrected imports. The targets are unchanged.

Tests

  • python_package_relative_import_paths (extraction): the recorded paths for all three forms, plus
    .m.f and absolute controls.
  • pipeline_python_package_relative_imports and ..._parallel: each form resolves to the own or parent
    package, never to the same-named function that sorts first, and not through a name guess; both
    resolvers. RED with the extractor change reverted ("..async_register" != ".async_register";
    own == 0), GREEN here.
  • pipeline_python_from_import_bare_call_issue1237 and ..._parallel (guard for trace_path/callers returns empty for plain "from module import func" + bare call across files (Python) #1237): a plain
    from pkg.db import get_row_or_none plus bare calls from a function, a method and the same file bind
    the imported def, never a same-named def elsewhere; a re-index that only adds a file leaves CALLS
    unchanged. Main already resolves this shape, so this guard has no RED on revert.
  • Affected suites (complexity, extraction, registry, pipeline, cross_repo, py_lsp,
    lsp_resolution_probe, incremental): 1,323 passed on the tree merged with main.

Notes

A package-relative from-import (`from . import x`, `from .. import x`,
`from . import x as y`) has a module made only of dots. The extractor joined
it to the imported name with another '.', recording "..x" for `from . import x`:
the parent package. The import resolver then missed the real binding and fell
to the symbol-name guess (the lexicographically first same-named definition in
the project), so the import map, and the Python LSP binding that consumes it,
sent every call to an unrelated function at import_map / lsp_callable_alias
confidence. On home-assistant, 7,175 caller/callee pairs bound to another
integration's function (webhook's `async_register` to accuweather's).

Join without the extra separator when the module already ends in '.'.
`from .m import f` (".m.f") and absolute imports are unchanged.

Tests: python_package_relative_import_paths (extraction: the recorded paths);
pipeline_python_package_relative_imports{,_parallel} (all three forms resolve
to the own / parent package, never to the namesake that sorts first, without a
name guess, on both resolvers); pipeline_python_from_import_bare_call_issue1237
{,_parallel} (#1237 guard: a plain from-import and bare calls bind the imported
def and keep it across a re-index that adds a file).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…art CI selection fix #2684)

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
yermakoffivan pushed a commit to yermakoffivan/codebase-memory-mcp that referenced this pull request Oct 18, 2026
…ONAL on every selection path

A runner suite registered only under a preprocessor condition (the four
coverage_* suites under `#if defined(CBM_TEST_COVERAGE) && !defined(_WIN32)`
in tests/test_main.c) is absent from builds without that condition. The
selection marked such a suite CONDITIONAL only on the per-registration path,
and only when a registration of its own was conditional and contributed a
reason. Every suite-level path (setup unknown or hit, suite-body, changed and
rule triggers, inventory, rejected artifact) made the suite whole without it,
and the runner registration's own condition was never recorded.

PR CI on the x86 Linux gcc leg then failed with "the test selection names
suites this build does not register: coverage_setup_success
coverage_setup_uncertain" (DeusData#2679, DeusData#2680): the suite-body trigger and an
incomplete setup row selected both suites whole without CONDITIONAL, so
selection.py left them out of optional-suites.txt.

The model now records whether a runner registration sits under a condition
(conditional only when every site of the suite is), and the selection marks
any selected conditionally registered suite whole with CONDITIONAL after every
whole-suite path. Unconditionally registered suites keep the strict contract.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit c06465b into main Oct 19, 2026
50 of 52 checks passed
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