tempest-cli 0.4.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 (53) hide show
  1. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/CHANGELOG.md +49 -0
  2. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/PKG-INFO +25 -3
  3. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/README.md +24 -2
  4. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/changelog.en.md +33 -0
  5. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/changelog.md +31 -0
  6. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/commands.en.md +129 -5
  7. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/commands.md +129 -5
  8. tempest_cli-0.5.1/docs/library.en.md +193 -0
  9. tempest_cli-0.5.1/docs/library.md +192 -0
  10. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/reference.md +6 -0
  11. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/pyproject.toml +1 -1
  12. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/__init__.py +8 -1
  13. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/lint.py +131 -35
  14. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/main.py +44 -6
  15. tempest_cli-0.5.1/tempest_cli/residuals.py +356 -0
  16. tempest_cli-0.5.1/tempest_cli/rewrite.py +417 -0
  17. tempest_cli-0.5.1/tempest_cli/steps.py +54 -0
  18. tempest_cli-0.5.1/tests/test_extra_steps.py +201 -0
  19. tempest_cli-0.5.1/tests/test_fix_residuals.py +194 -0
  20. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/test_lint_runners.py +4 -3
  21. tempest_cli-0.5.1/tests/test_rewrite.py +222 -0
  22. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/uv.lock +1 -1
  23. tempest_cli-0.4.0/docs/library.en.md +0 -97
  24. tempest_cli-0.4.0/docs/library.md +0 -97
  25. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/.github/workflows/ci.yml +0 -0
  26. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/.github/workflows/docs.yml +0 -0
  27. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/.github/workflows/release-pypi.yml +0 -0
  28. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/.gitignore +0 -0
  29. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/LICENSE +0 -0
  30. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/Makefile +0 -0
  31. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/configuration.en.md +0 -0
  32. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/configuration.md +0 -0
  33. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/index.en.md +0 -0
  34. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/index.md +0 -0
  35. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/installation.en.md +0 -0
  36. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/installation.md +0 -0
  37. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/pr-prompt.en.md +0 -0
  38. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/pr-prompt.md +0 -0
  39. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/reference.en.md +0 -0
  40. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/rules.en.md +0 -0
  41. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/docs/rules.md +0 -0
  42. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/mkdocs.yml +0 -0
  43. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
  44. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
  45. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/config.py +0 -0
  46. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/pr_prompt.py +0 -0
  47. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tempest_cli/py.typed +0 -0
  48. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/__init__.py +0 -0
  49. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/test_cli.py +0 -0
  50. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/test_config.py +0 -0
  51. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/test_fast.py +0 -0
  52. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/test_lint_strictness.py +0 -0
  53. {tempest_cli-0.4.0 → tempest_cli-0.5.1}/tests/test_pr_prompt.py +0 -0
@@ -5,6 +5,55 @@ 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
+
38
+ ## [0.5.0] — 2026-10-13
39
+
40
+ ### Added
41
+
42
+ - **`extra_steps`: a framework's own rules inside `check`.** The new
43
+ `CheckStep(name, run)` (frozen dataclass, exported from `tempest_cli`)
44
+ is a step `check` runs after `mypy` and before `pytest`: `run` receives
45
+ the `check` target and returns an exit code. `run_full_check(...,
46
+ extra_steps=)` and `register_commands(app, *, extra_steps=)` take a
47
+ sequence of them — keyword-only, empty by default, so every existing
48
+ call runs the same four steps as before. Each extra step is echoed as
49
+ `$ <name>` on stderr, runs in list order, and the first non-zero code
50
+ stops the gate and becomes its exit code, with or without `--fast`.
51
+ They sit before `pytest` because they are static checks that should
52
+ fail in a second, not after the suite. Requested in
53
+ mauriciobenjamin700/tempest-fastapi-sdk#520, where the SDK adds its
54
+ service-layer rules to `tempest check` without this package learning
55
+ about any framework.
56
+
8
57
  ## [0.4.0] — 2026-09-27
9
58
 
10
59
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tempest-cli
3
- Version: 0.4.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,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
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) |
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`) |
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
@@ -150,6 +150,28 @@ cli: typer.Typer = typer.Typer(name="mytool")
150
150
  register_commands(cli)
151
151
  ```
152
152
 
153
+ A framework that wants its own rules in `check` passes them as
154
+ `CheckStep`s — they run after `mypy` and before `pytest`, and the first
155
+ non-zero exit code stops the gate:
156
+
157
+ ```python
158
+ import typer
159
+
160
+ from tempest_cli import CheckStep
161
+ from tempest_cli.main import register_commands
162
+
163
+
164
+ def layers(target: str) -> int:
165
+ """Return 0 when every module under ``target`` follows the layering."""
166
+ return 0
167
+
168
+
169
+ cli: typer.Typer = typer.Typer(name="mytool")
170
+ register_commands(cli, extra_steps=[CheckStep(name="layers", run=layers)])
171
+ ```
172
+
173
+ `run_full_check(".", extra_steps=[...])` takes the same list.
174
+
153
175
  ## Where the tools come from
154
176
 
155
177
  **`ruff` comes with the package** — six of the eight commands are ruff,
@@ -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
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) |
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`) |
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
@@ -118,6 +118,28 @@ cli: typer.Typer = typer.Typer(name="mytool")
118
118
  register_commands(cli)
119
119
  ```
120
120
 
121
+ A framework that wants its own rules in `check` passes them as
122
+ `CheckStep`s — they run after `mypy` and before `pytest`, and the first
123
+ non-zero exit code stops the gate:
124
+
125
+ ```python
126
+ import typer
127
+
128
+ from tempest_cli import CheckStep
129
+ from tempest_cli.main import register_commands
130
+
131
+
132
+ def layers(target: str) -> int:
133
+ """Return 0 when every module under ``target`` follows the layering."""
134
+ return 0
135
+
136
+
137
+ cli: typer.Typer = typer.Typer(name="mytool")
138
+ register_commands(cli, extra_steps=[CheckStep(name="layers", run=layers)])
139
+ ```
140
+
141
+ `run_full_check(".", extra_steps=[...])` takes the same list.
142
+
121
143
  ## Where the tools come from
122
144
 
123
145
  **`ruff` comes with the package** — six of the eight commands are ruff,
@@ -3,6 +3,39 @@
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
+
26
+ ## [0.5.0] — 2026-10-13
27
+
28
+ ### Added
29
+
30
+ - **`extra_steps`: a framework's own rules inside `check`.** The new
31
+ `CheckStep(name, run)` is a step `check` runs after `mypy` and before
32
+ `pytest`; `run` receives the target and returns an exit code.
33
+ `run_full_check(..., extra_steps=)` and
34
+ `register_commands(app, *, extra_steps=)` take the list — empty by
35
+ default, so nothing changes for a caller that does not pass one. The
36
+ first non-zero code stops the gate, with or without `--fast`.
37
+ [Details](library.md#adding-steps-to-check).
38
+
6
39
  ## [0.4.0] — 2026-09-27
7
40
 
8
41
  ### Added
@@ -4,6 +4,37 @@ 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
+
25
+ ## [0.5.0] — 2026-10-13
26
+
27
+ ### Adicionado
28
+
29
+ - **`extra_steps`: regras do framework dentro do `check`.** O novo
30
+ `CheckStep(name, run)` é um passo que o `check` roda depois do `mypy` e
31
+ antes do `pytest`; `run` recebe o alvo e devolve um código de saída.
32
+ `run_full_check(..., extra_steps=)` e
33
+ `register_commands(app, *, extra_steps=)` aceitam a lista — vazia por
34
+ padrão, então nada muda para quem não passa. O primeiro código diferente
35
+ de `0` para o gate, com ou sem `--fast`.
36
+ [Detalhes](library.md#acrescentar-passos-ao-check).
37
+
7
38
  ## [0.4.0] — 2026-09-27
8
39
 
9
40
  ### Adicionado
@@ -6,12 +6,12 @@ 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` |
13
13
  | `test` | `pytest` |
14
- | `check` | the four above, in order, stopping at the first failure |
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
15
  | `pr-prompt` | builds the PR-description prompt — [its own page](pr-prompt.md) |
16
16
 
17
17
  !!! tip "`tc` is the short form"
@@ -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,12 +6,12 @@ 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` |
13
13
  | `test` | `pytest` |
14
- | `check` | os quatro acima, em ordem, parando na primeira falha |
14
+ | `check` | os quatro acima, em ordem, parando na primeira falha (uma CLI que monta o gate pode [acrescentar passos](library.md#acrescentar-passos-ao-check) entre o `mypy` e o `pytest`) |
15
15
  | `pr-prompt` | monta o prompt da descrição do PR — [página própria](pr-prompt.md) |
16
16
 
17
17
  !!! tip "`tc` é a forma curta"
@@ -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.