From caead06491143c75de275c4779a0c0866e17c576 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Tue, 5 Mar 2024 15:12:58 -0700 Subject: [PATCH 1/2] Fix asarray(copy=False) For NumPy 2.0, this is implemented directly. For NumPy 1, we emulate it by checking if asarray() creates a copy or not. This also removes support for the np._CopyMode enum in asarray(), as this is not portable. --- array-api-tests-xfails.txt | 4 --- array_api_strict/_creation_functions.py | 25 ++++++++++++------- .../tests/test_creation_functions.py | 19 ++++++++------ 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/array-api-tests-xfails.txt b/array-api-tests-xfails.txt index 51f63a0..07d782d 100644 --- a/array-api-tests-xfails.txt +++ b/array-api-tests-xfails.txt @@ -1,7 +1,3 @@ -# copy=False is not yet implemented -# https://github.com/numpy/numpy/pull/25168 -array_api_tests/test_creation_functions.py::test_asarray_arrays - # Some fft tests are currently wrong # (https://github.com/data-apis/array-api-tests/issues/231) array_api_tests/test_fft.py::test_fft diff --git a/array_api_strict/_creation_functions.py b/array_api_strict/_creation_functions.py index 90dc4e0..729c8f0 100644 --- a/array_api_strict/_creation_functions.py +++ b/array_api_strict/_creation_functions.py @@ -36,7 +36,7 @@ def asarray( *, dtype: Optional[Dtype] = None, device: Optional[Device] = None, - copy: Optional[Union[bool, np._CopyMode]] = None, + copy: Optional[bool] = None, ) -> Array: """ Array API compatible wrapper for :py:func:`np.asarray `. @@ -53,15 +53,22 @@ def asarray( _np_dtype = dtype._np_dtype if device not in [CPU_DEVICE, None]: raise ValueError(f"Unsupported device {device!r}") - if copy in (False, np._CopyMode.IF_NEEDED): - # Note: copy=False is not yet implemented in np.asarray - raise NotImplementedError("copy=False is not yet implemented") if isinstance(obj, Array): - if dtype is not None and obj.dtype != dtype: - copy = True - if copy in (True, np._CopyMode.ALWAYS): - return Array._new(np.array(obj._array, copy=True, dtype=_np_dtype)) - return obj + if np.__version__[0] < '2': + if copy is False: + # Note: copy=False is not yet implemented in np.asarray for + # NumPy 1 + + # Work around it by creating the new array and seeing if NumPy + # copies it. + new_array = np.array(obj._array, copy=copy, dtype=_np_dtype) + if new_array is not obj._array: + raise ValueError("Unable to avoid copy while creating an array from given array.") + return Array._new(new_array) + if copy is None: + # NumPy 1 treats copy=False the same as the standard copy=None + copy = False + return Array._new(np.array(obj._array, copy=copy, dtype=_np_dtype)) if dtype is None and isinstance(obj, int) and (obj > 2 ** 64 or obj < -(2 ** 63)): # Give a better error message in this case. NumPy would convert this # to an object array. TODO: This won't handle large integers in lists. diff --git a/array_api_strict/tests/test_creation_functions.py b/array_api_strict/tests/test_creation_functions.py index ee022c0..16d1067 100644 --- a/array_api_strict/tests/test_creation_functions.py +++ b/array_api_strict/tests/test_creation_functions.py @@ -50,19 +50,24 @@ def test_asarray_copy(): a[0] = 0 assert all(b[0] == 1) assert all(a[0] == 0) + a = asarray([1]) - b = asarray(a, copy=np._CopyMode.ALWAYS) + b = asarray(a, copy=False) a[0] = 0 - assert all(b[0] == 1) - assert all(a[0] == 0) + assert all(b[0] == 0) + a = asarray([1]) - b = asarray(a, copy=np._CopyMode.NEVER) + assert_raises(ValueError, lambda: asarray(a, copy=False, dtype=float64)) + + a = asarray([1]) + b = asarray(a, copy=None) a[0] = 0 assert all(b[0] == 0) - assert_raises(NotImplementedError, lambda: asarray(a, copy=False)) - assert_raises(NotImplementedError, - lambda: asarray(a, copy=np._CopyMode.IF_NEEDED)) + a = asarray([1]) + b = asarray(a, dtype=float64, copy=None) + a[0] = 0 + assert all(b[0] == 1.0) def test_arange_errors(): arange(1, device=CPU_DEVICE) # Doesn't error From e92eded2bd661ac5b76725ec9c1ba26233946fa2 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Thu, 7 Mar 2024 16:52:41 -0700 Subject: [PATCH 2/2] Update asarray copy flag to properly handle other input types --- array_api_strict/_creation_functions.py | 39 +++++++++++++------ .../tests/test_creation_functions.py | 25 ++++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/array_api_strict/_creation_functions.py b/array_api_strict/_creation_functions.py index 729c8f0..ad7ec82 100644 --- a/array_api_strict/_creation_functions.py +++ b/array_api_strict/_creation_functions.py @@ -11,7 +11,6 @@ NestedSequence, SupportsBufferProtocol, ) - from collections.abc import Sequence from ._dtypes import _DType, _all_dtypes import numpy as np @@ -22,6 +21,12 @@ def _check_valid_dtype(dtype): if dtype not in (None,) + _all_dtypes: raise ValueError("dtype must be one of the supported dtypes") +def _supports_buffer_protocol(obj): + try: + memoryview(obj) + except TypeError: + return False + return True def asarray( obj: Union[ @@ -53,27 +58,37 @@ def asarray( _np_dtype = dtype._np_dtype if device not in [CPU_DEVICE, None]: raise ValueError(f"Unsupported device {device!r}") - if isinstance(obj, Array): - if np.__version__[0] < '2': - if copy is False: - # Note: copy=False is not yet implemented in np.asarray for - # NumPy 1 - # Work around it by creating the new array and seeing if NumPy - # copies it. + if np.__version__[0] < '2': + if copy is False: + # Note: copy=False is not yet implemented in np.asarray for + # NumPy 1 + + # Work around it by creating the new array and seeing if NumPy + # copies it. + if isinstance(obj, Array): new_array = np.array(obj._array, copy=copy, dtype=_np_dtype) if new_array is not obj._array: raise ValueError("Unable to avoid copy while creating an array from given array.") return Array._new(new_array) - if copy is None: - # NumPy 1 treats copy=False the same as the standard copy=None - copy = False + elif _supports_buffer_protocol(obj): + # Buffer protocol will always support no-copy + return Array._new(np.array(obj, copy=copy, dtype=_np_dtype)) + else: + # No-copy is unsupported for Python built-in types. + raise ValueError("Unable to avoid copy while creating an array from given object.") + + if copy is None: + # NumPy 1 treats copy=False the same as the standard copy=None + copy = False + + if isinstance(obj, Array): return Array._new(np.array(obj._array, copy=copy, dtype=_np_dtype)) if dtype is None and isinstance(obj, int) and (obj > 2 ** 64 or obj < -(2 ** 63)): # Give a better error message in this case. NumPy would convert this # to an object array. TODO: This won't handle large integers in lists. raise OverflowError("Integer out of bounds for array dtypes") - res = np.asarray(obj, dtype=_np_dtype) + res = np.array(obj, dtype=_np_dtype, copy=copy) return Array._new(res) diff --git a/array_api_strict/tests/test_creation_functions.py b/array_api_strict/tests/test_creation_functions.py index 16d1067..78d4c80 100644 --- a/array_api_strict/tests/test_creation_functions.py +++ b/array_api_strict/tests/test_creation_functions.py @@ -69,6 +69,31 @@ def test_asarray_copy(): a[0] = 0 assert all(b[0] == 1.0) + # Python built-in types + for obj in [True, 0, 0.0, 0j, [0], [[0]]]: + asarray(obj, copy=True) # No error + asarray(obj, copy=None) # No error + assert_raises(ValueError, lambda: asarray(obj, copy=False)) + + # Buffer protocol + a = np.array([1]) + b = asarray(a, copy=True) + assert isinstance(b, Array) + a[0] = 0 + assert all(b[0] == 1) + + a = np.array([1]) + b = asarray(a, copy=False) + assert isinstance(b, Array) + a[0] = 0 + assert all(b[0] == 0) + + a = np.array([1]) + b = asarray(a, copy=None) + assert isinstance(b, Array) + a[0] = 0 + assert all(b[0] == 0) + def test_arange_errors(): arange(1, device=CPU_DEVICE) # Doesn't error assert_raises(ValueError, lambda: arange(1, device="cpu"))