How to contribute¶
This page walks you through a change to redsun itself, from the first idea
to the moment it's merged. You don't need to know the code well to start: a
bug report, or a fix to a confusing sentence in these docs, is a good first
contribution. If you're writing a session or a plugin that uses redsun,
start with the tutorial instead.
Everything you need is on this page: the rules for using AI tools, the steps a change goes through, and then a section for each task along the way, from setting up your environment to making a release.
Using AI tools¶
Note
This section is mirrored from the zarr-python AI contribution policy:
AI-assisted contributions,
adapted to redsun.
AI coding tools are increasingly common in open source development. These tools are welcome in redsun, but the same standards apply to all contributions regardless of how they were produced: whether written by hand, with AI assistance, or generated entirely by an AI tool.
You're responsible for your changes¶
If you submit a pull request, you are responsible for understanding and having fully reviewed the changes. You must be able to explain why each change is correct and how it fits into the project.
Write your own messages¶
PR descriptions, issue comments, and review responses must be in your own words. The substance and reasoning must come from you. Using AI to polish grammar or phrasing is fine, but do not paste AI-generated text as comments or review responses.
Read every line¶
You must have personally reviewed and understood all changes before submitting. If you used AI to generate code, you are expected to have read it critically and tested it. The PR description should explain the approach and reasoning; do not leave it to reviewers to figure out what the code does and why.
Keep pull requests small enough to review¶
Generating code with AI is fast; reviewing it is not. A large diff shifts the burden from the contributor to the reviewer. PRs that cannot be reviewed in reasonable time with reasonable effort may be closed, regardless of their potential usefulness or correctness. Use AI tools not only to write code but to prepare better, more reviewable PRs: well-structured commits, clear descriptions, and minimal scope.
If you are planning a large AI-assisted contribution (e.g., a significant refactor or a new subsystem), open an issue first to discuss the scope and approach with maintainers. Maintainers may also request that large changes be broken into smaller, reviewable pieces.
The same goes for documentation¶
The same principles apply to documentation. redsun has semantics of its own (the order of the build steps, what a session answers in setup, how services are launched and stopped, which transport a session speaks) that AI tools frequently get wrong. Do not submit documentation that you haven't carefully read and verified.
What you need¶
You need a GitHub account, git, and
uv, which installs Python and the project's
dependencies for you.
From idea to merged change¶
- Say what you want to change. Open an issue that describes the bug or the feature. A maintainer answers, and you agree on the change before you write it, so none of your work is wasted. A small fix, such as a typo, can skip this step. Issues shows how to title one.
- Get the code. Most people can't push to the
redsunrepository directly, so first fork it on GitHub and clone your fork. Then follow Setting up your copy once. - Make a branch from
main. Name it after the kind of change and what it does, such asfix/log-folder,feat/strict-sessionsordocs/glossary. The first part is the same type your commit messages start with. - Make the change, with tests. Before you push, run the tests and the commit checks. A change to the docs alone only needs the docs build.
- Open a pull request against
main. Write its title and description as Pull requests describes. It also needs the label that says which section of the changelog it belongs in. If you can't set labels, say which one you think fits, and a maintainer adds it. - Wait for CI and a review. CI runs the same checks as
uv run toxand reports the result on the pull request. If a reviewer asks for changes, push them to the same branch, and the pull request updates by itself.
If you're stuck at any step, ask in your issue or pull request.
Setting up your copy¶
These steps give you a copy of redsun you can change and test.
Get the code¶
If you work from a fork, clone your fork instead.
uv sync creates .venv and installs redsun in it, with the dev
dependency group. Without uv:
Check that everything works¶
Run every check once:
This runs every check CI runs: lint, type checks against both
Qt bindings, the tests and the docs
build. Each check gets its own environment built from uv.lock, so your
result matches CI's. Running the tests explains each environment.
Running the tests¶
Here you run the redsun test suite, type-check it against both
Qt bindings, and produce coverage
reports.
Everything at once¶
tox runs the environments CI runs, each built from uv.lock:
That lints, type-checks against both Qt bindings, runs the tests and builds
the docs. Run one environment with -e:
| environment | what it runs |
|---|---|
lint |
prek run --all-files: the commit checks |
mypy-pyqt / mypy-pyside |
mypy against that Qt binding |
tests |
pytest -q |
docs |
zensical build then the cross-reference check |
Only some tests¶
Arguments after -- go to pytest:
# the tests of the shared modules only
uv run tox -e tests -- tests/sdk/
# a specific test function
uv run tox -e tests -- tests/test_container.py::test_function_name
# everything matching a pattern
uv run tox -e tests -- -k "test_wiring"
The project environment skips the sync, so it's faster while editing:
Tests marked @pytest.mark.qt are skipped when there's no display.
Tests that need a container¶
Tests marked @pytest.mark.compose talk to an
IOC in a container started from
tests/compose/compose.yaml. They're skipped unless REDSUN_COMPOSE is set,
so the rest of the suite needs no container runtime. With Docker running:
docker compose -f tests/compose/compose.yaml up --detach --wait
REDSUN_COMPOSE=1 uv run pytest -m compose
docker compose -f tests/compose/compose.yaml down
The IOC listens on 127.0.0.1 port 5064, the default
Channel Access port, so stop any
other IOC on that port first. CI runs these tests in their own job on Ubuntu.
Type checks for both Qt bindings¶
mypy checks the tests in strict mode along with the sources. redsun
supports pyqt6 and pyside6, whose type stubs disagree on some signatures,
so both are checked:
Each environment installs only its own binding and sets QT_API, which
selects the branches qtpy shows the type checker. A green mypy-pyqt says
nothing about mypy-pyside.
mypy in the project environment gives different results
The project environment holds both bindings, so mypy there reports errors
neither binding has on its own and can miss errors CI catches. Run the
tox environments instead.
Coverage report¶
pyproject.toml configures the coverage sources:
Open htmlcov/index.html in a browser.
Checks before each commit¶
redsun checks formatting and lint with prek, which
runs the hooks listed in prek.toml at the project root. The same hooks run on
every commit once you install them, in uv run tox -e lint, and in CI.
prek and ruff are in the lint dependency group, which dev includes.
Turn on the git hook¶
Once per clone, from the project root:
This writes .git/hooks/pre-commit, so from then on git commit runs the hooks
on the staged files and stops the commit if one fails.
Run the checks by hand¶
To run every hook on every file, without committing:
uv run prek run # staged files only
uv run prek run --all-files # every tracked file, as CI does
uv run prek run ruff-check --all-files # a single hook
What gets checked¶
| hook | what it does |
|---|---|
end-of-file-fixer |
ends every file with exactly one newline |
trailing-whitespace |
strips spaces at the end of lines |
check-yaml, check-toml |
fails on files that do not parse |
check-added-large-files |
fails on large files added to the index |
check-merge-conflict |
fails on leftover conflict markers |
ruff-check |
ruff check --fix |
ruff-format |
ruff format |
When a check fails¶
A hook that can fix what it found rewrites the file and still reports a
failure, so the commit stops with the fix unstaged. Review the change, stage it
with git add, and commit again. If --fix can't repair a ruff violation,
the hook prints it with its rule code and you fix it by hand.
CI runs prek run --all-files --show-diff-on-failure, so a file a hook
rewrites fails the build there and the log shows the diff.
Which version of ruff runs¶
The ruff hooks run uv run --locked --only-group lint ruff, so they use the
version pinned in uv.lock, the same one tox and CI use. When you update ruff
in the lock file the hooks follow, because prek.toml holds no separate
version for it. --only-group lint makes a fresh clone install only prek and
ruff before the hooks run, and it adds to the project environment without
removing anything from it.
Building the docs¶
Here you build the documentation site, check it, and look at it locally while you write.
Build and preview the site¶
From the project root:
This builds the site, then runs scripts/check_xrefs.py, which reports every
cross-reference that resolves to nothing. zensical build alone passes with
such references, so prefer the tox environment, which also installs what the
docs need from uv.lock.
Zensical is in the docs dependency group, which dev doesn't include, so a
command that runs it directly names the group:
The site lands in site/. Serve it locally with:
The server listens on http://localhost:8000 and rebuilds on every change.
Pictures of example windows¶
A tutorial's code lives in a script next to its page, such as
docs/tutorials/first_session.py, and the page pulls each step from it. The
build runs the script and saves a picture of the window it opens:
uv run tox -e docs runs this first. If you serve the site with zensical
serve, run it once yourself, or the tutorial shows a missing image. The
pictures are generated, so they aren't in git. The window opens on screen for a
moment and needs a display, which CI provides with a virtual one.
To add a picture, add the script and the image's path to SCREENSHOTS in
scripts/screenshots.py.
Common problems¶
zensical is not found¶
uv run zensical without --group docs fails with
Failed to spawn: zensical. Add the flag, or install the group once:
Port already in use¶
Pick another port:
Commit messages and pull requests¶
Commit messages, issue titles and pull requests all follow one form, so the history reads the same everywhere and the changelog can be written from the pull requests.
Commit messages¶
The first line follows Conventional Commits:
typeis one offeat,fix,docs,refactor,test,ci,chore. Add!after it for a change that breaks existing code:feat!: ....scopeis optional and names the part changed:session,qt,log.- The summary is in the imperative ("add", "fix", "move"), with no full stop, and the whole line is 72 characters or fewer.
A commit that does several things gets a body of short bullets, each one imperative:
refactor(session): read each session file once
- merge the sources before validating them
- log the files in the order they are layered
Write what changed in plain words, so someone who hasn't seen the diff understands it.
Issues¶
An issue title takes the same form as a commit's first line and names the
change it asks for: fix: log files stay open after shutdown,
feat: add a toolbar placement. The issue templates start the title for
you.
Pull requests¶
Open the pull request against main. Its title follows the same rules as a
commit's first line, because the changelog entry is made from it:
feat(session): add strict sessions is listed under Added as "Add strict
sessions".
The description is a list of one-line bullets in the imperative, one per change a reviewer can see, with no headers:
- Add `strict` to the session file
- Stop a session whose component fails to build when `strict` is set
- Rename `MyMotor.go` to `MyMotor.move`, which existing code must follow
A change that existing code must follow says so in its bullet. Leave out tests, coverage and the checks you ran: CI reports them on the pull request.
Labels¶
Every pull request needs one label saying which changelog section it goes in, and CI refuses a pull request without one.
| label | changelog section |
|---|---|
added |
Added |
changed |
Changed |
deprecated |
Deprecated |
removed |
Removed |
fixed |
Fixed |
security |
Security |
skip-changelog |
none: tests, CI, refactors nobody using redsun would notice |
Add breaking as well when existing code has to change. The entry is then
marked Breaking.
Writing documentation¶
Write for someone new: picture a reader who knows some Python and nothing about
redsun or lab hardware, and explain things the way you would in person. Talk
to them as "you", keep most sentences short, and show a small code example
when it says more than a paragraph would. Docstrings count too, because the
API reference is made from them.
Where a page goes¶
The docs have four sections, and each page belongs in the one that matches what its reader wants:
| section | the reader wants to | example |
|---|---|---|
| Tutorials | learn by building something, step by step | build your first session |
| How-to Guides | get one task done | how to write a service |
| Explanations | understand how and why | how a session works, the glossary |
| Reference | look up a fact | an API page, the changelog |
Write each fact once, on the page where it belongs, and link to it from the
others. A wrong API page is fixed in its docstring, not in a .md file.
Headings, terms and wording¶
Give each section a short heading that names its topic, such as "Devices and services", so a reader scanning the table of contents finds it. Name it in the reader's words rather than after the class or mechanism behind it: "A tree of device settings", not "Descriptor tree view". How-to steps and tutorial steps are the exception: they say what the reader does, such as "Declare the device".
The first time a page uses a technical word, link it to its entry in the glossary:
If the glossary doesn't have the word yet, add it there first. Acronyms such
as ADR also go in includes/abbreviations.md, which shows them as tooltips on
every page.
Prefer plain words to the jargon of a field: write "can't change without
breaking X" rather than "load-bearing". Leave out em dashes and sales words
such as "powerful", and write arrows as ->.
If you work on the docs with an AI tool, point it at
.claude/skills/docs-conventions/SKILL.md, which holds the full set of rules
these pages follow.
Code in tutorials¶
A tutorial takes its code from the script beside it, so the page always shows
code that runs. Mark each part of the script with a start and an end
comment, and include it as docs/tutorials/first-session.md does. Write the
fence of an included part as {.python}: inside a python fence, ruff
format would rewrite the include line, and the page would show that line
instead of the code. End each tutorial with the whole script in a collapsed
block, before "What you built".
The tutorial scripts are type checked with the rest of the code, by
uv run tox -e mypy-pyqt,mypy-pyside.
Checking your changes¶
This builds the site, then checks that every cross-reference found its target, every included script was read, and every link to a part of a page reaches it. The build alone doesn't fail on any of these, which is why the check runs after it. See Building the docs.
Recording a design decision¶
An ADR records one decision about how
redsun is built, and the reasons for it. The records live in
docs/explanation/decisions/.
When to write one¶
Write one when a change decides how parts of redsun fit together, and a
future contributor would otherwise ask "why is it like this?". Examples are
the order a build runs in, what a component may ask for, or where acquisition
files are written. A bug fix, or a new option that follows an existing
decision, doesn't need one.
How to write one¶
- Copy
docs/explanation/decisions/COPYMEto the next free number, asNNNN-short-title.md. - Fill in the sections: the context, the decision, and its consequences. Follow Writing documentation.
- Add it to the
Decisionslist inzensical.tomland todocs/explanation/decisions/index.md.
Changing a decision¶
Don't edit an accepted ADR. Write a new one that replaces it, and set the old one's status to "Superseded by" with a link to the new one.
Making a release¶
A release starts with the changelog, which a script writes from the pull requests merged since the last release, grouped by their labels. Don't edit it by hand.
What you need for a release¶
You need a clone of the repository, and gh, the GitHub command-line tool,
signed in with access to it.
1. Write the changelog¶
On an up-to-date main, make a release branch and write the section, with
the version without its v:
git switch main
git pull
git switch -c release/v0.14.1
uv run python scripts/release_notes.py prepare 0.14.1
The script asks GitHub for the pull requests merged since the last final
release, and writes a section for them at the top of
docs/reference/changelog.md, with the date and a compare link.
Read the section. If a title reads badly, rename the pull request it came from,
so the changelog and GitHub agree. Then discard the section with git checkout
docs/reference/changelog.md and run the script again.
2. Merge the changelog¶
Commit the section, push the branch, and open a pull request for it:
git commit -am "docs: add the 0.14.1 changelog"
git push origin release/v0.14.1
gh pr create --base main --label skip-changelog \
--title "docs: add the 0.14.1 changelog" \
--body "Adds the changelog section for 0.14.1."
The checks start as on any pull request. Merge it once they pass.
3. Tag the release¶
Tag the merge commit and push the tag:
The tag publishes the package to PyPI and the docs, and creates the GitHub release, whose notes are the changelog section.
A final-release tag without a changelog section
If the tag's version has no section, the package build fails with a
message naming the prepare command, and neither PyPI nor the GitHub
release gets anything. Merge the changelog pull request (steps 1 and 2)
before you tag.
Release candidates¶
Tag a candidate v0.14.1rc1 without preparing anything. It publishes the
package, but no GitHub release and no changelog section: the final release's
section lists every pull request since the previous final release.