python-code-quality 0.1.6__tar.gz → 0.1.7__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.
Files changed (55) hide show
  1. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/PKG-INFO +177 -41
  2. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/README.md +175 -39
  3. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/pyproject.toml +2 -2
  4. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/cli.py +4 -10
  5. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/config/__init__.py +1 -3
  6. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/config/tools.yaml +1 -1
  7. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/execution_engine.py +7 -7
  8. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/banditparser.py +2 -2
  9. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/common.py +12 -0
  10. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/compileparser.py +2 -8
  11. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/ruffparser.py +2 -6
  12. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/typarser.py +2 -6
  13. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/vultureparser.py +2 -2
  14. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_execution_engine.py +7 -3
  15. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/uv.lock +1 -1
  16. python_code_quality-0.1.6/src/py_cq/storage.py +0 -27
  17. python_code_quality-0.1.6/tests/test_storage.py +0 -41
  18. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/.github/workflows/python-publish.yml +0 -0
  19. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/.gitignore +0 -0
  20. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/.python-version +0 -0
  21. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/CLAUDE.md +0 -0
  22. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/LICENSE +0 -0
  23. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/data/problems/travelling_salesman/ts_bad.py +0 -0
  24. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/data/problems/travelling_salesman/ts_good.py +0 -0
  25. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/__init__.py +0 -0
  26. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/context_hash.py +0 -0
  27. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/llm_formatter.py +0 -0
  28. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/localtypes.py +0 -0
  29. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/main.py +0 -0
  30. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/metric_aggregator.py +0 -0
  31. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/__init__.py +0 -0
  32. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/complexityparser.py +0 -0
  33. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/coverageparser.py +0 -0
  34. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/halsteadparser.py +0 -0
  35. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/interrogateparser.py +0 -0
  36. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/maintainabilityparser.py +0 -0
  37. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/pytestparser.py +0 -0
  38. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/py.typed +0 -0
  39. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/tool_registry.py +0 -0
  40. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/conftest.py +0 -0
  41. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_common.py +0 -0
  42. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_config.py +0 -0
  43. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_context_hash.py +0 -0
  44. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_llm_formatter.py +0 -0
  45. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_localtypes.py +0 -0
  46. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_bandit.py +0 -0
  47. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_compile.py +0 -0
  48. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_complexity.py +0 -0
  49. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_coverage.py +0 -0
  50. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_interrogate.py +0 -0
  51. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_maintainability.py +0 -0
  52. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_pytest.py +0 -0
  53. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_ruff.py +0 -0
  54. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_ty.py +0 -0
  55. {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_vulture.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-code-quality
3
- Version: 0.1.6
4
- Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ CQ straight into an LLM.
3
+ Version: 0.1.7
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,42 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # CQ - Python Code Quality Analysis Tool
28
28
 
29
- Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. The primary workflow is:
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
- cq check -o llm # get the single most critical defect as markdown
34
+ # get the single most critical defect as markdown
35
+ cq check . -o llm
33
36
  ```
37
+ Outputs the top error from the first priority tool where the score < warning_threshold,. The code context is expanded if available.
38
+ ```md
39
+ `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
34
40
 
35
- Feed that output to an LLM, apply the fix, repeat until the score is clean.
41
+ 18: min_dist = float("inf")
42
+ 19: nearest_city = None
43
+ 20: for city in cities:
44
+ 21: unused_variable = 67
45
+ 22: dist = calc_dist(current_city, city)
46
+ 23: if dist < min_dist:
47
+ 24: min_dist = dist
48
+ 25: nearest_city = city
49
+
50
+ Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
51
+ ```
52
+ Feed to an LLM with edit tools and repeat until there are no issues, e.g.
53
+
54
+ ```
55
+ cq check . -o llm | claude -p "fix this"
56
+ ```
36
57
 
37
58
  ## Install
38
59
 
39
60
  ```bash
61
+ # install the `cq` command line tool from PyPi
40
62
  uv tool install python-code-quality
41
63
 
42
- # or
64
+ # or, clone it then install
43
65
  git pull https://github.com/rhiza-fr/py-cq.git
44
66
  cd py-cq
45
67
  uv tool install .
@@ -47,7 +69,7 @@ uv tool install .
47
69
 
48
70
  ## Tools
49
71
 
50
- CQ runs these tools in *parallel*:
72
+ These tools are run in **parallel**:
51
73
 
52
74
  | Priority | Tool | Measures |
53
75
  |----------|------|----------|
@@ -63,40 +85,40 @@ CQ runs these tools in *parallel*:
63
85
  | 10 | vulture | Dead code |
64
86
  | 11 | interrogate | Docstring coverage |
65
87
 
88
+ Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
89
+
90
+
66
91
  ## Usage
67
92
 
68
93
  ```bash
69
94
  # LLM workflow: get the top defect as markdown (primary use case)
70
95
  cq check -o llm
71
96
 
72
- # Rich table with all metrics (default, also saves .cq.json)
73
- cq check
97
+ # Rich table with all metrics
98
+ cq check .
74
99
 
75
100
  # Numeric score only — useful in CI or scripts
76
- cq check -o score
101
+ cq check . -o score
77
102
 
78
- # Full JSON output
79
- cq check -o json
103
+ # Full JSON output, including raw test results
104
+ cq check . -o json
80
105
 
81
- # Explicit path (defaults to current directory)
106
+ # Explicit path
82
107
  cq check path/to/project/
83
108
  cq check path/to/file.py
84
109
 
85
- # Run sequentially (1 worker) instead of in parallel
86
- cq check --workers 1
110
+ # Run sequentially if you like things slow
111
+ cq check . --workers 1
87
112
 
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
113
+ # Clear cached results before running (rarely needed)
114
+ cq check . --clear-cache
93
115
 
94
116
  # Show effective tool configuration (thresholds, enabled/disabled status)
95
117
  cq config
96
118
  cq config path/to/project/
97
119
  ```
98
120
 
99
- ## Output
121
+ ## Table output
100
122
 
101
123
  ```bash
102
124
  > cq check .
@@ -123,6 +145,8 @@ cq config path/to/project/
123
145
  │ │ │ Score │ 0.965 │ │
124
146
  └──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
125
147
  ```
148
+
149
+ ## Single score output
126
150
  ```bash
127
151
  > cq check . -o score
128
152
  ```
@@ -130,23 +154,36 @@ cq config path/to/project/
130
154
  0.9662730667181059 # this is designed to approach but not reach 1.0
131
155
  ```
132
156
 
157
+ ## Json output
133
158
  ```bash
134
- > cq check . -o llm
159
+ > cq check . -o json
135
160
  ```
136
161
 
137
- ```md
138
- `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
139
-
140
- 18: min_dist = float("inf")
141
- 19: nearest_city = None
142
- 20: for city in cities:
143
- 21: unused_variable = 67
144
- 22: dist = calc_dist(current_city, city)
145
- 23: if dist < min_dist:
146
- 24: min_dist = dist
147
- 25: nearest_city = city
148
-
149
- Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
162
+ ```json
163
+ {
164
+ "metrics": [
165
+ {
166
+ "metrics": {
167
+ "compile": 1.0
168
+ },
169
+ "details": {},
170
+ "raw": {
171
+ "tool_name": "compile",
172
+ "command": ".venv\\Scripts\\python.exe -m compileall -r 10 -j 8 . -x .*venv",
173
+ "stdout": "Compiling './src/project/file.py'...",
174
+ "stderr": "",
175
+ "return_code": 0,
176
+ "timestamp": "2026-02-19 05:03:11"
177
+ },
178
+ "duration_s": 0.08294440002646297
179
+ },
180
+ {
181
+ "metrics": {
182
+ "security": 1.0
183
+ }
184
+ // many more lines ...
185
+ }
186
+ ]}
150
187
  ```
151
188
 
152
189
  ## Configuration
@@ -166,14 +203,111 @@ error = 0.7
166
203
 
167
204
  Tool IDs match the keys in `config/tools.yaml`: `compilation`, `bandit`, `ruff`, `ty`, `pytest`, `coverage`, `complexity`, `maintainability`, `halstead`, `vulture`, `interrogate`.
168
205
 
169
- ## LLM workflow
170
206
 
171
- `-o llm` selects the single worst-scoring tool and formats its top defect as
172
- concise markdown. The LLM fixes it, you re-run `cq check -o llm`, and repeat
173
- until all tools are green. Priority order ensures the most critical category
174
- (security, type errors, failing tests) is fixed before cosmetic ones.
207
+ ### Default config
208
+
209
+ ```yaml
210
+ tools:
211
+
212
+ compilation:
213
+ name: "compile"
214
+ command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
215
+ parser: "CompileParser"
216
+ priority: 1
217
+ warning_threshold: 0.9999
218
+ error_threshold: 0.9999
219
+
220
+ bandit:
221
+ name: "bandit"
222
+ command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
223
+ parser: "BanditParser"
224
+ priority: 2
225
+ warning_threshold: 0.9999
226
+ error_threshold: 0.8
227
+
228
+ ruff:
229
+ name: "ruff"
230
+ command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
231
+ parser: "RuffParser"
232
+ priority: 3
233
+ warning_threshold: 0.9999
234
+ error_threshold: 0.9
235
+
236
+ ty:
237
+ name: "ty"
238
+ command: "{python} -m ty check --output-format concise --color never {context_path}"
239
+ parser: "TyParser"
240
+ priority: 4
241
+ warning_threshold: 0.9999
242
+ error_threshold: 0.8
243
+ run_in_target_env: true
244
+ extra_deps:
245
+ - ty
246
+
247
+ pytest:
248
+ name: "pytest"
249
+ command: "{python} -m pytest -v {context_path}"
250
+ parser: "PytestParser"
251
+ priority: 5
252
+ warning_threshold: 0.7
253
+ error_threshold: 0.5
254
+ run_in_target_env: true
255
+
256
+ coverage:
257
+ name: "coverage"
258
+ command: "{python} -m coverage run -m pytest {context_path} && {python} -m coverage report"
259
+ parser: "CoverageParser"
260
+ priority: 6
261
+ warning_threshold: 0.9
262
+ error_threshold: 0.5
263
+ run_in_target_env: true
264
+ extra_deps:
265
+ - coverage
266
+ - pytest
267
+
268
+ complexity:
269
+ name: "radon cc"
270
+ command: "{python} -m radon cc --json {context_path}"
271
+ parser: "ComplexityParser"
272
+ priority: 7
273
+ warning_threshold: 0.6
274
+ error_threshold: 0.4
275
+
276
+ maintainability:
277
+ name: "radon mi"
278
+ command: "{python} -m radon mi -s --json {context_path}"
279
+ parser: "MaintainabilityParser"
280
+ priority: 8
281
+ warning_threshold: 0.6
282
+ error_threshold: 0.4
283
+
284
+ halstead:
285
+ name: "radon hal"
286
+ command: "{python} -m radon hal -f --json {context_path}"
287
+ parser: "HalsteadParser"
288
+ priority: 9
289
+ warning_threshold: 0.5
290
+ error_threshold: 0.3
291
+
292
+ vulture:
293
+ name: "vulture"
294
+ command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
295
+ parser: "VultureParser"
296
+ priority: 10
297
+ warning_threshold: 0.9999
298
+ error_threshold: 0.8
299
+
300
+ interrogate:
301
+ name: "interrogate"
302
+ command: "{python} -m interrogate {context_path} -v --fail-under 0"
303
+ parser: "InterrogateParser"
304
+ priority: 11
305
+ warning_threshold: 0.8
306
+ error_threshold: 0.3
175
307
 
176
- ## Tools
308
+ ```
309
+
310
+ ## Respect
177
311
 
178
312
  Many thanks to all the wonderful maintainers of :
179
313
 
@@ -186,3 +320,5 @@ Many thanks to all the wonderful maintainers of :
186
320
  - [radon](https://github.com/rubik/radon)
187
321
  - [vulture](https://github.com/jendrikseipp/vulture)
188
322
  - [interrogate](https://github.com/econchick/interrogate)
323
+ - [diskcache](https://github.com/grantjenks/python-diskcache)
324
+ - [typer](https://github.com/fastapi/typer)
@@ -1,19 +1,41 @@
1
1
  # CQ - Python Code Quality Analysis Tool
2
2
 
3
- Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. The primary workflow is:
3
+ Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
4
+
5
+ The primary workflow is:
4
6
 
5
7
  ```bash
6
- cq check -o llm # get the single most critical defect as markdown
8
+ # get the single most critical defect as markdown
9
+ cq check . -o llm
7
10
  ```
11
+ Outputs the top error from the first priority tool where the score < warning_threshold,. The code context is expanded if available.
12
+ ```md
13
+ `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
8
14
 
9
- Feed that output to an LLM, apply the fix, repeat until the score is clean.
15
+ 18: min_dist = float("inf")
16
+ 19: nearest_city = None
17
+ 20: for city in cities:
18
+ 21: unused_variable = 67
19
+ 22: dist = calc_dist(current_city, city)
20
+ 23: if dist < min_dist:
21
+ 24: min_dist = dist
22
+ 25: nearest_city = city
23
+
24
+ Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
25
+ ```
26
+ Feed to an LLM with edit tools and repeat until there are no issues, e.g.
27
+
28
+ ```
29
+ cq check . -o llm | claude -p "fix this"
30
+ ```
10
31
 
11
32
  ## Install
12
33
 
13
34
  ```bash
35
+ # install the `cq` command line tool from PyPi
14
36
  uv tool install python-code-quality
15
37
 
16
- # or
38
+ # or, clone it then install
17
39
  git pull https://github.com/rhiza-fr/py-cq.git
18
40
  cd py-cq
19
41
  uv tool install .
@@ -21,7 +43,7 @@ uv tool install .
21
43
 
22
44
  ## Tools
23
45
 
24
- CQ runs these tools in *parallel*:
46
+ These tools are run in **parallel**:
25
47
 
26
48
  | Priority | Tool | Measures |
27
49
  |----------|------|----------|
@@ -37,40 +59,40 @@ CQ runs these tools in *parallel*:
37
59
  | 10 | vulture | Dead code |
38
60
  | 11 | interrogate | Docstring coverage |
39
61
 
62
+ Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
63
+
64
+
40
65
  ## Usage
41
66
 
42
67
  ```bash
43
68
  # LLM workflow: get the top defect as markdown (primary use case)
44
69
  cq check -o llm
45
70
 
46
- # Rich table with all metrics (default, also saves .cq.json)
47
- cq check
71
+ # Rich table with all metrics
72
+ cq check .
48
73
 
49
74
  # Numeric score only — useful in CI or scripts
50
- cq check -o score
75
+ cq check . -o score
51
76
 
52
- # Full JSON output
53
- cq check -o json
77
+ # Full JSON output, including raw test results
78
+ cq check . -o json
54
79
 
55
- # Explicit path (defaults to current directory)
80
+ # Explicit path
56
81
  cq check path/to/project/
57
82
  cq check path/to/file.py
58
83
 
59
- # Run sequentially (1 worker) instead of in parallel
60
- cq check --workers 1
84
+ # Run sequentially if you like things slow
85
+ cq check . --workers 1
61
86
 
62
- # Clear cached results before running
63
- cq check --clear-cache
64
-
65
- # Save table output to a custom file
66
- cq check --out-file custom_results.json
87
+ # Clear cached results before running (rarely needed)
88
+ cq check . --clear-cache
67
89
 
68
90
  # Show effective tool configuration (thresholds, enabled/disabled status)
69
91
  cq config
70
92
  cq config path/to/project/
71
93
  ```
72
94
 
73
- ## Output
95
+ ## Table output
74
96
 
75
97
  ```bash
76
98
  > cq check .
@@ -97,6 +119,8 @@ cq config path/to/project/
97
119
  │ │ │ Score │ 0.965 │ │
98
120
  └──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
99
121
  ```
122
+
123
+ ## Single score output
100
124
  ```bash
101
125
  > cq check . -o score
102
126
  ```
@@ -104,23 +128,36 @@ cq config path/to/project/
104
128
  0.9662730667181059 # this is designed to approach but not reach 1.0
105
129
  ```
106
130
 
131
+ ## Json output
107
132
  ```bash
108
- > cq check . -o llm
133
+ > cq check . -o json
109
134
  ```
110
135
 
111
- ```md
112
- `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
113
-
114
- 18: min_dist = float("inf")
115
- 19: nearest_city = None
116
- 20: for city in cities:
117
- 21: unused_variable = 67
118
- 22: dist = calc_dist(current_city, city)
119
- 23: if dist < min_dist:
120
- 24: min_dist = dist
121
- 25: nearest_city = city
122
-
123
- Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
136
+ ```json
137
+ {
138
+ "metrics": [
139
+ {
140
+ "metrics": {
141
+ "compile": 1.0
142
+ },
143
+ "details": {},
144
+ "raw": {
145
+ "tool_name": "compile",
146
+ "command": ".venv\\Scripts\\python.exe -m compileall -r 10 -j 8 . -x .*venv",
147
+ "stdout": "Compiling './src/project/file.py'...",
148
+ "stderr": "",
149
+ "return_code": 0,
150
+ "timestamp": "2026-02-19 05:03:11"
151
+ },
152
+ "duration_s": 0.08294440002646297
153
+ },
154
+ {
155
+ "metrics": {
156
+ "security": 1.0
157
+ }
158
+ // many more lines ...
159
+ }
160
+ ]}
124
161
  ```
125
162
 
126
163
  ## Configuration
@@ -140,14 +177,111 @@ error = 0.7
140
177
 
141
178
  Tool IDs match the keys in `config/tools.yaml`: `compilation`, `bandit`, `ruff`, `ty`, `pytest`, `coverage`, `complexity`, `maintainability`, `halstead`, `vulture`, `interrogate`.
142
179
 
143
- ## LLM workflow
144
180
 
145
- `-o llm` selects the single worst-scoring tool and formats its top defect as
146
- concise markdown. The LLM fixes it, you re-run `cq check -o llm`, and repeat
147
- until all tools are green. Priority order ensures the most critical category
148
- (security, type errors, failing tests) is fixed before cosmetic ones.
181
+ ### Default config
182
+
183
+ ```yaml
184
+ tools:
185
+
186
+ compilation:
187
+ name: "compile"
188
+ command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
189
+ parser: "CompileParser"
190
+ priority: 1
191
+ warning_threshold: 0.9999
192
+ error_threshold: 0.9999
193
+
194
+ bandit:
195
+ name: "bandit"
196
+ command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
197
+ parser: "BanditParser"
198
+ priority: 2
199
+ warning_threshold: 0.9999
200
+ error_threshold: 0.8
201
+
202
+ ruff:
203
+ name: "ruff"
204
+ command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
205
+ parser: "RuffParser"
206
+ priority: 3
207
+ warning_threshold: 0.9999
208
+ error_threshold: 0.9
209
+
210
+ ty:
211
+ name: "ty"
212
+ command: "{python} -m ty check --output-format concise --color never {context_path}"
213
+ parser: "TyParser"
214
+ priority: 4
215
+ warning_threshold: 0.9999
216
+ error_threshold: 0.8
217
+ run_in_target_env: true
218
+ extra_deps:
219
+ - ty
220
+
221
+ pytest:
222
+ name: "pytest"
223
+ command: "{python} -m pytest -v {context_path}"
224
+ parser: "PytestParser"
225
+ priority: 5
226
+ warning_threshold: 0.7
227
+ error_threshold: 0.5
228
+ run_in_target_env: true
229
+
230
+ coverage:
231
+ name: "coverage"
232
+ command: "{python} -m coverage run -m pytest {context_path} && {python} -m coverage report"
233
+ parser: "CoverageParser"
234
+ priority: 6
235
+ warning_threshold: 0.9
236
+ error_threshold: 0.5
237
+ run_in_target_env: true
238
+ extra_deps:
239
+ - coverage
240
+ - pytest
241
+
242
+ complexity:
243
+ name: "radon cc"
244
+ command: "{python} -m radon cc --json {context_path}"
245
+ parser: "ComplexityParser"
246
+ priority: 7
247
+ warning_threshold: 0.6
248
+ error_threshold: 0.4
249
+
250
+ maintainability:
251
+ name: "radon mi"
252
+ command: "{python} -m radon mi -s --json {context_path}"
253
+ parser: "MaintainabilityParser"
254
+ priority: 8
255
+ warning_threshold: 0.6
256
+ error_threshold: 0.4
257
+
258
+ halstead:
259
+ name: "radon hal"
260
+ command: "{python} -m radon hal -f --json {context_path}"
261
+ parser: "HalsteadParser"
262
+ priority: 9
263
+ warning_threshold: 0.5
264
+ error_threshold: 0.3
265
+
266
+ vulture:
267
+ name: "vulture"
268
+ command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
269
+ parser: "VultureParser"
270
+ priority: 10
271
+ warning_threshold: 0.9999
272
+ error_threshold: 0.8
273
+
274
+ interrogate:
275
+ name: "interrogate"
276
+ command: "{python} -m interrogate {context_path} -v --fail-under 0"
277
+ parser: "InterrogateParser"
278
+ priority: 11
279
+ warning_threshold: 0.8
280
+ error_threshold: 0.3
149
281
 
150
- ## Tools
282
+ ```
283
+
284
+ ## Respect
151
285
 
152
286
  Many thanks to all the wonderful maintainers of :
153
287
 
@@ -160,3 +294,5 @@ Many thanks to all the wonderful maintainers of :
160
294
  - [radon](https://github.com/rubik/radon)
161
295
  - [vulture](https://github.com/jendrikseipp/vulture)
162
296
  - [interrogate](https://github.com/econchick/interrogate)
297
+ - [diskcache](https://github.com/grantjenks/python-diskcache)
298
+ - [typer](https://github.com/fastapi/typer)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "python-code-quality"
3
- version = "0.1.6"
4
- description = "Python Code Quality Analysis Tool - feed the results from 11 CQ CQ straight into an LLM."
3
+ version = "0.1.7"
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"
@@ -23,12 +23,11 @@ from rich.console import Console
23
23
  from rich.logging import RichHandler
24
24
  from rich.table import Table
25
25
 
26
- from py_cq.config import DEFAULT_STORAGE_FILE, load_user_config
26
+ from py_cq.config import load_user_config
27
27
  from py_cq.execution_engine import _cache as tool_cache
28
28
  from py_cq.execution_engine import run_tools
29
29
  from py_cq.localtypes import CombinedToolResults, ToolConfig
30
30
  from py_cq.metric_aggregator import aggregate_metrics
31
- from py_cq.storage import save_result
32
31
  from py_cq.tool_registry import tool_registry
33
32
 
34
33
  logging.basicConfig(
@@ -44,6 +43,7 @@ app = typer.Typer(
44
43
  " cq check . # full table with all metrics (default)\n\n"
45
44
  " cq check . -o llm # top defect as markdown (primary LLM workflow)\n\n"
46
45
  " cq check . -o score # numeric score only\n\n"
46
+ " cq check . -o json # full raw json of all tools"
47
47
  " cq config . # show effective tool configuration"
48
48
  ),
49
49
  )
@@ -78,7 +78,7 @@ class OutputMode(str, Enum):
78
78
 
79
79
  @app.callback()
80
80
  def callback():
81
- """CQ - Code Quality Analysis Tool."""
81
+ """Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm"""
82
82
  console = Console()
83
83
 
84
84
 
@@ -93,11 +93,6 @@ def check(
93
93
  "--log-level",
94
94
  help="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
95
95
  ),
96
- out_file: str = typer.Option(
97
- DEFAULT_STORAGE_FILE,
98
- "--out-file",
99
- help="File path to save results in table mode",
100
- ),
101
96
  clear_cache: bool = typer.Option(
102
97
  False, "--clear-cache", help="Clear cached tool results before running"
103
98
  ),
@@ -105,7 +100,7 @@ def check(
105
100
  0, "--workers", help="Max parallel workers (default: one per tool, use 1 for sequential)"
106
101
  ),
107
102
  ):
108
- """Run static analysis on a Python file or project directory."""
103
+ """Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
109
104
  path_obj = Path(path)
110
105
  if not path_obj.exists():
111
106
  raise typer.BadParameter(f"Path does not exist: {path}")
@@ -132,7 +127,6 @@ def check(
132
127
  from py_cq.llm_formatter import format_for_llm
133
128
  console.print(format_for_llm(effective_registry, combined_metrics))
134
129
  else:
135
- save_result(combined_tool_results=combined_metrics, file_name=out_file)
136
130
  console.print(format_as_table(combined_metrics, effective_registry))
137
131
 
138
132
 
@@ -1,10 +1,8 @@
1
- """Default storage path and user config loader."""
1
+ """User config loader."""
2
2
 
3
3
  import tomllib
4
4
  from pathlib import Path
5
5
 
6
- DEFAULT_STORAGE_FILE = ".cq.json"
7
-
8
6
 
9
7
  def load_user_config(project_path: Path) -> dict:
10
8
  """Read [tool.cq] from pyproject.toml at project_path, if present.
@@ -10,7 +10,7 @@ tools:
10
10
 
11
11
  bandit:
12
12
  name: "bandit"
13
- command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {context_dir}/.venv,{context_dir}/tests"
13
+ command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
14
14
  parser: "BanditParser"
15
15
  priority: 2
16
16
  warning_threshold: 0.9999
@@ -20,16 +20,16 @@ import time
20
20
  from collections.abc import Collection
21
21
  from concurrent.futures import ThreadPoolExecutor, as_completed
22
22
  from pathlib import Path
23
- from typing import cast
23
+ from typing import Any, cast
24
24
 
25
- import diskcache
25
+ from diskcache import Cache, JSONDisk
26
26
 
27
27
  from py_cq.context_hash import get_context_hash
28
28
  from py_cq.localtypes import RawResult, ToolConfig, ToolResult
29
29
 
30
30
  log = logging.getLogger("cq")
31
31
 
32
- _cache = diskcache.Cache(Path.home() / ".cache" / "cq")
32
+ _cache = Cache(Path.home() / ".cache" / "cq", size_limit=100 * 1024 * 1024, disk=JSONDisk)
33
33
 
34
34
 
35
35
  def _find_project_root(path: Path) -> Path | None:
@@ -75,12 +75,12 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
75
75
  with_flags = " ".join(f"--with {dep}" for dep in tool_config.extra_deps)
76
76
  python = f'"{uv}" run --directory "{abs_dir}" {with_flags}'.rstrip()
77
77
  abs_context_path = str(Path(context_path).resolve())
78
- context_dir = Path(context_path).as_posix().rstrip("/")
79
- command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, context_dir=context_dir, python=python)
78
+ input_path_posix = Path(context_path).as_posix().rstrip("/")
79
+ command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, input_path_posix=input_path_posix, python=python)
80
80
  cache_key = f"{command}:{get_context_hash(context_path)}"
81
81
  if cache_key in _cache:
82
82
  log.info(f"Cache hit: {command}")
83
- return cast(RawResult, _cache[cache_key])
83
+ return RawResult(**cast(dict[str, Any], _cache[cache_key]))
84
84
  log.info(f"Running: {command}")
85
85
  result = subprocess.run(command, capture_output=True, text=True, shell=True) # nosec
86
86
  timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
@@ -92,7 +92,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
92
92
  return_code=result.returncode,
93
93
  timestamp=timestamp,
94
94
  )
95
- _cache[cache_key] = raw_result
95
+ _cache.set(cache_key, raw_result.to_dict(), expire=5 * 24 * 60 * 60)
96
96
  return raw_result
97
97
 
98
98
 
@@ -9,7 +9,7 @@ logistic-variant score stored under the ``security`` metric key.
9
9
  import json
10
10
 
11
11
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
12
- from py_cq.parsers.common import score_logistic_variant
12
+ from py_cq.parsers.common import format_source_context, score_logistic_variant
13
13
 
14
14
  _SEVERITY_WEIGHT = {"HIGH": 5, "MEDIUM": 2, "LOW": 1}
15
15
 
@@ -51,4 +51,4 @@ class BanditParser(AbstractParser):
51
51
  code = issue.get("code", "")
52
52
  severity = issue.get("severity", "")
53
53
  message = issue.get("message", "")
54
- return f"`{file}:{line}` — **{code}** [{severity}]: {message}"
54
+ return f"`{file}:{line}` — **{code}** [{severity}]: {message}{format_source_context(file, line)}"
@@ -25,6 +25,18 @@ def read_source_lines(file_path: str, line: int, count: int = 5) -> str:
25
25
  return ""
26
26
 
27
27
 
28
+ def format_source_context(file: str, line: int | str, context: int = 3, count: int = 8) -> str:
29
+ """Return a fenced python code block for the source around `line`, or '' if unavailable."""
30
+ if not isinstance(line, int):
31
+ return ""
32
+ context_start = max(1, line - context)
33
+ raw_lines = read_source_lines(file, context_start, count=count).splitlines()
34
+ if not raw_lines:
35
+ return ""
36
+ src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines))
37
+ return f"\n```python\n{src}\n```"
38
+
39
+
28
40
  def inv_normalize(value: float, max_value: float) -> float:
29
41
  """Returns the inverse normalized value of `value` relative to `max_value`."""
30
42
  return (max_value - min(value, max_value)) / max_value
@@ -6,7 +6,7 @@ compile score, and providing concise help messages for any failures."""
6
6
  import logging
7
7
 
8
8
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
9
- from py_cq.parsers.common import read_source_lines, score_logistic_variant
9
+ from py_cq.parsers.common import format_source_context, score_logistic_variant
10
10
 
11
11
  log = logging.getLogger("cq")
12
12
 
@@ -124,11 +124,5 @@ class CompileParser(AbstractParser):
124
124
  line = info.get("line", "?")
125
125
  typ = info.get("type", "Error")
126
126
  help_msg = info.get("help", "")
127
- if isinstance(line, int):
128
- context_start = max(1, line - 3)
129
- raw_lines = read_source_lines(file, context_start, count=8).splitlines()
130
- src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines)) if raw_lines else info.get("src", "")
131
- else:
132
- src = info.get("src", "")
133
- code_block = f"\n```python\n{src}\n```" if src else ""
127
+ code_block = format_source_context(file, line) or (f"\n```python\n{info['src']}\n```" if info.get("src") else "")
134
128
  return f"`{file}:{line}` — **{typ}**: {help_msg}{code_block}"
@@ -13,7 +13,7 @@ followed by a summary line ``Found N error.`` or ``All checks passed!``."""
13
13
  import re
14
14
 
15
15
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
16
- from py_cq.parsers.common import read_source_lines, score_logistic_variant
16
+ from py_cq.parsers.common import format_source_context, score_logistic_variant
17
17
 
18
18
  _DIAG_RE = re.compile(r"^(.+):(\d+):(\d+): ([A-Z]\d+) (.+)$")
19
19
 
@@ -54,8 +54,4 @@ class RuffParser(AbstractParser):
54
54
  line = issue.get("line", "?")
55
55
  code = issue.get("code", "")
56
56
  message = issue.get("message", "")
57
- context_start = max(1, line - 3) if isinstance(line, int) else line
58
- raw_lines = read_source_lines(file, context_start, count=8).splitlines() if isinstance(line, int) else []
59
- src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines)) if raw_lines else ""
60
- code_block = f"\n```python\n{src}\n```" if src else ""
61
- return f"`{file}:{line}` — **{code}**: {message}{code_block}"
57
+ return f"`{file}:{line}` **{code}**: {message}{format_source_context(file, line)}"
@@ -14,7 +14,7 @@ Errors count more heavily than warnings toward the score."""
14
14
  import re
15
15
 
16
16
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
17
- from py_cq.parsers.common import read_source_lines, score_logistic_variant
17
+ from py_cq.parsers.common import format_source_context, score_logistic_variant
18
18
 
19
19
  _DIAG_RE = re.compile(r"^(.+):(\d+):\d+:\s+(error|warning)\[([^\]]+)\] (.+)$")
20
20
 
@@ -58,8 +58,4 @@ class TyParser(AbstractParser):
58
58
  line = issue.get("line", "?")
59
59
  code = issue.get("code", "")
60
60
  message = issue.get("message", "")
61
- context_start = max(1, line - 3) if isinstance(line, int) else line
62
- raw_lines = read_source_lines(file, context_start, count=8).splitlines() if isinstance(line, int) else []
63
- src = "\n".join(f"{context_start + i}: {rline}" for i, rline in enumerate(raw_lines)) if raw_lines else ""
64
- code_block = f"\n```python\n{src}\n```" if src else ""
65
- return f"`{file}:{line}` — **{code}**: {message}{code_block}"
61
+ return f"`{file}:{line}` **{code}**: {message}{format_source_context(file, line)}"
@@ -12,7 +12,7 @@ score stored under the ``dead_code`` metric key.
12
12
  import re
13
13
 
14
14
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
15
- from py_cq.parsers.common import score_logistic_variant
15
+ from py_cq.parsers.common import format_source_context, score_logistic_variant
16
16
 
17
17
  _LINE_RE = re.compile(r"^(.+):(\d+): (unused \S+) '(.+)' \((\d+)% confidence\)$")
18
18
 
@@ -45,4 +45,4 @@ class VultureParser(AbstractParser):
45
45
  kind = issue.get("type", "unused")
46
46
  name = issue.get("name", "")
47
47
  confidence = issue.get("confidence", "?")
48
- return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence)"
48
+ return f"`{file}:{line}` — **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line)}"
@@ -96,7 +96,9 @@ def test_run_tool_cache_miss_calls_subprocess(tmp_path):
96
96
  mock_result.stderr = ""
97
97
  mock_result.returncode = 0
98
98
 
99
- with patch("py_cq.execution_engine._cache", {}):
99
+ mock_cache = MagicMock()
100
+ mock_cache.__contains__ = MagicMock(return_value=False)
101
+ with patch("py_cq.execution_engine._cache", mock_cache):
100
102
  with patch("py_cq.execution_engine.subprocess.run", return_value=mock_result) as mock_sub:
101
103
  result = run_tool(cfg, str(tmp_path))
102
104
  mock_sub.assert_called_once()
@@ -114,11 +116,13 @@ def test_run_tool_cache_hit_skips_subprocess(tmp_path):
114
116
  cached = RawResult(tool_name="echo", stdout="cached!")
115
117
  # Build the cache key the same way run_tool does
116
118
  import sys
119
+ from pathlib import Path
117
120
 
118
121
  from py_cq.context_hash import get_context_hash
119
- command = cfg.command.format(context_path=str(tmp_path), python=sys.executable)
122
+ input_path_posix = Path(str(tmp_path)).as_posix().rstrip("/")
123
+ command = cfg.command.format(context_path=str(tmp_path), abs_context_path=str(tmp_path), input_path_posix=input_path_posix, python=sys.executable)
120
124
  cache_key = f"{command}:{get_context_hash(str(tmp_path))}"
121
- fake_cache = {cache_key: cached}
125
+ fake_cache = {cache_key: cached.to_dict()}
122
126
 
123
127
  with patch("py_cq.execution_engine._cache", fake_cache):
124
128
  with patch("py_cq.execution_engine.subprocess.run") as mock_sub:
@@ -300,7 +300,7 @@ wheels = [
300
300
 
301
301
  [[package]]
302
302
  name = "python-code-quality"
303
- version = "0.1.4"
303
+ version = "0.1.6"
304
304
  source = { editable = "." }
305
305
  dependencies = [
306
306
  { name = "bandit" },
@@ -1,27 +0,0 @@
1
- """Utilities for persisting combined tool results.
2
-
3
- This module provides a single helper function, :func:`save_result`, which
4
- serializes a :class:`CombinedToolResults` instance to a JSON file. The
5
- function ensures that the target directory exists, writes a readable
6
- representation of the results, and returns the absolute path of the created
7
- file.
8
-
9
- Example
10
- -------
11
- >>> from your_package import CombinedToolResults, save_result
12
- >>> results = CombinedToolResults(...)
13
- >>> output_path = save_result(results, "output.json")
14
- >>> print(output_path)"""
15
-
16
- import json
17
-
18
- from py_cq.localtypes import CombinedToolResults
19
-
20
-
21
- def save_result(combined_tool_results: CombinedToolResults, file_name: str):
22
- """Saves combined tool results to a JSON file named by `file_name`."""
23
- if not file_name:
24
- return
25
- data = combined_tool_results.to_dict()
26
- with open(file_name, "w") as f:
27
- json.dump(data, f, indent=4)
@@ -1,41 +0,0 @@
1
- """Tests for storage.save_result."""
2
-
3
- import json
4
-
5
- from py_cq.localtypes import CombinedToolResults, RawResult, ToolResult
6
- from py_cq.storage import save_result
7
-
8
-
9
- def _combined(path="test.py", tool_results=None):
10
- return CombinedToolResults(path=path, tool_results=tool_results or [])
11
-
12
-
13
- def test_save_result_writes_json(tmp_path):
14
- out = tmp_path / "results.json"
15
- save_result(_combined(), str(out))
16
- data = json.loads(out.read_text())
17
- assert "score" in data
18
- assert "path" in data
19
-
20
-
21
- def test_save_result_empty_filename_is_noop():
22
- save_result(_combined(), "") # must not raise
23
-
24
-
25
- def test_save_result_content_path_and_score(tmp_path):
26
- out = tmp_path / "out.json"
27
- save_result(_combined(path="myproject/"), str(out))
28
- data = json.loads(out.read_text())
29
- assert data["path"] == "myproject/"
30
-
31
-
32
- def test_save_result_includes_tool_metrics(tmp_path):
33
- out = tmp_path / "out.json"
34
- combined = CombinedToolResults(
35
- path=".",
36
- tool_results=[ToolResult(metrics={"lint": 0.9}, raw=RawResult(tool_name="ruff"))],
37
- )
38
- save_result(combined, str(out))
39
- data = json.loads(out.read_text())
40
- assert len(data["metrics"]) == 1
41
- assert data["metrics"][0]["metrics"]["lint"] == 0.9