python-code-quality 0.3.0__tar.gz → 0.3.1__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.3.0 → python_code_quality-0.3.1}/PKG-INFO +22 -11
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/README.md +20 -9
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/pyproject.toml +2 -2
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/pyproject.toml.orig +2 -2
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/api.py +9 -3
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/cli.py +3 -3
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/config/config.toml +9 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/execution_engine.py +26 -8
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/localtypes.py +4 -4
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/common.py +1 -3
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/interrogateparser.py +11 -3
- python_code_quality-0.3.1/src/py_cq/parsers/ruffformatparser.py +62 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/__init__.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/config/__init__.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/context_hash.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/language_detector.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/llm_formatter.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/main.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/metric_aggregator.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/__init__.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/banditparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/compileparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/complexityparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/coverageparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/exitcodeparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/halsteadparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/linecountparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/maintainabilityparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/pytestparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/regexcountparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/ruffparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/typarser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/vultureparser.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/py.typed +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/source_file.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/table_formatter.py +0 -0
- {python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/tool_registry.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-code-quality
|
|
3
|
-
Version: 0.3.
|
|
4
|
-
Summary: Python Code Quality Analysis Tool - feed the results from
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Python Code Quality Analysis Tool - feed the results from 12 CQ tools straight into an LLM. Minimal tokens.
|
|
5
5
|
Author: Chris Kilner
|
|
6
6
|
Author-email: Chris Kilner <chris@rhiza.fr>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
[](https://pypi.org/project/python-code-quality/)
|
|
36
36
|
[](LICENSE)
|
|
37
37
|
|
|
38
|
-
Run
|
|
38
|
+
Run 12 code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
|
|
39
39
|
|
|
40
40
|
This can dramatically reduce the amount of noise for LLMs (and humans) and remove the need for them to know about these tools.
|
|
41
41
|
|
|
@@ -81,6 +81,7 @@ When running '-o llm', we run sequentially and exit early at the first error.
|
|
|
81
81
|
| 9 | radon cc | Cyclomatic complexity |
|
|
82
82
|
| 10 | radon mi | Maintainability index |
|
|
83
83
|
| 11 | radon hal | Halstead volume / bug estimate |
|
|
84
|
+
| 12 | ruff format | Formatting (fraction of files already formatted) |
|
|
84
85
|
|
|
85
86
|
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
|
|
86
87
|
|
|
@@ -101,7 +102,7 @@ cq check . --exclude demo # Exclude paths from all tools
|
|
|
101
102
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
102
103
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
103
104
|
cq check . -o llm --hint # Append "run cq again to verify" (for human workflows)
|
|
104
|
-
cq config path/to/project/ # Show effective tool configuration
|
|
105
|
+
cq config --path path/to/project/ # Show effective tool configuration
|
|
105
106
|
cq is-fixed <fingerprint> # Check whether a specific issue has been resolved
|
|
106
107
|
```
|
|
107
108
|
|
|
@@ -250,14 +251,15 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
250
251
|
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
251
252
|
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
252
253
|
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
253
|
-
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
254
|
-
│ radon mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
255
|
-
│ radon hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
256
|
-
│ radon hal │ │ file_smallness │ 0.851 │ OK │
|
|
257
|
-
│ radon hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
258
|
-
│ radon hal │ │ functions_smallness │ 0.724 │ OK │
|
|
259
254
|
│ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
|
|
260
255
|
│ interrogate │ 0.36s │ doc_coverage │ 1.000 │ OK │
|
|
256
|
+
│ radon-cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
257
|
+
│ radon-mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
258
|
+
│ radon-hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
259
|
+
│ radon-hal │ │ file_smallness │ 0.851 │ OK │
|
|
260
|
+
│ radon-hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
261
|
+
│ radon-hal │ │ functions_smallness │ 0.724 │ OK │
|
|
262
|
+
│ ruff-format │ 0.12s │ formatted │ 1.000 │ OK │
|
|
261
263
|
│ │ │ Score │ 0.965 │ │
|
|
262
264
|
└──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
|
|
263
265
|
```
|
|
@@ -339,7 +341,7 @@ warning = 0.9
|
|
|
339
341
|
error = 0.7
|
|
340
342
|
```
|
|
341
343
|
|
|
342
|
-
Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
|
|
344
|
+
Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`, `ruff-format`.
|
|
343
345
|
|
|
344
346
|
|
|
345
347
|
### Default config
|
|
@@ -450,6 +452,15 @@ order = 11
|
|
|
450
452
|
gate_strict = false
|
|
451
453
|
warning_threshold = 0.35
|
|
452
454
|
error_threshold = 0.3
|
|
455
|
+
|
|
456
|
+
[python.ruff-format]
|
|
457
|
+
command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
|
|
458
|
+
exclude_format = " --exclude {path}"
|
|
459
|
+
parser = "RuffFormatParser"
|
|
460
|
+
order = 12
|
|
461
|
+
gate_strict = false
|
|
462
|
+
warning_threshold = 0.9999
|
|
463
|
+
error_threshold = 0.5
|
|
453
464
|
```
|
|
454
465
|
|
|
455
466
|
## Respect
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://pypi.org/project/python-code-quality/)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
Run
|
|
9
|
+
Run 12 code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
|
|
10
10
|
|
|
11
11
|
This can dramatically reduce the amount of noise for LLMs (and humans) and remove the need for them to know about these tools.
|
|
12
12
|
|
|
@@ -52,6 +52,7 @@ When running '-o llm', we run sequentially and exit early at the first error.
|
|
|
52
52
|
| 9 | radon cc | Cyclomatic complexity |
|
|
53
53
|
| 10 | radon mi | Maintainability index |
|
|
54
54
|
| 11 | radon hal | Halstead volume / bug estimate |
|
|
55
|
+
| 12 | ruff format | Formatting (fraction of files already formatted) |
|
|
55
56
|
|
|
56
57
|
Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
|
|
57
58
|
|
|
@@ -72,7 +73,7 @@ cq check . --exclude demo # Exclude paths from all tools
|
|
|
72
73
|
cq check . --workers 1 # Run sequentially if you like things slow
|
|
73
74
|
cq check . --clear-cache # Clear cached results before running (rarely needed)
|
|
74
75
|
cq check . -o llm --hint # Append "run cq again to verify" (for human workflows)
|
|
75
|
-
cq config path/to/project/ # Show effective tool configuration
|
|
76
|
+
cq config --path path/to/project/ # Show effective tool configuration
|
|
76
77
|
cq is-fixed <fingerprint> # Check whether a specific issue has been resolved
|
|
77
78
|
```
|
|
78
79
|
|
|
@@ -221,14 +222,15 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
|
|
|
221
222
|
│ bandit │ 0.56s │ security │ 1.000 │ OK │
|
|
222
223
|
│ pytest │ 0.91s │ tests │ 1.000 │ OK │
|
|
223
224
|
│ coverage │ 1.26s │ coverage │ 0.910 │ OK │
|
|
224
|
-
│ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
225
|
-
│ radon mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
226
|
-
│ radon hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
227
|
-
│ radon hal │ │ file_smallness │ 0.851 │ OK │
|
|
228
|
-
│ radon hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
229
|
-
│ radon hal │ │ functions_smallness │ 0.724 │ OK │
|
|
230
225
|
│ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
|
|
231
226
|
│ interrogate │ 0.36s │ doc_coverage │ 1.000 │ OK │
|
|
227
|
+
│ radon-cc │ 0.32s │ simplicity │ 0.982 │ OK │
|
|
228
|
+
│ radon-mi │ 0.38s │ maintainability │ 0.869 │ OK │
|
|
229
|
+
│ radon-hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
|
|
230
|
+
│ radon-hal │ │ file_smallness │ 0.851 │ OK │
|
|
231
|
+
│ radon-hal │ │ functions_bug_free │ 0.913 │ OK │
|
|
232
|
+
│ radon-hal │ │ functions_smallness │ 0.724 │ OK │
|
|
233
|
+
│ ruff-format │ 0.12s │ formatted │ 1.000 │ OK │
|
|
232
234
|
│ │ │ Score │ 0.965 │ │
|
|
233
235
|
└──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
|
|
234
236
|
```
|
|
@@ -310,7 +312,7 @@ warning = 0.9
|
|
|
310
312
|
error = 0.7
|
|
311
313
|
```
|
|
312
314
|
|
|
313
|
-
Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`.
|
|
315
|
+
Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`, `ruff-format`.
|
|
314
316
|
|
|
315
317
|
|
|
316
318
|
### Default config
|
|
@@ -421,6 +423,15 @@ order = 11
|
|
|
421
423
|
gate_strict = false
|
|
422
424
|
warning_threshold = 0.35
|
|
423
425
|
error_threshold = 0.3
|
|
426
|
+
|
|
427
|
+
[python.ruff-format]
|
|
428
|
+
command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
|
|
429
|
+
exclude_format = " --exclude {path}"
|
|
430
|
+
parser = "RuffFormatParser"
|
|
431
|
+
order = 12
|
|
432
|
+
gate_strict = false
|
|
433
|
+
warning_threshold = 0.9999
|
|
434
|
+
error_threshold = 0.5
|
|
424
435
|
```
|
|
425
436
|
|
|
426
437
|
## Respect
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-code-quality"
|
|
3
|
-
version = "0.3.
|
|
4
|
-
description = "Python Code Quality Analysis Tool - feed the results from
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "Python Code Quality Analysis Tool - feed the results from 12 CQ tools straight into an LLM. Minimal tokens."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = "MIT"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "python-code-quality"
|
|
3
|
-
version = "0.3.
|
|
4
|
-
description = "Python Code Quality Analysis Tool - feed the results from
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "Python Code Quality Analysis Tool - feed the results from 12 CQ tools straight into an LLM. Minimal tokens."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
7
7
|
license = "MIT"
|
|
@@ -148,7 +148,9 @@ class CQ:
|
|
|
148
148
|
if clear_cache:
|
|
149
149
|
_cache.clear()
|
|
150
150
|
|
|
151
|
-
def raw(
|
|
151
|
+
def raw(
|
|
152
|
+
self, *, early_exit: bool = False, order: str = "severity"
|
|
153
|
+
) -> list[ToolResult]:
|
|
152
154
|
"""Run all tools and return parsed results before aggregation."""
|
|
153
155
|
return run_tools(
|
|
154
156
|
self._registry.values(),
|
|
@@ -218,7 +220,7 @@ class CQ:
|
|
|
218
220
|
)
|
|
219
221
|
if fp.tool not in tool_registry:
|
|
220
222
|
raise ValueError(f"Unknown tool: {fp.tool!r}")
|
|
221
|
-
|
|
223
|
+
|
|
222
224
|
if fp.code and fp.path:
|
|
223
225
|
# Specific issue: target only the affected file for a fast re-check
|
|
224
226
|
file_path = Path(fp.path)
|
|
@@ -242,7 +244,11 @@ class CQ:
|
|
|
242
244
|
|
|
243
245
|
only_registry = {fp.tool: tool_registry[fp.tool]}
|
|
244
246
|
tool_results = run_tools(
|
|
245
|
-
only_registry.values(),
|
|
247
|
+
only_registry.values(),
|
|
248
|
+
target,
|
|
249
|
+
max_workers=1,
|
|
250
|
+
early_exit=False,
|
|
251
|
+
excludes=[],
|
|
246
252
|
project_root=str(self.path),
|
|
247
253
|
)
|
|
248
254
|
|
|
@@ -107,7 +107,7 @@ def callback(
|
|
|
107
107
|
help="Show version and dependencies",
|
|
108
108
|
),
|
|
109
109
|
) -> None:
|
|
110
|
-
"""Feed the results from
|
|
110
|
+
"""Feed the results from 12 code quality tools to an LLM. Try: cq check . -o llm"""
|
|
111
111
|
import sys
|
|
112
112
|
|
|
113
113
|
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
@@ -124,7 +124,7 @@ def check(
|
|
|
124
124
|
OutputMode.TABLE,
|
|
125
125
|
"--output",
|
|
126
126
|
"-o",
|
|
127
|
-
help="Output mode: table (default), score, json, llm",
|
|
127
|
+
help="Output mode: table (default), score, json, raw, llm, llm-json",
|
|
128
128
|
),
|
|
129
129
|
log_level: str = typer.Option(
|
|
130
130
|
"CRITICAL",
|
|
@@ -172,7 +172,7 @@ def check(
|
|
|
172
172
|
help="Issue selection order for -o llm/llm-json: severity (most severe first) or phase (first non-clean phase, in dependency order)",
|
|
173
173
|
),
|
|
174
174
|
):
|
|
175
|
-
"""Feed the results from
|
|
175
|
+
"""Feed the results from 12 code quality tools to an LLM. Try: cq check . -o llm""" # --help
|
|
176
176
|
path_obj = Path(path)
|
|
177
177
|
if not path_obj.exists():
|
|
178
178
|
raise typer.BadParameter(f"Path does not exist: {path}")
|
|
@@ -101,3 +101,12 @@ order = 11
|
|
|
101
101
|
gate_strict = false
|
|
102
102
|
warning_threshold = 0.35
|
|
103
103
|
error_threshold = 0.3
|
|
104
|
+
|
|
105
|
+
[python.ruff-format]
|
|
106
|
+
command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
|
|
107
|
+
exclude_format = " --exclude {path}"
|
|
108
|
+
parser = "RuffFormatParser"
|
|
109
|
+
order = 12
|
|
110
|
+
gate_strict = false
|
|
111
|
+
warning_threshold = 0.9999
|
|
112
|
+
error_threshold = 0.5
|
|
@@ -328,7 +328,9 @@ def run_tool(
|
|
|
328
328
|
timestamp=timestamp,
|
|
329
329
|
project_path=project_dir,
|
|
330
330
|
)
|
|
331
|
-
_cache.set(
|
|
331
|
+
_cache.set(
|
|
332
|
+
cache_key, raw_result.to_dict(), expire=5 * 24 * 60 * 60, tag=project_tag
|
|
333
|
+
)
|
|
332
334
|
return raw_result
|
|
333
335
|
|
|
334
336
|
|
|
@@ -386,7 +388,9 @@ def run_tools(
|
|
|
386
388
|
t_hash0 = time.perf_counter()
|
|
387
389
|
root = project_root or str(Path(path).resolve())
|
|
388
390
|
shared_hash = get_context_hash(root)
|
|
389
|
-
log.debug(
|
|
391
|
+
log.debug(
|
|
392
|
+
f"context_hash: {(time.perf_counter() - t_hash0) * 1000:.1f}ms {shared_hash}"
|
|
393
|
+
)
|
|
390
394
|
|
|
391
395
|
# Cache keys are content-based, so a changed project naturally produces new
|
|
392
396
|
# keys; stale entries age out via TTL + size_limit (no blanket eviction).
|
|
@@ -398,7 +402,9 @@ def run_tools(
|
|
|
398
402
|
if any(tc.cache_invariant == "ast" for tc in tool_configs):
|
|
399
403
|
t_norm0 = time.perf_counter()
|
|
400
404
|
norm_hash = get_context_hash(root, normalize=True)
|
|
401
|
-
log.debug(
|
|
405
|
+
log.debug(
|
|
406
|
+
f"context_hash(ast): {(time.perf_counter() - t_norm0) * 1000:.1f}ms {norm_hash}"
|
|
407
|
+
)
|
|
402
408
|
|
|
403
409
|
def _hash_for(tool_config: ToolConfig) -> str:
|
|
404
410
|
return norm_hash if tool_config.cache_invariant == "ast" else shared_hash
|
|
@@ -410,7 +416,14 @@ def run_tools(
|
|
|
410
416
|
# Only pass cancel_event when present so patched run_tool stubs in tests
|
|
411
417
|
# (which don't declare the kwarg) keep working for non-cancellable tools.
|
|
412
418
|
extra = {"cancel_event": cancel_event} if cancel_event is not None else {}
|
|
413
|
-
raw_result = run_tool(
|
|
419
|
+
raw_result = run_tool(
|
|
420
|
+
tool_config,
|
|
421
|
+
path,
|
|
422
|
+
excludes,
|
|
423
|
+
precomputed_hash=_hash_for(tool_config),
|
|
424
|
+
project_tag=root,
|
|
425
|
+
**extra,
|
|
426
|
+
)
|
|
414
427
|
tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
|
|
415
428
|
tr.duration_s = time.perf_counter() - t0
|
|
416
429
|
return tool_config.order, tr
|
|
@@ -423,7 +436,9 @@ def run_tools(
|
|
|
423
436
|
try:
|
|
424
437
|
prioritized.append(_run_and_parse(tool_config))
|
|
425
438
|
except Exception as exc: # noqa: BLE001 - a crashing tool must not abort the run
|
|
426
|
-
log.error(
|
|
439
|
+
log.error(
|
|
440
|
+
f"{tool_config.name} generated an exception: {exc} {exc.__traceback__}"
|
|
441
|
+
)
|
|
427
442
|
n_skipped = n_total - i - 1
|
|
428
443
|
if n_skipped:
|
|
429
444
|
remaining = ", ".join(tc.name for tc in sorted_configs[i + 1 :])
|
|
@@ -477,7 +492,10 @@ def run_tools(
|
|
|
477
492
|
_, dep_tr = dep_future.result()
|
|
478
493
|
except Exception: # noqa: BLE001 - a crashed dep cancels nothing
|
|
479
494
|
return
|
|
480
|
-
if
|
|
495
|
+
if (
|
|
496
|
+
dep_tr.metrics
|
|
497
|
+
and min(dep_tr.metrics.values()) < dep_config.error_threshold
|
|
498
|
+
):
|
|
481
499
|
log.debug(f"{name}: cancelling because {dep_config.name} failed")
|
|
482
500
|
ev.set()
|
|
483
501
|
|
|
@@ -485,8 +503,8 @@ def run_tools(
|
|
|
485
503
|
if tc.name in cancel_events:
|
|
486
504
|
dep_config = configs_by_name[tc.skip_if]
|
|
487
505
|
name_to_future[tc.skip_if].add_done_callback(
|
|
488
|
-
lambda fut, ev=cancel_events[tc.name], dc=dep_config, nm=tc.name:
|
|
489
|
-
fut, ev, dc, nm
|
|
506
|
+
lambda fut, ev=cancel_events[tc.name], dc=dep_config, nm=tc.name: (
|
|
507
|
+
_cancel_on_dep_failure(fut, ev, dc, nm)
|
|
490
508
|
)
|
|
491
509
|
)
|
|
492
510
|
for future in as_completed(future_to_tool):
|
|
@@ -60,11 +60,11 @@ class ToolConfig:
|
|
|
60
60
|
default_factory=list
|
|
61
61
|
) # Top-level dir/file names to omit from {scan_targets}
|
|
62
62
|
skip_for_file: bool = False # If True, skip when context_path is a single file
|
|
63
|
-
gate_strict: bool =
|
|
64
|
-
|
|
65
|
-
cache_invariant: str = (
|
|
66
|
-
"" # "ast": key the cache on the docstring-stripped AST, ignoring docstrings/comments/format
|
|
63
|
+
gate_strict: bool = (
|
|
64
|
+
True # If False, small regressions above warning_threshold are accepted
|
|
67
65
|
)
|
|
66
|
+
skip_if: str = "" # Tool name; if that tool's score is below its error_threshold, skip this tool
|
|
67
|
+
cache_invariant: str = "" # "ast": key the cache on the docstring-stripped AST, ignoring docstrings/comments/format
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
@dataclass
|
|
@@ -47,9 +47,7 @@ def format_source_context(
|
|
|
47
47
|
error_offset = line - context_start # 0-based index of the error line in raw_lines
|
|
48
48
|
collected = []
|
|
49
49
|
for i, rline in enumerate(raw_lines):
|
|
50
|
-
if i > error_offset and (
|
|
51
|
-
rline.startswith(("def ", "async def ", "class "))
|
|
52
|
-
):
|
|
50
|
+
if i > error_offset and (rline.startswith(("def ", "async def ", "class "))):
|
|
53
51
|
break
|
|
54
52
|
collected.append(f"{context_start + i}: {rline}")
|
|
55
53
|
src = "\n".join(collected)
|
{python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/interrogateparser.py
RENAMED
|
@@ -32,7 +32,10 @@ def _format_missing_docstring(file_str: str, line: int, code: str, message: str)
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
if code == "D100":
|
|
35
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
base
|
|
37
|
+
+ "\n\nInsert a module-level docstring as the very first statement in the file."
|
|
38
|
+
)
|
|
36
39
|
|
|
37
40
|
try:
|
|
38
41
|
tree = get_source(file_str).tree
|
|
@@ -74,6 +77,7 @@ def _format_missing_docstring(file_str: str, line: int, code: str, message: str)
|
|
|
74
77
|
|
|
75
78
|
return base + "\n\nInsert a docstring as the first statement in the body."
|
|
76
79
|
|
|
80
|
+
|
|
77
81
|
_ROW_RE = re.compile(
|
|
78
82
|
r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+\d+\s+\|\s+(\d+(?:\.\d+)?)%\s*\|"
|
|
79
83
|
)
|
|
@@ -85,7 +89,9 @@ def _load_interrogate_cfg(context_path: str) -> dict:
|
|
|
85
89
|
"""Read [tool.interrogate] from the nearest pyproject.toml."""
|
|
86
90
|
p = Path(context_path).resolve()
|
|
87
91
|
for candidate in [p, *p.parents]:
|
|
88
|
-
pyproject = (
|
|
92
|
+
pyproject = (
|
|
93
|
+
candidate if candidate.is_dir() else candidate.parent
|
|
94
|
+
) / "pyproject.toml"
|
|
89
95
|
if pyproject.exists():
|
|
90
96
|
try:
|
|
91
97
|
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
@@ -109,7 +115,9 @@ def _skip_node(name: str, cfg: dict) -> bool:
|
|
|
109
115
|
return bool(is_semiprivate and cfg.get("ignore-semiprivate"))
|
|
110
116
|
|
|
111
117
|
|
|
112
|
-
def _missing_docstrings(
|
|
118
|
+
def _missing_docstrings(
|
|
119
|
+
file_path: Path, cfg: dict | None = None
|
|
120
|
+
) -> list[tuple[int, str, str]]:
|
|
113
121
|
"""Return (line, kind, source_line) for each node missing a docstring.
|
|
114
122
|
|
|
115
123
|
kind is 'module', 'def', or 'class'. source_line is the verbatim text of
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Parses output from ruff format --check into a standardized ToolResult.
|
|
2
|
+
|
|
3
|
+
The output is one line per unformatted file. ruff >= 0.16 honours
|
|
4
|
+
``--output-format concise``::
|
|
5
|
+
|
|
6
|
+
<file>:<line>:<col>: unformatted: File would be reformatted
|
|
7
|
+
|
|
8
|
+
while older versions ignore it (it needs preview mode) and print::
|
|
9
|
+
|
|
10
|
+
Would reformat: <file>
|
|
11
|
+
|
|
12
|
+
Either is followed by a summary line such as
|
|
13
|
+
``2 files would be reformatted, 30 files already formatted`` or
|
|
14
|
+
``32 files already formatted``.
|
|
15
|
+
|
|
16
|
+
The score is the fraction of checked files that are already formatted,
|
|
17
|
+
taken from the summary so it stays correct if the per-file format changes."""
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
from py_cq.localtypes import AbstractParser, RawResult, ToolResult
|
|
22
|
+
from py_cq.parsers.common import extract_first_issue
|
|
23
|
+
|
|
24
|
+
_FILE_RE = re.compile(r"^(?:Would reformat: (.+)|(.+):\d+:\d+: unformatted: .*)$")
|
|
25
|
+
_FORMATTED_RE = re.compile(r"(\d+) files? already formatted")
|
|
26
|
+
_UNFORMATTED_RE = re.compile(r"(\d+) files? would be reformatted")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RuffFormatParser(AbstractParser):
|
|
30
|
+
"""Parses ``ruff format --check --output-format concise`` output into a ToolResult."""
|
|
31
|
+
|
|
32
|
+
def parse(self, raw_result: RawResult) -> ToolResult:
|
|
33
|
+
"""Collect unformatted files and score the fraction already formatted."""
|
|
34
|
+
stdout = raw_result.stdout or ""
|
|
35
|
+
# One issue per file, with no line: the line ruff reports is the first
|
|
36
|
+
# diff hunk, which would make the fingerprint unstable across edits.
|
|
37
|
+
files: dict[str, list] = {}
|
|
38
|
+
for line in stdout.splitlines():
|
|
39
|
+
m = _FILE_RE.match(line)
|
|
40
|
+
if m:
|
|
41
|
+
files[(m.group(1) or m.group(2)).replace("\\", "/")] = [{}]
|
|
42
|
+
m = _FORMATTED_RE.search(stdout)
|
|
43
|
+
formatted = int(m.group(1)) if m else 0
|
|
44
|
+
m = _UNFORMATTED_RE.search(stdout)
|
|
45
|
+
unformatted = max(int(m.group(1)) if m else 0, len(files))
|
|
46
|
+
total = formatted + unformatted
|
|
47
|
+
score = formatted / total if total else 1.0
|
|
48
|
+
return ToolResult(raw=raw_result, metrics={"formatted": score}, details=files)
|
|
49
|
+
|
|
50
|
+
def format_llm_message(
|
|
51
|
+
self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Ask for the formatter to be run rather than a hand edit."""
|
|
54
|
+
result = extract_first_issue(tr.details)
|
|
55
|
+
if result is None:
|
|
56
|
+
return "ruff format reported issues (no details available)"
|
|
57
|
+
file, _ = result
|
|
58
|
+
return (
|
|
59
|
+
f"{file} - **not formatted**. Run the formatter, do not edit by hand:\n\n"
|
|
60
|
+
f"```bash\nruff format {file}\n```\n\n"
|
|
61
|
+
"Other files may also need formatting; `ruff format .` fixes them all."
|
|
62
|
+
)
|
|
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.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/complexityparser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/linecountparser.py
RENAMED
|
File without changes
|
{python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/maintainabilityparser.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_code_quality-0.3.0 → python_code_quality-0.3.1}/src/py_cq/parsers/regexcountparser.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
|