diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d270069b..220d50d3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,7 +39,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -49,8 +49,22 @@ jobs: cache-dependency-path: "pyproject.toml" - run: sudo apt-get install -y libbz2-dev # required to build fitsio - run: pip install -c .github/test-constraints.txt '.[test]' - - run: pytest --cov=heracles --cov-report=lcov + - run: pytest --cov=heracles --cov-report=xml - uses: coverallsapp/github-action@v2 + with: + parallel: true + flag-name: run-${{ matrix.python-version }} + + finish: + name: Finish + runs-on: ubuntu-latest + needs: test + if: always() + steps: + - uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "run-3.9,run-3.10,run-3.11,run-3.12,run-3.13" build: name: Build diff --git a/heracles/io.py b/heracles/io.py index 90044a8f..03eed048 100644 --- a/heracles/io.py +++ b/heracles/io.py @@ -618,6 +618,8 @@ def __getitem__(self, key): data = self._cache.get(ext) if data is None: with self.fits as fits: + if ext not in fits: + raise KeyError(ext) data = self.reader(fits[ext]) self._cache[ext] = data return data diff --git a/tests/test_io.py b/tests/test_io.py index 239ab56d..27dc1882 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -8,18 +8,30 @@ def zbins(): return {0: (0.0, 0.8), 1: (1.0, 1.2)} +@pytest.fixture +def zbins2(): + return {2: (0.0, 0.8), 3: (1.0, 1.2)} + + @pytest.fixture def mock_alms(rng, zbins): + return generate_mock_alms(rng, zbins) + + +@pytest.fixture +def mock_alms2(rng, zbins2): + return generate_mock_alms(rng, zbins2) + + +def generate_mock_alms(rng, zbins): import numpy as np lmax = 32 - Nlm = (lmax + 1) * (lmax + 2) // 2 - # names and spins fields = {"P": 0, "G": 2} - alms = {} + for n, s in fields.items(): shape = (Nlm, 2) if s == 0 else (2, Nlm, 2) for i in zbins: @@ -258,17 +270,67 @@ def test_write_read_alms(mock_alms, tmp_path): import numpy as np path = tmp_path / "alms.fits" - heracles.write_alms(path, mock_alms) assert path.exists() - alms = heracles.read_alms(path) + alms = heracles.read_alms(path) assert alms.keys() == mock_alms.keys() + for key in mock_alms: np.testing.assert_array_equal(mock_alms[key], alms[key]) assert mock_alms[key].dtype.metadata == alms[key].dtype.metadata +def test_fits_dict_keyerror(mock_alms, tmp_path): + from heracles.io import FitsDict + from pathlib import Path + + tmp_path = Path(tmp_path) + path1 = tmp_path / "alms1.fits" + + heracles.write_alms(path1, mock_alms) + + assert path1.exists() + + alms1 = FitsDict(path1, clobber=False) + + # Ensure bad key raises a key error + with pytest.raises(KeyError): + _ = alms1["badkey"] + + # Ensure an existing key does NOT raise an error + _ = alms1[("P", 0)] + + +def test_chain_almfits(mock_alms, mock_alms2, tmp_path): + from heracles.io import AlmFits + from collections import ChainMap + import numpy as np + + path1 = tmp_path / "alms1.fits" + path2 = tmp_path / "alms2.fits" + + heracles.write_alms(path1, mock_alms) + heracles.write_alms(path2, mock_alms2) + + assert path1.exists() + assert path2.exists() + + alms1 = AlmFits(path1, clobber=False) + alms2 = AlmFits(path2, clobber=False) + chained_alms = ChainMap() + + chained_alms.maps.append(alms1) + chained_alms.maps.append(alms2) + + for key in mock_alms: + np.testing.assert_array_equal(mock_alms[key], chained_alms[key]) + assert mock_alms[key].dtype.metadata == chained_alms[key].dtype.metadata + for key in mock_alms2: + np.testing.assert_array_equal(mock_alms2[key], chained_alms[key]) + assert mock_alms2[key].dtype.metadata == chained_alms[key].dtype.metadata + + def test_write_read_cls(mock_cls, tmp_path): import numpy as np diff --git a/tests/test_result.py b/tests/test_result.py index 3b1935fe..921536a9 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -88,7 +88,7 @@ def test_result_2d(rng): @pytest.mark.parametrize("weight", [None, "l(l+1)", "2l+1", ""]) @pytest.mark.parametrize("ndim,axis", [(1, 0), (2, 0), (3, 1)]) def test_binned(ndim, axis, weight, rng): - shape = rng.integers(0, 100, ndim) + shape = rng.integers(1, 100, ndim) lmax = shape[axis] - 1 data = heracles.Result(rng.standard_normal(shape), axis=axis)