Development¶
This page defines the conventions for contributing Python code to Qanary. The
rules below apply to production code in src/qanary and should be followed in
new code and when modifying existing code.
Getting set up¶
git clone https://gitlab.com/squad-lab/qanary.git
cd qanary
uv sync --extra dev
uv run pre-commit install # once per clone
The dev extra carries pre-commit, Ruff, and the test tools. Everyday
commands:
Task |
Command |
|---|---|
Run the tests |
|
Tests with coverage |
|
Lint (includes import sorting) |
|
Format |
|
Build the docs |
|
Ruff’s configuration lives in pyproject.toml, and the pre-commit hooks use it
directly, so a local run and CI agree. Import sorting comes from ruff’s I
rules under ruff check – ruff format does not sort imports.
Public and internal API¶
Each public implementation module declares __all__. Anything not listed there
is internal and is prefixed with an underscore, including across module
boundaries: qanary.sweep imports _arm_instruments, _fetch_results, and
_buffered_sweep_progress_info from qanary.buffered.sweep. They stay private
despite the cross-module import because none of them is meant for measurement
scripts. Package __init__.py files are the exception: the top-level package
installs Qanary’s arithmetic operators on QCoDeS parameters, while
qanary.buffered only marks the subpackage.
When adding a callable, decide which side of that line it falls on. If it is
part of the measurement-script API, add it to __all__; otherwise prefix it.
Renaming a public name later is a breaking change, so err towards private.
Commits and releases¶
Releases are cut by semantic-release from the commit messages, so commit
subjects follow Conventional Commits:
fix: continue sweeps through transient Zarr errors
feat: add segmented sweep support
refactor: remove deprecated instrument classes
fix: produces a patch release, feat: a minor one, and a BREAKING CHANGE:
footer a major one. A commit that does not follow the convention produces no
release at all, so a bump can silently fail to happen – if a change should ship,
label it.
Do not edit the version in pyproject.toml or write an Unreleased section in
CHANGELOG.md by hand. CI owns the version, and semantic-release generates the
changelog from commit history. Keep prospective commit messages in todo.md
until the corresponding changes are committed.
Branch model: feature branches merge into preview, which is linted, tested,
documented, and package-built without publishing a release. preview then
merges into main, where semantic-release creates and publishes releases.
Python docstrings¶
Google-style sections¶
Production callable docstrings use Args:, Returns:, and Raises: sections
when those sections apply. Do not add empty sections or placeholder entries
such as None.. When multiple sections apply, keep them in that order. Include
explicit types so the documentation is useful when read in source code and when
rendered by Sphinx.
Use these forms:
Args:
argument_name (ArgumentType): Description.
Returns:
ReturnType: Description.
Raises:
ExceptionType: Condition that raises the exception.
For arguments:
Add
Args:when the callable accepts arguments other thanselforcls.Write the argument name first and its type in parentheses.
Use the same type expression as the function annotation where practical.
Describe the argument’s meaning, constraints, units, and defaults when they are relevant.
For return values:
Add
Returns:when the callable returns a meaningful value. Omit it when the callable only returnsNone.Begin with the complete return type.
Describe the meaning and structure of the returned value, not merely that a value is returned.
Document each element when returning a tuple.
For exceptions:
Add
Raises:only for deliberate, caller-relevant exceptions raised by the callable.Begin each entry with the concrete exception type.
State the condition that causes the exception.
Do not use a generic
Exceptionentry when a more precise exception is raised.Do not document arbitrary exceptions that may propagate from dependencies.
Classes and methods¶
Prefer docstrings on methods and functions. Do not add a class docstring merely
to repeat the constructor arguments or the class name. Put construction details
on __init__ and operational details on the methods that implement them. Add a
class docstring only when there is class-level behavior, lifecycle information,
or an invariant that cannot be explained clearly by the individual methods.
Complete example¶
def load_sweep(path: Path, strict: bool = True) -> tuple[np.ndarray, float]:
"""
Load sweep values and their point spacing from disk.
Args:
path (Path): Location of the saved sweep file.
strict (bool): Whether malformed sweep metadata should cause an error.
Returns:
tuple[np.ndarray, float]: Sweep values followed by their point spacing
in seconds.
Raises:
FileNotFoundError: If `path` does not exist.
ValueError: If strict validation is enabled and the metadata is
malformed.
"""
Indent continuation lines by four additional spaces so they remain attached to the corresponding argument, return value, or exception in generated documentation.
Tests¶
Tests are exempt from these docstring requirements. Test names should normally express the behavior being verified. A module or test docstring may still be used when the setup or reasoning needs additional explanation. When a test uses a docstring, retain the triple-quote layout described above.
Sphinx integration¶
Qanary uses Google-style docstrings so Sphinx renders the sections through
Napoleon. This is already configured in docs/source/conf.py, which enables
sphinx.ext.autodoc and sphinx.ext.napoleon with napoleon_google_docstring = True (and NumPy-style docstrings switched off).
The API reference is generated from these docstrings: each page under
docs/source/api/ is a short automodule stub, so documenting a new public
callable means writing its docstring, not editing the docs. A new module does
need a stub and a toctree entry.
Malformed docstrings surface as Sphinx warnings rather than silent bad output, so build the docs after a substantial docstring change:
uv sync --group docs
uv run sphinx-build -W --keep-going -M html docs/source docs/build
Watch for reStructuredText traps inside docstrings. Indented text that is meant
to be literal needs a :: marker, or docutils parses it as a block quote and
warns about the * in a signature; nested bullet lists need a blank line before
the sub-list.