tempest-cli 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/CHANGELOG.md +19 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/PKG-INFO +24 -2
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/README.md +23 -1
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/changelog.en.md +13 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/changelog.md +13 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/commands.en.md +1 -1
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/commands.md +1 -1
- tempest_cli-0.5.0/docs/library.en.md +193 -0
- tempest_cli-0.5.0/docs/library.md +192 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/reference.md +6 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/pyproject.toml +1 -1
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/__init__.py +8 -1
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/lint.py +55 -13
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/main.py +32 -2
- tempest_cli-0.5.0/tempest_cli/steps.py +54 -0
- tempest_cli-0.5.0/tests/test_extra_steps.py +201 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/uv.lock +1 -1
- tempest_cli-0.4.0/docs/library.en.md +0 -97
- tempest_cli-0.4.0/docs/library.md +0 -97
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.github/workflows/ci.yml +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.github/workflows/docs.yml +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.github/workflows/release-pypi.yml +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/.gitignore +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/LICENSE +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/Makefile +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/configuration.en.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/configuration.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/index.en.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/index.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/installation.en.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/installation.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/pr-prompt.en.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/pr-prompt.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/reference.en.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/rules.en.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/docs/rules.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/mkdocs.yml +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/config.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/pr_prompt.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tempest_cli/py.typed +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/__init__.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_cli.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_config.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_fast.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_lint_runners.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_lint_strictness.py +0 -0
- {tempest_cli-0.4.0 → tempest_cli-0.5.0}/tests/test_pr_prompt.py +0 -0
|
@@ -5,6 +5,25 @@ 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.0] — 2026-10-13
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`extra_steps`: a framework's own rules inside `check`.** The new
|
|
13
|
+
`CheckStep(name, run)` (frozen dataclass, exported from `tempest_cli`)
|
|
14
|
+
is a step `check` runs after `mypy` and before `pytest`: `run` receives
|
|
15
|
+
the `check` target and returns an exit code. `run_full_check(...,
|
|
16
|
+
extra_steps=)` and `register_commands(app, *, extra_steps=)` take a
|
|
17
|
+
sequence of them — keyword-only, empty by default, so every existing
|
|
18
|
+
call runs the same four steps as before. Each extra step is echoed as
|
|
19
|
+
`$ <name>` on stderr, runs in list order, and the first non-zero code
|
|
20
|
+
stops the gate and becomes its exit code, with or without `--fast`.
|
|
21
|
+
They sit before `pytest` because they are static checks that should
|
|
22
|
+
fail in a second, not after the suite. Requested in
|
|
23
|
+
mauriciobenjamin700/tempest-fastapi-sdk#520, where the SDK adds its
|
|
24
|
+
service-layer rules to `tempest check` without this package learning
|
|
25
|
+
about any framework.
|
|
26
|
+
|
|
8
27
|
## [0.4.0] — 2026-09-27
|
|
9
28
|
|
|
10
29
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tempest-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Framework-agnostic quality gate for Python projects: ruff + mypy + pytest behind one command, with a typing-strictness dial and a PR-description prompt generator.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mauriciobenjamin700/tempest-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/mauriciobenjamin700/tempest-cli
|
|
@@ -75,7 +75,7 @@ remembered to pass.
|
|
|
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,
|
|
@@ -43,7 +43,7 @@ remembered to pass.
|
|
|
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,19 @@
|
|
|
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.0] — 2026-10-13
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`extra_steps`: a framework's own rules inside `check`.** The new
|
|
11
|
+
`CheckStep(name, run)` is a step `check` runs after `mypy` and before
|
|
12
|
+
`pytest`; `run` receives the target and returns an exit code.
|
|
13
|
+
`run_full_check(..., extra_steps=)` and
|
|
14
|
+
`register_commands(app, *, extra_steps=)` take the list — empty by
|
|
15
|
+
default, so nothing changes for a caller that does not pass one. The
|
|
16
|
+
first non-zero code stops the gate, with or without `--fast`.
|
|
17
|
+
[Details](library.md#adding-steps-to-check).
|
|
18
|
+
|
|
6
19
|
## [0.4.0] — 2026-09-27
|
|
7
20
|
|
|
8
21
|
### Added
|
|
@@ -4,6 +4,19 @@ 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.0] — 2026-10-13
|
|
8
|
+
|
|
9
|
+
### Adicionado
|
|
10
|
+
|
|
11
|
+
- **`extra_steps`: regras do framework dentro do `check`.** O novo
|
|
12
|
+
`CheckStep(name, run)` é um passo que o `check` roda depois do `mypy` e
|
|
13
|
+
antes do `pytest`; `run` recebe o alvo e devolve um código de saída.
|
|
14
|
+
`run_full_check(..., extra_steps=)` e
|
|
15
|
+
`register_commands(app, *, extra_steps=)` aceitam a lista — vazia por
|
|
16
|
+
padrão, então nada muda para quem não passa. O primeiro código diferente
|
|
17
|
+
de `0` para o gate, com ou sem `--fast`.
|
|
18
|
+
[Detalhes](library.md#acrescentar-passos-ao-check).
|
|
19
|
+
|
|
7
20
|
## [0.4.0] — 2026-09-27
|
|
8
21
|
|
|
9
22
|
### Adicionado
|
|
@@ -11,7 +11,7 @@ tool's exit code.
|
|
|
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"
|
|
@@ -11,7 +11,7 @@ saída da ferramenta por baixo.
|
|
|
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"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Use as a library
|
|
2
|
+
|
|
3
|
+
Everything the CLI does is importable. Two situations are worth it.
|
|
4
|
+
|
|
5
|
+
## Calling the runners from your code
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from tempest_cli import load_tempest_config, run_full_check
|
|
9
|
+
|
|
10
|
+
config = load_tempest_config()
|
|
11
|
+
exit_code = run_full_check(".", config=config)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Each step is callable on its own too, with the same exit-code contract:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from tempest_cli import (
|
|
18
|
+
TempestConfig,
|
|
19
|
+
run_mypy,
|
|
20
|
+
run_pytest,
|
|
21
|
+
run_ruff_check,
|
|
22
|
+
run_ruff_fix,
|
|
23
|
+
run_ruff_format,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
strict = TempestConfig(typing_strictness="strict")
|
|
27
|
+
|
|
28
|
+
run_ruff_check("src", config=strict) # ruff check + the level's ANN rules
|
|
29
|
+
run_ruff_fix("src", unsafe=False, config=strict)
|
|
30
|
+
run_ruff_format("src", check=True) # --check: writes nothing
|
|
31
|
+
run_mypy("src", config=strict)
|
|
32
|
+
run_pytest("tests/unit")
|
|
33
|
+
run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`load_tempest_config(start)` walks up from `start` (the cwd by default)
|
|
37
|
+
to the nearest `pyproject.toml` and returns the resolved
|
|
38
|
+
`TempestConfig`. `find_pyproject` does the lookup alone, when you want
|
|
39
|
+
the path.
|
|
40
|
+
|
|
41
|
+
## Mounting the gate on your own CLI
|
|
42
|
+
|
|
43
|
+
A tool that already has its own CLI can expose these commands under its
|
|
44
|
+
own name — without copying a single command body:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import typer
|
|
48
|
+
|
|
49
|
+
from tempest_cli.main import register_commands
|
|
50
|
+
|
|
51
|
+
cli: typer.Typer = typer.Typer(name="mytool")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cli.command("deploy")
|
|
55
|
+
def deploy() -> None:
|
|
56
|
+
"""A command the tool already had."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
register_commands(cli)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Now `mytool check`, `mytool lint` and the rest exist, with the same
|
|
63
|
+
behavior and the same `[tool.tempest]` reading.
|
|
64
|
+
|
|
65
|
+
!!! info "This is exactly what `tempest-fastapi-sdk` does"
|
|
66
|
+
The SDK's `tempest check` is this `register_commands` called on its
|
|
67
|
+
app. That is why the two never diverge: there is one
|
|
68
|
+
implementation.
|
|
69
|
+
|
|
70
|
+
## Adding steps to `check`
|
|
71
|
+
|
|
72
|
+
`tempest-cli` knows nothing about any framework, on purpose. The
|
|
73
|
+
framework does, though: "every service inherits `BaseService`" is a rule
|
|
74
|
+
of whoever lays out the project, not of whoever runs ruff. `CheckStep`
|
|
75
|
+
is the seam — you write the rule, `check` runs it.
|
|
76
|
+
|
|
77
|
+
A `CheckStep` has two fields: `name`, the label `check` echoes as
|
|
78
|
+
`$ <name>` on stderr, and `run`, a function that takes the `check`
|
|
79
|
+
target (the path from the command line, `.` by default) and returns an
|
|
80
|
+
exit code — `0` means it passed.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
import typer
|
|
86
|
+
|
|
87
|
+
from tempest_cli import CheckStep
|
|
88
|
+
from tempest_cli.main import register_commands
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def no_print(target: str) -> int:
|
|
92
|
+
"""Fail when a module under ``target`` calls ``print``."""
|
|
93
|
+
offenders: list[str] = [
|
|
94
|
+
f"{path}:{number}: use logging instead of print()"
|
|
95
|
+
for path in sorted(Path(target).rglob("*.py"))
|
|
96
|
+
for number, line in enumerate(path.read_text().splitlines(), start=1)
|
|
97
|
+
if line.lstrip().startswith("print(")
|
|
98
|
+
]
|
|
99
|
+
for offender in offenders:
|
|
100
|
+
typer.echo(offender, err=True)
|
|
101
|
+
return 1 if offenders else 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
cli: typer.Typer = typer.Typer(name="mytool")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@cli.command("deploy")
|
|
108
|
+
def deploy() -> None:
|
|
109
|
+
"""A command the tool already had."""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
register_commands(cli, extra_steps=[CheckStep(name="no-print", run=no_print)])
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
cli()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
With a module `pkg/a.py` that calls `print("x")`:
|
|
119
|
+
|
|
120
|
+
```console
|
|
121
|
+
$ python mytool.py check pkg
|
|
122
|
+
$ ruff check --extend-select ANN001,ANN201,ANN202,ANN205,ANN206 pkg
|
|
123
|
+
All checks passed!
|
|
124
|
+
$ ruff format --check pkg
|
|
125
|
+
1 file already formatted
|
|
126
|
+
$ mypy --disallow-untyped-defs --disallow-incomplete-defs pkg
|
|
127
|
+
Success: no issues found in 1 source file
|
|
128
|
+
$ no-print
|
|
129
|
+
pkg/a.py:2: use logging instead of print()
|
|
130
|
+
$ echo $?
|
|
131
|
+
1
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The order is `ruff check` → `ruff format --check` → `mypy` → the extra
|
|
135
|
+
steps, in list order → `pytest`. An extra step is a static check: it
|
|
136
|
+
sits before the suite so it fails in a second, not after it. The
|
|
137
|
+
contract is the built-in steps' — the first non-zero code stops the gate
|
|
138
|
+
and becomes `check`'s exit code, with or without `--fast`.
|
|
139
|
+
|
|
140
|
+
!!! tip "The message is yours"
|
|
141
|
+
`check` only echoes the name. What the step prints is what the user
|
|
142
|
+
reads, so name the file, the line and the fix — as `no-print` does
|
|
143
|
+
above.
|
|
144
|
+
|
|
145
|
+
Without the CLI, the same holds for `run_full_check`:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from tempest_cli import CheckStep, run_full_check
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def always_passes(target: str) -> int:
|
|
152
|
+
"""A step that accepts any target."""
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
exit_code: int = run_full_check(
|
|
157
|
+
".", extra_steps=[CheckStep(name="always-passes", run=always_passes)]
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`extra_steps` is empty by default: without it, the gate is exactly what
|
|
162
|
+
it was.
|
|
163
|
+
|
|
164
|
+
## Generating the PR prompt without the CLI
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pathlib import Path
|
|
168
|
+
|
|
169
|
+
from tempest_cli import PromptLanguage, generate_pr_prompt
|
|
170
|
+
|
|
171
|
+
prompt, context, template = generate_pr_prompt(
|
|
172
|
+
base="main",
|
|
173
|
+
head=None,
|
|
174
|
+
cwd=Path("."),
|
|
175
|
+
template=None,
|
|
176
|
+
language=PromptLanguage.EN_US,
|
|
177
|
+
max_files=20,
|
|
178
|
+
max_chars=8000,
|
|
179
|
+
)
|
|
180
|
+
print(len(context.commits), "commits;", template.source)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`generate_pr_prompt` raises `GitError` when git fails or a ref does not
|
|
184
|
+
resolve.
|
|
185
|
+
|
|
186
|
+
## Recap
|
|
187
|
+
|
|
188
|
+
- Runners, config and the prompt generator are all importable.
|
|
189
|
+
- `register_commands(app)` mounts the eight commands on any `Typer`.
|
|
190
|
+
- `extra_steps=[CheckStep(...)]` puts your own rules in `check`, after
|
|
191
|
+
`mypy` and before `pytest`.
|
|
192
|
+
- Exit codes are the tools', identical from the CLI and from a direct
|
|
193
|
+
call.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Usar como biblioteca
|
|
2
|
+
|
|
3
|
+
Tudo que a CLI faz está importável. Duas situações valem a pena.
|
|
4
|
+
|
|
5
|
+
## Chamar os runners do seu código
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from tempest_cli import load_tempest_config, run_full_check
|
|
9
|
+
|
|
10
|
+
config = load_tempest_config()
|
|
11
|
+
exit_code = run_full_check(".", config=config)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Cada etapa também é chamável isolada, com o mesmo contrato de código de
|
|
15
|
+
saída:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from tempest_cli import (
|
|
19
|
+
TempestConfig,
|
|
20
|
+
run_mypy,
|
|
21
|
+
run_pytest,
|
|
22
|
+
run_ruff_check,
|
|
23
|
+
run_ruff_fix,
|
|
24
|
+
run_ruff_format,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
strict = TempestConfig(typing_strictness="strict")
|
|
28
|
+
|
|
29
|
+
run_ruff_check("src", config=strict) # ruff check + regras ANN do nível
|
|
30
|
+
run_ruff_fix("src", unsafe=False, config=strict)
|
|
31
|
+
run_ruff_format("src", check=True) # --check: não escreve
|
|
32
|
+
run_mypy("src", config=strict)
|
|
33
|
+
run_pytest("tests/unit")
|
|
34
|
+
run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`load_tempest_config(start)` procura o `pyproject.toml` mais próximo
|
|
38
|
+
subindo a partir de `start` (o cwd por padrão) e devolve o
|
|
39
|
+
`TempestConfig` resolvido. `find_pyproject` faz só a busca, quando você
|
|
40
|
+
quer o caminho.
|
|
41
|
+
|
|
42
|
+
## Montar o gate na sua própria CLI
|
|
43
|
+
|
|
44
|
+
Uma ferramenta que já tem CLI própria pode expor estes comandos sob o
|
|
45
|
+
nome dela — sem copiar corpo de comando nenhum:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import typer
|
|
49
|
+
|
|
50
|
+
from tempest_cli.main import register_commands
|
|
51
|
+
|
|
52
|
+
cli: typer.Typer = typer.Typer(name="minhaferramenta")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cli.command("deploy")
|
|
56
|
+
def deploy() -> None:
|
|
57
|
+
"""Um comando que a ferramenta já tinha."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
register_commands(cli)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Agora `minhaferramenta check`, `minhaferramenta lint` e os demais
|
|
64
|
+
existem, com o mesmo comportamento e a mesma leitura de
|
|
65
|
+
`[tool.tempest]`.
|
|
66
|
+
|
|
67
|
+
!!! info "É exatamente o que o `tempest-fastapi-sdk` faz"
|
|
68
|
+
O `tempest check` do SDK é este `register_commands` chamado no app
|
|
69
|
+
dele. Por isso os dois nunca divergem: há uma implementação só.
|
|
70
|
+
|
|
71
|
+
## Acrescentar passos ao `check`
|
|
72
|
+
|
|
73
|
+
O `tempest-cli` não sabe nada de framework, e é de propósito. Mas o
|
|
74
|
+
framework sabe: "todo service herda `BaseService`" é regra de quem monta
|
|
75
|
+
o projeto, não de quem roda o ruff. O `CheckStep` é a costura — você
|
|
76
|
+
escreve a regra, o `check` roda.
|
|
77
|
+
|
|
78
|
+
Um `CheckStep` tem dois campos: `name`, o rótulo que o `check` ecoa como
|
|
79
|
+
`$ <name>` no stderr, e `run`, uma função que recebe o alvo do `check`
|
|
80
|
+
(o caminho da linha de comando, `.` por padrão) e devolve um código de
|
|
81
|
+
saída — `0` é passou.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pathlib import Path
|
|
85
|
+
|
|
86
|
+
import typer
|
|
87
|
+
|
|
88
|
+
from tempest_cli import CheckStep
|
|
89
|
+
from tempest_cli.main import register_commands
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def no_print(target: str) -> int:
|
|
93
|
+
"""Fail when a module under ``target`` calls ``print``."""
|
|
94
|
+
offenders: list[str] = [
|
|
95
|
+
f"{path}:{number}: use logging instead of print()"
|
|
96
|
+
for path in sorted(Path(target).rglob("*.py"))
|
|
97
|
+
for number, line in enumerate(path.read_text().splitlines(), start=1)
|
|
98
|
+
if line.lstrip().startswith("print(")
|
|
99
|
+
]
|
|
100
|
+
for offender in offenders:
|
|
101
|
+
typer.echo(offender, err=True)
|
|
102
|
+
return 1 if offenders else 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
cli: typer.Typer = typer.Typer(name="minhaferramenta")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@cli.command("deploy")
|
|
109
|
+
def deploy() -> None:
|
|
110
|
+
"""Um comando que a ferramenta já tinha."""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
register_commands(cli, extra_steps=[CheckStep(name="no-print", run=no_print)])
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
cli()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Com um módulo `pkg/a.py` que chama `print("x")`:
|
|
120
|
+
|
|
121
|
+
```console
|
|
122
|
+
$ python minhaferramenta.py check pkg
|
|
123
|
+
$ ruff check --extend-select ANN001,ANN201,ANN202,ANN205,ANN206 pkg
|
|
124
|
+
All checks passed!
|
|
125
|
+
$ ruff format --check pkg
|
|
126
|
+
1 file already formatted
|
|
127
|
+
$ mypy --disallow-untyped-defs --disallow-incomplete-defs pkg
|
|
128
|
+
Success: no issues found in 1 source file
|
|
129
|
+
$ no-print
|
|
130
|
+
pkg/a.py:2: use logging instead of print()
|
|
131
|
+
$ echo $?
|
|
132
|
+
1
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A ordem é `ruff check` → `ruff format --check` → `mypy` → os passos
|
|
136
|
+
extras, na ordem da lista → `pytest`. Passo extra é checagem estática:
|
|
137
|
+
fica antes da suíte para falhar em um segundo, não depois dela. O
|
|
138
|
+
contrato é o dos passos embutidos — o primeiro código diferente de `0`
|
|
139
|
+
para o gate e vira o código de saída do `check`, com ou sem `--fast`.
|
|
140
|
+
|
|
141
|
+
!!! tip "A mensagem é sua"
|
|
142
|
+
O `check` só ecoa o nome. O que o passo imprime é o que o usuário lê,
|
|
143
|
+
então diga arquivo, linha e a correção — como o `no-print` acima.
|
|
144
|
+
|
|
145
|
+
Sem a CLI, o mesmo vale para `run_full_check`:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from tempest_cli import CheckStep, run_full_check
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def always_passes(target: str) -> int:
|
|
152
|
+
"""A step that accepts any target."""
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
exit_code: int = run_full_check(
|
|
157
|
+
".", extra_steps=[CheckStep(name="always-passes", run=always_passes)]
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`extra_steps` é vazio por padrão: sem ele, o gate é exatamente o de
|
|
162
|
+
antes.
|
|
163
|
+
|
|
164
|
+
## Gerar o prompt de PR sem a CLI
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pathlib import Path
|
|
168
|
+
|
|
169
|
+
from tempest_cli import PromptLanguage, generate_pr_prompt
|
|
170
|
+
|
|
171
|
+
prompt, context, template = generate_pr_prompt(
|
|
172
|
+
base="main",
|
|
173
|
+
head=None,
|
|
174
|
+
cwd=Path("."),
|
|
175
|
+
template=None,
|
|
176
|
+
language=PromptLanguage.PT_BR,
|
|
177
|
+
max_files=20,
|
|
178
|
+
max_chars=8000,
|
|
179
|
+
)
|
|
180
|
+
print(len(context.commits), "commits;", template.source)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`generate_pr_prompt` levanta `GitError` quando o git falha ou uma ref não
|
|
184
|
+
resolve.
|
|
185
|
+
|
|
186
|
+
## Recap
|
|
187
|
+
|
|
188
|
+
- Runners, config e o gerador de prompt são todos importáveis.
|
|
189
|
+
- `register_commands(app)` monta os oito comandos em qualquer `Typer`.
|
|
190
|
+
- `extra_steps=[CheckStep(...)]` põe regras suas no `check`, depois do
|
|
191
|
+
`mypy` e antes do `pytest`.
|
|
192
|
+
- Código de saída é o da ferramenta, igual na CLI e na chamada direta.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tempest-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
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"
|
|
@@ -30,6 +30,11 @@ config = load_tempest_config()
|
|
|
30
30
|
exit_code = run_full_check(".", config=config)
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
A framework that wants its own rules in ``check`` passes them as
|
|
34
|
+
:class:`tempest_cli.CheckStep` values — ``extra_steps`` on
|
|
35
|
+
:func:`run_full_check` and on :func:`tempest_cli.main.register_commands`
|
|
36
|
+
— and they run after ``mypy``, before ``pytest``.
|
|
37
|
+
|
|
33
38
|
And :func:`tempest_cli.main.register_commands` mounts the whole gate onto
|
|
34
39
|
an existing :class:`typer.Typer`, so another CLI can expose these
|
|
35
40
|
commands under its own name without copying them.
|
|
@@ -50,12 +55,14 @@ from tempest_cli.lint import run_ruff_format as run_ruff_format
|
|
|
50
55
|
from tempest_cli.pr_prompt import GitError as GitError
|
|
51
56
|
from tempest_cli.pr_prompt import PromptLanguage as PromptLanguage
|
|
52
57
|
from tempest_cli.pr_prompt import generate_pr_prompt as generate_pr_prompt
|
|
58
|
+
from tempest_cli.steps import CheckStep as CheckStep
|
|
53
59
|
|
|
54
|
-
__version__: str = "0.
|
|
60
|
+
__version__: str = "0.5.0"
|
|
55
61
|
"""Installed package version."""
|
|
56
62
|
|
|
57
63
|
__all__: list[str] = [
|
|
58
64
|
"DEFAULT_TYPING_STRICTNESS",
|
|
65
|
+
"CheckStep",
|
|
59
66
|
"GitError",
|
|
60
67
|
"PromptLanguage",
|
|
61
68
|
"TempestConfig",
|