python-code-quality 0.3.0__py3-none-any.whl → 0.4.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.
- py_cq/api.py +9 -3
- py_cq/cli.py +3 -3
- py_cq/config/config.toml +36 -15
- py_cq/execution_engine.py +34 -8
- py_cq/localtypes.py +1 -4
- py_cq/parsers/common.py +1 -3
- py_cq/parsers/interrogateparser.py +11 -3
- py_cq/parsers/kingfisherparser.py +73 -0
- py_cq/parsers/ruffformatparser.py +62 -0
- py_cq/parsers/uvauditparser.py +88 -0
- py_cq/parsers/vultureparser.py +18 -3
- py_cq/runners/pytest_serial.py +27 -0
- py_cq/tool_registry.py +0 -1
- {python_code_quality-0.3.0.dist-info → python_code_quality-0.4.0.dist-info}/METADATA +66 -34
- {python_code_quality-0.3.0.dist-info → python_code_quality-0.4.0.dist-info}/RECORD +17 -13
- {python_code_quality-0.3.0.dist-info → python_code_quality-0.4.0.dist-info}/WHEEL +1 -1
- {python_code_quality-0.3.0.dist-info → python_code_quality-0.4.0.dist-info}/entry_points.txt +0 -0
py_cq/api.py
CHANGED
|
@@ -148,7 +148,9 @@ class CQ:
|
|
|
148
148
|
if clear_cache:
|
|
149
149
|
_cache.clear()
|
|
150
150
|
|
|
151
|
-
def raw(
|
|
151
|
+
def raw(
|
|
152
|
+
self, *, early_exit: bool = False, order: str = "severity"
|
|
153
|
+
) -> list[ToolResult]:
|
|
152
154
|
"""Run all tools and return parsed results before aggregation."""
|
|
153
155
|
return run_tools(
|
|
154
156
|
self._registry.values(),
|
|
@@ -218,7 +220,7 @@ class CQ:
|
|
|
218
220
|
)
|
|
219
221
|
if fp.tool not in tool_registry:
|
|
220
222
|
raise ValueError(f"Unknown tool: {fp.tool!r}")
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
if fp.code and fp.path:
|
|
223
225
|
# Specific issue: target only the affected file for a fast re-check
|
|
224
226
|
file_path = Path(fp.path)
|
|
@@ -242,7 +244,11 @@ class CQ:
|
|
|
242
244
|
|
|
243
245
|
only_registry = {fp.tool: tool_registry[fp.tool]}
|
|
244
246
|
tool_results = run_tools(
|
|
245
|
-
only_registry.values(),
|
|
247
|
+
only_registry.values(),
|
|
248
|
+
target,
|
|
249
|
+
max_workers=1,
|
|
250
|
+
early_exit=False,
|
|
251
|
+
excludes=[],
|
|
246
252
|
project_root=str(self.path),
|
|
247
253
|
)
|
|
248
254
|
|
py_cq/cli.py
CHANGED
|
@@ -107,7 +107,7 @@ def callback(
|
|
|
107
107
|
help="Show version and dependencies",
|
|
108
108
|
),
|
|
109
109
|
) -> None:
|
|
110
|
-
"""Feed the results from
|
|
110
|
+
"""Feed the results from 13 code quality tools to an LLM. Try: cq check . -o llm"""
|
|
111
111
|
import sys
|
|
112
112
|
|
|
113
113
|
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
@@ -124,7 +124,7 @@ def check(
|
|
|
124
124
|
OutputMode.TABLE,
|
|
125
125
|
"--output",
|
|
126
126
|
"-o",
|
|
127
|
-
help="Output mode: table (default), score, json, llm",
|
|
127
|
+
help="Output mode: table (default), score, json, raw, llm, llm-json",
|
|
128
128
|
),
|
|
129
129
|
log_level: str = typer.Option(
|
|
130
130
|
"CRITICAL",
|
|
@@ -172,7 +172,7 @@ def check(
|
|
|
172
172
|
help="Issue selection order for -o llm/llm-json: severity (most severe first) or phase (first non-clean phase, in dependency order)",
|
|
173
173
|
),
|
|
174
174
|
):
|
|
175
|
-
"""Feed the results from
|
|
175
|
+
"""Feed the results from 13 code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
176
176
|
path_obj = Path(path)
|
|
177
177
|
if not path_obj.exists():
|
|
178
178
|
raise typer.BadParameter(f"Path does not exist: {path}")
|
py_cq/config/config.toml
CHANGED
|
@@ -7,8 +7,8 @@ error_threshold = 0.9999
|
|
|
7
7
|
cache_invariant = "ast"
|
|
8
8
|
|
|
9
9
|
[python.ruff]
|
|
10
|
-
command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --exclude .claude{exclude}"
|
|
11
|
-
exclude_format = " --exclude {path}"
|
|
10
|
+
command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude{exclude}"
|
|
11
|
+
exclude_format = " --extend-exclude {path}"
|
|
12
12
|
parser = "RuffParser"
|
|
13
13
|
order = 2
|
|
14
14
|
warning_threshold = 0.9999
|
|
@@ -33,11 +33,27 @@ order = 4
|
|
|
33
33
|
warning_threshold = 0.9999
|
|
34
34
|
error_threshold = 0.8
|
|
35
35
|
|
|
36
|
+
[python.kingfisher]
|
|
37
|
+
command = "kingfisher scan \"{context_path}\" --no-validate --no-update-check --git-history none --no-extract-archives --redact -q -f json --exclude .venv --exclude .git --exclude .hypothesis --exclude dist --exclude \"*_cache\" --exclude __pycache__ --exclude .claude{exclude}"
|
|
38
|
+
exclude_format = " --exclude {path}"
|
|
39
|
+
parser = "KingfisherParser"
|
|
40
|
+
order = 5
|
|
41
|
+
warning_threshold = 0.9999
|
|
42
|
+
error_threshold = 0.9999
|
|
43
|
+
|
|
44
|
+
[python.uv-audit]
|
|
45
|
+
command = "uv audit --project \"{abs_context_path}\" --frozen --output-format json"
|
|
46
|
+
parser = "UvAuditParser"
|
|
47
|
+
order = 6
|
|
48
|
+
warning_threshold = 0.9999
|
|
49
|
+
error_threshold = 0.8
|
|
50
|
+
skip_for_file = true
|
|
51
|
+
|
|
36
52
|
[python.pytest]
|
|
37
53
|
command = "{python} -m pytest -vv \"{context_path}\" --ignore .claude{exclude}"
|
|
38
54
|
exclude_format = " --ignore {path}"
|
|
39
55
|
parser = "PytestParser"
|
|
40
|
-
order =
|
|
56
|
+
order = 7
|
|
41
57
|
warning_threshold = 1.0
|
|
42
58
|
error_threshold = 1.0
|
|
43
59
|
run_in_target_env = true
|
|
@@ -46,10 +62,10 @@ skip_for_file = true
|
|
|
46
62
|
cache_invariant = "ast"
|
|
47
63
|
|
|
48
64
|
[python.coverage]
|
|
49
|
-
command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py
|
|
65
|
+
command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py,*/py_cq/runners/* \"{pytest_serial}\" \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --omit=*/tests/*,*/test_*.py,*/py_cq/runners/*"
|
|
50
66
|
exclude_format = " --ignore {path}"
|
|
51
67
|
parser = "CoverageParser"
|
|
52
|
-
order =
|
|
68
|
+
order = 8
|
|
53
69
|
warning_threshold = 0.9
|
|
54
70
|
error_threshold = 0.5
|
|
55
71
|
run_in_target_env = true
|
|
@@ -59,19 +75,19 @@ skip_if = "pytest"
|
|
|
59
75
|
cache_invariant = "ast"
|
|
60
76
|
|
|
61
77
|
[python.vulture]
|
|
62
|
-
command = "{python} -m vulture \"{context_path}\" --min-confidence
|
|
78
|
+
command = "{python} -m vulture \"{context_path}\" --min-confidence 60 --ignore-decorators \"@*.command,@*.callback,@*.route,@*.get,@*.post,@*.put,@*.delete,@*.patch,@*fixture,@*.task,@*hookimpl,@*validator,@*.errorhandler\" --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
|
|
63
79
|
exclude_format = ",{path}"
|
|
64
80
|
parser = "VultureParser"
|
|
65
|
-
order =
|
|
81
|
+
order = 9
|
|
66
82
|
warning_threshold = 0.9999
|
|
67
83
|
error_threshold = 0.8
|
|
68
84
|
cache_invariant = "ast"
|
|
69
85
|
|
|
70
86
|
[python.interrogate]
|
|
71
|
-
command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
|
|
87
|
+
command = "{python} -m interrogate \"{context_path}\" -e .claude -e \"{context_path}/tests\"{exclude} -v --fail-under 0"
|
|
72
88
|
exclude_format = " -e {path}"
|
|
73
89
|
parser = "InterrogateParser"
|
|
74
|
-
order =
|
|
90
|
+
order = 10
|
|
75
91
|
warning_threshold = 0.8
|
|
76
92
|
error_threshold = 0.5
|
|
77
93
|
|
|
@@ -81,23 +97,28 @@ skip_empty_init = true
|
|
|
81
97
|
[python.radon-cc]
|
|
82
98
|
command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
|
|
83
99
|
parser = "ComplexityParser"
|
|
84
|
-
order =
|
|
85
|
-
gate_strict = false
|
|
100
|
+
order = 11
|
|
86
101
|
warning_threshold = 0.6
|
|
87
102
|
error_threshold = 0.4
|
|
88
103
|
|
|
89
104
|
[python.radon-mi]
|
|
90
105
|
command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
|
|
91
106
|
parser = "MaintainabilityParser"
|
|
92
|
-
order =
|
|
93
|
-
gate_strict = false
|
|
107
|
+
order = 12
|
|
94
108
|
warning_threshold = 0.6
|
|
95
109
|
error_threshold = 0.4
|
|
96
110
|
|
|
97
111
|
[python.radon-hal]
|
|
98
112
|
command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
|
|
99
113
|
parser = "HalsteadParser"
|
|
100
|
-
order =
|
|
101
|
-
gate_strict = false
|
|
114
|
+
order = 13
|
|
102
115
|
warning_threshold = 0.35
|
|
103
116
|
error_threshold = 0.3
|
|
117
|
+
|
|
118
|
+
[python.ruff-format]
|
|
119
|
+
command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
|
|
120
|
+
exclude_format = " --extend-exclude {path}"
|
|
121
|
+
parser = "RuffFormatParser"
|
|
122
|
+
order = 14
|
|
123
|
+
warning_threshold = 0.9999
|
|
124
|
+
error_threshold = 0.5
|
py_cq/execution_engine.py
CHANGED
|
@@ -38,6 +38,9 @@ _cache = Cache(
|
|
|
38
38
|
Path.home() / ".cache" / "cq", size_limit=100 * 1024 * 1024, disk=JSONDisk
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
+
# Script that runs pytest in one process under coverage; see its docstring.
|
|
42
|
+
_PYTEST_SERIAL = str(Path(__file__).parent / "runners" / "pytest_serial.py")
|
|
43
|
+
|
|
41
44
|
|
|
42
45
|
def _find_project_root(path: Path) -> Path | None:
|
|
43
46
|
"""Walk up from path to find the nearest directory containing pyproject.toml."""
|
|
@@ -271,6 +274,7 @@ def run_tool(
|
|
|
271
274
|
scan_targets=scan_targets,
|
|
272
275
|
python=python,
|
|
273
276
|
exclude=exclude,
|
|
277
|
+
pytest_serial=_PYTEST_SERIAL,
|
|
274
278
|
)
|
|
275
279
|
if precomputed_hash is not None:
|
|
276
280
|
context_hash = precomputed_hash
|
|
@@ -328,7 +332,13 @@ def run_tool(
|
|
|
328
332
|
timestamp=timestamp,
|
|
329
333
|
project_path=project_dir,
|
|
330
334
|
)
|
|
331
|
-
|
|
335
|
+
if return_code != 0 and not stdout.strip():
|
|
336
|
+
# Failed without a report: the tool is missing from PATH or offline.
|
|
337
|
+
# Caching this would hide real results until the files change.
|
|
338
|
+
return raw_result
|
|
339
|
+
_cache.set(
|
|
340
|
+
cache_key, raw_result.to_dict(), expire=5 * 24 * 60 * 60, tag=project_tag
|
|
341
|
+
)
|
|
332
342
|
return raw_result
|
|
333
343
|
|
|
334
344
|
|
|
@@ -386,7 +396,9 @@ def run_tools(
|
|
|
386
396
|
t_hash0 = time.perf_counter()
|
|
387
397
|
root = project_root or str(Path(path).resolve())
|
|
388
398
|
shared_hash = get_context_hash(root)
|
|
389
|
-
log.debug(
|
|
399
|
+
log.debug(
|
|
400
|
+
f"context_hash: {(time.perf_counter() - t_hash0) * 1000:.1f}ms {shared_hash}"
|
|
401
|
+
)
|
|
390
402
|
|
|
391
403
|
# Cache keys are content-based, so a changed project naturally produces new
|
|
392
404
|
# keys; stale entries age out via TTL + size_limit (no blanket eviction).
|
|
@@ -398,7 +410,9 @@ def run_tools(
|
|
|
398
410
|
if any(tc.cache_invariant == "ast" for tc in tool_configs):
|
|
399
411
|
t_norm0 = time.perf_counter()
|
|
400
412
|
norm_hash = get_context_hash(root, normalize=True)
|
|
401
|
-
log.debug(
|
|
413
|
+
log.debug(
|
|
414
|
+
f"context_hash(ast): {(time.perf_counter() - t_norm0) * 1000:.1f}ms {norm_hash}"
|
|
415
|
+
)
|
|
402
416
|
|
|
403
417
|
def _hash_for(tool_config: ToolConfig) -> str:
|
|
404
418
|
return norm_hash if tool_config.cache_invariant == "ast" else shared_hash
|
|
@@ -410,7 +424,14 @@ def run_tools(
|
|
|
410
424
|
# Only pass cancel_event when present so patched run_tool stubs in tests
|
|
411
425
|
# (which don't declare the kwarg) keep working for non-cancellable tools.
|
|
412
426
|
extra = {"cancel_event": cancel_event} if cancel_event is not None else {}
|
|
413
|
-
raw_result = run_tool(
|
|
427
|
+
raw_result = run_tool(
|
|
428
|
+
tool_config,
|
|
429
|
+
path,
|
|
430
|
+
excludes,
|
|
431
|
+
precomputed_hash=_hash_for(tool_config),
|
|
432
|
+
project_tag=root,
|
|
433
|
+
**extra,
|
|
434
|
+
)
|
|
414
435
|
tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
|
|
415
436
|
tr.duration_s = time.perf_counter() - t0
|
|
416
437
|
return tool_config.order, tr
|
|
@@ -423,7 +444,9 @@ def run_tools(
|
|
|
423
444
|
try:
|
|
424
445
|
prioritized.append(_run_and_parse(tool_config))
|
|
425
446
|
except Exception as exc: # noqa: BLE001 - a crashing tool must not abort the run
|
|
426
|
-
log.error(
|
|
447
|
+
log.error(
|
|
448
|
+
f"{tool_config.name} generated an exception: {exc} {exc.__traceback__}"
|
|
449
|
+
)
|
|
427
450
|
n_skipped = n_total - i - 1
|
|
428
451
|
if n_skipped:
|
|
429
452
|
remaining = ", ".join(tc.name for tc in sorted_configs[i + 1 :])
|
|
@@ -477,7 +500,10 @@ def run_tools(
|
|
|
477
500
|
_, dep_tr = dep_future.result()
|
|
478
501
|
except Exception: # noqa: BLE001 - a crashed dep cancels nothing
|
|
479
502
|
return
|
|
480
|
-
if
|
|
503
|
+
if (
|
|
504
|
+
dep_tr.metrics
|
|
505
|
+
and min(dep_tr.metrics.values()) < dep_config.error_threshold
|
|
506
|
+
):
|
|
481
507
|
log.debug(f"{name}: cancelling because {dep_config.name} failed")
|
|
482
508
|
ev.set()
|
|
483
509
|
|
|
@@ -485,8 +511,8 @@ def run_tools(
|
|
|
485
511
|
if tc.name in cancel_events:
|
|
486
512
|
dep_config = configs_by_name[tc.skip_if]
|
|
487
513
|
name_to_future[tc.skip_if].add_done_callback(
|
|
488
|
-
lambda fut, ev=cancel_events[tc.name], dc=dep_config, nm=tc.name:
|
|
489
|
-
fut, ev, dc, nm
|
|
514
|
+
lambda fut, ev=cancel_events[tc.name], dc=dep_config, nm=tc.name: (
|
|
515
|
+
_cancel_on_dep_failure(fut, ev, dc, nm)
|
|
490
516
|
)
|
|
491
517
|
)
|
|
492
518
|
for future in as_completed(future_to_tool):
|
py_cq/localtypes.py
CHANGED
|
@@ -60,11 +60,8 @@ class ToolConfig:
|
|
|
60
60
|
default_factory=list
|
|
61
61
|
) # Top-level dir/file names to omit from {scan_targets}
|
|
62
62
|
skip_for_file: bool = False # If True, skip when context_path is a single file
|
|
63
|
-
gate_strict: bool = True # If False, small regressions above warning_threshold are accepted
|
|
64
63
|
skip_if: str = "" # Tool name; if that tool's score is below its error_threshold, skip this tool
|
|
65
|
-
cache_invariant: str =
|
|
66
|
-
"" # "ast": key the cache on the docstring-stripped AST, ignoring docstrings/comments/format
|
|
67
|
-
)
|
|
64
|
+
cache_invariant: str = "" # "ast": key the cache on the docstring-stripped AST, ignoring docstrings/comments/format
|
|
68
65
|
|
|
69
66
|
|
|
70
67
|
@dataclass
|
py_cq/parsers/common.py
CHANGED
|
@@ -47,9 +47,7 @@ def format_source_context(
|
|
|
47
47
|
error_offset = line - context_start # 0-based index of the error line in raw_lines
|
|
48
48
|
collected = []
|
|
49
49
|
for i, rline in enumerate(raw_lines):
|
|
50
|
-
if i > error_offset and (
|
|
51
|
-
rline.startswith(("def ", "async def ", "class "))
|
|
52
|
-
):
|
|
50
|
+
if i > error_offset and (rline.startswith(("def ", "async def ", "class "))):
|
|
53
51
|
break
|
|
54
52
|
collected.append(f"{context_start + i}: {rline}")
|
|
55
53
|
src = "\n".join(collected)
|
|
@@ -32,7 +32,10 @@ def _format_missing_docstring(file_str: str, line: int, code: str, message: str)
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
if code == "D100":
|
|
35
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
base
|
|
37
|
+
+ "\n\nInsert a module-level docstring as the very first statement in the file."
|
|
38
|
+
)
|
|
36
39
|
|
|
37
40
|
try:
|
|
38
41
|
tree = get_source(file_str).tree
|
|
@@ -74,6 +77,7 @@ def _format_missing_docstring(file_str: str, line: int, code: str, message: str)
|
|
|
74
77
|
|
|
75
78
|
return base + "\n\nInsert a docstring as the first statement in the body."
|
|
76
79
|
|
|
80
|
+
|
|
77
81
|
_ROW_RE = re.compile(
|
|
78
82
|
r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+\d+\s+\|\s+(\d+(?:\.\d+)?)%\s*\|"
|
|
79
83
|
)
|
|
@@ -85,7 +89,9 @@ def _load_interrogate_cfg(context_path: str) -> dict:
|
|
|
85
89
|
"""Read [tool.interrogate] from the nearest pyproject.toml."""
|
|
86
90
|
p = Path(context_path).resolve()
|
|
87
91
|
for candidate in [p, *p.parents]:
|
|
88
|
-
pyproject = (
|
|
92
|
+
pyproject = (
|
|
93
|
+
candidate if candidate.is_dir() else candidate.parent
|
|
94
|
+
) / "pyproject.toml"
|
|
89
95
|
if pyproject.exists():
|
|
90
96
|
try:
|
|
91
97
|
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
@@ -109,7 +115,9 @@ def _skip_node(name: str, cfg: dict) -> bool:
|
|
|
109
115
|
return bool(is_semiprivate and cfg.get("ignore-semiprivate"))
|
|
110
116
|
|
|
111
117
|
|
|
112
|
-
def _missing_docstrings(
|
|
118
|
+
def _missing_docstrings(
|
|
119
|
+
file_path: Path, cfg: dict | None = None
|
|
120
|
+
) -> list[tuple[int, str, str]]:
|
|
113
121
|
"""Return (line, kind, source_line) for each node missing a docstring.
|
|
114
122
|
|
|
115
123
|
kind is 'module', 'def', or 'class'. source_line is the verbatim text of
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Parses output from the Kingfisher secret scanner into a standardized ToolResult.
|
|
2
|
+
|
|
3
|
+
Kingfisher (``uv tool install kingfisher-bin``) is invoked with ``-f json`` and
|
|
4
|
+
``--redact``, so the report on stdout never contains the secret itself, only a
|
|
5
|
+
hash. ``--no-validate`` keeps it from sending findings to provider APIs.
|
|
6
|
+
|
|
7
|
+
Each finding is listed against its file. The count becomes a logistic-variant
|
|
8
|
+
score stored under the ``secrets`` metric key.
|
|
9
|
+
|
|
10
|
+
When no report can be read (``kingfisher`` not on PATH) the tool reports no
|
|
11
|
+
metric instead of a failing one.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
18
|
+
from py_cq.parsers.common import extract_first_issue, score_logistic_variant
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger("cq")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KingfisherParser(AbstractParser):
|
|
24
|
+
"""Parses raw JSON output from ``kingfisher scan -f json`` into a ToolResult."""
|
|
25
|
+
|
|
26
|
+
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
27
|
+
"""Parses the raw Kingfisher JSON report into a ToolResult."""
|
|
28
|
+
try:
|
|
29
|
+
findings = json.loads(raw_result.stdout)["findings"]
|
|
30
|
+
except (ValueError, KeyError, TypeError):
|
|
31
|
+
log.warning(
|
|
32
|
+
"kingfisher produced no report (return_code=%s), skipping. "
|
|
33
|
+
"Install it with `uv tool install kingfisher-bin`.",
|
|
34
|
+
raw_result.return_code,
|
|
35
|
+
)
|
|
36
|
+
return ToolResult(raw=raw_result)
|
|
37
|
+
|
|
38
|
+
files: dict[str, list] = {}
|
|
39
|
+
for entry in findings:
|
|
40
|
+
rule = entry.get("rule") or {}
|
|
41
|
+
finding = entry.get("finding") or {}
|
|
42
|
+
path = str(finding.get("path", "")).replace("\\", "/")
|
|
43
|
+
files.setdefault(path, []).append(
|
|
44
|
+
{
|
|
45
|
+
"line": finding.get("line", 0),
|
|
46
|
+
"code": rule.get("id", ""),
|
|
47
|
+
"confidence": finding.get("confidence", ""),
|
|
48
|
+
"message": rule.get("description", ""),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
count = sum(len(issues) for issues in files.values())
|
|
53
|
+
score = score_logistic_variant(count, scale_factor=5)
|
|
54
|
+
return ToolResult(raw=raw_result, metrics={"secrets": score}, details=files)
|
|
55
|
+
|
|
56
|
+
def format_llm_message(
|
|
57
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Formats the first secret finding into an LLM-friendly message."""
|
|
60
|
+
result = extract_first_issue(tr.details)
|
|
61
|
+
if result is None:
|
|
62
|
+
return "kingfisher reported secrets (no details available)"
|
|
63
|
+
file, issue = result
|
|
64
|
+
line = issue.get("line", "?")
|
|
65
|
+
# No source excerpt: it would put the plaintext secret into the LLM prompt.
|
|
66
|
+
return (
|
|
67
|
+
f"{file}:{line} - {issue.get('code', '')}: "
|
|
68
|
+
f"[{issue.get('confidence', '')} confidence] {issue.get('message', '')}\n\n"
|
|
69
|
+
"Move the secret out of the source (environment variable or an untracked "
|
|
70
|
+
"file) and rotate it if it was ever committed. Only if it is an intentional "
|
|
71
|
+
"test fixture or a false positive, add a `kingfisher:ignore` comment on "
|
|
72
|
+
"that line."
|
|
73
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Parses output from ruff format --check into a standardized ToolResult.
|
|
2
|
+
|
|
3
|
+
The output is one line per unformatted file. ruff >= 0.16 honours
|
|
4
|
+
``--output-format concise``::
|
|
5
|
+
|
|
6
|
+
<file>:<line>:<col>: unformatted: File would be reformatted
|
|
7
|
+
|
|
8
|
+
while older versions ignore it (it needs preview mode) and print::
|
|
9
|
+
|
|
10
|
+
Would reformat: <file>
|
|
11
|
+
|
|
12
|
+
Either is followed by a summary line such as
|
|
13
|
+
``2 files would be reformatted, 30 files already formatted`` or
|
|
14
|
+
``32 files already formatted``.
|
|
15
|
+
|
|
16
|
+
The score is the fraction of checked files that are already formatted,
|
|
17
|
+
taken from the summary so it stays correct if the per-file format changes."""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
22
|
+
from py_cq.parsers.common import extract_first_issue
|
|
23
|
+
|
|
24
|
+
_FILE_RE = re.compile(r"^(?:Would reformat: (.+)|(.+):\d+:\d+: unformatted: .*)$")
|
|
25
|
+
_FORMATTED_RE = re.compile(r"(\d+) files? already formatted")
|
|
26
|
+
_UNFORMATTED_RE = re.compile(r"(\d+) files? would be reformatted")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RuffFormatParser(AbstractParser):
|
|
30
|
+
"""Parses ``ruff format --check --output-format concise`` output into a ToolResult."""
|
|
31
|
+
|
|
32
|
+
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
33
|
+
"""Collect unformatted files and score the fraction already formatted."""
|
|
34
|
+
stdout = raw_result.stdout or ""
|
|
35
|
+
# One issue per file, with no line: the line ruff reports is the first
|
|
36
|
+
# diff hunk, which would make the fingerprint unstable across edits.
|
|
37
|
+
files: dict[str, list] = {}
|
|
38
|
+
for line in stdout.splitlines():
|
|
39
|
+
m = _FILE_RE.match(line)
|
|
40
|
+
if m:
|
|
41
|
+
files[(m.group(1) or m.group(2)).replace("\\", "/")] = [{}]
|
|
42
|
+
m = _FORMATTED_RE.search(stdout)
|
|
43
|
+
formatted = int(m.group(1)) if m else 0
|
|
44
|
+
m = _UNFORMATTED_RE.search(stdout)
|
|
45
|
+
unformatted = max(int(m.group(1)) if m else 0, len(files))
|
|
46
|
+
total = formatted + unformatted
|
|
47
|
+
score = formatted / total if total else 1.0
|
|
48
|
+
return ToolResult(raw=raw_result, metrics={"formatted": score}, details=files)
|
|
49
|
+
|
|
50
|
+
def format_llm_message(
|
|
51
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Ask for the formatter to be run rather than a hand edit."""
|
|
54
|
+
result = extract_first_issue(tr.details)
|
|
55
|
+
if result is None:
|
|
56
|
+
return "ruff format reported issues (no details available)"
|
|
57
|
+
file, _ = result
|
|
58
|
+
return (
|
|
59
|
+
f"{file} - **not formatted**. Run the formatter, do not edit by hand:\n\n"
|
|
60
|
+
f"```bash\nruff format {file}\n```\n\n"
|
|
61
|
+
"Other files may also need formatting; `ruff format .` fixes them all."
|
|
62
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Parses output from ``uv audit`` into a standardized ToolResult.
|
|
2
|
+
|
|
3
|
+
``uv audit --output-format json`` checks the packages pinned in ``uv.lock``
|
|
4
|
+
against the OSV advisory database and prints a JSON report on stdout.
|
|
5
|
+
|
|
6
|
+
Only advisories with a fix version are counted: those are the ones an upgrade
|
|
7
|
+
resolves. The same vulnerability is often listed once per advisory database
|
|
8
|
+
(GHSA, PYSEC), so entries that alias one already seen for a package are
|
|
9
|
+
dropped. The count becomes a logistic-variant score stored under the
|
|
10
|
+
``dependencies`` metric key.
|
|
11
|
+
|
|
12
|
+
When no report can be read (``uv`` missing, no ``pyproject.toml`` or
|
|
13
|
+
``uv.lock``, no network) the tool reports no metric instead of a failing one.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
20
|
+
from py_cq.parsers.common import extract_first_issue, score_logistic_variant
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger("cq")
|
|
23
|
+
|
|
24
|
+
_LOCKFILE = "uv.lock"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class UvAuditParser(AbstractParser):
|
|
28
|
+
"""Parses raw JSON output from ``uv audit`` into a ToolResult."""
|
|
29
|
+
|
|
30
|
+
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
31
|
+
"""Parses the raw ``uv audit`` JSON report into a ToolResult."""
|
|
32
|
+
try:
|
|
33
|
+
vulnerabilities = json.loads(raw_result.stdout)["vulnerabilities"]
|
|
34
|
+
except (ValueError, KeyError, TypeError):
|
|
35
|
+
reason = (raw_result.stderr or "").strip().splitlines()[-1:]
|
|
36
|
+
log.warning(
|
|
37
|
+
"uv audit produced no report (return_code=%s), skipping: %s",
|
|
38
|
+
raw_result.return_code,
|
|
39
|
+
reason[0] if reason else "no output",
|
|
40
|
+
)
|
|
41
|
+
return ToolResult(raw=raw_result)
|
|
42
|
+
|
|
43
|
+
seen: set[tuple[str, str]] = set()
|
|
44
|
+
issues: list[dict] = []
|
|
45
|
+
for vuln in vulnerabilities:
|
|
46
|
+
fix_versions = [str(v) for v in vuln.get("fix_versions") or []]
|
|
47
|
+
if not fix_versions:
|
|
48
|
+
continue
|
|
49
|
+
dependency = vuln.get("dependency") or {}
|
|
50
|
+
package = dependency.get("name", "")
|
|
51
|
+
ids = {vuln.get("id", ""), *(vuln.get("aliases") or [])}
|
|
52
|
+
if any((package, i) in seen for i in ids):
|
|
53
|
+
continue
|
|
54
|
+
seen.update((package, i) for i in ids)
|
|
55
|
+
issues.append(
|
|
56
|
+
{
|
|
57
|
+
"code": vuln.get("id", ""),
|
|
58
|
+
"package": package,
|
|
59
|
+
"version": dependency.get("version", ""),
|
|
60
|
+
"fix_versions": fix_versions,
|
|
61
|
+
"message": vuln.get("summary") or "",
|
|
62
|
+
"link": vuln.get("link") or "",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
score = score_logistic_variant(len(issues), scale_factor=5)
|
|
67
|
+
details = {_LOCKFILE: issues} if issues else {}
|
|
68
|
+
return ToolResult(
|
|
69
|
+
raw=raw_result, metrics={"dependencies": score}, details=details
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def format_llm_message(
|
|
73
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
74
|
+
) -> str:
|
|
75
|
+
"""Formats the first vulnerable dependency into an LLM-friendly message."""
|
|
76
|
+
result = extract_first_issue(tr.details)
|
|
77
|
+
if result is None:
|
|
78
|
+
return "uv audit reported issues (no details available)"
|
|
79
|
+
file, issue = result
|
|
80
|
+
package = issue.get("package", "")
|
|
81
|
+
fixed = ", ".join(issue.get("fix_versions", []))
|
|
82
|
+
message = (
|
|
83
|
+
f"{file} - {issue.get('code', '')}: **{package} {issue.get('version', '')}** "
|
|
84
|
+
f"has a known vulnerability: {issue.get('message', '')}\n\n"
|
|
85
|
+
f"Fixed in {fixed}. Upgrade with `uv lock --upgrade-package {package}`."
|
|
86
|
+
)
|
|
87
|
+
link = issue.get("link")
|
|
88
|
+
return f"{message}\n\n{link}" if link else message
|
py_cq/parsers/vultureparser.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""Parses output from vulture dead-code detector into a standardized ToolResult.
|
|
2
2
|
|
|
3
|
-
Vulture is invoked with ``--min-confidence
|
|
3
|
+
Vulture is invoked with ``--min-confidence 60``, producing one text line
|
|
4
4
|
per unused symbol::
|
|
5
5
|
|
|
6
|
-
src/foo.py:10: unused function 'bar' (
|
|
6
|
+
src/foo.py:10: unused function 'bar' (60% confidence)
|
|
7
|
+
|
|
8
|
+
60% is the confidence vulture gives unused functions, classes, methods and
|
|
9
|
+
variables, so anything higher only reports unused imports and unreachable
|
|
10
|
+
code. At that level vulture cannot see dynamic use, so the LLM message for a
|
|
11
|
+
finding below 80% says how to keep the symbol instead of deleting it.
|
|
7
12
|
|
|
8
13
|
The parser counts violations and converts the count into a logistic-variant
|
|
9
14
|
score stored under the ``dead_code`` metric key.
|
|
@@ -20,6 +25,13 @@ from py_cq.parsers.common import (
|
|
|
20
25
|
|
|
21
26
|
_LINE_RE = re.compile(r"^(.+):(\d+): (unused \S+) '(.+)' \((\d+)% confidence\)$")
|
|
22
27
|
|
|
28
|
+
_CERTAIN = 80
|
|
29
|
+
_KEEP_HINT = (
|
|
30
|
+
"\n\nRemove it if nothing uses it. If it is public API or is used dynamically,"
|
|
31
|
+
' keep it and add `"{name}"` to `ignore_names` under `[tool.vulture]` in'
|
|
32
|
+
" `pyproject.toml`."
|
|
33
|
+
)
|
|
34
|
+
|
|
23
35
|
|
|
24
36
|
class VultureParser(AbstractParser):
|
|
25
37
|
"""Parses raw text output from ``vulture`` into a ToolResult."""
|
|
@@ -55,4 +67,7 @@ class VultureParser(AbstractParser):
|
|
|
55
67
|
kind = issue.get("type", "unused")
|
|
56
68
|
name = issue.get("name", "")
|
|
57
69
|
confidence = issue.get("confidence", "?")
|
|
58
|
-
|
|
70
|
+
message = f"{file}:{line} - **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
|
|
71
|
+
if isinstance(confidence, int) and confidence < _CERTAIN:
|
|
72
|
+
message += _KEEP_HINT.format(name=name)
|
|
73
|
+
return message
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Run pytest in a single process, for the coverage tool.
|
|
2
|
+
|
|
3
|
+
cq runs this script under ``coverage run``. Coverage only measures the process
|
|
4
|
+
it starts, so if the target project enables pytest-xdist through its own
|
|
5
|
+
``addopts``, the tests run in worker processes that coverage never sees and the
|
|
6
|
+
coverage score collapses. Passing ``-n 0`` keeps the tests in this process. It
|
|
7
|
+
is only passed when xdist is installed, because pytest rejects ``-n`` otherwise.
|
|
8
|
+
|
|
9
|
+
The script runs in the target project's environment, so it imports only the
|
|
10
|
+
standard library and pytest. It lives in its own directory because running a
|
|
11
|
+
script puts that directory first on ``sys.path``; that entry is replaced with
|
|
12
|
+
the working directory so imports behave as they do under ``python -m pytest``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from importlib.util import find_spec
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
sys.path[0] = os.getcwd()
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
args = sys.argv[1:]
|
|
25
|
+
if find_spec("xdist") is not None:
|
|
26
|
+
args += ["-n", "0"]
|
|
27
|
+
sys.exit(pytest.main(args))
|
py_cq/tool_registry.py
CHANGED
|
@@ -32,7 +32,6 @@ def load_tool_configs() -> dict[str, ToolConfig]:
|
|
|
32
32
|
exclude_format=tool_data.get("exclude_format", ""),
|
|
33
33
|
scan_exclude_names=tool_data.get("scan_exclude_names", []),
|
|
34
34
|
skip_for_file=tool_data.get("skip_for_file", False),
|
|
35
|
-
gate_strict=tool_data.get("gate_strict", True),
|
|
36
35
|
skip_if=tool_data.get("skip_if", ""),
|
|
37
36
|
cache_invariant=tool_data.get("cache_invariant", ""),
|
|
38
37
|
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python Code Quality Analysis Tool - feed the results from
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python Code Quality Analysis Tool - feed the results from 14 CQ tools straight into an LLM. Minimal tokens.
|
|
5
5
|
Author: Chris Kilner
|
|
6
6
|
Author-email: Chris Kilner <chris@rhiza.fr>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
[](https://pypi.org/project/python-code-quality/)
|
|
36
36
|
[](LICENSE)
|
|
37
37
|
|
|
38
|
-
Run
|
|
38
|
+
Run 14 code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
|
|
39
39
|
|
|
40
40
|
This can dramatically reduce the amount of noise for LLMs (and humans) and remove the need for them to know about these tools.
|
|
41
41
|
|
|
@@ -74,15 +74,24 @@ When running '-o llm', we run sequentially and exit early at the first error.
|
|
|
74
74
|
| 2 | ruff | Lint / style |
|
|
75
75
|
| 3 | ty | Type errors |
|
|
76
76
|
| 4 | bandit | Security vulnerabilities |
|
|
77
|
-
| 5 |
|
|
78
|
-
| 6 |
|
|
79
|
-
| 7 |
|
|
80
|
-
| 8 |
|
|
81
|
-
| 9 |
|
|
82
|
-
| 10 |
|
|
83
|
-
| 11 | radon
|
|
77
|
+
| 5 | kingfisher | Hardcoded secrets (API keys, tokens, private keys) |
|
|
78
|
+
| 6 | uv audit | Known vulnerabilities in locked dependencies |
|
|
79
|
+
| 7 | pytest | Test pass rate |
|
|
80
|
+
| 8 | coverage | Test coverage |
|
|
81
|
+
| 9 | vulture | Dead code |
|
|
82
|
+
| 10 | interrogate | Docstring coverage |
|
|
83
|
+
| 11 | radon cc | Cyclomatic complexity |
|
|
84
|
+
| 12 | radon mi | Maintainability index |
|
|
85
|
+
| 13 | radon hal | Halstead volume / bug estimate |
|
|
86
|
+
| 14 | ruff format | Formatting (fraction of files already formatted) |
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
`kingfisher` scans the working tree for hardcoded secrets with [Kingfisher](https://github.com/mongodb/kingfisher) (502 rules, Rust). It is not a Python dependency of cq: install it once with `uv tool install kingfisher-bin` (about 41 MB). When it is not on the `PATH` the tool is skipped with a warning. cq runs it offline (`--no-validate --no-update-check`) and with `--redact`, so secrets are never sent to provider APIs and never stored in the cache; the LLM message gives the file, line and rule but no source excerpt. Silence a test fixture or false positive with a `kingfisher:ignore` comment on that line.
|
|
89
|
+
|
|
90
|
+
`uv audit` checks the versions pinned in `uv.lock` against the OSV advisory database. It needs `uv` on the `PATH`, a `uv.lock` and network access; when any is missing the tool is skipped with a warning instead of failing. Only advisories that have a fix version are reported, since those are the ones an upgrade resolves. Results are cached like every other tool, so an advisory published after a run shows up when the project changes or the cache entry expires.
|
|
91
|
+
|
|
92
|
+
`vulture` runs at `--min-confidence 60`, the level at which it reports unused functions, classes, methods and variables. It cannot see dynamic use, so common framework decorators (`@app.command`, `@app.route`, `@pytest.fixture`, validators and similar) are ignored. For anything else that is used but never referenced by name, such as public API, add it to `ignore_names` under `[tool.vulture]` in your `pyproject.toml`.
|
|
93
|
+
|
|
94
|
+
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, stored as JSON (cq never writes pickles).
|
|
86
95
|
|
|
87
96
|
|
|
88
97
|
## Usage
|
|
@@ -101,7 +110,7 @@ cq check . --exclude demo # Exclude paths from all tools
|
|
|
101
110
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
102
111
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
103
112
|
cq check . -o llm --hint # Append "run cq again to verify" (for human workflows)
|
|
104
|
-
cq config path/to/project/ # Show effective tool configuration
|
|
113
|
+
cq config --path path/to/project/ # Show effective tool configuration
|
|
105
114
|
cq is-fixed <fingerprint> # Check whether a specific issue has been resolved
|
|
106
115
|
```
|
|
107
116
|
|
|
@@ -248,16 +257,18 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
248
257
|
│ ruff │ 0.17s │ lint │ 1.000 │ OK │
|
|
249
258
|
│ ty │ 0.33s │ type_check │ 1.000 │ OK │
|
|
250
259
|
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
260
|
+
│ uv-audit │ 0.41s │ dependencies │ 1.000 │ OK │
|
|
251
261
|
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
252
262
|
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
253
|
-
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
254
|
-
│ radon mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
255
|
-
│ radon hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
256
|
-
│ radon hal │ │ file_smallness │ 0.851 │ OK │
|
|
257
|
-
│ radon hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
258
|
-
│ radon hal │ │ functions_smallness │ 0.724 │ OK │
|
|
259
263
|
│ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
|
|
260
264
|
│ interrogate │ 0.36s │ doc_coverage │ 1.000 │ OK │
|
|
265
|
+
│ radon-cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
266
|
+
│ radon-mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
267
|
+
│ radon-hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
268
|
+
│ radon-hal │ │ file_smallness │ 0.851 │ OK │
|
|
269
|
+
│ radon-hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
270
|
+
│ radon-hal │ │ functions_smallness │ 0.724 │ OK │
|
|
271
|
+
│ ruff-format │ 0.12s │ formatted │ 1.000 │ OK │
|
|
261
272
|
│ │ │ Score │ 0.965 │ │
|
|
262
273
|
└──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
|
|
263
274
|
```
|
|
@@ -339,7 +350,7 @@ warning = 0.9
|
|
|
339
350
|
error = 0.7
|
|
340
351
|
```
|
|
341
352
|
|
|
342
|
-
Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
|
|
353
|
+
Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `kingfisher`, `uv-audit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`, `ruff-format`.
|
|
343
354
|
|
|
344
355
|
|
|
345
356
|
### Default config
|
|
@@ -356,8 +367,8 @@ error_threshold = 0.9999
|
|
|
356
367
|
cache_invariant = "ast"
|
|
357
368
|
|
|
358
369
|
[python.ruff]
|
|
359
|
-
command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --exclude .claude{exclude}"
|
|
360
|
-
exclude_format = " --exclude {path}"
|
|
370
|
+
command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude{exclude}"
|
|
371
|
+
exclude_format = " --extend-exclude {path}"
|
|
361
372
|
parser = "RuffParser"
|
|
362
373
|
order = 2
|
|
363
374
|
warning_threshold = 0.9999
|
|
@@ -382,11 +393,27 @@ order = 4
|
|
|
382
393
|
warning_threshold = 0.9999
|
|
383
394
|
error_threshold = 0.8
|
|
384
395
|
|
|
396
|
+
[python.kingfisher]
|
|
397
|
+
command = "kingfisher scan \"{context_path}\" --no-validate --no-update-check --git-history none --no-extract-archives --redact -q -f json --exclude .venv --exclude .git --exclude .hypothesis --exclude dist --exclude \"*_cache\" --exclude __pycache__ --exclude .claude{exclude}"
|
|
398
|
+
exclude_format = " --exclude {path}"
|
|
399
|
+
parser = "KingfisherParser"
|
|
400
|
+
order = 5
|
|
401
|
+
warning_threshold = 0.9999
|
|
402
|
+
error_threshold = 0.9999
|
|
403
|
+
|
|
404
|
+
[python.uv-audit]
|
|
405
|
+
command = "uv audit --project \"{abs_context_path}\" --frozen --output-format json"
|
|
406
|
+
parser = "UvAuditParser"
|
|
407
|
+
order = 6
|
|
408
|
+
warning_threshold = 0.9999
|
|
409
|
+
error_threshold = 0.8
|
|
410
|
+
skip_for_file = true
|
|
411
|
+
|
|
385
412
|
[python.pytest]
|
|
386
413
|
command = "{python} -m pytest -vv \"{context_path}\" --ignore .claude{exclude}"
|
|
387
414
|
exclude_format = " --ignore {path}"
|
|
388
415
|
parser = "PytestParser"
|
|
389
|
-
order =
|
|
416
|
+
order = 7
|
|
390
417
|
warning_threshold = 1.0
|
|
391
418
|
error_threshold = 1.0
|
|
392
419
|
run_in_target_env = true
|
|
@@ -395,10 +422,10 @@ skip_for_file = true
|
|
|
395
422
|
cache_invariant = "ast"
|
|
396
423
|
|
|
397
424
|
[python.coverage]
|
|
398
|
-
command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py
|
|
425
|
+
command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py,*/py_cq/runners/* \"{pytest_serial}\" \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --omit=*/tests/*,*/test_*.py,*/py_cq/runners/*"
|
|
399
426
|
exclude_format = " --ignore {path}"
|
|
400
427
|
parser = "CoverageParser"
|
|
401
|
-
order =
|
|
428
|
+
order = 8
|
|
402
429
|
warning_threshold = 0.9
|
|
403
430
|
error_threshold = 0.5
|
|
404
431
|
run_in_target_env = true
|
|
@@ -408,19 +435,19 @@ skip_if = "pytest"
|
|
|
408
435
|
cache_invariant = "ast"
|
|
409
436
|
|
|
410
437
|
[python.vulture]
|
|
411
|
-
command = "{python} -m vulture \"{context_path}\" --min-confidence
|
|
438
|
+
command = "{python} -m vulture \"{context_path}\" --min-confidence 60 --ignore-decorators \"@*.command,@*.callback,@*.route,@*.get,@*.post,@*.put,@*.delete,@*.patch,@*fixture,@*.task,@*hookimpl,@*validator,@*.errorhandler\" --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
|
|
412
439
|
exclude_format = ",{path}"
|
|
413
440
|
parser = "VultureParser"
|
|
414
|
-
order =
|
|
441
|
+
order = 9
|
|
415
442
|
warning_threshold = 0.9999
|
|
416
443
|
error_threshold = 0.8
|
|
417
444
|
cache_invariant = "ast"
|
|
418
445
|
|
|
419
446
|
[python.interrogate]
|
|
420
|
-
command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
|
|
447
|
+
command = "{python} -m interrogate \"{context_path}\" -e .claude -e \"{context_path}/tests\"{exclude} -v --fail-under 0"
|
|
421
448
|
exclude_format = " -e {path}"
|
|
422
449
|
parser = "InterrogateParser"
|
|
423
|
-
order =
|
|
450
|
+
order = 10
|
|
424
451
|
warning_threshold = 0.8
|
|
425
452
|
error_threshold = 0.5
|
|
426
453
|
|
|
@@ -430,26 +457,31 @@ skip_empty_init = true
|
|
|
430
457
|
[python.radon-cc]
|
|
431
458
|
command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
|
|
432
459
|
parser = "ComplexityParser"
|
|
433
|
-
order =
|
|
434
|
-
gate_strict = false
|
|
460
|
+
order = 11
|
|
435
461
|
warning_threshold = 0.6
|
|
436
462
|
error_threshold = 0.4
|
|
437
463
|
|
|
438
464
|
[python.radon-mi]
|
|
439
465
|
command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
|
|
440
466
|
parser = "MaintainabilityParser"
|
|
441
|
-
order =
|
|
442
|
-
gate_strict = false
|
|
467
|
+
order = 12
|
|
443
468
|
warning_threshold = 0.6
|
|
444
469
|
error_threshold = 0.4
|
|
445
470
|
|
|
446
471
|
[python.radon-hal]
|
|
447
472
|
command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
|
|
448
473
|
parser = "HalsteadParser"
|
|
449
|
-
order =
|
|
450
|
-
gate_strict = false
|
|
474
|
+
order = 13
|
|
451
475
|
warning_threshold = 0.35
|
|
452
476
|
error_threshold = 0.3
|
|
477
|
+
|
|
478
|
+
[python.ruff-format]
|
|
479
|
+
command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
|
|
480
|
+
exclude_format = " --extend-exclude {path}"
|
|
481
|
+
parser = "RuffFormatParser"
|
|
482
|
+
order = 14
|
|
483
|
+
warning_threshold = 0.9999
|
|
484
|
+
error_threshold = 0.5
|
|
453
485
|
```
|
|
454
486
|
|
|
455
487
|
## Respect
|
|
@@ -1,36 +1,40 @@
|
|
|
1
1
|
py_cq/__init__.py,sha256=-C6_QzQeydxft3ndlExTgWnvlqH8fJNPJxfySXULQmU,56
|
|
2
|
-
py_cq/api.py,sha256=
|
|
3
|
-
py_cq/cli.py,sha256=
|
|
2
|
+
py_cq/api.py,sha256=u0ajk6ZYWFGjSsLYkRfUBBtL_Vw0HHZbWpa1AM79y8Y,10172
|
|
3
|
+
py_cq/cli.py,sha256=i_-VCXVzrj2kpzbx8lEJxWmQNFyiFa37nsCMMkxqvas,13827
|
|
4
4
|
py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
|
|
5
|
-
py_cq/config/config.toml,sha256=
|
|
5
|
+
py_cq/config/config.toml,sha256=iV0kUp6ifZvHu_3fyNyC6C3Ggv7ts1zhopU0-3LH1U4,4170
|
|
6
6
|
py_cq/context_hash.py,sha256=uKMkN394v6QYMukZj9usbxnq8px3i1khyIRPKwe09DE,5614
|
|
7
|
-
py_cq/execution_engine.py,sha256=
|
|
7
|
+
py_cq/execution_engine.py,sha256=1-AM1PbIAdizj-PGf3cmePMKGmsV4pk6K2YgDHyerdA,21279
|
|
8
8
|
py_cq/language_detector.py,sha256=rh5bAA3LyCQR3RODmx_EalqRNYvSYK2SJzh2hIkxOUo,1139
|
|
9
9
|
py_cq/llm_formatter.py,sha256=nCV3nFdxJAi2CnH9FKb5tB-0w-yjLWmjywQI5GJ2cSU,8412
|
|
10
|
-
py_cq/localtypes.py,sha256
|
|
10
|
+
py_cq/localtypes.py,sha256=-XxoasKVOQ6hLzNX2KdpTxxHu3F2c_9RV3DCLPPj0D0,7794
|
|
11
11
|
py_cq/main.py,sha256=5dgr8Zk1LNK5M-0wncYVksBp0qE-jWZYqmOXfb1eOhk,400
|
|
12
12
|
py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
|
|
13
13
|
py_cq/parsers/__init__.py,sha256=uCy7R0cnvDPNvB5aWMLgM3FB9nDcu1jwUmznFMRROSY,28
|
|
14
14
|
py_cq/parsers/banditparser.py,sha256=YqpWljU5D4IhLdtzat7XVGBm9NXrbOQbr9LI9NKybK8,3298
|
|
15
|
-
py_cq/parsers/common.py,sha256=
|
|
15
|
+
py_cq/parsers/common.py,sha256=ViYneYOG4RTlbHbSU4GN9kB_M1gikukiwQ81CTajAqA,13796
|
|
16
16
|
py_cq/parsers/compileparser.py,sha256=jl0DloyXTKqVHRIUft-t5lvJOndY-PdLPrXB38v9zuA,6927
|
|
17
17
|
py_cq/parsers/complexityparser.py,sha256=mX1WHPRgBBc6kOqHfubowRbYXY0E1k4JVwr908Cj10U,5582
|
|
18
18
|
py_cq/parsers/coverageparser.py,sha256=QGjZ8JJF0G-0uiZ07BzxSnblGiysHtOwq0SYvWrtOhA,7441
|
|
19
19
|
py_cq/parsers/exitcodeparser.py,sha256=b4LWIwwklPwCX3MnqNsGQC1slQU56DKE05vJ0OdWVoA,957
|
|
20
20
|
py_cq/parsers/halsteadparser.py,sha256=789c_WWfomskM4i7SUEbKvB67-F2y5nfqt48aFXOusY,10192
|
|
21
|
-
py_cq/parsers/interrogateparser.py,sha256=
|
|
21
|
+
py_cq/parsers/interrogateparser.py,sha256=KDqXTdnJ5JzWhN4ziRda90nZCQOJaKn3CCiq4Rh0IRw,11314
|
|
22
|
+
py_cq/parsers/kingfisherparser.py,sha256=-UbtZ4d_SAPoohgz9FwcVE4u33VB_8FtE1_dhAwJ1VM,3129
|
|
22
23
|
py_cq/parsers/linecountparser.py,sha256=-hqebwG4j7_QugzGLUUQRcPukGHicV2nbIp1pR8ZcAw,1310
|
|
23
24
|
py_cq/parsers/maintainabilityparser.py,sha256=3a4m93XNtUurw3IxEI1Iae9idgJY6qiEt7HMYl1wTu0,4793
|
|
24
25
|
py_cq/parsers/pytestparser.py,sha256=cIaTs2XxgeJlVragLEVTZcrbB-bzfNYYMdUrakRa-rQ,11545
|
|
25
26
|
py_cq/parsers/regexcountparser.py,sha256=zGzHEYDIori6aiWnVkRFixdE7fWt66npAAWjzvpoBF4,1592
|
|
27
|
+
py_cq/parsers/ruffformatparser.py,sha256=UXff5Qwh9i0goiSTJ_AaaIsmMSssUjNY2O_dERXjsFw,2636
|
|
26
28
|
py_cq/parsers/ruffparser.py,sha256=uE0QEaZ4Ch7IsWY_2ZkfHXPU0TYPGsGPfv66hGe5G34,7634
|
|
27
29
|
py_cq/parsers/typarser.py,sha256=x835OrapG1aUteW2NhC5U0bhj1w1pMtbfmbvb74-1AI,8077
|
|
28
|
-
py_cq/parsers/
|
|
30
|
+
py_cq/parsers/uvauditparser.py,sha256=rnNLp4UD48utALr-ut1fCt3CKIu5Q80x75ws5s6ArqI,3652
|
|
31
|
+
py_cq/parsers/vultureparser.py,sha256=C3teaMYtROHqFrXHBvTvLMK9jbDK5Q8X4gAq5ruNTGM,2889
|
|
29
32
|
py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
+
py_cq/runners/pytest_serial.py,sha256=lhhkaKdwrgPzP3uKnHAfNgEE0iETjbvVh9xc23pbkXo,1026
|
|
30
34
|
py_cq/source_file.py,sha256=Uyv-CSVeliFnE6TpRB4Y15ykFQs4n0r9-amzxXXlqN0,7562
|
|
31
35
|
py_cq/table_formatter.py,sha256=-22nzVIOaDe3QOBmZ2iBPH7ewIEIPEn5WYSjCoB4Hj4,1608
|
|
32
|
-
py_cq/tool_registry.py,sha256=
|
|
33
|
-
python_code_quality-0.
|
|
34
|
-
python_code_quality-0.
|
|
35
|
-
python_code_quality-0.
|
|
36
|
-
python_code_quality-0.
|
|
36
|
+
py_cq/tool_registry.py,sha256=1LkaBzWu5NzbaEZdZp1JLGFXZpEyke4pHHe01UvcfPo,1834
|
|
37
|
+
python_code_quality-0.4.0.dist-info/WHEEL,sha256=3OmCxFA0ffBIsX4x0o8CdfSvZAOvO3mzEG8U_z5TvV4,80
|
|
38
|
+
python_code_quality-0.4.0.dist-info/entry_points.txt,sha256=cfWbTw7eYO6Trv1-Z_odL6Zta9CqYU6Vk9lAHdfB60Q,40
|
|
39
|
+
python_code_quality-0.4.0.dist-info/METADATA,sha256=nHEPvS1bYCSgE-ev1QYGHOfdUehL8cEqv8CMCtVr89Y,19336
|
|
40
|
+
python_code_quality-0.4.0.dist-info/RECORD,,
|
{python_code_quality-0.3.0.dist-info → python_code_quality-0.4.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|