Skip to content

Commit 9a793b3

Browse files
authored
Merge pull request #133 from djhoese/bugfix-doctests
Add doctests to CI and fix existing failures
2 parents 826d1b6 + ec581f8 commit 9a793b3

7 files changed

Lines changed: 42 additions & 31 deletions

File tree

‎.github/workflows/ci.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- name: Run unit tests
7272
shell: bash -l {0}
7373
run: |
74-
pytest --cov=geotiepoints geotiepoints/tests --cov-report=xml --cov-report=
74+
pytest --cov=geotiepoints geotiepoints --cov-report=xml --cov-report=
7575
7676
- name: Upload unittest coverage to Codecov
7777
uses: codecov/codecov-action@v7

‎AGENTS.md‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ graph without computing.
125125
```bash
126126
pip install -e .
127127
python setup.py build_ext --inplace --cython-coverage --force # required before running tests
128-
pytest geotiepoints/tests
129-
pytest --cov=geotiepoints geotiepoints/tests --cov-report=xml # what CI runs
128+
pytest # whole package, doctests included
129+
pytest --cov=geotiepoints geotiepoints --cov-report=xml # what CI runs
130130
make -C doc doctest
131131
```
132132

@@ -135,7 +135,21 @@ make -C doc doctest
135135

136136
- Tests load HDF5 fixtures by path relative to the test file (`../../testdata/`), so they only work
137137
from a source checkout, never from an installed wheel.
138-
- There is **no `conftest.py` and no pytest configuration at all** — no markers, no ini options.
138+
- There is **no `conftest.py`**. All pytest configuration lives in `[tool.pytest.ini_options]` in
139+
`pyproject.toml` (mirroring trollimage): `--doctest-modules` plus `-ra --showlocals
140+
--strict-markers --strict-config`, `xfail_strict`, `filterwarnings = ["error"]`, and
141+
`testpaths = ["geotiepoints"]`. So a bare `pytest` runs the unit tests *and* every module
142+
docstring. No markers. Two consequences worth knowing:
143+
- Passing an explicit path (e.g. `pytest geotiepoints/tests`) overrides `testpaths` and skips the
144+
package doctests.
145+
- **Warnings are errors**, with one scoped exception: the `invalid value encountered in
146+
arcsin/arccos` `RuntimeWarning` from `geotiepoints.geointerpolator`. `xyz2lonlat` is *meant* to
147+
return NaN for coordinates that interpolation or extrapolation puts off the sphere -- an invalid
148+
pixel should stay visibly invalid rather than be clipped to a plausible-looking lat/lon -- while
149+
callers (Satpy readers) keep processing the rest of the swath. Do not "fix" that NaN.
150+
- `warnings.catch_warnings(record=True)` inherits the error filter and will raise instead of
151+
recording, so it needs an explicit `warnings.simplefilter("always")` inside the context (see
152+
`test_modisinterpolator.test_sat_angle_based_interp`).
139153
- `test_simple_modis_interpolator.py` imports its loaders and `assert_geodetic_distance` from
140154
`test_modisinterpolator.py`. Preserve that cross-module dependency.
141155
- `testdata/create_modis_test_data.py` regenerates fixtures but needs `pyhdf` plus a real MOD03 file;
@@ -145,6 +159,8 @@ make -C doc doctest
145159
- CI: ubuntu/macos/windows × Python 3.11/3.12/3.13, plus an experimental nightly-dependency job.
146160
`python_requires >= 3.11`.
147161
- There is no `[project]` table — package metadata still lives in `setup.py`.
162+
- `doc/source/conf.py` mocks nothing; the docs (and RTD, via `pip install .`) need the real package
163+
and its dependencies importable, which is what makes the `index.rst` doctests runnable.
148164
- `geotiepoints/version.py` is versioneer-generated; never edit it. Release steps are in `RELEASING.md`.
149165

150166
## Known defects and traps

‎doc/source/conf.py‎

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,6 @@
2424
sys.path.insert(0, os.path.abspath('../../geotiepoints'))
2525

2626

27-
class Mock(object):
28-
def __init__(self, *args, **kwargs):
29-
pass
30-
31-
def __call__(self, *args, **kwargs):
32-
return Mock()
33-
34-
@classmethod
35-
def __getattr__(cls, name):
36-
if name in ('__file__', '__path__'):
37-
return '/dev/null'
38-
if name[0] == name[0].upper():
39-
mockType = type(name, (), {})
40-
mockType.__module__ = __name__
41-
return mockType
42-
return Mock()
43-
44-
45-
MOCK_MODULES = ['numpy', 'scipy.interpolate', 'scipy',
46-
'pyhdf.SD', 'pyhdf.error']
47-
for mod_name in MOCK_MODULES:
48-
sys.modules[mod_name] = Mock()
49-
50-
5127
# -- General configuration -----------------------------------------------------
5228

5329
# If your documentation needs a minimal Sphinx version, state it here.

‎geotiepoints/interpolator.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ def _linear_extrapolate(pos, data, xev):
3737
>>> data = np.arange(10).reshape((2, 5), order="F")
3838
>>> xev = 5
3939
>>> retv = _linear_extrapolate(pos, data, xev)
40-
>>> print([val for val in retv])
40+
>>> print([float(val) for val in retv])
4141
[4.0, 6.0, 8.0, 10.0, 12.0]
4242
>>> xev = 0
4343
>>> retv = _linear_extrapolate(pos, data, xev)
44-
>>> print([val for val in retv])
44+
>>> print([float(val) for val in retv])
4545
[-1.0, 1.0, 3.0, 5.0, 7.0]
4646
"""
4747
if len(data) != 2 or len(pos) != 2:

‎geotiepoints/tests/test_modisinterpolator.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def test_sat_angle_based_interp(input_func, exp_func, interp_func, dist_max, exp
126126

127127
# when working with dask arrays, we shouldn't compute anything
128128
with dask.config.set(scheduler=CustomScheduler(0)), warnings.catch_warnings(record=True) as warns:
129+
warnings.simplefilter("always")
129130
lons, lats = interp_func(lon1, lat1, satz1)
130131
has_5km_warning = any("may result in poor quality" in str(w.message) for w in warns)
131132
if exp_5km_warning:

‎geotiepoints/tests/test_multilinear.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ def test_multilinear_interp(self):
6060
])
6161

6262
interp = MultilinearInterpolator(smin, smax, orders)
63-
interp.set_values(f(interp.grid))
63+
with np.errstate(invalid="ignore"):
64+
# x**3 + y**3 is negative over part of the grid; the resulting NaNs are
65+
# baked into the expected RES1 values.
66+
interp.set_values(f(interp.grid))
6467

6568
result = interp(ARR1)
6669
# exact_values = f(ARR1)

‎pyproject.toml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22
requires = ["setuptools", "wheel", "numpy>=2.0.0,<3", "Cython>=3.1.2", "versioneer[toml]"]
33
build-backend = "setuptools.build_meta"
44

5+
[tool.pytest.ini_options]
6+
minversion = "6.0"
7+
addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config", "--doctest-modules"]
8+
xfail_strict = true
9+
filterwarnings = [
10+
"error",
11+
# xyz2lonlat: coordinates that interpolation/extrapolation puts off the sphere make the
12+
# arcsin/arccos argument exceed 1. NaN is the intended result there -- an invalid pixel should
13+
# be visible as NaN rather than silently corrected -- and callers (Satpy readers) must keep
14+
# processing the rest of the swath, so the accompanying RuntimeWarning is not an error.
15+
"ignore:invalid value encountered in arc(sin|cos):RuntimeWarning:geotiepoints.geointerpolator",
16+
]
17+
log_cli_level = "info"
18+
testpaths = ["geotiepoints"]
19+
520
[tool.coverage.run]
621
relative_files = true
722
plugins = ["Cython.Coverage"]

0 commit comments

Comments
 (0)