Contributing¶
Setting up¶
Python 3.12 or later. Everything runs through uv.
The toolchain¶
make test # 597 tests
make lint # ruff, ty, pyrefly, mypy --strict, zuban --strict
make format # ruff format + --fix
Five type checkers, because they disagree and the disagreements are where the bugs are.
ty passed on code where mypy found eight real errors. mypy --strict finds one more
than zuban --strict. zuban can check examples/ where mypy cannot, because two
__main__.py files collide on module name.
Tests¶
A pyramid, and the directory decides the marker:
tests/a_unit/ pure functions, no I/O, no event loop (~0.3 s)
tests/b_integration/ widgets and apps through a UI
tests/c_e2e/ real programs on a pty
Conventions the existing tests follow:
- Name the behaviour.
test_a_listbox_scrolls_to_keep_the_selection_in_view. - Arrange, act, assert, separated by blank lines.
- Parametrize when it removes duplication.
- Assert on rendered output wherever that is possible.
House rules¶
Never silence a linter. No # noqa, no # type: ignore. If a rule is wrong for
this codebase, ignore it in ruff.toml with a one-line reason next to it. A growing
list of per-line suppressions means the types or the structure are wrong.
Comments say why. The code already says what.
Keyword-only arguments for anything after the required ones on a public function.
PLR0917 is deliberately not ignored, and it is what keeps this true.
Immutable by default. NamedTuples and frozen dataclasses for values; mutable
dataclasses only where something is genuinely assigned after construction (a Task, a
future's result).
Adding a widget¶
A widget is a function returning a View. There is no base class. See
the guide for the two conventions that matter:
the caller owns the state, and return a View unless the caller needs more than pixels.
Put pure helpers (width arithmetic, key tables, formatting) at module level so they can
be unit-tested without a UI. column_widths in widgets/table.py and
less_keys.action_for are the pattern.
Adding to the core¶
The bar is higher. Before adding to view.py or runtime.py, check whether an
application can do it. Prefer letting it, until a second application wants the same
thing.
Both ported applications needed screen
navigation; they needed different shapes, so termopy has open_modal and no Screen
class. One application would have produced an abstraction the second did not want.
The notes¶
notes/ holds the design record: a Pythonic-design review, a plan and verdict per port,
and lessons-learned.md. They are worth reading before proposing a change, because
several obvious-looking ideas are in there with a reason they were rejected.
If you find something that costs an hour to rediscover, add it.
Documentation¶
Built with Zensical:
The docs are their own project under docs/, with sources in docs/src/ and their own
Makefile:
make docs # or: cd docs && make build
make docs-serve # live reload
make docs-check # strict — warnings become errors
Zensical validates internal links. make docs reports a bad cross-reference as a
warning; make docs-check fails the build on it, which is what CI should run.