Custom lint checks for Beda Software projects — the conventions ruff and mypy don't cover. Each check is a standalone command and a pre-commit hook; pick the ones a project wants.
| Hook / command | Language | What it checks |
|---|---|---|
call-tree-order |
Python | Each function sits below its last caller (ORDER001) |
A module should read top-down like a call tree: the public entry point first, then its helpers in the order they're first used, each helper's own helpers below it. The check flags any function or method defined above a function that calls it:
app/orders.py:12: ORDER001 `_total` is defined above its caller `build_invoice` (line 30); move helpers below their last caller
What counts:
- A call or a reference to the function (
sorted(items, key=_key)) inside another function's body. - Methods are checked within their class, through
self.x,cls.xandClassName.x. - Functions that call each other in a cycle are not ordered among themselves.
- Decorators and default values run at definition time, so they are not counted as uses.
@propertygetters are read as attributes and never need to sit below their users.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/beda-software/beda-lint
rev: main
hooks:
- id: call-tree-order
# optional, for an existing codebase:
args: [--baseline, lint/order-baseline.txt]While beda-lint is in active development there are no release tags, so pin main. pre-commit warns about a mutable
rev and caches the first checkout; run pre-commit clean to pick up newer checks.
No install needed with uv:
uvx --from git+https://github.com/beda-software/beda-lint call-tree-order src testsOr add it as a dev dependency (uv add --dev git+https://github.com/beda-software/beda-lint, or the poetry
equivalent) and call call-tree-order <paths> from a Makefile or CI.
A legacy codebase usually has hundreds of violations. Rather than reorder everything at once, record the existing ones in a baseline file: the check then skips them and fails only on new violations, and old modules get fixed as they're touched.
-
Create the baseline from the folder you will always run the check from (usually the project root), over every path the check will cover:
uvx --from git+https://github.com/beda-software/beda-lint \ call-tree-order src tests --baseline lint/order-baseline.txt --update-baseline
It writes one
path::symbolline per existing violation, sorted, for example:src/billing/invoices.py::_total src/billing/invoices.py::InvoiceBuilder._add_line -
Commit the baseline next to the code.
-
Wire the check in with the same
--baselinepath — the pre-commit hook above, or a Makefile/CI step:call-tree-order src tests --baseline lint/order-baseline.txt
-
Shrink it over time. After moving some helpers below their callers, rerun step 1 and commit the shorter file. Always regenerate over the whole tree, never from pre-commit: pre-commit passes only the changed files, so a baseline written from them would drop every other entry.
How entries match:
- No line numbers: a baselined function stays skipped when code above it changes, or when it moves within its file.
- A new violation in a baselined file still fails, and so does a baselined function once it is renamed or its file moves.
- Paths are relative to the working directory, so create and check the baseline from the same folder.
uv sync
uv run pytest
uv run ruff format --check . && uv run ruff check . && uv run mypyA new check goes in beda_lint/<check>.py with a main(), a [project.scripts] entry in pyproject.toml, a hook
in .pre-commit-hooks.yaml, tests in tests/, and a row in the table above.