Contributing¶
Thanks for contributing to Caxton. This guide covers development and releases. If you get stuck, open a discussion.
Set up¶
Install the git hooks once:
Run the checks¶
Use the checked-in virtual environment:
Run repository-level gates through tox, matching CI:
uv run --no-sync tox run -e py314 # tests on one interpreter
uv run --no-sync tox run -e pre-commit # lint, typing, imports, hygiene
uv run --no-sync tox run -e build # wheel and sdist validation
uv run --no-sync tox run -e docs # strict documentation build
The full matrix is py310, py311, py312, py313, py314. CI also tests the built wheel and sdist on Linux, macOS
and Windows.
Two convenience targets exist:
Work on the documentation¶
uv run --no-sync tox run -e docs-serve # live reload on http://127.0.0.1:8000
uv run --no-sync tox run -e docs # strict build, as CI runs it
The site uses MkDocs with Material, and mkdocstrings generates the API reference from docstrings. The strict build fails on broken internal links and unresolved references.
Published prose pages live in docs/, and their navigation is defined in mkdocs.yml. ARCHITECTURE.md is the
normative source for architecture, while CHANGELOG.md and the Towncrier fragments are the sources for release notes.
Edit those source files instead of duplicating their content in a guide.
Architectural guardrails¶
- Public factories create immutable nodes; fluent methods return new ones.
- Semantic models hold intent only — no coordinates, resolved layout, caches or engine-native values.
- Dependency direction is defined in
ARCHITECTURE.md:apiandtestingmay use private implementation modules;_pipelinecoordinates_spreadsheet,_xlsxand_io;_xlsxmay use_spreadsheetand_io;_spreadsheet,_sourceand_iodepend only on Core. Private modules never importapiortesting. - Column
id,sourceandtitlestay distinct. - Coercion and structural validation never consume rows;
REITERABLE/ONE_SHOT/UNKNOWNbehavior is preserved and hidden extra passes are rejected. - Errors are stable
CaxtonErrorsubclasses with semantic context, chained to the original cause. - Treat deferred capabilities as absent — a name in a design note does not reserve a public API.
import-linter contracts and the Griffe API compatibility check enforce some of these rules. Breaking one usually fails
the pre-commit tox environment.
Workflow¶
- Inspect the affected public contract and tests; preserve unrelated changes.
- Add or update a focused test at the narrowest meaningful boundary — semantic model, layout, renderer or artifact.
- Make the smallest coherent change.
- Run focused tests, then checks proportional to the change.
- Review the final diff for accidental API exposure, eager data consumption, engine leakage, generated artifacts and stale documentation.
Release process¶
Caxton releases from main; there is no separate long-lived development branch.
The default cadence is a weekly release window, not a weekly obligation. Skip the release when there is no user-visible change worth publishing. A fix for a bad release does not wait for the next window.
Day-to-day development¶
Create a short branch from main and open the pull request against main. Use names such as
feat/declarative-columns, fix/short-buffer-write or
docs/template-guide; the prefix describes the work but does not determine the package version.
Add one Towncrier fragment for every user-visible change:
The supported types are breaking, feature, bugfix, doc, generation
and ci. Use the issue or pull request number when one exists. Otherwise use a short name prefixed with +:
hatch run towncrier create \
--content "Preserve text written through a short-writing buffer." \
123.bugfix.md
Write the fragment for a package user. State the behavior that changed and any action required during an upgrade. Implementation notes belong in the pull request.
Do not change src/caxton/__version__.py or generate CHANGELOG.md in a feature pull request. Those changes are made
once, in the release pull request, so parallel work does not compete over a version number or generated file.
Delete the branch after it is merged. Caxton does not use develop or permanent release branches. A maintenance branch
becomes useful only when the project commits to supporting two release lines at the same time.
Choose the version¶
Caxton uses three-part versions compatible with
Semantic Versioning and Python's
PEP 440. While the public API is below
1.0, the project follows a stricter convention than SemVer requires: a patch release must not break documented public
behavior.
| Change in the release | Before 1.0 |
From 1.0 onward |
|---|---|---|
| Backward-incompatible public API change | next minor, for example 0.3.0 |
next major, for example 2.0.0 |
| Backward-compatible public feature | next minor | next minor |
| Backward-compatible bug or performance fix | next patch | next patch |
| Documentation or CI only | no package release by default | no package release by default |
When a release contains several kinds of change, use the largest required bump. A breaking change needs a breaking
fragment even before 1.0.
Pre-releases are reserved for changes that need feedback before ordinary users upgrade, such as a broad rewrite of the
public DSL. Use PEP 440 spelling such as
0.4.0rc1. The current version bump script handles final releases only; extend and test that tooling before publishing
a pre-release.
Prepare the release pull request¶
Start from an up-to-date main and create a branch for the release:
Inspect the pending notes before choosing the final version:
Bump the appropriate component, then let Towncrier consume the fragments and write the new section at the top of
CHANGELOG.md:
Review the generated section. It should describe only shipped behavior, group entries under the right headings and call out every incompatible change. Check that the version file and generated heading agree:
Run the repository gates before opening the pull request:
uv run --no-sync tox run -e py314
uv run --no-sync tox run -e pre-commit
uv run --no-sync tox run -e build
Documentation changes also require the strict documentation build:
Commit the release preparation, push the branch and open a pull request into
main:
git add src/caxton/__version__.py CHANGELOG.md changelog.d
git commit -m "chore(release): prepare v0.3.0"
git push -u origin chore/release-v0.3.0
Apply the skip-changelog label to this pull request: it consumes the pending fragments instead of adding another one.
Merge only after the full CI run passes. The release commit must reach main before the tag is created.
Tag and publish¶
Update the local main, verify the version and create an annotated tag on the release commit:
git switch main
git pull --ff-only
test "$(hatch version)" = "0.3.0"
git tag -a v0.3.0 -m "Caxton 0.3.0"
git push origin v0.3.0
Pushing v* starts .github/workflows/release.yml. The workflow reruns the quality gates, then builds, verifies and
attests the wheel and source distribution. It creates the GitHub Release and dispatches the Trusted Publishing workflow
for PyPI.
The release is complete when all the following are true:
- the tag points to the intended commit on
main; - the GitHub Release is published with the wheel, source distribution and
SHA256SUMS; - the same version is available from PyPI;
- installing the published wheel succeeds in the CI package test.
When publication fails¶
If a transient CI or publishing step fails, fix its external cause and rerun the failed workflow. Do not move a public tag to another commit.
If the tagged source itself is wrong, merge the correction into main and make a new release. Published package
versions and tags are immutable; never delete and reuse their numbers. Use a patch release for a compatible correction.
Use the version table above if the correction changes public behavior.
Reaching 1.0.0¶
Release 1.0.0 when the documented public DSL is a compatibility commitment:
ordinary upgrades may add behavior or fix bugs, while incompatible changes need a major release. Before that tag,
document the supported public surface and the deprecation policy, and make sure the examples and compatibility checks
cover the API users are expected to depend on.