diff --git a/autonerves/fitsable.py b/autonerves/fitsable.py index 1f44679..67dc162 100644 --- a/autonerves/fitsable.py +++ b/autonerves/fitsable.py @@ -207,8 +207,10 @@ def ndarray_via_fits_from( -------- array_2d = ndarray_via_fits_from(file_path='/path/to/file/filename.fits', hdu=0) """ - hdu_list = fits.open(file_path, do_not_scale_image_data=do_not_scale_image_data) - return ndarray_via_hdu_from(hdu_list[hdu]) + with fits.open( + file_path, do_not_scale_image_data=do_not_scale_image_data + ) as hdu_list: + return ndarray_via_hdu_from(hdu_list[hdu]) def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: @@ -233,8 +235,8 @@ def header_obj_from(file_path: Union[Path, str], hdu: int) -> Dict: -------- array_2d = ndarray_via_fits_from(file_path='/path/to/file/filename.fits', hdu=0) """ - hdu_list = fits.open(file_path) - return hdu_list[hdu].header + with fits.open(file_path) as hdu_list: + return hdu_list[hdu].header diff --git a/autonerves/workspace.py b/autonerves/workspace.py index 0994608..2cd621e 100644 --- a/autonerves/workspace.py +++ b/autonerves/workspace.py @@ -82,6 +82,22 @@ def _version_date(parsed_version): return None +def _is_source_checkout(root): + """ + True when ``root`` is a Python package source checkout (a ``setup.py`` or + ``pyproject.toml`` at its top level) rather than a workspace clone. + + ``check_version`` is called unconditionally on library import with + ``workspace_root`` defaulting to the current working directory, so any + pytest run or script executed from inside a library's own repo would + otherwise warn "Cannot verify the workspace ..." on every import — a + false positive, since a source checkout is not a workspace and records + no version floor to verify. Workspace clones ship neither file, so a + genuine workspace missing its version keys still warns. + """ + return (root / "setup.py").exists() or (root / "pyproject.toml").exists() + + def _library_name_from_workspace(workspace_root): name = workspace_root.name suffix = "_workspace" @@ -168,7 +184,10 @@ def check_version(library_version, workspace_root=None): installs) warn on inequality rather than raising. If no floor source is found, a warning is emitted and the check is - skipped. + skipped — unless ``workspace_root`` is a package source checkout + (``setup.py``/``pyproject.toml`` at its top level), in which case the + check is skipped silently: running from inside a library's own repo is + not a workspace-compatibility question at all. The check can be disabled in two ways: @@ -203,6 +222,8 @@ def check_version(library_version, workspace_root=None): floor_version = version_file.read_text().strip() if floor_version is None or floor_version == "": + if _is_source_checkout(root): + return warnings.warn(_missing_version_warning(root, library_version)) return diff --git a/test_autonerves/test_fitsable.py b/test_autonerves/test_fitsable.py index 9a135a2..e243d53 100644 --- a/test_autonerves/test_fitsable.py +++ b/test_autonerves/test_fitsable.py @@ -70,6 +70,22 @@ def test__output_to_fits__header_dict(): assert header["A"] == 1 +def test__fits_readers_close_their_file_handles(): + """Regression: `fits.open` without close leaked file handles, emitting + `ResourceWarning: unclosed file` throughout every downstream repo that + loads FITS via these helpers.""" + import gc + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("error", ResourceWarning) + fitsable.ndarray_via_fits_from( + file_path=test_data_path / "3x3_ones.fits", hdu=0 + ) + fitsable.header_obj_from(file_path=test_data_path / "3x3_ones.fits", hdu=0) + gc.collect() + + def test__header_obj_from(): header_obj = fitsable.header_obj_from( file_path=test_data_path / "3x3_ones.fits", hdu=0 diff --git a/test_autonerves/test_test_mode.py b/test_autonerves/test_test_mode.py index 22cad07..3870502 100644 --- a/test_autonerves/test_test_mode.py +++ b/test_autonerves/test_test_mode.py @@ -10,11 +10,14 @@ from autonerves.test_mode import ( is_test_mode, - test_mode_level, - test_mode_samples, with_test_mode_segment, ) +# ``test_mode_samples`` is real API, but its ``test_`` prefix means a bare-name +# import here would be collected by pytest as a test function +# (PytestReturnNotNoneWarning, an ERROR in future pytest) — alias it instead. +from autonerves.test_mode import test_mode_samples as _test_mode_samples + @pytest.fixture(autouse=True) def _restore_test_mode_env(): @@ -69,13 +72,13 @@ def _restore_samples_env(self): def test__env_unset_returns_historical_default_of_four(self): os.environ.pop("PYAUTO_TEST_MODE_SAMPLES", None) - assert test_mode_samples() == 4 + assert _test_mode_samples() == 4 def test__env_set_returns_value(self): os.environ["PYAUTO_TEST_MODE_SAMPLES"] = "50000" - assert test_mode_samples() == 50000 + assert _test_mode_samples() == 50000 def test__values_below_four_raise(self): os.environ["PYAUTO_TEST_MODE_SAMPLES"] = "3" with pytest.raises(ValueError): - test_mode_samples() + _test_mode_samples() diff --git a/test_autonerves/test_workspace.py b/test_autonerves/test_workspace.py index 1c1cbfa..0106181 100644 --- a/test_autonerves/test_workspace.py +++ b/test_autonerves/test_workspace.py @@ -33,6 +33,26 @@ def test_missing_sources_warns(tmp_path): check_version("2026.7.22.1", workspace_root=tmp_path) +@pytest.mark.parametrize("marker", ["setup.py", "pyproject.toml"]) +def test_missing_sources_in_source_checkout_skips_silently(tmp_path, marker): + """A package source checkout (setup.py/pyproject.toml at the root) is not + a workspace — importing a library from inside its own repo must not warn + that the "workspace" version cannot be verified.""" + (tmp_path / marker).write_text("") + with warnings.catch_warnings(): + warnings.simplefilter("error") + check_version("2026.7.22.1", workspace_root=tmp_path) + + +def test_source_checkout_with_version_floor_still_checked(tmp_path): + """The source-checkout skip only covers the no-floor false positive — a + recorded floor is still enforced even with a setup.py present.""" + (tmp_path / "setup.py").write_text("") + (tmp_path / "version.txt").write_text("2026.7.22.1\n") + with pytest.raises(WorkspaceVersionMismatchError): + check_version("2025.1.1.1", workspace_root=tmp_path) + + def test_env_override_skips_mismatch(tmp_path, monkeypatch): monkeypatch.setenv("PYAUTO_SKIP_WORKSPACE_VERSION_CHECK", "1") (tmp_path / "version.txt").write_text("2025.1.1.1\n")