- Run autoformatters and linters via
tox. Never invokeflake8,isort,mypy,vulture,pylint(etc.) directly - go through their respectivetox -e <env>invocation so dependency versions stay pinned. - Exception: do NOT run
tox -e flake8ortox -e vulture(or those binaries directly). They hang indefinitely inside Cursor's terminal for an unidentified reason - rely on CI to catch any violations. - Run tests with
tox -e py [-- <pytest flags like -k>...]. Do NOT runpytest,python -m pytest, or any tox-built venv's pytest binary directly. Tox'spassenvshielding is what keeps test runs deterministic- bypassing it lets dev-shell env vars (e.g.
GIT_MACHETE_DIFF_OPTS) leak into the test process and produce failures that don't reproduce in CI.
- bypassing it lets dev-shell env vars (e.g.
- Use
tox -e py(the full suite, several minutes) sparingly - by default scope it to the most likely affected tests via-k,-m, or explicit file/class/method selectors, e.g.tox -e py -- tests/test_cli.py tests/test_anno.py -k mutex. Only fall back to the full suite when the change is wide in scope (parser refactors, shared fixtures, base classes, ...) or when a scoped run already passes and you want a final pre-push sanity check. - On macOS, skip the zsh subset of completion E2E tests (flaky on Mac):
tox -e test-completions -- -k "not zsh". CI runs the full set on Linux.
- Don't
git commitorgit pushunless explicitly asked.
- Skip tests that require a minimum Git version with
@pytest.mark.skipif(get_git_version() < (X, Y), reason="..."), not with anif get_git_version() < (X, Y): returnearly-return at the top of the test body. The decorator surfaces the skip in pytest's report (and in the JUnit XML CI uploads); the early-return silently masquerades as a pass. - Prefer
assert_success(cmd_and_args, expected_output)over a barelaunch_command(cmd_and_args)whenever a command is run for its effect. A barelaunch_commandthrows the command's entire output on the floor, so any regression in what it prints (a reworded message, a wrong/missing branch name, a stray extra line, a wrong singular/plural) goes completely unnoticed - the call still "passes".assert_successpins the whole output down, so it doubles as the regression test for that output. Reserve a barelaunch_commandfor the cases where asserting the full output is genuinely troublesome (non-deterministic output such as commit hashes or--verbose/git-subprocess noise leaking into the captured text), and even then prefer asserting whatever deterministic prefix/suffix you can. - Assert on the full command output rather than substring presence.
Use
assert_success(cmd_and_args, expected_output)(or, for ANSI/colored runs where--color=alwaysproduces escape codes,raw_output = launch_command(...); assert raw_output == expected_ansi) so the whole rendering is pinned down at once. Avoidassert "<phrase>" in output/assert "<phrase>" not in outputfor command-output checks - a substring match silently tolerates stray extra lines, misordered sections, the same phrase landing on the wrong row, or new (unintended) labels appearing elsewhere, all of which a full-output equality assertion would catch on the first run. Exception: substring checks are legitimate when the asserted invariant is genuinely scoped to one fragment of the output (e.g. "this warning text appears somewhere") and the rest of the output is either non-deterministic or already covered by another test.
- Always use absolute imports (
from git_machete.<...> import ...,from tests.<...> import ...) - never relative ones (from .x import y,from ..x import y, ...), not even for siblings inside the same package and not even inside a function body. Absolute imports are easier to follow when reading code in isolation (no need to know the file's package) and survive moving a file across packages without a silent semantic change. Enforced byci/checks/prohibit-relative-imports.sh.
- Use f-strings for interpolation - never
str.format()(or%-formatting). An f-string keeps the value next to its placeholder, so the whole string can be read in one place, whereas a.format()template hides the arguments in a separate call below.flake8-string-format(FS003) also flags any{...}placeholder living in a string without anfprefix, so a.format()template trips the linter even when the interpolation is actually supplied later. If you genuinely need a reusable template parameterized at several call sites, wrap an f-string in a small helper function (def msg(x: str) -> str: return f"... {x} ...") rather than reaching for.format().
- Don't add code comments that narrate test-harness internals or explain why an assertion's expected value was massaged
to match the harness's output (e.g. "backticks are stripped by
_fmtin ASCII mode, so the expected string omits them", "the harness lower-cases this, so we compare against lower-case", etc.). If the actual and expected values match, the assertion already documents itself; if they don't, fix the production code or the harness, don't annotate the workaround. This generalizes: avoid comments that exist solely to justify a specific literal in a test - the test name and the assertion are the contract.
- No trailing whitespace on any line.
- Always leave a single newline at the end of every file.
- Always use American English spelling (
color,behavior,honor,organize,modeled,normalized,unrecognized, ...) - never the British variants (colour,behaviour,honour,organise,modelled,normalised,unrecognised, ...). Applies to code, identifiers, comments, docstrings, Markdown, commit messages and PR descriptions. - Don't hard-wrap prose (code comments, docstrings, Markdown, commit messages, PR descriptions) at 80 or so columns -
the line-length limit is 140 (see
[flake8]intox.ini). Break on sentence or clause boundaries instead, so each line carries one thought rather than a fragment chopped by column count. Prefer one sentence per line; for a sentence that exceeds 140 columns, split at a natural clause boundary (semicolons, parentheticals, conjunctions, ...). - This matters especially for PR descriptions (and commit message bodies): never insert mid-sentence line breaks at a fixed column. Write them in natural flow - one sentence per line, exactly like code comments - and let GitHub / the git client wrap them for display. Note that a PR opened from a single commit inherits that commit's message verbatim, so a hard-wrapped commit body produces a hard-wrapped PR description; keep the commit body unwrapped too.