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.
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/CHANGELOG.md +30 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/PKG-INFO +2 -2
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/README.md +1 -1
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/changelog.en.md +20 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/changelog.md +18 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/commands.en.md +128 -4
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/commands.md +128 -4
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/pyproject.toml +1 -1
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/__init__.py +1 -1
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/lint.py +76 -22
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/main.py +12 -4
- tempest_cli-0.5.1/tempest_cli/residuals.py +356 -0
- tempest_cli-0.5.1/tempest_cli/rewrite.py +417 -0
- tempest_cli-0.5.1/tests/test_fix_residuals.py +194 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_lint_runners.py +4 -3
- tempest_cli-0.5.1/tests/test_rewrite.py +222 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/uv.lock +1 -1
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.github/workflows/ci.yml +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.github/workflows/docs.yml +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.github/workflows/release-pypi.yml +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/.gitignore +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/LICENSE +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/Makefile +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/configuration.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/configuration.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/index.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/index.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/installation.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/installation.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/library.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/library.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/pr-prompt.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/pr-prompt.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/reference.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/reference.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/rules.en.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/docs/rules.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/mkdocs.yml +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/config.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/pr_prompt.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/py.typed +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tempest_cli/steps.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/__init__.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_cli.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_config.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_extra_steps.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_fast.py +0 -0
- {tempest_cli-0.5.0 → tempest_cli-0.5.1}/tests/test_lint_strictness.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
275
|
-
(sort + dedupe), remove unused imports, normalize string
|
|
276
|
-
drop trailing whitespace, fix the rest of the lint rules
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
333
|
+
fix and report passes. When ``None`` the default level is used.
|
|
297
334
|
|
|
298
335
|
Returns:
|
|
299
|
-
int: ``0`` when
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
340
|
+
ann_args = _ruff_ann_args(config)
|
|
341
|
+
fix_args = ["check", "--fix-only", *ann_args]
|
|
304
342
|
if unsafe:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
normalizes string quotes, removes trailing
|
|
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))
|