Contributing¶
See the repository-root CONTRIBUTING.md for ground rules, PR expectations,
and how to report bugs. This page covers the detailed dev workflow, repository
layout, and step-by-step instructions for adding a check or schema.
Dev workflow¶
This project uses uv for everything —
environment management, dependencies, running commands, and building.
uv sync # install/lock dependencies
uv run pytest # run the test suite
uv run ruff check . # lint
uv run ruff format --check . # verify formatting (uv run ruff format . to fix)
uv run mypy src # type-check the package
uv build # build sdist + wheel into dist/
All four checks (pytest, ruff check, ruff format --check, mypy) and
uv build must pass before a change is considered done. This matches
exactly what CI (and the milestone acceptance criteria used to build this
package) run.
For a full release checklist (including the isolated wheel smoke test and
package/schema version independence checks), see the repository-root
CONTRIBUTING.md (“Release checklist”).
Repository layout¶
src/cp_anndata_validator/
loading.py, sampling.py, profiles.py # layer 1: read AnnData safely
schema/ # layer 2: schema model, loader, alias resolution
models/ # Issue, Report, and friends
checks/ # one module per validation category
orchestrator.py # runs checks, assembles the Report
api.py # validate() — the one integration point
reporting/ # console / JSON / HTML renderers
cli/ # Typer wiring only; delegates to api.py + reporting
tests/
fixtures/synthetic.py # shared synthetic AnnData builders
test_*.py # one test module per source module, mirroring the tree
docs/ # this documentation set
Adding a check¶
- Pick (or create) the
checks/<category>.pymodule matching your validation category. - Write a function taking a
CheckContext(seechecks/registry.py) and returninglist[Issue]. Never print or raise for expected validation failures — returnIssueobjects instead; the orchestrator already isolates unexpected exceptions intoENGINE001, so you don't need defensivetry/exceptinside a check for that. - Register it with
@register_check(name="...", category=Category.X). - Add a new rule code, or reuse an existing one if the check refines an existing meaning. New codes are never reused for a different meaning later — pick a fresh number in the right category's block. Document it in Rule catalogue.
- Add a test module
tests/test_checks_<category>.pyexercising both the passing and failing paths, using or extendingtests/fixtures/synthetic.py. - If the check needs new
.uns/.obs/.varconventions, document them in AnnData mapping.
Because checks/__init__.py imports every check submodule (registering
them as an import side effect), a new check module must be added to that
file's import list to be picked up by orchestrator.run_checks() and, by
extension, validate() and the CLI.
Adding a schema¶
- Add a new YAML file under
schema/resources/, following the shape described in Custom schema format. - Register its filename in
schema/loader.py's builtin-schema lookup socp-validate schema list/showandload_schema("your-name")find it. - Add a test in
tests/test_schema_loader.py(or a new module) confirming it loads and resolves against a representative synthetic fixture. - If the schema is derived from a specific external convention (like
jump-cp), document that provenance in a newdocs/<name>-derivation.mdfile, citing primary sources — see JUMP compatibility derivation as a template.
The argv shim¶
typer/click cannot mix a bare top-level positional argument with
subcommands in the same command tree. Since the CLI needs both
cp-validate experiment.h5ad (bare path) and cp-validate schema list
(subcommand) to work, cli/app.py exposes an explicit validate
subcommand internally, and the main() console-script entry point
rewrites sys.argv to insert "validate" before Click parses it, whenever
the first token isn't already a known subcommand, a help/version flag, or
an option. See apply_argv_shim() in
src/cp_anndata_validator/cli/app.py and its dedicated tests in
tests/test_cli.py.
Testing conventions¶
- Synthetic fixtures live in
tests/fixtures/synthetic.pyand coversingle-cell,well, andtreatmentprofiles, in both dense and sparse form, with realistic.unsmetadata. Prefer extending these over building bespoke AnnData objects per test. - Loading/sampling tests use spy objects to assert that no full-matrix
materialization (
.toarray(), full[:]slicing) occurs for sparse or backed input — keep that guarantee intact when touchingloading.pyorsampling.py. - Check registry state is global by design (for
orchestrator.run_checks()to work simply); tests that register fake checks must restore the registry afterward viachecks/registry.py'sclear_registry()/restore_registry()helpers (seetests/test_orchestrator.py).