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 CHANGED
@@ -148,7 +148,9 @@ class CQ:
148
148
  if clear_cache:
149
149
  _cache.clear()
150
150
 
151
- def raw(self, *, early_exit: bool = False, order: str = "severity") -> list[ToolResult]:
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(), target, max_workers=1, early_exit=False, excludes=[],
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 11+ code quality tools to an LLM. Try: cq check . -o llm"""
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 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
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 = 5
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 -m pytest \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --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 = 6
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 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
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 = 7
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 = 8
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 = 9
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 = 10
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 = 11
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
- _cache.set(cache_key, raw_result.to_dict(), expire=5 * 24 * 60 * 60, tag=project_tag)
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(f"context_hash: {(time.perf_counter() - t_hash0) * 1000:.1f}ms {shared_hash}")
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(f"context_hash(ast): {(time.perf_counter() - t_norm0) * 1000:.1f}ms {norm_hash}")
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(tool_config, path, excludes, precomputed_hash=_hash_for(tool_config), project_tag=root, **extra)
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(f"{tool_config.name} generated an exception: {exc} {exc.__traceback__}")
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 dep_tr.metrics and min(dep_tr.metrics.values()) < dep_config.error_threshold:
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: _cancel_on_dep_failure(
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 base + "\n\nInsert a module-level docstring as the very first statement in the file."
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 = (candidate if candidate.is_dir() else candidate.parent) / "pyproject.toml"
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(file_path: Path, cfg: dict | None = None) -> list[tuple[int, str, str]]:
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
@@ -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 80``, producing one text line
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' (80% confidence)
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
- return f"{file}:{line} - **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
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.3.0
4
- Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
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
  [![Python versions](https://img.shields.io/pypi/pyversions/python-code-quality?)](https://pypi.org/project/python-code-quality/)
36
36
  [![License](https://img.shields.io/github/license/rhiza-fr/py-cq)](LICENSE)
37
37
 
38
- Run 11+ 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.
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 | pytest | Test pass rate |
78
- | 6 | coverage | Test coverage |
79
- | 7 | vulture | Dead code |
80
- | 8 | interrogate | Docstring coverage |
81
- | 9 | radon cc | Cyclomatic complexity |
82
- | 10 | radon mi | Maintainability index |
83
- | 11 | radon hal | Halstead volume / bug estimate |
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
- Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
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 = 5
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 -m pytest \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --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 = 6
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 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
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 = 7
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 = 8
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 = 9
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 = 10
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 = 11
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=AxhTQVX4ACZmgCpdfJgB2_1VXtClxc531NkRoLK1vA0,10118
3
- py_cq/cli.py,sha256=aaLnBXF81fkUtCvO7DGzmogcq50X5bF5dC4XFC4sbqY,13814
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=fco0qKWfIg84e3ex2pmAcSPYBmcHdbiqL-Geo__EIkM,3017
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=vAbNDCHP5c-2ADWs9SM0eCEpZBiabhpl-WZp6Er3qb4,20644
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=mt4sqUg1bz2BenirGA18X4CdtyZahTAuKyOfwFnwfSE,7907
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=r8UD7pch-txlJSTsJrOff2o3_0WHRlZhgKbJ-C58L7g,13818
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=55HZ2j1Rz34eremwGx-W9oIMI21DIqwxEG524UgsAhk,11249
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/vultureparser.py,sha256=-yGAXzRY4_fzLRTk7dfwDrrRZ-n97j1Ph5_xo3c6TfU,2229
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=0s0sD9_iQW-HU0SIKSS8CFDlrzW0c-vlYrAFODEHiSc,1894
33
- python_code_quality-0.3.0.dist-info/WHEEL,sha256=ZFFp7t7R4RYQ5KYZkmiFWoQvHay7SrTmn-6ZYfoFZ3U,80
34
- python_code_quality-0.3.0.dist-info/entry_points.txt,sha256=cfWbTw7eYO6Trv1-Z_odL6Zta9CqYU6Vk9lAHdfB60Q,40
35
- python_code_quality-0.3.0.dist-info/METADATA,sha256=hQBSoblR8TKmTPRriXRt5Ed-uO9jexstNj0nPU4Li4A,16213
36
- python_code_quality-0.3.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.12.7
2
+ Generator: uv 0.13.8
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any