Contributing
The conventions for changing GRANDlib: setting up, the checks a change must pass and how code, tests, notebooks and documentation are written.
Setting up
Install from a clone as described in Installation and requirements, then add an editable install of the package:
pip install -e . --no-deps --no-build-isolation
The checks and how to run them
python -m pytest tests/ -q # the suite
ruff check grand/ tests/ quality/ notebooks/ docs/dev/ granddb/
cd docs && make html # the documentation
python notebooks/make_notebooks.py # the notebooks
python quality/docstring_coverage.py # docstring coverage
This is the lint scope CI checks; sim2root/, examples/ and
src_outlib/ are not linted yet.
The lint ratchet
per-file-ignores in pyproject.toml lists the lint findings in code
written before linting was enforced. The list may shrink and must never
grow: new code is written clean and a change that cleans a listed file of a
rule removes that rule from its line.
Checking input
A public function checks what it is given, using grand.basis.validate
and refuses bad input at the door rather than failing deep inside or, worse,
returning a plausible wrong number. The helpers convert and check in one line
and write the message, which starts with GRANDlib: and names the function
and the argument:
from grand.basis import validate as _validate
def fit(Xants, tants, sigma=None):
where = "fit"
Xants = _validate.as_array(Xants, "Xants", where, shape=(None, 3), finite=True)
sigma = _validate.positive(_validate.as_real(sigma, "sigma", where), "sigma", where, "s")
Raise TypeError for the wrong kind of value and ValueError for a value
of the right kind but out of range or shape; warn with
grand.basis.validate.warn() for input that is suspicious but usable.
Never print and return None, never call exit() and do not use
assert for user input: Python skips asserts under -O, where their message
says nothing. Asserts remain fine for internal invariants.
Docstrings
numpydoc, on every function, including private ones. A description, then
Parameters and Returns where the function has parameters or returns
something, then Examples where an example earns its keep.
House style, which differs from the numpydoc default in one place:
Summaries are third person: “Returns the effective length”, not “Return the effective length”.
D401is disabled for this reason; do not re-enable it.Do not use the legacy
:param:/:type:/:return:fields... versionadded::and.. versionchanged::when behavior changes, with the reason.
python quality/docstring_coverage.py reports where you stand.
Tests
Write the test with the change. Test suite lists the conventions:
committed samples or built inputs instead of files in data/, properties
over stored values, seeded random draws and an entry in
tests/conftest.py for a known defect instead of a skip.
Notebooks
The notebooks are written by notebooks/make_notebooks.py, which holds
their source, executes each one and stores its outputs. A change made only in
the .ipynb is lost the next time the notebooks are rebuilt. To keep it,
edit the notebook in Jupyter as usual, then bring it back into the generator:
python notebooks/import_notebook.py notebooks/05_galactic_noise.ipynb
A new notebook is added the same way. Give it the next free number, start it
with a # NN — Title heading and keep its data paths relative to
notebooks/:
python notebooks/import_notebook.py ~/my_analysis.ipynb --name 13_my_analysis.ipynb
The script checks that every code cell compiles and that no cell uses an
absolute path such as /home/..., which would not exist on another machine.
It then writes the notebook’s block in make_notebooks.py, executes the
notebook and stores its outputs. If anything fails, it restores both files
and says why. For a new notebook, it also reminds you to add it to the list
in Tutorial notebooks. Commit make_notebooks.py and the .ipynb together.
To rebuild notebooks from the generator directly:
python notebooks/make_notebooks.py # rebuild and execute all
python notebooks/make_notebooks.py --only 03,05 # just those two
python notebooks/make_notebooks.py --check # check, writing nothing
CI fails if a committed notebook does not match the generator or does not execute.
Comment the notebooks’ code cells: they are tutorials.
Documentation
docs/source/ is the whole tree; make html builds it and it should build
with zero warnings. Prose pages carry executable examples through
.. jupyter-execute::, so an example that stops working fails the build.
Diagrams are generated too, by docs/dev/make_*_diagram.py and are
committed as SVG. Embed them so they can be opened full size:
.. image:: _static/pipeline.svg
:target: _static/pipeline.svg
:alt: what the diagram shows, for a reader who cannot see it
:width: 100%
The handbook section under docs/source/handbook/ is generated from
resources/GRANDlib_Handbook.zip by docs/dev/build_handbook.py. Do not
edit those pages; corrections go in the ERRATA table in that script.
Writing style
The documentation and the docstrings are written in American English, in plain sentences that say what the code does now. In practice:
American spelling: behavior, normalization, meters, toward.
No comma before and:
A, B and C. Where two clauses would be joined by `` and``, write two sentences.State the fact. Leave out
it is worth noting,said plainly,why it mattersand the like.Describe the current behavior, not its history: no issue numbers, no
it used to, no dates except those a reader needs. The history belongs in the changelog and the commit messages.Give the unit of every quantity and the frame of every position or direction.
python docs/dev/check_style.py checks the pages for the first three. The
lint job runs it on every push.
Editing source with scripts
When editing many docstrings or signatures with a script, use ast to
find each line range and edit only within it; a regular expression over
Python source can land inside a loop or a string. Check every file you
touched with python -c "import ast; ast.parse(open(f).read())" before
committing.
The sim2root converters
sim2root/README.md documents the converters in detail. Their code is not
yet linted and the tests check them end to end only, by converting the
committed samples (tests/sim2root/). After a change, convert a sample and
read the result back with grand.dataio. Common/raw_root_trees.py
defines the RawRoot format separately from grand/dataio, so a field added
to one must be added to the other. Edit the files under sim2root/, not
the stale copy in src_outlib/, which nothing imports.
Branches and merging
Work goes to dev-next through pull requests. Before merging a branch
that touches the data format or the simulation, run
python quality/premerge_check.py <branch>: it finds two names for one
quantity and two implementations of one thing, that a clean merge would not
reveal.
Commits
Say what changed and why, with measurements where there are any. A commit that corrects an earlier commit or a document says so and gives the number.