tempest-cli 0.5.1__tar.gz → 0.6.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 (51) hide show
  1. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/CHANGELOG.md +22 -0
  2. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/PKG-INFO +5 -4
  3. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/README.md +4 -3
  4. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/changelog.en.md +16 -1
  5. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/changelog.md +16 -1
  6. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/commands.en.md +63 -43
  7. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/commands.md +48 -29
  8. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/installation.en.md +3 -2
  9. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/installation.md +3 -2
  10. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/library.en.md +3 -2
  11. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/library.md +3 -2
  12. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/pyproject.toml +1 -1
  13. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/__init__.py +1 -1
  14. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/lint.py +88 -29
  15. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/main.py +55 -27
  16. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_extra_steps.py +1 -1
  17. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_fast.py +82 -9
  18. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/uv.lock +1 -1
  19. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/.github/workflows/ci.yml +0 -0
  20. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/.github/workflows/docs.yml +0 -0
  21. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/.github/workflows/release-pypi.yml +0 -0
  22. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/.gitignore +0 -0
  23. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/LICENSE +0 -0
  24. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/Makefile +0 -0
  25. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/configuration.en.md +0 -0
  26. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/configuration.md +0 -0
  27. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/index.en.md +0 -0
  28. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/index.md +0 -0
  29. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/pr-prompt.en.md +0 -0
  30. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/pr-prompt.md +0 -0
  31. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/reference.en.md +0 -0
  32. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/reference.md +0 -0
  33. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/rules.en.md +0 -0
  34. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/docs/rules.md +0 -0
  35. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/mkdocs.yml +0 -0
  36. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/_templates/pull_request_template.en-US.md +0 -0
  37. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/_templates/pull_request_template.pt-BR.md +0 -0
  38. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/config.py +0 -0
  39. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/pr_prompt.py +0 -0
  40. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/py.typed +0 -0
  41. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/residuals.py +0 -0
  42. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/rewrite.py +0 -0
  43. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tempest_cli/steps.py +0 -0
  44. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/__init__.py +0 -0
  45. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_cli.py +0 -0
  46. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_config.py +0 -0
  47. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_fix_residuals.py +0 -0
  48. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_lint_runners.py +0 -0
  49. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_lint_strictness.py +0 -0
  50. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_pr_prompt.py +0 -0
  51. {tempest_cli-0.5.1 → tempest_cli-0.6.0}/tests/test_rewrite.py +0 -0
@@ -5,6 +5,28 @@ 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
+ ## [Unreleased]
9
+
10
+ ## [0.6.0] — 2026-10-20
11
+
12
+ ### Changed
13
+
14
+ - **`test` and `check` run the suite in parallel by default.** The
15
+ pytest-xdist run that needed `--fast` (`-n auto -p no:cacheprovider`) is
16
+ now what a bare `tempest-cli test` does; `--serial` opts out (for
17
+ `--lf` / `--ff`, or a debugger), and `--workers` tunes the default
18
+ without a flag. Measured on a FastAPI service suite of 1136 tests on a
19
+ 12-CPU machine: 262 s serially, 37.6 s in parallel, same outcome.
20
+ - **Without pytest-xdist the default degrades instead of failing.** When
21
+ the environment pytest runs in cannot import it, a bare `test` / `check`
22
+ prints a one-line notice naming the install command and runs the suite
23
+ serially (exit code is pytest's); an explicit `--fast` keeps the old
24
+ behavior — the fix is printed and the exit code is 127, nothing runs.
25
+ `run_pytest` / `run_full_check` take `require_xdist=` (default `True`,
26
+ the old library behavior) to pick between the two.
27
+ - `--workers` with `--serial` is a usage error (exit 2), as `--workers`
28
+ without `--fast` used to be.
29
+
8
30
  ## [0.5.1] — 2026-10-15
9
31
 
10
32
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tempest-cli
3
- Version: 0.5.1
3
+ Version: 0.6.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
@@ -74,8 +74,8 @@ remembered to pass.
74
74
  | `tempest-cli format` | `ruff format` (writes) |
75
75
  | `tempest-cli fmt-check` | `ruff format --check` (read-only) |
76
76
  | `tempest-cli type` | `mypy` |
77
- | `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; a host CLI can add steps between `mypy` and `pytest`) |
77
+ | `tempest-cli test` | `pytest`, in parallel by default with pytest-xdist (`-n auto -p no:cacheprovider`, `-w N` to choose; serial with a notice when it is not installed; `--serial` opts out) |
78
+ | `tempest-cli check` | all four, in order, stopping at the first failure (the test step in parallel as in `test`; 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
@@ -188,7 +188,8 @@ your suite. Add them yourself, or take the bundle:
188
188
  uv add --dev "tempest-cli[tools]" # mypy + pytest + pytest-xdist
189
189
  ```
190
190
 
191
- `pytest-xdist` is what `test --fast` runs the suite with; its
191
+ `pytest-xdist` is what `test` runs the suite in parallel with, by
192
+ default (without it the suite runs serially, with a notice); its
192
193
  `requires-dist` (`execnet>=2.1`, `pytest>=7.0.0`) carries no upper bound.
193
194
 
194
195
  Whatever you pin wins over the bundled ruff. The lookup runs in this
@@ -42,8 +42,8 @@ remembered to pass.
42
42
  | `tempest-cli format` | `ruff format` (writes) |
43
43
  | `tempest-cli fmt-check` | `ruff format --check` (read-only) |
44
44
  | `tempest-cli type` | `mypy` |
45
- | `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; a host CLI can add steps between `mypy` and `pytest`) |
45
+ | `tempest-cli test` | `pytest`, in parallel by default with pytest-xdist (`-n auto -p no:cacheprovider`, `-w N` to choose; serial with a notice when it is not installed; `--serial` opts out) |
46
+ | `tempest-cli check` | all four, in order, stopping at the first failure (the test step in parallel as in `test`; 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
@@ -156,7 +156,8 @@ your suite. Add them yourself, or take the bundle:
156
156
  uv add --dev "tempest-cli[tools]" # mypy + pytest + pytest-xdist
157
157
  ```
158
158
 
159
- `pytest-xdist` is what `test --fast` runs the suite with; its
159
+ `pytest-xdist` is what `test` runs the suite in parallel with, by
160
+ default (without it the suite runs serially, with a notice); its
160
161
  `requires-dist` (`execnet>=2.1`, `pytest>=7.0.0`) carries no upper bound.
161
162
 
162
163
  Whatever you pin wins over the bundled ruff. The lookup runs in this
@@ -3,6 +3,21 @@
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.6.0] — 2026-10-20
7
+
8
+ ### Changed
9
+
10
+ - **`test` and `check` run the suite in parallel by default.** The
11
+ pytest-xdist `-n auto` run that needed `--fast` is now what a bare
12
+ `test` does; `--serial` opts out (for `--lf` / `--ff`, or a debugger).
13
+ Measured on a FastAPI suite of 1136 tests on 12 CPUs: 262 s serially,
14
+ 37.6 s in parallel, same outcome.
15
+ [Details](commands.md#the-suite-in-parallel-the-default).
16
+ - **Without pytest-xdist the default degrades to serial instead of
17
+ failing**, with a notice naming the install command; only an explicit
18
+ `--fast` still requires the plugin (exit code 127).
19
+ - `--workers` with `--serial` is a usage error (exit code 2).
20
+
6
21
  ## [0.5.1] — 2026-10-15
7
22
 
8
23
  ### Fixed
@@ -46,7 +61,7 @@ The full history lives in the repository's
46
61
  the default — or `logical`) and is a usage error without `--fast`. The
47
62
  target is forwarded and pytest's exit code comes back untranslated. It
48
63
  is a flag, not a subcommand, because `test fast` keeps meaning the
49
- `fast/` folder. [Details](commands.md#the-suite-in-parallel-fast).
64
+ `fast/` folder. [Details](commands.md#the-suite-in-parallel-the-default).
50
65
  - **A missing pytest-xdist is a sentence, not `unrecognized arguments:
51
66
  -n`.** `--fast` asks the interpreter that will run pytest whether it
52
67
  can import `xdist`; when it cannot, the message names the package and
@@ -4,6 +4,21 @@ 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.6.0] — 2026-10-20
8
+
9
+ ### Alterado
10
+
11
+ - **O `test` e o `check` rodam a suíte em paralelo por padrão.** O
12
+ `-n auto` do pytest-xdist, que pedia `--fast`, agora é o que um
13
+ `test` puro faz; `--serial` desliga (para `--lf` / `--ff` ou depurador).
14
+ Medido numa suíte FastAPI de 1136 testes em 12 CPUs: 262 s em série,
15
+ 37,6 s em paralelo, mesmo resultado.
16
+ [Detalhes](commands.md#a-suite-em-paralelo-o-padrao).
17
+ - **Sem o pytest-xdist, o padrão cai para série em vez de falhar**, com um
18
+ aviso de como instalar; só o `--fast` explícito continua exigindo o
19
+ plugin (código de saída 127).
20
+ - `--workers` com `--serial` é erro de uso (código de saída 2).
21
+
7
22
  ## [0.5.1] — 2026-10-15
8
23
 
9
24
  ### Corrigido
@@ -45,7 +60,7 @@ do repositório.
45
60
  o padrão — ou `logical`) e é erro de uso sem `--fast`. O alvo é
46
61
  repassado e o código de saída do pytest volta sem tradução. É flag, e
47
62
  não subcomando, porque `test fast` continua significando a pasta
48
- `fast/`. [Detalhes](commands.md#a-suite-em-paralelo-fast).
63
+ `fast/`. [Detalhes](commands.md#a-suite-em-paralelo-o-padrao).
49
64
  - **pytest-xdist ausente vira frase, não `unrecognized arguments: -n`.**
50
65
  O `--fast` pergunta ao interpretador que vai rodar o pytest se ele
51
66
  importa o `xdist`; faltando, a mensagem nomeia o pacote e os extras
@@ -181,76 +181,93 @@ tempest-cli test tests/unit
181
181
  `test` forwards the path to pytest as a filter; with no argument it runs
182
182
  the whole suite.
183
183
 
184
- ## The suite in parallel: `--fast`
184
+ ## The suite in parallel (the default)
185
185
 
186
186
  A suite whose tests are already isolated (a database per test, no global
187
- state) pays the serial time and buys nothing for it. `--fast` spreads the
188
- suite across the cores with
189
- [pytest-xdist](https://pytest-xdist.readthedocs.io/):
187
+ state) pays the serial time and buys nothing for it. That is why `test`
188
+ and `check` spread the suite across cores with
189
+ [pytest-xdist](https://pytest-xdist.readthedocs.io/) **without being
190
+ asked**:
190
191
 
191
192
  ```bash
192
- tempest-cli test --fast # -n auto: one worker per core
193
- tempest-cli test --fast -w 4 # four workers
194
- tempest-cli test tests/unit --fast # the target is still forwarded
195
- tempest-cli check --fast # the full gate, with the test step in parallel
193
+ tempest-cli test # -n auto: one worker per core
194
+ tempest-cli test -w 4 # four workers
195
+ tempest-cli test tests/unit # the target is still forwarded
196
+ tempest-cli check # the full gate, with the test step in parallel
197
+ tempest-cli test --serial # a single process (for --lf / --ff, or a debugger)
196
198
  ```
197
199
 
198
200
  Underneath it becomes `pytest -n <workers> -p no:cacheprovider [target]`.
199
201
  `-p no:cacheprovider` is there because several workers writing
200
202
  `.pytest_cache` at once is a race that buys nothing — and that cache is
201
- what `--lf` / `--ff` read, so those two remain a serial-run affair.
203
+ what `--lf` / `--ff` read, so those two are a `--serial` affair.
204
+
205
+ On a FastAPI service suite of 1136 tests (an in-memory SQLite database
206
+ per test), on a 12-CPU machine: **262 s** serially, **37.6 s** with
207
+ `-n auto` — same outcome, 1136 passed.
202
208
 
203
209
  `--workers` (`-w`) takes what `pytest -n` takes: an integer, `auto` (the
204
- default) or `logical`. Without `--fast` it is a usage error (exit 2)
210
+ default) or `logical`. With `--serial` it is a usage error (exit 2)
205
211
  instead of being silently ignored.
206
212
 
207
213
  !!! info "`auto` counts physical cores when `psutil` is installed"
208
214
  pytest-xdist decides: with `psutil` importable, `auto` is the number of
209
- **physical** cores; without it, the number of logical CPUs. On a machine
210
- with 6 cores and 12 threads, with `psutil` in the environment, `auto`
211
- started `created: 6/6 workers`. Want the 12 threads? `-w logical` or
215
+ **physical** cores; without it, the number of logical CPUs. On a
216
+ 6-core, 12-thread machine with `psutil` in the environment, `auto`
217
+ reported `created: 6/6 workers`. Want the 12 threads? `-w logical` or
212
218
  `-w 12`.
213
219
 
214
- !!! note "Why a flag, not `tempest-cli test fast`"
215
- `test` already takes a positional path. `test fast` keeps meaning
216
- "run pytest on the `fast/` folder" — a subcommand by that name would
217
- break anyone who has one.
220
+ !!! note "Why a flag, and not `tempest-cli test serial`"
221
+ `test` already takes a positional path. `test fast` keeps meaning "run
222
+ pytest on the `fast/` folder" — a subcommand by that name would break
223
+ anyone who has that folder.
218
224
 
219
225
  ### Without pytest-xdist
220
226
 
221
227
  Before running anything, the CLI asks **the very interpreter that will
222
228
  run pytest** whether it can import `xdist` — not the CLI's own
223
- interpreter, which under `uv tool install` or pipx lives in a different
224
- environment. When it is missing, the message names what to install and
225
- the exit code is **127**, with no traceback:
226
-
227
- ```console
228
- $ tempest-cli test --fast
229
- error: --fast needs pytest-xdist, which is not installed in the environment pytest runs in (.venv/bin/pytest). Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — and retry, or drop --fast to run the suite serially.
230
- $ echo $?
231
- 127
232
- ```
229
+ interpreter, which under `uv tool install` or pipx lives in another
230
+ environment. When it is missing, it depends on who asked for parallel:
231
+
232
+ - **Nobody (the default):** the suite runs serially, with a notice on
233
+ stderr naming what to install. `tempest-cli test` keeps working in a
234
+ project that never installed pytest-xdist — it is just slower.
235
+
236
+ ```console
237
+ $ tempest-cli test
238
+ note: pytest-xdist is not installed in the environment pytest runs in (.venv/bin/pytest), so the suite runs serially. Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — to run it in parallel.
239
+ ```
240
+
241
+ - **`--fast` by name:** parallel becomes a requirement. The message names
242
+ what to install and the exit code is **127**, with nothing run:
243
+
244
+ ```console
245
+ $ tempest-cli test --fast
246
+ error: --fast needs pytest-xdist, which is not installed in the environment pytest runs in (.venv/bin/pytest). Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — and retry, or pass --serial to run the suite serially.
247
+ $ echo $?
248
+ 127
249
+ ```
233
250
 
234
- Under `check --fast` the check happens **before** the first step: there
235
- is no point waiting for lint and mypy to finish to learn the tests will
236
- not run.
251
+ Under `check` the probe happens **before** the first step: with `--fast`
252
+ there is no point waiting for lint and mypy to finish only to learn the
253
+ tests will not run.
237
254
 
238
255
  ### A test that fails only in parallel
239
256
 
240
- Parallelism exposes tests that depend on order or on an idle machine:
241
- two tests writing the same file, the same port, a module-level global, a
242
- fixed `sleep` waiting for something that takes longer once every core is
257
+ Parallelism exposes tests that depend on order or on an idle machine: two
258
+ tests writing the same file, the same port, a module-level global, a
259
+ fixed `sleep` waiting for something that takes longer with every core
243
260
  busy. Before treating the failure as a regression, run the test **alone
244
261
  and serially**:
245
262
 
246
263
  ```bash
247
- tempest-cli test "tests/test_scheduler.py::test_lease_expires"
264
+ tempest-cli test --serial "tests/test_scheduler.py::test_lease_expires"
248
265
  ```
249
266
 
250
- - **It passes alone**: the defect is the test's isolation, not the change
267
+ - **Passes alone**: the defect is the test's isolation, not the change
251
268
  under review. Fix the test (a file under `tmp_path`, a free port, wait
252
- on a condition instead of a `sleep`).
253
- - **It fails alone too**: it is a real regression.
269
+ on a condition instead of `sleep`).
270
+ - **Fails alone too**: it is a real regression.
254
271
 
255
272
  ## What each returns
256
273
 
@@ -265,8 +282,9 @@ of the places the CLI looks — the run's environment, `PATH`, `uv run
265
282
  --with` — and the message then names what was missing and how to install
266
283
  it. The lookup order is in
267
284
  [Installation](installation.md#where-ruff-mypy-and-pytest-come-from).
268
- The same 127 comes out of `--fast` when pytest-xdist is not in pytest's
269
- environment.
285
+ The same 127 comes out of an explicit `--fast` when pytest-xdist is not in
286
+ pytest's environment; without the flag, its absence only makes the suite
287
+ serial.
270
288
 
271
289
  ## Recap
272
290
 
@@ -276,8 +294,10 @@ environment.
276
294
  - Long comments and strings `fix` shortens on its own, with the AST
277
295
  proven equal. An E501 left after it has no mechanical rewrite: `fix`
278
296
  names the token and the action — change the code's shape.
279
- - `test --fast` / `check --fast` run the suite with pytest-xdist
280
- (`-n auto`, `-w N` to choose); a test that fails only in parallel is
281
- checked by running it alone.
297
+ - `test` and `check` run the suite in parallel by default, with
298
+ pytest-xdist (`-n auto`, `-w N` to choose); `--serial` turns it off.
299
+ Without pytest-xdist the default falls back to serial with a notice,
300
+ and an explicit `--fast` becomes exit 127. A test that fails only in
301
+ parallel is checked by running it alone, with `--serial`.
282
302
  - Exit codes are the tools'; 127 means a missing tool (or the
283
- pytest-xdist `--fast` needs).
303
+ pytest-xdist an explicit `--fast` requires).
@@ -181,28 +181,33 @@ tempest-cli test tests/unit
181
181
  `test` repassa o caminho ao pytest como filtro; sem argumento, roda a
182
182
  suíte inteira.
183
183
 
184
- ## A suíte em paralelo: `--fast`
184
+ ## A suíte em paralelo (o padrão)
185
185
 
186
186
  Suíte cujos testes já são isolados (banco por teste, nada de estado
187
- global) paga o tempo serial sem comprar nada. `--fast` espalha a suíte
188
- pelos núcleos com o [pytest-xdist](https://pytest-xdist.readthedocs.io/):
187
+ global) paga o tempo serial sem comprar nada. Por isso `test` e `check`
188
+ espalham a suíte pelos núcleos com o
189
+ [pytest-xdist](https://pytest-xdist.readthedocs.io/) **sem você pedir**:
189
190
 
190
191
  ```bash
191
- tempest-cli test --fast # -n auto: um worker por núcleo
192
- tempest-cli test --fast -w 4 # quatro workers
193
- tempest-cli test tests/unit --fast # o alvo continua sendo repassado
194
- tempest-cli check --fast # o gate completo, com o passo de teste em paralelo
192
+ tempest-cli test # -n auto: um worker por núcleo
193
+ tempest-cli test -w 4 # quatro workers
194
+ tempest-cli test tests/unit # o alvo continua sendo repassado
195
+ tempest-cli check # o gate completo, com o passo de teste em paralelo
196
+ tempest-cli test --serial # um processo só (para --lf / --ff, ou depurar)
195
197
  ```
196
198
 
197
199
  Por baixo, vira `pytest -n <workers> -p no:cacheprovider [alvo]`. O
198
200
  `-p no:cacheprovider` existe porque vários workers escrevendo
199
201
  `.pytest_cache` ao mesmo tempo é corrida à toa — e é esse cache que o
200
- `--lf` / `--ff` leem, então esses dois continuam sendo coisa de execução
201
- serial.
202
+ `--lf` / `--ff` leem, então esses dois são coisa de `--serial`.
203
+
204
+ Numa suíte de serviço FastAPI com 1136 testes (banco SQLite in-memory por
205
+ teste), numa máquina de 12 CPUs: **262 s** em série, **37,6 s** com
206
+ `-n auto` — o mesmo resultado, 1136 passed.
202
207
 
203
208
  `--workers` (`-w`) aceita o que o `pytest -n` aceita: um inteiro, `auto`
204
- (o padrão) ou `logical`. Sem `--fast` ele é erro de uso (saída 2), em vez
205
- de ser ignorado em silêncio.
209
+ (o padrão) ou `logical`. Com `--serial` ele é erro de uso (saída 2), em
210
+ vez de ser ignorado em silêncio.
206
211
 
207
212
  !!! info "`auto` conta núcleo físico quando o `psutil` está instalado"
208
213
  É o pytest-xdist que decide: com `psutil` importável, `auto` é o número
@@ -210,7 +215,7 @@ de ser ignorado em silêncio.
210
215
  núcleos e 12 threads, com `psutil` no ambiente, `auto` subiu
211
216
  `created: 6/6 workers`. Quer as 12 threads? `-w logical` ou `-w 12`.
212
217
 
213
- !!! note "Por que flag, e não `tempest-cli test fast`"
218
+ !!! note "Por que flag, e não `tempest-cli test serial`"
214
219
  `test` já recebe um caminho posicional. `test fast` continua
215
220
  significando "rode o pytest na pasta `fast/`" — um subcomando com esse
216
221
  nome quebraria quem tem essa pasta.
@@ -220,18 +225,30 @@ de ser ignorado em silêncio.
220
225
  Antes de rodar qualquer coisa, a CLI pergunta ao **mesmo interpretador
221
226
  que vai rodar o pytest** se ele importa o `xdist` — não ao interpretador
222
227
  da própria CLI, que sob `uv tool install` ou pipx mora em outro ambiente.
223
- Faltou, a mensagem diz o que instalar e a saída é **127**, sem traceback:
228
+ Faltou, depende de quem pediu o paralelismo:
224
229
 
225
- ```console
226
- $ tempest-cli test --fast
227
- error: --fast needs pytest-xdist, which is not installed in the environment pytest runs in (.venv/bin/pytest). Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — and retry, or drop --fast to run the suite serially.
228
- $ echo $?
229
- 127
230
- ```
230
+ - **Ninguém pediu (o padrão):** a suíte roda em série, com um aviso no
231
+ stderr dizendo o que instalar. `tempest-cli test` continua funcionando
232
+ em projeto que nunca instalou o pytest-xdist — só fica mais lento.
233
+
234
+ ```console
235
+ $ tempest-cli test
236
+ note: pytest-xdist is not installed in the environment pytest runs in (.venv/bin/pytest), so the suite runs serially. Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — to run it in parallel.
237
+ ```
238
+
239
+ - **`--fast` explícito:** o paralelismo vira exigência. A mensagem diz o
240
+ que instalar e a saída é **127**, sem rodar nada:
241
+
242
+ ```console
243
+ $ tempest-cli test --fast
244
+ error: --fast needs pytest-xdist, which is not installed in the environment pytest runs in (.venv/bin/pytest). Install it with 'uv add --dev pytest-xdist' — or a bundle that carries it, 'uv add --dev "tempest-cli[tools]"' or 'uv add --dev "tempest-fastapi-sdk[tests]"' — and retry, or pass --serial to run the suite serially.
245
+ $ echo $?
246
+ 127
247
+ ```
231
248
 
232
- No `check --fast` essa checagem acontece **antes** do primeiro passo:
233
- não adianta esperar lint e mypy terminarem para descobrir que o teste
234
- não vai rodar.
249
+ No `check` essa checagem acontece **antes** do primeiro passo: com
250
+ `--fast`, não adianta esperar lint e mypy terminarem para descobrir que o
251
+ teste não vai rodar.
235
252
 
236
253
  ### Teste que só falha em paralelo
237
254
 
@@ -242,7 +259,7 @@ demora mais. Antes de tratar a falha como regressão, rode o teste
242
259
  **sozinho e em série**:
243
260
 
244
261
  ```bash
245
- tempest-cli test "tests/test_scheduler.py::test_lease_expires"
262
+ tempest-cli test --serial "tests/test_scheduler.py::test_lease_expires"
246
263
  ```
247
264
 
248
265
  - **Passou sozinho**: o defeito é o isolamento do teste, não a mudança
@@ -262,8 +279,8 @@ Só há um código próprio: **127**, quando a ferramenta não está em nenhum
262
279
  dos lugares onde a CLI procura — ambiente da execução, `PATH`, `uv run
263
280
  --with` — e aí a mensagem diz qual faltou e como instalá-la. A ordem de
264
281
  busca está em [Instalação](installation.md#de-onde-vem-o-ruff-o-mypy-e-o-pytest).
265
- O mesmo 127 sai do `--fast` quando o pytest-xdist não está no ambiente do
266
- pytest.
282
+ O mesmo 127 sai do `--fast` explícito quando o pytest-xdist não está no
283
+ ambiente do pytest; sem a flag, a falta dele só deixa a suíte em série.
267
284
 
268
285
  ## Recap
269
286
 
@@ -273,8 +290,10 @@ pytest.
273
290
  - Comentário e string longos o `fix` encurta sozinho, com a AST provada
274
291
  igual. O E501 que sobra é linha sem reescrita mecânica: o `fix` diz o
275
292
  token e a ação — mude a forma do código.
276
- - `test --fast` / `check --fast` rodam a suíte com pytest-xdist
277
- (`-n auto`, `-w N` para escolher); teste que só falha em paralelo se
278
- confere rodando-o sozinho.
293
+ - `test` e `check` rodam a suíte em paralelo por padrão, com pytest-xdist
294
+ (`-n auto`, `-w N` para escolher); `--serial` desliga. Sem o
295
+ pytest-xdist, o padrão cai para série com um aviso, e o `--fast`
296
+ explícito vira erro 127. Teste que só falha em paralelo se confere
297
+ rodando-o sozinho, com `--serial`.
279
298
  - Código de saída é o da ferramenta; 127 significa ferramenta (ou o
280
- pytest-xdist do `--fast`) ausente.
299
+ pytest-xdist que o `--fast` exige) ausente.
@@ -45,8 +45,9 @@ take the bundle:
45
45
  uv add --dev "tempest-cli[tools]" # mypy + pytest + pytest-xdist
46
46
  ```
47
47
 
48
- `pytest-xdist` is what [`test --fast`](commands.md#the-suite-in-parallel-fast)
49
- uses to run the suite in parallel. Its `requires-dist` (`execnet>=2.1`,
48
+ `pytest-xdist` is what [`test`](commands.md#the-suite-in-parallel-the-default)
49
+ uses to run the suite in parallel, the default; without it the suite runs
50
+ serially, with a notice. Its `requires-dist` (`execnet>=2.1`,
50
51
  `pytest>=7.0.0`) carries no upper bound, so it tightens nobody's
51
52
  resolution.
52
53
 
@@ -46,8 +46,9 @@ mesmo, ou pegue o pacote pronto:
46
46
  uv add --dev "tempest-cli[tools]" # mypy + pytest + pytest-xdist
47
47
  ```
48
48
 
49
- O `pytest-xdist` é o que o [`test --fast`](commands.md#a-suite-em-paralelo-fast)
50
- usa para rodar a suíte em paralelo. O `requires-dist` dele
49
+ O `pytest-xdist` é o que o [`test`](commands.md#a-suite-em-paralelo-o-padrao)
50
+ usa para rodar a suíte em paralelo, o padrão; sem ele a suíte roda em
51
+ série, com um aviso. O `requires-dist` dele
51
52
  (`execnet>=2.1`, `pytest>=7.0.0`) não tem teto nenhum, então ele não
52
53
  aperta a resolução de ninguém.
53
54
 
@@ -30,7 +30,8 @@ run_ruff_fix("src", unsafe=False, config=strict)
30
30
  run_ruff_format("src", check=True) # --check: writes nothing
31
31
  run_mypy("src", config=strict)
32
32
  run_pytest("tests/unit")
33
- run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4
33
+ run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4; without it, 127
34
+ run_pytest("tests", fast=True, require_xdist=False) # without pytest-xdist: serial, with a notice
34
35
  ```
35
36
 
36
37
  `load_tempest_config(start)` walks up from `start` (the cwd by default)
@@ -135,7 +136,7 @@ The order is `ruff check` → `ruff format --check` → `mypy` → the extra
135
136
  steps, in list order → `pytest`. An extra step is a static check: it
136
137
  sits before the suite so it fails in a second, not after it. The
137
138
  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
+ and becomes `check`'s exit code, in parallel or with `--serial`.
139
140
 
140
141
  !!! tip "The message is yours"
141
142
  `check` only echoes the name. What the step prints is what the user
@@ -31,7 +31,8 @@ run_ruff_fix("src", unsafe=False, config=strict)
31
31
  run_ruff_format("src", check=True) # --check: não escreve
32
32
  run_mypy("src", config=strict)
33
33
  run_pytest("tests/unit")
34
- run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4
34
+ run_pytest("tests/unit", fast=True, workers="4") # pytest-xdist: -n 4; sem ele, 127
35
+ run_pytest("tests", fast=True, require_xdist=False) # sem pytest-xdist: série, com aviso
35
36
  ```
36
37
 
37
38
  `load_tempest_config(start)` procura o `pyproject.toml` mais próximo
@@ -136,7 +137,7 @@ A ordem é `ruff check` → `ruff format --check` → `mypy` → os passos
136
137
  extras, na ordem da lista → `pytest`. Passo extra é checagem estática:
137
138
  fica antes da suíte para falhar em um segundo, não depois dela. O
138
139
  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
+ para o gate e vira o código de saída do `check`, em paralelo ou com `--serial`.
140
141
 
141
142
  !!! tip "A mensagem é sua"
142
143
  O `check` só ecoa o nome. O que o passo imprime é o que o usuário lê,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tempest-cli"
3
- version = "0.5.1"
3
+ version = "0.6.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"
@@ -57,7 +57,7 @@ from tempest_cli.pr_prompt import PromptLanguage as PromptLanguage
57
57
  from tempest_cli.pr_prompt import generate_pr_prompt as generate_pr_prompt
58
58
  from tempest_cli.steps import CheckStep as CheckStep
59
59
 
60
- __version__: str = "0.5.1"
60
+ __version__: str = "0.6.0"
61
61
  """Installed package version."""
62
62
 
63
63
  __all__: list[str] = [
@@ -8,6 +8,7 @@ import subprocess
8
8
  import sys
9
9
  from collections.abc import Callable, Sequence
10
10
  from pathlib import Path
11
+ from typing import Literal
11
12
 
12
13
  import typer
13
14
 
@@ -494,41 +495,86 @@ def _module_available(interpreter: list[str], module: str) -> bool | None:
494
495
  return None
495
496
 
496
497
 
497
- def _missing_xdist_code() -> int:
498
- """Check that the environment pytest runs in can import pytest-xdist.
498
+ ParallelDecision = Literal["parallel", "serial", "stop"]
499
+ """How a requested parallel run proceeds once pytest-xdist was looked for."""
500
+
501
+ XDIST_INSTALL_HINT = (
502
+ "'uv add --dev pytest-xdist' — or a bundle that carries it, "
503
+ "'uv add --dev \"tempest-cli[tools]\"' or "
504
+ "'uv add --dev \"tempest-fastapi-sdk[tests]\"'"
505
+ )
506
+ """How to get pytest-xdist, quoted by both the error and the fallback notice."""
507
+
508
+
509
+ def _xdist_missing() -> list[str] | None:
510
+ """Tell whether the environment pytest runs in lacks pytest-xdist.
499
511
 
500
512
  Without the plugin, ``pytest -n auto`` stops at argument parsing with
501
513
  ``unrecognized arguments: -n`` — true, but it names neither the
502
514
  missing package nor the way to get it. This preflight answers first,
503
- in words.
515
+ so the caller can say it in words.
504
516
 
505
- An unknown answer is not a failure: when pytest cannot be resolved
517
+ An unknown answer is not "missing": when pytest cannot be resolved
506
518
  (:func:`_execute` reports that with its own message) or its
507
519
  interpreter cannot be told, the run proceeds and pytest speaks.
508
520
 
509
521
  Returns:
510
- int: ``0`` when the plugin is present or its presence cannot be
511
- told; :data:`MISSING_TOOL_EXIT_CODE` after printing the fix when
512
- it is absent.
522
+ list[str] | None: The pytest argv prefix when the plugin is
523
+ known to be absent; ``None`` when it is present or its presence
524
+ cannot be told.
513
525
  """
514
526
  argv = resolve_tool("pytest")
515
527
  if argv is None:
516
- return 0
528
+ return None
517
529
  interpreter = _pytest_interpreter(argv)
518
530
  if interpreter is None:
519
- return 0
531
+ return None
520
532
  if _module_available(interpreter, XDIST_MODULE) is not False:
521
- return 0
533
+ return None
534
+ return argv
535
+
536
+
537
+ def _parallel_allowed(*, require_xdist: bool) -> ParallelDecision:
538
+ """Decide how a parallel run proceeds, given whether xdist is present.
539
+
540
+ Two policies, picked by ``require_xdist``:
541
+
542
+ - **required** (the parallel run was asked for by name, ``--fast``):
543
+ a missing plugin is an error — the fix is printed and
544
+ :data:`MISSING_TOOL_EXIT_CODE` comes back, so nothing runs;
545
+ - **preferred** (parallel is only the default): a missing plugin
546
+ degrades the run to serial, with a one-line notice on stderr
547
+ naming the install command. ``tempest test`` keeps working in a
548
+ project that never installed pytest-xdist, it is just slower.
549
+
550
+ Args:
551
+ require_xdist (bool): Whether a missing plugin is an error.
552
+
553
+ Returns:
554
+ ParallelDecision: ``"parallel"`` when the plugin is there (or its
555
+ presence cannot be told), ``"serial"`` to fall back, ``"stop"``
556
+ after printing the fix.
557
+ """
558
+ argv = _xdist_missing()
559
+ if argv is None:
560
+ return "parallel"
561
+ where = " ".join(argv)
562
+ if require_xdist:
563
+ typer.echo(
564
+ "error: --fast needs pytest-xdist, which is not installed in the "
565
+ f"environment pytest runs in ({where}). Install it with "
566
+ f"{XDIST_INSTALL_HINT} — and retry, or pass --serial to run the "
567
+ "suite serially.",
568
+ err=True,
569
+ )
570
+ return "stop"
522
571
  typer.echo(
523
- "error: --fast needs pytest-xdist, which is not installed in the "
524
- f"environment pytest runs in ({' '.join(argv)}). Install it with "
525
- "'uv add --dev pytest-xdist' — or a bundle that carries it, "
526
- "'uv add --dev \"tempest-cli[tools]\"' or "
527
- "'uv add --dev \"tempest-fastapi-sdk[tests]\"' — and retry, "
528
- "or drop --fast to run the suite serially.",
572
+ "note: pytest-xdist is not installed in the environment pytest runs "
573
+ f"in ({where}), so the suite runs serially. Install it with "
574
+ f"{XDIST_INSTALL_HINT} — to run it in parallel.",
529
575
  err=True,
530
576
  )
531
- return MISSING_TOOL_EXIT_CODE
577
+ return "serial"
532
578
 
533
579
 
534
580
  def _pytest_args(target: str | None, *, fast: bool, workers: str) -> list[str]:
@@ -555,6 +601,7 @@ def run_pytest(
555
601
  *,
556
602
  fast: bool = False,
557
603
  workers: str = DEFAULT_FAST_WORKERS,
604
+ require_xdist: bool = True,
558
605
  ) -> int:
559
606
  """Invoke ``pytest`` with an optional target.
560
607
 
@@ -563,8 +610,8 @@ def run_pytest(
563
610
  writing ``.pytest_cache`` at once is a race that buys nothing. The
564
611
  cache is what ``--lf`` / ``--ff`` read, so those belong to serial
565
612
  runs. Before spawning, the interpreter pytest runs under is asked
566
- whether it can import pytest-xdist; when it cannot, the fix is
567
- printed and nothing runs.
613
+ whether it can import pytest-xdist; what happens when it cannot is
614
+ ``require_xdist``'s call (:func:`_parallel_allowed`).
568
615
 
569
616
  Args:
570
617
  target (str | None): Optional pytest path filter. ``None`` runs
@@ -574,16 +621,21 @@ def run_pytest(
574
621
  workers (str): Worker count for ``fast`` — an integer, ``auto``
575
622
  (one per CPU core: physical when ``psutil`` is importable) or
576
623
  ``logical``, exactly as ``pytest -n`` takes it. Ignored when
577
- ``fast`` is False.
624
+ the run is serial.
625
+ require_xdist (bool): With ``fast``, whether a missing
626
+ pytest-xdist stops the run (``True``, the default) or degrades
627
+ it to serial with a notice (``False``, what the CLI passes
628
+ when parallel is only its default).
578
629
 
579
630
  Returns:
580
631
  int: The pytest exit code, or :data:`MISSING_TOOL_EXIT_CODE` when
581
- pytest — or, with ``fast``, pytest-xdist — is absent.
632
+ pytest — or a required pytest-xdist — is absent.
582
633
  """
583
634
  if fast:
584
- code = _missing_xdist_code()
585
- if code != 0:
586
- return code
635
+ decision = _parallel_allowed(require_xdist=require_xdist)
636
+ if decision == "stop":
637
+ return MISSING_TOOL_EXIT_CODE
638
+ fast = decision == "parallel"
587
639
  return _execute("pytest", _pytest_args(target, fast=fast, workers=workers))
588
640
 
589
641
 
@@ -622,6 +674,7 @@ def run_full_check(
622
674
  fast: bool = False,
623
675
  workers: str = DEFAULT_FAST_WORKERS,
624
676
  extra_steps: Sequence[CheckStep] = (),
677
+ require_xdist: bool = True,
625
678
  ) -> int:
626
679
  """Run the entire quality gate sequentially.
627
680
 
@@ -635,8 +688,9 @@ def run_full_check(
635
688
 
636
689
  With ``fast=True`` the pytest step runs the way :func:`run_pytest`
637
690
  runs it with ``fast=True``. The pytest-xdist preflight happens
638
- *before* the first step, so a missing plugin fails in a second
639
- instead of after lint and mypy have already run.
691
+ *before* the first step, so a required plugin that is missing fails
692
+ in a second instead of after lint and mypy have already run, and a
693
+ merely preferred one degrades the pytest step to serial up front.
640
694
 
641
695
  Args:
642
696
  target (str): The path inspected by ruff/mypy and handed to every
@@ -651,15 +705,19 @@ def run_full_check(
651
705
  extra_steps (Sequence[CheckStep]): Caller-supplied steps run
652
706
  after ``mypy`` and before ``pytest``. Empty by default, which
653
707
  leaves the gate exactly as it was without them.
708
+ require_xdist (bool): With ``fast``, whether a missing
709
+ pytest-xdist stops the gate or degrades the pytest step to
710
+ serial — as in :func:`run_pytest`.
654
711
 
655
712
  Returns:
656
713
  int: The first non-zero exit code — a tool's or an extra step's —
657
714
  or ``0`` when every gate passed.
658
715
  """
659
716
  if fast:
660
- code = _missing_xdist_code()
661
- if code != 0:
662
- return code
717
+ decision = _parallel_allowed(require_xdist=require_xdist)
718
+ if decision == "stop":
719
+ return MISSING_TOOL_EXIT_CODE
720
+ fast = decision == "parallel"
663
721
  resolved = config or TempestConfig()
664
722
  steps: list[tuple[str, Callable[[], int]]] = [
665
723
  _tool_step("ruff", ["check", *_ruff_ann_args(resolved), target]),
@@ -679,6 +737,7 @@ def run_full_check(
679
737
  __all__: list[str] = [
680
738
  "DEFAULT_FAST_WORKERS",
681
739
  "MISSING_TOOL_EXIT_CODE",
740
+ "XDIST_INSTALL_HINT",
682
741
  "XDIST_MODULE",
683
742
  "resolve_tool",
684
743
  "run_full_check",
@@ -77,7 +77,11 @@ WORKER_KEYWORDS: frozenset[str] = frozenset({"auto", "logical"})
77
77
 
78
78
 
79
79
  def _fast_option() -> typer.models.OptionInfo:
80
- """Build a fresh ``--fast`` option.
80
+ """Build a fresh ``--fast/--serial`` option.
81
+
82
+ Parallel is the default. ``--fast`` asks for it by name, which turns a
83
+ missing pytest-xdist into an error; ``--serial`` opts out (for
84
+ ``--lf`` / ``--ff``, or a debugger).
81
85
 
82
86
  Returns:
83
87
  typer.models.OptionInfo: The configured option.
@@ -85,11 +89,12 @@ def _fast_option() -> typer.models.OptionInfo:
85
89
  return cast(
86
90
  "typer.models.OptionInfo",
87
91
  typer.Option(
88
- "--fast",
92
+ "--fast/--serial",
89
93
  help=(
90
- "Run the suite in parallel with pytest-xdist: -n <workers> "
91
- "-p no:cacheprovider. Needs pytest-xdist in the environment "
92
- "pytest runs in."
94
+ "Parallel (the default) runs the suite with pytest-xdist: "
95
+ "-n <workers> -p no:cacheprovider. Without pytest-xdist the "
96
+ "default falls back to a serial run with a notice; --fast "
97
+ "makes that an error instead. --serial runs one process."
93
98
  ),
94
99
  ),
95
100
  )
@@ -107,9 +112,9 @@ def _workers_option() -> typer.models.OptionInfo:
107
112
  "--workers",
108
113
  "-w",
109
114
  help=(
110
- "Worker count for --fast: a number, 'auto' (the default: one "
111
- "per physical core when psutil is installed, per logical CPU "
112
- "otherwise) or 'logical'. Requires --fast."
115
+ "Worker count for a parallel run: a number, 'auto' (the "
116
+ "default: one per physical core when psutil is installed, per "
117
+ "logical CPU otherwise) or 'logical'. Not with --serial."
113
118
  ),
114
119
  ),
115
120
  )
@@ -121,19 +126,20 @@ def _resolve_workers(
121
126
  fast: bool,
122
127
  workers: str,
123
128
  ) -> str:
124
- """Validate ``--workers`` against ``--fast`` and pytest-xdist's grammar.
129
+ """Validate ``--workers`` against ``--serial`` and pytest-xdist's grammar.
125
130
 
126
131
  Args:
127
132
  ctx (typer.Context): The command context, used to tell an
128
133
  explicit ``--workers`` from its default.
129
- fast (bool): The ``--fast`` flag value.
134
+ fast (bool): The ``--fast/--serial`` value (``False`` is
135
+ ``--serial``).
130
136
  workers (str): The ``--workers`` value.
131
137
 
132
138
  Returns:
133
139
  str: The value to hand to ``pytest -n``.
134
140
 
135
141
  Raises:
136
- typer.BadParameter: When ``--workers`` is given without ``--fast``,
142
+ typer.BadParameter: When ``--workers`` is given with ``--serial``,
137
143
  or is neither a non-negative integer nor ``auto``/``logical``.
138
144
  """
139
145
  explicit = getattr(ctx.get_parameter_source("workers"), "name", "") == (
@@ -141,7 +147,7 @@ def _resolve_workers(
141
147
  )
142
148
  if explicit and not fast:
143
149
  raise typer.BadParameter(
144
- "--workers only applies to a parallel run; add --fast.",
150
+ "--workers only applies to a parallel run; drop --serial.",
145
151
  param_hint="--workers",
146
152
  )
147
153
  if workers in WORKER_KEYWORDS or workers.isdigit():
@@ -154,6 +160,21 @@ def _resolve_workers(
154
160
  )
155
161
 
156
162
 
163
+ def _require_xdist(ctx: typer.Context) -> bool:
164
+ """Tell whether the parallel run was asked for by name.
165
+
166
+ ``--fast`` on the command line makes a missing pytest-xdist an error;
167
+ the default (no flag) only prefers parallel and degrades to serial.
168
+
169
+ Args:
170
+ ctx (typer.Context): The command context.
171
+
172
+ Returns:
173
+ bool: ``True`` when ``--fast`` was typed.
174
+ """
175
+ return getattr(ctx.get_parameter_source("fast"), "name", "") == "COMMANDLINE"
176
+
177
+
157
178
  def register_commands(
158
179
  app: typer.Typer,
159
180
  *,
@@ -166,7 +187,7 @@ def register_commands(
166
187
  help text are left untouched; only commands are added.
167
188
  extra_steps (Sequence[CheckStep]): Steps the ``check`` command
168
189
  runs after ``mypy`` and before ``pytest``, in every variant
169
- (``--fast`` included). This is how a framework adds its own
190
+ (parallel or ``--serial``). This is how a framework adds its own
170
191
  rules to the gate without this package knowing about it.
171
192
  Empty by default.
172
193
 
@@ -286,27 +307,33 @@ def register_commands(
286
307
  str | None,
287
308
  typer.Argument(help="Optional pytest path filter."),
288
309
  ] = None,
289
- fast: Annotated[bool, _fast_option()] = False,
310
+ fast: Annotated[bool, _fast_option()] = True,
290
311
  workers: Annotated[str, _workers_option()] = lint_module.DEFAULT_FAST_WORKERS,
291
312
  ) -> None:
292
313
  """Run ``pytest`` (forwarding the optional path argument).
293
314
 
294
- --fast spreads the suite across pytest-xdist workers (-n auto
295
- unless --workers says otherwise) and turns the cache plugin off,
296
- so --lf / --ff belong to serial runs. Without pytest-xdist in the
297
- environment pytest runs in, it stops with the install command and
298
- exit code 127.
315
+ The suite runs in parallel by default: pytest-xdist workers (-n
316
+ auto unless --workers says otherwise), with the cache plugin off,
317
+ so --lf / --ff belong to --serial runs. Without pytest-xdist in the
318
+ environment pytest runs in, the default falls back to a serial run
319
+ and says how to install it; --fast makes that an error (exit code
320
+ 127) instead.
299
321
 
300
- A test that fails only under --fast shares state with another
322
+ A test that fails only in parallel shares state with another
301
323
  one: a file, a port, a module-level global, a fixed sleep racing
302
324
  a busy machine. Before treating it as a regression, rerun it
303
325
  alone, serially, with its node id as the target
304
- (test tests/test_x.py::test_name). Passing alone means the defect
305
- is the test's isolation, not the change under review.
326
+ (test --serial tests/test_x.py::test_name). Passing alone means the
327
+ defect is the test's isolation, not the change under review.
306
328
  """
307
329
  resolved_workers = _resolve_workers(ctx, fast=fast, workers=workers)
308
330
  raise typer.Exit(
309
- lint_module.run_pytest(target, fast=fast, workers=resolved_workers)
331
+ lint_module.run_pytest(
332
+ target,
333
+ fast=fast,
334
+ workers=resolved_workers,
335
+ require_xdist=_require_xdist(ctx),
336
+ )
310
337
  )
311
338
 
312
339
  @app.command("check")
@@ -317,14 +344,14 @@ def register_commands(
317
344
  typer.Argument(help="Path to inspect. Defaults to the current directory."),
318
345
  ] = ".",
319
346
  strictness: Annotated[str | None, _strictness_option()] = None,
320
- fast: Annotated[bool, _fast_option()] = False,
347
+ fast: Annotated[bool, _fast_option()] = True,
321
348
  workers: Annotated[str, _workers_option()] = lint_module.DEFAULT_FAST_WORKERS,
322
349
  ) -> None:
323
350
  """Run the full quality gate (lint + fmt-check + type + test).
324
351
 
325
- ``--fast`` runs the test step as ``test --fast`` does, with the
326
- pytest-xdist check made before the first step. Steps the host CLI
327
- registered run after type and before test.
352
+ The test step runs as ``test`` does — in parallel unless
353
+ ``--serial`` — with the pytest-xdist check made before the first
354
+ step. Steps the host CLI registered run after type and before test.
328
355
  """
329
356
  config = _resolve_config(target, strictness)
330
357
  resolved_workers = _resolve_workers(ctx, fast=fast, workers=workers)
@@ -335,6 +362,7 @@ def register_commands(
335
362
  fast=fast,
336
363
  workers=resolved_workers,
337
364
  extra_steps=extra_steps,
365
+ require_xdist=_require_xdist(ctx),
338
366
  )
339
367
  )
340
368
 
@@ -59,7 +59,7 @@ def log(monkeypatch: pytest.MonkeyPatch) -> list[str]:
59
59
 
60
60
  monkeypatch.setattr(subprocess, "call", fake_call)
61
61
  monkeypatch.setattr(shutil, "which", _on_path_only)
62
- monkeypatch.setattr(lint, "_missing_xdist_code", lambda: 0)
62
+ monkeypatch.setattr(lint, "_xdist_missing", lambda: None)
63
63
  return entries
64
64
 
65
65
 
@@ -1,4 +1,4 @@
1
- """Tests for ``test --fast`` / ``check --fast``: the parallel pytest run.
1
+ """Tests for the parallel pytest run of ``test`` / ``check`` (the default).
2
2
 
3
3
  Three things matter: the argv handed to pytest, the preflight that asks
4
4
  *pytest's* interpreter whether pytest-xdist is importable, and the exit
@@ -163,6 +163,40 @@ def test_missing_xdist_names_the_fix_and_reports_127(
163
163
  assert "Traceback" not in err
164
164
 
165
165
 
166
+ def test_preferred_parallel_without_xdist_runs_serially_and_says_so(
167
+ monkeypatch: pytest.MonkeyPatch,
168
+ calls: list[list[str]],
169
+ capsys: pytest.CaptureFixture[str],
170
+ ) -> None:
171
+ """The default only prefers parallel: no plugin is a slower run, not an error."""
172
+ monkeypatch.setattr(subprocess, "run", _probe_answering(1))
173
+ assert lint.run_pytest("tests", fast=True, require_xdist=False) == 0
174
+ assert calls[0][1:] == ["tests"]
175
+ err = capsys.readouterr().err
176
+ assert "runs serially" in err
177
+ assert "pytest-xdist" in err
178
+ assert "tempest-cli[tools]" in err
179
+
180
+
181
+ def test_preferred_parallel_with_xdist_still_spreads(calls: list[list[str]]) -> None:
182
+ assert lint.run_pytest(None, fast=True, require_xdist=False) == 0
183
+ assert calls[0][1:] == ["-n", "auto", "-p", "no:cacheprovider"]
184
+
185
+
186
+ def test_full_check_preferred_parallel_falls_back_without_stopping(
187
+ monkeypatch: pytest.MonkeyPatch,
188
+ calls: list[list[str]],
189
+ ) -> None:
190
+ """The gate runs every step and the pytest one serially."""
191
+ monkeypatch.setattr(subprocess, "run", _probe_answering(1))
192
+ code = lint.run_full_check(
193
+ "src", config=TempestConfig(), fast=True, require_xdist=False
194
+ )
195
+ assert code == 0
196
+ assert len(calls) == 4
197
+ assert "-n" not in calls[-1]
198
+
199
+
166
200
  def test_an_unanswerable_probe_lets_pytest_speak(
167
201
  monkeypatch: pytest.MonkeyPatch,
168
202
  fake_pytest: Path,
@@ -311,13 +345,31 @@ def test_cli_test_fast_forwards_workers_and_target(
311
345
  main_module.app, ["test", "tests/unit", "--fast", "--workers", "4"]
312
346
  )
313
347
  assert result.exit_code == 0, result.output
314
- assert seen == [(("tests/unit",), {"fast": True, "workers": "4"})]
348
+ assert seen == [
349
+ (("tests/unit",), {"fast": True, "workers": "4", "require_xdist": True})
350
+ ]
315
351
 
316
352
 
317
- def test_cli_fast_defaults_to_auto(monkeypatch: pytest.MonkeyPatch) -> None:
353
+ def test_cli_runs_in_parallel_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
354
+ """No flag: parallel on auto workers, degrading to serial without xdist."""
355
+ seen = _capture_runner(monkeypatch, "run_pytest")
356
+ runner.invoke(main_module.app, ["test"])
357
+ assert seen == [
358
+ ((None,), {"fast": True, "workers": "auto", "require_xdist": False})
359
+ ]
360
+
361
+
362
+ def test_cli_fast_by_name_requires_xdist(monkeypatch: pytest.MonkeyPatch) -> None:
318
363
  seen = _capture_runner(monkeypatch, "run_pytest")
319
364
  runner.invoke(main_module.app, ["test", "--fast"])
320
- assert seen == [((None,), {"fast": True, "workers": "auto"})]
365
+ assert seen == [((None,), {"fast": True, "workers": "auto", "require_xdist": True})]
366
+
367
+
368
+ def test_cli_serial_opts_out(monkeypatch: pytest.MonkeyPatch) -> None:
369
+ seen = _capture_runner(monkeypatch, "run_pytest")
370
+ runner.invoke(main_module.app, ["test", "--serial", "tests/unit"])
371
+ assert seen[0][0] == ("tests/unit",)
372
+ assert seen[0][1]["fast"] is False
321
373
 
322
374
 
323
375
  def test_a_folder_named_fast_is_still_a_target(
@@ -326,18 +378,29 @@ def test_a_folder_named_fast_is_still_a_target(
326
378
  """Why ``--fast`` is a flag: ``tempest test fast`` keeps its meaning."""
327
379
  seen = _capture_runner(monkeypatch, "run_pytest")
328
380
  runner.invoke(main_module.app, ["test", "fast"])
329
- assert seen == [(("fast",), {"fast": False, "workers": "auto"})]
381
+ assert seen == [
382
+ (("fast",), {"fast": True, "workers": "auto", "require_xdist": False})
383
+ ]
330
384
 
331
385
 
332
- def test_cli_workers_without_fast_is_a_usage_error(
386
+ def test_cli_workers_with_serial_is_a_usage_error(
333
387
  monkeypatch: pytest.MonkeyPatch,
334
388
  ) -> None:
335
389
  seen = _capture_runner(monkeypatch, "run_pytest")
336
- result = runner.invoke(main_module.app, ["test", "--workers", "4"])
390
+ result = runner.invoke(main_module.app, ["test", "--serial", "--workers", "4"])
337
391
  assert result.exit_code == 2
338
392
  assert seen == []
339
393
 
340
394
 
395
+ def test_cli_workers_without_a_flag_tunes_the_default(
396
+ monkeypatch: pytest.MonkeyPatch,
397
+ ) -> None:
398
+ seen = _capture_runner(monkeypatch, "run_pytest")
399
+ result = runner.invoke(main_module.app, ["test", "--workers", "4"])
400
+ assert result.exit_code == 0, result.output
401
+ assert seen[0][1]["workers"] == "4"
402
+
403
+
341
404
  @pytest.mark.parametrize("workers", ["many", "-1", "2.5", ""])
342
405
  def test_cli_rejects_a_worker_count_xdist_would_refuse(
343
406
  monkeypatch: pytest.MonkeyPatch,
@@ -368,6 +431,16 @@ def test_cli_check_fast_reaches_the_gate(monkeypatch: pytest.MonkeyPatch) -> Non
368
431
  assert args == ("src",)
369
432
  assert kwargs["fast"] is True
370
433
  assert kwargs["workers"] == "3"
434
+ assert kwargs["require_xdist"] is True
435
+
436
+
437
+ def test_cli_check_is_parallel_by_default(monkeypatch: pytest.MonkeyPatch) -> None:
438
+ seen = _capture_runner(monkeypatch, "run_full_check")
439
+ result = runner.invoke(main_module.app, ["check", "src"])
440
+ assert result.exit_code == 0, result.output
441
+ kwargs = seen[0][1]
442
+ assert kwargs["fast"] is True
443
+ assert kwargs["require_xdist"] is False
371
444
 
372
445
 
373
446
  def test_cli_propagates_the_runner_exit_code(
@@ -384,5 +457,5 @@ def test_test_help_explains_the_parallel_only_failure() -> None:
384
457
  )
385
458
  assert command.callback is not None
386
459
  doc = command.callback.__doc__ or ""
387
- assert "fails only under --fast" in doc
388
- assert "tests/test_x.py::test_name" in doc
460
+ assert "fails only in parallel" in doc
461
+ assert "test --serial tests/test_x.py::test_name" in doc
@@ -1148,7 +1148,7 @@ wheels = [
1148
1148
 
1149
1149
  [[package]]
1150
1150
  name = "tempest-cli"
1151
- version = "0.5.1"
1151
+ version = "0.6.0"
1152
1152
  source = { editable = "." }
1153
1153
  dependencies = [
1154
1154
  { name = "ruff" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes