Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion Include/internal/pycore_import.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ PyAPI_FUNC(PyObject *) _PyImport_LoadLazyImportTstate(
// new reference, NULL without an exception if absent, or NULL on error.
// Set recheck_dict if a concurrent lookup may have already bound the child.
extern PyObject * _PyImport_TryLoadLazySubmodule(
PyObject *module, PyObject *attr_name, int *recheck_dict);
PyObject *module, PyObject *attr_name, int suppress, int *recheck_dict);
extern int _PyImport_ClearLazySubmodule(
PyThreadState *tstate, PyObject *name, int bind);
extern PyObject * _PyImport_LazyImportModuleLevelObject(
PyThreadState *tstate, PyObject *name, PyObject *builtins,
PyObject *globals, PyObject *locals, PyObject *fromlist, int level);
Expand Down
6 changes: 4 additions & 2 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,9 +356,11 @@ struct _import_state {
// lazily imported. This value is only for debugging/introspection
// purposes and is not used by the runtime.
PyObject *lazy_modules;
// A dict mapping package names to a set of submodule names that
// may need to be imported when an attribute is missing on the package.
// Package names map to pending children: declarations for plain imports,
// or None for from-import names that may be ordinary attributes.
PyObject *lazy_pending_submodules;
// Avoid pending-child work for ordinary cached imports.
int has_lazy_submodules;
#ifdef Py_GIL_DISABLED
PyMutex lazy_mutex;
#endif
Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_lazyimportobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ PyAPI_FUNC(PyObject *) _PyLazyImport_New(
struct _PyInterpreterFrame *frame, PyObject *builtins,
PyObject *name, PyObject *fromlist);

extern PyObject *_PyLazyImport_LoadChild(
PyThreadState *tstate, PyObject *declaration, PyObject *name);
extern int _PyLazyImport_IsActive(PyObject *declaration);

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

// Resolve a placeholder and replace its binding if it is unchanged or holds
Expand Down
115 changes: 114 additions & 1 deletion Lib/test/test_lazy_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,119 @@ def test_lazy_with_from(self):
self.assertNotIn("test.test_lazy_import.data.basic2", sys.modules)


@support.requires_subprocess()
class IndependentSubmoduleTests(LazyImportTestCase):
def check(self, code):
assert_python_ok('-c', textwrap.dedent(code))

def test_siblings(self):
for package, children in (
('test.test_lazy_import.data.pkg', ('b', 'bar')),
('test.test_lazy_import.data.metasyntactic.foo', ('ack', 'bar')),
):
for first, second in (children, children[::-1]):
with self.subTest(package=package, first=first):
self.check(f"""
import sys
lazy import {package}.{first}
lazy import {package}.{second}
assert {package}.{first} is sys.modules['{package}.{first}']
assert '{package}.{second}' not in sys.modules
assert {package}.{second} is sys.modules['{package}.{second}']
""")

def test_star_import(self):
self.check("""
lazy import urllib.nonexistent
lazy import urllib
assert urllib.__name__ == 'urllib'
from urllib import *
""")

def test_deleted_child(self):
self.check("""
lazy from xml import dom
assert dom.__name__ == 'xml.dom'
import xml
del xml.dom
assert not hasattr(xml, 'dom')
Comment thread
Yhg1s marked this conversation as resolved.
""")

def test_cached_import_releases_builtins(self):
for statement in ('lazy from xml.dom import Node',
'lazy import xml.dom.minidom'):
with self.subTest(statement=statement):
self.check(f"""
import builtins, gc, weakref
import xml.dom.minidom
class Payload:
pass
payload = Payload()
reference = weakref.ref(payload)
namespace = {{'__builtins__': dict(vars(builtins), payload=payload)}}
exec({statement!r}, namespace)
exec('Node' if 'from' in {statement!r} else 'xml.dom.minidom', namespace)
del namespace, payload
gc.collect()
assert reference() is None
""")

def test_invalid_descendant_retries(self):
self.check("""
lazy import test.test_lazy_import.data.pkg.b.foo
for _ in range(2):
try:
test.test_lazy_import.data.pkg.b.foo
except ModuleNotFoundError:
pass
else:
raise AssertionError('imported an attribute as a module')
import test.test_lazy_import.data.pkg.b
assert callable(test.test_lazy_import.data.pkg.b.foo)
""")

def test_namespace_import_hooks(self):
self.check("""
import builtins
first = {'__builtins__': vars(builtins).copy()}
second = {'__builtins__': vars(builtins).copy()}
exec('lazy import xml.dom', first)
exec('lazy import xml.dom', second)
def denied(*args):
raise AssertionError('used another namespace import hook')
second['__builtins__']['__import__'] = denied
exec('assert xml.dom.__name__ == "xml.dom"', first)
""")

def test_retry_after_recursive_access(self):
self.check("""
import sys, xml
from importlib.machinery import ModuleSpec
class Loader:
calls = 0
def find_spec(self, name, path=None, target=None):
if name == 'xml.broken':
return ModuleSpec(name, self)
def create_module(self, spec):
return None
def exec_module(self, module):
self.calls += 1
xml.broken
raise RuntimeError('failed initialization')
loader = Loader()
sys.meta_path.insert(0, loader)
lazy import xml.broken
for _ in range(2):
try:
xml.broken
except RuntimeError as exc:
assert str(exc) == 'failed initialization'
else:
raise AssertionError('returned a failed partial module')
assert loader.calls == 2
""")


class PackageTests(LazyImportTestCase):
"""Tests for lazy imports with packages."""

Expand Down Expand Up @@ -785,7 +898,7 @@ def test_chained_exception_import_shows_notes(self, name):
lazy import {name}

try:
_ = test
_ = {name}
except ValueError as e:
assert any(
note.startswith("lazy import of '{name}' declared in ")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Resolve lazily imported sibling submodules independently, so accessing one
does not import another merely because its declaration came last.
127 changes: 126 additions & 1 deletion Objects/lazyimportobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "pycore_lazyimportobject.h"
#include "pycore_long.h"
#include "pycore_moduleobject.h"
#include "pycore_pyatomic_ft_wrappers.h"
#include "pycore_pyerrors.h"
#include "pycore_traceback.h"
#include "pycore_tstate.h"
Expand All @@ -25,6 +26,8 @@ typedef struct {
// Declaration location.
PyCodeObject *lz_code;
int lz_instr_offset;
// The root was accessed, so its declared path requires import semantics.
int lz_active;
} PyLazyImportObject;

#define PyLazyImportObject_CAST(op) ((PyLazyImportObject *)(op))
Expand Down Expand Up @@ -70,6 +73,7 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,

m->lz_code = NULL;
m->lz_instr_offset = -1;
m->lz_active = 0;

if (frame != NULL) {
m->lz_code = (PyCodeObject *)Py_NewRef(_PyFrame_GetCode(frame));
Expand Down Expand Up @@ -429,10 +433,81 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject *lazy_import,
PyErr_SetString(PyExc_ImportError, "__import__ not found");
goto done;
}
PyObject *name = Py_NewRef(root->lz_from);
if (first == NULL &&
(fromlist == Py_None ||
(PyTuple_Check(fromlist) && PyTuple_GET_SIZE(fromlist) == 0)) &&
_PyImport_IsDefaultImportFunc(tstate->interp, import_func) &&
Comment thread
Yhg1s marked this conversation as resolved.
root->lz_builtins == tstate->interp->builtins) {
// A plain import binds the root package. Its pending children are
// imported separately when their attributes are accessed.
Py_ssize_t dot = PyUnicode_FindChar(
name, '.', 0, PyUnicode_GET_LENGTH(name), 1);
if (dot >= 0) {
// Preserve eager traversal through aliased or non-package parents.
int regular = 1;
int complete = 1;
Py_ssize_t end = PyUnicode_GET_LENGTH(name);
for (Py_ssize_t i = dot; i >= 0 && regular;
i = PyUnicode_FindChar(name, '.', i + 1, end, 1)) {
PyObject *prefix = PyUnicode_Substring(name, 0, i);
PyObject *cached = prefix == NULL ? NULL :
lazy_import_get_loaded_attr(tstate, prefix, &_Py_ID(__name__));
if (cached == NULL) {
complete = 0;
Py_XDECREF(prefix);
break;
}
PyObject *path = lazy_import_get_loaded_attr(
tstate, prefix, &_Py_ID(__path__));
regular = path != NULL && PyUnicode_Check(cached) &&
PyUnicode_Compare(cached, prefix) == 0;
Py_XDECREF(path);
Py_DECREF(cached);
Py_DECREF(prefix);
}
PyObject *loaded = lazy_import_get_loaded_attr(
tstate, name, &_Py_ID(__name__));
if (regular && (!complete || loaded == NULL) && !PyErr_Occurred()) {
Py_SETREF(name, PyUnicode_Substring(name, 0, dot));
}
Py_XDECREF(loaded);
if (PyErr_Occurred()) {
Py_CLEAR(name);
}
}
else if (dot == -2) {
Py_CLEAR(name);
}
if (name == NULL) {
goto done;
}
}
obj = _PyEval_ImportNameWithImport(
tstate, import_func, globals, globals,
root->lz_from, fromlist, _PyLong_GetZero()
name, fromlist, _PyLong_GetZero()
);
if (obj != NULL && name != root->lz_from) {
FT_ATOMIC_STORE_INT_RELAXED(root->lz_active, 1);
FT_ATOMIC_STORE_INT_RELAXED(tstate->interp->imports.has_lazy_submodules, 1);
Comment thread
Yhg1s marked this conversation as resolved.
int package = PyModule_CheckExact(obj) ?
PyDict_Contains(_PyModule_GetDict(obj), &_Py_ID(__path__)) : 0;
if (package < 0) {
Py_CLEAR(obj);
}
else if (!package) {
Py_DECREF(obj);
obj = _PyEval_ImportNameWithImport(
tstate, import_func, globals, globals,
root->lz_from, fromlist, _PyLong_GetZero());
}
}
if (obj != NULL && name == root->lz_from &&
_PyImport_IsDefaultImportFunc(tstate->interp, import_func) &&
_PyImport_ClearLazySubmodule(tstate, name, 0) < 0) {
Py_CLEAR(obj);
}
Py_DECREF(name);
obj = lazy_import_resolve_result(tstate, obj);
// The normal importer may publish this module on its parent. Custom
// hooks retain control of their own assignments to the parent.
Expand Down Expand Up @@ -476,6 +551,56 @@ _PyImport_LoadLazyImportTstate(PyThreadState *tstate, PyObject *lazy_import)
return lazy_import_resolve_impl(tstate, lazy_import, NULL);
}

// Resolve a pending child as an aliased import, preserving its declaration.
PyObject *
_PyLazyImport_LoadChild(PyThreadState *tstate, PyObject *declaration,
PyObject *name)
{
PyLazyImportObject *source = (PyLazyImportObject *)declaration;
Comment thread
Yhg1s marked this conversation as resolved.
Outdated
PyObject *lz = _PyLazyImport_New(NULL, source->lz_builtins, name, NULL);
Py_ssize_t end = PyUnicode_GET_LENGTH(name);
Py_ssize_t dot = PyUnicode_FindChar(name, '.', 0, end, 1);
if (dot == -2) {
Py_CLEAR(lz);
}
while (lz != NULL && dot >= 0) {
Py_ssize_t start = dot + 1;
dot = PyUnicode_FindChar(name, '.', start, end, 1);
if (dot == -2) {
Py_CLEAR(lz);
break;
}
PyObject *attr = PyUnicode_Substring(name, start, dot < 0 ? end : dot);
PyObject *next = attr == NULL ? NULL :
_PyLazyImport_New(NULL, NULL, lz, attr);
Py_XDECREF(attr);
Py_SETREF(lz, next);
}
if (lz == NULL) {
return NULL;
}
PyLazyImportObject *child = (PyLazyImportObject *)lz;
Comment thread
Yhg1s marked this conversation as resolved.
Outdated
child->lz_code = (PyCodeObject *)Py_XNewRef(source->lz_code);
child->lz_instr_offset = source->lz_instr_offset;
PyObject *result = _PyImport_LoadLazyImportTstate(tstate, lz);
Py_DECREF(lz);
if (result != NULL && PyModule_Check(result) &&
PyUnicode_Compare(source->lz_from, name) != 0 &&
!PyDict_Contains(_PyModule_GetDict(result), &_Py_ID(__path__))) {
// Reaching a non-package must still reject an invalid dotted import.
Py_DECREF(result);
return _PyLazyImport_LoadChild(tstate, declaration, source->lz_from);
}
return result;
}

int
_PyLazyImport_IsActive(PyObject *declaration)
{
return FT_ATOMIC_LOAD_INT_RELAXED(
Comment thread
Yhg1s marked this conversation as resolved.
((PyLazyImportObject *)declaration)->lz_active);
Comment thread
Yhg1s marked this conversation as resolved.
Outdated
}

// Loading pkg.child can replace a placeholder in pkg.child with the module
// before a from-import retrieves the value that belongs in that binding.
// This is an optimization that can be safely skipped.
Expand Down
3 changes: 2 additions & 1 deletion Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1338,7 +1338,8 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress)
}
assert(m->md_dict != NULL);
int recheck_dict;
attr = _PyImport_TryLoadLazySubmodule((PyObject *)m, name, &recheck_dict);
attr = _PyImport_TryLoadLazySubmodule((PyObject *)m, name, suppress,
&recheck_dict);
if (attr != NULL || PyErr_Occurred()) {
return attr;
}
Expand Down
Loading
Loading