Skip to content

[3.15] gh-157757: Fix to make lazy import a.b as c import the module a.b (GH-158092) - #158257

Merged
pablogsal merged 1 commit into
python:3.15from
pablogsal:backport-158092-3.15
Sep 26, 2026
Merged

pablogsal merged 1 commit into
python:3.15from
pablogsal:backport-158092-3.15

Conversation

@pablogsal

Copy link
Copy Markdown
Member

Backport of #158092 to 3.15. The automatic cherry-pick conflicted because 3.15 lacks the surrounding test class and the lazy-import exclusion file. The regression tests now have their own class and use xml.dom.minidom for the nested-import case.

The debug build passes test_lazy_import, test_import, test_importlib, and test_trace, with 1,506 tests run.

(cherry picked from commit 1e8ff18)

Co-authored-by: Brittany Reynoso breynoso@meta.com

… `a.b` (python#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.

* pythongh-157757: Preserve empty fromlists for custom import hooks

---------

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
(cherry picked from commit 1e8ff18)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants