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

uv run pytest

Tests with coverage

uv run pytest --cov

Lint (includes import sorting)

uv run ruff check .

Format

uv run ruff format .

Build the docs

uv sync --group docs && uv run sphinx-build -M html docs/source docs/build

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 checkruff 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 than self or cls.

  • 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 returns None.

  • 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 Exception entry 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.