diff --git a/.github/workflows/array-api-tests.yml b/.github/workflows/array-api-tests.yml index a659332..bafb6f5 100644 --- a/.github/workflows/array-api-tests.yml +++ b/.github/workflows/array-api-tests.yml @@ -11,12 +11,16 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.12', '3.13'] - numpy-version: ['1.26', '2.2', 'dev'] + python-version: ['3.10', '3.11', '3.12', '3.13'] + numpy-version: ['1.26', '2.3', 'dev'] exclude: + - python-version: '3.10' + numpy-version: '2.3' + - python-version: '3.10' + numpy-version: 'dev' - python-version: '3.13' numpy-version: '1.26' - + fail-fast: false steps: - name: Checkout array-api-strict uses: actions/checkout@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 703e6e7..da6961e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,11 +6,15 @@ jobs: strategy: matrix: python-version: ['3.10', '3.11', '3.12', '3.13'] - numpy-version: ['1.26', 'dev'] + numpy-version: ['1.26', '2.3', 'dev'] exclude: + - python-version: '3.10' + numpy-version: '2.3' + - python-version: '3.10' + numpy-version: 'dev' - python-version: '3.13' numpy-version: '1.26' - fail-fast: true + fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 49fad82..d9d485f 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -16,6 +16,7 @@ from __future__ import annotations import operator +import sys from collections.abc import Iterator from enum import IntEnum from types import EllipsisType, ModuleType @@ -67,8 +68,6 @@ def __hash__(self) -> int: CPU_DEVICE = Device() ALL_DEVICES = (CPU_DEVICE, Device("device1"), Device("device2")) -_default = object() - class Array: """ @@ -149,29 +148,40 @@ def __repr__(self) -> str: __str__ = __repr__ - # `__array__` was implemented historically for compatibility, and removing it has - # caused issues for some libraries (see - # https://github.com/data-apis/array-api-strict/issues/67). - - # Instead of `__array__` we now implement the buffer protocol. - # Note that it makes array-apis-strict requiring python>=3.12 def __buffer__(self, flags): if self._device != CPU_DEVICE: - raise RuntimeError(f"Can not convert array on the '{self._device}' device to a Numpy array.") + raise RuntimeError( + # NumPy swallows this exception and falls back to __array__. + f"Can't extract host buffer from array on the '{self._device}' device." + ) return self._array.__buffer__(flags) - # We do not define __release_buffer__, per the discussion at - # https://github.com/data-apis/array-api-strict/pull/115#pullrequestreview-2917178729 - - def __array__(self, *args, **kwds): - # a stub for python < 3.12; otherwise numpy silently produces object arrays - import sys - minor, major = sys.version_info.minor, sys.version_info.major - if major < 3 or minor < 12: + # `__array__` is not part of the Array API. Ideally we want to support + # `xp.asarray(Array)` exclusively through the __buffer__ protocol; however this is + # only possible on Python >=3.12. Additionally, when __buffer__ raises (e.g. because + # the array is not on the CPU device, NumPy will try to fall back on __array__ but, + # if that doesn't exist, create a scalar numpy array of objects which contains the + # array_api_strict.Array. So we can't get rid of __array__ entirely. + def __array__( + self, dtype: None | np.dtype[Any] = None, copy: None | bool = None + ) -> npt.NDArray[Any]: + if self._device != CPU_DEVICE: + # We arrive here from np.asarray() on Python >=3.12 when __buffer__ raises. + raise RuntimeError( + f"Can't convert array on the '{self._device}' device to a " + "NumPy array." + ) + if sys.version_info >= (3, 12): raise TypeError( - "Interoperation with NumPy requires python >= 3.12. Please upgrade." + "The __array__ method is not supported by the Array API. " + "Please use the __buffer__ interface instead." ) + # copy keyword is new in 2.0 + if np.__version__[0] < '2': + return np.asarray(self._array, dtype=dtype) + return np.asarray(self._array, dtype=dtype, copy=copy) + # These are various helper functions to make the array behavior match the # spec in places where it either deviates from or is more strict than # NumPy behavior diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index e3c16f4..6e171a0 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -559,36 +559,54 @@ def test_array_properties(): assert b.mT.shape == (3, 2) -@pytest.mark.xfail(sys.version_info.major*100 + sys.version_info.minor < 312, - reason="array conversion relies on buffer protocol, and " - "requires python >= 3.12" -) def test_array_conversion(): # Check that arrays on the CPU device can be converted to NumPy # but arrays on other devices can't. Note this is testing the logic in - # __array__, which is only used in asarray when converting lists of - # arrays. + # __array__ on Python 3.10~3.11 and in __buffer__ on Python >=3.12. a = ones((2, 3)) - np.asarray(a) + na = np.asarray(a) + assert na.shape == (2, 3) + assert na.dtype == np.float64 + na[0, 0] = 10 + assert a[0, 0] == 10 # return view when possible + + a = arange(5, dtype=uint8) + na = np.asarray(a) + assert na.dtype == np.uint8 for device in ("device1", "device2"): a = ones((2, 3), device=array_api_strict.Device(device)) - with pytest.raises((RuntimeError, ValueError)): + with pytest.raises(RuntimeError, match=device): np.asarray(a) - # __buffer__ should work for now for conversion to numpy - a = ones((2, 3)) - na = np.array(a) - assert na.shape == (2, 3) - assert na.dtype == np.float64 -@pytest.mark.skipif(not sys.version_info.major*100 + sys.version_info.minor < 312, - reason="conversion to numpy errors out unless python >= 3.12" +@pytest.mark.skipif(np.__version__ < "2", reason="np.asarray has no copy kwarg") +def test_array_conversion_copy(): + a = arange(5) + na = np.asarray(a, copy=False) + na[0] = 10 + assert a[0] == 10 + + a = arange(5) + na = np.asarray(a, copy=True) + na[0] = 10 + assert a[0] == 0 + + a = arange(5) + with pytest.raises(ValueError): + np.asarray(a, dtype=np.uint8, copy=False) + + +@pytest.mark.skipif( + sys.version_info < (3, 12), reason="Python <3.12 has no __buffer__ interface" ) -def test_array_conversion_2(): +def test_no_array_interface(): + """When the __buffer__ interface is available, the __array__ interface is not.""" a = ones((2, 3)) - with pytest.raises(TypeError): - np.array(a) + with pytest.raises(TypeError, match="not supported"): + # Because NumPy prefers __buffer__ when available, we can't trigger this + # exception from np.asarray(). + a.__array__() def test_allow_newaxis(): diff --git a/docs/changelog.md b/docs/changelog.md index 350428f..e399f09 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,30 @@ # Changelog +## 2.4.1 (unreleased) + +### Major Changes + +- The array object defines `__array__` again when `__buffer__` is not available. + +- Support for Python versions 3.10 and 3.11 has been reinstated. + + +### Minor Changes + +- Arithmetic operations no longer accept NumPy arrays. + +- Disallow `__setitem__` for invalid dtype combinations (e.g. setting a float value +into an integer array) + + +### Contributors + +The following users contributed to this release: + +Evgeni Burovski, +Guido Imperiale + + ## 2.4.0 (2025-06-16) ### Major Changes