| from __future__ import annotations | |
| import sys | |
| import pytest | |
| import env # noqa: F401 | |
| from pybind11_tests import numpy_array as m | |
| np = pytest.importorskip("numpy") | |
| # numpy < 2.4 fails to detect aliasing in ndarray.resize on Python 3.14, so a | |
| # resize that should raise instead succeeds: numpy/numpy#30265 (fixed in 2.4.0). | |
| NUMPY_RESIZE_REFCHECK_BROKEN = sys.version_info >= (3, 14) and tuple( | |
| int(x) for x in np.__version__.split(".")[:2] | |
| ) < (2, 4) | |
| def test_dtypes(): | |
| # See issue #1328. | |
| # - Platform-dependent sizes. | |
| for size_check in m.get_platform_dtype_size_checks(): | |
| print(size_check) | |
| assert size_check.size_cpp == size_check.size_numpy, size_check | |
| # - Concrete sizes. | |
| for check in m.get_concrete_dtype_checks(): | |
| print(check) | |
| assert check.numpy == check.pybind11, check | |
| if check.numpy.num != check.pybind11.num: | |
| print( | |
| f"NOTE: typenum mismatch for {check}: {check.numpy.num} != {check.pybind11.num}" | |
| ) | |
| @pytest.fixture | |
| def arr(): | |
| return np.array([[1, 2, 3], [4, 5, 6]], "=u2") | |
| def test_array_attributes(): | |
| a = np.array(0, "f8") | |
| assert m.ndim(a) == 0 | |
| assert all(m.shape(a) == []) | |
| assert all(m.strides(a) == []) | |
| with pytest.raises(IndexError) as excinfo: | |
| m.shape(a, 0) | |
| assert str(excinfo.value) == "invalid axis: 0 (ndim = 0)" | |
| with pytest.raises(IndexError) as excinfo: | |
| m.strides(a, 0) | |
| assert str(excinfo.value) == "invalid axis: 0 (ndim = 0)" | |
| assert m.writeable(a) | |
| assert m.size(a) == 1 | |
| assert m.itemsize(a) == 8 | |
| assert m.nbytes(a) == 8 | |
| assert m.owndata(a) | |
| a = np.array([[1, 2, 3], [4, 5, 6]], "u2").view() | |
| a.flags.writeable = False | |
| assert m.ndim(a) == 2 | |
| assert all(m.shape(a) == [2, 3]) | |
| assert m.shape(a, 0) == 2 | |
| assert m.shape(a, 1) == 3 | |
| assert all(m.strides(a) == [6, 2]) | |
| assert m.strides(a, 0) == 6 | |
| assert m.strides(a, 1) == 2 | |
| with pytest.raises(IndexError) as excinfo: | |
| m.shape(a, 2) | |
| assert str(excinfo.value) == "invalid axis: 2 (ndim = 2)" | |
| with pytest.raises(IndexError) as excinfo: | |
| m.strides(a, 2) | |
| assert str(excinfo.value) == "invalid axis: 2 (ndim = 2)" | |
| assert m.shape(a, -1) == 3 | |
| assert m.shape(a, -2) == 2 | |
| assert m.strides(a, -1) == 2 | |
| assert m.strides(a, -2) == 6 | |
| assert not m.writeable(a) | |
| assert m.size(a) == 6 | |
| assert m.itemsize(a) == 2 | |
| assert m.nbytes(a) == 12 | |
| 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)] | |
| ) | |
| def test_index_offset(arr, args, ret): | |
| assert m.index_at(arr, *args) == ret | |
| assert m.index_at_t(arr, *args) == ret | |
| assert m.offset_at(arr, *args) == ret * arr.dtype.itemsize | |
| assert m.offset_at_t(arr, *args) == ret * arr.dtype.itemsize | |
| def test_dim_check_fail(arr): | |
| for func in ( | |
| m.index_at, | |
| m.index_at_t, | |
| m.offset_at, | |
| m.offset_at_t, | |
| m.data, | |
| m.data_t, | |
| m.mutate_data, | |
| m.mutate_data_t, | |
| ): | |
| with pytest.raises(IndexError) as excinfo: | |
| func(arr, 1, 2, 3) | |
| assert str(excinfo.value) == "too many indices for an array: 3 (ndim = 2)" | |
| @pytest.mark.parametrize( | |
| ("args", "ret"), | |
| [ | |
| ([], [1, 2, 3, 4, 5, 6]), | |
| ([1], [4, 5, 6]), | |
| ([0, 1], [2, 3, 4, 5, 6]), | |
| ([1, 2], [6]), | |
| ], | |
| ) | |
| def test_data(arr, args, ret): | |
| from sys import byteorder | |
| assert all(m.data_t(arr, *args) == ret) | |
| assert all(m.data(arr, *args)[(0 if byteorder == "little" else 1) :: 2] == ret) | |
| assert all(m.data(arr, *args)[(1 if byteorder == "little" else 0) :: 2] == 0) | |
| @pytest.mark.parametrize("dim", [0, 1, 3]) | |
| def test_at_fail(arr, dim): | |
| for func in m.at_t, m.mutate_at_t: | |
| with pytest.raises(IndexError) as excinfo: | |
| func(arr, *([0] * dim)) | |
| assert str(excinfo.value) == f"index dimension mismatch: {dim} (ndim = 2)" | |
| def test_at(arr): | |
| assert m.at_t(arr, 0, 2) == 3 | |
| assert m.at_t(arr, 1, 0) == 4 | |
| assert all(m.mutate_at_t(arr, 0, 2).ravel() == [1, 2, 4, 4, 5, 6]) | |
| assert all(m.mutate_at_t(arr, 1, 0).ravel() == [1, 2, 4, 5, 5, 6]) | |
| def test_mutate_readonly(arr): | |
| arr.flags.writeable = False | |
| for func, args in ( | |
| (m.mutate_data, ()), | |
| (m.mutate_data_t, ()), | |
| (m.mutate_at_t, (0, 0)), | |
| ): | |
| with pytest.raises(ValueError) as excinfo: | |
| func(arr, *args) | |
| assert str(excinfo.value) == "array is not writeable" | |
| def test_mutate_data(arr): | |
| assert all(m.mutate_data(arr).ravel() == [2, 4, 6, 8, 10, 12]) | |
| assert all(m.mutate_data(arr).ravel() == [4, 8, 12, 16, 20, 24]) | |
| assert all(m.mutate_data(arr, 1).ravel() == [4, 8, 12, 32, 40, 48]) | |
| assert all(m.mutate_data(arr, 0, 1).ravel() == [4, 16, 24, 64, 80, 96]) | |
| assert all(m.mutate_data(arr, 1, 2).ravel() == [4, 16, 24, 64, 80, 192]) | |
| assert all(m.mutate_data_t(arr).ravel() == [5, 17, 25, 65, 81, 193]) | |
| assert all(m.mutate_data_t(arr).ravel() == [6, 18, 26, 66, 82, 194]) | |
| assert all(m.mutate_data_t(arr, 1).ravel() == [6, 18, 26, 67, 83, 195]) | |
| assert all(m.mutate_data_t(arr, 0, 1).ravel() == [6, 19, 27, 68, 84, 196]) | |
| assert all(m.mutate_data_t(arr, 1, 2).ravel() == [6, 19, 27, 68, 84, 197]) | |
| def test_bounds_check(arr): | |
| for func in ( | |
| m.index_at, | |
| m.index_at_t, | |
| m.offset_at, | |
| m.offset_at_t, | |
| m.data, | |
| m.data_t, | |
| m.mutate_data, | |
| m.mutate_data_t, | |
| m.at_t, | |
| m.mutate_at_t, | |
| ): | |
| with pytest.raises(IndexError) as excinfo: | |
| func(arr, 2, 0) | |
| assert str(excinfo.value) == "index 2 is out of bounds for axis 0 with size 2" | |
| with pytest.raises(IndexError) as excinfo: | |
| func(arr, 0, 4) | |
| assert str(excinfo.value) == "index 4 is out of bounds for axis 1 with size 3" | |
| with pytest.raises(IndexError) as excinfo: | |
| func(arr, -3, 0) | |
| assert str(excinfo.value) == "index -3 is out of bounds for axis 0 with size 2" | |
| with pytest.raises(IndexError) as excinfo: | |
| func(arr, 0, -4) | |
| assert str(excinfo.value) == "index -4 is out of bounds for axis 1 with size 3" | |
| @pytest.mark.parametrize("shape", [(), (0,), (2, 0), (2, 3)]) | |
| def test_negative_axes(shape): | |
| a = np.empty(shape, dtype="=u2") | |
| for func, values in ((m.shape, a.shape), (m.strides, a.strides)): | |
| for axis in range(-a.ndim, a.ndim): | |
| assert func(a, axis) == values[axis] | |
| for axis in (-sys.maxsize - 1, -a.ndim - 1, a.ndim, sys.maxsize): | |
| with pytest.raises(IndexError) as excinfo: | |
| func(a, axis) | |
| assert str(excinfo.value) == f"invalid axis: {axis} (ndim = {a.ndim})" | |
| @pytest.mark.parametrize( | |
| "indices", [(-1,), (-2,), (-1, 0), (0, -1), (-2, 0), (0, -3), (-2, -3), (-1, -1)] | |
| ) | |
| def test_negative_indices(arr, indices): | |
| normalized = tuple(i + n if i < 0 else i for i, n in zip(indices, arr.shape)) | |
| funcs = ( | |
| m.index_at, | |
| m.index_at_t, | |
| m.offset_at, | |
| m.offset_at_t, | |
| m.data, | |
| m.data_t, | |
| m.mutate_data, | |
| m.mutate_data_t, | |
| ) | |
| if len(indices) == arr.ndim: | |
| funcs += (m.at_t, m.mutate_at_t) | |
| for func in funcs: | |
| actual = arr.copy() | |
| expected = arr.copy() | |
| np.testing.assert_array_equal( | |
| func(actual, *indices), func(expected, *normalized) | |
| ) | |
| np.testing.assert_array_equal(actual, expected) | |
| @pytest.mark.parametrize("shape", [(0,), (2, 0), (2, 3)]) | |
| def test_negative_index_boundaries(shape): | |
| a = np.zeros(shape, dtype="=u2") | |
| for func in ( | |
| m.index_at, | |
| m.index_at_t, | |
| m.offset_at, | |
| m.offset_at_t, | |
| m.data, | |
| m.data_t, | |
| m.mutate_data, | |
| m.mutate_data_t, | |
| m.at_t, | |
| m.mutate_at_t, | |
| ): | |
| for axis, size in enumerate(shape): | |
| for value in (-sys.maxsize - 1, -size - 1, size, sys.maxsize): | |
| indices = [0] * len(shape) | |
| indices[axis] = value | |
| bad_axis = next( | |
| dim | |
| for dim, (i, n) in enumerate(zip(indices, shape)) | |
| if i < -n or i >= n | |
| ) | |
| with pytest.raises(IndexError) as excinfo: | |
| func(a, *indices) | |
| assert str(excinfo.value) == ( | |
| f"index {indices[bad_axis]} is out of bounds for axis {bad_axis} " | |
| f"with size {shape[bad_axis]}" | |
| ) | |
| @pytest.mark.parametrize("layout", ["C", "F", "reversed", "broadcast"]) | |
| def test_negative_indices_strided(layout): | |
| a = np.arange(6, dtype="=u2").reshape(2, 3) | |
| if layout == "F": | |
| a = np.asfortranarray(a) | |
| elif layout == "reversed": | |
| a = a[::-1, ::-1] | |
| elif layout == "broadcast": | |
| a = np.broadcast_to(a[:1, :], (2, 3)) | |
| for i in range(-2, 2): | |
| for j in range(-3, 3): | |
| assert m.at_t(a, i, j) == a[i, j] | |
| offset = (i + 2 if i < 0 else i) * a.strides[0] | |
| offset += (j + 3 if j < 0 else j) * a.strides[1] | |
| assert m.offset_at(a, i, j) == offset | |
| assert m.offset_at_t(a, i, j) == offset | |
| assert m.index_at(a, i, j) == offset // a.itemsize | |
| assert m.index_at_t(a, i, j) == offset // a.itemsize | |
| if a.flags.writeable: | |
| expected = int(a[i, j]) + 1 | |
| m.mutate_at_t(a, i, j) | |
| assert a[i, j] == expected | |
| a.flags.writeable = False | |
| for func in (m.mutate_data, m.mutate_data_t, m.mutate_at_t): | |
| with pytest.raises(ValueError, match="array is not writeable"): | |
| func(a, -1, -1) | |
| def test_make_c_f_array(): | |
| assert m.make_c_array().flags.c_contiguous | |
| assert not m.make_c_array().flags.f_contiguous | |
| assert m.make_f_array().flags.f_contiguous | |
| assert not m.make_f_array().flags.c_contiguous | |
| def test_make_empty_shaped_array(): | |
| m.make_empty_shaped_array() | |
| # empty shape means numpy scalar, PEP 3118 | |
| assert m.scalar_int().ndim == 0 | |
| assert m.scalar_int().shape == () | |
| assert m.scalar_int() == 42 | |
| def test_wrap(): | |
| def assert_references(a, b, base=None): | |
| if base is None: | |
| base = a | |
| assert a is not b | |
| assert a.__array_interface__["data"][0] == b.__array_interface__["data"][0] | |
| assert a.shape == b.shape | |
| assert a.strides == b.strides | |
| assert a.flags.c_contiguous == b.flags.c_contiguous | |
| assert a.flags.f_contiguous == b.flags.f_contiguous | |
| assert a.flags.writeable == b.flags.writeable | |
| assert a.flags.aligned == b.flags.aligned | |
| assert a.flags.writebackifcopy == b.flags.writebackifcopy | |
| assert np.all(a == b) | |
| assert not b.flags.owndata | |
| assert b.base is base | |
| if a.flags.writeable and a.ndim == 2: | |
| a[0, 0] = 1234 | |
| assert b[0, 0] == 1234 | |
| a1 = np.array([1, 2], dtype=np.int16) | |
| assert a1.flags.owndata | |
| assert a1.base is None | |
| a2 = m.wrap(a1) | |
| assert_references(a1, a2) | |
| a1 = np.array([[1, 2], [3, 4]], dtype=np.float32, order="F") | |
| assert a1.flags.owndata | |
| assert a1.base is None | |
| a2 = m.wrap(a1) | |
| assert_references(a1, a2) | |
| a1 = np.array([[1, 2], [3, 4]], dtype=np.float32, order="C") | |
| a1.flags.writeable = False | |
| a2 = m.wrap(a1) | |
| assert_references(a1, a2) | |
| a1 = np.random.random((4, 4, 4)) | |
| a2 = m.wrap(a1) | |
| assert_references(a1, a2) | |
| a1t = a1.transpose() | |
| a2 = m.wrap(a1t) | |
| assert_references(a1t, a2, a1) | |
| a1d = a1.diagonal() | |
| a2 = m.wrap(a1d) | |
| assert_references(a1d, a2, a1) | |
| a1m = a1[::-1, ::-1, ::-1] | |
| a2 = m.wrap(a1m) | |
| assert_references(a1m, a2, a1) | |
| @pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") | |
| def test_numpy_view(capture): | |
| with capture: | |
| ac = m.ArrayClass() | |
| ac_view_1 = ac.numpy_view() | |
| ac_view_2 = ac.numpy_view() | |
| assert np.all(ac_view_1 == np.array([1, 2], dtype=np.int32)) | |
| del ac | |
| pytest.gc_collect() | |
| assert ( | |
| capture | |
| == """ | |
| ArrayClass() | |
| ArrayClass::numpy_view() | |
| ArrayClass::numpy_view() | |
| """ | |
| ) | |
| ac_view_1[0] = 4 | |
| ac_view_1[1] = 3 | |
| assert ac_view_2[0] == 4 | |
| assert ac_view_2[1] == 3 | |
| with capture: | |
| del ac_view_1 | |
| del ac_view_2 | |
| pytest.gc_collect() | |
| pytest.gc_collect() | |
| assert ( | |
| capture | |
| == """ | |
| ~ArrayClass() | |
| """ | |
| ) | |
| def test_cast_numpy_int64_to_uint64(): | |
| m.function_taking_uint64(123) | |
| m.function_taking_uint64(np.uint64(123)) | |
| def test_isinstance(): | |
| assert m.isinstance_untyped(np.array([1, 2, 3]), "not an array") | |
| assert m.isinstance_typed(np.array([1.0, 2.0, 3.0])) | |
| def test_constructors(): | |
| defaults = m.default_constructors() | |
| for a in defaults.values(): | |
| assert a.size == 0 | |
| assert defaults["array"].dtype == np.array([]).dtype | |
| assert defaults["array_t<int32>"].dtype == np.int32 | |
| assert defaults["array_t<double>"].dtype == np.float64 | |
| results = m.converting_constructors([1, 2, 3]) | |
| for a in results.values(): | |
| np.testing.assert_array_equal(a, [1, 2, 3]) | |
| assert results["array"].dtype == np.dtype(int) | |
| assert results["array_t<int32>"].dtype == np.int32 | |
| assert results["array_t<double>"].dtype == np.float64 | |
| def test_array_object_type(doc): | |
| assert ( | |
| doc(m.pass_array_object_return_as_list) | |
| == "pass_array_object_return_as_list(arg0: typing.Annotated[numpy.typing.ArrayLike, numpy.object_]) -> list" | |
| ) | |
| def test_overload_resolution(msg): | |
| # Exact overload matches: | |
| assert m.overloaded(np.array([1], dtype="float64")) == "double" | |
| assert m.overloaded(np.array([1], dtype="float32")) == "float" | |
| assert m.overloaded(np.array([1], dtype="ushort")) == "unsigned short" | |
| assert m.overloaded(np.array([1], dtype="intc")) == "int" | |
| assert m.overloaded(np.array([1], dtype="longlong")) == "long long" | |
| assert m.overloaded(np.array([1], dtype="complex")) == "double complex" | |
| assert m.overloaded(np.array([1], dtype="csingle")) == "float complex" | |
| # No exact match, should call first convertible version: | |
| assert m.overloaded(np.array([1], dtype="uint8")) == "double" | |
| with pytest.raises(TypeError) as excinfo: | |
| m.overloaded("not an array") | |
| assert ( | |
| msg(excinfo.value) | |
| == """ | |
| overloaded(): incompatible function arguments. The following argument types are supported: | |
| 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' | |
| """ | |
| ) | |
| assert m.overloaded2(np.array([1], dtype="float64")) == "double" | |
| assert m.overloaded2(np.array([1], dtype="float32")) == "float" | |
| assert m.overloaded2(np.array([1], dtype="complex64")) == "float complex" | |
| assert m.overloaded2(np.array([1], dtype="complex128")) == "double complex" | |
| assert m.overloaded2(np.array([1], dtype="float32")) == "float" | |
| assert m.overloaded3(np.array([1], dtype="float64")) == "double" | |
| assert m.overloaded3(np.array([1], dtype="intc")) == "int" | |
| expected_exc = """ | |
| overloaded3(): incompatible function arguments. The following argument types are supported: | |
| 1. (arg0: numpy.typing.NDArray[numpy.int32]) -> str | |
| 2. (arg0: numpy.typing.NDArray[numpy.float64]) -> str | |
| Invoked with: """ | |
| with pytest.raises(TypeError) as excinfo: | |
| m.overloaded3(np.array([1], dtype="uintc")) | |
| assert msg(excinfo.value) == expected_exc + repr(np.array([1], dtype="uint32")) | |
| with pytest.raises(TypeError) as excinfo: | |
| m.overloaded3(np.array([1], dtype="float32")) | |
| assert msg(excinfo.value) == expected_exc + repr(np.array([1.0], dtype="float32")) | |
| with pytest.raises(TypeError) as excinfo: | |
| m.overloaded3(np.array([1], dtype="complex")) | |
| assert msg(excinfo.value) == expected_exc + repr(np.array([1.0 + 0.0j])) | |
| # Exact matches: | |
| assert m.overloaded4(np.array([1], dtype="double")) == "double" | |
| assert m.overloaded4(np.array([1], dtype="longlong")) == "long long" | |
| # Non-exact matches requiring conversion. Since float to integer isn't a | |
| # save conversion, it should go to the double overload, but short can go to | |
| # either (and so should end up on the first-registered, the long long). | |
| assert m.overloaded4(np.array([1], dtype="float32")) == "double" | |
| assert m.overloaded4(np.array([1], dtype="short")) == "long long" | |
| assert m.overloaded5(np.array([1], dtype="double")) == "double" | |
| assert m.overloaded5(np.array([1], dtype="uintc")) == "unsigned int" | |
| assert m.overloaded5(np.array([1], dtype="float32")) == "unsigned int" | |
| def test_greedy_string_overload(): | |
| """Tests fix for #685 - ndarray shouldn't go to std::string overload""" | |
| assert m.issue685("abc") == "string" | |
| assert m.issue685(np.array([97, 98, 99], dtype="b")) == "array" | |
| assert m.issue685(123) == "other" | |
| def test_array_unchecked_fixed_dims(msg): | |
| z1 = np.array([[1, 2], [3, 4]], dtype="float64") | |
| m.proxy_add2(z1, 10) | |
| assert np.all(z1 == [[11, 12], [13, 14]]) | |
| with pytest.raises(ValueError) as excinfo: | |
| m.proxy_add2(np.array([1.0, 2, 3]), 5.0) | |
| assert ( | |
| msg(excinfo.value) == "array has incorrect number of dimensions: 1; expected 2" | |
| ) | |
| expect_c = np.ndarray(shape=(3, 3, 3), buffer=np.array(range(3, 30)), dtype="int") | |
| assert np.all(m.proxy_init3(3.0) == expect_c) | |
| expect_f = np.transpose(expect_c) | |
| assert np.all(m.proxy_init3F(3.0) == expect_f) | |
| assert m.proxy_squared_L2_norm(np.array(range(6))) == 55 | |
| assert m.proxy_squared_L2_norm(np.array(range(6), dtype="float64")) == 55 | |
| assert m.proxy_auxiliaries2(z1) == [11, 11, True, 2, 8, 2, 2, 4, 32] | |
| assert m.proxy_auxiliaries2(z1) == m.array_auxiliaries2(z1) | |
| assert m.proxy_auxiliaries1_const_ref(z1[0, :]) | |
| assert m.proxy_auxiliaries2_const_ref(z1) | |
| def test_array_unchecked_dyn_dims(): | |
| z1 = np.array([[1, 2], [3, 4]], dtype="float64") | |
| m.proxy_add2_dyn(z1, 10) | |
| assert np.all(z1 == [[11, 12], [13, 14]]) | |
| expect_c = np.ndarray(shape=(3, 3, 3), buffer=np.array(range(3, 30)), dtype="int") | |
| assert np.all(m.proxy_init3_dyn(3.0) == expect_c) | |
| assert m.proxy_auxiliaries2_dyn(z1) == [11, 11, True, 2, 8, 2, 2, 4, 32] | |
| assert m.proxy_auxiliaries2_dyn(z1) == m.array_auxiliaries2(z1) | |
| def test_array_failure(): | |
| with pytest.raises(ValueError) as excinfo: | |
| m.array_fail_test() | |
| assert str(excinfo.value) == "cannot create a pybind11::array from a nullptr" | |
| with pytest.raises(ValueError) as excinfo: | |
| m.array_t_fail_test() | |
| assert str(excinfo.value) == "cannot create a pybind11::array_t from a nullptr" | |
| with pytest.raises(ValueError) as excinfo: | |
| m.array_fail_test_negative_size() | |
| assert str(excinfo.value) == "negative dimensions are not allowed" | |
| def test_initializer_list(): | |
| assert m.array_initializer_list1().shape == (1,) | |
| assert m.array_initializer_list2().shape == (1, 2) | |
| assert m.array_initializer_list3().shape == (1, 2, 3) | |
| assert m.array_initializer_list4().shape == (1, 2, 3, 4) | |
| @pytest.mark.xfail( | |
| NUMPY_RESIZE_REFCHECK_BROKEN, | |
| reason="numpy<2.4 resize(refcheck) regression on Python 3.14", | |
| ) | |
| def test_array_resize(): | |
| a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9], dtype="float64") | |
| m.array_reshape2(a) | |
| assert a.size == 9 | |
| assert np.all(a == [[1, 2, 3], [4, 5, 6], [7, 8, 9]]) | |
| # total size change should succced with refcheck off | |
| m.array_resize3(a, 4, False) | |
| assert a.size == 64 | |
| # ... and fail with refcheck on | |
| try: | |
| m.array_resize3(a, 3, True) | |
| except ValueError as e: | |
| assert str(e).startswith("cannot resize an array") # noqa: PT017 | |
| # transposed array doesn't own data | |
| b = a.transpose() | |
| try: | |
| m.array_resize3(b, 3, False) | |
| except ValueError as e: | |
| assert str(e).startswith( # noqa: PT017 | |
| "cannot resize this array: it does not own its data" | |
| ) | |
| # ... but reshape should be fine | |
| m.array_reshape2(b) | |
| assert b.shape == (8, 8) | |
| @pytest.mark.xfail("env.PYPY or env.GRAALPY") | |
| def test_array_create_and_resize(): | |
| a = m.create_and_resize(2) | |
| assert a.size == 4 | |
| assert np.all(a == 42.0) | |
| def test_array_view(): | |
| a = np.ones(100 * 4).astype("uint8") | |
| a_float_view = m.array_view(a, "float32") | |
| assert a_float_view.shape == (100 * 1,) # 1 / 4 bytes = 8 / 32 | |
| a_int16_view = m.array_view(a, "int16") # 1 / 2 bytes = 16 / 32 | |
| assert a_int16_view.shape == (100 * 2,) | |
| def test_array_view_invalid(): | |
| a = np.ones(100 * 4).astype("uint8") | |
| with pytest.raises(TypeError): | |
| m.array_view(a, "deadly_dtype") | |
| def test_reshape_initializer_list(): | |
| a = np.arange(2 * 7 * 3) + 1 | |
| x = m.reshape_initializer_list(a, 2, 7, 3) | |
| assert x.shape == (2, 7, 3) | |
| assert list(x[1][4]) == [34, 35, 36] | |
| with pytest.raises(ValueError) as excinfo: | |
| m.reshape_initializer_list(a, 1, 7, 3) | |
| assert str(excinfo.value) == "cannot reshape array of size 42 into shape (1,7,3)" | |
| def test_reshape_tuple(): | |
| a = np.arange(3 * 7 * 2) + 1 | |
| x = m.reshape_tuple(a, (3, 7, 2)) | |
| assert x.shape == (3, 7, 2) | |
| assert list(x[1][4]) == [23, 24] | |
| y = m.reshape_tuple(x, (x.size,)) | |
| assert y.shape == (42,) | |
| with pytest.raises(ValueError) as excinfo: | |
| m.reshape_tuple(a, (3, 7, 1)) | |
| assert str(excinfo.value) == "cannot reshape array of size 42 into shape (3,7,1)" | |
| with pytest.raises(ValueError) as excinfo: | |
| m.reshape_tuple(a, ()) | |
| assert str(excinfo.value) == "cannot reshape array of size 42 into shape ()" | |
| def test_index_using_ellipsis(): | |
| a = m.index_using_ellipsis(np.zeros((5, 6, 7))) | |
| assert a.shape == (6,) | |
| @pytest.mark.parametrize( | |
| "test_func", | |
| [ | |
| m.test_fmt_desc_float, | |
| m.test_fmt_desc_double, | |
| m.test_fmt_desc_const_float, | |
| m.test_fmt_desc_const_double, | |
| ], | |
| ) | |
| def test_format_descriptors_for_floating_point_types(test_func): | |
| assert "numpy.typing.ArrayLike, numpy.float" in test_func.__doc__ | |
| @pytest.mark.parametrize("forcecast", [False, True]) | |
| @pytest.mark.parametrize("contiguity", [None, "C", "F"]) | |
| @pytest.mark.parametrize("noconvert", [False, True]) | |
| @pytest.mark.filterwarnings( | |
| "ignore:Casting complex values to real discards the imaginary part:" | |
| + ( | |
| "numpy.exceptions.ComplexWarning" | |
| if hasattr(np, "exceptions") | |
| else "numpy.ComplexWarning" | |
| ) | |
| ) | |
| def test_argument_conversions(forcecast, contiguity, noconvert): | |
| function_name = "accept_double" | |
| if contiguity == "C": | |
| function_name += "_c_style" | |
| elif contiguity == "F": | |
| function_name += "_f_style" | |
| if forcecast: | |
| function_name += "_forcecast" | |
| if noconvert: | |
| function_name += "_noconvert" | |
| function = getattr(m, function_name) | |
| for dtype in [np.dtype("float32"), np.dtype("float64"), np.dtype("complex128")]: | |
| for order in ["C", "F"]: | |
| for shape in [(2, 2), (1, 3, 1, 1), (1, 1, 1), (0,)]: | |
| if not noconvert: | |
| # If noconvert is not passed, only complex128 needs to be truncated and | |
| # "cannot be safely obtained". So without `forcecast`, the argument shouldn't | |
| # be accepted. | |
| should_raise = dtype.name == "complex128" and not forcecast | |
| else: | |
| # If noconvert is passed, only float64 and the matching order is accepted. | |
| # If at most one dimension has a size greater than 1, the array is also | |
| # trivially contiguous. | |
| trivially_contiguous = sum(1 for d in shape if d > 1) <= 1 | |
| should_raise = dtype.name != "float64" or ( | |
| contiguity is not None | |
| and contiguity != order | |
| and not trivially_contiguous | |
| ) | |
| array = np.zeros(shape, dtype=dtype, order=order) | |
| if not should_raise: | |
| function(array) | |
| else: | |
| with pytest.raises( | |
| TypeError, match="incompatible function arguments" | |
| ): | |
| function(array) | |
| @pytest.mark.xfail("env.PYPY") | |
| def test_dtype_refcount_leak(): | |
| from sys import getrefcount | |
| # Was np.float_ but that alias for float64 was removed in NumPy 2. | |
| dtype = np.dtype(np.float64) | |
| a = np.array([1], dtype=dtype) | |
| before = getrefcount(dtype) | |
| m.ndim(a) | |
| after = getrefcount(dtype) | |
| assert after == before | |
| def test_round_trip_float(): | |
| arr = np.zeros((), np.float64) | |
| arr[()] = 37.2 | |
| assert m.round_trip_float(arr) == 37.2 | |
| # HINT: An easy and robust way (although only manual unfortunately) to check for | |
| # ref-count leaks in the test_.*pyobject_ptr.* functions below is to | |
| # * temporarily insert `while True:` (one-by-one), | |
| # * run this test, and | |
| # * run the Linux `top` command in another shell to visually monitor | |
| # `RES` for a minute or two. | |
| # If there is a leak, it is usually evident in seconds because the `RES` | |
| # value increases without bounds. (Don't forget to Ctrl-C the test!) | |
| # For use as a temporary user-defined object, to maximize sensitivity of the tests below: | |
| # * Ref-count leaks will be immediately evident. | |
| # * Sanitizers are much more likely to detect heap-use-after-free due to | |
| # other ref-count bugs. | |
| class PyValueHolder: | |
| def __init__(self, value): | |
| self.value = value | |
| def WrapWithPyValueHolder(*values): | |
| return [PyValueHolder(v) for v in values] | |
| def UnwrapPyValueHolder(vhs): | |
| return [vh.value for vh in vhs] | |
| PASS_ARRAY_PYOBJECT_RETURN_SUM_STR_VALUES_FUNCTIONS = [ | |
| m.pass_array_pyobject_ptr_return_sum_str_values, | |
| m.pass_array_handle_return_sum_str_values, | |
| m.pass_array_object_return_sum_str_values, | |
| ] | |
| @pytest.mark.parametrize( | |
| "pass_array", PASS_ARRAY_PYOBJECT_RETURN_SUM_STR_VALUES_FUNCTIONS | |
| ) | |
| def test_pass_array_object_return_sum_str_values_ndarray(pass_array): | |
| # Intentionally all temporaries, do not change. | |
| assert ( | |
| pass_array(np.array(WrapWithPyValueHolder(-3, "four", 5.0), dtype=object)) | |
| == "-3four5.0" | |
| ) | |
| @pytest.mark.parametrize( | |
| "pass_array", PASS_ARRAY_PYOBJECT_RETURN_SUM_STR_VALUES_FUNCTIONS | |
| ) | |
| def test_pass_array_object_return_sum_str_values_list(pass_array): | |
| # Intentionally all temporaries, do not change. | |
| assert pass_array(WrapWithPyValueHolder(2, "three", -4.0)) == "2three-4.0" | |
| @pytest.mark.parametrize( | |
| "pass_array", | |
| [ | |
| m.pass_array_pyobject_ptr_return_as_list, | |
| m.pass_array_handle_return_as_list, | |
| m.pass_array_object_return_as_list, | |
| ], | |
| ) | |
| def test_pass_array_object_return_as_list(pass_array): | |
| # Intentionally all temporaries, do not change. | |
| assert UnwrapPyValueHolder( | |
| pass_array(np.array(WrapWithPyValueHolder(-1, "two", 3.0), dtype=object)) | |
| ) == [-1, "two", 3.0] | |
| @pytest.mark.parametrize( | |
| ("return_array", "unwrap"), | |
| [ | |
| (m.return_array_pyobject_ptr_cpp_loop, list), | |
| (m.return_array_handle_cpp_loop, list), | |
| (m.return_array_object_cpp_loop, list), | |
| (m.return_array_pyobject_ptr_from_list, UnwrapPyValueHolder), | |
| (m.return_array_handle_from_list, UnwrapPyValueHolder), | |
| (m.return_array_object_from_list, UnwrapPyValueHolder), | |
| ], | |
| ) | |
| def test_return_array_object_cpp_loop(return_array, unwrap): | |
| # Intentionally all temporaries, do not change. | |
| arr_from_list = return_array(WrapWithPyValueHolder(6, "seven", -8.0)) | |
| 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]) |