Skip to content

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 into BrokerTransaction values. Keep broker-specific columns and normalisation here, and keep tax matching outside the parsers.
  • cgt_calc/ingestion.py validates transactions and runs the first pass. It records dividends and interest for income.py to process, and delegates share-reorganisation planning to stock_split_planning.py.
  • cgt_calc/calculator_state.py holds PreparedHistory, which the first pass fills and the second pass only reads, and RunState, which the second pass builds and which is reset at the start of every calculation.
  • cgt_calc/matching.py runs the second pass, applying the same-day, bed-and-breakfast and Section 104 matching rules.
  • cgt_calc/model.py defines shared values and the report model. render_text.py and render_latex.py format that report without calculating it.
  • cgt_calc/main.py connects the stages behind CapitalGainsCalculator, whose calculate() runs the two passes in order. cgt_calc/cli.py handles 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