python-code-quality 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_cq/api.py +26 -5
- py_cq/cli.py +13 -1
- py_cq/config/config.toml +26 -22
- py_cq/context_hash.py +4 -1
- py_cq/execution_engine.py +72 -19
- py_cq/localtypes.py +7 -6
- py_cq/parsers/common.py +15 -13
- py_cq/parsers/complexityparser.py +2 -0
- py_cq/parsers/coverageparser.py +4 -0
- py_cq/parsers/halsteadparser.py +4 -3
- py_cq/parsers/ruffparser.py +1 -1
- py_cq/tool_registry.py +2 -0
- {python_code_quality-0.2.1.dist-info → python_code_quality-0.2.3.dist-info}/METADATA +1 -1
- {python_code_quality-0.2.1.dist-info → python_code_quality-0.2.3.dist-info}/RECORD +16 -16
- {python_code_quality-0.2.1.dist-info → python_code_quality-0.2.3.dist-info}/WHEEL +1 -1
- {python_code_quality-0.2.1.dist-info → python_code_quality-0.2.3.dist-info}/entry_points.txt +0 -0
py_cq/api.py
CHANGED
|
@@ -148,13 +148,14 @@ class CQ:
|
|
|
148
148
|
if clear_cache:
|
|
149
149
|
_cache.clear()
|
|
150
150
|
|
|
151
|
-
def raw(self, *, early_exit: bool = False) -> list[ToolResult]:
|
|
151
|
+
def raw(self, *, early_exit: bool = False, order: str = "severity") -> list[ToolResult]:
|
|
152
152
|
"""Run all tools and return parsed results before aggregation."""
|
|
153
153
|
return run_tools(
|
|
154
154
|
self._registry.values(),
|
|
155
155
|
str(self.path),
|
|
156
156
|
self._workers,
|
|
157
157
|
early_exit=early_exit,
|
|
158
|
+
order=order,
|
|
158
159
|
excludes=self._excludes,
|
|
159
160
|
)
|
|
160
161
|
|
|
@@ -168,22 +169,42 @@ class CQ:
|
|
|
168
169
|
limit: int = 1,
|
|
169
170
|
silence: list[str] | None = None,
|
|
170
171
|
hint: bool = False,
|
|
172
|
+
order: str = "severity",
|
|
171
173
|
) -> dict:
|
|
172
174
|
"""Return the top defect as a dict with keys: id, file, project, message.
|
|
173
175
|
|
|
174
|
-
Stops running tools after the first
|
|
176
|
+
Stops running tools after the first issue (early_exit) for speed.
|
|
177
|
+
``order="phase"`` selects the first issue of the earliest non-clean
|
|
178
|
+
phase (dependency order); ``"severity"`` (default) selects the most
|
|
179
|
+
severe issue first.
|
|
175
180
|
"""
|
|
176
|
-
|
|
181
|
+
_silence = silence or []
|
|
182
|
+
results = self.raw(early_exit=True, order=order)
|
|
177
183
|
combined = aggregate_metrics(str(self.path), results)
|
|
178
|
-
|
|
184
|
+
issue = format_for_llm_json(
|
|
179
185
|
self._registry,
|
|
180
186
|
combined,
|
|
181
187
|
context_lines=self._context_lines,
|
|
182
188
|
hint=hint,
|
|
183
189
|
limit=limit,
|
|
184
|
-
silence=
|
|
190
|
+
silence=_silence,
|
|
185
191
|
project_root=self._project_root,
|
|
186
192
|
)
|
|
193
|
+
if issue.get("id") is None and _silence:
|
|
194
|
+
# Early-exit stopped at a phase whose only issues are silenced.
|
|
195
|
+
# Re-run all tools to surface issues in later phases.
|
|
196
|
+
results = self.raw(early_exit=False, order=order)
|
|
197
|
+
combined = aggregate_metrics(str(self.path), results)
|
|
198
|
+
issue = format_for_llm_json(
|
|
199
|
+
self._registry,
|
|
200
|
+
combined,
|
|
201
|
+
context_lines=self._context_lines,
|
|
202
|
+
hint=hint,
|
|
203
|
+
limit=limit,
|
|
204
|
+
silence=_silence,
|
|
205
|
+
project_root=self._project_root,
|
|
206
|
+
)
|
|
207
|
+
return issue
|
|
187
208
|
|
|
188
209
|
def is_fixed(self, fingerprint: str) -> bool:
|
|
189
210
|
"""Return True if the fingerprinted issue is no longer present.
|
py_cq/cli.py
CHANGED
|
@@ -67,6 +67,13 @@ class OutputMode(str, Enum):
|
|
|
67
67
|
RAW = "raw"
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class OrderMode(str, Enum):
|
|
71
|
+
"""Selection order for the top issue in -o llm/llm-json."""
|
|
72
|
+
|
|
73
|
+
SEVERITY = "severity"
|
|
74
|
+
PHASE = "phase"
|
|
75
|
+
|
|
76
|
+
|
|
70
77
|
def _version_callback(value: bool) -> None:
|
|
71
78
|
if not value:
|
|
72
79
|
return
|
|
@@ -159,6 +166,11 @@ def check(
|
|
|
159
166
|
"-s",
|
|
160
167
|
help="Silence issues from -o llm output (e.g. -s src/foo.py or -s src/foo.py:42:E501)",
|
|
161
168
|
),
|
|
169
|
+
order: OrderMode = typer.Option(
|
|
170
|
+
OrderMode.SEVERITY,
|
|
171
|
+
"--order",
|
|
172
|
+
help="Issue selection order for -o llm/llm-json: severity (most severe first) or phase (first non-clean phase, in dependency order)",
|
|
173
|
+
),
|
|
162
174
|
):
|
|
163
175
|
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
164
176
|
path_obj = Path(path)
|
|
@@ -200,7 +212,7 @@ def check(
|
|
|
200
212
|
|
|
201
213
|
is_llm = output in (OutputMode.LLM, OutputMode.LLM_JSON)
|
|
202
214
|
t0 = time.perf_counter()
|
|
203
|
-
tool_results = cq.raw(early_exit=is_llm)
|
|
215
|
+
tool_results = cq.raw(early_exit=is_llm, order=order.value)
|
|
204
216
|
total_s = time.perf_counter() - t0
|
|
205
217
|
combined = aggregate_metrics(path, tool_results)
|
|
206
218
|
|
py_cq/config/config.toml
CHANGED
|
@@ -53,43 +53,47 @@ error_threshold = 0.5
|
|
|
53
53
|
run_in_target_env = true
|
|
54
54
|
extra_deps = ["coverage", "pytest"]
|
|
55
55
|
skip_for_file = true
|
|
56
|
+
skip_if = "pytest"
|
|
57
|
+
|
|
58
|
+
[python.vulture]
|
|
59
|
+
command = "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
|
|
60
|
+
exclude_format = ",{path}"
|
|
61
|
+
parser = "VultureParser"
|
|
62
|
+
order = 7
|
|
63
|
+
warning_threshold = 0.9999
|
|
64
|
+
error_threshold = 0.8
|
|
65
|
+
|
|
66
|
+
[python.interrogate]
|
|
67
|
+
command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
|
|
68
|
+
exclude_format = " -e {path}"
|
|
69
|
+
parser = "InterrogateParser"
|
|
70
|
+
order = 8
|
|
71
|
+
warning_threshold = 0.8
|
|
72
|
+
error_threshold = 0.5
|
|
73
|
+
|
|
74
|
+
[python.interrogate.parser_config]
|
|
75
|
+
skip_empty_init = true
|
|
56
76
|
|
|
57
77
|
[python.radon-cc]
|
|
58
78
|
command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
|
|
59
79
|
parser = "ComplexityParser"
|
|
60
|
-
order =
|
|
80
|
+
order = 9
|
|
81
|
+
gate_strict = false
|
|
61
82
|
warning_threshold = 0.6
|
|
62
83
|
error_threshold = 0.4
|
|
63
84
|
|
|
64
85
|
[python.radon-mi]
|
|
65
86
|
command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
|
|
66
87
|
parser = "MaintainabilityParser"
|
|
67
|
-
order =
|
|
88
|
+
order = 10
|
|
89
|
+
gate_strict = false
|
|
68
90
|
warning_threshold = 0.6
|
|
69
91
|
error_threshold = 0.4
|
|
70
92
|
|
|
71
93
|
[python.radon-hal]
|
|
72
94
|
command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
|
|
73
95
|
parser = "HalsteadParser"
|
|
74
|
-
order = 9
|
|
75
|
-
warning_threshold = 0.5
|
|
76
|
-
error_threshold = 0.3
|
|
77
|
-
|
|
78
|
-
[python.vulture]
|
|
79
|
-
command = "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
|
|
80
|
-
exclude_format = ",{path}"
|
|
81
|
-
parser = "VultureParser"
|
|
82
|
-
order = 10
|
|
83
|
-
warning_threshold = 0.9999
|
|
84
|
-
error_threshold = 0.8
|
|
85
|
-
|
|
86
|
-
[python.interrogate]
|
|
87
|
-
command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
|
|
88
|
-
exclude_format = " -e {path}"
|
|
89
|
-
parser = "InterrogateParser"
|
|
90
96
|
order = 11
|
|
91
|
-
|
|
97
|
+
gate_strict = false
|
|
98
|
+
warning_threshold = 0.35
|
|
92
99
|
error_threshold = 0.3
|
|
93
|
-
|
|
94
|
-
[python.interrogate.parser_config]
|
|
95
|
-
skip_empty_init = true
|
py_cq/context_hash.py
CHANGED
|
@@ -41,12 +41,15 @@ def get_sigs(path: str):
|
|
|
41
41
|
>>> get_sigs('/tmp/project')
|
|
42
42
|
['/tmp/project/main.py:1024:1680000000.0', ...]
|
|
43
43
|
"""
|
|
44
|
+
_ENV_FILES = {".python-version", "pyproject.toml", "uv.lock"}
|
|
44
45
|
items = []
|
|
45
46
|
with os.scandir(path) as entries:
|
|
46
47
|
for entry in entries:
|
|
47
48
|
# Use follow_symlinks=False to prevent cache poisoning from
|
|
48
49
|
# symlinks pointing outside the project tree (M-2)
|
|
49
|
-
if entry.is_file(follow_symlinks=False) and
|
|
50
|
+
if entry.is_file(follow_symlinks=False) and (
|
|
51
|
+
entry.name.endswith(".py") or entry.name in _ENV_FILES
|
|
52
|
+
):
|
|
50
53
|
stat_info = entry.stat(follow_symlinks=False)
|
|
51
54
|
items.append(f"{entry.path}:{stat_info.st_size}:{stat_info.st_mtime}")
|
|
52
55
|
if entry.is_dir(follow_symlinks=False) and entry.name not in [
|
py_cq/execution_engine.py
CHANGED
|
@@ -18,6 +18,7 @@ import shlex
|
|
|
18
18
|
import shutil
|
|
19
19
|
import subprocess
|
|
20
20
|
import sys
|
|
21
|
+
import tempfile
|
|
21
22
|
import time
|
|
22
23
|
from collections.abc import Collection
|
|
23
24
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -215,16 +216,24 @@ def run_tool(
|
|
|
215
216
|
# All user-supplied values (context_path, excludes) are properly quoted
|
|
216
217
|
# via shlex.quote() to prevent injection - see _build_exclude_str and
|
|
217
218
|
# the uv command assembly above.
|
|
219
|
+
if run_env is None:
|
|
220
|
+
run_env = dict(os.environ)
|
|
221
|
+
_fd, coverage_tmp = tempfile.mkstemp(prefix=".coverage.cq.")
|
|
222
|
+
os.close(_fd)
|
|
223
|
+
run_env["COVERAGE_FILE"] = coverage_tmp
|
|
218
224
|
t_sub0 = time.perf_counter()
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
try:
|
|
226
|
+
result = subprocess.run(
|
|
227
|
+
command,
|
|
228
|
+
capture_output=True,
|
|
229
|
+
text=True,
|
|
230
|
+
shell=True,
|
|
231
|
+
encoding="utf-8",
|
|
232
|
+
errors="replace",
|
|
233
|
+
env=run_env,
|
|
234
|
+
) # nosec
|
|
235
|
+
finally:
|
|
236
|
+
Path(coverage_tmp).unlink(missing_ok=True)
|
|
228
237
|
t_sub = time.perf_counter() - t_sub0
|
|
229
238
|
log.debug(
|
|
230
239
|
f"{tool_config.name}: [MISS] cache={t_cache * 1000:.1f}ms tool={t_sub * 1000:.0f}ms: {command}"
|
|
@@ -248,6 +257,7 @@ def run_tools(
|
|
|
248
257
|
path: str,
|
|
249
258
|
max_workers: int = 0,
|
|
250
259
|
early_exit: bool = False,
|
|
260
|
+
order: str = "severity",
|
|
251
261
|
excludes: list[str] | None = None,
|
|
252
262
|
project_root: str | None = None,
|
|
253
263
|
) -> list[ToolResult]:
|
|
@@ -327,28 +337,71 @@ def run_tools(
|
|
|
327
337
|
log.warning(f"Early exit: skipped {n_skipped} tool(s): {remaining}")
|
|
328
338
|
break
|
|
329
339
|
_, tr = prioritized[-1]
|
|
330
|
-
|
|
340
|
+
# order="phase" stops at the first phase with *any* finding (not
|
|
341
|
+
# clean), so phase order is absolute and independent of error/warning
|
|
342
|
+
# threshold tuning; "severity" stops at the first error.
|
|
343
|
+
threshold = (
|
|
344
|
+
tool_config.warning_threshold
|
|
345
|
+
if order == "phase"
|
|
346
|
+
else tool_config.error_threshold
|
|
347
|
+
)
|
|
348
|
+
if tr.metrics and min(tr.metrics.values()) < threshold:
|
|
331
349
|
n_skipped = n_total - i - 1
|
|
332
350
|
if n_skipped:
|
|
333
351
|
remaining = ", ".join(tc.name for tc in sorted_configs[i + 1 :])
|
|
334
352
|
log.debug(
|
|
335
|
-
f"Error threshold hit at {tool_config.name}: skipped {n_skipped} tool(s): {remaining}"
|
|
353
|
+
f"{'Not clean' if order == 'phase' else 'Error threshold hit'} at {tool_config.name}: skipped {n_skipped} tool(s): {remaining}"
|
|
336
354
|
)
|
|
337
355
|
break
|
|
338
356
|
log.info(f"cq run_tools elapsed: {time.perf_counter() - t_start:.2f}s")
|
|
339
357
|
return [tr for _, tr in sorted(prioritized)]
|
|
358
|
+
configs_by_name = {tc.name: tc for tc in tool_configs}
|
|
359
|
+
|
|
360
|
+
def _run_if_dep_passed(dep_future, tool_config: ToolConfig, dep_config: ToolConfig) -> tuple[int, ToolResult]:
|
|
361
|
+
# Wait for the dependency within the same thread pool - no phase split,
|
|
362
|
+
# so all other tools keep running in parallel while we block here.
|
|
363
|
+
try:
|
|
364
|
+
_, dep_tr = dep_future.result()
|
|
365
|
+
except Exception:
|
|
366
|
+
return _run_and_parse(tool_config)
|
|
367
|
+
if dep_tr.metrics and min(dep_tr.metrics.values()) < dep_config.error_threshold:
|
|
368
|
+
# Dependency failed its error threshold: skip this tool and return a
|
|
369
|
+
# synthetic empty result. Coverage declares skip_if = "pytest" so that
|
|
370
|
+
# it never re-invokes pytest (which the coverage command does internally)
|
|
371
|
+
# when we already know pytest is broken.
|
|
372
|
+
log.debug(f"{tool_config.name}: skipping because {tool_config.skip_if} failed")
|
|
373
|
+
t0 = time.perf_counter()
|
|
374
|
+
tr = tool_config.parser_class(tool_config.parser_config).parse(
|
|
375
|
+
RawResult(tool_name=tool_config.name)
|
|
376
|
+
)
|
|
377
|
+
tr.duration_s = time.perf_counter() - t0
|
|
378
|
+
return tool_config.order, tr
|
|
379
|
+
return _run_and_parse(tool_config)
|
|
380
|
+
|
|
381
|
+
timings: list[tuple[int, str, float]] = []
|
|
382
|
+
# Sort by order so that when we build name_to_future below, a dependency's
|
|
383
|
+
# future is always registered before the tool that declares skip_if on it.
|
|
384
|
+
sorted_configs = sorted(tool_configs, key=lambda tc: tc.order)
|
|
385
|
+
name_to_future: dict[str, Any] = {}
|
|
340
386
|
with ThreadPoolExecutor(max_workers=max_workers or len(tool_configs)) as executor:
|
|
341
|
-
future_to_tool = {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
387
|
+
future_to_tool = {}
|
|
388
|
+
for tc in sorted_configs:
|
|
389
|
+
dep_name = tc.skip_if
|
|
390
|
+
dep_future = name_to_future.get(dep_name) if dep_name else None
|
|
391
|
+
dep_config = configs_by_name.get(dep_name) if dep_name else None
|
|
392
|
+
if dep_future and dep_config:
|
|
393
|
+
# Submit a wrapper that waits on the dependency future first.
|
|
394
|
+
f = executor.submit(_run_if_dep_passed, dep_future, tc, dep_config)
|
|
395
|
+
else:
|
|
396
|
+
f = executor.submit(_run_and_parse, tc)
|
|
397
|
+
future_to_tool[f] = tc
|
|
398
|
+
name_to_future[tc.name] = f
|
|
346
399
|
for future in as_completed(future_to_tool):
|
|
347
400
|
tool_config = future_to_tool[future]
|
|
348
401
|
try:
|
|
349
|
-
|
|
350
|
-
prioritized.append((
|
|
351
|
-
timings.append((
|
|
402
|
+
tc_order, tr = future.result()
|
|
403
|
+
prioritized.append((tc_order, tr))
|
|
404
|
+
timings.append((tc_order, tool_config.name, tr.duration_s))
|
|
352
405
|
except Exception as exc:
|
|
353
406
|
log.error(f"{tool_config.name} generated an exception: {exc}")
|
|
354
407
|
per_tool = ", ".join(f"{name}={dur:.2f}s" for _, name, dur in sorted(timings))
|
py_cq/localtypes.py
CHANGED
|
@@ -43,7 +43,7 @@ class ToolConfig:
|
|
|
43
43
|
|
|
44
44
|
name: str # e.g., "pytest", "coverage", "pydocstyle"
|
|
45
45
|
command: str # The command to execute (can include placeholders)
|
|
46
|
-
parser_class: Callable #
|
|
46
|
+
parser_class: Callable # The parser class itself (resolved from its name in config)
|
|
47
47
|
context_path: str = "" # Path to project or file
|
|
48
48
|
order: int = 5 # 1=first (compilation), 11=last (style)
|
|
49
49
|
warning_threshold: float = 0.7 # Yellow warning if below this
|
|
@@ -60,6 +60,8 @@ class ToolConfig:
|
|
|
60
60
|
default_factory=list
|
|
61
61
|
) # Top-level dir/file names to omit from {scan_targets}
|
|
62
62
|
skip_for_file: bool = False # If True, skip when context_path is a single file
|
|
63
|
+
gate_strict: bool = True # If False, small regressions above warning_threshold are accepted
|
|
64
|
+
skip_if: str = "" # Tool name; if that tool's score is below its error_threshold, skip this tool
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
@dataclass
|
|
@@ -123,10 +125,12 @@ class ToolResult:
|
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
|
|
126
|
-
@dataclass
|
|
127
128
|
class CombinedToolResults:
|
|
128
129
|
"""Aggregates results from multiple tools, stores the associated path, and calculates an overall score by averaging the mean metric values of each ``ToolResult``. If a ``ToolResult`` has no metrics, it contributes zero, and the score defaults to ``0.0`` when the list is empty."""
|
|
129
130
|
|
|
131
|
+
score: float
|
|
132
|
+
path: str
|
|
133
|
+
|
|
130
134
|
def __init__(self, path: str, tool_results: list[ToolResult]):
|
|
131
135
|
"""Initializes a CombinedToolResults instance.
|
|
132
136
|
|
|
@@ -148,9 +152,6 @@ class CombinedToolResults:
|
|
|
148
152
|
else 0.0
|
|
149
153
|
)
|
|
150
154
|
|
|
151
|
-
score: float = 0.0
|
|
152
|
-
path: str = ""
|
|
153
|
-
|
|
154
155
|
def to_dict(self) -> dict:
|
|
155
156
|
"""Returns a dictionary containing the path, overall score, and each ToolResult serialized."""
|
|
156
157
|
return {
|
|
@@ -163,7 +164,7 @@ class CombinedToolResults:
|
|
|
163
164
|
class AbstractParser(ABC):
|
|
164
165
|
"""Base class for parsers that transform raw tool output into structured `ToolResult` objects.
|
|
165
166
|
|
|
166
|
-
Subclasses must implement `parse` to convert a `RawResult` into a `ToolResult`.
|
|
167
|
+
Subclasses must implement `parse` to convert a `RawResult` into a `ToolResult`. The `format_llm_message` method can be overridden to supply a richer single-defect description for a parsed result."""
|
|
167
168
|
|
|
168
169
|
def __init__(self, parser_config: dict | None = None):
|
|
169
170
|
self.parser_config = parser_config or {}
|
py_cq/parsers/common.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
This module
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
"""Shared helpers used across parsers and the LLM formatter.
|
|
2
|
+
|
|
3
|
+
This module groups small utilities for several concerns:
|
|
4
|
+
|
|
5
|
+
* Scoring - :func:`inv_normalize` and :func:`score_logistic_variant` map error
|
|
6
|
+
magnitudes to bounded ``[0, 1]`` scores.
|
|
7
|
+
* Source context - :func:`read_source_lines`, :func:`format_source_context`,
|
|
8
|
+
:func:`find_function_source`, :func:`find_enclosing_function`, and
|
|
9
|
+
:func:`enclosing_function_range` extract fenced code snippets for LLM prompts.
|
|
10
|
+
* Project navigation - :func:`find_in_project`, :func:`format_callee_context`,
|
|
11
|
+
:func:`extract_callee_name`, :func:`resolve_path`, and :func:`_relative_path`
|
|
12
|
+
locate and reference definitions across a project tree.
|
|
13
|
+
* Output formatting - :func:`format_issue_header` builds issue headers.
|
|
14
|
+
* Parsing - :func:`parse_json_dict` and :func:`extract_first_issue` read tool
|
|
15
|
+
output into Python structures."""
|
|
14
16
|
|
|
15
17
|
import json
|
|
16
18
|
import re
|
|
@@ -74,6 +74,8 @@ class ComplexityParser(AbstractParser):
|
|
|
74
74
|
num_items = 0
|
|
75
75
|
max_complexity = 30
|
|
76
76
|
for file, functions in data.items():
|
|
77
|
+
if not isinstance(functions, list):
|
|
78
|
+
continue
|
|
77
79
|
file_name = file.replace("\\", "/")
|
|
78
80
|
if file_name not in tr.details:
|
|
79
81
|
tr.details[file_name] = {}
|
py_cq/parsers/coverageparser.py
CHANGED
|
@@ -146,6 +146,8 @@ class CoverageParser(AbstractParser):
|
|
|
146
146
|
]
|
|
147
147
|
|
|
148
148
|
tr.details = details
|
|
149
|
+
if "coverage" not in tr.metrics:
|
|
150
|
+
tr.metrics["coverage"] = 0.0
|
|
149
151
|
return tr
|
|
150
152
|
|
|
151
153
|
def format_llm_message(
|
|
@@ -199,4 +201,6 @@ class CoverageParser(AbstractParser):
|
|
|
199
201
|
parts.append(f"\nAdd tests to: {location}")
|
|
200
202
|
|
|
201
203
|
return "\n".join(parts)
|
|
204
|
+
if tr.metrics.get("coverage", 1.0) == 0.0:
|
|
205
|
+
return "**Coverage: 0%** - pytest failed or collected no tests, so no coverage data is available."
|
|
202
206
|
return ""
|
py_cq/parsers/halsteadparser.py
CHANGED
|
@@ -83,11 +83,12 @@ class HalsteadParser(AbstractParser):
|
|
|
83
83
|
"functions": {},
|
|
84
84
|
}
|
|
85
85
|
if "error" in values:
|
|
86
|
+
tr.details[file_name]["error"] = values["error"]
|
|
86
87
|
min_file_nb = 0.0
|
|
87
88
|
min_file_sm = 0.0
|
|
88
89
|
min_function_nb = 0.0
|
|
89
90
|
min_function_sm = 0.0
|
|
90
|
-
|
|
91
|
+
continue
|
|
91
92
|
if "total" in values:
|
|
92
93
|
nb, sm = self.extract_bugs_and_volume(
|
|
93
94
|
values.get("total", {}), MAX_FILE_BUGS, MAX_FILE_VOLUME
|
|
@@ -106,7 +107,7 @@ class HalsteadParser(AbstractParser):
|
|
|
106
107
|
min_function_nb = min(nb, min_function_nb)
|
|
107
108
|
min_function_sm = min(sm, min_function_sm)
|
|
108
109
|
tr.details[file_name]["functions"][function] = {
|
|
109
|
-
"
|
|
110
|
+
"bug_free": nb,
|
|
110
111
|
"smallness": sm,
|
|
111
112
|
"bugs": function_values.get("bugs", 0),
|
|
112
113
|
"volume": function_values.get("volume", 0),
|
|
@@ -141,7 +142,7 @@ class HalsteadParser(AbstractParser):
|
|
|
141
142
|
for file_name, file_data in tr.details.items():
|
|
142
143
|
if is_function_metric:
|
|
143
144
|
for func_name, func_data in file_data.get("functions", {}).items():
|
|
144
|
-
s = func_data.get("
|
|
145
|
+
s = func_data.get("bug_free" if is_bug_metric else "smallness", 1.0)
|
|
145
146
|
if s < worst_score:
|
|
146
147
|
worst_score = s
|
|
147
148
|
worst_file = file_name
|
py_cq/parsers/ruffparser.py
CHANGED
|
@@ -123,7 +123,7 @@ def _format_F821(file: str, line: int, message: str) -> str:
|
|
|
123
123
|
base += f"\n\nOther references to `{name}` in this file:\n{ref_lines}"
|
|
124
124
|
else:
|
|
125
125
|
base += f"\n\n`{name}` is not imported or defined anywhere else in this file."
|
|
126
|
-
if enclosing:
|
|
126
|
+
if enclosing and enclosing not in base:
|
|
127
127
|
base += f"\n\nEnclosing function:{enclosing}"
|
|
128
128
|
return base
|
|
129
129
|
|
py_cq/tool_registry.py
CHANGED
|
@@ -32,6 +32,8 @@ def load_tool_configs() -> dict[str, ToolConfig]:
|
|
|
32
32
|
exclude_format=tool_data.get("exclude_format", ""),
|
|
33
33
|
scan_exclude_names=tool_data.get("scan_exclude_names", []),
|
|
34
34
|
skip_for_file=tool_data.get("skip_for_file", False),
|
|
35
|
+
gate_strict=tool_data.get("gate_strict", True),
|
|
36
|
+
skip_if=tool_data.get("skip_if", ""),
|
|
35
37
|
)
|
|
36
38
|
return registry
|
|
37
39
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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,35 +1,35 @@
|
|
|
1
1
|
py_cq/__init__.py,sha256=-C6_QzQeydxft3ndlExTgWnvlqH8fJNPJxfySXULQmU,56
|
|
2
|
-
py_cq/api.py,sha256=
|
|
3
|
-
py_cq/cli.py,sha256=
|
|
2
|
+
py_cq/api.py,sha256=AxhTQVX4ACZmgCpdfJgB2_1VXtClxc531NkRoLK1vA0,10118
|
|
3
|
+
py_cq/cli.py,sha256=pP7lKoGE_cW9AGXEfDQMcfpb7inAtn6LJ0-cv0t5ONo,13784
|
|
4
4
|
py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
|
|
5
|
-
py_cq/config/config.toml,sha256=
|
|
6
|
-
py_cq/context_hash.py,sha256=
|
|
7
|
-
py_cq/execution_engine.py,sha256=
|
|
5
|
+
py_cq/config/config.toml,sha256=1-S9e5WK6K19FC1_SaGI5dfePuSwCufH0CssZPfIZgE,2921
|
|
6
|
+
py_cq/context_hash.py,sha256=qYPMacgSEwy0nYLCBW8JllNWsGfaSjVq31EFI811ZcU,3449
|
|
7
|
+
py_cq/execution_engine.py,sha256=ap3tDMxVD9x4bq_leq7IGYZgLM-ryJRnrVFSxSy5jK0,16893
|
|
8
8
|
py_cq/language_detector.py,sha256=rh5bAA3LyCQR3RODmx_EalqRNYvSYK2SJzh2hIkxOUo,1139
|
|
9
9
|
py_cq/llm_formatter.py,sha256=nCV3nFdxJAi2CnH9FKb5tB-0w-yjLWmjywQI5GJ2cSU,8412
|
|
10
|
-
py_cq/localtypes.py,sha256=
|
|
10
|
+
py_cq/localtypes.py,sha256=kHldvTJlwyoWg0xKbOL-jplLqwKnGmv6aRh95iq8r8Q,7783
|
|
11
11
|
py_cq/main.py,sha256=5dgr8Zk1LNK5M-0wncYVksBp0qE-jWZYqmOXfb1eOhk,400
|
|
12
12
|
py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
|
|
13
13
|
py_cq/parsers/__init__.py,sha256=uCy7R0cnvDPNvB5aWMLgM3FB9nDcu1jwUmznFMRROSY,28
|
|
14
14
|
py_cq/parsers/banditparser.py,sha256=YqpWljU5D4IhLdtzat7XVGBm9NXrbOQbr9LI9NKybK8,3298
|
|
15
|
-
py_cq/parsers/common.py,sha256=
|
|
15
|
+
py_cq/parsers/common.py,sha256=csIv0M7Xq68HjXn37OAD2RfMmoQWaChQ9Jf9lamMkvg,13890
|
|
16
16
|
py_cq/parsers/compileparser.py,sha256=jl0DloyXTKqVHRIUft-t5lvJOndY-PdLPrXB38v9zuA,6927
|
|
17
|
-
py_cq/parsers/complexityparser.py,sha256=
|
|
18
|
-
py_cq/parsers/coverageparser.py,sha256=
|
|
17
|
+
py_cq/parsers/complexityparser.py,sha256=mX1WHPRgBBc6kOqHfubowRbYXY0E1k4JVwr908Cj10U,5582
|
|
18
|
+
py_cq/parsers/coverageparser.py,sha256=9HtXyAMcQbhsUTb1VNClIHQajBwqCt26H-25t2ZQnY4,8092
|
|
19
19
|
py_cq/parsers/exitcodeparser.py,sha256=b4LWIwwklPwCX3MnqNsGQC1slQU56DKE05vJ0OdWVoA,957
|
|
20
|
-
py_cq/parsers/halsteadparser.py,sha256=
|
|
20
|
+
py_cq/parsers/halsteadparser.py,sha256=789c_WWfomskM4i7SUEbKvB67-F2y5nfqt48aFXOusY,10192
|
|
21
21
|
py_cq/parsers/interrogateparser.py,sha256=cc5Q4mOQlLUIPl3XUyH29QJ_JMBzAA7SiPAAiNGDJdM,11546
|
|
22
22
|
py_cq/parsers/linecountparser.py,sha256=-hqebwG4j7_QugzGLUUQRcPukGHicV2nbIp1pR8ZcAw,1310
|
|
23
23
|
py_cq/parsers/maintainabilityparser.py,sha256=3a4m93XNtUurw3IxEI1Iae9idgJY6qiEt7HMYl1wTu0,4793
|
|
24
24
|
py_cq/parsers/pytestparser.py,sha256=rhzFzsBEXv1xloOuEjQyBn8Vp0AyGoq2t5siyAwWWaA,11549
|
|
25
25
|
py_cq/parsers/regexcountparser.py,sha256=zGzHEYDIori6aiWnVkRFixdE7fWt66npAAWjzvpoBF4,1592
|
|
26
|
-
py_cq/parsers/ruffparser.py,sha256=
|
|
26
|
+
py_cq/parsers/ruffparser.py,sha256=uE0QEaZ4Ch7IsWY_2ZkfHXPU0TYPGsGPfv66hGe5G34,7634
|
|
27
27
|
py_cq/parsers/typarser.py,sha256=x835OrapG1aUteW2NhC5U0bhj1w1pMtbfmbvb74-1AI,8077
|
|
28
28
|
py_cq/parsers/vultureparser.py,sha256=-yGAXzRY4_fzLRTk7dfwDrrRZ-n97j1Ph5_xo3c6TfU,2229
|
|
29
29
|
py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
30
|
py_cq/table_formatter.py,sha256=2_WZ2RHCwsdna0bzP6ZFxB2YtHtW95wxJB1yBXgCIDo,1610
|
|
31
|
-
py_cq/tool_registry.py,sha256=
|
|
32
|
-
python_code_quality-0.2.
|
|
33
|
-
python_code_quality-0.2.
|
|
34
|
-
python_code_quality-0.2.
|
|
35
|
-
python_code_quality-0.2.
|
|
31
|
+
py_cq/tool_registry.py,sha256=8B6EG7mGCKR6JeHtesFHa79svNfj1PUbboysXfwV7vY,1828
|
|
32
|
+
python_code_quality-0.2.3.dist-info/WHEEL,sha256=wXwAVsgVaOZ_pwDFqQm5Rd6PID-Fc74nkLc8X8gHiDo,81
|
|
33
|
+
python_code_quality-0.2.3.dist-info/entry_points.txt,sha256=cfWbTw7eYO6Trv1-Z_odL6Zta9CqYU6Vk9lAHdfB60Q,40
|
|
34
|
+
python_code_quality-0.2.3.dist-info/METADATA,sha256=dBNJKPiG5BRMtuPT8jUcYMoasGRsP4uQQG9RvFy_xig,15650
|
|
35
|
+
python_code_quality-0.2.3.dist-info/RECORD,,
|
{python_code_quality-0.2.1.dist-info → python_code_quality-0.2.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|