Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
33fb4a6
Bump internals version
XuehaiPan Jan 22, 2026
a4a6a1e
Prevent internals destruction before all pybind11 types are destroyed
XuehaiPan Jan 22, 2026
6d8fa8a
Merge remote-tracking branch 'upstream/master' into fix-segfault
XuehaiPan Jan 22, 2026
1d49006
Use Py_XINCREF and Py_XDECREF
XuehaiPan Jan 22, 2026
b147430
Hold GIL before decref
XuehaiPan Jan 22, 2026
05576f1
Use weakrefs
XuehaiPan Jan 22, 2026
740f693
Remove unused code
XuehaiPan Jan 22, 2026
d9227ce
Move code location
XuehaiPan Jan 22, 2026
7c5d505
Move code location
XuehaiPan Jan 22, 2026
436d812
Move code location
XuehaiPan Jan 22, 2026
ce9ca7f
Try add tests
XuehaiPan Jan 24, 2026
fed1749
Fix PYTHONPATH
XuehaiPan Jan 24, 2026
a407438
Fix PYTHONPATH
XuehaiPan Jan 24, 2026
3df427c
Skip tests for subprocess
XuehaiPan Jan 24, 2026
72c2e0a
Revert to leak internals
XuehaiPan Jan 24, 2026
c5ec1cf
Revert to leak internals
XuehaiPan Jan 24, 2026
8f25a25
Revert "Revert to leak internals"
XuehaiPan Jan 24, 2026
97e12d2
Revert internals version bump
XuehaiPan Jan 25, 2026
cdefbf3
Reapply to leak internals
XuehaiPan Jan 25, 2026
6ed2830
Add re-entrancy detection for internals creation
XuehaiPan Jan 25, 2026
2c83462
Fix C++11/C++14 support
XuehaiPan Jan 25, 2026
6922d7d
Add lock under multiple interpreters
XuehaiPan Jan 25, 2026
d61f17c
Try fix tests
XuehaiPan Jan 25, 2026
26e7509
Try fix tests
XuehaiPan Jan 25, 2026
6820ead
Try fix tests
XuehaiPan Jan 25, 2026
15bcbf8
Update comments and assertion messages
XuehaiPan Jan 25, 2026
b0d350e
Update comments and assertion messages
XuehaiPan Jan 25, 2026
33ffa8e
Update comments
XuehaiPan Jan 26, 2026
708ca55
Update lock scope
XuehaiPan Jan 26, 2026
85dc7e6
Use original pointer type for Windows
XuehaiPan Jan 26, 2026
404457e
Change hard error to warning
XuehaiPan Jan 26, 2026
51a70ab
Update lock scope
XuehaiPan Jan 26, 2026
5f96327
Update lock scope to resolve deadlock
XuehaiPan Jan 26, 2026
40731b7
Remove scope release of GIL
XuehaiPan Jan 26, 2026
aa1767c
Update comments
XuehaiPan Jan 26, 2026
dea5660
Lock pp on reset
XuehaiPan Jan 26, 2026
691241a
Mark content created after assignment
XuehaiPan Jan 26, 2026
552f8b0
Update comments
XuehaiPan Jan 26, 2026
79a80d2
Simplify implementation
XuehaiPan Jan 27, 2026
56926f6
Update lock scope when delete unique_ptr
XuehaiPan Jan 27, 2026
6f3014a
Merge branch 'master' into fix-segfault
XuehaiPan Feb 1, 2026
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
37 changes: 28 additions & 9 deletions docs/advanced/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1381,11 +1381,22 @@ You can do that using ``py::custom_type_setup``:

.. code-block:: cpp

struct OwnsPythonObjects {
py::object value = py::none();
struct ContainerOwnsPythonObjects {
std::vector<py::object> list;

void append(const py::object &obj) { list.emplace_back(obj); }
py::object at(py::ssize_t index) const {
if (index >= size() || index < 0) {
throw py::index_error("Index out of range");
}
return list.at(py::size_t(index));
}
py::ssize_t size() const { return py::ssize_t_cast(list.size()); }
void clear() { list.clear(); }
};
py::class_<OwnsPythonObjects> cls(
m, "OwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {

py::class_<ContainerOwnsPythonObjects> cls(
m, "ContainerOwnsPythonObjects", py::custom_type_setup([](PyHeapTypeObject *heap_type) {
auto *type = &heap_type->ht_type;
type->tp_flags |= Py_TPFLAGS_HAVE_GC;
type->tp_traverse = [](PyObject *self_base, visitproc visit, void *arg) {
Expand All @@ -1394,20 +1405,28 @@ You can do that using ``py::custom_type_setup``:
Py_VISIT(Py_TYPE(self_base));
#endif
if (py::detail::is_holder_constructed(self_base)) {
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
Py_VISIT(self.value.ptr());
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
for (auto &item : self.list) {
Py_VISIT(item.ptr());
}
}
return 0;
};
type->tp_clear = [](PyObject *self_base) {
if (py::detail::is_holder_constructed(self_base)) {
auto &self = py::cast<OwnsPythonObjects&>(py::handle(self_base));
self.value = py::none();
auto &self = py::cast<ContainerOwnsPythonObjects &>(py::handle(self_base));
for (auto &item : self.list) {
Py_CLEAR(item.ptr());
}
self.list.clear();
}
return 0;
};
}));
cls.def(py::init<>());
cls.def_readwrite("value", &OwnsPythonObjects::value);
cls.def("append", &ContainerOwnsPythonObjects::append);
cls.def("at", &ContainerOwnsPythonObjects::at);
cls.def("size", &ContainerOwnsPythonObjects::size);
cls.def("clear", &ContainerOwnsPythonObjects::clear);

.. versionadded:: 2.8
157 changes: 97 additions & 60 deletions include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -337,19 +337,7 @@ struct internals {
internals(internals &&other) = delete;
internals &operator=(const internals &other) = delete;
internals &operator=(internals &&other) = delete;
~internals() {
// Normally this destructor runs during interpreter finalization and it may DECREF things.
// In odd finalization scenarios it might end up running after the interpreter has
// completely shut down, In that case, we should not decref these objects because pymalloc
// is gone. This also applies across sub-interpreters, we should only DECREF when the
// original owning interpreter is active.
auto *cur_istate = get_interpreter_state_unchecked();
if (cur_istate && cur_istate == istate) {
Py_CLEAR(instance_base);
Py_CLEAR(default_metaclass);
Py_CLEAR(static_property_type);
}
}
~internals() = default;
};

// the internals struct (above) is shared between all the modules. local_internals are only
Expand All @@ -359,28 +347,13 @@ struct internals {
// impact any other modules, because the only things accessing the local internals is the
// module that contains them.
struct local_internals {
local_internals() : istate(get_interpreter_state_unchecked()) {}

// It should be safe to use fast_type_map here because this entire
// data structure is scoped to our single module, and thus a single
// DSO and single instance of type_info for any particular type.
fast_type_map<type_info *> registered_types_cpp;

std::forward_list<ExceptionTranslator> registered_exception_translators;
PyTypeObject *function_record_py_type = nullptr;
PyInterpreterState *istate = nullptr;

~local_internals() {
// Normally this destructor runs during interpreter finalization and it may DECREF things.
// In odd finalization scenarios it might end up running after the interpreter has
// completely shut down, In that case, we should not decref these objects because pymalloc
// is gone. This also applies across sub-interpreters, we should only DECREF when the
// original owning interpreter is active.
auto *cur_istate = get_interpreter_state_unchecked();
if (cur_istate && cur_istate == istate) {
Py_CLEAR(function_record_py_type);
}
}
};

enum class holder_enum_t : uint8_t {
Expand Down Expand Up @@ -576,6 +549,10 @@ inline void translate_local_exception(std::exception_ptr p) {
}
#endif

// Sentinel value for the `dtor` parameter of `atomic_get_or_create_in_state_dict`.
// Indicates no destructor was explicitly provided (distinct from nullptr, which means "leak").
#define PYBIND11_DTOR_USE_DELETE (reinterpret_cast<void (*)(PyObject *)>(1))

// Get or create per-storage capsule in the current interpreter's state dict.
// - The storage is interpreter-dependent: different interpreters will have different storage.
// This is important when using multiple-interpreters, to avoid sharing unshareable objects
Expand All @@ -592,9 +569,14 @@ inline void translate_local_exception(std::exception_ptr p) {
//
// Returns: pair of (pointer to storage, bool indicating if newly created).
// The bool follows std::map::insert convention: true = created, false = existed.
// `dtor`: optional destructor called when the interpreter shuts down.
// - If not provided: the storage will be deleted using `delete`.
// - If nullptr: the storage will be leaked (useful for singletons that outlive the interpreter).
// - If a function: that function will be called with the capsule object.
template <typename Payload>
std::pair<Payload *, bool> atomic_get_or_create_in_state_dict(const char *key,
void (*dtor)(PyObject *) = nullptr) {
void (*dtor)(PyObject *)
= PYBIND11_DTOR_USE_DELETE) {
error_scope err_scope; // preserve any existing Python error states

auto state_dict = reinterpret_borrow<dict>(get_python_state_dict());
Expand Down Expand Up @@ -640,7 +622,7 @@ std::pair<Payload *, bool> atomic_get_or_create_in_state_dict(const char *key,
// - Otherwise, our `new_capsule` is now in the dict, and it owns the storage and the state
// dict will incref it. We need to set the caller's destructor on it, which will be
// called when the interpreter shuts down.
if (created && dtor) {
if (created && dtor != PYBIND11_DTOR_USE_DELETE) {
if (PyCapsule_SetDestructor(capsule_obj, dtor) < 0) {
throw error_already_set();
}
Expand All @@ -657,6 +639,8 @@ std::pair<Payload *, bool> atomic_get_or_create_in_state_dict(const char *key,
return std::pair<Payload *, bool>(static_cast<Payload *>(raw_ptr), created);
}

#undef PYBIND11_DTOR_USE_DELETE

template <typename InternalsType>
class internals_pp_manager {
public:
Expand Down Expand Up @@ -713,42 +697,63 @@ class internals_pp_manager {
// this could be called without an active interpreter, just use what was cached
if (!tstate || tstate->interp == last_istate_tls()) {
auto tpp = internals_p_tls();

delete tpp;
{
std::lock_guard<std::mutex> lock(pp_set_mutex_);
pps_have_created_content_.erase(tpp); // untrack deleted pp
delete tpp;
}
}
unref();
return;
}
#endif
delete internals_singleton_pp_;
{
std::lock_guard<std::mutex> lock(pp_set_mutex_);
pps_have_created_content_.erase(internals_singleton_pp_); // untrack deleted pp
delete internals_singleton_pp_;
}
unref();
}

private:
internals_pp_manager(char const *id, on_fetch_function *on_fetch)
: holder_id_(id), on_fetch_(on_fetch) {}
void create_pp_content_once(std::unique_ptr<InternalsType> *const pp) {
std::lock_guard<std::mutex> lock(pp_set_mutex_);

static void internals_shutdown(PyObject *capsule) {
auto *pp = static_cast<std::unique_ptr<InternalsType> *>(
PyCapsule_GetPointer(capsule, nullptr));
if (pp) {
pp->reset();
if (*pp) {
// Already created in another thread.
return;
}
// We reset the unique_ptr's contents but cannot delete the unique_ptr itself here.
// The pp_manager in this module (and possibly other modules sharing internals) holds
// a raw pointer to this unique_ptr, and that pointer would dangle if we deleted it now.
//
// For pybind11-owned interpreters (via embed.h or subinterpreter.h), destroy() is
// called after Py_Finalize/Py_EndInterpreter completes, which safely deletes the
// unique_ptr. For interpreters not owned by pybind11 (e.g., a pybind11 extension
// loaded into an external interpreter), destroy() is never called and the unique_ptr
// shell (8 bytes, not its contents) is leaked.
// (See PR #5958 for ideas to eliminate this leak.)

// Detect re-creation of internals after destruction during interpreter shutdown.
// If pybind11 code (e.g., tp_traverse/tp_clear calling py::cast) runs after internals have
// been destroyed, a new empty internals would be created, causing type lookup failures.
// See also get_or_create_pp_in_state_dict() comments.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR reverted the "re-creation during shutdown" scenario, so why is this code necessary now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two reasons:

  1. Add an explicit check to prevent potential bugs in the future. Maybe we can eventually not leak the internals.
  2. The reverted code still creates the internals raw pointers multiple times (pp->reset(new InternalsType())). This PR adds a lock to ensure consistency.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this code should have this unordered map and the associated book keeping around for an unreachable failure condition. At least make it debug only, or have its own ifdef, or completely remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overhead is very small, and ideally, it should only run once. I think it is acceptable.

an unreachable failure condition

We do run into this here:

https://github.com/pybind/pybind11/actions/runs/21558860079/job/62119622188?pr=5979#step:12:1536

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do run into this here:

I'm glad you understand this already! (I didn't)

@XuehaiPan do you have ideas how we should take care of that failure? This PR, or a follow-on? — We definitely need to bring back the commit I backed out (91189c9).

Copy link
Contributor Author

@XuehaiPan XuehaiPan Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concurrent.interpreters.NotShareableError: func not shareable is unrelated. And it is unexpected because the func function is not using any global variables (it uses local imports). It should be shareable between interpreters.

UPDATE: the root cause is:

E           Exception: Traceback (most recent call last):
E             File "<frozen importlib._bootstrap>", line 1371, in _find_and_load
E             File "<frozen importlib._bootstrap>", line 1333, in _find_and_load_unlocked
E             File "<frozen importlib._bootstrap>", line 1267, in _find_spec
E             File "<frozen importlib._bootstrap_external>", line 1292, in find_spec
E             File "<frozen importlib._bootstrap_external>", line 1266, in _get_spec
E             File "<frozen importlib._bootstrap_external>", line 1369, in find_spec
E             File "<frozen importlib._bootstrap_external>", line 1412, in _fill_cache
E           BlockingIOError: [Errno 11] Resource temporarily unavailable: '/opt/python/cp314-cp314t/lib/python3.14t/concurrent'
E           
E           The above exception was the direct cause of the following exception:
E           
E           concurrent.interpreters.NotShareableError: object could not be unpickled

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it looks like it's flaky, at best. I'll try reruns under 5850 to see if I can get it to pass with enough trials.

It worked only on the 10th attempt!

https://github.com/pybind/pybind11/actions/runs/21580597316/job/62255061573?pr=5850

@XuehaiPan @b-pass we need to handle this somehow, it'll be super distracting. What should we do? Create an issue and add an xfail pointing to it?

Copy link
Contributor Author

@XuehaiPan XuehaiPan Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot reproduce this. Does this only fail with C++11? If so, we can add a skipif.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tried reproducing before, with C++20, I saw another kind of failure, and only when running test_multiple_interpreters.py in isolation.

I tried again just now, with C++11, and it's the same behavior. See below, JIC it's useful somehow.

I have to give up for now; I don't have a lot of spare time during the week.

Probably, if I had the free bandwidth, I'd try to set up a Manylinux container, identically to what we have in the CI.


( cd /wrk/forked/pybind11/tests && PYTHONPATH=/wrk/bld/pybind11_gcc_v3.14.2_df793163d58_freethreaded/lib /wrk/bld/pybind11_gcc_v3.14.2_df793163d58_freethreaded/TestVenv/bin/python3 -m pytest -v test_multiple_interpreters.py )

============================================================================= test session starts ==============================================================================
platform linux -- Python 3.14.2, pytest-9.0.2, pluggy-1.6.0 -- /wrk/bld/pybind11_gcc_v3.14.2_df793163d58_freethreaded/TestVenv/bin/python3
cachedir: .pytest_cache
installed packages of interest: build==1.4.0 numpy==2.4.2 scipy==1.17.0
C++ Info: 13.3.0 C++11 __pybind11_internals_v11_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1__ PYBIND11_SIMPLE_GIL_MANAGEMENT=False
free-threaded Python build
rootdir: /wrk/forked/pybind11/tests
configfile: pytest.ini
plugins: timeout-2.4.0, xdist-3.8.0
collected 7 items

test_multiple_interpreters.py::test_independent_subinterpreters PASSED                                                                                                   [ 14%]
test_multiple_interpreters.py::test_independent_subinterpreters_modern PASSED                                                                                            [ 28%]
test_multiple_interpreters.py::test_dependent_subinterpreters FAILED                                                                                                     [ 42%]
test_multiple_interpreters.py::test_import_module_with_singleton_per_interpreter PASSED                                                                                  [ 57%]
test_multiple_interpreters.py::test_import_in_subinterpreter_after_main PASSED                                                                                           [ 71%]
test_multiple_interpreters.py::test_import_in_subinterpreter_before_main PASSED                                                                                          [ 85%]
test_multiple_interpreters.py::test_import_in_subinterpreter_concurrently PASSED                                                                                         [100%]

=================================================================================== FAILURES ===================================================================================
________________________________________________________________________ test_dependent_subinterpreters ________________________________________________________________________

    @pytest.mark.skipif(
        sys.platform.startswith("emscripten"), reason="Requires loadable modules"
    )
    def test_dependent_subinterpreters():
        """Makes sure the internals object differs across subinterpreters"""

        sys.path.insert(0, os.path.dirname(pybind11_tests.__file__))

        run_string, create = get_interpreters(modern=False)

>       import mod_shared_interpreter_gil as m
E       RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'mod_shared_interpreter_gil', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.

create     = <function get_interpreters.<locals>.create at 0x4f3e3193a80>
run_string = <function get_interpreters.<locals>.run_string at 0x4f3e3193c00>

test_multiple_interpreters.py:193: RuntimeWarning
=========================================================================== short test summary info ============================================================================
FAILED test_multiple_interpreters.py::test_dependent_subinterpreters - RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'mod_shared_interpreter_gil', which has not declared that it can run safely without the G...
========================================================================= 1 failed, 6 passed in 19.00s =========================================================================

ERROR: completed_process.returncode=1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (pps_have_created_content_.find(pp) != pps_have_created_content_.end()) {
pybind11_fail("pybind11::detail::internals_pp_manager::create_pp_content_once() "
"FAILED: reentrant call detected while fetching pybind11 internals!");
}
// Each interpreter can only create its internals once.
pps_have_created_content_.insert(pp);

// Assume the GIL is held here. May call back into Python.
// Create the internals content.
pp->reset(new InternalsType());
}

private:
internals_pp_manager(char const *id, on_fetch_function *on_fetch)
: holder_id_(id), on_fetch_(on_fetch) {}

std::unique_ptr<InternalsType> *get_or_create_pp_in_state_dict() {
// The `unique_ptr<InternalsType>` is intentionally leaked on interpreter shutdown.
// Once an instance is created, it will never be deleted until the process exits (compare
// to interpreter shutdown in multiple-interpreter scenarios).
// We cannot guarantee the destruction order of capsules in the interpreter state dict on
// interpreter shutdown, so deleting internals too early could cause undefined behavior
// when other pybind11 objects access `get_internals()` during finalization (which would
// recreate empty internals). See also create_pp_content_once() above.
// See https://github.com/pybind/pybind11/pull/5958#discussion_r2717645230.
auto result = atomic_get_or_create_in_state_dict<std::unique_ptr<InternalsType>>(
holder_id_, &internals_shutdown);
holder_id_, /*dtor=*/nullptr /* leak the capsule content */);
auto *pp = result.first;
bool created = result.second;
// Only call on_fetch_ when fetching existing internals, not when creating new ones.
Expand All @@ -774,7 +779,12 @@ class internals_pp_manager {
on_fetch_function *on_fetch_ = nullptr;
// Pointer-to-pointer to the singleton internals for the first seen interpreter (may not be the
// main interpreter)
std::unique_ptr<InternalsType> *internals_singleton_pp_;
std::unique_ptr<InternalsType> *internals_singleton_pp_ = nullptr;

// Track pointer-to-pointers whose internals have been created, to detect re-entrancy.
// Use instance member over static due to singleton pattern of this class.
std::unordered_set<std::unique_ptr<InternalsType> *> pps_have_created_content_;
std::mutex pp_set_mutex_;
};

// If We loaded the internals through `state_dict`, our `error_already_set`
Expand Down Expand Up @@ -815,7 +825,8 @@ PYBIND11_NOINLINE internals &get_internals() {
// Slow path, something needs fetched from the state dict or created
gil_scoped_acquire_simple gil;
error_scope err_scope;
internals_ptr.reset(new internals());

ppmgr.create_pp_content_once(&internals_ptr);

if (!internals_ptr->instance_base) {
// This calls get_internals, so cannot be called from within the internals constructor
Expand All @@ -826,6 +837,31 @@ PYBIND11_NOINLINE internals &get_internals() {
return *internals_ptr;
}

/// Return the PyObject* for the internals capsule (borrowed reference).
/// Returns nullptr if the capsule doesn't exist yet.
inline PyObject *get_internals_capsule() {
auto state_dict = reinterpret_borrow<dict>(get_python_state_dict());
return dict_getitemstring(state_dict.ptr(), PYBIND11_INTERNALS_ID);
}

/// Return the key used for local_internals in the state dict.
/// This function ensures a consistent key is used across all call sites within the same
/// compilation unit. The key includes the address of a static variable to make it unique per
/// module (DSO), matching the behavior of get_local_internals_pp_manager().
inline const std::string &get_local_internals_key() {
static const std::string key
= PYBIND11_MODULE_LOCAL_ID + std::to_string(reinterpret_cast<uintptr_t>(&key));
return key;
}

/// Return the PyObject* for the local_internals capsule (borrowed reference).
/// Returns nullptr if the capsule doesn't exist yet.
inline PyObject *get_local_internals_capsule() {
const auto &key = get_local_internals_key();
auto state_dict = reinterpret_borrow<dict>(get_python_state_dict());
return dict_getitemstring(state_dict.ptr(), key.c_str());
}

inline void ensure_internals() {
pybind11::detail::get_internals_pp_manager().unref();
#ifdef PYBIND11_HAS_SUBINTERPRETER_SUPPORT
Expand All @@ -837,20 +873,21 @@ inline void ensure_internals() {
}

inline internals_pp_manager<local_internals> &get_local_internals_pp_manager() {
// Use the address of this static itself as part of the key, so that the value is uniquely tied
// Use the address of a static variable as part of the key, so that the value is uniquely tied
// to where the module is loaded in memory
static const std::string this_module_idstr
= PYBIND11_MODULE_LOCAL_ID
+ std::to_string(reinterpret_cast<uintptr_t>(&this_module_idstr));
return internals_pp_manager<local_internals>::get_instance(this_module_idstr.c_str(), nullptr);
return internals_pp_manager<local_internals>::get_instance(get_local_internals_key().c_str(),
nullptr);
}

/// Works like `get_internals`, but for things which are locally registered.
inline local_internals &get_local_internals() {
auto &ppmgr = get_local_internals_pp_manager();
auto &internals_ptr = *ppmgr.get_pp();
if (!internals_ptr) {
internals_ptr.reset(new local_internals());
gil_scoped_acquire_simple gil;
error_scope err_scope;

ppmgr.create_pp_content_once(&internals_ptr);
}
return *internals_ptr;
}
Expand Down
28 changes: 28 additions & 0 deletions tests/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,31 @@
or GRAALPY
or (CPYTHON and PY_GIL_DISABLED and (3, 13) <= sys.version_info < (3, 14))
)


def check_script_success_in_subprocess(code: str, *, rerun: int = 8) -> None:
"""Runs the given code in a subprocess."""
import os
import subprocess
import sys
import textwrap

code = textwrap.dedent(code).strip()
try:
for _ in range(rerun): # run flakily failing test multiple times
subprocess.check_output(
[sys.executable, "-c", code],
cwd=os.getcwd(),
stderr=subprocess.STDOUT,
text=True,
)
except subprocess.CalledProcessError as ex:
raise RuntimeError(
f"Subprocess failed with exit code {ex.returncode}.\n\n"
f"Code:\n"
f"```python\n"
f"{code}\n"
f"```\n\n"
f"Output:\n"
f"{ex.output}"
) from None
Loading
Loading