python-code-quality 0.1.8__py3-none-any.whl → 0.1.9__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/cli.py CHANGED
@@ -16,6 +16,7 @@ import json
16
16
  import logging
17
17
  import tomllib
18
18
  from enum import Enum
19
+ from importlib import import_module
19
20
  from pathlib import Path
20
21
 
21
22
  import typer
@@ -26,6 +27,7 @@ from rich.table import Table
26
27
  from py_cq.config import load_user_config
27
28
  from py_cq.execution_engine import _cache as tool_cache
28
29
  from py_cq.execution_engine import run_tools
30
+ from py_cq.language_detector import detect_language
29
31
  from py_cq.localtypes import CombinedToolResults, ToolConfig
30
32
  from py_cq.metric_aggregator import aggregate_metrics
31
33
  from py_cq.tool_registry import tool_registry
@@ -56,6 +58,7 @@ def _apply_user_config(base: dict[str, ToolConfig], user_cfg: dict) -> dict[str,
56
58
  Supports:
57
59
  - ``disable``: list of tool IDs to remove
58
60
  - ``thresholds.<tool_id>.warning`` / ``.error``: override per-tool thresholds
61
+ - ``tools.<tool_id>``: declare new tools (or override built-ins)
59
62
  """
60
63
  registry = {k: copy.copy(v) for k, v in base.items()}
61
64
  for tool_id in user_cfg.get("disable", []):
@@ -66,6 +69,24 @@ def _apply_user_config(base: dict[str, ToolConfig], user_cfg: dict) -> dict[str,
66
69
  registry[tool_id].warning_threshold = float(thresholds["warning"])
67
70
  if "error" in thresholds:
68
71
  registry[tool_id].error_threshold = float(thresholds["error"])
72
+ for tool_id, tool_data in user_cfg.get("tools", {}).items():
73
+ try:
74
+ parser_name = tool_data["parser"]
75
+ module = import_module(f"py_cq.parsers.{parser_name.lower()}")
76
+ parser_class = getattr(module, parser_name)
77
+ registry[tool_id] = ToolConfig(
78
+ name=tool_id,
79
+ command=tool_data["command"],
80
+ parser_class=parser_class,
81
+ order=tool_data["order"],
82
+ warning_threshold=tool_data["warning_threshold"],
83
+ error_threshold=tool_data["error_threshold"],
84
+ run_in_target_env=tool_data.get("run_in_target_env", False),
85
+ extra_deps=tool_data.get("extra_deps", []),
86
+ parser_config=tool_data.get("parser_config", {}),
87
+ )
88
+ except KeyError as e:
89
+ raise typer.BadParameter(f"[tool.cq.tools.{tool_id}] missing required field {e}")
69
90
  return registry
70
91
 
71
92
 
@@ -101,11 +122,27 @@ def check(
101
122
  workers: int = typer.Option(
102
123
  0, "--workers", help="Max parallel workers (default: one per tool, use 1 for sequential)"
103
124
  ),
125
+ language: str | None = typer.Option(
126
+ None, "--language", "-l", help="Override language detection (e.g. python, typescript, rust)"
127
+ ),
104
128
  ):
105
129
  """Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
106
130
  path_obj = Path(path)
107
131
  if not path_obj.exists():
108
132
  raise typer.BadParameter(f"Path does not exist: {path}")
133
+
134
+ resolved_language = language or detect_language(path_obj)
135
+
136
+ if resolved_language is not None and resolved_language != "python":
137
+ console.print(
138
+ f"[yellow]{resolved_language.capitalize()} project detected. "
139
+ "Non-Python language support is not yet available.[/yellow]"
140
+ )
141
+ raise typer.Exit(0)
142
+
143
+ # Python path (or unknown — fall through to existing validation).
144
+ # Note: --language python still requires pyproject.toml; the flag selects
145
+ # the tool set, not the input validation rules.
109
146
  if path_obj.is_file():
110
147
  if path_obj.suffix != ".py":
111
148
  raise typer.BadParameter(f"File must be a Python file (.py): {path}")
@@ -113,7 +150,9 @@ def check(
113
150
  if not (path_obj / "pyproject.toml").exists():
114
151
  raise typer.BadParameter(f"Directory must contain pyproject.toml: {path}")
115
152
  log.setLevel(log_level)
116
- effective_registry = _apply_user_config(tool_registry, load_user_config(path_obj))
153
+ user_cfg = load_user_config(path_obj)
154
+ context_lines: int = int(user_cfg.get("context_lines", 15))
155
+ effective_registry = _apply_user_config(tool_registry, user_cfg)
117
156
  if clear_cache:
118
157
  tool_cache.clear()
119
158
  tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM))
@@ -129,10 +168,19 @@ def check(
129
168
  elif output == OutputMode.LLM:
130
169
  # log.setLevel("CRITICAL")
131
170
  from py_cq.llm_formatter import format_for_llm
132
- console.print(format_for_llm(effective_registry, combined_metrics))
171
+ console.print(format_for_llm(effective_registry, combined_metrics, context_lines=context_lines))
133
172
  else:
173
+ console.print(f"[bold green]{path_obj.resolve()}[/]")
134
174
  console.print(format_as_table(combined_metrics, effective_registry))
135
175
 
176
+ tool_by_name = {tc.name: tc for tc in effective_registry.values()}
177
+ if any(
178
+ min(tr.metrics.values()) < tool_by_name[tr.raw.tool_name].error_threshold
179
+ for tr in tool_results
180
+ if tr.metrics and tr.raw.tool_name in tool_by_name
181
+ ):
182
+ raise typer.Exit(code=1)
183
+
136
184
 
137
185
  @app.command()
138
186
  def config(
@@ -172,8 +220,9 @@ def config(
172
220
  table.add_column("Error", justify="right")
173
221
  table.add_column("Status", justify="center")
174
222
 
175
- for tool_id in sorted(tool_registry, key=lambda t: tool_registry[t].order):
176
- tc = effective_registry.get(tool_id, tool_registry[tool_id])
223
+ all_tool_ids = set(tool_registry) | set(effective_registry)
224
+ for tool_id in sorted(all_tool_ids, key=lambda t: (effective_registry.get(t) or tool_registry[t]).order):
225
+ tc = effective_registry.get(tool_id) or tool_registry[tool_id]
177
226
  is_disabled = tool_id in disabled_ids
178
227
  status = "[red]disabled[/red]" if is_disabled else "[green]enabled[/green]"
179
228
  table.add_row(
@@ -204,7 +253,7 @@ def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig]):
204
253
  >>> table = format_as_table(combined_results)
205
254
  >>> console.print(table)
206
255
  """
207
- table = Table(title=f"[bold green]{data.path}[/]", width=80)
256
+ table = Table(width=80)
208
257
  table.add_column("Tool", justify="left", no_wrap=True)
209
258
  table.add_column("Time", justify="right", style="dim")
210
259
  table.add_column("Metric", justify="right", style="cyan", no_wrap=True)
@@ -1,52 +1,48 @@
1
- tools:
1
+ python:
2
2
 
3
- compilation:
4
- name: "compile"
5
- command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
3
+ compile:
4
+ command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
6
5
  parser: "CompileParser"
7
6
  order: 1
8
7
  warning_threshold: 0.9999
9
8
  error_threshold: 0.9999
10
9
 
11
- bandit:
12
- name: "bandit"
13
- command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
14
- parser: "BanditParser"
15
- order: 2
16
- warning_threshold: 0.9999
17
- error_threshold: 0.8
18
-
19
10
  ruff:
20
- name: "ruff"
21
- command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
11
+ command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\""
22
12
  parser: "RuffParser"
23
- order: 3
13
+ order: 2
24
14
  warning_threshold: 0.9999
25
15
  error_threshold: 0.9
26
16
 
27
17
  ty:
28
- name: "ty"
29
- command: "{python} -m ty check --output-format concise --color never {context_path}"
18
+ command: "{python} -m ty check --output-format concise --color never \"{context_path}\""
30
19
  parser: "TyParser"
31
- order: 4
20
+ order: 3
32
21
  warning_threshold: 0.9999
33
22
  error_threshold: 0.8
34
23
  run_in_target_env: true
35
24
  extra_deps:
36
25
  - ty
37
26
 
27
+ bandit:
28
+ command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests\""
29
+ parser: "BanditParser"
30
+ order: 4
31
+ warning_threshold: 0.9999
32
+ error_threshold: 0.8
33
+
38
34
  pytest:
39
- name: "pytest"
40
- command: "{python} -m pytest -v {context_path}"
35
+ command: "{python} -m pytest -v \"{context_path}\""
41
36
  parser: "PytestParser"
42
37
  order: 5
43
- warning_threshold: 0.7
44
- error_threshold: 0.5
38
+ warning_threshold: 1.0
39
+ error_threshold: 1.0
45
40
  run_in_target_env: true
41
+ extra_deps:
42
+ - pytest
46
43
 
47
44
  coverage:
48
- name: "coverage"
49
- command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
45
+ command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
50
46
  parser: "CoverageParser"
51
47
  order: 6
52
48
  warning_threshold: 0.9
@@ -56,41 +52,36 @@ tools:
56
52
  - coverage
57
53
  - pytest
58
54
 
59
- complexity:
60
- name: "radon cc"
61
- command: "{python} -m radon cc --json {context_path}"
55
+ radon-cc:
56
+ command: "{python} -m radon cc --json \"{context_path}\""
62
57
  parser: "ComplexityParser"
63
58
  order: 7
64
59
  warning_threshold: 0.6
65
60
  error_threshold: 0.4
66
61
 
67
- maintainability:
68
- name: "radon mi"
69
- command: "{python} -m radon mi -s --json {context_path}"
62
+ radon-mi:
63
+ command: "{python} -m radon mi -s --json \"{context_path}\""
70
64
  parser: "MaintainabilityParser"
71
65
  order: 8
72
66
  warning_threshold: 0.6
73
67
  error_threshold: 0.4
74
68
 
75
- halstead:
76
- name: "radon hal"
77
- command: "{python} -m radon hal -f --json {context_path}"
69
+ radon-hal:
70
+ command: "{python} -m radon hal -f --json \"{context_path}\""
78
71
  parser: "HalsteadParser"
79
72
  order: 9
80
73
  warning_threshold: 0.5
81
74
  error_threshold: 0.3
82
75
 
83
76
  vulture:
84
- name: "vulture"
85
- command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
77
+ command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
86
78
  parser: "VultureParser"
87
79
  order: 10
88
80
  warning_threshold: 0.9999
89
81
  error_threshold: 0.8
90
82
 
91
83
  interrogate:
92
- name: "interrogate"
93
- command: "{python} -m interrogate {context_path} -v --fail-under 0"
84
+ command: "{python} -m interrogate \"{context_path}\" -v --fail-under 0"
94
85
  parser: "InterrogateParser"
95
86
  order: 11
96
87
  warning_threshold: 0.8
py_cq/execution_engine.py CHANGED
@@ -82,7 +82,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
82
82
  log.info(f"Cache hit: {command}")
83
83
  return RawResult(**cast(dict[str, Any], _cache[cache_key]))
84
84
  log.info(f"Running: {command}")
85
- result = subprocess.run(command, capture_output=True, text=True, shell=True) # nosec
85
+ result = subprocess.run(command, capture_output=True, text=True, shell=True, encoding="utf-8", errors="replace") # nosec
86
86
  timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
87
87
  raw_result = RawResult(
88
88
  tool_name=tool_config.name,
@@ -139,7 +139,7 @@ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int
139
139
  def _run_and_parse(tool_config: ToolConfig) -> tuple[int, ToolResult]:
140
140
  t0 = time.perf_counter()
141
141
  raw_result = run_tool(tool_config, path)
142
- tr = tool_config.parser_class().parse(raw_result)
142
+ tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
143
143
  tr.duration_s = time.perf_counter() - t0
144
144
  return tool_config.order, tr
145
145
 
@@ -0,0 +1,29 @@
1
+ """Detect the primary language of a project from its file markers."""
2
+
3
+ from pathlib import Path
4
+
5
+ # Ordered: first match wins. Python is listed first so it takes priority.
6
+ _MARKERS: list[tuple[str, list[str]]] = [
7
+ ("python", ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile"]),
8
+ ("typescript", ["tsconfig.json", "package.json"]),
9
+ ("rust", ["Cargo.toml"]),
10
+ ("go", ["go.mod"]),
11
+ ("ruby", ["Gemfile"]),
12
+ ("java", ["pom.xml", "build.gradle"]),
13
+ ]
14
+
15
+ _DOTNET_SUFFIXES = {".csproj", ".sln"}
16
+
17
+
18
+ def detect_language(path: Path) -> str | None:
19
+ """Return the detected language for a project path, or None if unrecognised.
20
+
21
+ If path is a file, the parent directory is checked.
22
+ Dotnet is checked last as it uses suffix matching rather than fixed filenames."""
23
+ directory = path if path.is_dir() else path.parent
24
+ for language, markers in _MARKERS:
25
+ if any((directory / marker).exists() for marker in markers):
26
+ return language
27
+ if any(f.suffix in _DOTNET_SUFFIXES for f in directory.iterdir() if f.is_file()):
28
+ return "dotnet"
29
+ return None
py_cq/llm_formatter.py CHANGED
@@ -18,6 +18,7 @@ def format_for_llm(
18
18
  tool_configs: dict,
19
19
  combined: CombinedToolResults,
20
20
  cq_invocation: str | None = None,
21
+ context_lines: int = 15,
21
22
  ) -> str:
22
23
  """Return a markdown prompt describing the single most important defect."""
23
24
  by_name = {tc.name: tc for tc in tool_configs.values()}
@@ -38,7 +39,7 @@ def format_for_llm(
38
39
 
39
40
  worst = failing[0]
40
41
  config = by_name[worst.raw.tool_name]
41
- defect_md = config.parser_class().format_llm_message(worst)
42
+ defect_md = config.parser_class().format_llm_message(worst, context_lines=context_lines)
42
43
  if cq_invocation is None:
43
44
  cq_invocation = "cq " + " ".join(sys.argv[1:])
44
45
  return (
py_cq/localtypes.py CHANGED
@@ -21,6 +21,7 @@ class ToolConfig:
21
21
  error_threshold: float = 0.5 # Red error if below this
22
22
  run_in_target_env: bool = False # If True, run in target project's env via uv
23
23
  extra_deps: list[str] = field(default_factory=list) # Extra deps to inject via uv --with
24
+ parser_config: dict[str, Any] = field(default_factory=dict)
24
25
 
25
26
 
26
27
  @dataclass
@@ -119,12 +120,15 @@ class AbstractParser(ABC):
119
120
 
120
121
  Subclasses must implement `parse` to convert a `RawResult` into a `ToolResult`. An optional `provide_help` can be overridden to supply contextual guidance for a parsed result."""
121
122
 
123
+ def __init__(self, parser_config: dict | None = None):
124
+ self.parser_config = parser_config or {}
125
+
122
126
  @abstractmethod
123
127
  def parse(self, raw_result: RawResult) -> ToolResult:
124
128
  """Converts raw tool output into a structured ToolResult."""
125
129
  pass
126
130
 
127
- def format_llm_message(self, tr: ToolResult) -> str:
131
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
128
132
  """Return a single-defect description for LLM consumption.
129
133
 
130
134
  Default implementation reports the worst metric by name and score.
@@ -42,7 +42,7 @@ class BanditParser(AbstractParser):
42
42
  score = score_logistic_variant(weighted, scale_factor=10)
43
43
  return ToolResult(raw=raw_result, metrics={"security": score}, details=files)
44
44
 
45
- def format_llm_message(self, tr: ToolResult) -> str:
45
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
46
46
  if not tr.details:
47
47
  return "bandit reported issues (no details available)"
48
48
  file, issues = next(iter(tr.details.items()))
@@ -51,4 +51,4 @@ class BanditParser(AbstractParser):
51
51
  code = issue.get("code", "")
52
52
  severity = issue.get("severity", "")
53
53
  message = issue.get("message", "")
54
- return f"`{file}:{line}` — **{code}** [{severity}]: {message}{format_source_context(file, line)}"
54
+ return f"`{file}:{line}` — **{code}** [{severity}]: {message}{format_source_context(file, line, count=context_lines)}"
py_cq/parsers/common.py CHANGED
@@ -37,6 +37,37 @@ def format_source_context(file: str, line: int | str, context: int = 3, count: i
37
37
  return f"\n```python\n{src}\n```"
38
38
 
39
39
 
40
+ def find_function_source(file: str, func_name: str, max_lines: int = 15) -> str:
41
+ """Return a fenced python block for the body of func_name, or '' if unavailable."""
42
+ from pathlib import Path
43
+ try:
44
+ all_lines = Path(file).read_text(encoding="utf-8").splitlines()
45
+ except OSError:
46
+ return ""
47
+ import re
48
+ pattern = re.compile(rf"^(\s*)(?:async\s+)?def\s+{re.escape(func_name)}\s*\(")
49
+ match_result: tuple[int, int] | None = None
50
+ for i, line in enumerate(all_lines):
51
+ m = pattern.match(line)
52
+ if m:
53
+ match_result = (i, len(m.group(1)))
54
+ break
55
+ if match_result is None:
56
+ return ""
57
+ start_idx, baseline_indent = match_result
58
+ collected = [all_lines[start_idx]]
59
+ for line in all_lines[start_idx + 1 :]:
60
+ stripped = line.lstrip()
61
+ indent = len(line) - len(stripped)
62
+ if stripped and indent <= baseline_indent:
63
+ break
64
+ collected.append(line)
65
+ if len(collected) >= max_lines:
66
+ break
67
+ numbered = "\n".join(f"{start_idx + 1 + i}: {ln}" for i, ln in enumerate(collected))
68
+ return f"\n```python\n{numbered}\n```"
69
+
70
+
40
71
  def inv_normalize(value: float, max_value: float) -> float:
41
72
  """Returns the inverse normalized value of `value` relative to `max_value`."""
42
73
  return (max_value - min(value, max_value)) / max_value
@@ -115,7 +115,7 @@ class CompileParser(AbstractParser):
115
115
  tr.details["failed_files"] = failed_files
116
116
  return tr
117
117
 
118
- def format_llm_message(self, tr: ToolResult) -> str:
118
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
119
119
  """Return the first compilation failure as a defect description."""
120
120
  failed = tr.details.get("failed_files", {})
121
121
  if not failed:
@@ -124,5 +124,5 @@ class CompileParser(AbstractParser):
124
124
  line = info.get("line", "?")
125
125
  typ = info.get("type", "Error")
126
126
  help_msg = info.get("help", "")
127
- code_block = format_source_context(file, line) or (f"\n```python\n{info['src']}\n```" if info.get("src") else "")
127
+ code_block = format_source_context(file, line, count=context_lines) or (f"\n```python\n{info['src']}\n```" if info.get("src") else "")
128
128
  return f"`{file}:{line}` — **{typ}**: {help_msg}{code_block}"
@@ -71,7 +71,7 @@ class CoverageParser(AbstractParser):
71
71
  tr.details = details
72
72
  return tr
73
73
 
74
- def format_llm_message(self, tr: ToolResult) -> str:
74
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
75
75
  """Return the files with lowest coverage as a defect description."""
76
76
  score = tr.metrics.get("coverage", 0)
77
77
  uncovered = sorted(
@@ -0,0 +1,16 @@
1
+ """Parser that scores a tool pass/fail based solely on its exit code."""
2
+
3
+ from py_cq.localtypes import AbstractParser, RawResult, ToolResult
4
+
5
+
6
+ class ExitCodeParser(AbstractParser):
7
+ """Score 1.0 if the tool exited with code 0, else 0.0."""
8
+
9
+ def parse(self, raw_result: RawResult) -> ToolResult:
10
+ score = 1.0 if raw_result.return_code == 0 else 0.0
11
+ return ToolResult(raw=raw_result, metrics={"exit_code": score})
12
+
13
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
14
+ output = tr.raw.stdout.strip() or tr.raw.stderr.strip()
15
+ lines = output.splitlines()[:context_lines]
16
+ return "\n".join(lines) if lines else "Tool exited with non-zero status (no output)"
@@ -113,7 +113,7 @@ class HalsteadParser(AbstractParser):
113
113
  }
114
114
  return tr
115
115
 
116
- def format_llm_message(self, tr: ToolResult) -> str:
116
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
117
117
  """Return the worst Halstead offender as an actionable defect description."""
118
118
  if not tr.metrics:
119
119
  return "No Halstead details available"
@@ -42,7 +42,7 @@ class InterrogateParser(AbstractParser):
42
42
  score = total_coverage if total_coverage is not None else 1.0
43
43
  return ToolResult(raw=raw_result, metrics={"doc_coverage": score}, details=files)
44
44
 
45
- def format_llm_message(self, tr: ToolResult) -> str:
45
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
46
46
  score = tr.metrics.get("doc_coverage", 0)
47
47
  uncovered = sorted(
48
48
  [(f, d) for f, d in tr.details.items() if d.get("missing", 0) > 0],
@@ -0,0 +1,26 @@
1
+ """Parser that scores a tool by counting non-empty output lines as violations."""
2
+
3
+ from py_cq.localtypes import AbstractParser, RawResult, ToolResult
4
+ from py_cq.parsers.common import score_logistic_variant
5
+
6
+
7
+ class LineCountParser(AbstractParser):
8
+ """Score based on number of non-empty stdout lines.
9
+
10
+ parser_config keys:
11
+ scale_factor (int, default 15): passed to score_logistic_variant.
12
+ """
13
+
14
+ def parse(self, raw_result: RawResult) -> ToolResult:
15
+ lines = [ln for ln in (raw_result.stdout or "").splitlines() if ln.strip()]
16
+ count = len(lines)
17
+ scale = self.parser_config.get("scale_factor", 15)
18
+ score = score_logistic_variant(count, scale_factor=scale)
19
+ return ToolResult(raw=raw_result, metrics={"violations": score}, details={"lines": lines})
20
+
21
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
22
+ lines = tr.details.get("lines", [])
23
+ if not lines:
24
+ return "No violations found"
25
+ shown = lines[:context_lines]
26
+ return "\n".join(shown)
@@ -8,11 +8,33 @@ process return code so downstream components can uniformly consume results
8
8
  from multiple test tools. It is part of the test-collection framework and
9
9
  enables consistent handling of pytest output across the system."""
10
10
 
11
- import re
11
+ import re as _re
12
12
 
13
13
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
14
14
 
15
15
 
16
+ def _extract_failure(stdout: str, test_name: str, max_lines: int) -> str:
17
+ """Extract the failure section for test_name from pytest stdout."""
18
+ lines = stdout.splitlines()
19
+ pattern = _re.compile(rf"_{{4,}}\s+{_re.escape(test_name)}\s+_{{4,}}")
20
+ start = None
21
+ for i, line in enumerate(lines):
22
+ if pattern.search(line):
23
+ start = i + 1
24
+ break
25
+ if start is None:
26
+ return ""
27
+ collected = []
28
+ for line in lines[start:]:
29
+ if line.strip().startswith("_") or line.strip().startswith("="):
30
+ break
31
+ collected.append(line)
32
+ if len(collected) >= max_lines:
33
+ break
34
+ text = "\n".join(collected).strip()
35
+ return f"\n```\n{text}\n```" if text else ""
36
+
37
+
16
38
  class PytestParser(AbstractParser):
17
39
  """Parses raw pytest output into a structured `ToolResult`.
18
40
 
@@ -51,14 +73,14 @@ class PytestParser(AbstractParser):
51
73
  lines = raw_result.stdout.splitlines()
52
74
  tr = ToolResult(raw=raw_result)
53
75
  if "no tests ran" in raw_result.stdout:
54
- pass
76
+ tr.metrics["tests"] = 0.0
55
77
  else:
56
78
  tests_found = dict()
57
79
  num_tests = 0
58
80
  passed_tests = 0
59
81
  for line in lines:
60
82
  # tests/test_common.py::test_name[param] PASSED [ 8%]
61
- tests_match = re.search(r"(.*\.py)::([\w\[\].,+\- ]+) (PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS)", line)
83
+ tests_match = _re.search(r"(.*\.py)::([\w\[\].,+\- ]+) (PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS)", line)
62
84
  if tests_match:
63
85
  test_file = tests_match.group(1)
64
86
  test_name = tests_match.group(2).strip()
@@ -70,7 +92,7 @@ class PytestParser(AbstractParser):
70
92
  if num_tests == 0:
71
93
  # No individual test lines found (e.g. non-verbose output);
72
94
  # fall back to parsing the pytest summary line.
73
- summary = re.search(r"(\d+) passed(?:.*?(\d+) failed)?", raw_result.stdout)
95
+ summary = _re.search(r"(\d+) passed(?:.*?(\d+) failed)?", raw_result.stdout)
74
96
  if summary:
75
97
  passed_tests = int(summary.group(1))
76
98
  failed_tests = int(summary.group(2)) if summary.group(2) else 0
@@ -79,11 +101,32 @@ class PytestParser(AbstractParser):
79
101
  tr.details = tests_found
80
102
  return tr
81
103
 
82
- def format_llm_message(self, tr: ToolResult) -> str:
83
- """Return the first failing test as a defect description."""
104
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
105
+ """Return the first failing test with function body and failure output."""
106
+ from py_cq.parsers.common import find_function_source
84
107
  for file, tests in tr.details.items():
85
- if isinstance(tests, dict):
86
- for test_name, status in tests.items():
87
- if status == "FAILED":
88
- return f"`{file}::{test_name}` test **FAILED**"
108
+ if not isinstance(tests, dict):
109
+ continue
110
+ for test_name, status in tests.items():
111
+ if status != "FAILED":
112
+ continue
113
+ header = f"`{file}::{test_name}` — test **FAILED**"
114
+ body = find_function_source(file, test_name, max_lines=context_lines)
115
+ failure = _extract_failure(tr.raw.stdout, test_name, max_lines=context_lines)
116
+ parts = [header]
117
+ if body:
118
+ parts.append(body)
119
+ if failure:
120
+ parts.append(failure)
121
+ return "\n".join(parts)
122
+ if "no tests ran" in tr.raw.stdout:
123
+ return (
124
+ "**No tests found.** This project has no pytest test suite.\n\n"
125
+ "Add a `tests/` directory with at least one test file (e.g. `tests/test_basic.py`) "
126
+ "and write a first test covering a core function."
127
+ )
128
+ output = (tr.raw.stdout + tr.raw.stderr).strip()
129
+ if output:
130
+ tail = "\n".join(output.splitlines()[-30:])
131
+ return f"pytest reported failures:\n\n```\n{tail}\n```"
89
132
  return "pytest reported failures (no details available)"
@@ -0,0 +1,35 @@
1
+ """Parser that counts stdout lines matching a regex pattern."""
2
+
3
+ import re
4
+
5
+ from py_cq.localtypes import AbstractParser, RawResult, ToolResult
6
+ from py_cq.parsers.common import score_logistic_variant
7
+
8
+
9
+ class RegexCountParser(AbstractParser):
10
+ """Score based on the number of stdout lines matching a regex.
11
+
12
+ parser_config keys:
13
+ pattern (str, required): regex pattern to match against each line.
14
+ scale_factor (int, default 15): passed to score_logistic_variant.
15
+ """
16
+
17
+ def parse(self, raw_result: RawResult) -> ToolResult:
18
+ pattern = re.compile(self.parser_config["pattern"])
19
+ scale = self.parser_config.get("scale_factor", 15)
20
+ lines = (raw_result.stdout or "").splitlines()
21
+ matches = [ln for ln in lines if pattern.search(ln)]
22
+ count = len(matches)
23
+ score = score_logistic_variant(count, scale_factor=scale)
24
+ return ToolResult(
25
+ raw=raw_result,
26
+ metrics={"violations": score},
27
+ details={"count": count, "matches": matches},
28
+ )
29
+
30
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
31
+ matches = tr.details.get("matches", [])
32
+ if not matches:
33
+ return "No violations found"
34
+ shown = matches[:context_lines]
35
+ return "\n".join(shown)
@@ -45,7 +45,7 @@ class RuffParser(AbstractParser):
45
45
  )
46
46
  return ToolResult(raw=raw_result, metrics={"lint": score}, details=files)
47
47
 
48
- def format_llm_message(self, tr: ToolResult) -> str:
48
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
49
49
  """Return the first lint violation as a defect description."""
50
50
  if not tr.details:
51
51
  return "ruff reported issues (no details available)"
@@ -54,4 +54,4 @@ class RuffParser(AbstractParser):
54
54
  line = issue.get("line", "?")
55
55
  code = issue.get("code", "")
56
56
  message = issue.get("message", "")
57
- return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line)}"
57
+ return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line, count=context_lines)}"
py_cq/parsers/typarser.py CHANGED
@@ -49,7 +49,7 @@ class TyParser(AbstractParser):
49
49
  score = score_logistic_variant(weighted, scale_factor=10)
50
50
  return ToolResult(raw=raw_result, metrics={"type_check": score}, details=files)
51
51
 
52
- def format_llm_message(self, tr: ToolResult) -> str:
52
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
53
53
  """Return the first type-check diagnostic as a defect description."""
54
54
  if not tr.details:
55
55
  return "ty reported issues (no details available)"
@@ -58,4 +58,4 @@ class TyParser(AbstractParser):
58
58
  line = issue.get("line", "?")
59
59
  code = issue.get("code", "")
60
60
  message = issue.get("message", "")
61
- return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line)}"
61
+ return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line, count=context_lines)}"
@@ -36,7 +36,7 @@ class VultureParser(AbstractParser):
36
36
  score = score_logistic_variant(count, scale_factor=15)
37
37
  return ToolResult(raw=raw_result, metrics={"dead_code": score}, details=files)
38
38
 
39
- def format_llm_message(self, tr: ToolResult) -> str:
39
+ def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
40
40
  if not tr.details:
41
41
  return "vulture reported issues (no details available)"
42
42
  file, issues = next(iter(tr.details.items()))
@@ -45,4 +45,4 @@ class VultureParser(AbstractParser):
45
45
  kind = issue.get("type", "unused")
46
46
  name = issue.get("name", "")
47
47
  confidence = issue.get("confidence", "?")
48
- return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line)}"
48
+ return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
py_cq/tool_registry.py CHANGED
@@ -9,19 +9,19 @@ from py_cq.localtypes import ToolConfig
9
9
 
10
10
 
11
11
  def load_tool_configs() -> dict[str, ToolConfig]:
12
- """Load tool configurations from the bundled tools.yaml and return a registry.
12
+ """Load tool configurations from the bundled config.yaml and return a registry.
13
13
 
14
14
  Returns:
15
15
  dict[str, ToolConfig]: A mapping from tool ID to its configuration instance."""
16
- yaml_text = files("py_cq.config").joinpath("tools.yaml").read_text(encoding="utf-8")
16
+ yaml_text = files("py_cq.config").joinpath("config.yaml").read_text(encoding="utf-8")
17
17
  config = yaml.safe_load(yaml_text)
18
18
  registry = {}
19
- for tool_id, tool_data in config["tools"].items():
19
+ for tool_id, tool_data in config["python"].items():
20
20
  # Dynamically import parser class
21
21
  module = import_module(f"py_cq.parsers.{tool_data['parser'].lower()}")
22
22
  parser_class = getattr(module, tool_data["parser"])
23
23
  registry[tool_id] = ToolConfig(
24
- name=tool_data["name"],
24
+ name=tool_id,
25
25
  command=tool_data["command"],
26
26
  parser_class=parser_class,
27
27
  order=tool_data["order"],
@@ -29,6 +29,7 @@ def load_tool_configs() -> dict[str, ToolConfig]:
29
29
  error_threshold=tool_data["error_threshold"],
30
30
  run_in_target_env=tool_data.get("run_in_target_env", False),
31
31
  extra_deps=tool_data.get("extra_deps", []),
32
+ parser_config=tool_data.get("parser_config", {}),
32
33
  )
33
34
  return registry
34
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-code-quality
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
5
5
  Project-URL: Homepage, https://github.com/rhiza-fr/py-cq
6
6
  Project-URL: Repository, https://github.com/rhiza-fr/py-cq
@@ -28,20 +28,16 @@ Description-Content-Type: text/markdown
28
28
 
29
29
  Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
30
30
 
31
+ Why? It removes the mental burden of understanding all these tools and parsing their results.
32
+
31
33
  The primary workflow is:
32
34
 
33
35
  ```bash
34
36
  # get the single most critical defect as markdown
35
37
  cq check . -o llm
36
38
  ```
37
- Selects the single most critical defect using this priority order:
38
-
39
- 1. **Severity** — tools with score below `error_threshold` come before those only below `warning_threshold`
40
- 2. **Order** — among tools at the same severity, lower-order tools win (compile before lint before style)
41
- 3. **Score** — among ties, the lower score wins
42
39
 
43
- The code context is expanded if available.
44
- ```md
40
+ ```python
45
41
  `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
46
42
 
47
43
  18: min_dist = float("inf")
@@ -60,9 +56,10 @@ Feed to an LLM with edit tools and repeat until there are no issues, e.g.
60
56
  ```python
61
57
  cq check . -o llm | claude -p "fix this"
62
58
  # or
63
- cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
59
+ cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
64
60
  ```
65
61
 
62
+
66
63
  ## Install
67
64
 
68
65
  ```bash
@@ -70,21 +67,22 @@ cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
70
67
  uv tool install python-code-quality
71
68
 
72
69
  # or, clone it then install
73
- git pull https://github.com/rhiza-fr/py-cq.git
70
+ git clone https://github.com/rhiza-fr/py-cq.git
74
71
  cd py-cq
75
72
  uv tool install .
76
73
  ```
77
74
 
78
75
  ## Tools
79
76
 
80
- These tools are run in **parallel** except when looking for the first error in -o llm mode:
77
+ These tools are run in **parallel** except:
78
+ When running '-o llm', we run sequentially and exit early at the first error.
81
79
 
82
80
  | Order | Tool | Measures |
83
81
  |----------|------|----------|
84
82
  | 1 | compileall | Syntax errors |
85
- | 2 | bandit | Security vulnerabilities |
86
- | 3 | ruff | Lint / style |
87
- | 4 | ty | Type errors |
83
+ | 2 | ruff | Lint / style |
84
+ | 3 | ty | Type errors |
85
+ | 4 | bandit | Security vulnerabilities |
88
86
  | 5 | pytest | Test pass rate |
89
87
  | 6 | coverage | Test coverage |
90
88
  | 7 | radon cc | Cyclomatic complexity |
@@ -93,14 +91,14 @@ These tools are run in **parallel** except when looking for the first error in -
93
91
  | 10 | vulture | Dead code |
94
92
  | 11 | interrogate | Docstring coverage |
95
93
 
96
- Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
94
+ Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
97
95
 
98
96
 
99
97
  ## Usage
100
98
 
101
99
  ```bash
102
100
  cq check . # Table overview of scores for humans
103
- cq check -o llm # Top defect as markdown for LLMs
101
+ cq check . -o llm # Top defect as markdown for LLMs
104
102
  cq check . -o score # Numeric score only for CI
105
103
  cq check . -o json # Detailed parsed JSON output for jq
106
104
  cq check . -o raw # Raw tool output for debug
@@ -110,6 +108,13 @@ cq check . --clear-cache # Clear cached results before running (rarely needed)
110
108
  cq config path/to/project/ # Show effective tool configuration
111
109
  ```
112
110
 
111
+ **Exit codes:** `cq check` exits with code `1` if any tool metric falls below its `error_threshold`, making it suitable as a CI gate:
112
+
113
+ ```bash
114
+ cq check . && deploy # block deploy on errors
115
+ cq check . -o score # print score, exit 1 on errors
116
+ ```
117
+
113
118
  ## Table output
114
119
 
115
120
  ```bash
@@ -121,9 +126,9 @@ cq config path/to/project/ # Show effective tool configuration
121
126
  ┃ Tool ┃ Time ┃ Metric ┃ Score ┃ Status ┃
122
127
  ┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
123
128
  │ compile │ 0.42s │ compile │ 1.000 │ OK │
124
- │ bandit │ 0.56s │ security │ 1.000 │ OK │
125
129
  │ ruff │ 0.17s │ lint │ 1.000 │ OK │
126
130
  │ ty │ 0.33s │ type_check │ 1.000 │ OK │
131
+ │ bandit │ 0.56s │ security │ 1.000 │ OK │
127
132
  │ pytest │ 0.91s │ tests │ 1.000 │ OK │
128
133
  │ coverage │ 1.26s │ coverage │ 0.910 │ OK │
129
134
  │ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
@@ -167,7 +172,7 @@ cq config path/to/project/ # Show effective tool configuration
167
172
 
168
173
  ## Raw output
169
174
  ```bash
170
- > cq check -o raw
175
+ > cq check . -o raw
171
176
  ```
172
177
 
173
178
  ```json
@@ -193,66 +198,63 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
193
198
  # Skip tools that are slow or not relevant to your project
194
199
  disable = ["coverage", "interrogate"]
195
200
 
201
+ # Lines of source context shown around each defect in LLM output (default: 15)
202
+ context_lines = 15
203
+
196
204
  # Override warning/error thresholds per tool
197
205
  [tool.cq.thresholds.coverage]
198
206
  warning = 0.9
199
207
  error = 0.7
200
208
  ```
201
209
 
202
- Tool IDs match the keys in `config/tools.yaml`: `compilation`, `bandit`, `ruff`, `ty`, `pytest`, `coverage`, `complexity`, `maintainability`, `halstead`, `vulture`, `interrogate`.
210
+ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
203
211
 
204
212
 
205
213
  ### Default config
206
214
 
207
215
  ```yaml
208
- tools:
216
+ python:
209
217
 
210
- compilation:
211
- name: "compile"
218
+ compile:
212
219
  command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
213
220
  parser: "CompileParser"
214
221
  order: 1
215
222
  warning_threshold: 0.9999
216
223
  error_threshold: 0.9999
217
224
 
218
- bandit:
219
- name: "bandit"
220
- command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
221
- parser: "BanditParser"
222
- order: 2
223
- warning_threshold: 0.9999
224
- error_threshold: 0.8
225
-
226
225
  ruff:
227
- name: "ruff"
228
226
  command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
229
227
  parser: "RuffParser"
230
- order: 3
228
+ order: 2
231
229
  warning_threshold: 0.9999
232
230
  error_threshold: 0.9
233
231
 
234
232
  ty:
235
- name: "ty"
236
233
  command: "{python} -m ty check --output-format concise --color never {context_path}"
237
234
  parser: "TyParser"
238
- order: 4
235
+ order: 3
239
236
  warning_threshold: 0.9999
240
237
  error_threshold: 0.8
241
238
  run_in_target_env: true
242
239
  extra_deps:
243
240
  - ty
244
241
 
242
+ bandit:
243
+ command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
244
+ parser: "BanditParser"
245
+ order: 4
246
+ warning_threshold: 0.9999
247
+ error_threshold: 0.8
248
+
245
249
  pytest:
246
- name: "pytest"
247
250
  command: "{python} -m pytest -v {context_path}"
248
251
  parser: "PytestParser"
249
252
  order: 5
250
- warning_threshold: 0.7
251
- error_threshold: 0.5
253
+ warning_threshold: 1.0
254
+ error_threshold: 1.0
252
255
  run_in_target_env: true
253
256
 
254
257
  coverage:
255
- name: "coverage"
256
258
  command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
257
259
  parser: "CoverageParser"
258
260
  order: 6
@@ -263,24 +265,21 @@ tools:
263
265
  - coverage
264
266
  - pytest
265
267
 
266
- complexity:
267
- name: "radon cc"
268
+ radon-cc:
268
269
  command: "{python} -m radon cc --json {context_path}"
269
270
  parser: "ComplexityParser"
270
271
  order: 7
271
272
  warning_threshold: 0.6
272
273
  error_threshold: 0.4
273
274
 
274
- maintainability:
275
- name: "radon mi"
275
+ radon-mi:
276
276
  command: "{python} -m radon mi -s --json {context_path}"
277
277
  parser: "MaintainabilityParser"
278
278
  order: 8
279
279
  warning_threshold: 0.6
280
280
  error_threshold: 0.4
281
281
 
282
- halstead:
283
- name: "radon hal"
282
+ radon-hal:
284
283
  command: "{python} -m radon hal -f --json {context_path}"
285
284
  parser: "HalsteadParser"
286
285
  order: 9
@@ -288,7 +287,6 @@ tools:
288
287
  error_threshold: 0.3
289
288
 
290
289
  vulture:
291
- name: "vulture"
292
290
  command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
293
291
  parser: "VultureParser"
294
292
  order: 10
@@ -296,7 +294,6 @@ tools:
296
294
  error_threshold: 0.8
297
295
 
298
296
  interrogate:
299
- name: "interrogate"
300
297
  command: "{python} -m interrogate {context_path} -v --fail-under 0"
301
298
  parser: "InterrogateParser"
302
299
  order: 11
@@ -0,0 +1,34 @@
1
+ py_cq/__init__.py,sha256=rS7kf1RU1zZskvJlkbZaMqEpdRYPspwDPAFbxzF3tXg,373
2
+ py_cq/cli.py,sha256=wu1GlxSDxS835i9-mO4-xmyBLfr6puU-ES-26T7Mty0,11007
3
+ py_cq/context_hash.py,sha256=h-i7Rhd7AUfLv9SkQvE79bjJvTsm_ZwoVwSmUKXWmfM,2977
4
+ py_cq/execution_engine.py,sha256=tgNGFOO3h-EyetCEzC_RS2K-b9OkOFpOwGwrEAHIpZA,7477
5
+ py_cq/language_detector.py,sha256=6av5HaimcZ54RkN69xQmGgC0mxtTvGzPV3SL8NGG8Uc,1116
6
+ py_cq/llm_formatter.py,sha256=l74O5iqNWMR16789Xncoi93xOqgQJhW5IQK9_OyA9mE,1632
7
+ py_cq/localtypes.py,sha256=jCX1ZuwTixQooEk2N1Gi1XRuDcdBc2NlOdQBKysEybk,6128
8
+ py_cq/main.py,sha256=VKoXI8R8rB2fEROBYoTVURfinLqyh8XTNIIWAOtH7dw,380
9
+ py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
10
+ py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ py_cq/tool_registry.py,sha256=oMEkFHkU3gg5UpeGD4zHtynOYmWieRgDN5kTwZ5KsE8,1584
12
+ py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
13
+ py_cq/config/config.yaml,sha256=R8CjZH8erBQSbJFKgHt-CCwD1Mgj6ZCGQ3OtWH0Ebig,2402
14
+ py_cq/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ py_cq/parsers/banditparser.py,sha256=Ju_CkuXtkVn1Th9aQ6mv6fTUTpb9pc1YD8Nzt_nMgFQ,2326
16
+ py_cq/parsers/common.py,sha256=thbziNjqaf-SY3Y0W-KmPwzlcC-YCV98Z8jubUJM7_4,5203
17
+ py_cq/parsers/compileparser.py,sha256=YsT7ePUDRjsUHTLqgRv4x6jJBCTt6cbm1mgxplzxETg,6256
18
+ py_cq/parsers/complexityparser.py,sha256=2t1-wmNjUu65fULcIm5jcgv7ZLWwjakyiY_r-Fx1QQg,3983
19
+ py_cq/parsers/coverageparser.py,sha256=n4h_RCxKvJoYgp356PA4laAmFcngjQ41b_GXZvKkVgc,4041
20
+ py_cq/parsers/exitcodeparser.py,sha256=ZFL3EbPhGvFjm2qZhfLD1_5dNjR9ULYNQpKgy0Z_dGo,729
21
+ py_cq/parsers/halsteadparser.py,sha256=ZCN7LP1iUZ91tf3tlspKAPQrTEa78XVdE9Fjn9yPvYk,9008
22
+ py_cq/parsers/interrogateparser.py,sha256=G07beMaGR0Mq-pGVozAbwK0xOx9MdMaNVGq2CGrUb7w,2225
23
+ py_cq/parsers/linecountparser.py,sha256=nxWvFntuR8T6FDGpafrlevvt-jR3rwkbVo_t2JX4TF0,1067
24
+ py_cq/parsers/maintainabilityparser.py,sha256=Ax0ZFA6zzqYIWZH1hP1_GUtdVn2LIJ8SKWtqVNdszYs,3411
25
+ py_cq/parsers/pytestparser.py,sha256=3N1X4wKhJ2h-2U1GuM3gHWHpHxU9f0LPGcPaojra9W4,6377
26
+ py_cq/parsers/regexcountparser.py,sha256=KSoNh2spucXU06pxxr2QW0LrPLfJkFMAsmSgjooaFv0,1316
27
+ py_cq/parsers/ruffparser.py,sha256=ZdIya4sct2PrsOyfKfMGmXHLh3Qu7HtFqXNY9IuNFog,2275
28
+ py_cq/parsers/typarser.py,sha256=zrI0KS65MUGPxYPP74B6BRyTbjcPhKyQPjH2KZIyxN0,2495
29
+ py_cq/parsers/vultureparser.py,sha256=U6zC7P0ATA_N4SB90BahKF5QHMITf_z-NsOgrh_Q5rA,1995
30
+ python_code_quality-0.1.9.dist-info/METADATA,sha256=MtSk0DgDh6bkeATKhz03Y8thLZvxxuOcYlvfvOmHkD8,10149
31
+ python_code_quality-0.1.9.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
32
+ python_code_quality-0.1.9.dist-info/entry_points.txt,sha256=j5Q_gGr0b7389lddt1JlZ7gcL4Z7RHxuDhij_G-IhBY,39
33
+ python_code_quality-0.1.9.dist-info/licenses/LICENSE,sha256=Bpuh8tbf37so8M5NtRGTLmT5ue7diJ17223L9f9nsT0,1086
34
+ python_code_quality-0.1.9.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,30 +0,0 @@
1
- py_cq/__init__.py,sha256=rS7kf1RU1zZskvJlkbZaMqEpdRYPspwDPAFbxzF3tXg,373
2
- py_cq/cli.py,sha256=9rNdLdU2mcZVz3qdkczO4Rs_PU0-brZMmj5wJfv-Jcs,8688
3
- py_cq/context_hash.py,sha256=h-i7Rhd7AUfLv9SkQvE79bjJvTsm_ZwoVwSmUKXWmfM,2977
4
- py_cq/execution_engine.py,sha256=Q7Z8iibkE_E9VfkbUdnI_g5wA8GdwDbnJI4Mex-V8mE,7416
5
- py_cq/llm_formatter.py,sha256=EdUMhvsnPLplSSUKDknMHiaLdKsd9B6aH-tTdpxukdY,1574
6
- py_cq/localtypes.py,sha256=_PAx-F0cj03r_3YR1cyR9ilYYmYxUC14TkRtgLjH-Rc,5927
7
- py_cq/main.py,sha256=VKoXI8R8rB2fEROBYoTVURfinLqyh8XTNIIWAOtH7dw,380
8
- py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
9
- py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- py_cq/tool_registry.py,sha256=Ov5kQIRc5C5vkAq5Nc2Otp5kYiQzJuK5_nA-ZkY_-NQ,1529
11
- py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
12
- py_cq/config/tools.yaml,sha256=cNs4h4sIJA-j28QhEFG0uWL9ELaPy_z8BNo4l5F3is4,2553
13
- py_cq/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- py_cq/parsers/banditparser.py,sha256=vj23tTbipaeVkhS_ldWI7GrpHGwlIkOuaEgszkrjzh0,2277
15
- py_cq/parsers/common.py,sha256=lc9Chtr3H5l3vTk-vRVhptQVJfOeLax0UWTRjhA9IOU,4044
16
- py_cq/parsers/compileparser.py,sha256=mVY8qh1oZQ8n9GJLW2ruF9j89G5GuxWb7fb6JeogTJ4,6207
17
- py_cq/parsers/complexityparser.py,sha256=2t1-wmNjUu65fULcIm5jcgv7ZLWwjakyiY_r-Fx1QQg,3983
18
- py_cq/parsers/coverageparser.py,sha256=xDNlLNEsA0U3z4GV02iEq97IL90-UJAQrhMlFzpIdy0,4013
19
- py_cq/parsers/halsteadparser.py,sha256=9z_abpPPuclUQBgq4P6u2vIfIB7ZShX2NbFnubTatqI,8980
20
- py_cq/parsers/interrogateparser.py,sha256=eMROINtyZE2eHrRxVU0jA-nYTdvr0PZ8iERVn7kPH5o,2197
21
- py_cq/parsers/maintainabilityparser.py,sha256=Ax0ZFA6zzqYIWZH1hP1_GUtdVn2LIJ8SKWtqVNdszYs,3411
22
- py_cq/parsers/pytestparser.py,sha256=ERgS1aTTi7aB-Xk_Y9Xpo9h5jT9n0q3_vAfHCZwBTFE,4539
23
- py_cq/parsers/ruffparser.py,sha256=Wgch9rDkR-6tJlfhDfG1CXXEZu0Gfk0qRpsI3KzjCZU,2226
24
- py_cq/parsers/typarser.py,sha256=u7ktoH0TzmhB2saCJF1iY1LkfGui5ami2leLlYSCcns,2446
25
- py_cq/parsers/vultureparser.py,sha256=X2fen6yQu-r_zoRt1qKfIGlGZKORkHCiZE4BFlxtzdE,1946
26
- python_code_quality-0.1.8.dist-info/METADATA,sha256=2-E2SuWhsQMwYtv4NERKmE9CajsglC81tZY5DPPefrc,10266
27
- python_code_quality-0.1.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- python_code_quality-0.1.8.dist-info/entry_points.txt,sha256=j5Q_gGr0b7389lddt1JlZ7gcL4Z7RHxuDhij_G-IhBY,39
29
- python_code_quality-0.1.8.dist-info/licenses/LICENSE,sha256=Bpuh8tbf37so8M5NtRGTLmT5ue7diJ17223L9f9nsT0,1086
30
- python_code_quality-0.1.8.dist-info/RECORD,,