tempest-cli 0.1.0__py3-none-any.whl
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/__init__.py +70 -0
- tempest_cli/_templates/pull_request_template.en-US.md +39 -0
- tempest_cli/_templates/pull_request_template.pt-BR.md +39 -0
- tempest_cli/config.py +164 -0
- tempest_cli/lint.py +238 -0
- tempest_cli/main.py +411 -0
- tempest_cli/pr_prompt.py +779 -0
- tempest_cli/py.typed +0 -0
- tempest_cli-0.1.0.dist-info/METADATA +167 -0
- tempest_cli-0.1.0.dist-info/RECORD +13 -0
- tempest_cli-0.1.0.dist-info/WHEEL +4 -0
- tempest_cli-0.1.0.dist-info/entry_points.txt +2 -0
- tempest_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
tempest_cli/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Framework-agnostic quality gate for Python projects.
|
|
2
|
+
|
|
3
|
+
``tempest-cli`` runs ruff, mypy and pytest behind one command, with a
|
|
4
|
+
typing-strictness dial read from ``[tool.tempest]`` in the project's
|
|
5
|
+
``pyproject.toml`` — and a generator for the prompt that makes an AI fill
|
|
6
|
+
a pull-request description from the branch's own diff.
|
|
7
|
+
|
|
8
|
+
It knows nothing about any web framework. The only runtime dependency is
|
|
9
|
+
``typer``; the tools themselves are invoked from the active environment,
|
|
10
|
+
so a project pins the ruff and mypy versions it wants.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
tempest-cli check # lint + fmt-check + type + test
|
|
14
|
+
tempest-cli fix # every ruff autofix, then format
|
|
15
|
+
tempest-cli type -s strict # override the configured strictness
|
|
16
|
+
tempest-cli pr-prompt | claude -p
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Everything is importable too, for a project that would rather wire the
|
|
20
|
+
gate into its own tooling:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from tempest_cli import load_tempest_config, run_full_check
|
|
24
|
+
|
|
25
|
+
config = load_tempest_config()
|
|
26
|
+
exit_code = run_full_check(".", config=config)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
And :func:`tempest_cli.main.register_commands` mounts the whole gate onto
|
|
30
|
+
an existing :class:`typer.Typer`, so another CLI can expose these
|
|
31
|
+
commands under its own name without copying them.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from tempest_cli.config import DEFAULT_TYPING_STRICTNESS as DEFAULT_TYPING_STRICTNESS
|
|
35
|
+
from tempest_cli.config import TempestConfig as TempestConfig
|
|
36
|
+
from tempest_cli.config import TypingStrictness as TypingStrictness
|
|
37
|
+
from tempest_cli.config import find_pyproject as find_pyproject
|
|
38
|
+
from tempest_cli.config import load_tempest_config as load_tempest_config
|
|
39
|
+
from tempest_cli.lint import resolve_tool as resolve_tool
|
|
40
|
+
from tempest_cli.lint import run_full_check as run_full_check
|
|
41
|
+
from tempest_cli.lint import run_mypy as run_mypy
|
|
42
|
+
from tempest_cli.lint import run_pytest as run_pytest
|
|
43
|
+
from tempest_cli.lint import run_ruff_check as run_ruff_check
|
|
44
|
+
from tempest_cli.lint import run_ruff_fix as run_ruff_fix
|
|
45
|
+
from tempest_cli.lint import run_ruff_format as run_ruff_format
|
|
46
|
+
from tempest_cli.pr_prompt import GitError as GitError
|
|
47
|
+
from tempest_cli.pr_prompt import PromptLanguage as PromptLanguage
|
|
48
|
+
from tempest_cli.pr_prompt import generate_pr_prompt as generate_pr_prompt
|
|
49
|
+
|
|
50
|
+
__version__: str = "0.1.0"
|
|
51
|
+
"""Installed package version."""
|
|
52
|
+
|
|
53
|
+
__all__: list[str] = [
|
|
54
|
+
"DEFAULT_TYPING_STRICTNESS",
|
|
55
|
+
"GitError",
|
|
56
|
+
"PromptLanguage",
|
|
57
|
+
"TempestConfig",
|
|
58
|
+
"TypingStrictness",
|
|
59
|
+
"__version__",
|
|
60
|
+
"find_pyproject",
|
|
61
|
+
"generate_pr_prompt",
|
|
62
|
+
"load_tempest_config",
|
|
63
|
+
"resolve_tool",
|
|
64
|
+
"run_full_check",
|
|
65
|
+
"run_mypy",
|
|
66
|
+
"run_pytest",
|
|
67
|
+
"run_ruff_check",
|
|
68
|
+
"run_ruff_fix",
|
|
69
|
+
"run_ruff_format",
|
|
70
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
| Has Script? | New Env Vars |
|
|
2
|
+
| :---------: | :----------: |
|
|
3
|
+
| Yes/No | Yes/No |
|
|
4
|
+
|
|
5
|
+
> :warning: **NOTE**
|
|
6
|
+
> Use this section to highlight what the reviewer must not miss:
|
|
7
|
+
> - Dependency on other PRs
|
|
8
|
+
> - New environment variables
|
|
9
|
+
> - Reasons for extra attention
|
|
10
|
+
|
|
11
|
+
## Problem
|
|
12
|
+
|
|
13
|
+
_What problem are you trying to solve?_
|
|
14
|
+
|
|
15
|
+
## Solution
|
|
16
|
+
|
|
17
|
+
_How did you solve it?_
|
|
18
|
+
|
|
19
|
+
## Screenshots
|
|
20
|
+
|
|
21
|
+
**Screenshot description**:
|
|
22
|
+
[insert screenshot here]
|
|
23
|
+
|
|
24
|
+
## Other changes
|
|
25
|
+
|
|
26
|
+
(e.g. bugfixes, UI tweaks, improvements, refactors, etc)
|
|
27
|
+
|
|
28
|
+
## Deploy notes
|
|
29
|
+
|
|
30
|
+
_Notes about deploying these changes._
|
|
31
|
+
|
|
32
|
+
**New Environment Variables**:
|
|
33
|
+
- `ENV_VAR`: details
|
|
34
|
+
|
|
35
|
+
**New Scripts and/or Background Tasks**:
|
|
36
|
+
- `script`: details
|
|
37
|
+
|
|
38
|
+
**New Dependencies**:
|
|
39
|
+
- `dependency`: details
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
| Tem Script? | Novas Env Vars |
|
|
2
|
+
| :---------: | :------------: |
|
|
3
|
+
| Sim/Não | Sim/Não |
|
|
4
|
+
|
|
5
|
+
> :warning: **NOTA**
|
|
6
|
+
> Use esta seção para destacar pontos importantes sobre o PR:
|
|
7
|
+
> - Dependência de outros PRs
|
|
8
|
+
> - Novas variáveis de ambiente
|
|
9
|
+
> - Motivos para atenção especial
|
|
10
|
+
|
|
11
|
+
## Problema
|
|
12
|
+
|
|
13
|
+
_Qual é o problema que você está tentando resolver?_
|
|
14
|
+
|
|
15
|
+
## Solução
|
|
16
|
+
|
|
17
|
+
_Como você resolveu o problema?_
|
|
18
|
+
|
|
19
|
+
## Screenshots
|
|
20
|
+
|
|
21
|
+
**Descrição do Screenshot**:
|
|
22
|
+
[insira o screenshot aqui]
|
|
23
|
+
|
|
24
|
+
## Outras mudanças
|
|
25
|
+
|
|
26
|
+
(e.g. bugfixes, alterações na UI, melhorias, refatorações, etc)
|
|
27
|
+
|
|
28
|
+
## Notas sobre deploy
|
|
29
|
+
|
|
30
|
+
_Notas sobre a implantação das alterações._
|
|
31
|
+
|
|
32
|
+
**Novas Variáveis de Ambiente**:
|
|
33
|
+
- `ENV_VAR`: detalhes
|
|
34
|
+
|
|
35
|
+
**Novos Scripts e/ou Tarefas de Background**:
|
|
36
|
+
- `script`: detalhes
|
|
37
|
+
|
|
38
|
+
**Novas Dependências**:
|
|
39
|
+
- `dependency`: detalhes
|
tempest_cli/config.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""``[tool.tempest]`` configuration read from ``pyproject.toml``.
|
|
2
|
+
|
|
3
|
+
The quality gates (``tempest-cli lint`` / ``fix`` / ``type`` / ``check``)
|
|
4
|
+
let a project dial how strictly typing is enforced without editing the
|
|
5
|
+
gate commands themselves. The single knob is ``typing_strictness`` under
|
|
6
|
+
``[tool.tempest]``:
|
|
7
|
+
|
|
8
|
+
```toml
|
|
9
|
+
[tool.tempest]
|
|
10
|
+
typing_strictness = "strict" # lenient | standard | strict
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Each level layers *additional* ruff and mypy flags on top of whatever
|
|
14
|
+
``[tool.ruff]`` / ``[tool.mypy]`` already declares -- it never relaxes
|
|
15
|
+
the project's own configuration. ``Any`` is always a valid annotation,
|
|
16
|
+
so ANN401 is never enabled at any level: the levels enforce that things
|
|
17
|
+
*are* annotated, never that they avoid ``Any``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import tomllib
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Literal, get_args
|
|
26
|
+
|
|
27
|
+
TypingStrictness = Literal["lenient", "standard", "strict"]
|
|
28
|
+
"""Allowed values for ``[tool.tempest] typing_strictness``."""
|
|
29
|
+
|
|
30
|
+
DEFAULT_TYPING_STRICTNESS: TypingStrictness = "standard"
|
|
31
|
+
"""Level applied when the key is absent or no ``pyproject.toml`` is found."""
|
|
32
|
+
|
|
33
|
+
# ANN rules layered onto ruff per level. ANN401 (Any) is intentionally
|
|
34
|
+
# never included -- ``Any`` is a valid annotation. ANN002/ANN003
|
|
35
|
+
# (``*args`` / ``**kwargs``) are also left out as noise.
|
|
36
|
+
_RUFF_ANN_BY_LEVEL: dict[TypingStrictness, list[str]] = {
|
|
37
|
+
"lenient": [],
|
|
38
|
+
"standard": ["ANN001", "ANN201", "ANN202", "ANN205", "ANN206"],
|
|
39
|
+
"strict": [
|
|
40
|
+
"ANN001",
|
|
41
|
+
"ANN201",
|
|
42
|
+
"ANN202",
|
|
43
|
+
"ANN204",
|
|
44
|
+
"ANN205",
|
|
45
|
+
"ANN206",
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# mypy flags layered on per level (additive over [tool.mypy]).
|
|
50
|
+
_MYPY_FLAGS_BY_LEVEL: dict[TypingStrictness, list[str]] = {
|
|
51
|
+
"lenient": [],
|
|
52
|
+
"standard": ["--disallow-untyped-defs", "--disallow-incomplete-defs"],
|
|
53
|
+
"strict": ["--strict"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class TempestConfig:
|
|
59
|
+
"""Resolved ``[tool.tempest]`` settings.
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
typing_strictness (TypingStrictness): How strictly the CLI gates
|
|
63
|
+
enforce typing. One of ``"lenient"``, ``"standard"``,
|
|
64
|
+
``"strict"``.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
typing_strictness: TypingStrictness = DEFAULT_TYPING_STRICTNESS
|
|
68
|
+
|
|
69
|
+
def ruff_ann_select(self) -> list[str]:
|
|
70
|
+
"""Return the ANN rule codes to add to ruff for this level.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
list[str]: Rule codes for ``--extend-select`` (empty for
|
|
74
|
+
``"lenient"``). ANN401 is never present.
|
|
75
|
+
"""
|
|
76
|
+
return list(_RUFF_ANN_BY_LEVEL[self.typing_strictness])
|
|
77
|
+
|
|
78
|
+
def mypy_flags(self) -> list[str]:
|
|
79
|
+
"""Return the extra mypy flags to add for this level.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
list[str]: Flags layered on top of the project's
|
|
83
|
+
``[tool.mypy]`` config (empty for ``"lenient"``).
|
|
84
|
+
"""
|
|
85
|
+
return list(_MYPY_FLAGS_BY_LEVEL[self.typing_strictness])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def find_pyproject(start: Path | None = None) -> Path | None:
|
|
89
|
+
"""Locate the nearest ``pyproject.toml`` walking up from ``start``.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
start (Path | None): Directory to begin the search. Defaults to
|
|
93
|
+
the current working directory.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Path | None: The path to the first ``pyproject.toml`` found in
|
|
97
|
+
``start`` or an ancestor, or ``None`` when none exists.
|
|
98
|
+
"""
|
|
99
|
+
current = (start or Path.cwd()).resolve()
|
|
100
|
+
for directory in (current, *current.parents):
|
|
101
|
+
candidate = directory / "pyproject.toml"
|
|
102
|
+
if candidate.is_file():
|
|
103
|
+
return candidate
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _coerce_strictness(value: object, *, source: str) -> TypingStrictness:
|
|
108
|
+
"""Validate a raw ``typing_strictness`` value.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
value (object): The raw value read from the TOML table.
|
|
112
|
+
source (str): Human-readable origin used in the error message.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
TypingStrictness: The validated level.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ValueError: When ``value`` is not one of the allowed levels.
|
|
119
|
+
"""
|
|
120
|
+
allowed = get_args(TypingStrictness)
|
|
121
|
+
if value not in allowed:
|
|
122
|
+
allowed_str = ", ".join(repr(level) for level in allowed)
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"{source}: invalid typing_strictness {value!r}; "
|
|
125
|
+
f"expected one of {allowed_str}."
|
|
126
|
+
)
|
|
127
|
+
return value # type: ignore[return-value]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def load_tempest_config(start: Path | None = None) -> TempestConfig:
|
|
131
|
+
"""Load ``[tool.tempest]`` from the nearest ``pyproject.toml``.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
start (Path | None): Directory to begin the search. Defaults to
|
|
135
|
+
the current working directory.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
TempestConfig: The resolved config. Falls back to defaults when
|
|
139
|
+
no ``pyproject.toml`` or no ``[tool.tempest]`` table is found.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: When ``typing_strictness`` is present but invalid.
|
|
143
|
+
"""
|
|
144
|
+
pyproject = find_pyproject(start)
|
|
145
|
+
if pyproject is None:
|
|
146
|
+
return TempestConfig()
|
|
147
|
+
with pyproject.open("rb") as handle:
|
|
148
|
+
data = tomllib.load(handle)
|
|
149
|
+
table = data.get("tool", {}).get("tempest", {})
|
|
150
|
+
strictness = (
|
|
151
|
+
_coerce_strictness(table["typing_strictness"], source=str(pyproject))
|
|
152
|
+
if "typing_strictness" in table
|
|
153
|
+
else DEFAULT_TYPING_STRICTNESS
|
|
154
|
+
)
|
|
155
|
+
return TempestConfig(typing_strictness=strictness)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__: list[str] = [
|
|
159
|
+
"DEFAULT_TYPING_STRICTNESS",
|
|
160
|
+
"TempestConfig",
|
|
161
|
+
"TypingStrictness",
|
|
162
|
+
"find_pyproject",
|
|
163
|
+
"load_tempest_config",
|
|
164
|
+
]
|
tempest_cli/lint.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Quality-gate helpers backing ``tempest lint``/``check``/etc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from tempest_cli.config import TempestConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ruff_ann_args(config: TempestConfig | None) -> list[str]:
|
|
15
|
+
"""Build the ruff ``--extend-select`` args for ``config``'s level.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
config (TempestConfig | None): The resolved config, or ``None``
|
|
19
|
+
to use the default level.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
list[str]: ``["--extend-select", "ANN001,..."]`` for a level that
|
|
23
|
+
adds ANN rules, or ``[]`` for the lenient level.
|
|
24
|
+
"""
|
|
25
|
+
codes = (config or TempestConfig()).ruff_ann_select()
|
|
26
|
+
if not codes:
|
|
27
|
+
return []
|
|
28
|
+
return ["--extend-select", ",".join(codes)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_tool(executable: str) -> list[str] | None:
|
|
32
|
+
"""Return an argv prefix invoking ``executable`` or ``None`` when absent.
|
|
33
|
+
|
|
34
|
+
Public because callers outside the gate need the same lookup — the
|
|
35
|
+
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.
|
|
38
|
+
|
|
39
|
+
Preference order:
|
|
40
|
+
|
|
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).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
executable (str): The command name (``ruff``/``mypy``/``pytest``).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
list[str] | None: argv prefix to extend with extra arguments, or
|
|
51
|
+
``None`` when no runner could be found.
|
|
52
|
+
"""
|
|
53
|
+
direct = shutil.which(executable)
|
|
54
|
+
if direct is not None:
|
|
55
|
+
return [direct]
|
|
56
|
+
uv = shutil.which("uv")
|
|
57
|
+
if uv is not None:
|
|
58
|
+
return [uv, "run", executable]
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _execute(executable: str, args: list[str]) -> int:
|
|
63
|
+
"""Run ``executable args`` and return its exit code.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
executable (str): The command to run.
|
|
67
|
+
args (list[str]): Extra arguments to forward.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
int: The child process exit code. Returns ``127`` when neither
|
|
71
|
+
the executable nor ``uv`` is available.
|
|
72
|
+
"""
|
|
73
|
+
argv = resolve_tool(executable)
|
|
74
|
+
if argv is None:
|
|
75
|
+
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.",
|
|
78
|
+
err=True,
|
|
79
|
+
)
|
|
80
|
+
return 127
|
|
81
|
+
return subprocess.call([*argv, *args])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run_ruff_check(target: str, *, config: TempestConfig | None = None) -> int:
|
|
85
|
+
"""Invoke ``ruff check <target>`` with the configured ANN rules.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
target (str): The path passed verbatim to ruff.
|
|
89
|
+
config (TempestConfig | None): Resolved ``[tool.tempest]`` config
|
|
90
|
+
controlling the typing-strictness ANN rules layered on. When
|
|
91
|
+
``None`` the default level is used.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
int: The ruff exit code.
|
|
95
|
+
"""
|
|
96
|
+
return _execute("ruff", ["check", *_ruff_ann_args(config), target])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_ruff_fix(
|
|
100
|
+
target: str,
|
|
101
|
+
*,
|
|
102
|
+
unsafe: bool = False,
|
|
103
|
+
config: TempestConfig | None = None,
|
|
104
|
+
) -> int:
|
|
105
|
+
"""Apply every automatic fix ruff can perform, then format the target.
|
|
106
|
+
|
|
107
|
+
Runs in two passes so the second one sees the rewritten file:
|
|
108
|
+
|
|
109
|
+
1. ``ruff check --fix [--unsafe-fixes] <target>`` — autofix imports
|
|
110
|
+
(sort + dedupe), remove unused imports, normalize string quotes,
|
|
111
|
+
drop trailing whitespace, fix the rest of the lint rules that
|
|
112
|
+
have safe (or, with ``unsafe=True``, also unsafe) autofixers.
|
|
113
|
+
2. ``ruff format <target>`` — normalize indentation, line length,
|
|
114
|
+
blank lines and trailing newlines.
|
|
115
|
+
|
|
116
|
+
Both passes always run. ``ruff check --fix`` exits non-zero whenever
|
|
117
|
+
*any* residual violation remains that it cannot autofix (an
|
|
118
|
+
over-length string/comment, an undefined name, etc.) — even though
|
|
119
|
+
it already rewrote everything it could. Short-circuiting on that
|
|
120
|
+
exit code would skip ``ruff format`` entirely, leaving the file
|
|
121
|
+
un-wrapped and its extra blank lines intact. So the formatter runs
|
|
122
|
+
unconditionally; the lint exit code is surfaced afterwards so CI
|
|
123
|
+
still fails on the leftover issues.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
target (str): The path passed verbatim to ruff.
|
|
127
|
+
unsafe (bool): When True, pass ``--unsafe-fixes`` so ruff also
|
|
128
|
+
applies the fixes it would otherwise leave alone.
|
|
129
|
+
config (TempestConfig | None): Resolved ``[tool.tempest]`` config
|
|
130
|
+
controlling the typing-strictness ANN rules layered onto the
|
|
131
|
+
fix pass. When ``None`` the default level is used.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
int: ``0`` when both passes succeed with nothing left to fix;
|
|
135
|
+
otherwise the lint pass exit code (residual violations), or the
|
|
136
|
+
format pass exit code when the lint pass was clean.
|
|
137
|
+
"""
|
|
138
|
+
check_args = ["check", "--fix", *_ruff_ann_args(config)]
|
|
139
|
+
if unsafe:
|
|
140
|
+
check_args.append("--unsafe-fixes")
|
|
141
|
+
check_args.append(target)
|
|
142
|
+
check_code = _execute("ruff", check_args)
|
|
143
|
+
format_code = _execute("ruff", ["format", target])
|
|
144
|
+
return check_code or format_code
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def run_ruff_format(target: str, *, check: bool) -> int:
|
|
148
|
+
"""Invoke ``ruff format`` (write or check-only).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
target (str): The path passed verbatim to ruff.
|
|
152
|
+
check (bool): When True, run ``ruff format --check`` (read-only).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
int: The ruff exit code.
|
|
156
|
+
"""
|
|
157
|
+
args = ["format"]
|
|
158
|
+
if check:
|
|
159
|
+
args.append("--check")
|
|
160
|
+
args.append(target)
|
|
161
|
+
return _execute("ruff", args)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def run_mypy(target: str, *, config: TempestConfig | None = None) -> int:
|
|
165
|
+
"""Invoke ``mypy <target>`` with the configured strictness flags.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
target (str): The path passed verbatim to mypy.
|
|
169
|
+
config (TempestConfig | None): Resolved ``[tool.tempest]`` config
|
|
170
|
+
controlling the mypy strictness flags layered on top of the
|
|
171
|
+
project's ``[tool.mypy]``. When ``None`` the default level is
|
|
172
|
+
used.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
int: The mypy exit code.
|
|
176
|
+
"""
|
|
177
|
+
flags = (config or TempestConfig()).mypy_flags()
|
|
178
|
+
return _execute("mypy", [*flags, target])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run_pytest(target: str | None) -> int:
|
|
182
|
+
"""Invoke ``pytest`` with an optional target.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
target (str | None): Optional pytest path filter. ``None`` runs
|
|
186
|
+
the default test suite.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
int: The pytest exit code.
|
|
190
|
+
"""
|
|
191
|
+
args = [target] if target else []
|
|
192
|
+
return _execute("pytest", args)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def run_full_check(target: str, *, config: TempestConfig | None = None) -> int:
|
|
196
|
+
"""Run the entire quality gate sequentially.
|
|
197
|
+
|
|
198
|
+
Order: ``ruff check`` → ``ruff format --check`` → ``mypy`` → ``pytest``.
|
|
199
|
+
Stops at the first non-zero exit code so failures surface fast.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
target (str): The path inspected by ruff/mypy. Pytest always runs
|
|
203
|
+
against the project's configured ``testpaths``.
|
|
204
|
+
config (TempestConfig | None): Resolved ``[tool.tempest]`` config
|
|
205
|
+
controlling the ANN rules and mypy flags layered onto the
|
|
206
|
+
ruff/mypy steps. When ``None`` the default level is used.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
int: The first non-zero exit code, or ``0`` when every gate passed.
|
|
210
|
+
"""
|
|
211
|
+
resolved = config or TempestConfig()
|
|
212
|
+
steps: list[tuple[str, list[str]]] = [
|
|
213
|
+
("ruff", ["check", *_ruff_ann_args(resolved), target]),
|
|
214
|
+
("ruff", ["format", "--check", target]),
|
|
215
|
+
("mypy", [*resolved.mypy_flags(), target]),
|
|
216
|
+
("pytest", []),
|
|
217
|
+
]
|
|
218
|
+
for executable, args in steps:
|
|
219
|
+
typer.echo(f"$ {executable} {' '.join(args)}", err=True)
|
|
220
|
+
code = _execute(executable, args)
|
|
221
|
+
if code != 0:
|
|
222
|
+
return code
|
|
223
|
+
return 0
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
__all__: list[str] = [
|
|
227
|
+
"resolve_tool",
|
|
228
|
+
"run_full_check",
|
|
229
|
+
"run_mypy",
|
|
230
|
+
"run_pytest",
|
|
231
|
+
"run_ruff_check",
|
|
232
|
+
"run_ruff_fix",
|
|
233
|
+
"run_ruff_format",
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__": # pragma: no cover - manual invocation only
|
|
238
|
+
sys.exit(run_full_check("."))
|