python-vibe-guard 0.12.1__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.1 → python_vibe_guard-0.12.2}/PKG-INFO +9 -3
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/README.md +8 -2
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyproject.toml +4 -1
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/PKG-INFO +9 -3
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/SOURCES.txt +22 -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.1 → python_vibe_guard-0.12.2}/pyvibe/explain.py +10 -5
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/celery_time_limit.py +2 -1
- python_vibe_guard-0.12.2/tests/test_evidence_sync.py +57 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_suppressions.py +32 -0
- python_vibe_guard-0.12.1/pyvibe/__init__.py +0 -1
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/entry_points.txt +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/requires.txt +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/top_level.txt +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/__main__.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/analyzer.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/audit.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/autofix.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/baseline.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/cli.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/config.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/diff.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rule_docs.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/__init__.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/async_requests.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/async_sleep.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/asyncio_run.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/base.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/contextvar_cleanup.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/create_task_orphan.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/ensure_future_orphan.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/httpx_client_sync.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/httpx_sync.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/loop_run_until_complete.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/open_async.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/os_blocking.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/queue_put_nowait.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/retry_no_backoff.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/silent_except.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/sqlite_async.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/subprocess_async.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/threading_lock.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/rules/while_true_no_await.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/sarif.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/pyvibe/suppressions.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/setup.cfg +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_audit.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_baseline.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_config.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_diff.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_exclude.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_explain.py +0 -0
- {python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/tests/test_rules.py +0 -0
- {python_vibe_guard-0.12.1 → 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.
|
|
@@ -451,7 +451,7 @@ Add to your `.pre-commit-config.yaml`:
|
|
|
451
451
|
```yaml
|
|
452
452
|
repos:
|
|
453
453
|
- repo: https://github.com/Joaquinriosheredia/python-vibe-guard
|
|
454
|
-
rev: v0.
|
|
454
|
+
rev: v0.12.2
|
|
455
455
|
hooks:
|
|
456
456
|
- id: python-vibe-guard
|
|
457
457
|
```
|
|
@@ -498,6 +498,12 @@ python tests/test_rules.py
|
|
|
498
498
|
|
|
499
499
|
---
|
|
500
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.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
501
507
|
## Ecosystem
|
|
502
508
|
|
|
503
509
|
This project is part of the **vibe-guard** family of runtime anti-pattern scanners:
|
|
@@ -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.
|
|
@@ -435,7 +435,7 @@ Add to your `.pre-commit-config.yaml`:
|
|
|
435
435
|
```yaml
|
|
436
436
|
repos:
|
|
437
437
|
- repo: https://github.com/Joaquinriosheredia/python-vibe-guard
|
|
438
|
-
rev: v0.
|
|
438
|
+
rev: v0.12.2
|
|
439
439
|
hooks:
|
|
440
440
|
- id: python-vibe-guard
|
|
441
441
|
```
|
|
@@ -482,6 +482,12 @@ python tests/test_rules.py
|
|
|
482
482
|
|
|
483
483
|
---
|
|
484
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.
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
485
491
|
## Ecosystem
|
|
486
492
|
|
|
487
493
|
This project is part of the **vibe-guard** family of runtime anti-pattern scanners:
|
|
@@ -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.
|
|
@@ -451,7 +451,7 @@ Add to your `.pre-commit-config.yaml`:
|
|
|
451
451
|
```yaml
|
|
452
452
|
repos:
|
|
453
453
|
- repo: https://github.com/Joaquinriosheredia/python-vibe-guard
|
|
454
|
-
rev: v0.
|
|
454
|
+
rev: v0.12.2
|
|
455
455
|
hooks:
|
|
456
456
|
- id: python-vibe-guard
|
|
457
457
|
```
|
|
@@ -498,6 +498,12 @@ python tests/test_rules.py
|
|
|
498
498
|
|
|
499
499
|
---
|
|
500
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.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
501
507
|
## Ecosystem
|
|
502
508
|
|
|
503
509
|
This project is part of the **vibe-guard** family of runtime anti-pattern scanners:
|
{python_vibe_guard-0.12.1 → python_vibe_guard-0.12.2}/python_vibe_guard.egg-info/SOURCES.txt
RENAMED
|
@@ -19,6 +19,27 @@ pyvibe/explain.py
|
|
|
19
19
|
pyvibe/rule_docs.py
|
|
20
20
|
pyvibe/sarif.py
|
|
21
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
|
|
22
43
|
pyvibe/rules/__init__.py
|
|
23
44
|
pyvibe/rules/async_requests.py
|
|
24
45
|
pyvibe/rules/async_sleep.py
|
|
@@ -45,6 +66,7 @@ tests/test_audit.py
|
|
|
45
66
|
tests/test_baseline.py
|
|
46
67
|
tests/test_config.py
|
|
47
68
|
tests/test_diff.py
|
|
69
|
+
tests/test_evidence_sync.py
|
|
48
70
|
tests/test_exclude.py
|
|
49
71
|
tests/test_explain.py
|
|
50
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.**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# PYVIBE-003 — asyncio.run() dentro de async def
|
|
2
|
+
|
|
3
|
+
**Severidad:** CRITICAL
|
|
4
|
+
**Archivo:** `pyvibe/rules/asyncio_run.py`
|
|
5
|
+
**Patrón:** `asyncio.run(coro())` llamado dentro de `async def`
|
|
6
|
+
|
|
7
|
+
## Datos objetivos
|
|
8
|
+
|
|
9
|
+
| Métrica | 100 repos | 250 repos |
|
|
10
|
+
|---------|-----------|-----------|
|
|
11
|
+
| Repos afectados | 1/100 (1.0%) | 1/250 (0.4%) |
|
|
12
|
+
| Total hits | 1 | 1 |
|
|
13
|
+
| Estabilidad 100→250 | Alta (−0.6 pp) | |
|
|
14
|
+
| Falsos positivos documentados | 0 | |
|
|
15
|
+
|
|
16
|
+
## Repos representativos (sweep 250)
|
|
17
|
+
|
|
18
|
+
- `piccolo-orm/piccolo` — 1 hit (único repo afectado en ambos sweeps)
|
|
19
|
+
|
|
20
|
+
## Evidence Level: B
|
|
21
|
+
|
|
22
|
+
- ✅ Validada en repos reales: 1 repo afectado confirmado en ambas muestras
|
|
23
|
+
- ⏳ Documentación oficial o incidentes públicos: **PENDIENTE — requiere investigación manual**
|
|
24
|
+
- ✅ Falsos positivos documentados en campo: 0
|
|
25
|
+
|
|
26
|
+
## Nota
|
|
27
|
+
|
|
28
|
+
Baja prevalencia (0.4% en 250 repos) pero el patrón es siempre incorrecto:
|
|
29
|
+
`asyncio.run()` crea un nuevo event loop y falla si ya hay uno activo,
|
|
30
|
+
lo que dentro de `async def` genera `RuntimeError: This event loop is already running`.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Auditoría de Precisión (Sweep 250)
|
|
35
|
+
|
|
36
|
+
**Muestra analizada:** 1 hit de 1 total (1 repo)
|
|
37
|
+
**Metodología:** 100% audit (1 hit)
|
|
38
|
+
|
|
39
|
+
### Clasificación hit-by-hit
|
|
40
|
+
|
|
41
|
+
| # | Repo | File:Line | Function | Clasificación | Razón |
|
|
42
|
+
|---|------|-----------|----------|---------------|-------|
|
|
43
|
+
| 1 | piccolo-orm__piccolo | tests/table/test_batch.py:129 | test_batch | FP | Llamada dentro de un método de `unittest.TestCase` (`def test_batch`, no `async def`); el hit es en la línea donde el patrón AST lo detecta pero la función contenedora es síncrona — se usa `asyncio.run()` para ejecutar la corrutina desde un test síncrono, que es el uso correcto |
|
|
44
|
+
|
|
45
|
+
**Nota:** La función `test_batch` es un método de `TestCase` (síncrono, no `async def`). El uso de `asyncio.run()` para ejecutar una corrutina desde código síncrono de test es el patrón idiomático correcto. El linter parece haber flagueado este caso porque la línea con `asyncio.run()` está dentro de un método de clase que hereda de `TestCase`, sin verificar si la función es `async def` o `def` regular.
|
|
46
|
+
|
|
47
|
+
### Resultado
|
|
48
|
+
|
|
49
|
+
| Métrica | Valor |
|
|
50
|
+
|---------|-------|
|
|
51
|
+
| TP | 0 |
|
|
52
|
+
| FP | 1 |
|
|
53
|
+
| EDGE | 0 |
|
|
54
|
+
| Precisión (TP/total) | 0% |
|
|
55
|
+
|
|
56
|
+
### Patrones de FP identificados
|
|
57
|
+
|
|
58
|
+
1. **TEST_SYNC_METHOD** — `asyncio.run()` llamado desde método síncrono de `TestCase` para ejecutar una corrutina. Uso idiomático correcto.
|
|
59
|
+
- Ejemplo: `asyncio.run(self.run_batch(batch_size=batch_size), debug=True)`
|
|
60
|
+
- Repos afectados: 1 (piccolo-orm/piccolo)
|
|
61
|
+
|
|
62
|
+
### Recomendación de Evidence Level
|
|
63
|
+
|
|
64
|
+
**Revisar detección.** Con 1/1 FP la muestra es mínima, pero el FP indica una posible debilidad en la regla: si el detector no verifica que la función inmediata contenedora sea `async def`, va a flagear `asyncio.run()` en métodos síncronos que legítimamente lanzan corrutinas. Confirmar que `pyvibe/rules/asyncio_run.py` comprueba que el nodo padre directo es un `AsyncFunctionDef`, no solo cualquier `FunctionDef`. Si la regla ya lo verifica, este FP es un bug del detector a corregir. Mantener Evidence B hasta ampliar muestra.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# PYVIBE-004 — threading.Lock() en async def
|
|
2
|
+
|
|
3
|
+
**Severidad:** CRITICAL
|
|
4
|
+
**Archivo:** `pyvibe/rules/threading_lock.py`
|
|
5
|
+
**Patrón:** `threading.Lock()` / `threading.RLock()` instanciado o usado dentro de `async def`
|
|
6
|
+
|
|
7
|
+
## Datos objetivos
|
|
8
|
+
|
|
9
|
+
| Métrica | 100 repos | 250 repos |
|
|
10
|
+
|---------|-----------|-----------|
|
|
11
|
+
| Repos afectados | 5/100 (5.0%) | 5/250 (2.0%) |
|
|
12
|
+
| Total hits | 60 | 60 |
|
|
13
|
+
| Estabilidad 100→250 | Alta (−3.0 pp) | |
|
|
14
|
+
| Falsos positivos documentados | 0 | |
|
|
15
|
+
|
|
16
|
+
## Repos representativos (sweep 250)
|
|
17
|
+
|
|
18
|
+
- `agronholm/anyio` — 36 hits
|
|
19
|
+
- `BeanieODM/beanie` — 11 hits
|
|
20
|
+
- `home-assistant/core` — 8 hits
|
|
21
|
+
- `IBM/mcp-context-forge` — 3 hits
|
|
22
|
+
- `polarsource/polar` — 2 hits
|
|
23
|
+
|
|
24
|
+
## Evidence Level: B
|
|
25
|
+
|
|
26
|
+
- ✅ Validada en repos reales: 5 repos afectados; el total de hits (60) se mantiene igual en ambos sweeps, concentrado en los mismos repos
|
|
27
|
+
- ⏳ Documentación oficial o incidentes públicos: **PENDIENTE — requiere investigación manual**
|
|
28
|
+
- ✅ Falsos positivos documentados en campo: 0
|
|
29
|
+
|
|
30
|
+
## Nota
|
|
31
|
+
|
|
32
|
+
El total de hits es idéntico entre 100 y 250 repos (60), lo que indica que los 5 repos afectados ya estaban en el sweep de 100. Los 150 repos nuevos no añaden ningún hit.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Auditoría de Precisión (Sweep 250)
|
|
37
|
+
|
|
38
|
+
**Muestra analizada:** 14 hits de 60 totales (5 repos) — todos los repos tienen < 3 hits salvo anyio (36 hits, toma 3)
|
|
39
|
+
**Metodología:** Muestra estratificada (max 3 por repo) — total 14 de 60 hits; anyio concentra 36/60
|
|
40
|
+
|
|
41
|
+
### Clasificación hit-by-hit
|
|
42
|
+
|
|
43
|
+
| # | Repo | File:Line | Function | Clasificación | Razón |
|
|
44
|
+
|---|------|-----------|----------|---------------|-------|
|
|
45
|
+
| 1 | BeanieODM__beanie | test_validation_on_save.py:70 | test_validate_on_save_dbref | FP | `lock = Lock(k=1)` — `Lock` aquí es un modelo de Beanie/MongoDB (clase ORM de la aplicación, no `threading.Lock`). Colisión de nombre de clase: el detector flagueó la instanciación de un documento ORM llamado `Lock`. |
|
|
46
|
+
| 2 | BeanieODM__beanie | test_find.py:435 | test_fetch_links_with_chained_delete | FP | Mismo repo — `Lock` es un documento ORM de Beanie en los tests. Colisión de nombre. |
|
|
47
|
+
| 3 | BeanieODM__beanie | test_find.py:511 | test_distinct_with_fetch_links | FP | Mismo repo — mismo patrón, `Lock` es modelo ORM. |
|
|
48
|
+
| 4 | IBM__mcp-context-forge | test_cache_invalidation_subscriber.py:125 | test_process_tool_lookup_name_invalidation | FP | `mock_tool_lookup._lock = threading.Lock()` — se asigna un `threading.Lock` a un objeto mock en un test. En el test no hay `async def` que lo use como mutex — se está configurando un atributo del mock para que el SUT pueda acceder a él. El `threading.Lock()` aquí es un setup de datos de test, no un mutex que bloquee el event loop. FP contextual. |
|
|
49
|
+
| 5 | IBM__mcp-context-forge | test_cache_invalidation_subscriber.py:143 | test_process_tool_lookup_gateway_invalidation | FP | Mismo repo y mismo patrón — `threading.Lock()` en atributo de mock. |
|
|
50
|
+
| 6 | IBM__mcp-context-forge | test_cache_invalidation_subscriber.py:161 | test_process_admin_invalidation | FP | Mismo repo — `threading.Lock()` en atributo de mock de admin cache. |
|
|
51
|
+
| 7 | agronholm__anyio | src/anyio/functools.py:173 | __call__ | EDGE | `Lock(fast_acquire=not self._always_checkpoint)` — este es `anyio.Lock`, no `threading.Lock`. Si el detector flagueó esto por el nombre `Lock`, es una colisión de nombre. anyio.Lock es el mutex async correcto. |
|
|
52
|
+
| 8 | agronholm__anyio | src/anyio/functools.py:183 | __call__ | EDGE | Mismo contexto — `Lock(fast_acquire=...)` es `anyio.Lock` en el memoize cache de anyio. Uso correcto de Lock async. |
|
|
53
|
+
| 9 | agronholm__anyio | tests/test_synchronization.py:38 | test_contextmanager | FP | Test de anyio — verificación de comportamiento del Lock async de anyio. En test de la librería de sync. |
|
|
54
|
+
| 10 | home-assistant__core | homeassistant/util/async_.py:105 | gather_with_limited_concurrency | FP | `semaphore = Semaphore(limit)` — `Semaphore` es `asyncio.Semaphore`, no `threading.Semaphore`. Incluso si fuera `threading.Lock`, la función no usa la primitiva directamente como mutex bloqueante sino para limitar concurrencia a través de `async with semaphore:`. FP si el detector flagueó el Semaphore como Lock threading. |
|
|
55
|
+
| 11 | home-assistant__core | tests/components/backblaze_b2/test_backup.py:911 | test_metadata_downloads_are_sequential | FP | Test de HA — probablemente `threading.Lock()` en mock o fixture de test. |
|
|
56
|
+
| 12 | home-assistant__core | tests/components/homekit/test_type_locks.py:44 | test_lock_unlock | FP | El nombre del archivo ya indica que se testea la integración "locks" de HomeKit — `Lock` en este contexto es un dispositivo HomeKit, no `threading.Lock`. Colisión de dominio. |
|
|
57
|
+
| 13 | polarsource__polar | server/polar/locker.py:63 | lock | FP | `Lock(self.redis, self._get_key(name), ...)` — este `Lock` es `redis.asyncio.lock.Lock` (importado en la línea 7 del archivo). Es un distributed lock async sobre Redis, no `threading.Lock`. |
|
|
58
|
+
| 14 | polarsource__polar | server/polar/locker.py:122 | is_locked | FP | Mismo repo — `Lock(self.redis, ...)` es `redis.asyncio.lock.Lock`. Correcto. |
|
|
59
|
+
|
|
60
|
+
### Resultado
|
|
61
|
+
|
|
62
|
+
| Métrica | Valor |
|
|
63
|
+
|---------|-------|
|
|
64
|
+
| TP | 0 |
|
|
65
|
+
| FP | 12 |
|
|
66
|
+
| EDGE | 2 |
|
|
67
|
+
| Precisión (TP/total sin EDGE) | 0/12 = 0% |
|
|
68
|
+
| Precisión (TP+EDGE/total) | 2/14 = 14% |
|
|
69
|
+
|
|
70
|
+
### Patrones de FP identificados
|
|
71
|
+
|
|
72
|
+
1. **DOMAIN_CLASS_NAME_COLLISION** — clase de dominio llamada `Lock` (ORM model, HomeKit device) que el detector confundió con `threading.Lock`.
|
|
73
|
+
- Ejemplo: `lock = Lock(k=1)` donde `Lock` es un documento de Beanie MongoDB
|
|
74
|
+
- Repos afectados: 1 (BeanieODM/beanie, 3 instancias)
|
|
75
|
+
|
|
76
|
+
2. **ASYNC_LOCK_SAME_NAME** — `anyio.Lock`, `redis.asyncio.lock.Lock` importados y usados correctamente dentro de `async def` — son primitivas async válidas, no `threading.Lock`.
|
|
77
|
+
- Ejemplo: `from redis.asyncio.lock import Lock; ... Lock(self.redis, ...)`
|
|
78
|
+
- Repos afectados: 2 (polarsource/polar, agronholm/anyio)
|
|
79
|
+
|
|
80
|
+
3. **MOCK_ATTRIBUTE_SETUP** — `threading.Lock()` asignado como atributo de un objeto mock en test (`mock._lock = threading.Lock()`). El lock no se adquiere en la función async bajo prueba — se pasa como dato al SUT que lo usa internamente.
|
|
81
|
+
- Repos afectados: 1 (IBM/mcp-context-forge, 3 instancias)
|
|
82
|
+
|
|
83
|
+
4. **TEST_INTEGRATION_DOMAIN** — tests de integraciones HomeKit donde `Lock` es un tipo de dispositivo de automatización del hogar.
|
|
84
|
+
- Repos afectados: 1 (home-assistant/core)
|
|
85
|
+
|
|
86
|
+
### Recomendación de Evidence Level
|
|
87
|
+
|
|
88
|
+
**ALERTA: Precisión 0-14% — regla requiere revisión profunda del detector.** Todos los hits en la muestra son falsos positivos. El problema es sistemático:
|
|
89
|
+
- El detector no verifica que `Lock` sea específicamente `threading.Lock` (verificando el módulo de origen del símbolo)
|
|
90
|
+
- Flagueó `anyio.Lock`, `redis.asyncio.Lock`, clases ORM llamadas `Lock`, y mocks
|
|
91
|
+
- Para reparar: el detector debe verificar que el import de `Lock` sea `from threading import Lock` o `import threading; threading.Lock()`, no cualquier clase con nombre `Lock`
|
|
92
|
+
- Los 36 hits de anyio probablemente son todos FPs del mismo tipo (anyio.Lock siendo confundido con threading.Lock)
|
|
93
|
+
- **Recomendación: degradar a 🔵 Limited Scope hasta que el detector se corrija.** Con 0% de precisión en la muestra, la regla genera más ruido que valor.
|