python-code-quality 0.1.15__tar.gz → 0.1.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/PKG-INFO +1 -1
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/pyproject.toml +10 -1
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/cli.py +5 -42
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/main.py +1 -1
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/banditparser.py +6 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/common.py +3 -10
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/compileparser.py +12 -5
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/complexityparser.py +8 -1
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/coverageparser.py +3 -3
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/halsteadparser.py +8 -1
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/interrogateparser.py +4 -4
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/maintainabilityparser.py +8 -1
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/ruffparser.py +4 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/typarser.py +4 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/vultureparser.py +4 -0
- python_code_quality-0.1.16/src/py_cq/table_formatter.py +29 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/README.md +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/__init__.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/config/__init__.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/config/config.yaml +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/execution_engine.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/language_detector.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/llm_formatter.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/localtypes.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/exitcodeparser.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/linecountparser.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/pytestparser.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/regexcountparser.py +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/py.typed +0 -0
- {python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/tool_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.16
|
|
4
4
|
Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
|
|
5
5
|
Author: Chris Kilner
|
|
6
6
|
Author-email: Chris Kilner <chris@rhiza.fr>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-code-quality"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.16"
|
|
4
4
|
description = "Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -47,5 +47,14 @@ cq = "py_cq.main:main"
|
|
|
47
47
|
testpaths = ["tests"]
|
|
48
48
|
norecursedirs = [".venv", "dist"]
|
|
49
49
|
|
|
50
|
+
[[tool.ty.overrides]]
|
|
51
|
+
include = ["demo/**"]
|
|
52
|
+
rules = {unresolved-import = "ignore"}
|
|
53
|
+
|
|
50
54
|
[tool.cq]
|
|
51
55
|
exclude = ["demo"]
|
|
56
|
+
|
|
57
|
+
[dependency-groups]
|
|
58
|
+
dev = [
|
|
59
|
+
"hypothesis>=6.151.10",
|
|
60
|
+
]
|
|
@@ -29,8 +29,9 @@ from py_cq.config import load_user_config
|
|
|
29
29
|
from py_cq.execution_engine import _cache as tool_cache
|
|
30
30
|
from py_cq.execution_engine import run_tools
|
|
31
31
|
from py_cq.language_detector import detect_language
|
|
32
|
-
from py_cq.localtypes import
|
|
32
|
+
from py_cq.localtypes import ToolConfig
|
|
33
33
|
from py_cq.metric_aggregator import aggregate_metrics
|
|
34
|
+
from py_cq.table_formatter import format_as_table
|
|
34
35
|
from py_cq.tool_registry import tool_registry
|
|
35
36
|
|
|
36
37
|
logging.basicConfig(
|
|
@@ -106,7 +107,7 @@ def _version_callback(value: bool) -> None:
|
|
|
106
107
|
return
|
|
107
108
|
import re
|
|
108
109
|
import sys
|
|
109
|
-
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
110
|
+
if isinstance(sys.stdout, io.TextIOWrapper): # pragma: no branch
|
|
110
111
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
111
112
|
pkg = "python-code-quality"
|
|
112
113
|
pkg_version = version(pkg)
|
|
@@ -185,7 +186,7 @@ def check(
|
|
|
185
186
|
if path_obj.is_file():
|
|
186
187
|
if path_obj.suffix != ".py":
|
|
187
188
|
raise typer.BadParameter(f"File must be a Python file (.py): {path}")
|
|
188
|
-
elif path_obj.is_dir():
|
|
189
|
+
elif path_obj.is_dir(): # pragma: no branch
|
|
189
190
|
if not (path_obj / "pyproject.toml").exists():
|
|
190
191
|
raise typer.BadParameter(f"Directory must contain pyproject.toml: {path}")
|
|
191
192
|
log.setLevel(log_level)
|
|
@@ -216,7 +217,7 @@ def check(
|
|
|
216
217
|
elif output == OutputMode.LLM:
|
|
217
218
|
# log.setLevel("CRITICAL")
|
|
218
219
|
from py_cq.llm_formatter import format_for_llm
|
|
219
|
-
|
|
220
|
+
print(format_for_llm(effective_registry, combined_metrics, context_lines=context_lines))
|
|
220
221
|
else:
|
|
221
222
|
console.print(f"[bold green]{path_obj.resolve()}[/]")
|
|
222
223
|
console.print(format_as_table(combined_metrics, effective_registry))
|
|
@@ -284,41 +285,3 @@ def config(
|
|
|
284
285
|
console.print(table)
|
|
285
286
|
|
|
286
287
|
|
|
287
|
-
def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig]):
|
|
288
|
-
"""Format combined tool results into a Rich Table.
|
|
289
|
-
|
|
290
|
-
Args:
|
|
291
|
-
data (CombinedToolResults): Aggregated tool results, including the path,
|
|
292
|
-
individual tool results, and the overall score.
|
|
293
|
-
|
|
294
|
-
Returns:
|
|
295
|
-
rich.table.Table: A Rich table with columns ``Tool``, ``Metric``, ``Score`` and
|
|
296
|
-
``Status``. Each metric row displays a status icon based on thresholds from
|
|
297
|
-
the tool's configuration. The table is titled with the data path and ends
|
|
298
|
-
with a row showing the overall score.
|
|
299
|
-
|
|
300
|
-
Example:
|
|
301
|
-
>>> table = format_as_table(combined_results)
|
|
302
|
-
>>> console.print(table)
|
|
303
|
-
"""
|
|
304
|
-
table = Table(width=80)
|
|
305
|
-
table.add_column("Tool", justify="left", no_wrap=True)
|
|
306
|
-
table.add_column("Time", justify="right", style="dim")
|
|
307
|
-
table.add_column("Metric", justify="right", style="cyan", no_wrap=True)
|
|
308
|
-
table.add_column("Score", style="magenta")
|
|
309
|
-
table.add_column("Status")
|
|
310
|
-
for tr in data.tool_results:
|
|
311
|
-
tool_name = tr.raw.tool_name
|
|
312
|
-
config = next((t for t in registry.values() if t.name == tool_name))
|
|
313
|
-
for i, (name, value) in enumerate(tr.metrics.items()):
|
|
314
|
-
status = ""
|
|
315
|
-
if value < config.error_threshold:
|
|
316
|
-
status = "[bold red]Error[/]"
|
|
317
|
-
elif value < config.warning_threshold:
|
|
318
|
-
status = "[yellow]Warning[/]"
|
|
319
|
-
else:
|
|
320
|
-
status = "[green]OK[/]"
|
|
321
|
-
time_str = f"{tr.duration_s:.2f}s" if i == 0 else ""
|
|
322
|
-
table.add_row(tool_name, time_str, name, f"{value:0.3f}", status)
|
|
323
|
-
table.add_row("", "", "[bold]Score[/]", f"[bold]{data.score:0.3f}[/]", "")
|
|
324
|
-
return table
|
|
@@ -22,6 +22,8 @@ class BanditParser(AbstractParser):
|
|
|
22
22
|
data = json.loads(raw_result.stdout)
|
|
23
23
|
except (json.JSONDecodeError, ValueError):
|
|
24
24
|
return ToolResult(raw=raw_result, metrics={"security": 1.0})
|
|
25
|
+
if not isinstance(data, dict):
|
|
26
|
+
return ToolResult(raw=raw_result, metrics={"security": 1.0})
|
|
25
27
|
|
|
26
28
|
files: dict[str, list] = {}
|
|
27
29
|
weighted = 0
|
|
@@ -46,7 +48,11 @@ class BanditParser(AbstractParser):
|
|
|
46
48
|
if not tr.details:
|
|
47
49
|
return "bandit reported issues (no details available)"
|
|
48
50
|
file, issues = next(iter(tr.details.items()))
|
|
51
|
+
if not isinstance(issues, list) or not issues:
|
|
52
|
+
return "bandit reported issues (no details available)"
|
|
49
53
|
issue = issues[0]
|
|
54
|
+
if not isinstance(issue, dict):
|
|
55
|
+
return "bandit reported issues (no details available)"
|
|
50
56
|
line = issue.get("line", "?")
|
|
51
57
|
code = issue.get("code", "")
|
|
52
58
|
severity = issue.get("severity", "")
|
|
@@ -12,17 +12,17 @@ performance metrics or error scores:
|
|
|
12
12
|
Both functions return a float and can be used directly in downstream analytics,
|
|
13
13
|
visualisation or decision-making pipelines."""
|
|
14
14
|
|
|
15
|
+
import re
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def read_source_lines(file_path: str, line: int, count: int = 5) -> str:
|
|
19
20
|
"""Return up to `count` source lines starting at the given 1-based line number."""
|
|
20
|
-
from pathlib import Path
|
|
21
21
|
try:
|
|
22
22
|
all_lines = Path(file_path).read_text(encoding="utf-8").splitlines()
|
|
23
23
|
start = max(0, line - 1)
|
|
24
24
|
return "\n".join(all_lines[start : start + count])
|
|
25
|
-
except OSError:
|
|
25
|
+
except (OSError, ValueError):
|
|
26
26
|
return ""
|
|
27
27
|
|
|
28
28
|
|
|
@@ -66,7 +66,6 @@ def extract_callee_name(source_line: str) -> str | None:
|
|
|
66
66
|
``func`` rather than the variable on the left. Python keywords and
|
|
67
67
|
built-ins listed in ``_PYTHON_KEYWORDS`` are excluded.
|
|
68
68
|
"""
|
|
69
|
-
import re
|
|
70
69
|
stripped = source_line.strip()
|
|
71
70
|
rhs = stripped
|
|
72
71
|
if "=" in stripped and not stripped.startswith(("assert", "return")):
|
|
@@ -78,7 +77,6 @@ def extract_callee_name(source_line: str) -> str | None:
|
|
|
78
77
|
|
|
79
78
|
|
|
80
79
|
def _find_project_root(hint_file: str) -> Path:
|
|
81
|
-
from pathlib import Path
|
|
82
80
|
root = Path(hint_file).resolve().parent
|
|
83
81
|
current = root
|
|
84
82
|
for _ in range(8):
|
|
@@ -96,7 +94,6 @@ def find_in_project(func_name: str, hint_file: str, max_lines: int = 10) -> tupl
|
|
|
96
94
|
|
|
97
95
|
Returns ``(file_path, code_block)`` for the first match, or ``("", "")`` if not found.
|
|
98
96
|
"""
|
|
99
|
-
from pathlib import Path
|
|
100
97
|
result = find_function_source(hint_file, func_name, max_lines=max_lines)
|
|
101
98
|
if result:
|
|
102
99
|
return hint_file, result
|
|
@@ -112,7 +109,6 @@ def find_in_project(func_name: str, hint_file: str, max_lines: int = 10) -> tupl
|
|
|
112
109
|
|
|
113
110
|
def _relative_path(path: str) -> str:
|
|
114
111
|
"""Return path relative to cwd, normalised to forward slashes."""
|
|
115
|
-
from pathlib import Path
|
|
116
112
|
try:
|
|
117
113
|
return str(Path(path).relative_to(Path.cwd())).replace("\\", "/")
|
|
118
114
|
except ValueError:
|
|
@@ -130,7 +126,6 @@ def format_callee_context(func_name: str, hint_file: str, max_lines: int = 10) -
|
|
|
130
126
|
...
|
|
131
127
|
```
|
|
132
128
|
"""
|
|
133
|
-
import re
|
|
134
129
|
callee_file, code_block = find_in_project(func_name, hint_file, max_lines=max_lines)
|
|
135
130
|
if not code_block:
|
|
136
131
|
return ""
|
|
@@ -141,12 +136,10 @@ def format_callee_context(func_name: str, hint_file: str, max_lines: int = 10) -
|
|
|
141
136
|
|
|
142
137
|
def find_function_source(file: str, func_name: str, max_lines: int = 15) -> str:
|
|
143
138
|
"""Return a fenced python block for the body of func_name, or '' if unavailable."""
|
|
144
|
-
from pathlib import Path
|
|
145
139
|
try:
|
|
146
140
|
all_lines = Path(file).read_text(encoding="utf-8").splitlines()
|
|
147
|
-
except OSError:
|
|
141
|
+
except (OSError, ValueError):
|
|
148
142
|
return ""
|
|
149
|
-
import re
|
|
150
143
|
pattern = re.compile(rf"^(\s*)(?:async\s+)?def\s+{re.escape(func_name)}\s*\(")
|
|
151
144
|
match_result: tuple[int, int] | None = None
|
|
152
145
|
for i, line in enumerate(all_lines):
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/compileparser.py
RENAMED
|
@@ -64,7 +64,10 @@ class CompileParser(AbstractParser):
|
|
|
64
64
|
compilations += 1
|
|
65
65
|
elif line.startswith("*** File "):
|
|
66
66
|
# This indicates a compilation error
|
|
67
|
-
|
|
67
|
+
parts = line.split('"')
|
|
68
|
+
if len(parts) < 2:
|
|
69
|
+
continue
|
|
70
|
+
file_path = parts[1]
|
|
68
71
|
current_error = {"file": file_path, "error": line}
|
|
69
72
|
elif current_error and line.strip():
|
|
70
73
|
# Append additional error context
|
|
@@ -80,17 +83,21 @@ class CompileParser(AbstractParser):
|
|
|
80
83
|
error_info = {}
|
|
81
84
|
# Extract line number if present
|
|
82
85
|
if "line " in error_lines[0]:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
try:
|
|
87
|
+
error_info["line"] = int(
|
|
88
|
+
error_lines[0].split("line ")[1].split(",")[0]
|
|
89
|
+
)
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
86
92
|
# Get source code context if available
|
|
87
93
|
if len(error_lines) > 1:
|
|
88
94
|
error_info["src"] = error_lines[1].strip()
|
|
89
95
|
if len(error_lines) > 3:
|
|
90
96
|
if "Error:" in error_lines[3]:
|
|
91
97
|
error_parts = error_lines[3].split(":")
|
|
98
|
+
type_tokens = error_parts[0].strip().split()
|
|
92
99
|
error_info["type"] = (
|
|
93
|
-
|
|
100
|
+
type_tokens[-1] if type_tokens else "Unknown"
|
|
94
101
|
) # Gets "SyntaxError"
|
|
95
102
|
error_info["help"] = ",".join(
|
|
96
103
|
error_parts[1:]
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/complexityparser.py
RENAMED
|
@@ -64,7 +64,14 @@ class ComplexityParser(AbstractParser):
|
|
|
64
64
|
>>> result.metrics["simplicity"]
|
|
65
65
|
0.4"""
|
|
66
66
|
tr = ToolResult(raw=raw_result)
|
|
67
|
-
|
|
67
|
+
try:
|
|
68
|
+
data = json.loads(raw_result.stdout)
|
|
69
|
+
except (json.JSONDecodeError, ValueError):
|
|
70
|
+
tr.metrics["simplicity"] = 0.0
|
|
71
|
+
return tr
|
|
72
|
+
if not isinstance(data, dict):
|
|
73
|
+
tr.metrics["simplicity"] = 0.0
|
|
74
|
+
return tr
|
|
68
75
|
score = 0
|
|
69
76
|
num_items = 0
|
|
70
77
|
max_complexity = 30
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/coverageparser.py
RENAMED
|
@@ -76,13 +76,13 @@ class CoverageParser(AbstractParser):
|
|
|
76
76
|
score = tr.metrics.get("coverage", 0)
|
|
77
77
|
uncovered = sorted(
|
|
78
78
|
[(f, d) for f, d in tr.details.items() if isinstance(d, dict) and d.get("missing")],
|
|
79
|
-
key=lambda x: x[1]
|
|
79
|
+
key=lambda x: x[1].get("coverage", 0.0),
|
|
80
80
|
)[:5]
|
|
81
81
|
if not uncovered:
|
|
82
82
|
return f"**coverage** score: {score:.3f}"
|
|
83
83
|
lines = [f"**coverage** score: {score:.3f} — files with lowest coverage:"]
|
|
84
84
|
for path, data in uncovered:
|
|
85
|
-
pct = data
|
|
86
|
-
miss = data
|
|
85
|
+
pct = data.get("coverage", 0.0)
|
|
86
|
+
miss = data.get("missing", 0)
|
|
87
87
|
lines.append(f"- `{path}`: {pct:.0%} ({miss} uncovered statements)")
|
|
88
88
|
return "\n".join(lines)
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/halsteadparser.py
RENAMED
|
@@ -59,6 +59,14 @@ class HalsteadParser(AbstractParser):
|
|
|
59
59
|
# {"total": {"h1": 6, "h2": 18, "N1": 13, "N2": 22, "vocabulary": 24, "length": 35, "calculated_length": 90.56842503028855, "volume": 160.4736875252405, "difficulty": 3.6666666666666665, "effort": 588.4035209258818, "time": 32.68908449588233, "bugs": 0.05349122917508017},
|
|
60
60
|
# "functions": {"calc_dist": {"h1": 3, "h2": 9, "N1": 5, "N2": 10, "vocabulary": 12, "length": 15, "calculated_length": 33.28421251514428, "volume": 53.77443751081735, "difficulty": 1.6666666666666667, "effort": 89.62406251802892, "time": 4.9791145843349405, "bugs": 0.017924812503605784}, "find_nearest_city": {"h1": 1, "h2": 2, "N1": 1, "N2": 2, "vocabulary": 3, "length": 3, "calculated_length": 2.0, "volume": 4.754887502163469, "difficulty": 0.5, "effort": 2.3774437510817346, "time": 0.1320802083934297, "bugs": 0.0015849625007211565}, "generate_tour": {"h1": 2, "h2": 5, "N1": 6, "N2": 8, "vocabulary": 7, "length": 14, "calculated_length": 13.60964047443681, "volume": 39.302968908806456, "difficulty": 1.6, "effort": 62.884750254090335, "time": 3.493597236338352, "bugs": 0.01310098963626882}, "main": {"h1": 0, "h2": 0, "N1": 0, "N2": 0, "vocabulary": 0, "length": 0, "calculated_length": 0, "volume": 0, "difficulty": 0, "effort": 0, "time": 0.0, "bugs": 0.0}}}
|
|
61
61
|
tr = ToolResult(raw=raw_result)
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(raw_result.stdout)
|
|
64
|
+
except (json.JSONDecodeError, ValueError):
|
|
65
|
+
tr.metrics = {"file_bug_free": 1.0, "file_smallness": 1.0, "functions_bug_free": 1.0, "functions_smallness": 1.0}
|
|
66
|
+
return tr
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
tr.metrics = {"file_bug_free": 1.0, "file_smallness": 1.0, "functions_bug_free": 1.0, "functions_smallness": 1.0}
|
|
69
|
+
return tr
|
|
62
70
|
MAX_FILE_BUGS = 1
|
|
63
71
|
MAX_FILE_VOLUME = 2000
|
|
64
72
|
MAX_FUNCTION_BUGS = 0.2
|
|
@@ -67,7 +75,6 @@ class HalsteadParser(AbstractParser):
|
|
|
67
75
|
min_file_sm = 1.0
|
|
68
76
|
min_function_nb = 1.0
|
|
69
77
|
min_function_sm = 1.0
|
|
70
|
-
data = json.loads(raw_result.stdout)
|
|
71
78
|
for file, values in data.items():
|
|
72
79
|
file_name = file.replace("\\", "/")
|
|
73
80
|
if file_name not in tr.details:
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/interrogateparser.py
RENAMED
|
@@ -45,14 +45,14 @@ class InterrogateParser(AbstractParser):
|
|
|
45
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
|
-
[(f, d) for f, d in tr.details.items() if d.get("missing", 0) > 0],
|
|
49
|
-
key=lambda x: x[1]
|
|
48
|
+
[(f, d) for f, d in tr.details.items() if isinstance(d, dict) and d.get("missing", 0) > 0],
|
|
49
|
+
key=lambda x: x[1].get("coverage", 0.0),
|
|
50
50
|
)[:5]
|
|
51
51
|
if not uncovered:
|
|
52
52
|
return f"**doc_coverage** score: {score:.3f}"
|
|
53
53
|
lines = [f"**doc coverage** {score:.1%} — files with most missing docstrings:"]
|
|
54
54
|
for path, data in uncovered:
|
|
55
|
-
miss = data
|
|
56
|
-
pct = data
|
|
55
|
+
miss = data.get("missing", 0)
|
|
56
|
+
pct = data.get("coverage", 0.0)
|
|
57
57
|
lines.append(f"- `{path}`: {pct:.0%} ({miss} undocumented)")
|
|
58
58
|
return "\n".join(lines)
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/maintainabilityparser.py
RENAMED
|
@@ -41,7 +41,14 @@ class MaintainabilityParser(AbstractParser):
|
|
|
41
41
|
* ``details`` - a mapping from each file name (converted to use forward slashes) to a dictionary with keys ``mi``, ``rank``, and optionally ``error``.
|
|
42
42
|
* ``details['return_code']`` - the tool's exit code."""
|
|
43
43
|
tr = ToolResult(raw=raw_result)
|
|
44
|
-
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(raw_result.stdout)
|
|
46
|
+
except (json.JSONDecodeError, ValueError):
|
|
47
|
+
tr.metrics["maintainability"] = 0.0
|
|
48
|
+
return tr
|
|
49
|
+
if not isinstance(data, dict):
|
|
50
|
+
tr.metrics["maintainability"] = 0.0
|
|
51
|
+
return tr
|
|
45
52
|
num_items = 0
|
|
46
53
|
score = 0
|
|
47
54
|
for file, values in data.items():
|
|
@@ -50,7 +50,11 @@ class RuffParser(AbstractParser):
|
|
|
50
50
|
if not tr.details:
|
|
51
51
|
return "ruff reported issues (no details available)"
|
|
52
52
|
file, issues = next(iter(tr.details.items()))
|
|
53
|
+
if not isinstance(issues, list) or not issues:
|
|
54
|
+
return "ruff reported issues (no details available)"
|
|
53
55
|
issue = issues[0]
|
|
56
|
+
if not isinstance(issue, dict):
|
|
57
|
+
return "ruff reported issues (no details available)"
|
|
54
58
|
line = issue.get("line", "?")
|
|
55
59
|
code = issue.get("code", "")
|
|
56
60
|
message = issue.get("message", "")
|
|
@@ -64,7 +64,11 @@ class TyParser(AbstractParser):
|
|
|
64
64
|
if not tr.details:
|
|
65
65
|
return "ty reported issues (no details available)"
|
|
66
66
|
file, issues = next(iter(tr.details.items()))
|
|
67
|
+
if not isinstance(issues, list) or not issues:
|
|
68
|
+
return "ty reported issues (no details available)"
|
|
67
69
|
issue = issues[0]
|
|
70
|
+
if not isinstance(issue, dict):
|
|
71
|
+
return "ty reported issues (no details available)"
|
|
68
72
|
line = issue.get("line", "?")
|
|
69
73
|
code = issue.get("code", "")
|
|
70
74
|
message = issue.get("message", "")
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/vultureparser.py
RENAMED
|
@@ -40,7 +40,11 @@ class VultureParser(AbstractParser):
|
|
|
40
40
|
if not tr.details:
|
|
41
41
|
return "vulture reported issues (no details available)"
|
|
42
42
|
file, issues = next(iter(tr.details.items()))
|
|
43
|
+
if not isinstance(issues, list) or not issues:
|
|
44
|
+
return "vulture reported issues (no details available)"
|
|
43
45
|
issue = issues[0]
|
|
46
|
+
if not isinstance(issue, dict):
|
|
47
|
+
return "vulture reported issues (no details available)"
|
|
44
48
|
line = issue.get("line", "?")
|
|
45
49
|
kind = issue.get("type", "unused")
|
|
46
50
|
name = issue.get("name", "")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Rich table formatter for combined tool results."""
|
|
2
|
+
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
from py_cq.localtypes import CombinedToolResults, ToolConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def format_as_table(data: CombinedToolResults, registry: dict[str, ToolConfig]) -> Table:
|
|
9
|
+
"""Format combined tool results into a Rich Table."""
|
|
10
|
+
table = Table(width=80)
|
|
11
|
+
table.add_column("Tool", justify="left", no_wrap=True)
|
|
12
|
+
table.add_column("Time", justify="right", style="dim")
|
|
13
|
+
table.add_column("Metric", justify="right", style="cyan", no_wrap=True)
|
|
14
|
+
table.add_column("Score", style="magenta")
|
|
15
|
+
table.add_column("Status")
|
|
16
|
+
for tr in data.tool_results:
|
|
17
|
+
tool_name = tr.raw.tool_name
|
|
18
|
+
config = next((t for t in registry.values() if t.name == tool_name))
|
|
19
|
+
for i, (name, value) in enumerate(tr.metrics.items()):
|
|
20
|
+
if value < config.error_threshold:
|
|
21
|
+
status = "[bold red]Error[/]"
|
|
22
|
+
elif value < config.warning_threshold:
|
|
23
|
+
status = "[yellow]Warning[/]"
|
|
24
|
+
else:
|
|
25
|
+
status = "[green]OK[/]"
|
|
26
|
+
time_str = f"{tr.duration_s:.2f}s" if i == 0 else ""
|
|
27
|
+
table.add_row(tool_name, time_str, name, f"{value:0.3f}", status)
|
|
28
|
+
table.add_row("", "", "[bold]Score[/]", f"[bold]{data.score:0.3f}[/]", "")
|
|
29
|
+
return table
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/exitcodeparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/linecountparser.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.15 → python_code_quality-0.1.16}/src/py_cq/parsers/regexcountparser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|