Skip to content

Commit 748ae22

Browse files
authored
Add missing error handling to module_::def_submodule (#3973)
* Add missing error handling to module_::def_submodule * Add test_def_submodule_failures * PyPy only: Skip test with trigger for PyModule_GetName() failure. * Reapply minor fix that accidentally got lost in transfer from PR #3964
1 parent 68f8010 commit 748ae22

File tree

3 files changed

+42
-3
lines changed

3 files changed

+42
-3
lines changed

include/pybind11/pybind11.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,9 +1173,16 @@ class module_ : public object {
11731173
py::module_ m3 = m2.def_submodule("subsub", "A submodule of 'example.sub'");
11741174
\endrst */
11751175
module_ def_submodule(const char *name, const char *doc = nullptr) {
1176-
std::string full_name
1177-
= std::string(PyModule_GetName(m_ptr)) + std::string(".") + std::string(name);
1178-
auto result = reinterpret_borrow<module_>(PyImport_AddModule(full_name.c_str()));
1176+
const char *this_name = PyModule_GetName(m_ptr);
1177+
if (this_name == nullptr) {
1178+
throw error_already_set();
1179+
}
1180+
std::string full_name = std::string(this_name) + '.' + name;
1181+
handle submodule = PyImport_AddModule(full_name.c_str());
1182+
if (!submodule) {
1183+
throw error_already_set();
1184+
}
1185+
auto result = reinterpret_borrow<module_>(submodule);
11791186
if (doc && options::show_user_defined_docstrings()) {
11801187
result.attr("__doc__") = pybind11::str(doc);
11811188
}

tests/test_modules.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,6 @@ TEST_SUBMODULE(modules, m) {
120120

121121
return failures;
122122
});
123+
124+
m.def("def_submodule", [](py::module_ m, const char *name) { return m.def_submodule(name); });
123125
}

tests/test_modules.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import pytest
2+
3+
import env
14
from pybind11_tests import ConstructorStats
25
from pybind11_tests import modules as m
36
from pybind11_tests.modules import subsubmodule as ms
@@ -89,3 +92,30 @@ def test_builtin_key_type():
8992
keys = __builtins__.__dict__.keys()
9093

9194
assert {type(k) for k in keys} == {str}
95+
96+
97+
@pytest.mark.xfail("env.PYPY", reason="PyModule_GetName()")
98+
def test_def_submodule_failures():
99+
sm = m.def_submodule(m, b"ScratchSubModuleName") # Using bytes to show it works.
100+
assert sm.__name__ == m.__name__ + "." + "ScratchSubModuleName"
101+
malformed_utf8 = b"\x80"
102+
if env.PYPY:
103+
# It is not worth the effort finding a trigger for a failure when running with PyPy.
104+
pytest.skip("Sufficiently exercised on platforms other than PyPy.")
105+
else:
106+
# Meant to trigger PyModule_GetName() failure:
107+
sm_name_orig = sm.__name__
108+
sm.__name__ = malformed_utf8
109+
try:
110+
with pytest.raises(Exception):
111+
# Seen with Python 3.9: SystemError: nameless module
112+
# But we do not want to exercise the internals of PyModule_GetName(), which could
113+
# change in future versions of Python, but a bad __name__ is very likely to cause
114+
# some kind of failure indefinitely.
115+
m.def_submodule(sm, b"SubSubModuleName")
116+
finally:
117+
# Clean up to ensure nothing gets upset by a module with an invalid __name__.
118+
sm.__name__ = sm_name_orig # Purely precautionary.
119+
# Meant to trigger PyImport_AddModule() failure:
120+
with pytest.raises(UnicodeDecodeError):
121+
m.def_submodule(sm, malformed_utf8)

0 commit comments

Comments
 (0)