Skip to content

Contributor Guide

How to set up a development environment, follow project conventions, write tests, and submit changes to kazunoko.


Development Setup

See Installation — For Developers for environment setup and prerequisites.

Quality checks:

uvx ruff check src/       # lint
uvx ruff format src/      # format
uvx mypy src/             # type check
uv run pytest             # tests with coverage
uvx pre-commit run --all-files  # all checks at once

Code Conventions

Variable naming

Context Convention Example
CLI / example scripts detector detector = connect(port, ...)
Library API device def measure(device: DeviceProtocol)
Output file path output or output_path output = Path("events.jsonl")
Input directory input_dir input_dir = Path("20241216/")
File handle fd with open(path) as fd:
File counter file_id generate_filename(mac, file_id=0)
Command object cmd cmd = Command(device)
Response object response response = cmd.status()
Response view view view = ResponseView(response)
List of DataFrames dataframes dataframes: list[pd.DataFrame]

Console output

import sys
from rich.console import Console

stdout = Console(file=sys.stdout)  # data output
stderr = Console(file=sys.stderr)  # status / logging
  • stdout: data only (JSONL, tables, final results)
  • stderr: user-facing status messages (stderr.print()) and structured logs (logger)

Color markup

Color Use
[cyan] Progress, info, verbose messages
[green] Success, completion
[red] Errors
[yellow] Warnings
[magenta] Table column styling

Comments

Write comments only when the why is non-obvious (a hidden constraint, a workaround, a subtle invariant). Do not describe what the code does — well-named identifiers do that.

Package vs module name

  • osechi-kazunoko — in pip install commands and PEP 723 dependencies blocks
  • kazunoko — in Python import statements

Testing

Tests live in tests/ and use pytest with coverage reporting.

uv run pytest             # run all tests
uv run pytest -v          # verbose output
uv run pytest tests/test_cli.py  # single file

Test philosophy

Prefer coverage of real behavior over exhaustive edge cases. The current suite (~12 tests across 3 modules) covers:

  • test_parser.pyparse_jsonl happy path and error cases
  • test_workspace.pyget_workspace() env var and generate_filename() format
  • test_cli.py — CLI smoke tests via CliRunner (--help, query, sync, generate)

Writing CLI tests

Use typer.testing.CliRunner with --mock so tests run without hardware:

from typer.testing import CliRunner
from kazunoko.cli import app

runner = CliRunner()

def test_status_mock():
    result = runner.invoke(app, ["--mock", "status"])
    assert result.exit_code == 0

Writing library tests

Use MockDevice as a drop-in for PortDevice:

from kazunoko import MockDevice, Command

def test_command_status():
    with MockDevice() as device:
        cmd = Command(device)
        response = cmd.status()
    assert response.status == "ok"

Commit Conventions

kazunoko uses Conventional Commits enforced by Commitizen via pre-commit hooks.

Format

<type>(<scope>): <description>

Common types:

Type Use
feat New feature
fix Bug fix
docs Documentation only
refactor Code change without feature or fix
test Tests only
chore Build, deps, config

Examples

feat(cli): add sync command for standalone RTC sync
fix(measure): handle EventTimeout during setup()
docs(design): add measurement layer design page
test(cli): add smoke tests for query and generate commands

Pre-commit hooks

The commitizen check hook validates commit message format on every commit. If it fails, rewrite the message to follow the format above.


Adding a New CLI Command

  1. Add a function to cli.py with @app.command().
  2. Use typer.Argument() for positional args and Options.* from options.py for flags.
  3. Use Command(device) for device operations.
  4. Use ResponseView + ResponseFormatter for output.
  5. Catch KazunokoError at the top level and print to stderr.
  6. Add a smoke test in tests/test_cli.py.
  7. Add a documentation page in docs/cli-guide/.

Extending the Command Class

  1. Add a method to Command in command.py.
  2. Call self._device.query(command_string) for request/response interactions.
  3. Call self._device.receive_response(field_type="event") for event reads.
  4. Do not validate input — the device validates and returns status: "error" if invalid.
  5. Do not catch exceptions — let them propagate to the caller.

Version Management

Do not bump versions without explicit user authorization.

When a release is requested, follow this order:

task bump:check          # preview next version (dry run)
task bump:patch          # or bump:minor / bump:major
task docs:release -- X.Y.Z  # create release notes from template
# fill in release notes, update zensical.toml nav, commit
task push:tags           # push commits and tags
task push:release -- X.Y.Z  # create GitLab release