tempest-cli 0.1.0__tar.gz → 0.2.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.1.0 → tempest_cli-0.2.0}/CHANGELOG.md +33 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/PKG-INFO +13 -3
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/README.md +12 -2
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/changelog.en.md +23 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/changelog.md +23 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/commands.en.md +10 -2
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/commands.md +9 -2
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/index.en.md +1 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/index.md +1 -0
- tempest_cli-0.2.0/docs/installation.en.md +90 -0
- tempest_cli-0.2.0/docs/installation.md +89 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/pyproject.toml +5 -1
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/__init__.py +3 -1
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/lint.py +140 -11
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_cli.py +20 -0
- tempest_cli-0.2.0/tests/test_lint_runners.py +348 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/uv.lock +1 -1
- tempest_cli-0.1.0/docs/installation.en.md +0 -61
- tempest_cli-0.1.0/docs/installation.md +0 -59
- tempest_cli-0.1.0/tests/test_lint_runners.py +0 -154
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.github/workflows/ci.yml +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.github/workflows/docs.yml +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.github/workflows/release-pypi.yml +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.gitignore +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/LICENSE +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/Makefile +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/configuration.en.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/configuration.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/library.en.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/library.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/pr-prompt.en.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/pr-prompt.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/reference.en.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/reference.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/mkdocs.yml +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/config.py +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/main.py +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/pr_prompt.py +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/py.typed +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/__init__.py +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_config.py +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_lint_strictness.py +0 -0
- {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_pr_prompt.py +0 -0
|
@@ -5,6 +5,39 @@ 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.2.0] — 2026-08-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`tc`, the short alias.** The package now installs a second console
|
|
13
|
+
script pointing at the same entry point, so `tc check` is `tempest-cli
|
|
14
|
+
check`. While the installing environment is on `PATH`, `tc` shadows
|
|
15
|
+
iproute2's `tc(8)`; call the traffic controller as `/usr/sbin/tc` when
|
|
16
|
+
you need it.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **The gate no longer runs a dead pyenv/asdf shim.** `tempest-cli fix`
|
|
21
|
+
in a project whose environment has no ruff answered `pyenv: ruff:
|
|
22
|
+
command not found` and exited 127 — the lookup took the first `PATH`
|
|
23
|
+
hit, and on a version-manager machine that hit is a shim that exists
|
|
24
|
+
for every tool any installed interpreter ever provided. The lookup now
|
|
25
|
+
searches the environments that belong to the run first (the CLI's own
|
|
26
|
+
interpreter directory, `$VIRTUAL_ENV`, the nearest `.venv`), accepts a
|
|
27
|
+
shim only after `<tool> --version` proves it dispatches, and applies
|
|
28
|
+
the same check to `uv` itself.
|
|
29
|
+
|
|
30
|
+
- **The `uv` fallback stopped leaking back to `PATH`.** It ran `uv run
|
|
31
|
+
<tool>`, which falls back to `PATH` when the project environment has
|
|
32
|
+
no such tool — landing on the very shim that had just been rejected.
|
|
33
|
+
It is now `uv run --with <tool> <tool>`, so the tool is always present
|
|
34
|
+
in the run's overlay; a version pinned by the project still wins,
|
|
35
|
+
since uv resolves the overlay against the project's requirements.
|
|
36
|
+
|
|
37
|
+
- **The 127 message names the fix.** It now says what to install
|
|
38
|
+
(`uv add --dev ruff`, or `"tempest-cli[tools]"` for the set) instead
|
|
39
|
+
of only reporting that `PATH` and `uv` came up empty.
|
|
40
|
+
|
|
8
41
|
## [0.1.0] — 2026-08-15
|
|
9
42
|
|
|
10
43
|
First release. Extracted from
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tempest-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -50,6 +50,8 @@ uv add --dev tempest-cli
|
|
|
50
50
|
tempest-cli check # lint + fmt-check + type + test, in order, stops at the first failure
|
|
51
51
|
tempest-cli fix # every ruff autofix, then format
|
|
52
52
|
tempest-cli type -s strict # override the configured strictness for one run
|
|
53
|
+
|
|
54
|
+
tc check # `tc` is the short alias for the same program
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
## Why it exists
|
|
@@ -142,8 +144,16 @@ register_commands(cli)
|
|
|
142
144
|
## Installing the tools
|
|
143
145
|
|
|
144
146
|
`tempest-cli` shells out to whatever `ruff`, `mypy` and `pytest` it
|
|
145
|
-
finds — it does not pin them, so your project chooses the versions.
|
|
146
|
-
|
|
147
|
+
finds — it does not pin them, so your project chooses the versions. The
|
|
148
|
+
lookup runs in this order:
|
|
149
|
+
|
|
150
|
+
1. the environments of the run — the interpreter's own directory, then
|
|
151
|
+
`$VIRTUAL_ENV`, then the nearest `.venv` up the tree;
|
|
152
|
+
2. `PATH`, skipping a pyenv/asdf shim that dispatches nowhere (the one
|
|
153
|
+
that answers `pyenv: ruff: command not found`);
|
|
154
|
+
3. `uv run --with <tool> <tool>`, when `uv` is available.
|
|
155
|
+
|
|
156
|
+
To install the three alongside it:
|
|
147
157
|
|
|
148
158
|
```bash
|
|
149
159
|
uv add --dev "tempest-cli[tools]"
|
|
@@ -19,6 +19,8 @@ uv add --dev tempest-cli
|
|
|
19
19
|
tempest-cli check # lint + fmt-check + type + test, in order, stops at the first failure
|
|
20
20
|
tempest-cli fix # every ruff autofix, then format
|
|
21
21
|
tempest-cli type -s strict # override the configured strictness for one run
|
|
22
|
+
|
|
23
|
+
tc check # `tc` is the short alias for the same program
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
## Why it exists
|
|
@@ -111,8 +113,16 @@ register_commands(cli)
|
|
|
111
113
|
## Installing the tools
|
|
112
114
|
|
|
113
115
|
`tempest-cli` shells out to whatever `ruff`, `mypy` and `pytest` it
|
|
114
|
-
finds — it does not pin them, so your project chooses the versions.
|
|
115
|
-
|
|
116
|
+
finds — it does not pin them, so your project chooses the versions. The
|
|
117
|
+
lookup runs in this order:
|
|
118
|
+
|
|
119
|
+
1. the environments of the run — the interpreter's own directory, then
|
|
120
|
+
`$VIRTUAL_ENV`, then the nearest `.venv` up the tree;
|
|
121
|
+
2. `PATH`, skipping a pyenv/asdf shim that dispatches nowhere (the one
|
|
122
|
+
that answers `pyenv: ruff: command not found`);
|
|
123
|
+
3. `uv run --with <tool> <tool>`, when `uv` is available.
|
|
124
|
+
|
|
125
|
+
To install the three alongside it:
|
|
116
126
|
|
|
117
127
|
```bash
|
|
118
128
|
uv add --dev "tempest-cli[tools]"
|
|
@@ -3,6 +3,29 @@
|
|
|
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.2.0] — 2026-08-15
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`tc`**, a short alias installed next to `tempest-cli` — the same
|
|
11
|
+
program. While the installing environment is on `PATH` it shadows
|
|
12
|
+
iproute2's `tc(8)` (call `/usr/sbin/tc` for the network one).
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **The gate no longer runs a dead pyenv/asdf shim.** `tempest-cli fix`
|
|
17
|
+
in a project without ruff answered `pyenv: ruff: command not found`
|
|
18
|
+
and exited 127. The lookup now searches the run's own environments
|
|
19
|
+
first (the CLI's interpreter directory, `$VIRTUAL_ENV`, the nearest
|
|
20
|
+
`.venv`) and accepts a shim only after `<tool> --version` proves it
|
|
21
|
+
dispatches — the same check applies to `uv` itself.
|
|
22
|
+
- **The `uv` fallback stopped leaking back to `PATH`.** It was `uv run
|
|
23
|
+
<tool>`, which falls back to `PATH` when the project environment has
|
|
24
|
+
no such tool — landing on the shim just rejected. It is now `uv run
|
|
25
|
+
--with <tool> <tool>`.
|
|
26
|
+
- **The 127 message names the fix** (`uv add --dev ruff`, or
|
|
27
|
+
`"tempest-cli[tools]"` for the set).
|
|
28
|
+
|
|
6
29
|
## [0.1.0] — 2026-08-15
|
|
7
30
|
|
|
8
31
|
First release. Extracted from
|
|
@@ -4,6 +4,29 @@ 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.2.0] — 2026-08-15
|
|
8
|
+
|
|
9
|
+
### Adicionado
|
|
10
|
+
|
|
11
|
+
- **`tc`**, apelido curto instalado ao lado de `tempest-cli` — mesmo
|
|
12
|
+
programa. Enquanto o ambiente instalador estiver no `PATH`, ele
|
|
13
|
+
sombreia o `tc(8)` do iproute2 (chame `/usr/sbin/tc` para o de rede).
|
|
14
|
+
|
|
15
|
+
### Corrigido
|
|
16
|
+
|
|
17
|
+
- **O gate não roda mais um shim morto do pyenv/asdf.** `tempest-cli
|
|
18
|
+
fix` num projeto sem ruff respondia `pyenv: ruff: command not found` e
|
|
19
|
+
saía 127. A busca agora tenta primeiro os ambientes da execução
|
|
20
|
+
(diretório do interpretador da CLI, `$VIRTUAL_ENV`, `.venv` mais
|
|
21
|
+
próximo) e só aceita um shim depois que `<ferramenta> --version` prova
|
|
22
|
+
que ele despacha — o mesmo vale para o próprio `uv`.
|
|
23
|
+
- **O fallback do `uv` parou de vazar para o `PATH`.** Era `uv run
|
|
24
|
+
<ferramenta>`, que cai no `PATH` quando o ambiente do projeto não tem
|
|
25
|
+
a ferramenta — voltando ao shim recém-rejeitado. Agora é `uv run
|
|
26
|
+
--with <ferramenta> <ferramenta>`.
|
|
27
|
+
- **A mensagem do 127 diz o que instalar** (`uv add --dev ruff`, ou
|
|
28
|
+
`"tempest-cli[tools]"` para as três).
|
|
29
|
+
|
|
7
30
|
## [0.1.0] — 2026-08-15
|
|
8
31
|
|
|
9
32
|
Primeira versão. Extraída do
|
|
@@ -14,6 +14,11 @@ tool's exit code.
|
|
|
14
14
|
| `check` | the four above, in order, stopping at the first failure |
|
|
15
15
|
| `pr-prompt` | builds the PR-description prompt — [its own page](pr-prompt.md) |
|
|
16
16
|
|
|
17
|
+
!!! tip "`tc` is the short form"
|
|
18
|
+
The package installs `tempest-cli` and `tc` pointing at the same
|
|
19
|
+
program. The examples use the long name; `tc check`, `tc fix` and
|
|
20
|
+
`tc type -s strict` work exactly the same.
|
|
21
|
+
|
|
17
22
|
## The full gate
|
|
18
23
|
|
|
19
24
|
```bash
|
|
@@ -63,8 +68,11 @@ The exit code is the tool's own, untranslated:
|
|
|
63
68
|
tempest-cli lint; echo "exited $?"
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
There is exactly one code of its own: **127**, when the tool is
|
|
67
|
-
|
|
71
|
+
There is exactly one code of its own: **127**, when the tool is in none
|
|
72
|
+
of the places the CLI looks — the run's environment, `PATH`, `uv run
|
|
73
|
+
--with` — and the message then names what was missing and how to install
|
|
74
|
+
it. The lookup order is in
|
|
75
|
+
[Installation](installation.md#where-ruff-mypy-and-pytest-come-from).
|
|
68
76
|
|
|
69
77
|
## Recap
|
|
70
78
|
|
|
@@ -14,6 +14,11 @@ saída da ferramenta por baixo.
|
|
|
14
14
|
| `check` | os quatro acima, em ordem, parando na primeira falha |
|
|
15
15
|
| `pr-prompt` | monta o prompt da descrição do PR — [página própria](pr-prompt.md) |
|
|
16
16
|
|
|
17
|
+
!!! tip "`tc` é a forma curta"
|
|
18
|
+
O pacote instala `tempest-cli` e `tc` apontando para o mesmo
|
|
19
|
+
programa. Os exemplos usam o nome longo; `tc check`, `tc fix` e
|
|
20
|
+
`tc type -s strict` funcionam igual.
|
|
21
|
+
|
|
17
22
|
## O gate completo
|
|
18
23
|
|
|
19
24
|
```bash
|
|
@@ -63,8 +68,10 @@ O código de saída é o da ferramenta, sem tradução:
|
|
|
63
68
|
tempest-cli lint; echo "saiu $?"
|
|
64
69
|
```
|
|
65
70
|
|
|
66
|
-
Só há um código próprio: **127**, quando a ferramenta não está
|
|
67
|
-
|
|
71
|
+
Só há um código próprio: **127**, quando a ferramenta não está em nenhum
|
|
72
|
+
dos lugares onde a CLI procura — ambiente da execução, `PATH`, `uv run
|
|
73
|
+
--with` — e aí a mensagem diz qual faltou e como instalá-la. A ordem de
|
|
74
|
+
busca está em [Instalação](installation.md#de-onde-vem-o-ruff-o-mypy-e-o-pytest).
|
|
68
75
|
|
|
69
76
|
## Recap
|
|
70
77
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
uv add --dev tempest-cli
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Or with pip:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install tempest-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires **Python 3.11+**. The only runtime dependency is `typer`.
|
|
14
|
+
|
|
15
|
+
It installs **two executables**: `tempest-cli` and the short alias `tc`.
|
|
16
|
+
They are the same program — everything in these docs applies to both.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
tc check # identical to tempest-cli check
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
!!! warning "`tc` is also iproute2's `tc(8)`"
|
|
23
|
+
While the environment that installed the package is on `PATH`, `tc`
|
|
24
|
+
resolves to this CLI. For Linux traffic control, call it by absolute
|
|
25
|
+
path (`/usr/sbin/tc`) — or use `tempest-cli` and leave `tc` alone.
|
|
26
|
+
|
|
27
|
+
## Where ruff, mypy and pytest come from
|
|
28
|
+
|
|
29
|
+
`tempest-cli` does **not** pin any of the three: it runs whatever it
|
|
30
|
+
finds, in this order:
|
|
31
|
+
|
|
32
|
+
1. **the environments of the run** — the directory of the interpreter
|
|
33
|
+
running the CLI (where the `[tools]` extra installs all three), then
|
|
34
|
+
`$VIRTUAL_ENV`, then the nearest `.venv` up the tree;
|
|
35
|
+
2. **`PATH`** — skipping a version-manager shim that dispatches nowhere
|
|
36
|
+
(see below);
|
|
37
|
+
3. **`uv run --with <tool> <tool>`** when `uv` is available — the
|
|
38
|
+
project's environment plus the tool, with no activation and no prior
|
|
39
|
+
`uv sync` required.
|
|
40
|
+
|
|
41
|
+
If nothing resolves, the command exits with **127**, naming the missing
|
|
42
|
+
tool and how to install it, instead of raising a traceback.
|
|
43
|
+
|
|
44
|
+
!!! info "Why `PATH` does not come first"
|
|
45
|
+
On a pyenv/asdf machine the `shims` directory is on `PATH`
|
|
46
|
+
globally and holds a stub for every tool any installed version ever
|
|
47
|
+
provided. Looking up `ruff` there finds `~/.pyenv/shims/ruff` even
|
|
48
|
+
when the project has no ruff at all — and running it prints
|
|
49
|
+
`pyenv: ruff: command not found`. So the run's own environment wins
|
|
50
|
+
over `PATH`, and a shim is only accepted after proving it dispatches
|
|
51
|
+
(`<tool> --version` exiting `0`).
|
|
52
|
+
|
|
53
|
+
!!! tip "Why the versions are not pinned"
|
|
54
|
+
A ruff version pinned here would become the ceiling for every
|
|
55
|
+
project installing this package — and upgrading the linter is a
|
|
56
|
+
decision for whoever writes the code, not for whoever packages the
|
|
57
|
+
gate.
|
|
58
|
+
|
|
59
|
+
If you would rather install all three alongside:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv add --dev "tempest-cli[tools]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Verifying
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
tempest-cli --version
|
|
69
|
+
tc --version # the same program
|
|
70
|
+
tempest-cli check --help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## In CI
|
|
74
|
+
|
|
75
|
+
Every command returns the underlying tool's exit code, so the job reads
|
|
76
|
+
it exactly as it would read `ruff` directly:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
- name: Quality gate
|
|
80
|
+
run: uv run tempest-cli check
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Recap
|
|
84
|
+
|
|
85
|
+
- `uv add --dev tempest-cli`, Python 3.11+, `typer` as the only
|
|
86
|
+
dependency.
|
|
87
|
+
- Two executables: `tempest-cli` and the `tc` alias.
|
|
88
|
+
- Tools come from the run's environment, from `PATH` (never a dead
|
|
89
|
+
shim), or through `uv run --with`.
|
|
90
|
+
- A missing tool means exit 127 with a message, not a traceback.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Instalação
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
uv add --dev tempest-cli
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Ou com pip:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install tempest-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requer **Python 3.11+**. A única dependência de runtime é o `typer`.
|
|
14
|
+
|
|
15
|
+
Ficam instalados **dois executáveis**: `tempest-cli` e o apelido curto
|
|
16
|
+
`tc`. São o mesmo programa — tudo nesta documentação vale para os dois.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
tc check # idêntico a tempest-cli check
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
!!! warning "`tc` também é o `tc(8)` do iproute2"
|
|
23
|
+
Enquanto o ambiente que instalou o pacote estiver no `PATH`, `tc`
|
|
24
|
+
resolve para esta CLI. Para o controlador de tráfego do Linux, chame
|
|
25
|
+
pelo caminho absoluto (`/usr/sbin/tc`) ou use `tempest-cli` e deixe
|
|
26
|
+
o `tc` livre.
|
|
27
|
+
|
|
28
|
+
## De onde vêm o ruff, o mypy e o pytest
|
|
29
|
+
|
|
30
|
+
O `tempest-cli` **não** fixa versão de nenhum dos três: ele executa o que
|
|
31
|
+
encontrar, nesta ordem:
|
|
32
|
+
|
|
33
|
+
1. **o ambiente da execução** — o diretório do interpretador que está
|
|
34
|
+
rodando a CLI (onde o extra `[tools]` instala as três), depois
|
|
35
|
+
`$VIRTUAL_ENV`, depois o `.venv` mais próximo subindo a árvore;
|
|
36
|
+
2. **o `PATH`** — pulando um *shim* de gerenciador de versão que não
|
|
37
|
+
despacha para lugar nenhum (veja abaixo);
|
|
38
|
+
3. **`uv run --with <ferramenta> <ferramenta>`**, quando o `uv` está
|
|
39
|
+
disponível — o ambiente do projeto mais a ferramenta, sem exigir
|
|
40
|
+
ativação nem `uv sync` prévio.
|
|
41
|
+
|
|
42
|
+
Se nada resolver, o comando sai com **127** e diz qual ferramenta faltou
|
|
43
|
+
e como instalá-la, em vez de estourar um traceback.
|
|
44
|
+
|
|
45
|
+
!!! info "Por que o `PATH` não vem primeiro"
|
|
46
|
+
Em máquina com pyenv/asdf, o diretório `shims` está no `PATH`
|
|
47
|
+
globalmente e tem um stub para toda ferramenta que qualquer versão
|
|
48
|
+
instalada já forneceu. Procurar `ruff` ali acha
|
|
49
|
+
`~/.pyenv/shims/ruff` mesmo quando o projeto não tem ruff nenhum — e
|
|
50
|
+
rodá-lo dá `pyenv: ruff: command not found`. Por isso o ambiente da
|
|
51
|
+
execução ganha do `PATH`, e um shim só é aceito depois de provar que
|
|
52
|
+
despacha (`<ferramenta> --version` saindo `0`).
|
|
53
|
+
|
|
54
|
+
!!! tip "Por que não fixar as versões"
|
|
55
|
+
Uma versão de ruff fixada aqui viraria o teto de todo projeto que
|
|
56
|
+
instalasse este pacote — e atualizar o linter é decisão de quem
|
|
57
|
+
escreve o código, não de quem empacota o gate.
|
|
58
|
+
|
|
59
|
+
Se você prefere instalar as três junto:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv add --dev "tempest-cli[tools]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Verificando
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
tempest-cli --version
|
|
69
|
+
tc --version # o mesmo programa
|
|
70
|
+
tempest-cli check --help
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Em CI
|
|
74
|
+
|
|
75
|
+
Cada comando devolve o código de saída da ferramenta por baixo, então o
|
|
76
|
+
job lê exatamente como leria o `ruff` direto:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
- name: Quality gate
|
|
80
|
+
run: uv run tempest-cli check
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Recap
|
|
84
|
+
|
|
85
|
+
- `uv add --dev tempest-cli`, Python 3.11+, só o `typer` de dependência.
|
|
86
|
+
- Dois executáveis: `tempest-cli` e o apelido `tc`.
|
|
87
|
+
- As ferramentas vêm do ambiente da execução, do `PATH` (sem shim morto)
|
|
88
|
+
ou do `uv run --with`.
|
|
89
|
+
- Ferramenta ausente = saída 127 com mensagem, não traceback.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tempest-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.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"
|
|
@@ -53,6 +53,10 @@ Documentation = "https://mauriciobenjamin700.github.io/tempest-cli/"
|
|
|
53
53
|
|
|
54
54
|
[project.scripts]
|
|
55
55
|
tempest-cli = "tempest_cli.main:main"
|
|
56
|
+
# `tc` is the short alias for the same entry point. It shadows iproute2's
|
|
57
|
+
# `tc(8)` while the environment providing it is on PATH — a traffic-control
|
|
58
|
+
# call must then be made through its absolute path (/usr/sbin/tc).
|
|
59
|
+
tc = "tempest_cli.main:main"
|
|
56
60
|
|
|
57
61
|
[build-system]
|
|
58
62
|
requires = ["hatchling"]
|
|
@@ -14,6 +14,8 @@ tempest-cli check # lint + fmt-check + type + test
|
|
|
14
14
|
tempest-cli fix # every ruff autofix, then format
|
|
15
15
|
tempest-cli type -s strict # override the configured strictness
|
|
16
16
|
tempest-cli pr-prompt | claude -p
|
|
17
|
+
|
|
18
|
+
tc check # `tc` is the short alias for the same CLI
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
Everything is importable too, for a project that would rather wire the
|
|
@@ -47,7 +49,7 @@ from tempest_cli.pr_prompt import GitError as GitError
|
|
|
47
49
|
from tempest_cli.pr_prompt import PromptLanguage as PromptLanguage
|
|
48
50
|
from tempest_cli.pr_prompt import generate_pr_prompt as generate_pr_prompt
|
|
49
51
|
|
|
50
|
-
__version__: str = "0.
|
|
52
|
+
__version__: str = "0.2.0"
|
|
51
53
|
"""Installed package version."""
|
|
52
54
|
|
|
53
55
|
__all__: list[str] = [
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
|
|
9
11
|
import typer
|
|
10
12
|
|
|
11
13
|
from tempest_cli.config import TempestConfig
|
|
12
14
|
|
|
15
|
+
SHIM_PROBE_TIMEOUT_SECONDS = 20.0
|
|
16
|
+
"""Seconds allowed for the ``<tool> --version`` probe of a version-manager shim."""
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
def _ruff_ann_args(config: TempestConfig | None) -> list[str]:
|
|
15
20
|
"""Build the ruff ``--extend-select`` args for ``config``'s level.
|
|
@@ -28,20 +33,138 @@ def _ruff_ann_args(config: TempestConfig | None) -> list[str]:
|
|
|
28
33
|
return ["--extend-select", ",".join(codes)]
|
|
29
34
|
|
|
30
35
|
|
|
36
|
+
def _environment_dirs() -> list[Path]:
|
|
37
|
+
"""Return the executable directories searched ahead of ``PATH``.
|
|
38
|
+
|
|
39
|
+
``PATH`` is a poor first answer on a machine running a version
|
|
40
|
+
manager: pyenv/asdf put their shim directory on it globally, so a
|
|
41
|
+
lookup finds ``~/.pyenv/shims/ruff`` even when the project's own
|
|
42
|
+
environment has no ruff at all. The environments below are the ones
|
|
43
|
+
that actually belong to the run, so they are consulted first:
|
|
44
|
+
|
|
45
|
+
1. the directory of the interpreter running this CLI — where the
|
|
46
|
+
``[tools]`` extra installs ruff/mypy/pytest next to the
|
|
47
|
+
``tempest-cli`` executable itself;
|
|
48
|
+
2. ``$VIRTUAL_ENV`` — the activated environment, which matters when
|
|
49
|
+
the CLI itself lives elsewhere (``uv tool install`` / pipx);
|
|
50
|
+
3. the nearest ``.venv`` walking up from the working directory — a
|
|
51
|
+
project environment that was never activated.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
list[Path]: Existing directories, in preference order, without
|
|
55
|
+
duplicates.
|
|
56
|
+
"""
|
|
57
|
+
roots: list[Path] = [Path(sys.executable).parent]
|
|
58
|
+
virtual_env = os.environ.get("VIRTUAL_ENV")
|
|
59
|
+
if virtual_env:
|
|
60
|
+
roots.extend((Path(virtual_env) / "bin", Path(virtual_env) / "Scripts"))
|
|
61
|
+
try:
|
|
62
|
+
cwd = Path.cwd()
|
|
63
|
+
except OSError:
|
|
64
|
+
cwd = None
|
|
65
|
+
if cwd is not None:
|
|
66
|
+
for directory in (cwd, *cwd.parents):
|
|
67
|
+
candidate = directory / ".venv"
|
|
68
|
+
if candidate.is_dir():
|
|
69
|
+
roots.extend((candidate / "bin", candidate / "Scripts"))
|
|
70
|
+
break
|
|
71
|
+
seen: set[Path] = set()
|
|
72
|
+
ordered: list[Path] = []
|
|
73
|
+
for root in roots:
|
|
74
|
+
if root not in seen and root.is_dir():
|
|
75
|
+
seen.add(root)
|
|
76
|
+
ordered.append(root)
|
|
77
|
+
return ordered
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_shim(path: Path) -> bool:
|
|
81
|
+
"""Report whether ``path`` looks like a version-manager shim.
|
|
82
|
+
|
|
83
|
+
pyenv, asdf and rbenv all install their shims into a directory named
|
|
84
|
+
``shims``. A shim is a stub that exists for every tool any installed
|
|
85
|
+
interpreter version ever provided, so its presence says nothing
|
|
86
|
+
about whether the command can actually run right now.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path (Path): The resolved executable path.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
bool: True when the executable sits in a ``shims`` directory.
|
|
93
|
+
"""
|
|
94
|
+
return path.parent.name == "shims"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _shim_runs(path: Path) -> bool:
|
|
98
|
+
"""Report whether a shim actually dispatches to a real executable.
|
|
99
|
+
|
|
100
|
+
Runs ``<path> --version`` and reads the exit code. A pyenv shim for
|
|
101
|
+
a tool missing from the selected version prints ``pyenv: ruff:
|
|
102
|
+
command not found`` and exits non-zero — running the gate through it
|
|
103
|
+
would fail with that message instead of falling back to a runner
|
|
104
|
+
that works.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
path (Path): The shim to probe.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
bool: True when the probe exits ``0``. False on a non-zero exit,
|
|
111
|
+
a timeout, or an OS-level failure to spawn.
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
completed = subprocess.run(
|
|
115
|
+
[str(path), "--version"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
timeout=SHIM_PROBE_TIMEOUT_SECONDS,
|
|
118
|
+
)
|
|
119
|
+
except (OSError, subprocess.SubprocessError):
|
|
120
|
+
return False
|
|
121
|
+
return completed.returncode == 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _path_lookup(executable: str) -> str | None:
|
|
125
|
+
"""Find ``executable`` on ``PATH``, rejecting a shim that goes nowhere.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
executable (str): The command name to look up.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
str | None: The resolved path, or ``None`` when the command is
|
|
132
|
+
absent or resolves to a shim that does not dispatch.
|
|
133
|
+
"""
|
|
134
|
+
found = shutil.which(executable)
|
|
135
|
+
if found is None:
|
|
136
|
+
return None
|
|
137
|
+
path = Path(found)
|
|
138
|
+
if _is_shim(path) and not _shim_runs(path):
|
|
139
|
+
return None
|
|
140
|
+
return found
|
|
141
|
+
|
|
142
|
+
|
|
31
143
|
def resolve_tool(executable: str) -> list[str] | None:
|
|
32
144
|
"""Return an argv prefix invoking ``executable`` or ``None`` when absent.
|
|
33
145
|
|
|
34
146
|
Public because callers outside the gate need the same lookup — the
|
|
35
147
|
SDK's OpenAPI code generator formats what it emits with the project's
|
|
36
|
-
own ruff, and reimplementing the
|
|
37
|
-
be a second answer to the same question.
|
|
148
|
+
own ruff, and reimplementing the environment/``uv run`` fallback
|
|
149
|
+
there would be a second answer to the same question.
|
|
38
150
|
|
|
39
151
|
Preference order:
|
|
40
152
|
|
|
41
|
-
1.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
153
|
+
1. the environments that belong to this run (see
|
|
154
|
+
:func:`_environment_dirs`): the CLI's own interpreter directory,
|
|
155
|
+
``$VIRTUAL_ENV``, then the nearest ``.venv``;
|
|
156
|
+
2. ``executable`` on ``PATH`` — skipped when it resolves to a
|
|
157
|
+
version-manager shim that does not dispatch anywhere;
|
|
158
|
+
3. ``uv run --with <executable> <executable>`` when ``uv`` is on the
|
|
159
|
+
``PATH``: the project's own environment plus the tool, without
|
|
160
|
+
requiring activation or a prior ``uv sync``.
|
|
161
|
+
|
|
162
|
+
Step 3 carries ``--with`` on purpose. A plain ``uv run ruff`` falls
|
|
163
|
+
back to ``PATH`` when the project environment has no ruff — landing
|
|
164
|
+
right back on the dead shim this lookup just rejected. ``--with``
|
|
165
|
+
puts the tool in the run's own overlay, so the command is always the
|
|
166
|
+
one that runs. When the project pins a version, uv resolves the
|
|
167
|
+
overlay against the project's requirements, so the pin still wins.
|
|
45
168
|
|
|
46
169
|
Args:
|
|
47
170
|
executable (str): The command name (``ruff``/``mypy``/``pytest``).
|
|
@@ -50,12 +173,16 @@ def resolve_tool(executable: str) -> list[str] | None:
|
|
|
50
173
|
list[str] | None: argv prefix to extend with extra arguments, or
|
|
51
174
|
``None`` when no runner could be found.
|
|
52
175
|
"""
|
|
53
|
-
|
|
176
|
+
for directory in _environment_dirs():
|
|
177
|
+
local = shutil.which(executable, path=str(directory))
|
|
178
|
+
if local is not None:
|
|
179
|
+
return [local]
|
|
180
|
+
direct = _path_lookup(executable)
|
|
54
181
|
if direct is not None:
|
|
55
182
|
return [direct]
|
|
56
|
-
uv =
|
|
183
|
+
uv = _path_lookup("uv")
|
|
57
184
|
if uv is not None:
|
|
58
|
-
return [uv, "run", executable]
|
|
185
|
+
return [uv, "run", "--with", executable, executable]
|
|
59
186
|
return None
|
|
60
187
|
|
|
61
188
|
|
|
@@ -73,8 +200,10 @@ def _execute(executable: str, args: list[str]) -> int:
|
|
|
73
200
|
argv = resolve_tool(executable)
|
|
74
201
|
if argv is None:
|
|
75
202
|
typer.echo(
|
|
76
|
-
f"error: '{executable}'
|
|
77
|
-
f"
|
|
203
|
+
f"error: '{executable}' was not found in this project's environment, "
|
|
204
|
+
f"on PATH, and 'uv' is unavailable to run it. "
|
|
205
|
+
f"Install it with 'uv add --dev {executable}' — or the whole set "
|
|
206
|
+
f"with 'uv add --dev \"tempest-cli[tools]\"' — and retry.",
|
|
78
207
|
err=True,
|
|
79
208
|
)
|
|
80
209
|
return 127
|
|
@@ -82,6 +82,26 @@ def test_public_surface_is_importable() -> None:
|
|
|
82
82
|
assert hasattr(tempest_cli, name), name
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def test_both_console_scripts_point_at_the_same_entry_point() -> None:
|
|
86
|
+
"""`tc` is the short alias, so it must stay wired to `main`.
|
|
87
|
+
|
|
88
|
+
Read from the installed distribution rather than from
|
|
89
|
+
``pyproject.toml``: what matters is the script a user actually got,
|
|
90
|
+
which is what the wheel declared at build time.
|
|
91
|
+
"""
|
|
92
|
+
from importlib.metadata import entry_points
|
|
93
|
+
|
|
94
|
+
scripts = {
|
|
95
|
+
entry.name: entry.value
|
|
96
|
+
for entry in entry_points(group="console_scripts")
|
|
97
|
+
if entry.name in {"tempest-cli", "tc"}
|
|
98
|
+
}
|
|
99
|
+
assert scripts == {
|
|
100
|
+
"tempest-cli": "tempest_cli.main:main",
|
|
101
|
+
"tc": "tempest_cli.main:main",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
85
105
|
def test_importing_the_package_pulls_no_web_framework() -> None:
|
|
86
106
|
"""The whole point of the extraction: no FastAPI, no SQLAlchemy."""
|
|
87
107
|
import subprocess
|