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.
Files changed (45) hide show
  1. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/CHANGELOG.md +33 -0
  2. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/PKG-INFO +13 -3
  3. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/README.md +12 -2
  4. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/changelog.en.md +23 -0
  5. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/changelog.md +23 -0
  6. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/commands.en.md +10 -2
  7. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/commands.md +9 -2
  8. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/index.en.md +1 -0
  9. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/index.md +1 -0
  10. tempest_cli-0.2.0/docs/installation.en.md +90 -0
  11. tempest_cli-0.2.0/docs/installation.md +89 -0
  12. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/pyproject.toml +5 -1
  13. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/__init__.py +3 -1
  14. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/lint.py +140 -11
  15. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_cli.py +20 -0
  16. tempest_cli-0.2.0/tests/test_lint_runners.py +348 -0
  17. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/uv.lock +1 -1
  18. tempest_cli-0.1.0/docs/installation.en.md +0 -61
  19. tempest_cli-0.1.0/docs/installation.md +0 -59
  20. tempest_cli-0.1.0/tests/test_lint_runners.py +0 -154
  21. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.github/workflows/ci.yml +0 -0
  22. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.github/workflows/docs.yml +0 -0
  23. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.github/workflows/release-pypi.yml +0 -0
  24. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/.gitignore +0 -0
  25. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/LICENSE +0 -0
  26. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/Makefile +0 -0
  27. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/configuration.en.md +0 -0
  28. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/configuration.md +0 -0
  29. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/library.en.md +0 -0
  30. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/library.md +0 -0
  31. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/pr-prompt.en.md +0 -0
  32. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/pr-prompt.md +0 -0
  33. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/reference.en.md +0 -0
  34. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/docs/reference.md +0 -0
  35. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/mkdocs.yml +0 -0
  36. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
  37. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
  38. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/config.py +0 -0
  39. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/main.py +0 -0
  40. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/pr_prompt.py +0 -0
  41. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tempest_cli/py.typed +0 -0
  42. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/__init__.py +0 -0
  43. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_config.py +0 -0
  44. {tempest_cli-0.1.0 → tempest_cli-0.2.0}/tests/test_lint_strictness.py +0 -0
  45. {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.1.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. To
146
- install them alongside it:
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. To
115
- install them alongside it:
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 not on
67
- `PATH` and `uv` is not either — the message then names what was missing.
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á no `PATH`
67
- e o `uv` também não — aí a mensagem diz qual faltou.
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
 
@@ -10,6 +10,7 @@ library, a script. The only runtime dependency is `typer`.
10
10
  ```bash
11
11
  uv add --dev tempest-cli
12
12
  tempest-cli check
13
+ tc check # the same, through the short alias
13
14
  ```
14
15
 
15
16
  ```console
@@ -10,6 +10,7 @@ uma biblioteca, um script. A única dependência de runtime é o `typer`.
10
10
  ```bash
11
11
  uv add --dev tempest-cli
12
12
  tempest-cli check
13
+ tc check # o mesmo, pelo apelido curto
13
14
  ```
14
15
 
15
16
  ```console
@@ -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.1.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.1.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 PATH/``uv run`` fallback there would
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. ``executable`` available on ``PATH`` directly (already activated venv,
42
- global install, etc.).
43
- 2. ``uv run <executable>`` when ``uv`` is on the ``PATH`` (handles
44
- project-local virtualenvs without requiring activation).
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
- direct = shutil.which(executable)
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 = shutil.which("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}' is not on PATH and 'uv' is unavailable. "
77
- f"Install it (or activate the project venv) and retry.",
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