python-code-quality 0.1.6__py3-none-any.whl → 0.1.8__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 +16 -18
- py_cq/config/__init__.py +1 -3
- py_cq/config/tools.yaml +13 -13
- py_cq/execution_engine.py +21 -9
- py_cq/llm_formatter.py +1 -1
- py_cq/localtypes.py +4 -4
- py_cq/parsers/banditparser.py +2 -2
- py_cq/parsers/common.py +12 -0
- py_cq/parsers/compileparser.py +2 -8
- py_cq/parsers/halsteadparser.py +1 -1
- py_cq/parsers/pytestparser.py +8 -0
- py_cq/parsers/ruffparser.py +2 -6
- py_cq/parsers/typarser.py +2 -6
- py_cq/parsers/vultureparser.py +2 -2
- py_cq/tool_registry.py +1 -1
- {python_code_quality-0.1.6.dist-info → python_code_quality-0.1.8.dist-info}/METADATA +189 -55
- python_code_quality-0.1.8.dist-info/RECORD +30 -0
- py_cq/storage.py +0 -27
- python_code_quality-0.1.6.dist-info/RECORD +0 -31
- {python_code_quality-0.1.6.dist-info → python_code_quality-0.1.8.dist-info}/WHEEL +0 -0
- {python_code_quality-0.1.6.dist-info → python_code_quality-0.1.8.dist-info}/entry_points.txt +0 -0
- {python_code_quality-0.1.6.dist-info → python_code_quality-0.1.8.dist-info}/licenses/LICENSE +0 -0
py_cq/cli.py
CHANGED
|
@@ -23,12 +23,11 @@ from rich.console import Console
|
|
|
23
23
|
from rich.logging import RichHandler
|
|
24
24
|
from rich.table import Table
|
|
25
25
|
|
|
26
|
-
from py_cq.config import
|
|
26
|
+
from py_cq.config import load_user_config
|
|
27
27
|
from py_cq.execution_engine import _cache as tool_cache
|
|
28
28
|
from py_cq.execution_engine import run_tools
|
|
29
29
|
from py_cq.localtypes import CombinedToolResults, ToolConfig
|
|
30
30
|
from py_cq.metric_aggregator import aggregate_metrics
|
|
31
|
-
from py_cq.storage import save_result
|
|
32
31
|
from py_cq.tool_registry import tool_registry
|
|
33
32
|
|
|
34
33
|
logging.basicConfig(
|
|
@@ -44,6 +43,8 @@ app = typer.Typer(
|
|
|
44
43
|
" cq check . # full table with all metrics (default)\n\n"
|
|
45
44
|
" cq check . -o llm # top defect as markdown (primary LLM workflow)\n\n"
|
|
46
45
|
" cq check . -o score # numeric score only\n\n"
|
|
46
|
+
" cq check . -o json # parsed metrics as json\n\n"
|
|
47
|
+
" cq check . -o raw # unprocessed tool output as json\n\n"
|
|
47
48
|
" cq config . # show effective tool configuration"
|
|
48
49
|
),
|
|
49
50
|
)
|
|
@@ -74,11 +75,12 @@ class OutputMode(str, Enum):
|
|
|
74
75
|
SCORE = "score"
|
|
75
76
|
JSON = "json"
|
|
76
77
|
LLM = "llm"
|
|
78
|
+
RAW = "raw"
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
@app.callback()
|
|
80
82
|
def callback():
|
|
81
|
-
"""
|
|
83
|
+
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm"""
|
|
82
84
|
console = Console()
|
|
83
85
|
|
|
84
86
|
|
|
@@ -93,11 +95,6 @@ def check(
|
|
|
93
95
|
"--log-level",
|
|
94
96
|
help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
95
97
|
),
|
|
96
|
-
out_file: str = typer.Option(
|
|
97
|
-
DEFAULT_STORAGE_FILE,
|
|
98
|
-
"--out-file",
|
|
99
|
-
help="File path to save results in table mode",
|
|
100
|
-
),
|
|
101
98
|
clear_cache: bool = typer.Option(
|
|
102
99
|
False, "--clear-cache", help="Clear cached tool results before running"
|
|
103
100
|
),
|
|
@@ -105,7 +102,7 @@ def check(
|
|
|
105
102
|
0, "--workers", help="Max parallel workers (default: one per tool, use 1 for sequential)"
|
|
106
103
|
),
|
|
107
104
|
):
|
|
108
|
-
"""
|
|
105
|
+
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
109
106
|
path_obj = Path(path)
|
|
110
107
|
if not path_obj.exists():
|
|
111
108
|
raise typer.BadParameter(f"Path does not exist: {path}")
|
|
@@ -119,20 +116,21 @@ def check(
|
|
|
119
116
|
effective_registry = _apply_user_config(tool_registry, load_user_config(path_obj))
|
|
120
117
|
if clear_cache:
|
|
121
118
|
tool_cache.clear()
|
|
122
|
-
tool_results = run_tools(effective_registry.values(), path, workers)
|
|
123
|
-
for tr in tool_results:
|
|
124
|
-
|
|
119
|
+
tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM))
|
|
120
|
+
# for tr in tool_results:
|
|
121
|
+
# log.debug(json.dumps(tr.to_dict(), indent=2))
|
|
125
122
|
combined_metrics = aggregate_metrics(path=path, metrics=tool_results)
|
|
126
123
|
if output == OutputMode.SCORE:
|
|
127
124
|
console.print(combined_metrics.score)
|
|
128
125
|
elif output == OutputMode.JSON:
|
|
129
|
-
console.print(json.dumps(
|
|
126
|
+
console.print(json.dumps([tr.to_dict() for tr in tool_results], indent=2))
|
|
127
|
+
elif output == OutputMode.RAW:
|
|
128
|
+
console.print(json.dumps([tr.raw.to_dict() for tr in tool_results], indent=2))
|
|
130
129
|
elif output == OutputMode.LLM:
|
|
131
|
-
log.setLevel("CRITICAL")
|
|
130
|
+
# log.setLevel("CRITICAL")
|
|
132
131
|
from py_cq.llm_formatter import format_for_llm
|
|
133
132
|
console.print(format_for_llm(effective_registry, combined_metrics))
|
|
134
133
|
else:
|
|
135
|
-
save_result(combined_tool_results=combined_metrics, file_name=out_file)
|
|
136
134
|
console.print(format_as_table(combined_metrics, effective_registry))
|
|
137
135
|
|
|
138
136
|
|
|
@@ -169,18 +167,18 @@ def config(
|
|
|
169
167
|
|
|
170
168
|
table = Table()
|
|
171
169
|
table.add_column("Tool", style="cyan")
|
|
172
|
-
table.add_column("
|
|
170
|
+
table.add_column("Order", justify="right")
|
|
173
171
|
table.add_column("Warning", justify="right")
|
|
174
172
|
table.add_column("Error", justify="right")
|
|
175
173
|
table.add_column("Status", justify="center")
|
|
176
174
|
|
|
177
|
-
for tool_id in sorted(tool_registry, key=lambda t: tool_registry[t].
|
|
175
|
+
for tool_id in sorted(tool_registry, key=lambda t: tool_registry[t].order):
|
|
178
176
|
tc = effective_registry.get(tool_id, tool_registry[tool_id])
|
|
179
177
|
is_disabled = tool_id in disabled_ids
|
|
180
178
|
status = "[red]disabled[/red]" if is_disabled else "[green]enabled[/green]"
|
|
181
179
|
table.add_row(
|
|
182
180
|
tc.name,
|
|
183
|
-
str(tc.
|
|
181
|
+
str(tc.order),
|
|
184
182
|
f"{tc.warning_threshold:.2f}",
|
|
185
183
|
f"{tc.error_threshold:.2f}",
|
|
186
184
|
status,
|
py_cq/config/__init__.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""User config loader."""
|
|
2
2
|
|
|
3
3
|
import tomllib
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
DEFAULT_STORAGE_FILE = ".cq.json"
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
def load_user_config(project_path: Path) -> dict:
|
|
10
8
|
"""Read [tool.cq] from pyproject.toml at project_path, if present.
|
py_cq/config/tools.yaml
CHANGED
|
@@ -4,15 +4,15 @@ tools:
|
|
|
4
4
|
name: "compile"
|
|
5
5
|
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
6
6
|
parser: "CompileParser"
|
|
7
|
-
|
|
7
|
+
order: 1
|
|
8
8
|
warning_threshold: 0.9999
|
|
9
9
|
error_threshold: 0.9999
|
|
10
10
|
|
|
11
11
|
bandit:
|
|
12
12
|
name: "bandit"
|
|
13
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {
|
|
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
14
|
parser: "BanditParser"
|
|
15
|
-
|
|
15
|
+
order: 2
|
|
16
16
|
warning_threshold: 0.9999
|
|
17
17
|
error_threshold: 0.8
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ tools:
|
|
|
20
20
|
name: "ruff"
|
|
21
21
|
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
22
22
|
parser: "RuffParser"
|
|
23
|
-
|
|
23
|
+
order: 3
|
|
24
24
|
warning_threshold: 0.9999
|
|
25
25
|
error_threshold: 0.9
|
|
26
26
|
|
|
@@ -28,7 +28,7 @@ tools:
|
|
|
28
28
|
name: "ty"
|
|
29
29
|
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
30
30
|
parser: "TyParser"
|
|
31
|
-
|
|
31
|
+
order: 4
|
|
32
32
|
warning_threshold: 0.9999
|
|
33
33
|
error_threshold: 0.8
|
|
34
34
|
run_in_target_env: true
|
|
@@ -39,16 +39,16 @@ tools:
|
|
|
39
39
|
name: "pytest"
|
|
40
40
|
command: "{python} -m pytest -v {context_path}"
|
|
41
41
|
parser: "PytestParser"
|
|
42
|
-
|
|
42
|
+
order: 5
|
|
43
43
|
warning_threshold: 0.7
|
|
44
44
|
error_threshold: 0.5
|
|
45
45
|
run_in_target_env: true
|
|
46
46
|
|
|
47
47
|
coverage:
|
|
48
48
|
name: "coverage"
|
|
49
|
-
command: "{python} -m coverage run -m pytest {context_path} && {python} -m coverage report"
|
|
49
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
50
50
|
parser: "CoverageParser"
|
|
51
|
-
|
|
51
|
+
order: 6
|
|
52
52
|
warning_threshold: 0.9
|
|
53
53
|
error_threshold: 0.5
|
|
54
54
|
run_in_target_env: true
|
|
@@ -60,7 +60,7 @@ tools:
|
|
|
60
60
|
name: "radon cc"
|
|
61
61
|
command: "{python} -m radon cc --json {context_path}"
|
|
62
62
|
parser: "ComplexityParser"
|
|
63
|
-
|
|
63
|
+
order: 7
|
|
64
64
|
warning_threshold: 0.6
|
|
65
65
|
error_threshold: 0.4
|
|
66
66
|
|
|
@@ -68,7 +68,7 @@ tools:
|
|
|
68
68
|
name: "radon mi"
|
|
69
69
|
command: "{python} -m radon mi -s --json {context_path}"
|
|
70
70
|
parser: "MaintainabilityParser"
|
|
71
|
-
|
|
71
|
+
order: 8
|
|
72
72
|
warning_threshold: 0.6
|
|
73
73
|
error_threshold: 0.4
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ tools:
|
|
|
76
76
|
name: "radon hal"
|
|
77
77
|
command: "{python} -m radon hal -f --json {context_path}"
|
|
78
78
|
parser: "HalsteadParser"
|
|
79
|
-
|
|
79
|
+
order: 9
|
|
80
80
|
warning_threshold: 0.5
|
|
81
81
|
error_threshold: 0.3
|
|
82
82
|
|
|
@@ -84,7 +84,7 @@ tools:
|
|
|
84
84
|
name: "vulture"
|
|
85
85
|
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
86
86
|
parser: "VultureParser"
|
|
87
|
-
|
|
87
|
+
order: 10
|
|
88
88
|
warning_threshold: 0.9999
|
|
89
89
|
error_threshold: 0.8
|
|
90
90
|
|
|
@@ -92,6 +92,6 @@ tools:
|
|
|
92
92
|
name: "interrogate"
|
|
93
93
|
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
94
94
|
parser: "InterrogateParser"
|
|
95
|
-
|
|
95
|
+
order: 11
|
|
96
96
|
warning_threshold: 0.8
|
|
97
97
|
error_threshold: 0.3
|
py_cq/execution_engine.py
CHANGED
|
@@ -20,16 +20,16 @@ import time
|
|
|
20
20
|
from collections.abc import Collection
|
|
21
21
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import cast
|
|
23
|
+
from typing import Any, cast
|
|
24
24
|
|
|
25
|
-
import
|
|
25
|
+
from diskcache import Cache, JSONDisk
|
|
26
26
|
|
|
27
27
|
from py_cq.context_hash import get_context_hash
|
|
28
28
|
from py_cq.localtypes import RawResult, ToolConfig, ToolResult
|
|
29
29
|
|
|
30
30
|
log = logging.getLogger("cq")
|
|
31
31
|
|
|
32
|
-
_cache =
|
|
32
|
+
_cache = Cache(Path.home() / ".cache" / "cq", size_limit=100 * 1024 * 1024, disk=JSONDisk)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def _find_project_root(path: Path) -> Path | None:
|
|
@@ -75,12 +75,12 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
75
75
|
with_flags = " ".join(f"--with {dep}" for dep in tool_config.extra_deps)
|
|
76
76
|
python = f'"{uv}" run --directory "{abs_dir}" {with_flags}'.rstrip()
|
|
77
77
|
abs_context_path = str(Path(context_path).resolve())
|
|
78
|
-
|
|
79
|
-
command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path,
|
|
78
|
+
input_path_posix = Path(context_path).as_posix().rstrip("/")
|
|
79
|
+
command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, input_path_posix=input_path_posix, python=python)
|
|
80
80
|
cache_key = f"{command}:{get_context_hash(context_path)}"
|
|
81
81
|
if cache_key in _cache:
|
|
82
82
|
log.info(f"Cache hit: {command}")
|
|
83
|
-
return cast(
|
|
83
|
+
return RawResult(**cast(dict[str, Any], _cache[cache_key]))
|
|
84
84
|
log.info(f"Running: {command}")
|
|
85
85
|
result = subprocess.run(command, capture_output=True, text=True, shell=True) # nosec
|
|
86
86
|
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
@@ -92,11 +92,11 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
92
92
|
return_code=result.returncode,
|
|
93
93
|
timestamp=timestamp,
|
|
94
94
|
)
|
|
95
|
-
_cache
|
|
95
|
+
_cache.set(cache_key, raw_result.to_dict(), expire=5 * 24 * 60 * 60)
|
|
96
96
|
return raw_result
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0) -> list[ToolResult]:
|
|
99
|
+
def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False) -> list[ToolResult]:
|
|
100
100
|
"""Run multiple tools and return their parsed results.
|
|
101
101
|
|
|
102
102
|
Runs each tool specified in *tool_configs* on the file or directory at
|
|
@@ -141,12 +141,24 @@ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int
|
|
|
141
141
|
raw_result = run_tool(tool_config, path)
|
|
142
142
|
tr = tool_config.parser_class().parse(raw_result)
|
|
143
143
|
tr.duration_s = time.perf_counter() - t0
|
|
144
|
-
return tool_config.
|
|
144
|
+
return tool_config.order, tr
|
|
145
145
|
|
|
146
146
|
if not tool_configs:
|
|
147
147
|
return []
|
|
148
148
|
t_start = time.perf_counter()
|
|
149
149
|
prioritized: list[tuple[int, ToolResult]] = []
|
|
150
|
+
if early_exit:
|
|
151
|
+
for tool_config in sorted(tool_configs, key=lambda tc: tc.order):
|
|
152
|
+
try:
|
|
153
|
+
prioritized.append(_run_and_parse(tool_config))
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
log.error(f"{tool_config.name} generated an exception: {exc}")
|
|
156
|
+
break
|
|
157
|
+
_, tr = prioritized[-1]
|
|
158
|
+
if tr.metrics and min(tr.metrics.values()) < tool_config.error_threshold:
|
|
159
|
+
break
|
|
160
|
+
log.info(f"run_tools elapsed: {time.perf_counter() - t_start:.2f}s")
|
|
161
|
+
return [tr for _, tr in sorted(prioritized)]
|
|
150
162
|
with ThreadPoolExecutor(max_workers=max_workers or len(tool_configs)) as executor:
|
|
151
163
|
future_to_tool = {
|
|
152
164
|
executor.submit(_run_and_parse, tool_config): tool_config
|
py_cq/llm_formatter.py
CHANGED
py_cq/localtypes.py
CHANGED
|
@@ -10,13 +10,13 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
@dataclass
|
|
12
12
|
class ToolConfig:
|
|
13
|
-
"""Represents the configuration for an analysis tool, including its name, command, parser class, context path,
|
|
13
|
+
"""Represents the configuration for an analysis tool, including its name, command, parser class, context path, order, and thresholds for warnings and errors."""
|
|
14
14
|
|
|
15
15
|
name: str # e.g., "pytest", "coverage", "pydocstyle"
|
|
16
16
|
command: str # The command to execute (can include placeholders)
|
|
17
17
|
parser_class: Callable # Name of the parser class to use
|
|
18
18
|
context_path: str = "" # Path to project or file
|
|
19
|
-
|
|
19
|
+
order: int = 5 # 1=first (compilation), 11=last (style)
|
|
20
20
|
warning_threshold: float = 0.7 # Yellow warning if below this
|
|
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
|
|
@@ -73,11 +73,11 @@ class ToolResult:
|
|
|
73
73
|
self.metrics = {}
|
|
74
74
|
|
|
75
75
|
def to_dict(self) -> dict:
|
|
76
|
-
"""Returns a dictionary containing the metrics, details, and
|
|
76
|
+
"""Returns a dictionary containing the tool name, metrics, details, and duration."""
|
|
77
77
|
return {
|
|
78
|
+
"tool_name": self.raw.tool_name,
|
|
78
79
|
"metrics": self.metrics,
|
|
79
80
|
"details": self.details,
|
|
80
|
-
"raw": self.raw.to_dict(),
|
|
81
81
|
"duration_s": self.duration_s,
|
|
82
82
|
}
|
|
83
83
|
|
py_cq/parsers/banditparser.py
CHANGED
|
@@ -9,7 +9,7 @@ logistic-variant score stored under the ``security`` metric key.
|
|
|
9
9
|
import json
|
|
10
10
|
|
|
11
11
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
12
|
-
from py_cq.parsers.common import score_logistic_variant
|
|
12
|
+
from py_cq.parsers.common import format_source_context, score_logistic_variant
|
|
13
13
|
|
|
14
14
|
_SEVERITY_WEIGHT = {"HIGH": 5, "MEDIUM": 2, "LOW": 1}
|
|
15
15
|
|
|
@@ -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}"
|
|
54
|
+
return f"`{file}:{line}` — **{code}** [{severity}]: {message}{format_source_context(file, line)}"
|
py_cq/parsers/common.py
CHANGED
|
@@ -25,6 +25,18 @@ def read_source_lines(file_path: str, line: int, count: int = 5) -> str:
|
|
|
25
25
|
return ""
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def format_source_context(file: str, line: int | str, context: int = 3, count: int = 8) -> str:
|
|
29
|
+
"""Return a fenced python code block for the source around `line`, or '' if unavailable."""
|
|
30
|
+
if not isinstance(line, int):
|
|
31
|
+
return ""
|
|
32
|
+
context_start = max(1, line - context)
|
|
33
|
+
raw_lines = read_source_lines(file, context_start, count=count).splitlines()
|
|
34
|
+
if not raw_lines:
|
|
35
|
+
return ""
|
|
36
|
+
src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines))
|
|
37
|
+
return f"\n```python\n{src}\n```"
|
|
38
|
+
|
|
39
|
+
|
|
28
40
|
def inv_normalize(value: float, max_value: float) -> float:
|
|
29
41
|
"""Returns the inverse normalized value of `value` relative to `max_value`."""
|
|
30
42
|
return (max_value - min(value, max_value)) / max_value
|
py_cq/parsers/compileparser.py
CHANGED
|
@@ -6,7 +6,7 @@ compile score, and providing concise help messages for any failures."""
|
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
9
|
-
from py_cq.parsers.common import
|
|
9
|
+
from py_cq.parsers.common import format_source_context, score_logistic_variant
|
|
10
10
|
|
|
11
11
|
log = logging.getLogger("cq")
|
|
12
12
|
|
|
@@ -124,11 +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
|
-
|
|
128
|
-
context_start = max(1, line - 3)
|
|
129
|
-
raw_lines = read_source_lines(file, context_start, count=8).splitlines()
|
|
130
|
-
src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines)) if raw_lines else info.get("src", "")
|
|
131
|
-
else:
|
|
132
|
-
src = info.get("src", "")
|
|
133
|
-
code_block = f"\n```python\n{src}\n```" if src else ""
|
|
127
|
+
code_block = format_source_context(file, line) or (f"\n```python\n{info['src']}\n```" if info.get("src") else "")
|
|
134
128
|
return f"`{file}:{line}` — **{typ}**: {help_msg}{code_block}"
|
py_cq/parsers/halsteadparser.py
CHANGED
py_cq/parsers/pytestparser.py
CHANGED
|
@@ -67,6 +67,14 @@ class PytestParser(AbstractParser):
|
|
|
67
67
|
num_tests += 1
|
|
68
68
|
if test_status == "PASSED":
|
|
69
69
|
passed_tests += 1
|
|
70
|
+
if num_tests == 0:
|
|
71
|
+
# No individual test lines found (e.g. non-verbose output);
|
|
72
|
+
# fall back to parsing the pytest summary line.
|
|
73
|
+
summary = re.search(r"(\d+) passed(?:.*?(\d+) failed)?", raw_result.stdout)
|
|
74
|
+
if summary:
|
|
75
|
+
passed_tests = int(summary.group(1))
|
|
76
|
+
failed_tests = int(summary.group(2)) if summary.group(2) else 0
|
|
77
|
+
num_tests = passed_tests + failed_tests
|
|
70
78
|
tr.metrics["tests"] = passed_tests / num_tests if num_tests else 0
|
|
71
79
|
tr.details = tests_found
|
|
72
80
|
return tr
|
py_cq/parsers/ruffparser.py
CHANGED
|
@@ -13,7 +13,7 @@ followed by a summary line ``Found N error.`` or ``All checks passed!``."""
|
|
|
13
13
|
import re
|
|
14
14
|
|
|
15
15
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
16
|
-
from py_cq.parsers.common import
|
|
16
|
+
from py_cq.parsers.common import format_source_context, score_logistic_variant
|
|
17
17
|
|
|
18
18
|
_DIAG_RE = re.compile(r"^(.+):(\d+):(\d+): ([A-Z]\d+) (.+)$")
|
|
19
19
|
|
|
@@ -54,8 +54,4 @@ class RuffParser(AbstractParser):
|
|
|
54
54
|
line = issue.get("line", "?")
|
|
55
55
|
code = issue.get("code", "")
|
|
56
56
|
message = issue.get("message", "")
|
|
57
|
-
|
|
58
|
-
raw_lines = read_source_lines(file, context_start, count=8).splitlines() if isinstance(line, int) else []
|
|
59
|
-
src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines)) if raw_lines else ""
|
|
60
|
-
code_block = f"\n```python\n{src}\n```" if src else ""
|
|
61
|
-
return f"`{file}:{line}` — **{code}**: {message}{code_block}"
|
|
57
|
+
return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line)}"
|
py_cq/parsers/typarser.py
CHANGED
|
@@ -14,7 +14,7 @@ Errors count more heavily than warnings toward the score."""
|
|
|
14
14
|
import re
|
|
15
15
|
|
|
16
16
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
17
|
-
from py_cq.parsers.common import
|
|
17
|
+
from py_cq.parsers.common import format_source_context, score_logistic_variant
|
|
18
18
|
|
|
19
19
|
_DIAG_RE = re.compile(r"^(.+):(\d+):\d+:\s+(error|warning)\[([^\]]+)\] (.+)$")
|
|
20
20
|
|
|
@@ -58,8 +58,4 @@ class TyParser(AbstractParser):
|
|
|
58
58
|
line = issue.get("line", "?")
|
|
59
59
|
code = issue.get("code", "")
|
|
60
60
|
message = issue.get("message", "")
|
|
61
|
-
|
|
62
|
-
raw_lines = read_source_lines(file, context_start, count=8).splitlines() if isinstance(line, int) else []
|
|
63
|
-
src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines)) if raw_lines else ""
|
|
64
|
-
code_block = f"\n```python\n{src}\n```" if src else ""
|
|
65
|
-
return f"`{file}:{line}` — **{code}**: {message}{code_block}"
|
|
61
|
+
return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line)}"
|
py_cq/parsers/vultureparser.py
CHANGED
|
@@ -12,7 +12,7 @@ score stored under the ``dead_code`` metric key.
|
|
|
12
12
|
import re
|
|
13
13
|
|
|
14
14
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
15
|
-
from py_cq.parsers.common import score_logistic_variant
|
|
15
|
+
from py_cq.parsers.common import format_source_context, score_logistic_variant
|
|
16
16
|
|
|
17
17
|
_LINE_RE = re.compile(r"^(.+):(\d+): (unused \S+) '(.+)' \((\d+)% confidence\)$")
|
|
18
18
|
|
|
@@ -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)"
|
|
48
|
+
return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line)}"
|
py_cq/tool_registry.py
CHANGED
|
@@ -24,7 +24,7 @@ def load_tool_configs() -> dict[str, ToolConfig]:
|
|
|
24
24
|
name=tool_data["name"],
|
|
25
25
|
command=tool_data["command"],
|
|
26
26
|
parser_class=parser_class,
|
|
27
|
-
|
|
27
|
+
order=tool_data["order"],
|
|
28
28
|
warning_threshold=tool_data["warning_threshold"],
|
|
29
29
|
error_threshold=tool_data["error_threshold"],
|
|
30
30
|
run_in_target_env=tool_data.get("run_in_target_env", False),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ
|
|
3
|
+
Version: 0.1.8
|
|
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
|
|
7
7
|
Author-email: Chris Kilner <chris@rhiza.fr>
|
|
@@ -26,20 +26,50 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# CQ - Python Code Quality Analysis Tool
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
|
|
30
|
+
|
|
31
|
+
The primary workflow is:
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
|
-
|
|
34
|
+
# get the single most critical defect as markdown
|
|
35
|
+
cq check . -o llm
|
|
33
36
|
```
|
|
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
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
The code context is expanded if available.
|
|
44
|
+
```md
|
|
45
|
+
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
46
|
+
|
|
47
|
+
18: min_dist = float("inf")
|
|
48
|
+
19: nearest_city = None
|
|
49
|
+
20: for city in cities:
|
|
50
|
+
21: unused_variable = 67
|
|
51
|
+
22: dist = calc_dist(current_city, city)
|
|
52
|
+
23: if dist < min_dist:
|
|
53
|
+
24: min_dist = dist
|
|
54
|
+
25: nearest_city = city
|
|
55
|
+
|
|
56
|
+
Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
|
|
57
|
+
```
|
|
58
|
+
Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
cq check . -o llm | claude -p "fix this"
|
|
62
|
+
# or
|
|
63
|
+
cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
|
|
64
|
+
```
|
|
36
65
|
|
|
37
66
|
## Install
|
|
38
67
|
|
|
39
68
|
```bash
|
|
69
|
+
# install the `cq` command line tool from PyPi
|
|
40
70
|
uv tool install python-code-quality
|
|
41
71
|
|
|
42
|
-
# or
|
|
72
|
+
# or, clone it then install
|
|
43
73
|
git pull https://github.com/rhiza-fr/py-cq.git
|
|
44
74
|
cd py-cq
|
|
45
75
|
uv tool install .
|
|
@@ -47,9 +77,9 @@ uv tool install .
|
|
|
47
77
|
|
|
48
78
|
## Tools
|
|
49
79
|
|
|
50
|
-
|
|
80
|
+
These tools are run in **parallel** except when looking for the first error in -o llm mode:
|
|
51
81
|
|
|
52
|
-
|
|
|
82
|
+
| Order | Tool | Measures |
|
|
53
83
|
|----------|------|----------|
|
|
54
84
|
| 1 | compileall | Syntax errors |
|
|
55
85
|
| 2 | bandit | Security vulnerabilities |
|
|
@@ -63,40 +93,24 @@ CQ runs these tools in *parallel*:
|
|
|
63
93
|
| 10 | vulture | Dead code |
|
|
64
94
|
| 11 | interrogate | Docstring coverage |
|
|
65
95
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
# LLM workflow: get the top defect as markdown (primary use case)
|
|
70
|
-
cq check -o llm
|
|
71
|
-
|
|
72
|
-
# Rich table with all metrics (default, also saves .cq.json)
|
|
73
|
-
cq check
|
|
96
|
+
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
|
|
74
97
|
|
|
75
|
-
# Numeric score only — useful in CI or scripts
|
|
76
|
-
cq check -o score
|
|
77
98
|
|
|
78
|
-
|
|
79
|
-
cq check -o json
|
|
80
|
-
|
|
81
|
-
# Explicit path (defaults to current directory)
|
|
82
|
-
cq check path/to/project/
|
|
83
|
-
cq check path/to/file.py
|
|
84
|
-
|
|
85
|
-
# Run sequentially (1 worker) instead of in parallel
|
|
86
|
-
cq check --workers 1
|
|
87
|
-
|
|
88
|
-
# Clear cached results before running
|
|
89
|
-
cq check --clear-cache
|
|
90
|
-
|
|
91
|
-
# Save table output to a custom file
|
|
92
|
-
cq check --out-file custom_results.json
|
|
99
|
+
## Usage
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
cq
|
|
96
|
-
cq
|
|
101
|
+
```bash
|
|
102
|
+
cq check . # Table overview of scores for humans
|
|
103
|
+
cq check -o llm # Top defect as markdown for LLMs
|
|
104
|
+
cq check . -o score # Numeric score only for CI
|
|
105
|
+
cq check . -o json # Detailed parsed JSON output for jq
|
|
106
|
+
cq check . -o raw # Raw tool output for debug
|
|
107
|
+
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
108
|
+
cq check . --workers 1 # Run sequentially if you like things slow
|
|
109
|
+
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
110
|
+
cq config path/to/project/ # Show effective tool configuration
|
|
97
111
|
```
|
|
98
112
|
|
|
99
|
-
##
|
|
113
|
+
## Table output
|
|
100
114
|
|
|
101
115
|
```bash
|
|
102
116
|
> cq check .
|
|
@@ -123,6 +137,8 @@ cq config path/to/project/
|
|
|
123
137
|
│ │ │ Score │ 0.965 │ │
|
|
124
138
|
└──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
|
|
125
139
|
```
|
|
140
|
+
|
|
141
|
+
## Single score output
|
|
126
142
|
```bash
|
|
127
143
|
> cq check . -o score
|
|
128
144
|
```
|
|
@@ -130,23 +146,42 @@ cq config path/to/project/
|
|
|
130
146
|
0.9662730667181059 # this is designed to approach but not reach 1.0
|
|
131
147
|
```
|
|
132
148
|
|
|
149
|
+
## Json output
|
|
133
150
|
```bash
|
|
134
|
-
> cq check . -o
|
|
151
|
+
> cq check . -o json
|
|
135
152
|
```
|
|
136
153
|
|
|
137
|
-
```
|
|
138
|
-
|
|
154
|
+
```json
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
"tool_name": "compile",
|
|
158
|
+
"metrics": {
|
|
159
|
+
"compile": 1.0
|
|
160
|
+
},
|
|
161
|
+
"details": {},
|
|
162
|
+
"duration_s": 0.05611889995634556
|
|
163
|
+
}
|
|
164
|
+
...
|
|
165
|
+
]
|
|
166
|
+
```
|
|
139
167
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
22: dist = calc_dist(current_city, city)
|
|
145
|
-
23: if dist < min_dist:
|
|
146
|
-
24: min_dist = dist
|
|
147
|
-
25: nearest_city = city
|
|
168
|
+
## Raw output
|
|
169
|
+
```bash
|
|
170
|
+
> cq check -o raw
|
|
171
|
+
```
|
|
148
172
|
|
|
149
|
-
|
|
173
|
+
```json
|
|
174
|
+
[
|
|
175
|
+
{
|
|
176
|
+
"tool_name": "compile",
|
|
177
|
+
"command": "D:\\ai\\py-cq\\.venv\\Scripts\\python.exe -m compileall -r 10 -j 8 . -x .*venv",
|
|
178
|
+
"stdout": "",
|
|
179
|
+
"stderr": "",
|
|
180
|
+
"return_code": 0,
|
|
181
|
+
"timestamp": "2026-02-20 10:01:22"
|
|
182
|
+
}
|
|
183
|
+
...
|
|
184
|
+
]
|
|
150
185
|
```
|
|
151
186
|
|
|
152
187
|
## Configuration
|
|
@@ -166,14 +201,111 @@ error = 0.7
|
|
|
166
201
|
|
|
167
202
|
Tool IDs match the keys in `config/tools.yaml`: `compilation`, `bandit`, `ruff`, `ty`, `pytest`, `coverage`, `complexity`, `maintainability`, `halstead`, `vulture`, `interrogate`.
|
|
168
203
|
|
|
169
|
-
## LLM workflow
|
|
170
204
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
205
|
+
### Default config
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
tools:
|
|
209
|
+
|
|
210
|
+
compilation:
|
|
211
|
+
name: "compile"
|
|
212
|
+
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
213
|
+
parser: "CompileParser"
|
|
214
|
+
order: 1
|
|
215
|
+
warning_threshold: 0.9999
|
|
216
|
+
error_threshold: 0.9999
|
|
217
|
+
|
|
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
|
+
ruff:
|
|
227
|
+
name: "ruff"
|
|
228
|
+
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
229
|
+
parser: "RuffParser"
|
|
230
|
+
order: 3
|
|
231
|
+
warning_threshold: 0.9999
|
|
232
|
+
error_threshold: 0.9
|
|
233
|
+
|
|
234
|
+
ty:
|
|
235
|
+
name: "ty"
|
|
236
|
+
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
237
|
+
parser: "TyParser"
|
|
238
|
+
order: 4
|
|
239
|
+
warning_threshold: 0.9999
|
|
240
|
+
error_threshold: 0.8
|
|
241
|
+
run_in_target_env: true
|
|
242
|
+
extra_deps:
|
|
243
|
+
- ty
|
|
244
|
+
|
|
245
|
+
pytest:
|
|
246
|
+
name: "pytest"
|
|
247
|
+
command: "{python} -m pytest -v {context_path}"
|
|
248
|
+
parser: "PytestParser"
|
|
249
|
+
order: 5
|
|
250
|
+
warning_threshold: 0.7
|
|
251
|
+
error_threshold: 0.5
|
|
252
|
+
run_in_target_env: true
|
|
253
|
+
|
|
254
|
+
coverage:
|
|
255
|
+
name: "coverage"
|
|
256
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
257
|
+
parser: "CoverageParser"
|
|
258
|
+
order: 6
|
|
259
|
+
warning_threshold: 0.9
|
|
260
|
+
error_threshold: 0.5
|
|
261
|
+
run_in_target_env: true
|
|
262
|
+
extra_deps:
|
|
263
|
+
- coverage
|
|
264
|
+
- pytest
|
|
265
|
+
|
|
266
|
+
complexity:
|
|
267
|
+
name: "radon cc"
|
|
268
|
+
command: "{python} -m radon cc --json {context_path}"
|
|
269
|
+
parser: "ComplexityParser"
|
|
270
|
+
order: 7
|
|
271
|
+
warning_threshold: 0.6
|
|
272
|
+
error_threshold: 0.4
|
|
273
|
+
|
|
274
|
+
maintainability:
|
|
275
|
+
name: "radon mi"
|
|
276
|
+
command: "{python} -m radon mi -s --json {context_path}"
|
|
277
|
+
parser: "MaintainabilityParser"
|
|
278
|
+
order: 8
|
|
279
|
+
warning_threshold: 0.6
|
|
280
|
+
error_threshold: 0.4
|
|
281
|
+
|
|
282
|
+
halstead:
|
|
283
|
+
name: "radon hal"
|
|
284
|
+
command: "{python} -m radon hal -f --json {context_path}"
|
|
285
|
+
parser: "HalsteadParser"
|
|
286
|
+
order: 9
|
|
287
|
+
warning_threshold: 0.5
|
|
288
|
+
error_threshold: 0.3
|
|
289
|
+
|
|
290
|
+
vulture:
|
|
291
|
+
name: "vulture"
|
|
292
|
+
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
293
|
+
parser: "VultureParser"
|
|
294
|
+
order: 10
|
|
295
|
+
warning_threshold: 0.9999
|
|
296
|
+
error_threshold: 0.8
|
|
297
|
+
|
|
298
|
+
interrogate:
|
|
299
|
+
name: "interrogate"
|
|
300
|
+
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
301
|
+
parser: "InterrogateParser"
|
|
302
|
+
order: 11
|
|
303
|
+
warning_threshold: 0.8
|
|
304
|
+
error_threshold: 0.3
|
|
175
305
|
|
|
176
|
-
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Respect
|
|
177
309
|
|
|
178
310
|
Many thanks to all the wonderful maintainers of :
|
|
179
311
|
|
|
@@ -186,3 +318,5 @@ Many thanks to all the wonderful maintainers of :
|
|
|
186
318
|
- [radon](https://github.com/rubik/radon)
|
|
187
319
|
- [vulture](https://github.com/jendrikseipp/vulture)
|
|
188
320
|
- [interrogate](https://github.com/econchick/interrogate)
|
|
321
|
+
- [diskcache](https://github.com/grantjenks/python-diskcache)
|
|
322
|
+
- [typer](https://github.com/fastapi/typer)
|
|
@@ -0,0 +1,30 @@
|
|
|
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,,
|
py_cq/storage.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"""Utilities for persisting combined tool results.
|
|
2
|
-
|
|
3
|
-
This module provides a single helper function, :func:`save_result`, which
|
|
4
|
-
serializes a :class:`CombinedToolResults` instance to a JSON file. The
|
|
5
|
-
function ensures that the target directory exists, writes a readable
|
|
6
|
-
representation of the results, and returns the absolute path of the created
|
|
7
|
-
file.
|
|
8
|
-
|
|
9
|
-
Example
|
|
10
|
-
-------
|
|
11
|
-
>>> from your_package import CombinedToolResults, save_result
|
|
12
|
-
>>> results = CombinedToolResults(...)
|
|
13
|
-
>>> output_path = save_result(results, "output.json")
|
|
14
|
-
>>> print(output_path)"""
|
|
15
|
-
|
|
16
|
-
import json
|
|
17
|
-
|
|
18
|
-
from py_cq.localtypes import CombinedToolResults
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def save_result(combined_tool_results: CombinedToolResults, file_name: str):
|
|
22
|
-
"""Saves combined tool results to a JSON file named by `file_name`."""
|
|
23
|
-
if not file_name:
|
|
24
|
-
return
|
|
25
|
-
data = combined_tool_results.to_dict()
|
|
26
|
-
with open(file_name, "w") as f:
|
|
27
|
-
json.dump(data, f, indent=4)
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
py_cq/__init__.py,sha256=rS7kf1RU1zZskvJlkbZaMqEpdRYPspwDPAFbxzF3tXg,373
|
|
2
|
-
py_cq/cli.py,sha256=M0t5noGEt-a1TmTaDvsGKfApcpOZnXjXHiUo_tSdcjk,8586
|
|
3
|
-
py_cq/context_hash.py,sha256=h-i7Rhd7AUfLv9SkQvE79bjJvTsm_ZwoVwSmUKXWmfM,2977
|
|
4
|
-
py_cq/execution_engine.py,sha256=xNZHLVbkNf97FwDI29R1_njyy-xrlHD_RDUX86PLlQU,6675
|
|
5
|
-
py_cq/llm_formatter.py,sha256=tOjoWIFEq3ZRGKLcwAnLswQ00B79sM3yJT3K3bh857A,1577
|
|
6
|
-
py_cq/localtypes.py,sha256=Ix871IDcZh8_EldhOrmvR4TPxvWfDi0YhZVSGpI6pl0,5961
|
|
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/storage.py,sha256=J8360OUnGFyQAl_jJ6Eoy5hn7DDpShQvqaU6dFtfrkc,916
|
|
11
|
-
py_cq/tool_registry.py,sha256=9QFqeXxrK7NF-qMd9ZxP5zfqc0nsHVQAbrbAqinAA4g,1535
|
|
12
|
-
py_cq/config/__init__.py,sha256=JZgIe38Lq0XYd-zwOVzi6SxobYBc4lofhidmU4T9_oU,730
|
|
13
|
-
py_cq/config/tools.yaml,sha256=ajZANuj2vBsXF70dVzFW1zMvNjnJ9ns3kkIr2P6Cexg,2518
|
|
14
|
-
py_cq/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
py_cq/parsers/banditparser.py,sha256=G4wg7Jry6GsrHm5LOfTAvBuwoA6PvGqz8OKdNf0Ifs4,2219
|
|
16
|
-
py_cq/parsers/common.py,sha256=FWkQIm03IKoCB7pvW4MRbQKjC_quINIKkGq5xKk86qM,3506
|
|
17
|
-
py_cq/parsers/compileparser.py,sha256=GcIcKGVSL8W3zTwfA7WzD045-gNdSeSkljOrPA0qG0M,6496
|
|
18
|
-
py_cq/parsers/complexityparser.py,sha256=2t1-wmNjUu65fULcIm5jcgv7ZLWwjakyiY_r-Fx1QQg,3983
|
|
19
|
-
py_cq/parsers/coverageparser.py,sha256=xDNlLNEsA0U3z4GV02iEq97IL90-UJAQrhMlFzpIdy0,4013
|
|
20
|
-
py_cq/parsers/halsteadparser.py,sha256=Rj690m_C4KRdlfrTC_bXtT5_-oJPKT38I7V-v1CDoJY,8980
|
|
21
|
-
py_cq/parsers/interrogateparser.py,sha256=eMROINtyZE2eHrRxVU0jA-nYTdvr0PZ8iERVn7kPH5o,2197
|
|
22
|
-
py_cq/parsers/maintainabilityparser.py,sha256=Ax0ZFA6zzqYIWZH1hP1_GUtdVn2LIJ8SKWtqVNdszYs,3411
|
|
23
|
-
py_cq/parsers/pytestparser.py,sha256=FrAaNJfDHZmEr_vRYQpp04qRj4Cq3jaDhpnAFxLv5dQ,4047
|
|
24
|
-
py_cq/parsers/ruffparser.py,sha256=WMJhsYA5wwBcIhqGSfjBxy78RGv_iCJJ1Qdmt1dqTOw,2567
|
|
25
|
-
py_cq/parsers/typarser.py,sha256=bC6-P-NGBMJBLyr1DkX1yMtnTS8afuRO0Keb_XyBeg0,2787
|
|
26
|
-
py_cq/parsers/vultureparser.py,sha256=fe-LPagivKGtmf6Kyu_gT0OrWJjowAsJ2OhXQiv_zQY,1888
|
|
27
|
-
python_code_quality-0.1.6.dist-info/METADATA,sha256=90Ylk0f3eOWGyq2Nw1OmoZ0897OEzTQay0lSfLY-EtY,6745
|
|
28
|
-
python_code_quality-0.1.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
29
|
-
python_code_quality-0.1.6.dist-info/entry_points.txt,sha256=j5Q_gGr0b7389lddt1JlZ7gcL4Z7RHxuDhij_G-IhBY,39
|
|
30
|
-
python_code_quality-0.1.6.dist-info/licenses/LICENSE,sha256=Bpuh8tbf37so8M5NtRGTLmT5ue7diJ17223L9f9nsT0,1086
|
|
31
|
-
python_code_quality-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
{python_code_quality-0.1.6.dist-info → python_code_quality-0.1.8.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{python_code_quality-0.1.6.dist-info → python_code_quality-0.1.8.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|