Contributing¶
Thanks for your interest in improving cgt-calc — contributions of all kinds are welcome! If you find a bug or have feature ideas, please open an issue or pull request.
Getting started¶
This project uses uv for dependency management, testing, and builds.
1. Install uv¶
Follow uv’s installation guide:
curl -LsSf https://astral.sh/uv/install.sh | sh
2. Clone the repository¶
git clone https://github.com/cgt-calc/capital-gains-calculator.git
cd capital-gains-calculator
3. Set up the environment¶
uv sync
This command creates a virtual environment and installs all project and development dependencies into it. Run it again after pulling new changes to update dependencies.
Code layout¶
The calculation has two passes:
broker files → parsers → BrokerTransaction → ingestion → matching → CapitalGainsReport → renderers
cgt_calc/parsers/reads broker exports and converts their rows intoBrokerTransactionvalues. Keep broker-specific columns and normalisation here, and keep tax matching outside the parsers.cgt_calc/ingestion.pyvalidates transactions and runs the first pass. It records dividends and interest forincome.pyto process, and delegates share-reorganisation planning tostock_split_planning.py.cgt_calc/calculator_state.pyholdsPreparedHistory, which the first pass fills and the second pass only reads, andRunState, which the second pass builds and which is reset at the start of every calculation.cgt_calc/matching.pyruns the second pass, applying the same-day, bed-and-breakfast and Section 104 matching rules.cgt_calc/model.pydefines shared values and the report model.render_text.pyandrender_latex.pyformat that report without calculating it.cgt_calc/main.pyconnects the stages behindCapitalGainsCalculator, whosecalculate()runs the two passes in order.cgt_calc/cli.pyhandles command-line input and output.
Put a change in the module that owns its behaviour. Extract a new module only when one complete concern can move behind explicit inputs; do not split a file only to reduce its line count.
Code style¶
All checks in CI must pass before merging changes.
We use:
- ruff — for Python linting and formatting
- mypy — for static type checking
- ty — for additional static type checking
- pytest — for running tests
- dprint — for formatting Markdown, YAML, TOML, JSON, and Dockerfiles
- shfmt - for formatting shell scripts
- codespell - for catching common misspellings
- Harper - for grammar and spell checking of comments, docstrings, and Markdown docs
prek can be used to run all checks with one command (see below).
Links in Markdown are checked separately by lychee, which runs in CI. Run
scripts/check_links.sh to check them locally. It makes two passes: an offline one over local files
and heading anchors, then a network one over external links. Pass offline or external to run
just one. Shared settings live in lychee.toml.
The project uses Python 3.12 as the minimum supported version.
Prek¶
prek is fully compatible with pre-commit, so pre-commit can be used as well.
Install prek first, e.g. using uv or pipx:
uv tool install prek
Installing it globally avoids issues when prek invokes uv inside hooks.
Activate the prek hook:
prek install
This will automatically check code style, linting, and types before each commit.
Type suppressions must name the diagnostic for both checkers. When both tools report the same intentional violation, keep their targeted comments together:
value = untyped_value # type: ignore[assignment] # ty: ignore[invalid-assignment]
You can also run all checks on the repository manually:
prek run --all-files
Or you can run single hook:
prek run mypy --all-files
prek run ty --all-files
prek run pytest
prek run --hook-stage manual python-typing-update --all-files
Harper runs as a regular hook via uv (the harper-cli dev dependency), so no separate
installation is needed. Project vocabulary (tickers, broker names, identifiers) lives in
.harper-dictionary.txt; the Harper editor extension picks it up automatically. Words that the
dictionary cannot whitelist — Harper dialect-gates US spellings such as product names — are accepted
via regexes in .harper-ignore.txt.
Running linters and tests manually¶
You can also run linters and tests directly:
uv run pytest
uv run pytest -k <expr> -q # run subset
uv run ruff check .
uv run mypy cgt_calc tests scripts
uv run ty check cgt_calc tests scripts
Managing dependencies¶
You can manage dependencies either with uv commands or by editing pyproject.toml directly.
Add a new runtime dependency¶
uv add <package-name>
Add a development dependency¶
uv add --group dev <package-name>
Upgrade existing dependencies¶
uv lock --upgrade
uv sync
Manual changes¶
If you edit pyproject.toml manually (for example, to bump a version), run uv sync afterwards to
apply the changes and update uv.lock.
Updating the example report¶
To regenerate the example PDF report used in the docs, run:
./scripts/generate_example_report.sh
This writes docs/assets/example_report.pdf. Commit the updated file if your changes affect report
generation. The example source transactions and fixed exchange rates live in
scripts/example_data/, so generation is deterministic and does not call the exchange-rate API.
After regenerating the PDF, update the images made from it: the teaser the README shows and the full-page images the docs show:
./scripts/generate_example_preview.sh