Skip to content

Commit ad34fae

Browse files
committed
Keep imported attributes on their parent package
Loading a child can replace the parent binding with the module before we fetch the imported attribute. Keep the module returned by the normal importer so the shared helper can replace it in its actual parent namespace. Other values and deletions still prevent replacement, and custom hooks keep control of their assignments.
1 parent 4dc64c8 commit ad34fae

2 files changed

Lines changed: 73 additions & 4 deletions

File tree

‎Include/internal/pycore_lazyimportobject.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_New(
2020

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

23-
// Resolve a placeholder and replace its binding only if it is unchanged.
23+
// Resolve a placeholder and replace its binding if it is unchanged or holds
24+
// the child module published by the normal importer during resolution.
2425
// namespace is the source captured during lookup, before resolution runs.
2526
PyAPI_FUNC(PyObject *) _PyLazyImport_Reify(
2627
PyThreadState *tstate, PyObject *placeholder,

‎Objects/lazyimportobject.c‎

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,9 @@ _PyLazyImport_IsResolving(PyThreadState *tstate, PyObject *op)
340340
return active;
341341
}
342342

343-
PyObject *
344-
_PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
343+
static PyObject *
344+
lazy_import_resolve_impl(PyThreadState *tstate, PyObject *lazy_import,
345+
PyObject **imported_module)
345346
{
346347
PyObject *obj = NULL;
347348
PyObject *fromlist = NULL;
@@ -421,6 +422,12 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
421422
root->lz_from, fromlist, _PyLong_GetZero()
422423
);
423424
obj = lazy_import_resolve_result(tstate, obj);
425+
// The normal importer may publish this module on its parent. Custom
426+
// hooks retain control of their own assignments to the parent.
427+
if (imported_module != NULL && obj != NULL && PyModule_Check(obj) &&
428+
_PyImport_IsDefaultImportFunc(tstate->interp, import_func)) {
429+
*imported_module = Py_NewRef(obj);
430+
}
424431
if (obj != NULL && first != NULL) {
425432
// Keep the hook and root result alive until all attribute lookups finish.
426433
PyObject *from = obj;
@@ -451,21 +458,81 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
451458
return obj;
452459
}
453460

461+
PyObject *
462+
_PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
463+
{
464+
return lazy_import_resolve_impl(tstate, lazy_import, NULL);
465+
}
466+
467+
// Loading pkg.child can replace a placeholder in pkg.child with the module
468+
// before a from-import retrieves the value that belongs in that binding.
469+
static int
470+
lazy_import_replace_child(PyThreadState *tstate, PyObject *placeholder,
471+
PyObject *name, PyObject *namespace,
472+
PyObject *child, PyObject *value)
473+
{
474+
PyLazyImportObject *root = (PyLazyImportObject *)placeholder;
475+
if (!PyLazyImport_CheckExact(root->lz_from)) {
476+
return 0;
477+
}
478+
while (PyLazyImport_CheckExact(root->lz_from)) {
479+
root = (PyLazyImportObject *)root->lz_from;
480+
}
481+
Py_ssize_t end = PyUnicode_GET_LENGTH(root->lz_from);
482+
Py_ssize_t dot = PyUnicode_FindChar(root->lz_from, '.', 0, end, -1);
483+
if (dot < 0) {
484+
return dot == -1 ? 0 : -1;
485+
}
486+
if (end - dot - 1 != PyUnicode_GET_LENGTH(name)) {
487+
return 0;
488+
}
489+
int matches = PyUnicode_Tailmatch(root->lz_from, name, dot + 1, end, 1);
490+
if (matches <= 0) {
491+
return matches;
492+
}
493+
PyObject *parent_name = PyUnicode_Substring(root->lz_from, 0, dot);
494+
if (parent_name == NULL) {
495+
return -1;
496+
}
497+
PyObject *modules = Py_XNewRef(_PyImport_GetModules(tstate->interp));
498+
PyObject *parent = NULL;
499+
int rc = 0;
500+
if (modules != NULL) {
501+
rc = PyMapping_GetOptionalItem(modules, parent_name, &parent);
502+
if (rc > 0 && PyModule_Check(parent) &&
503+
_PyModule_GetDict(parent) == namespace) {
504+
rc = _PyDict_ReplaceItemIf(namespace, name, child, value);
505+
}
506+
}
507+
Py_XDECREF(parent);
508+
Py_XDECREF(modules);
509+
Py_DECREF(parent_name);
510+
return rc;
511+
}
512+
454513
PyObject *
455514
_PyLazyImport_Reify(PyThreadState *tstate, PyObject *placeholder,
456515
PyObject *name, PyObject *namespace)
457516
{
458-
PyObject *value = _PyImport_LoadLazyImportTstate(tstate, placeholder);
517+
PyObject *imported_module = NULL;
518+
PyObject *value = lazy_import_resolve_impl(
519+
tstate, placeholder, &imported_module);
459520
if (value == NULL) {
521+
Py_XDECREF(imported_module);
460522
return NULL;
461523
}
462524
int rc;
463525
if (PyDict_CheckExact(namespace)) {
464526
rc = _PyDict_ReplaceItemIf(namespace, name, placeholder, value);
527+
if (rc == 0 && imported_module != NULL) {
528+
rc = lazy_import_replace_child(
529+
tstate, placeholder, name, namespace, imported_module, value);
530+
}
465531
}
466532
else if (Py_TYPE(namespace)->tp_as_mapping == NULL ||
467533
Py_TYPE(namespace)->tp_as_mapping->mp_ass_subscript == NULL) {
468534
// Read-only namespaces can resolve a value without caching it.
535+
Py_XDECREF(imported_module);
469536
return value;
470537
}
471538
else {
@@ -483,6 +550,7 @@ _PyLazyImport_Reify(PyThreadState *tstate, PyObject *placeholder,
483550
if (rc < 0) {
484551
Py_CLEAR(value);
485552
}
553+
Py_XDECREF(imported_module);
486554
return value;
487555
}
488556

0 commit comments

Comments
 (0)