From b8149370bec6810d6416966b0efeb51c919deec5 Mon Sep 17 00:00:00 2001 From: James Nightingale Date: Sat, 22 Aug 2026 13:20:27 +0000 Subject: [PATCH] test: skip-if-missing guards for the [optional] sampler/astropy tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_nautilus.py::test__single_core_builds_no_pool` runs a real `search.fit`, which imports `nautilus` — a package that ships only via the `[optional]` extras. With no skip guard it hard-failed with `ModuleNotFoundError: No module named 'nautilus'` in every env installed without those extras (cloud sandboxes, local venvs), while passing in CI, which does install `[optional]`. That single test has been misreported as "pre-existing on clean main" in at least six task records since 2026-08-16, costing a control-run each time. It is not a defect on main: main is green (2024 passed, 3 skipped). The same class of noise came from `astropy`: two modules failed collection outright on its top-level import, and nine aggregator tests errored on it at runtime. Guards follow the existing house pattern (`test_blackjax_nuts.py`, `nest/nss/test_search.py`): an `importlib.util.find_spec` skipif marker, or `pytest.importorskip` where the import is module-level. Verified three ways on this branch: - no extras at all: the nautilus test skips with its reason; 3 passed, 1 skipped - no astropy: 1985 passed, 33 skipped, 0 failed, 0 errors (was: 2 collection errors, then 4 failed / 9 errors) - full `[optional]` extras, i.e. the CI env: 2024 passed, 3 skipped — identical to CI on main, so the guards are inert where the deps exist and no coverage is lost. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F1uQdHt11NPBc5cXA5cvme --- .../summary_files/test_aggregate_fits.py | 12 ++++++++++++ test_autofit/aggregator/test_child_analysis.py | 11 +++++++++++ test_autofit/aggregator/test_reference.py | 14 ++++++++++++++ test_autofit/aggregator/test_scrape.py | 12 ++++++++++++ test_autofit/database/test_file_types.py | 8 ++++++++ .../non_linear/paths/test_save_and_load.py | 10 +++++++++- .../non_linear/search/nest/test_nautilus.py | 12 ++++++++++++ 7 files changed, 78 insertions(+), 1 deletion(-) diff --git a/test_autofit/aggregator/summary_files/test_aggregate_fits.py b/test_autofit/aggregator/summary_files/test_aggregate_fits.py index aa11d584c..b3fccf8b0 100644 --- a/test_autofit/aggregator/summary_files/test_aggregate_fits.py +++ b/test_autofit/aggregator/summary_files/test_aggregate_fits.py @@ -1,3 +1,4 @@ +import importlib.util from enum import Enum import pytest @@ -5,6 +6,17 @@ from pathlib import Path +# `astropy` ships via the `[optional]` extras. Envs installed without them +# must skip rather than fail with `No module named 'astropy'`. +requires_astropy = pytest.mark.skipif( + importlib.util.find_spec("astropy") is None, + reason="requires astropy (installed via the [optional] extras)", +) + + +pytestmark = requires_astropy + + class FITSFit(Enum): """ The HDUs that can be extracted from the fit.fits file. diff --git a/test_autofit/aggregator/test_child_analysis.py b/test_autofit/aggregator/test_child_analysis.py index e9e2a68b1..123963150 100644 --- a/test_autofit/aggregator/test_child_analysis.py +++ b/test_autofit/aggregator/test_child_analysis.py @@ -1,3 +1,4 @@ +import importlib.util from pathlib import Path import pytest @@ -7,6 +8,14 @@ import autofit as af +# `astropy` ships via the `[optional]` extras. Envs installed without them +# must skip rather than fail with `No module named 'astropy'`. +requires_astropy = pytest.mark.skipif( + importlib.util.find_spec("astropy") is None, + reason="requires astropy (installed via the [optional] extras)", +) + + @pytest.fixture(name="directory") def make_directory(): return Path(__file__).parent @@ -50,12 +59,14 @@ def make_aggregator(session, directory): return aggregator +@requires_astropy def test_database_aggregator(aggregator): assert list(aggregator.child_values("example")) == [ ["hello world", "hello world"], ] +@requires_astropy def test_child_values(aggregator): fit, *_ = list(aggregator) assert fit.child_values("example") == ["hello world", "hello world"] diff --git a/test_autofit/aggregator/test_reference.py b/test_autofit/aggregator/test_reference.py index 118a75729..f14841d19 100644 --- a/test_autofit/aggregator/test_reference.py +++ b/test_autofit/aggregator/test_reference.py @@ -1,3 +1,4 @@ +import importlib.util import os import pytest @@ -9,6 +10,14 @@ from autofit.database.aggregator.info import Info +# `astropy` ships via the `[optional]` extras. Envs installed without them +# must skip rather than fail with `No module named 'astropy'`. +requires_astropy = pytest.mark.skipif( + importlib.util.find_spec("astropy") is None, + reason="requires astropy (installed via the [optional] extras)", +) + + @pytest.fixture(name="directory") def make_directory(): return Path(__file__).parent @@ -56,6 +65,7 @@ def database_aggregator( return aggregator +@requires_astropy def test_database(database_aggregator): fit = list(database_aggregator)[0] model = fit.model @@ -67,20 +77,24 @@ def make_info(database_aggregator): return Info(database_aggregator.session) +@requires_astropy def test_query_fits(info): fits = info.fits assert len(info.fits) == 3 assert fits[0].total_parameters == 4 +@requires_astropy def test_headers_and_rows(info): assert len(info.headers) == len(info.rows[0]) +@requires_astropy def test_info_path(info, output_directory): assert info.path == output_directory / "database.info" +@requires_astropy def test_database_info( database_aggregator, output_directory, diff --git a/test_autofit/aggregator/test_scrape.py b/test_autofit/aggregator/test_scrape.py index 70cf35b56..9cdaf2e4f 100644 --- a/test_autofit/aggregator/test_scrape.py +++ b/test_autofit/aggregator/test_scrape.py @@ -1,9 +1,19 @@ +import importlib.util + import pytest from autofit import SearchOutput from autofit.database.aggregator.scrape import _add_files +# `astropy` ships via the `[optional]` extras. Envs installed without them +# must skip rather than fail with `No module named 'astropy'`. +requires_astropy = pytest.mark.skipif( + importlib.util.find_spec("astropy") is None, + reason="requires astropy (installed via the [optional] extras)", +) + + class MockFit: def __init__(self): self.jsons = {} @@ -36,6 +46,7 @@ def make_fit(directory): return fit +@requires_astropy def test_add_files(fit): assert fit.jsons["model"] == { "class_path": "autofit.example.model.Gaussian", @@ -63,6 +74,7 @@ def test_add_files(fit): } +@requires_astropy def test_add_recursive(fit): assert fit.jsons["directory.example"] == { "hello": "world", diff --git a/test_autofit/database/test_file_types.py b/test_autofit/database/test_file_types.py index 8500a79cf..59bc8e161 100644 --- a/test_autofit/database/test_file_types.py +++ b/test_autofit/database/test_file_types.py @@ -3,6 +3,14 @@ from autofit.database import JSON from autofit import database as db + +# `astropy` ships via the `[optional]` extras; skip the module there +# rather than fail collection with `No module named 'astropy'`. +pytest.importorskip( + "astropy", + reason="requires astropy (installed via the [optional] extras)", +) + from astropy.io import fits diff --git a/test_autofit/non_linear/paths/test_save_and_load.py b/test_autofit/non_linear/paths/test_save_and_load.py index 3e5aa3e7f..be4e66745 100644 --- a/test_autofit/non_linear/paths/test_save_and_load.py +++ b/test_autofit/non_linear/paths/test_save_and_load.py @@ -1,9 +1,17 @@ import numpy as np import pytest -from astropy.io import fits import autofit as af +# `astropy` ships via the `[optional]` extras; skip the module there +# rather than fail collection with `No module named 'astropy'`. +pytest.importorskip( + "astropy", + reason="requires astropy (installed via the [optional] extras)", +) + +from astropy.io import fits + @pytest.fixture(name="dictionary") def make_dictionary(): diff --git a/test_autofit/non_linear/search/nest/test_nautilus.py b/test_autofit/non_linear/search/nest/test_nautilus.py index ca217876b..87e621157 100644 --- a/test_autofit/non_linear/search/nest/test_nautilus.py +++ b/test_autofit/non_linear/search/nest/test_nautilus.py @@ -1,3 +1,5 @@ +import importlib.util + import numpy as np import pytest @@ -5,6 +7,15 @@ pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") +# The regression test below runs a real `search.fit`, which imports the +# `nautilus` sampler; it ships via the `[optional]` extras. Envs installed +# without those extras must skip rather than fail with +# `No module named 'nautilus'` — the other tests here only read config. +requires_nautilus = pytest.mark.skipif( + importlib.util.find_spec("nautilus") is None, + reason="requires nautilus-sampler (installed via the [optional] extras)", +) + def test__explicit_params(): search = af.Nautilus( @@ -54,6 +65,7 @@ def test__test_mode(): assert search.n_like_max == 1 +@requires_nautilus def test__single_core_builds_no_pool(monkeypatch): """ number_of_cores=1 must not construct a multiprocessing pool: nautilus