Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
33b0efe
ENH: test side=left,right in searchsorted
ev-br Nov 17, 2025
4bf7e34
ENH: test searchsorted with x2.ndim > 1
ev-br Nov 17, 2025
78c969f
ENH: searchsorted: draw x1.dtype
ev-br Nov 17, 2025
5bd524b
MAINT: searchsorted: restrict inputs to be finite real values
ev-br Nov 17, 2025
9cd8883
ENH: test take_along_axis with indices < 0
ev-br Nov 22, 2025
2784516
ENH: test take with negative indices
ev-br Nov 22, 2025
cf3bf26
ENH: add "repro_snippets" to test_utility_functions.py
ev-br Nov 21, 2025
5089588
ENH: add "repro_snippets" to test_indexing_functions.py
ev-br Nov 21, 2025
c9f6772
ENH: add "repro_snippets" to test_array_object.py
ev-br Nov 21, 2025
0c73a6f
Update .git-blame-ignore-revs for whitespace heavy commits
ev-br Nov 21, 2025
5acac26
MAINT: python 3.10 compatible add_note
ev-br Dec 1, 2025
0f04191
Merge pull request #396 from ev-br/repro_snippets_3
ev-br Dec 1, 2025
ee3d9b7
Merge pull request #393 from ev-br/searchsorted_todos
ev-br Dec 2, 2025
b7ee3d2
Merge pull request #397 from ev-br/take_along_axis_torch
ev-br Dec 2, 2025
538c24b
CI: bump versions of GH actions on CI
ev-br Dec 2, 2025
be9dd9e
Merge pull request #400 from ev-br/bump_actions
ev-br Dec 2, 2025
bdc84e8
add "repro snippets" to test_operators_and_elementwise_functions.py
ev-br Jan 9, 2026
2835f03
MAINT: ignore a whitespace heavy trivial commit
ev-br Jan 9, 2026
602b412
Merge pull request #402 from ev-br/clip_repro_snippet
ev-br Jan 9, 2026
e8d1165
ENH: expand testing of meshgrid: draw `indexing`, check shapes
ev-br Jan 10, 2026
f24e548
TST: test_{r,}fftfreq dtype argument
ev-br Jan 12, 2026
63317c6
ENH: parse complex special cases from stubs of unary functions
Copilot Jan 15, 2026
eae7d2a
TST: update array-api-strict-skips.txt for the complex cases
ev-br Jan 16, 2026
add6964
MAINT: cleaner float<->complex mappings
ev-br Jan 17, 2026
29733fb
ENH: Implement ± symbol handling in complex value parsing
Copilot Jan 19, 2026
45e566d
TST: update array-api-strict-skips.txt with an acosh case
ev-br Jan 19, 2026
4dae9dc
Merge pull request #405 from ev-br/test_meshgrid_indexing
ev-br Jan 21, 2026
3249a73
Merge pull request #408 from ev-br/fftfreq_dtype
ev-br Jan 21, 2026
b00ac41
Merge pull request #409 from ev-br/copilot_spec_cases_unary
ev-br Jan 30, 2026
48879e5
MAINT: clean up scalars/array_and_py_scalar strategies
ev-br Jan 17, 2026
05e89f3
ENH: test func(float_array, int_scalar)
ev-br Jan 17, 2026
0580097
Merge pull request #410 from ev-br/scalars_stragegy
ev-br Jan 30, 2026
54e5862
ENH: test expand_dims with tuple axis
ev-br Nov 21, 2025
d597b5c
MAINT: move expand_dims tests to a class
ev-br Feb 7, 2026
779e1c3
Initial plan
Copilot Feb 7, 2026
7a9935d
Fix unvectorized marker for class methods
Copilot Feb 7, 2026
2c4cbba
Fix unvectorized marker for class methods
Copilot Feb 7, 2026
fa58c60
Merge unvectorize_classes with fix
Copilot Feb 7, 2026
4173840
Use pytest-provided item.cls instead of manual class access
Copilot Feb 7, 2026
4cc1072
.
ev-br Feb 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,10 @@ e807ffe526c7330691e8f39d31347dc2b3106de3
bd42e84d2e5aae26ade8d70384e74effd1de89cb
f7e822883b7e24b5aa540e2413759a85128b42ef
a37f348ba27b6818e92fda8aee2406c653c671ea
# gh-396
ec5a3b4e185c262b0a5f5b1631b84a09f766d80e
9058908b58ce627467ac34e768098a25f5863d31
c80e1823c2e738381ca02f27cea1e2b89dde0ac5
# gh-402
bdc84e8316046cb5bdc637067460057eef17d0f1

4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ jobs:

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Run pre-commit hook
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ jobs:
python-version: ["3.10", "3.12", "3.13", "3.14"]
steps:
- name: Checkout array-api-tests
uses: actions/checkout@v1
uses: actions/checkout@v6
with:
submodules: 'true'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
18 changes: 18 additions & 0 deletions array-api-strict-skips.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,21 @@ array_api_tests/test_data_type_functions.py::test_finfo
array_api_tests/test_data_type_functions.py::test_finfo_dtype
array_api_tests/test_data_type_functions.py::test_iinfo
array_api_tests/test_data_type_functions.py::test_iinfo_dtype


# complex special cases which failed "forever"
array_api_tests/test_special_cases.py::test_unary[expm1(real(x_i) is +infinity and imag(x_i) is +0) -> +infinity + 0j]
array_api_tests/test_special_cases.py::test_unary[expm1(real(x_i) is -infinity and imag(x_i) is +infinity) -> -1 + 0j]
array_api_tests/test_special_cases.py::test_unary[expm1(real(x_i) is +infinity and imag(x_i) is +infinity) -> infinity + NaN j]
array_api_tests/test_special_cases.py::test_unary[expm1(real(x_i) is -infinity and imag(x_i) is NaN) -> -1 + 0j]
array_api_tests/test_special_cases.py::test_unary[expm1(real(x_i) is +infinity and imag(x_i) is NaN) -> infinity + NaN j]
array_api_tests/test_special_cases.py::test_unary[expm1(real(x_i) is NaN and imag(x_i) is +0) -> NaN + 0j]
array_api_tests/test_special_cases.py::test_unary[expm1((real(x_i) is +0 or real(x_i) == -0) and imag(x_i) is +0) -> 0 + 0j]

array_api_tests/test_special_cases.py::test_unary[sign((real(x_i) is -0 or real(x_i) == +0) and (imag(x_i) is -0 or imag(x_i) == +0)) -> 0 + 0j]
array_api_tests/test_special_cases.py::test_unary[tanh(real(x_i) is +infinity and isfinite(imag(x_i)) and imag(x_i) > 0) -> 1 + 0j]

# this acosh failure is only seen with python==3.10 and numpy==2.2.6, and not e.g. python 3.12 & numpy 2.4.1
array_api_tests/test_special_cases.py::test_unary[acosh(real(x_i) is +0 and imag(x_i) is NaN) -> NaN \xb1 \u03c0j/2]

array_api_tests/test_special_cases.py::test_unary[sqrt(real(x_i) is +infinity and isfinite(imag(x_i)) and imag(x_i) > 0) -> +0 + infinity j]
33 changes: 33 additions & 0 deletions array_api_tests/dtype_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,39 @@ def is_scalar(x):
return isinstance(x, (int, float, complex, bool))


def complex_dtype_for(dtyp):
"""Complex dtype for a float or complex."""
if dtyp in complex_dtypes:
return dtyp
if dtyp not in real_float_dtypes:
raise ValueError(f"no complex dtype to match {dtyp}")

real_name = dtype_to_name[dtyp]
complex_name = {"float32": "complex64", "float64": "complex128"}[real_name]

complex_dtype = _name_to_dtype.get(complex_name, None)
if complex_dtype is None:
raise ValueError(f"no complex dtype to match {dtyp}")
return complex_dtype


def real_dtype_for(dtyp):
"""Real float dtype for a float or complex."""
if dtyp in real_float_dtypes:
return dtyp
if dtyp not in complex_dtypes:
raise ValueError(f"no real float dtype to match {dtyp}")

complex_name = dtype_to_name[dtyp]
real_name = {"complex64": "float32", "complex128": "float64"}[complex_name]

real_dtype = _name_to_dtype.get(real_name, None)
if real_dtype is None:
raise ValueError(f"no real dtype to match {dtyp}")
return real_dtype



def _make_dtype_mapping_from_names(mapping: Dict[str, Any]) -> EqualityMapping:
dtype_value_pairs = []
for name, value in mapping.items():
Expand Down
39 changes: 25 additions & 14 deletions array_api_tests/hypothesis_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,13 +457,12 @@ def scalars(draw, dtypes, finite=False, **kwds):
dtypes should be one of the shared_* dtypes strategies.
"""
dtype = draw(dtypes)
mM = kwds.pop('mM', None)
if dh.is_int_dtype(dtype):
if mM is None:
m, M = dh.dtype_ranges[dtype]
else:
m, M = mM
return draw(integers(m, M))
m, M = dh.dtype_ranges[dtype]
min_value = kwds.get('min_value', m)
max_value = kwds.get('max_value', M)

return draw(integers(min_value, max_value))
elif dtype == bool_dtype:
return draw(booleans())
elif dtype == float64:
Expand Down Expand Up @@ -593,20 +592,32 @@ def two_mutual_arrays(


@composite
def array_and_py_scalar(draw, dtypes, mM=None, positive=False):
def array_and_py_scalar(draw, dtypes, **kwds):
"""Draw a pair: (array, scalar) or (scalar, array)."""
dtype = draw(sampled_from(dtypes))

scalar_var = draw(scalars(just(dtype), finite=True, mM=mM))
if positive:
assume (scalar_var > 0)
# draw the scalar: for float arrays, draw a float or an int
if dtype in dh.real_float_dtypes:
scalar_strategy = sampled_from([xp.int32, dtype])
else:
scalar_strategy = just(dtype)
scalar_var = draw(scalars(scalar_strategy, finite=True, **kwds))

# draw the array.
# XXX artificially limit the range of values for floats, otherwise value testing is flaky
elements={}
if dtype in dh.real_float_dtypes:
elements = {'allow_nan': False, 'allow_infinity': False,
'min_value': 1.0 / (2<<5), 'max_value': 2<<5}
if positive:
elements = {'min_value': 0}
elements = {
'allow_nan': False,
'allow_infinity': False,
'min_value': kwds.get('min_value', 1.0 / (2<<5)),
'max_value': kwds.get('max_value', 2<<5)
}
elif dtype in dh.int_dtypes:
elements = {
'min_value': kwds.get('min_value', None),
'max_value': kwds.get('max_value', None)
}
array_var = draw(arrays(dtype, shape=shapes(min_dims=1), elements=elements))

if draw(booleans()):
Expand Down
176 changes: 99 additions & 77 deletions array_api_tests/test_array_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ def test_getitem(shape, dtype, data):
key = data.draw(xps.indices(shape=shape, allow_newaxis=True), label="key")

repro_snippet = ph.format_snippet(f"{x!r}[{key!r}]")

try:
out = x[key]

Expand All @@ -109,6 +108,7 @@ def test_getitem(shape, dtype, data):
ph.add_note(exc, repro_snippet)
raise


@pytest.mark.unvectorized
@given(
shape=hh.shapes(),
Expand All @@ -133,28 +133,34 @@ def test_setitem(shape, dtypes, data):
value = data.draw(value_strat, label="value")

res = xp.asarray(x, copy=True)
res[key] = value

ph.assert_dtype("__setitem__", in_dtype=x.dtype, out_dtype=res.dtype, repr_name="x.dtype")
ph.assert_shape("__setitem__", out_shape=res.shape, expected=x.shape, repr_name="x.shape")
f_res = sh.fmt_idx("x", key)
if isinstance(value, get_args(Scalar)):
msg = f"{f_res}={res[key]!r}, but should be {value=} [__setitem__()]"
if cmath.isnan(value):
assert xp.isnan(res[key]), msg

repro_snippet = ph.format_snippet(f"{res!r}[{key!r}] = {value!r}")
try:
res[key] = value

ph.assert_dtype("__setitem__", in_dtype=x.dtype, out_dtype=res.dtype, repr_name="x.dtype")
ph.assert_shape("__setitem__", out_shape=res.shape, expected=x.shape, repr_name="x.shape")
f_res = sh.fmt_idx("x", key)
if isinstance(value, get_args(Scalar)):
msg = f"{f_res}={res[key]!r}, but should be {value=} [__setitem__()]"
if cmath.isnan(value):
assert xp.isnan(res[key]), msg
else:
assert res[key] == value, msg
else:
assert res[key] == value, msg
else:
ph.assert_array_elements("__setitem__", out=res[key], expected=value, out_repr=f_res)
unaffected_indices = set(sh.ndindex(res.shape)) - set(product(*axes_indices))
for idx in unaffected_indices:
ph.assert_0d_equals(
"__setitem__",
x_repr=f"old {f_res}",
x_val=x[idx],
out_repr=f"modified {f_res}",
out_val=res[idx],
)
ph.assert_array_elements("__setitem__", out=res[key], expected=value, out_repr=f_res)
unaffected_indices = set(sh.ndindex(res.shape)) - set(product(*axes_indices))
for idx in unaffected_indices:
ph.assert_0d_equals(
"__setitem__",
x_repr=f"old {f_res}",
x_val=x[idx],
out_repr=f"modified {f_res}",
out_val=res[idx],
)
except Exception as exc:
ph.add_note(exc, repro_snippet)
raise


@pytest.mark.unvectorized
Expand All @@ -178,29 +184,34 @@ def test_getitem_masking(shape, data):
x[key]
return

out = x[key]
repro_snippet = ph.format_snippet(f"out = {x!r}[{key!r}]")
try:
out = x[key]

ph.assert_dtype("__getitem__", in_dtype=x.dtype, out_dtype=out.dtype)
if key.ndim == 0:
expected_shape = (1,) if key else (0,)
expected_shape += x.shape
else:
size = int(xp.sum(xp.astype(key, xp.uint8)))
expected_shape = (size,) + x.shape[key.ndim :]
ph.assert_shape("__getitem__", out_shape=out.shape, expected=expected_shape)
if not any(s == 0 for s in key.shape):
assume(key.ndim == x.ndim) # TODO: test key.ndim < x.ndim scenarios
out_indices = sh.ndindex(out.shape)
for x_idx in sh.ndindex(x.shape):
if key[x_idx]:
out_idx = next(out_indices)
ph.assert_0d_equals(
"__getitem__",
x_repr=f"x[{x_idx}]",
x_val=x[x_idx],
out_repr=f"out[{out_idx}]",
out_val=out[out_idx],
)
ph.assert_dtype("__getitem__", in_dtype=x.dtype, out_dtype=out.dtype)
if key.ndim == 0:
expected_shape = (1,) if key else (0,)
expected_shape += x.shape
else:
size = int(xp.sum(xp.astype(key, xp.uint8)))
expected_shape = (size,) + x.shape[key.ndim :]
ph.assert_shape("__getitem__", out_shape=out.shape, expected=expected_shape)
if not any(s == 0 for s in key.shape):
assume(key.ndim == x.ndim) # TODO: test key.ndim < x.ndim scenarios
out_indices = sh.ndindex(out.shape)
for x_idx in sh.ndindex(x.shape):
if key[x_idx]:
out_idx = next(out_indices)
ph.assert_0d_equals(
"__getitem__",
x_repr=f"x[{x_idx}]",
x_val=x[x_idx],
out_repr=f"out[{out_idx}]",
out_val=out[out_idx],
)
except Exception as exc:
ph.add_note(exc, repro_snippet)
raise


@pytest.mark.unvectorized
Expand All @@ -213,38 +224,44 @@ def test_setitem_masking(shape, data):
)

res = xp.asarray(x, copy=True)
res[key] = value

ph.assert_dtype("__setitem__", in_dtype=x.dtype, out_dtype=res.dtype, repr_name="x.dtype")
ph.assert_shape("__setitem__", out_shape=res.shape, expected=x.shape, repr_name="x.dtype")
scalar_type = dh.get_scalar_type(x.dtype)
for idx in sh.ndindex(x.shape):
if key[idx]:
if isinstance(value, get_args(Scalar)):
ph.assert_scalar_equals(
"__setitem__",
type_=scalar_type,
idx=idx,
out=scalar_type(res[idx]),
expected=value,
repr_name="modified x",
)

repro_snippet = ph.format_snippet(f"{res}[{key!r}] = {value!r}")
try:
res[key] = value

ph.assert_dtype("__setitem__", in_dtype=x.dtype, out_dtype=res.dtype, repr_name="x.dtype")
ph.assert_shape("__setitem__", out_shape=res.shape, expected=x.shape, repr_name="x.dtype")
scalar_type = dh.get_scalar_type(x.dtype)
for idx in sh.ndindex(x.shape):
if key[idx]:
if isinstance(value, get_args(Scalar)):
ph.assert_scalar_equals(
"__setitem__",
type_=scalar_type,
idx=idx,
out=scalar_type(res[idx]),
expected=value,
repr_name="modified x",
)
else:
ph.assert_0d_equals(
"__setitem__",
x_repr="value",
x_val=value,
out_repr=f"modified x[{idx}]",
out_val=res[idx]
)
else:
ph.assert_0d_equals(
"__setitem__",
x_repr="value",
x_val=value,
x_repr=f"old x[{idx}]",
x_val=x[idx],
out_repr=f"modified x[{idx}]",
out_val=res[idx]
)
else:
ph.assert_0d_equals(
"__setitem__",
x_repr=f"old x[{idx}]",
x_val=x[idx],
out_repr=f"modified x[{idx}]",
out_val=res[idx]
)
except Exception as exc:
ph.add_note(exc, repro_snippet)
raise


# ### Fancy indexing ###
Expand Down Expand Up @@ -309,15 +326,20 @@ def _test_getitem_arrays_and_ints(shape, data, idx_max_dims):
key.append(data.draw(st.integers(-shape[i], shape[i]-1)))

key = tuple(key)
out = x[key]
repro_snippet = ph.format_snippet(f"out = {x!r}[{key!r}]")
try:
out = x[key]

arrays = [xp.asarray(k) for k in key]
bcast_shape = sh.broadcast_shapes(*[arr.shape for arr in arrays])
bcast_key = [xp.broadcast_to(arr, bcast_shape) for arr in arrays]
arrays = [xp.asarray(k) for k in key]
bcast_shape = sh.broadcast_shapes(*[arr.shape for arr in arrays])
bcast_key = [xp.broadcast_to(arr, bcast_shape) for arr in arrays]

for idx in sh.ndindex(bcast_shape):
tpl = tuple(k[idx] for k in bcast_key)
assert out[idx] == x[tpl], f"failing at {idx = } w/ {key = }"
for idx in sh.ndindex(bcast_shape):
tpl = tuple(k[idx] for k in bcast_key)
assert out[idx] == x[tpl], f"failing at {idx = } w/ {key = }"
except Exception as exc:
ph.add_note(exc, repro_snippet)
raise


def make_scalar_casting_param(
Expand Down
Loading