Skip to content

Commit f995729

Browse files
committed
gh-158140: Resolve lazy submodules independently with custom import hooks
1 parent 587b7a5 commit f995729

16 files changed

Lines changed: 3618 additions & 155 deletions

File tree

‎Doc/reference/simple_stmts.rst‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,36 @@ If an error occurs during module loading (such as :exc:`ImportError` or
917917
:exc:`SyntaxError`), it is raised at the point where the lazy import is first
918918
used, not at the import statement itself.
919919

920+
Plain dotted lazy imports resolve the root package before their submodules,
921+
including when a custom :func:`__import__` hook is present. For example,
922+
``lazy import pkg.a`` calls the hook with ``pkg`` on first use of ``pkg``.
923+
Access to a pending ``pkg.a`` then calls the hook with ``pkg.a``.
924+
For packages stored under their declared names, other declared submodules
925+
remain unresolved. Existing attributes are accessed normally. Aliased imports,
926+
such as ``lazy import pkg.a as a``, still request the full module name.
927+
928+
The root uses the declaring namespace's builtins. A pending child uses the
929+
declaration whose builtins match the namespace that accesses it. If no
930+
declaration matches, Python uses the first namespace that declared that child.
931+
Custom hooks control the attributes of objects that they supply outside
932+
:data:`sys.modules`.
933+
934+
.. impl-detail::
935+
936+
Assigning plain lazy placeholders to the same root name may merge their
937+
declaration metadata. This includes assignments from containers or namespace
938+
dictionaries and need not preserve placeholder identity.
939+
940+
Once a root resolves, its pending children retain their declaring builtins
941+
mappings. Private modules own this metadata and release it when collected.
942+
943+
Custom import hook exceptions propagate through optional attribute access,
944+
including :func:`hasattr` and :func:`getattr` with a default value.
945+
946+
Canonical modules under another name and non-packages retain full-path
947+
validation, so an invalid import such as ``lazy import math.pi`` still raises
948+
an exception.
949+
920950
See :pep:`810` for the full specification of lazy imports.
921951

922952
.. versionadded:: 3.15

‎Include/internal/pycore_import.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ extern PyObject * _PyImport_TryLoadLazySubmodule(
4444
PyObject *module, PyObject *attr_name, int suppress, int *recheck_dict);
4545
extern int _PyImport_ClearLazySubmodule(
4646
PyThreadState *tstate, PyObject *name, int bind);
47+
extern int _PyImport_RefreshLazySubmodules(
48+
PyThreadState *tstate, PyObject *source, PyObject *root, PyObject *prefix);
4749
extern PyObject * _PyImport_LazyImportModuleLevelObject(
4850
PyThreadState *tstate, PyObject *name, PyObject *builtins,
4951
PyObject *globals, PyObject *locals, PyObject *fromlist, int level);

‎Include/internal/pycore_lazyimportobject.h‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,17 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_New(
1919
PyObject *name, PyObject *fromlist);
2020

2121
extern PyObject *_PyLazyImport_LoadChild(
22-
PyThreadState *tstate, PyObject *declaration, PyObject *name);
22+
PyThreadState *tstate, PyObject *declaration, PyObject *name,
23+
PyObject *import_func);
24+
// Borrowed references from the original import declaration.
25+
extern PyObject *_PyLazyImport_GetBuiltins(PyObject *declaration);
26+
extern PyObject *_PyLazyImport_GetName(PyObject *declaration);
27+
// Return a new binding that retains earlier plain imports for the same root.
28+
extern PyObject *_PyLazyImport_MergeBindings(
29+
PyObject *value, PyObject *previous, PyObject *name);
30+
// Store a lazy binding in a dictionary, retaining concurrent declarations.
31+
PyAPI_FUNC(int) _PyLazyImport_Store(
32+
PyObject *ns, PyObject *name, PyObject *value);
2333
extern int _PyLazyImport_IsActive(PyObject *declaration);
2434

2535
extern int _PyLazyImport_IsResolving(PyThreadState *tstate, PyObject *op);

‎Include/internal/pycore_moduleobject.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ typedef int (*_Py_modexecfunc)(PyObject *);
2626
typedef struct {
2727
PyObject_HEAD
2828
PyObject *md_dict;
29+
// Pending imports on module objects supplied outside sys.modules.
30+
// An empty dictionary still identifies a module supplied privately.
31+
PyObject *md_lazy_submodules;
2932
void *md_state;
3033
PyObject *md_weaklist;
3134
// for logging purposes after md_dict is cleared
3235
PyObject *md_name;
3336
bool md_token_is_def; /* if true, `md_token` is the PyModuleDef */
37+
// Retain global pending imports after the module leaves sys.modules.
38+
uint8_t md_lazy_global_lookup;
3439
#ifdef Py_GIL_DISABLED
3540
bool md_requires_gil;
3641
#endif
@@ -72,6 +77,11 @@ static inline PyObject* _PyModule_GetDict(PyObject *mod) {
7277
}
7378

7479
extern PyObject * _PyModule_GetFilenameObject(PyObject *);
80+
// Both return new references; Get returns NULL without error if absent.
81+
extern PyObject *_PyModule_GetLazySubmodules(PyObject *module);
82+
extern PyObject *_PyModule_EnsureLazySubmodules(PyObject *module);
83+
extern void _PyModule_EnableLazyGlobalLookup(PyObject *module);
84+
extern int _PyModule_HasLazyGlobalLookup(PyObject *module);
7585
extern Py_ssize_t _PyModule_GetFilenameUTF8(
7686
PyObject *module,
7787
char *buffer,

‎Include/internal/pycore_tstate.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ typedef struct _PyThreadStateImpl {
5151
PyObject *asyncio_running_loop; // Strong reference
5252
PyObject *asyncio_running_task; // Strong reference
5353

54-
// Reused for the thread's lifetime; owns placeholders while they resolve.
54+
// Reused for the thread's lifetime. Owns resolving declarations and
55+
// (builtins identity, fullname) keys for pending child imports.
5556
PyObject *lazy_imports;
5657

5758
// Distinguishes between yield and return from PyEval_EvalFrame().

0 commit comments

Comments
 (0)