python-code-quality 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_cq/cli.py +66 -13
- py_cq/config/config.yaml +88 -0
- py_cq/execution_engine.py +16 -4
- py_cq/language_detector.py +29 -0
- py_cq/llm_formatter.py +3 -2
- py_cq/localtypes.py +9 -5
- py_cq/parsers/banditparser.py +2 -2
- py_cq/parsers/common.py +31 -0
- py_cq/parsers/compileparser.py +2 -2
- py_cq/parsers/coverageparser.py +1 -1
- py_cq/parsers/exitcodeparser.py +16 -0
- py_cq/parsers/halsteadparser.py +2 -2
- py_cq/parsers/interrogateparser.py +1 -1
- py_cq/parsers/linecountparser.py +26 -0
- py_cq/parsers/pytestparser.py +60 -9
- py_cq/parsers/regexcountparser.py +35 -0
- py_cq/parsers/ruffparser.py +2 -2
- py_cq/parsers/typarser.py +2 -2
- py_cq/parsers/vultureparser.py +2 -2
- py_cq/tool_registry.py +6 -5
- {python_code_quality-0.1.7.dist-info → python_code_quality-0.1.9.dist-info}/METADATA +90 -95
- python_code_quality-0.1.9.dist-info/RECORD +34 -0
- {python_code_quality-0.1.7.dist-info → python_code_quality-0.1.9.dist-info}/WHEEL +1 -1
- py_cq/config/tools.yaml +0 -97
- python_code_quality-0.1.7.dist-info/RECORD +0 -30
- {python_code_quality-0.1.7.dist-info → python_code_quality-0.1.9.dist-info}/entry_points.txt +0 -0
- {python_code_quality-0.1.7.dist-info → python_code_quality-0.1.9.dist-info}/licenses/LICENSE +0 -0
py_cq/parsers/pytestparser.py
CHANGED
|
@@ -8,11 +8,33 @@ process return code so downstream components can uniformly consume results
|
|
|
8
8
|
from multiple test tools. It is part of the test-collection framework and
|
|
9
9
|
enables consistent handling of pytest output across the system."""
|
|
10
10
|
|
|
11
|
-
import re
|
|
11
|
+
import re as _re
|
|
12
12
|
|
|
13
13
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def _extract_failure(stdout: str, test_name: str, max_lines: int) -> str:
|
|
17
|
+
"""Extract the failure section for test_name from pytest stdout."""
|
|
18
|
+
lines = stdout.splitlines()
|
|
19
|
+
pattern = _re.compile(rf"_{{4,}}\s+{_re.escape(test_name)}\s+_{{4,}}")
|
|
20
|
+
start = None
|
|
21
|
+
for i, line in enumerate(lines):
|
|
22
|
+
if pattern.search(line):
|
|
23
|
+
start = i + 1
|
|
24
|
+
break
|
|
25
|
+
if start is None:
|
|
26
|
+
return ""
|
|
27
|
+
collected = []
|
|
28
|
+
for line in lines[start:]:
|
|
29
|
+
if line.strip().startswith("_") or line.strip().startswith("="):
|
|
30
|
+
break
|
|
31
|
+
collected.append(line)
|
|
32
|
+
if len(collected) >= max_lines:
|
|
33
|
+
break
|
|
34
|
+
text = "\n".join(collected).strip()
|
|
35
|
+
return f"\n```\n{text}\n```" if text else ""
|
|
36
|
+
|
|
37
|
+
|
|
16
38
|
class PytestParser(AbstractParser):
|
|
17
39
|
"""Parses raw pytest output into a structured `ToolResult`.
|
|
18
40
|
|
|
@@ -51,14 +73,14 @@ class PytestParser(AbstractParser):
|
|
|
51
73
|
lines = raw_result.stdout.splitlines()
|
|
52
74
|
tr = ToolResult(raw=raw_result)
|
|
53
75
|
if "no tests ran" in raw_result.stdout:
|
|
54
|
-
|
|
76
|
+
tr.metrics["tests"] = 0.0
|
|
55
77
|
else:
|
|
56
78
|
tests_found = dict()
|
|
57
79
|
num_tests = 0
|
|
58
80
|
passed_tests = 0
|
|
59
81
|
for line in lines:
|
|
60
82
|
# tests/test_common.py::test_name[param] PASSED [ 8%]
|
|
61
|
-
tests_match =
|
|
83
|
+
tests_match = _re.search(r"(.*\.py)::([\w\[\].,+\- ]+) (PASSED|FAILED|ERROR|SKIPPED|XFAIL|XPASS)", line)
|
|
62
84
|
if tests_match:
|
|
63
85
|
test_file = tests_match.group(1)
|
|
64
86
|
test_name = tests_match.group(2).strip()
|
|
@@ -67,15 +89,44 @@ class PytestParser(AbstractParser):
|
|
|
67
89
|
num_tests += 1
|
|
68
90
|
if test_status == "PASSED":
|
|
69
91
|
passed_tests += 1
|
|
92
|
+
if num_tests == 0:
|
|
93
|
+
# No individual test lines found (e.g. non-verbose output);
|
|
94
|
+
# fall back to parsing the pytest summary line.
|
|
95
|
+
summary = _re.search(r"(\d+) passed(?:.*?(\d+) failed)?", raw_result.stdout)
|
|
96
|
+
if summary:
|
|
97
|
+
passed_tests = int(summary.group(1))
|
|
98
|
+
failed_tests = int(summary.group(2)) if summary.group(2) else 0
|
|
99
|
+
num_tests = passed_tests + failed_tests
|
|
70
100
|
tr.metrics["tests"] = passed_tests / num_tests if num_tests else 0
|
|
71
101
|
tr.details = tests_found
|
|
72
102
|
return tr
|
|
73
103
|
|
|
74
|
-
def format_llm_message(self, tr: ToolResult) -> str:
|
|
75
|
-
"""Return the first failing test
|
|
104
|
+
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
105
|
+
"""Return the first failing test with function body and failure output."""
|
|
106
|
+
from py_cq.parsers.common import find_function_source
|
|
76
107
|
for file, tests in tr.details.items():
|
|
77
|
-
if isinstance(tests, dict):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
108
|
+
if not isinstance(tests, dict):
|
|
109
|
+
continue
|
|
110
|
+
for test_name, status in tests.items():
|
|
111
|
+
if status != "FAILED":
|
|
112
|
+
continue
|
|
113
|
+
header = f"`{file}::{test_name}` — test **FAILED**"
|
|
114
|
+
body = find_function_source(file, test_name, max_lines=context_lines)
|
|
115
|
+
failure = _extract_failure(tr.raw.stdout, test_name, max_lines=context_lines)
|
|
116
|
+
parts = [header]
|
|
117
|
+
if body:
|
|
118
|
+
parts.append(body)
|
|
119
|
+
if failure:
|
|
120
|
+
parts.append(failure)
|
|
121
|
+
return "\n".join(parts)
|
|
122
|
+
if "no tests ran" in tr.raw.stdout:
|
|
123
|
+
return (
|
|
124
|
+
"**No tests found.** This project has no pytest test suite.\n\n"
|
|
125
|
+
"Add a `tests/` directory with at least one test file (e.g. `tests/test_basic.py`) "
|
|
126
|
+
"and write a first test covering a core function."
|
|
127
|
+
)
|
|
128
|
+
output = (tr.raw.stdout + tr.raw.stderr).strip()
|
|
129
|
+
if output:
|
|
130
|
+
tail = "\n".join(output.splitlines()[-30:])
|
|
131
|
+
return f"pytest reported failures:\n\n```\n{tail}\n```"
|
|
81
132
|
return "pytest reported failures (no details available)"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Parser that counts stdout lines matching a regex pattern."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
6
|
+
from py_cq.parsers.common import score_logistic_variant
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegexCountParser(AbstractParser):
|
|
10
|
+
"""Score based on the number of stdout lines matching a regex.
|
|
11
|
+
|
|
12
|
+
parser_config keys:
|
|
13
|
+
pattern (str, required): regex pattern to match against each line.
|
|
14
|
+
scale_factor (int, default 15): passed to score_logistic_variant.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
18
|
+
pattern = re.compile(self.parser_config["pattern"])
|
|
19
|
+
scale = self.parser_config.get("scale_factor", 15)
|
|
20
|
+
lines = (raw_result.stdout or "").splitlines()
|
|
21
|
+
matches = [ln for ln in lines if pattern.search(ln)]
|
|
22
|
+
count = len(matches)
|
|
23
|
+
score = score_logistic_variant(count, scale_factor=scale)
|
|
24
|
+
return ToolResult(
|
|
25
|
+
raw=raw_result,
|
|
26
|
+
metrics={"violations": score},
|
|
27
|
+
details={"count": count, "matches": matches},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
31
|
+
matches = tr.details.get("matches", [])
|
|
32
|
+
if not matches:
|
|
33
|
+
return "No violations found"
|
|
34
|
+
shown = matches[:context_lines]
|
|
35
|
+
return "\n".join(shown)
|
py_cq/parsers/ruffparser.py
CHANGED
|
@@ -45,7 +45,7 @@ class RuffParser(AbstractParser):
|
|
|
45
45
|
)
|
|
46
46
|
return ToolResult(raw=raw_result, metrics={"lint": score}, details=files)
|
|
47
47
|
|
|
48
|
-
def format_llm_message(self, tr: ToolResult) -> str:
|
|
48
|
+
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
49
49
|
"""Return the first lint violation as a defect description."""
|
|
50
50
|
if not tr.details:
|
|
51
51
|
return "ruff reported issues (no details available)"
|
|
@@ -54,4 +54,4 @@ class RuffParser(AbstractParser):
|
|
|
54
54
|
line = issue.get("line", "?")
|
|
55
55
|
code = issue.get("code", "")
|
|
56
56
|
message = issue.get("message", "")
|
|
57
|
-
return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line)}"
|
|
57
|
+
return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line, count=context_lines)}"
|
py_cq/parsers/typarser.py
CHANGED
|
@@ -49,7 +49,7 @@ class TyParser(AbstractParser):
|
|
|
49
49
|
score = score_logistic_variant(weighted, scale_factor=10)
|
|
50
50
|
return ToolResult(raw=raw_result, metrics={"type_check": score}, details=files)
|
|
51
51
|
|
|
52
|
-
def format_llm_message(self, tr: ToolResult) -> str:
|
|
52
|
+
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
53
53
|
"""Return the first type-check diagnostic as a defect description."""
|
|
54
54
|
if not tr.details:
|
|
55
55
|
return "ty reported issues (no details available)"
|
|
@@ -58,4 +58,4 @@ class TyParser(AbstractParser):
|
|
|
58
58
|
line = issue.get("line", "?")
|
|
59
59
|
code = issue.get("code", "")
|
|
60
60
|
message = issue.get("message", "")
|
|
61
|
-
return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line)}"
|
|
61
|
+
return f"`{file}:{line}` — **{code}**: {message}{format_source_context(file, line, count=context_lines)}"
|
py_cq/parsers/vultureparser.py
CHANGED
|
@@ -36,7 +36,7 @@ class VultureParser(AbstractParser):
|
|
|
36
36
|
score = score_logistic_variant(count, scale_factor=15)
|
|
37
37
|
return ToolResult(raw=raw_result, metrics={"dead_code": score}, details=files)
|
|
38
38
|
|
|
39
|
-
def format_llm_message(self, tr: ToolResult) -> str:
|
|
39
|
+
def format_llm_message(self, tr: ToolResult, *, context_lines: int = 15) -> str:
|
|
40
40
|
if not tr.details:
|
|
41
41
|
return "vulture reported issues (no details available)"
|
|
42
42
|
file, issues = next(iter(tr.details.items()))
|
|
@@ -45,4 +45,4 @@ class VultureParser(AbstractParser):
|
|
|
45
45
|
kind = issue.get("type", "unused")
|
|
46
46
|
name = issue.get("name", "")
|
|
47
47
|
confidence = issue.get("confidence", "?")
|
|
48
|
-
return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line)}"
|
|
48
|
+
return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
|
py_cq/tool_registry.py
CHANGED
|
@@ -9,26 +9,27 @@ from py_cq.localtypes import ToolConfig
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def load_tool_configs() -> dict[str, ToolConfig]:
|
|
12
|
-
"""Load tool configurations from the bundled
|
|
12
|
+
"""Load tool configurations from the bundled config.yaml and return a registry.
|
|
13
13
|
|
|
14
14
|
Returns:
|
|
15
15
|
dict[str, ToolConfig]: A mapping from tool ID to its configuration instance."""
|
|
16
|
-
yaml_text = files("py_cq.config").joinpath("
|
|
16
|
+
yaml_text = files("py_cq.config").joinpath("config.yaml").read_text(encoding="utf-8")
|
|
17
17
|
config = yaml.safe_load(yaml_text)
|
|
18
18
|
registry = {}
|
|
19
|
-
for tool_id, tool_data in config["
|
|
19
|
+
for tool_id, tool_data in config["python"].items():
|
|
20
20
|
# Dynamically import parser class
|
|
21
21
|
module = import_module(f"py_cq.parsers.{tool_data['parser'].lower()}")
|
|
22
22
|
parser_class = getattr(module, tool_data["parser"])
|
|
23
23
|
registry[tool_id] = ToolConfig(
|
|
24
|
-
name=
|
|
24
|
+
name=tool_id,
|
|
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),
|
|
31
31
|
extra_deps=tool_data.get("extra_deps", []),
|
|
32
|
+
parser_config=tool_data.get("parser_config", {}),
|
|
32
33
|
)
|
|
33
34
|
return registry
|
|
34
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rhiza-fr/py-cq
|
|
6
6
|
Project-URL: Repository, https://github.com/rhiza-fr/py-cq
|
|
@@ -28,14 +28,16 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
|
|
30
30
|
|
|
31
|
+
Why? It removes the mental burden of understanding all these tools and parsing their results.
|
|
32
|
+
|
|
31
33
|
The primary workflow is:
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
36
|
# get the single most critical defect as markdown
|
|
35
37
|
cq check . -o llm
|
|
36
38
|
```
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
39
41
|
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
40
42
|
|
|
41
43
|
18: min_dist = float("inf")
|
|
@@ -51,10 +53,13 @@ Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
|
|
|
51
53
|
```
|
|
52
54
|
Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
53
55
|
|
|
54
|
-
```
|
|
56
|
+
```python
|
|
55
57
|
cq check . -o llm | claude -p "fix this"
|
|
58
|
+
# or
|
|
59
|
+
cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
|
|
56
60
|
```
|
|
57
61
|
|
|
62
|
+
|
|
58
63
|
## Install
|
|
59
64
|
|
|
60
65
|
```bash
|
|
@@ -62,21 +67,22 @@ cq check . -o llm | claude -p "fix this"
|
|
|
62
67
|
uv tool install python-code-quality
|
|
63
68
|
|
|
64
69
|
# or, clone it then install
|
|
65
|
-
git
|
|
70
|
+
git clone https://github.com/rhiza-fr/py-cq.git
|
|
66
71
|
cd py-cq
|
|
67
72
|
uv tool install .
|
|
68
73
|
```
|
|
69
74
|
|
|
70
75
|
## Tools
|
|
71
76
|
|
|
72
|
-
These tools are run in **parallel
|
|
77
|
+
These tools are run in **parallel** except:
|
|
78
|
+
When running '-o llm', we run sequentially and exit early at the first error.
|
|
73
79
|
|
|
74
|
-
|
|
|
80
|
+
| Order | Tool | Measures |
|
|
75
81
|
|----------|------|----------|
|
|
76
82
|
| 1 | compileall | Syntax errors |
|
|
77
|
-
| 2 |
|
|
78
|
-
| 3 |
|
|
79
|
-
| 4 |
|
|
83
|
+
| 2 | ruff | Lint / style |
|
|
84
|
+
| 3 | ty | Type errors |
|
|
85
|
+
| 4 | bandit | Security vulnerabilities |
|
|
80
86
|
| 5 | pytest | Test pass rate |
|
|
81
87
|
| 6 | coverage | Test coverage |
|
|
82
88
|
| 7 | radon cc | Cyclomatic complexity |
|
|
@@ -85,37 +91,28 @@ These tools are run in **parallel**:
|
|
|
85
91
|
| 10 | vulture | Dead code |
|
|
86
92
|
| 11 | interrogate | Docstring coverage |
|
|
87
93
|
|
|
88
|
-
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
|
|
94
|
+
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
|
|
89
95
|
|
|
90
96
|
|
|
91
97
|
## Usage
|
|
92
98
|
|
|
93
99
|
```bash
|
|
94
|
-
|
|
95
|
-
cq check -o llm
|
|
96
|
-
|
|
97
|
-
#
|
|
98
|
-
cq check .
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cq check . -
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
cq check . -o json
|
|
105
|
-
|
|
106
|
-
# Explicit path
|
|
107
|
-
cq check path/to/project/
|
|
108
|
-
cq check path/to/file.py
|
|
109
|
-
|
|
110
|
-
# Run sequentially if you like things slow
|
|
111
|
-
cq check . --workers 1
|
|
100
|
+
cq check . # Table overview of scores for humans
|
|
101
|
+
cq check . -o llm # Top defect as markdown for LLMs
|
|
102
|
+
cq check . -o score # Numeric score only for CI
|
|
103
|
+
cq check . -o json # Detailed parsed JSON output for jq
|
|
104
|
+
cq check . -o raw # Raw tool output for debug
|
|
105
|
+
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
106
|
+
cq check . --workers 1 # Run sequentially if you like things slow
|
|
107
|
+
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
108
|
+
cq config path/to/project/ # Show effective tool configuration
|
|
109
|
+
```
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
cq check . --clear-cache
|
|
111
|
+
**Exit codes:** `cq check` exits with code `1` if any tool metric falls below its `error_threshold`, making it suitable as a CI gate:
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
cq
|
|
118
|
-
cq
|
|
113
|
+
```bash
|
|
114
|
+
cq check . && deploy # block deploy on errors
|
|
115
|
+
cq check . -o score # print score, exit 1 on errors
|
|
119
116
|
```
|
|
120
117
|
|
|
121
118
|
## Table output
|
|
@@ -129,9 +126,9 @@ cq config path/to/project/
|
|
|
129
126
|
┃ Tool ┃ Time ┃ Metric ┃ Score ┃ Status ┃
|
|
130
127
|
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
|
|
131
128
|
│ compile │ 0.42s │ compile │ 1.000 │ OK │
|
|
132
|
-
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
133
129
|
│ ruff │ 0.17s │ lint │ 1.000 │ OK │
|
|
134
130
|
│ ty │ 0.33s │ type_check │ 1.000 │ OK │
|
|
131
|
+
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
135
132
|
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
136
133
|
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
137
134
|
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
@@ -160,30 +157,36 @@ cq config path/to/project/
|
|
|
160
157
|
```
|
|
161
158
|
|
|
162
159
|
```json
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
"details": {},
|
|
170
|
-
"raw": {
|
|
171
|
-
"tool_name": "compile",
|
|
172
|
-
"command": ".venv\\Scripts\\python.exe -m compileall -r 10 -j 8 . -x .*venv",
|
|
173
|
-
"stdout": "Compiling './src/project/file.py'...",
|
|
174
|
-
"stderr": "",
|
|
175
|
-
"return_code": 0,
|
|
176
|
-
"timestamp": "2026-02-19 05:03:11"
|
|
177
|
-
},
|
|
178
|
-
"duration_s": 0.08294440002646297
|
|
160
|
+
[
|
|
161
|
+
{
|
|
162
|
+
"tool_name": "compile",
|
|
163
|
+
"metrics": {
|
|
164
|
+
"compile": 1.0
|
|
179
165
|
},
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
166
|
+
"details": {},
|
|
167
|
+
"duration_s": 0.05611889995634556
|
|
168
|
+
}
|
|
169
|
+
...
|
|
170
|
+
]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Raw output
|
|
174
|
+
```bash
|
|
175
|
+
> cq check . -o raw
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
[
|
|
180
|
+
{
|
|
181
|
+
"tool_name": "compile",
|
|
182
|
+
"command": "D:\\ai\\py-cq\\.venv\\Scripts\\python.exe -m compileall -r 10 -j 8 . -x .*venv",
|
|
183
|
+
"stdout": "",
|
|
184
|
+
"stderr": "",
|
|
185
|
+
"return_code": 0,
|
|
186
|
+
"timestamp": "2026-02-20 10:01:22"
|
|
187
|
+
}
|
|
188
|
+
...
|
|
189
|
+
]
|
|
187
190
|
```
|
|
188
191
|
|
|
189
192
|
## Configuration
|
|
@@ -195,69 +198,66 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
195
198
|
# Skip tools that are slow or not relevant to your project
|
|
196
199
|
disable = ["coverage", "interrogate"]
|
|
197
200
|
|
|
201
|
+
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
202
|
+
context_lines = 15
|
|
203
|
+
|
|
198
204
|
# Override warning/error thresholds per tool
|
|
199
205
|
[tool.cq.thresholds.coverage]
|
|
200
206
|
warning = 0.9
|
|
201
207
|
error = 0.7
|
|
202
208
|
```
|
|
203
209
|
|
|
204
|
-
Tool IDs match the keys in `config/
|
|
210
|
+
Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
|
|
205
211
|
|
|
206
212
|
|
|
207
213
|
### Default config
|
|
208
214
|
|
|
209
215
|
```yaml
|
|
210
|
-
|
|
216
|
+
python:
|
|
211
217
|
|
|
212
|
-
|
|
213
|
-
name: "compile"
|
|
218
|
+
compile:
|
|
214
219
|
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
215
220
|
parser: "CompileParser"
|
|
216
|
-
|
|
221
|
+
order: 1
|
|
217
222
|
warning_threshold: 0.9999
|
|
218
223
|
error_threshold: 0.9999
|
|
219
224
|
|
|
220
|
-
bandit:
|
|
221
|
-
name: "bandit"
|
|
222
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
223
|
-
parser: "BanditParser"
|
|
224
|
-
priority: 2
|
|
225
|
-
warning_threshold: 0.9999
|
|
226
|
-
error_threshold: 0.8
|
|
227
|
-
|
|
228
225
|
ruff:
|
|
229
|
-
name: "ruff"
|
|
230
226
|
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
231
227
|
parser: "RuffParser"
|
|
232
|
-
|
|
228
|
+
order: 2
|
|
233
229
|
warning_threshold: 0.9999
|
|
234
230
|
error_threshold: 0.9
|
|
235
231
|
|
|
236
232
|
ty:
|
|
237
|
-
name: "ty"
|
|
238
233
|
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
239
234
|
parser: "TyParser"
|
|
240
|
-
|
|
235
|
+
order: 3
|
|
241
236
|
warning_threshold: 0.9999
|
|
242
237
|
error_threshold: 0.8
|
|
243
238
|
run_in_target_env: true
|
|
244
239
|
extra_deps:
|
|
245
240
|
- ty
|
|
246
241
|
|
|
242
|
+
bandit:
|
|
243
|
+
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
244
|
+
parser: "BanditParser"
|
|
245
|
+
order: 4
|
|
246
|
+
warning_threshold: 0.9999
|
|
247
|
+
error_threshold: 0.8
|
|
248
|
+
|
|
247
249
|
pytest:
|
|
248
|
-
name: "pytest"
|
|
249
250
|
command: "{python} -m pytest -v {context_path}"
|
|
250
251
|
parser: "PytestParser"
|
|
251
|
-
|
|
252
|
-
warning_threshold: 0
|
|
253
|
-
error_threshold: 0
|
|
252
|
+
order: 5
|
|
253
|
+
warning_threshold: 1.0
|
|
254
|
+
error_threshold: 1.0
|
|
254
255
|
run_in_target_env: true
|
|
255
256
|
|
|
256
257
|
coverage:
|
|
257
|
-
|
|
258
|
-
command: "{python} -m coverage run -m pytest {context_path} && {python} -m coverage report"
|
|
258
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
259
259
|
parser: "CoverageParser"
|
|
260
|
-
|
|
260
|
+
order: 6
|
|
261
261
|
warning_threshold: 0.9
|
|
262
262
|
error_threshold: 0.5
|
|
263
263
|
run_in_target_env: true
|
|
@@ -265,43 +265,38 @@ tools:
|
|
|
265
265
|
- coverage
|
|
266
266
|
- pytest
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
name: "radon cc"
|
|
268
|
+
radon-cc:
|
|
270
269
|
command: "{python} -m radon cc --json {context_path}"
|
|
271
270
|
parser: "ComplexityParser"
|
|
272
|
-
|
|
271
|
+
order: 7
|
|
273
272
|
warning_threshold: 0.6
|
|
274
273
|
error_threshold: 0.4
|
|
275
274
|
|
|
276
|
-
|
|
277
|
-
name: "radon mi"
|
|
275
|
+
radon-mi:
|
|
278
276
|
command: "{python} -m radon mi -s --json {context_path}"
|
|
279
277
|
parser: "MaintainabilityParser"
|
|
280
|
-
|
|
278
|
+
order: 8
|
|
281
279
|
warning_threshold: 0.6
|
|
282
280
|
error_threshold: 0.4
|
|
283
281
|
|
|
284
|
-
|
|
285
|
-
name: "radon hal"
|
|
282
|
+
radon-hal:
|
|
286
283
|
command: "{python} -m radon hal -f --json {context_path}"
|
|
287
284
|
parser: "HalsteadParser"
|
|
288
|
-
|
|
285
|
+
order: 9
|
|
289
286
|
warning_threshold: 0.5
|
|
290
287
|
error_threshold: 0.3
|
|
291
288
|
|
|
292
289
|
vulture:
|
|
293
|
-
name: "vulture"
|
|
294
290
|
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
295
291
|
parser: "VultureParser"
|
|
296
|
-
|
|
292
|
+
order: 10
|
|
297
293
|
warning_threshold: 0.9999
|
|
298
294
|
error_threshold: 0.8
|
|
299
295
|
|
|
300
296
|
interrogate:
|
|
301
|
-
name: "interrogate"
|
|
302
297
|
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
303
298
|
parser: "InterrogateParser"
|
|
304
|
-
|
|
299
|
+
order: 11
|
|
305
300
|
warning_threshold: 0.8
|
|
306
301
|
error_threshold: 0.3
|
|
307
302
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
py_cq/__init__.py,sha256=rS7kf1RU1zZskvJlkbZaMqEpdRYPspwDPAFbxzF3tXg,373
|
|
2
|
+
py_cq/cli.py,sha256=wu1GlxSDxS835i9-mO4-xmyBLfr6puU-ES-26T7Mty0,11007
|
|
3
|
+
py_cq/context_hash.py,sha256=h-i7Rhd7AUfLv9SkQvE79bjJvTsm_ZwoVwSmUKXWmfM,2977
|
|
4
|
+
py_cq/execution_engine.py,sha256=tgNGFOO3h-EyetCEzC_RS2K-b9OkOFpOwGwrEAHIpZA,7477
|
|
5
|
+
py_cq/language_detector.py,sha256=6av5HaimcZ54RkN69xQmGgC0mxtTvGzPV3SL8NGG8Uc,1116
|
|
6
|
+
py_cq/llm_formatter.py,sha256=l74O5iqNWMR16789Xncoi93xOqgQJhW5IQK9_OyA9mE,1632
|
|
7
|
+
py_cq/localtypes.py,sha256=jCX1ZuwTixQooEk2N1Gi1XRuDcdBc2NlOdQBKysEybk,6128
|
|
8
|
+
py_cq/main.py,sha256=VKoXI8R8rB2fEROBYoTVURfinLqyh8XTNIIWAOtH7dw,380
|
|
9
|
+
py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
|
|
10
|
+
py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
py_cq/tool_registry.py,sha256=oMEkFHkU3gg5UpeGD4zHtynOYmWieRgDN5kTwZ5KsE8,1584
|
|
12
|
+
py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
|
|
13
|
+
py_cq/config/config.yaml,sha256=R8CjZH8erBQSbJFKgHt-CCwD1Mgj6ZCGQ3OtWH0Ebig,2402
|
|
14
|
+
py_cq/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
py_cq/parsers/banditparser.py,sha256=Ju_CkuXtkVn1Th9aQ6mv6fTUTpb9pc1YD8Nzt_nMgFQ,2326
|
|
16
|
+
py_cq/parsers/common.py,sha256=thbziNjqaf-SY3Y0W-KmPwzlcC-YCV98Z8jubUJM7_4,5203
|
|
17
|
+
py_cq/parsers/compileparser.py,sha256=YsT7ePUDRjsUHTLqgRv4x6jJBCTt6cbm1mgxplzxETg,6256
|
|
18
|
+
py_cq/parsers/complexityparser.py,sha256=2t1-wmNjUu65fULcIm5jcgv7ZLWwjakyiY_r-Fx1QQg,3983
|
|
19
|
+
py_cq/parsers/coverageparser.py,sha256=n4h_RCxKvJoYgp356PA4laAmFcngjQ41b_GXZvKkVgc,4041
|
|
20
|
+
py_cq/parsers/exitcodeparser.py,sha256=ZFL3EbPhGvFjm2qZhfLD1_5dNjR9ULYNQpKgy0Z_dGo,729
|
|
21
|
+
py_cq/parsers/halsteadparser.py,sha256=ZCN7LP1iUZ91tf3tlspKAPQrTEa78XVdE9Fjn9yPvYk,9008
|
|
22
|
+
py_cq/parsers/interrogateparser.py,sha256=G07beMaGR0Mq-pGVozAbwK0xOx9MdMaNVGq2CGrUb7w,2225
|
|
23
|
+
py_cq/parsers/linecountparser.py,sha256=nxWvFntuR8T6FDGpafrlevvt-jR3rwkbVo_t2JX4TF0,1067
|
|
24
|
+
py_cq/parsers/maintainabilityparser.py,sha256=Ax0ZFA6zzqYIWZH1hP1_GUtdVn2LIJ8SKWtqVNdszYs,3411
|
|
25
|
+
py_cq/parsers/pytestparser.py,sha256=3N1X4wKhJ2h-2U1GuM3gHWHpHxU9f0LPGcPaojra9W4,6377
|
|
26
|
+
py_cq/parsers/regexcountparser.py,sha256=KSoNh2spucXU06pxxr2QW0LrPLfJkFMAsmSgjooaFv0,1316
|
|
27
|
+
py_cq/parsers/ruffparser.py,sha256=ZdIya4sct2PrsOyfKfMGmXHLh3Qu7HtFqXNY9IuNFog,2275
|
|
28
|
+
py_cq/parsers/typarser.py,sha256=zrI0KS65MUGPxYPP74B6BRyTbjcPhKyQPjH2KZIyxN0,2495
|
|
29
|
+
py_cq/parsers/vultureparser.py,sha256=U6zC7P0ATA_N4SB90BahKF5QHMITf_z-NsOgrh_Q5rA,1995
|
|
30
|
+
python_code_quality-0.1.9.dist-info/METADATA,sha256=MtSk0DgDh6bkeATKhz03Y8thLZvxxuOcYlvfvOmHkD8,10149
|
|
31
|
+
python_code_quality-0.1.9.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
32
|
+
python_code_quality-0.1.9.dist-info/entry_points.txt,sha256=j5Q_gGr0b7389lddt1JlZ7gcL4Z7RHxuDhij_G-IhBY,39
|
|
33
|
+
python_code_quality-0.1.9.dist-info/licenses/LICENSE,sha256=Bpuh8tbf37so8M5NtRGTLmT5ue7diJ17223L9f9nsT0,1086
|
|
34
|
+
python_code_quality-0.1.9.dist-info/RECORD,,
|