python-vibe-guard 0.10.0__tar.gz → 0.12.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 (53) hide show
  1. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/PKG-INFO +94 -3
  2. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/README.md +92 -2
  3. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyproject.toml +4 -1
  4. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/PKG-INFO +94 -3
  5. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/SOURCES.txt +8 -1
  6. python_vibe_guard-0.12.0/python_vibe_guard.egg-info/requires.txt +3 -0
  7. python_vibe_guard-0.12.0/pyvibe/__init__.py +1 -0
  8. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/analyzer.py +97 -8
  9. python_vibe_guard-0.12.0/pyvibe/baseline.py +73 -0
  10. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/cli.py +253 -20
  11. python_vibe_guard-0.12.0/pyvibe/config.py +99 -0
  12. python_vibe_guard-0.12.0/pyvibe/suppressions.py +53 -0
  13. python_vibe_guard-0.12.0/tests/test_baseline.py +234 -0
  14. python_vibe_guard-0.12.0/tests/test_config.py +268 -0
  15. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_diff.py +5 -0
  16. python_vibe_guard-0.12.0/tests/test_suppressions.py +158 -0
  17. python_vibe_guard-0.10.0/pyvibe/__init__.py +0 -1
  18. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
  19. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/entry_points.txt +0 -0
  20. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/top_level.txt +0 -0
  21. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/__main__.py +0 -0
  22. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/autofix.py +0 -0
  23. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/diff.py +0 -0
  24. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/explain.py +0 -0
  25. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rule_docs.py +0 -0
  26. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/__init__.py +0 -0
  27. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/async_requests.py +0 -0
  28. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/async_sleep.py +0 -0
  29. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/asyncio_run.py +0 -0
  30. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/base.py +0 -0
  31. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/celery_time_limit.py +0 -0
  32. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/contextvar_cleanup.py +0 -0
  33. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/create_task_orphan.py +0 -0
  34. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/ensure_future_orphan.py +0 -0
  35. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
  36. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/httpx_client_sync.py +0 -0
  37. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/httpx_sync.py +0 -0
  38. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/loop_run_until_complete.py +0 -0
  39. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/open_async.py +0 -0
  40. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/os_blocking.py +0 -0
  41. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/queue_put_nowait.py +0 -0
  42. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/retry_no_backoff.py +0 -0
  43. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/silent_except.py +0 -0
  44. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/sqlite_async.py +0 -0
  45. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/subprocess_async.py +0 -0
  46. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/threading_lock.py +0 -0
  47. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/while_true_no_await.py +0 -0
  48. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/sarif.py +0 -0
  49. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/setup.cfg +0 -0
  50. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_exclude.py +0 -0
  51. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_explain.py +0 -0
  52. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_rules.py +0 -0
  53. {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/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.10.0
3
+ Version: 0.12.0
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
 
@@ -173,6 +174,13 @@ python -m pyvibe src/ --exclude tests
173
174
  # Show the research evidence behind a rule (accuracy, false positives, sources)
174
175
  python -m pyvibe explain PYVIBE-002
175
176
 
177
+ # Baseline mode: suppress pre-existing findings, only fail on new ones
178
+ python -m pyvibe baseline create src/ # snapshot current findings
179
+ python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
180
+
181
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
+ python -m pyvibe src/ --verbose
183
+
176
184
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
177
185
  ```
178
186
 
@@ -212,7 +220,7 @@ python -m pyvibe explain PYVIBE-002
212
220
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
213
221
 
214
222
  ─────────────────────────────────────────────
215
- 4 violation(s) in 1 file(s)
223
+ 4 reported · 0 suppressed
216
224
  ```
217
225
 
218
226
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -255,6 +263,89 @@ python -m pyvibe explain PYVIBE-002
255
263
  If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
256
264
  and exits non-zero — it never invents data.
257
265
 
266
+ ### Baseline mode
267
+
268
+ Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
269
+ findings you can't fix before CI needs to go green. Baseline mode snapshots the current
270
+ findings once, then only fails on genuinely **new** ones:
271
+
272
+ ```bash
273
+ # Snapshot every current finding into .pyvibe-baseline.json
274
+ python -m pyvibe baseline create src/
275
+
276
+ # Refuses to run if a baseline already exists — use `update` to overwrite it
277
+ python -m pyvibe baseline update src/
278
+
279
+ # Full scan, but only reports findings NOT already in the baseline
280
+ python -m pyvibe src/ --baseline
281
+ python -m pyvibe src/ --baseline --json
282
+ python -m pyvibe src/ --baseline --sarif
283
+
284
+ # `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
285
+ python -m pyvibe scan src/ --baseline
286
+ ```
287
+
288
+ A finding is considered "already known" on an exact match of `(file path, rule ID, line
289
+ number)`. Anything that doesn't match — a new violation, or an existing one that moved to
290
+ a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
291
+ produces exit code `0`.
292
+
293
+ `.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
294
+ `.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
295
+ and reviewed like any other file; gitignore it if each contributor/CI run should
296
+ regenerate its own baseline instead.
297
+
298
+ ### Suppressing findings
299
+
300
+ For one-off exceptions, use an inline comment right in the source:
301
+
302
+ ```python
303
+ # pyvibe: ignore PYVIBE-008
304
+ conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
305
+
306
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
307
+
308
+ time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
309
+
310
+ # pyvibe: ignore-next-line PYVIBE-008
311
+ conn = sqlite3.connect(...) # suppressed — always targets the next line
312
+ ```
313
+
314
+ `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
315
+ (treated as a regular comment).
316
+
317
+ For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
318
+ from the scan target to find it, same convention as `pyproject.toml`):
319
+
320
+ ```toml
321
+ [tool.pyvibe]
322
+ ignore = ["PYVIBE-019"]
323
+ exclude = ["tests/**", "examples/**", "docs/**"]
324
+
325
+ [tool.pyvibe.severity]
326
+ PYVIBE-008 = "warning"
327
+ ```
328
+
329
+ - `ignore`: rule IDs suppressed everywhere, project-wide.
330
+ - `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
331
+ to skip entirely.
332
+ - `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
333
+ applied after the built-in test-file downgrade.
334
+
335
+ Both mechanisms feed into the same summary line:
336
+
337
+ ```
338
+ 4 reported · 2 suppressed
339
+ ```
340
+
341
+ Add `--verbose` to see exactly what was suppressed and why:
342
+
343
+ ```
344
+ Suppressed:
345
+ PYVIBE-008 app/db.py:42 (inline)
346
+ PYVIBE-019 legacy.py:81 (config)
347
+ ```
348
+
258
349
  ---
259
350
 
260
351
  ## CI/CD integration
@@ -352,7 +443,7 @@ python -m pytest tests/ -v
352
443
  python tests/test_rules.py
353
444
  ```
354
445
 
355
- 226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
446
+ 309 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, and suppressions (inline comments + pyvibe.toml) coverage.
356
447
 
357
448
  ---
358
449
 
@@ -158,6 +158,13 @@ python -m pyvibe src/ --exclude tests
158
158
  # Show the research evidence behind a rule (accuracy, false positives, sources)
159
159
  python -m pyvibe explain PYVIBE-002
160
160
 
161
+ # Baseline mode: suppress pre-existing findings, only fail on new ones
162
+ python -m pyvibe baseline create src/ # snapshot current findings
163
+ python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
164
+
165
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
166
+ python -m pyvibe src/ --verbose
167
+
161
168
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
162
169
  ```
163
170
 
@@ -197,7 +204,7 @@ python -m pyvibe explain PYVIBE-002
197
204
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
198
205
 
199
206
  ─────────────────────────────────────────────
200
- 4 violation(s) in 1 file(s)
207
+ 4 reported · 0 suppressed
201
208
  ```
202
209
 
203
210
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -240,6 +247,89 @@ python -m pyvibe explain PYVIBE-002
240
247
  If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
241
248
  and exits non-zero — it never invents data.
242
249
 
250
+ ### Baseline mode
251
+
252
+ Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
253
+ findings you can't fix before CI needs to go green. Baseline mode snapshots the current
254
+ findings once, then only fails on genuinely **new** ones:
255
+
256
+ ```bash
257
+ # Snapshot every current finding into .pyvibe-baseline.json
258
+ python -m pyvibe baseline create src/
259
+
260
+ # Refuses to run if a baseline already exists — use `update` to overwrite it
261
+ python -m pyvibe baseline update src/
262
+
263
+ # Full scan, but only reports findings NOT already in the baseline
264
+ python -m pyvibe src/ --baseline
265
+ python -m pyvibe src/ --baseline --json
266
+ python -m pyvibe src/ --baseline --sarif
267
+
268
+ # `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
269
+ python -m pyvibe scan src/ --baseline
270
+ ```
271
+
272
+ A finding is considered "already known" on an exact match of `(file path, rule ID, line
273
+ number)`. Anything that doesn't match — a new violation, or an existing one that moved to
274
+ a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
275
+ produces exit code `0`.
276
+
277
+ `.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
278
+ `.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
279
+ and reviewed like any other file; gitignore it if each contributor/CI run should
280
+ regenerate its own baseline instead.
281
+
282
+ ### Suppressing findings
283
+
284
+ For one-off exceptions, use an inline comment right in the source:
285
+
286
+ ```python
287
+ # pyvibe: ignore PYVIBE-008
288
+ conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
289
+
290
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
291
+
292
+ time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
293
+
294
+ # pyvibe: ignore-next-line PYVIBE-008
295
+ conn = sqlite3.connect(...) # suppressed — always targets the next line
296
+ ```
297
+
298
+ `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
299
+ (treated as a regular comment).
300
+
301
+ For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
302
+ from the scan target to find it, same convention as `pyproject.toml`):
303
+
304
+ ```toml
305
+ [tool.pyvibe]
306
+ ignore = ["PYVIBE-019"]
307
+ exclude = ["tests/**", "examples/**", "docs/**"]
308
+
309
+ [tool.pyvibe.severity]
310
+ PYVIBE-008 = "warning"
311
+ ```
312
+
313
+ - `ignore`: rule IDs suppressed everywhere, project-wide.
314
+ - `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
315
+ to skip entirely.
316
+ - `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
317
+ applied after the built-in test-file downgrade.
318
+
319
+ Both mechanisms feed into the same summary line:
320
+
321
+ ```
322
+ 4 reported · 2 suppressed
323
+ ```
324
+
325
+ Add `--verbose` to see exactly what was suppressed and why:
326
+
327
+ ```
328
+ Suppressed:
329
+ PYVIBE-008 app/db.py:42 (inline)
330
+ PYVIBE-019 legacy.py:81 (config)
331
+ ```
332
+
243
333
  ---
244
334
 
245
335
  ## CI/CD integration
@@ -337,7 +427,7 @@ python -m pytest tests/ -v
337
427
  python tests/test_rules.py
338
428
  ```
339
429
 
340
- 226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
430
+ 309 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, and suppressions (inline comments + pyvibe.toml) coverage.
341
431
 
342
432
  ---
343
433
 
@@ -4,10 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-vibe-guard"
7
- version = "0.10.0"
7
+ version = "0.12.0"
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.10.0
3
+ Version: 0.12.0
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
 
@@ -173,6 +174,13 @@ python -m pyvibe src/ --exclude tests
173
174
  # Show the research evidence behind a rule (accuracy, false positives, sources)
174
175
  python -m pyvibe explain PYVIBE-002
175
176
 
177
+ # Baseline mode: suppress pre-existing findings, only fail on new ones
178
+ python -m pyvibe baseline create src/ # snapshot current findings
179
+ python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
180
+
181
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
+ python -m pyvibe src/ --verbose
183
+
176
184
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
177
185
  ```
178
186
 
@@ -212,7 +220,7 @@ python -m pyvibe explain PYVIBE-002
212
220
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
213
221
 
214
222
  ─────────────────────────────────────────────
215
- 4 violation(s) in 1 file(s)
223
+ 4 reported · 0 suppressed
216
224
  ```
217
225
 
218
226
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -255,6 +263,89 @@ python -m pyvibe explain PYVIBE-002
255
263
  If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
256
264
  and exits non-zero — it never invents data.
257
265
 
266
+ ### Baseline mode
267
+
268
+ Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
269
+ findings you can't fix before CI needs to go green. Baseline mode snapshots the current
270
+ findings once, then only fails on genuinely **new** ones:
271
+
272
+ ```bash
273
+ # Snapshot every current finding into .pyvibe-baseline.json
274
+ python -m pyvibe baseline create src/
275
+
276
+ # Refuses to run if a baseline already exists — use `update` to overwrite it
277
+ python -m pyvibe baseline update src/
278
+
279
+ # Full scan, but only reports findings NOT already in the baseline
280
+ python -m pyvibe src/ --baseline
281
+ python -m pyvibe src/ --baseline --json
282
+ python -m pyvibe src/ --baseline --sarif
283
+
284
+ # `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
285
+ python -m pyvibe scan src/ --baseline
286
+ ```
287
+
288
+ A finding is considered "already known" on an exact match of `(file path, rule ID, line
289
+ number)`. Anything that doesn't match — a new violation, or an existing one that moved to
290
+ a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
291
+ produces exit code `0`.
292
+
293
+ `.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
294
+ `.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
295
+ and reviewed like any other file; gitignore it if each contributor/CI run should
296
+ regenerate its own baseline instead.
297
+
298
+ ### Suppressing findings
299
+
300
+ For one-off exceptions, use an inline comment right in the source:
301
+
302
+ ```python
303
+ # pyvibe: ignore PYVIBE-008
304
+ conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
305
+
306
+ conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
307
+
308
+ time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
309
+
310
+ # pyvibe: ignore-next-line PYVIBE-008
311
+ conn = sqlite3.connect(...) # suppressed — always targets the next line
312
+ ```
313
+
314
+ `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
315
+ (treated as a regular comment).
316
+
317
+ For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
318
+ from the scan target to find it, same convention as `pyproject.toml`):
319
+
320
+ ```toml
321
+ [tool.pyvibe]
322
+ ignore = ["PYVIBE-019"]
323
+ exclude = ["tests/**", "examples/**", "docs/**"]
324
+
325
+ [tool.pyvibe.severity]
326
+ PYVIBE-008 = "warning"
327
+ ```
328
+
329
+ - `ignore`: rule IDs suppressed everywhere, project-wide.
330
+ - `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
331
+ to skip entirely.
332
+ - `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
333
+ applied after the built-in test-file downgrade.
334
+
335
+ Both mechanisms feed into the same summary line:
336
+
337
+ ```
338
+ 4 reported · 2 suppressed
339
+ ```
340
+
341
+ Add `--verbose` to see exactly what was suppressed and why:
342
+
343
+ ```
344
+ Suppressed:
345
+ PYVIBE-008 app/db.py:42 (inline)
346
+ PYVIBE-019 legacy.py:81 (config)
347
+ ```
348
+
258
349
  ---
259
350
 
260
351
  ## CI/CD integration
@@ -352,7 +443,7 @@ python -m pytest tests/ -v
352
443
  python tests/test_rules.py
353
444
  ```
354
445
 
355
- 226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
446
+ 309 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, and suppressions (inline comments + pyvibe.toml) coverage.
356
447
 
357
448
  ---
358
449
 
@@ -4,16 +4,20 @@ 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
11
12
  pyvibe/autofix.py
13
+ pyvibe/baseline.py
12
14
  pyvibe/cli.py
15
+ pyvibe/config.py
13
16
  pyvibe/diff.py
14
17
  pyvibe/explain.py
15
18
  pyvibe/rule_docs.py
16
19
  pyvibe/sarif.py
20
+ pyvibe/suppressions.py
17
21
  pyvibe/rules/__init__.py
18
22
  pyvibe/rules/async_requests.py
19
23
  pyvibe/rules/async_sleep.py
@@ -36,8 +40,11 @@ pyvibe/rules/sqlite_async.py
36
40
  pyvibe/rules/subprocess_async.py
37
41
  pyvibe/rules/threading_lock.py
38
42
  pyvibe/rules/while_true_no_await.py
43
+ tests/test_baseline.py
44
+ tests/test_config.py
39
45
  tests/test_diff.py
40
46
  tests/test_exclude.py
41
47
  tests/test_explain.py
42
48
  tests/test_rules.py
43
- tests/test_sarif.py
49
+ tests/test_sarif.py
50
+ 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.0"
@@ -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()):