Contributing¶
The short version: open a small pull request, run the linter and the tests, write the docs for anything that changes user-visible behavior.
Before you start¶
Skim two things first.
- Lowering onto hardware. It explains the five modules, the registration hooks, and what a platform does with each operation. Almost every change touches one of those.
- The core DSL's developer guide, in particular
building a vendor extension,
the template this package follows, and
capability protocol internals,
the normative account of tokens, slots and predicates. The AST, the
validator, and the
.qpformat live there too. A change to this package that needs a core change is usually a core issue instead.
Then decide which repository the change belongs in. An operation only a QDAC can do belongs here. Anything a second backend would also want belongs in the core, so every backend gets it.
Development workflow¶
- Install the package.
- Install the docs environment (optional, 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.
-
Update the docs. Anything user-visible needs an entry in the relevant guide page and, for a new class or method, an entry in
docs/reference/api.md, whosemembers:lists are explicit. -
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 +trigger-network-reset.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 pull request. The workflows under
.github/workflows/run on it.tests.ymlruns the suite on Python 3.11 and 3.14 for a pull request, across 3.11 through 3.14 on a push tomain, and uploads coverage from the 3.13 job.code_quality.ymlrunsruff checkandruff format --diffonce on 3.13, thenty checkonce per supported version.docs.ymlbuilds this site, and deploys it on a push tomain.
What "small PR" means¶
One concept per pull request. A new operation plus a new predicate plus a serialization fix is three pull requests. Each one is easier to review, easier to revert, and easier to bisect against.
The checklist for a new operation¶
All five code edits are listed in Adding an operation. A complete pull request carries four kinds of change:
| Kind | What it means |
|---|---|
| Code | The node in operations.py, the method on QdacNamespace, the token in profiles.py twice, the registration in __init__.py. |
| Tests | One test per module in tests/: operations, namespace, serialization, registration, profile. |
| Docs | The operation's row and section in the user guide, its wire form in the serialization page, and anything the platform must know in Lowering onto hardware. |
| API reference | The class and the namespace method added to the members: lists in docs/reference/api.md. |
Adding an operation is a minor version bump of the package, which is also the
vendor protocol version in the require qdac <major>.<minor> header. Removing
or renaming one, or changing what it means on the wire, is a major bump.
Style notes¶
These are the rules the project enforces; they are not exhaustive. 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 reason for each exemption reads off the config. Suppression comments use the same form:# ruff: ignore[import-outside-top-level], never a numeric code. Line length is 120 and the formatter owns it. Expect the linter to push back on most external code. - Docstrings are enforced. Preview mode means both the
Dand theDOCfamilies run, under the Google convention. Every parameter gets aname (type): Description.entry, with the parenthesized type as house style even though the signature is annotated. A non-Nonereturn needs aReturns:section unless the summary line already opens with the word "Return", and every exception a caller can observe needs aRaises:entry. Constructor arguments are documented on the class docstring, sincemerge_init_into_classis on andundocumented-public-initis ignored.Attributes:is for a value object whose fields are the interface and are not all constructor arguments. - Cross-references are Markdown, not Sphinx roles. Write
[`SetOffset`][qprogram_qdac.SetOffset]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, above the module docstring. Ruff'smissing-copyright-noticerule fails the lint on a file without it. Tests included. - Type hints everywhere.
tyis in the dev group and checkssrc, and CI runs it once per supported Python version. - Function-style tests. No test classes. Use fixtures from
tests/conftest.pyand parametrization. Tests are exempt from theANN,D, andDOCfamilies, so a test needs no docstring, but one explaining why a case exists is worth writing. - Two-space indentation in
.qpfixtures. That is what the writer emits. - No new runtime dependencies. This package depends on
qprogramand nothing else, and that is on purpose. A vendor SDK belongs in the platform library that drives the instrument, not in the package that describes it.
What goes where¶
| Change kind | Where it lands |
|---|---|
| New operation | src/qprogram_qdac/operations.py plus the four other edits. |
| New method on the namespace | src/qprogram_qdac/namespace.py. |
| New capability token or predicate | src/qprogram_qdac/profiles.py. |
| New limit | src/qprogram_qdac/profiles.py, in QDAC_DEFAULT_V1.limits. |
| New profile bundle | src/qprogram_qdac/profiles.py, registered from __init__.py. |
| Registration or the typed program | src/qprogram_qdac/__init__.py. |
| Docs | docs/; the nav lives in zensical.toml. |
Anything about the AST or .qp |
The core DSL, not here. |
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. The history in git log is a good template.
License and attribution¶
qprogram-qdac is licensed under the Apache License, Version 2.0. The full text
is in the LICENSE file at the repository root, and every source file carries
the matching 13-line header. By opening a pull request you agree to license your
contribution under the same terms.
Where to ask¶
- Bug reports and feature requests. Open a GitHub issue; the templates are
short on purpose. Include a minimal program and, for anything about the file
format, its
.qptext. A problem that reproduces without importingqprogram_qdacbelongs in the core repository. - Design discussion. Open an issue naming the affected code, the behavior you expect, and the guide page that documents it.
Pull requests with a clear scope and tests are the fastest path to a merge.