Contributing¶
The short version: keep the PR small, run the linter and the tests, and write the docs for anything a caller can see.
Before you start¶
Two things to read first.
- Lowering onto hardware. It describes the five modules, the registration that runs at import, and what a platform does with each operation. Almost every change to this package touches one of those.
- The core DSL's own developer guide, in particular building a vendor extension and capability protocol internals. This package is one instance of that template. A change that fits the template is easy to review; a change that fights it usually belongs in the core instead.
Development workflow¶
- Install the package and the dev tools.
- Install the docs environment (only if you change docs).
mkdocstrings imports the package to render the API reference, so the
docs build needs the project installed, not just the docs tooling.
-
Make your change.
-
Lint and format.
- Type-check.
- Run the tests.
uv run pytest
uv run pytest --cov=qprogram_qblox --cov-report=term-missing
uv run pytest tests/test_profile.py -v # one file
uv run pytest -k "rotation" # by keyword
The suite is function-style, roughly a hundred tests across six modules, and runs in about a second. Anything much slower than that is doing work a unit test should not.
-
Update the docs. Anything a caller can see needs an entry in the guide, and every new public class or method needs its
mkdocstringsentry in the API reference. -
Add a changelog entry. Anything a caller would notice gets one news fragment under
changelog/, named<pr-number>.<type>.md, where the type isadded,changed,fixed, orremoved.
Write one or two sentences about what changed for somebody using the
package. A fragment written before the pull request has a number takes a +
prefix and any name, as in +marker-pulse-width.added.md; rename it once
the number exists so the entry carries a link. Internal refactors, test-only
changes, and docs corrections do not need one.
- Open the PR. The workflows under
.github/workflows/run on it.tests.ymlruns the suite on 3.11 and 3.14 for a pull request, and on the whole of 3.11 through 3.14 for a push tomain; the coverage upload rides on the 3.13 job, so only a push produces it.code_quality.ymlrunsruff checkandruff format --diffonce on 3.13, thenty checkonce per supported version.docs.ymlbuilds this site.
What "small PR" means¶
One concept per PR. A new operation plus a profile change plus a fix in the namespace is three PRs. Each one is easier to review, easier to revert, and easier to bisect against.
The checklist for a new operation¶
Four kinds of change land together, in the same PR. The five source edits are the first item.
| Kind | What it means here |
|---|---|
| Code | The node class in operations.py, the typed method in namespace.py, the token registration and profile membership in profiles.py, the register_vendor_operation call and the __all__ entry in __init__.py. |
| Tests | tests/test_operations.py, tests/test_namespace.py, tests/test_serialization.py, tests/test_registration.py, tests/test_profile.py. One per module the change touched. |
| Docs | Operations for what it means and when to reach for it, and Capabilities and profiles if the profile changed. |
| API reference | The class and the namespace method in the API reference, so mkdocstrings renders the docstrings you just wrote. |
A missing token registration or a missing profile entry is the failure mode
to watch for: the operation builds and serializes, and every program using
it fails validation. tests/test_profile.py is where that gets caught.
Style notes¶
These are the rules the project enforces. All of them are configured in
pyproject.toml.
- Ruff with
preview = trueandselect = ["ALL"], minus a curated ignore list written as rule names rather than codes, so the config says why each exemption exists. Line length is 120 and the formatter owns it. Expect the linter to push back on most external code. - Suppressions use the same names. Write
# ruff: ignore[import-outside-top-level], not a numeric code. There is not a single numeric suppression in this package. - Docstrings are enforced. Preview mode runs both the
Dand theDOCfamilies under the Google convention. Every parameter gets aname (type): Description.entry: the parenthesized type is house style even though the signature is annotated. A function returning something either opens its summary with the word "Return" or carries aReturns:section, and every exception a caller can observe gets aRaises:entry readingExceptionType: If <condition>.Constructor arguments are documented in anArgs:section on the class docstring, with no docstring on__init__, becausemkdocstringsruns withmerge_init_into_class = true. - Cross-references are Markdown, not Sphinx roles. Write
[`Acquire`][qprogram_qblox.Acquire]for a target this site documents, and[`Expression`][qprogram.Expression]for one the core DSL documents:zensical.tomlloads that project's publishedobjects.invas aninventoriesentry, so a core type resolves to its page on that site. Plain`Expression`is for anything neither site renders, such as a builtin or a stdlib name. A Sphinx role such as:class:`~qprogram.Expression`would reach the page as literal text, since mkdocstrings reads a docstring as Markdown and has no reStructuredText reader;tests/test_docstring_style.pyfails the suite on one. The docs build runs with--strict, so a cross-reference neither site can resolve fails CI as well. - Every file carries the Apache header, the standard 13-line notice with
Copyright 2026 Qilimanjaro Quantum Tech, before the module docstring and in test files too. Ruff'smissing-copyright-noticerule fails the lint on a file without it. - Comments say something the code does not. A comment that restates the next line is worse than no comment. Docstrings follow the same rule and describe current behavior, never the change that produced it.
- Type hints everywhere.
tycheckssrcagainst every supported Python version. - Two-space indentation in
.qptext. Match it in test fixtures. - Function-style tests. No test classes. Use fixtures and
parametrization.
tests/conftest.pyholds the shared schema and program fixtures. - No new runtime dependencies. This package depends on
qprogramand nothing else, and it should stay that way. Anything a single operation needs belongs behind an optional extra, if anywhere.
Releasing¶
CHANGELOG.md is assembled from the fragments in changelog/, so it is written
once per release rather than edited per PR. A release goes out from its own pull
request:
- Branch from an up-to-date
main. - Set the new version. This writes both
pyproject.tomlanduv.lock; nothing else holds the literal, since the version is read from the installed metadata.
- Assemble the changelog. Pass the version explicitly. Left to guess, towncrier reads the installed metadata and can render a stale number into a heading that is never regenerated.
uv run towncrier build --draft --version "$(uv version --short)" # preview
uv run towncrier build --version "$(uv version --short)" --yes
- Read the rendered section and edit it. Fragments are written weeks apart by different people and rarely read as one voice when they land together.
- Open the release PR, and merge it once CI is green.
- Create the GitHub Release on the merge commit, tagged with the version now in
pyproject.tomland novprefix. Publishing it startspublish.yml, which runsuv buildfor the wheel and the sdist, checks both withtwine check, and uploads them through trusted publishing. Pre-releases publish too. - Approve the deployment. The run waits on the
pypienvironment until a reviewer releases it. PyPI never lets a file be replaced, so this approval is the last point at which a wrong version can be stopped.
publish.yml can also be started by hand from the Actions tab, which is how the
first release goes out and how a run that failed on a transient error is
retried. A manual run takes three inputs: platform chooses between PyPI and
the qilimanjaro AWS CodeArtifact domain, repository names the CodeArtifact
repository, and dry_run builds and validates the distributions without
uploading them.
Commit messages¶
Short, imperative, explanatory. The body matters more than the title:
explain why, not what. git log is a good template.
License and attribution¶
Apache License 2.0. The full text is in LICENSE at the repository root,
and every source file carries the matching header. By opening a PR you agree
to license your contribution under the same terms.
Where to ask¶
- Bugs and feature requests. Open an issue with a minimal program that
reproduces the problem, and the
.qptext for it. - Design questions. Open an issue naming the affected code, the behavior you expect, and the page that documents it.
- Something that belongs in the core language. Open it against https://github.com/qilimanjaro-tech/qprogram instead. If you are not sure which side a change belongs on, say so in the issue; the boundary is the interesting part of the question.