Skip to content

Commit 8869984

Browse files
arman-novikovpre-commit-ci[bot]rwgk
authored
scoped_interpreter. overloaded constructor: PyConfig param (pybind#4330)
* scoped_interpreter overloaded ctor: PyConfig param * style: pre-commit fixes * refact: some logics extracted into funcs (precheck_interpreter, _initialize_interpreter); config_guard * style: pre-commit fixes * refact: scoped_config, some funcs hidden in detail ns * refact: macro PYBIND11_PYCONFIG_SUPPORT_PY_VERSION + undef * feat: PYBIND11_PYCONFIG_SUPPORT_PY_VERSION set to 3.8 * tests: Custom PyConfig * ci: python 3.6 -> 3.8 * ci: reverted py 38 back to 36; refact: initialize_interpreter overloads * style: pre-commit fixes * fix: readability-implicit-bool-conversion * refact: each initialize_interpreter overloads in pybind11 ns * Move `initialize_interpreter_pre_pyconfig()` into the `detail` namespace. Move the `PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX` define down to where it is used for the first time, and check if it is defined already, so that it is possible to customize from the compilation command line, just in case there is some unforeseen issue for Python 3.8, 3.9, 3.10. * tests: Add program dir to path, Custom PyConfig with argv * refact: clang-formatted * tests: Add-program-dir-to-path covers both scoped_interpreter overloads * tests: Add-program-dir-to-path fixed * tests: Add-program-dir-to-path py_version dependant validation Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]>
1 parent 06003e8 commit 8869984

File tree

2 files changed

+143
-42
lines changed

2 files changed

+143
-42
lines changed

include/pybind11/embed.h

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -86,37 +86,22 @@ inline wchar_t *widen_chars(const char *safe_arg) {
8686
return widened_arg;
8787
}
8888

89-
PYBIND11_NAMESPACE_END(detail)
90-
91-
/** \rst
92-
Initialize the Python interpreter. No other pybind11 or CPython API functions can be
93-
called before this is done; with the exception of `PYBIND11_EMBEDDED_MODULE`. The
94-
optional `init_signal_handlers` parameter can be used to skip the registration of
95-
signal handlers (see the `Python documentation`_ for details). Calling this function
96-
again after the interpreter has already been initialized is a fatal error.
97-
98-
If initializing the Python interpreter fails, then the program is terminated. (This
99-
is controlled by the CPython runtime and is an exception to pybind11's normal behavior
100-
of throwing exceptions on errors.)
101-
102-
The remaining optional parameters, `argc`, `argv`, and `add_program_dir_to_path` are
103-
used to populate ``sys.argv`` and ``sys.path``.
104-
See the |PySys_SetArgvEx documentation|_ for details.
105-
106-
.. _Python documentation: https://docs.python.org/3/c-api/init.html#c.Py_InitializeEx
107-
.. |PySys_SetArgvEx documentation| replace:: ``PySys_SetArgvEx`` documentation
108-
.. _PySys_SetArgvEx documentation: https://docs.python.org/3/c-api/init.html#c.PySys_SetArgvEx
109-
\endrst */
110-
inline void initialize_interpreter(bool init_signal_handlers = true,
111-
int argc = 0,
112-
const char *const *argv = nullptr,
113-
bool add_program_dir_to_path = true) {
89+
inline void precheck_interpreter() {
11490
if (Py_IsInitialized() != 0) {
11591
pybind11_fail("The interpreter is already running");
11692
}
93+
}
11794

118-
#if PY_VERSION_HEX < 0x030B0000
95+
#if !defined(PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX)
96+
# define PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX (0x03080000)
97+
#endif
11998

99+
#if PY_VERSION_HEX < PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX
100+
inline void initialize_interpreter_pre_pyconfig(bool init_signal_handlers,
101+
int argc,
102+
const char *const *argv,
103+
bool add_program_dir_to_path) {
104+
detail::precheck_interpreter();
120105
Py_InitializeEx(init_signal_handlers ? 1 : 0);
121106
# if defined(WITH_THREAD) && PY_VERSION_HEX < 0x03070000
122107
PyEval_InitThreads();
@@ -150,33 +135,74 @@ inline void initialize_interpreter(bool init_signal_handlers = true,
150135
auto *pysys_argv = widened_argv.get();
151136

152137
PySys_SetArgvEx(argc, pysys_argv, static_cast<int>(add_program_dir_to_path));
153-
#else
154-
PyConfig config;
155-
PyConfig_InitIsolatedConfig(&config);
156-
config.isolated = 0;
157-
config.use_environment = 1;
158-
config.install_signal_handlers = init_signal_handlers ? 1 : 0;
138+
}
139+
#endif
159140

160-
PyStatus status = PyConfig_SetBytesArgv(&config, argc, const_cast<char *const *>(argv));
161-
if (PyStatus_Exception(status)) {
141+
PYBIND11_NAMESPACE_END(detail)
142+
143+
#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX
144+
inline void initialize_interpreter(PyConfig *config,
145+
int argc = 0,
146+
const char *const *argv = nullptr,
147+
bool add_program_dir_to_path = true) {
148+
detail::precheck_interpreter();
149+
PyStatus status = PyConfig_SetBytesArgv(config, argc, const_cast<char *const *>(argv));
150+
if (PyStatus_Exception(status) != 0) {
162151
// A failure here indicates a character-encoding failure or the python
163152
// interpreter out of memory. Give up.
164-
PyConfig_Clear(&config);
165-
throw std::runtime_error(PyStatus_IsError(status) ? status.err_msg
166-
: "Failed to prepare CPython");
153+
PyConfig_Clear(config);
154+
throw std::runtime_error(PyStatus_IsError(status) != 0 ? status.err_msg
155+
: "Failed to prepare CPython");
167156
}
168-
status = Py_InitializeFromConfig(&config);
169-
PyConfig_Clear(&config);
170-
if (PyStatus_Exception(status)) {
171-
throw std::runtime_error(PyStatus_IsError(status) ? status.err_msg
172-
: "Failed to init CPython");
157+
status = Py_InitializeFromConfig(config);
158+
if (PyStatus_Exception(status) != 0) {
159+
PyConfig_Clear(config);
160+
throw std::runtime_error(PyStatus_IsError(status) != 0 ? status.err_msg
161+
: "Failed to init CPython");
173162
}
174163
if (add_program_dir_to_path) {
175164
PyRun_SimpleString("import sys, os.path; "
176165
"sys.path.insert(0, "
177166
"os.path.abspath(os.path.dirname(sys.argv[0])) "
178167
"if sys.argv and os.path.exists(sys.argv[0]) else '')");
179168
}
169+
PyConfig_Clear(config);
170+
}
171+
#endif
172+
173+
/** \rst
174+
Initialize the Python interpreter. No other pybind11 or CPython API functions can be
175+
called before this is done; with the exception of `PYBIND11_EMBEDDED_MODULE`. The
176+
optional `init_signal_handlers` parameter can be used to skip the registration of
177+
signal handlers (see the `Python documentation`_ for details). Calling this function
178+
again after the interpreter has already been initialized is a fatal error.
179+
180+
If initializing the Python interpreter fails, then the program is terminated. (This
181+
is controlled by the CPython runtime and is an exception to pybind11's normal behavior
182+
of throwing exceptions on errors.)
183+
184+
The remaining optional parameters, `argc`, `argv`, and `add_program_dir_to_path` are
185+
used to populate ``sys.argv`` and ``sys.path``.
186+
See the |PySys_SetArgvEx documentation|_ for details.
187+
188+
.. _Python documentation: https://docs.python.org/3/c-api/init.html#c.Py_InitializeEx
189+
.. |PySys_SetArgvEx documentation| replace:: ``PySys_SetArgvEx`` documentation
190+
.. _PySys_SetArgvEx documentation: https://docs.python.org/3/c-api/init.html#c.PySys_SetArgvEx
191+
\endrst */
192+
inline void initialize_interpreter(bool init_signal_handlers = true,
193+
int argc = 0,
194+
const char *const *argv = nullptr,
195+
bool add_program_dir_to_path = true) {
196+
#if PY_VERSION_HEX < PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX
197+
detail::initialize_interpreter_pre_pyconfig(
198+
init_signal_handlers, argc, argv, add_program_dir_to_path);
199+
#else
200+
PyConfig config;
201+
PyConfig_InitIsolatedConfig(&config);
202+
config.isolated = 0;
203+
config.use_environment = 1;
204+
config.install_signal_handlers = init_signal_handlers ? 1 : 0;
205+
initialize_interpreter(&config, argc, argv, add_program_dir_to_path);
180206
#endif
181207
}
182208

@@ -264,6 +290,15 @@ class scoped_interpreter {
264290
initialize_interpreter(init_signal_handlers, argc, argv, add_program_dir_to_path);
265291
}
266292

293+
#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX
294+
explicit scoped_interpreter(PyConfig *config,
295+
int argc = 0,
296+
const char *const *argv = nullptr,
297+
bool add_program_dir_to_path = true) {
298+
initialize_interpreter(config, argc, argv, add_program_dir_to_path);
299+
}
300+
#endif
301+
267302
scoped_interpreter(const scoped_interpreter &) = delete;
268303
scoped_interpreter(scoped_interpreter &&other) noexcept { other.is_valid = false; }
269304
scoped_interpreter &operator=(const scoped_interpreter &) = delete;

tests/test_embed/test_interpreter.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,72 @@ TEST_CASE("There can be only one interpreter") {
166166
py::initialize_interpreter();
167167
}
168168

169+
#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX
170+
TEST_CASE("Custom PyConfig") {
171+
py::finalize_interpreter();
172+
PyConfig config;
173+
PyConfig_InitPythonConfig(&config);
174+
REQUIRE_NOTHROW(py::scoped_interpreter{&config});
175+
{
176+
py::scoped_interpreter p{&config};
177+
REQUIRE(py::module_::import("widget_module").attr("add")(1, 41).cast<int>() == 42);
178+
}
179+
py::initialize_interpreter();
180+
}
181+
182+
TEST_CASE("Custom PyConfig with argv") {
183+
py::finalize_interpreter();
184+
{
185+
PyConfig config;
186+
PyConfig_InitIsolatedConfig(&config);
187+
char *argv[] = {strdup("a.out")};
188+
py::scoped_interpreter argv_scope{&config, 1, argv};
189+
std::free(argv[0]);
190+
auto module = py::module::import("test_interpreter");
191+
auto py_widget = module.attr("DerivedWidget")("The question");
192+
const auto &cpp_widget = py_widget.cast<const Widget &>();
193+
REQUIRE(cpp_widget.argv0() == "a.out");
194+
}
195+
py::initialize_interpreter();
196+
}
197+
#endif
198+
199+
TEST_CASE("Add program dir to path") {
200+
static auto get_sys_path_size = []() -> size_t {
201+
auto sys_path = py::module::import("sys").attr("path");
202+
return py::len(sys_path);
203+
};
204+
static auto validate_path_len = [](size_t default_len) {
205+
#if PY_VERSION_HEX < 0x030A0000
206+
// It seems a value remains in sys.path
207+
// left by the previous call of scoped_interpreter ctor.
208+
REQUIRE(get_sys_path_size() > default_len);
209+
#else
210+
REQUIRE(get_sys_path_size() == default_len + 1);
211+
#endif
212+
};
213+
py::finalize_interpreter();
214+
215+
size_t sys_path_default_size = 0;
216+
{
217+
py::scoped_interpreter scoped_interp{true, 0, nullptr, false};
218+
sys_path_default_size = get_sys_path_size();
219+
}
220+
{
221+
py::scoped_interpreter scoped_interp{}; // expected to append some to sys.path
222+
validate_path_len(sys_path_default_size);
223+
}
224+
#if PY_VERSION_HEX >= PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX
225+
{
226+
PyConfig config;
227+
PyConfig_InitPythonConfig(&config);
228+
py::scoped_interpreter scoped_interp{&config}; // expected to append some to sys.path
229+
validate_path_len(sys_path_default_size);
230+
}
231+
#endif
232+
py::initialize_interpreter();
233+
}
234+
169235
bool has_pybind11_internals_builtin() {
170236
auto builtins = py::handle(PyEval_GetBuiltins());
171237
return builtins.contains(PYBIND11_INTERNALS_ID);

0 commit comments

Comments
 (0)