python-vibe-guard 0.11.0__tar.gz → 0.12.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.
Files changed (55) hide show
  1. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/PKG-INFO +109 -3
  2. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/README.md +107 -2
  3. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyproject.toml +4 -1
  4. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/PKG-INFO +109 -3
  5. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/SOURCES.txt +8 -1
  6. python_vibe_guard-0.12.1/python_vibe_guard.egg-info/requires.txt +3 -0
  7. python_vibe_guard-0.12.1/pyvibe/__init__.py +1 -0
  8. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/analyzer.py +97 -8
  9. python_vibe_guard-0.12.1/pyvibe/audit.py +119 -0
  10. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/cli.py +211 -18
  11. python_vibe_guard-0.12.1/pyvibe/config.py +99 -0
  12. python_vibe_guard-0.12.1/pyvibe/suppressions.py +102 -0
  13. python_vibe_guard-0.12.1/tests/test_audit.py +274 -0
  14. python_vibe_guard-0.12.1/tests/test_config.py +268 -0
  15. python_vibe_guard-0.12.1/tests/test_suppressions.py +158 -0
  16. python_vibe_guard-0.11.0/pyvibe/__init__.py +0 -1
  17. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
  18. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/entry_points.txt +0 -0
  19. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/top_level.txt +0 -0
  20. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/__main__.py +0 -0
  21. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/autofix.py +0 -0
  22. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/baseline.py +0 -0
  23. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/diff.py +0 -0
  24. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/explain.py +0 -0
  25. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rule_docs.py +0 -0
  26. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/__init__.py +0 -0
  27. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/async_requests.py +0 -0
  28. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/async_sleep.py +0 -0
  29. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/asyncio_run.py +0 -0
  30. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/base.py +0 -0
  31. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/celery_time_limit.py +0 -0
  32. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/contextvar_cleanup.py +0 -0
  33. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/create_task_orphan.py +0 -0
  34. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/ensure_future_orphan.py +0 -0
  35. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
  36. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/httpx_client_sync.py +0 -0
  37. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/httpx_sync.py +0 -0
  38. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/loop_run_until_complete.py +0 -0
  39. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/open_async.py +0 -0
  40. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/os_blocking.py +0 -0
  41. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/queue_put_nowait.py +0 -0
  42. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/retry_no_backoff.py +0 -0
  43. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/silent_except.py +0 -0
  44. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/sqlite_async.py +0 -0
  45. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/subprocess_async.py +0 -0
  46. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/threading_lock.py +0 -0
  47. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/while_true_no_await.py +0 -0
  48. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/sarif.py +0 -0
  49. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/setup.cfg +0 -0
  50. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_baseline.py +0 -0
  51. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_diff.py +0 -0
  52. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_exclude.py +0 -0
  53. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_explain.py +0 -0
  54. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_rules.py +0 -0
  55. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_sarif.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-vibe-guard
3
- Version: 0.11.0
3
+ Version: 0.12.1
4
4
  Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
5
  License: MIT
6
6
  Keywords: async,linter,fastapi,asyncio,static-analysis
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
15
16
 
16
17
  # python-vibe-guard
17
18
 
@@ -177,6 +178,14 @@ python -m pyvibe explain PYVIBE-002
177
178
  python -m pyvibe baseline create src/ # snapshot current findings
178
179
  python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
179
180
 
181
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
+ python -m pyvibe src/ --verbose
183
+
184
+ # Audit inline suppressions: justification coverage + orphaned (unused) suppressions
185
+ python -m pyvibe audit src/
186
+ python -m pyvibe audit src/ --json
187
+ python -m pyvibe audit src/ --fail-on-unused --fail-on-unjustified
188
+
180
189
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
181
190
  ```
182
191
 
@@ -216,7 +225,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
216
225
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
217
226
 
218
227
  ─────────────────────────────────────────────
219
- 4 violation(s) in 1 file(s)
228
+ 4 reported · 0 suppressed
220
229
  ```
221
230
 
222
231
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -291,6 +300,103 @@ produces exit code `0`.
291
300
  and reviewed like any other file; gitignore it if each contributor/CI run should
292
301
  regenerate its own baseline instead.
293
302
 
303
+ ### Suppressing findings
304
+
305
+ For one-off exceptions, use an inline comment right in the source:
306
+
307
+ ```python
308
+ # pyvibe: ignore PYVIBE-008
309
+ conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
310
+
311
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
312
+
313
+ time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
314
+
315
+ # pyvibe: ignore-next-line PYVIBE-008
316
+ conn = sqlite3.connect(...) # suppressed — always targets the next line
317
+ ```
318
+
319
+ `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
320
+ (treated as a regular comment).
321
+
322
+ Add an optional justification after `--` — it's free text, stored and shown in `pyvibe audit`:
323
+
324
+ ```python
325
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 -- legacy sqlite wrapper
326
+ # pyvibe: ignore-next-line PYVIBE-008 -- startup only
327
+ conn = sqlite3.connect(...)
328
+ ```
329
+
330
+ For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
331
+ from the scan target to find it, same convention as `pyproject.toml`):
332
+
333
+ ```toml
334
+ [tool.pyvibe]
335
+ ignore = ["PYVIBE-019"]
336
+ exclude = ["tests/**", "examples/**", "docs/**"]
337
+
338
+ [tool.pyvibe.severity]
339
+ PYVIBE-008 = "warning"
340
+ ```
341
+
342
+ - `ignore`: rule IDs suppressed everywhere, project-wide.
343
+ - `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
344
+ to skip entirely.
345
+ - `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
346
+ applied after the built-in test-file downgrade.
347
+
348
+ Both mechanisms feed into the same summary line:
349
+
350
+ ```
351
+ 4 reported · 2 suppressed
352
+ ```
353
+
354
+ Add `--verbose` to see exactly what was suppressed and why:
355
+
356
+ ```
357
+ Suppressed:
358
+ PYVIBE-008 app/db.py:42 (inline)
359
+ PYVIBE-019 legacy.py:81 (config)
360
+ ```
361
+
362
+ ### `pyvibe audit`
363
+
364
+ Audits every inline `# pyvibe: ignore` comment in a codebase: how many carry a justification,
365
+ and how many are **orphaned** — the rule they name never actually fires on the target line,
366
+ usually because the underlying code changed since the suppression was added.
367
+
368
+ ```
369
+ $ pyvibe audit src/
370
+
371
+ Suppressions audit
372
+ ──────────────────
373
+ Total suppressions: 12
374
+ With justification: 9 (75%)
375
+ Without justification: 3
376
+ Unused (no violation found): 2
377
+
378
+ By rule:
379
+ PYVIBE-008 5
380
+ PYVIBE-019 4
381
+ PYVIBE-003 3
382
+
383
+ Unused suppressions:
384
+ app/legacy.py:41 # pyvibe: ignore PYVIBE-008
385
+
386
+ Without justification:
387
+ models/db.py:88 # pyvibe: ignore PYVIBE-019
388
+ ```
389
+
390
+ - `--json` — machine-readable output (`total`, `with_justification`, `without_justification`,
391
+ `unused`, `by_rule`, `unused_suppressions`, `without_justification_suppressions`).
392
+ - `--fail-on-unused` — exit `1` if any orphaned suppression is found.
393
+ - `--fail-on-unjustified` — exit `1` if any suppression is missing a justification.
394
+ - `--max-unused N` — exit `1` if more than `N` orphaned suppressions are found.
395
+ - `--exclude DIR` — same directory-exclusion convention as `pyvibe scan`.
396
+
397
+ With no flags, `pyvibe audit` is purely informational (exit `0`) — the flags above are what
398
+ you wire into CI to enforce a justification/cleanup policy over time.
399
+
294
400
  ---
295
401
 
296
402
  ## CI/CD integration
@@ -388,7 +494,7 @@ python -m pytest tests/ -v
388
494
  python tests/test_rules.py
389
495
  ```
390
496
 
391
- 268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode coverage.
497
+ 331 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, suppressions (inline comments + pyvibe.toml), and `pyvibe audit` (justifications + orphan detection) coverage.
392
498
 
393
499
  ---
394
500
 
@@ -162,6 +162,14 @@ python -m pyvibe explain PYVIBE-002
162
162
  python -m pyvibe baseline create src/ # snapshot current findings
163
163
  python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
164
164
 
165
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
166
+ python -m pyvibe src/ --verbose
167
+
168
+ # Audit inline suppressions: justification coverage + orphaned (unused) suppressions
169
+ python -m pyvibe audit src/
170
+ python -m pyvibe audit src/ --json
171
+ python -m pyvibe audit src/ --fail-on-unused --fail-on-unjustified
172
+
165
173
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
166
174
  ```
167
175
 
@@ -201,7 +209,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
201
209
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
202
210
 
203
211
  ─────────────────────────────────────────────
204
- 4 violation(s) in 1 file(s)
212
+ 4 reported · 0 suppressed
205
213
  ```
206
214
 
207
215
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -276,6 +284,103 @@ produces exit code `0`.
276
284
  and reviewed like any other file; gitignore it if each contributor/CI run should
277
285
  regenerate its own baseline instead.
278
286
 
287
+ ### Suppressing findings
288
+
289
+ For one-off exceptions, use an inline comment right in the source:
290
+
291
+ ```python
292
+ # pyvibe: ignore PYVIBE-008
293
+ conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
294
+
295
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
296
+
297
+ time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
298
+
299
+ # pyvibe: ignore-next-line PYVIBE-008
300
+ conn = sqlite3.connect(...) # suppressed — always targets the next line
301
+ ```
302
+
303
+ `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
304
+ (treated as a regular comment).
305
+
306
+ Add an optional justification after `--` — it's free text, stored and shown in `pyvibe audit`:
307
+
308
+ ```python
309
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 -- legacy sqlite wrapper
310
+ # pyvibe: ignore-next-line PYVIBE-008 -- startup only
311
+ conn = sqlite3.connect(...)
312
+ ```
313
+
314
+ For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
315
+ from the scan target to find it, same convention as `pyproject.toml`):
316
+
317
+ ```toml
318
+ [tool.pyvibe]
319
+ ignore = ["PYVIBE-019"]
320
+ exclude = ["tests/**", "examples/**", "docs/**"]
321
+
322
+ [tool.pyvibe.severity]
323
+ PYVIBE-008 = "warning"
324
+ ```
325
+
326
+ - `ignore`: rule IDs suppressed everywhere, project-wide.
327
+ - `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
328
+ to skip entirely.
329
+ - `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
330
+ applied after the built-in test-file downgrade.
331
+
332
+ Both mechanisms feed into the same summary line:
333
+
334
+ ```
335
+ 4 reported · 2 suppressed
336
+ ```
337
+
338
+ Add `--verbose` to see exactly what was suppressed and why:
339
+
340
+ ```
341
+ Suppressed:
342
+ PYVIBE-008 app/db.py:42 (inline)
343
+ PYVIBE-019 legacy.py:81 (config)
344
+ ```
345
+
346
+ ### `pyvibe audit`
347
+
348
+ Audits every inline `# pyvibe: ignore` comment in a codebase: how many carry a justification,
349
+ and how many are **orphaned** — the rule they name never actually fires on the target line,
350
+ usually because the underlying code changed since the suppression was added.
351
+
352
+ ```
353
+ $ pyvibe audit src/
354
+
355
+ Suppressions audit
356
+ ──────────────────
357
+ Total suppressions: 12
358
+ With justification: 9 (75%)
359
+ Without justification: 3
360
+ Unused (no violation found): 2
361
+
362
+ By rule:
363
+ PYVIBE-008 5
364
+ PYVIBE-019 4
365
+ PYVIBE-003 3
366
+
367
+ Unused suppressions:
368
+ app/legacy.py:41 # pyvibe: ignore PYVIBE-008
369
+
370
+ Without justification:
371
+ models/db.py:88 # pyvibe: ignore PYVIBE-019
372
+ ```
373
+
374
+ - `--json` — machine-readable output (`total`, `with_justification`, `without_justification`,
375
+ `unused`, `by_rule`, `unused_suppressions`, `without_justification_suppressions`).
376
+ - `--fail-on-unused` — exit `1` if any orphaned suppression is found.
377
+ - `--fail-on-unjustified` — exit `1` if any suppression is missing a justification.
378
+ - `--max-unused N` — exit `1` if more than `N` orphaned suppressions are found.
379
+ - `--exclude DIR` — same directory-exclusion convention as `pyvibe scan`.
380
+
381
+ With no flags, `pyvibe audit` is purely informational (exit `0`) — the flags above are what
382
+ you wire into CI to enforce a justification/cleanup policy over time.
383
+
279
384
  ---
280
385
 
281
386
  ## CI/CD integration
@@ -373,7 +478,7 @@ python -m pytest tests/ -v
373
478
  python tests/test_rules.py
374
479
  ```
375
480
 
376
- 268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode coverage.
481
+ 331 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, suppressions (inline comments + pyvibe.toml), and `pyvibe audit` (justifications + orphan detection) coverage.
377
482
 
378
483
  ---
379
484
 
@@ -4,10 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-vibe-guard"
7
- version = "0.11.0"
7
+ version = "0.12.1"
8
8
  description = "Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
+ dependencies = [
12
+ "tomli>=2.0.1; python_version < '3.11'",
13
+ ]
11
14
  license = {text = "MIT"}
12
15
  keywords = ["async", "linter", "fastapi", "asyncio", "static-analysis"]
13
16
  classifiers = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-vibe-guard
3
- Version: 0.11.0
3
+ Version: 0.12.1
4
4
  Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
5
  License: MIT
6
6
  Keywords: async,linter,fastapi,asyncio,static-analysis
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
15
16
 
16
17
  # python-vibe-guard
17
18
 
@@ -177,6 +178,14 @@ python -m pyvibe explain PYVIBE-002
177
178
  python -m pyvibe baseline create src/ # snapshot current findings
178
179
  python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
179
180
 
181
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
+ python -m pyvibe src/ --verbose
183
+
184
+ # Audit inline suppressions: justification coverage + orphaned (unused) suppressions
185
+ python -m pyvibe audit src/
186
+ python -m pyvibe audit src/ --json
187
+ python -m pyvibe audit src/ --fail-on-unused --fail-on-unjustified
188
+
180
189
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
181
190
  ```
182
191
 
@@ -216,7 +225,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
216
225
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
217
226
 
218
227
  ─────────────────────────────────────────────
219
- 4 violation(s) in 1 file(s)
228
+ 4 reported · 0 suppressed
220
229
  ```
221
230
 
222
231
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -291,6 +300,103 @@ produces exit code `0`.
291
300
  and reviewed like any other file; gitignore it if each contributor/CI run should
292
301
  regenerate its own baseline instead.
293
302
 
303
+ ### Suppressing findings
304
+
305
+ For one-off exceptions, use an inline comment right in the source:
306
+
307
+ ```python
308
+ # pyvibe: ignore PYVIBE-008
309
+ conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
310
+
311
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
312
+
313
+ time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
314
+
315
+ # pyvibe: ignore-next-line PYVIBE-008
316
+ conn = sqlite3.connect(...) # suppressed — always targets the next line
317
+ ```
318
+
319
+ `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
320
+ (treated as a regular comment).
321
+
322
+ Add an optional justification after `--` — it's free text, stored and shown in `pyvibe audit`:
323
+
324
+ ```python
325
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 -- legacy sqlite wrapper
326
+ # pyvibe: ignore-next-line PYVIBE-008 -- startup only
327
+ conn = sqlite3.connect(...)
328
+ ```
329
+
330
+ For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
331
+ from the scan target to find it, same convention as `pyproject.toml`):
332
+
333
+ ```toml
334
+ [tool.pyvibe]
335
+ ignore = ["PYVIBE-019"]
336
+ exclude = ["tests/**", "examples/**", "docs/**"]
337
+
338
+ [tool.pyvibe.severity]
339
+ PYVIBE-008 = "warning"
340
+ ```
341
+
342
+ - `ignore`: rule IDs suppressed everywhere, project-wide.
343
+ - `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
344
+ to skip entirely.
345
+ - `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
346
+ applied after the built-in test-file downgrade.
347
+
348
+ Both mechanisms feed into the same summary line:
349
+
350
+ ```
351
+ 4 reported · 2 suppressed
352
+ ```
353
+
354
+ Add `--verbose` to see exactly what was suppressed and why:
355
+
356
+ ```
357
+ Suppressed:
358
+ PYVIBE-008 app/db.py:42 (inline)
359
+ PYVIBE-019 legacy.py:81 (config)
360
+ ```
361
+
362
+ ### `pyvibe audit`
363
+
364
+ Audits every inline `# pyvibe: ignore` comment in a codebase: how many carry a justification,
365
+ and how many are **orphaned** — the rule they name never actually fires on the target line,
366
+ usually because the underlying code changed since the suppression was added.
367
+
368
+ ```
369
+ $ pyvibe audit src/
370
+
371
+ Suppressions audit
372
+ ──────────────────
373
+ Total suppressions: 12
374
+ With justification: 9 (75%)
375
+ Without justification: 3
376
+ Unused (no violation found): 2
377
+
378
+ By rule:
379
+ PYVIBE-008 5
380
+ PYVIBE-019 4
381
+ PYVIBE-003 3
382
+
383
+ Unused suppressions:
384
+ app/legacy.py:41 # pyvibe: ignore PYVIBE-008
385
+
386
+ Without justification:
387
+ models/db.py:88 # pyvibe: ignore PYVIBE-019
388
+ ```
389
+
390
+ - `--json` — machine-readable output (`total`, `with_justification`, `without_justification`,
391
+ `unused`, `by_rule`, `unused_suppressions`, `without_justification_suppressions`).
392
+ - `--fail-on-unused` — exit `1` if any orphaned suppression is found.
393
+ - `--fail-on-unjustified` — exit `1` if any suppression is missing a justification.
394
+ - `--max-unused N` — exit `1` if more than `N` orphaned suppressions are found.
395
+ - `--exclude DIR` — same directory-exclusion convention as `pyvibe scan`.
396
+
397
+ With no flags, `pyvibe audit` is purely informational (exit `0`) — the flags above are what
398
+ you wire into CI to enforce a justification/cleanup policy over time.
399
+
294
400
  ---
295
401
 
296
402
  ## CI/CD integration
@@ -388,7 +494,7 @@ python -m pytest tests/ -v
388
494
  python tests/test_rules.py
389
495
  ```
390
496
 
391
- 268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode coverage.
497
+ 331 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, suppressions (inline comments + pyvibe.toml), and `pyvibe audit` (justifications + orphan detection) coverage.
392
498
 
393
499
  ---
394
500
 
@@ -4,17 +4,21 @@ python_vibe_guard.egg-info/PKG-INFO
4
4
  python_vibe_guard.egg-info/SOURCES.txt
5
5
  python_vibe_guard.egg-info/dependency_links.txt
6
6
  python_vibe_guard.egg-info/entry_points.txt
7
+ python_vibe_guard.egg-info/requires.txt
7
8
  python_vibe_guard.egg-info/top_level.txt
8
9
  pyvibe/__init__.py
9
10
  pyvibe/__main__.py
10
11
  pyvibe/analyzer.py
12
+ pyvibe/audit.py
11
13
  pyvibe/autofix.py
12
14
  pyvibe/baseline.py
13
15
  pyvibe/cli.py
16
+ pyvibe/config.py
14
17
  pyvibe/diff.py
15
18
  pyvibe/explain.py
16
19
  pyvibe/rule_docs.py
17
20
  pyvibe/sarif.py
21
+ pyvibe/suppressions.py
18
22
  pyvibe/rules/__init__.py
19
23
  pyvibe/rules/async_requests.py
20
24
  pyvibe/rules/async_sleep.py
@@ -37,9 +41,12 @@ pyvibe/rules/sqlite_async.py
37
41
  pyvibe/rules/subprocess_async.py
38
42
  pyvibe/rules/threading_lock.py
39
43
  pyvibe/rules/while_true_no_await.py
44
+ tests/test_audit.py
40
45
  tests/test_baseline.py
46
+ tests/test_config.py
41
47
  tests/test_diff.py
42
48
  tests/test_exclude.py
43
49
  tests/test_explain.py
44
50
  tests/test_rules.py
45
- tests/test_sarif.py
51
+ tests/test_sarif.py
52
+ tests/test_suppressions.py
@@ -0,0 +1,3 @@
1
+
2
+ [:python_version < "3.11"]
3
+ tomli>=2.0.1
@@ -0,0 +1 @@
1
+ __version__ = "0.12.1"
@@ -1,8 +1,10 @@
1
1
  import ast
2
2
  from pathlib import Path
3
- from typing import FrozenSet, List, Optional
3
+ from typing import FrozenSet, List, Optional, Tuple
4
4
 
5
+ from pyvibe.config import PyvibeConfig, is_excluded
5
6
  from pyvibe.rules.base import Violation
7
+ from pyvibe.suppressions import parse_inline_suppressions
6
8
  from pyvibe.rules.async_sleep import AsyncSleepRule
7
9
  from pyvibe.rules.async_requests import AsyncRequestsRule
8
10
  from pyvibe.rules.asyncio_run import AsyncioRunRule
@@ -90,22 +92,29 @@ def _is_test_file(filepath: str) -> bool:
90
92
  )
91
93
 
92
94
 
93
- def analyze_source(
95
+ def analyze_source_full(
94
96
  source: str,
95
97
  filepath: str = "<string>",
96
98
  *,
97
99
  downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
98
- ) -> List[Violation]:
99
- """Parse source and run all rules. Returns list of violations.
100
+ config: Optional[PyvibeConfig] = None,
101
+ ) -> Tuple[List[Violation], List[Tuple[Violation, str]]]:
102
+ """Parse source and run all rules. Returns (reported, suppressed) where
103
+ suppressed is a list of (Violation, reason) with reason "inline" (a
104
+ `# pyvibe: ignore ...` comment) or "config" (pyvibe.toml's [tool.pyvibe]
105
+ ignore list).
100
106
 
101
107
  downgrade_in_tests: rule IDs whose severity is lowered to WARNING when
102
108
  the file is detected as a test file. Pass frozenset() to disable all
103
109
  downgrading, or ALL_RULE_IDS to downgrade every rule.
110
+
111
+ config: optional PyvibeConfig (see pyvibe/config.py) applying a global
112
+ ignore list and per-rule severity overrides on top of the above.
104
113
  """
105
114
  try:
106
115
  tree = ast.parse(source)
107
116
  except SyntaxError:
108
- return []
117
+ return [], []
109
118
 
110
119
  source_lines = source.splitlines()
111
120
  violations = []
@@ -124,8 +133,53 @@ def analyze_source(
124
133
  if v.rule_id in downgrade_in_tests:
125
134
  v.severity = "WARNING"
126
135
 
136
+ if config and config.severity:
137
+ for v in violations:
138
+ if v.rule_id in config.severity:
139
+ v.severity = config.severity[v.rule_id]
140
+
127
141
  violations.sort(key=lambda v: v.line)
128
- return violations
142
+
143
+ inline_suppressions = parse_inline_suppressions(source)
144
+ reported: List[Violation] = []
145
+ suppressed: List[Tuple[Violation, str]] = []
146
+ for v in violations:
147
+ if v.rule_id in inline_suppressions.get(v.line, frozenset()):
148
+ suppressed.append((v, "inline"))
149
+ elif config and v.rule_id in config.ignore:
150
+ suppressed.append((v, "config"))
151
+ else:
152
+ reported.append(v)
153
+
154
+ return reported, suppressed
155
+
156
+
157
+ def analyze_source(
158
+ source: str,
159
+ filepath: str = "<string>",
160
+ *,
161
+ downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
162
+ config: Optional[PyvibeConfig] = None,
163
+ ) -> List[Violation]:
164
+ """Parse source and run all rules. Returns the reported (non-suppressed)
165
+ violations — see analyze_source_full() for the suppressed ones too.
166
+ """
167
+ reported, _ = analyze_source_full(
168
+ source, filepath, downgrade_in_tests=downgrade_in_tests, config=config
169
+ )
170
+ return reported
171
+
172
+
173
+ def analyze_file_full(
174
+ path: Path,
175
+ *,
176
+ downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
177
+ config: Optional[PyvibeConfig] = None,
178
+ ) -> Tuple[List[Violation], List[Tuple[Violation, str]]]:
179
+ source = path.read_text(encoding="utf-8", errors="ignore")
180
+ return analyze_source_full(
181
+ source, filepath=str(path), downgrade_in_tests=downgrade_in_tests, config=config
182
+ )
129
183
 
130
184
 
131
185
  def analyze_file(
@@ -133,12 +187,15 @@ def analyze_file(
133
187
  *,
134
188
  downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
135
189
  line_filter: Optional[FrozenSet[int]] = None,
190
+ config: Optional[PyvibeConfig] = None,
136
191
  ) -> List[Violation]:
137
192
  """line_filter: if given, only violations whose `line` is in this set
138
193
  are returned (used by `pyvibe review` to report only diff-touched lines).
139
194
  """
140
195
  source = path.read_text(encoding="utf-8", errors="ignore")
141
- violations = analyze_source(source, filepath=str(path), downgrade_in_tests=downgrade_in_tests)
196
+ violations = analyze_source(
197
+ source, filepath=str(path), downgrade_in_tests=downgrade_in_tests, config=config
198
+ )
142
199
  if line_filter is not None:
143
200
  violations = [v for v in violations if v.line in line_filter]
144
201
  return violations
@@ -156,23 +213,55 @@ def analyze_directory(
156
213
  *,
157
214
  skip_test_files: bool = False,
158
215
  downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
216
+ config: Optional[PyvibeConfig] = None,
159
217
  ) -> dict:
160
218
  """Walk directory and analyze all .py files. Returns {path: [violations]}.
161
219
 
162
220
  Directories whose *name* appears in `exclude` are skipped entirely.
163
221
  skip_test_files: if True, files matching test_*.py / *_test.py / tests/* are
164
222
  omitted from results entirely rather than downgraded.
223
+ config: optional PyvibeConfig — files matching its exclude glob patterns
224
+ are also skipped entirely.
165
225
  """
166
226
  results = {}
167
227
  for py_file in _walk(root, exclude):
168
228
  if skip_test_files and _is_test_file(str(py_file)):
169
229
  continue
170
- violations = analyze_file(py_file, downgrade_in_tests=downgrade_in_tests)
230
+ if config and is_excluded(py_file, config):
231
+ continue
232
+ violations = analyze_file(py_file, downgrade_in_tests=downgrade_in_tests, config=config)
171
233
  if violations:
172
234
  results[py_file] = violations
173
235
  return results
174
236
 
175
237
 
238
+ def analyze_directory_full(
239
+ root: Path,
240
+ exclude: frozenset = DEFAULT_EXCLUDES,
241
+ *,
242
+ skip_test_files: bool = False,
243
+ downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
244
+ config: Optional[PyvibeConfig] = None,
245
+ ) -> Tuple[dict, List[Tuple[Path, Violation, str]]]:
246
+ """Like analyze_directory(), but also returns the suppressed findings
247
+ (across every scanned file) as a flat list of (path, Violation, reason).
248
+ """
249
+ results = {}
250
+ all_suppressed: List[Tuple[Path, Violation, str]] = []
251
+ for py_file in _walk(root, exclude):
252
+ if skip_test_files and _is_test_file(str(py_file)):
253
+ continue
254
+ if config and is_excluded(py_file, config):
255
+ continue
256
+ reported, suppressed = analyze_file_full(
257
+ py_file, downgrade_in_tests=downgrade_in_tests, config=config
258
+ )
259
+ if reported:
260
+ results[py_file] = reported
261
+ all_suppressed.extend((py_file, v, reason) for v, reason in suppressed)
262
+ return results, all_suppressed
263
+
264
+
176
265
  def _walk(root: Path, exclude: frozenset):
177
266
  """Yield .py files under root, skipping any directory in exclude."""
178
267
  for entry in sorted(root.iterdir()):