Skip to content

Commit f9abb6a

Browse files
authored
docs(snapshots): warn about numpy version compatibility (#310)
Snapshots created by the modflow_devtools.snapshots module fixtures may be tied to specific NumPy versions. Warn about this in the docs, and show a warning if a different NumPy version is detected than was used to create the snapshots.
1 parent 06e14c2 commit f9abb6a

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

docs/md/snapshots.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,18 @@ Snapshot comparisons can be disabled by invoked `pytest` with the `--snapshot-di
2222

2323
## Caveats & gotchas
2424

25-
NumPy major versions may introduce changes to `np.save()`'s binary format, causing binary array snapshot failures for arrays with object dtypes. To avoid this, check object (e.g. string) columns explicitly and then omit them from the comparison array.
25+
### NumPy version compatibility
26+
27+
Snapshot files are tied to the NumPy major version used to generate them. Upgrading NumPy may cause snapshot failures.
28+
29+
- **`array_snapshot` (binary)**: `np.save()` uses `.npy` format version 3.0 in NumPy 2.0+ for arrays whose dtype description cannot be encoded as Latin-1 (e.g. structured arrays with unicode field names). Snapshots generated with NumPy 1.x will not match bytes produced by NumPy 2.x for these arrays. For plain numeric dtypes (`float64`, `int32`, etc.) the format is stable across versions.
30+
- **`readable_array_snapshot` (text)**: `np.array2string()` array printing is stable across NumPy major versions. However, scalar `__repr__` changed in NumPy 2.0 (e.g. `np.float64(1.1)` instead of `1.1`), which does not affect array element printing but may affect snapshot output if scalars are passed directly rather than as arrays.
31+
- **`text_array_snapshot` (text)**: `np.savetxt()` output is stable across NumPy versions and is the safest choice when version-portability matters.
32+
33+
As such, snapshot fixtures should ideally be used in a dependency-locked environment. At minimum, NumPy should be pinned, and after upgrading NumPy, all snapshots regenerated:
34+
35+
```shell
36+
pytest --snapshot-update
37+
```
38+
39+
To make NumPy version mismatches easier to notice, a warning will be emitted at the start of a test session if the NumPy major version differs from the one used to create the snapshots. This mechanism works by storing a `.numpy_snapshot_version` file in the `__snapshots__` directory. This file should be committed alongside snapshot files.

modflow_devtools/misc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,8 @@ def is_github_rate_limited() -> bool | None:
362362
return None
363363

364364

365-
_has_exe_cache = {}
366-
_has_pkg_cache = {}
365+
_has_exe_cache = {} # type: ignore
366+
_has_pkg_cache = {} # type: ignore
367367

368368

369369
def has_exe(exe):

modflow_devtools/snapshots.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import warnings
12
from io import BytesIO, StringIO
3+
from pathlib import Path
24
from typing import Optional, Union
35

46
from modflow_devtools.imports import import_optional_dependency
@@ -137,6 +139,47 @@ def readable_array_snapshot(snapshot, snapshot_disable):
137139

138140
# pytest config hooks
139141

142+
_NUMPY_VERSION_FILENAME = ".numpy_snapshot_version"
143+
144+
145+
def _find_snapshot_dirs(rootdir: Path) -> list:
146+
return [p for p in rootdir.rglob("__snapshots__") if p.is_dir()]
147+
148+
149+
def pytest_sessionstart(session):
150+
if np is None:
151+
return
152+
current_major = int(np.__version__.split(".")[0])
153+
rootdir = Path(session.config.rootdir)
154+
for snap_dir in _find_snapshot_dirs(rootdir):
155+
version_file = snap_dir / _NUMPY_VERSION_FILENAME
156+
if not version_file.exists():
157+
continue
158+
stored = version_file.read_text().strip()
159+
try:
160+
stored_major = int(stored.split(".")[0])
161+
except (ValueError, IndexError):
162+
continue
163+
if stored_major != current_major:
164+
warnings.warn(
165+
f"NumPy major version changed from {stored_major} to {current_major}. "
166+
"Array snapshots may no longer match. "
167+
"Regenerate them with: pytest --snapshot-update",
168+
UserWarning,
169+
stacklevel=2,
170+
)
171+
break
172+
173+
174+
def pytest_sessionfinish(session, exitstatus):
175+
if np is None:
176+
return
177+
if not getattr(session.config.option, "update_snapshots", False):
178+
return
179+
rootdir = Path(session.config.rootdir)
180+
for snap_dir in _find_snapshot_dirs(rootdir):
181+
(snap_dir / _NUMPY_VERSION_FILENAME).write_text(np.__version__ + "\n")
182+
140183

141184
def pytest_addoption(parser):
142185
parser.addoption(

0 commit comments

Comments
 (0)