python-linters 0.2.2__tar.gz → 0.2.3__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.
- python_linters-0.2.3/PKG-INFO +105 -0
- python_linters-0.2.3/README.md +93 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/pyproject.toml +1 -1
- {python_linters-0.2.2 → python_linters-0.2.3}/pyproject.toml.orig +1 -1
- python_linters-0.2.3/src/python_linters/baseline_report.py +199 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/cli_commands.py +72 -5
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/lint_report.py +130 -13
- python_linters-0.2.3/src/python_linters/text_report.py +54 -0
- python_linters-0.2.3/src/python_linters/tool_versions.py +22 -0
- python_linters-0.2.2/PKG-INFO +0 -44
- python_linters-0.2.2/README.md +0 -32
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/__init__.py +0 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/__main__.py +0 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/config_check.py +0 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/reference_files.py +0 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/references/ruff.reference.toml +0 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/references/ty.reference.toml +0 -0
- {python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/tool_errors.py +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-linters
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Run ruff, ty and pymodularity on a Python project and print one JSON report grouped by rule.
|
|
5
|
+
Author: Tilo Himmelsbach
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Dist: ruff==0.17.5
|
|
8
|
+
Requires-Dist: ty==0.0.103
|
|
9
|
+
Requires-Dist: pymodularity==0.5.0
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# python-linters
|
|
14
|
+
|
|
15
|
+
Run ruff, ty and pymodularity on a Python project and print one JSON report
|
|
16
|
+
grouped by rule, for agents and CI to read.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
As a dev dependency of the project you want to lint:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
uv add --dev python-linters
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
- `uv run python-linters lint [path] [--out FILE] [--format json|text]
|
|
29
|
+
[--skip TOOL]... [--python PATH] [--extra-search-path DIR]...
|
|
30
|
+
[--baseline REF]` — runs
|
|
31
|
+
ruff, `ruff format --check`, ty and pymodularity (default path: `.`) and
|
|
32
|
+
prints the merged report to stdout, or writes it to `FILE` with `--out`.
|
|
33
|
+
- `--format text` prints one line per finding (`file:line:col
|
|
34
|
+
tool/rule message`, sorted by file then line) with a totals line
|
|
35
|
+
last, instead of the default JSON. `--out` writes whichever format
|
|
36
|
+
was chosen.
|
|
37
|
+
- `--skip TOOL` (repeatable; `ruff`, `ruff-format`, `ty`,
|
|
38
|
+
`pymodularity`) drops that tool: it does not run, gets no section and
|
|
39
|
+
no summary entry, and is listed under the report's top-level
|
|
40
|
+
`"skipped"`. Skipping all 4 is a usage error.
|
|
41
|
+
- `--python PATH` and `--extra-search-path DIR` (repeatable) are
|
|
42
|
+
forwarded verbatim to `ty check`'s own `--python`/`--extra-search-path`.
|
|
43
|
+
Needed whenever the project's imports do not resolve from a `.venv`
|
|
44
|
+
under the linted path — e.g. a ROS 2 package that resolves `rclpy`
|
|
45
|
+
and friends from a system interpreter and `/opt/ros/...`.
|
|
46
|
+
- ty looks for a `.venv` at *its own project root* — the directory
|
|
47
|
+
holding the nearest `ty.toml`, or a `pyproject.toml` with a
|
|
48
|
+
`[tool.ty]` section — not at the path you pass to `lint`. A shared
|
|
49
|
+
parent config can point ty's `.venv` search at the wrong directory for
|
|
50
|
+
a sub-package; the report's `"ty-environment"` key (below) shows what
|
|
51
|
+
ty actually resolved.
|
|
52
|
+
- ty ranks `$PYTHONPATH` entries ahead of first-party code. If
|
|
53
|
+
`PYTHONPATH` is set, a stale installed copy on it can shadow the
|
|
54
|
+
project tree being linted and silently report 0 findings; the report
|
|
55
|
+
names it under `"ty-environment"` when set.
|
|
56
|
+
- the report's top-level `"tools"` key names the bundled ruff/ty/
|
|
57
|
+
pymodularity versions; `"ty-environment"` names ty's resolved project
|
|
58
|
+
root and Python environment (absent if `ty` is skipped).
|
|
59
|
+
- `--baseline REF` (a branch, tag, SHA, `HEAD~N`, ...) reports only
|
|
60
|
+
findings that are new since that git ref: it checks the ref out into a
|
|
61
|
+
fresh `git worktree add --detach` scratch directory (never a reused
|
|
62
|
+
one — a stale copy there can carry findings from an earlier run),
|
|
63
|
+
lints the same sub-path there, and drops every current finding whose
|
|
64
|
+
`(file, rule, message)` triple already exists at the baseline. The
|
|
65
|
+
line number is excluded from that triple on purpose: a finding does
|
|
66
|
+
not become "new" just because a line was inserted above it. The
|
|
67
|
+
worktree is removed afterwards regardless of outcome. A ref that
|
|
68
|
+
doesn't resolve, or a `path` outside any git repo, exits 2 with the
|
|
69
|
+
reason on stderr. The report gets a top-level `"baseline": {"ref":
|
|
70
|
+
..., "sha": ...}`; the summary and exit code count only the new
|
|
71
|
+
findings; `--format text` prints only the new findings plus the
|
|
72
|
+
baseline ref on the totals line. Since a fresh worktree has no
|
|
73
|
+
`.venv` of its own, the baseline's ty run is given `--python`
|
|
74
|
+
explicitly — the current run's resolved `ty-environment` Python
|
|
75
|
+
environment, unless you passed your own `--python` — and the report
|
|
76
|
+
names which one it used under `"baseline"`'s `"ty-python"` key.
|
|
77
|
+
`--skip` and `--extra-search-path` apply to both runs.
|
|
78
|
+
- **File-move artifact in the delta.** The baseline keys findings by
|
|
79
|
+
`(file, rule, message)`. A refactor that moves code from `old/x.py`
|
|
80
|
+
to `new/y.py` reports the entry at `old/x.py` as *gone* and the same
|
|
81
|
+
finding at `new/y.py` as *new* — net delta +1 even though the
|
|
82
|
+
codebase's lint posture is unchanged. Two responses: (a) accept the
|
|
83
|
+
+1 and document the move in the commit message; (b) commit and
|
|
84
|
+
refresh the baseline. Prefer (a) for one-off moves; (b) only when a
|
|
85
|
+
move is large enough to otherwise swamp the real new findings.
|
|
86
|
+
- `uv run python-linters --version` — prints this package's version and
|
|
87
|
+
the bundled ruff/ty/pymodularity versions on one line.
|
|
88
|
+
- `uv run python-linters reference ruff|ty` — prints this package's shipped
|
|
89
|
+
reference config for the given tool to stdout. The package never writes
|
|
90
|
+
it into your project — copying it into `ruff.toml`/`ty.toml` (or
|
|
91
|
+
`pyproject.toml`) stays with you.
|
|
92
|
+
- `uv run python-linters check-config ruff|ty <path/to/config>` — compares
|
|
93
|
+
your copy against the pinned tool's current rule set: rules the tool
|
|
94
|
+
knows that your config omits (untriaged), and rules your config names
|
|
95
|
+
that the tool no longer knows (stale). The report names the version of
|
|
96
|
+
the tool it checked against on its first line.
|
|
97
|
+
|
|
98
|
+
## Exit codes
|
|
99
|
+
|
|
100
|
+
- `lint`: 0 clean, 1 findings, 2 any tool failure (named on stderr), a
|
|
101
|
+
usage error (e.g. `--skip` naming all 4 tools), or — with
|
|
102
|
+
`--baseline` — a ref that doesn't resolve or a path outside any git
|
|
103
|
+
repo.
|
|
104
|
+
- `check-config`: 0 up to date, 1 untriaged or stale rules found, 2 tool
|
|
105
|
+
failure.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# python-linters
|
|
2
|
+
|
|
3
|
+
Run ruff, ty and pymodularity on a Python project and print one JSON report
|
|
4
|
+
grouped by rule, for agents and CI to read.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
As a dev dependency of the project you want to lint:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
uv add --dev python-linters
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
- `uv run python-linters lint [path] [--out FILE] [--format json|text]
|
|
17
|
+
[--skip TOOL]... [--python PATH] [--extra-search-path DIR]...
|
|
18
|
+
[--baseline REF]` — runs
|
|
19
|
+
ruff, `ruff format --check`, ty and pymodularity (default path: `.`) and
|
|
20
|
+
prints the merged report to stdout, or writes it to `FILE` with `--out`.
|
|
21
|
+
- `--format text` prints one line per finding (`file:line:col
|
|
22
|
+
tool/rule message`, sorted by file then line) with a totals line
|
|
23
|
+
last, instead of the default JSON. `--out` writes whichever format
|
|
24
|
+
was chosen.
|
|
25
|
+
- `--skip TOOL` (repeatable; `ruff`, `ruff-format`, `ty`,
|
|
26
|
+
`pymodularity`) drops that tool: it does not run, gets no section and
|
|
27
|
+
no summary entry, and is listed under the report's top-level
|
|
28
|
+
`"skipped"`. Skipping all 4 is a usage error.
|
|
29
|
+
- `--python PATH` and `--extra-search-path DIR` (repeatable) are
|
|
30
|
+
forwarded verbatim to `ty check`'s own `--python`/`--extra-search-path`.
|
|
31
|
+
Needed whenever the project's imports do not resolve from a `.venv`
|
|
32
|
+
under the linted path — e.g. a ROS 2 package that resolves `rclpy`
|
|
33
|
+
and friends from a system interpreter and `/opt/ros/...`.
|
|
34
|
+
- ty looks for a `.venv` at *its own project root* — the directory
|
|
35
|
+
holding the nearest `ty.toml`, or a `pyproject.toml` with a
|
|
36
|
+
`[tool.ty]` section — not at the path you pass to `lint`. A shared
|
|
37
|
+
parent config can point ty's `.venv` search at the wrong directory for
|
|
38
|
+
a sub-package; the report's `"ty-environment"` key (below) shows what
|
|
39
|
+
ty actually resolved.
|
|
40
|
+
- ty ranks `$PYTHONPATH` entries ahead of first-party code. If
|
|
41
|
+
`PYTHONPATH` is set, a stale installed copy on it can shadow the
|
|
42
|
+
project tree being linted and silently report 0 findings; the report
|
|
43
|
+
names it under `"ty-environment"` when set.
|
|
44
|
+
- the report's top-level `"tools"` key names the bundled ruff/ty/
|
|
45
|
+
pymodularity versions; `"ty-environment"` names ty's resolved project
|
|
46
|
+
root and Python environment (absent if `ty` is skipped).
|
|
47
|
+
- `--baseline REF` (a branch, tag, SHA, `HEAD~N`, ...) reports only
|
|
48
|
+
findings that are new since that git ref: it checks the ref out into a
|
|
49
|
+
fresh `git worktree add --detach` scratch directory (never a reused
|
|
50
|
+
one — a stale copy there can carry findings from an earlier run),
|
|
51
|
+
lints the same sub-path there, and drops every current finding whose
|
|
52
|
+
`(file, rule, message)` triple already exists at the baseline. The
|
|
53
|
+
line number is excluded from that triple on purpose: a finding does
|
|
54
|
+
not become "new" just because a line was inserted above it. The
|
|
55
|
+
worktree is removed afterwards regardless of outcome. A ref that
|
|
56
|
+
doesn't resolve, or a `path` outside any git repo, exits 2 with the
|
|
57
|
+
reason on stderr. The report gets a top-level `"baseline": {"ref":
|
|
58
|
+
..., "sha": ...}`; the summary and exit code count only the new
|
|
59
|
+
findings; `--format text` prints only the new findings plus the
|
|
60
|
+
baseline ref on the totals line. Since a fresh worktree has no
|
|
61
|
+
`.venv` of its own, the baseline's ty run is given `--python`
|
|
62
|
+
explicitly — the current run's resolved `ty-environment` Python
|
|
63
|
+
environment, unless you passed your own `--python` — and the report
|
|
64
|
+
names which one it used under `"baseline"`'s `"ty-python"` key.
|
|
65
|
+
`--skip` and `--extra-search-path` apply to both runs.
|
|
66
|
+
- **File-move artifact in the delta.** The baseline keys findings by
|
|
67
|
+
`(file, rule, message)`. A refactor that moves code from `old/x.py`
|
|
68
|
+
to `new/y.py` reports the entry at `old/x.py` as *gone* and the same
|
|
69
|
+
finding at `new/y.py` as *new* — net delta +1 even though the
|
|
70
|
+
codebase's lint posture is unchanged. Two responses: (a) accept the
|
|
71
|
+
+1 and document the move in the commit message; (b) commit and
|
|
72
|
+
refresh the baseline. Prefer (a) for one-off moves; (b) only when a
|
|
73
|
+
move is large enough to otherwise swamp the real new findings.
|
|
74
|
+
- `uv run python-linters --version` — prints this package's version and
|
|
75
|
+
the bundled ruff/ty/pymodularity versions on one line.
|
|
76
|
+
- `uv run python-linters reference ruff|ty` — prints this package's shipped
|
|
77
|
+
reference config for the given tool to stdout. The package never writes
|
|
78
|
+
it into your project — copying it into `ruff.toml`/`ty.toml` (or
|
|
79
|
+
`pyproject.toml`) stays with you.
|
|
80
|
+
- `uv run python-linters check-config ruff|ty <path/to/config>` — compares
|
|
81
|
+
your copy against the pinned tool's current rule set: rules the tool
|
|
82
|
+
knows that your config omits (untriaged), and rules your config names
|
|
83
|
+
that the tool no longer knows (stale). The report names the version of
|
|
84
|
+
the tool it checked against on its first line.
|
|
85
|
+
|
|
86
|
+
## Exit codes
|
|
87
|
+
|
|
88
|
+
- `lint`: 0 clean, 1 findings, 2 any tool failure (named on stderr), a
|
|
89
|
+
usage error (e.g. `--skip` naming all 4 tools), or — with
|
|
90
|
+
`--baseline` — a ref that doesn't resolve or a path outside any git
|
|
91
|
+
repo.
|
|
92
|
+
- `check-config`: 0 up to date, 1 untriaged or stale rules found, 2 tool
|
|
93
|
+
failure.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""`lint --baseline REF`: report only findings new since a git ref.
|
|
2
|
+
|
|
3
|
+
Ported from the baseline mode of the semgrep-wrapping gate script, minus
|
|
4
|
+
semgrep.
|
|
5
|
+
|
|
6
|
+
Mechanism:
|
|
7
|
+
1. Resolve *ref* to a SHA with `git rev-parse --verify` (a bad ref, or a
|
|
8
|
+
project outside any git repo, fails here).
|
|
9
|
+
2. Lint the current tree first, so its resolved ty environment is known.
|
|
10
|
+
3. Check out that SHA into a fresh `git worktree add --detach` scratch
|
|
11
|
+
directory — never a reused directory, which can carry stale files from
|
|
12
|
+
an earlier run.
|
|
13
|
+
4. Lint the same sub-path inside the worktree, with the same `--skip` set.
|
|
14
|
+
ty's `.venv` discovery has nothing to find in a fresh worktree, so the
|
|
15
|
+
baseline run gets `--python` forwarded explicitly: the caller's own
|
|
16
|
+
`--python` if given, otherwise the current run's resolved
|
|
17
|
+
`ty-environment.python_environment`.
|
|
18
|
+
5. Drop every current finding whose (file, rule, message) triple also
|
|
19
|
+
appears at the baseline. The line number is excluded from this key on
|
|
20
|
+
purpose: a finding does not become "new" just because a line was
|
|
21
|
+
inserted above it in the diff's range.
|
|
22
|
+
6. Remove the worktree in `finally`, even on a lint failure or a Ctrl-C.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import tempfile
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from python_linters.lint_report import TOOL_NAMES, build_report
|
|
33
|
+
|
|
34
|
+
# Resolved once to a full path: a bare "git" is a partial-executable-path
|
|
35
|
+
# finding, and the fallback keeps this working even if `which` ever fails.
|
|
36
|
+
_GIT = shutil.which("git") or "git"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BaselineError(Exception):
|
|
40
|
+
"""A baseline ref could not be resolved, or project_path is not in a git repo."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _resolve_ref(project_path: Path, ref: str) -> str:
|
|
44
|
+
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] # fixed argv list built by this package, never shell-interpolated or from untrusted input
|
|
45
|
+
[_GIT, "-C", str(project_path), "rev-parse", "--verify", ref],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
check=False,
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
msg = f"cannot resolve baseline ref '{ref}': {result.stderr.strip()}"
|
|
52
|
+
raise BaselineError(msg)
|
|
53
|
+
return result.stdout.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _git_root(project_path: Path) -> Path:
|
|
57
|
+
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] # fixed argv list built by this package, never shell-interpolated or from untrusted input
|
|
58
|
+
[_GIT, "-C", str(project_path), "rev-parse", "--show-toplevel"],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
check=False,
|
|
62
|
+
)
|
|
63
|
+
return Path(result.stdout.strip())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _triple(file: str, rule: str, message: str) -> tuple[str, str, str]:
|
|
67
|
+
return (file, rule, message)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _baseline_triples(report: dict[str, object]) -> set[tuple[str, str, str]]:
|
|
71
|
+
triples: set[tuple[str, str, str]] = set()
|
|
72
|
+
for tool in TOOL_NAMES:
|
|
73
|
+
section = report.get(tool)
|
|
74
|
+
if not isinstance(section, dict):
|
|
75
|
+
continue
|
|
76
|
+
for rule, group in section.items():
|
|
77
|
+
triples.update(
|
|
78
|
+
_triple(loc["file"], rule, loc["message"]) for loc in group["locations"]
|
|
79
|
+
)
|
|
80
|
+
return triples
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _diff_report(
|
|
84
|
+
report: dict[str, object], baseline_triples: set[tuple[str, str, str]]
|
|
85
|
+
) -> tuple[dict[str, object], int]:
|
|
86
|
+
"""Keep only locations whose triple is not in *baseline_triples*.
|
|
87
|
+
|
|
88
|
+
Recomputes each kept rule's `count` and each tool's `summary` entry;
|
|
89
|
+
drops a rule whose locations list becomes empty.
|
|
90
|
+
"""
|
|
91
|
+
new_report = dict(report)
|
|
92
|
+
summary: dict[str, object] = {}
|
|
93
|
+
grand_total = 0
|
|
94
|
+
|
|
95
|
+
for tool in TOOL_NAMES:
|
|
96
|
+
section = report.get(tool)
|
|
97
|
+
if not isinstance(section, dict):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
new_section: dict[str, dict[str, object]] = {}
|
|
101
|
+
counts: dict[str, int] = {}
|
|
102
|
+
for rule, group in section.items():
|
|
103
|
+
keep = [
|
|
104
|
+
loc
|
|
105
|
+
for loc in group["locations"]
|
|
106
|
+
if _triple(loc["file"], rule, loc["message"]) not in baseline_triples
|
|
107
|
+
]
|
|
108
|
+
if not keep:
|
|
109
|
+
continue
|
|
110
|
+
new_group = dict(group)
|
|
111
|
+
new_group["locations"] = keep
|
|
112
|
+
new_group["count"] = len(keep)
|
|
113
|
+
new_section[rule] = new_group
|
|
114
|
+
counts[rule] = len(keep)
|
|
115
|
+
|
|
116
|
+
new_report[tool] = new_section
|
|
117
|
+
total = sum(counts.values())
|
|
118
|
+
summary[tool] = {"total": total, "rules": len(new_section)}
|
|
119
|
+
grand_total += total
|
|
120
|
+
|
|
121
|
+
new_report["summary"] = summary
|
|
122
|
+
return new_report, grand_total
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_with_baseline(
|
|
126
|
+
project_path: Path,
|
|
127
|
+
baseline_ref: str,
|
|
128
|
+
skip: frozenset[str] = frozenset(),
|
|
129
|
+
ty_python: str | None = None,
|
|
130
|
+
ty_extra_search_paths: tuple[str, ...] = (),
|
|
131
|
+
) -> tuple[dict[str, object], int]:
|
|
132
|
+
"""Lint *project_path*, then drop every finding already present at
|
|
133
|
+
*baseline_ref*. Returns the same report shape as `build_report`, plus a
|
|
134
|
+
top-level `"baseline"` key, with the summary and total counting only
|
|
135
|
+
the new findings.
|
|
136
|
+
|
|
137
|
+
Raises `BaselineError` if the ref does not resolve or project_path is
|
|
138
|
+
not inside a git repo.
|
|
139
|
+
"""
|
|
140
|
+
sha = _resolve_ref(project_path, baseline_ref)
|
|
141
|
+
git_root = _git_root(project_path)
|
|
142
|
+
try:
|
|
143
|
+
rel_to_root = project_path.relative_to(git_root)
|
|
144
|
+
except ValueError:
|
|
145
|
+
rel_to_root = Path()
|
|
146
|
+
|
|
147
|
+
current_report, _ = build_report(
|
|
148
|
+
project_path,
|
|
149
|
+
skip=skip,
|
|
150
|
+
ty_python=ty_python,
|
|
151
|
+
ty_extra_search_paths=ty_extra_search_paths,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# The baseline worktree has no `.venv` of its own to discover: forward
|
|
155
|
+
# the current run's resolved ty environment, unless the caller already
|
|
156
|
+
# pinned one explicitly.
|
|
157
|
+
baseline_ty_python = ty_python
|
|
158
|
+
if baseline_ty_python is None and "ty" not in skip:
|
|
159
|
+
ty_environment = current_report.get("ty-environment", {})
|
|
160
|
+
baseline_ty_python = (
|
|
161
|
+
ty_environment.get("python_environment")
|
|
162
|
+
if isinstance(ty_environment, dict)
|
|
163
|
+
else None
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
tmpdir = tempfile.mkdtemp(prefix="python-linters-baseline-")
|
|
167
|
+
try:
|
|
168
|
+
worktree = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] # fixed argv list built by this package, never shell-interpolated or from untrusted input
|
|
169
|
+
[_GIT, "-C", str(project_path), "worktree", "add", "--detach", tmpdir, sha],
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
check=False,
|
|
173
|
+
)
|
|
174
|
+
if worktree.returncode != 0:
|
|
175
|
+
msg = f"git worktree add failed: {worktree.stderr.strip()}"
|
|
176
|
+
raise BaselineError(msg)
|
|
177
|
+
|
|
178
|
+
baseline_path = Path(tmpdir) / rel_to_root
|
|
179
|
+
baseline_report, _ = build_report(
|
|
180
|
+
baseline_path,
|
|
181
|
+
skip=skip,
|
|
182
|
+
ty_python=baseline_ty_python,
|
|
183
|
+
ty_extra_search_paths=ty_extra_search_paths,
|
|
184
|
+
)
|
|
185
|
+
finally:
|
|
186
|
+
subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] # fixed argv list built by this package, never shell-interpolated or from untrusted input
|
|
187
|
+
[_GIT, "-C", str(project_path), "worktree", "remove", "--force", tmpdir],
|
|
188
|
+
capture_output=True,
|
|
189
|
+
text=True,
|
|
190
|
+
check=False,
|
|
191
|
+
)
|
|
192
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
193
|
+
|
|
194
|
+
baseline_triples = _baseline_triples(baseline_report)
|
|
195
|
+
new_report, total = _diff_report(current_report, baseline_triples)
|
|
196
|
+
new_report["baseline"] = {"ref": baseline_ref, "sha": sha}
|
|
197
|
+
if baseline_ty_python is not None:
|
|
198
|
+
new_report["baseline"]["ty-python"] = baseline_ty_python
|
|
199
|
+
return new_report, total
|
|
@@ -5,30 +5,54 @@ import json
|
|
|
5
5
|
import sys
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from python_linters.baseline_report import BaselineError, run_with_baseline
|
|
8
9
|
from python_linters.config_check import (
|
|
9
10
|
check_ruff_config,
|
|
10
11
|
check_ty_config,
|
|
11
12
|
format_ruff_report,
|
|
12
13
|
format_ty_report,
|
|
13
14
|
)
|
|
14
|
-
from python_linters.lint_report import build_report
|
|
15
|
+
from python_linters.lint_report import TOOL_NAMES, build_report
|
|
15
16
|
from python_linters.reference_files import reference_text
|
|
17
|
+
from python_linters.text_report import format_text_report
|
|
16
18
|
from python_linters.tool_errors import ToolFailedError
|
|
19
|
+
from python_linters.tool_versions import package_version, tool_versions
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def _lint(args: argparse.Namespace) -> int:
|
|
23
|
+
if set(args.skip) >= set(TOOL_NAMES):
|
|
24
|
+
args.parser.error("--skip cannot skip every tool")
|
|
25
|
+
|
|
20
26
|
project_path = Path(args.path).resolve()
|
|
21
27
|
if not project_path.exists():
|
|
22
28
|
print(f"Error: {project_path} does not exist", file=sys.stderr)
|
|
23
29
|
return 2
|
|
24
30
|
|
|
25
31
|
try:
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
if args.baseline:
|
|
33
|
+
output, total_findings = run_with_baseline(
|
|
34
|
+
project_path,
|
|
35
|
+
args.baseline,
|
|
36
|
+
skip=frozenset(args.skip),
|
|
37
|
+
ty_python=args.ty_python,
|
|
38
|
+
ty_extra_search_paths=tuple(args.ty_extra_search_path),
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
output, total_findings = build_report(
|
|
42
|
+
project_path,
|
|
43
|
+
skip=frozenset(args.skip),
|
|
44
|
+
ty_python=args.ty_python,
|
|
45
|
+
ty_extra_search_paths=tuple(args.ty_extra_search_path),
|
|
46
|
+
)
|
|
47
|
+
except (ToolFailedError, BaselineError) as e:
|
|
28
48
|
print(str(e), file=sys.stderr)
|
|
29
49
|
return 2
|
|
30
50
|
|
|
31
|
-
serialized =
|
|
51
|
+
serialized = (
|
|
52
|
+
format_text_report(output)
|
|
53
|
+
if args.format == "text"
|
|
54
|
+
else json.dumps(output, indent=2)
|
|
55
|
+
)
|
|
32
56
|
|
|
33
57
|
if args.out:
|
|
34
58
|
Path(args.out).write_text(serialized, encoding="utf-8")
|
|
@@ -39,6 +63,11 @@ def _lint(args: argparse.Namespace) -> int:
|
|
|
39
63
|
return 1 if total_findings else 0
|
|
40
64
|
|
|
41
65
|
|
|
66
|
+
def _version_text() -> str:
|
|
67
|
+
tools = ", ".join(f"{name} {v}" for name, v in tool_versions().items())
|
|
68
|
+
return f"python-linters {package_version()} ({tools})"
|
|
69
|
+
|
|
70
|
+
|
|
42
71
|
def _reference(args: argparse.Namespace) -> int:
|
|
43
72
|
print(reference_text(args.tool), end="")
|
|
44
73
|
return 0
|
|
@@ -72,6 +101,12 @@ def _check_config(args: argparse.Namespace) -> int:
|
|
|
72
101
|
|
|
73
102
|
def build_parser() -> argparse.ArgumentParser:
|
|
74
103
|
parser = argparse.ArgumentParser(prog="python-linters")
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"--version",
|
|
106
|
+
action="version",
|
|
107
|
+
version=_version_text(),
|
|
108
|
+
help="print this package's version and its bundled ruff/ty/pymodularity versions",
|
|
109
|
+
)
|
|
75
110
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
76
111
|
|
|
77
112
|
lint_parser = subparsers.add_parser(
|
|
@@ -79,7 +114,39 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
79
114
|
)
|
|
80
115
|
lint_parser.add_argument("path", nargs="?", default=".")
|
|
81
116
|
lint_parser.add_argument("--out", default=None)
|
|
82
|
-
lint_parser.
|
|
117
|
+
lint_parser.add_argument(
|
|
118
|
+
"--format",
|
|
119
|
+
choices=["json", "text"],
|
|
120
|
+
default="json",
|
|
121
|
+
help="json (default) or one finding per line, sorted by file then line",
|
|
122
|
+
)
|
|
123
|
+
lint_parser.add_argument(
|
|
124
|
+
"--skip",
|
|
125
|
+
action="append",
|
|
126
|
+
default=[],
|
|
127
|
+
choices=list(TOOL_NAMES),
|
|
128
|
+
help="skip this tool (repeatable); skipping all 4 is an error",
|
|
129
|
+
)
|
|
130
|
+
lint_parser.add_argument(
|
|
131
|
+
"--python",
|
|
132
|
+
dest="ty_python",
|
|
133
|
+
default=None,
|
|
134
|
+
help="forwarded verbatim as ty check's --python",
|
|
135
|
+
)
|
|
136
|
+
lint_parser.add_argument(
|
|
137
|
+
"--extra-search-path",
|
|
138
|
+
dest="ty_extra_search_path",
|
|
139
|
+
action="append",
|
|
140
|
+
default=[],
|
|
141
|
+
help="forwarded verbatim as ty check's --extra-search-path (repeatable)",
|
|
142
|
+
)
|
|
143
|
+
lint_parser.add_argument(
|
|
144
|
+
"--baseline",
|
|
145
|
+
default=None,
|
|
146
|
+
help="git ref: report only findings new since this ref "
|
|
147
|
+
"(identity: file, rule, message — line excluded)",
|
|
148
|
+
)
|
|
149
|
+
lint_parser.set_defaults(func=_lint, parser=lint_parser)
|
|
83
150
|
|
|
84
151
|
reference_parser = subparsers.add_parser(
|
|
85
152
|
"reference", help="print a shipped reference config to stdout"
|
|
@@ -5,6 +5,13 @@ Ported from lint_grouped.py (quality-and-security-tools howtos), minus eslint.
|
|
|
5
5
|
Output structure:
|
|
6
6
|
{
|
|
7
7
|
"project": "/abs/path",
|
|
8
|
+
"tools": {"ruff": "0.17.5", "ty": "0.0.103", "pymodularity": "0.5.0"},
|
|
9
|
+
"skipped": ["pymodularity"],
|
|
10
|
+
"ty-environment": {
|
|
11
|
+
"project_root": "/abs/path",
|
|
12
|
+
"python_environment": "/abs/path/.venv",
|
|
13
|
+
"pythonpath": "/abs/legacy-copy"
|
|
14
|
+
},
|
|
8
15
|
"summary": {
|
|
9
16
|
"ruff": {"total": 100, "rules": 12},
|
|
10
17
|
"ruff-format": {"total": 2, "rules": 1},
|
|
@@ -41,6 +48,10 @@ Output structure:
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
Rules are sorted by count (descending) so the most common issues come first.
|
|
51
|
+
|
|
52
|
+
`ty-environment` is present only when ty itself ran (absent if "ty" is in
|
|
53
|
+
`skipped`); its `pythonpath` key is present only when `PYTHONPATH` was set.
|
|
54
|
+
A skipped tool gets no `summary` entry and no section of its own.
|
|
44
55
|
"""
|
|
45
56
|
|
|
46
57
|
from __future__ import annotations
|
|
@@ -54,6 +65,7 @@ from pathlib import Path
|
|
|
54
65
|
from typing import TYPE_CHECKING
|
|
55
66
|
|
|
56
67
|
from python_linters.tool_errors import ToolFailedError, run_checked
|
|
68
|
+
from python_linters.tool_versions import tool_versions
|
|
57
69
|
|
|
58
70
|
if TYPE_CHECKING:
|
|
59
71
|
from collections.abc import Iterable
|
|
@@ -64,6 +76,24 @@ _TY_SEVERITY_MAP = {"major": "error", "minor": "warning", "info": "info"}
|
|
|
64
76
|
# --> my_package/main_module.py:1:2
|
|
65
77
|
_RUFF_FORMAT_LOCATION_RE = re.compile(r"^ --> (?P<file>.+):(?P<line>\d+):(?P<col>\d+)$")
|
|
66
78
|
|
|
79
|
+
# `ty check -vv`'s DEBUG lines that name the project root it resolved.
|
|
80
|
+
# "Found project at" fires when the root differs from (or has its own
|
|
81
|
+
# `tool.ty` config at) the searched path; "Project without `tool.ty`
|
|
82
|
+
# section" fires when ty found no config and settled on the searched path
|
|
83
|
+
# itself. Either line, found first, names the actual root.
|
|
84
|
+
_TY_FOUND_PROJECT_RE = re.compile(r"Found project at '(?P<root>[^']+)'")
|
|
85
|
+
_TY_PROJECT_WITHOUT_CONFIG_RE = re.compile(
|
|
86
|
+
r"Project without `tool\.ty` section: '(?P<root>[^']+)'"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# `ty check -vv`'s DEBUG line that names the sys.prefix of the Python
|
|
90
|
+
# environment it actually resolved for import resolution — the same line
|
|
91
|
+
# fires whether that environment came from VIRTUAL_ENV, a discovered
|
|
92
|
+
# `.venv`, or an explicit `--python`.
|
|
93
|
+
_TY_SYS_PREFIX_RE = re.compile(
|
|
94
|
+
r"Searching for site-packages directory in sys\.prefix (?P<prefix>\S+)"
|
|
95
|
+
)
|
|
96
|
+
|
|
67
97
|
|
|
68
98
|
@dataclass
|
|
69
99
|
class Location:
|
|
@@ -220,7 +250,49 @@ def run_ruff_format(project_path: Path) -> dict[str, FindingGroup]:
|
|
|
220
250
|
return groups
|
|
221
251
|
|
|
222
252
|
|
|
223
|
-
def
|
|
253
|
+
def _parse_ty_environment(stderr: str) -> dict[str, str]:
|
|
254
|
+
"""Pull ty's resolved project root and Python environment out of its
|
|
255
|
+
`-vv` DEBUG log lines.
|
|
256
|
+
|
|
257
|
+
ty prints no structured ("--output-format json"-style) way to ask for
|
|
258
|
+
this, so the log lines are the only source. Both a project root line
|
|
259
|
+
and a sys.prefix line are expected on every run (see the regex
|
|
260
|
+
docstrings above) — their absence means ty changed its logging, so
|
|
261
|
+
this fails loudly instead of reporting a guessed or stale environment.
|
|
262
|
+
"""
|
|
263
|
+
environment: dict[str, str] = {}
|
|
264
|
+
|
|
265
|
+
root_match = _TY_FOUND_PROJECT_RE.search(
|
|
266
|
+
stderr
|
|
267
|
+
) or _TY_PROJECT_WITHOUT_CONFIG_RE.search(stderr)
|
|
268
|
+
if root_match is None:
|
|
269
|
+
tool = "ty"
|
|
270
|
+
detail = f"-vv output named no project root; ty may have changed its logging:\n{stderr}"
|
|
271
|
+
raise ToolFailedError(tool, detail)
|
|
272
|
+
environment["project_root"] = root_match["root"]
|
|
273
|
+
|
|
274
|
+
prefix_match = _TY_SYS_PREFIX_RE.search(stderr)
|
|
275
|
+
if prefix_match is None:
|
|
276
|
+
tool = "ty"
|
|
277
|
+
detail = (
|
|
278
|
+
"-vv output named no resolved Python environment; ty may have "
|
|
279
|
+
f"changed its logging:\n{stderr}"
|
|
280
|
+
)
|
|
281
|
+
raise ToolFailedError(tool, detail)
|
|
282
|
+
environment["python_environment"] = prefix_match["prefix"]
|
|
283
|
+
|
|
284
|
+
pythonpath = os.environ.get("PYTHONPATH", "")
|
|
285
|
+
if pythonpath != "":
|
|
286
|
+
environment["pythonpath"] = pythonpath
|
|
287
|
+
|
|
288
|
+
return environment
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def run_ty(
|
|
292
|
+
project_path: Path,
|
|
293
|
+
python: str | None = None,
|
|
294
|
+
extra_search_paths: tuple[str, ...] = (),
|
|
295
|
+
) -> tuple[dict[str, FindingGroup], dict[str, str]]:
|
|
224
296
|
# Resolution: `sys.executable -m ty` from the python-linters
|
|
225
297
|
# environment, never a bare `ty` entry point (that one falls through to
|
|
226
298
|
# PATH when the environment has no ty on it) and never a PATH `ty`.
|
|
@@ -247,16 +319,35 @@ def run_ty(project_path: Path) -> dict[str, FindingGroup]:
|
|
|
247
319
|
# VIRTUAL_ENV here makes ty fall through to discovering `.venv` under
|
|
248
320
|
# project_path itself. In dev-dependency mode this is a no-op: there
|
|
249
321
|
# the project's own venv was VIRTUAL_ENV already.
|
|
322
|
+
#
|
|
323
|
+
# `--python`/`--extra-search-path` let a caller override this
|
|
324
|
+
# discovery outright — needed e.g. for a ROS 2 package that resolves
|
|
325
|
+
# its imports from a system interpreter and `/opt/ros/...`, never a
|
|
326
|
+
# `.venv` under the linted path.
|
|
327
|
+
#
|
|
328
|
+
# `-vv`: only source for ty's resolved project root and Python
|
|
329
|
+
# environment (see `_parse_ty_environment`). Its DEBUG lines go to
|
|
330
|
+
# stderr, never stdout, so the `--output-format gitlab` JSON below is
|
|
331
|
+
# unaffected.
|
|
250
332
|
ty_env = {k: v for k, v in os.environ.items() if k != "VIRTUAL_ENV"}
|
|
333
|
+
cmd = [sys.executable, "-m", "ty", "check", "--output-format", "gitlab", "-vv"]
|
|
334
|
+
if python is not None:
|
|
335
|
+
cmd += ["--python", python]
|
|
336
|
+
for search_path in extra_search_paths:
|
|
337
|
+
cmd += ["--extra-search-path", search_path]
|
|
338
|
+
cmd.append(".")
|
|
339
|
+
|
|
251
340
|
result = run_checked(
|
|
252
|
-
|
|
341
|
+
cmd,
|
|
253
342
|
project_path,
|
|
254
343
|
"ty",
|
|
255
344
|
ok_codes=(0, 1),
|
|
256
345
|
env=ty_env,
|
|
257
346
|
)
|
|
347
|
+
ty_environment = _parse_ty_environment(result.stderr)
|
|
348
|
+
|
|
258
349
|
if not result.stdout.strip():
|
|
259
|
-
return {}
|
|
350
|
+
return {}, ty_environment
|
|
260
351
|
|
|
261
352
|
diagnostics = json.loads(result.stdout)
|
|
262
353
|
|
|
@@ -287,7 +378,7 @@ def run_ty(project_path: Path) -> dict[str, FindingGroup]:
|
|
|
287
378
|
)
|
|
288
379
|
yield rule, None, severity, location
|
|
289
380
|
|
|
290
|
-
return _group_findings(findings())
|
|
381
|
+
return _group_findings(findings()), ty_environment
|
|
291
382
|
|
|
292
383
|
|
|
293
384
|
def run_pymodularity(project_path: Path) -> dict[str, FindingGroup]:
|
|
@@ -333,20 +424,38 @@ def run_pymodularity(project_path: Path) -> dict[str, FindingGroup]:
|
|
|
333
424
|
return _group_findings(findings())
|
|
334
425
|
|
|
335
426
|
|
|
336
|
-
|
|
427
|
+
# The 4 tools `lint` can run, in report order. Shared with the CLI's
|
|
428
|
+
# `--skip` choices so the two never drift apart.
|
|
429
|
+
TOOL_NAMES = ("ruff", "ruff-format", "ty", "pymodularity")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def build_report(
|
|
433
|
+
project_path: Path,
|
|
434
|
+
skip: frozenset[str] = frozenset(),
|
|
435
|
+
ty_python: str | None = None,
|
|
436
|
+
ty_extra_search_paths: tuple[str, ...] = (),
|
|
437
|
+
) -> tuple[dict[str, object], int]:
|
|
337
438
|
"""Return the JSON-ready report and the total finding count across all tools."""
|
|
338
439
|
summary: dict[str, object] = {}
|
|
339
440
|
sections: dict[str, object] = {}
|
|
340
441
|
grand_total = 0
|
|
442
|
+
ty_environment: dict[str, str] = {}
|
|
443
|
+
|
|
444
|
+
for tool in TOOL_NAMES:
|
|
445
|
+
if tool in skip:
|
|
446
|
+
continue
|
|
341
447
|
|
|
342
|
-
for tool, run in (
|
|
343
|
-
("ruff", run_ruff),
|
|
344
|
-
("ruff-format", run_ruff_format),
|
|
345
|
-
("ty", run_ty),
|
|
346
|
-
("pymodularity", run_pymodularity),
|
|
347
|
-
):
|
|
348
448
|
print(f"Running {tool} on {project_path} ...", file=sys.stderr)
|
|
349
|
-
|
|
449
|
+
if tool == "ruff":
|
|
450
|
+
violations = run_ruff(project_path)
|
|
451
|
+
elif tool == "ruff-format":
|
|
452
|
+
violations = run_ruff_format(project_path)
|
|
453
|
+
elif tool == "ty":
|
|
454
|
+
violations, ty_environment = run_ty(
|
|
455
|
+
project_path, python=ty_python, extra_search_paths=ty_extra_search_paths
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
violations = run_pymodularity(project_path)
|
|
350
459
|
total = sum(group.count for group in violations.values())
|
|
351
460
|
print(f" {total} violations across {len(violations)} rules", file=sys.stderr)
|
|
352
461
|
|
|
@@ -354,5 +463,13 @@ def build_report(project_path: Path) -> tuple[dict[str, object], int]:
|
|
|
354
463
|
sections[tool] = {rule: group.to_dict() for rule, group in violations.items()}
|
|
355
464
|
grand_total += total
|
|
356
465
|
|
|
357
|
-
report
|
|
466
|
+
report: dict[str, object] = {
|
|
467
|
+
"project": str(project_path),
|
|
468
|
+
"tools": tool_versions(),
|
|
469
|
+
"skipped": sorted(skip),
|
|
470
|
+
}
|
|
471
|
+
if ty_environment:
|
|
472
|
+
report["ty-environment"] = ty_environment
|
|
473
|
+
report["summary"] = summary
|
|
474
|
+
report.update(sections)
|
|
358
475
|
return report, grand_total
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Render a `build_report` result as one line per finding, for a terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from python_linters.lint_report import TOOL_NAMES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_text_report(report: dict[str, object]) -> str:
|
|
9
|
+
"""One line per finding, sorted by file then line, with a totals line last.
|
|
10
|
+
|
|
11
|
+
`file:line:col tool/rule message`, matching how ruff and ty print their
|
|
12
|
+
own findings — unlike the JSON report, this is for a human reading a
|
|
13
|
+
terminal, not a script.
|
|
14
|
+
"""
|
|
15
|
+
rows: list[tuple[str, int, int, str, str, str]] = []
|
|
16
|
+
for tool in TOOL_NAMES:
|
|
17
|
+
section = report.get(tool)
|
|
18
|
+
if not isinstance(section, dict):
|
|
19
|
+
continue
|
|
20
|
+
for rule, group in section.items():
|
|
21
|
+
rows.extend(
|
|
22
|
+
(
|
|
23
|
+
location["file"],
|
|
24
|
+
location["line"],
|
|
25
|
+
location["col"],
|
|
26
|
+
tool,
|
|
27
|
+
rule,
|
|
28
|
+
location["message"],
|
|
29
|
+
)
|
|
30
|
+
for location in group["locations"]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
rows.sort(key=lambda row: (row[0], row[1]))
|
|
34
|
+
|
|
35
|
+
lines = [
|
|
36
|
+
f"{file}:{line}:{col} {tool}/{rule} {message}"
|
|
37
|
+
for file, line, col, tool, rule, message in rows
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
summary_obj = report.get("summary", {})
|
|
41
|
+
summary: dict[str, dict[str, int]] = (
|
|
42
|
+
summary_obj if isinstance(summary_obj, dict) else {}
|
|
43
|
+
)
|
|
44
|
+
totals = " ".join(f"{tool}={data['total']}" for tool, data in summary.items())
|
|
45
|
+
grand_total = sum(data["total"] for data in summary.values())
|
|
46
|
+
totals_line = f"{totals} total={grand_total}" if totals else f"total={grand_total}"
|
|
47
|
+
|
|
48
|
+
baseline = report.get("baseline")
|
|
49
|
+
if isinstance(baseline, dict):
|
|
50
|
+
totals_line += f" baseline={baseline['ref']}"
|
|
51
|
+
|
|
52
|
+
lines.append(totals_line)
|
|
53
|
+
|
|
54
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Resolved versions of this package and the tools it bundles.
|
|
2
|
+
|
|
3
|
+
Read from `importlib.metadata`, never from a subprocess call: the versions
|
|
4
|
+
are pinned in `pyproject.toml` and installed into the same environment this
|
|
5
|
+
package itself runs in, so the installed-distribution metadata already
|
|
6
|
+
names exactly what `sys.executable -m <tool>` will run.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
TOOL_DISTRIBUTIONS = ("ruff", "ty", "pymodularity")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def package_version() -> str:
|
|
17
|
+
return version("python-linters")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def tool_versions() -> dict[str, str]:
|
|
21
|
+
"""Bundled ruff/ty/pymodularity versions, keyed by distribution name."""
|
|
22
|
+
return {name: version(name) for name in TOOL_DISTRIBUTIONS}
|
python_linters-0.2.2/PKG-INFO
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-linters
|
|
3
|
-
Version: 0.2.2
|
|
4
|
-
Summary: Run ruff, ty and pymodularity on a Python project and print one JSON report grouped by rule.
|
|
5
|
-
Author: Tilo Himmelsbach
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Dist: ruff==0.17.5
|
|
8
|
-
Requires-Dist: ty==0.0.103
|
|
9
|
-
Requires-Dist: pymodularity==0.5.0
|
|
10
|
-
Requires-Python: >=3.11
|
|
11
|
-
Description-Content-Type: text/markdown
|
|
12
|
-
|
|
13
|
-
# python-linters
|
|
14
|
-
|
|
15
|
-
Run ruff, ty and pymodularity on a Python project and print one JSON report
|
|
16
|
-
grouped by rule, for agents and CI to read.
|
|
17
|
-
|
|
18
|
-
## Install
|
|
19
|
-
|
|
20
|
-
As a dev dependency of the project you want to lint:
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
uv add --dev python-linters
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Commands
|
|
27
|
-
|
|
28
|
-
- `uv run python-linters lint [path] [--out FILE]` — runs ruff, `ruff
|
|
29
|
-
format --check`, ty and pymodularity (default path: `.`) and prints the
|
|
30
|
-
merged JSON report to stdout, or writes it to `FILE` with `--out`.
|
|
31
|
-
- `uv run python-linters reference ruff|ty` — prints this package's shipped
|
|
32
|
-
reference config for the given tool to stdout. The package never writes
|
|
33
|
-
it into your project — copying it into `ruff.toml`/`ty.toml` (or
|
|
34
|
-
`pyproject.toml`) stays with you.
|
|
35
|
-
- `uv run python-linters check-config ruff|ty <path/to/config>` — compares
|
|
36
|
-
your copy against the pinned tool's current rule set: rules the tool
|
|
37
|
-
knows that your config omits (untriaged), and rules your config names
|
|
38
|
-
that the tool no longer knows (stale).
|
|
39
|
-
|
|
40
|
-
## Exit codes
|
|
41
|
-
|
|
42
|
-
- `lint`: 0 clean, 1 findings, 2 any tool failure (named on stderr).
|
|
43
|
-
- `check-config`: 0 up to date, 1 untriaged or stale rules found, 2 tool
|
|
44
|
-
failure.
|
python_linters-0.2.2/README.md
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# python-linters
|
|
2
|
-
|
|
3
|
-
Run ruff, ty and pymodularity on a Python project and print one JSON report
|
|
4
|
-
grouped by rule, for agents and CI to read.
|
|
5
|
-
|
|
6
|
-
## Install
|
|
7
|
-
|
|
8
|
-
As a dev dependency of the project you want to lint:
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
uv add --dev python-linters
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Commands
|
|
15
|
-
|
|
16
|
-
- `uv run python-linters lint [path] [--out FILE]` — runs ruff, `ruff
|
|
17
|
-
format --check`, ty and pymodularity (default path: `.`) and prints the
|
|
18
|
-
merged JSON report to stdout, or writes it to `FILE` with `--out`.
|
|
19
|
-
- `uv run python-linters reference ruff|ty` — prints this package's shipped
|
|
20
|
-
reference config for the given tool to stdout. The package never writes
|
|
21
|
-
it into your project — copying it into `ruff.toml`/`ty.toml` (or
|
|
22
|
-
`pyproject.toml`) stays with you.
|
|
23
|
-
- `uv run python-linters check-config ruff|ty <path/to/config>` — compares
|
|
24
|
-
your copy against the pinned tool's current rule set: rules the tool
|
|
25
|
-
knows that your config omits (untriaged), and rules your config names
|
|
26
|
-
that the tool no longer knows (stale).
|
|
27
|
-
|
|
28
|
-
## Exit codes
|
|
29
|
-
|
|
30
|
-
- `lint`: 0 clean, 1 findings, 2 any tool failure (named on stderr).
|
|
31
|
-
- `check-config`: 0 up to date, 1 untriaged or stale rules found, 2 tool
|
|
32
|
-
failure.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/references/ruff.reference.toml
RENAMED
|
File without changes
|
{python_linters-0.2.2 → python_linters-0.2.3}/src/python_linters/references/ty.reference.toml
RENAMED
|
File without changes
|
|
File without changes
|