Merge tag 'v3.0.4' into stable

v3.0.4 release
This commit is contained in:
Ralf W. Grosse-Kunstleve
2026-04-18 20:05:13 -07:00
23 changed files with 297 additions and 30 deletions

View File

@@ -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
@@ -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

View File

@@ -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
@@ -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 }}

View File

@@ -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: |

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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: |

View File

@@ -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

View File

@@ -13,6 +13,34 @@ 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 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)
- 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<T>` 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:

View File

@@ -1026,7 +1026,7 @@ public:
}
if (parent) {
return type_caster_base<type>::cast(
return type_caster_generic::cast_non_owning(
srcs, return_value_policy::reference_internal, parent);
}

View File

@@ -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);

View File

@@ -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)

View File

@@ -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,

View File

@@ -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<int>(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;
};

View File

@@ -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"

View File

@@ -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
@@ -299,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()

View File

@@ -104,6 +104,10 @@ TEST_SUBMODULE(class_, m) {
~NoConstructorNew() { print_destroyed(this); }
};
struct DynamicAttr {
DynamicAttr() = default;
};
py::class_<NoConstructor>(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_<DynamicAttr>(m, "DynamicAttr", py::dynamic_attr()).def(py::init<>());
// test_pass_unique_ptr
struct ToBeHeldByUniquePtr {};
py::class_<ToBeHeldByUniquePtr, std::unique_ptr<ToBeHeldByUniquePtr>>(m, "ToBeHeldByUniquePtr")

View File

@@ -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__)

View File

@@ -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

View File

@@ -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;
});
}

View File

@@ -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):

View File

@@ -0,0 +1,62 @@
#include "pybind11_tests.h"
#include <memory>
#include <string>
#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> {
TracingState() = default;
~TracingState() = default;
int value = 0;
};
const std::shared_ptr<TracingState> &get_tracing_state() {
static std::shared_ptr<TracingState> state = std::make_shared<TracingState>();
return state;
}
struct InterfaceType {
~InterfaceType() = default;
int value = 0;
};
using InterfaceTypePtr = std::shared_ptr<InterfaceType>;
struct CompilationUnit {
InterfaceTypePtr iface = std::make_shared<InterfaceType>();
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_<TracingState, std::shared_ptr<TracingState>>(m, "TracingState")
.def(py::init<>())
.def_readwrite("value", &TracingState::value);
m.def("_get_tracing_state", []() { return get_tracing_state(); });
py::class_<InterfaceType, InterfaceTypePtr>(m, "InterfaceType")
.def(py::init<>())
.def_readwrite("value", &InterfaceType::value);
py::class_<CompilationUnit, std::shared_ptr<CompilationUnit>>(m, "CompilationUnit")
.def(py::init<>())
.def("get_interface",
[](const std::shared_ptr<CompilationUnit> &self, const std::string &name) {
return self->get_interface(name);
});
}

View File

@@ -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