python-code-quality 0.1.11__tar.gz → 0.1.14__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.11 → python_code_quality-0.1.14}/PKG-INFO +54 -48
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/README.md +53 -47
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/pyproject.toml +8 -1
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/__init__.py +0 -4
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/cli.py +22 -3
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/config/config.yaml +12 -6
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/execution_engine.py +33 -7
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/localtypes.py +1 -0
- python_code_quality-0.1.14/src/py_cq/parsers/__init__.py +1 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/interrogateparser.py +4 -4
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/tool_registry.py +1 -0
- python_code_quality-0.1.11/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/config/__init__.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/language_detector.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/llm_formatter.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/main.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/banditparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/common.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/compileparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/complexityparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/coverageparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/exitcodeparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/halsteadparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/linecountparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/maintainabilityparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/pytestparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/regexcountparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/ruffparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/typarser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/vultureparser.py +0 -0
- {python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/py.typed +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.14
|
|
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>
|
|
@@ -31,43 +31,19 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
[](https://github.com/rhiza-fr/py-cq/actions/workflows/ci.yml)
|
|
33
33
|
[](https://codecov.io/gh/rhiza-fr/py-cq)
|
|
34
|
-
[](https://pypi.org/project/python-code-quality/)
|
|
35
|
-
[](https://pypi.org/project/python-code-quality/)
|
|
34
|
+
[](https://pypi.org/project/python-code-quality/)
|
|
35
|
+
[](https://pypi.org/project/python-code-quality/)
|
|
36
36
|
[](LICENSE)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Why? It removes the mental burden of understanding all these tools and parsing their results.
|
|
41
|
-
|
|
42
|
-
The primary workflow is:
|
|
38
|
+
Run 11+ code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
|
|
43
39
|
|
|
44
40
|
```bash
|
|
45
|
-
|
|
46
|
-
cq check .
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```python
|
|
50
|
-
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
51
|
-
|
|
52
|
-
18: min_dist = float("inf")
|
|
53
|
-
19: nearest_city = None
|
|
54
|
-
20: for city in cities:
|
|
55
|
-
21: unused_variable = 67
|
|
56
|
-
22: dist = calc_dist(current_city, city)
|
|
57
|
-
23: if dist < min_dist:
|
|
58
|
-
24: min_dist = dist
|
|
59
|
-
25: nearest_city = city
|
|
60
|
-
|
|
61
|
-
Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
|
|
62
|
-
```
|
|
63
|
-
Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
64
|
-
|
|
65
|
-
```python
|
|
66
|
-
cq check . -o llm | claude -p "fix this"
|
|
67
|
-
# or
|
|
68
|
-
cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
|
|
41
|
+
cq check . -o llm # top defect as markdown, pipe to an LLM
|
|
42
|
+
cq check . # table overview of all scores
|
|
43
|
+
cq check . -o score # numeric score only, exits 1 on errors (CI gate)
|
|
69
44
|
```
|
|
70
45
|
|
|
46
|
+

|
|
71
47
|
|
|
72
48
|
## Install
|
|
73
49
|
|
|
@@ -112,6 +88,9 @@ cq check . -o score # Numeric score only for CI
|
|
|
112
88
|
cq check . -o json # Detailed parsed JSON output for jq
|
|
113
89
|
cq check . -o raw # Raw tool output for debug
|
|
114
90
|
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
91
|
+
cq check . --only ruff,ty # Run only specific tools
|
|
92
|
+
cq check . --skip bandit # Skip specific tools
|
|
93
|
+
cq check . --exclude demo # Exclude paths from all tools
|
|
115
94
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
116
95
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
117
96
|
cq config path/to/project/ # Show effective tool configuration
|
|
@@ -131,10 +110,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
|
|
|
131
110
|
```json
|
|
132
111
|
{
|
|
133
112
|
"hooks": {
|
|
134
|
-
"Stop": [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
113
|
+
"Stop": [
|
|
114
|
+
{
|
|
115
|
+
"matcher": "",
|
|
116
|
+
"hooks": [
|
|
117
|
+
{
|
|
118
|
+
"type": "command",
|
|
119
|
+
"command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm; true"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
]
|
|
138
124
|
}
|
|
139
125
|
}
|
|
140
126
|
```
|
|
@@ -231,6 +217,16 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
231
217
|
]
|
|
232
218
|
```
|
|
233
219
|
|
|
220
|
+
Both `json` and `raw` output pipe cleanly to `jq`:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Get the coverage section
|
|
224
|
+
cq check . -o raw | jq '.[] | select(.tool_name == "coverage")'
|
|
225
|
+
|
|
226
|
+
# Get parsed coverage metrics only
|
|
227
|
+
cq check . -o json | jq '.[] | select(.tool_name == "coverage") | .metrics'
|
|
228
|
+
```
|
|
229
|
+
|
|
234
230
|
## Configuration
|
|
235
231
|
|
|
236
232
|
Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
@@ -240,6 +236,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
240
236
|
# Skip tools that are slow or not relevant to your project
|
|
241
237
|
disable = ["coverage", "interrogate"]
|
|
242
238
|
|
|
239
|
+
# Exclude paths from all tools (merged with --exclude CLI flag)
|
|
240
|
+
exclude = ["demo", "docs"]
|
|
241
|
+
|
|
243
242
|
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
244
243
|
context_lines = 15
|
|
245
244
|
|
|
@@ -258,21 +257,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
|
|
|
258
257
|
python:
|
|
259
258
|
|
|
260
259
|
compile:
|
|
261
|
-
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
260
|
+
command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
|
|
262
261
|
parser: "CompileParser"
|
|
263
262
|
order: 1
|
|
264
263
|
warning_threshold: 0.9999
|
|
265
264
|
error_threshold: 0.9999
|
|
266
265
|
|
|
267
266
|
ruff:
|
|
268
|
-
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
267
|
+
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
|
|
268
|
+
exclude_format: " --exclude {path}"
|
|
269
269
|
parser: "RuffParser"
|
|
270
270
|
order: 2
|
|
271
271
|
warning_threshold: 0.9999
|
|
272
272
|
error_threshold: 0.9
|
|
273
273
|
|
|
274
274
|
ty:
|
|
275
|
-
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
275
|
+
command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
|
|
276
|
+
exclude_format: " --exclude {path}"
|
|
276
277
|
parser: "TyParser"
|
|
277
278
|
order: 3
|
|
278
279
|
warning_threshold: 0.9999
|
|
@@ -282,22 +283,26 @@ python:
|
|
|
282
283
|
- ty
|
|
283
284
|
|
|
284
285
|
bandit:
|
|
285
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
286
|
+
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests{exclude}\""
|
|
287
|
+
exclude_format: ",{input_path_posix}/{path}"
|
|
286
288
|
parser: "BanditParser"
|
|
287
289
|
order: 4
|
|
288
290
|
warning_threshold: 0.9999
|
|
289
291
|
error_threshold: 0.8
|
|
290
292
|
|
|
291
293
|
pytest:
|
|
292
|
-
command: "{python} -m pytest -v {context_path}"
|
|
294
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
295
|
+
exclude_format: " --ignore {path}"
|
|
293
296
|
parser: "PytestParser"
|
|
294
297
|
order: 5
|
|
295
298
|
warning_threshold: 1.0
|
|
296
299
|
error_threshold: 1.0
|
|
297
300
|
run_in_target_env: true
|
|
301
|
+
extra_deps:
|
|
302
|
+
- pytest
|
|
298
303
|
|
|
299
304
|
coverage:
|
|
300
|
-
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
305
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
301
306
|
parser: "CoverageParser"
|
|
302
307
|
order: 6
|
|
303
308
|
warning_threshold: 0.9
|
|
@@ -308,40 +313,41 @@ python:
|
|
|
308
313
|
- pytest
|
|
309
314
|
|
|
310
315
|
radon-cc:
|
|
311
|
-
command: "{python} -m radon cc --json {context_path}"
|
|
316
|
+
command: "{python} -m radon cc --json \"{context_path}\""
|
|
312
317
|
parser: "ComplexityParser"
|
|
313
318
|
order: 7
|
|
314
319
|
warning_threshold: 0.6
|
|
315
320
|
error_threshold: 0.4
|
|
316
321
|
|
|
317
322
|
radon-mi:
|
|
318
|
-
command: "{python} -m radon mi -s --json {context_path}"
|
|
323
|
+
command: "{python} -m radon mi -s --json \"{context_path}\""
|
|
319
324
|
parser: "MaintainabilityParser"
|
|
320
325
|
order: 8
|
|
321
326
|
warning_threshold: 0.6
|
|
322
327
|
error_threshold: 0.4
|
|
323
328
|
|
|
324
329
|
radon-hal:
|
|
325
|
-
command: "{python} -m radon hal -f --json {context_path}"
|
|
330
|
+
command: "{python} -m radon hal -f --json \"{context_path}\""
|
|
326
331
|
parser: "HalsteadParser"
|
|
327
332
|
order: 9
|
|
328
333
|
warning_threshold: 0.5
|
|
329
334
|
error_threshold: 0.3
|
|
330
335
|
|
|
331
336
|
vulture:
|
|
332
|
-
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
337
|
+
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
|
|
338
|
+
exclude_format: ",{path}"
|
|
333
339
|
parser: "VultureParser"
|
|
334
340
|
order: 10
|
|
335
341
|
warning_threshold: 0.9999
|
|
336
342
|
error_threshold: 0.8
|
|
337
343
|
|
|
338
344
|
interrogate:
|
|
339
|
-
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
345
|
+
command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
|
|
346
|
+
exclude_format: " -e {path}"
|
|
340
347
|
parser: "InterrogateParser"
|
|
341
348
|
order: 11
|
|
342
349
|
warning_threshold: 0.8
|
|
343
350
|
error_threshold: 0.3
|
|
344
|
-
|
|
345
351
|
```
|
|
346
352
|
|
|
347
353
|
## Respect
|
|
@@ -2,43 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/rhiza-fr/py-cq/actions/workflows/ci.yml)
|
|
4
4
|
[](https://codecov.io/gh/rhiza-fr/py-cq)
|
|
5
|
-
[](https://pypi.org/project/python-code-quality/)
|
|
6
|
-
[](https://pypi.org/project/python-code-quality/)
|
|
5
|
+
[](https://pypi.org/project/python-code-quality/)
|
|
6
|
+
[](https://pypi.org/project/python-code-quality/)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Why? It removes the mental burden of understanding all these tools and parsing their results.
|
|
12
|
-
|
|
13
|
-
The primary workflow is:
|
|
9
|
+
Run 11+ code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
|
|
14
10
|
|
|
15
11
|
```bash
|
|
16
|
-
|
|
17
|
-
cq check .
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
```python
|
|
21
|
-
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
22
|
-
|
|
23
|
-
18: min_dist = float("inf")
|
|
24
|
-
19: nearest_city = None
|
|
25
|
-
20: for city in cities:
|
|
26
|
-
21: unused_variable = 67
|
|
27
|
-
22: dist = calc_dist(current_city, city)
|
|
28
|
-
23: if dist < min_dist:
|
|
29
|
-
24: min_dist = dist
|
|
30
|
-
25: nearest_city = city
|
|
31
|
-
|
|
32
|
-
Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
|
|
33
|
-
```
|
|
34
|
-
Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
35
|
-
|
|
36
|
-
```python
|
|
37
|
-
cq check . -o llm | claude -p "fix this"
|
|
38
|
-
# or
|
|
39
|
-
cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
|
|
12
|
+
cq check . -o llm # top defect as markdown, pipe to an LLM
|
|
13
|
+
cq check . # table overview of all scores
|
|
14
|
+
cq check . -o score # numeric score only, exits 1 on errors (CI gate)
|
|
40
15
|
```
|
|
41
16
|
|
|
17
|
+

|
|
42
18
|
|
|
43
19
|
## Install
|
|
44
20
|
|
|
@@ -83,6 +59,9 @@ cq check . -o score # Numeric score only for CI
|
|
|
83
59
|
cq check . -o json # Detailed parsed JSON output for jq
|
|
84
60
|
cq check . -o raw # Raw tool output for debug
|
|
85
61
|
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
62
|
+
cq check . --only ruff,ty # Run only specific tools
|
|
63
|
+
cq check . --skip bandit # Skip specific tools
|
|
64
|
+
cq check . --exclude demo # Exclude paths from all tools
|
|
86
65
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
87
66
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
88
67
|
cq config path/to/project/ # Show effective tool configuration
|
|
@@ -102,10 +81,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
|
|
|
102
81
|
```json
|
|
103
82
|
{
|
|
104
83
|
"hooks": {
|
|
105
|
-
"Stop": [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
84
|
+
"Stop": [
|
|
85
|
+
{
|
|
86
|
+
"matcher": "",
|
|
87
|
+
"hooks": [
|
|
88
|
+
{
|
|
89
|
+
"type": "command",
|
|
90
|
+
"command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm; true"
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
109
95
|
}
|
|
110
96
|
}
|
|
111
97
|
```
|
|
@@ -202,6 +188,16 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
202
188
|
]
|
|
203
189
|
```
|
|
204
190
|
|
|
191
|
+
Both `json` and `raw` output pipe cleanly to `jq`:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Get the coverage section
|
|
195
|
+
cq check . -o raw | jq '.[] | select(.tool_name == "coverage")'
|
|
196
|
+
|
|
197
|
+
# Get parsed coverage metrics only
|
|
198
|
+
cq check . -o json | jq '.[] | select(.tool_name == "coverage") | .metrics'
|
|
199
|
+
```
|
|
200
|
+
|
|
205
201
|
## Configuration
|
|
206
202
|
|
|
207
203
|
Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
@@ -211,6 +207,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
211
207
|
# Skip tools that are slow or not relevant to your project
|
|
212
208
|
disable = ["coverage", "interrogate"]
|
|
213
209
|
|
|
210
|
+
# Exclude paths from all tools (merged with --exclude CLI flag)
|
|
211
|
+
exclude = ["demo", "docs"]
|
|
212
|
+
|
|
214
213
|
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
215
214
|
context_lines = 15
|
|
216
215
|
|
|
@@ -229,21 +228,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
|
|
|
229
228
|
python:
|
|
230
229
|
|
|
231
230
|
compile:
|
|
232
|
-
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
231
|
+
command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
|
|
233
232
|
parser: "CompileParser"
|
|
234
233
|
order: 1
|
|
235
234
|
warning_threshold: 0.9999
|
|
236
235
|
error_threshold: 0.9999
|
|
237
236
|
|
|
238
237
|
ruff:
|
|
239
|
-
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
238
|
+
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
|
|
239
|
+
exclude_format: " --exclude {path}"
|
|
240
240
|
parser: "RuffParser"
|
|
241
241
|
order: 2
|
|
242
242
|
warning_threshold: 0.9999
|
|
243
243
|
error_threshold: 0.9
|
|
244
244
|
|
|
245
245
|
ty:
|
|
246
|
-
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
246
|
+
command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
|
|
247
|
+
exclude_format: " --exclude {path}"
|
|
247
248
|
parser: "TyParser"
|
|
248
249
|
order: 3
|
|
249
250
|
warning_threshold: 0.9999
|
|
@@ -253,22 +254,26 @@ python:
|
|
|
253
254
|
- ty
|
|
254
255
|
|
|
255
256
|
bandit:
|
|
256
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
257
|
+
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests{exclude}\""
|
|
258
|
+
exclude_format: ",{input_path_posix}/{path}"
|
|
257
259
|
parser: "BanditParser"
|
|
258
260
|
order: 4
|
|
259
261
|
warning_threshold: 0.9999
|
|
260
262
|
error_threshold: 0.8
|
|
261
263
|
|
|
262
264
|
pytest:
|
|
263
|
-
command: "{python} -m pytest -v {context_path}"
|
|
265
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
266
|
+
exclude_format: " --ignore {path}"
|
|
264
267
|
parser: "PytestParser"
|
|
265
268
|
order: 5
|
|
266
269
|
warning_threshold: 1.0
|
|
267
270
|
error_threshold: 1.0
|
|
268
271
|
run_in_target_env: true
|
|
272
|
+
extra_deps:
|
|
273
|
+
- pytest
|
|
269
274
|
|
|
270
275
|
coverage:
|
|
271
|
-
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
276
|
+
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
272
277
|
parser: "CoverageParser"
|
|
273
278
|
order: 6
|
|
274
279
|
warning_threshold: 0.9
|
|
@@ -279,40 +284,41 @@ python:
|
|
|
279
284
|
- pytest
|
|
280
285
|
|
|
281
286
|
radon-cc:
|
|
282
|
-
command: "{python} -m radon cc --json {context_path}"
|
|
287
|
+
command: "{python} -m radon cc --json \"{context_path}\""
|
|
283
288
|
parser: "ComplexityParser"
|
|
284
289
|
order: 7
|
|
285
290
|
warning_threshold: 0.6
|
|
286
291
|
error_threshold: 0.4
|
|
287
292
|
|
|
288
293
|
radon-mi:
|
|
289
|
-
command: "{python} -m radon mi -s --json {context_path}"
|
|
294
|
+
command: "{python} -m radon mi -s --json \"{context_path}\""
|
|
290
295
|
parser: "MaintainabilityParser"
|
|
291
296
|
order: 8
|
|
292
297
|
warning_threshold: 0.6
|
|
293
298
|
error_threshold: 0.4
|
|
294
299
|
|
|
295
300
|
radon-hal:
|
|
296
|
-
command: "{python} -m radon hal -f --json {context_path}"
|
|
301
|
+
command: "{python} -m radon hal -f --json \"{context_path}\""
|
|
297
302
|
parser: "HalsteadParser"
|
|
298
303
|
order: 9
|
|
299
304
|
warning_threshold: 0.5
|
|
300
305
|
error_threshold: 0.3
|
|
301
306
|
|
|
302
307
|
vulture:
|
|
303
|
-
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
308
|
+
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
|
|
309
|
+
exclude_format: ",{path}"
|
|
304
310
|
parser: "VultureParser"
|
|
305
311
|
order: 10
|
|
306
312
|
warning_threshold: 0.9999
|
|
307
313
|
error_threshold: 0.8
|
|
308
314
|
|
|
309
315
|
interrogate:
|
|
310
|
-
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
316
|
+
command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
|
|
317
|
+
exclude_format: " -e {path}"
|
|
311
318
|
parser: "InterrogateParser"
|
|
312
319
|
order: 11
|
|
313
320
|
warning_threshold: 0.8
|
|
314
321
|
error_threshold: 0.3
|
|
315
|
-
|
|
316
322
|
```
|
|
317
323
|
|
|
318
324
|
## Respect
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-code-quality"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.14"
|
|
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"
|
|
@@ -42,3 +42,10 @@ module-name = "py_cq"
|
|
|
42
42
|
|
|
43
43
|
[project.scripts]
|
|
44
44
|
cq = "py_cq.main:main"
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
norecursedirs = [".venv", "dist"]
|
|
49
|
+
|
|
50
|
+
[tool.cq]
|
|
51
|
+
exclude = ["demo"]
|
|
@@ -4,7 +4,3 @@ The module defines a single function, `hello`, which returns the string
|
|
|
4
4
|
`'Hello from py_cq!'`. It can serve as a minimal example, placeholder, or
|
|
5
5
|
testing stub in larger applications."""
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
def hello() -> str:
|
|
9
|
-
"""Returns the greeting string `'Hello from py_cq!'`."""
|
|
10
|
-
return "Hello from py_cq!"
|
|
@@ -84,6 +84,7 @@ def _apply_user_config(base: dict[str, ToolConfig], user_cfg: dict) -> dict[str,
|
|
|
84
84
|
run_in_target_env=tool_data.get("run_in_target_env", False),
|
|
85
85
|
extra_deps=tool_data.get("extra_deps", []),
|
|
86
86
|
parser_config=tool_data.get("parser_config", {}),
|
|
87
|
+
exclude_format=tool_data.get("exclude_format", ""),
|
|
87
88
|
)
|
|
88
89
|
except KeyError as e:
|
|
89
90
|
raise typer.BadParameter(f"[tool.cq.tools.{tool_id}] missing required field {e}")
|
|
@@ -125,6 +126,15 @@ def check(
|
|
|
125
126
|
language: str | None = typer.Option(
|
|
126
127
|
None, "--language", "-l", help="Override language detection (e.g. python, typescript, rust)"
|
|
127
128
|
),
|
|
129
|
+
only: str | None = typer.Option(
|
|
130
|
+
None, "--only", help="Comma-separated tool IDs to run (e.g. ruff,ty,pytest)"
|
|
131
|
+
),
|
|
132
|
+
skip: str | None = typer.Option(
|
|
133
|
+
None, "--skip", help="Comma-separated tool IDs to skip (e.g. bandit,vulture)"
|
|
134
|
+
),
|
|
135
|
+
exclude: str | None = typer.Option(
|
|
136
|
+
None, "--exclude", help="Comma-separated paths to exclude (e.g. demo,docs)"
|
|
137
|
+
),
|
|
128
138
|
):
|
|
129
139
|
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
130
140
|
path_obj = Path(path)
|
|
@@ -153,18 +163,27 @@ def check(
|
|
|
153
163
|
user_cfg = load_user_config(path_obj)
|
|
154
164
|
context_lines: int = int(user_cfg.get("context_lines", 15))
|
|
155
165
|
effective_registry = _apply_user_config(tool_registry, user_cfg)
|
|
166
|
+
if only:
|
|
167
|
+
keep = set(only.split(","))
|
|
168
|
+
effective_registry = {k: v for k, v in effective_registry.items() if k in keep}
|
|
169
|
+
if skip:
|
|
170
|
+
drop = set(skip.split(","))
|
|
171
|
+
effective_registry = {k: v for k, v in effective_registry.items() if k not in drop}
|
|
172
|
+
config_excludes: list[str] = user_cfg.get("exclude", [])
|
|
173
|
+
cli_excludes: list[str] = [e.strip() for e in exclude.split(",")] if exclude else []
|
|
174
|
+
excludes = list(dict.fromkeys(config_excludes + cli_excludes))
|
|
156
175
|
if clear_cache:
|
|
157
176
|
tool_cache.clear()
|
|
158
|
-
tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM))
|
|
177
|
+
tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM), excludes=excludes)
|
|
159
178
|
# for tr in tool_results:
|
|
160
179
|
# log.debug(json.dumps(tr.to_dict(), indent=2))
|
|
161
180
|
combined_metrics = aggregate_metrics(path=path, metrics=tool_results)
|
|
162
181
|
if output == OutputMode.SCORE:
|
|
163
182
|
console.print(combined_metrics.score)
|
|
164
183
|
elif output == OutputMode.JSON:
|
|
165
|
-
|
|
184
|
+
print(json.dumps([tr.to_dict() for tr in tool_results], indent=2))
|
|
166
185
|
elif output == OutputMode.RAW:
|
|
167
|
-
|
|
186
|
+
print(json.dumps([tr.raw.to_dict() for tr in tool_results], indent=2))
|
|
168
187
|
elif output == OutputMode.LLM:
|
|
169
188
|
# log.setLevel("CRITICAL")
|
|
170
189
|
from py_cq.llm_formatter import format_for_llm
|
|
@@ -8,14 +8,16 @@ python:
|
|
|
8
8
|
error_threshold: 0.9999
|
|
9
9
|
|
|
10
10
|
ruff:
|
|
11
|
-
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\""
|
|
11
|
+
command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
|
|
12
|
+
exclude_format: " --exclude {path}"
|
|
12
13
|
parser: "RuffParser"
|
|
13
14
|
order: 2
|
|
14
15
|
warning_threshold: 0.9999
|
|
15
16
|
error_threshold: 0.9
|
|
16
17
|
|
|
17
18
|
ty:
|
|
18
|
-
command: "{python} -m ty check --output-format concise --color never \"{context_path}\""
|
|
19
|
+
command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
|
|
20
|
+
exclude_format: " --exclude {path}"
|
|
19
21
|
parser: "TyParser"
|
|
20
22
|
order: 3
|
|
21
23
|
warning_threshold: 0.9999
|
|
@@ -25,14 +27,16 @@ python:
|
|
|
25
27
|
- ty
|
|
26
28
|
|
|
27
29
|
bandit:
|
|
28
|
-
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests\""
|
|
30
|
+
command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests{exclude}\""
|
|
31
|
+
exclude_format: ",{input_path_posix}/{path}"
|
|
29
32
|
parser: "BanditParser"
|
|
30
33
|
order: 4
|
|
31
34
|
warning_threshold: 0.9999
|
|
32
35
|
error_threshold: 0.8
|
|
33
36
|
|
|
34
37
|
pytest:
|
|
35
|
-
command: "{python} -m pytest -v \"{context_path}\""
|
|
38
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
39
|
+
exclude_format: " --ignore {path}"
|
|
36
40
|
parser: "PytestParser"
|
|
37
41
|
order: 5
|
|
38
42
|
warning_threshold: 1.0
|
|
@@ -74,14 +78,16 @@ python:
|
|
|
74
78
|
error_threshold: 0.3
|
|
75
79
|
|
|
76
80
|
vulture:
|
|
77
|
-
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
81
|
+
command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
|
|
82
|
+
exclude_format: ",{path}"
|
|
78
83
|
parser: "VultureParser"
|
|
79
84
|
order: 10
|
|
80
85
|
warning_threshold: 0.9999
|
|
81
86
|
error_threshold: 0.8
|
|
82
87
|
|
|
83
88
|
interrogate:
|
|
84
|
-
command: "{python} -m interrogate \"{context_path}\" -v --fail-under 0"
|
|
89
|
+
command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
|
|
90
|
+
exclude_format: " -e {path}"
|
|
85
91
|
parser: "InterrogateParser"
|
|
86
92
|
order: 11
|
|
87
93
|
warning_threshold: 0.8
|
|
@@ -41,7 +41,29 @@ def _find_project_root(path: Path) -> Path | None:
|
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def _dep_in_venv(dep: str, project_root: Path) -> bool:
|
|
45
|
+
"""Return True if `dep` is installed in the project's .venv."""
|
|
46
|
+
venv = project_root / ".venv"
|
|
47
|
+
if not venv.exists():
|
|
48
|
+
return False
|
|
49
|
+
for subdir in ("Scripts", "bin"):
|
|
50
|
+
for suffix in ("", ".exe", ".cmd"):
|
|
51
|
+
if (venv / subdir / f"{dep}{suffix}").exists():
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _build_exclude_str(exclude_format: str, excludes: list[str], **extra_vars: str) -> str:
|
|
57
|
+
if not exclude_format or not excludes:
|
|
58
|
+
return ""
|
|
59
|
+
parts = []
|
|
60
|
+
for exc in excludes:
|
|
61
|
+
abs_posix_path = Path(exc).resolve().as_posix()
|
|
62
|
+
parts.append(exclude_format.format(path=exc, abs_posix_path=abs_posix_path, **extra_vars))
|
|
63
|
+
return "".join(parts)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_tool(tool_config: ToolConfig, context_path: str, excludes: list[str] | None = None) -> RawResult:
|
|
45
67
|
"""Runs a tool defined by its configuration and returns the execution result.
|
|
46
68
|
|
|
47
69
|
Args:
|
|
@@ -60,7 +82,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
60
82
|
>>> result.return_code
|
|
61
83
|
0"""
|
|
62
84
|
python = sys.executable
|
|
63
|
-
path = context_path
|
|
85
|
+
path = str(Path(context_path))
|
|
64
86
|
if tool_config.run_in_target_env:
|
|
65
87
|
uv = shutil.which("uv")
|
|
66
88
|
if uv:
|
|
@@ -72,11 +94,15 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
72
94
|
project_root = _find_project_root(resolved)
|
|
73
95
|
abs_dir = str(project_root) if project_root else str(resolved.parent)
|
|
74
96
|
path = str(resolved)
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
project_root_path = Path(abs_dir)
|
|
98
|
+
missing_deps = [d for d in tool_config.extra_deps if not _dep_in_venv(d, project_root_path)]
|
|
99
|
+
with_flags = " ".join(f"--with {dep}" for dep in missing_deps)
|
|
100
|
+
no_sync = "--no-sync" if sys.executable.startswith(abs_dir) else ""
|
|
101
|
+
python = f'"{uv}" run {no_sync} --directory "{abs_dir}" {with_flags}'.strip()
|
|
77
102
|
abs_context_path = str(Path(context_path).resolve())
|
|
78
103
|
input_path_posix = Path(context_path).as_posix().rstrip("/")
|
|
79
|
-
|
|
104
|
+
exclude = _build_exclude_str(tool_config.exclude_format, excludes or [], input_path_posix=input_path_posix)
|
|
105
|
+
command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, input_path_posix=input_path_posix, python=python, exclude=exclude)
|
|
80
106
|
cache_key = f"{command}:{get_context_hash(context_path)}"
|
|
81
107
|
if cache_key in _cache:
|
|
82
108
|
log.info(f"Cache hit: {command}")
|
|
@@ -96,7 +122,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
96
122
|
return raw_result
|
|
97
123
|
|
|
98
124
|
|
|
99
|
-
def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False) -> list[ToolResult]:
|
|
125
|
+
def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False, excludes: list[str] | None = None) -> list[ToolResult]:
|
|
100
126
|
"""Run multiple tools and return their parsed results.
|
|
101
127
|
|
|
102
128
|
Runs each tool specified in *tool_configs* on the file or directory at
|
|
@@ -138,7 +164,7 @@ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int
|
|
|
138
164
|
>>> results = run_tools(configs, '/path/to/project', parallel=True)"""
|
|
139
165
|
def _run_and_parse(tool_config: ToolConfig) -> tuple[int, ToolResult]:
|
|
140
166
|
t0 = time.perf_counter()
|
|
141
|
-
raw_result = run_tool(tool_config, path)
|
|
167
|
+
raw_result = run_tool(tool_config, path, excludes)
|
|
142
168
|
tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
|
|
143
169
|
tr.duration_s = time.perf_counter() - t0
|
|
144
170
|
return tool_config.order, tr
|
|
@@ -22,6 +22,7 @@ class ToolConfig:
|
|
|
22
22
|
run_in_target_env: bool = False # If True, run in target project's env via uv
|
|
23
23
|
extra_deps: list[str] = field(default_factory=list) # Extra deps to inject via uv --with
|
|
24
24
|
parser_config: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
exclude_format: str = "" # Per-path template for --exclude injection, e.g. " --exclude {path}"
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
@dataclass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tool Response parsers"""
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/interrogateparser.py
RENAMED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
Interrogate is invoked with ``-v --fail-under 0``, producing a table of
|
|
4
4
|
per-file docstring coverage on stdout::
|
|
5
5
|
|
|
6
|
-
| src/foo.py | 5 | 2 | 60% |
|
|
7
|
-
| TOTAL | 5 | 2 | 60% |
|
|
6
|
+
| src/foo.py | 5 | 2 | 3 | 60% |
|
|
7
|
+
| TOTAL | 5 | 2 | 3 | 60.0% |
|
|
8
8
|
|
|
9
9
|
The parser extracts per-file coverage and the TOTAL row, storing the TOTAL
|
|
10
10
|
as the ``doc_coverage`` metric (0.0–1.0).
|
|
@@ -14,7 +14,7 @@ import re
|
|
|
14
14
|
|
|
15
15
|
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
16
16
|
|
|
17
|
-
_ROW_RE = re.compile(r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)%\s
|
|
17
|
+
_ROW_RE = re.compile(r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+\d+\s+\|\s+(\d+(?:\.\d+)?)%\s*\|")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class InterrogateParser(AbstractParser):
|
|
@@ -30,7 +30,7 @@ class InterrogateParser(AbstractParser):
|
|
|
30
30
|
name = m.group(1).strip()
|
|
31
31
|
total = int(m.group(2))
|
|
32
32
|
miss = int(m.group(3))
|
|
33
|
-
cover =
|
|
33
|
+
cover = float(m.group(4))
|
|
34
34
|
if name == "TOTAL":
|
|
35
35
|
total_coverage = cover / 100.0
|
|
36
36
|
elif total > 0:
|
|
@@ -30,6 +30,7 @@ def load_tool_configs() -> dict[str, ToolConfig]:
|
|
|
30
30
|
run_in_target_env=tool_data.get("run_in_target_env", False),
|
|
31
31
|
extra_deps=tool_data.get("extra_deps", []),
|
|
32
32
|
parser_config=tool_data.get("parser_config", {}),
|
|
33
|
+
exclude_format=tool_data.get("exclude_format", ""),
|
|
33
34
|
)
|
|
34
35
|
return registry
|
|
35
36
|
|
|
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.11 → python_code_quality-0.1.14}/src/py_cq/parsers/compileparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/complexityparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/coverageparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/exitcodeparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/halsteadparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/linecountparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/maintainabilityparser.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/regexcountparser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.11 → python_code_quality-0.1.14}/src/py_cq/parsers/vultureparser.py
RENAMED
|
File without changes
|
|
File without changes
|