python-code-quality 0.3.1__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/PKG-INFO +53 -32
  2. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/README.md +51 -30
  3. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/pyproject.toml +10 -2
  4. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/pyproject.toml.orig +6 -2
  5. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/cli.py +2 -2
  6. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/config/config.toml +31 -19
  7. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/execution_engine.py +8 -0
  8. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/localtypes.py +0 -3
  9. python_code_quality-0.4.0/src/py_cq/parsers/kingfisherparser.py +73 -0
  10. python_code_quality-0.4.0/src/py_cq/parsers/uvauditparser.py +88 -0
  11. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/vultureparser.py +18 -3
  12. python_code_quality-0.4.0/src/py_cq/runners/pytest_serial.py +27 -0
  13. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/tool_registry.py +0 -1
  14. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/__init__.py +0 -0
  15. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/api.py +0 -0
  16. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/config/__init__.py +0 -0
  17. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/context_hash.py +0 -0
  18. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/language_detector.py +0 -0
  19. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/llm_formatter.py +0 -0
  20. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/main.py +0 -0
  21. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/metric_aggregator.py +0 -0
  22. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/__init__.py +0 -0
  23. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/banditparser.py +0 -0
  24. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/common.py +0 -0
  25. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/compileparser.py +0 -0
  26. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/complexityparser.py +0 -0
  27. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/coverageparser.py +0 -0
  28. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/exitcodeparser.py +0 -0
  29. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/halsteadparser.py +0 -0
  30. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/interrogateparser.py +0 -0
  31. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/linecountparser.py +0 -0
  32. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/maintainabilityparser.py +0 -0
  33. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/pytestparser.py +0 -0
  34. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/regexcountparser.py +0 -0
  35. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/ruffformatparser.py +0 -0
  36. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/ruffparser.py +0 -0
  37. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/parsers/typarser.py +0 -0
  38. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/py.typed +0 -0
  39. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/source_file.py +0 -0
  40. {python_code_quality-0.3.1 → python_code_quality-0.4.0}/src/py_cq/table_formatter.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-code-quality
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.
3
+ Version: 0.4.0
4
+ Summary: Python Code Quality Analysis Tool - feed the results from 14 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
  [![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 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.
38
+ Run 14 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
 
@@ -74,16 +74,24 @@ When running '-o llm', we run sequentially and exit early at the first error.
74
74
  | 2 | ruff | Lint / style |
75
75
  | 3 | ty | Type errors |
76
76
  | 4 | bandit | Security vulnerabilities |
77
- | 5 | pytest | Test pass rate |
78
- | 6 | coverage | Test 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) |
77
+ | 5 | kingfisher | Hardcoded secrets (API keys, tokens, private keys) |
78
+ | 6 | uv audit | Known vulnerabilities in locked dependencies |
79
+ | 7 | pytest | Test pass rate |
80
+ | 8 | coverage | Test coverage |
81
+ | 9 | vulture | Dead code |
82
+ | 10 | interrogate | Docstring coverage |
83
+ | 11 | radon cc | Cyclomatic complexity |
84
+ | 12 | radon mi | Maintainability index |
85
+ | 13 | radon hal | Halstead volume / bug estimate |
86
+ | 14 | ruff format | Formatting (fraction of files already formatted) |
85
87
 
86
- Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
88
+ `kingfisher` scans the working tree for hardcoded secrets with [Kingfisher](https://github.com/mongodb/kingfisher) (502 rules, Rust). It is not a Python dependency of cq: install it once with `uv tool install kingfisher-bin` (about 41 MB). When it is not on the `PATH` the tool is skipped with a warning. cq runs it offline (`--no-validate --no-update-check`) and with `--redact`, so secrets are never sent to provider APIs and never stored in the cache; the LLM message gives the file, line and rule but no source excerpt. Silence a test fixture or false positive with a `kingfisher:ignore` comment on that line.
89
+
90
+ `uv audit` checks the versions pinned in `uv.lock` against the OSV advisory database. It needs `uv` on the `PATH`, a `uv.lock` and network access; when any is missing the tool is skipped with a warning instead of failing. Only advisories that have a fix version are reported, since those are the ones an upgrade resolves. Results are cached like every other tool, so an advisory published after a run shows up when the project changes or the cache entry expires.
91
+
92
+ `vulture` runs at `--min-confidence 60`, the level at which it reports unused functions, classes, methods and variables. It cannot see dynamic use, so common framework decorators (`@app.command`, `@app.route`, `@pytest.fixture`, validators and similar) are ignored. For anything else that is used but never referenced by name, such as public API, add it to `ignore_names` under `[tool.vulture]` in your `pyproject.toml`.
93
+
94
+ Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, stored as JSON (cq never writes pickles).
87
95
 
88
96
 
89
97
  ## Usage
@@ -249,6 +257,7 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
249
257
  │ ruff │ 0.17s │ lint │ 1.000 │ OK │
250
258
  │ ty │ 0.33s │ type_check │ 1.000 │ OK │
251
259
  │ bandit │ 0.56s │ security │ 1.000 │ OK │
260
+ │ uv-audit │ 0.41s │ dependencies │ 1.000 │ OK │
252
261
  │ pytest │ 0.91s │ tests │ 1.000 │ OK │
253
262
  │ coverage │ 1.26s │ coverage │ 0.910 │ OK │
254
263
  │ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
@@ -341,7 +350,7 @@ warning = 0.9
341
350
  error = 0.7
342
351
  ```
343
352
 
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`.
353
+ Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `kingfisher`, `uv-audit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`, `ruff-format`.
345
354
 
346
355
 
347
356
  ### Default config
@@ -358,8 +367,8 @@ error_threshold = 0.9999
358
367
  cache_invariant = "ast"
359
368
 
360
369
  [python.ruff]
361
- command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --exclude .claude{exclude}"
362
- exclude_format = " --exclude {path}"
370
+ command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude{exclude}"
371
+ exclude_format = " --extend-exclude {path}"
363
372
  parser = "RuffParser"
364
373
  order = 2
365
374
  warning_threshold = 0.9999
@@ -384,11 +393,27 @@ order = 4
384
393
  warning_threshold = 0.9999
385
394
  error_threshold = 0.8
386
395
 
396
+ [python.kingfisher]
397
+ command = "kingfisher scan \"{context_path}\" --no-validate --no-update-check --git-history none --no-extract-archives --redact -q -f json --exclude .venv --exclude .git --exclude .hypothesis --exclude dist --exclude \"*_cache\" --exclude __pycache__ --exclude .claude{exclude}"
398
+ exclude_format = " --exclude {path}"
399
+ parser = "KingfisherParser"
400
+ order = 5
401
+ warning_threshold = 0.9999
402
+ error_threshold = 0.9999
403
+
404
+ [python.uv-audit]
405
+ command = "uv audit --project \"{abs_context_path}\" --frozen --output-format json"
406
+ parser = "UvAuditParser"
407
+ order = 6
408
+ warning_threshold = 0.9999
409
+ error_threshold = 0.8
410
+ skip_for_file = true
411
+
387
412
  [python.pytest]
388
413
  command = "{python} -m pytest -vv \"{context_path}\" --ignore .claude{exclude}"
389
414
  exclude_format = " --ignore {path}"
390
415
  parser = "PytestParser"
391
- order = 5
416
+ order = 7
392
417
  warning_threshold = 1.0
393
418
  error_threshold = 1.0
394
419
  run_in_target_env = true
@@ -397,10 +422,10 @@ skip_for_file = true
397
422
  cache_invariant = "ast"
398
423
 
399
424
  [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"
425
+ command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py,*/py_cq/runners/* \"{pytest_serial}\" \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --omit=*/tests/*,*/test_*.py,*/py_cq/runners/*"
401
426
  exclude_format = " --ignore {path}"
402
427
  parser = "CoverageParser"
403
- order = 6
428
+ order = 8
404
429
  warning_threshold = 0.9
405
430
  error_threshold = 0.5
406
431
  run_in_target_env = true
@@ -410,19 +435,19 @@ skip_if = "pytest"
410
435
  cache_invariant = "ast"
411
436
 
412
437
  [python.vulture]
413
- command = "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
438
+ command = "{python} -m vulture \"{context_path}\" --min-confidence 60 --ignore-decorators \"@*.command,@*.callback,@*.route,@*.get,@*.post,@*.put,@*.delete,@*.patch,@*fixture,@*.task,@*hookimpl,@*validator,@*.errorhandler\" --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
414
439
  exclude_format = ",{path}"
415
440
  parser = "VultureParser"
416
- order = 7
441
+ order = 9
417
442
  warning_threshold = 0.9999
418
443
  error_threshold = 0.8
419
444
  cache_invariant = "ast"
420
445
 
421
446
  [python.interrogate]
422
- command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
447
+ command = "{python} -m interrogate \"{context_path}\" -e .claude -e \"{context_path}/tests\"{exclude} -v --fail-under 0"
423
448
  exclude_format = " -e {path}"
424
449
  parser = "InterrogateParser"
425
- order = 8
450
+ order = 10
426
451
  warning_threshold = 0.8
427
452
  error_threshold = 0.5
428
453
 
@@ -432,33 +457,29 @@ skip_empty_init = true
432
457
  [python.radon-cc]
433
458
  command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
434
459
  parser = "ComplexityParser"
435
- order = 9
436
- gate_strict = false
460
+ order = 11
437
461
  warning_threshold = 0.6
438
462
  error_threshold = 0.4
439
463
 
440
464
  [python.radon-mi]
441
465
  command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
442
466
  parser = "MaintainabilityParser"
443
- order = 10
444
- gate_strict = false
467
+ order = 12
445
468
  warning_threshold = 0.6
446
469
  error_threshold = 0.4
447
470
 
448
471
  [python.radon-hal]
449
472
  command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
450
473
  parser = "HalsteadParser"
451
- order = 11
452
- gate_strict = false
474
+ order = 13
453
475
  warning_threshold = 0.35
454
476
  error_threshold = 0.3
455
477
 
456
478
  [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}"
479
+ command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
480
+ exclude_format = " --extend-exclude {path}"
459
481
  parser = "RuffFormatParser"
460
- order = 12
461
- gate_strict = false
482
+ order = 14
462
483
  warning_threshold = 0.9999
463
484
  error_threshold = 0.5
464
485
  ```
@@ -6,7 +6,7 @@
6
6
  [![Python versions](https://img.shields.io/pypi/pyversions/python-code-quality?)](https://pypi.org/project/python-code-quality/)
7
7
  [![License](https://img.shields.io/github/license/rhiza-fr/py-cq)](LICENSE)
8
8
 
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.
9
+ Run 14 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
 
@@ -45,16 +45,24 @@ When running '-o llm', we run sequentially and exit early at the first error.
45
45
  | 2 | ruff | Lint / style |
46
46
  | 3 | ty | Type errors |
47
47
  | 4 | bandit | Security vulnerabilities |
48
- | 5 | pytest | Test pass rate |
49
- | 6 | coverage | Test coverage |
50
- | 7 | vulture | Dead code |
51
- | 8 | interrogate | Docstring coverage |
52
- | 9 | radon cc | Cyclomatic complexity |
53
- | 10 | radon mi | Maintainability index |
54
- | 11 | radon hal | Halstead volume / bug estimate |
55
- | 12 | ruff format | Formatting (fraction of files already formatted) |
48
+ | 5 | kingfisher | Hardcoded secrets (API keys, tokens, private keys) |
49
+ | 6 | uv audit | Known vulnerabilities in locked dependencies |
50
+ | 7 | pytest | Test pass rate |
51
+ | 8 | coverage | Test coverage |
52
+ | 9 | vulture | Dead code |
53
+ | 10 | interrogate | Docstring coverage |
54
+ | 11 | radon cc | Cyclomatic complexity |
55
+ | 12 | radon mi | Maintainability index |
56
+ | 13 | radon hal | Halstead volume / bug estimate |
57
+ | 14 | ruff format | Formatting (fraction of files already formatted) |
56
58
 
57
- Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, No pickle risk.
59
+ `kingfisher` scans the working tree for hardcoded secrets with [Kingfisher](https://github.com/mongodb/kingfisher) (502 rules, Rust). It is not a Python dependency of cq: install it once with `uv tool install kingfisher-bin` (about 41 MB). When it is not on the `PATH` the tool is skipped with a warning. cq runs it offline (`--no-validate --no-update-check`) and with `--redact`, so secrets are never sent to provider APIs and never stored in the cache; the LLM message gives the file, line and rule but no source excerpt. Silence a test fixture or false positive with a `kingfisher:ignore` comment on that line.
60
+
61
+ `uv audit` checks the versions pinned in `uv.lock` against the OSV advisory database. It needs `uv` on the `PATH`, a `uv.lock` and network access; when any is missing the tool is skipped with a warning instead of failing. Only advisories that have a fix version are reported, since those are the ones an upgrade resolves. Results are cached like every other tool, so an advisory published after a run shows up when the project changes or the cache entry expires.
62
+
63
+ `vulture` runs at `--min-confidence 60`, the level at which it reports unused functions, classes, methods and variables. It cannot see dynamic use, so common framework decorators (`@app.command`, `@app.route`, `@pytest.fixture`, validators and similar) are ignored. For anything else that is used but never referenced by name, such as public API, add it to `ignore_names` under `[tool.vulture]` in your `pyproject.toml`.
64
+
65
+ Diskcache is used to cache tool output for lightning fast re-runs. Sane defaults: <100 Mb, <5 days, stored as JSON (cq never writes pickles).
58
66
 
59
67
 
60
68
  ## Usage
@@ -220,6 +228,7 @@ Then invoke it with `/cq-fix` in Claude Code. The `$(...)` embeds the live `cq`
220
228
  │ ruff │ 0.17s │ lint │ 1.000 │ OK │
221
229
  │ ty │ 0.33s │ type_check │ 1.000 │ OK │
222
230
  │ bandit │ 0.56s │ security │ 1.000 │ OK │
231
+ │ uv-audit │ 0.41s │ dependencies │ 1.000 │ OK │
223
232
  │ pytest │ 0.91s │ tests │ 1.000 │ OK │
224
233
  │ coverage │ 1.26s │ coverage │ 0.910 │ OK │
225
234
  │ vulture │ 0.32s │ dead_code │ 1.000 │ OK │
@@ -312,7 +321,7 @@ warning = 0.9
312
321
  error = 0.7
313
322
  ```
314
323
 
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`.
324
+ Tool IDs match the keys in `src/py_cq/config/config.toml`: `compile`, `ruff`, `ty`, `bandit`, `kingfisher`, `uv-audit`, `pytest`, `coverage`, `radon-cc`, `radon-mi`, `radon-hal`, `vulture`, `interrogate`, `ruff-format`.
316
325
 
317
326
 
318
327
  ### Default config
@@ -329,8 +338,8 @@ error_threshold = 0.9999
329
338
  cache_invariant = "ast"
330
339
 
331
340
  [python.ruff]
332
- command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --exclude .claude{exclude}"
333
- exclude_format = " --exclude {path}"
341
+ command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude{exclude}"
342
+ exclude_format = " --extend-exclude {path}"
334
343
  parser = "RuffParser"
335
344
  order = 2
336
345
  warning_threshold = 0.9999
@@ -355,11 +364,27 @@ order = 4
355
364
  warning_threshold = 0.9999
356
365
  error_threshold = 0.8
357
366
 
367
+ [python.kingfisher]
368
+ command = "kingfisher scan \"{context_path}\" --no-validate --no-update-check --git-history none --no-extract-archives --redact -q -f json --exclude .venv --exclude .git --exclude .hypothesis --exclude dist --exclude \"*_cache\" --exclude __pycache__ --exclude .claude{exclude}"
369
+ exclude_format = " --exclude {path}"
370
+ parser = "KingfisherParser"
371
+ order = 5
372
+ warning_threshold = 0.9999
373
+ error_threshold = 0.9999
374
+
375
+ [python.uv-audit]
376
+ command = "uv audit --project \"{abs_context_path}\" --frozen --output-format json"
377
+ parser = "UvAuditParser"
378
+ order = 6
379
+ warning_threshold = 0.9999
380
+ error_threshold = 0.8
381
+ skip_for_file = true
382
+
358
383
  [python.pytest]
359
384
  command = "{python} -m pytest -vv \"{context_path}\" --ignore .claude{exclude}"
360
385
  exclude_format = " --ignore {path}"
361
386
  parser = "PytestParser"
362
- order = 5
387
+ order = 7
363
388
  warning_threshold = 1.0
364
389
  error_threshold = 1.0
365
390
  run_in_target_env = true
@@ -368,10 +393,10 @@ skip_for_file = true
368
393
  cache_invariant = "ast"
369
394
 
370
395
  [python.coverage]
371
- 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"
396
+ command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py,*/py_cq/runners/* \"{pytest_serial}\" \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --omit=*/tests/*,*/test_*.py,*/py_cq/runners/*"
372
397
  exclude_format = " --ignore {path}"
373
398
  parser = "CoverageParser"
374
- order = 6
399
+ order = 8
375
400
  warning_threshold = 0.9
376
401
  error_threshold = 0.5
377
402
  run_in_target_env = true
@@ -381,19 +406,19 @@ skip_if = "pytest"
381
406
  cache_invariant = "ast"
382
407
 
383
408
  [python.vulture]
384
- command = "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
409
+ command = "{python} -m vulture \"{context_path}\" --min-confidence 60 --ignore-decorators \"@*.command,@*.callback,@*.route,@*.get,@*.post,@*.put,@*.delete,@*.patch,@*fixture,@*.task,@*hookimpl,@*validator,@*.errorhandler\" --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
385
410
  exclude_format = ",{path}"
386
411
  parser = "VultureParser"
387
- order = 7
412
+ order = 9
388
413
  warning_threshold = 0.9999
389
414
  error_threshold = 0.8
390
415
  cache_invariant = "ast"
391
416
 
392
417
  [python.interrogate]
393
- command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
418
+ command = "{python} -m interrogate \"{context_path}\" -e .claude -e \"{context_path}/tests\"{exclude} -v --fail-under 0"
394
419
  exclude_format = " -e {path}"
395
420
  parser = "InterrogateParser"
396
- order = 8
421
+ order = 10
397
422
  warning_threshold = 0.8
398
423
  error_threshold = 0.5
399
424
 
@@ -403,33 +428,29 @@ skip_empty_init = true
403
428
  [python.radon-cc]
404
429
  command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
405
430
  parser = "ComplexityParser"
406
- order = 9
407
- gate_strict = false
431
+ order = 11
408
432
  warning_threshold = 0.6
409
433
  error_threshold = 0.4
410
434
 
411
435
  [python.radon-mi]
412
436
  command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
413
437
  parser = "MaintainabilityParser"
414
- order = 10
415
- gate_strict = false
438
+ order = 12
416
439
  warning_threshold = 0.6
417
440
  error_threshold = 0.4
418
441
 
419
442
  [python.radon-hal]
420
443
  command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
421
444
  parser = "HalsteadParser"
422
- order = 11
423
- gate_strict = false
445
+ order = 13
424
446
  warning_threshold = 0.35
425
447
  error_threshold = 0.3
426
448
 
427
449
  [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}"
450
+ command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
451
+ exclude_format = " --extend-exclude {path}"
430
452
  parser = "RuffFormatParser"
431
- order = 12
432
- gate_strict = false
453
+ order = 14
433
454
  warning_threshold = 0.9999
434
455
  error_threshold = 0.5
435
456
  ```
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "python-code-quality"
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."
3
+ version = "0.4.0"
4
+ description = "Python Code Quality Analysis Tool - feed the results from 14 CQ tools straight into an LLM. Minimal tokens."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  license = "MIT"
@@ -63,5 +63,13 @@ unresolved-import = "ignore"
63
63
  [tool.cq]
64
64
  exclude = ["demo"]
65
65
 
66
+ [tool.vulture]
67
+ ignore_names = [
68
+ "check",
69
+ "check_llm_json",
70
+ "PHASE",
71
+ "COL_*",
72
+ ]
73
+
66
74
  [dependency-groups]
67
75
  dev = ["hypothesis>=6.151.10"]
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "python-code-quality"
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."
3
+ version = "0.4.0"
4
+ description = "Python Code Quality Analysis Tool - feed the results from 14 CQ tools straight into an LLM. Minimal tokens."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  license = "MIT"
@@ -65,6 +65,10 @@ rules = {unresolved-import = "ignore"}
65
65
  [tool.cq]
66
66
  exclude = ["demo"]
67
67
 
68
+ [tool.vulture]
69
+ # Public API, enum members and positional test constants that nothing references by name.
70
+ ignore_names = ["check", "check_llm_json", "PHASE", "COL_*"]
71
+
68
72
  [dependency-groups]
69
73
  dev = [
70
74
  "hypothesis>=6.151.10",
@@ -107,7 +107,7 @@ def callback(
107
107
  help="Show version and dependencies",
108
108
  ),
109
109
  ) -> None:
110
- """Feed the results from 12 code quality tools to an LLM. Try: cq check . -o llm"""
110
+ """Feed the results from 13 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):
@@ -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 12 code quality tools to an LLM. Try: cq check . -o llm""" # --help
175
+ """Feed the results from 13 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}")
@@ -7,8 +7,8 @@ error_threshold = 0.9999
7
7
  cache_invariant = "ast"
8
8
 
9
9
  [python.ruff]
10
- command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --exclude .claude{exclude}"
11
- exclude_format = " --exclude {path}"
10
+ command = "{python} -m ruff check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude{exclude}"
11
+ exclude_format = " --extend-exclude {path}"
12
12
  parser = "RuffParser"
13
13
  order = 2
14
14
  warning_threshold = 0.9999
@@ -33,11 +33,27 @@ order = 4
33
33
  warning_threshold = 0.9999
34
34
  error_threshold = 0.8
35
35
 
36
+ [python.kingfisher]
37
+ command = "kingfisher scan \"{context_path}\" --no-validate --no-update-check --git-history none --no-extract-archives --redact -q -f json --exclude .venv --exclude .git --exclude .hypothesis --exclude dist --exclude \"*_cache\" --exclude __pycache__ --exclude .claude{exclude}"
38
+ exclude_format = " --exclude {path}"
39
+ parser = "KingfisherParser"
40
+ order = 5
41
+ warning_threshold = 0.9999
42
+ error_threshold = 0.9999
43
+
44
+ [python.uv-audit]
45
+ command = "uv audit --project \"{abs_context_path}\" --frozen --output-format json"
46
+ parser = "UvAuditParser"
47
+ order = 6
48
+ warning_threshold = 0.9999
49
+ error_threshold = 0.8
50
+ skip_for_file = true
51
+
36
52
  [python.pytest]
37
53
  command = "{python} -m pytest -vv \"{context_path}\" --ignore .claude{exclude}"
38
54
  exclude_format = " --ignore {path}"
39
55
  parser = "PytestParser"
40
- order = 5
56
+ order = 7
41
57
  warning_threshold = 1.0
42
58
  error_threshold = 1.0
43
59
  run_in_target_env = true
@@ -46,10 +62,10 @@ skip_for_file = true
46
62
  cache_invariant = "ast"
47
63
 
48
64
  [python.coverage]
49
- 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"
65
+ command = "{python} -m coverage run --omit=*/tests/*,*/test_*.py,*/py_cq/runners/* \"{pytest_serial}\" \"{context_path}\" --ignore .claude{exclude} && {python} -m coverage report --show-missing --omit=*/tests/*,*/test_*.py,*/py_cq/runners/*"
50
66
  exclude_format = " --ignore {path}"
51
67
  parser = "CoverageParser"
52
- order = 6
68
+ order = 8
53
69
  warning_threshold = 0.9
54
70
  error_threshold = 0.5
55
71
  run_in_target_env = true
@@ -59,19 +75,19 @@ skip_if = "pytest"
59
75
  cache_invariant = "ast"
60
76
 
61
77
  [python.vulture]
62
- command = "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
78
+ command = "{python} -m vulture \"{context_path}\" --min-confidence 60 --ignore-decorators \"@*.command,@*.callback,@*.route,@*.get,@*.post,@*.put,@*.delete,@*.patch,@*fixture,@*.task,@*hookimpl,@*validator,@*.errorhandler\" --exclude .venv,dist,.*_cache,docs,.git,.claude{exclude}"
63
79
  exclude_format = ",{path}"
64
80
  parser = "VultureParser"
65
- order = 7
81
+ order = 9
66
82
  warning_threshold = 0.9999
67
83
  error_threshold = 0.8
68
84
  cache_invariant = "ast"
69
85
 
70
86
  [python.interrogate]
71
- command = "{python} -m interrogate \"{context_path}\" -e .claude{exclude} -v --fail-under 0"
87
+ command = "{python} -m interrogate \"{context_path}\" -e .claude -e \"{context_path}/tests\"{exclude} -v --fail-under 0"
72
88
  exclude_format = " -e {path}"
73
89
  parser = "InterrogateParser"
74
- order = 8
90
+ order = 10
75
91
  warning_threshold = 0.8
76
92
  error_threshold = 0.5
77
93
 
@@ -81,32 +97,28 @@ skip_empty_init = true
81
97
  [python.radon-cc]
82
98
  command = "{python} -m radon cc --json --exclude '.claude/**' \"{context_path}\""
83
99
  parser = "ComplexityParser"
84
- order = 9
85
- gate_strict = false
100
+ order = 11
86
101
  warning_threshold = 0.6
87
102
  error_threshold = 0.4
88
103
 
89
104
  [python.radon-mi]
90
105
  command = "{python} -m radon mi -s --json --exclude '.claude/**' \"{context_path}\""
91
106
  parser = "MaintainabilityParser"
92
- order = 10
93
- gate_strict = false
107
+ order = 12
94
108
  warning_threshold = 0.6
95
109
  error_threshold = 0.4
96
110
 
97
111
  [python.radon-hal]
98
112
  command = "{python} -m radon hal -f --json --exclude '.claude/**' \"{context_path}\""
99
113
  parser = "HalsteadParser"
100
- order = 11
101
- gate_strict = false
114
+ order = 13
102
115
  warning_threshold = 0.35
103
116
  error_threshold = 0.3
104
117
 
105
118
  [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}"
119
+ command = "{python} -m ruff format --check --output-format concise --no-cache \"{context_path}\" --extend-exclude .claude --config \"include=['*.py','*.pyi','*.ipynb']\"{exclude}"
120
+ exclude_format = " --extend-exclude {path}"
108
121
  parser = "RuffFormatParser"
109
- order = 12
110
- gate_strict = false
122
+ order = 14
111
123
  warning_threshold = 0.9999
112
124
  error_threshold = 0.5
@@ -38,6 +38,9 @@ _cache = Cache(
38
38
  Path.home() / ".cache" / "cq", size_limit=100 * 1024 * 1024, disk=JSONDisk
39
39
  )
40
40
 
41
+ # Script that runs pytest in one process under coverage; see its docstring.
42
+ _PYTEST_SERIAL = str(Path(__file__).parent / "runners" / "pytest_serial.py")
43
+
41
44
 
42
45
  def _find_project_root(path: Path) -> Path | None:
43
46
  """Walk up from path to find the nearest directory containing pyproject.toml."""
@@ -271,6 +274,7 @@ def run_tool(
271
274
  scan_targets=scan_targets,
272
275
  python=python,
273
276
  exclude=exclude,
277
+ pytest_serial=_PYTEST_SERIAL,
274
278
  )
275
279
  if precomputed_hash is not None:
276
280
  context_hash = precomputed_hash
@@ -328,6 +332,10 @@ def run_tool(
328
332
  timestamp=timestamp,
329
333
  project_path=project_dir,
330
334
  )
335
+ if return_code != 0 and not stdout.strip():
336
+ # Failed without a report: the tool is missing from PATH or offline.
337
+ # Caching this would hide real results until the files change.
338
+ return raw_result
331
339
  _cache.set(
332
340
  cache_key, raw_result.to_dict(), expire=5 * 24 * 60 * 60, tag=project_tag
333
341
  )
@@ -60,9 +60,6 @@ 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
- True # If False, small regressions above warning_threshold are accepted
65
- )
66
63
  skip_if: str = "" # Tool name; if that tool's score is below its error_threshold, skip this tool
67
64
  cache_invariant: str = "" # "ast": key the cache on the docstring-stripped AST, ignoring docstrings/comments/format
68
65
 
@@ -0,0 +1,73 @@
1
+ """Parses output from the Kingfisher secret scanner into a standardized ToolResult.
2
+
3
+ Kingfisher (``uv tool install kingfisher-bin``) is invoked with ``-f json`` and
4
+ ``--redact``, so the report on stdout never contains the secret itself, only a
5
+ hash. ``--no-validate`` keeps it from sending findings to provider APIs.
6
+
7
+ Each finding is listed against its file. The count becomes a logistic-variant
8
+ score stored under the ``secrets`` metric key.
9
+
10
+ When no report can be read (``kingfisher`` not on PATH) the tool reports no
11
+ metric instead of a failing one.
12
+ """
13
+
14
+ import json
15
+ import logging
16
+
17
+ from py_cq.localtypes import AbstractParser, RawResult, ToolResult
18
+ from py_cq.parsers.common import extract_first_issue, score_logistic_variant
19
+
20
+ log = logging.getLogger("cq")
21
+
22
+
23
+ class KingfisherParser(AbstractParser):
24
+ """Parses raw JSON output from ``kingfisher scan -f json`` into a ToolResult."""
25
+
26
+ def parse(self, raw_result: RawResult) -> ToolResult:
27
+ """Parses the raw Kingfisher JSON report into a ToolResult."""
28
+ try:
29
+ findings = json.loads(raw_result.stdout)["findings"]
30
+ except (ValueError, KeyError, TypeError):
31
+ log.warning(
32
+ "kingfisher produced no report (return_code=%s), skipping. "
33
+ "Install it with `uv tool install kingfisher-bin`.",
34
+ raw_result.return_code,
35
+ )
36
+ return ToolResult(raw=raw_result)
37
+
38
+ files: dict[str, list] = {}
39
+ for entry in findings:
40
+ rule = entry.get("rule") or {}
41
+ finding = entry.get("finding") or {}
42
+ path = str(finding.get("path", "")).replace("\\", "/")
43
+ files.setdefault(path, []).append(
44
+ {
45
+ "line": finding.get("line", 0),
46
+ "code": rule.get("id", ""),
47
+ "confidence": finding.get("confidence", ""),
48
+ "message": rule.get("description", ""),
49
+ }
50
+ )
51
+
52
+ count = sum(len(issues) for issues in files.values())
53
+ score = score_logistic_variant(count, scale_factor=5)
54
+ return ToolResult(raw=raw_result, metrics={"secrets": score}, details=files)
55
+
56
+ def format_llm_message(
57
+ self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
58
+ ) -> str:
59
+ """Formats the first secret finding into an LLM-friendly message."""
60
+ result = extract_first_issue(tr.details)
61
+ if result is None:
62
+ return "kingfisher reported secrets (no details available)"
63
+ file, issue = result
64
+ line = issue.get("line", "?")
65
+ # No source excerpt: it would put the plaintext secret into the LLM prompt.
66
+ return (
67
+ f"{file}:{line} - {issue.get('code', '')}: "
68
+ f"[{issue.get('confidence', '')} confidence] {issue.get('message', '')}\n\n"
69
+ "Move the secret out of the source (environment variable or an untracked "
70
+ "file) and rotate it if it was ever committed. Only if it is an intentional "
71
+ "test fixture or a false positive, add a `kingfisher:ignore` comment on "
72
+ "that line."
73
+ )
@@ -0,0 +1,88 @@
1
+ """Parses output from ``uv audit`` into a standardized ToolResult.
2
+
3
+ ``uv audit --output-format json`` checks the packages pinned in ``uv.lock``
4
+ against the OSV advisory database and prints a JSON report on stdout.
5
+
6
+ Only advisories with a fix version are counted: those are the ones an upgrade
7
+ resolves. The same vulnerability is often listed once per advisory database
8
+ (GHSA, PYSEC), so entries that alias one already seen for a package are
9
+ dropped. The count becomes a logistic-variant score stored under the
10
+ ``dependencies`` metric key.
11
+
12
+ When no report can be read (``uv`` missing, no ``pyproject.toml`` or
13
+ ``uv.lock``, no network) the tool reports no metric instead of a failing one.
14
+ """
15
+
16
+ import json
17
+ import logging
18
+
19
+ from py_cq.localtypes import AbstractParser, RawResult, ToolResult
20
+ from py_cq.parsers.common import extract_first_issue, score_logistic_variant
21
+
22
+ log = logging.getLogger("cq")
23
+
24
+ _LOCKFILE = "uv.lock"
25
+
26
+
27
+ class UvAuditParser(AbstractParser):
28
+ """Parses raw JSON output from ``uv audit`` into a ToolResult."""
29
+
30
+ def parse(self, raw_result: RawResult) -> ToolResult:
31
+ """Parses the raw ``uv audit`` JSON report into a ToolResult."""
32
+ try:
33
+ vulnerabilities = json.loads(raw_result.stdout)["vulnerabilities"]
34
+ except (ValueError, KeyError, TypeError):
35
+ reason = (raw_result.stderr or "").strip().splitlines()[-1:]
36
+ log.warning(
37
+ "uv audit produced no report (return_code=%s), skipping: %s",
38
+ raw_result.return_code,
39
+ reason[0] if reason else "no output",
40
+ )
41
+ return ToolResult(raw=raw_result)
42
+
43
+ seen: set[tuple[str, str]] = set()
44
+ issues: list[dict] = []
45
+ for vuln in vulnerabilities:
46
+ fix_versions = [str(v) for v in vuln.get("fix_versions") or []]
47
+ if not fix_versions:
48
+ continue
49
+ dependency = vuln.get("dependency") or {}
50
+ package = dependency.get("name", "")
51
+ ids = {vuln.get("id", ""), *(vuln.get("aliases") or [])}
52
+ if any((package, i) in seen for i in ids):
53
+ continue
54
+ seen.update((package, i) for i in ids)
55
+ issues.append(
56
+ {
57
+ "code": vuln.get("id", ""),
58
+ "package": package,
59
+ "version": dependency.get("version", ""),
60
+ "fix_versions": fix_versions,
61
+ "message": vuln.get("summary") or "",
62
+ "link": vuln.get("link") or "",
63
+ }
64
+ )
65
+
66
+ score = score_logistic_variant(len(issues), scale_factor=5)
67
+ details = {_LOCKFILE: issues} if issues else {}
68
+ return ToolResult(
69
+ raw=raw_result, metrics={"dependencies": score}, details=details
70
+ )
71
+
72
+ def format_llm_message(
73
+ self, tr: ToolResult, *, context_lines: int = 15, limit: int = 1
74
+ ) -> str:
75
+ """Formats the first vulnerable dependency into an LLM-friendly message."""
76
+ result = extract_first_issue(tr.details)
77
+ if result is None:
78
+ return "uv audit reported issues (no details available)"
79
+ file, issue = result
80
+ package = issue.get("package", "")
81
+ fixed = ", ".join(issue.get("fix_versions", []))
82
+ message = (
83
+ f"{file} - {issue.get('code', '')}: **{package} {issue.get('version', '')}** "
84
+ f"has a known vulnerability: {issue.get('message', '')}\n\n"
85
+ f"Fixed in {fixed}. Upgrade with `uv lock --upgrade-package {package}`."
86
+ )
87
+ link = issue.get("link")
88
+ return f"{message}\n\n{link}" if link else message
@@ -1,9 +1,14 @@
1
1
  """Parses output from vulture dead-code detector into a standardized ToolResult.
2
2
 
3
- Vulture is invoked with ``--min-confidence 80``, producing one text line
3
+ Vulture is invoked with ``--min-confidence 60``, producing one text line
4
4
  per unused symbol::
5
5
 
6
- src/foo.py:10: unused function 'bar' (80% confidence)
6
+ src/foo.py:10: unused function 'bar' (60% confidence)
7
+
8
+ 60% is the confidence vulture gives unused functions, classes, methods and
9
+ variables, so anything higher only reports unused imports and unreachable
10
+ code. At that level vulture cannot see dynamic use, so the LLM message for a
11
+ finding below 80% says how to keep the symbol instead of deleting it.
7
12
 
8
13
  The parser counts violations and converts the count into a logistic-variant
9
14
  score stored under the ``dead_code`` metric key.
@@ -20,6 +25,13 @@ from py_cq.parsers.common import (
20
25
 
21
26
  _LINE_RE = re.compile(r"^(.+):(\d+): (unused \S+) '(.+)' \((\d+)% confidence\)$")
22
27
 
28
+ _CERTAIN = 80
29
+ _KEEP_HINT = (
30
+ "\n\nRemove it if nothing uses it. If it is public API or is used dynamically,"
31
+ ' keep it and add `"{name}"` to `ignore_names` under `[tool.vulture]` in'
32
+ " `pyproject.toml`."
33
+ )
34
+
23
35
 
24
36
  class VultureParser(AbstractParser):
25
37
  """Parses raw text output from ``vulture`` into a ToolResult."""
@@ -55,4 +67,7 @@ class VultureParser(AbstractParser):
55
67
  kind = issue.get("type", "unused")
56
68
  name = issue.get("name", "")
57
69
  confidence = issue.get("confidence", "?")
58
- return f"{file}:{line} - **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
70
+ message = f"{file}:{line} - **{kind}** `{name}` ({confidence}% confidence){format_source_context(file, line, count=context_lines)}"
71
+ if isinstance(confidence, int) and confidence < _CERTAIN:
72
+ message += _KEEP_HINT.format(name=name)
73
+ return message
@@ -0,0 +1,27 @@
1
+ """Run pytest in a single process, for the coverage tool.
2
+
3
+ cq runs this script under ``coverage run``. Coverage only measures the process
4
+ it starts, so if the target project enables pytest-xdist through its own
5
+ ``addopts``, the tests run in worker processes that coverage never sees and the
6
+ coverage score collapses. Passing ``-n 0`` keeps the tests in this process. It
7
+ is only passed when xdist is installed, because pytest rejects ``-n`` otherwise.
8
+
9
+ The script runs in the target project's environment, so it imports only the
10
+ standard library and pytest. It lives in its own directory because running a
11
+ script puts that directory first on ``sys.path``; that entry is replaced with
12
+ the working directory so imports behave as they do under ``python -m pytest``.
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ from importlib.util import find_spec
18
+
19
+ if __name__ == "__main__":
20
+ sys.path[0] = os.getcwd()
21
+
22
+ import pytest
23
+
24
+ args = sys.argv[1:]
25
+ if find_spec("xdist") is not None:
26
+ args += ["-n", "0"]
27
+ sys.exit(pytest.main(args))
@@ -32,7 +32,6 @@ def load_tool_configs() -> dict[str, ToolConfig]:
32
32
  exclude_format=tool_data.get("exclude_format", ""),
33
33
  scan_exclude_names=tool_data.get("scan_exclude_names", []),
34
34
  skip_for_file=tool_data.get("skip_for_file", False),
35
- gate_strict=tool_data.get("gate_strict", True),
36
35
  skip_if=tool_data.get("skip_if", ""),
37
36
  cache_invariant=tool_data.get("cache_invariant", ""),
38
37
  )