Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from unittest import mock
import _imp

from test import support
from test.support import os_helper
from test.support import (
STDLIB_DIR,
Expand Down Expand Up @@ -416,6 +417,27 @@ def test_from_import_missing_attr_path_is_canonical(self):
self.assertIn(cm.exception.name, {'posixpath', 'ntpath'})
self.assertIsNotNone(cm.exception)

@support.requires_subprocess()
def test_from_import_module_attr_from_non_package(self):
code = textwrap.dedent("""
import sys
import types

module_name = 'test_from_import_module_attr_from_non_package'
child_name = f'{module_name}.child'
module = types.ModuleType(module_name)
child = types.ModuleType(child_name)
module.child = child
sys.modules[module_name] = module

# A plain module can expose a module-valued attribute without
# being a package, so from-import must return the attribute as-is.
from test_from_import_module_attr_from_non_package import child as imported
assert imported is child
assert child_name not in sys.modules, child_name
""")
script_helper.assert_python_ok("-c", code)

def test_from_import_star_invalid_type(self):
with ready_to_import() as (name, path):
with open(path, 'w', encoding='utf-8') as f:
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,36 @@ def test_package_from_import_with_module_getattr(self):
""")
assert_python_ok("-c", code)

@support.requires_subprocess()
def test_lazy_import_reimports_submodule_evicted_from_sys_modules(self):
"""A submodule evicted from sys.modules but still cached on its parent
must be re-imported, like eager 'import a.b.c as t' is."""
code = textwrap.dedent("""
import sys
modname = "test.test_lazy_import.data.pkg.bar"
import test.test_lazy_import.data.pkg.bar
del sys.modules[modname]
lazy import test.test_lazy_import.data.pkg.bar as t
t.f()
assert modname in sys.modules, modname
""")
assert_python_ok("-c", code)

@support.requires_subprocess()
def test_lazy_from_import_reimports_submodule_evicted_from_sys_modules(self):
"""Same as above for 'from a.b import c': the stale parent attribute
must not shadow a re-import after sys.modules eviction."""
code = textwrap.dedent("""
import sys
modname = "test.test_lazy_import.data.pkg.bar"
import test.test_lazy_import.data.pkg.bar
del sys.modules[modname]
lazy from test.test_lazy_import.data.pkg import bar
bar.f()
assert modname in sys.modules, modname
""")
assert_python_ok("-c", code)


class DunderLazyImportTests(LazyImportTestCase):
"""Tests for __lazy_import__ builtin function."""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix lazy submodule imports so package submodules removed from
:data:`sys.modules` are properly re-imported.
95 changes: 92 additions & 3 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -3123,13 +3123,99 @@ _PyEval_LazyImportName(PyThreadState *tstate, PyObject *builtins,
return res;
}

static PyObject *
import_from_resolve_module_attr(PyThreadState *tstate, PyObject *package_name,
PyObject *name, PyObject *attr)
{
if (!PyModule_Check(attr) || package_name == NULL
|| !PyUnicode_Check(package_name)) {
return Py_NewRef(attr);
}

/* Only packages can legitimately republish importable submodules here.
Plain modules may expose arbitrary module-valued attributes. */
PyObject *package = PyImport_GetModule(package_name);
if (package == NULL) {
if (_PyErr_Occurred(tstate)) {
return NULL;
}
return Py_NewRef(attr);
}

int is_package = PyObject_HasAttrWithError(package, &_Py_ID(__path__));
Py_DECREF(package);
if (is_package <= 0) {
if (is_package < 0) {
return NULL;
}
return Py_NewRef(attr);
}

PyObject *fullmodname = PyUnicode_FromFormat("%U.%U", package_name, name);
if (fullmodname == NULL) {
return NULL;
}

PyObject *attr_name;
if (PyObject_GetOptionalAttr(attr, &_Py_ID(__name__), &attr_name) < 0) {
Py_DECREF(fullmodname);
return NULL;
}

int matches = (attr_name != NULL && PyUnicode_Check(attr_name))
? PyObject_RichCompareBool(attr_name, fullmodname, Py_EQ)
: 0;
Py_XDECREF(attr_name);
if (matches <= 0) {
/* Not the canonical submodule (matches == 0), or compare failed
(matches < 0, exception set). */
Py_DECREF(fullmodname);
return matches < 0 ? NULL : Py_NewRef(attr);
}

/* If the package still caches a submodule object after its entry was
removed from sys.modules, import the canonical submodule again. */
PyObject *submod = PyImport_GetModule(fullmodname);
if (submod == NULL && !_PyErr_Occurred(tstate)) {
PyObject *imported = PyImport_ImportModuleLevelObject(
fullmodname, NULL, NULL, NULL, 0);
if (imported == NULL) {
Py_DECREF(fullmodname);
return NULL;
}
Py_DECREF(imported);
submod = PyImport_GetModule(fullmodname);
}
Py_DECREF(fullmodname);
if (submod != NULL) {
return submod;
}
if (_PyErr_Occurred(tstate)) {
return NULL;
}
return Py_NewRef(attr);
}

PyObject *
_PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name)
{
PyObject *x;
PyObject *fullmodname, *mod_name, *origin, *mod_name_or_unknown, *errmsg, *spec;

if (PyObject_GetOptionalAttr(v, name, &x) != 0) {
if (x != NULL && PyModule_Check(x)) {
PyObject *resolved;
if (PyObject_GetOptionalAttr(v, &_Py_ID(__name__), &mod_name) < 0) {
Py_DECREF(x);
return NULL;
}
/* If this is a cached submodule, resolve it through sys.modules
so from-import repairs stale package entries. */
resolved = import_from_resolve_module_attr(tstate, mod_name, name, x);
Py_XDECREF(mod_name);
Py_DECREF(x);
return resolved;
}
return x;
}
/* Issue #17636: in case this failed because of a circular relative
Expand Down Expand Up @@ -3301,8 +3387,8 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
PyLazyImportObject *d = (PyLazyImportObject *)v;
PyObject *mod = PyImport_GetModule(d->lz_from);
if (mod != NULL) {
// Check if the module already has the attribute, if so, resolve it
// eagerly.
/* If the parent is already imported, resolve any module-valued
attribute through the same stale-submodule check. */
if (PyModule_Check(mod)) {
PyObject *mod_dict = PyModule_GetDict(mod);
if (mod_dict != NULL) {
Expand All @@ -3311,8 +3397,11 @@ _PyEval_LazyImportFrom(PyThreadState *tstate, _PyInterpreterFrame *frame, PyObje
return NULL;
}
if (ret != NULL) {
PyObject *resolved = import_from_resolve_module_attr(
tstate, d->lz_from, name, ret);
Py_DECREF(ret);
Py_DECREF(mod);
return ret;
return resolved;
}
}
}
Expand Down
Loading