gh-157757: Fix to make lazy import a.b as c import the module a.b - #158092
Conversation
import a.b as c namesimport a.b as c names
import a.b as c nameslazy import a.b as c import the module a.b
|
Hmm, maybe we can have each deferred For Perhaps we can use the existing fields for this: I prototyped this and it seems to work. We would keep the intermediate placeholders around until resolution, so it uses more memory. I think it’s worth considering, though. It follows what the bytecode does and avoids needing the extra flag. |
|
Thanks @pablogsal! That was very useful context + direction. |
|
Yep, I think the chaining is much nicer than the flag: we just replay the same |
|
Moved the change into the constructor + added two tests |
`import a.b as c` compiles to `IMPORT_NAME a.b` followed by `IMPORT_FROM b`. Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM rewrote it into the placeholder `lazy from a import b` produces. Reification then imported `a` alone and read `b` off it, so the module `a.b` was never imported under its own name: an attribute of the package shadowing it answered instead, and `math.pi`, which no module backs, bound the float where the eager statement raises ModuleNotFoundError. Mark the dotted import on the placeholder and keep the whole name on it. Reification imports that name and then walks its components with IMPORT_FROM, which is what the eager statement does. The test pinning `lazy import math.pi as pi` as working is inverted, since the eager statement raises.
It passes now that a lazy `import a.b as c` imports the module: the KeyError on 'test.tracedmodules.testmod' came from the submodule never being imported under its own name.
The flag means "bind the whole dotted name, not the root", which the old name did not say, and import.c already has unrelated lazy_pending_submodules machinery to be confused with.
Each deferred IMPORT_FROM off a placeholder without a fromlist now keeps the previous placeholder in lz_from and the attribute name in lz_attr. Reification walks back to the placeholder IMPORT_NAME left, runs that import, and replays the lookups in order with _PyEval_ImportFrom, which is what the eager bytecode does. This drops the lz_dotted_as flag and also follows a custom __lazy_import__ that returns a placeholder for a different module name.
`lazy from a import b` now records its lookup the same way as `import a.b as c`, so a placeholder holds either the module name and fromlist or the previous placeholder and an attribute name, and reification has a single path. The import passes only the name being resolved as the fromlist, so accessing b still does not import the other names' submodules.
__import__("a.b", fromlist=()) returns the top-level package `a`, the same
as fromlist=None, but the placeholder kept the empty tuple. Every consumer
of lz_attr then read it as a real fromlist: reification narrowed it to the
chained attribute and replayed the lookups on `a.b`, _PyEval_LazyImportFrom
took the attribute off sys.modules["a.b"], and the repr named `a.b.attr`.
_PyLazyImport_New already collapses None to NULL for exactly this reason,
so collapse an empty tuple there too and every site follows.
c10339c to
48244f1
Compare
|
Thanks @brittanyrey for the PR, and @pablogsal for merging it 🌮🎉.. I'm working now to backport this PR to: 3.15. |
|
Sorry, @brittanyrey and @pablogsal, I could not cleanly backport this to |
|
Great work on the lazy import fix, @brittanyrey! Thanks a lot! ❤️ |
|
GH-158257 is a backport of this pull request to the 3.15 branch. |
…e `a.b` (GH-158092) (#158257) gh-157757: Fix to make `lazy import a.b as c` import the module `a.b` (#158092) * Import the module a lazy `import a.b as c` names `import a.b as c` compiles to `IMPORT_NAME a.b` followed by `IMPORT_FROM b`. Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM rewrote it into the placeholder `lazy from a import b` produces. Reification then imported `a` alone and read `b` off it, so the module `a.b` was never imported under its own name: an attribute of the package shadowing it answered instead, and `math.pi`, which no module backs, bound the float where the eager statement raises ModuleNotFoundError. Mark the dotted import on the placeholder and keep the whole name on it. Reification imports that name and then walks its components with IMPORT_FROM, which is what the eager statement does. The test pinning `lazy import math.pi as pi` as working is inverted, since the eager statement raises. * Stop excluding test_trace from the lazy-imports-all run It passes now that a lazy `import a.b as c` imports the module: the KeyError on 'test.tracedmodules.testmod' came from the submodule never being imported under its own name. * Rename lz_submodule to lz_dotted_as and trim the comments The flag means "bind the whole dotted name, not the root", which the old name did not say, and import.c already has unrelated lazy_pending_submodules machinery to be confused with. * Chain lazy IMPORT_FROM placeholders instead of flagging dotted imports Each deferred IMPORT_FROM off a placeholder without a fromlist now keeps the previous placeholder in lz_from and the attribute name in lz_attr. Reification walks back to the placeholder IMPORT_NAME left, runs that import, and replays the lookups in order with _PyEval_ImportFrom, which is what the eager bytecode does. This drops the lz_dotted_as flag and also follows a custom __lazy_import__ that returns a placeholder for a different module name. * Chain every deferred IMPORT_FROM onto the previous placeholder `lazy from a import b` now records its lookup the same way as `import a.b as c`, so a placeholder holds either the module name and fromlist or the previous placeholder and an attribute name, and reification has a single path. The import passes only the name being resolved as the fromlist, so accessing b still does not import the other names' submodules. * Treat an empty fromlist on a lazy import placeholder as no fromlist __import__("a.b", fromlist=()) returns the top-level package `a`, the same as fromlist=None, but the placeholder kept the empty tuple. Every consumer of lz_attr then read it as a real fromlist: reification narrowed it to the chained attribute and replayed the lookups on `a.b`, _PyEval_LazyImportFrom took the attribute off sys.modules["a.b"], and the repr named `a.b.attr`. _PyLazyImport_New already collapses None to NULL for exactly this reason, so collapse an empty tuple there too and every site follows. * gh-157757: Preserve empty fromlists for custom import hooks --------- (cherry picked from commit 1e8ff18) Co-authored-by: Brittany Reynoso <breynoso@meta.com>
Bug:
import a.b as ccompiles toIMPORT_NAME a.bfollowed byIMPORT_FROM b. Lazily, IMPORT_NAME leaves a placeholder holding "a.b", and IMPORT_FROM rewrote it into the placeholderlazy from a import bproduces. Reification then importedaalone and readboff it, so the modulea.bwas never imported under its own name: an attribute of the package shadowing it answered instead, andmath.pi, which no module backs, bound the float where the eager statement raises ModuleNotFoundError.Fix: Follow @pablogsal's suggestion to have each deferred
IMPORT_FROMkeep the previous placeholder and the attribute name. Reification runs the original import and then applies the recorded lookups in order with_PyEval_ImportFrom, as the eager statement does.For
lazy from a import b, c, the import gets only the name being resolved as thefromlist, so accessingbdoesn't importa.c.lazy import a.b as cresolvesbas an attribute ofainstead of importing the modulea.b#157757