diff --git a/include/pybind11/eigen/matrix.h b/include/pybind11/eigen/matrix.h index d2999d61b..ca599c954 100644 --- a/include/pybind11/eigen/matrix.h +++ b/include/pybind11/eigen/matrix.h @@ -225,19 +225,22 @@ struct EigenProps { = !show_c_contiguous && show_order && requires_col_major; static constexpr auto descriptor - = const_name("numpy.ndarray[") + npy_format_descriptor::name + const_name("[") + = const_name("typing.Annotated[") + + io_name("numpy.typing.ArrayLike, ", "numpy.typing.NDArray[") + + npy_format_descriptor::name + io_name("", "]") + const_name(", \"[") + const_name(const_name<(size_t) rows>(), const_name("m")) + const_name(", ") - + const_name(const_name<(size_t) cols>(), const_name("n")) + const_name("]") - + + + const_name(const_name<(size_t) cols>(), const_name("n")) + + const_name("]\"") // For a reference type (e.g. Ref) we have other constraints that might need to // be satisfied: writeable=True (for a mutable reference), and, depending on the map's // stride options, possibly f_contiguous or c_contiguous. We include them in the // descriptor output to provide some hint as to why a TypeError is occurring (otherwise - // it can be confusing to see that a function accepts a 'numpy.ndarray[float64[3,2]]' and - // an error message that you *gave* a numpy.ndarray of the right type and dimensions. - const_name(", flags.writeable", "") - + const_name(", flags.c_contiguous", "") - + const_name(", flags.f_contiguous", "") + const_name("]"); + // it can be confusing to see that a function accepts a + // 'typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3,2]"]' and an error message + // that you *gave* a numpy.ndarray of the right type and dimensions. + + const_name(", \"flags.writeable\"", "") + + const_name(", \"flags.c_contiguous\"", "") + + const_name(", \"flags.f_contiguous\"", "") + const_name("]"); }; // Casts an Eigen type to numpy array. If given a base, the numpy array references the src data, @@ -441,7 +444,9 @@ public: } } - static constexpr auto name = props::descriptor; + // return_descr forces the use of NDArray instead of ArrayLike in args + // since Ref<...> args can only accept arrays. + static constexpr auto name = return_descr(props::descriptor); // Explicitly delete these: support python -> C++ conversion on these (i.e. these can be return // types but not bound arguments). We still provide them (with an explicitly delete) so that diff --git a/include/pybind11/eigen/tensor.h b/include/pybind11/eigen/tensor.h index 9b5d9e89b..50e8b50b1 100644 --- a/include/pybind11/eigen/tensor.h +++ b/include/pybind11/eigen/tensor.h @@ -124,13 +124,16 @@ struct eigen_tensor_helper< template struct get_tensor_descriptor { static constexpr auto details - = const_name(", flags.writeable", "") + const_name + = const_name(", \"flags.writeable\"", "") + const_name < static_cast(Type::Layout) - == static_cast(Eigen::RowMajor) > (", flags.c_contiguous", ", flags.f_contiguous"); + == static_cast(Eigen::RowMajor) + > (", \"flags.c_contiguous\"", ", \"flags.f_contiguous\""); static constexpr auto value - = const_name("numpy.ndarray[") + npy_format_descriptor::name - + const_name("[") + eigen_tensor_helper>::dimensions_descriptor - + const_name("]") + const_name(details, const_name("")) + const_name("]"); + = const_name("typing.Annotated[") + + io_name("numpy.typing.ArrayLike, ", "numpy.typing.NDArray[") + + npy_format_descriptor::name + io_name("", "]") + + const_name(", \"[") + eigen_tensor_helper>::dimensions_descriptor + + const_name("]\"") + const_name(details, const_name("")) + const_name("]"); }; // When EIGEN_AVOID_STL_ARRAY is defined, Eigen::DSizes does not have the begin() member @@ -502,7 +505,10 @@ protected: std::unique_ptr value; public: - static constexpr auto name = get_tensor_descriptor::value; + // return_descr forces the use of NDArray instead of ArrayLike since refs can only reference + // arrays + static constexpr auto name + = return_descr(get_tensor_descriptor::value); explicit operator MapType *() { return value.get(); } explicit operator MapType &() { return *value; } explicit operator MapType &&() && { return std::move(*value); } diff --git a/include/pybind11/numpy.h b/include/pybind11/numpy.h index ab224e1f1..3a370fe5a 100644 --- a/include/pybind11/numpy.h +++ b/include/pybind11/numpy.h @@ -175,7 +175,6 @@ inline numpy_internals &get_numpy_internals() { PYBIND11_NOINLINE module_ import_numpy_core_submodule(const char *submodule_name) { module_ numpy = module_::import("numpy"); str version_string = numpy.attr("__version__"); - module_ numpy_lib = module_::import("numpy.lib"); object numpy_version = numpy_lib.attr("NumpyVersion")(version_string); int major_version = numpy_version.attr("major").cast(); @@ -2183,7 +2182,8 @@ vectorize_helper vectorize_extractor(const Func &f, Retur template struct handle_type_name> { static constexpr auto name - = const_name("numpy.ndarray[") + npy_format_descriptor::name + const_name("]"); + = io_name("typing.Annotated[numpy.typing.ArrayLike, ", "numpy.typing.NDArray[") + + npy_format_descriptor::name + const_name("]"); }; PYBIND11_NAMESPACE_END(detail) diff --git a/tests/test_eigen_matrix.cpp b/tests/test_eigen_matrix.cpp index cb8e8c625..4e6689a79 100644 --- a/tests/test_eigen_matrix.cpp +++ b/tests/test_eigen_matrix.cpp @@ -440,4 +440,8 @@ TEST_SUBMODULE(eigen_matrix, m) { py::module_::import("numpy").attr("ones")(10); return v[0](5); }); + m.def("round_trip_vector", [](const Eigen::VectorXf &x) -> Eigen::VectorXf { return x; }); + m.def("round_trip_dense", [](const DenseMatrixR &m) -> DenseMatrixR { return m; }); + m.def("round_trip_dense_ref", + [](const Eigen::Ref &m) -> Eigen::Ref { return m; }); } diff --git a/tests/test_eigen_matrix.py b/tests/test_eigen_matrix.py index 47efc9d8f..9324c2a7d 100644 --- a/tests/test_eigen_matrix.py +++ b/tests/test_eigen_matrix.py @@ -95,19 +95,20 @@ def test_mutator_descriptors(): with pytest.raises(TypeError) as excinfo: m.fixed_mutator_r(zc) assert ( - "(arg0: numpy.ndarray[numpy.float32[5, 6]," - " flags.writeable, flags.c_contiguous]) -> None" in str(excinfo.value) + '(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float32], "[5, 6]",' + ' "flags.writeable", "flags.c_contiguous"]) -> None' in str(excinfo.value) ) with pytest.raises(TypeError) as excinfo: m.fixed_mutator_c(zr) assert ( - "(arg0: numpy.ndarray[numpy.float32[5, 6]," - " flags.writeable, flags.f_contiguous]) -> None" in str(excinfo.value) + '(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float32], "[5, 6]",' + ' "flags.writeable", "flags.f_contiguous"]) -> None' in str(excinfo.value) ) with pytest.raises(TypeError) as excinfo: m.fixed_mutator_a(np.array([[1, 2], [3, 4]], dtype="float32")) - assert "(arg0: numpy.ndarray[numpy.float32[5, 6], flags.writeable]) -> None" in str( - excinfo.value + assert ( + '(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float32], "[5, 6]", "flags.writeable"]) -> None' + in str(excinfo.value) ) zr.flags.writeable = False with pytest.raises(TypeError): @@ -201,7 +202,7 @@ def test_negative_stride_from_python(msg): msg(excinfo.value) == """ double_threer(): incompatible function arguments. The following argument types are supported: - 1. (arg0: numpy.ndarray[numpy.float32[1, 3], flags.writeable]) -> None + 1. (arg0: typing.Annotated[numpy.typing.NDArray[numpy.float32], "[1, 3]", "flags.writeable"]) -> None Invoked with: """ + repr(np.array([5.0, 4.0, 3.0], dtype="float32")) @@ -213,7 +214,7 @@ def test_negative_stride_from_python(msg): msg(excinfo.value) == """ double_threec(): incompatible function arguments. The following argument types are supported: - 1. (arg0: numpy.ndarray[numpy.float32[3, 1], flags.writeable]) -> None + 1. (arg0: typing.Annotated[numpy.typing.NDArray[numpy.float32], "[3, 1]", "flags.writeable"]) -> None Invoked with: """ + repr(np.array([7.0, 4.0, 1.0], dtype="float32")) @@ -634,16 +635,16 @@ def test_nocopy_wrapper(): with pytest.raises(TypeError) as excinfo: m.get_elem_nocopy(int_matrix_colmajor) assert "get_elem_nocopy(): incompatible function arguments." in str(excinfo.value) - assert ", flags.f_contiguous" in str(excinfo.value) + assert ', "flags.f_contiguous"' in str(excinfo.value) assert m.get_elem_nocopy(dbl_matrix_colmajor) == 8 with pytest.raises(TypeError) as excinfo: m.get_elem_nocopy(int_matrix_rowmajor) assert "get_elem_nocopy(): incompatible function arguments." in str(excinfo.value) - assert ", flags.f_contiguous" in str(excinfo.value) + assert ', "flags.f_contiguous"' in str(excinfo.value) with pytest.raises(TypeError) as excinfo: m.get_elem_nocopy(dbl_matrix_rowmajor) assert "get_elem_nocopy(): incompatible function arguments." in str(excinfo.value) - assert ", flags.f_contiguous" in str(excinfo.value) + assert ', "flags.f_contiguous"' in str(excinfo.value) # For the row-major test, we take a long matrix in row-major, so only the third is allowed: with pytest.raises(TypeError) as excinfo: @@ -651,20 +652,20 @@ def test_nocopy_wrapper(): assert "get_elem_rm_nocopy(): incompatible function arguments." in str( excinfo.value ) - assert ", flags.c_contiguous" in str(excinfo.value) + assert ', "flags.c_contiguous"' in str(excinfo.value) with pytest.raises(TypeError) as excinfo: m.get_elem_rm_nocopy(dbl_matrix_colmajor) assert "get_elem_rm_nocopy(): incompatible function arguments." in str( excinfo.value ) - assert ", flags.c_contiguous" in str(excinfo.value) + assert ', "flags.c_contiguous"' in str(excinfo.value) assert m.get_elem_rm_nocopy(int_matrix_rowmajor) == 8 with pytest.raises(TypeError) as excinfo: m.get_elem_rm_nocopy(dbl_matrix_rowmajor) assert "get_elem_rm_nocopy(): incompatible function arguments." in str( excinfo.value ) - assert ", flags.c_contiguous" in str(excinfo.value) + assert ', "flags.c_contiguous"' in str(excinfo.value) def test_eigen_ref_life_support(): @@ -700,25 +701,25 @@ def test_dense_signature(doc): assert ( doc(m.double_col) == """ - double_col(arg0: numpy.ndarray[numpy.float32[m, 1]]) -> numpy.ndarray[numpy.float32[m, 1]] + double_col(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float32, "[m, 1]"]) -> typing.Annotated[numpy.typing.NDArray[numpy.float32], "[m, 1]"] """ ) assert ( doc(m.double_row) == """ - double_row(arg0: numpy.ndarray[numpy.float32[1, n]]) -> numpy.ndarray[numpy.float32[1, n]] + double_row(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float32, "[1, n]"]) -> typing.Annotated[numpy.typing.NDArray[numpy.float32], "[1, n]"] """ ) assert doc(m.double_complex) == ( """ - double_complex(arg0: numpy.ndarray[numpy.complex64[m, 1]])""" - """ -> numpy.ndarray[numpy.complex64[m, 1]] + double_complex(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.complex64, "[m, 1]"])""" + """ -> typing.Annotated[numpy.typing.NDArray[numpy.complex64], "[m, 1]"] """ ) assert doc(m.double_mat_rm) == ( """ - double_mat_rm(arg0: numpy.ndarray[numpy.float32[m, n]])""" - """ -> numpy.ndarray[numpy.float32[m, n]] + double_mat_rm(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float32, "[m, n]"])""" + """ -> typing.Annotated[numpy.typing.NDArray[numpy.float32], "[m, n]"] """ ) @@ -817,3 +818,22 @@ def test_custom_operator_new(): o = m.CustomOperatorNew() np.testing.assert_allclose(o.a, 0.0) np.testing.assert_allclose(o.b.diagonal(), 1.0) + + +def test_arraylike_signature(doc): + assert doc(m.round_trip_vector) == ( + 'round_trip_vector(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float32, "[m, 1]"])' + ' -> typing.Annotated[numpy.typing.NDArray[numpy.float32], "[m, 1]"]' + ) + assert doc(m.round_trip_dense) == ( + 'round_trip_dense(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float32, "[m, n]"])' + ' -> typing.Annotated[numpy.typing.NDArray[numpy.float32], "[m, n]"]' + ) + assert doc(m.round_trip_dense_ref) == ( + 'round_trip_dense_ref(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float32], "[m, n]", "flags.writeable", "flags.c_contiguous"])' + ' -> typing.Annotated[numpy.typing.NDArray[numpy.float32], "[m, n]", "flags.writeable", "flags.c_contiguous"]' + ) + m.round_trip_vector([1.0, 2.0]) + m.round_trip_dense([[1.0, 2.0], [3.0, 4.0]]) + with pytest.raises(TypeError, match="incompatible function arguments"): + m.round_trip_dense_ref([[1.0, 2.0], [3.0, 4.0]]) diff --git a/tests/test_eigen_tensor.py b/tests/test_eigen_tensor.py index 0860c1dad..4b018551b 100644 --- a/tests/test_eigen_tensor.py +++ b/tests/test_eigen_tensor.py @@ -271,23 +271,46 @@ def test_round_trip_references_actually_refer(m): @pytest.mark.parametrize("m", submodules) def test_doc_string(m, doc): assert ( - doc(m.copy_tensor) == "copy_tensor() -> numpy.ndarray[numpy.float64[?, ?, ?]]" + doc(m.copy_tensor) + == 'copy_tensor() -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]"]' ) assert ( doc(m.copy_fixed_tensor) - == "copy_fixed_tensor() -> numpy.ndarray[numpy.float64[3, 5, 2]]" + == 'copy_fixed_tensor() -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[3, 5, 2]"]' ) assert ( doc(m.reference_const_tensor) - == "reference_const_tensor() -> numpy.ndarray[numpy.float64[?, ?, ?]]" + == 'reference_const_tensor() -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]"]' ) - order_flag = f"flags.{m.needed_options.lower()}_contiguous" + order_flag = f'"flags.{m.needed_options.lower()}_contiguous"' assert doc(m.round_trip_view_tensor) == ( - f"round_trip_view_tensor(arg0: numpy.ndarray[numpy.float64[?, ?, ?], flags.writeable, {order_flag}])" - f" -> numpy.ndarray[numpy.float64[?, ?, ?], flags.writeable, {order_flag}]" + f'round_trip_view_tensor(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]", "flags.writeable", {order_flag}])' + f' -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]", "flags.writeable", {order_flag}]' ) assert doc(m.round_trip_const_view_tensor) == ( - f"round_trip_const_view_tensor(arg0: numpy.ndarray[numpy.float64[?, ?, ?], {order_flag}])" - " -> numpy.ndarray[numpy.float64[?, ?, ?]]" + f'round_trip_const_view_tensor(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]", {order_flag}])' + ' -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]"]' ) + + +@pytest.mark.parametrize("m", submodules) +def test_arraylike_signature(m, doc): + order_flag = f'"flags.{m.needed_options.lower()}_contiguous"' + assert doc(m.round_trip_tensor) == ( + 'round_trip_tensor(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64, "[?, ?, ?]"])' + ' -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]"]' + ) + assert doc(m.round_trip_tensor_noconvert) == ( + 'round_trip_tensor_noconvert(tensor: typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]"])' + ' -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]"]' + ) + assert doc(m.round_trip_view_tensor) == ( + f'round_trip_view_tensor(arg0: typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]", "flags.writeable", {order_flag}])' + f' -> typing.Annotated[numpy.typing.NDArray[numpy.float64], "[?, ?, ?]", "flags.writeable", {order_flag}]' + ) + m.round_trip_tensor(tensor_ref.tolist()) + with pytest.raises(TypeError, match="incompatible function arguments"): + m.round_trip_tensor_noconvert(tensor_ref.tolist()) + with pytest.raises(TypeError, match="incompatible function arguments"): + m.round_trip_view_tensor(tensor_ref.tolist()) diff --git a/tests/test_numpy_array.cpp b/tests/test_numpy_array.cpp index 79ade3ba1..1bfca33bb 100644 --- a/tests/test_numpy_array.cpp +++ b/tests/test_numpy_array.cpp @@ -586,4 +586,13 @@ TEST_SUBMODULE(numpy_array, sm) { sm.def("return_array_pyobject_ptr_from_list", return_array_from_list); sm.def("return_array_handle_from_list", return_array_from_list); sm.def("return_array_object_from_list", return_array_from_list); + + sm.def( + "round_trip_array_t", + [](const py::array_t &x) -> py::array_t { return x; }, + py::arg("x")); + sm.def( + "round_trip_array_t_noconvert", + [](const py::array_t &x) -> py::array_t { return x; }, + py::arg("x").noconvert()); } diff --git a/tests/test_numpy_array.py b/tests/test_numpy_array.py index b1c6875f9..3a3f22a64 100644 --- a/tests/test_numpy_array.py +++ b/tests/test_numpy_array.py @@ -321,13 +321,13 @@ def test_overload_resolution(msg): msg(excinfo.value) == """ overloaded(): incompatible function arguments. The following argument types are supported: - 1. (arg0: numpy.ndarray[numpy.float64]) -> str - 2. (arg0: numpy.ndarray[numpy.float32]) -> str - 3. (arg0: numpy.ndarray[numpy.int32]) -> str - 4. (arg0: numpy.ndarray[numpy.uint16]) -> str - 5. (arg0: numpy.ndarray[numpy.int64]) -> str - 6. (arg0: numpy.ndarray[numpy.complex128]) -> str - 7. (arg0: numpy.ndarray[numpy.complex64]) -> str + 1. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]) -> str + 2. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.float32]) -> str + 3. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.int32]) -> str + 4. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.uint16]) -> str + 5. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.int64]) -> str + 6. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.complex128]) -> str + 7. (arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.complex64]) -> str Invoked with: 'not an array' """ @@ -343,8 +343,8 @@ def test_overload_resolution(msg): assert m.overloaded3(np.array([1], dtype="intc")) == "int" expected_exc = """ overloaded3(): incompatible function arguments. The following argument types are supported: - 1. (arg0: numpy.ndarray[numpy.int32]) -> str - 2. (arg0: numpy.ndarray[numpy.float64]) -> str + 1. (arg0: numpy.typing.NDArray[numpy.int32]) -> str + 2. (arg0: numpy.typing.NDArray[numpy.float64]) -> str Invoked with: """ @@ -528,7 +528,7 @@ def test_index_using_ellipsis(): ], ) def test_format_descriptors_for_floating_point_types(test_func): - assert "numpy.ndarray[numpy.float" in test_func.__doc__ + assert "numpy.typing.ArrayLike, numpy.float" in test_func.__doc__ @pytest.mark.parametrize("forcecast", [False, True]) @@ -687,3 +687,17 @@ def test_return_array_object_cpp_loop(return_array, unwrap): assert isinstance(arr_from_list, np.ndarray) assert arr_from_list.dtype == np.dtype("O") assert unwrap(arr_from_list) == [6, "seven", -8.0] + + +def test_arraylike_signature(doc): + assert ( + doc(m.round_trip_array_t) + == "round_trip_array_t(x: typing.Annotated[numpy.typing.ArrayLike, numpy.float32]) -> numpy.typing.NDArray[numpy.float32]" + ) + assert ( + doc(m.round_trip_array_t_noconvert) + == "round_trip_array_t_noconvert(x: numpy.typing.NDArray[numpy.float32]) -> numpy.typing.NDArray[numpy.float32]" + ) + m.round_trip_array_t([1, 2, 3]) + with pytest.raises(TypeError, match="incompatible function arguments"): + m.round_trip_array_t_noconvert([1, 2, 3]) diff --git a/tests/test_numpy_dtypes.py b/tests/test_numpy_dtypes.py index 5d839933c..685a76fd3 100644 --- a/tests/test_numpy_dtypes.py +++ b/tests/test_numpy_dtypes.py @@ -373,7 +373,7 @@ def test_complex_array(): def test_signature(doc): assert ( doc(m.create_rec_nested) - == "create_rec_nested(arg0: int) -> numpy.ndarray[NestedStruct]" + == "create_rec_nested(arg0: int) -> numpy.typing.NDArray[NestedStruct]" ) diff --git a/tests/test_numpy_vectorize.py b/tests/test_numpy_vectorize.py index ce38d72d9..0768759d1 100644 --- a/tests/test_numpy_vectorize.py +++ b/tests/test_numpy_vectorize.py @@ -150,7 +150,7 @@ def test_docs(doc): assert ( doc(m.vectorized_func) == """ - vectorized_func(arg0: numpy.ndarray[numpy.int32], arg1: numpy.ndarray[numpy.float32], arg2: numpy.ndarray[numpy.float64]) -> object + vectorized_func(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.int32], arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float32], arg2: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]) -> object """ ) @@ -212,12 +212,12 @@ def test_passthrough_arguments(doc): + ", ".join( [ "arg0: float", - "arg1: numpy.ndarray[numpy.float64]", - "arg2: numpy.ndarray[numpy.float64]", - "arg3: numpy.ndarray[numpy.int32]", + "arg1: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", + "arg2: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", + "arg3: typing.Annotated[numpy.typing.ArrayLike, numpy.int32]", "arg4: int", "arg5: m.numpy_vectorize.NonPODClass", - "arg6: numpy.ndarray[numpy.float64]", + "arg6: typing.Annotated[numpy.typing.ArrayLike, numpy.float64]", ] ) + ") -> object"