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.
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/PKG-INFO +177 -41
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/README.md +175 -39
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/pyproject.toml +2 -2
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/cli.py +4 -10
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/config/__init__.py +1 -3
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/config/tools.yaml +1 -1
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/execution_engine.py +7 -7
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/banditparser.py +2 -2
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/common.py +12 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/compileparser.py +2 -8
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/ruffparser.py +2 -6
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/typarser.py +2 -6
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/vultureparser.py +2 -2
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_execution_engine.py +7 -3
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/uv.lock +1 -1
- python_code_quality-0.1.6/src/py_cq/storage.py +0 -27
- python_code_quality-0.1.6/tests/test_storage.py +0 -41
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/.github/workflows/python-publish.yml +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/.gitignore +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/.python-version +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/CLAUDE.md +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/LICENSE +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/data/problems/travelling_salesman/ts_bad.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/data/problems/travelling_salesman/ts_good.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/__init__.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/llm_formatter.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/localtypes.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/main.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/complexityparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/coverageparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/halsteadparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/interrogateparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/maintainabilityparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/pytestparser.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/py.typed +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/tool_registry.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/conftest.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_common.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_config.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_context_hash.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_llm_formatter.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_localtypes.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_bandit.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_compile.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_complexity.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_coverage.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_interrogate.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_maintainability.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_pytest.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_ruff.py +0 -0
- {python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_ty.py +0 -0
- {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.
|
|
4
|
-
Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ
|
|
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
|
-
|
|
29
|
+
Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
|
|
30
|
+
|
|
31
|
+
The primary workflow is:
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
|
-
|
|
34
|
+
# get the single most critical defect as markdown
|
|
35
|
+
cq check . -o llm
|
|
33
36
|
```
|
|
37
|
+
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
106
|
+
# Explicit path
|
|
82
107
|
cq check path/to/project/
|
|
83
108
|
cq check path/to/file.py
|
|
84
109
|
|
|
85
|
-
# Run sequentially
|
|
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
|
-
##
|
|
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
|
|
159
|
+
> cq check . -o json
|
|
135
160
|
```
|
|
136
161
|
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
80
|
+
# Explicit path
|
|
56
81
|
cq check path/to/project/
|
|
57
82
|
cq check path/to/file.py
|
|
58
83
|
|
|
59
|
-
# Run sequentially
|
|
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
|
-
##
|
|
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
|
|
133
|
+
> cq check . -o json
|
|
109
134
|
```
|
|
110
135
|
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
description = "Python Code Quality Analysis Tool - feed the results from 11 CQ
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
79
|
-
command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path,
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
{python_code_quality-0.1.6 → python_code_quality-0.1.7}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.6 → python_code_quality-0.1.7}/data/problems/travelling_salesman/ts_bad.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.6 → python_code_quality-0.1.7}/data/problems/travelling_salesman/ts_good.py
RENAMED
|
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.6 → python_code_quality-0.1.7}/src/py_cq/parsers/complexityparser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/interrogateparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.1.6 → python_code_quality-0.1.7}/src/py_cq/parsers/maintainabilityparser.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.1.6 → python_code_quality-0.1.7}/tests/test_parser_maintainability.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|