python-code-quality 0.1.6__tar.gz → 0.1.8__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.6 → python_code_quality-0.1.8}/CLAUDE.md +5 -5
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/PKG-INFO +189 -55
- python_code_quality-0.1.8/README.md +296 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/pyproject.toml +2 -2
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/cli.py +16 -18
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/config/__init__.py +1 -3
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/config/tools.yaml +13 -13
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/execution_engine.py +21 -9
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/llm_formatter.py +1 -1
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/localtypes.py +4 -4
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/banditparser.py +2 -2
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/common.py +12 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/compileparser.py +2 -8
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/halsteadparser.py +1 -1
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/pytestparser.py +8 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/ruffparser.py +2 -6
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/typarser.py +2 -6
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/vultureparser.py +2 -2
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/tool_registry.py +1 -1
- python_code_quality-0.1.8/tests/test_cli.py +228 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_common.py +18 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_config.py +2 -2
- python_code_quality-0.1.8/tests/test_execution_engine.py +291 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_llm_formatter.py +14 -14
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_localtypes.py +17 -2
- python_code_quality-0.1.8/tests/test_parser_halstead.py +116 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_pytest.py +14 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/uv.lock +1 -1
- python_code_quality-0.1.6/README.md +0 -162
- python_code_quality-0.1.6/src/py_cq/storage.py +0 -27
- python_code_quality-0.1.6/tests/test_execution_engine.py +0 -127
- python_code_quality-0.1.6/tests/test_storage.py +0 -41
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/.github/workflows/python-publish.yml +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/.gitignore +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/.python-version +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/LICENSE +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/data/problems/travelling_salesman/ts_bad.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/data/problems/travelling_salesman/ts_good.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/__init__.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/main.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/complexityparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/coverageparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/interrogateparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/parsers/maintainabilityparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/src/py_cq/py.typed +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/conftest.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_context_hash.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_bandit.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_compile.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_complexity.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_coverage.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_interrogate.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_maintainability.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_ruff.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_ty.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.8}/tests/test_parser_vulture.py +0 -0
|
@@ -11,7 +11,7 @@ cq check -o llm # returns the single most critical defect as markdown
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
The LLM fixes it, the user re-runs, and repeats until all tools pass. CQ runs
|
|
14
|
-
11 static analysis tools in
|
|
14
|
+
11 static analysis tools in execution order (compile → security → lint → types →
|
|
15
15
|
tests → coverage → complexity → dead code → style) and aggregates results into
|
|
16
16
|
a single score.
|
|
17
17
|
|
|
@@ -40,15 +40,15 @@ uv run ruff check src/
|
|
|
40
40
|
|
|
41
41
|
- **`cli.py`**: Typer app with a single `check` command. Output mode selected via `--output`/`-o` enum (`table`, `score`, `json`, `llm`). Runs tools in parallel by default. Reads `[tool.cq]` from the target project's `pyproject.toml` and applies overrides before running.
|
|
42
42
|
- **`tool_registry.py`**: Loads `src/cq/config/tools.yaml` at import time via `importlib.resources`, dynamically imports parser classes, builds a `dict[str, ToolConfig]` registry.
|
|
43
|
-
- **`config/tools.yaml`** (at `src/cq/config/tools.yaml`): Declares each analysis tool: shell command template (with `{context_path}` placeholder), parser class name,
|
|
44
|
-
- **`execution_engine.py`**: Runs shell commands via `subprocess.run`, caches results with `diskcache` using a content-based hash. Parallel execution via `ThreadPoolExecutor`; results are sorted by
|
|
43
|
+
- **`config/tools.yaml`** (at `src/cq/config/tools.yaml`): Declares each analysis tool: shell command template (with `{context_path}` placeholder), parser class name, order, warning/error thresholds. Tools are listed and executed in order.
|
|
44
|
+
- **`execution_engine.py`**: Runs shell commands via `subprocess.run`, caches results with `diskcache` using a content-based hash. Parallel execution via `ThreadPoolExecutor`; results are sorted by order before returning.
|
|
45
45
|
- **`parsers/`**: Each parser subclasses `AbstractParser` (from `localtypes.py`), implementing `parse(RawResult) -> ToolResult` and optionally `format_llm_message(ToolResult) -> str`. Parser module names must match the lowercase parser class name (e.g., `PytestParser` → `pytestparser.py`).
|
|
46
46
|
- **`localtypes.py`**: Core dataclasses — `ToolConfig`, `RawResult`, `ToolResult`, `CombinedToolResults`, and `AbstractParser` ABC.
|
|
47
47
|
- **`metric_aggregator.py`**: Wraps results into `CombinedToolResults`, which computes an overall score as the average of per-tool mean metrics.
|
|
48
|
-
- **`llm_formatter.py`**: Selects the worst-scoring tool by severity tier then
|
|
48
|
+
- **`llm_formatter.py`**: Selects the worst-scoring tool by severity tier then order, formats its top defect as markdown for LLM consumption.
|
|
49
49
|
|
|
50
50
|
## Adding a New Analysis Tool
|
|
51
51
|
|
|
52
|
-
1. Add tool entry in `config/tools.yaml` with command template, parser name,
|
|
52
|
+
1. Add tool entry in `config/tools.yaml` with command template, parser name, order, and thresholds.
|
|
53
53
|
2. Create `src/cq/parsers/<parsername>.py` with a class matching the `parser` field in YAML.
|
|
54
54
|
3. The parser must subclass `AbstractParser` and implement `parse(RawResult) -> ToolResult`.
|
|
@@ -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,296 @@
|
|
|
1
|
+
# CQ - Python Code Quality Analysis Tool
|
|
2
|
+
|
|
3
|
+
Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
|
|
4
|
+
|
|
5
|
+
The primary workflow is:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# get the single most critical defect as markdown
|
|
9
|
+
cq check . -o llm
|
|
10
|
+
```
|
|
11
|
+
Selects the single most critical defect using this priority order:
|
|
12
|
+
|
|
13
|
+
1. **Severity** — tools with score below `error_threshold` come before those only below `warning_threshold`
|
|
14
|
+
2. **Order** — among tools at the same severity, lower-order tools win (compile before lint before style)
|
|
15
|
+
3. **Score** — among ties, the lower score wins
|
|
16
|
+
|
|
17
|
+
The code context is expanded if available.
|
|
18
|
+
```md
|
|
19
|
+
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
20
|
+
|
|
21
|
+
18: min_dist = float("inf")
|
|
22
|
+
19: nearest_city = None
|
|
23
|
+
20: for city in cities:
|
|
24
|
+
21: unused_variable = 67
|
|
25
|
+
22: dist = calc_dist(current_city, city)
|
|
26
|
+
23: if dist < min_dist:
|
|
27
|
+
24: min_dist = dist
|
|
28
|
+
25: nearest_city = city
|
|
29
|
+
|
|
30
|
+
Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
|
|
31
|
+
```
|
|
32
|
+
Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
cq check . -o llm | claude -p "fix this"
|
|
36
|
+
# or
|
|
37
|
+
cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# install the `cq` command line tool from PyPi
|
|
44
|
+
uv tool install python-code-quality
|
|
45
|
+
|
|
46
|
+
# or, clone it then install
|
|
47
|
+
git pull https://github.com/rhiza-fr/py-cq.git
|
|
48
|
+
cd py-cq
|
|
49
|
+
uv tool install .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
These tools are run in **parallel** except when looking for the first error in -o llm mode:
|
|
55
|
+
|
|
56
|
+
| Order | Tool | Measures |
|
|
57
|
+
|----------|------|----------|
|
|
58
|
+
| 1 | compileall | Syntax errors |
|
|
59
|
+
| 2 | bandit | Security vulnerabilities |
|
|
60
|
+
| 3 | ruff | Lint / style |
|
|
61
|
+
| 4 | ty | Type errors |
|
|
62
|
+
| 5 | pytest | Test pass rate |
|
|
63
|
+
| 6 | coverage | Test coverage |
|
|
64
|
+
| 7 | radon cc | Cyclomatic complexity |
|
|
65
|
+
| 8 | radon mi | Maintainability index |
|
|
66
|
+
| 9 | radon hal | Halstead volume / bug estimate |
|
|
67
|
+
| 10 | vulture | Dead code |
|
|
68
|
+
| 11 | interrogate | Docstring coverage |
|
|
69
|
+
|
|
70
|
+
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cq check . # Table overview of scores for humans
|
|
77
|
+
cq check -o llm # Top defect as markdown for LLMs
|
|
78
|
+
cq check . -o score # Numeric score only for CI
|
|
79
|
+
cq check . -o json # Detailed parsed JSON output for jq
|
|
80
|
+
cq check . -o raw # Raw tool output for debug
|
|
81
|
+
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
82
|
+
cq check . --workers 1 # Run sequentially if you like things slow
|
|
83
|
+
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
84
|
+
cq config path/to/project/ # Show effective tool configuration
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Table output
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
> cq check .
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┓
|
|
95
|
+
┃ Tool ┃ Time ┃ Metric ┃ Score ┃ Status ┃
|
|
96
|
+
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
|
|
97
|
+
│ compile │ 0.42s │ compile │ 1.000 │ OK │
|
|
98
|
+
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
99
|
+
│ ruff │ 0.17s │ lint │ 1.000 │ OK │
|
|
100
|
+
│ ty │ 0.33s │ type_check │ 1.000 │ OK │
|
|
101
|
+
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
102
|
+
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
103
|
+
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
104
|
+
│ radon mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
105
|
+
│ radon hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
106
|
+
│ radon hal │ │ file_smallness │ 0.851 │ OK │
|
|
107
|
+
│ radon hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
108
|
+
│ radon hal │ │ functions_smallness │ 0.724 │ OK │
|
|
109
|
+
│ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
|
|
110
|
+
│ interrogate │ 0.36s │ doc_coverage │ 1.000 │ OK │
|
|
111
|
+
│ │ │ Score │ 0.965 │ │
|
|
112
|
+
└──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Single score output
|
|
116
|
+
```bash
|
|
117
|
+
> cq check . -o score
|
|
118
|
+
```
|
|
119
|
+
```python
|
|
120
|
+
0.9662730667181059 # this is designed to approach but not reach 1.0
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Json output
|
|
124
|
+
```bash
|
|
125
|
+
> cq check . -o json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
"tool_name": "compile",
|
|
132
|
+
"metrics": {
|
|
133
|
+
"compile": 1.0
|
|
134
|
+
},
|
|
135
|
+
"details": {},
|
|
136
|
+
"duration_s": 0.05611889995634556
|
|
137
|
+
}
|
|
138
|
+
...
|
|
139
|
+
]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Raw output
|
|
143
|
+
```bash
|
|
144
|
+
> cq check -o raw
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
[
|
|
149
|
+
{
|
|
150
|
+
"tool_name": "compile",
|
|
151
|
+
"command": "D:\\ai\\py-cq\\.venv\\Scripts\\python.exe -m compileall -r 10 -j 8 . -x .*venv",
|
|
152
|
+
"stdout": "",
|
|
153
|
+
"stderr": "",
|
|
154
|
+
"return_code": 0,
|
|
155
|
+
"timestamp": "2026-02-20 10:01:22"
|
|
156
|
+
}
|
|
157
|
+
...
|
|
158
|
+
]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
164
|
+
|
|
165
|
+
```toml
|
|
166
|
+
[tool.cq]
|
|
167
|
+
# Skip tools that are slow or not relevant to your project
|
|
168
|
+
disable = ["coverage", "interrogate"]
|
|
169
|
+
|
|
170
|
+
# Override warning/error thresholds per tool
|
|
171
|
+
[tool.cq.thresholds.coverage]
|
|
172
|
+
warning = 0.9
|
|
173
|
+
error = 0.7
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Tool IDs match the keys in `config/tools.yaml`: `compilation`, `bandit`, `ruff`, `ty`, `pytest`, `coverage`, `complexity`, `maintainability`, `halstead`, `vulture`, `interrogate`.
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
### Default config
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
tools:
|
|
183
|
+
|
|
184
|
+
compilation:
|
|
185
|
+
name: "compile"
|
|
186
|
+
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
187
|
+
parser: "CompileParser"
|
|
188
|
+
order: 1
|
|
189
|
+
warning_threshold: 0.9999
|
|
190
|
+
error_threshold: 0.9999
|
|
191
|
+
|
|
192
|
+
bandit:
|
|
193
|
+
name: "bandit"
|
|
194
|
+
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
195
|
+
parser: "BanditParser"
|
|
196
|
+
order: 2
|
|
197
|
+
warning_threshold: 0.9999
|
|
198
|
+
error_threshold: 0.8
|
|
199
|
+
|
|
200
|
+
ruff:
|
|
201
|
+
name: "ruff"
|
|
202
|
+
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
203
|
+
parser: "RuffParser"
|
|
204
|
+
order: 3
|
|
205
|
+
warning_threshold: 0.9999
|
|
206
|
+
error_threshold: 0.9
|
|
207
|
+
|
|
208
|
+
ty:
|
|
209
|
+
name: "ty"
|
|
210
|
+
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
211
|
+
parser: "TyParser"
|
|
212
|
+
order: 4
|
|
213
|
+
warning_threshold: 0.9999
|
|
214
|
+
error_threshold: 0.8
|
|
215
|
+
run_in_target_env: true
|
|
216
|
+
extra_deps:
|
|
217
|
+
- ty
|
|
218
|
+
|
|
219
|
+
pytest:
|
|
220
|
+
name: "pytest"
|
|
221
|
+
command: "{python} -m pytest -v {context_path}"
|
|
222
|
+
parser: "PytestParser"
|
|
223
|
+
order: 5
|
|
224
|
+
warning_threshold: 0.7
|
|
225
|
+
error_threshold: 0.5
|
|
226
|
+
run_in_target_env: true
|
|
227
|
+
|
|
228
|
+
coverage:
|
|
229
|
+
name: "coverage"
|
|
230
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
231
|
+
parser: "CoverageParser"
|
|
232
|
+
order: 6
|
|
233
|
+
warning_threshold: 0.9
|
|
234
|
+
error_threshold: 0.5
|
|
235
|
+
run_in_target_env: true
|
|
236
|
+
extra_deps:
|
|
237
|
+
- coverage
|
|
238
|
+
- pytest
|
|
239
|
+
|
|
240
|
+
complexity:
|
|
241
|
+
name: "radon cc"
|
|
242
|
+
command: "{python} -m radon cc --json {context_path}"
|
|
243
|
+
parser: "ComplexityParser"
|
|
244
|
+
order: 7
|
|
245
|
+
warning_threshold: 0.6
|
|
246
|
+
error_threshold: 0.4
|
|
247
|
+
|
|
248
|
+
maintainability:
|
|
249
|
+
name: "radon mi"
|
|
250
|
+
command: "{python} -m radon mi -s --json {context_path}"
|
|
251
|
+
parser: "MaintainabilityParser"
|
|
252
|
+
order: 8
|
|
253
|
+
warning_threshold: 0.6
|
|
254
|
+
error_threshold: 0.4
|
|
255
|
+
|
|
256
|
+
halstead:
|
|
257
|
+
name: "radon hal"
|
|
258
|
+
command: "{python} -m radon hal -f --json {context_path}"
|
|
259
|
+
parser: "HalsteadParser"
|
|
260
|
+
order: 9
|
|
261
|
+
warning_threshold: 0.5
|
|
262
|
+
error_threshold: 0.3
|
|
263
|
+
|
|
264
|
+
vulture:
|
|
265
|
+
name: "vulture"
|
|
266
|
+
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
267
|
+
parser: "VultureParser"
|
|
268
|
+
order: 10
|
|
269
|
+
warning_threshold: 0.9999
|
|
270
|
+
error_threshold: 0.8
|
|
271
|
+
|
|
272
|
+
interrogate:
|
|
273
|
+
name: "interrogate"
|
|
274
|
+
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
275
|
+
parser: "InterrogateParser"
|
|
276
|
+
order: 11
|
|
277
|
+
warning_threshold: 0.8
|
|
278
|
+
error_threshold: 0.3
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Respect
|
|
283
|
+
|
|
284
|
+
Many thanks to all the wonderful maintainers of :
|
|
285
|
+
|
|
286
|
+
- [compileall](https://docs.python.org/3/library/compileall.html)
|
|
287
|
+
- [bandit](https://github.com/PyCQA/bandit)
|
|
288
|
+
- [ruff](https://github.com/astral-sh/ruff)
|
|
289
|
+
- [ty](https://github.com/astral-sh/ty)
|
|
290
|
+
- [pytest](https://github.com/pytest-dev/pytest)
|
|
291
|
+
- [coverage.py](https://github.com/nedbat/coveragepy)
|
|
292
|
+
- [radon](https://github.com/rubik/radon)
|
|
293
|
+
- [vulture](https://github.com/jendrikseipp/vulture)
|
|
294
|
+
- [interrogate](https://github.com/econchick/interrogate)
|
|
295
|
+
- [diskcache](https://github.com/grantjenks/python-diskcache)
|
|
296
|
+
- [typer](https://github.com/fastapi/typer)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-code-quality"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "Python Code Quality Analysis Tool - feed the results from 11 CQ
|
|
3
|
+
version = "0.1.8"
|
|
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"
|
|
7
7
|
license = "MIT"
|