From f880da434d45a864f3e00b1433e9d476991c8c78 Mon Sep 17 00:00:00 2001 From: Daniel Simon Date: Mon, 26 Jan 2026 12:37:28 -0800 Subject: [PATCH 1/3] Add helper functions to pybind11::array to return the shape and strides as a std::span. These functions are hidden with macros unless PYBIND11_CPP20 is defined and the include has been found. --- include/pybind11/detail/common.h | 4 ++++ include/pybind11/numpy.h | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 19ebc8532e..4225042879 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -214,6 +214,10 @@ # define PYBIND11_HAS_STRING_VIEW 1 #endif +#if defined(PYBIND11_CPP20) && defined(__has_include) && __has_include() +# define PYBIND11_HAS_SPAN 1 +#endif + #if (defined(PYPY_VERSION) || defined(GRAALVM_PYTHON)) && !defined(PYBIND11_SIMPLE_GIL_MANAGEMENT) # define PYBIND11_SIMPLE_GIL_MANAGEMENT #endif diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index 6fa6c772b9..d2dc65b23e 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -29,6 +29,11 @@ #include #include +#ifdef PYBIND11_HAS_SPAN +#include +#endif + + #if defined(PYBIND11_NUMPY_1_ONLY) # error "PYBIND11_NUMPY_1_ONLY is no longer supported (see PR #5595)." #endif @@ -1143,6 +1148,13 @@ class array : public buffer { /// Dimensions of the array const ssize_t *shape() const { return detail::array_proxy(m_ptr)->dimensions; } +#ifdef PYBIND11_HAS_SPAN + /// Dimensions of the array as a span + std::span shape_span() const { + return std::span(shape(), static_cast(ndim())); + } +#endif + /// Dimension along a given axis ssize_t shape(ssize_t dim) const { if (dim >= ndim()) { @@ -1154,6 +1166,13 @@ class array : public buffer { /// Strides of the array const ssize_t *strides() const { return detail::array_proxy(m_ptr)->strides; } +#ifdef PYBIND11_HAS_SPAN + /// Strides of the array as a span + std::span strides_span() const { + return std::span(strides(), static_cast(ndim())); + } +#endif + /// Stride along a given axis ssize_t strides(ssize_t dim) const { if (dim >= ndim()) { From 341d5ad803643d5d6696a1622e7fdadc02159078 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:46:30 +0000 Subject: [PATCH 2/3] style: pre-commit fixes --- include/pybind11/numpy.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index d2dc65b23e..408d1699cf 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -30,10 +30,9 @@ #include #ifdef PYBIND11_HAS_SPAN -#include +# include #endif - #if defined(PYBIND11_NUMPY_1_ONLY) # error "PYBIND11_NUMPY_1_ONLY is no longer supported (see PR #5595)." #endif From 6847f18dacb151deae21756a34a17a4e940950e9 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 28 Jan 2026 23:23:08 -0800 Subject: [PATCH 3/3] tests: Add unit tests for shape_span() and strides_span() Add comprehensive unit tests for the new std::span helper functions: - Test 0D, 1D, 2D, and 3D arrays - Verify spans match regular shape()/strides() methods - Test that spans can be used to construct new arrays - Tests are conditionally compiled only when PYBIND11_HAS_SPAN is defined --- tests/test_numpy_array.cpp | 17 +++++++++++++++++ tests/test_numpy_array.py | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/tests/test_numpy_array.cpp b/tests/test_numpy_array.cpp index ac6b1cfe35..90e88d37c1 100644 --- a/tests/test_numpy_array.cpp +++ b/tests/test_numpy_array.cpp @@ -14,6 +14,7 @@ #include #include +#include // Size / dtype checks. struct DtypeCheck { @@ -246,6 +247,22 @@ TEST_SUBMODULE(numpy_array, sm) { sm.def("nbytes", [](const arr &a) { return a.nbytes(); }); sm.def("owndata", [](const arr &a) { return a.owndata(); }); +#ifdef PYBIND11_HAS_SPAN + // test_shape_strides_span + sm.def("shape_span", [](const arr &a) { + auto span = a.shape_span(); + return std::vector(span.begin(), span.end()); + }); + sm.def("strides_span", [](const arr &a) { + auto span = a.strides_span(); + return std::vector(span.begin(), span.end()); + }); + // Test that spans can be used to construct new arrays + sm.def("array_from_spans", [](const arr &a) { + return py::array(a.dtype(), a.shape_span(), a.strides_span(), a.data(), a); + }); +#endif + // test_index_offset def_index_fn(index_at, const arr &); def_index_fn(index_at_t, const arr_t &); diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py index 19c07ca11c..93477aa23f 100644 --- a/tests/test_numpy_array.py +++ b/tests/test_numpy_array.py @@ -68,6 +68,45 @@ def test_array_attributes(): assert not m.owndata(a) +@pytest.mark.skipif(not hasattr(m, "shape_span"), reason="std::span not available") +def test_shape_strides_span(): + # Test 0-dimensional array (scalar) + a = np.array(42, "f8") + assert m.ndim(a) == 0 + assert m.shape_span(a) == [] + assert m.strides_span(a) == [] + + # Test 1-dimensional array + a = np.array([1, 2, 3, 4], "u2") + assert m.ndim(a) == 1 + assert m.shape_span(a) == [4] + assert m.strides_span(a) == [2] + + # Test 2-dimensional array + a = np.array([[1, 2, 3], [4, 5, 6]], "u2").view() + a.flags.writeable = False + assert m.ndim(a) == 2 + assert m.shape_span(a) == [2, 3] + assert m.strides_span(a) == [6, 2] + + # Test 3-dimensional array + a = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]], "i4") + assert m.ndim(a) == 3 + assert m.shape_span(a) == [2, 2, 2] + # Verify spans match regular shape/strides + assert list(m.shape_span(a)) == list(m.shape(a)) + assert list(m.strides_span(a)) == list(m.strides(a)) + + # Test that spans can be used to construct new arrays + original = np.array([[1, 2, 3], [4, 5, 6]], "f4") + new_array = m.array_from_spans(original) + assert new_array.shape == original.shape + assert new_array.strides == original.strides + assert new_array.dtype == original.dtype + # Verify data is shared (since we pass the same data pointer) + np.testing.assert_array_equal(new_array, original) + + @pytest.mark.parametrize( ("args", "ret"), [([], 0), ([0], 0), ([1], 3), ([0, 1], 1), ([1, 2], 5)] )