Development environment and checks#

Use Python 3.14 and uv for development. Dependencies are declared in pyproject.toml and resolved in uv.lock. From a source checkout, install the development and documentation dependencies with:

uv sync --group docs

The default development group includes the test, formatting, linting, and type checking tools, as well as Qt for the GUI tests. Run commands through uv run to use the project environment.

Check a change#

One command runs the whole definition-of-done gate, in order, exactly as Linux CI does:

uv run poe check

It runs ruff check --fix, ruff format, ruff check (what autofix could not fix), pyright, codespell, poe changelog-check (the pending release notes under changelog.d/ and the changelog’s structure), and finally poe cov: the test suite on every core (pytest-xdist), doctests included, measuring statement and branch coverage of the whole croak package, GUI included, and failing below 100%. The XML report is written to build/coverage/coverage.xml.

GitHub Actions uses Python 3.14 and the committed lockfile on ubuntu-latest, windows-latest, and macos-latest. Linux runs the full gate above, including the 100% statement and branch coverage requirement, and uploads its coverage report to Codecov. Windows and macOS run uv run poe test: the same complete suite, including GUI tests and doctests, without coverage instrumentation. Lint, formatting, type, spelling, and changelog checks run once, on Linux. Linux installs Qt system libraries and runs under Xvfb; Windows and macOS run their tests directly. Qt uses its offscreen backend on all three platforms. A failure on one platform leaves the others running so all failures can be inspected together. Each test log lists the 20 slowest test phases to help diagnose differences in runtime. The final lint-type-test check succeeds only when every platform passes, preserving the check name required by the main branch’s repository rules.

The workflow runs for pull requests and pushes to main or master, and can also be started manually from the Actions tab. CI fails if the gate’s lint autofixes or formatting leave a tracked-file diff.

For focused work run single files serially, without coverage:

uv run pytest tests/test_preprocess.py
uv run poe test          # the whole suite on every core, no coverage

The suite is strict, and each rule is set in pyproject.toml:

  • Warnings are errors. A test that expects a warning asserts it with pytest.warns; a warning nobody expects is a bug to fix, not to filter.

  • Random order. pytest-randomly shuffles the tests (the seed is printed; rerun with -p randomly --randomly-seed=N), and xdist spreads modules over workers, so tests must not depend on each other or share writable files. Write under tmp_path.

  • Doctests run. Every Examples section is executed, so examples must be deterministic (seed every random generator).

  • Full coverage. New code arrives with tests that exercise its branches and error paths, with assertions about its behaviour. Code that cannot be reached is removed rather than excluded; # pragma: no cover is reserved for code that cannot run under test at all (an if __name__ == "__main__": block, a guarded optional import) and carries its reason on the same line. The threshold and the exclusions are not lowered to make a change pass.

Keep orchestration regressions small and separate from numerical convergence tests. For example, the two-phase LM polish tests use a 32-point SHG pulse and 17 delays: live snapshots show the delay offset held during the first phase, then the joint phase recovers it and reduces the remaining trace error. The separate thickness-fitting tests exercise slab-thickness recovery. Reserve that larger dispersive convergence problem for those dedicated tests, and use the compact example to check the two-phase sequence.

GUI tests live in tests/gui/ and run offscreen through pytest-qt; the shared tests/conftest.py points QSettings at a per-test directory for them. The evo extra (evosax) is part of the development group so the cma-es retriever is tested.

Tests live in tests/ and follow the package’s domain structure. Numerical tests should state their tolerances and check physical or mathematical invariants as well as individual values. Document public behaviour in NumPy-style docstrings and in the manual, and record notable changes in a changelog.d/ fragment (see releasing).

See numerical validation for model and gradient checks, and building the documentation for executed tutorial caches.