diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8dfd7f4..dc58f42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,7 +71,7 @@ jobs: - name: Run unit tests shell: bash -l {0} run: | - pytest --cov=geotiepoints geotiepoints/tests --cov-report=xml --cov-report= + pytest --cov=geotiepoints geotiepoints --cov-report=xml --cov-report= - name: Upload unittest coverage to Codecov uses: codecov/codecov-action@v7 diff --git a/AGENTS.md b/AGENTS.md index db1a9c2..8565bcf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,8 +125,8 @@ graph without computing. ```bash pip install -e . python setup.py build_ext --inplace --cython-coverage --force # required before running tests -pytest geotiepoints/tests -pytest --cov=geotiepoints geotiepoints/tests --cov-report=xml # what CI runs +pytest # whole package, doctests included +pytest --cov=geotiepoints geotiepoints --cov-report=xml # what CI runs make -C doc doctest ``` @@ -135,7 +135,21 @@ make -C doc doctest - Tests load HDF5 fixtures by path relative to the test file (`../../testdata/`), so they only work from a source checkout, never from an installed wheel. -- There is **no `conftest.py` and no pytest configuration at all** — no markers, no ini options. +- There is **no `conftest.py`**. All pytest configuration lives in `[tool.pytest.ini_options]` in + `pyproject.toml` (mirroring trollimage): `--doctest-modules` plus `-ra --showlocals + --strict-markers --strict-config`, `xfail_strict`, `filterwarnings = ["error"]`, and + `testpaths = ["geotiepoints"]`. So a bare `pytest` runs the unit tests *and* every module + docstring. No markers. Two consequences worth knowing: + - Passing an explicit path (e.g. `pytest geotiepoints/tests`) overrides `testpaths` and skips the + package doctests. + - **Warnings are errors**, with one scoped exception: the `invalid value encountered in + arcsin/arccos` `RuntimeWarning` from `geotiepoints.geointerpolator`. `xyz2lonlat` is *meant* to + return NaN for coordinates that interpolation or extrapolation puts off the sphere -- an invalid + pixel should stay visibly invalid rather than be clipped to a plausible-looking lat/lon -- while + callers (Satpy readers) keep processing the rest of the swath. Do not "fix" that NaN. + - `warnings.catch_warnings(record=True)` inherits the error filter and will raise instead of + recording, so it needs an explicit `warnings.simplefilter("always")` inside the context (see + `test_modisinterpolator.test_sat_angle_based_interp`). - `test_simple_modis_interpolator.py` imports its loaders and `assert_geodetic_distance` from `test_modisinterpolator.py`. Preserve that cross-module dependency. - `testdata/create_modis_test_data.py` regenerates fixtures but needs `pyhdf` plus a real MOD03 file; @@ -145,6 +159,8 @@ make -C doc doctest - CI: ubuntu/macos/windows × Python 3.11/3.12/3.13, plus an experimental nightly-dependency job. `python_requires >= 3.11`. - There is no `[project]` table — package metadata still lives in `setup.py`. +- `doc/source/conf.py` mocks nothing; the docs (and RTD, via `pip install .`) need the real package + and its dependencies importable, which is what makes the `index.rst` doctests runnable. - `geotiepoints/version.py` is versioneer-generated; never edit it. Release steps are in `RELEASING.md`. ## Known defects and traps diff --git a/doc/source/conf.py b/doc/source/conf.py index 55d7f90..a2c6b5a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -24,30 +24,6 @@ sys.path.insert(0, os.path.abspath('../../geotiepoints')) -class Mock(object): - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - if name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - return Mock() - - -MOCK_MODULES = ['numpy', 'scipy.interpolate', 'scipy', - 'pyhdf.SD', 'pyhdf.error'] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() - - # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. diff --git a/geotiepoints/interpolator.py b/geotiepoints/interpolator.py index c35fbbf..d4b2df4 100644 --- a/geotiepoints/interpolator.py +++ b/geotiepoints/interpolator.py @@ -37,11 +37,11 @@ def _linear_extrapolate(pos, data, xev): >>> data = np.arange(10).reshape((2, 5), order="F") >>> xev = 5 >>> retv = _linear_extrapolate(pos, data, xev) - >>> print([val for val in retv]) + >>> print([float(val) for val in retv]) [4.0, 6.0, 8.0, 10.0, 12.0] >>> xev = 0 >>> retv = _linear_extrapolate(pos, data, xev) - >>> print([val for val in retv]) + >>> print([float(val) for val in retv]) [-1.0, 1.0, 3.0, 5.0, 7.0] """ if len(data) != 2 or len(pos) != 2: diff --git a/geotiepoints/tests/test_modisinterpolator.py b/geotiepoints/tests/test_modisinterpolator.py index 86a4faf..d04c4bc 100644 --- a/geotiepoints/tests/test_modisinterpolator.py +++ b/geotiepoints/tests/test_modisinterpolator.py @@ -126,6 +126,7 @@ def test_sat_angle_based_interp(input_func, exp_func, interp_func, dist_max, exp # when working with dask arrays, we shouldn't compute anything with dask.config.set(scheduler=CustomScheduler(0)), warnings.catch_warnings(record=True) as warns: + warnings.simplefilter("always") lons, lats = interp_func(lon1, lat1, satz1) has_5km_warning = any("may result in poor quality" in str(w.message) for w in warns) if exp_5km_warning: diff --git a/geotiepoints/tests/test_multilinear.py b/geotiepoints/tests/test_multilinear.py index f8b0a8f..ff691b5 100644 --- a/geotiepoints/tests/test_multilinear.py +++ b/geotiepoints/tests/test_multilinear.py @@ -60,7 +60,10 @@ def test_multilinear_interp(self): ]) interp = MultilinearInterpolator(smin, smax, orders) - interp.set_values(f(interp.grid)) + with np.errstate(invalid="ignore"): + # x**3 + y**3 is negative over part of the grid; the resulting NaNs are + # baked into the expected RES1 values. + interp.set_values(f(interp.grid)) result = interp(ARR1) # exact_values = f(ARR1) diff --git a/pyproject.toml b/pyproject.toml index 18a5122..8aaa8b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,21 @@ requires = ["setuptools", "wheel", "numpy>=2.0.0,<3", "Cython>=3.1.2", "versioneer[toml]"] build-backend = "setuptools.build_meta" +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config", "--doctest-modules"] +xfail_strict = true +filterwarnings = [ + "error", + # xyz2lonlat: coordinates that interpolation/extrapolation puts off the sphere make the + # arcsin/arccos argument exceed 1. NaN is the intended result there -- an invalid pixel should + # be visible as NaN rather than silently corrected -- and callers (Satpy readers) must keep + # processing the rest of the swath, so the accompanying RuntimeWarning is not an error. + "ignore:invalid value encountered in arc(sin|cos):RuntimeWarning:geotiepoints.geointerpolator", +] +log_cli_level = "info" +testpaths = ["geotiepoints"] + [tool.coverage.run] relative_files = true plugins = ["Cython.Coverage"]