From 1e55cafb4b8f1d3323c7d9350fe17fc26c0e67fe 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 03cb0238cd..eaa1c2c0cd 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 931c0bff29..e3d99dd872 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 bf534316af..f7bca76e77 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 051cffc04a..51354d68c3 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 c9999fd4c4a31ab20ca8171eac1515b21b0d5a23 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 f07abfea3d..9ebabcebb4 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1026,7 +1026,7 @@ struct copyable_holder_caster< } 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 b0c59e1138..8fbf700e12 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -1004,6 +1004,18 @@ class type_caster_generic { 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 e87b1e93b3..d5d597cdff 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 4a7b77c69a..b8a5933e5f 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 0000000000..1425962242 --- /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 0000000000..b7c393b325 --- /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 e8381b9ebb5ef5e138a3b7d3b530493b6130d05d 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 5ee5846b5f..637dc3f94f 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 6a300985ad..03214c6f4b 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 cb7bb8a1477311c3dcf34063c00ce07026e927f5 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 4b7422eee2..8b9d0b8e9d 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 2030cd6715..84efb800db 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 fae6a31899..201c7e339e 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 034f35fd2ca6ea6ea4ec40741d4ab93a871b7132 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 eaa1c2c0cd..214968572d 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 e3d99dd872..e849421146 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 98211ca552..d7ab9b0413 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 981884a955..c52857cff5 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 6e22d0f38b..e53c27551f 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 a6516ad275589b29a360e3bb6d34448b5a73372b 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 44261e881e..df7fa3c381 100644 --- a/include/pybind11/iostream.h +++ b/include/pybind11/iostream.h @@ -131,7 +131,22 @@ class pythonbuf : public std::streambuf { 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 @@ class scoped_ostream_redirect { 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 @@ class scoped_ostream_redirect { 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 421eaa2dd8..7484e734be 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 791b9e0483..857e0b5f73 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 fc709ff4d455b2c28b588cd964d473d1e20bcd1f 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 f993034f14..37a3efbb3d 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 d342b9d4447ec2a65b037dcdf0e9c2b270b4c9d3 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 7158923084..38cb01d70a 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 2ecfb28f4d57bd6a6c527ec78e17e1e59bc446c0 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 37a3efbb3d..ecff9705e2 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 d5d597cdff..fc08a9056f 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()