Contributing#
Setup#
$ git clone https://github.com/Quantum-ARISE-Acad/qa-materials-cli
$ cd qa-materials-cli
$ python -m venv .venv && .venv/bin/pip install -e ".[dev]"
$ .venv/bin/matmelt --help
Do not install the science stack in the dev environment. That is not an oversight — the entire test suite must pass without SMACT, PyXtal, MACE or AiiDA, because that is what proves the base install is light and the lazy imports are really lazy. The science-dependent tests skip cleanly when the packages are absent, and the AiiDA-touching tests fake the modules outright.
The documentation toolchain lives in its own environment so it can never leak into a user install:
$ python -m venv .venv-docs
$ .venv-docs/bin/pip install --upgrade pip # --group needs pip >= 25.1
$ .venv-docs/bin/pip install --group docs -e .
The check battery#
Every change ends with all four green:
$ .venv/bin/ruff check src tests docs/diagrams
$ .venv/bin/ruff format --check src tests docs/diagrams
$ .venv/bin/mypy src
$ .venv/bin/pytest
and, for any docs change:
$ .venv-docs/bin/sphinx-build -W -b html docs docs/_build/html
-W turns warnings into errors on purpose: a broken cross-reference or an
unparseable docstring must fail rather than land quietly on the site.
A change in behaviour ships with the test that proves it. CI runs the same checks, plus a security job (bandit, pip-audit) and a build job that installs the wheel in a clean environment and runs the CLI from outside the source tree.
How the tests are organised#
tests/unit/ mirrors src/, and tests/integration/ holds the end-to-end
proofs. Four files are worth knowing before you change anything:
File |
Why it matters |
|---|---|
|
|
|
The ten conformance checks every backend must pass |
|
The whole tool below the CLI, on fakes only. This stays green. |
|
Checks the published docs against the real code |
The last one runs in both directions: every documented command, flag and config
key must exist, and every config field, stage option, backend option and
material state must be documented. A new option fails a test until
reference/config.md covers it. That is intentional — it is cheaper than
discovering an undocumented option a year later.
FakeBackend fabricates each declared output, which is what lets a stage’s full
lifecycle run without compute. If you add a stage, its tests almost certainly
belong on that rail rather than on real science.
Adding things#
A stage. Write <name>_runner.py first — standard library plus its science
package, --help-runnable by hand, exiting 3 with the exact pip install
command when its package is missing. Then the thin Stage class, then
register, FUNNEL_ORDER and STAGE_OUTPUT_STATE. An AST guard will fail if
the runner imports matmelt. See The two contracts.
A backend. Subclass ComputeBackend, implement the six methods, register
the kind, and add one branch to the conformance fixture.
A config key. config/schema.py, then reference/config.md.
A diagram. Scripts live in docs/diagrams/; the palette is encoded once in
_palette.py so every diagram shares one vocabulary.
$ python docs/diagrams/render.py # regenerate .dot and .svg
$ python docs/diagrams/render.py --check # what CI asks: is anything stale?
Rendered SVGs are committed, so building the site never needs Graphviz. Guards check that the committed artifacts match what the scripts produce now, that both themes exist and differ, and that every page embedding a diagram uses both themes with the diagram’s own alt text.
Conventions#
Docstrings are NumPy style, and parameter types are never written in them — they come from the annotations, so type information has one source.
Attributesentries do carry types.All output through
ui/. Noprint()incore/.Commands return
int, they do not callsys.exit.Optional means unknown, never zero.
Don’t add a dependency without asking first.
One upstream trap, recorded in docs/conf.py because it has bitten twice: in a
function returning None, do not end a docstring with a Notes section
following a multi-line Raises. sphinx-autodoc-typehints misplaces the
injected return-type field and the build fails confusingly. Put that content in
the extended description instead.