Skip to content

Make isschecker a package - #13

Merged
xylar merged 13 commits into
ismip:mainfrom
xylar:make-package
Jul 25, 2026
Merged

Make isschecker a package#13
xylar merged 13 commits into
ismip:mainfrom
xylar:make-package

Conversation

@xylar

@xylar xylar commented Jul 25, 2026

Copy link
Copy Markdown
Member

Package the compliance checker so it installs cleanly and runs from anywhere

Summary

The checker was a standalone script that had to be run from the repository root, because it resolved its criteria CSVs relative to the working directory. This PR turns it into an installed package — ismip7-compliance-checker on the PATH, criteria and grid definitions bundled as package data, no assumptions about where it is run from — and then does the work that makes that safe to hand to ice sheet modelers: bounded dependency versions, an install that cannot silently pull from PyPI, and CI that tests the package it ships rather than the source tree.

The conversion itself is @aaschwanden's work (#9). This PR carries his commits and replaces the earlier PR for it, adding the version constraints, the packaging fixes that only surface once the checker really is run from elsewhere, and the tests to keep them fixed.

The requirement driving the rest is that a modeler running the checker on their own machine and OS should get the same answer we do from the same files. That is now tested rather than intended: the log produced under python 3.11 / numpy 2.1 / pandas 2.2 / xarray 2025.1.2 is byte-identical to the log produced under python 3.14 / numpy 2.5 / pandas 3.0 / xarray 2026.7.

What changes for users

Before After
Install conda env create -f isschecker_env.yml same, then python -m pip install --no-deps --no-build-isolation .
Run the checker python compliance_checker.py ..., from the repository root only ismip7-compliance-checker ... (or python -m isschecker ...), from any directory
Generate test files python generate/generate_test_files.py ... ismip7-generate-test-files ...
Import name import compliance_checker import isschecker
Criteria files conventions/ISMIP7_variable_request.csv, experiments_ismip7.csv, read relative to the working directory bundled package data under isschecker/data/, found wherever the package is installed
Log provenance line Commit Number: <git log in your working directory> isschecker version: 0.1.0

Deliberately unchanged, because modelers depend on them: the ismip7-compliance-checker command name, the compliance_checker_log.txt filename and its contents, and all check logic. The version is not bumped — no version of this software has ever been tagged, so 0.1.0 still stands.

The package

compliance_checker.py becomes isschecker/checker.py with a thin __init__.py re-exporting main, run_checker and __version__, plus a __main__.py so python -m isschecker works. The criteria CSVs and the grid definitions move into isschecker/data/ and are located with importlib.resources, which is what frees the checker from the working directory. pyproject.toml declares two console scripts, ismip7-compliance-checker and ismip7-generate-test-files.

The import package is isschecker, matching the distribution name rather than the compliance_checker the script grew up as. That is not just tidiness: compliance_checker is precisely the import name of IOOS compliance-checker, a CF/ACDD netCDF compliance tool published on both conda-forge and PyPI and squarely in our users' domain. Installing both in one environment would give whichever came first on the path.

The test-file generator moves into the package too (isschecker/generate.py, with the grid definitions as package data). It is a test dependency, and while it sat outside the package the suite loaded it by file path after inserting the repository root into sys.path — which meant the tests exercised the source tree no matter what was installed, and is exactly what let the defects listed below go unnoticed. As a bonus, modelers now get the generator, which is useful for producing sample output.

Dependency constraints

isschecker_env.yml previously pinned every package to an exact version, which is unmaintainable and eventually fails to solve on some platforms; the conversion commit dropped constraints entirely, which reproduces nothing. It now carries bounded ranges, and CI verifies both ends of every one of them.

Package Constraint Why
python >=3.11,<3.15 str | None annotations need 3.10; 3.10 is EOL in Oct 2026
numpy >=2.1,<3 what recent pandas and xarray are built against
pandas >=2.2,<4 reads the criteria CSVs; 3.0 changed the default string dtype, so both lines are exercised
xarray >=2025.1.2,<2027 hard requirement: xarray.coders.CFDatetimeCoder, which the time checks use, only became public API in 2025.01.1 (the release that deprecated bare use_cftime); 2025.01.2 added the non-nanosecond datetime decoding the code is written against
cftime >=1.6.4,<2 date arithmetic in the start/end/duration checks
netCDF4 >=1.7,<2 _FillValue checks compare against netCDF4.default_fillvals
tqdm >=4.66 progress bar only; never reaches the log

Upper bounds exclude the next major — or, for xarray's CalVer, the next year — so raising a ceiling is a deliberate act with a CI run behind it. That is the release line that bit us before, via the use_cftime deprecation.

Two additions that have nothing to do with versions but everything to do with reproducibility: nodefaults in the channel list, so a modeler whose ~/.condarc includes defaults cannot silently solve against different builds; and setuptools, which conda-forge python no longer vendors and without which the documented install fails outright.

pyproject.toml mirrors these ranges exactly. Every dependency is on PyPI, so nothing had to be dropped from the metadata. It also gains PEP 639 license metadata, project URLs, and a requires-python that matches the environment file.

Install instructions

pip install . is the wrong instruction for a conda environment: pip is free to satisfy dependencies from PyPI, and a PyPI netCDF4 wheel brings its own copy of the netCDF C library, which is a direct route to two people getting different results from the same files. Build isolation is a second, independent path to PyPI, since it downloads a build backend even when nothing else needs fetching. The README documents python -m pip install --no-deps --no-build-isolation ., explains why, and mentions --no-index for anyone who wants a hard failure on any network access. The dependency table above is in the README as well, so the docs and the environment file cannot drift apart.

Defects fixed along the way

Each of these is invisible while the checker is run from its own checkout, and breaks the moment it is not:

  • python -m compliance_checker was documented but raised. The conversion deleted the if __name__ == "__main__" block without adding a __main__.py.
  • The log's provenance line described the wrong thing. _get_commit_number() ran git log in the process's working directory, so a modeler running the checker from their data directory would get the commit of whatever unrelated repository they happened to be standing in, or a placeholder. The header now reports the installed version, with the git commit appended only when running from a checkout. Incidentally, the old --pretty=format:'%h' was passed through str.split(), so the shell quotes survived into the recorded string.
  • The generator wrote into site-packages. create_netcdf_file defaulted its output to Path(__file__).parent.parent / 'Models', which for an installed package is a Models directory inside site-packages. It now defaults to ./Models, matching the checker's own --source-path default.
  • get_available_grids ignored its conventions_dir argument, always reading a hard-coded repository path, so --conventions-dir silently did nothing for grids.
  • All 1516 lines lived in __init__.py, so importing the package pulled in the whole CLI.
  • isschecker/data/ had no __init__.py, so it resolved as a namespace package and importlib.resources.files() returned a MultiplexedPath spanning every same-named portion on the path.
  • Synthetic test data was unseeded, using the legacy global np.random. Every run produced different values — the wrong default for this project, and one unlucky draw away from a value close enough to a min/max bound to flip a numerical check. Now drawn from a seeded np.random.Generator, with --seed.

Also added: --version, so there is something exact to quote in a problem report.

Testing

CI previously created the environment and ran pytest from the checkout, never installing the package; combined with the sys.path insertion in the tests, it tested the source tree. Every defect above would have passed it indefinitely. CI now installs with the same flags the README gives modelers, and runs the entry-point smoke checks and pytest from runner.temp, outside the checkout, so the tests import what was installed. The matrix covers {ubuntu, macos} × {latest ranges, pinned floors from ci/isschecker_env_floor.yml}, which is what makes both ends of every range verified rather than asserted. Environment caching is enabled only for the floor job, since caching the "latest" solve would hide the day a new release breaks the open end of a range — the thing that job exists to catch.

New in tests/test_golden_log.py: the checker runs over a fixed, seeded dataset (27 files, scalars and x,y,t, all five check categories) and its log is compared line by line against a stored reference, with the version, date, and source path masked out. This is what turns "results should agree across machines" into something CI can check, and it is the payoff for seeding the generator — without fixed input data there is nothing stable to compare. When a change is meant to change the log, ISSCHECKER_UPDATE_GOLDEN_LOG=1 pytest tests/test_golden_log.py regenerates the reference for review alongside it.

Verification

  • Both ends of every range were solved and the suite run in each: floor (python 3.11.15, numpy 2.1.3, pandas 2.2.3, xarray 2025.1.2, cftime 1.6.4, netCDF4 1.7.4, tqdm 4.66.6) and latest (python 3.14.6, numpy 2.5.1, pandas 3.0.3, xarray 2026.7.0, cftime 1.6.5, netCDF4 1.7.4, tqdm 4.69.0). 7 passed in both, run from outside the checkout against the installed package.
  • Those two environments produce identical logs, which is the reproducibility claim this PR exists to support.
  • End to end from an empty directory with no checkout in sight: ismip7-generate-test-files wrote files under ./Models, and ismip7-compliance-checker reported no errors with the version in the log header.
  • The golden test is not vacuous: reinstalling with a single log message reworded fails with exactly that diff.
  • The CI arrangement earns its keep: with the criteria CSV temporarily resolved relative to the working directory — the bug class this conversion exists to eliminate — pytest from the checkout reports 6 passed while pytest from a temporary directory reports 6 errors.
  • The wheel builds with no setuptools deprecation warnings, carries License-Expression: MIT and the six Requires-Dist lines, and bundles the criteria CSVs and grid definitions.

Not verified: the workflow file itself, which can only run on push; in particular, whether the floor pins solve on macos-arm64 is untested. I also did not install IOOS compliance-checker alongside ours to demonstrate the collision, since it would drag conflicting pins into the environment; the fix is verified structurally instead (top_level.txt is isschecker, and import compliance_checker now raises).

Notes for review

  • One gotcha worth knowing while reviewing locally: setuptools reuses a stale build/ directory, so after the rename an install can keep resolving import compliance_checker from files that no longer exist in the tree. rm -rf build fixes it; the README says so.
  • The tests import the installed package, so during development use the editable install (-e) or edits will not affect a test run until you reinstall.
  • Each commit is independently reviewable and leaves the suite green; the commit messages carry the per-change rationale and verification.
  • Follow-up worth doing separately: publish isschecker on conda-forge so modelers can conda create -n isschecker isschecker=X.Y and skip the pip step entirely. That, with a tag, is the real fix for cross-machine reproducibility; the ranges here are the best available substitute until then.

Closes #9

aaschwanden and others added 13 commits July 25, 2026 18:25
  `pip install .` works and the checker runs from any directory.

  - Move compliance_checker.py to compliance_checker/__init__.py and add
    __main__.py (enables `python -m compliance_checker`).
  - Move the runtime CSVs into compliance_checker/data/ as the single
    source of truth; load them via importlib.resources instead of paths
    relative to the working directory. Drops the workdir arg on
    run_checker().
  - Add pyproject.toml declaring the package, data files, dependencies,
    and the `ismip7-compliance-checker` console script.
  - Repoint the test-file generator at the packaged CSV and the top-level
    gdfs/ directory; update tests to use the bundled defaults.
  - Update READMEs and .gitignore.
Andy's package conversion dropped all version constraints from the
environment file.  Unconstrained solves mean modelers can get arbitrarily
different results from ours, so restore bounds -- but as tested ranges
rather than the previous exact pins, which were unmaintainable and could
fail to solve on some platforms.

The floors are the oldest versions the test suite is verified against,
not the oldest that might work:

  xarray >=2025.1.2  `xarray.coders.CFDatetimeCoder`, which the checker
                     uses to decode times, only became public API in
                     2025.1.1 (the release that deprecated bare
                     `use_cftime`); 2025.1.2 added the non-nanosecond
                     datetime decoding the time checks are written
                     against.  Anything older raises AttributeError.
  cftime >=1.6.4     date arithmetic in the start/end/duration checks.
  pandas >=2.2       reads the criteria CSVs; 3.0 changed the default
                     string dtype, so both lines are exercised in CI.
  netcdf4 >=1.7      `_FillValue` checks compare against
                     `netCDF4.default_fillvals`.
  numpy >=2.1        what pandas 3 and recent xarray are built against.
  python >=3.11      `str | None` annotations need 3.10; 3.10 is EOL in
                     Oct 2026.

Upper bounds exclude the next major (or, for xarray's CalVer, the next
year), so raising a ceiling is a deliberate act with a CI run behind it.
tqdm only drives the progress bar and never reaches the log, so it gets a
bare floor.

Two additions unrelated to versions:

- `nodefaults` in the channel list, so a modeler whose ~/.condarc
  includes `defaults` cannot silently solve against different builds.
- setuptools, which conda-forge python no longer vendors.  Without it
  `pip install --no-build-isolation .` fails outright; >=77 for PEP 639
  license metadata.

Verified by solving both ends of every range and running the suite in
each: floor (python 3.11.15, numpy 2.1.3, pandas 2.2.3, xarray 2025.1.2,
cftime 1.6.4, netCDF4 1.7.4, tqdm 4.66.6) and latest (python 3.14.6,
numpy 2.5.1, pandas 3.0.3, xarray 2026.7.0, cftime 1.6.5, netCDF4 1.7.4,
tqdm 4.69.0).  6 passed in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dependency list carried no constraints at all, so the metadata made no
statement about what the checker actually needs.  Every dependency is on
PyPI (numpy, pandas, xarray, cftime, netCDF4, tqdm), so the list can mirror
isschecker_env.yml exactly -- nothing has to be dropped.  Because the
supported install is `--no-deps` into a conda environment, pip never
resolves these; they exist so the metadata is honest, and a comment says so.

Also in the metadata:

- `requires-python` now matches the environment file (>=3.11,<3.15) instead
  of claiming 3.10 support.
- PEP 639 license: `license = "MIT"` plus `license-files`, replacing the
  `{ file = "LICENSE" }` table.  That table is deprecated from setuptools 77
  on and inlined the entire MIT text into the wheel METADATA; the wheel now
  carries `License-Expression: MIT`.
- Homepage/Issues URLs and a few classifiers.
- build-system requires setuptools>=77 for the license handling above, which
  is why isschecker_env.yml asks for the same floor.

The version stays static here; a later commit exposes it to the code via
importlib.metadata rather than the reverse, keeping pyproject.toml the single
source.  No bump, since no version of this software has ever been tagged.

Verified with `pip wheel --no-deps --no-build-isolation .`: builds with no
deprecation warnings, emits the six Requires-Dist lines and
License-Expression: MIT, and still bundles both criteria CSVs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`pip install .` is the wrong instruction for a conda environment: pip is
free to satisfy dependencies from PyPI, and a PyPI `netCDF4` wheel brings
its own netCDF C library, which is a direct route to two people getting
different results from the same files.  Build isolation is a second,
independent path to PyPI -- it downloads a build backend even when nothing
else needs fetching.  Document
`python -m pip install --no-deps --no-build-isolation .` instead, say why,
and mention `--no-index` for anyone who wants a hard failure on any network
access.  `python -m pip` rather than `pip` so it is the environment's pip
and not whatever is first on PATH.

The editable-install line loses `[test]`, which does nothing under
`--no-deps`; pytest comes from the conda environment.

Replace the free-text dependency line, which pinned versions that no longer
match anything, with a table of the real constraints and the reason each is
bounded, and ask for `conda list` output in problem reports.

Drop the claim that `python -m compliance_checker` is equivalent to the
console script: there is no `__main__.py`, so it raises.  The next commit
adds one and restores the claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README documented `python -m compliance_checker` as equivalent to the
console script, but the conversion deleted the old
`if __name__ == "__main__"` block without adding a `__main__.py`, so the
documented command raised "No module named compliance_checker.__main__".
Add the module and restore the README claim.

Also expose the installed version.  `__version__` comes from
importlib.metadata, so pyproject.toml stays the single source and there is
nothing to keep in sync; it falls back to "unknown" when the package is
imported from an uninstalled source tree.  `--version` gives modelers
something exact to quote in a problem report, and the next commit puts the
same string in the log header.

Verified from outside the checkout: `python -m compliance_checker --version`
and `ismip7-compliance-checker --version` both report 0.1.0; suite still
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The conversion put all 1516 lines of the checker into
`compliance_checker/__init__.py`, so merely importing the package pulled in
the whole CLI, and there was nowhere to put package-level API without
adding to that file.  Move the code to `checker.py` (a pure `git mv`, minus
the now-meaningless shebang and executable bit) and leave `__init__.py` as
a five-line re-export of the public API: `main`, `run_checker`,
`__version__`.  The console-script and `python -m` entry points are
unchanged because they go through that re-export.  Tests now name the
module they exercise (`compliance_checker.checker`) rather than reaching
into the package namespace for private constants.

`compliance_checker/data/` had no `__init__.py`, so it resolved as a
namespace package and `resources.files()` returned a `MultiplexedPath`
covering every directory of that name on the path.  It worked, but it would
pick up a same-named namespace portion from any other distribution.  Adding
`__init__.py` makes it a regular package (verified: `files()` now returns a
`PosixPath`).

Data access also goes through one helper, `_read_data_csv`, which uses
`files(f"{__package__}.data")` -- so it follows the package rename in the
next commit without edits -- and `as_file`, the documented way to get a
usable path out of a resource.  `_load_experiments_csv` no longer takes a
path assembled by its caller, and the failure message for a missing
criteria file now points at a packaging problem, which is what it would
actually mean.

Suite green in both the floor and latest environments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The distribution is named `isschecker` but the import package was
`compliance_checker` -- which is precisely the import name of IOOS
compliance-checker, a CF/ACDD netCDF compliance tool published on both
conda-forge and PyPI and squarely in our users' domain.  Installing both in
one environment gives whichever `compliance_checker` comes first on the
path, in an order nobody controls.  Rename ours to match its distribution
name, which removes the collision and the name mismatch at once.

Deliberately unchanged, because modelers depend on them:

- the console script `ismip7-compliance-checker`;
- the log filename `compliance_checker_log.txt`;
- the `_run_compliance_checker` / `_check_*` internals.

`python -m compliance_checker` becomes `python -m isschecker` in the README.
The data lookup needed no edit: it derives the data package from
`__package__`.

While verifying this, an install kept resolving `import compliance_checker`
even after the rename.  The cause was setuptools reusing a stale `build/`
directory, which still held the old package tree and shipped it inside the
new wheel.  Documented in the README development notes, since anyone with a
pre-rename checkout will hit it.

Verified with a clean build: the wheel and installed RECORD contain no
`compliance_checker` entries, `import compliance_checker` now raises
ModuleNotFoundError, and both entry points report 0.1.0 from outside the
checkout.  Suite green in the floor and latest environments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_get_commit_number()` ran `git log -n 1` in the process's working
directory.  That was defensible when the checker was a script run from its
own checkout, but the whole point of the packaging work is that modelers now
run it from their data directories -- where the header would report the
commit of whatever unrelated repository they happened to be standing in, or
print "Is there a .git directory here?" and record a placeholder.  Either
way the log's provenance line described something other than the checker.

The header now reads `isschecker version: <version>`, taken from
importlib.metadata.  When the package is being run from a checkout (source
tree or editable install) the short git commit is appended, so our own
development runs stay traceable to a commit.  The lookup is keyed to the
directory of this file rather than the working directory, which is what
makes it describe the checker.

Incidental fixes from the rewrite: `git log --pretty=format:'%h'` was passed
through `str.split()`, so the shell quotes survived into the recorded string;
git's stderr is captured instead of leaking into the checker's output; and a
missing git binary is handled rather than caught by a bare `except
Exception`.  `run_checker`'s `commit_num` argument becomes `version`.

Verified both paths: installed non-editable with the working directory
inside an unrelated git repository reports `0.1.0`, and a source-tree run
reports `0.1.0 (git df397e9)`, matching `git log -1`.  Suite green in the
floor and latest environments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tests depend on `generate/generate_test_files.py`, but it sat outside
the package and was loaded by file path after inserting the repository root
into `sys.path`.  So the suite exercised the source tree no matter what was
installed, which is precisely the arrangement that let the packaging bugs in
the preceding commits go unnoticed.  Move the generator to
`isschecker/generate.py` and the grid definitions to `isschecker/data/gdfs/`
(48 KB), add a `ismip7-generate-test-files` console script, and let the
tests import `isschecker` like any other consumer.  Modelers get the
generator for free, which is useful in its own right for producing sample
output.

Two path bugs fall out of the move:

- `create_netcdf_file` defaulted its output to `Path(__file__).parent.parent
  / 'Models'`, which for an installed package is a `Models` directory inside
  site-packages.  It now defaults to `./Models` in the working directory,
  matching the checker's own `--source-path` default.
- `get_available_grids` took a `conventions_dir` argument and then ignored
  it, always reading a hard-coded repository path, so `--conventions-dir`
  silently did nothing for grids.  Grid definitions now resolve under
  whichever conventions directory is in effect, which is also what makes
  them work as package data.

`generate/README.md` becomes `docs/generating_test_files.md`, since there is
no longer a `generate/` directory.

Verified with the package installed and no checkout in sight: from an empty
temporary directory, `ismip7-generate-test-files --grid GrIS_16000m
--scenario historical --start-year 2013 --nyears 2 --scalars` wrote 10 files
under `./Models`, and `ismip7-compliance-checker` then reported
"Successfully verified with no errors" with `isschecker version: 0.1.0` in
the header.  Suite green in the floor and latest environments, and when run
from outside the checkout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`generate_synthetic_data` used the legacy global `np.random.uniform`, so
every invocation produced different values.  For a project whose goal is
that everyone gets the same answer from the same files, test data that
changes on every run is the wrong default, and it leaves the suite one
unlucky draw away from a value close enough to a min/max bound to flip a
numerical check.

A `np.random.Generator` is created once per `create_netcdf_file` call from a
`seed` argument (default 0) and threaded through to each draw, so variables
within a run still differ from one another while the run as a whole is
reproducible.  `create_multiple_files` offsets the seed per file for the same
reason.  `--seed` exposes it on the command line.

Verified: two runs with the default seed produce identical values across all
10 generated files, `--seed 1` produces different ones, and variables within
a run remain distinct.  Suite green in the floor and latest environments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI created the environment and ran pytest from the checkout, never
installing the package.  Combined with the tests' `sys.path` insertion, that
meant it tested the source tree: the missing `__main__.py`, the working-
directory git lookup and every other packaging defect fixed in this series
would have passed CI indefinitely.

The workflow now installs with the flags the README gives modelers, and runs
both the entry-point smoke checks and pytest from `runner.temp`, outside the
checkout, so the tests import what was installed.

The matrix covers ubuntu and macos against two environments: the ranges in
isschecker_env.yml solved fresh, and `ci/isschecker_env_floor.yml`, which
pins every one of those floors exactly.  Both ends of every range are
therefore verified rather than asserted, which is what the ranges are worth
as a reproducibility claim.  Environment caching is enabled only for the
floor job -- caching the "latest" solve would hide the day a new release
breaks the open end of a range, which is the thing that job exists to catch.
`micromamba list` runs before the tests so a failing job says which versions
it was actually using.

Verified that the arrangement earns its keep: with the criteria CSV
temporarily resolved relative to the working directory -- the bug class this
whole conversion was meant to eliminate -- pytest from the checkout reports 6
passed, while pytest from a temporary directory reports 6 errors.  Restored,
both environments are green from outside the checkout.

The workflow itself can only be verified by pushing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Version ranges make it likely that a modeler's log matches ours; nothing so
far made it checked.  The log file is the checker's deliverable, so this test
runs the checker over a fixed, seeded dataset -- scalars and x,y,t together,
27 files, all five check categories -- and compares the result line by line
against a stored reference, printing a unified diff on failure.

Masked out: the version line, the date line, and the source path, since all
three are expected to differ per run and none of them is a regression.  The
reference is stored already masked, so it reads as what it is and masking it
again is a no-op.  When a change to the checker is meant to change the log,
`ISSCHECKER_UPDATE_GOLDEN_LOG=1 pytest tests/test_golden_log.py` rewrites the
reference for review alongside the change; the README says so.

This is the payoff for the seeded generator in the preceding commit: without
fixed input data there is nothing stable to compare.

Two things verified, in that order.  The test is not vacuous: reinstalling
with one log message reworded (`The unit is correct` -> `Unit is correct`)
fails with exactly that diff.  And the claim it exists to defend holds right
now -- the reference generated under python 3.14 / numpy 2.5 / pandas 3.0 /
xarray 2026.7 matches, line for line, the log produced under python 3.11 /
numpy 2.1 / pandas 2.2 / xarray 2025.1.2.  Both ends of every supported range
produce the same log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`pip wheel .` and a stray `.venv/` are both easy to leave lying around while
testing the install instructions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xylar

xylar commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

@hgoelzer, as nice as it would be to have your review, I'm going to go ahead with this since you're away.

@aaschwanden, thanks for getting the ball rolling!

@xylar
xylar merged commit 9680e50 into ismip:main Jul 25, 2026
4 checks passed
@xylar
xylar deleted the make-package branch July 25, 2026 16:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants