Repository navigation
Conversation
xv1rcn
requested review from
DinoV,
FFY00,
Yhg1s,
brettcannon,
ericsnowcurrently,
kumaraditya303,
ncoghlan,
pablogsal and
warsaw
as code owners
October 15, 2026 16:08
A pending child in `lazy_pending_submodules` is keyed only by `(parent, child)`, so a later declaration of the same name by another module replaces the earlier one. Resolving the child then copies the location out of the surviving declaration, so the chained `ImportError` points into the file of a module that merely declared the name. Key the declarations by the declaring module, and select one from the frame's globals at access time, so the child resolves with the declaration in scope where the access is written. A declaration made by another module still names the same child and imports the same thing, so it is still used when there is nothing in scope; its location is then not reported as a frame of the accessing module's traceback. The selection also decides which `__import__` runs, since `_PyLazyImport_LoadChild()` takes `lz_builtins` from the declaration it is given. An access that has a declaration of its own in scope now uses that module's hook instead of the last-registered one's — the case Yhg1s described in the pythongh-158521 review. An access with no declaration in scope is unchanged.
xv1rcn
force-pushed
the
gh-158597-lazy-declaration-scope
branch
from
October 15, 2026 16:11
3762e81 to
00db886
Compare
Member
|
I'm closing this per our AI policy. Please don't open more PRs without achieving first consensus in the issue on the approach |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
gh-158597: Resolve lazy submodules with the declaration in scope
lazy import xml.missingrecords a pending child ininterp->imports.lazy_pending_submodules, keyed only by(parent, child):pending["xml"]["missing"] =
register_lazy_on_parent()writes that slot withPyDict_SetItem(), so a later declaration of the same name by another module replaces the earlier one. When a module then evaluatesxml.missing,_PyImport_TryLoadLazySubmodule()reads that one slot and_PyLazyImport_LoadChild()copieslz_code/lz_instr_offsetout of it, so the chainedImportErrorreports whichever declaration was registered last, no matter who is accessing.Measured before the change (main at 64018ac, debug build):
A.py:1A.py:2B.py:1C.py:1This keys the declarations by the declaring module and selects one at access time from the frame's globals, so a name resolves with the declaration in scope where the access is written:
pending["xml"]["missing"] == {"A": , "B": } # declarations pending["xml"]["dom"] == None # pending, no declaration
A declaration made by another module still names the same child and imports the same thing, so it is still used when there is nothing in scope. It is not reported as a frame of the accessing module's traceback:
lz_attributedon the placeholder records whether the location belongs to the module performing the access, andlazy_import_add_exception_cause()only adds a frame when it does. The chainedImportErrorthen carries no frame rather than pointing into an unrelated file, and the exception type is unchanged.One visible change beyond the reported case:
_PyLazyImport_LoadChild()takeslz_builtinsfrom the declaration it is given, so the selected declaration also decides which__import__runs. An access with a declaration of its own in scope now runs that module's hook instead of the last-registered one's — the case Yhg1s described in the gh-158521 review ("module A may end up calling module B's__import__when it does its reification"), and the principletest_namespace_import_hooksalready covers for placeholders. It does not cover it completely: an access with no declaration in scope still resolves with the most recently registered one, as before, so that case is unchanged.Remaining gaps:
__name__are still indistinguishable, as noted in the gh-158140: Resolve lazily imported sibling submodules independently #158521 review discussion.add_lazy_declaration()reads and then writes the per-child dict. The surrounding code already readslazy_pending_submoduleswithout a lock in_PyImport_TryLoadLazySubmodule(), and the consequence here is only that concurrent registrations of the same child can drop one declaration (the child stays pending and is resolved by the remaining one). I did not add locking because it would be a separate change; happy to do it if you want it in this PR.Testing
./python -m test test_lazy_import test_import test_importlib test_module test_sys test_capi— 3267 tests pass (debug build);-R 3:3clean.--disable-gil --with-pydebugbuild.ErrorHandlingTests, both failing on unmodified main:test_pending_child_reports_accessing_module_declarationtest_unattributed_access_is_not_given_another_declarationAI assistance was used to locate the cause and to draft the change and the tests. I have reviewed the analysis and the patch and can explain every part of the diff.