Skip to content

Commit 0db3d59

Browse files
Allow multiphase modules to be re-imported (#5782)
* Failing unit test * Potential fix for the issue of re-importing a multi-phase module - When a module is successfully imported and exec'd, save its handle in a dict in the interpreter state - Use a special Py_mod_create slot to look in the cache and return the cached handle if it is in the cache - Don't re-run the user exec function if the module is in the interpreter's cache (implying it was already successfully imported) * Oops, need to inline these. * Clang-Tidy fixes * Oops, debug code * Add xfail for this GraalPy bug * Remove static from these function defs, it was a cut-and-paste error in the first place. * Fix test comment * Proper error handling * Oops * Split up this line, but still just ignore failure .. if the module doesn't have the right properties to check the cache then just allow exec to run. * Clean up - already looked up the name, just use that. * Some compilers complain if the pointer isn't taken here, weird. * Allow attribute errors to be thrown here, will be converted to import errors by the exception handler. * Remove bogus incref, unconditionally expect a __spec__.name on the module * Add PR to test comment * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 780ec11 commit 0db3d59

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

include/pybind11/detail/common.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,10 @@ PyModuleDef_Init should be treated like any other PyObject (so not shared across
455455
int PYBIND11_CONCAT(pybind11_exec_, name)(PyObject * pm) { \
456456
try { \
457457
auto m = pybind11::reinterpret_borrow<::pybind11::module_>(pm); \
458-
PYBIND11_CONCAT(pybind11_init_, name)(m); \
458+
if (!pybind11::detail::get_cached_module(m.attr("__spec__").attr("name"))) { \
459+
PYBIND11_CONCAT(pybind11_init_, name)(m); \
460+
pybind11::detail::cache_completed_module(m); \
461+
} \
459462
return 0; \
460463
} \
461464
PYBIND11_CATCH_INIT_EXCEPTIONS \

include/pybind11/pybind11.h

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,18 +1329,71 @@ inline void *multi_interp_slot(F &&, O &&...o) {
13291329
}
13301330
#endif
13311331

1332+
/*
1333+
Return a borrowed reference to the named module if it has been successfully initialized within this
1334+
interpreter before. nullptr if it has not been successfully initialized.
1335+
*/
1336+
inline PyObject *get_cached_module(pybind11::str const &nameobj) {
1337+
dict state = detail::get_python_state_dict();
1338+
if (!state.contains("__pybind11_module_cache")) {
1339+
return nullptr;
1340+
}
1341+
dict cache = state["__pybind11_module_cache"];
1342+
if (!cache.contains(nameobj)) {
1343+
return nullptr;
1344+
}
1345+
return cache[nameobj].ptr();
1346+
}
1347+
1348+
/*
1349+
Add successfully initialized a module object to the internal cache.
1350+
1351+
The module must have a __spec__ attribute with a name attribute.
1352+
*/
1353+
inline void cache_completed_module(pybind11::object const &mod) {
1354+
dict state = detail::get_python_state_dict();
1355+
if (!state.contains("__pybind11_module_cache")) {
1356+
state["__pybind11_module_cache"] = dict();
1357+
}
1358+
state["__pybind11_module_cache"][mod.attr("__spec__").attr("name")] = mod;
1359+
}
1360+
1361+
/*
1362+
A Py_mod_create slot function which will return the previously created module from the cache if one
1363+
exists, and otherwise will create a new module object.
1364+
*/
1365+
inline PyObject *cached_create_module(PyObject *spec, PyModuleDef *) {
1366+
(void) &cache_completed_module; // silence unused-function warnings, it is used in a macro
1367+
1368+
auto nameobj = getattr(reinterpret_borrow<object>(spec), "name", none());
1369+
if (nameobj.is_none()) {
1370+
set_error(PyExc_ImportError, "module spec is missing a name");
1371+
return nullptr;
1372+
}
1373+
1374+
auto *mod = get_cached_module(nameobj);
1375+
if (mod) {
1376+
Py_INCREF(mod);
1377+
} else {
1378+
mod = PyModule_NewObject(nameobj.ptr());
1379+
}
1380+
return mod;
1381+
}
1382+
13321383
/// Must be a POD type, and must hold enough entries for all of the possible slots PLUS ONE for
13331384
/// the sentinel (0) end slot.
1334-
using slots_array = std::array<PyModuleDef_Slot, 4>;
1385+
using slots_array = std::array<PyModuleDef_Slot, 5>;
13351386

13361387
/// Initialize an array of slots based on the supplied exec slot and options.
13371388
template <typename... Options>
1338-
static slots_array init_slots(int (*exec_fn)(PyObject *), Options &&...options) noexcept {
1389+
inline slots_array init_slots(int (*exec_fn)(PyObject *), Options &&...options) noexcept {
13391390
/* NOTE: slots_array MUST be large enough to hold all possible options. If you add an option
13401391
here, you MUST also increase the size of slots_array in the type alias above! */
13411392
slots_array slots;
13421393
size_t next_slot = 0;
13431394

1395+
slots[next_slot++] = {Py_mod_create, reinterpret_cast<void *>(&cached_create_module)};
1396+
13441397
if (exec_fn != nullptr) {
13451398
slots[next_slot++] = {Py_mod_exec, reinterpret_cast<void *>(exec_fn)};
13461399
}

tests/test_modules.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ def test_importing():
7171
assert OD is OrderedDict
7272

7373

74+
def test_reimport():
75+
import sys
76+
77+
import pybind11_tests as x
78+
79+
del sys.modules["pybind11_tests"]
80+
81+
# exercise pybind11::detail::get_cached_module()
82+
import pybind11_tests as y
83+
84+
assert x is y
85+
86+
87+
@pytest.mark.xfail(
88+
"env.GRAALPY",
89+
reason="TODO should be fixed on GraalPy side (failure was introduced by pr #5782)",
90+
)
7491
def test_pydoc():
7592
"""Pydoc needs to be able to provide help() for everything inside a pybind11 module"""
7693
import pydoc

0 commit comments

Comments
 (0)