python-code-quality 0.1.8__tar.gz → 0.1.9__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.9/.gitea/workflows/python-multi-version-test.yaml +65 -0
- python_code_quality-0.1.9/.gitea/workflows/test.yaml +11 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/.gitignore +1 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/CLAUDE.md +3 -3
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/PKG-INFO +43 -46
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/README.md +42 -45
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/pyproject.toml +1 -1
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/cli.py +54 -5
- python_code_quality-0.1.8/src/py_cq/config/tools.yaml → python_code_quality-0.1.9/src/py_cq/config/config.yaml +28 -37
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/execution_engine.py +2 -2
- python_code_quality-0.1.9/src/py_cq/language_detector.py +29 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/llm_formatter.py +2 -1
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/localtypes.py +5 -1
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/banditparser.py +2 -2
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/common.py +31 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/compileparser.py +2 -2
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/coverageparser.py +1 -1
- python_code_quality-0.1.9/src/py_cq/parsers/exitcodeparser.py +16 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/halsteadparser.py +1 -1
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/interrogateparser.py +1 -1
- python_code_quality-0.1.9/src/py_cq/parsers/linecountparser.py +26 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/pytestparser.py +53 -10
- python_code_quality-0.1.9/src/py_cq/parsers/regexcountparser.py +35 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/ruffparser.py +2 -2
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/typarser.py +2 -2
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/vultureparser.py +2 -2
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/tool_registry.py +5 -4
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_cli.py +135 -2
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_common.py +46 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_execution_engine.py +3 -1
- python_code_quality-0.1.9/tests/test_integration_user_tools.py +152 -0
- python_code_quality-0.1.9/tests/test_language_detector.py +87 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_localtypes.py +28 -0
- python_code_quality-0.1.9/tests/test_parser_exitcode.py +39 -0
- python_code_quality-0.1.9/tests/test_parser_linecount.py +42 -0
- python_code_quality-0.1.9/tests/test_parser_pytest.py +112 -0
- python_code_quality-0.1.9/tests/test_parser_regexcount.py +42 -0
- python_code_quality-0.1.9/tests/test_tool_registry.py +16 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/uv.lock +56 -56
- python_code_quality-0.1.8/data/problems/travelling_salesman/ts_bad.py +0 -65
- python_code_quality-0.1.8/data/problems/travelling_salesman/ts_good.py +0 -61
- python_code_quality-0.1.8/tests/test_parser_pytest.py +0 -49
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/.github/workflows/python-publish.yml +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/.python-version +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/LICENSE +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/__init__.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/config/__init__.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/main.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/complexityparser.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/parsers/maintainabilityparser.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/src/py_cq/py.typed +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/conftest.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_config.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_context_hash.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_llm_formatter.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_bandit.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_compile.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_complexity.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_coverage.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_halstead.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_interrogate.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_maintainability.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_ruff.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_ty.py +0 -0
- {python_code_quality-0.1.8 → python_code_quality-0.1.9}/tests/test_parser_vulture.py +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Template: Multi-version Python testing workflow for Gitea Actions
|
|
2
|
+
# Copy this file to your project's .gitea/workflows/ directory
|
|
3
|
+
#
|
|
4
|
+
# Runs tests against multiple Python versions using generic ubuntu runners.
|
|
5
|
+
# Includes optional dependency upgrade testing.
|
|
6
|
+
|
|
7
|
+
name: Python Tests
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
name: Python ${{ matrix.python-version }}
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ['3.12', '3.13']
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
28
|
+
uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Install uv
|
|
33
|
+
uses: astral-sh/setup-uv@v7
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: uv sync --all-extras
|
|
37
|
+
|
|
38
|
+
- name: Run tests
|
|
39
|
+
run: uv run pytest
|
|
40
|
+
|
|
41
|
+
# Optional: Test if dependencies can be upgraded
|
|
42
|
+
upgrade-test:
|
|
43
|
+
name: Dependency Upgrade Test
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
continue-on-error: true # Don't fail the whole workflow if upgrades break
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v4
|
|
49
|
+
|
|
50
|
+
- name: Set up Python
|
|
51
|
+
uses: actions/setup-python@v5
|
|
52
|
+
with:
|
|
53
|
+
python-version: '3.14' # Test upgrades on latest Python
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
uses: astral-sh/setup-uv@v7
|
|
57
|
+
|
|
58
|
+
- name: Upgrade all dependencies
|
|
59
|
+
run: uv lock --upgrade
|
|
60
|
+
|
|
61
|
+
- name: Install upgraded dependencies
|
|
62
|
+
run: uv sync --all-extras
|
|
63
|
+
|
|
64
|
+
- name: Run tests with upgraded dependencies
|
|
65
|
+
run: uv run pytest
|
|
@@ -39,8 +39,8 @@ uv run ruff check src/
|
|
|
39
39
|
**Pipeline flow:** CLI (`cli.py`) → tool registry → execution engine → parsers → metric aggregator → output
|
|
40
40
|
|
|
41
41
|
- **`cli.py`**: Typer app with a single `check` command. Output mode selected via `--output`/`-o` enum (`table`, `score`, `json`, `llm`). Runs tools in parallel by default. Reads `[tool.cq]` from the target project's `pyproject.toml` and applies overrides before running.
|
|
42
|
-
- **`tool_registry.py`**: Loads `src/cq/config/
|
|
43
|
-
- **`config/
|
|
42
|
+
- **`tool_registry.py`**: Loads `src/cq/config/config.yaml` at import time via `importlib.resources`, dynamically imports parser classes, builds a `dict[str, ToolConfig]` registry.
|
|
43
|
+
- **`config/config.yaml`** (at `src/cq/config/config.yaml`): Declares each analysis tool: shell command template (with `{context_path}` placeholder), parser class name, order, warning/error thresholds. Tools are listed and executed in order.
|
|
44
44
|
- **`execution_engine.py`**: Runs shell commands via `subprocess.run`, caches results with `diskcache` using a content-based hash. Parallel execution via `ThreadPoolExecutor`; results are sorted by order before returning.
|
|
45
45
|
- **`parsers/`**: Each parser subclasses `AbstractParser` (from `localtypes.py`), implementing `parse(RawResult) -> ToolResult` and optionally `format_llm_message(ToolResult) -> str`. Parser module names must match the lowercase parser class name (e.g., `PytestParser` → `pytestparser.py`).
|
|
46
46
|
- **`localtypes.py`**: Core dataclasses — `ToolConfig`, `RawResult`, `ToolResult`, `CombinedToolResults`, and `AbstractParser` ABC.
|
|
@@ -49,6 +49,6 @@ uv run ruff check src/
|
|
|
49
49
|
|
|
50
50
|
## Adding a New Analysis Tool
|
|
51
51
|
|
|
52
|
-
1. Add tool entry in `config/
|
|
52
|
+
1. Add tool entry in `config/config.yaml` with command template, parser name, order, and thresholds.
|
|
53
53
|
2. Create `src/cq/parsers/<parsername>.py` with a class matching the `parser` field in YAML.
|
|
54
54
|
3. The parser must subclass `AbstractParser` and implement `parse(RawResult) -> ToolResult`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
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
|
|
@@ -28,20 +28,16 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
|
|
30
30
|
|
|
31
|
+
Why? It removes the mental burden of understanding all these tools and parsing their results.
|
|
32
|
+
|
|
31
33
|
The primary workflow is:
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
36
|
# get the single most critical defect as markdown
|
|
35
37
|
cq check . -o llm
|
|
36
38
|
```
|
|
37
|
-
Selects the single most critical defect using this priority order:
|
|
38
|
-
|
|
39
|
-
1. **Severity** — tools with score below `error_threshold` come before those only below `warning_threshold`
|
|
40
|
-
2. **Order** — among tools at the same severity, lower-order tools win (compile before lint before style)
|
|
41
|
-
3. **Score** — among ties, the lower score wins
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
```md
|
|
40
|
+
```python
|
|
45
41
|
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
46
42
|
|
|
47
43
|
18: min_dist = float("inf")
|
|
@@ -60,9 +56,10 @@ Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
|
60
56
|
```python
|
|
61
57
|
cq check . -o llm | claude -p "fix this"
|
|
62
58
|
# or
|
|
63
|
-
cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
|
|
59
|
+
cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
|
|
64
60
|
```
|
|
65
61
|
|
|
62
|
+
|
|
66
63
|
## Install
|
|
67
64
|
|
|
68
65
|
```bash
|
|
@@ -70,21 +67,22 @@ cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
|
|
|
70
67
|
uv tool install python-code-quality
|
|
71
68
|
|
|
72
69
|
# or, clone it then install
|
|
73
|
-
git
|
|
70
|
+
git clone https://github.com/rhiza-fr/py-cq.git
|
|
74
71
|
cd py-cq
|
|
75
72
|
uv tool install .
|
|
76
73
|
```
|
|
77
74
|
|
|
78
75
|
## Tools
|
|
79
76
|
|
|
80
|
-
These tools are run in **parallel** except
|
|
77
|
+
These tools are run in **parallel** except:
|
|
78
|
+
When running '-o llm', we run sequentially and exit early at the first error.
|
|
81
79
|
|
|
82
80
|
| Order | Tool | Measures |
|
|
83
81
|
|----------|------|----------|
|
|
84
82
|
| 1 | compileall | Syntax errors |
|
|
85
|
-
| 2 |
|
|
86
|
-
| 3 |
|
|
87
|
-
| 4 |
|
|
83
|
+
| 2 | ruff | Lint / style |
|
|
84
|
+
| 3 | ty | Type errors |
|
|
85
|
+
| 4 | bandit | Security vulnerabilities |
|
|
88
86
|
| 5 | pytest | Test pass rate |
|
|
89
87
|
| 6 | coverage | Test coverage |
|
|
90
88
|
| 7 | radon cc | Cyclomatic complexity |
|
|
@@ -93,14 +91,14 @@ These tools are run in **parallel** except when looking for the first error in -
|
|
|
93
91
|
| 10 | vulture | Dead code |
|
|
94
92
|
| 11 | interrogate | Docstring coverage |
|
|
95
93
|
|
|
96
|
-
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
|
|
94
|
+
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
|
|
97
95
|
|
|
98
96
|
|
|
99
97
|
## Usage
|
|
100
98
|
|
|
101
99
|
```bash
|
|
102
100
|
cq check . # Table overview of scores for humans
|
|
103
|
-
cq check -o llm
|
|
101
|
+
cq check . -o llm # Top defect as markdown for LLMs
|
|
104
102
|
cq check . -o score # Numeric score only for CI
|
|
105
103
|
cq check . -o json # Detailed parsed JSON output for jq
|
|
106
104
|
cq check . -o raw # Raw tool output for debug
|
|
@@ -110,6 +108,13 @@ cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
|
110
108
|
cq config path/to/project/ # Show effective tool configuration
|
|
111
109
|
```
|
|
112
110
|
|
|
111
|
+
**Exit codes:** `cq check` exits with code `1` if any tool metric falls below its `error_threshold`, making it suitable as a CI gate:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
cq check . && deploy # block deploy on errors
|
|
115
|
+
cq check . -o score # print score, exit 1 on errors
|
|
116
|
+
```
|
|
117
|
+
|
|
113
118
|
## Table output
|
|
114
119
|
|
|
115
120
|
```bash
|
|
@@ -121,9 +126,9 @@ cq config path/to/project/ # Show effective tool configuration
|
|
|
121
126
|
┃ Tool ┃ Time ┃ Metric ┃ Score ┃ Status ┃
|
|
122
127
|
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
|
|
123
128
|
│ compile │ 0.42s │ compile │ 1.000 │ OK │
|
|
124
|
-
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
125
129
|
│ ruff │ 0.17s │ lint │ 1.000 │ OK │
|
|
126
130
|
│ ty │ 0.33s │ type_check │ 1.000 │ OK │
|
|
131
|
+
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
127
132
|
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
128
133
|
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
129
134
|
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
@@ -167,7 +172,7 @@ cq config path/to/project/ # Show effective tool configuration
|
|
|
167
172
|
|
|
168
173
|
## Raw output
|
|
169
174
|
```bash
|
|
170
|
-
> cq check -o raw
|
|
175
|
+
> cq check . -o raw
|
|
171
176
|
```
|
|
172
177
|
|
|
173
178
|
```json
|
|
@@ -193,66 +198,63 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
193
198
|
# Skip tools that are slow or not relevant to your project
|
|
194
199
|
disable = ["coverage", "interrogate"]
|
|
195
200
|
|
|
201
|
+
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
202
|
+
context_lines = 15
|
|
203
|
+
|
|
196
204
|
# Override warning/error thresholds per tool
|
|
197
205
|
[tool.cq.thresholds.coverage]
|
|
198
206
|
warning = 0.9
|
|
199
207
|
error = 0.7
|
|
200
208
|
```
|
|
201
209
|
|
|
202
|
-
Tool IDs match the keys in `config/
|
|
210
|
+
Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
|
|
203
211
|
|
|
204
212
|
|
|
205
213
|
### Default config
|
|
206
214
|
|
|
207
215
|
```yaml
|
|
208
|
-
|
|
216
|
+
python:
|
|
209
217
|
|
|
210
|
-
|
|
211
|
-
name: "compile"
|
|
218
|
+
compile:
|
|
212
219
|
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
213
220
|
parser: "CompileParser"
|
|
214
221
|
order: 1
|
|
215
222
|
warning_threshold: 0.9999
|
|
216
223
|
error_threshold: 0.9999
|
|
217
224
|
|
|
218
|
-
bandit:
|
|
219
|
-
name: "bandit"
|
|
220
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
221
|
-
parser: "BanditParser"
|
|
222
|
-
order: 2
|
|
223
|
-
warning_threshold: 0.9999
|
|
224
|
-
error_threshold: 0.8
|
|
225
|
-
|
|
226
225
|
ruff:
|
|
227
|
-
name: "ruff"
|
|
228
226
|
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
229
227
|
parser: "RuffParser"
|
|
230
|
-
order:
|
|
228
|
+
order: 2
|
|
231
229
|
warning_threshold: 0.9999
|
|
232
230
|
error_threshold: 0.9
|
|
233
231
|
|
|
234
232
|
ty:
|
|
235
|
-
name: "ty"
|
|
236
233
|
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
237
234
|
parser: "TyParser"
|
|
238
|
-
order:
|
|
235
|
+
order: 3
|
|
239
236
|
warning_threshold: 0.9999
|
|
240
237
|
error_threshold: 0.8
|
|
241
238
|
run_in_target_env: true
|
|
242
239
|
extra_deps:
|
|
243
240
|
- ty
|
|
244
241
|
|
|
242
|
+
bandit:
|
|
243
|
+
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
244
|
+
parser: "BanditParser"
|
|
245
|
+
order: 4
|
|
246
|
+
warning_threshold: 0.9999
|
|
247
|
+
error_threshold: 0.8
|
|
248
|
+
|
|
245
249
|
pytest:
|
|
246
|
-
name: "pytest"
|
|
247
250
|
command: "{python} -m pytest -v {context_path}"
|
|
248
251
|
parser: "PytestParser"
|
|
249
252
|
order: 5
|
|
250
|
-
warning_threshold: 0
|
|
251
|
-
error_threshold: 0
|
|
253
|
+
warning_threshold: 1.0
|
|
254
|
+
error_threshold: 1.0
|
|
252
255
|
run_in_target_env: true
|
|
253
256
|
|
|
254
257
|
coverage:
|
|
255
|
-
name: "coverage"
|
|
256
258
|
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
257
259
|
parser: "CoverageParser"
|
|
258
260
|
order: 6
|
|
@@ -263,24 +265,21 @@ tools:
|
|
|
263
265
|
- coverage
|
|
264
266
|
- pytest
|
|
265
267
|
|
|
266
|
-
|
|
267
|
-
name: "radon cc"
|
|
268
|
+
radon-cc:
|
|
268
269
|
command: "{python} -m radon cc --json {context_path}"
|
|
269
270
|
parser: "ComplexityParser"
|
|
270
271
|
order: 7
|
|
271
272
|
warning_threshold: 0.6
|
|
272
273
|
error_threshold: 0.4
|
|
273
274
|
|
|
274
|
-
|
|
275
|
-
name: "radon mi"
|
|
275
|
+
radon-mi:
|
|
276
276
|
command: "{python} -m radon mi -s --json {context_path}"
|
|
277
277
|
parser: "MaintainabilityParser"
|
|
278
278
|
order: 8
|
|
279
279
|
warning_threshold: 0.6
|
|
280
280
|
error_threshold: 0.4
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
name: "radon hal"
|
|
282
|
+
radon-hal:
|
|
284
283
|
command: "{python} -m radon hal -f --json {context_path}"
|
|
285
284
|
parser: "HalsteadParser"
|
|
286
285
|
order: 9
|
|
@@ -288,7 +287,6 @@ tools:
|
|
|
288
287
|
error_threshold: 0.3
|
|
289
288
|
|
|
290
289
|
vulture:
|
|
291
|
-
name: "vulture"
|
|
292
290
|
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
293
291
|
parser: "VultureParser"
|
|
294
292
|
order: 10
|
|
@@ -296,7 +294,6 @@ tools:
|
|
|
296
294
|
error_threshold: 0.8
|
|
297
295
|
|
|
298
296
|
interrogate:
|
|
299
|
-
name: "interrogate"
|
|
300
297
|
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
301
298
|
parser: "InterrogateParser"
|
|
302
299
|
order: 11
|
|
@@ -2,20 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
|
|
4
4
|
|
|
5
|
+
Why? It removes the mental burden of understanding all these tools and parsing their results.
|
|
6
|
+
|
|
5
7
|
The primary workflow is:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
# get the single most critical defect as markdown
|
|
9
11
|
cq check . -o llm
|
|
10
12
|
```
|
|
11
|
-
Selects the single most critical defect using this priority order:
|
|
12
|
-
|
|
13
|
-
1. **Severity** — tools with score below `error_threshold` come before those only below `warning_threshold`
|
|
14
|
-
2. **Order** — among tools at the same severity, lower-order tools win (compile before lint before style)
|
|
15
|
-
3. **Score** — among ties, the lower score wins
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
```md
|
|
14
|
+
```python
|
|
19
15
|
`data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
|
|
20
16
|
|
|
21
17
|
18: min_dist = float("inf")
|
|
@@ -34,9 +30,10 @@ Feed to an LLM with edit tools and repeat until there are no issues, e.g.
|
|
|
34
30
|
```python
|
|
35
31
|
cq check . -o llm | claude -p "fix this"
|
|
36
32
|
# or
|
|
37
|
-
cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
|
|
33
|
+
cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
|
|
38
34
|
```
|
|
39
35
|
|
|
36
|
+
|
|
40
37
|
## Install
|
|
41
38
|
|
|
42
39
|
```bash
|
|
@@ -44,21 +41,22 @@ cq check . -o llm | ollama gpt-oss:20b "Explain how to fix this"
|
|
|
44
41
|
uv tool install python-code-quality
|
|
45
42
|
|
|
46
43
|
# or, clone it then install
|
|
47
|
-
git
|
|
44
|
+
git clone https://github.com/rhiza-fr/py-cq.git
|
|
48
45
|
cd py-cq
|
|
49
46
|
uv tool install .
|
|
50
47
|
```
|
|
51
48
|
|
|
52
49
|
## Tools
|
|
53
50
|
|
|
54
|
-
These tools are run in **parallel** except
|
|
51
|
+
These tools are run in **parallel** except:
|
|
52
|
+
When running '-o llm', we run sequentially and exit early at the first error.
|
|
55
53
|
|
|
56
54
|
| Order | Tool | Measures |
|
|
57
55
|
|----------|------|----------|
|
|
58
56
|
| 1 | compileall | Syntax errors |
|
|
59
|
-
| 2 |
|
|
60
|
-
| 3 |
|
|
61
|
-
| 4 |
|
|
57
|
+
| 2 | ruff | Lint / style |
|
|
58
|
+
| 3 | ty | Type errors |
|
|
59
|
+
| 4 | bandit | Security vulnerabilities |
|
|
62
60
|
| 5 | pytest | Test pass rate |
|
|
63
61
|
| 6 | coverage | Test coverage |
|
|
64
62
|
| 7 | radon cc | Cyclomatic complexity |
|
|
@@ -67,14 +65,14 @@ These tools are run in **parallel** except when looking for the first error in -
|
|
|
67
65
|
| 10 | vulture | Dead code |
|
|
68
66
|
| 11 | interrogate | Docstring coverage |
|
|
69
67
|
|
|
70
|
-
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle
|
|
68
|
+
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
|
|
71
69
|
|
|
72
70
|
|
|
73
71
|
## Usage
|
|
74
72
|
|
|
75
73
|
```bash
|
|
76
74
|
cq check . # Table overview of scores for humans
|
|
77
|
-
cq check -o llm
|
|
75
|
+
cq check . -o llm # Top defect as markdown for LLMs
|
|
78
76
|
cq check . -o score # Numeric score only for CI
|
|
79
77
|
cq check . -o json # Detailed parsed JSON output for jq
|
|
80
78
|
cq check . -o raw # Raw tool output for debug
|
|
@@ -84,6 +82,13 @@ cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
|
84
82
|
cq config path/to/project/ # Show effective tool configuration
|
|
85
83
|
```
|
|
86
84
|
|
|
85
|
+
**Exit codes:** `cq check` exits with code `1` if any tool metric falls below its `error_threshold`, making it suitable as a CI gate:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cq check . && deploy # block deploy on errors
|
|
89
|
+
cq check . -o score # print score, exit 1 on errors
|
|
90
|
+
```
|
|
91
|
+
|
|
87
92
|
## Table output
|
|
88
93
|
|
|
89
94
|
```bash
|
|
@@ -95,9 +100,9 @@ cq config path/to/project/ # Show effective tool configuration
|
|
|
95
100
|
┃ Tool ┃ Time ┃ Metric ┃ Score ┃ Status ┃
|
|
96
101
|
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
|
|
97
102
|
│ compile │ 0.42s │ compile │ 1.000 │ OK │
|
|
98
|
-
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
99
103
|
│ ruff │ 0.17s │ lint │ 1.000 │ OK │
|
|
100
104
|
│ ty │ 0.33s │ type_check │ 1.000 │ OK │
|
|
105
|
+
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
101
106
|
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
102
107
|
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
103
108
|
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
@@ -141,7 +146,7 @@ cq config path/to/project/ # Show effective tool configuration
|
|
|
141
146
|
|
|
142
147
|
## Raw output
|
|
143
148
|
```bash
|
|
144
|
-
> cq check -o raw
|
|
149
|
+
> cq check . -o raw
|
|
145
150
|
```
|
|
146
151
|
|
|
147
152
|
```json
|
|
@@ -167,66 +172,63 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
|
|
|
167
172
|
# Skip tools that are slow or not relevant to your project
|
|
168
173
|
disable = ["coverage", "interrogate"]
|
|
169
174
|
|
|
175
|
+
# Lines of source context shown around each defect in LLM output (default: 15)
|
|
176
|
+
context_lines = 15
|
|
177
|
+
|
|
170
178
|
# Override warning/error thresholds per tool
|
|
171
179
|
[tool.cq.thresholds.coverage]
|
|
172
180
|
warning = 0.9
|
|
173
181
|
error = 0.7
|
|
174
182
|
```
|
|
175
183
|
|
|
176
|
-
Tool IDs match the keys in `config/
|
|
184
|
+
Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
|
|
177
185
|
|
|
178
186
|
|
|
179
187
|
### Default config
|
|
180
188
|
|
|
181
189
|
```yaml
|
|
182
|
-
|
|
190
|
+
python:
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
name: "compile"
|
|
192
|
+
compile:
|
|
186
193
|
command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
|
|
187
194
|
parser: "CompileParser"
|
|
188
195
|
order: 1
|
|
189
196
|
warning_threshold: 0.9999
|
|
190
197
|
error_threshold: 0.9999
|
|
191
198
|
|
|
192
|
-
bandit:
|
|
193
|
-
name: "bandit"
|
|
194
|
-
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
195
|
-
parser: "BanditParser"
|
|
196
|
-
order: 2
|
|
197
|
-
warning_threshold: 0.9999
|
|
198
|
-
error_threshold: 0.8
|
|
199
|
-
|
|
200
199
|
ruff:
|
|
201
|
-
name: "ruff"
|
|
202
200
|
command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
|
|
203
201
|
parser: "RuffParser"
|
|
204
|
-
order:
|
|
202
|
+
order: 2
|
|
205
203
|
warning_threshold: 0.9999
|
|
206
204
|
error_threshold: 0.9
|
|
207
205
|
|
|
208
206
|
ty:
|
|
209
|
-
name: "ty"
|
|
210
207
|
command: "{python} -m ty check --output-format concise --color never {context_path}"
|
|
211
208
|
parser: "TyParser"
|
|
212
|
-
order:
|
|
209
|
+
order: 3
|
|
213
210
|
warning_threshold: 0.9999
|
|
214
211
|
error_threshold: 0.8
|
|
215
212
|
run_in_target_env: true
|
|
216
213
|
extra_deps:
|
|
217
214
|
- ty
|
|
218
215
|
|
|
216
|
+
bandit:
|
|
217
|
+
command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
|
|
218
|
+
parser: "BanditParser"
|
|
219
|
+
order: 4
|
|
220
|
+
warning_threshold: 0.9999
|
|
221
|
+
error_threshold: 0.8
|
|
222
|
+
|
|
219
223
|
pytest:
|
|
220
|
-
name: "pytest"
|
|
221
224
|
command: "{python} -m pytest -v {context_path}"
|
|
222
225
|
parser: "PytestParser"
|
|
223
226
|
order: 5
|
|
224
|
-
warning_threshold: 0
|
|
225
|
-
error_threshold: 0
|
|
227
|
+
warning_threshold: 1.0
|
|
228
|
+
error_threshold: 1.0
|
|
226
229
|
run_in_target_env: true
|
|
227
230
|
|
|
228
231
|
coverage:
|
|
229
|
-
name: "coverage"
|
|
230
232
|
command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
|
|
231
233
|
parser: "CoverageParser"
|
|
232
234
|
order: 6
|
|
@@ -237,24 +239,21 @@ tools:
|
|
|
237
239
|
- coverage
|
|
238
240
|
- pytest
|
|
239
241
|
|
|
240
|
-
|
|
241
|
-
name: "radon cc"
|
|
242
|
+
radon-cc:
|
|
242
243
|
command: "{python} -m radon cc --json {context_path}"
|
|
243
244
|
parser: "ComplexityParser"
|
|
244
245
|
order: 7
|
|
245
246
|
warning_threshold: 0.6
|
|
246
247
|
error_threshold: 0.4
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
name: "radon mi"
|
|
249
|
+
radon-mi:
|
|
250
250
|
command: "{python} -m radon mi -s --json {context_path}"
|
|
251
251
|
parser: "MaintainabilityParser"
|
|
252
252
|
order: 8
|
|
253
253
|
warning_threshold: 0.6
|
|
254
254
|
error_threshold: 0.4
|
|
255
255
|
|
|
256
|
-
|
|
257
|
-
name: "radon hal"
|
|
256
|
+
radon-hal:
|
|
258
257
|
command: "{python} -m radon hal -f --json {context_path}"
|
|
259
258
|
parser: "HalsteadParser"
|
|
260
259
|
order: 9
|
|
@@ -262,7 +261,6 @@ tools:
|
|
|
262
261
|
error_threshold: 0.3
|
|
263
262
|
|
|
264
263
|
vulture:
|
|
265
|
-
name: "vulture"
|
|
266
264
|
command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
|
|
267
265
|
parser: "VultureParser"
|
|
268
266
|
order: 10
|
|
@@ -270,7 +268,6 @@ tools:
|
|
|
270
268
|
error_threshold: 0.8
|
|
271
269
|
|
|
272
270
|
interrogate:
|
|
273
|
-
name: "interrogate"
|
|
274
271
|
command: "{python} -m interrogate {context_path} -v --fail-under 0"
|
|
275
272
|
parser: "InterrogateParser"
|
|
276
273
|
order: 11
|