From 5289f58deb9d9c6905b71d05bc86c73a869ccca1 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:22:17 +0530 Subject: [PATCH 1/5] Complying with ruff==0.16 Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- .github/workflows/ruff.yml | 2 +- array_api_strict/__init__.py | 90 +++++--- array_api_strict/_array_object.py | 211 +++++++++--------- array_api_strict/_creation_functions.py | 19 +- array_api_strict/_devices.py | 22 +- array_api_strict/_dtypes.py | 2 +- array_api_strict/_fft.py | 20 +- array_api_strict/_flags.py | 4 +- array_api_strict/_linalg.py | 22 +- array_api_strict/_manipulation_functions.py | 2 +- array_api_strict/_searching_functions.py | 15 +- array_api_strict/_set_functions.py | 4 +- array_api_strict/tests/conftest.py | 5 +- array_api_strict/tests/test_array_object.py | 58 ++--- .../tests/test_creation_functions.py | 21 +- .../tests/test_elementwise_functions.py | 22 +- array_api_strict/tests/test_flags.py | 97 +++++--- array_api_strict/tests/test_linalg.py | 4 +- .../tests/test_manipulation_functions.py | 8 +- .../tests/test_searching_functions.py | 10 +- .../tests/test_statistical_functions.py | 11 +- docs/conf.py | 3 +- 22 files changed, 370 insertions(+), 282 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 46112047..6eb1a323 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -13,7 +13,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install "ruff<0.16" + pip install "ruff" # Update output format to enable automatic inline annotations. - name: Run Ruff run: ruff check --output-format=github . diff --git a/array_api_strict/__init__.py b/array_api_strict/__init__.py index a4b578e7..d1763a4e 100644 --- a/array_api_strict/__init__.py +++ b/array_api_strict/__init__.py @@ -28,13 +28,13 @@ __all__ += ["__array_api_version__"] -from ._constants import e, inf, nan, pi, newaxis +from ._constants import e, inf, nan, newaxis, pi -__all__ += ["e", "inf", "nan", "pi", "newaxis"] +__all__ += ["e", "inf", "nan", "newaxis", "pi"] from ._creation_functions import ( - asarray, arange, + asarray, empty, empty_like, eye, @@ -52,8 +52,8 @@ ) __all__ += [ - "asarray", "arange", + "asarray", "empty", "empty_like", "eye", @@ -77,8 +77,8 @@ broadcast_to, can_cast, finfo, - isdtype, iinfo, + isdtype, result_type, ) @@ -89,12 +89,17 @@ "broadcast_to", "can_cast", "finfo", - "isdtype", "iinfo", + "isdtype", "result_type", ] from ._dtypes import ( + bool, + complex64, + complex128, + float32, + float64, int8, int16, int32, @@ -103,14 +108,14 @@ uint16, uint32, uint64, - float32, - float64, - complex64, - complex128, - bool, ) __all__ += [ + "bool", + "complex64", + "complex128", + "float32", + "float64", "int8", "int16", "int32", @@ -119,11 +124,6 @@ "uint16", "uint32", "uint64", - "float32", - "float64", - "complex64", - "complex128", - "bool", ] from ._elementwise_functions import ( @@ -137,8 +137,8 @@ atan2, atanh, bitwise_and, - bitwise_left_shift, bitwise_invert, + bitwise_left_shift, bitwise_or, bitwise_right_shift, bitwise_xor, @@ -188,8 +188,8 @@ signbit, sin, sinh, - square, sqrt, + square, subtract, tan, tanh, @@ -207,8 +207,8 @@ "atan2", "atanh", "bitwise_and", - "bitwise_left_shift", "bitwise_invert", + "bitwise_left_shift", "bitwise_or", "bitwise_right_shift", "bitwise_xor", @@ -258,8 +258,8 @@ "signbit", "sin", "sinh", - "square", "sqrt", + "square", "subtract", "tan", "tanh", @@ -276,9 +276,9 @@ "__array_namespace_info__", ] -from ._linear_algebra_functions import matmul, tensordot, matrix_transpose, vecdot +from ._linear_algebra_functions import matmul, matrix_transpose, tensordot, vecdot -__all__ += ["matmul", "tensordot", "matrix_transpose", "vecdot"] +__all__ += ["matmul", "matrix_transpose", "tensordot", "vecdot"] from ._manipulation_functions import ( concat, @@ -297,44 +297,68 @@ __all__ += ["concat", "expand_dims", "flip", "moveaxis", "permute_dims", "repeat", "reshape", "roll", "squeeze", "stack", "tile", "unstack"] -from ._searching_functions import argmax, argmin, nonzero, count_nonzero, searchsorted, where +from ._searching_functions import ( + argmax, + argmin, + count_nonzero, + nonzero, + searchsorted, + where, +) -__all__ += ["argmax", "argmin", "nonzero", "count_nonzero", "searchsorted", "where"] +__all__ += ["argmax", "argmin", "count_nonzero", "nonzero", "searchsorted", "where"] -from ._set_functions import unique_all, unique_counts, unique_inverse, unique_values, isin +from ._set_functions import ( + isin, + unique_all, + unique_counts, + unique_inverse, + unique_values, +) -__all__ += ["unique_all", "unique_counts", "unique_inverse", "unique_values", "isin"] +__all__ += ["isin", "unique_all", "unique_counts", "unique_inverse", "unique_values"] from ._sorting_functions import argsort, sort __all__ += ["argsort", "sort"] -from ._statistical_functions import cumulative_sum, cumulative_prod, max, mean, min, prod, std, sum, var +from ._statistical_functions import ( + cumulative_prod, + cumulative_sum, + max, + mean, + min, + prod, + std, + sum, + var, +) -__all__ += ["cumulative_sum", "cumulative_prod", "max", "mean", "min", "prod", "std", "sum", "var"] +__all__ += ["cumulative_prod", "cumulative_sum", "max", "mean", "min", "prod", "std", "sum", "var"] from ._utility_functions import all, any, diff __all__ += ["all", "any", "diff"] from ._array_object import Device + __all__ += ["Device"] # Helper functions that are not part of the standard from ._flags import ( - set_array_api_strict_flags, + ArrayAPIStrictFlags, get_array_api_strict_flags, reset_array_api_strict_flags, - ArrayAPIStrictFlags, + set_array_api_strict_flags, ) __all__ += [ - 'set_array_api_strict_flags', - 'get_array_api_strict_flags', - 'reset_array_api_strict_flags', 'ArrayAPIStrictFlags', '__version__', + 'get_array_api_strict_flags', + 'reset_array_api_strict_flags', + 'set_array_api_strict_flags', ] try: diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 25e6595a..565bd4c7 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -17,15 +17,17 @@ import operator import sys -from collections.abc import Iterator +from collections.abc import Callable, Iterator from enum import IntEnum from types import EllipsisType, ModuleType -from typing import Any, Literal, SupportsIndex, Callable +from typing import Any, Literal, SupportsIndex import numpy as np import numpy.typing as npt +from typing_extensions import Self from ._creation_functions import Undef, _undef, asarray +from ._devices import CPU_DEVICE, Device, device_supports_dtype from ._dtypes import ( DType, _all_dtypes, @@ -40,7 +42,6 @@ _real_to_complex_map, _result_type, ) -from ._devices import CPU_DEVICE, Device, device_supports_dtype from ._flags import get_array_api_strict_flags, set_array_api_strict_flags from ._typing import PyCapsule @@ -64,7 +65,7 @@ class Array: _array: npt.NDArray[Any] _dtype: DType _device: Device - __slots__ = ("_array", "_dtype", "_device", "__weakref__") + __slots__ = ("__weakref__", "_array", "_device", "_dtype") # Use a custom constructor instead of __init__, as manually initializing # this class is not supported API. @@ -102,7 +103,7 @@ def _new(cls, x: npt.NDArray[Any] | np.generic, /, device: Device | None) -> Arr return obj # Prevent Array() from working - def __new__(cls, *args: object, **kwargs: object) -> Array: + def __new__(cls, *args: object, **kwargs: object) -> Self: raise TypeError( "The array_api_strict Array object should not be instantiated directly. Use an array creation function, such as asarray(), instead." ) @@ -197,19 +198,18 @@ def _check_allowed_dtypes( # to promote in the spec (even if the NumPy array operator would # promote them). res_dtype = _result_type(self.dtype, other.dtype) - if op.startswith("__i"): - # Note: NumPy will allow in-place operators in some cases where - # the type promoted operator does not match the left-hand side - # operand. For example, + # Note: NumPy will allow in-place operators in some cases where + # the type promoted operator does not match the left-hand side + # operand. For example, - # >>> a = np.array(1, dtype=np.int8) - # >>> a += np.array(1, dtype=np.int16) + # >>> a = np.array(1, dtype=np.int8) + # >>> a += np.array(1, dtype=np.int16) - # The spec explicitly disallows this. - if res_dtype != self.dtype: - raise TypeError( - f"Cannot perform {op} with dtypes {self.dtype} and {other.dtype}" - ) + # The spec explicitly disallows this. + if op.startswith("__i") and res_dtype != self.dtype: + raise TypeError( + f"Cannot perform {op} with dtypes {self.dtype} and {other.dtype}" + ) return other @@ -378,12 +378,10 @@ def _validate_index( _key = key if isinstance(key, tuple) else (key,) for i in _key: if isinstance(i, bool) or not ( - isinstance(i, SupportsIndex) # i.e. ints - or isinstance(i, slice) + isinstance(i, (SupportsIndex, slice)) or i == Ellipsis or (op == "getitem" and i is None) # `None` disallowed in setitem - or isinstance(i, Array) - or isinstance(i, np.ndarray) + or isinstance(i, (Array, np.ndarray)) ): raise IndexError( f"Single-axes index {i} has {type(i)=}, but only " @@ -391,9 +389,8 @@ def _validate_index( "zero-dimensional integer arrays and boolean arrays " "are specified in the Array API." ) - if op == "setitem": - if isinstance(i, Array) and i.dtype in _integer_dtypes: - raise IndexError("Fancy indexing __setitem__ is not supported.") + if op == "setitem" and isinstance(i, Array) and i.dtype in _integer_dtypes: + raise IndexError("Fancy indexing __setitem__ is not supported.") nonexpanding_key = [] single_axes = [] @@ -424,18 +421,17 @@ def _validate_index( n_single_axes = len(single_axes) if n_ellipsis > 1: return # handled by ndarray - elif n_ellipsis == 0: - # Note boolean masks must be the sole index, which we check for - # later on. - if not key_has_mask and n_single_axes < self.ndim: - raise IndexError( - f"{self.ndim=}, but the multi-axes index only specifies " - f"{n_single_axes} dimensions. If this was intentional, " - "add a trailing ellipsis (...) which expands into as many " - "slices (:) as necessary - this is what np.ndarray arrays " - "implicitly do, but such flat indexing behaviour is not " - "specified in the Array API." - ) + # Note boolean masks must be the sole index, which we check for + # later on. + elif n_ellipsis == 0 and not key_has_mask and n_single_axes < self.ndim: + raise IndexError( + f"{self.ndim=}, but the multi-axes index only specifies " + f"{n_single_axes} dimensions. If this was intentional, " + "add a trailing ellipsis (...) which expands into as many " + "slices (:) as necessary - this is what np.ndarray arrays " + "implicitly do, but such flat indexing behaviour is not " + "specified in the Array API." + ) if (key_has_index_array and (n_ellipsis > 0 or key_has_slices or key_has_mask)): raise IndexError( @@ -448,10 +444,9 @@ def _validate_index( else: ellipsis_start = None for pos, i in enumerate(nonexpanding_key): - if not (isinstance(i, Array) or isinstance(i, np.ndarray)): - if i == Ellipsis: - ellipsis_start = pos - break + if not (isinstance(i, (Array, np.ndarray))) and i == Ellipsis: + ellipsis_start = pos + break assert ellipsis_start is not None # sanity check ellipsis_end = self.ndim - (n_single_axes - ellipsis_start) indexed_shape = ( @@ -527,8 +522,8 @@ def __add__(self, other: Array | complex, /) -> Array: other = self._check_allowed_dtypes(other, "numeric", "__add__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__add__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__add__(other._array) return self.__class__._new(res, device=self.device) def __and__(self, other: Array | int, /) -> Array: @@ -539,8 +534,8 @@ def __and__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer or boolean", "__and__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__and__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__and__(other._array) return self.__class__._new(res, device=self.device) def __array_namespace__(self, /, *, api_version: str | None = None) -> ModuleType: @@ -638,8 +633,8 @@ def __eq__(self, other: Array | complex, /) -> Array: # type: ignore[override] other = self._check_allowed_dtypes(other, "all", "__eq__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__eq__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__eq__(other._array) return self.__class__._new(res, device=self.device) def __float__(self) -> float: @@ -662,8 +657,8 @@ def __floordiv__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__floordiv__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__floordiv__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__floordiv__(other._array) return self.__class__._new(res, device=self.device) def __ge__(self, other: Array | float, /) -> Array: @@ -674,8 +669,8 @@ def __ge__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__ge__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__ge__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__ge__(other._array) return self.__class__._new(res, device=self.device) def __getitem__( @@ -726,8 +721,8 @@ def __gt__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__gt__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__gt__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__gt__(other._array) return self.__class__._new(res, device=other.device) def __int__(self) -> int: @@ -781,8 +776,8 @@ def __le__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__le__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__le__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__le__(other._array) return self.__class__._new(res, device=self.device) def __lshift__(self, other: Array | int, /) -> Array: @@ -793,8 +788,8 @@ def __lshift__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer", "__lshift__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__lshift__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__lshift__(other._array) return self.__class__._new(res, device=self.device) def __lt__(self, other: Array | float, /) -> Array: @@ -805,8 +800,8 @@ def __lt__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__lt__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__lt__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__lt__(other._array) return self.__class__._new(res, device=self.device) def __matmul__(self, other: Array, /) -> Array: @@ -830,8 +825,8 @@ def __mod__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__mod__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__mod__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__mod__(other._array) return self.__class__._new(res, device=self.device) def __mul__(self, other: Array | complex, /) -> Array: @@ -842,8 +837,8 @@ def __mul__(self, other: Array | complex, /) -> Array: other = self._check_allowed_dtypes(other, "numeric", "__mul__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__mul__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__mul__(other._array) return self.__class__._new(res, device=self.device) def __ne__(self, other: Array | complex, /) -> Array: # type: ignore[override] @@ -854,8 +849,8 @@ def __ne__(self, other: Array | complex, /) -> Array: # type: ignore[override] other = self._check_allowed_dtypes(other, "all", "__ne__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__ne__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__ne__(other._array) return self.__class__._new(res, device=self.device) def __neg__(self) -> Array: @@ -875,8 +870,8 @@ def __or__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer or boolean", "__or__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__or__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__or__(other._array) return self.__class__._new(res, device=self.device) def __pos__(self) -> Array: @@ -910,8 +905,8 @@ def __rshift__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer", "__rshift__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__rshift__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rshift__(other._array) return self.__class__._new(res, device=self.device) def __setitem__( @@ -959,8 +954,8 @@ def __sub__(self, other: Array | complex, /) -> Array: other = self._check_allowed_dtypes(other, "numeric", "__sub__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__sub__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__sub__(other._array) return self.__class__._new(res, device=self.device) # PEP 484 requires int to be a subtype of float, but __truediv__ should @@ -973,8 +968,8 @@ def __truediv__(self, other: Array | complex, /) -> Array: other = self._check_allowed_dtypes(other, "floating-point", "__truediv__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__truediv__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__truediv__(other._array) return self.__class__._new(res, device=self.device) def __xor__(self, other: Array | int, /) -> Array: @@ -985,11 +980,11 @@ def __xor__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer or boolean", "__xor__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__xor__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__xor__(other._array) return self.__class__._new(res, device=self.device) - def __iadd__(self, other: Array | complex, /) -> Array: + def __iadd__(self, other: Array | complex, /) -> Self: """ Performs the operation __iadd__. """ @@ -1008,11 +1003,11 @@ def __radd__(self, other: Array | complex, /) -> Array: other = self._check_allowed_dtypes(other, "numeric", "__radd__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__radd__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__radd__(other._array) return self.__class__._new(res, device=self.device) - def __iand__(self, other: Array | int, /) -> Array: + def __iand__(self, other: Array | int, /) -> Self: """ Performs the operation __iand__. """ @@ -1031,11 +1026,11 @@ def __rand__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer or boolean", "__rand__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__rand__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rand__(other._array) return self.__class__._new(res, device=self.device) - def __ifloordiv__(self, other: Array | float, /) -> Array: + def __ifloordiv__(self, other: Array | float, /) -> Self: """ Performs the operation __ifloordiv__. """ @@ -1054,11 +1049,11 @@ def __rfloordiv__(self, other: Array | float, /) -> Array: other = self._check_allowed_dtypes(other, "real numeric", "__rfloordiv__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__rfloordiv__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rfloordiv__(other._array) return self.__class__._new(res, device=self.device) - def __ilshift__(self, other: Array | int, /) -> Array: + def __ilshift__(self, other: Array | int, /) -> Self: """ Performs the operation __ilshift__. """ @@ -1077,11 +1072,11 @@ def __rlshift__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer", "__rlshift__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__rlshift__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rlshift__(other._array) return self.__class__._new(res, device=self.device) - def __imatmul__(self, other: Array, /) -> Array: + def __imatmul__(self, other: Array, /) -> Self: """ Performs the operation __imatmul__. """ @@ -1107,7 +1102,7 @@ def __rmatmul__(self, other: Array, /) -> Array: res = self._array.__rmatmul__(other._array) return self.__class__._new(res, device=self.device) - def __imod__(self, other: Array | float, /) -> Array: + def __imod__(self, other: Array | float, /) -> Self: """ Performs the operation __imod__. """ @@ -1126,11 +1121,11 @@ def __rmod__(self, other: Array | float, /) -> Array: if other is NotImplemented: return other self._check_type_device(other) - self, other = self._normalize_two_args(self, other) - res = self._array.__rmod__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rmod__(other._array) return self.__class__._new(res, device=self.device) - def __imul__(self, other: Array | complex, /) -> Array: + def __imul__(self, other: Array | complex, /) -> Self: """ Performs the operation __imul__. """ @@ -1149,11 +1144,11 @@ def __rmul__(self, other: Array | complex, /) -> Array: if other is NotImplemented: return other self._check_type_device(other) - self, other = self._normalize_two_args(self, other) - res = self._array.__rmul__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rmul__(other._array) return self.__class__._new(res, device=self.device) - def __ior__(self, other: Array | int, /) -> Array: + def __ior__(self, other: Array | int, /) -> Self: """ Performs the operation __ior__. """ @@ -1172,11 +1167,11 @@ def __ror__(self, other: Array | int, /) -> Array: other = self._check_allowed_dtypes(other, "integer or boolean", "__ror__") if other is NotImplemented: return other - self, other = self._normalize_two_args(self, other) - res = self._array.__ror__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__ror__(other._array) return self.__class__._new(res, device=self.device) - def __ipow__(self, other: Array | complex, /) -> Array: + def __ipow__(self, other: Array | complex, /) -> Self: """ Performs the operation __ipow__. """ @@ -1201,7 +1196,7 @@ def __rpow__(self, other: Array | complex, /) -> Array: # for 0-d arrays, so we use pow() here instead. return pow(other, self) - def __irshift__(self, other: Array | int, /) -> Array: + def __irshift__(self, other: Array | int, /) -> Self: """ Performs the operation __irshift__. """ @@ -1220,11 +1215,11 @@ def __rrshift__(self, other: Array | int, /) -> Array: if other is NotImplemented: return other self._check_type_device(other) - self, other = self._normalize_two_args(self, other) - res = self._array.__rrshift__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rrshift__(other._array) return self.__class__._new(res, device=self.device) - def __isub__(self, other: Array | complex, /) -> Array: + def __isub__(self, other: Array | complex, /) -> Self: """ Performs the operation __isub__. """ @@ -1243,11 +1238,11 @@ def __rsub__(self, other: Array | complex, /) -> Array: if other is NotImplemented: return other self._check_type_device(other) - self, other = self._normalize_two_args(self, other) - res = self._array.__rsub__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rsub__(other._array) return self.__class__._new(res, device=self.device) - def __itruediv__(self, other: Array | complex, /) -> Array: + def __itruediv__(self, other: Array | complex, /) -> Self: """ Performs the operation __itruediv__. """ @@ -1266,11 +1261,11 @@ def __rtruediv__(self, other: Array | complex, /) -> Array: if other is NotImplemented: return other self._check_type_device(other) - self, other = self._normalize_two_args(self, other) - res = self._array.__rtruediv__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rtruediv__(other._array) return self.__class__._new(res, device=self.device) - def __ixor__(self, other: Array | int, /) -> Array: + def __ixor__(self, other: Array | int, /) -> Self: """ Performs the operation __ixor__. """ @@ -1289,8 +1284,8 @@ def __rxor__(self, other: Array | int, /) -> Array: if other is NotImplemented: return other self._check_type_device(other) - self, other = self._normalize_two_args(self, other) - res = self._array.__rxor__(other._array) + _self, other = self._normalize_two_args(self, other) + res = _self._array.__rxor__(other._array) return self.__class__._new(res, device=self.device) def to_device(self, device: Device, /, stream: None = None) -> Array: diff --git a/array_api_strict/_creation_functions.py b/array_api_strict/_creation_functions.py index 685044df..e97d846d 100644 --- a/array_api_strict/_creation_functions.py +++ b/array_api_strict/_creation_functions.py @@ -5,11 +5,10 @@ import numpy as np -from ._dtypes import DType, _all_dtypes, _np_dtype, bool as xp_bool -from ._devices import ( - Device, device_supports_dtype, get_default_dtypes, - check_device as _check_device -) +from ._devices import Device, device_supports_dtype, get_default_dtypes +from ._devices import check_device as _check_device +from ._dtypes import DType, _all_dtypes, _np_dtype +from ._dtypes import bool as xp_bool from ._flags import get_array_api_strict_flags from ._typing import NestedSequence, SupportsBufferProtocol, SupportsDLPack @@ -34,9 +33,8 @@ def _check_valid_dtype(dtype: DType | None, device: Device | None = None) -> Non if dtype not in _all_dtypes: raise ValueError(f"dtype must be one of the supported dtypes, got {dtype!r}") - if device is not None: - if not device_supports_dtype(device, dtype): - raise ValueError(f"Device {device!r} does not support dtype={dtype!r}.") + if device is not None and not device_supports_dtype(device, dtype): + raise ValueError(f"Device {device!r} does not support dtype={dtype!r}.") def _supports_buffer_protocol(obj: object) -> TypeIs[SupportsBufferProtocol]: @@ -96,9 +94,8 @@ def asarray( if isinstance(obj, Array): return Array._new(np.array(obj._array, copy=copy, dtype=_np_dtype), device=device) - elif isinstance(obj, list | tuple): - if any(isinstance(x, Array) for x in obj): - raise TypeError("Nested Arrays are not allowed. Use `stack` instead.") + elif isinstance(obj, list | tuple) and any(isinstance(x, Array) for x in obj): + raise TypeError("Nested Arrays are not allowed. Use `stack` instead.") 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 diff --git a/array_api_strict/_devices.py b/array_api_strict/_devices.py index 18831991..f4ddab10 100644 --- a/array_api_strict/_devices.py +++ b/array_api_strict/_devices.py @@ -1,17 +1,29 @@ from typing import Final from ._dtypes import ( - DType, float32, float64, complex64, complex128, int64, uint64, int32, - _all_dtypes, _boolean_dtypes, _signed_integer_dtypes, - _unsigned_integer_dtypes, _integer_dtypes, _real_floating_dtypes, - _complex_floating_dtypes, _numeric_dtypes + DType, + _all_dtypes, + _boolean_dtypes, + _complex_floating_dtypes, + _integer_dtypes, + _numeric_dtypes, + _real_floating_dtypes, + _signed_integer_dtypes, + _unsigned_integer_dtypes, + complex64, + complex128, + float32, + float64, + int32, + int64, + uint64, ) _ALL_DEVICE_NAMES = ("CPU_DEVICE", "device1", "device2", "no_float64", "no_x64") class Device: _device: Final[str] - __slots__ = ("_device", "__weakref__") + __slots__ = ("__weakref__", "_device") def __init__(self, device: str = "CPU_DEVICE"): if device not in _ALL_DEVICE_NAMES: diff --git a/array_api_strict/_dtypes.py b/array_api_strict/_dtypes.py index 158d8d96..874b8623 100644 --- a/array_api_strict/_dtypes.py +++ b/array_api_strict/_dtypes.py @@ -12,7 +12,7 @@ class DType: _np_dtype: Final[np.dtype[Any]] _canonical_name: Final[Any] - __slots__ = ("_np_dtype", "_canonical_name", "__weakref__") + __slots__ = ("__weakref__", "_canonical_name", "_np_dtype") def __init__(self, np_dtype: npt.DTypeLike): self._canonical_name = np_dtype diff --git a/array_api_strict/_fft.py b/array_api_strict/_fft.py index e069435f..aecaa7a9 100644 --- a/array_api_strict/_fft.py +++ b/array_api_strict/_fft.py @@ -4,8 +4,8 @@ import numpy as np from ._array_object import Array -from ._devices import ALL_DEVICES, Device, device_supports_dtype from ._data_type_functions import astype +from ._devices import ALL_DEVICES, Device, device_supports_dtype from ._dtypes import ( DType, _complex_floating_dtypes, @@ -14,8 +14,8 @@ complex64, float32, ) -from ._info import __array_namespace_info__ from ._flags import requires_extension +from ._info import __array_namespace_info__ @requires_extension('fft') @@ -338,17 +338,17 @@ def ifftshift(x: Array, /, *, axes: int | Sequence[int] | None = None) -> Array: __all__ = [ "fft", - "ifft", + "fftfreq", "fftn", + "fftshift", + "hfft", + "ifft", "ifftn", - "rfft", + "ifftshift", + "ihfft", "irfft", - "rfftn", "irfftn", - "hfft", - "ihfft", - "fftfreq", + "rfft", "rfftfreq", - "fftshift", - "ifftshift", + "rfftn", ] diff --git a/array_api_strict/_flags.py b/array_api_strict/_flags.py index 450c91e2..caa0d16c 100644 --- a/array_api_strict/_flags.py +++ b/array_api_strict/_flags.py @@ -14,9 +14,9 @@ import functools import os import warnings -from collections.abc import Callable +from collections.abc import Callable, Collection from types import TracebackType -from typing import Any, Collection, ParamSpec, TypeVar, cast +from typing import Any, ParamSpec, TypeVar, cast import array_api_strict diff --git a/array_api_strict/_linalg.py b/array_api_strict/_linalg.py index c512963a..aa68894c 100644 --- a/array_api_strict/_linalg.py +++ b/array_api_strict/_linalg.py @@ -9,7 +9,7 @@ from ._data_type_functions import finfo from ._dtypes import DType, _floating_dtypes, _numeric_dtypes, complex64, complex128 from ._elementwise_functions import conj -from ._flags import get_array_api_strict_flags, requires_extension, requires_api_version +from ._flags import get_array_api_strict_flags, requires_api_version, requires_extension from ._manipulation_functions import reshape from ._statistical_functions import _np_dtype_sumprod @@ -232,7 +232,7 @@ def matrix_norm( *, keepdims: bool = False, ord: float | Literal["fro", "nuc"] | None = "fro", -) -> Array: # noqa: F821 +) -> Array: """ Array API compatible wrapper for :py:func:`np.linalg.norm `. @@ -330,7 +330,7 @@ def pinv(x: Array, /, *, rtol: float | Array | None = None) -> Array: return Array._new(np.linalg.pinv(x._array, rcond=rtol_np), device=x.device) @requires_extension('linalg') -def qr(x: Array, /, *, mode: Literal['reduced', 'complete'] = 'reduced') -> QRResult: # noqa: F821 +def qr(x: Array, /, *, mode: Literal['reduced', 'complete'] = 'reduced') -> QRResult: """ Array API compatible wrapper for :py:func:`np.linalg.qr `. @@ -373,13 +373,21 @@ def slogdet(x: Array, /) -> SlogdetResult: def _solve(a: np.ndarray, b: np.ndarray) -> np.ndarray: try: from numpy.linalg._linalg import ( # type: ignore[attr-defined] - _makearray, _assert_stacked_2d, _assert_stacked_square, - _commonType, isComplexType, _raise_linalgerror_singular + _assert_stacked_2d, + _assert_stacked_square, + _commonType, + _makearray, + _raise_linalgerror_singular, + isComplexType, ) except ImportError: from numpy.linalg.linalg import ( # type: ignore[attr-defined] - _makearray, _assert_stacked_2d, _assert_stacked_square, - _commonType, isComplexType, _raise_linalgerror_singular + _assert_stacked_2d, + _assert_stacked_square, + _commonType, + _makearray, + _raise_linalgerror_singular, + isComplexType, ) from numpy.linalg import _umath_linalg diff --git a/array_api_strict/_manipulation_functions.py b/array_api_strict/_manipulation_functions.py index 02dcf163..53197a38 100644 --- a/array_api_strict/_manipulation_functions.py +++ b/array_api_strict/_manipulation_functions.py @@ -142,7 +142,7 @@ def roll( See its docstring for more information. """ if not isinstance(shift, int | tuple): - raise ValueError( + raise TypeError( f"`shift` can only be an int or a tuple, got {type(shift)=} instead." ) return Array._new(np.roll(x._array, shift, axis=axis), device=x.device) diff --git a/array_api_strict/_searching_functions.py b/array_api_strict/_searching_functions.py index b435856b..1747df03 100644 --- a/array_api_strict/_searching_functions.py +++ b/array_api_strict/_searching_functions.py @@ -5,7 +5,11 @@ from ._array_object import Array from ._dtypes import _real_numeric_dtypes, _result_type from ._dtypes import bool as _bool -from ._flags import requires_api_version, requires_data_dependent_shapes, get_array_api_strict_flags +from ._flags import ( + get_array_api_strict_flags, + requires_api_version, + requires_data_dependent_shapes, +) from ._helpers import _maybe_normalize_py_scalars @@ -64,7 +68,7 @@ def count_nonzero( @requires_api_version('2023.12') def searchsorted( x1: Array, - x2: Array | int | float, + x2: Array | float, /, *, side: Literal["left", "right"] = "left", @@ -76,10 +80,9 @@ def searchsorted( See its docstring for more information. """ flags = get_array_api_strict_flags() - if flags["api_version"] >= "2025.12": - # scalar x2 support is new in 2025.12 - if isinstance(x2, bool | int | float | complex): - x2 = x1._promote_scalar(x2) + # scalar x2 support is new in 2025.12 + if flags["api_version"] >= "2025.12" and isinstance(x2, bool | int | float | complex): + x2 = x1._promote_scalar(x2) if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes: raise TypeError("Only real numeric dtypes are allowed in searchsorted") diff --git a/array_api_strict/_set_functions.py b/array_api_strict/_set_functions.py index 1661045f..fe40b0df 100644 --- a/array_api_strict/_set_functions.py +++ b/array_api_strict/_set_functions.py @@ -3,9 +3,9 @@ import numpy as np from ._array_object import Array -from ._flags import requires_data_dependent_shapes, requires_api_version -from ._helpers import _maybe_normalize_py_scalars from ._dtypes import _result_type +from ._flags import requires_api_version, requires_data_dependent_shapes +from ._helpers import _maybe_normalize_py_scalars # Note: np.unique() is split into four functions in the array API: # unique_all, unique_counts, unique_inverse, and unique_values (this is done diff --git a/array_api_strict/tests/conftest.py b/array_api_strict/tests/conftest.py index 1a9d507e..e7d668b4 100644 --- a/array_api_strict/tests/conftest.py +++ b/array_api_strict/tests/conftest.py @@ -1,9 +1,10 @@ import os -from .._flags import reset_array_api_strict_flags, ENVIRONMENT_VARIABLES - import pytest +from .._flags import ENVIRONMENT_VARIABLES, reset_array_api_strict_flags + + def pytest_configure(config): for env_var in ENVIRONMENT_VARIABLES: if env_var in os.environ: diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index 445ee80b..5aa7d23e 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -1,39 +1,42 @@ -import sys -import warnings import operator import pickle +import sys +import warnings from builtins import all as all_ -from numpy.testing import assert_raises import numpy as np import pytest +from numpy.testing import assert_raises -from .. import ones, arange, reshape, asarray, result_type, all, equal, stack +import array_api_strict + +from .. import all, arange, asarray, equal, ones, reshape, result_type, stack from .._array_object import Array from .._devices import CPU_DEVICE, Device from .._dtypes import ( _all_dtypes, _boolean_dtypes, - _real_floating_dtypes, - _floating_dtypes, _complex_floating_dtypes, + _floating_dtypes, _integer_dtypes, _integer_or_boolean_dtypes, - _real_numeric_dtypes, _numeric_dtypes, - uint8, + _real_floating_dtypes, + _real_numeric_dtypes, + complex128, + float64, int8, int16, int32, int64, + uint8, uint64, - float64, - complex128, +) +from .._dtypes import ( bool as bool_, ) from .._flags import set_array_api_strict_flags -import array_api_strict def test_validate_index(): # The indexing tests in the official array API test suite test that the @@ -360,14 +363,11 @@ def _array_vals(): or y.dtype in _boolean_dtypes and x.dtype not in _boolean_dtypes or x.dtype in _floating_dtypes and y.dtype not in _floating_dtypes or y.dtype in _floating_dtypes and x.dtype not in _floating_dtypes - ): - assert_raises(TypeError, lambda: getattr(x, _op)(y)) - # Ensure in-place operators only promote to the same dtype as the left operand. - elif ( + ) or ( _op.startswith("__i") and result_type(x.dtype, y.dtype) != x.dtype ): - assert_raises(TypeError, lambda: getattr(x, _op)(y)) + assert_raises(TypeError, lambda x=x, _op=_op, y=y: getattr(x, _op)(y)) # Ensure only those dtypes that are required for every operator are allowed. elif (dtypes == "all" and (x.dtype in _boolean_dtypes and y.dtype in _boolean_dtypes or x.dtype in _numeric_dtypes and y.dtype in _numeric_dtypes) @@ -381,7 +381,7 @@ def _array_vals(): ): getattr(x, _op)(y) else: - assert_raises(TypeError, lambda: getattr(x, _op)(y)) + assert_raises(TypeError, lambda x=x, _op=_op, y=y: getattr(x, _op)(y)) # finally, test that array op ndarray raises # XXX: as long as there is __array__ or __buffer__, __rop__s # still return ndarrays @@ -401,7 +401,7 @@ def _array_vals(): # Only test for no error getattr(a, op)() else: - assert_raises(TypeError, lambda: getattr(a, op)()) + assert_raises(TypeError, lambda a=a, op=op: getattr(a, op)()) # Finally, matmul() must be tested separately, because it works a bit # different from the other operations. @@ -420,9 +420,9 @@ def _matmul_array_vals(): or type(s) == int and a.dtype in _integer_dtypes): # Type promotion is valid, but @ is not allowed on 0-D # inputs, so the error is a ValueError - assert_raises(ValueError, lambda: getattr(a, _op)(s)) + assert_raises(ValueError, lambda a=a, _op=_op, s=s: getattr(a, _op)(s)) else: - assert_raises(TypeError, lambda: getattr(a, _op)(s)) + assert_raises(TypeError, lambda a=a, _op=_op, s=s: getattr(a, _op)(s)) for x in _matmul_array_vals(): for y in _matmul_array_vals(): @@ -435,24 +435,24 @@ def _matmul_array_vals(): or x.dtype in _boolean_dtypes or y.dtype in _boolean_dtypes ): - assert_raises(TypeError, lambda: x.__matmul__(y)) - assert_raises(TypeError, lambda: y.__rmatmul__(x)) - assert_raises(TypeError, lambda: x.__imatmul__(y)) + assert_raises(TypeError, lambda x=x, y=y: x.__matmul__(y)) + assert_raises(TypeError, lambda x=x, y=y: y.__rmatmul__(x)) + assert_raises(TypeError, lambda x=x, y=y: x.__imatmul__(y)) elif x.shape == () or y.shape == () or x.shape[1] != y.shape[0]: - assert_raises(ValueError, lambda: x.__matmul__(y)) - assert_raises(ValueError, lambda: y.__rmatmul__(x)) + assert_raises(ValueError, lambda x=x, y=y: x.__matmul__(y)) + assert_raises(ValueError, lambda x=x, y=y: y.__rmatmul__(x)) if result_type(x.dtype, y.dtype) != x.dtype: - assert_raises(TypeError, lambda: x.__imatmul__(y)) + assert_raises(TypeError, lambda x=x, y=y: x.__imatmul__(y)) else: - assert_raises(ValueError, lambda: x.__imatmul__(y)) + assert_raises(ValueError, lambda x=x, y=y: x.__imatmul__(y)) else: x.__matmul__(y) y.__rmatmul__(x) if result_type(x.dtype, y.dtype) != x.dtype: - assert_raises(TypeError, lambda: x.__imatmul__(y)) + assert_raises(TypeError, lambda x=x, y=y: x.__imatmul__(y)) elif y.shape[0] != y.shape[1]: # This one fails because x @ y has a different shape from x - assert_raises(ValueError, lambda: x.__imatmul__(y)) + assert_raises(ValueError, lambda x=x, y=y: x.__imatmul__(y)) else: x.__imatmul__(y) diff --git a/array_api_strict/tests/test_creation_functions.py b/array_api_strict/tests/test_creation_functions.py index a28be649..0fe9b328 100644 --- a/array_api_strict/tests/test_creation_functions.py +++ b/array_api_strict/tests/test_creation_functions.py @@ -1,14 +1,14 @@ import warnings -from numpy.testing import assert_raises import numpy as np - import pytest +from numpy.testing import assert_raises from .. import all +from .._array_object import Array from .._creation_functions import ( - asarray, arange, + asarray, empty, empty_like, eye, @@ -22,11 +22,12 @@ zeros, zeros_like, ) -from .._dtypes import float32, float64, complex64, int32, int64, bool as xp_bool -from .._array_object import Array -from .._devices import CPU_DEVICE, ALL_DEVICES, Device -from .._info import __array_namespace_info__ +from .._devices import ALL_DEVICES, CPU_DEVICE, Device +from .._dtypes import bool as xp_bool +from .._dtypes import complex64, float32, float64, int32, int64 from .._flags import set_array_api_strict_flags +from .._info import __array_namespace_info__ + def test_asarray_errors(): # Test various protections against incorrect usage @@ -78,7 +79,7 @@ def test_asarray_copy(): 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)) + assert_raises(ValueError, lambda obj=obj: asarray(obj, copy=False)) # Buffer protocol a = np.array([1]) @@ -226,11 +227,11 @@ def test_meshgrid_dtype_errors(): def _full(a, *args, **kwds): - return full(a, fill_value=42.0, *args, **kwds) + return full(a, *args, fill_value=42.0, **kwds) def _full_like(a, *args, **kwds): - return full_like(a, fill_value=42.0, *args, **kwds) + return full_like(a, *args, fill_value=42.0, **kwds) class TestDefaultDType: diff --git a/array_api_strict/tests/test_elementwise_functions.py b/array_api_strict/tests/test_elementwise_functions.py index 7fb6e339..5a74754f 100644 --- a/array_api_strict/tests/test_elementwise_functions.py +++ b/array_api_strict/tests/test_elementwise_functions.py @@ -1,19 +1,18 @@ import warnings -from inspect import signature, getmodule +from inspect import getmodule, signature import numpy as np import pytest +import array_api_strict -from .. import asarray, _elementwise_functions +from .. import _elementwise_functions, asarray from .._devices import ALL_DEVICES, CPU_DEVICE, Device -from .._elementwise_functions import bitwise_left_shift, bitwise_right_shift from .._dtypes import ( - _dtype_categories, _boolean_dtypes, + _dtype_categories, _floating_dtypes, _integer_dtypes, - bool as xp_bool, float64, int8, int16, @@ -21,10 +20,12 @@ int64, uint64, ) +from .._dtypes import ( + bool as xp_bool, +) +from .._elementwise_functions import bitwise_left_shift, bitwise_right_shift from .._info import __array_namespace_info__ -from .test_array_object import _check_op_array_scalar, BIG_INT - -import array_api_strict +from .test_array_object import BIG_INT, _check_op_array_scalar def nargs(func): @@ -293,7 +294,10 @@ def _array_vals(): for s in [1, 1.0, 1j, BIG_INT, False]: for a in _array_vals(): - for func1 in [lambda s: func(a, s), lambda s: func(s, a)]: + for func1 in [ + lambda s, func=func, a=a: func(a, s), + lambda s, func=func, a=a: func(s, a), + ]: if func_name in nocomplex and type(s) == complex: allowed = False diff --git a/array_api_strict/tests/test_flags.py b/array_api_strict/tests/test_flags.py index 8edf5d66..eb0dd6a1 100644 --- a/array_api_strict/tests/test_flags.py +++ b/array_api_strict/tests/test_flags.py @@ -1,20 +1,66 @@ -import sys import subprocess +import sys -from .._flags import (set_array_api_strict_flags, get_array_api_strict_flags, - reset_array_api_strict_flags) -from .._fft import (fft, ifft, fftn, ifftn, rfft, irfft, rfftn, irfftn, hfft, - ihfft, fftfreq, rfftfreq, fftshift, ifftshift) -from .._linalg import (cholesky, cross, det, diagonal, eigh, eigvalsh, inv, - matmul, matrix_norm, matrix_power, matrix_rank, matrix_transpose, outer, pinv, - qr, slogdet, solve, svd, svdvals, tensordot, trace, vecdot, vector_norm) - -from .. import (asarray, unique_all, unique_counts, unique_inverse, - unique_values, nonzero, repeat) +import pytest import array_api_strict as xp -import pytest +from .. import ( + asarray, + nonzero, + repeat, + unique_all, + unique_counts, + unique_inverse, + unique_values, +) +from .._fft import ( + fft, + fftfreq, + fftn, + fftshift, + hfft, + ifft, + ifftn, + ifftshift, + ihfft, + irfft, + irfftn, + rfft, + rfftfreq, + rfftn, +) +from .._flags import ( + get_array_api_strict_flags, + reset_array_api_strict_flags, + set_array_api_strict_flags, +) +from .._linalg import ( + cholesky, + cross, + det, + diagonal, + eigh, + eigvalsh, + inv, + matmul, + matrix_norm, + matrix_power, + matrix_rank, + matrix_transpose, + outer, + pinv, + qr, + slogdet, + solve, + svd, + svdvals, + tensordot, + trace, + vecdot, + vector_norm, +) + def test_flag_defaults(): flags = get_array_api_strict_flags() @@ -223,10 +269,7 @@ def test_boolean_indexing(): @pytest.mark.parametrize('func_name', linalg_examples.keys()) def test_linalg(func_name): func = linalg_examples[func_name] - if func_name in linalg_main_namespace_examples: - main_namespace_func = linalg_main_namespace_examples[func_name] - else: - main_namespace_func = lambda: None + main_namespace_func = linalg_main_namespace_examples.get(func_name, lambda: None) # First make sure the example actually works func() @@ -391,20 +434,20 @@ def test_disabled_extensions(): assert 'linalg' in xp.__all__ assert 'fft' in xp.__all__ - xp.linalg # No error - xp.fft # No error + xp.linalg # No error # noqa: B018 + xp.fft # No error # noqa: B018 ns = {} - exec('from array_api_strict import *', ns) + exec('from array_api_strict import *', ns) # noqa: S102 assert 'linalg' in ns assert 'fft' in ns set_array_api_strict_flags(enabled_extensions=('linalg',)) assert 'linalg' in xp.__all__ assert 'fft' not in xp.__all__ - xp.linalg # No error + xp.linalg # No error # noqa: B018 pytest.raises(AttributeError, lambda: xp.fft) ns = {} - exec('from array_api_strict import *', ns) + exec('from array_api_strict import *', ns) # noqa: S102 assert 'linalg' in ns assert 'fft' not in ns @@ -412,9 +455,9 @@ def test_disabled_extensions(): assert 'linalg' not in xp.__all__ assert 'fft' in xp.__all__ pytest.raises(AttributeError, lambda: xp.linalg) - xp.fft # No error + xp.fft # No error # noqa: B018 ns = {} - exec('from array_api_strict import *', ns) + exec('from array_api_strict import *', ns) # noqa: S102 assert 'linalg' not in ns assert 'fft' in ns @@ -424,17 +467,17 @@ def test_disabled_extensions(): pytest.raises(AttributeError, lambda: xp.linalg) pytest.raises(AttributeError, lambda: xp.fft) ns = {} - exec('from array_api_strict import *', ns) + exec('from array_api_strict import *', ns) # noqa: S102 assert 'linalg' not in ns assert 'fft' not in ns reset_array_api_strict_flags() assert 'linalg' in xp.__all__ assert 'fft' in xp.__all__ - xp.linalg # No error - xp.fft # No error + xp.linalg # No error # noqa: B018 + xp.fft # No error # noqa: B018 ns = {} - exec('from array_api_strict import *', ns) + exec('from array_api_strict import *', ns) # noqa: S102 assert 'linalg' in ns assert 'fft' in ns diff --git a/array_api_strict/tests/test_linalg.py b/array_api_strict/tests/test_linalg.py index 04023bc2..7e6c05f2 100644 --- a/array_api_strict/tests/test_linalg.py +++ b/array_api_strict/tests/test_linalg.py @@ -1,9 +1,9 @@ import pytest -from .._flags import set_array_api_strict_flags - import array_api_strict as xp +from .._flags import set_array_api_strict_flags + # TODO: Maybe all of these exceptions should be IndexError? # Technically this is linear_algebra, not linalg, but it's simpler to keep diff --git a/array_api_strict/tests/test_manipulation_functions.py b/array_api_strict/tests/test_manipulation_functions.py index 53b8fcf5..0fe66da9 100644 --- a/array_api_strict/tests/test_manipulation_functions.py +++ b/array_api_strict/tests/test_manipulation_functions.py @@ -1,13 +1,9 @@ -from numpy.testing import assert_raises import numpy as np +from numpy.testing import assert_raises from .._creation_functions import asarray from .._dtypes import float64, int8 -from .._manipulation_functions import ( - concat, - reshape, - stack -) +from .._manipulation_functions import concat, reshape, stack def test_concat_errors(): diff --git a/array_api_strict/tests/test_searching_functions.py b/array_api_strict/tests/test_searching_functions.py index 409deb5b..25a3074e 100644 --- a/array_api_strict/tests/test_searching_functions.py +++ b/array_api_strict/tests/test_searching_functions.py @@ -1,8 +1,8 @@ import pytest import array_api_strict as xp - from array_api_strict import ArrayAPIStrictFlags + from .._devices import ALL_DEVICES, CPU_DEVICE, Device from .._dtypes import _all_dtypes @@ -11,9 +11,11 @@ def test_where_with_scalars(): x = xp.asarray([1, 2, 3, 1]) # Versions up to and including 2023.12 don't support scalar arguments - with ArrayAPIStrictFlags(api_version='2023.12'): - with pytest.raises(AttributeError, match="object has no attribute 'dtype'"): - xp.where(x == 1, 42, 44) + with ( + ArrayAPIStrictFlags(api_version='2023.12'), + pytest.raises(AttributeError, match="object has no attribute 'dtype'"), + ): + xp.where(x == 1, 42, 44) # Versions after 2023.12 support scalar arguments x_where = xp.where(x == 1, xp.asarray(42), 44) diff --git a/array_api_strict/tests/test_statistical_functions.py b/array_api_strict/tests/test_statistical_functions.py index 52ad53a0..4903ae4d 100644 --- a/array_api_strict/tests/test_statistical_functions.py +++ b/array_api_strict/tests/test_statistical_functions.py @@ -1,10 +1,12 @@ import cmath -import pytest -from .._flags import set_array_api_strict_flags, ArrayAPIStrictFlags +import pytest import array_api_strict as xp +from .._flags import ArrayAPIStrictFlags, set_array_api_strict_flags + + # sum, prod, and trace were changed in 2023.12 to not upcast floating-point dtypes # with dtype=None @pytest.mark.parametrize('func_name', ['sum', 'prod', 'trace']) @@ -44,9 +46,8 @@ def test_sum_prod_trace_2023_12(func_name): def test_mean_complex(): a = xp.asarray([1j, 2j, 3j]) - with ArrayAPIStrictFlags(api_version='2023.12'): - with pytest.raises(TypeError): - xp.mean(a) + with ArrayAPIStrictFlags(api_version='2023.12'), pytest.raises(TypeError): + xp.mean(a) m = xp.mean(a) assert cmath.isclose(complex(m), 2j) diff --git a/docs/conf.py b/docs/conf.py index 14d7503b..778d7893 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,8 +6,9 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -import sys import os +import sys + sys.path.insert(0, os.path.abspath('..')) project = 'array-api-strict' From 127d3fc32b71704a562a749c4dd32a6bb8b1d276 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:32:17 +0530 Subject: [PATCH 2/5] import Self from typing Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- array_api_strict/_array_object.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 565bd4c7..3eee88f3 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -20,11 +20,10 @@ from collections.abc import Callable, Iterator from enum import IntEnum from types import EllipsisType, ModuleType -from typing import Any, Literal, SupportsIndex +from typing import Any, Literal, Self, SupportsIndex import numpy as np import numpy.typing as npt -from typing_extensions import Self from ._creation_functions import Undef, _undef, asarray from ._devices import CPU_DEVICE, Device, device_supports_dtype From 277448d6840b241c9952a519201b13e9e1f64de7 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:36:04 +0530 Subject: [PATCH 3/5] import Self from typing for py >= 3.11 Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- array_api_strict/_array_object.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 3eee88f3..81b5871e 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -20,7 +20,12 @@ from collections.abc import Callable, Iterator from enum import IntEnum from types import EllipsisType, ModuleType -from typing import Any, Literal, Self, SupportsIndex +from typing import Any, Literal, SupportsIndex + +try: + from typing import Self +except ImportError: # Python < 3.11 + from typing_extensions import Self import numpy as np import numpy.typing as npt From 2ff0c893be16ef29f42d1133815b48cef2353002 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:45:22 +0530 Subject: [PATCH 4/5] adding back xp_bool import Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- array_api_strict/tests/test_elementwise_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/array_api_strict/tests/test_elementwise_functions.py b/array_api_strict/tests/test_elementwise_functions.py index 6831a3b8..a933715a 100644 --- a/array_api_strict/tests/test_elementwise_functions.py +++ b/array_api_strict/tests/test_elementwise_functions.py @@ -13,6 +13,7 @@ _dtype_categories, _floating_dtypes, _integer_dtypes, + bool as xp_bool, float64, int8, int16, From 93229cef1977fbb24dc10f75198fe5bbedecb5d8 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:32:05 +0530 Subject: [PATCH 5/5] remove ignored ruff changes + ignore B023 + apply suggestions Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- array_api_strict/_array_object.py | 44 ++++++++++--------- array_api_strict/_searching_functions.py | 7 +-- array_api_strict/tests/test_array_object.py | 42 ++++++++++++------ .../tests/test_creation_functions.py | 3 +- .../tests/test_elementwise_functions.py | 5 +-- .../tests/test_searching_functions.py | 8 ++-- .../tests/test_statistical_functions.py | 5 ++- ruff.toml | 2 + 8 files changed, 66 insertions(+), 50 deletions(-) diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 2bd566b1..4738d028 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -202,18 +202,19 @@ def _check_allowed_dtypes( # to promote in the spec (even if the NumPy array operator would # promote them). res_dtype = _result_type(self.dtype, other.dtype) - # Note: NumPy will allow in-place operators in some cases where - # the type promoted operator does not match the left-hand side - # operand. For example, + if op.startswith("__i"): + # Note: NumPy will allow in-place operators in some cases where + # the type promoted operator does not match the left-hand side + # operand. For example, - # >>> a = np.array(1, dtype=np.int8) - # >>> a += np.array(1, dtype=np.int16) + # >>> a = np.array(1, dtype=np.int8) + # >>> a += np.array(1, dtype=np.int16) - # The spec explicitly disallows this. - if op.startswith("__i") and res_dtype != self.dtype: - raise TypeError( - f"Cannot perform {op} with dtypes {self.dtype} and {other.dtype}" - ) + # The spec explicitly disallows this. + if res_dtype != self.dtype: + raise TypeError( + f"Cannot perform {op} with dtypes {self.dtype} and {other.dtype}" + ) return other @@ -425,17 +426,18 @@ def _validate_index( n_single_axes = len(single_axes) if n_ellipsis > 1: return # handled by ndarray - # Note boolean masks must be the sole index, which we check for - # later on. - elif n_ellipsis == 0 and not key_has_mask and n_single_axes < self.ndim: - raise IndexError( - f"{self.ndim=}, but the multi-axes index only specifies " - f"{n_single_axes} dimensions. If this was intentional, " - "add a trailing ellipsis (...) which expands into as many " - "slices (:) as necessary - this is what np.ndarray arrays " - "implicitly do, but such flat indexing behaviour is not " - "specified in the Array API." - ) + elif n_ellipsis == 0: + # Note boolean masks must be the sole index, which we check for + # later on. + if not key_has_mask and n_single_axes < self.ndim: + raise IndexError( + f"{self.ndim=}, but the multi-axes index only specifies " + f"{n_single_axes} dimensions. If this was intentional, " + "add a trailing ellipsis (...) which expands into as many " + "slices (:) as necessary - this is what np.ndarray arrays " + "implicitly do, but such flat indexing behaviour is not " + "specified in the Array API." + ) if (key_has_index_array and (n_ellipsis > 0 or key_has_slices or key_has_mask)): raise IndexError( diff --git a/array_api_strict/_searching_functions.py b/array_api_strict/_searching_functions.py index 3b2982b9..6b8659b2 100644 --- a/array_api_strict/_searching_functions.py +++ b/array_api_strict/_searching_functions.py @@ -79,9 +79,10 @@ def searchsorted( See its docstring for more information. """ flags = get_array_api_strict_flags() - # scalar x2 support is new in 2025.12 - if flags["api_version"] >= "2025.12" and isinstance(x2, bool | int | float | complex): - x2 = x1._promote_scalar(x2) + if flags["api_version"] >= "2025.12": + # scalar x2 support is new in 2025.12 + if isinstance(x2, bool | int | float | complex): + x2 = x1._promote_scalar(x2) if x1.dtype not in _real_numeric_dtypes or x2.dtype not in _real_numeric_dtypes: raise TypeError("Only real numeric dtypes are allowed in searchsorted") diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index 4bd49216..fd09276d 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -365,7 +365,8 @@ def _array_vals(): _op.startswith("__i") and result_type(x.dtype, y.dtype) != x.dtype ): - assert_raises(TypeError, lambda x=x, _op=_op, y=y: getattr(x, _op)(y)) + with assert_raises(TypeError): + getattr(x, _op)(y) # Ensure only those dtypes that are required for every operator are allowed. elif (dtypes == "all" and (x.dtype in _boolean_dtypes and y.dtype in _boolean_dtypes or x.dtype in _numeric_dtypes and y.dtype in _numeric_dtypes) @@ -379,7 +380,8 @@ def _array_vals(): ): getattr(x, _op)(y) else: - assert_raises(TypeError, lambda x=x, _op=_op, y=y: getattr(x, _op)(y)) + with assert_raises(TypeError): + getattr(x, _op)(y) # finally, test that array op ndarray raises # XXX: as long as there is __array__ or __buffer__, __rop__s # still return ndarrays @@ -399,7 +401,8 @@ def _array_vals(): # Only test for no error getattr(a, op)() else: - assert_raises(TypeError, lambda a=a, op=op: getattr(a, op)()) + with assert_raises(TypeError): + getattr(a, op)() # Finally, matmul() must be tested separately, because it works a bit # different from the other operations. @@ -418,9 +421,11 @@ def _matmul_array_vals(): or type(s) == int and a.dtype in _integer_dtypes): # Type promotion is valid, but @ is not allowed on 0-D # inputs, so the error is a ValueError - assert_raises(ValueError, lambda a=a, _op=_op, s=s: getattr(a, _op)(s)) + with assert_raises(ValueError): + getattr(a, _op)(s) else: - assert_raises(TypeError, lambda a=a, _op=_op, s=s: getattr(a, _op)(s)) + with assert_raises(TypeError): + getattr(a, _op)(s) for x in _matmul_array_vals(): for y in _matmul_array_vals(): @@ -433,24 +438,33 @@ def _matmul_array_vals(): or x.dtype in _boolean_dtypes or y.dtype in _boolean_dtypes ): - assert_raises(TypeError, lambda x=x, y=y: x.__matmul__(y)) - assert_raises(TypeError, lambda x=x, y=y: y.__rmatmul__(x)) - assert_raises(TypeError, lambda x=x, y=y: x.__imatmul__(y)) + with assert_raises(TypeError): + x.__matmul__(y) + with assert_raises(TypeError): + y.__rmatmul__(x) + with assert_raises(TypeError): + x.__imatmul__(y) elif x.shape == () or y.shape == () or x.shape[1] != y.shape[0]: - assert_raises(ValueError, lambda x=x, y=y: x.__matmul__(y)) - assert_raises(ValueError, lambda x=x, y=y: y.__rmatmul__(x)) + with assert_raises(ValueError): + x.__matmul__(y) + with assert_raises(ValueError): + y.__rmatmul__(x) if result_type(x.dtype, y.dtype) != x.dtype: - assert_raises(TypeError, lambda x=x, y=y: x.__imatmul__(y)) + with assert_raises(TypeError): + x.__imatmul__(y) else: - assert_raises(ValueError, lambda x=x, y=y: x.__imatmul__(y)) + with assert_raises(ValueError): + x.__imatmul__(y) else: x.__matmul__(y) y.__rmatmul__(x) if result_type(x.dtype, y.dtype) != x.dtype: - assert_raises(TypeError, lambda x=x, y=y: x.__imatmul__(y)) + with assert_raises(TypeError): + x.__imatmul__(y) elif y.shape[0] != y.shape[1]: # This one fails because x @ y has a different shape from x - assert_raises(ValueError, lambda x=x, y=y: x.__imatmul__(y)) + with assert_raises(ValueError): + x.__imatmul__(y) else: x.__imatmul__(y) diff --git a/array_api_strict/tests/test_creation_functions.py b/array_api_strict/tests/test_creation_functions.py index 5d276932..acee119d 100644 --- a/array_api_strict/tests/test_creation_functions.py +++ b/array_api_strict/tests/test_creation_functions.py @@ -78,7 +78,8 @@ def test_asarray_copy(): 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 obj=obj: asarray(obj, copy=False)) + with assert_raises(ValueError): + asarray(obj, copy=False) # Buffer protocol a = np.array([1]) diff --git a/array_api_strict/tests/test_elementwise_functions.py b/array_api_strict/tests/test_elementwise_functions.py index a933715a..6f7ef755 100644 --- a/array_api_strict/tests/test_elementwise_functions.py +++ b/array_api_strict/tests/test_elementwise_functions.py @@ -292,10 +292,7 @@ def _array_vals(): for s in [1, 1.0, 1j, BIG_INT, False]: for a in _array_vals(): - for func1 in [ - lambda s, func=func, a=a: func(a, s), - lambda s, func=func, a=a: func(s, a), - ]: + for func1 in [lambda s: func(a, s), lambda s: func(s, a)]: if func_name in nocomplex and type(s) == complex: allowed = False diff --git a/array_api_strict/tests/test_searching_functions.py b/array_api_strict/tests/test_searching_functions.py index 25a3074e..2be514b0 100644 --- a/array_api_strict/tests/test_searching_functions.py +++ b/array_api_strict/tests/test_searching_functions.py @@ -11,11 +11,9 @@ def test_where_with_scalars(): x = xp.asarray([1, 2, 3, 1]) # Versions up to and including 2023.12 don't support scalar arguments - with ( - ArrayAPIStrictFlags(api_version='2023.12'), - pytest.raises(AttributeError, match="object has no attribute 'dtype'"), - ): - xp.where(x == 1, 42, 44) + with ArrayAPIStrictFlags(api_version='2023.12'): + with pytest.raises(AttributeError, match="object has no attribute 'dtype'"): + xp.where(x == 1, 42, 44) # Versions after 2023.12 support scalar arguments x_where = xp.where(x == 1, xp.asarray(42), 44) diff --git a/array_api_strict/tests/test_statistical_functions.py b/array_api_strict/tests/test_statistical_functions.py index 4903ae4d..68b85f82 100644 --- a/array_api_strict/tests/test_statistical_functions.py +++ b/array_api_strict/tests/test_statistical_functions.py @@ -46,8 +46,9 @@ def test_sum_prod_trace_2023_12(func_name): def test_mean_complex(): a = xp.asarray([1j, 2j, 3j]) - with ArrayAPIStrictFlags(api_version='2023.12'), pytest.raises(TypeError): - xp.mean(a) + with ArrayAPIStrictFlags(api_version='2023.12'): + with pytest.raises(TypeError): + xp.mean(a) m = xp.mean(a) assert cmath.isclose(complex(m), 2j) diff --git a/ruff.toml b/ruff.toml index 68780a5d..af682d16 100644 --- a/ruff.toml +++ b/ruff.toml @@ -8,6 +8,8 @@ ignore = [ "SIM117", # Keep nested if statements "SIM102", + # Allow lambdas in loops + "B023", ] [lint.isort] combine-as-imports = true