From 34c3019218bad42de38344e640b49154e6279211 Mon Sep 17 00:00:00 2001 From: Advit Arora Date: Wed, 19 Aug 2026 14:03:37 +0530 Subject: [PATCH 1/3] fix(numpy): accept PYBIND11_TYPE-wrapped types in the dtype macros --- include/pybind11/numpy.h | 15 ++++++++++----- tests/test_numpy_dtypes.cpp | 14 ++++++++++++++ tests/test_numpy_dtypes.py | 7 +++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 10c0c9446c..94c42aa822 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -1799,12 +1799,17 @@ struct npy_format_descriptor { # define PYBIND11_NUMPY_DTYPE_EX(Type, ...) ((void) 0) #else +// T arrives parenthesized: the PYBIND11_MAP_LIST expansion below re-splits on commas (see #4018). +# define PYBIND11_UNPAREN_TYPE(T) PYBIND11_TYPE T + # define PYBIND11_FIELD_DESCRIPTOR_EX(T, Field, Name) \ ::pybind11::detail::field_descriptor { \ - Name, offsetof(T, Field), sizeof(decltype(std::declval().Field)), \ - ::pybind11::format_descriptor().Field)>::format(), \ + Name, offsetof(PYBIND11_UNPAREN_TYPE(T), Field), \ + sizeof(decltype(std::declval().Field)), \ + ::pybind11::format_descriptor< \ + decltype(std::declval().Field)>::format(), \ ::pybind11::detail::npy_format_descriptor< \ - decltype(std::declval().Field)>::dtype() \ + decltype(std::declval().Field)>::dtype() \ } // Extract name, offset and format descriptor for a struct field @@ -1846,7 +1851,7 @@ struct npy_format_descriptor { # define PYBIND11_NUMPY_DTYPE(Type, ...) \ ::pybind11::detail::npy_format_descriptor::register_dtype( \ ::std::vector<::pybind11::detail::field_descriptor>{ \ - PYBIND11_MAP_LIST(PYBIND11_FIELD_DESCRIPTOR, Type, __VA_ARGS__)}) + PYBIND11_MAP_LIST(PYBIND11_FIELD_DESCRIPTOR, (Type), __VA_ARGS__)}) # if defined(_MSC_VER) && !defined(__clang__) # define PYBIND11_MAP2_LIST_NEXT1(test, next) \ @@ -1868,7 +1873,7 @@ struct npy_format_descriptor { # define PYBIND11_NUMPY_DTYPE_EX(Type, ...) \ ::pybind11::detail::npy_format_descriptor::register_dtype( \ ::std::vector<::pybind11::detail::field_descriptor>{ \ - PYBIND11_MAP2_LIST(PYBIND11_FIELD_DESCRIPTOR_EX, Type, __VA_ARGS__)}) + PYBIND11_MAP2_LIST(PYBIND11_FIELD_DESCRIPTOR_EX, (Type), __VA_ARGS__)}) #endif // __CLION_IDE__ diff --git a/tests/test_numpy_dtypes.cpp b/tests/test_numpy_dtypes.cpp index f206da7323..02a17fb85f 100644 --- a/tests/test_numpy_dtypes.cpp +++ b/tests/test_numpy_dtypes.cpp @@ -102,6 +102,12 @@ PYBIND11_PACKED(struct StructWithUglyNames { uint64_t __y__; }); +template +struct TemplatedStruct { + T1 a; + T2 b; +}; + enum class E1 : int64_t { A = -1, B = 1 }; enum E2 : uint8_t { X = 1, Y = 2 }; @@ -352,6 +358,14 @@ TEST_SUBMODULE(numpy_dtypes, m) { PYBIND11_NUMPY_DTYPE(EnumStruct, e1, e2); PYBIND11_NUMPY_DTYPE(ComplexStruct, cflt, cdbl); + // test_templated_dtype + PYBIND11_NUMPY_DTYPE(PYBIND11_TYPE(TemplatedStruct), a, b); + PYBIND11_NUMPY_DTYPE_EX(PYBIND11_TYPE(TemplatedStruct), a, "x", b, "y"); + m.def("templated_dtypes", []() { + return py::make_tuple(py::dtype::of>(), + py::dtype::of>()); + }); + // ... or after py::class_(m, "PackedStruct"); diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py index ba45d8bf63..89fd491ef9 100644 --- a/tests/test_numpy_dtypes.py +++ b/tests/test_numpy_dtypes.py @@ -205,6 +205,13 @@ def test_dtype(simple_dtype): assert (m.test_dtype_switch(arr.astype("longdouble")) == arr + 1).all() +def test_templated_dtype(): + """A type spelled with a comma needs PYBIND11_TYPE here.""" + plain, renamed = m.templated_dtypes() + assert plain == np.dtype([("a", "i4"), ("b", "f4")]) + assert renamed == np.dtype([("x", "i2"), ("y", "i2")]) + + def test_recarray(simple_dtype, packed_dtype): elements = [(False, 0, 0.0, -0.0), (True, 1, 1.5, -2.5), (False, 2, 3.0, -5.0)] From a6e0d7c11e5a66f3badae963c2e0170728514780 Mon Sep 17 00:00:00 2001 From: Advit Arora Date: Wed, 19 Aug 2026 14:03:37 +0530 Subject: [PATCH 2/3] docs: point at the macro notes from the structured types section --- docs/advanced/pycpp/numpy.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst index e0b9ff46e9..fff529678f 100644 --- a/docs/advanced/pycpp/numpy.rst +++ b/docs/advanced/pycpp/numpy.rst @@ -232,6 +232,10 @@ prevent many types of unsupported structures, it is still the user's responsibility to use only "plain" structures that can be safely manipulated as raw memory without violating invariants. +Types whose spelling contains a comma must be wrapped in ``PYBIND11_TYPE``: +``PYBIND11_NUMPY_DTYPE(PYBIND11_TYPE(C), x, y)``. +See :ref:`macro_notes`. + Scalar types ============ From 1fee84ed8ab42b9c856bfb598576d886b79d9eb3 Mon Sep 17 00:00:00 2001 From: Advit Arora Date: Wed, 19 Aug 2026 22:44:23 +0530 Subject: [PATCH 3/3] fix(numpy): accept bare types in direct field descriptor macro calls --- include/pybind11/numpy.h | 18 +++++++++++++----- tests/test_numpy_dtypes.cpp | 17 +++++++++++++++-- tests/test_numpy_dtypes.py | 6 +++++- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 94c42aa822..22e11bcbad 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -1799,10 +1799,11 @@ struct npy_format_descriptor { # define PYBIND11_NUMPY_DTYPE_EX(Type, ...) ((void) 0) #else -// T arrives parenthesized: the PYBIND11_MAP_LIST expansion below re-splits on commas (see #4018). +// The _IMPL variants take T parenthesized to survive the comma re-splitting in the +// PYBIND11_MAP_LIST expansions below; the plain variants keep accepting a bare type (see #4018). # define PYBIND11_UNPAREN_TYPE(T) PYBIND11_TYPE T -# define PYBIND11_FIELD_DESCRIPTOR_EX(T, Field, Name) \ +# define PYBIND11_FIELD_DESCRIPTOR_EX_IMPL(T, Field, Name) \ ::pybind11::detail::field_descriptor { \ Name, offsetof(PYBIND11_UNPAREN_TYPE(T), Field), \ sizeof(decltype(std::declval().Field)), \ @@ -1812,8 +1813,15 @@ struct npy_format_descriptor { decltype(std::declval().Field)>::dtype() \ } +# define PYBIND11_FIELD_DESCRIPTOR_IMPL(T, Field) \ + PYBIND11_FIELD_DESCRIPTOR_EX_IMPL(T, Field, #Field) + +# define PYBIND11_FIELD_DESCRIPTOR_EX(T, Field, Name) \ + PYBIND11_FIELD_DESCRIPTOR_EX_IMPL((T), Field, Name) + // Extract name, offset and format descriptor for a struct field -# define PYBIND11_FIELD_DESCRIPTOR(T, Field) PYBIND11_FIELD_DESCRIPTOR_EX(T, Field, #Field) +# define PYBIND11_FIELD_DESCRIPTOR(T, Field) \ + PYBIND11_FIELD_DESCRIPTOR_EX_IMPL((T), Field, #Field) // The main idea of this macro is borrowed from https://github.com/swansontec/map-macro // (C) William Swanson, Paul Fultz @@ -1851,7 +1859,7 @@ struct npy_format_descriptor { # define PYBIND11_NUMPY_DTYPE(Type, ...) \ ::pybind11::detail::npy_format_descriptor::register_dtype( \ ::std::vector<::pybind11::detail::field_descriptor>{ \ - PYBIND11_MAP_LIST(PYBIND11_FIELD_DESCRIPTOR, (Type), __VA_ARGS__)}) + PYBIND11_MAP_LIST(PYBIND11_FIELD_DESCRIPTOR_IMPL, (Type), __VA_ARGS__)}) # if defined(_MSC_VER) && !defined(__clang__) # define PYBIND11_MAP2_LIST_NEXT1(test, next) \ @@ -1873,7 +1881,7 @@ struct npy_format_descriptor { # define PYBIND11_NUMPY_DTYPE_EX(Type, ...) \ ::pybind11::detail::npy_format_descriptor::register_dtype( \ ::std::vector<::pybind11::detail::field_descriptor>{ \ - PYBIND11_MAP2_LIST(PYBIND11_FIELD_DESCRIPTOR_EX, (Type), __VA_ARGS__)}) + PYBIND11_MAP2_LIST(PYBIND11_FIELD_DESCRIPTOR_EX_IMPL, (Type), __VA_ARGS__)}) #endif // __CLION_IDE__ diff --git a/tests/test_numpy_dtypes.cpp b/tests/test_numpy_dtypes.cpp index 02a17fb85f..d6d79e2fb8 100644 --- a/tests/test_numpy_dtypes.cpp +++ b/tests/test_numpy_dtypes.cpp @@ -360,10 +360,23 @@ TEST_SUBMODULE(numpy_dtypes, m) { // test_templated_dtype PYBIND11_NUMPY_DTYPE(PYBIND11_TYPE(TemplatedStruct), a, b); - PYBIND11_NUMPY_DTYPE_EX(PYBIND11_TYPE(TemplatedStruct), a, "x", b, "y"); + PYBIND11_NUMPY_DTYPE_EX(PYBIND11_TYPE(TemplatedStruct), a, "x", b, "y"); m.def("templated_dtypes", []() { return py::make_tuple(py::dtype::of>(), - py::dtype::of>()); + py::dtype::of>()); + }); + + // test_direct_field_descriptor + m.def("direct_field_descriptors", []() { + py::detail::field_descriptor direct[] + = {PYBIND11_FIELD_DESCRIPTOR(SimpleStruct, uint_), + PYBIND11_FIELD_DESCRIPTOR_EX(SimpleStruct, float_, "flt"), + PYBIND11_FIELD_DESCRIPTOR(PYBIND11_TYPE(TemplatedStruct), b)}; + py::list names; + for (const auto &fd : direct) { + names.append(fd.name); + } + return names; }); // ... or after diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py index 89fd491ef9..13a696c1b5 100644 --- a/tests/test_numpy_dtypes.py +++ b/tests/test_numpy_dtypes.py @@ -209,7 +209,11 @@ def test_templated_dtype(): """A type spelled with a comma needs PYBIND11_TYPE here.""" plain, renamed = m.templated_dtypes() assert plain == np.dtype([("a", "i4"), ("b", "f4")]) - assert renamed == np.dtype([("x", "i2"), ("y", "i2")]) + assert renamed == np.dtype([("x", "i2"), ("y", "u2")]) + + +def test_direct_field_descriptor(): + assert m.direct_field_descriptors() == ["uint_", "flt", "b"] def test_recarray(simple_dtype, packed_dtype):