From 3e219cfd9c6bb47f4993ef3a0763ef60a8beb9f9 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 May 2026 11:39:31 +0100 Subject: [PATCH 1/4] Fix compatibility with pytest-run-parallel and add free-threaded CI jobs --- .github/workflows/ci_workflows.yml | 3 +++ pytest_arraydiff/plugin.py | 20 +++++++++++----- tests/test_pytest_arraydiff.py | 37 ++++++++++++++++++++++++++++++ tox.ini | 3 +++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 0844a3d..7627b3b 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -30,6 +30,9 @@ jobs: - windows: py312-test-pytest74 - linux: py313-test-pytest83 - linux: py313-test-pytest90 + - linux: py313-test-parallel + - linux: py313t-test + - linux: py313t-test-parallel - linux: py313-test-devdeps publish: needs: tests diff --git a/pytest_arraydiff/plugin.py b/pytest_arraydiff/plugin.py index 0801570..5a46f24 100755 --- a/pytest_arraydiff/plugin.py +++ b/pytest_arraydiff/plugin.py @@ -240,12 +240,18 @@ def wrap_array_interceptor(plugin, item): # Only intercept array on marked array tests if item.get_closest_marker('array_compare') is not None: + # Guard against wrapping more than once (e.g. when pytest-run-parallel + # runs the same item multiple times). + if getattr(item.obj, '_arraydiff_wrapped', False): + return + # Use the full test name as a key to ensure correct array is being retrieved test_name = generate_test_name(item) def array_interceptor(store, obj): def wrapper(*args, **kwargs): store.return_value[test_name] = obj(*args, **kwargs) + wrapper._arraydiff_wrapped = True return wrapper item.obj = array_interceptor(plugin, item.obj) @@ -260,6 +266,10 @@ def __init__(self, config, reference_dir=None, generate_dir=None, default_format self.default_format = default_format self.return_value = {} + def pytest_collection_modifyitems(self, items): + for item in items: + wrap_array_interceptor(self, item) + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(self, item): @@ -298,8 +308,6 @@ def pytest_runtest_call(self, item): baseline_remote = reference_dir.startswith('http') - # Run test and get array object - wrap_array_interceptor(self, item) yield test_name = generate_test_name(item) if test_name not in self.return_value: @@ -372,11 +380,11 @@ def __init__(self, config): self.config = config self.return_value = {} - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): - - if item.get_closest_marker('array_compare') is not None: + def pytest_collection_modifyitems(self, items): + for item in items: wrap_array_interceptor(self, item) + @pytest.hookimpl(hookwrapper=True) + def pytest_runtest_call(self, item): yield return diff --git a/tests/test_pytest_arraydiff.py b/tests/test_pytest_arraydiff.py index 11a191b..90fccdb 100644 --- a/tests/test_pytest_arraydiff.py +++ b/tests/test_pytest_arraydiff.py @@ -177,3 +177,40 @@ def test_single_reference(self, spam): def test_nofile(): pass + + +TEST_PARALLEL = """ +import pytest +import numpy as np +from astropy.io import fits +@pytest.mark.array_compare(file_format='fits') +def test_parallel(): + return fits.PrimaryHDU(np.arange(3 * 5).reshape((3, 5)).astype(np.int64)) +""" + + +def test_parallel_iterations(): + """Regression test: arraydiff should work with pytest-run-parallel.""" + pytest.importorskip('pytest_run_parallel') + + tmpdir = tempfile.mkdtemp() + test_file = os.path.join(tmpdir, 'test.py') + with open(test_file, 'w') as f: + f.write(TEST_PARALLEL) + + gen_dir = os.path.join(tmpdir, 'reference') + + # Generate the reference file first + code = subprocess.call( + ['pytest', f'--arraydiff-generate-path={gen_dir}', test_file], + timeout=30, + ) + assert code == 0 + + # Now run with --arraydiff and multiple iterations + code = subprocess.call( + ['pytest', '--arraydiff', f'--arraydiff-reference-path={gen_dir}', + '--parallel-threads=2', '--iterations=3', test_file], + timeout=30, + ) + assert code == 0 diff --git a/tox.ini b/tox.ini index 11a12cc..49b8d78 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,14 @@ [tox] envlist = py{39,310,311,312,313,314}-test{,-pytestoldest,-pytest52,-pytest53,-pytest60,-pytest61,-pytest62,-pytest70,-pytest71,-pytest72,-pytest73,-pytest74,-devdeps} + py{313t,314t}-test{,-parallel} codestyle isolated_build = true [testenv] changedir = .tmp/{envname} setenv = + py313t,py314t: PYTHON_GIL = 0 devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple description = run tests deps = @@ -24,6 +26,7 @@ deps = pytest80: pytest==8.0.* pytest83: pytest==8.3.* pytest90: pytest==9.0.* + parallel: pytest-run-parallel devdeps: git+https://github.com/pytest-dev/pytest#egg=pytest devdeps: numpy>=0.0.dev0 devdeps: pandas>=0.0.dev0 From 340e541f9e69aa67019bf7a28fe6bd2392d51b2d Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 28 May 2026 17:03:58 +0000 Subject: [PATCH 2/4] Skip installing tables (PyTables) on free-threaded CPython envs since it has no free-threaded wheel and its sdist requires libhdf5-dev which CI does not install --- setup.cfg | 6 +++++- tox.ini | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1740775..b651341 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,11 +35,15 @@ install_requires = pytest>=5.0 numpy -# tables limitation is until 3.9.3 is out as that supports ARM OSX. +# tables limitation is until 3.9.3 is out as that supports ARM OSX, and +# tables wheels are not yet available for free-threaded CPython. Since +# PEP 508 has no marker for that, tables is split into its own extra and +# tox only installs it on the non-free-threaded envs. [options.extras_require] test = astropy pandas +test_hdf5 = tables;platform_machine!='arm64' [options.entry_points] diff --git a/tox.ini b/tox.ini index 49b8d78..a25e74b 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,11 @@ deps = devdeps: astropy>=0.0.dev0 extras = test + # tables (PyTables) is needed for the pandas/HDF5 file_format, but + # has no free-threaded wheels and its sdist requires libhdf5-dev, + # which CI does not install. Skip the HDF5 extras on free-threaded + # envs. + !py313t-!py314t: test_hdf5 commands = # Force numpy-dev after something in the stack downgrades it devdeps: python -m pip install --pre --upgrade --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy From afaae64b0a49b7ceff6574ad8ae2a71a792bac23 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 2 Jun 2026 10:39:45 +0100 Subject: [PATCH 3/4] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément Robert --- tests/test_pytest_arraydiff.py | 9 +++------ tox.ini | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_pytest_arraydiff.py b/tests/test_pytest_arraydiff.py index 90fccdb..81be353 100644 --- a/tests/test_pytest_arraydiff.py +++ b/tests/test_pytest_arraydiff.py @@ -193,12 +193,9 @@ def test_parallel_iterations(): """Regression test: arraydiff should work with pytest-run-parallel.""" pytest.importorskip('pytest_run_parallel') - tmpdir = tempfile.mkdtemp() - test_file = os.path.join(tmpdir, 'test.py') - with open(test_file, 'w') as f: - f.write(TEST_PARALLEL) - - gen_dir = os.path.join(tmpdir, 'reference') + test_file = tmp_path / 'test.py' + test_file.write_test(TEST_PARALLEL) + gen_dir = tmp_path / 'reference' # Generate the reference file first code = subprocess.call( diff --git a/tox.ini b/tox.ini index a25e74b..1c4fc9b 100644 --- a/tox.ini +++ b/tox.ini @@ -36,8 +36,7 @@ extras = test # tables (PyTables) is needed for the pandas/HDF5 file_format, but # has no free-threaded wheels and its sdist requires libhdf5-dev, - # which CI does not install. Skip the HDF5 extras on free-threaded - # envs. + # which CI does not install. Only run these test on GIL-enabled builds. !py313t-!py314t: test_hdf5 commands = # Force numpy-dev after something in the stack downgrades it From d6fc9dc3c89ac350c7076451ad8e4d5c1d3cf523 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 2 Jun 2026 10:24:04 +0000 Subject: [PATCH 4/4] Address review feedback by switching the parallel regression test to the pytester fixture with a pytest 6.2 minimum, testing free-threaded Python 3.14 instead of 3.13, trimming the tables comment in setup.cfg, and dropping the redundant array interceptor hook --- .github/workflows/ci_workflows.yml | 9 +++------ pytest_arraydiff/plugin.py | 5 ----- setup.cfg | 9 +++------ tests/conftest.py | 1 + tests/test_pytest_arraydiff.py | 23 +++++++++-------------- tox.ini | 7 +------ 6 files changed, 17 insertions(+), 37 deletions(-) create mode 100644 tests/conftest.py diff --git a/.github/workflows/ci_workflows.yml b/.github/workflows/ci_workflows.yml index 7627b3b..2e07a35 100644 --- a/.github/workflows/ci_workflows.yml +++ b/.github/workflows/ci_workflows.yml @@ -18,10 +18,7 @@ jobs: with: envs: | - linux: codestyle - - windows: py39-test-pytestoldest - - linux: py39-test-pytest53 - - macos: py39-test-pytest60 - - windows: py39-test-pytest61 + - windows: py39-test-pytest62 - linux: py310-test-pytest62 - macos: py310-test-pytest70 - windows: py310-test-pytest71 @@ -31,8 +28,8 @@ jobs: - linux: py313-test-pytest83 - linux: py313-test-pytest90 - linux: py313-test-parallel - - linux: py313t-test - - linux: py313t-test-parallel + - linux: py314t-test + - linux: py314t-test-parallel - linux: py313-test-devdeps publish: needs: tests diff --git a/pytest_arraydiff/plugin.py b/pytest_arraydiff/plugin.py index 5a46f24..a431e6f 100755 --- a/pytest_arraydiff/plugin.py +++ b/pytest_arraydiff/plugin.py @@ -383,8 +383,3 @@ def __init__(self, config): def pytest_collection_modifyitems(self, items): for item in items: wrap_array_interceptor(self, item) - - @pytest.hookimpl(hookwrapper=True) - def pytest_runtest_call(self, item): - yield - return diff --git a/setup.cfg b/setup.cfg index b651341..93b22c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,13 +32,10 @@ python_requires = >=3.9 setup_requires = setuptools_scm install_requires = - pytest>=5.0 + pytest>=6.2 numpy -# tables limitation is until 3.9.3 is out as that supports ARM OSX, and -# tables wheels are not yet available for free-threaded CPython. Since -# PEP 508 has no marker for that, tables is split into its own extra and -# tox only installs it on the non-free-threaded envs. +# tables limitation is until 3.9.3 is out as that supports ARM OSX. [options.extras_require] test = astropy @@ -51,7 +48,7 @@ pytest11 = pytest_arraydiff = pytest_arraydiff.plugin [tool:pytest] -minversion = 5.0 +minversion = 6.2 testpaths = tests xfail_strict = true markers = diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c6481d5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ["pytester"] diff --git a/tests/test_pytest_arraydiff.py b/tests/test_pytest_arraydiff.py index 81be353..cbeb10d 100644 --- a/tests/test_pytest_arraydiff.py +++ b/tests/test_pytest_arraydiff.py @@ -189,25 +189,20 @@ def test_parallel(): """ -def test_parallel_iterations(): +def test_parallel_iterations(pytester): """Regression test: arraydiff should work with pytest-run-parallel.""" pytest.importorskip('pytest_run_parallel') - test_file = tmp_path / 'test.py' - test_file.write_test(TEST_PARALLEL) - gen_dir = tmp_path / 'reference' + pytester.makepyfile(test_parallel=TEST_PARALLEL) + gen_dir = pytester.path / 'reference' # Generate the reference file first - code = subprocess.call( - ['pytest', f'--arraydiff-generate-path={gen_dir}', test_file], - timeout=30, - ) - assert code == 0 + result = pytester.runpytest_subprocess(f'--arraydiff-generate-path={gen_dir}') + assert result.ret == 0 # Now run with --arraydiff and multiple iterations - code = subprocess.call( - ['pytest', '--arraydiff', f'--arraydiff-reference-path={gen_dir}', - '--parallel-threads=2', '--iterations=3', test_file], - timeout=30, + result = pytester.runpytest_subprocess( + '--arraydiff', f'--arraydiff-reference-path={gen_dir}', + '--parallel-threads=2', '--iterations=3', ) - assert code == 0 + assert result.ret == 0 diff --git a/tox.ini b/tox.ini index 1c4fc9b..37f21a5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{39,310,311,312,313,314}-test{,-pytestoldest,-pytest52,-pytest53,-pytest60,-pytest61,-pytest62,-pytest70,-pytest71,-pytest72,-pytest73,-pytest74,-devdeps} + py{39,310,311,312,313,314}-test{,-pytest62,-pytest70,-pytest71,-pytest72,-pytest73,-pytest74,-devdeps} py{313t,314t}-test{,-parallel} codestyle isolated_build = true @@ -12,11 +12,6 @@ setenv = devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple description = run tests deps = - pytestoldest: pytest==5.0.0 - pytest52: pytest==5.2.* - pytest53: pytest==5.3.* - pytest60: pytest==6.0.* - pytest61: pytest==6.1.* pytest62: pytest==6.2.* pytest70: pytest==7.0.* pytest71: pytest==7.1.*