fix(numpy): accept PYBIND11_TYPE-wrapped types in the dtype macros (#6148)
* fix(numpy): accept PYBIND11_TYPE-wrapped types in the dtype macros
* docs: point at the macro notes from the structured types section
* fix(numpy): accept bare types in direct field descriptor macro calls
diff --git a/docs/advanced/pycpp/numpy.rst b/docs/advanced/pycpp/numpy.rst
index e0b9ff4..fff5296 100644
--- a/docs/advanced/pycpp/numpy.rst
+++ b/docs/advanced/pycpp/numpy.rst
@@ -232,6 +232,10 @@
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<int, double>), x, y)``.
+See :ref:`macro_notes`.
+
Scalar types
============
diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h
index 10c0c94..22e11bc 100644
--- a/include/pybind11/numpy.h
+++ b/include/pybind11/numpy.h
@@ -1799,16 +1799,29 @@
# define PYBIND11_NUMPY_DTYPE_EX(Type, ...) ((void) 0)
#else
-# define PYBIND11_FIELD_DESCRIPTOR_EX(T, Field, Name) \
+// 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_IMPL(T, Field, Name) \
::pybind11::detail::field_descriptor { \
- Name, offsetof(T, Field), sizeof(decltype(std::declval<T>().Field)), \
- ::pybind11::format_descriptor<decltype(std::declval<T>().Field)>::format(), \
+ Name, offsetof(PYBIND11_UNPAREN_TYPE(T), Field), \
+ sizeof(decltype(std::declval<PYBIND11_UNPAREN_TYPE(T)>().Field)), \
+ ::pybind11::format_descriptor< \
+ decltype(std::declval<PYBIND11_UNPAREN_TYPE(T)>().Field)>::format(), \
::pybind11::detail::npy_format_descriptor< \
- decltype(std::declval<T>().Field)>::dtype() \
+ decltype(std::declval<PYBIND11_UNPAREN_TYPE(T)>().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
@@ -1846,7 +1859,7 @@
# define PYBIND11_NUMPY_DTYPE(Type, ...) \
::pybind11::detail::npy_format_descriptor<Type>::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) \
@@ -1868,7 +1881,7 @@
# define PYBIND11_NUMPY_DTYPE_EX(Type, ...) \
::pybind11::detail::npy_format_descriptor<Type>::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 f206da7..d6d79e2 100644
--- a/tests/test_numpy_dtypes.cpp
+++ b/tests/test_numpy_dtypes.cpp
@@ -102,6 +102,12 @@
uint64_t __y__;
});
+template <typename T1, typename T2>
+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,27 @@
PYBIND11_NUMPY_DTYPE(EnumStruct, e1, e2);
PYBIND11_NUMPY_DTYPE(ComplexStruct, cflt, cdbl);
+ // test_templated_dtype
+ PYBIND11_NUMPY_DTYPE(PYBIND11_TYPE(TemplatedStruct<int32_t, float>), a, b);
+ PYBIND11_NUMPY_DTYPE_EX(PYBIND11_TYPE(TemplatedStruct<int16_t, uint16_t>), a, "x", b, "y");
+ m.def("templated_dtypes", []() {
+ return py::make_tuple(py::dtype::of<TemplatedStruct<int32_t, float>>(),
+ py::dtype::of<TemplatedStruct<int16_t, uint16_t>>());
+ });
+
+ // 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<int32_t, float>), b)};
+ py::list names;
+ for (const auto &fd : direct) {
+ names.append(fd.name);
+ }
+ return names;
+ });
+
// ... or after
py::class_<PackedStruct>(m, "PackedStruct");
diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py
index ba45d8b..13a696c 100644
--- a/tests/test_numpy_dtypes.py
+++ b/tests/test_numpy_dtypes.py
@@ -205,6 +205,17 @@
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", "u2")])
+
+
+def test_direct_field_descriptor():
+ assert m.direct_field_descriptors() == ["uint_", "flt", "b"]
+
+
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)]