Contributing¶
Contributions are welcome: bug fixes, new features, tests, and documentation improvements. This page explains the conventions the project follows so your changes fit in and pass CI on the first try.
Set up a development environment¶
Follow Getting started. The short version: run scripts/dev-setup.sh
(or open the project in a Dev Container) and you get a virtual environment, migrations, test users,
fake data, Mailpit, and a running dev server.
Code style¶
- Python 3.14. Use modern syntax and features. No need to support older versions.
- 100-column width for code, comments, and docstrings. Ruff enforces this.
- Plain, simple language in comments and docstrings. Explain the why, not the what.
- Small, focused functions. If a function grows complex (deep nesting, many branches, hard to test), refactor before adding more to it. Extract helpers, split responsibilities.
Tool configuration lives in pyproject.toml: ruff (linting and formatting), zuban (strict type checking with django-stubs), djlint (Django templates), bandit (security), and coverage.
Tests¶
Add or update tests whenever you change behavior. The coverage target is 90%+.
uv run pytest # full suite, random order, coverage report
uv run pytest -k test_name # a single test
Coverage is written to the terminal and to reports/lcov.info and reports/coverage.xml. The suite
covers models, views, admin, management commands, template tags, validators, and utilities; new code
should follow that pattern.
Pre-commit hooks¶
The project uses prek, a drop-in replacement for pre-commit. Run all hooks before committing:
uv run ruff check --fix . # lint + auto-fix
uv run ruff format . # format
zuban check # strict type check
prek run -a # all pre-commit hooks
The hooks configured in .pre-commit-config.yaml:
| Hook | What it does |
|---|---|
pre-commit-hooks |
File size, syntax (TOML/YAML), debug statements, line endings, whitespace |
uv-lock |
Keeps uv.lock in sync with pyproject.toml |
ruff-check / ruff-format |
Lints (with --fix) and formats Python code |
djlint |
Reformats and lints Django templates |
mdformat |
Formats Markdown (GitHub Flavored Markdown, frontmatter-aware) |
cspell |
Spell checks changed files and the commit message |
bandit |
Security scan, configured via pyproject.toml |
pyupgrade |
Upgrades syntax to Python 3.14+ idioms |
renovate-config-validator |
Validates renovate.json5 so dependency updates do not silently break |
zuban |
Strict type check (mypy-compatible, with django-stubs) |
Do not skip hooks
Never commit with --no-verify. If a hook fails, fix the cause. Likewise, do not silence lint or
type findings just to make CI pass; silence only with a documented reason when a finding clearly
does not apply.
Commits¶
- Small, atomic commits. One logical change per commit: a single fix, one refactor, one file's worth of related edits. Do not batch many unrelated edits into one large commit.
- Commit as you go. Run the pre-commit checks and commit each discrete change before moving on to the next one.
- Clear messages. Short imperative subject ("Fix schedule grid overflow", not "Fixed" or "Fixes"), with a body that explains the why.
Pull requests and CI¶
Opening a pull request triggers the
ci workflow,
which calls the reusable
checks workflow.
Superseded runs on the same PR are cancelled automatically to save minutes.
The quality gate runs, in order:
uv sync --all-groups --locked --no-build- install dev, test, and prod dependencies from the lockfile; every dependency must ship a prebuilt wheeluv run ruff check- lintuv run ruff format --check- formatting checkuv run zuban check- strict type checkuv run pytest- test suite (CI-safe defaults: SQLite, plain static files storage)
Deploys never run from pull requests. The same checks workflow is also called by the deploy
pipeline before any image is built, so only green code reaches production. See
Deployment for how releases work.
Tip
Run prek run -a and uv run pytest locally before pushing. They cover everything CI checks, so a
green local run almost always means a green PR.