python-vibe-guard 0.12.0__tar.gz → 0.12.2__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 (78) hide show
  1. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/PKG-INFO +61 -4
  2. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/README.md +60 -3
  3. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyproject.toml +4 -1
  4. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/PKG-INFO +61 -4
  5. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/SOURCES.txt +24 -0
  6. python_vibe_guard-0.12.2/pyvibe/__init__.py +1 -0
  7. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-001.md +172 -0
  8. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-002.md +86 -0
  9. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-003.md +64 -0
  10. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-004.md +93 -0
  11. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-005.md +294 -0
  12. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-006.md +85 -0
  13. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-007.md +85 -0
  14. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-008.md +91 -0
  15. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-009.md +289 -0
  16. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-010.md +76 -0
  17. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-011.md +61 -0
  18. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-012.md +85 -0
  19. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-013.md +362 -0
  20. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-014.md +82 -0
  21. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-015.md +75 -0
  22. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-016.md +62 -0
  23. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-017.md +360 -0
  24. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-018.md +96 -0
  25. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-019.md +988 -0
  26. python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-020.md +87 -0
  27. python_vibe_guard-0.12.2/pyvibe/_evidence/precision-audit.md +1241 -0
  28. python_vibe_guard-0.12.2/pyvibe/audit.py +119 -0
  29. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/cli.py +134 -0
  30. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/explain.py +10 -5
  31. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/celery_time_limit.py +2 -1
  32. python_vibe_guard-0.12.2/pyvibe/suppressions.py +102 -0
  33. python_vibe_guard-0.12.2/tests/test_audit.py +274 -0
  34. python_vibe_guard-0.12.2/tests/test_evidence_sync.py +57 -0
  35. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_suppressions.py +32 -0
  36. python_vibe_guard-0.12.0/pyvibe/__init__.py +0 -1
  37. python_vibe_guard-0.12.0/pyvibe/suppressions.py +0 -53
  38. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
  39. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/entry_points.txt +0 -0
  40. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/requires.txt +0 -0
  41. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/top_level.txt +0 -0
  42. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/__main__.py +0 -0
  43. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/analyzer.py +0 -0
  44. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/autofix.py +0 -0
  45. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/baseline.py +0 -0
  46. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/config.py +0 -0
  47. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/diff.py +0 -0
  48. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rule_docs.py +0 -0
  49. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/__init__.py +0 -0
  50. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/async_requests.py +0 -0
  51. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/async_sleep.py +0 -0
  52. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/asyncio_run.py +0 -0
  53. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/base.py +0 -0
  54. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/contextvar_cleanup.py +0 -0
  55. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/create_task_orphan.py +0 -0
  56. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/ensure_future_orphan.py +0 -0
  57. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
  58. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/httpx_client_sync.py +0 -0
  59. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/httpx_sync.py +0 -0
  60. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/loop_run_until_complete.py +0 -0
  61. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/open_async.py +0 -0
  62. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/os_blocking.py +0 -0
  63. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/queue_put_nowait.py +0 -0
  64. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/retry_no_backoff.py +0 -0
  65. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/silent_except.py +0 -0
  66. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/sqlite_async.py +0 -0
  67. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/subprocess_async.py +0 -0
  68. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/threading_lock.py +0 -0
  69. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/while_true_no_await.py +0 -0
  70. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/sarif.py +0 -0
  71. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/setup.cfg +0 -0
  72. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_baseline.py +0 -0
  73. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_config.py +0 -0
  74. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_diff.py +0 -0
  75. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_exclude.py +0 -0
  76. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_explain.py +0 -0
  77. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_rules.py +0 -0
  78. {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/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.12.0
3
+ Version: 0.12.2
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
@@ -79,7 +79,7 @@ Patterns that introduce data races, swallowed errors, or runaway loops under con
79
79
  | PYVIBE-020 | `put_nowait()` without `asyncio.QueueFull` handler | any | `QueueFull` propagates unhandled; item is silently lost on bounded queues |
80
80
 
81
81
  **Severity notes:**
82
- - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# noqa: PYVIBE-005`.
82
+ - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# pyvibe: ignore PYVIBE-005` on the task's `def` line.
83
83
  - PYVIBE-009: `CRITICAL` in production files; automatically downgraded to `WARNING` in test files (`test_*.py`, `*_test.py`, `tests/`). **Context matters:** `open()` in a hot-path request handler blocks all concurrent coroutines (CRITICAL); `open()` in a startup/lifespan function runs before requests are served and has zero practical impact. The rule cannot distinguish these contexts via AST — if you use `open()` in an `async def` lifespan or one-time initializer, the idiomatic fix is to use plain `def` instead (FastAPI's own docs do this), which avoids the flag entirely.
84
84
  - PYVIBE-017: bare `except` → `CRITICAL` (catches `KeyboardInterrupt`/`SystemExit`); `except Exception` with empty body → `WARNING`. Specific exceptions (`except ValueError: pass`) are not flagged.
85
85
  - PYVIBE-013 in test files: automatically downgraded to `WARNING` in files matching `test_*.py`, `*_test.py`, or paths under `tests/` — exceptions should propagate for assertions in test code.
@@ -181,6 +181,11 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
181
181
  # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
182
  python -m pyvibe src/ --verbose
183
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
+
184
189
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
185
190
  ```
186
191
 
@@ -314,6 +319,14 @@ conn = sqlite3.connect(...) # suppressed — always targets the nex
314
319
  `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
315
320
  (treated as a regular comment).
316
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
+
317
330
  For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
318
331
  from the scan target to find it, same convention as `pyproject.toml`):
319
332
 
@@ -346,6 +359,44 @@ Add `--verbose` to see exactly what was suppressed and why:
346
359
  PYVIBE-019 legacy.py:81 (config)
347
360
  ```
348
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
+
349
400
  ---
350
401
 
351
402
  ## CI/CD integration
@@ -400,7 +451,7 @@ Add to your `.pre-commit-config.yaml`:
400
451
  ```yaml
401
452
  repos:
402
453
  - repo: https://github.com/Joaquinriosheredia/python-vibe-guard
403
- rev: v0.7.0
454
+ rev: v0.12.2
404
455
  hooks:
405
456
  - id: python-vibe-guard
406
457
  ```
@@ -443,7 +494,13 @@ python -m pytest tests/ -v
443
494
  python tests/test_rules.py
444
495
  ```
445
496
 
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.
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.
498
+
499
+ ---
500
+
501
+ ## Documentation
502
+
503
+ - [research/public-findings.md](research/public-findings.md) — real findings reported to public open source projects, including confirmed cases and cases where context changed the outcome.
447
504
 
448
505
  ---
449
506
 
@@ -63,7 +63,7 @@ Patterns that introduce data races, swallowed errors, or runaway loops under con
63
63
  | PYVIBE-020 | `put_nowait()` without `asyncio.QueueFull` handler | any | `QueueFull` propagates unhandled; item is silently lost on bounded queues |
64
64
 
65
65
  **Severity notes:**
66
- - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# noqa: PYVIBE-005`.
66
+ - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# pyvibe: ignore PYVIBE-005` on the task's `def` line.
67
67
  - PYVIBE-009: `CRITICAL` in production files; automatically downgraded to `WARNING` in test files (`test_*.py`, `*_test.py`, `tests/`). **Context matters:** `open()` in a hot-path request handler blocks all concurrent coroutines (CRITICAL); `open()` in a startup/lifespan function runs before requests are served and has zero practical impact. The rule cannot distinguish these contexts via AST — if you use `open()` in an `async def` lifespan or one-time initializer, the idiomatic fix is to use plain `def` instead (FastAPI's own docs do this), which avoids the flag entirely.
68
68
  - PYVIBE-017: bare `except` → `CRITICAL` (catches `KeyboardInterrupt`/`SystemExit`); `except Exception` with empty body → `WARNING`. Specific exceptions (`except ValueError: pass`) are not flagged.
69
69
  - PYVIBE-013 in test files: automatically downgraded to `WARNING` in files matching `test_*.py`, `*_test.py`, or paths under `tests/` — exceptions should propagate for assertions in test code.
@@ -165,6 +165,11 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
165
165
  # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
166
166
  python -m pyvibe src/ --verbose
167
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
+
168
173
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
169
174
  ```
170
175
 
@@ -298,6 +303,14 @@ conn = sqlite3.connect(...) # suppressed — always targets the nex
298
303
  `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
299
304
  (treated as a regular comment).
300
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
+
301
314
  For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
302
315
  from the scan target to find it, same convention as `pyproject.toml`):
303
316
 
@@ -330,6 +343,44 @@ Add `--verbose` to see exactly what was suppressed and why:
330
343
  PYVIBE-019 legacy.py:81 (config)
331
344
  ```
332
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
+
333
384
  ---
334
385
 
335
386
  ## CI/CD integration
@@ -384,7 +435,7 @@ Add to your `.pre-commit-config.yaml`:
384
435
  ```yaml
385
436
  repos:
386
437
  - repo: https://github.com/Joaquinriosheredia/python-vibe-guard
387
- rev: v0.7.0
438
+ rev: v0.12.2
388
439
  hooks:
389
440
  - id: python-vibe-guard
390
441
  ```
@@ -427,7 +478,13 @@ python -m pytest tests/ -v
427
478
  python tests/test_rules.py
428
479
  ```
429
480
 
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.
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.
482
+
483
+ ---
484
+
485
+ ## Documentation
486
+
487
+ - [research/public-findings.md](research/public-findings.md) — real findings reported to public open source projects, including confirmed cases and cases where context changed the outcome.
431
488
 
432
489
  ---
433
490
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-vibe-guard"
7
- version = "0.12.0"
7
+ version = "0.12.2"
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"
@@ -28,3 +28,6 @@ pyvibe = "pyvibe.cli:main"
28
28
  [tool.setuptools.packages.find]
29
29
  where = ["."]
30
30
  include = ["pyvibe*"]
31
+
32
+ [tool.setuptools.package-data]
33
+ pyvibe = ["_evidence/*.md", "_evidence/accepted/*.md"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-vibe-guard
3
- Version: 0.12.0
3
+ Version: 0.12.2
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
@@ -79,7 +79,7 @@ Patterns that introduce data races, swallowed errors, or runaway loops under con
79
79
  | PYVIBE-020 | `put_nowait()` without `asyncio.QueueFull` handler | any | `QueueFull` propagates unhandled; item is silently lost on bounded queues |
80
80
 
81
81
  **Severity notes:**
82
- - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# noqa: PYVIBE-005`.
82
+ - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# pyvibe: ignore PYVIBE-005` on the task's `def` line.
83
83
  - PYVIBE-009: `CRITICAL` in production files; automatically downgraded to `WARNING` in test files (`test_*.py`, `*_test.py`, `tests/`). **Context matters:** `open()` in a hot-path request handler blocks all concurrent coroutines (CRITICAL); `open()` in a startup/lifespan function runs before requests are served and has zero practical impact. The rule cannot distinguish these contexts via AST — if you use `open()` in an `async def` lifespan or one-time initializer, the idiomatic fix is to use plain `def` instead (FastAPI's own docs do this), which avoids the flag entirely.
84
84
  - PYVIBE-017: bare `except` → `CRITICAL` (catches `KeyboardInterrupt`/`SystemExit`); `except Exception` with empty body → `WARNING`. Specific exceptions (`except ValueError: pass`) are not flagged.
85
85
  - PYVIBE-013 in test files: automatically downgraded to `WARNING` in files matching `test_*.py`, `*_test.py`, or paths under `tests/` — exceptions should propagate for assertions in test code.
@@ -181,6 +181,11 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
181
181
  # Show suppressed findings (inline comments + pyvibe.toml) alongside the report
182
182
  python -m pyvibe src/ --verbose
183
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
+
184
189
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
185
190
  ```
186
191
 
@@ -314,6 +319,14 @@ conn = sqlite3.connect(...) # suppressed — always targets the nex
314
319
  `pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
315
320
  (treated as a regular comment).
316
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
+
317
330
  For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
318
331
  from the scan target to find it, same convention as `pyproject.toml`):
319
332
 
@@ -346,6 +359,44 @@ Add `--verbose` to see exactly what was suppressed and why:
346
359
  PYVIBE-019 legacy.py:81 (config)
347
360
  ```
348
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
+
349
400
  ---
350
401
 
351
402
  ## CI/CD integration
@@ -400,7 +451,7 @@ Add to your `.pre-commit-config.yaml`:
400
451
  ```yaml
401
452
  repos:
402
453
  - repo: https://github.com/Joaquinriosheredia/python-vibe-guard
403
- rev: v0.7.0
454
+ rev: v0.12.2
404
455
  hooks:
405
456
  - id: python-vibe-guard
406
457
  ```
@@ -443,7 +494,13 @@ python -m pytest tests/ -v
443
494
  python tests/test_rules.py
444
495
  ```
445
496
 
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.
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.
498
+
499
+ ---
500
+
501
+ ## Documentation
502
+
503
+ - [research/public-findings.md](research/public-findings.md) — real findings reported to public open source projects, including confirmed cases and cases where context changed the outcome.
447
504
 
448
505
  ---
449
506
 
@@ -9,6 +9,7 @@ python_vibe_guard.egg-info/top_level.txt
9
9
  pyvibe/__init__.py
10
10
  pyvibe/__main__.py
11
11
  pyvibe/analyzer.py
12
+ pyvibe/audit.py
12
13
  pyvibe/autofix.py
13
14
  pyvibe/baseline.py
14
15
  pyvibe/cli.py
@@ -18,6 +19,27 @@ pyvibe/explain.py
18
19
  pyvibe/rule_docs.py
19
20
  pyvibe/sarif.py
20
21
  pyvibe/suppressions.py
22
+ pyvibe/_evidence/precision-audit.md
23
+ pyvibe/_evidence/accepted/PYVIBE-001.md
24
+ pyvibe/_evidence/accepted/PYVIBE-002.md
25
+ pyvibe/_evidence/accepted/PYVIBE-003.md
26
+ pyvibe/_evidence/accepted/PYVIBE-004.md
27
+ pyvibe/_evidence/accepted/PYVIBE-005.md
28
+ pyvibe/_evidence/accepted/PYVIBE-006.md
29
+ pyvibe/_evidence/accepted/PYVIBE-007.md
30
+ pyvibe/_evidence/accepted/PYVIBE-008.md
31
+ pyvibe/_evidence/accepted/PYVIBE-009.md
32
+ pyvibe/_evidence/accepted/PYVIBE-010.md
33
+ pyvibe/_evidence/accepted/PYVIBE-011.md
34
+ pyvibe/_evidence/accepted/PYVIBE-012.md
35
+ pyvibe/_evidence/accepted/PYVIBE-013.md
36
+ pyvibe/_evidence/accepted/PYVIBE-014.md
37
+ pyvibe/_evidence/accepted/PYVIBE-015.md
38
+ pyvibe/_evidence/accepted/PYVIBE-016.md
39
+ pyvibe/_evidence/accepted/PYVIBE-017.md
40
+ pyvibe/_evidence/accepted/PYVIBE-018.md
41
+ pyvibe/_evidence/accepted/PYVIBE-019.md
42
+ pyvibe/_evidence/accepted/PYVIBE-020.md
21
43
  pyvibe/rules/__init__.py
22
44
  pyvibe/rules/async_requests.py
23
45
  pyvibe/rules/async_sleep.py
@@ -40,9 +62,11 @@ pyvibe/rules/sqlite_async.py
40
62
  pyvibe/rules/subprocess_async.py
41
63
  pyvibe/rules/threading_lock.py
42
64
  pyvibe/rules/while_true_no_await.py
65
+ tests/test_audit.py
43
66
  tests/test_baseline.py
44
67
  tests/test_config.py
45
68
  tests/test_diff.py
69
+ tests/test_evidence_sync.py
46
70
  tests/test_exclude.py
47
71
  tests/test_explain.py
48
72
  tests/test_rules.py
@@ -0,0 +1 @@
1
+ __version__ = "0.12.2"
@@ -0,0 +1,172 @@
1
+ # PYVIBE-001 — time.sleep() en async def
2
+
3
+ **Severidad:** CRITICAL
4
+ **Archivo:** `pyvibe/rules/async_sleep.py`
5
+ **Patrón:** `time.sleep(N)` llamado directamente dentro de `async def`
6
+
7
+ ---
8
+
9
+ ## Datos objetivos
10
+
11
+ | Métrica | 100 repos | 250 repos |
12
+ |---------|-----------|-----------|
13
+ | Repos afectados | 16/100 (16.0%) | 31/250 (12.4%) |
14
+ | Total hits | 55 | 87 |
15
+ | Estabilidad 100→250 | Alta (−3.6 pp) | |
16
+ | Falsos positivos documentados | 0 | |
17
+
18
+ ## Repos representativos (sweep 250)
19
+
20
+ - `home-assistant/core` — 17 hits
21
+ - `pmh1314520/WebRPA` — 9 hits
22
+ - `MODSetter/SurfSense` — 7 hits
23
+ - `IBM/mcp-context-forge` — 6 hits
24
+ - `learning-at-home/hivemind` — 5 hits
25
+
26
+ ---
27
+
28
+ ## Evidence Review Protocol v1
29
+
30
+ ### Paso 1 — Documentación oficial
31
+
32
+ **Resultado: CONFIRMADO — mención explícita en docs.python.org**
33
+
34
+ `docs.python.org/3/library/asyncio-task.html` (sección `asyncio.to_thread()`) usa `time.sleep()` como el ejemplo canónico de llamada bloqueante, con la nota explícita:
35
+
36
+ > *"Note that time.sleep() can be replaced with any blocking IO-bound operation, such as file operations."*
37
+
38
+ y el efecto documentado:
39
+
40
+ > *"Directly calling blocking_io() in any coroutine would block the event loop for its duration, resulting in an additional 1 second of run time."*
41
+
42
+ El ejemplo de código en la doc muestra `time.sleep(1)` dentro de una función sync (correctamente) y la llamada bloqueante desde una coroutine como el antipatrón a evitar, mostrando `asyncio.to_thread(blocking_io)` como la solución.
43
+
44
+ `docs.python.org/3/library/asyncio-dev.html` (Developing with asyncio) añade:
45
+
46
+ > *"Blocking (CPU-bound) code should not be called directly. For example, if a function performs a CPU-intensive calculation for 1 second, all concurrent asyncio Tasks and IO operations would be delayed by 1 second."*
47
+
48
+ Este texto no nombra `time.sleep()` explícitamente, pero lo describe de forma genérica.
49
+
50
+ **Tornado FAQ** (framework de referencia para async Python predecesor de asyncio):
51
+
52
+ > *"time.sleep is a blocking function: it doesn't allow control to return to the IOLoop so that other handlers can be run."*
53
+
54
+ **Veredicto paso 1:** documentación oficial Python nombra `time.sleep()` explícitamente como el ejemplo estándar de llamada bloqueante que no debe usarse directamente en coroutines.
55
+
56
+ ---
57
+
58
+ ### Paso 2 — Incidentes reales
59
+
60
+ **Resultado: CONFIRMADO — issue real en producción**
61
+
62
+ **[home-assistant/core#119628](https://github.com/home-assistant/core/issues/119628)** (cerrado):
63
+ `time.sleep(0.3)` en la librería `pyserial-asyncio` (línea 104 de `serial/urlhandler/protocol_socket.py`) era llamado desde dentro del event loop de Home Assistant. Home Assistant 2024.6.2 introdujo detección activa de llamadas bloqueantes y produjo el warning:
64
+
65
+ > *"Detected blocking call to sleep inside the event loop by integration 'config'..."*
66
+
67
+ La issue fue reportada en core-2024.6.2 y no ocurría en core-2024.4.4. Issue cerrada — el impacto fue degradación de responsividad durante reloads de integration.
68
+
69
+ **[MagicStack/uvloop#29](https://github.com/MagicStack/uvloop/issues/29)** (aparece en búsqueda, no verificado con fetch):
70
+ Mencionado en el contexto de que "el tiempo del loop no avanza cuando el loop está bloqueado por time.sleep()" — consistente con el comportamiento documentado.
71
+
72
+ **DEV.to "3-Hour Debugging: How time.sleep in Async Functions Killed Our asyncio Concurrency":**
73
+ Post con formato de caso de debugging. El síntoma: un servicio de recolección de datos que no mejoró al convertirse a async, tomando los mismos 30 minutos por ejecución. Causa raíz: `time.sleep(0.5)` enterrado en una función anidada. **No es un postmortem con nombre de empresa verificable** — es contenido educativo presentado como caso de estudio. No cuenta como incidente público confirmado.
74
+
75
+ **Veredicto paso 2:** un incidente real confirmado en GitHub (home-assistant/core#119628, cerrado). El impacto fue observable en producción.
76
+
77
+ ---
78
+
79
+ ### Paso 3 — Comunidad
80
+
81
+ **Resultado: CONSENSO UNIVERSAL — sin casos de desacuerdo**
82
+
83
+ La búsqueda no encontró ninguna discusión en Stack Overflow, DEV.to, Medium o foros que sugiera que `time.sleep()` dentro de `async def` sea aceptable. El consenso es unánime:
84
+
85
+ - La causa síntomática más reportada: *"convertí mi código a async pero no mejora la velocidad"*
86
+ - Solución universal documentada: reemplazar `time.sleep(n)` por `await asyncio.sleep(n)`, o por `await asyncio.to_thread(time.sleep, n)` si la intención es bloquear en un thread aparte
87
+ - El Mergify blog documenta la detección de event loop blocking mediante medición de latencia; usa `time.sleep()` como ejemplo paradigmático del problema a detectar
88
+
89
+ **Veredicto paso 3:** la comunidad no tiene posiciones encontradas sobre este patrón. Es tratado como bug obvio.
90
+
91
+ ---
92
+
93
+ ### Paso 4 — Evidencia empírica propia
94
+
95
+ **Resultado: FUERTE**
96
+
97
+ - 31/250 repos afectados (12.4%) — estabilidad Alta (−3.6 pp entre 100 y 250 repos)
98
+ - 87 hits totales en proyectos de alta estrella con asyncio
99
+ - Presente en repos de referencia: `home-assistant/core`, `MODSetter/SurfSense`, `IBM/mcp-context-forge`
100
+ - 0 falsos positivos documentados en 250 repos escaneados
101
+
102
+ ---
103
+
104
+ ### Paso 5 — Intento de refutación
105
+
106
+ **Búsqueda activa de excepciones legítimas.**
107
+
108
+ **¿Existe algún caso donde `time.sleep()` dentro de `async def` sea intencional y correcto?**
109
+
110
+ La búsqueda encontró tres escenarios candidatos:
111
+
112
+ **Candidato A — Testing de comportamiento bloqueante**
113
+ Usar `time.sleep()` en un test `async def` para simular una llamada bloqueante y verificar que el detector de blocking funciona. *Técnicamente funciona, pero:*
114
+ - `asynctest` y `sleepfake` (librerías de testing para asyncio) proveen `advance()` y mocking de clock precisamente para evitar `time.sleep()` real en tests
115
+ - Ningún framework de testing Python recomienda `time.sleep()` real en async tests; se recomienda mockear o usar `asyncio.sleep()` con un loop de testing
116
+ - **Veredicto: no es una excepción legítima que invalide la regla**
117
+
118
+ **Candidato B — Delay deliberado en un sistema sin concurrencia**
119
+ Código que usa `async def` pero en la práctica solo ejecuta una tarea a la vez y quiere añadir un delay. `time.sleep()` funciona en este caso porque no hay otra coroutine esperando.
120
+ - Esto es una antipatrón de diseño: si no se necesita concurrencia, no debería usarse asyncio
121
+ - Si se necesita asyncio pero se quiere bloquear un momento, `await asyncio.sleep(n)` es siempre equivalente y correcto
122
+ - **Veredicto: funciona técnicamente pero nunca es la opción correcta**
123
+
124
+ **Candidato C — Wrapper para `asyncio.to_thread()`**
125
+ `await asyncio.to_thread(time.sleep, n)` — llamar `time.sleep` desde `async def` *como argumento de `to_thread`*, no como llamada directa. Esto ES correcto.
126
+ - PYVIBE-001 **no flagea este patrón**: la regla detecta `time.sleep(...)` como expresión directa en el cuerpo de `async def`, no como argumento de otra función
127
+ - **Veredicto: caso correcto, y la regla ya lo maneja bien**
128
+
129
+ **Excepción conocida documentada:**
130
+ No se encontró ningún caso donde `time.sleep(n)` directamente en el cuerpo de `async def` sea la opción técnicamente correcta sobre `await asyncio.sleep(n)`. Tornado FAQ, Python docs y comunidad son unánimes.
131
+
132
+ **Límite honesto:** los 31 repos con hits no han sido auditados manualmente para verificar si algún hit está en un test que específicamente testea comportamiento bloqueante. Este caso extremo existe en teoría pero no se ha verificado en el scan actual.
133
+
134
+ ---
135
+
136
+ ## Clasificación final
137
+
138
+ **Evidence Level: A+**
139
+
140
+ | Criterio | Estado |
141
+ |----------|--------|
142
+ | Validada en repos reales (31/250, 12.4%) | ✅ |
143
+ | Documentación oficial Python nombra `time.sleep()` explícitamente | ✅ |
144
+ | Incidente público confirmado (HA core#119628, cerrado) | ✅ |
145
+ | Falsos positivos documentados en campo | ✅ 0 |
146
+ | Excepción legítima conocida | ✅ ninguna para llamada directa |
147
+
148
+ ---
149
+
150
+ ## Confidence
151
+
152
+ ```
153
+ Confidence:
154
+ Detection: High — AST directo sobre name="sleep" en module time;
155
+ 0 FP documentados en 250 repos; el Candidato C
156
+ (to_thread) no es flagueado correctamente
157
+ Runtime impact: High — bloquea el event loop completo para toda la
158
+ duración del sleep; efecto demostrado en HA prod
159
+ External evidence: High — docs.python.org nombra time.sleep() explícitamente
160
+ + HA issue real cerrado + consenso comunitario unánime
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Fuentes verificadas
166
+
167
+ - [asyncio-task.html — asyncio.to_thread() example](https://docs.python.org/3/library/asyncio-task.html)
168
+ - [asyncio-dev.html — Developing with asyncio](https://docs.python.org/3/library/asyncio-dev.html)
169
+ - [Tornado FAQ — time.sleep is a blocking function](https://www.tornadoweb.org/en/stable/faq.html)
170
+ - [home-assistant/core#119628 — Blocking call to sleep inside event loop](https://github.com/home-assistant/core/issues/119628)
171
+ - [Mergify — Detecting Blocking Tasks in Asyncio](https://mergify.com/blog/detecting-blocking-tasks-in-asyncio-by-measuring-event-loop-latency/)
172
+ - [DEV.to — 3-Hour Debugging case study (educativo, sin empresa verificable)](https://dev.to/_eb7f2a654e97a60ae9f96e/3-hour-debugging-how-timesleep-in-async-functions-killed-our-asyncio-concurrency-43c3)
@@ -0,0 +1,86 @@
1
+ # PYVIBE-002 — requests.get/post en async def
2
+
3
+ **Severidad:** CRITICAL
4
+ **Archivo:** `pyvibe/rules/async_requests.py`
5
+ **Patrón:** `requests.get(...)` / `requests.post(...)` / etc. dentro de `async def`
6
+
7
+ ## Datos objetivos
8
+
9
+ | Métrica | 100 repos | 250 repos |
10
+ |---------|-----------|-----------|
11
+ | Repos afectados | 3/100 (3.0%) | 10/250 (4.0%) |
12
+ | Total hits | 5 | 14 |
13
+ | Estabilidad 100→250 | Alta (+1.0 pp) | |
14
+ | Falsos positivos documentados | 0 | |
15
+
16
+ ## Repos representativos (sweep 250)
17
+
18
+ - `paulpierre/RasaGPT` — 3 hits
19
+ - `home-assistant/core` — 2 hits
20
+ - `learning-at-home/hivemind` — 2 hits
21
+ - `bmoscon/cryptofeed` — 1 hit
22
+ - `pydantic/logfire` — 1 hit
23
+
24
+ ## Evidence Level: B
25
+
26
+ - ✅ Validada en repos reales: 10 repos afectados en muestra de 250
27
+ - ⏳ Documentación oficial o incidentes públicos confirmando `requests` sync en async como bug: **PENDIENTE — requiere investigación manual**
28
+ - ✅ Falsos positivos documentados en campo: 0
29
+
30
+ ---
31
+
32
+ ## Auditoría de Precisión (Sweep 250)
33
+
34
+ **Muestra analizada:** 14 hits de 14 totales (10 repos)
35
+ **Metodología:** 100% audit (14 hits)
36
+
37
+ ### Clasificación hit-by-hit
38
+
39
+ | # | Repo | File:Line | Function | Clasificación | Razón |
40
+ |---|------|-----------|----------|---------------|-------|
41
+ | 1 | BetaStreetOmnis__xhs_ai_publisher | src/core/pages/tools.py:63 | async_process | FP | `requests.post()` dentro de `loop.run_in_executor(None, lambda: requests.post(...))` — la llamada síncrona está correctamente delegada al executor de threads; el comentario en código incluso menciona que se podría usar aiohttp |
42
+ | 2 | Kav-K__GPTDiscord | models/openai_model.py:1312 | save_image_urls_and_return | FP | `requests.get()` dentro de `asyncio.get_running_loop().run_in_executor(None, lambda: [...])` — correctamente envuelto en executor |
43
+ | 3 | bmoscon__cryptofeed | cryptofeed/exchanges/binance.py:158 | _refresh_token | TP | `requests.put()` directo en `async def _refresh_token`, sin executor. Bug real — bloquea el event loop durante token refresh en un bucle while True |
44
+ | 4 | home-assistant__core | homeassistant/components/downloader/services.py:64 | download_file | FP | `requests.get()` dentro de `def do_download()` (función síncrona), que se llama luego con `await service.hass.async_add_executor_job(do_download)` — el detector flagueó la línea del requests.get pero está en una función def interna que se ejecuta en executor |
45
+ | 5 | home-assistant__core | homeassistant/components/xmpp/notify.py:274 | upload_file_from_url | FP | `requests.get()` en `def get_url(url)` síncrona interna, llamada con `await hass.async_add_executor_job(get_url, url)` — mismo patrón que hit 4 |
46
+ | 6 | learning-at-home__hivemind | hivemind/p2p/p2p_daemon.py:440 | _read_stream | FP | La variable `requests` en este contexto es `asyncio.Queue(max_prefetch)` — no es el módulo `requests`. El detector tiene un FP por colisión de nombre de variable: `requests = asyncio.Queue(...)`, luego `request = await requests.get()` |
47
+ | 7 | learning-at-home__hivemind | hivemind/p2p/p2p_daemon.py:477 | _handle_stream | FP | Mismo contexto que hit 6 — `requests` es una `asyncio.Queue`, no el módulo HTTP |
48
+ | 8 | paulpierre__RasaGPT | app/rasa-credentials/main.py:65 | get_active_tunnels | TP | `requests.get()` directo en `async def get_active_tunnels()`, sin executor. Bug real. |
49
+ | 9 | paulpierre__RasaGPT | app/rasa-credentials/main.py:78 | stop_tunnel | TP | `requests.delete()` directo en `async def stop_tunnel()`. Bug real. |
50
+ | 10 | paulpierre__RasaGPT | app/rasa-credentials/main.py:118 | create_tunnel | TP | `requests.post()` directo en `async def create_tunnel()`. Bug real. |
51
+ | 11 | pydantic__logfire | tests/otel_integrations/test_requests.py:41 | test_requests_instrumentation | FP | Test async que verifica instrumentación del módulo `requests` — `requests.get()` es el objeto bajo prueba (test de observabilidad). Uso legítimo en test. |
52
+ | 12 | python-kasa__python-kasa | kasa/protocols/smartprotocol.py:299 | _execute_multiple_query | FP | La variable `requests` es un `dict` local (mapa de requests al protocolo KASA), no el módulo `requests`. `requests.get(method)` es `dict.get()` en un dict llamado `requests`. Colisión de nombre. |
53
+ | 13 | raullenchai__Rapid-MLX | tests/evals/gsm8k/gsm8k_eval.py:136 | evaluate_with_server | EDGE | `requests.post()` en bucle de evaluación async — es un test/benchmark que llama a un servidor local; técnicamente bloquea el loop pero es un script de evaluación no de producción. Contexto borderline. |
54
+ | 14 | wyeeeee__hajimi | app/utils/version.py:23 | check_version | TP | `requests.get()` directo en `async def check_version()`, sin executor. Bug real — bloquea el loop en cada check de versión. |
55
+
56
+ ### Resultado
57
+
58
+ | Métrica | Valor |
59
+ |---------|-------|
60
+ | TP | 5 |
61
+ | FP | 8 |
62
+ | EDGE | 1 |
63
+ | Precisión (TP/total sin EDGE) | 5/13 = 38% |
64
+ | Precisión (TP/total con EDGE) | 5/14 = 36% |
65
+
66
+ ### Patrones de FP identificados
67
+
68
+ 1. **EXECUTOR_WRAPPER** — `requests.*()` dentro de `run_in_executor(None, lambda: ...)` o `async_add_executor_job()`. La llamada síncrona está correctamente delegada al pool de threads.
69
+ - Ejemplo: `await loop.run_in_executor(None, lambda: requests.post(...))`
70
+ - Repos afectados: 2 (xhs_ai_publisher, GPTDiscord) + 2 en Home Assistant (como inner def)
71
+
72
+ 2. **INNER_SYNC_FUNCTION_EXECUTOR** — `requests.*()` en una función `def` interna que luego se pasa a `executor_job`. El detector flagueó la llamada en el scope síncrono pero no detectó que se ejecutará en executor.
73
+ - Ejemplo: `def do_download(): requests.get(...)` → `await hass.async_add_executor_job(do_download)`
74
+ - Repos afectados: 1 (home-assistant/core, 2 instancias)
75
+
76
+ 3. **VARIABLE_NAME_COLLISION** — variable local llamada `requests` que no es el módulo HTTP (puede ser un dict, una Queue, etc.).
77
+ - Ejemplo: `requests = asyncio.Queue(max_prefetch)` → detector flagueó `requests.get()` como llamada HTTP
78
+ - Ejemplo: `requests = {}` (dict) → `requests.get(method)` es un dict lookup
79
+ - Repos afectados: 2 (hivemind, python-kasa)
80
+
81
+ 4. **TEST_SUBJECT** — test async que verifica el módulo `requests` como objeto bajo prueba para instrumentación.
82
+ - Repos afectados: 1 (pydantic/logfire)
83
+
84
+ ### Recomendación de Evidence Level
85
+
86
+ **Mantener B. Precisión preocupante: 36-38%.** El problema principal es triple: (a) colisiones de nombre de variable (`requests` como nombre de dict/Queue), (b) `requests.*()` en funciones `def` síncronas internas que se pasan a executor, (c) test subjects. El detector necesita mejorar: verificar que `requests` sea realmente el módulo importado (no una variable local), y detectar el patrón `run_in_executor` en el contexto inmediato. Los 5 TP reales son genuinamente problemáticos. **FP rate alto (62%) requiere acción antes de considerar elevación a Evidence A.**