python-code-quality 0.2.3__py3-none-any.whl → 0.3.1__py3-none-any.whl

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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-code-quality
3
- Version: 0.2.3
4
- Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
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
@@ -17,7 +17,7 @@ Requires-Dist: pytest-cov>=6.1.1
17
17
  Requires-Dist: pytest-json-report>=1.5.0
18
18
  Requires-Dist: radon>=6.0.1
19
19
  Requires-Dist: rich>=14.0.0
20
- Requires-Dist: ruff>=0.14.1
20
+ Requires-Dist: ruff>=0.14.1,<0.17
21
21
  Requires-Dist: tomlkit>=0.13.0
22
22
  Requires-Dist: ty>=0.0.17
23
23
  Requires-Dist: typer>=0.16.0
@@ -35,7 +35,7 @@ Description-Content-Type: text/markdown
35
35
  [![Python versions](https://img.shields.io/pypi/pyversions/python-code-quality?)](https://pypi.org/project/python-code-quality/)
36
36
  [![License](https://img.shields.io/github/license/rhiza-fr/py-cq)](LICENSE)
37
37
 
38
- Run 11+ code quality tools, aggregate results into one score, and surface the single most critical defect as a focused markdown prompt — ready to pipe to any LLM.
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
 
@@ -65,6 +65,8 @@ uv tool install .
65
65
 
66
66
  These tools are run in **parallel** except:
67
67
  When running '-o llm', we run sequentially and exit early at the first error.
68
+ `coverage` is a special case: it starts alongside `pytest` and is cancelled if
69
+ `pytest` fails, scoring `0%` without a second test run.
68
70
 
69
71
  | Order | Tool | Measures |
70
72
  |----------|------|----------|
@@ -74,11 +76,12 @@ When running '-o llm', we run sequentially and exit early at the first error.
74
76
  | 4 | bandit | Security vulnerabilities |
75
77
  | 5 | pytest | Test pass rate |
76
78
  | 6 | coverage | Test coverage |
77
- | 7 | radon cc | Cyclomatic complexity |
78
- | 8 | radon mi | Maintainability index |
79
- | 9 | radon hal | Halstead volume / bug estimate |
80
- | 10 | vulture | Dead code |
81
- | 11 | interrogate | Docstring coverage |
79
+ | 7 | vulture | Dead code |
80
+ | 8 | interrogate | Docstring coverage |
81
+ | 9 | radon cc | Cyclomatic complexity |
82
+ | 10 | radon mi | Maintainability index |
83
+ | 11 | radon hal | Halstead volume / bug estimate |
84
+ | 12 | ruff format | Formatting (fraction of files already formatted) |
82
85
 
83
86
  Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
84
87
 
@@ -99,7 +102,7 @@ cq check . --exclude demo # Exclude paths from all tools
99
102
  cq check . --workers 1 # Run sequentially if you like things slow
100
103
  cq check . --clear-cache # Clear cached results before running (rarely needed)
101
104
  cq check . -o llm --hint # Append "run cq again to verify" (for human workflows)
102
- cq config path/to/project/ # Show effective tool configuration
105
+ cq config --path path/to/project/ # Show effective tool configuration
103
106
  cq is-fixed <fingerprint> # Check whether a specific issue has been resolved
104
107
  ```
105
108
 
@@ -248,14 +251,15 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
248
251
  │ bandit │ 0.56s │ security │ 1.000 │ OK │
249
252
  │ pytest │ 0.91s │ tests │ 1.000 │ OK │
250
253
  │ coverage │ 1.26s │ coverage │ 0.910 │ OK │
251
- │ radon cc │ 0.32s │ simplicity │ 0.982 │ OK │
252
- │ radon mi │ 0.38s │ maintainability │ 0.869 │ OK │
253
- │ radon hal │ 0.30s │ file_bug_free │ 0.928 │ OK │
254
- │ radon hal │ │ file_smallness │ 0.851 │ OK │
255
- │ radon hal │ │ functions_bug_free │ 0.913 │ OK │
256
- │ radon hal │ │ functions_smallness │ 0.724 │ OK │
257
254
  │ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
258
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 │
259
263
  │ │ │ Score │ 0.965 │ │
260
264
  └──────────────────┴──────────┴───────────────────────────┴─────────┴──────────┘
261
265
  ```
@@ -337,106 +341,126 @@ warning = 0.9
337
341
  error = 0.7
338
342
  ```
339
343
 
340
- Tool IDs match the keys in `config/config.yaml`: `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`.
341
345
 
342
346
 
343
347
  ### Default config
344
348
 
345
- ```yaml
346
- python:
347
-
348
- compile:
349
- command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
350
- parser: "CompileParser"
351
- order: 1
352
- warning_threshold: 0.9999
353
- error_threshold: 0.9999
354
-
355
- ruff:
356
- command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
357
- exclude_format: " --exclude {path}"
358
- parser: "RuffParser"
359
- order: 2
360
- warning_threshold: 0.9999
361
- error_threshold: 0.9
362
-
363
- ty:
364
- command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
365
- exclude_format: " --exclude {path}"
366
- parser: "TyParser"
367
- order: 3
368
- warning_threshold: 0.9999
369
- error_threshold: 0.8
370
- run_in_target_env: true
371
- extra_deps:
372
- - ty
373
-
374
- bandit:
375
- command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests{exclude}\""
376
- exclude_format: ",{input_path_posix}/{path}"
377
- parser: "BanditParser"
378
- order: 4
379
- warning_threshold: 0.9999
380
- error_threshold: 0.8
381
-
382
- pytest:
383
- command: "{python} -m pytest -v \"{context_path}\"{exclude}"
384
- exclude_format: " --ignore {path}"
385
- parser: "PytestParser"
386
- order: 5
387
- warning_threshold: 1.0
388
- error_threshold: 1.0
389
- run_in_target_env: true
390
- extra_deps:
391
- - pytest
392
-
393
- coverage:
394
- command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
395
- parser: "CoverageParser"
396
- order: 6
397
- warning_threshold: 0.9
398
- error_threshold: 0.5
399
- run_in_target_env: true
400
- extra_deps:
401
- - coverage
402
- - pytest
403
-
404
- radon-cc:
405
- command: "{python} -m radon cc --json \"{context_path}\""
406
- parser: "ComplexityParser"
407
- order: 7
408
- warning_threshold: 0.6
409
- error_threshold: 0.4
410
-
411
- radon-mi:
412
- command: "{python} -m radon mi -s --json \"{context_path}\""
413
- parser: "MaintainabilityParser"
414
- order: 8
415
- warning_threshold: 0.6
416
- error_threshold: 0.4
417
-
418
- radon-hal:
419
- command: "{python} -m radon hal -f --json \"{context_path}\""
420
- parser: "HalsteadParser"
421
- order: 9
422
- warning_threshold: 0.5
423
- error_threshold: 0.3
424
-
425
- vulture:
426
- command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
427
- exclude_format: ",{path}"
428
- parser: "VultureParser"
429
- order: 10
430
- warning_threshold: 0.9999
431
- error_threshold: 0.8
432
-
433
- interrogate:
434
- command: "{python} -m interrogate \"{context_path}\"{exclude} -v --fail-under 0"
435
- exclude_format: " -e {path}"
436
- parser: "InterrogateParser"
437
- order: 11
438
- warning_threshold: 0.8
439
- error_threshold: 0.3
349
+ Defined in `src/py_cq/config/config.toml` (sections are namespaced by language):
350
+
351
+ ```toml
352
+ [python.compile]
353
+ command = "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv|\\.claude"
354
+ parser = "CompileParser"
355
+ order = 1
356
+ warning_threshold = 0.9999
357
+ error_threshold = 0.9999
358
+ cache_invariant = "ast"
359
+
360
+ [python.ruff]
361
+ command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --exclude .claude{exclude}"
362
+ exclude_format = " --exclude {path}"
363
+ parser = "RuffParser"
364
+ order = 2
365
+ warning_threshold = 0.9999
366
+ error_threshold = 0.9
367
+
368
+ [python.ty]
369
+ command = "{python} -m ty check --output-format concise --color never \"{context_path}\" --exclude .claude{exclude}"
370
+ exclude_format = " --exclude {path}"
371
+ parser = "TyParser"
372
+ order = 3
373
+ warning_threshold = 0.9999
374
+ error_threshold = 0.8
375
+ run_in_target_env = true
376
+ extra_deps = ["ty"]
377
+
378
+ [python.bandit]
379
+ command = "{python} -m bandit -r {scan_targets} -f json -q -s B101 --severity-level medium{exclude}"
380
+ scan_exclude_names = [".venv", ".claude", "tests"]
381
+ exclude_format = " --exclude {abs_native_path}"
382
+ parser = "BanditParser"
383
+ order = 4
384
+ warning_threshold = 0.9999
385
+ error_threshold = 0.8
386
+
387
+ [python.pytest]
388
+ command = "{python} -m pytest -vv \"{context_path}\" --ignore .claude{exclude}"
389
+ exclude_format = " --ignore {path}"
390
+ parser = "PytestParser"
391
+ order = 5
392
+ warning_threshold = 1.0
393
+ error_threshold = 1.0
394
+ run_in_target_env = true
395
+ extra_deps = ["pytest"]
396
+ skip_for_file = true
397
+ cache_invariant = "ast"
398
+
399
+ [python.coverage]
400
+ command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --omit=*/tests/*,*/test_*.py"
401
+ exclude_format = " --ignore {path}"
402
+ parser = "CoverageParser"
403
+ order = 6
404
+ warning_threshold = 0.9
405
+ error_threshold = 0.5
406
+ run_in_target_env = true
407
+ extra_deps = ["coverage", "pytest"]
408
+ skip_for_file = true
409
+ skip_if = "pytest"
410
+ cache_invariant = "ast"
411
+
412
+ [python.vulture]
413
+ command = "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
414
+ exclude_format = ",{path}"
415
+ parser = "VultureParser"
416
+ order = 7
417
+ warning_threshold = 0.9999
418
+ error_threshold = 0.8
419
+ cache_invariant = "ast"
420
+
421
+ [python.interrogate]
422
+ command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
423
+ exclude_format = " -e {path}"
424
+ parser = "InterrogateParser"
425
+ order = 8
426
+ warning_threshold = 0.8
427
+ error_threshold = 0.5
428
+
429
+ [python.interrogate.parser_config]
430
+ skip_empty_init = true
431
+
432
+ [python.radon-cc]
433
+ command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
434
+ parser = "ComplexityParser"
435
+ order = 9
436
+ gate_strict = false
437
+ warning_threshold = 0.6
438
+ error_threshold = 0.4
439
+
440
+ [python.radon-mi]
441
+ command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
442
+ parser = "MaintainabilityParser"
443
+ order = 10
444
+ gate_strict = false
445
+ warning_threshold = 0.6
446
+ error_threshold = 0.4
447
+
448
+ [python.radon-hal]
449
+ command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
450
+ parser = "HalsteadParser"
451
+ order = 11
452
+ gate_strict = false
453
+ warning_threshold = 0.35
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
440
464
  ```
441
465
 
442
466
  ## Respect
@@ -1,35 +1,37 @@
1
1
  py_cq/__init__.py,sha256=-C6_QzQeydxft3ndlExTgWnvlqH8fJNPJxfySXULQmU,56
2
- py_cq/api.py,sha256=AxhTQVX4ACZmgCpdfJgB2_1VXtClxc531NkRoLK1vA0,10118
3
- py_cq/cli.py,sha256=pP7lKoGE_cW9AGXEfDQMcfpb7inAtn6LJ0-cv0t5ONo,13784
2
+ py_cq/api.py,sha256=u0ajk6ZYWFGjSsLYkRfUBBtL_Vw0HHZbWpa1AM79y8Y,10172
3
+ py_cq/cli.py,sha256=zciffdqyz_TXO8xfTlItWBS8s8eFGhG7Iv0I2M9eBK4,13827
4
4
  py_cq/config/__init__.py,sha256=f0wc51O_3kGDTZUnCbGv8_zWnC5yYGl4NWcf2buSImQ,670
5
- py_cq/config/config.toml,sha256=1-S9e5WK6K19FC1_SaGI5dfePuSwCufH0CssZPfIZgE,2921
6
- py_cq/context_hash.py,sha256=qYPMacgSEwy0nYLCBW8JllNWsGfaSjVq31EFI811ZcU,3449
7
- py_cq/execution_engine.py,sha256=ap3tDMxVD9x4bq_leq7IGYZgLM-ryJRnrVFSxSy5jK0,16893
5
+ py_cq/config/config.toml,sha256=TnUzP2cKKsyfN9GbnGeveBnqKTQl_PEYggOrpqBPg3s,3357
6
+ py_cq/context_hash.py,sha256=uKMkN394v6QYMukZj9usbxnq8px3i1khyIRPKwe09DE,5614
7
+ py_cq/execution_engine.py,sha256=nuW-N8X1ZmtxzkohFxxYvVPBrBIR2TPExMsPQCTAZ6A,20865
8
8
  py_cq/language_detector.py,sha256=rh5bAA3LyCQR3RODmx_EalqRNYvSYK2SJzh2hIkxOUo,1139
9
9
  py_cq/llm_formatter.py,sha256=nCV3nFdxJAi2CnH9FKb5tB-0w-yjLWmjywQI5GJ2cSU,8412
10
- py_cq/localtypes.py,sha256=kHldvTJlwyoWg0xKbOL-jplLqwKnGmv6aRh95iq8r8Q,7783
10
+ py_cq/localtypes.py,sha256=_g6RpV-JS6T0lD_Uqq4iA5e2Fw6RM56Lqrv_GiP4gm8,7907
11
11
  py_cq/main.py,sha256=5dgr8Zk1LNK5M-0wncYVksBp0qE-jWZYqmOXfb1eOhk,400
12
12
  py_cq/metric_aggregator.py,sha256=M2ymo62S7p7qPUqjjoiPg4IVyXQhLMuTr9-jxLiFjCY,853
13
13
  py_cq/parsers/__init__.py,sha256=uCy7R0cnvDPNvB5aWMLgM3FB9nDcu1jwUmznFMRROSY,28
14
14
  py_cq/parsers/banditparser.py,sha256=YqpWljU5D4IhLdtzat7XVGBm9NXrbOQbr9LI9NKybK8,3298
15
- py_cq/parsers/common.py,sha256=csIv0M7Xq68HjXn37OAD2RfMmoQWaChQ9Jf9lamMkvg,13890
15
+ py_cq/parsers/common.py,sha256=ViYneYOG4RTlbHbSU4GN9kB_M1gikukiwQ81CTajAqA,13796
16
16
  py_cq/parsers/compileparser.py,sha256=jl0DloyXTKqVHRIUft-t5lvJOndY-PdLPrXB38v9zuA,6927
17
17
  py_cq/parsers/complexityparser.py,sha256=mX1WHPRgBBc6kOqHfubowRbYXY0E1k4JVwr908Cj10U,5582
18
- py_cq/parsers/coverageparser.py,sha256=9HtXyAMcQbhsUTb1VNClIHQajBwqCt26H-25t2ZQnY4,8092
18
+ py_cq/parsers/coverageparser.py,sha256=QGjZ8JJF0G-0uiZ07BzxSnblGiysHtOwq0SYvWrtOhA,7441
19
19
  py_cq/parsers/exitcodeparser.py,sha256=b4LWIwwklPwCX3MnqNsGQC1slQU56DKE05vJ0OdWVoA,957
20
20
  py_cq/parsers/halsteadparser.py,sha256=789c_WWfomskM4i7SUEbKvB67-F2y5nfqt48aFXOusY,10192
21
- py_cq/parsers/interrogateparser.py,sha256=cc5Q4mOQlLUIPl3XUyH29QJ_JMBzAA7SiPAAiNGDJdM,11546
21
+ py_cq/parsers/interrogateparser.py,sha256=KDqXTdnJ5JzWhN4ziRda90nZCQOJaKn3CCiq4Rh0IRw,11314
22
22
  py_cq/parsers/linecountparser.py,sha256=-hqebwG4j7_QugzGLUUQRcPukGHicV2nbIp1pR8ZcAw,1310
23
23
  py_cq/parsers/maintainabilityparser.py,sha256=3a4m93XNtUurw3IxEI1Iae9idgJY6qiEt7HMYl1wTu0,4793
24
- py_cq/parsers/pytestparser.py,sha256=rhzFzsBEXv1xloOuEjQyBn8Vp0AyGoq2t5siyAwWWaA,11549
24
+ py_cq/parsers/pytestparser.py,sha256=cIaTs2XxgeJlVragLEVTZcrbB-bzfNYYMdUrakRa-rQ,11545
25
25
  py_cq/parsers/regexcountparser.py,sha256=zGzHEYDIori6aiWnVkRFixdE7fWt66npAAWjzvpoBF4,1592
26
+ py_cq/parsers/ruffformatparser.py,sha256=UXff5Qwh9i0goiSTJ_AaaIsmMSssUjNY2O_dERXjsFw,2636
26
27
  py_cq/parsers/ruffparser.py,sha256=uE0QEaZ4Ch7IsWY_2ZkfHXPU0TYPGsGPfv66hGe5G34,7634
27
28
  py_cq/parsers/typarser.py,sha256=x835OrapG1aUteW2NhC5U0bhj1w1pMtbfmbvb74-1AI,8077
28
29
  py_cq/parsers/vultureparser.py,sha256=-yGAXzRY4_fzLRTk7dfwDrrRZ-n97j1Ph5_xo3c6TfU,2229
29
30
  py_cq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- py_cq/table_formatter.py,sha256=2_WZ2RHCwsdna0bzP6ZFxB2YtHtW95wxJB1yBXgCIDo,1610
31
- py_cq/tool_registry.py,sha256=8B6EG7mGCKR6JeHtesFHa79svNfj1PUbboysXfwV7vY,1828
32
- python_code_quality-0.2.3.dist-info/WHEEL,sha256=wXwAVsgVaOZ_pwDFqQm5Rd6PID-Fc74nkLc8X8gHiDo,81
33
- python_code_quality-0.2.3.dist-info/entry_points.txt,sha256=cfWbTw7eYO6Trv1-Z_odL6Zta9CqYU6Vk9lAHdfB60Q,40
34
- python_code_quality-0.2.3.dist-info/METADATA,sha256=dBNJKPiG5BRMtuPT8jUcYMoasGRsP4uQQG9RvFy_xig,15650
35
- python_code_quality-0.2.3.dist-info/RECORD,,
31
+ py_cq/source_file.py,sha256=Uyv-CSVeliFnE6TpRB4Y15ykFQs4n0r9-amzxXXlqN0,7562
32
+ py_cq/table_formatter.py,sha256=-22nzVIOaDe3QOBmZ2iBPH7ewIEIPEn5WYSjCoB4Hj4,1608
33
+ py_cq/tool_registry.py,sha256=0s0sD9_iQW-HU0SIKSS8CFDlrzW0c-vlYrAFODEHiSc,1894
34
+ python_code_quality-0.3.1.dist-info/WHEEL,sha256=xTCBFIZIIyRSxyowOv5Wqg_jFU6KsdIffuulBB09Ph8,80
35
+ python_code_quality-0.3.1.dist-info/entry_points.txt,sha256=cfWbTw7eYO6Trv1-Z_odL6Zta9CqYU6Vk9lAHdfB60Q,40
36
+ python_code_quality-0.3.1.dist-info/METADATA,sha256=E3HJ8qZESv7gyGqBQrSiQeeyDTyFYe9v7cm6Kt8x8_8,16739
37
+ python_code_quality-0.3.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.11.19
2
+ Generator: uv 0.13.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any