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.
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/PKG-INFO +61 -4
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/README.md +60 -3
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyproject.toml +4 -1
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/PKG-INFO +61 -4
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/SOURCES.txt +24 -0
- python_vibe_guard-0.12.2/pyvibe/__init__.py +1 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-001.md +172 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-002.md +86 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-003.md +64 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-004.md +93 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-005.md +294 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-006.md +85 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-007.md +85 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-008.md +91 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-009.md +289 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-010.md +76 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-011.md +61 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-012.md +85 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-013.md +362 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-014.md +82 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-015.md +75 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-016.md +62 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-017.md +360 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-018.md +96 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-019.md +988 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/accepted/PYVIBE-020.md +87 -0
- python_vibe_guard-0.12.2/pyvibe/_evidence/precision-audit.md +1241 -0
- python_vibe_guard-0.12.2/pyvibe/audit.py +119 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/cli.py +134 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/explain.py +10 -5
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/celery_time_limit.py +2 -1
- python_vibe_guard-0.12.2/pyvibe/suppressions.py +102 -0
- python_vibe_guard-0.12.2/tests/test_audit.py +274 -0
- python_vibe_guard-0.12.2/tests/test_evidence_sync.py +57 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_suppressions.py +32 -0
- python_vibe_guard-0.12.0/pyvibe/__init__.py +0 -1
- python_vibe_guard-0.12.0/pyvibe/suppressions.py +0 -53
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/entry_points.txt +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/requires.txt +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/top_level.txt +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/__main__.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/analyzer.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/autofix.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/baseline.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/config.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/diff.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rule_docs.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/__init__.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/async_requests.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/async_sleep.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/asyncio_run.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/base.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/contextvar_cleanup.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/create_task_orphan.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/ensure_future_orphan.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/httpx_client_sync.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/httpx_sync.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/loop_run_until_complete.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/open_async.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/os_blocking.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/queue_put_nowait.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/retry_no_backoff.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/silent_except.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/sqlite_async.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/subprocess_async.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/threading_lock.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/rules/while_true_no_await.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/pyvibe/sarif.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/setup.cfg +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_baseline.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_config.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_diff.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_exclude.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_explain.py +0 -0
- {python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/tests/test_rules.py +0 -0
- {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.
|
|
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 `#
|
|
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.
|
|
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
|
-
|
|
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 `#
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 `#
|
|
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.
|
|
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
|
-
|
|
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
|
|
{python_vibe_guard-0.12.0 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/SOURCES.txt
RENAMED
|
@@ -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.**
|