python / python/cpython

`lazy import a.b as c` resolves `b` as an attribute of `a` instead of importing the module `a.b`

Offen
#157,757 1 Kommentar 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

interpreter-core release-blocker topic-lazy-imports type-bug
Vorherrschende Sprache
Python
Sterne
77.2k
Forks
35.9k
PR-Merge-Kennzahlen
PR-Kennzahlen ausstehend

Beschreibung

Bug report

Bug description:

lazy import a.b as c never imports the module a.b by name. On first use it imports a and reads the attribute b off it, as lazy from a import b would, so c becomes whatever that attribute lookup finds. This manifests in two ways.

Case A: a.b is not a module. The eager statement raises; the lazy one binds the attribute.

lazy import math.pi as lazy_x
print(lazy_x)  # 3.141592653589793

import math.pi as x  # ModuleNotFoundError: No module named 'math.pi'; 'math' is not a package

The bare lazy import math.pi was fixed in #155194; the as form was not.

Case B: a.b is a module, but a has an attribute b that shadows it and a.b has not been imported yet. The eager statement imports the submodule, which rebinds a.b to the module; the lazy one binds the attribute and never imports the submodule. json does not import json.tool, so the assignment below stands in for an attribute a package's __init__.py would define:

import json
json.tool = "attribute"

lazy import json.tool as lazy_c
print(type(lazy_c).__name__)  # str

import json.tool as c
print(type(c).__name__)  # module

Case B is related to #151208, which reports the same shadowing for import a.foo under -X lazy_imports=all followed by an attribute access a.foo, resolved in Objects/moduleobject.c. That script now passes on main; this statement still fails, because it is resolved on a different path.

Possible root cause

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 _PyEval_LazyImportFrom (Python/ceval.c:3329-3384) replaces it with one holding lz_from = "a", lz_attr = "b". That is the same placeholder lazy from a import b produces.

Reification in _PyImport_LoadLazyImportTstate (Python/import.c:3902-4093) therefore runs __import__("a", fromlist=("b",)) followed by getattr(a, "b") for both statements. Nothing records that the statement was an import, so the module a.b is never imported by its own name: the "is not a package" check never runs, and an existing attribute b is never overwritten by the submodule.

This is not #157614, which is about the sys.modules fast path in _PyEval_LazyImportFrom consulting the wrong module. The present issue reproduces with the fix in #157626 applied.

CPython versions tested on:

3.15

Operating systems tested on:

Linux

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne damit, _PyEval_LazyImportFrom in Python/ceval.c und _PyImport_LoadLazyImportTstate in Python/import.c zu lesen, und reproduziere anschließend die Beispiele Case A und Case B aus dem Issue unter Python 3.15. Als erledigt gilt die Aufgabe, wenn lazy import a.b as c der Semantik eines eager import folgt: Es importiert a.b nach Namen, löst bei einem Nicht-Modul-Subpfad einen Fehler aus und verwendet kein überschattendes Attribut.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
backend
Issue-Typ
Bug
Schwierigkeit
3/5
Geschätzter Aufwand
1-2 Tage
Aktivitätsstatus
Aktiv
Klarheit
Klar beschrieben
Anfängerfreundlichkeit
68/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.