From ad5bc9e80e2c127c7b7171ab66787c8bbcdff478 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:43:00 -0400 Subject: [PATCH 1/9] chore(deps): bump the actions group with 2 updates (#6027) --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/configure.yml | 2 +- .github/workflows/tests-cibw.yml | 6 +++--- .github/workflows/upstream.yml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03cb0238c..eaa1c2c0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,7 +302,7 @@ jobs: debug: ${{ matrix.python-debug }} - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Valgrind cache if: matrix.valgrind @@ -570,7 +570,7 @@ jobs: run: python3 -m pip install --upgrade pip - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Configure shell: bash @@ -906,7 +906,7 @@ jobs: ${{ matrix.python == '3.13' && runner.os == 'Windows' }} - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Prepare MSVC uses: ilammy/msvc-dev-cmd@v1.13.0 @@ -956,7 +956,7 @@ jobs: architecture: x86 - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Prepare MSVC uses: ilammy/msvc-dev-cmd@v1.13.0 @@ -1007,7 +1007,7 @@ jobs: run: python3 -m pip install -r tests/requirements.txt - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Configure C++20 run: > @@ -1189,7 +1189,7 @@ jobs: python-version: ${{ matrix.python }} - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Install ninja-build tool uses: seanmiddleditch/gha-setup-ninja@v6 diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index 931c0bff2..e3d99dd87 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -64,7 +64,7 @@ jobs: # An action for adding a specific version of CMake: # https://github.com/jwlawson/actions-setup-cmake - name: Setup CMake ${{ matrix.cmake }} - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 with: cmake-version: ${{ matrix.cmake }} diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index bf534316a..f7bca76e7 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -22,7 +22,7 @@ jobs: submodules: true fetch-depth: 0 - - uses: pypa/cibuildwheel@v3.3 + - uses: pypa/cibuildwheel@v3.4 env: PYODIDE_BUILD_EXPORTS: whole_archive with: @@ -45,7 +45,7 @@ jobs: # We have to uninstall first because GH is now using a local tap to build cmake<4, iOS needs cmake>=4 - run: brew uninstall cmake && brew install cmake - - uses: pypa/cibuildwheel@v3.3 + - uses: pypa/cibuildwheel@v3.4 env: CIBW_PLATFORM: ios CIBW_SKIP: cp314-* # https://github.com/pypa/cibuildwheel/issues/2494 @@ -70,7 +70,7 @@ jobs: if: contains(matrix.runs-on, 'macos') run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - - uses: pypa/cibuildwheel@v3.3 + - uses: pypa/cibuildwheel@v3.4 env: CIBW_PLATFORM: android with: diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 051cffc04..51354d68c 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -36,7 +36,7 @@ jobs: run: sudo apt-get install libboost-dev - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.1 + uses: jwlawson/actions-setup-cmake@v2.2 - name: Run pip installs run: | From ab392bd84587ebdb4d9b2d744b4ac3fd34024281 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Mon, 6 Apr 2026 11:50:55 -0400 Subject: [PATCH 2/9] fix: avoid copy constructor instantiation in shared_ptr fallback cast (#6028) * tests: add regressions for shared_ptr reference_internal fallback * fix: avoid copy constructor instantiation in shared_ptr fallback cast * Remove stray empty line * tests: rename PyTorch shared_ptr regression test files * refactor: add cast_non_owning helper for reference-like casts Name the non-owning generic cast path so callers do not have to rediscover that reference-like policies must pass null copy/move constructor callbacks. This keeps the shared_ptr reference_internal fallback self-documenting and points future maintainers toward the safe API. Made-with: Cursor * tests: guard deprecated-copy warning probes with __has_warning Use __has_warning for the Clang-only regression test so older compiler jobs skip unsupported warning groups instead of failing with -Wunknown-warning-option. A simple __clang_major__ >= 13 guard would be shorter, but it bakes in a version cutoff; __has_warning is slightly more verbose while being more robust to vendor builds, backports, and future packaging differences. Made-with: Cursor --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- include/pybind11/cast.h | 2 +- include/pybind11/detail/type_caster_base.h | 12 ++++ tests/CMakeLists.txt | 1 + tests/test_class_sh_property.py | 12 ++++ ...est_pytorch_shared_ptr_cast_regression.cpp | 62 +++++++++++++++++++ ...test_pytorch_shared_ptr_cast_regression.py | 25 ++++++++ 6 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/test_pytorch_shared_ptr_cast_regression.cpp create mode 100644 tests/test_pytorch_shared_ptr_cast_regression.py diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index f07abfea3..9ebabcebb 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1026,7 +1026,7 @@ public: } if (parent) { - return type_caster_base::cast( + return type_caster_generic::cast_non_owning( srcs, return_value_policy::reference_internal, parent); } diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index b0c59e113..8fbf700e1 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -1004,6 +1004,18 @@ public: return cast(srcs, policy, parent, copy_constructor, move_constructor, existing_holder); } + static handle cast_non_owning(const cast_sources &srcs, + return_value_policy policy, + handle parent, + const void *existing_holder = nullptr) { + // Reference-like policies alias an existing C++ object instead of creating + // a new one, so copy/move constructor callbacks must remain null here. + assert(policy == return_value_policy::reference + || policy == return_value_policy::reference_internal + || policy == return_value_policy::automatic_reference); + return cast(srcs, policy, parent, nullptr, nullptr, existing_holder); + } + PYBIND11_NOINLINE static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e87b1e93b..d5d597cdf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -167,6 +167,7 @@ set(PYBIND11_TEST_FILES test_operator_overloading test_pickling test_potentially_slicing_weak_ptr + test_pytorch_shared_ptr_cast_regression test_python_multiple_inheritance test_pytypes test_scoped_critical_section diff --git a/tests/test_class_sh_property.py b/tests/test_class_sh_property.py index 4a7b77c69..b8a5933e5 100644 --- a/tests/test_class_sh_property.py +++ b/tests/test_class_sh_property.py @@ -204,3 +204,15 @@ def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member(): legacy = obj.legacy legacy.value = 13 assert obj.legacy.value == 13 + + +def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member_multiple_reads(): + obj = m.ShWithSimpleStructMember() + + a = obj.legacy + b = obj.legacy + + a.value = 13 + + assert b.value == 13 + assert obj.legacy.value == 13 diff --git a/tests/test_pytorch_shared_ptr_cast_regression.cpp b/tests/test_pytorch_shared_ptr_cast_regression.cpp new file mode 100644 index 000000000..142596224 --- /dev/null +++ b/tests/test_pytorch_shared_ptr_cast_regression.cpp @@ -0,0 +1,62 @@ +#include "pybind11_tests.h" + +#include +#include + +#if defined(__clang__) +# if __has_warning("-Wdeprecated-copy-with-user-provided-dtor") +# pragma clang diagnostic error "-Wdeprecated-copy-with-user-provided-dtor" +# endif +# if __has_warning("-Wdeprecated-copy-with-dtor") +# pragma clang diagnostic error "-Wdeprecated-copy-with-dtor" +# endif +#endif + +namespace test_pytorch_regressions { + +// Directly extracted from PyTorch patterns that regressed in CI. +struct TracingState : std::enable_shared_from_this { + TracingState() = default; + ~TracingState() = default; + int value = 0; +}; + +const std::shared_ptr &get_tracing_state() { + static std::shared_ptr state = std::make_shared(); + return state; +} + +struct InterfaceType { + ~InterfaceType() = default; + int value = 0; +}; +using InterfaceTypePtr = std::shared_ptr; + +struct CompilationUnit { + InterfaceTypePtr iface = std::make_shared(); + + InterfaceTypePtr get_interface(const std::string &) const { return iface; } +}; + +} // namespace test_pytorch_regressions + +TEST_SUBMODULE(pybind11_pytorch_regressions, m) { + using namespace test_pytorch_regressions; + + py::class_>(m, "TracingState") + .def(py::init<>()) + .def_readwrite("value", &TracingState::value); + + m.def("_get_tracing_state", []() { return get_tracing_state(); }); + + py::class_(m, "InterfaceType") + .def(py::init<>()) + .def_readwrite("value", &InterfaceType::value); + + py::class_>(m, "CompilationUnit") + .def(py::init<>()) + .def("get_interface", + [](const std::shared_ptr &self, const std::string &name) { + return self->get_interface(name); + }); +} diff --git a/tests/test_pytorch_shared_ptr_cast_regression.py b/tests/test_pytorch_shared_ptr_cast_regression.py new file mode 100644 index 000000000..b7c393b32 --- /dev/null +++ b/tests/test_pytorch_shared_ptr_cast_regression.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pybind11_tests import pybind11_pytorch_regressions as m + + +def test_pytorch_like_get_tracing_state_aliases_singleton_shared_ptr(): + a = m._get_tracing_state() + b = m._get_tracing_state() + + a.value = 17 + + assert b.value == 17 + assert m._get_tracing_state().value == 17 + + +def test_pytorch_like_compilation_unit_get_interface_aliases_member_shared_ptr(): + cu = m.CompilationUnit() + + a = cu.get_interface("iface") + b = cu.get_interface("iface") + + a.value = 23 + + assert b.value == 23 + assert cu.get_interface("iface").value == 23 From 98003e29c878f72452d43c54616b72051eed06fe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 13:45:53 -0700 Subject: [PATCH 3/9] chore(deps): update pre-commit hooks (#6029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(deps): update pre-commit hooks updates: - [github.com/pre-commit/mirrors-clang-format: v22.1.0 → v22.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.0...v22.1.2) - [github.com/astral-sh/ruff-pre-commit: v0.15.4 → v0.15.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.4...v0.15.9) - [github.com/pre-commit/mirrors-mypy: v1.19.1 → v1.20.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.1...v1.20.0) - [github.com/codespell-project/codespell: v2.4.1 → v2.4.2](https://github.com/codespell-project/codespell/compare/v2.4.1...v2.4.2) - [github.com/adhtruong/mirrors-typos: v1.44.0 → v1.45.0](https://github.com/adhtruong/mirrors-typos/compare/v1.44.0...v1.45.0) - [github.com/python-jsonschema/check-jsonschema: 0.37.0 → 0.37.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.37.0...0.37.1) * fix: allow NumPy writeable spelling in typos NumPy uses `writeable` in public flags and API names, so typos should treat that spelling as intentional instead of blocking pre-commit runs. Made-with: Cursor --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve --- .pre-commit-config.yaml | 12 ++++++------ pyproject.toml | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ee5846b5..637dc3f94 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: # Clang format the codebase automatically - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v22.1.0" + rev: "v22.1.2" hooks: - id: clang-format types_or: [c++, c, cuda] # Ruff, the Python auto-correcting linter/formatter written in Rust - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.4 + rev: v0.15.9 hooks: - id: ruff-check args: ["--fix", "--show-fixes"] @@ -40,7 +40,7 @@ repos: # Check static types with mypy - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.19.1" + rev: "v1.20.0" hooks: - id: mypy args: [] @@ -112,7 +112,7 @@ repos: # Use tools/codespell_ignore_lines_from_errors.py # to rebuild .codespell-ignore-lines - repo: https://github.com/codespell-project/codespell - rev: "v2.4.1" + rev: "v2.4.2" hooks: - id: codespell exclude: "(.supp|^pyproject.toml)$" @@ -122,7 +122,7 @@ repos: # Use mirror because pre-commit autoupdate confuses tags in the upstream repo. # See https://github.com/crate-ci/typos/issues/390 - repo: https://github.com/adhtruong/mirrors-typos - rev: "v1.44.0" + rev: "v1.45.0" hooks: - id: typos args: [] @@ -151,7 +151,7 @@ repos: # Check schemas on some of our YAML files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.0 + rev: 0.37.1 hooks: - id: check-readthedocs - id: check-github-workflows diff --git a/pyproject.toml b/pyproject.toml index 6a300985a..03214c6f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -202,6 +202,10 @@ fo = "fo" quater = "quater" optin = "optin" othr = "othr" +# NumPy uses "writeable" in public API names and flags. +writeable = "writeable" +Writeable = "Writeable" +WRITEABLE = "WRITEABLE" #[tool.typos.type.cpp.extend-words] setp = "setp" From e2fdf43609ef15d3958312a3faada7393383315b Mon Sep 17 00:00:00 2001 From: Max Bachmann Date: Sun, 12 Apr 2026 05:02:27 +0200 Subject: [PATCH 4/9] Handle result from PyObject_VisitManagedDict (#6032) * Handle result from PyObject_VisitManagedDict * add unit test * style: pre-commit fixes * use different variable name This avoids a warning on msvc about Py_Visit shadowing the vret variable. * skip test_get_referrers on unsupported runtimes The managed-dict referrer check is only known to work on CPython 3.13.13+ and 3.14.4+, while earlier releases and non-CPython interpreters can report different traversal behavior. Made-with: Cursor --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ralf W. Grosse-Kunstleve --- include/pybind11/detail/class.h | 5 ++++- tests/test_class.cpp | 6 ++++++ tests/test_class.py | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index 4b7422eee..8b9d0b8e9 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -578,7 +578,10 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) { /// dynamic_attr: Allow the garbage collector to traverse the internal instance `__dict__`. extern "C" inline int pybind11_traverse(PyObject *self, visitproc visit, void *arg) { #if PY_VERSION_HEX >= 0x030D0000 - PyObject_VisitManagedDict(self, visit, arg); + int ret = PyObject_VisitManagedDict(self, visit, arg); + if (ret) { + return ret; + } #else PyObject *&dict = *_PyObject_GetDictPtr(self); Py_VISIT(dict); diff --git a/tests/test_class.cpp b/tests/test_class.cpp index 2030cd671..84efb800d 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -104,6 +104,10 @@ TEST_SUBMODULE(class_, m) { ~NoConstructorNew() { print_destroyed(this); } }; + struct DynamicAttr { + DynamicAttr() = default; + }; + py::class_(m, "NoConstructor") .def_static("new_instance", &NoConstructor::new_instance, "Return an instance"); @@ -112,6 +116,8 @@ TEST_SUBMODULE(class_, m) { .def_static("__new__", [](const py::object &) { return NoConstructorNew::new_instance(); }); + py::class_(m, "DynamicAttr", py::dynamic_attr()).def(py::init<>()); + // test_pass_unique_ptr struct ToBeHeldByUniquePtr {}; py::class_>(m, "ToBeHeldByUniquePtr") diff --git a/tests/test_class.py b/tests/test_class.py index fae6a3189..201c7e339 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -1,5 +1,6 @@ from __future__ import annotations +import gc import sys from unittest import mock @@ -18,6 +19,13 @@ def refcount_immortal(ob: object) -> int: return sys.getrefcount(ob) +MANAGED_DICT_GET_REFERRERS_SUPPORTED = ( + env.CPYTHON + and sys.version_info >= (3, 13, 13) + and (sys.version_info < (3, 14) or sys.version_info >= (3, 14, 4)) +) + + def test_obj_class_name(): expected_name = "UserType" if env.PYPY else "pybind11_tests.UserType" assert m.obj_class_name(UserType(1)) == expected_name @@ -45,6 +53,16 @@ def test_instance(msg): assert cstats.alive() == 0 +@pytest.mark.skipif( + not MANAGED_DICT_GET_REFERRERS_SUPPORTED, + reason="Requires CPython 3.13.13+ or 3.14.4+ managed dict traversal support", +) +def test_get_referrers(): + instance = m.DynamicAttr() + instance.a = "test" + assert instance in gc.get_referrers(instance.__dict__) + + def test_instance_new(): instance = m.NoConstructorNew() # .__new__(m.NoConstructor.__class__) From a15579cba8b0906937392bb710a16270328c7d27 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 12 Apr 2026 01:45:57 -0400 Subject: [PATCH 5/9] ci: bump setup-uv to maintained tag scheme (#6035) The old vX tags have been dropped to (force) (usually) better security practices. Dependabot will not update, however, leaving this v7 tag forever. Manually updating now. See https://github.com/astral-sh/setup-uv/issues/830 Committed via https://github.com/asottile/all-repos --- .github/workflows/ci.yml | 2 +- .github/workflows/configure.yml | 2 +- .github/workflows/nightlies.yml | 2 +- .github/workflows/pip.yml | 4 ++-- .github/workflows/reusable-standard.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaa1c2c0c..214968572 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,7 +188,7 @@ jobs: allow-prereleases: true - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: true diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index e3d99dd87..e84942114 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -56,7 +56,7 @@ jobs: python-version: 3.11 - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 - name: Prepare env run: uv pip install --python=python --system -r tests/requirements.txt diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 98211ca55..d7ab9b041 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -25,7 +25,7 @@ jobs: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 - name: Build SDist and wheels run: | diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 981884a95..c52857cff 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -31,7 +31,7 @@ jobs: python-version: 3.8 - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 - name: Prepare env run: uv pip install --system -r tests/requirements.txt @@ -55,7 +55,7 @@ jobs: python-version: 3.8 - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 - name: Prepare env run: uv pip install --system -r tests/requirements.txt twine nox diff --git a/.github/workflows/reusable-standard.yml b/.github/workflows/reusable-standard.yml index 6e22d0f38..e53c27551 100644 --- a/.github/workflows/reusable-standard.yml +++ b/.github/workflows/reusable-standard.yml @@ -51,7 +51,7 @@ jobs: run: brew install boost - name: Install uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@v8.0.0 with: enable-cache: true From 804e2c1b39ec55899efcfe3b49b324b10d8cbc9c Mon Sep 17 00:00:00 2001 From: Agis Kounelis <36283973+kounelisagis@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:08:06 +0300 Subject: [PATCH 6/9] fix: segfault when moving `scoped_ostream_redirect` (#6033) * fix: segfault when moving `scoped_ostream_redirect` The default move constructor left the stream (`std::cout`) pointing at the moved-from `pythonbuf`, whose internal buffer and streambuf pointers were nulled by the move. Any subsequent write through the stream dereferenced null, causing a segfault. Replace `= default` with an explicit move constructor that re-points the stream to the new buffer and disarms the moved-from destructor. * fix: mark move constructor noexcept to satisfy clang-tidy * fix: use bool flag instead of nullptr sentinel for moved-from state Using `old == nullptr` as the moved-from sentinel was incorrect because nullptr is a valid original rdbuf() value (e.g. `std::ostream os(nullptr)`). Replace with an explicit `active` flag so the destructor correctly restores nullptr buffers. Add tests for the nullptr-rdbuf edge case. * fix: remove noexcept and propagate active flag from source - Remove noexcept: pythonbuf inherits from std::streambuf whose move is not guaranteed nothrow on all implementations. Suppress clang-tidy with NOLINTNEXTLINE instead. - Initialize active from other.active so that moving an already moved-from object does not incorrectly re-activate the redirect. - Only rebind the stream and disarm the source when active. * test: add unflushed ostream redirect regression Cover the buffered-before-move case for `scoped_ostream_redirect`, which still crashes despite the current move fix. This gives the PR a direct reproducer for the remaining bug path. Made-with: Cursor * fix: disarm moved-from pythonbuf after redirect move The redirect guard now survives moves, but buffered output could still remain in the moved-from `pythonbuf` and be flushed during destruction through moved-out Python handles. Rebuild the destination put area from the transferred storage and clear the source put area so unflushed bytes follow the active redirect instead of crashing in the moved-from destructor. Made-with: Cursor --------- Co-authored-by: Ralf W. Grosse-Kunstleve --- include/pybind11/iostream.h | 34 +++++++++++++++++++++++++++++++--- tests/test_iostream.cpp | 36 ++++++++++++++++++++++++++++++++++++ tests/test_iostream.py | 25 +++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/include/pybind11/iostream.h b/include/pybind11/iostream.h index 44261e881..df7fa3c38 100644 --- a/include/pybind11/iostream.h +++ b/include/pybind11/iostream.h @@ -131,7 +131,22 @@ public: setp(d_buffer.get(), d_buffer.get() + buf_size - 1); } - pythonbuf(pythonbuf &&) = default; + pythonbuf(pythonbuf &&other) noexcept + : buf_size(other.buf_size), d_buffer(std::move(other.d_buffer)), + pywrite(std::move(other.pywrite)), pyflush(std::move(other.pyflush)) { + const auto pending = (other.pbase() != nullptr && other.pptr() != nullptr) + ? static_cast(other.pptr() - other.pbase()) + : 0; + if (d_buffer != nullptr) { + // Rebuild the put area from the transferred storage. + setp(d_buffer.get(), d_buffer.get() + buf_size - 1); + pbump(pending); + } else { + setp(nullptr, nullptr); + } + // Prevent the moved-from destructor from flushing through moved-out handles. + other.setp(nullptr, nullptr); + } /// Sync before destroy ~pythonbuf() override { _sync(); } @@ -169,6 +184,7 @@ protected: std::streambuf *old; std::ostream &costream; detail::pythonbuf buffer; + bool active = true; public: explicit scoped_ostream_redirect(std::ostream &costream = std::cout, @@ -178,10 +194,22 @@ public: old = costream.rdbuf(&buffer); } - ~scoped_ostream_redirect() { costream.rdbuf(old); } + ~scoped_ostream_redirect() { + if (active) { + costream.rdbuf(old); + } + } scoped_ostream_redirect(const scoped_ostream_redirect &) = delete; - scoped_ostream_redirect(scoped_ostream_redirect &&other) = default; + // NOLINTNEXTLINE(performance-noexcept-move-constructor) + scoped_ostream_redirect(scoped_ostream_redirect &&other) + : old(other.old), costream(other.costream), buffer(std::move(other.buffer)), + active(other.active) { + if (active) { + costream.rdbuf(&buffer); // Re-point stream to our buffer + other.active = false; + } + } scoped_ostream_redirect &operator=(const scoped_ostream_redirect &) = delete; scoped_ostream_redirect &operator=(scoped_ostream_redirect &&) = delete; }; diff --git a/tests/test_iostream.cpp b/tests/test_iostream.cpp index 421eaa2dd..7484e734b 100644 --- a/tests/test_iostream.cpp +++ b/tests/test_iostream.cpp @@ -123,4 +123,40 @@ TEST_SUBMODULE(iostream, m) { .def("stop", &TestThread::stop) .def("join", &TestThread::join) .def("sleep", &TestThread::sleep); + + m.def("move_redirect_output", [](const std::string &msg_before, const std::string &msg_after) { + py::scoped_ostream_redirect redir1(std::cout, py::module_::import("sys").attr("stdout")); + std::cout << msg_before << std::flush; + py::scoped_ostream_redirect redir2(std::move(redir1)); + std::cout << msg_after << std::flush; + }); + + m.def("move_redirect_output_unflushed", + [](const std::string &msg_before, const std::string &msg_after) { + py::scoped_ostream_redirect redir1(std::cout, + py::module_::import("sys").attr("stdout")); + std::cout << msg_before; + py::scoped_ostream_redirect redir2(std::move(redir1)); + std::cout << msg_after << std::flush; + }); + + // Redirect a stream whose original rdbuf is nullptr, then move the redirect. + // Verifies that nullptr is correctly restored (not confused with a moved-from sentinel). + m.def("move_redirect_null_rdbuf", [](const std::string &msg) { + std::ostream os(nullptr); + py::scoped_ostream_redirect redir1(os, py::module_::import("sys").attr("stdout")); + os << msg << std::flush; + py::scoped_ostream_redirect redir2(std::move(redir1)); + os << msg << std::flush; + // After redir2 goes out of scope, os.rdbuf() should be restored to nullptr. + }); + + m.def("get_null_rdbuf_restored", [](const std::string &msg) -> bool { + std::ostream os(nullptr); + { + py::scoped_ostream_redirect redir(os, py::module_::import("sys").attr("stdout")); + os << msg << std::flush; + } + return os.rdbuf() == nullptr; + }); } diff --git a/tests/test_iostream.py b/tests/test_iostream.py index 791b9e048..857e0b5f7 100644 --- a/tests/test_iostream.py +++ b/tests/test_iostream.py @@ -284,6 +284,31 @@ def test_redirect_both(capfd): assert stream2.getvalue() == msg2 +def test_move_redirect(capsys): + m.move_redirect_output("before_move", "after_move") + stdout, stderr = capsys.readouterr() + assert stdout == "before_moveafter_move" + assert not stderr + + +def test_move_redirect_unflushed(capsys): + m.move_redirect_output_unflushed("before_move", "after_move") + stdout, stderr = capsys.readouterr() + assert stdout == "before_moveafter_move" + assert not stderr + + +def test_move_redirect_null_rdbuf(capsys): + m.move_redirect_null_rdbuf("hello") + stdout, stderr = capsys.readouterr() + assert stdout == "hellohello" + assert not stderr + + +def test_null_rdbuf_restored(): + assert m.get_null_rdbuf_restored("test") + + @pytest.mark.skipif(sys.platform.startswith("emscripten"), reason="Requires threads") def test_threading(): with m.ostream_redirect(stdout=True, stderr=False): From 2c1b391115ba605ada1f31f8ed0cfb266925843a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 19 Apr 2026 01:47:16 +0700 Subject: [PATCH 7/9] [skip ci] docs: add v3.0.4 changelog updates. (#6041) Document the post-v3.0.3 fixes and CI changes ahead of the patch release so the release prep can be reviewed before the version bump work. Made-with: Cursor --- docs/changelog.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index f993034f1..37a3efbb3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -13,6 +13,31 @@ Changes will be added here periodically from the "Suggested changelog entry" block in pull request descriptions. +## Version 3.0.4 (April 18, 2026) + +Bug fixes: + +- Fixed move semantics of `scoped_ostream_redirect` to preserve buffered output and avoid crashes when moved redirects restore stream buffers. + [#6033](https://github.com/pybind/pybind11/pull/6033) + +- Fixed `py::dynamic_attr()` traversal on Python 3.13+ to correctly propagate `PyObject_VisitManagedDict()` results. + [#6032](https://github.com/pybind/pybind11/pull/6032) + +- Fixed `std::shared_ptr` fallback casting to avoid unnecessary copy-constructor instantiation in `reference_internal` paths. + [#6028](https://github.com/pybind/pybind11/pull/6028) + +CI: + +- Updated `setup-uv` to the maintained GitHub Action tag scheme. + [#6035](https://github.com/pybind/pybind11/pull/6035) + +- Updated pre-commit hooks. + [#6029](https://github.com/pybind/pybind11/pull/6029) + +- Updated GitHub Actions dependencies, including `actions-setup-cmake` and `cibuildwheel`. + [#6027](https://github.com/pybind/pybind11/pull/6027) + + ## Version 3.0.3 (March 31, 2026) Bug fixes: From 3d8aabc041f55bb522318b5949db3bac25648f8d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 18 Apr 2026 11:57:02 -0700 Subject: [PATCH 8/9] =?UTF-8?q?Bump=20version=20from=20v3.0.3=20=E2=86=92?= =?UTF-8?q?=20v3.0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- include/pybind11/detail/common.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 715892308..38cb01d70 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -19,7 +19,7 @@ /* -- start version constants -- */ #define PYBIND11_VERSION_MAJOR 3 #define PYBIND11_VERSION_MINOR 0 -#define PYBIND11_VERSION_MICRO 3 +#define PYBIND11_VERSION_MICRO 4 // ALPHA = 0xA, BETA = 0xB, GAMMA = 0xC (release candidate), FINAL = 0xF (stable release) // - The release level is set to "alpha" for development versions. // Use 0xA0 (LEVEL=0xA, SERIAL=0) for development versions. @@ -27,7 +27,7 @@ #define PYBIND11_VERSION_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL #define PYBIND11_VERSION_RELEASE_SERIAL 0 // String version of (micro, release level, release serial), e.g.: 0a0, 0b1, 0rc1, 0 -#define PYBIND11_VERSION_PATCH 3 +#define PYBIND11_VERSION_PATCH 4 /* -- end version constants -- */ #if !defined(Py_PACK_FULL_VERSION) From d03662f0984f652b60e7ddce53d3868002275197 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Sun, 19 Apr 2026 07:10:30 +0900 Subject: [PATCH 9/9] build: support Eigen 5 (#6036) * build: support Eigen 5 fix #6034 * build: probe Eigen 3 and 5 separately in CMake config mode Avoid relying on package-specific handling of a bounded version range when discovering Eigen through Eigen3Config.cmake. Made-with: Cursor * build: clarify Eigen 5 module fallback comment Explain that the MODULE-mode fallback only exists for older Eigen 3 setups so the remaining fallback path does not look like an unresolved Eigen 5 issue. Made-with: Cursor * docs: add Eigen 5 entry to v3.0.4 changelog Document the Eigen 5 CMake package detection fix in the 3.0.4 release notes before merging the PR. Made-with: Cursor --------- Co-authored-by: Eisuke Kawashima Co-authored-by: Ralf W. Grosse-Kunstleve --- docs/changelog.md | 3 +++ tests/CMakeLists.txt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 37a3efbb3..ecff9705e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -17,6 +17,9 @@ entry" block in pull request descriptions. Bug fixes: +- Fixed test builds with installed Eigen 5 by improving `Eigen3` CMake package detection. + [#6036](https://github.com/pybind/pybind11/pull/6036) + - Fixed move semantics of `scoped_ostream_redirect` to preserve buffered output and avoid crashes when moved redirects restore stream buffers. [#6033](https://github.com/pybind/pybind11/pull/6033) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d5d597cdf..fc08a9056 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -300,10 +300,17 @@ if(PYBIND11_TEST_FILES_EIGEN_I GREATER -1) else() find_package(Eigen3 3.2.7 QUIET CONFIG) + if(NOT Eigen3_FOUND) + find_package(Eigen3 5 QUIET CONFIG) + endif() + set(EIGEN3_FOUND ${Eigen3_FOUND}) + set(EIGEN3_VERSION ${Eigen3_VERSION}) if(NOT EIGEN3_FOUND) # Couldn't load via target, so fall back to allowing module mode finding, which will pick up # tools/FindEigen3.cmake + # This MODULE-mode fallback is for older Eigen 3 setups; Eigen 5 is expected to be found + # via the CONFIG-mode probes above. find_package(Eigen3 3.2.7 QUIET) endif() endif()