From 54e58620b5d5ec7689bace11196a5668a20f5912 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Fri, 21 Nov 2025 14:05:46 +0100 Subject: [PATCH 1/8] ENH: test expand_dims with tuple axis cf data-apis#760 for discussion We test here that expand_dims with multiple axes is equivalent to expanding axes one by one---the key is that the axes to add need to be pre-sorted. --- .../test_manipulation_functions.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/array_api_tests/test_manipulation_functions.py b/array_api_tests/test_manipulation_functions.py index 3af7b959..6954e5f9 100644 --- a/array_api_tests/test_manipulation_functions.py +++ b/array_api_tests/test_manipulation_functions.py @@ -156,6 +156,42 @@ def test_expand_dims(x, axis): raise +@given( + x=hh.arrays(dtype=hh.all_dtypes, shape=shared_shapes(max_dims=4)), + axes=shared_shapes().flatmap( + lambda s: st.lists( + st.integers(2*(-len(s)-1), 2*len(s)), + min_size=0 if len(s)==0 else 1, + max_size=len(s) + ).map(tuple) + ) +) +def test_expand_dims_tuples(x, axes): + # normalize the axes + y_ndim = x.ndim + len(axes) + n_axes = tuple(ax + y_ndim if ax < 0 else ax for ax in axes) + unique_axes = set(n_axes) + + if any(ax < 0 or ax >= y_ndim for ax in n_axes) or len(n_axes) != len(unique_axes): + with pytest.raises((IndexError, ValueError)): + xp.expand_dims(x, axis=axes) + return + + repro_snippet = ph.format_snippet(f"xp.expand_dims({x!r}, axis={axes!r})") + try: + y = xp.expand_dims(x, axis=axes) + + ye = x + for ax in sorted(n_axes): + ye = xp.expand_dims(ye, axis=ax) + assert y.shape == ye.shape + # TODO value tests; check that y.shape is 1s and items from x.shape, in order + + except Exception as exc: + ph.add_note(exc, repro_snippet) + raise + + @pytest.mark.min_version("2023.12") @given(x=hh.arrays(dtype=hh.all_dtypes, shape=hh.shapes(min_dims=1)), data=st.data()) def test_moveaxis(x, data): From d597b5c7d4ee0d752c399d683372c5b293da5923 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 7 Feb 2026 15:27:12 +0100 Subject: [PATCH 2/8] MAINT: move expand_dims tests to a class --- .../test_manipulation_functions.py | 127 +++++++++--------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/array_api_tests/test_manipulation_functions.py b/array_api_tests/test_manipulation_functions.py index 6954e5f9..a5e969a5 100644 --- a/array_api_tests/test_manipulation_functions.py +++ b/array_api_tests/test_manipulation_functions.py @@ -122,74 +122,75 @@ def test_concat(dtypes, base_shape, data): raise -@pytest.mark.unvectorized -@given( - x=hh.arrays(dtype=hh.all_dtypes, shape=shared_shapes()), - axis=shared_shapes().flatmap( - # Generate both valid and invalid axis - lambda s: st.integers(2 * (-len(s) - 1), 2 * len(s)) - ), -) -def test_expand_dims(x, axis): - if axis < -x.ndim - 1 or axis > x.ndim: - with pytest.raises(IndexError): - xp.expand_dims(x, axis=axis) - return - - repro_snippet = ph.format_snippet(f"xp.expand_dims({x!r}, axis={axis!r})") - try: - out = xp.expand_dims(x, axis=axis) +class TestExpandDims: + @pytest.mark.unvectorized + @given( + x=hh.arrays(dtype=hh.all_dtypes, shape=shared_shapes()), + axis=shared_shapes().flatmap( + # Generate both valid and invalid axis + lambda s: st.integers(2 * (-len(s) - 1), 2 * len(s)) + ), + ) + def test_expand_dims(self, x, axis): + if axis < -x.ndim - 1 or axis > x.ndim: + with pytest.raises(IndexError): + xp.expand_dims(x, axis=axis) + return - ph.assert_dtype("expand_dims", in_dtype=x.dtype, out_dtype=out.dtype) + repro_snippet = ph.format_snippet(f"xp.expand_dims({x!r}, axis={axis!r})") + try: + out = xp.expand_dims(x, axis=axis) - shape = [side for side in x.shape] - index = axis if axis >= 0 else x.ndim + axis + 1 - shape.insert(index, 1) - shape = tuple(shape) - ph.assert_result_shape("expand_dims", in_shapes=[x.shape], out_shape=out.shape, expected=shape) + ph.assert_dtype("expand_dims", in_dtype=x.dtype, out_dtype=out.dtype) - assert_array_ndindex( - "expand_dims", x, x_indices=sh.ndindex(x.shape), out=out, out_indices=sh.ndindex(out.shape) + shape = [side for side in x.shape] + index = axis if axis >= 0 else x.ndim + axis + 1 + shape.insert(index, 1) + shape = tuple(shape) + ph.assert_result_shape("expand_dims", in_shapes=[x.shape], out_shape=out.shape, expected=shape) + + assert_array_ndindex( + "expand_dims", x, x_indices=sh.ndindex(x.shape), out=out, out_indices=sh.ndindex(out.shape) + ) + except Exception as exc: + ph.add_note(exc, repro_snippet) + raise + + + @given( + x=hh.arrays(dtype=hh.all_dtypes, shape=shared_shapes(max_dims=4)), + axes=shared_shapes().flatmap( + lambda s: st.lists( + st.integers(2*(-len(s)-1), 2*len(s)), + min_size=0 if len(s)==0 else 1, + max_size=len(s) + ).map(tuple) ) - except Exception as exc: - ph.add_note(exc, repro_snippet) - raise - - -@given( - x=hh.arrays(dtype=hh.all_dtypes, shape=shared_shapes(max_dims=4)), - axes=shared_shapes().flatmap( - lambda s: st.lists( - st.integers(2*(-len(s)-1), 2*len(s)), - min_size=0 if len(s)==0 else 1, - max_size=len(s) - ).map(tuple) ) -) -def test_expand_dims_tuples(x, axes): - # normalize the axes - y_ndim = x.ndim + len(axes) - n_axes = tuple(ax + y_ndim if ax < 0 else ax for ax in axes) - unique_axes = set(n_axes) - - if any(ax < 0 or ax >= y_ndim for ax in n_axes) or len(n_axes) != len(unique_axes): - with pytest.raises((IndexError, ValueError)): - xp.expand_dims(x, axis=axes) - return - - repro_snippet = ph.format_snippet(f"xp.expand_dims({x!r}, axis={axes!r})") - try: - y = xp.expand_dims(x, axis=axes) - - ye = x - for ax in sorted(n_axes): - ye = xp.expand_dims(ye, axis=ax) - assert y.shape == ye.shape - # TODO value tests; check that y.shape is 1s and items from x.shape, in order - - except Exception as exc: - ph.add_note(exc, repro_snippet) - raise + def test_expand_dims_tuples(self, x, axes): + # normalize the axes + y_ndim = x.ndim + len(axes) + n_axes = tuple(ax + y_ndim if ax < 0 else ax for ax in axes) + unique_axes = set(n_axes) + + if any(ax < 0 or ax >= y_ndim for ax in n_axes) or len(n_axes) != len(unique_axes): + with pytest.raises((IndexError, ValueError)): + xp.expand_dims(x, axis=axes) + return + + repro_snippet = ph.format_snippet(f"xp.expand_dims({x!r}, axis={axes!r})") + try: + y = xp.expand_dims(x, axis=axes) + + ye = x + for ax in sorted(n_axes): + ye = xp.expand_dims(ye, axis=ax) + assert y.shape == ye.shape + # TODO value tests; check that y.shape is 1s and items from x.shape, in order + + except Exception as exc: + ph.add_note(exc, repro_snippet) + raise @pytest.mark.min_version("2023.12") From 779e1c3fc34d0578747d973fcc4c37790ee1061d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:34:28 +0000 Subject: [PATCH 3/8] Initial plan From 7a9935dcdf069404afeecefa5f3236f3f175e55d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:39:04 +0000 Subject: [PATCH 4/8] Fix unvectorized marker for class methods Co-authored-by: ev-br <2133832+ev-br@users.noreply.github.com> --- conftest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index dc50c9ae..f77aa81e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ from functools import lru_cache from pathlib import Path import argparse +import inspect import warnings import os @@ -253,9 +254,20 @@ def pytest_collection_modifyitems(config, items): # reduce max generated Hypothesis example for unvectorized tests if any(m.name == "unvectorized" for m in markers): # TODO: limit generated examples when settings already applied - if not hasattr(item.obj, "_hypothesis_internal_settings_applied"): + # For class methods, we need to access the underlying function + test_func = item.obj + if inspect.ismethod(test_func): + test_func = test_func.__func__ + + if not hasattr(test_func, "_hypothesis_internal_settings_applied"): try: - item.obj = settings(max_examples=unvectorized_max_examples)(item.obj) + decorated_func = settings(max_examples=unvectorized_max_examples)(test_func) + # For class methods, replace the function in the class + if inspect.ismethod(item.obj): + # Get the class and method name + setattr(item.obj.__self__.__class__, item.obj.__name__, decorated_func) + else: + item.obj = decorated_func except InvalidArgument as e: warnings.warn( f"Tried decorating {item.name} with settings() but got " From 2c4cbbab0a62342a74dc7dcb9b205abc53e7496a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:39:04 +0000 Subject: [PATCH 5/8] Fix unvectorized marker for class methods Co-authored-by: ev-br <2133832+ev-br@users.noreply.github.com> --- conftest.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/conftest.py b/conftest.py index dc50c9ae..f77aa81e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ from functools import lru_cache from pathlib import Path import argparse +import inspect import warnings import os @@ -253,9 +254,20 @@ def pytest_collection_modifyitems(config, items): # reduce max generated Hypothesis example for unvectorized tests if any(m.name == "unvectorized" for m in markers): # TODO: limit generated examples when settings already applied - if not hasattr(item.obj, "_hypothesis_internal_settings_applied"): + # For class methods, we need to access the underlying function + test_func = item.obj + if inspect.ismethod(test_func): + test_func = test_func.__func__ + + if not hasattr(test_func, "_hypothesis_internal_settings_applied"): try: - item.obj = settings(max_examples=unvectorized_max_examples)(item.obj) + decorated_func = settings(max_examples=unvectorized_max_examples)(test_func) + # For class methods, replace the function in the class + if inspect.ismethod(item.obj): + # Get the class and method name + setattr(item.obj.__self__.__class__, item.obj.__name__, decorated_func) + else: + item.obj = decorated_func except InvalidArgument as e: warnings.warn( f"Tried decorating {item.name} with settings() but got " From 417384059f42d2f66a40fc156a6f9cdb1e633537 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 14:52:41 +0000 Subject: [PATCH 6/8] Use pytest-provided item.cls instead of manual class access Co-authored-by: ev-br <2133832+ev-br@users.noreply.github.com> --- conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index f77aa81e..0cbad101 100644 --- a/conftest.py +++ b/conftest.py @@ -262,10 +262,10 @@ def pytest_collection_modifyitems(config, items): if not hasattr(test_func, "_hypothesis_internal_settings_applied"): try: decorated_func = settings(max_examples=unvectorized_max_examples)(test_func) - # For class methods, replace the function in the class + # For class methods, use pytest's item.cls to access the class if inspect.ismethod(item.obj): - # Get the class and method name - setattr(item.obj.__self__.__class__, item.obj.__name__, decorated_func) + # Use pytest-provided item.cls instead of manually accessing the class + setattr(item.cls, item.obj.__name__, decorated_func) else: item.obj = decorated_func except InvalidArgument as e: From 4cc1072ae54c381660ef45b7ba8c15e207d67407 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 7 Feb 2026 16:02:26 +0100 Subject: [PATCH 7/8] . --- conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 0cbad101..7b3499af 100644 --- a/conftest.py +++ b/conftest.py @@ -258,13 +258,11 @@ def pytest_collection_modifyitems(config, items): test_func = item.obj if inspect.ismethod(test_func): test_func = test_func.__func__ - + if not hasattr(test_func, "_hypothesis_internal_settings_applied"): try: decorated_func = settings(max_examples=unvectorized_max_examples)(test_func) - # For class methods, use pytest's item.cls to access the class if inspect.ismethod(item.obj): - # Use pytest-provided item.cls instead of manually accessing the class setattr(item.cls, item.obj.__name__, decorated_func) else: item.obj = decorated_func From c0e148c07e540fc71a0daf688210eb1e61cd9d81 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 7 Feb 2026 19:13:45 +0100 Subject: [PATCH 8/8] MAINT: use a recommended way to set max_examples for unvectorized tests Follow the recommendation at https://groups.google.com/g/hypothesis-users/c/6K6WPR5knAs --- conftest.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/conftest.py b/conftest.py index 7b3499af..f7d2d96e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,6 @@ from functools import lru_cache from pathlib import Path import argparse -import inspect import warnings import os @@ -254,18 +253,15 @@ def pytest_collection_modifyitems(config, items): # reduce max generated Hypothesis example for unvectorized tests if any(m.name == "unvectorized" for m in markers): # TODO: limit generated examples when settings already applied - # For class methods, we need to access the underlying function - test_func = item.obj - if inspect.ismethod(test_func): - test_func = test_func.__func__ + # account for both test functions and test methods of test classes + test_func = getattr(item.obj, "__func__", item.obj) + + # https://groups.google.com/g/hypothesis-users/c/6K6WPR5knAs if not hasattr(test_func, "_hypothesis_internal_settings_applied"): try: - decorated_func = settings(max_examples=unvectorized_max_examples)(test_func) - if inspect.ismethod(item.obj): - setattr(item.cls, item.obj.__name__, decorated_func) - else: - item.obj = decorated_func + sett = settings(max_examples=unvectorized_max_examples) + test_func._hypothesis_internal_use_settings = sett except InvalidArgument as e: warnings.warn( f"Tried decorating {item.name} with settings() but got "