python-code-quality 0.1.13__tar.gz → 0.1.14__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 (33) hide show
  1. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/PKG-INFO +40 -46
  2. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/README.md +39 -45
  3. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/pyproject.toml +8 -1
  4. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/__init__.py +0 -4
  5. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/cli.py +8 -1
  6. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/config/config.yaml +12 -6
  7. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/execution_engine.py +18 -6
  8. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/localtypes.py +1 -0
  9. python_code_quality-0.1.14/src/py_cq/parsers/__init__.py +1 -0
  10. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/interrogateparser.py +4 -4
  11. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/tool_registry.py +1 -0
  12. python_code_quality-0.1.13/src/py_cq/parsers/__init__.py +0 -0
  13. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/config/__init__.py +0 -0
  14. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/context_hash.py +0 -0
  15. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/language_detector.py +0 -0
  16. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/llm_formatter.py +0 -0
  17. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/main.py +0 -0
  18. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/metric_aggregator.py +0 -0
  19. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/banditparser.py +0 -0
  20. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/common.py +0 -0
  21. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/compileparser.py +0 -0
  22. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/complexityparser.py +0 -0
  23. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/coverageparser.py +0 -0
  24. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/exitcodeparser.py +0 -0
  25. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/halsteadparser.py +0 -0
  26. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/linecountparser.py +0 -0
  27. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/maintainabilityparser.py +0 -0
  28. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/pytestparser.py +0 -0
  29. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/regexcountparser.py +0 -0
  30. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/ruffparser.py +0 -0
  31. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/typarser.py +0 -0
  32. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/parsers/vultureparser.py +0 -0
  33. {python_code_quality-0.1.13 → python_code_quality-0.1.14}/src/py_cq/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-code-quality
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens.
5
5
  Author: Chris Kilner
6
6
  Author-email: Chris Kilner <chris@rhiza.fr>
@@ -35,39 +35,15 @@ 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
- Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
39
-
40
- Why? It removes the mental burden of understanding all these tools and parsing their results.
41
-
42
- The primary workflow is:
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.
43
39
 
44
40
  ```bash
45
- # get the single most critical defect as markdown
46
- cq check . -o llm
47
- ```
48
-
49
- ```python
50
- `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
51
-
52
- 18: min_dist = float("inf")
53
- 19: nearest_city = None
54
- 20: for city in cities:
55
- 21: unused_variable = 67
56
- 22: dist = calc_dist(current_city, city)
57
- 23: if dist < min_dist:
58
- 24: min_dist = dist
59
- 25: nearest_city = city
60
-
61
- Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
62
- ```
63
- Feed to an LLM with edit tools and repeat until there are no issues, e.g.
64
-
65
- ```python
66
- cq check . -o llm | claude -p "fix this"
67
- # or
68
- cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
41
+ cq check . -o llm # top defect as markdown, pipe to an LLM
42
+ cq check . # table overview of all scores
43
+ cq check . -o score # numeric score only, exits 1 on errors (CI gate)
69
44
  ```
70
45
 
46
+ ![cq demo](demo/output/demo.gif)
71
47
 
72
48
  ## Install
73
49
 
@@ -114,6 +90,7 @@ cq check . -o raw # Raw tool output for debug
114
90
  cq check path/to/file.py # Just one file (skips pytest and coverage)
115
91
  cq check . --only ruff,ty # Run only specific tools
116
92
  cq check . --skip bandit # Skip specific tools
93
+ cq check . --exclude demo # Exclude paths from all tools
117
94
  cq check . --workers 1 # Run sequentially if you like things slow
118
95
  cq check . --clear-cache # Clear cached results before running (rarely needed)
119
96
  cq config path/to/project/ # Show effective tool configuration
@@ -133,10 +110,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
133
110
  ```json
134
111
  {
135
112
  "hooks": {
136
- "Stop": [{
137
- "matcher": "",
138
- "hooks": [{"type": "command", "command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm"}]
139
- }]
113
+ "Stop": [
114
+ {
115
+ "matcher": "",
116
+ "hooks": [
117
+ {
118
+ "type": "command",
119
+ "command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm; true"
120
+ }
121
+ ]
122
+ }
123
+ ]
140
124
  }
141
125
  }
142
126
  ```
@@ -252,6 +236,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
252
236
  # Skip tools that are slow or not relevant to your project
253
237
  disable = ["coverage", "interrogate"]
254
238
 
239
+ # Exclude paths from all tools (merged with --exclude CLI flag)
240
+ exclude = ["demo", "docs"]
241
+
255
242
  # Lines of source context shown around each defect in LLM output (default: 15)
256
243
  context_lines = 15
257
244
 
@@ -270,21 +257,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
270
257
  python:
271
258
 
272
259
  compile:
273
- command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
260
+ command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
274
261
  parser: "CompileParser"
275
262
  order: 1
276
263
  warning_threshold: 0.9999
277
264
  error_threshold: 0.9999
278
265
 
279
266
  ruff:
280
- command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
267
+ command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
268
+ exclude_format: " --exclude {path}"
281
269
  parser: "RuffParser"
282
270
  order: 2
283
271
  warning_threshold: 0.9999
284
272
  error_threshold: 0.9
285
273
 
286
274
  ty:
287
- command: "{python} -m ty check --output-format concise --color never {context_path}"
275
+ command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
276
+ exclude_format: " --exclude {path}"
288
277
  parser: "TyParser"
289
278
  order: 3
290
279
  warning_threshold: 0.9999
@@ -294,22 +283,26 @@ python:
294
283
  - ty
295
284
 
296
285
  bandit:
297
- command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
286
+ 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}\""
287
+ exclude_format: ",{input_path_posix}/{path}"
298
288
  parser: "BanditParser"
299
289
  order: 4
300
290
  warning_threshold: 0.9999
301
291
  error_threshold: 0.8
302
292
 
303
293
  pytest:
304
- command: "{python} -m pytest -v {context_path}"
294
+ command: "{python} -m pytest -v \"{context_path}\"{exclude}"
295
+ exclude_format: " --ignore {path}"
305
296
  parser: "PytestParser"
306
297
  order: 5
307
298
  warning_threshold: 1.0
308
299
  error_threshold: 1.0
309
300
  run_in_target_env: true
301
+ extra_deps:
302
+ - pytest
310
303
 
311
304
  coverage:
312
- command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
305
+ command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
313
306
  parser: "CoverageParser"
314
307
  order: 6
315
308
  warning_threshold: 0.9
@@ -320,40 +313,41 @@ python:
320
313
  - pytest
321
314
 
322
315
  radon-cc:
323
- command: "{python} -m radon cc --json {context_path}"
316
+ command: "{python} -m radon cc --json \"{context_path}\""
324
317
  parser: "ComplexityParser"
325
318
  order: 7
326
319
  warning_threshold: 0.6
327
320
  error_threshold: 0.4
328
321
 
329
322
  radon-mi:
330
- command: "{python} -m radon mi -s --json {context_path}"
323
+ command: "{python} -m radon mi -s --json \"{context_path}\""
331
324
  parser: "MaintainabilityParser"
332
325
  order: 8
333
326
  warning_threshold: 0.6
334
327
  error_threshold: 0.4
335
328
 
336
329
  radon-hal:
337
- command: "{python} -m radon hal -f --json {context_path}"
330
+ command: "{python} -m radon hal -f --json \"{context_path}\""
338
331
  parser: "HalsteadParser"
339
332
  order: 9
340
333
  warning_threshold: 0.5
341
334
  error_threshold: 0.3
342
335
 
343
336
  vulture:
344
- command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
337
+ command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
338
+ exclude_format: ",{path}"
345
339
  parser: "VultureParser"
346
340
  order: 10
347
341
  warning_threshold: 0.9999
348
342
  error_threshold: 0.8
349
343
 
350
344
  interrogate:
351
- command: "{python} -m interrogate {context_path} -v --fail-under 0"
345
+ command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
346
+ exclude_format: " -e {path}"
352
347
  parser: "InterrogateParser"
353
348
  order: 11
354
349
  warning_threshold: 0.8
355
350
  error_threshold: 0.3
356
-
357
351
  ```
358
352
 
359
353
  ## Respect
@@ -6,39 +6,15 @@
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
- Feed the results from 11+ code quality tools to an LLM. Minimal tokens.
10
-
11
- Why? It removes the mental burden of understanding all these tools and parsing their results.
12
-
13
- The primary workflow is:
9
+ 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.
14
10
 
15
11
  ```bash
16
- # get the single most critical defect as markdown
17
- cq check . -o llm
18
- ```
19
-
20
- ```python
21
- `data/problems/travelling_salesman/ts_bad.py:21` — **F841**: Local variable `unused_variable` is assigned to but never used
22
-
23
- 18: min_dist = float("inf")
24
- 19: nearest_city = None
25
- 20: for city in cities:
26
- 21: unused_variable = 67
27
- 22: dist = calc_dist(current_city, city)
28
- 23: if dist < min_dist:
29
- 24: min_dist = dist
30
- 25: nearest_city = city
31
-
32
- Please fix only this issue. After fixing, run `cq check . -o llm` to verify.
33
- ```
34
- Feed to an LLM with edit tools and repeat until there are no issues, e.g.
35
-
36
- ```python
37
- cq check . -o llm | claude -p "fix this"
38
- # or
39
- cq check . -o llm | ollama run gpt-oss:20b "Explain how to fix this"
12
+ cq check . -o llm # top defect as markdown, pipe to an LLM
13
+ cq check . # table overview of all scores
14
+ cq check . -o score # numeric score only, exits 1 on errors (CI gate)
40
15
  ```
41
16
 
17
+ ![cq demo](demo/output/demo.gif)
42
18
 
43
19
  ## Install
44
20
 
@@ -85,6 +61,7 @@ cq check . -o raw # Raw tool output for debug
85
61
  cq check path/to/file.py # Just one file (skips pytest and coverage)
86
62
  cq check . --only ruff,ty # Run only specific tools
87
63
  cq check . --skip bandit # Skip specific tools
64
+ cq check . --exclude demo # Exclude paths from all tools
88
65
  cq check . --workers 1 # Run sequentially if you like things slow
89
66
  cq check . --clear-cache # Clear cached results before running (rarely needed)
90
67
  cq config path/to/project/ # Show effective tool configuration
@@ -104,10 +81,17 @@ Add a stop hook to your project's `.claude/settings.json` so Claude automaticall
104
81
  ```json
105
82
  {
106
83
  "hooks": {
107
- "Stop": [{
108
- "matcher": "",
109
- "hooks": [{"type": "command", "command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm"}]
110
- }]
84
+ "Stop": [
85
+ {
86
+ "matcher": "",
87
+ "hooks": [
88
+ {
89
+ "type": "command",
90
+ "command": "cq check . -o score && echo 'CQ: all clear' || cq check . -o llm; true"
91
+ }
92
+ ]
93
+ }
94
+ ]
111
95
  }
112
96
  }
113
97
  ```
@@ -223,6 +207,9 @@ Add a `[tool.cq]` section to your project's `pyproject.toml`:
223
207
  # Skip tools that are slow or not relevant to your project
224
208
  disable = ["coverage", "interrogate"]
225
209
 
210
+ # Exclude paths from all tools (merged with --exclude CLI flag)
211
+ exclude = ["demo", "docs"]
212
+
226
213
  # Lines of source context shown around each defect in LLM output (default: 15)
227
214
  context_lines = 15
228
215
 
@@ -241,21 +228,23 @@ Tool IDs match the keys in `config/config.yaml`: `compile`, `ruff`, `ty`, `bandi
241
228
  python:
242
229
 
243
230
  compile:
244
- command: "{python} -m compileall -r 10 -j 8 {context_path} -x .*venv"
231
+ command: "{python} -m compileall -r 10 -j 8 \"{context_path}\" -x .*venv"
245
232
  parser: "CompileParser"
246
233
  order: 1
247
234
  warning_threshold: 0.9999
248
235
  error_threshold: 0.9999
249
236
 
250
237
  ruff:
251
- command: "{python} -m ruff check --output-format concise --no-cache {context_path}"
238
+ command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
239
+ exclude_format: " --exclude {path}"
252
240
  parser: "RuffParser"
253
241
  order: 2
254
242
  warning_threshold: 0.9999
255
243
  error_threshold: 0.9
256
244
 
257
245
  ty:
258
- command: "{python} -m ty check --output-format concise --color never {context_path}"
246
+ command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
247
+ exclude_format: " --exclude {path}"
259
248
  parser: "TyParser"
260
249
  order: 3
261
250
  warning_threshold: 0.9999
@@ -265,22 +254,26 @@ python:
265
254
  - ty
266
255
 
267
256
  bandit:
268
- command: "{python} -m bandit -r {context_path} -f json -q -s B101 --severity-level medium --exclude {input_path_posix}/.venv,{input_path_posix}/tests"
257
+ 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}\""
258
+ exclude_format: ",{input_path_posix}/{path}"
269
259
  parser: "BanditParser"
270
260
  order: 4
271
261
  warning_threshold: 0.9999
272
262
  error_threshold: 0.8
273
263
 
274
264
  pytest:
275
- command: "{python} -m pytest -v {context_path}"
265
+ command: "{python} -m pytest -v \"{context_path}\"{exclude}"
266
+ exclude_format: " --ignore {path}"
276
267
  parser: "PytestParser"
277
268
  order: 5
278
269
  warning_threshold: 1.0
279
270
  error_threshold: 1.0
280
271
  run_in_target_env: true
272
+ extra_deps:
273
+ - pytest
281
274
 
282
275
  coverage:
283
- command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest {context_path} && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
276
+ command: "{python} -m coverage run --omit=*/tests/*,*/test_*.py -m pytest \"{context_path}\" && {python} -m coverage report --omit=*/tests/*,*/test_*.py"
284
277
  parser: "CoverageParser"
285
278
  order: 6
286
279
  warning_threshold: 0.9
@@ -291,40 +284,41 @@ python:
291
284
  - pytest
292
285
 
293
286
  radon-cc:
294
- command: "{python} -m radon cc --json {context_path}"
287
+ command: "{python} -m radon cc --json \"{context_path}\""
295
288
  parser: "ComplexityParser"
296
289
  order: 7
297
290
  warning_threshold: 0.6
298
291
  error_threshold: 0.4
299
292
 
300
293
  radon-mi:
301
- command: "{python} -m radon mi -s --json {context_path}"
294
+ command: "{python} -m radon mi -s --json \"{context_path}\""
302
295
  parser: "MaintainabilityParser"
303
296
  order: 8
304
297
  warning_threshold: 0.6
305
298
  error_threshold: 0.4
306
299
 
307
300
  radon-hal:
308
- command: "{python} -m radon hal -f --json {context_path}"
301
+ command: "{python} -m radon hal -f --json \"{context_path}\""
309
302
  parser: "HalsteadParser"
310
303
  order: 9
311
304
  warning_threshold: 0.5
312
305
  error_threshold: 0.3
313
306
 
314
307
  vulture:
315
- command: "{python} -m vulture {context_path} --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
308
+ command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
309
+ exclude_format: ",{path}"
316
310
  parser: "VultureParser"
317
311
  order: 10
318
312
  warning_threshold: 0.9999
319
313
  error_threshold: 0.8
320
314
 
321
315
  interrogate:
322
- command: "{python} -m interrogate {context_path} -v --fail-under 0"
316
+ command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
317
+ exclude_format: " -e {path}"
323
318
  parser: "InterrogateParser"
324
319
  order: 11
325
320
  warning_threshold: 0.8
326
321
  error_threshold: 0.3
327
-
328
322
  ```
329
323
 
330
324
  ## Respect
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-code-quality"
3
- version = "0.1.13"
3
+ version = "0.1.14"
4
4
  description = "Python Code Quality Analysis Tool - feed the results from 11 CQ tools straight into an LLM. Minimal tokens."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -42,3 +42,10 @@ module-name = "py_cq"
42
42
 
43
43
  [project.scripts]
44
44
  cq = "py_cq.main:main"
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ norecursedirs = [".venv", "dist"]
49
+
50
+ [tool.cq]
51
+ exclude = ["demo"]
@@ -4,7 +4,3 @@ The module defines a single function, `hello`, which returns the string
4
4
  `'Hello from py_cq!'`. It can serve as a minimal example, placeholder, or
5
5
  testing stub in larger applications."""
6
6
 
7
-
8
- def hello() -> str:
9
- """Returns the greeting string `'Hello from py_cq!'`."""
10
- return "Hello from py_cq!"
@@ -84,6 +84,7 @@ def _apply_user_config(base: dict[str, ToolConfig], user_cfg: dict) -> dict[str,
84
84
  run_in_target_env=tool_data.get("run_in_target_env", False),
85
85
  extra_deps=tool_data.get("extra_deps", []),
86
86
  parser_config=tool_data.get("parser_config", {}),
87
+ exclude_format=tool_data.get("exclude_format", ""),
87
88
  )
88
89
  except KeyError as e:
89
90
  raise typer.BadParameter(f"[tool.cq.tools.{tool_id}] missing required field {e}")
@@ -131,6 +132,9 @@ def check(
131
132
  skip: str | None = typer.Option(
132
133
  None, "--skip", help="Comma-separated tool IDs to skip (e.g. bandit,vulture)"
133
134
  ),
135
+ exclude: str | None = typer.Option(
136
+ None, "--exclude", help="Comma-separated paths to exclude (e.g. demo,docs)"
137
+ ),
134
138
  ):
135
139
  """Feed the results from 11+ code quality tools to an LLM. Try: cq check . -o llm""" # --help
136
140
  path_obj = Path(path)
@@ -165,9 +169,12 @@ def check(
165
169
  if skip:
166
170
  drop = set(skip.split(","))
167
171
  effective_registry = {k: v for k, v in effective_registry.items() if k not in drop}
172
+ config_excludes: list[str] = user_cfg.get("exclude", [])
173
+ cli_excludes: list[str] = [e.strip() for e in exclude.split(",")] if exclude else []
174
+ excludes = list(dict.fromkeys(config_excludes + cli_excludes))
168
175
  if clear_cache:
169
176
  tool_cache.clear()
170
- tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM))
177
+ tool_results = run_tools(effective_registry.values(), path, workers, early_exit=(output == OutputMode.LLM), excludes=excludes)
171
178
  # for tr in tool_results:
172
179
  # log.debug(json.dumps(tr.to_dict(), indent=2))
173
180
  combined_metrics = aggregate_metrics(path=path, metrics=tool_results)
@@ -8,14 +8,16 @@ python:
8
8
  error_threshold: 0.9999
9
9
 
10
10
  ruff:
11
- command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\""
11
+ command: "{python} -m ruff check --output-format concise --no-cache \"{context_path}\"{exclude}"
12
+ exclude_format: " --exclude {path}"
12
13
  parser: "RuffParser"
13
14
  order: 2
14
15
  warning_threshold: 0.9999
15
16
  error_threshold: 0.9
16
17
 
17
18
  ty:
18
- command: "{python} -m ty check --output-format concise --color never \"{context_path}\""
19
+ command: "{python} -m ty check --output-format concise --color never \"{context_path}\"{exclude}"
20
+ exclude_format: " --exclude {path}"
19
21
  parser: "TyParser"
20
22
  order: 3
21
23
  warning_threshold: 0.9999
@@ -25,14 +27,16 @@ python:
25
27
  - ty
26
28
 
27
29
  bandit:
28
- command: "{python} -m bandit -r \"{context_path}\" -f json -q -s B101 --severity-level medium --exclude \"{input_path_posix}/.venv,{input_path_posix}/tests\""
30
+ 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}\""
31
+ exclude_format: ",{input_path_posix}/{path}"
29
32
  parser: "BanditParser"
30
33
  order: 4
31
34
  warning_threshold: 0.9999
32
35
  error_threshold: 0.8
33
36
 
34
37
  pytest:
35
- command: "{python} -m pytest -v \"{context_path}\""
38
+ command: "{python} -m pytest -v \"{context_path}\"{exclude}"
39
+ exclude_format: " --ignore {path}"
36
40
  parser: "PytestParser"
37
41
  order: 5
38
42
  warning_threshold: 1.0
@@ -74,14 +78,16 @@ python:
74
78
  error_threshold: 0.3
75
79
 
76
80
  vulture:
77
- command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git"
81
+ command: "{python} -m vulture \"{context_path}\" --min-confidence 80 --exclude .venv,dist,.*_cache,docs,.git{exclude}"
82
+ exclude_format: ",{path}"
78
83
  parser: "VultureParser"
79
84
  order: 10
80
85
  warning_threshold: 0.9999
81
86
  error_threshold: 0.8
82
87
 
83
88
  interrogate:
84
- command: "{python} -m interrogate \"{context_path}\" -v --fail-under 0"
89
+ command: "{python} -m interrogate \"{context_path}\" -e tests{exclude} -v --fail-under 0"
90
+ exclude_format: " -e {path}"
85
91
  parser: "InterrogateParser"
86
92
  order: 11
87
93
  warning_threshold: 0.8
@@ -53,7 +53,17 @@ def _dep_in_venv(dep: str, project_root: Path) -> bool:
53
53
  return False
54
54
 
55
55
 
56
- def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
56
+ def _build_exclude_str(exclude_format: str, excludes: list[str], **extra_vars: str) -> str:
57
+ if not exclude_format or not excludes:
58
+ return ""
59
+ parts = []
60
+ for exc in excludes:
61
+ abs_posix_path = Path(exc).resolve().as_posix()
62
+ parts.append(exclude_format.format(path=exc, abs_posix_path=abs_posix_path, **extra_vars))
63
+ return "".join(parts)
64
+
65
+
66
+ def run_tool(tool_config: ToolConfig, context_path: str, excludes: list[str] | None = None) -> RawResult:
57
67
  """Runs a tool defined by its configuration and returns the execution result.
58
68
 
59
69
  Args:
@@ -72,7 +82,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
72
82
  >>> result.return_code
73
83
  0"""
74
84
  python = sys.executable
75
- path = context_path
85
+ path = str(Path(context_path))
76
86
  if tool_config.run_in_target_env:
77
87
  uv = shutil.which("uv")
78
88
  if uv:
@@ -87,10 +97,12 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
87
97
  project_root_path = Path(abs_dir)
88
98
  missing_deps = [d for d in tool_config.extra_deps if not _dep_in_venv(d, project_root_path)]
89
99
  with_flags = " ".join(f"--with {dep}" for dep in missing_deps)
90
- python = f'"{uv}" run --directory "{abs_dir}" {with_flags}'.rstrip()
100
+ no_sync = "--no-sync" if sys.executable.startswith(abs_dir) else ""
101
+ python = f'"{uv}" run {no_sync} --directory "{abs_dir}" {with_flags}'.strip()
91
102
  abs_context_path = str(Path(context_path).resolve())
92
103
  input_path_posix = Path(context_path).as_posix().rstrip("/")
93
- command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, input_path_posix=input_path_posix, python=python)
104
+ exclude = _build_exclude_str(tool_config.exclude_format, excludes or [], input_path_posix=input_path_posix)
105
+ command = tool_config.command.format(context_path=path, abs_context_path=abs_context_path, input_path_posix=input_path_posix, python=python, exclude=exclude)
94
106
  cache_key = f"{command}:{get_context_hash(context_path)}"
95
107
  if cache_key in _cache:
96
108
  log.info(f"Cache hit: {command}")
@@ -110,7 +122,7 @@ def run_tool(tool_config: ToolConfig, context_path: str) -> RawResult:
110
122
  return raw_result
111
123
 
112
124
 
113
- def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False) -> list[ToolResult]:
125
+ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int = 0, early_exit: bool = False, excludes: list[str] | None = None) -> list[ToolResult]:
114
126
  """Run multiple tools and return their parsed results.
115
127
 
116
128
  Runs each tool specified in *tool_configs* on the file or directory at
@@ -152,7 +164,7 @@ def run_tools(tool_configs: Collection[ToolConfig], path: str, max_workers: int
152
164
  >>> results = run_tools(configs, '/path/to/project', parallel=True)"""
153
165
  def _run_and_parse(tool_config: ToolConfig) -> tuple[int, ToolResult]:
154
166
  t0 = time.perf_counter()
155
- raw_result = run_tool(tool_config, path)
167
+ raw_result = run_tool(tool_config, path, excludes)
156
168
  tr = tool_config.parser_class(tool_config.parser_config).parse(raw_result)
157
169
  tr.duration_s = time.perf_counter() - t0
158
170
  return tool_config.order, tr
@@ -22,6 +22,7 @@ class ToolConfig:
22
22
  run_in_target_env: bool = False # If True, run in target project's env via uv
23
23
  extra_deps: list[str] = field(default_factory=list) # Extra deps to inject via uv --with
24
24
  parser_config: dict[str, Any] = field(default_factory=dict)
25
+ exclude_format: str = "" # Per-path template for --exclude injection, e.g. " --exclude {path}"
25
26
 
26
27
 
27
28
  @dataclass
@@ -0,0 +1 @@
1
+ """Tool Response parsers"""
@@ -3,8 +3,8 @@
3
3
  Interrogate is invoked with ``-v --fail-under 0``, producing a table of
4
4
  per-file docstring coverage on stdout::
5
5
 
6
- | src/foo.py | 5 | 2 | 60% |
7
- | TOTAL | 5 | 2 | 60% |
6
+ | src/foo.py | 5 | 2 | 3 | 60% |
7
+ | TOTAL | 5 | 2 | 3 | 60.0% |
8
8
 
9
9
  The parser extracts per-file coverage and the TOTAL row, storing the TOTAL
10
10
  as the ``doc_coverage`` metric (0.0–1.0).
@@ -14,7 +14,7 @@ import re
14
14
 
15
15
  from py_cq.localtypes import AbstractParser, RawResult, ToolResult
16
16
 
17
- _ROW_RE = re.compile(r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+(\d+)%\s+\|")
17
+ _ROW_RE = re.compile(r"^\|\s+(.+?)\s+\|\s+(\d+)\s+\|\s+(\d+)\s+\|\s+\d+\s+\|\s+(\d+(?:\.\d+)?)%\s*\|")
18
18
 
19
19
 
20
20
  class InterrogateParser(AbstractParser):
@@ -30,7 +30,7 @@ class InterrogateParser(AbstractParser):
30
30
  name = m.group(1).strip()
31
31
  total = int(m.group(2))
32
32
  miss = int(m.group(3))
33
- cover = int(m.group(4))
33
+ cover = float(m.group(4))
34
34
  if name == "TOTAL":
35
35
  total_coverage = cover / 100.0
36
36
  elif total > 0:
@@ -30,6 +30,7 @@ def load_tool_configs() -> dict[str, ToolConfig]:
30
30
  run_in_target_env=tool_data.get("run_in_target_env", False),
31
31
  extra_deps=tool_data.get("extra_deps", []),
32
32
  parser_config=tool_data.get("parser_config", {}),
33
+ exclude_format=tool_data.get("exclude_format", ""),
33
34
  )
34
35
  return registry
35
36
 
File without changes