python-code-quality 0.1.13__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.13 → python_code_quality-0.1.14}/PKG-INFO +40 -46
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/README.md +39 -45
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/pyproject.toml +8 -1
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/__init__.py +0 -4
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/cli.py +8 -1
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/config/config.yaml +12 -6
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/execution_engine.py +18 -6
- {python_code_quality-0.1.13 → 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.13 → python_code_quality-0.1.14}/src/py_cq/parsers/interrogateparser.py +4 -4
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/tool_registry.py +1 -0
- python_code_quality-0.1.13/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/config/__init__.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/language_detector.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/llm_formatter.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/main.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/banditparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/common.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/compileparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/complexityparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/coverageparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/exitcodeparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/halsteadparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/linecountparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/maintainabilityparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/pytestparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/regexcountparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/ruffparser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/typarser.py +0 -0
- {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/vultureparser.py +0 -0
- {python_code_quality-0.1.13 → 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>
|
|
@@ -35,39 +35,15 @@ Description-Content-Type: text/markdown
|
|
|
35
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
|
|
|
@@ -114,6 +90,7 @@ cq check . -o raw # Raw tool output for debug
|
|
|
114
90
|
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
115
91
|
cq check . --only ruff,ty # Run only specific tools
|
|
116
92
|
cq check . --skip bandit # Skip specific tools
|
|
93
|
+
cq check . --exclude demo # Exclude paths from all tools
|
|
117
94
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
118
95
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
119
96
|
cq config path/to/project/ # Show effective tool configuration
|
|
@@ -133,10 +110,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
|
|
|
133
110
|
```json
|
|
134
111
|
{
|
|
135
112
|
"hooks": {
|
|
136
|
-
"Stop": [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
]
|
|
140
124
|
}
|
|
141
125
|
}
|
|
142
126
|
```
|
|
@@ -252,6 +236,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
252
236
|
# Skip tools that are slow or not relevant to your project
|
|
253
237
|
disable = ["coverage", "interrogate"]
|
|
254
238
|
|
|
239
|
+
# Exclude paths from all tools (merged with --exclude CLI flag)
|
|
240
|
+
exclude = ["demo", "docs"]
|
|
241
|
+
|
|
255
242
|
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
256
243
|
context_lines = 15
|
|
257
244
|
|
|
@@ -270,21 +257,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
|
|
|
270
257
|
python:
|
|
271
258
|
|
|
272
259
|
compile:
|
|
273
|
-
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"
|
|
274
261
|
parser: "CompileParser"
|
|
275
262
|
order: 1
|
|
276
263
|
warning_threshold: 0.9999
|
|
277
264
|
error_threshold: 0.9999
|
|
278
265
|
|
|
279
266
|
ruff:
|
|
280
|
-
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}"
|
|
281
269
|
parser: "RuffParser"
|
|
282
270
|
order: 2
|
|
283
271
|
warning_threshold: 0.9999
|
|
284
272
|
error_threshold: 0.9
|
|
285
273
|
|
|
286
274
|
ty:
|
|
287
|
-
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}"
|
|
288
277
|
parser: "TyParser"
|
|
289
278
|
order: 3
|
|
290
279
|
warning_threshold: 0.9999
|
|
@@ -294,22 +283,26 @@ python:
|
|
|
294
283
|
- ty
|
|
295
284
|
|
|
296
285
|
bandit:
|
|
297
|
-
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}"
|
|
298
288
|
parser: "BanditParser"
|
|
299
289
|
order: 4
|
|
300
290
|
warning_threshold: 0.9999
|
|
301
291
|
error_threshold: 0.8
|
|
302
292
|
|
|
303
293
|
pytest:
|
|
304
|
-
command: "{python} -m pytest -v {context_path}"
|
|
294
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
295
|
+
exclude_format: " --ignore {path}"
|
|
305
296
|
parser: "PytestParser"
|
|
306
297
|
order: 5
|
|
307
298
|
warning_threshold: 1.0
|
|
308
299
|
error_threshold: 1.0
|
|
309
300
|
run_in_target_env: true
|
|
301
|
+
extra_deps:
|
|
302
|
+
- pytest
|
|
310
303
|
|
|
311
304
|
coverage:
|
|
312
|
-
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"
|
|
313
306
|
parser: "CoverageParser"
|
|
314
307
|
order: 6
|
|
315
308
|
warning_threshold: 0.9
|
|
@@ -320,40 +313,41 @@ python:
|
|
|
320
313
|
- pytest
|
|
321
314
|
|
|
322
315
|
radon-cc:
|
|
323
|
-
command: "{python} -m radon cc --json {context_path}"
|
|
316
|
+
command: "{python} -m radon cc --json \"{context_path}\""
|
|
324
317
|
parser: "ComplexityParser"
|
|
325
318
|
order: 7
|
|
326
319
|
warning_threshold: 0.6
|
|
327
320
|
error_threshold: 0.4
|
|
328
321
|
|
|
329
322
|
radon-mi:
|
|
330
|
-
command: "{python} -m radon mi -s --json {context_path}"
|
|
323
|
+
command: "{python} -m radon mi -s --json \"{context_path}\""
|
|
331
324
|
parser: "MaintainabilityParser"
|
|
332
325
|
order: 8
|
|
333
326
|
warning_threshold: 0.6
|
|
334
327
|
error_threshold: 0.4
|
|
335
328
|
|
|
336
329
|
radon-hal:
|
|
337
|
-
command: "{python} -m radon hal -f --json {context_path}"
|
|
330
|
+
command: "{python} -m radon hal -f --json \"{context_path}\""
|
|
338
331
|
parser: "HalsteadParser"
|
|
339
332
|
order: 9
|
|
340
333
|
warning_threshold: 0.5
|
|
341
334
|
error_threshold: 0.3
|
|
342
335
|
|
|
343
336
|
vulture:
|
|
344
|
-
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}"
|
|
345
339
|
parser: "VultureParser"
|
|
346
340
|
order: 10
|
|
347
341
|
warning_threshold: 0.9999
|
|
348
342
|
error_threshold: 0.8
|
|
349
343
|
|
|
350
344
|
interrogate:
|
|
351
|
-
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}"
|
|
352
347
|
parser: "InterrogateParser"
|
|
353
348
|
order: 11
|
|
354
349
|
warning_threshold: 0.8
|
|
355
350
|
error_threshold: 0.3
|
|
356
|
-
|
|
357
351
|
```
|
|
358
352
|
|
|
359
353
|
## Respect
|
|
@@ -6,39 +6,15 @@
|
|
|
6
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
|
|
|
@@ -85,6 +61,7 @@ cq check . -o raw # Raw tool output for debug
|
|
|
85
61
|
cq check path/to/file.py # Just one file (skips pytest and coverage)
|
|
86
62
|
cq check . --only ruff,ty # Run only specific tools
|
|
87
63
|
cq check . --skip bandit # Skip specific tools
|
|
64
|
+
cq check . --exclude demo # Exclude paths from all tools
|
|
88
65
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
89
66
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
90
67
|
cq config path/to/project/ # Show effective tool configuration
|
|
@@ -104,10 +81,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
|
|
|
104
81
|
```json
|
|
105
82
|
{
|
|
106
83
|
"hooks": {
|
|
107
|
-
"Stop": [
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
]
|
|
111
95
|
}
|
|
112
96
|
}
|
|
113
97
|
```
|
|
@@ -223,6 +207,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
223
207
|
# Skip tools that are slow or not relevant to your project
|
|
224
208
|
disable = ["coverage", "interrogate"]
|
|
225
209
|
|
|
210
|
+
# Exclude paths from all tools (merged with --exclude CLI flag)
|
|
211
|
+
exclude = ["demo", "docs"]
|
|
212
|
+
|
|
226
213
|
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
227
214
|
context_lines = 15
|
|
228
215
|
|
|
@@ -241,21 +228,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
|
|
|
241
228
|
python:
|
|
242
229
|
|
|
243
230
|
compile:
|
|
244
|
-
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"
|
|
245
232
|
parser: "CompileParser"
|
|
246
233
|
order: 1
|
|
247
234
|
warning_threshold: 0.9999
|
|
248
235
|
error_threshold: 0.9999
|
|
249
236
|
|
|
250
237
|
ruff:
|
|
251
|
-
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}"
|
|
252
240
|
parser: "RuffParser"
|
|
253
241
|
order: 2
|
|
254
242
|
warning_threshold: 0.9999
|
|
255
243
|
error_threshold: 0.9
|
|
256
244
|
|
|
257
245
|
ty:
|
|
258
|
-
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}"
|
|
259
248
|
parser: "TyParser"
|
|
260
249
|
order: 3
|
|
261
250
|
warning_threshold: 0.9999
|
|
@@ -265,22 +254,26 @@ python:
|
|
|
265
254
|
- ty
|
|
266
255
|
|
|
267
256
|
bandit:
|
|
268
|
-
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}"
|
|
269
259
|
parser: "BanditParser"
|
|
270
260
|
order: 4
|
|
271
261
|
warning_threshold: 0.9999
|
|
272
262
|
error_threshold: 0.8
|
|
273
263
|
|
|
274
264
|
pytest:
|
|
275
|
-
command: "{python} -m pytest -v {context_path}"
|
|
265
|
+
command: "{python} -m pytest -v \"{context_path}\"{exclude}"
|
|
266
|
+
exclude_format: " --ignore {path}"
|
|
276
267
|
parser: "PytestParser"
|
|
277
268
|
order: 5
|
|
278
269
|
warning_threshold: 1.0
|
|
279
270
|
error_threshold: 1.0
|
|
280
271
|
run_in_target_env: true
|
|
272
|
+
extra_deps:
|
|
273
|
+
- pytest
|
|
281
274
|
|
|
282
275
|
coverage:
|
|
283
|
-
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"
|
|
284
277
|
parser: "CoverageParser"
|
|
285
278
|
order: 6
|
|
286
279
|
warning_threshold: 0.9
|
|
@@ -291,40 +284,41 @@ python:
|
|
|
291
284
|
- pytest
|
|
292
285
|
|
|
293
286
|
radon-cc:
|
|
294
|
-
command: "{python} -m radon cc --json {context_path}"
|
|
287
|
+
command: "{python} -m radon cc --json \"{context_path}\""
|
|
295
288
|
parser: "ComplexityParser"
|
|
296
289
|
order: 7
|
|
297
290
|
warning_threshold: 0.6
|
|
298
291
|
error_threshold: 0.4
|
|
299
292
|
|
|
300
293
|
radon-mi:
|
|
301
|
-
command: "{python} -m radon mi -s --json {context_path}"
|
|
294
|
+
command: "{python} -m radon mi -s --json \"{context_path}\""
|
|
302
295
|
parser: "MaintainabilityParser"
|
|
303
296
|
order: 8
|
|
304
297
|
warning_threshold: 0.6
|
|
305
298
|
error_threshold: 0.4
|
|
306
299
|
|
|
307
300
|
radon-hal:
|
|
308
|
-
command: "{python} -m radon hal -f --json {context_path}"
|
|
301
|
+
command: "{python} -m radon hal -f --json \"{context_path}\""
|
|
309
302
|
parser: "HalsteadParser"
|
|
310
303
|
order: 9
|
|
311
304
|
warning_threshold: 0.5
|
|
312
305
|
error_threshold: 0.3
|
|
313
306
|
|
|
314
307
|
vulture:
|
|
315
|
-
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}"
|
|
316
310
|
parser: "VultureParser"
|
|
317
311
|
order: 10
|
|
318
312
|
warning_threshold: 0.9999
|
|
319
313
|
error_threshold: 0.8
|
|
320
314
|
|
|
321
315
|
interrogate:
|
|
322
|
-
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}"
|
|
323
318
|
parser: "InterrogateParser"
|
|
324
319
|
order: 11
|
|
325
320
|
warning_threshold: 0.8
|
|
326
321
|
error_threshold: 0.3
|
|
327
|
-
|
|
328
322
|
```
|
|
329
323
|
|
|
330
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}")
|
|
@@ -131,6 +132,9 @@ def check(
|
|
|
131
132
|
skip: str | None = typer.Option(
|
|
132
133
|
None, "--skip", help="Comma-separated tool IDs to skip (e.g. bandit,vulture)"
|
|
133
134
|
),
|
|
135
|
+
exclude: str | None = typer.Option(
|
|
136
|
+
None, "--exclude", help="Comma-separated paths to exclude (e.g. demo,docs)"
|
|
137
|
+
),
|
|
134
138
|
):
|
|
135
139
|
"""Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
136
140
|
path_obj = Path(path)
|
|
@@ -165,9 +169,12 @@ def check(
|
|
|
165
169
|
if skip:
|
|
166
170
|
drop = set(skip.split(","))
|
|
167
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))
|
|
168
175
|
if clear_cache:
|
|
169
176
|
tool_cache.clear()
|
|
170
|
-
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)
|
|
171
178
|
# for tr in tool_results:
|
|
172
179
|
# log.debug(json.dumps(tr.to_dict(), indent=2))
|
|
173
180
|
combined_metrics = aggregate_metrics(path=path, metrics=tool_results)
|
|
@@ -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
|
|
@@ -53,7 +53,17 @@ def _dep_in_venv(dep: str, project_root: Path) -> bool:
|
|
|
53
53
|
return False
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def
|
|
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:
|
|
57
67
|
"""Runs a tool defined by its configuration and returns the execution result.
|
|
58
68
|
|
|
59
69
|
Args:
|
|
@@ -72,7 +82,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
72
82
|
>>> result.return_code
|
|
73
83
|
0"""
|
|
74
84
|
python = sys.executable
|
|
75
|
-
path = context_path
|
|
85
|
+
path = str(Path(context_path))
|
|
76
86
|
if tool_config.run_in_target_env:
|
|
77
87
|
uv = shutil.which("uv")
|
|
78
88
|
if uv:
|
|
@@ -87,10 +97,12 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
87
97
|
project_root_path = Path(abs_dir)
|
|
88
98
|
missing_deps = [d for d in tool_config.extra_deps if not _dep_in_venv(d, project_root_path)]
|
|
89
99
|
with_flags = " ".join(f"--with {dep}" for dep in missing_deps)
|
|
90
|
-
|
|
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()
|
|
91
102
|
abs_context_path = str(Path(context_path).resolve())
|
|
92
103
|
input_path_posix = Path(context_path).as_posix().rstrip("/")
|
|
93
|
-
|
|
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)
|
|
94
106
|
cache_key = f"{command}:{get_context_hash(context_path)}"
|
|
95
107
|
if cache_key in _cache:
|
|
96
108
|
log.info(f"Cache hit: {command}")
|
|
@@ -110,7 +122,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
|
|
|
110
122
|
return raw_result
|
|
111
123
|
|
|
112
124
|
|
|
113
|
-
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]:
|
|
114
126
|
"""Run multiple tools and return their parsed results.
|
|
115
127
|
|
|
116
128
|
Runs each tool specified in *tool_configs* on the file or directory at
|
|
@@ -152,7 +164,7 @@ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int
|
|
|
152
164
|
>>> results = run_tools(configs, '/path/to/project', parallel=True)"""
|
|
153
165
|
def _run_and_parse(tool_config: ToolConfig) -> tuple[int, ToolResult]:
|
|
154
166
|
t0 = time.perf_counter()
|
|
155
|
-
raw_result = run_tool(tool_config, path)
|
|
167
|
+
raw_result = run_tool(tool_config, path, excludes)
|
|
156
168
|
tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
|
|
157
169
|
tr.duration_s = time.perf_counter() - t0
|
|
158
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.13 → 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.13 → python_code_quality-0.1.14}/src/py_cq/parsers/compileparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/complexityparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/coverageparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/exitcodeparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/halsteadparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/linecountparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/maintainabilityparser.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.13 → 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.13 → python_code_quality-0.1.14}/src/py_cq/parsers/vultureparser.py
RENAMED
|
File without changes
|
|
File without changes
|