tempest-cli 0.5.0__tar.gz → 0.5.1__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 (51) hide show
  1. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/CHANGELOG.md +30 -0
  2. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/PKG-INFO +2 -2
  3. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/README.md +1 -1
  4. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/changelog.en.md +20 -0
  5. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/changelog.md +18 -0
  6. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/commands.en.md +128 -4
  7. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/commands.md +128 -4
  8. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/pyproject.toml +1 -1
  9. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/__init__.py +1 -1
  10. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/lint.py +76 -22
  11. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/main.py +12 -4
  12. tempest_cli-0.5.1/tempest_cli/residuals.py +356 -0
  13. tempest_cli-0.5.1/tempest_cli/rewrite.py +417 -0
  14. tempest_cli-0.5.1/tests/test_fix_residuals.py +194 -0
  15. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_lint_runners.py +4 -3
  16. tempest_cli-0.5.1/tests/test_rewrite.py +222 -0
  17. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/uv.lock +1 -1
  18. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.github/workflows/ci.yml +0 -0
  19. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.github/workflows/docs.yml +0 -0
  20. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.github/workflows/release-pypi.yml +0 -0
  21. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.gitignore +0 -0
  22. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/LICENSE +0 -0
  23. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/Makefile +0 -0
  24. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/configuration.en.md +0 -0
  25. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/configuration.md +0 -0
  26. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/index.en.md +0 -0
  27. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/index.md +0 -0
  28. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/installation.en.md +0 -0
  29. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/installation.md +0 -0
  30. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/library.en.md +0 -0
  31. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/library.md +0 -0
  32. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/pr-prompt.en.md +0 -0
  33. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/pr-prompt.md +0 -0
  34. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/reference.en.md +0 -0
  35. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/reference.md +0 -0
  36. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/rules.en.md +0 -0
  37. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/rules.md +0 -0
  38. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/mkdocs.yml +0 -0
  39. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
  40. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
  41. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/config.py +0 -0
  42. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/pr_prompt.py +0 -0
  43. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/py.typed +0 -0
  44. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/steps.py +0 -0
  45. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/__init__.py +0 -0
  46. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_cli.py +0 -0
  47. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_config.py +0 -0
  48. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_extra_steps.py +0 -0
  49. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_fast.py +0 -0
  50. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_lint_strictness.py +0 -0
  51. {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_pr_prompt.py +0 -0
@@ -5,6 +5,36 @@ 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
+ ## [0.5.1] — 2026-10-15
9
+
10
+ ### Fixed
11
+
12
+ - **`fix` explains the E501 the formatter cannot resolve.** A line still
13
+ too long after `ruff format` is one the formatter has nowhere to break
14
+ — a dotted name (it never splits one, and drops parentheses that do not
15
+ make the line fit), a string literal, a comment — but the bare
16
+ `Line too long (94 > 79)` read as if `line-length` were being ignored.
17
+ `fix` now reads ruff's JSON report of the leftovers and names each such
18
+ line with the overflowing token, its kind and the action that does
19
+ resolve it, then splits the residual violations into those with no
20
+ autofix and those `fix --unsafe` would fix. The exit code is unchanged:
21
+ CI still fails on them. Closes #3.
22
+ - **`fix` shortens the long comments and strings the formatter leaves
23
+ alone.** After `ruff format`, each E501 that is a comment is wrapped at
24
+ the limit (a trailing one moves above its line) and each that is a
25
+ plain string literal is split at its spaces into implicitly
26
+ concatenated parts inside parentheses; the touched files are formatted
27
+ again. Every edit is kept only when the file's AST is identical before
28
+ and after, so the program cannot change. Tool pragmas (`noqa`,
29
+ `type: ignore`, `fmt:`, …), f-strings, triple-quoted strings and words
30
+ longer than a line are left alone and explained instead. On by
31
+ default: neither rewrite changes behavior.
32
+ - **`fix` reports on the formatted file.** The passes are now
33
+ `ruff check --fix-only`, `ruff format`, then `ruff check`; before, the
34
+ report and the exit code came from the lint pass that ran *before* the
35
+ formatter, so a violation the formatter went on to resolve still showed
36
+ up and still failed the command.
37
+
8
38
  ## [0.5.0] — 2026-10-13
9
39
 
10
40
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tempest-cli
3
- Version: 0.5.0
3
+ Version: 0.5.1
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,7 +70,7 @@ 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` |
@@ -38,7 +38,7 @@ 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` |
@@ -3,6 +3,26 @@
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.5.1] — 2026-10-15
7
+
8
+ ### Fixed
9
+
10
+ - **`fix` explains the E501 the formatter cannot resolve.** A line still
11
+ too long after `ruff format` (a dotted name, a string, a comment) comes
12
+ out with its token, its kind and the action that resolves it, and the
13
+ summary separates what has no autofix from what `--unsafe` would fix.
14
+ The exit code is unchanged.
15
+ [Details](commands.md#an-e501-ruff-format-cannot-fix).
16
+ - **`fix` shortens the long comments and strings the formatter leaves
17
+ alone.** A comment is wrapped at the limit and a string is split into
18
+ concatenated parts; each edit is kept only when the file's AST is
19
+ identical before and after. Pragmas, f-strings, triple quotes and long
20
+ words are left out and explained. On by default.
21
+ - **`fix` reports on the formatted file.** The passes are now
22
+ `ruff check --fix-only`, `ruff format` and `ruff check`; before, the
23
+ report and the exit code came from the pass that ran before the
24
+ formatter.
25
+
6
26
  ## [0.5.0] — 2026-10-13
7
27
 
8
28
  ### Added
@@ -4,6 +4,24 @@ 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.5.1] — 2026-10-15
8
+
9
+ ### Corrigido
10
+
11
+ - **O `fix` explica o E501 que o formatador não resolve.** Linha ainda
12
+ longa depois do `ruff format` (nome pontuado, string, comentário) sai
13
+ com o token, o tipo e a ação que resolve, e o resumo separa o que não
14
+ tem autofix do que o `--unsafe` resolveria. O código de saída não muda.
15
+ [Detalhes](commands.md#e501-que-o-ruff-format-nao-resolve).
16
+ - **O `fix` encurta comentário e string longos que o formatador não
17
+ quebra.** Comentário é quebrado no limite e string é partida em pedaços
18
+ concatenados; cada edição só fica se a AST do arquivo for idêntica
19
+ antes e depois. Pragma, f-string, três aspas e palavra longa ficam de
20
+ fora e são explicados. Ligado por padrão.
21
+ - **O `fix` relata sobre o arquivo formatado.** As passadas agora são
22
+ `ruff check --fix-only`, `ruff format` e `ruff check`; antes, o
23
+ relatório e o código de saída vinham da passada anterior ao formatador.
24
+
7
25
  ## [0.5.0] — 2026-10-13
8
26
 
9
27
  ### Adicionado
@@ -6,7 +6,7 @@ tool's exit code.
6
6
  | Command | Runs |
7
7
  | --- | --- |
8
8
  | `lint` | `ruff check` |
9
- | `fix` | `ruff check --fix` then `ruff format` |
9
+ | `fix` | `ruff check --fix-only`, `ruff format`, shortens long comments/strings, then a final `ruff check` explaining what is left |
10
10
  | `format` | `ruff format` (writes) |
11
11
  | `fmt-check` | `ruff format --check` (read-only) |
12
12
  | `type` | `mypy` |
@@ -39,15 +39,136 @@ tempest-cli fix # safe autofixes + formatting
39
39
  tempest-cli fix --unsafe # also ruff's risky autofixes
40
40
  ```
41
41
 
42
- `fix` makes two passes: `ruff check --fix` (sorts and dedupes imports,
43
- drops unused ones, normalizes quotes) and then `ruff format`
44
- (indentation, line length, blank lines).
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.
45
53
 
46
54
  !!! warning "`--unsafe` can change behavior"
47
55
  Ruff's "unsafe" autofixes are the ones that may alter semantics.
48
56
  They are off by default. Turned them on? Read the `git diff` before
49
57
  committing.
50
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
+
51
172
  ## One at a time
52
173
 
53
174
  ```bash
@@ -152,6 +273,9 @@ environment.
152
273
  - `check` is lint + fmt-check + type + test, in order, stopping at the
153
274
  first failure.
154
275
  - `fix` is the pass that repairs; `--unsafe` only when you will review.
276
+ - Long comments and strings `fix` shortens on its own, with the AST
277
+ proven equal. An E501 left after it has no mechanical rewrite: `fix`
278
+ names the token and the action — change the code's shape.
155
279
  - `test --fast` / `check --fast` run the suite with pytest-xdist
156
280
  (`-n auto`, `-w N` to choose); a test that fails only in parallel is
157
281
  checked by running it alone.
@@ -6,7 +6,7 @@ saída da ferramenta por baixo.
6
6
  | Comando | Roda |
7
7
  | --- | --- |
8
8
  | `lint` | `ruff check` |
9
- | `fix` | `ruff check --fix` e depois `ruff format` |
9
+ | `fix` | `ruff check --fix-only`, `ruff format`, encurta comentário/string longos e um `ruff check` final que explica o que sobrou |
10
10
  | `format` | `ruff format` (escreve) |
11
11
  | `fmt-check` | `ruff format --check` (só leitura) |
12
12
  | `type` | `mypy` |
@@ -39,15 +39,136 @@ tempest-cli fix # autofixes seguros + formatação
39
39
  tempest-cli fix --unsafe # inclui os autofixes de risco do ruff
40
40
  ```
41
41
 
42
- O `fix` faz duas passadas: `ruff check --fix` (ordena e deduplica
43
- imports, remove imports não usados, normaliza aspas) e depois `ruff
44
- format` (indentação, comprimento de linha, linhas em branco).
42
+ O `fix` faz quatro passadas:
43
+
44
+ 1. `ruff check --fix-only` — ordena e deduplica imports, remove imports
45
+ não usados, normaliza aspas.
46
+ 2. `ruff format` — indentação, comprimento de linha, linhas em branco.
47
+ 3. Encurta as linhas longas que o formatador não quebra, quando dá para
48
+ fazer isso sem mudar o programa: comentários e strings (veja abaixo).
49
+ 4. `ruff check` — o relatório do que sobrou, já sobre o arquivo final.
50
+
51
+ O código de saída é o da última passada: sobrou violação, o `fix` sai
52
+ com erro e o CI falha.
45
53
 
46
54
  !!! warning "`--unsafe` muda comportamento"
47
55
  Os autofixes "unsafe" do ruff são os que podem alterar semântica.
48
56
  Ficam desligados por padrão. Ligou? Leia o `git diff` antes de
49
57
  commitar.
50
58
 
59
+ ### E501 que o `ruff format` não resolve
60
+
61
+ Às vezes o `fix` termina assim, mesmo com o `line-length` certo no
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
+ Não é configuração errada — o `fix` não passa `line-length` nenhum ao
80
+ ruff, o valor do projeto já vale. É limite do formatador: ele **não
81
+ divide um nome pontuado** (`a.b.C`), e o parêntese que você pôs em volta
82
+ (`x = (\n a.b.C\n)`) some, porque não faz a linha caber. 🤷
83
+
84
+ #### O que o `fix` encurta sozinho
85
+
86
+ Dois desses casos têm uma única resposta certa, e o `fix` aplica:
87
+
88
+ - **comentário** é quebrado no limite; comentário no fim da linha sobe
89
+ para cima do código que ele comenta;
90
+ - **string** é partida nos espaços em pedaços concatenados entre
91
+ parênteses — o Python junta tudo de volta no mesmo valor.
92
+
93
+ ```python
94
+ # antes
95
+ text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbb" # o texto
96
+
97
+ # depois do fix
98
+ # o texto
99
+ text = (
100
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
101
+ "bbbbbbbbbbbbbbbb"
102
+ )
103
+ ```
104
+
105
+ ```text
106
+ shortened 1 long line(s) ruff format cannot break (comments wrapped, strings split into concatenated parts):
107
+ mod.py:2 [comment+string]
108
+ ```
109
+
110
+ !!! check "Só entra o que é provado igual"
111
+ Antes de gravar, o `fix` compara a AST do arquivo antes e depois.
112
+ Comentário não entra na AST e o parser junta string concatenada numa
113
+ constante só, então uma reescrita certa sempre passa. Qualquer outra
114
+ coisa — um `\N{...}` cortado no meio, comentário empurrado para
115
+ dentro de uma continuação com `\` — muda a AST, e a linha fica como
116
+ estava.
117
+
118
+ Fica de fora, de propósito: comentário de ferramenta (`noqa`,
119
+ `type: ignore`, `fmt:`, `pragma`…), que precisa ficar na linha que
120
+ governa; f-string e string com três aspas; e palavra mais longa que uma
121
+ linha (URL, por exemplo).
122
+
123
+ #### O que sobra
124
+
125
+ Nome pontuado, nome longo e expressão não têm reescrita mecânica — e
126
+ quebrá-los à mão é desfeito pelo próximo `ruff format`. Para esses, o
127
+ `fix` explica cada linha logo abaixo do relatório do ruff:
128
+
129
+ ```text
130
+ 1 line(s) still too long after ruff format — the line-length setting was applied; the formatter has nowhere to break these:
131
+ controller.py:5 [dotted-name] `notifications.NOTIFICATION_USER_BODY_ALO_STATUS_CHANGE_WAITING`
132
+ 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.
133
+ 1 residual violation(s): 1 with no autofix (edit by hand).
134
+ ```
135
+
136
+ O rótulo entre colchetes diz o que está no ponto onde a linha estoura:
137
+
138
+ | Rótulo | O que é | O que resolve |
139
+ | --- | --- | --- |
140
+ | `dotted-name` | nome pontuado (`modulo.CONSTANTE`) | alias, tabela/constante, indentação menor |
141
+ | `name` | um nome sozinho longo demais | renomear ou indentar menos |
142
+ | `string` | string que o `fix` não pôde partir (f-string, três aspas, palavra longa) | partir à mão em pedaços concatenados |
143
+ | `comment` | comentário que o `fix` não pôde quebrar (pragma, palavra longa) | quebrar à mão ou levar para a docstring |
144
+ | `expression` | qualquer outra coisa | extrair variável ou indentar menos |
145
+
146
+ A última linha separa o que **não tem autofix** do que o `fix --unsafe`
147
+ resolveria — sem ela, `Found 5 errors` não diz se vale rodar de novo.
148
+
149
+ !!! tip "Leve a escolha para uma tabela"
150
+ No exemplo acima, a indentação vem do `match`. Trocar o `match` por
151
+ um dicionário no nível do módulo tira quatro níveis e a linha cabe
152
+ exatamente em 79, sem 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 "E o `# noqa: E501`?"
168
+ Funciona, mas esconde o problema até o próximo refactor tirar o
169
+ `noqa` — que foi exatamente como esse caso apareceu. Prefira mudar a
170
+ forma do código.
171
+
51
172
  ## Um comando por vez
52
173
 
53
174
  ```bash
@@ -149,6 +270,9 @@ pytest.
149
270
  - `check` é lint + fmt-check + type + test, nessa ordem, parando na
150
271
  primeira falha.
151
272
  - `fix` é a passada que conserta; `--unsafe` só quando você for revisar.
273
+ - Comentário e string longos o `fix` encurta sozinho, com a AST provada
274
+ igual. O E501 que sobra é linha sem reescrita mecânica: o `fix` diz o
275
+ token e a ação — mude a forma do código.
152
276
  - `test --fast` / `check --fast` rodam a suíte com pytest-xdist
153
277
  (`-n auto`, `-w N` para escolher); teste que só falha em paralelo se
154
278
  confere rodando-o sozinho.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tempest-cli"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "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
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -57,7 +57,7 @@ from tempest_cli.pr_prompt import PromptLanguage as PromptLanguage
57
57
  from tempest_cli.pr_prompt import generate_pr_prompt as generate_pr_prompt
58
58
  from tempest_cli.steps import CheckStep as CheckStep
59
59
 
60
- __version__: str = "0.5.0"
60
+ __version__: str = "0.5.1"
61
61
  """Installed package version."""
62
62
 
63
63
  __all__: list[str] = [
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
 
12
12
  import typer
13
13
 
14
+ from tempest_cli import residuals, rewrite
14
15
  from tempest_cli.config import TempestConfig
15
16
  from tempest_cli.steps import CheckStep
16
17
 
@@ -261,6 +262,32 @@ def run_ruff_check(target: str, *, config: TempestConfig | None = None) -> int:
261
262
  return _execute("ruff", ["check", *_ruff_ann_args(config), target])
262
263
 
263
264
 
265
+ def _capture(executable: str, args: list[str]) -> str | None:
266
+ """Run ``executable args`` silently and return what it wrote to stdout.
267
+
268
+ Args:
269
+ executable (str): The command to run.
270
+ args (list[str]): Extra arguments to forward.
271
+
272
+ Returns:
273
+ str | None: The child's stdout, or ``None`` when the tool cannot
274
+ be found or started.
275
+ """
276
+ argv = resolve_tool(executable)
277
+ if argv is None:
278
+ return None
279
+ try:
280
+ completed = subprocess.run(
281
+ [*argv, *args],
282
+ capture_output=True,
283
+ text=True,
284
+ check=False,
285
+ )
286
+ except OSError:
287
+ return None
288
+ return completed.stdout
289
+
290
+
264
291
  def run_ruff_fix(
265
292
  target: str,
266
293
  *,
@@ -269,23 +296,33 @@ def run_ruff_fix(
269
296
  ) -> int:
270
297
  """Apply every automatic fix ruff can perform, then format the target.
271
298
 
272
- Runs in two passes so the second one sees the rewritten file:
299
+ Runs these passes, so the report describes the file the formatter
300
+ left behind rather than the one it started from:
273
301
 
274
- 1. ``ruff check --fix [--unsafe-fixes] <target>`` — autofix imports
275
- (sort + dedupe), remove unused imports, normalize string quotes,
276
- drop trailing whitespace, fix the rest of the lint rules that
277
- have safe (or, with ``unsafe=True``, also unsafe) autofixers.
302
+ 1. ``ruff check --fix-only [--unsafe-fixes] <target>`` — autofix
303
+ imports (sort + dedupe), remove unused imports, normalize string
304
+ quotes, drop trailing whitespace, fix the rest of the lint rules
305
+ that have safe (or, with ``unsafe=True``, also unsafe) autofixers.
306
+ ``--fix-only`` keeps it from reporting the leftovers, which the
307
+ formatter may still resolve.
278
308
  2. ``ruff format <target>`` — normalize indentation, line length,
279
309
  blank lines and trailing newlines.
280
-
281
- Both passes always run. ``ruff check --fix`` exits non-zero whenever
282
- *any* residual violation remains that it cannot autofix (an
283
- over-length string/comment, an undefined name, etc.) — even though
284
- it already rewrote everything it could. Short-circuiting on that
285
- exit code would skip ``ruff format`` entirely, leaving the file
286
- un-wrapped and its extra blank lines intact. So the formatter runs
287
- unconditionally; the lint exit code is surfaced afterwards so CI
288
- still fails on the leftover issues.
310
+ 3. Long lines the formatter cannot break, shortened where that is
311
+ safe (see :mod:`tempest_cli.rewrite`): ruff's JSON report is read,
312
+ comments past the limit are wrapped and string literals are split
313
+ into implicitly concatenated parts, each edit kept only when the
314
+ file's AST is unchanged. The files touched go through ``ruff
315
+ format`` once more.
316
+ 4. ``ruff check <target>`` — the report of what is left, and the exit
317
+ code of the command.
318
+
319
+ When the last pass fails, ruff's JSON report of the same target is
320
+ read to explain the leftovers (see :mod:`tempest_cli.residuals`): an
321
+ E501 that survived is one with no mechanical fix — a dotted name, a
322
+ long name, an expression — and is named as such with the action that
323
+ does resolve it, and the residual violations are split into those
324
+ with no autofix and those ``--unsafe`` would fix. The exit code is
325
+ unaffected, so CI still fails on them.
289
326
 
290
327
  Args:
291
328
  target (str): The path passed verbatim to ruff.
@@ -293,19 +330,36 @@ def run_ruff_fix(
293
330
  applies the fixes it would otherwise leave alone.
294
331
  config (TempestConfig | None): Resolved ``[tool.tempest]`` config
295
332
  controlling the typing-strictness ANN rules layered onto the
296
- fix pass. When ``None`` the default level is used.
333
+ fix and report passes. When ``None`` the default level is used.
297
334
 
298
335
  Returns:
299
- int: ``0`` when both passes succeed with nothing left to fix;
300
- otherwise the lint pass exit code (residual violations), or the
301
- format pass exit code when the lint pass was clean.
336
+ int: ``0`` when nothing is left to fix; otherwise the report pass
337
+ exit code (residual violations), or the format pass exit code
338
+ when the report was clean. ``127`` when ruff cannot be found.
302
339
  """
303
- check_args = ["check", "--fix", *_ruff_ann_args(config)]
340
+ ann_args = _ruff_ann_args(config)
341
+ fix_args = ["check", "--fix-only", *ann_args]
304
342
  if unsafe:
305
- check_args.append("--unsafe-fixes")
306
- check_args.append(target)
307
- check_code = _execute("ruff", check_args)
343
+ fix_args.append("--unsafe-fixes")
344
+ fix_args.append(target)
345
+ fix_code = _execute("ruff", fix_args)
346
+ if fix_code == MISSING_TOOL_EXIT_CODE:
347
+ return fix_code
308
348
  format_code = _execute("ruff", ["format", target])
349
+ report_args = ["check", *ann_args, "--output-format", "json", "--exit-zero"]
350
+ long_lines = _capture("ruff", [*report_args, target])
351
+ shortened = rewrite.rewrite_long_lines(long_lines) if long_lines else []
352
+ if shortened:
353
+ typer.echo(rewrite.render_rewrites(shortened), err=True, nl=False)
354
+ touched = sorted({item.path for item in shortened})
355
+ reformat_code = _execute("ruff", ["format", "--force-exclude", *touched])
356
+ format_code = format_code or reformat_code
357
+ check_code = _execute("ruff", ["check", *ann_args, target])
358
+ if check_code:
359
+ report = _capture("ruff", [*report_args, target])
360
+ found = residuals.collect_residuals(report) if report is not None else None
361
+ if found is not None:
362
+ typer.echo(residuals.render_residuals(found), err=True, nl=False)
309
363
  return check_code or format_code
310
364
 
311
365
 
@@ -231,10 +231,18 @@ def register_commands(
231
231
  ) -> None:
232
232
  """Apply every ruff autofix + format the target in one pass.
233
233
 
234
- Equivalent to running ``ruff check --fix`` followed by ``ruff
235
- format``: sorts and dedupes imports, drops unused imports,
236
- normalizes string quotes, removes trailing whitespace, normalizes
237
- indentation, line length and blank lines.
234
+ Runs ``ruff check --fix-only``, ``ruff format``, then ``ruff
235
+ check`` to report what is left: sorts and dedupes imports, drops
236
+ unused imports, normalizes string quotes, removes trailing
237
+ whitespace, normalizes indentation, line length and blank lines.
238
+
239
+ A line still too long after formatting is one ruff format has
240
+ nowhere to break. Long comments are wrapped and long strings are
241
+ split into concatenated parts, each edit kept only when the AST
242
+ is unchanged. Whatever is left (a dotted name, a long name, an
243
+ expression) is named with the action that resolves it, and the
244
+ leftovers are split into those with no autofix and those
245
+ --unsafe would fix.
238
246
  """
239
247
  config = _resolve_config(target, strictness)
240
248
  raise typer.Exit(lint_module.run_ruff_fix(target, unsafe=unsafe, config=config))