python-vibe-guard 0.11.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.11.0/python_vibe_guard.egg-info → python_vibe_guard-0.12.0}/PKG-INFO +58 -3
  2. python_vibe_guard-0.11.0/PKG-INFO → python_vibe_guard-0.12.0/README.md +56 -17
  3. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyproject.toml +4 -1
  4. python_vibe_guard-0.11.0/README.md → python_vibe_guard-0.12.0/python_vibe_guard.egg-info/PKG-INFO +72 -2
  5. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/SOURCES.txt +6 -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.11.0 → python_vibe_guard-0.12.0}/pyvibe/analyzer.py +97 -8
  9. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/cli.py +77 -18
  10. python_vibe_guard-0.12.0/pyvibe/config.py +99 -0
  11. python_vibe_guard-0.12.0/pyvibe/suppressions.py +53 -0
  12. python_vibe_guard-0.12.0/tests/test_config.py +268 -0
  13. python_vibe_guard-0.12.0/tests/test_suppressions.py +158 -0
  14. python_vibe_guard-0.11.0/pyvibe/__init__.py +0 -1
  15. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
  16. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/entry_points.txt +0 -0
  17. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/top_level.txt +0 -0
  18. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/__main__.py +0 -0
  19. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/autofix.py +0 -0
  20. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/baseline.py +0 -0
  21. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/diff.py +0 -0
  22. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/explain.py +0 -0
  23. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rule_docs.py +0 -0
  24. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/__init__.py +0 -0
  25. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/async_requests.py +0 -0
  26. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/async_sleep.py +0 -0
  27. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/asyncio_run.py +0 -0
  28. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/base.py +0 -0
  29. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/celery_time_limit.py +0 -0
  30. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/contextvar_cleanup.py +0 -0
  31. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/create_task_orphan.py +0 -0
  32. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/ensure_future_orphan.py +0 -0
  33. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
  34. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/httpx_client_sync.py +0 -0
  35. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/httpx_sync.py +0 -0
  36. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/loop_run_until_complete.py +0 -0
  37. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/open_async.py +0 -0
  38. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/os_blocking.py +0 -0
  39. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/queue_put_nowait.py +0 -0
  40. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/retry_no_backoff.py +0 -0
  41. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/silent_except.py +0 -0
  42. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/sqlite_async.py +0 -0
  43. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/subprocess_async.py +0 -0
  44. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/threading_lock.py +0 -0
  45. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/rules/while_true_no_await.py +0 -0
  46. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/pyvibe/sarif.py +0 -0
  47. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/setup.cfg +0 -0
  48. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/tests/test_baseline.py +0 -0
  49. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/tests/test_diff.py +0 -0
  50. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/tests/test_exclude.py +0 -0
  51. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/tests/test_explain.py +0 -0
  52. {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.0}/tests/test_rules.py +0 -0
  53. {python_vibe_guard-0.11.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.11.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
 
@@ -177,6 +178,9 @@ 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
+
180
184
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
181
185
  ```
182
186
 
@@ -216,7 +220,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
216
220
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
217
221
 
218
222
  ─────────────────────────────────────────────
219
- 4 violation(s) in 1 file(s)
223
+ 4 reported · 0 suppressed
220
224
  ```
221
225
 
222
226
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -291,6 +295,57 @@ produces exit code `0`.
291
295
  and reviewed like any other file; gitignore it if each contributor/CI run should
292
296
  regenerate its own baseline instead.
293
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
+
294
349
  ---
295
350
 
296
351
  ## CI/CD integration
@@ -388,7 +443,7 @@ python -m pytest tests/ -v
388
443
  python tests/test_rules.py
389
444
  ```
390
445
 
391
- 268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode 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.
392
447
 
393
448
  ---
394
449
 
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: python-vibe-guard
3
- Version: 0.11.0
4
- Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
- License: MIT
6
- Keywords: async,linter,fastapi,asyncio,static-analysis
7
- Classifier: Development Status :: 3 - Alpha
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Topic :: Software Development :: Quality Assurance
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
-
16
1
  # python-vibe-guard
17
2
 
18
3
  [![tests](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml)
@@ -177,6 +162,9 @@ python -m pyvibe explain PYVIBE-002
177
162
  python -m pyvibe baseline create src/ # snapshot current findings
178
163
  python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
179
164
 
165
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
166
+ python -m pyvibe src/ --verbose
167
+
180
168
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
181
169
  ```
182
170
 
@@ -216,7 +204,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
216
204
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
217
205
 
218
206
  ─────────────────────────────────────────────
219
- 4 violation(s) in 1 file(s)
207
+ 4 reported · 0 suppressed
220
208
  ```
221
209
 
222
210
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -291,6 +279,57 @@ produces exit code `0`.
291
279
  and reviewed like any other file; gitignore it if each contributor/CI run should
292
280
  regenerate its own baseline instead.
293
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
+
294
333
  ---
295
334
 
296
335
  ## CI/CD integration
@@ -388,7 +427,7 @@ python -m pytest tests/ -v
388
427
  python tests/test_rules.py
389
428
  ```
390
429
 
391
- 268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode 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.
392
431
 
393
432
  ---
394
433
 
@@ -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.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,3 +1,19 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-vibe-guard
3
+ Version: 0.12.0
4
+ Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
+ License: MIT
6
+ Keywords: async,linter,fastapi,asyncio,static-analysis
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Software Development :: Quality Assurance
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
16
+
1
17
  # python-vibe-guard
2
18
 
3
19
  [![tests](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml)
@@ -162,6 +178,9 @@ python -m pyvibe explain PYVIBE-002
162
178
  python -m pyvibe baseline create src/ # snapshot current findings
163
179
  python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
164
180
 
181
+ # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
+ python -m pyvibe src/ --verbose
183
+
165
184
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
166
185
  ```
167
186
 
@@ -201,7 +220,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
201
220
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
202
221
 
203
222
  ─────────────────────────────────────────────
204
- 4 violation(s) in 1 file(s)
223
+ 4 reported · 0 suppressed
205
224
  ```
206
225
 
207
226
  `Suggested fix:` blocks are generated from the real code on the flagged line — they are
@@ -276,6 +295,57 @@ produces exit code `0`.
276
295
  and reviewed like any other file; gitignore it if each contributor/CI run should
277
296
  regenerate its own baseline instead.
278
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
+
279
349
  ---
280
350
 
281
351
  ## CI/CD integration
@@ -373,7 +443,7 @@ python -m pytest tests/ -v
373
443
  python tests/test_rules.py
374
444
  ```
375
445
 
376
- 268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode 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.
377
447
 
378
448
  ---
379
449
 
@@ -4,6 +4,7 @@ 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
@@ -11,10 +12,12 @@ pyvibe/analyzer.py
11
12
  pyvibe/autofix.py
12
13
  pyvibe/baseline.py
13
14
  pyvibe/cli.py
15
+ pyvibe/config.py
14
16
  pyvibe/diff.py
15
17
  pyvibe/explain.py
16
18
  pyvibe/rule_docs.py
17
19
  pyvibe/sarif.py
20
+ pyvibe/suppressions.py
18
21
  pyvibe/rules/__init__.py
19
22
  pyvibe/rules/async_requests.py
20
23
  pyvibe/rules/async_sleep.py
@@ -38,8 +41,10 @@ pyvibe/rules/subprocess_async.py
38
41
  pyvibe/rules/threading_lock.py
39
42
  pyvibe/rules/while_true_no_await.py
40
43
  tests/test_baseline.py
44
+ tests/test_config.py
41
45
  tests/test_diff.py
42
46
  tests/test_exclude.py
43
47
  tests/test_explain.py
44
48
  tests/test_rules.py
45
- 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()):