tempest-cli 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/CHANGELOG.md +52 -0
  2. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/PKG-INFO +6 -5
  3. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/README.md +5 -4
  4. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/changelog.en.md +36 -1
  5. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/changelog.md +34 -1
  6. tempest_cli-0.6.0/docs/commands.en.md +303 -0
  7. tempest_cli-0.6.0/docs/commands.md +299 -0
  8. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/installation.en.md +3 -2
  9. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/installation.md +3 -2
  10. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/library.en.md +3 -2
  11. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/library.md +3 -2
  12. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/pyproject.toml +1 -1
  13. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/__init__.py +1 -1
  14. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/lint.py +164 -51
  15. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/main.py +67 -31
  16. tempest_cli-0.6.0/tempest_cli/residuals.py +356 -0
  17. tempest_cli-0.6.0/tempest_cli/rewrite.py +417 -0
  18. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_extra_steps.py +1 -1
  19. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_fast.py +82 -9
  20. tempest_cli-0.6.0/tests/test_fix_residuals.py +194 -0
  21. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_lint_runners.py +4 -3
  22. tempest_cli-0.6.0/tests/test_rewrite.py +222 -0
  23. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/uv.lock +1 -1
  24. tempest_cli-0.5.0/docs/commands.en.md +0 -159
  25. tempest_cli-0.5.0/docs/commands.md +0 -156
  26. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/.github/workflows/ci.yml +0 -0
  27. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/.github/workflows/docs.yml +0 -0
  28. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/.github/workflows/release-pypi.yml +0 -0
  29. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/.gitignore +0 -0
  30. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/LICENSE +0 -0
  31. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/Makefile +0 -0
  32. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/configuration.en.md +0 -0
  33. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/configuration.md +0 -0
  34. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/index.en.md +0 -0
  35. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/index.md +0 -0
  36. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/pr-prompt.en.md +0 -0
  37. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/pr-prompt.md +0 -0
  38. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/reference.en.md +0 -0
  39. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/reference.md +0 -0
  40. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/rules.en.md +0 -0
  41. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/docs/rules.md +0 -0
  42. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/mkdocs.yml +0 -0
  43. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
  44. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
  45. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/config.py +0 -0
  46. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/pr_prompt.py +0 -0
  47. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/py.typed +0 -0
  48. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tempest_cli/steps.py +0 -0
  49. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/__init__.py +0 -0
  50. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_cli.py +0 -0
  51. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_config.py +0 -0
  52. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_lint_strictness.py +0 -0
  53. {tempest_cli-0.5.0 → tempest_cli-0.6.0}/tests/test_pr_prompt.py +0 -0
@@ -5,6 +5,58 @@ All notable changes to **tempest-cli** are listed below.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.6.0] — 2026-10-20
11
+
12
+ ### Changed
13
+
14
+ - **`test` and `check` run the suite in parallel by default.** The
15
+ pytest-xdist run that needed `--fast` (`-n auto -p no:cacheprovider`) is
16
+ now what a bare `tempest-cli test` does; `--serial` opts out (for
17
+ `--lf` / `--ff`, or a debugger), and `--workers` tunes the default
18
+ without a flag. Measured on a FastAPI service suite of 1136 tests on a
19
+ 12-CPU machine: 262 s serially, 37.6 s in parallel, same outcome.
20
+ - **Without pytest-xdist the default degrades instead of failing.** When
21
+ the environment pytest runs in cannot import it, a bare `test` / `check`
22
+ prints a one-line notice naming the install command and runs the suite
23
+ serially (exit code is pytest's); an explicit `--fast` keeps the old
24
+ behavior — the fix is printed and the exit code is 127, nothing runs.
25
+ `run_pytest` / `run_full_check` take `require_xdist=` (default `True`,
26
+ the old library behavior) to pick between the two.
27
+ - `--workers` with `--serial` is a usage error (exit 2), as `--workers`
28
+ without `--fast` used to be.
29
+
30
+ ## [0.5.1] — 2026-10-15
31
+
32
+ ### Fixed
33
+
34
+ - **`fix` explains the E501 the formatter cannot resolve.** A line still
35
+ too long after `ruff format` is one the formatter has nowhere to break
36
+ — a dotted name (it never splits one, and drops parentheses that do not
37
+ make the line fit), a string literal, a comment — but the bare
38
+ `Line too long (94 > 79)` read as if `line-length` were being ignored.
39
+ `fix` now reads ruff's JSON report of the leftovers and names each such
40
+ line with the overflowing token, its kind and the action that does
41
+ resolve it, then splits the residual violations into those with no
42
+ autofix and those `fix --unsafe` would fix. The exit code is unchanged:
43
+ CI still fails on them. Closes #3.
44
+ - **`fix` shortens the long comments and strings the formatter leaves
45
+ alone.** After `ruff format`, each E501 that is a comment is wrapped at
46
+ the limit (a trailing one moves above its line) and each that is a
47
+ plain string literal is split at its spaces into implicitly
48
+ concatenated parts inside parentheses; the touched files are formatted
49
+ again. Every edit is kept only when the file's AST is identical before
50
+ and after, so the program cannot change. Tool pragmas (`noqa`,
51
+ `type: ignore`, `fmt:`, …), f-strings, triple-quoted strings and words
52
+ longer than a line are left alone and explained instead. On by
53
+ default: neither rewrite changes behavior.
54
+ - **`fix` reports on the formatted file.** The passes are now
55
+ `ruff check --fix-only`, `ruff format`, then `ruff check`; before, the
56
+ report and the exit code came from the lint pass that ran *before* the
57
+ formatter, so a violation the formatter went on to resolve still showed
58
+ up and still failed the command.
59
+
8
60
  ## [0.5.0] — 2026-10-13
9
61
 
10
62
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tempest-cli
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Framework-agnostic quality gate for Python projects: ruff + mypy + pytest behind one command, with a typing-strictness dial and a PR-description prompt generator.
5
5
  Project-URL: Homepage, https://github.com/mauriciobenjamin700/tempest-cli
6
6
  Project-URL: Repository, https://github.com/mauriciobenjamin700/tempest-cli
@@ -70,12 +70,12 @@ remembered to pass.
70
70
  | Command | Runs |
71
71
  | --- | --- |
72
72
  | `tempest-cli lint` | `ruff check` |
73
- | `tempest-cli fix` | `ruff check --fix` then `ruff format` (`--unsafe` for the risky autofixes) |
73
+ | `tempest-cli fix` | `ruff check --fix-only`, `ruff format`, wraps long comments and splits long strings the formatter leaves alone, then a final `ruff check` that explains what is left (`--unsafe` for the risky autofixes) |
74
74
  | `tempest-cli format` | `ruff format` (writes) |
75
75
  | `tempest-cli fmt-check` | `ruff format --check` (read-only) |
76
76
  | `tempest-cli type` | `mypy` |
77
- | `tempest-cli test` | `pytest` (`--fast` spreads it across cores with pytest-xdist: `-n auto -p no:cacheprovider`, `-w N` to choose) |
78
- | `tempest-cli check` | all four, in order, stopping at the first failure (`--fast` runs the test step in parallel; a host CLI can add steps between `mypy` and `pytest`) |
77
+ | `tempest-cli test` | `pytest`, in parallel by default with pytest-xdist (`-n auto -p no:cacheprovider`, `-w N` to choose; serial with a notice when it is not installed; `--serial` opts out) |
78
+ | `tempest-cli check` | all four, in order, stopping at the first failure (the test step in parallel as in `test`; a host CLI can add steps between `mypy` and `pytest`) |
79
79
  | `tempest-cli pr-prompt` | builds the prompt that makes an AI write this branch's PR description |
80
80
 
81
81
  Every command takes an optional path (`tempest-cli lint src/`) and
@@ -188,7 +188,8 @@ your suite. Add them yourself, or take the bundle:
188
188
  uv add --dev "tempest-cli[tools]" # mypy + pytest + pytest-xdist
189
189
  ```
190
190
 
191
- `pytest-xdist` is what `test --fast` runs the suite with; its
191
+ `pytest-xdist` is what `test` runs the suite in parallel with, by
192
+ default (without it the suite runs serially, with a notice); its
192
193
  `requires-dist` (`execnet>=2.1`, `pytest>=7.0.0`) carries no upper bound.
193
194
 
194
195
  Whatever you pin wins over the bundled ruff. The lookup runs in this
@@ -38,12 +38,12 @@ remembered to pass.
38
38
  | Command | Runs |
39
39
  | --- | --- |
40
40
  | `tempest-cli lint` | `ruff check` |
41
- | `tempest-cli fix` | `ruff check --fix` then `ruff format` (`--unsafe` for the risky autofixes) |
41
+ | `tempest-cli fix` | `ruff check --fix-only`, `ruff format`, wraps long comments and splits long strings the formatter leaves alone, then a final `ruff check` that explains what is left (`--unsafe` for the risky autofixes) |
42
42
  | `tempest-cli format` | `ruff format` (writes) |
43
43
  | `tempest-cli fmt-check` | `ruff format --check` (read-only) |
44
44
  | `tempest-cli type` | `mypy` |
45
- | `tempest-cli test` | `pytest` (`--fast` spreads it across cores with pytest-xdist: `-n auto -p no:cacheprovider`, `-w N` to choose) |
46
- | `tempest-cli check` | all four, in order, stopping at the first failure (`--fast` runs the test step in parallel; a host CLI can add steps between `mypy` and `pytest`) |
45
+ | `tempest-cli test` | `pytest`, in parallel by default with pytest-xdist (`-n auto -p no:cacheprovider`, `-w N` to choose; serial with a notice when it is not installed; `--serial` opts out) |
46
+ | `tempest-cli check` | all four, in order, stopping at the first failure (the test step in parallel as in `test`; a host CLI can add steps between `mypy` and `pytest`) |
47
47
  | `tempest-cli pr-prompt` | builds the prompt that makes an AI write this branch's PR description |
48
48
 
49
49
  Every command takes an optional path (`tempest-cli lint src/`) and
@@ -156,7 +156,8 @@ your suite. Add them yourself, or take the bundle:
156
156
  uv add --dev "tempest-cli[tools]" # mypy + pytest + pytest-xdist
157
157
  ```
158
158
 
159
- `pytest-xdist` is what `test --fast` runs the suite with; its
159
+ `pytest-xdist` is what `test` runs the suite in parallel with, by
160
+ default (without it the suite runs serially, with a notice); its
160
161
  `requires-dist` (`execnet>=2.1`, `pytest>=7.0.0`) carries no upper bound.
161
162
 
162
163
  Whatever you pin wins over the bundled ruff. The lookup runs in this
@@ -3,6 +3,41 @@
3
3
  The full history lives in the repository's
4
4
  [`CHANGELOG.md`](https://github.com/mauriciobenjamin700/tempest-cli/blob/main/CHANGELOG.md).
5
5
 
6
+ ## [0.6.0] — 2026-10-20
7
+
8
+ ### Changed
9
+
10
+ - **`test` and `check` run the suite in parallel by default.** The
11
+ pytest-xdist `-n auto` run that needed `--fast` is now what a bare
12
+ `test` does; `--serial` opts out (for `--lf` / `--ff`, or a debugger).
13
+ Measured on a FastAPI suite of 1136 tests on 12 CPUs: 262 s serially,
14
+ 37.6 s in parallel, same outcome.
15
+ [Details](commands.md#the-suite-in-parallel-the-default).
16
+ - **Without pytest-xdist the default degrades to serial instead of
17
+ failing**, with a notice naming the install command; only an explicit
18
+ `--fast` still requires the plugin (exit code 127).
19
+ - `--workers` with `--serial` is a usage error (exit code 2).
20
+
21
+ ## [0.5.1] — 2026-10-15
22
+
23
+ ### Fixed
24
+
25
+ - **`fix` explains the E501 the formatter cannot resolve.** A line still
26
+ too long after `ruff format` (a dotted name, a string, a comment) comes
27
+ out with its token, its kind and the action that resolves it, and the
28
+ summary separates what has no autofix from what `--unsafe` would fix.
29
+ The exit code is unchanged.
30
+ [Details](commands.md#an-e501-ruff-format-cannot-fix).
31
+ - **`fix` shortens the long comments and strings the formatter leaves
32
+ alone.** A comment is wrapped at the limit and a string is split into
33
+ concatenated parts; each edit is kept only when the file's AST is
34
+ identical before and after. Pragmas, f-strings, triple quotes and long
35
+ words are left out and explained. On by default.
36
+ - **`fix` reports on the formatted file.** The passes are now
37
+ `ruff check --fix-only`, `ruff format` and `ruff check`; before, the
38
+ report and the exit code came from the pass that ran before the
39
+ formatter.
40
+
6
41
  ## [0.5.0] — 2026-10-13
7
42
 
8
43
  ### Added
@@ -26,7 +61,7 @@ The full history lives in the repository's
26
61
  the default — or `logical`) and is a usage error without `--fast`. The
27
62
  target is forwarded and pytest's exit code comes back untranslated. It
28
63
  is a flag, not a subcommand, because `test fast` keeps meaning the
29
- `fast/` folder. [Details](commands.md#the-suite-in-parallel-fast).
64
+ `fast/` folder. [Details](commands.md#the-suite-in-parallel-the-default).
30
65
  - **A missing pytest-xdist is a sentence, not `unrecognized arguments:
31
66
  -n`.** `--fast` asks the interpreter that will run pytest whether it
32
67
  can import `xdist`; when it cannot, the message names the package and
@@ -4,6 +4,39 @@ O histórico completo vive no
4
4
  [`CHANGELOG.md`](https://github.com/mauriciobenjamin700/tempest-cli/blob/main/CHANGELOG.md)
5
5
  do repositório.
6
6
 
7
+ ## [0.6.0] — 2026-10-20
8
+
9
+ ### Alterado
10
+
11
+ - **O `test` e o `check` rodam a suíte em paralelo por padrão.** O
12
+ `-n auto` do pytest-xdist, que pedia `--fast`, agora é o que um
13
+ `test` puro faz; `--serial` desliga (para `--lf` / `--ff` ou depurador).
14
+ Medido numa suíte FastAPI de 1136 testes em 12 CPUs: 262 s em série,
15
+ 37,6 s em paralelo, mesmo resultado.
16
+ [Detalhes](commands.md#a-suite-em-paralelo-o-padrao).
17
+ - **Sem o pytest-xdist, o padrão cai para série em vez de falhar**, com um
18
+ aviso de como instalar; só o `--fast` explícito continua exigindo o
19
+ plugin (código de saída 127).
20
+ - `--workers` com `--serial` é erro de uso (código de saída 2).
21
+
22
+ ## [0.5.1] — 2026-10-15
23
+
24
+ ### Corrigido
25
+
26
+ - **O `fix` explica o E501 que o formatador não resolve.** Linha ainda
27
+ longa depois do `ruff format` (nome pontuado, string, comentário) sai
28
+ com o token, o tipo e a ação que resolve, e o resumo separa o que não
29
+ tem autofix do que o `--unsafe` resolveria. O código de saída não muda.
30
+ [Detalhes](commands.md#e501-que-o-ruff-format-nao-resolve).
31
+ - **O `fix` encurta comentário e string longos que o formatador não
32
+ quebra.** Comentário é quebrado no limite e string é partida em pedaços
33
+ concatenados; cada edição só fica se a AST do arquivo for idêntica
34
+ antes e depois. Pragma, f-string, três aspas e palavra longa ficam de
35
+ fora e são explicados. Ligado por padrão.
36
+ - **O `fix` relata sobre o arquivo formatado.** As passadas agora são
37
+ `ruff check --fix-only`, `ruff format` e `ruff check`; antes, o
38
+ relatório e o código de saída vinham da passada anterior ao formatador.
39
+
7
40
  ## [0.5.0] — 2026-10-13
8
41
 
9
42
  ### Adicionado
@@ -27,7 +60,7 @@ do repositório.
27
60
  o padrão — ou `logical`) e é erro de uso sem `--fast`. O alvo é
28
61
  repassado e o código de saída do pytest volta sem tradução. É flag, e
29
62
  não subcomando, porque `test fast` continua significando a pasta
30
- `fast/`. [Detalhes](commands.md#a-suite-em-paralelo-fast).
63
+ `fast/`. [Detalhes](commands.md#a-suite-em-paralelo-o-padrao).
31
64
  - **pytest-xdist ausente vira frase, não `unrecognized arguments: -n`.**
32
65
  O `--fast` pergunta ao interpretador que vai rodar o pytest se ele
33
66
  importa o `xdist`; faltando, a mensagem nomeia o pacote e os extras
@@ -0,0 +1,303 @@
1
+ # Commands
2
+
3
+ Eight commands. All take an optional path and return the underlying
4
+ tool's exit code.
5
+
6
+ | Command | Runs |
7
+ | --- | --- |
8
+ | `lint` | `ruff check` |
9
+ | `fix` | `ruff check --fix-only`, `ruff format`, shortens long comments/strings, then a final `ruff check` explaining what is left |
10
+ | `format` | `ruff format` (writes) |
11
+ | `fmt-check` | `ruff format --check` (read-only) |
12
+ | `type` | `mypy` |
13
+ | `test` | `pytest` |
14
+ | `check` | the four above, in order, stopping at the first failure (a CLI that mounts the gate can [add steps](library.md#adding-steps-to-check) between `mypy` and `pytest`) |
15
+ | `pr-prompt` | builds the PR-description prompt — [its own page](pr-prompt.md) |
16
+
17
+ !!! tip "`tc` is the short form"
18
+ The package installs `tempest-cli` and `tc` pointing at the same
19
+ program. The examples use the long name; `tc check`, `tc fix` and
20
+ `tc type -s strict` work exactly the same.
21
+
22
+ ## The full gate
23
+
24
+ ```bash
25
+ tempest-cli check # the whole project
26
+ tempest-cli check src/ # a single path
27
+ tempest-cli check -s strict # with raised typing strictness for this run
28
+ ```
29
+
30
+ The order is deliberate: **lint → formatting → types → tests**. What
31
+ fails fastest and is cheapest to fix comes first, and the run stops at
32
+ the first failure — there is no point running the whole suite when the
33
+ imports are unsorted.
34
+
35
+ ## Fixing what can be fixed
36
+
37
+ ```bash
38
+ tempest-cli fix # safe autofixes + formatting
39
+ tempest-cli fix --unsafe # also ruff's risky autofixes
40
+ ```
41
+
42
+ `fix` makes four passes:
43
+
44
+ 1. `ruff check --fix-only` — sorts and dedupes imports, drops unused
45
+ ones, normalizes quotes.
46
+ 2. `ruff format` — indentation, line length, blank lines.
47
+ 3. Shortens the long lines the formatter cannot break, when that can be
48
+ done without changing the program: comments and strings (see below).
49
+ 4. `ruff check` — the report of what is left, on the final file.
50
+
51
+ The exit code is the last pass's: anything left, `fix` exits with an
52
+ error and CI fails.
53
+
54
+ !!! warning "`--unsafe` can change behavior"
55
+ Ruff's "unsafe" autofixes are the ones that may alter semantics.
56
+ They are off by default. Turned them on? Read the `git diff` before
57
+ committing.
58
+
59
+ ### An E501 `ruff format` cannot fix
60
+
61
+ Sometimes `fix` ends like this, even with the right `line-length` in
62
+ `pyproject.toml`:
63
+
64
+ ```python
65
+ class AloController:
66
+ def template(self, kind: str) -> str:
67
+ match kind:
68
+ case "waiting":
69
+ user_template = notifications.NOTIFICATION_USER_BODY_ALO_STATUS_CHANGE_WAITING
70
+ return user_template
71
+ return ""
72
+ ```
73
+
74
+ ```text
75
+ E501 Line too long (94 > 79)
76
+ --> controller.py:5:80
77
+ ```
78
+
79
+ It is not a wrong setting — `fix` passes no `line-length` to ruff, the
80
+ project's value already applies. It is a limit of the formatter: it
81
+ **never splits a dotted name** (`a.b.C`), and the parentheses you put
82
+ around it (`x = (\n a.b.C\n)`) go away, because they do not make the
83
+ line fit. 🤷
84
+
85
+ #### What `fix` shortens on its own
86
+
87
+ Two of these cases have a single right answer, and `fix` applies it:
88
+
89
+ - a **comment** is wrapped at the limit; a trailing one moves above the
90
+ code it annotates;
91
+ - a **string** is split at its spaces into concatenated parts inside
92
+ parentheses — Python joins them back into the same value.
93
+
94
+ ```python
95
+ # before
96
+ text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbb" # the text
97
+
98
+ # after fix
99
+ # the text
100
+ text = (
101
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
102
+ "bbbbbbbbbbbbbbbb"
103
+ )
104
+ ```
105
+
106
+ ```text
107
+ shortened 1 long line(s) ruff format cannot break (comments wrapped, strings split into concatenated parts):
108
+ mod.py:2 [comment+string]
109
+ ```
110
+
111
+ !!! check "Only what is proven equal goes in"
112
+ Before writing, `fix` compares the file's AST before and after.
113
+ Comments never reach the AST and the parser folds concatenated
114
+ strings into one constant, so a correct rewrite always passes.
115
+ Anything else — a `\N{...}` cut in half, a comment pushed into a
116
+ `\` continuation — changes the AST, and the line stays as it was.
117
+
118
+ Left out on purpose: tool comments (`noqa`, `type: ignore`, `fmt:`,
119
+ `pragma`…), which must stay on the line they govern; f-strings and
120
+ triple-quoted strings; and a word longer than a line (a URL, say).
121
+
122
+ #### What is left
123
+
124
+ Dotted names, long names and expressions have no mechanical rewrite —
125
+ and splitting them by hand is undone by the next `ruff format`. For
126
+ those, `fix` explains each line right below ruff's report:
127
+
128
+ ```text
129
+ 1 line(s) still too long after ruff format — the line-length setting was applied; the formatter has nowhere to break these:
130
+ controller.py:5 [dotted-name] `notifications.NOTIFICATION_USER_BODY_ALO_STATUS_CHANGE_WAITING`
131
+ ruff format never splits a dotted name, and drops parentheses that do not make the line fit. Shorten the expression: import or alias the attribute, move the choice into a table or constant, or lower the indentation.
132
+ 1 residual violation(s): 1 with no autofix (edit by hand).
133
+ ```
134
+
135
+ The bracketed label says what sits where the line crosses the limit:
136
+
137
+ | Label | What it is | What resolves it |
138
+ | --- | --- | --- |
139
+ | `dotted-name` | a dotted name (`module.CONSTANT`) | an alias, a table/constant, less indentation |
140
+ | `name` | one name that is too long on its own | rename it or indent less |
141
+ | `string` | a string `fix` could not split (f-string, triple quotes, long word) | split it by hand into concatenated parts |
142
+ | `comment` | a comment `fix` could not wrap (pragma, long word) | wrap it by hand or move it into the docstring |
143
+ | `expression` | anything else | extract a variable or indent less |
144
+
145
+ The last line separates what has **no autofix** from what `fix --unsafe`
146
+ would resolve — without it, `Found 5 errors` does not say whether
147
+ another run is worth it.
148
+
149
+ !!! tip "Move the choice into a table"
150
+ In the example above, the indentation comes from the `match`.
151
+ Replacing it with a module-level dictionary removes four levels and
152
+ the line fits in exactly 79, without an alias:
153
+
154
+ ```python
155
+ import notifications
156
+
157
+ TEMPLATES: dict[str, str] = {
158
+ "waiting": notifications.NOTIFICATION_USER_BODY_ALO_STATUS_CHANGE_WAITING,
159
+ }
160
+
161
+
162
+ class AloController:
163
+ def template(self, kind: str) -> str:
164
+ return TEMPLATES.get(kind, "")
165
+ ```
166
+
167
+ !!! note "What about `# noqa: E501`?"
168
+ It works, but hides the problem until the next refactor drops the
169
+ `noqa` — which is exactly how this case showed up. Prefer changing
170
+ the shape of the code.
171
+
172
+ ## One at a time
173
+
174
+ ```bash
175
+ tempest-cli lint src/
176
+ tempest-cli fmt-check
177
+ tempest-cli type mypackage
178
+ tempest-cli test tests/unit
179
+ ```
180
+
181
+ `test` forwards the path to pytest as a filter; with no argument it runs
182
+ the whole suite.
183
+
184
+ ## The suite in parallel (the default)
185
+
186
+ A suite whose tests are already isolated (a database per test, no global
187
+ state) pays the serial time and buys nothing for it. That is why `test`
188
+ and `check` spread the suite across cores with
189
+ [pytest-xdist](https://pytest-xdist.readthedocs.io/) **without being
190
+ asked**:
191
+
192
+ ```bash
193
+ tempest-cli test # -n auto: one worker per core
194
+ tempest-cli test -w 4 # four workers
195
+ tempest-cli test tests/unit # the target is still forwarded
196
+ tempest-cli check # the full gate, with the test step in parallel
197
+ tempest-cli test --serial # a single process (for --lf / --ff, or a debugger)
198
+ ```
199
+
200
+ Underneath it becomes `pytest -n <workers> -p no:cacheprovider [target]`.
201
+ `-p no:cacheprovider` is there because several workers writing
202
+ `.pytest_cache` at once is a race that buys nothing — and that cache is
203
+ what `--lf` / `--ff` read, so those two are a `--serial` affair.
204
+
205
+ On a FastAPI service suite of 1136 tests (an in-memory SQLite database
206
+ per test), on a 12-CPU machine: **262 s** serially, **37.6 s** with
207
+ `-n auto` — same outcome, 1136 passed.
208
+
209
+ `--workers` (`-w`) takes what `pytest -n` takes: an integer, `auto` (the
210
+ default) or `logical`. With `--serial` it is a usage error (exit 2)
211
+ instead of being silently ignored.
212
+
213
+ !!! info "`auto` counts physical cores when `psutil` is installed"
214
+ pytest-xdist decides: with `psutil` importable, `auto` is the number of
215
+ **physical** cores; without it, the number of logical CPUs. On a
216
+ 6-core, 12-thread machine with `psutil` in the environment, `auto`
217
+ reported `created: 6/6 workers`. Want the 12 threads? `-w logical` or
218
+ `-w 12`.
219
+
220
+ !!! note "Why a flag, and not `tempest-cli test serial`"
221
+ `test` already takes a positional path. `test fast` keeps meaning "run
222
+ pytest on the `fast/` folder" — a subcommand by that name would break
223
+ anyone who has that folder.
224
+
225
+ ### Without pytest-xdist
226
+
227
+ Before running anything, the CLI asks **the very interpreter that will
228
+ run pytest** whether it can import `xdist` — not the CLI's own
229
+ interpreter, which under `uv tool install` or pipx lives in another
230
+ environment. When it is missing, it depends on who asked for parallel:
231
+
232
+ - **Nobody (the default):** the suite runs serially, with a notice on
233
+ stderr naming what to install. `tempest-cli test` keeps working in a
234
+ project that never installed pytest-xdist — it is just slower.
235
+
236
+ ```console
237
+ $ tempest-cli test
238
+ note: pytest-xdist is not installed in the environment pytest runs in (.venv/bin/pytest), so the suite runs serially. Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — to run it in parallel.
239
+ ```
240
+
241
+ - **`--fast` by name:** parallel becomes a requirement. The message names
242
+ what to install and the exit code is **127**, with nothing run:
243
+
244
+ ```console
245
+ $ tempest-cli test --fast
246
+ error: --fast needs pytest-xdist, which is not installed in the environment pytest runs in (.venv/bin/pytest). Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — and retry, or pass --serial to run the suite serially.
247
+ $ echo $?
248
+ 127
249
+ ```
250
+
251
+ Under `check` the probe happens **before** the first step: with `--fast`
252
+ there is no point waiting for lint and mypy to finish only to learn the
253
+ tests will not run.
254
+
255
+ ### A test that fails only in parallel
256
+
257
+ Parallelism exposes tests that depend on order or on an idle machine: two
258
+ tests writing the same file, the same port, a module-level global, a
259
+ fixed `sleep` waiting for something that takes longer with every core
260
+ busy. Before treating the failure as a regression, run the test **alone
261
+ and serially**:
262
+
263
+ ```bash
264
+ tempest-cli test --serial "tests/test_scheduler.py::test_lease_expires"
265
+ ```
266
+
267
+ - **Passes alone**: the defect is the test's isolation, not the change
268
+ under review. Fix the test (a file under `tmp_path`, a free port, wait
269
+ on a condition instead of `sleep`).
270
+ - **Fails alone too**: it is a real regression.
271
+
272
+ ## What each returns
273
+
274
+ The exit code is the tool's own, untranslated:
275
+
276
+ ```bash
277
+ tempest-cli lint; echo "exited $?"
278
+ ```
279
+
280
+ There is exactly one code of its own: **127**, when the tool is in none
281
+ of the places the CLI looks — the run's environment, `PATH`, `uv run
282
+ --with` — and the message then names what was missing and how to install
283
+ it. The lookup order is in
284
+ [Installation](installation.md#where-ruff-mypy-and-pytest-come-from).
285
+ The same 127 comes out of an explicit `--fast` when pytest-xdist is not in
286
+ pytest's environment; without the flag, its absence only makes the suite
287
+ serial.
288
+
289
+ ## Recap
290
+
291
+ - `check` is lint + fmt-check + type + test, in order, stopping at the
292
+ first failure.
293
+ - `fix` is the pass that repairs; `--unsafe` only when you will review.
294
+ - Long comments and strings `fix` shortens on its own, with the AST
295
+ proven equal. An E501 left after it has no mechanical rewrite: `fix`
296
+ names the token and the action — change the code's shape.
297
+ - `test` and `check` run the suite in parallel by default, with
298
+ pytest-xdist (`-n auto`, `-w N` to choose); `--serial` turns it off.
299
+ Without pytest-xdist the default falls back to serial with a notice,
300
+ and an explicit `--fast` becomes exit 127. A test that fails only in
301
+ parallel is checked by running it alone, with `--serial`.
302
+ - Exit codes are the tools'; 127 means a missing tool (or the
303
+ pytest-xdist an explicit `--fast` requires).