* feat!: drop support for Python 3.8 The minimum supported version is now Python 3.9. pybind11 v3.0 was the last release that supports Python 3.8. The deprecation note said that support goes away in 3.1. Remove the code paths that this makes dead: - the `_PyObject_Vectorcall` fallback in `cast.h` - the `frame->f_code` and `frame->f_back` fallbacks in `pytypes.h` - the `PyFrame_FastToLocals` path in `get_type_override` - the conditional `Py_VISIT(Py_TYPE(self))` in `tp_traverse` - `PYBIND11_PYCONFIG_SUPPORT_PY_VERSION_HEX`, the pre-PyConfig interpreter init, and the `widen_chars` helpers in `embed.h` Assisted-by: ClaudeCode:claude-opus-5 * ci(appveyor): use Python 3.9 The AppVeyor job set `PYTHON: 38`, which makes the path `C:\Python38`. The image gives Python 3.9.13 as `C:\Python39`. Assisted-by: ClaudeCode:claude-opus-5 * feat!: require MSVC 2019 or newer Python 3.9 is the new minimum, so MSVC 2017 is no longer needed. Raise the compile-time floor to _MSC_VER 1920 and remove the workarounds that only applied below it: std::launder, fold expressions, weak_from_this, aligned new/delete, the C4100 warning helper, and the func_handle syntax error. AppVeyor now builds with Visual Studio 2019. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): build against the Python that has the test packages CMake 4 has no FindPythonLibs, so pybind11 uses FindPython. FindPython reads the registry before PATH and selected `C:\Python314-x64`, but the test packages go into the Python on PATH. Pass `Python_ROOT_DIR` to name the correct one. Also set `CMAKE_ARCH`. It was never set, so the architecture came from the generator. Visual Studio 2017 defaults to Win32, but Visual Studio 2019 defaults to x64, which made this x86 job build 64-bit code. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): give the linker Python's libs directory The build compiled but failed to link with LNK1104 on a bare `python39.lib`. That name comes from the `#pragma comment(lib, ...)` in pyconfig.h, so the linker needs the directory. CMake 4 has no FindPythonLibs, and FindPython does not add it for this Debug x86 build. Put it on LIB instead. The directory listing is temporary, to confirm the library is present. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): link the release Python library in Debug The image ships python39_d.lib next to python39.lib, so FindPython picks the debug import library for a Debug build. pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib instead and the link failed with LNK1104. Name the release library for the debug slot. Setting LIB does not work, because MSBuild replaces it from the toolset, and it would link both import libraries. Assisted-by: ClaudeCode:claude-opus-5 * chore(appveyor): print link settings to debug LNK1104 Revert the two attempted fixes. Neither changed the failure: setting LIB does not survive MSBuild, and naming the release library for Python_LIBRARY_DEBUG had no effect. Print the Python cache entries and the link settings of a generated project file instead, to see what the linker really gets. Temporary. Assisted-by: ClaudeCode:claude-opus-5 * fix(appveyor): take the release Python library in Debug The generated project file linked C:\Python39\libs\python39_d.lib in the Debug configuration, because the image ships debug binaries next to the release ones. pybind11 undefines _DEBUG around Python.h, so pyconfig.h asks for python39.lib in a #pragma comment(lib), which nothing on the link line satisfies and no library directory holds. Map Debug to the release artifacts. Assisted-by: ClaudeCode:claude-opus-5 * chore(appveyor): drop the temporary link diagnostic Assisted-by: ClaudeCode:claude-opus-5 * fix(tests): guard the unraisable warning filter for pytest < 6 The distro pytest in the Clang and GCC Docker jobs has no PytestUnraisableExceptionWarning, so an unconditional filterwarnings mark makes pytest fail with an INTERNALERROR after the tests pass. Assisted-by: ClaudeCode:claude-opus-5 Claude-Session: https://claude.ai/code/session_01Aimf6HuSz1vLRwBnbxmCTc * docs: address review items on version hints, embed docs, and a PyPy xfail Extend Python_ADDITIONAL_VERSIONS through 3.15, describe the PyConfig behavior of initialize_interpreter, and drop the stale Python 3.8 wording from the PyPy xfail reason. Assisted-by: ClaudeCode:claude-opus-5 * Update README.rst Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com> --------- Co-authored-by: Ralf W. Grosse-Kunstleve <rwgkio@gmail.com>
540 lines
20 KiB
C++
540 lines
20 KiB
C++
#include <pybind11/critical_section.h>
|
|
#include <pybind11/embed.h>
|
|
#include <pybind11/pybind11.h>
|
|
|
|
// Silence MSVC C++17 deprecation warning from Catch regarding std::uncaught_exceptions (up to
|
|
// catch 2.0.1; this should be fixed in the next catch release after 2.0.1).
|
|
PYBIND11_WARNING_DISABLE_MSVC(4996)
|
|
|
|
#include "catch_skip.h"
|
|
|
|
#include <catch.hpp>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <thread>
|
|
#include <utility>
|
|
|
|
namespace py = pybind11;
|
|
using namespace py::literals;
|
|
|
|
size_t get_sys_path_size() {
|
|
auto sys_path = py::module::import("sys").attr("path");
|
|
return py::len(sys_path);
|
|
}
|
|
|
|
bool has_state_dict_internals_obj() {
|
|
py::dict state = py::detail::get_python_state_dict();
|
|
return state.contains(PYBIND11_INTERNALS_ID);
|
|
}
|
|
|
|
uintptr_t get_details_as_uintptr() {
|
|
return reinterpret_cast<uintptr_t>(py::detail::get_internals_pp_manager().get_pp()->get());
|
|
}
|
|
|
|
class Widget {
|
|
public:
|
|
explicit Widget(std::string message) : message(std::move(message)) {}
|
|
virtual ~Widget() = default;
|
|
|
|
std::string the_message() const { return message; }
|
|
virtual int the_answer() const = 0;
|
|
virtual std::string argv0() const = 0;
|
|
|
|
private:
|
|
std::string message;
|
|
};
|
|
|
|
class PyWidget final : public Widget {
|
|
using Widget::Widget;
|
|
|
|
int the_answer() const override { PYBIND11_OVERRIDE_PURE(int, Widget, the_answer, ); }
|
|
std::string argv0() const override { PYBIND11_OVERRIDE_PURE(std::string, Widget, argv0, ); }
|
|
};
|
|
|
|
class test_override_cache_helper {
|
|
|
|
public:
|
|
virtual int func() { return 0; }
|
|
|
|
test_override_cache_helper() = default;
|
|
virtual ~test_override_cache_helper() = default;
|
|
// Non-copyable
|
|
test_override_cache_helper &operator=(test_override_cache_helper const &Right) = delete;
|
|
test_override_cache_helper(test_override_cache_helper const &Copy) = delete;
|
|
};
|
|
|
|
class test_override_cache_helper_trampoline : public test_override_cache_helper {
|
|
int func() override { PYBIND11_OVERRIDE(int, test_override_cache_helper, func, ); }
|
|
};
|
|
|
|
PYBIND11_EMBEDDED_MODULE(widget_module, m, py::multiple_interpreters::per_interpreter_gil()) {
|
|
py::class_<Widget, PyWidget>(m, "Widget")
|
|
.def(py::init<std::string>())
|
|
.def_property_readonly("the_message", &Widget::the_message);
|
|
|
|
m.def("add", [](int i, int j) { return i + j; });
|
|
|
|
auto sub = m.def_submodule("sub");
|
|
sub.def("add", [](int i, int j) { return i + j; });
|
|
}
|
|
|
|
PYBIND11_EMBEDDED_MODULE(trampoline_module, m, py::multiple_interpreters::not_supported()) {
|
|
py::class_<test_override_cache_helper,
|
|
test_override_cache_helper_trampoline,
|
|
std::shared_ptr<test_override_cache_helper>>(m, "test_override_cache_helper")
|
|
.def(py::init_alias<>())
|
|
.def("func", &test_override_cache_helper::func);
|
|
}
|
|
|
|
enum class SomeEnum { value1, value2 }; // Added in PR #6015
|
|
|
|
PYBIND11_EMBEDDED_MODULE(enum_module, m, py::multiple_interpreters::per_interpreter_gil()) {
|
|
py::enum_<SomeEnum>(m, "SomeEnum")
|
|
.value("value1", SomeEnum::value1)
|
|
.value("value2", SomeEnum::value2);
|
|
}
|
|
|
|
PYBIND11_EMBEDDED_MODULE(throw_exception, , py::multiple_interpreters::not_supported()) {
|
|
throw std::runtime_error("C++ Error");
|
|
}
|
|
|
|
PYBIND11_EMBEDDED_MODULE(throw_error_already_set, , py::multiple_interpreters::not_supported()) {
|
|
auto d = py::dict();
|
|
d["missing"].cast<py::object>();
|
|
}
|
|
|
|
TEST_CASE("PYTHONPATH is used to update sys.path") {
|
|
// The setup for this TEST_CASE is in catch.cpp!
|
|
auto sys_path = py::str(py::module_::import("sys").attr("path")).cast<std::string>();
|
|
REQUIRE_THAT(
|
|
sys_path,
|
|
Catch::Matchers::Contains("pybind11_test_with_catch_PYTHONPATH_2099743835476552"));
|
|
}
|
|
|
|
TEST_CASE("Pass classes and data between modules defined in C++ and Python") {
|
|
auto module_ = py::module_::import("test_interpreter");
|
|
REQUIRE(py::hasattr(module_, "DerivedWidget"));
|
|
|
|
auto locals = py::dict("hello"_a = "Hello, World!", "x"_a = 5, **module_.attr("__dict__"));
|
|
py::exec(R"(
|
|
widget = DerivedWidget("{} - {}".format(hello, x))
|
|
message = widget.the_message
|
|
)",
|
|
py::globals(),
|
|
locals);
|
|
REQUIRE(locals["message"].cast<std::string>() == "Hello, World! - 5");
|
|
|
|
auto py_widget = module_.attr("DerivedWidget")("The question");
|
|
auto message = py_widget.attr("the_message");
|
|
REQUIRE(message.cast<std::string>() == "The question");
|
|
|
|
const auto &cpp_widget = py_widget.cast<const Widget &>();
|
|
REQUIRE(cpp_widget.the_answer() == 42);
|
|
}
|
|
|
|
TEST_CASE("Override cache") {
|
|
auto module_ = py::module_::import("test_trampoline");
|
|
REQUIRE(py::hasattr(module_, "func"));
|
|
REQUIRE(py::hasattr(module_, "func2"));
|
|
|
|
auto locals = py::dict(**module_.attr("__dict__"));
|
|
|
|
int i = 0;
|
|
for (; i < 1500; ++i) {
|
|
std::shared_ptr<test_override_cache_helper> p_obj;
|
|
std::shared_ptr<test_override_cache_helper> p_obj2;
|
|
|
|
py::object loc_inst = locals["func"]();
|
|
p_obj = py::cast<std::shared_ptr<test_override_cache_helper>>(loc_inst);
|
|
|
|
int ret = p_obj->func();
|
|
|
|
REQUIRE(ret == 42);
|
|
|
|
loc_inst = locals["func2"]();
|
|
|
|
p_obj2 = py::cast<std::shared_ptr<test_override_cache_helper>>(loc_inst);
|
|
|
|
p_obj2->func();
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Import error handling") {
|
|
REQUIRE_NOTHROW(py::module_::import("widget_module"));
|
|
REQUIRE_THROWS_WITH(py::module_::import("throw_exception"), "ImportError: C++ Error");
|
|
REQUIRE_THROWS_WITH(py::module_::import("throw_error_already_set"),
|
|
Catch::Contains("ImportError: initialization failed"));
|
|
|
|
auto locals = py::dict("is_keyerror"_a = false, "message"_a = "not set");
|
|
py::exec(R"(
|
|
try:
|
|
import throw_error_already_set
|
|
except ImportError as e:
|
|
is_keyerror = type(e.__cause__) == KeyError
|
|
message = str(e.__cause__)
|
|
)",
|
|
py::globals(),
|
|
locals);
|
|
REQUIRE(locals["is_keyerror"].cast<bool>() == true);
|
|
REQUIRE(locals["message"].cast<std::string>() == "'missing'");
|
|
}
|
|
|
|
TEST_CASE("There can be only one interpreter") {
|
|
static_assert(std::is_move_constructible<py::scoped_interpreter>::value, "");
|
|
static_assert(!std::is_move_assignable<py::scoped_interpreter>::value, "");
|
|
static_assert(!std::is_copy_constructible<py::scoped_interpreter>::value, "");
|
|
static_assert(!std::is_copy_assignable<py::scoped_interpreter>::value, "");
|
|
|
|
REQUIRE_THROWS_WITH(py::initialize_interpreter(), "The interpreter is already running");
|
|
REQUIRE_THROWS_WITH(py::scoped_interpreter(), "The interpreter is already running");
|
|
|
|
py::finalize_interpreter();
|
|
REQUIRE_NOTHROW(py::scoped_interpreter());
|
|
{
|
|
auto pyi1 = py::scoped_interpreter();
|
|
auto pyi2 = std::move(pyi1);
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("Custom PyConfig") {
|
|
py::finalize_interpreter();
|
|
PyConfig config;
|
|
PyConfig_InitPythonConfig(&config);
|
|
REQUIRE_NOTHROW(py::scoped_interpreter{&config});
|
|
{
|
|
py::scoped_interpreter p{&config};
|
|
REQUIRE(py::module_::import("widget_module").attr("add")(1, 41).cast<int>() == 42);
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("scoped_interpreter with PyConfig_InitIsolatedConfig and argv") {
|
|
std::vector<std::string> path;
|
|
for (auto p : py::module::import("sys").attr("path")) {
|
|
path.emplace_back(py::str(p));
|
|
}
|
|
|
|
py::finalize_interpreter();
|
|
{
|
|
PyConfig config;
|
|
PyConfig_InitIsolatedConfig(&config);
|
|
char *argv[] = {strdup("a.out")};
|
|
py::scoped_interpreter argv_scope{&config, 1, argv, true};
|
|
std::free(argv[0]);
|
|
// Because this config is isolated, setting the path during init will not work, we have to
|
|
// set it manually. If we don't set it, then we can't import "test_interpreter"
|
|
for (auto &&p : path) {
|
|
py::list(py::module::import("sys").attr("path")).append(p);
|
|
}
|
|
try {
|
|
auto module = py::module::import("test_interpreter");
|
|
auto py_widget = module.attr("DerivedWidget")("The question");
|
|
const auto &cpp_widget = py_widget.cast<const Widget &>();
|
|
REQUIRE(cpp_widget.argv0() == "a.out");
|
|
} catch (py::error_already_set &e) {
|
|
// catch here so that the exception doesn't escape the interpreter that owns it
|
|
FAIL(e.what());
|
|
}
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("scoped_interpreter with PyConfig_InitPythonConfig and argv") {
|
|
py::finalize_interpreter();
|
|
{
|
|
PyConfig config;
|
|
PyConfig_InitPythonConfig(&config);
|
|
|
|
// `initialize_interpreter() overrides the default value for config.parse_argv (`1`) by
|
|
// changing it to `0`. This test exercises `scoped_interpreter` with the default config.
|
|
char *argv[] = {strdup("a.out"), strdup("arg1")};
|
|
py::scoped_interpreter argv_scope(&config, 2, argv);
|
|
std::free(argv[0]);
|
|
std::free(argv[1]);
|
|
auto module = py::module::import("test_interpreter");
|
|
auto py_widget = module.attr("DerivedWidget")("The question");
|
|
const auto &cpp_widget = py_widget.cast<const Widget &>();
|
|
REQUIRE(cpp_widget.argv0() == "arg1");
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("Add program dir to path without PyConfig") {
|
|
py::finalize_interpreter();
|
|
size_t path_size_add_program_dir_to_path_false = 0;
|
|
{
|
|
py::scoped_interpreter scoped_interp{true, 0, nullptr, false};
|
|
path_size_add_program_dir_to_path_false = get_sys_path_size();
|
|
}
|
|
{
|
|
py::scoped_interpreter scoped_interp{};
|
|
REQUIRE(get_sys_path_size() == path_size_add_program_dir_to_path_false + 1);
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("Add program dir to path using PyConfig") {
|
|
py::finalize_interpreter();
|
|
size_t path_size_add_program_dir_to_path_false = 0;
|
|
{
|
|
PyConfig config;
|
|
PyConfig_InitPythonConfig(&config);
|
|
py::scoped_interpreter scoped_interp{&config, 0, nullptr, false};
|
|
path_size_add_program_dir_to_path_false = get_sys_path_size();
|
|
}
|
|
{
|
|
PyConfig config;
|
|
PyConfig_InitPythonConfig(&config);
|
|
py::scoped_interpreter scoped_interp{&config};
|
|
REQUIRE(get_sys_path_size() == path_size_add_program_dir_to_path_false + 1);
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("Restart the interpreter") {
|
|
// Verify pre-restart state.
|
|
REQUIRE(py::module_::import("widget_module").attr("add")(1, 2).cast<int>() == 3);
|
|
REQUIRE(has_state_dict_internals_obj());
|
|
REQUIRE(py::module_::import("external_module").attr("A")(123).attr("value").cast<int>()
|
|
== 123);
|
|
|
|
// local and foreign module internals should point to the same internals:
|
|
REQUIRE(get_details_as_uintptr()
|
|
== py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>());
|
|
|
|
// Restart the interpreter.
|
|
py::finalize_interpreter();
|
|
REQUIRE(Py_IsInitialized() == 0);
|
|
|
|
py::initialize_interpreter();
|
|
REQUIRE(Py_IsInitialized() == 1);
|
|
|
|
// Internals are deleted after a restart.
|
|
REQUIRE_FALSE(has_state_dict_internals_obj());
|
|
REQUIRE(get_details_as_uintptr() == 0);
|
|
pybind11::detail::get_internals();
|
|
REQUIRE(has_state_dict_internals_obj());
|
|
REQUIRE(get_details_as_uintptr() != 0);
|
|
REQUIRE(get_details_as_uintptr()
|
|
== py::module_::import("external_module").attr("internals_at")().cast<uintptr_t>());
|
|
|
|
// Make sure that an interpreter with no get_internals() created until finalize still gets the
|
|
// internals destroyed
|
|
py::finalize_interpreter();
|
|
py::initialize_interpreter();
|
|
bool ran = false;
|
|
py::module_::import("__main__").attr("internals_destroy_test")
|
|
= py::capsule(&ran, [](void *ran) {
|
|
py::detail::get_internals();
|
|
REQUIRE(has_state_dict_internals_obj());
|
|
*static_cast<bool *>(ran) = true;
|
|
});
|
|
REQUIRE_FALSE(has_state_dict_internals_obj());
|
|
REQUIRE_FALSE(ran);
|
|
py::finalize_interpreter();
|
|
REQUIRE(ran);
|
|
py::initialize_interpreter();
|
|
REQUIRE_FALSE(has_state_dict_internals_obj());
|
|
REQUIRE(get_details_as_uintptr() == 0);
|
|
|
|
// C++ modules can be reloaded.
|
|
auto cpp_module = py::module_::import("widget_module");
|
|
REQUIRE(cpp_module.attr("add")(1, 2).cast<int>() == 3);
|
|
|
|
// Also verify submodules work
|
|
REQUIRE(cpp_module.attr("sub").attr("add")(1, 41).cast<int>() == 42);
|
|
|
|
// C++ type information is reloaded and can be used in python modules.
|
|
auto py_module = py::module_::import("test_interpreter");
|
|
auto py_widget = py_module.attr("DerivedWidget")("Hello after restart");
|
|
REQUIRE(py_widget.attr("the_message").cast<std::string>() == "Hello after restart");
|
|
}
|
|
|
|
TEST_CASE("Enum module survives restart") { // Added in PR #6015
|
|
// Regression test for gh-5976: py::enum_ uses def_property_static, which
|
|
// calls process_attributes::init after initialize_generic's strdup loop,
|
|
// leaving arg names as string literals. Without the fix, destruct() would
|
|
// call free() on those literals during interpreter finalization.
|
|
PYBIND11_CATCH2_SKIP_IF(PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION == 12,
|
|
"Pre-existing crash in enum cleanup during finalize on Python 3.12");
|
|
|
|
auto enum_mod = py::module_::import("enum_module");
|
|
REQUIRE(enum_mod.attr("SomeEnum").attr("value1").attr("name").cast<std::string>() == "value1");
|
|
|
|
py::finalize_interpreter();
|
|
py::initialize_interpreter();
|
|
|
|
enum_mod = py::module_::import("enum_module");
|
|
REQUIRE(enum_mod.attr("SomeEnum").attr("value2").attr("name").cast<std::string>() == "value2");
|
|
}
|
|
|
|
TEST_CASE("Execution frame") {
|
|
// When the interpreter is embedded, there is no execution frame, but `py::exec`
|
|
// should still function by using reasonable globals: `__main__.__dict__`.
|
|
py::exec("var = dict(number=42)");
|
|
REQUIRE(py::globals()["var"]["number"].cast<int>() == 42);
|
|
}
|
|
|
|
TEST_CASE("Threads") {
|
|
// Restart interpreter to ensure threads are not initialized
|
|
py::finalize_interpreter();
|
|
py::initialize_interpreter();
|
|
|
|
constexpr auto num_threads = 10;
|
|
auto locals = py::dict("count"_a = 0);
|
|
|
|
{
|
|
py::gil_scoped_release gil_release{};
|
|
#if defined(Py_GIL_DISABLED) && PY_VERSION_HEX < 0x030E0000
|
|
std::mutex mutex;
|
|
#endif
|
|
|
|
auto threads = std::vector<std::thread>();
|
|
for (auto i = 0; i < num_threads; ++i) {
|
|
threads.emplace_back([&]() {
|
|
py::gil_scoped_acquire gil{};
|
|
#ifdef Py_GIL_DISABLED
|
|
# if PY_VERSION_HEX < 0x030E0000
|
|
// This will not run with the GIL, so it won't deadlock. That's
|
|
// because of how we run our tests. Be more careful of
|
|
// deadlocks if the "free-threaded" GIL could be enabled (at
|
|
// runtime).
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
# else
|
|
// CPython's thread-safe API in no-GIL mode.
|
|
py::scoped_critical_section lock(locals);
|
|
# endif
|
|
#endif
|
|
locals["count"] = locals["count"].cast<int>() + 1;
|
|
});
|
|
}
|
|
|
|
for (auto &thread : threads) {
|
|
thread.join();
|
|
}
|
|
}
|
|
|
|
REQUIRE(locals["count"].cast<int>() == num_threads);
|
|
}
|
|
|
|
// Scope exit utility https://stackoverflow.com/a/36644501/7255855
|
|
struct scope_exit {
|
|
std::function<void()> f_;
|
|
explicit scope_exit(std::function<void()> f) noexcept : f_(std::move(f)) {}
|
|
~scope_exit() {
|
|
if (f_) {
|
|
f_();
|
|
}
|
|
}
|
|
};
|
|
|
|
TEST_CASE("Reload module from file") {
|
|
// Disable generation of cached bytecode (.pyc files) for this test, otherwise
|
|
// Python might pick up an old version from the cache instead of the new versions
|
|
// of the .py files generated below
|
|
auto sys = py::module_::import("sys");
|
|
bool dont_write_bytecode = sys.attr("dont_write_bytecode").cast<bool>();
|
|
sys.attr("dont_write_bytecode") = true;
|
|
// Reset the value at scope exit
|
|
scope_exit reset_dont_write_bytecode(
|
|
[&]() { sys.attr("dont_write_bytecode") = dont_write_bytecode; });
|
|
|
|
std::string module_name = "test_module_reload";
|
|
std::string module_file = module_name + ".py";
|
|
|
|
// Create the module .py file
|
|
std::ofstream test_module(module_file);
|
|
test_module << "def test():\n";
|
|
test_module << " return 1\n";
|
|
test_module.close();
|
|
// Delete the file at scope exit
|
|
scope_exit delete_module_file([&]() { std::remove(module_file.c_str()); });
|
|
|
|
// Import the module from file
|
|
auto module_ = py::module_::import(module_name.c_str());
|
|
int result = module_.attr("test")().cast<int>();
|
|
REQUIRE(result == 1);
|
|
|
|
// Update the module .py file with a small change
|
|
test_module.open(module_file);
|
|
test_module << "def test():\n";
|
|
test_module << " return 2\n";
|
|
test_module.close();
|
|
|
|
// Reload the module
|
|
module_.reload();
|
|
result = module_.attr("test")().cast<int>();
|
|
REQUIRE(result == 2);
|
|
}
|
|
|
|
TEST_CASE("sys.argv gets initialized properly") {
|
|
py::finalize_interpreter();
|
|
{
|
|
py::scoped_interpreter default_scope;
|
|
auto module = py::module::import("test_interpreter");
|
|
auto py_widget = module.attr("DerivedWidget")("The question");
|
|
const auto &cpp_widget = py_widget.cast<const Widget &>();
|
|
REQUIRE(cpp_widget.argv0().empty());
|
|
}
|
|
|
|
{
|
|
char *argv[] = {strdup("a.out")};
|
|
py::scoped_interpreter argv_scope(true, 1, argv);
|
|
std::free(argv[0]);
|
|
auto module = py::module::import("test_interpreter");
|
|
auto py_widget = module.attr("DerivedWidget")("The question");
|
|
const auto &cpp_widget = py_widget.cast<const Widget &>();
|
|
REQUIRE(cpp_widget.argv0() == "a.out");
|
|
}
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
TEST_CASE("make_iterator can be called before then after finalizing an interpreter") {
|
|
// Reproduction of issue #2101 (https://github.com/pybind/pybind11/issues/2101)
|
|
py::finalize_interpreter();
|
|
|
|
std::vector<int> container;
|
|
{
|
|
pybind11::scoped_interpreter g;
|
|
auto iter = pybind11::make_iterator(container.begin(), container.end());
|
|
}
|
|
|
|
REQUIRE_NOTHROW([&]() {
|
|
pybind11::scoped_interpreter g;
|
|
auto iter = pybind11::make_iterator(container.begin(), container.end());
|
|
}());
|
|
|
|
py::initialize_interpreter();
|
|
}
|
|
|
|
#ifdef PYBIND11_HAS_STRING_VIEW
|
|
TEST_CASE("Casting to a string_view outside a bound function") {
|
|
// Regression for PR #6092: view casters add the source to loader_life_support, but
|
|
// outside a bound function there is no frame. The caller owns the source's lifetime
|
|
// here, so the cast must succeed rather than throw.
|
|
py::str unicode("hello");
|
|
py::bytes bytes_obj("world", 5);
|
|
auto bytearray_obj
|
|
= py::reinterpret_steal<py::object>(PyByteArray_FromStringAndSize("bytes", 5));
|
|
|
|
REQUIRE(py::cast<std::string_view>(unicode) == "hello");
|
|
REQUIRE(py::cast<std::string_view>(bytes_obj) == "world");
|
|
REQUIRE(py::cast<std::string_view>(bytearray_obj) == "bytes");
|
|
|
|
// Wide string views require an encoded temporary. With no loader life-support
|
|
// frame, returning a view into that temporary must fail.
|
|
REQUIRE_THROWS_AS(py::cast<std::u16string_view>(unicode), py::cast_error);
|
|
REQUIRE_THROWS_AS(py::cast<std::u32string_view>(unicode), py::cast_error);
|
|
|
|
// Bound-function dispatch provides a frame that keeps both temporaries alive.
|
|
auto accepts_wide_views
|
|
= py::cpp_function([](std::u16string_view value16, std::u32string_view value32) {
|
|
return value16 == std::u16string_view(u"hello")
|
|
&& value32 == std::u32string_view(U"hello");
|
|
});
|
|
REQUIRE(accepts_wide_views(unicode, unicode).cast<bool>());
|
|
}
|
|
#endif
|