python-vibe-guard 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. python_vibe_guard-0.7.1/PKG-INFO +286 -0
  2. python_vibe_guard-0.7.1/README.md +271 -0
  3. python_vibe_guard-0.7.1/pyproject.toml +27 -0
  4. python_vibe_guard-0.7.1/python_vibe_guard.egg-info/PKG-INFO +286 -0
  5. python_vibe_guard-0.7.1/python_vibe_guard.egg-info/SOURCES.txt +35 -0
  6. python_vibe_guard-0.7.1/python_vibe_guard.egg-info/dependency_links.txt +1 -0
  7. python_vibe_guard-0.7.1/python_vibe_guard.egg-info/entry_points.txt +2 -0
  8. python_vibe_guard-0.7.1/python_vibe_guard.egg-info/top_level.txt +1 -0
  9. python_vibe_guard-0.7.1/pyvibe/__init__.py +1 -0
  10. python_vibe_guard-0.7.1/pyvibe/__main__.py +3 -0
  11. python_vibe_guard-0.7.1/pyvibe/analyzer.py +163 -0
  12. python_vibe_guard-0.7.1/pyvibe/cli.py +148 -0
  13. python_vibe_guard-0.7.1/pyvibe/rules/__init__.py +0 -0
  14. python_vibe_guard-0.7.1/pyvibe/rules/async_requests.py +65 -0
  15. python_vibe_guard-0.7.1/pyvibe/rules/async_sleep.py +44 -0
  16. python_vibe_guard-0.7.1/pyvibe/rules/asyncio_run.py +48 -0
  17. python_vibe_guard-0.7.1/pyvibe/rules/base.py +65 -0
  18. python_vibe_guard-0.7.1/pyvibe/rules/celery_time_limit.py +103 -0
  19. python_vibe_guard-0.7.1/pyvibe/rules/contextvar_cleanup.py +129 -0
  20. python_vibe_guard-0.7.1/pyvibe/rules/create_task_orphan.py +44 -0
  21. python_vibe_guard-0.7.1/pyvibe/rules/ensure_future_orphan.py +44 -0
  22. python_vibe_guard-0.7.1/pyvibe/rules/gather_no_return_exceptions.py +57 -0
  23. python_vibe_guard-0.7.1/pyvibe/rules/httpx_client_sync.py +43 -0
  24. python_vibe_guard-0.7.1/pyvibe/rules/httpx_sync.py +52 -0
  25. python_vibe_guard-0.7.1/pyvibe/rules/loop_run_until_complete.py +37 -0
  26. python_vibe_guard-0.7.1/pyvibe/rules/open_async.py +39 -0
  27. python_vibe_guard-0.7.1/pyvibe/rules/os_blocking.py +49 -0
  28. python_vibe_guard-0.7.1/pyvibe/rules/queue_put_nowait.py +98 -0
  29. python_vibe_guard-0.7.1/pyvibe/rules/retry_no_backoff.py +255 -0
  30. python_vibe_guard-0.7.1/pyvibe/rules/silent_except.py +107 -0
  31. python_vibe_guard-0.7.1/pyvibe/rules/sqlite_async.py +82 -0
  32. python_vibe_guard-0.7.1/pyvibe/rules/subprocess_async.py +54 -0
  33. python_vibe_guard-0.7.1/pyvibe/rules/threading_lock.py +89 -0
  34. python_vibe_guard-0.7.1/pyvibe/rules/while_true_no_await.py +111 -0
  35. python_vibe_guard-0.7.1/setup.cfg +4 -0
  36. python_vibe_guard-0.7.1/tests/test_exclude.py +174 -0
  37. python_vibe_guard-0.7.1/tests/test_rules.py +2440 -0
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-vibe-guard
3
+ Version: 0.7.1
4
+ Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
+ License: MIT
6
+ Keywords: async,linter,fastapi,asyncio,static-analysis
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Topic :: Software Development :: Quality Assurance
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+
16
+ # python-vibe-guard
17
+
18
+ [![tests](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml)
19
+
20
+ ## The incident
21
+
22
+ A FastAPI service that handled 50 concurrent requests in staging started timing out in production at 200 rps. The team spent two days adding replicas, tweaking Gunicorn workers, and profiling CPU — nothing helped. The p99 latency was 8 seconds for an endpoint that should take 80ms.
23
+
24
+ > Scenario based on a common pattern observed in async Python codebases.
25
+ > Reproduced in controlled testing with locust against a FastAPI service.
26
+
27
+ Root cause: one engineer had asked an AI assistant to "add a retry with backoff" to an async handler. The AI generated `time.sleep(2)` inside the `async def`. In staging with a handful of requests it was invisible. In production it froze the entire event loop for 2 seconds per request, serializing all 200 concurrent calls through a single bottleneck.
28
+
29
+ The code passed every unit test. It passed the integration tests. It shipped to production in a Friday deploy.
30
+
31
+ **python-vibe-guard catches this in CI before it reaches staging.**
32
+
33
+ ---
34
+
35
+ ## What it detects
36
+
37
+ Twenty patterns that AI models generate repeatedly, that pass all static checks, and that silently destroy async performance under real load:
38
+
39
+ ### A. Event Loop Blocking
40
+
41
+ Synchronous calls inside `async def` — each stalls the event loop for its entire duration, serializing all concurrent requests.
42
+
43
+ | Rule | Pattern | Gate | Runtime effect |
44
+ |------|---------|------|----------------|
45
+ | PYVIBE-001 | `time.sleep()` inside `async def` | `async def` | Freezes entire event loop for sleep duration |
46
+ | PYVIBE-002 | `requests.*` inside `async def` | `async def` | Blocks OS thread, serializes all concurrent I/O |
47
+ | PYVIBE-007 | `subprocess.run/call/check_output/Popen` inside `async def` | `async def` | Blocks OS thread for entire subprocess duration |
48
+ | PYVIBE-008 | `sqlite3.connect()` inside `async def` | `async def` | Synchronous file I/O blocks the event loop |
49
+ | PYVIBE-009 | `open()` builtin inside `async def` | `async def` | Synchronous file I/O blocks the event loop |
50
+ | PYVIBE-010 | `httpx.get/post/put/…` inside `async def` | `async def` | httpx sync API blocks OS thread for full HTTP round-trip |
51
+ | PYVIBE-011 | `os.system/popen/waitpid` inside `async def` | `async def` | Blocking OS calls with no direct async equivalent |
52
+ | PYVIBE-016 | `httpx.Client()` instantiated inside `async def` | `async def` | Sync client blocks OS thread per request; `httpx.AsyncClient()` is excluded |
53
+
54
+ ### B. Async Lifecycle Misuse
55
+
56
+ Incorrect use of asyncio primitives that raise `RuntimeError` at runtime or silently discard tasks mid-execution.
57
+
58
+ | Rule | Pattern | Gate | Runtime effect |
59
+ |------|---------|------|----------------|
60
+ | PYVIBE-003 | `asyncio.run()` inside `async def` | `async def` | `RuntimeError: This event loop is already running` |
61
+ | PYVIBE-012 | `asyncio.create_task()` with discarded return value | `async def` | Task GC'd mid-execution; exceptions silently swallowed |
62
+ | PYVIBE-013 | `asyncio.gather()` without `return_exceptions=True` | `async def` | First exception leaks remaining tasks; no per-task error handling |
63
+ | PYVIBE-014 | `asyncio.ensure_future()` with discarded return value | `async def` | Same GC hazard as PYVIBE-012; pre-3.7 API still common in older codebases |
64
+ | PYVIBE-015 | `loop.run_until_complete()` inside `async def` | `async def` | `RuntimeError: This event loop is already running` |
65
+
66
+ ### C. Concurrency & State Hazards
67
+
68
+ Patterns that introduce data races, swallowed errors, or runaway loops under concurrent async load.
69
+
70
+ | Rule | Pattern | Gate | Runtime effect |
71
+ |------|---------|------|----------------|
72
+ | PYVIBE-004 | `threading.Lock/RLock/…` inside `async def` | `async def` | Blocks event loop under contention |
73
+ | PYVIBE-005 | `@app.task` / `@shared_task` without `soft_time_limit` or `time_limit` | decorator | Worker hangs forever if external call never returns |
74
+ | PYVIBE-006 | `ContextVar.set()` inside `async def` without `try/finally reset()` | `async def` | Context leaks into sibling async tasks |
75
+ | PYVIBE-017 | `except Exception: pass` / bare `except: pass` (empty body) | any | Swallows all errors silently; bare except also catches `KeyboardInterrupt`/`SystemExit` |
76
+ | PYVIBE-018 | `while True:` inside `async def` with no `await` in body | `async def` | Event loop blocked indefinitely; CPU hits 100% |
77
+ | PYVIBE-019 | retry `for`/`while` loop in `async def` with no backoff in `except` | `async def` | Tight retry loop on failure: thousands of failed requests/sec, cascading failures |
78
+ | PYVIBE-020 | `put_nowait()` without `asyncio.QueueFull` handler | any | `QueueFull` propagates unhandled; item is silently lost on bounded queues |
79
+
80
+ **Severity notes:**
81
+ - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# noqa: PYVIBE-005`.
82
+ - PYVIBE-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.
83
+ - PYVIBE-017: bare `except` → `CRITICAL` (catches `KeyboardInterrupt`/`SystemExit`); `except Exception` with empty body → `WARNING`. Specific exceptions (`except ValueError: pass`) are not flagged.
84
+ - 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.
85
+ - PYVIBE-019: `WARNING` — flags `except` that ends with `continue` or is solely `pass` with no sleep/backoff call. Suppressed when an escalation pattern (`if … : raise/break`) is present.
86
+ - PYVIBE-020: `WARNING` — fires in any function context (sync and async). Suppressed when the `put_nowait()` is inside a `try` whose handlers include `asyncio.QueueFull`, `QueueFull`, bare `except`, or `Exception`.
87
+
88
+ ---
89
+
90
+ ## Validation
91
+
92
+ ### 100-repo sweep (automated, v0.7.0)
93
+
94
+ Automated scan of 100 GitHub repos (`stars:>100`, updated last year, async Python keywords). Script: [`validation/run_massive_scan.py`](validation/run_massive_scan.py).
95
+
96
+ | Metric | Value |
97
+ |--------|-------|
98
+ | Repos scanned | 100 |
99
+ | .py files | 64,335 |
100
+ | Total violations | 3,639 |
101
+
102
+ **Rule prevalence (top 10):**
103
+
104
+ | Rule | Hits | Repos affected | % of repos |
105
+ |------|------|----------------|-----------|
106
+ | PYVIBE-017 `except Exception: pass` | 999 | 57 | **57%** |
107
+ | PYVIBE-013 `gather()` no return_exceptions | 766 | 47 | **47%** |
108
+ | PYVIBE-019 retry loop no backoff | 408 | 43 | **43%** |
109
+ | PYVIBE-009 `open()` in async def | 178 | 33 | **33%** |
110
+ | PYVIBE-005 `@task` no time_limit | 882 | 15 | 15% |
111
+ | PYVIBE-001 `time.sleep` in async | 55 | 16 | 16% |
112
+ | PYVIBE-012 orphaned `create_task()` | 58 | 13 | 13% |
113
+ | PYVIBE-008 `sqlite3` in async def | 121 | 10 | 10% |
114
+ | PYVIBE-018 `while True` no await | 36 | 10 | 10% |
115
+ | PYVIBE-006 ContextVar no reset | 19 | 10 | 10% |
116
+
117
+ Full data: [`validation/aggregate.json`](validation/aggregate.json) · [`validation/massive-results.md`](validation/massive-results.md)
118
+
119
+ ### Reference scan — 4 repos (reproducible)
120
+
121
+ | Repo | .py files | Violations |
122
+ |------|-----------|-----------|
123
+ | [fastapi/fastapi](https://github.com/tiangolo/fastapi) | 1 121 | 5 |
124
+ | [celery/celery](https://github.com/celery/celery) | 416 | 363 |
125
+ | [aio-libs/aiohttp](https://github.com/aio-libs/aiohttp) | 164 | 29 |
126
+ | [encode/httpx](https://github.com/encode/httpx) | 60 | 0 |
127
+ | **Total** | **1 761** | **397** |
128
+
129
+ Raw data: [`validation/breakdown.json`](validation/breakdown.json)
130
+
131
+ **Notable findings:**
132
+
133
+ - **home-assistant/core** — 418 violations across 17,702 files; 331 PYVIBE-013, 21 PYVIBE-018.
134
+ - **Celery** — 352 tasks without `time_limit`; workers can hang indefinitely on broker timeouts.
135
+ - **aiortc, anyio, uvicorn** — `while True` loops without `await` in production async I/O code (PYVIBE-018, 29 confirmed real hits after async-generator FP fix).
136
+ - **aiohttp examples** — `open()` in async WebSocket handlers; synchronous I/O in production-facing code.
137
+
138
+ ---
139
+
140
+ ## Installation
141
+
142
+ ```bash
143
+ pip install python-vibe-guard
144
+ ```
145
+
146
+ Or run without installing:
147
+
148
+ ```bash
149
+ pip install -e .
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Usage
155
+
156
+ ```bash
157
+ # Scan a file
158
+ python -m pyvibe path/to/file.py
159
+
160
+ # Scan a directory recursively
161
+ python -m pyvibe src/
162
+
163
+ # JSON output for CI/CD pipelines
164
+ python -m pyvibe src/ --json
165
+
166
+ # Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
167
+ python -m pyvibe src/ --exclude tests
168
+
169
+ # Exit code: 0 = clean, 1 = violations found, 2 = path error
170
+ ```
171
+
172
+ ### Example output
173
+
174
+ ```
175
+ python-vibe-guard
176
+ ─────────────────────────────────────────────
177
+
178
+ demo/bad_async.py
179
+
180
+ [CRITICAL] [PYVIBE-001] — line 14
181
+ Function : process_order()
182
+ Problem : time.sleep() blocks the entire event loop
183
+ Fix : Use `await asyncio.sleep(n)` instead
184
+
185
+ [CRITICAL] [PYVIBE-002] — line 20
186
+ Function : fetch_user()
187
+ Problem : requests.get() is synchronous — blocks the event loop
188
+ Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
189
+
190
+ [CRITICAL] [PYVIBE-003] — line 26
191
+ Function : orchestrate()
192
+ Problem : asyncio.run() inside async def raises RuntimeError at runtime
193
+ Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
194
+
195
+ [CRITICAL] [PYVIBE-004] — line 32
196
+ Function : update_counter()
197
+ Problem : threading.Lock() blocks the event loop under contention
198
+ Fix : Use `asyncio.Lock()` with `async with lock:` instead
199
+
200
+ ─────────────────────────────────────────────
201
+ 4 violation(s) in 1 file(s)
202
+ ```
203
+
204
+ ---
205
+
206
+ ## CI/CD integration
207
+
208
+ Add to your GitHub Actions workflow:
209
+
210
+ ```yaml
211
+ - name: python-vibe-guard scan
212
+ run: |
213
+ pip install python-vibe-guard
214
+ python -m pyvibe src/ --json
215
+ ```
216
+
217
+ The scanner exits with code `1` when violations are found, failing the CI job.
218
+
219
+ ---
220
+
221
+ ## Pre-commit integration
222
+
223
+ Add to your `.pre-commit-config.yaml`:
224
+
225
+ ```yaml
226
+ repos:
227
+ - repo: https://github.com/Joaquinriosheredia/python-vibe-guard
228
+ rev: v0.7.0
229
+ hooks:
230
+ - id: python-vibe-guard
231
+ ```
232
+
233
+ Then install the hook:
234
+
235
+ ```bash
236
+ pre-commit install
237
+ ```
238
+
239
+ The hook runs on every `git commit`, scans all Python files in the project, and blocks the commit if any violations are found.
240
+
241
+ ---
242
+
243
+ ## Design
244
+
245
+ - **Pure AST, zero runtime dependencies** — uses only Python's built-in `ast` module
246
+ - **Each rule is an independent `ast.NodeVisitor`** — easy to add, disable, or extend
247
+ - **Gate on `async def`** — most rules check `_current_async_func` before firing; PYVIBE-017 (silent except) fires in any function context
248
+ - **No import resolution** — works on any Python file without installing its dependencies
249
+ - **Test-aware severity** — PYVIBE-013 is automatically downgraded to WARNING in test files
250
+
251
+ ---
252
+
253
+ ## Run the demo
254
+
255
+ ```bash
256
+ python -m pyvibe demo/bad_async.py
257
+ ```
258
+
259
+ Expected: 20 findings (17 CRITICAL + 3 WARNING for PYVIBE-017 `except Exception`, PYVIBE-019 retry without backoff, and PYVIBE-020 `put_nowait` without handler), one per rule. `demo/bad_async.py` also contains a sync function that mirrors the async-specific patterns — those produce zero findings.
260
+
261
+ ---
262
+
263
+ ## Run the tests
264
+
265
+ ```bash
266
+ python -m pytest tests/ -v
267
+ # or
268
+ python tests/test_rules.py
269
+ ```
270
+
271
+ 123 tests: true positives + false-positive guards for every rule.
272
+
273
+ ---
274
+
275
+ ## Ecosystem
276
+
277
+ This project is part of the **vibe-guard** family of runtime anti-pattern scanners:
278
+
279
+ - [java-vibe-guard](https://github.com/jouninno/java-vibe-guard) — Spring Boot / async Java (blocking Kafka, @Transactional + Virtual Threads)
280
+ - **python-vibe-guard** — FastAPI / asyncio (this project)
281
+
282
+ ---
283
+
284
+ ## License
285
+
286
+ MIT
@@ -0,0 +1,271 @@
1
+ # python-vibe-guard
2
+
3
+ [![tests](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/Joaquinriosheredia/python-vibe-guard/actions/workflows/ci.yml)
4
+
5
+ ## The incident
6
+
7
+ A FastAPI service that handled 50 concurrent requests in staging started timing out in production at 200 rps. The team spent two days adding replicas, tweaking Gunicorn workers, and profiling CPU — nothing helped. The p99 latency was 8 seconds for an endpoint that should take 80ms.
8
+
9
+ > Scenario based on a common pattern observed in async Python codebases.
10
+ > Reproduced in controlled testing with locust against a FastAPI service.
11
+
12
+ Root cause: one engineer had asked an AI assistant to "add a retry with backoff" to an async handler. The AI generated `time.sleep(2)` inside the `async def`. In staging with a handful of requests it was invisible. In production it froze the entire event loop for 2 seconds per request, serializing all 200 concurrent calls through a single bottleneck.
13
+
14
+ The code passed every unit test. It passed the integration tests. It shipped to production in a Friday deploy.
15
+
16
+ **python-vibe-guard catches this in CI before it reaches staging.**
17
+
18
+ ---
19
+
20
+ ## What it detects
21
+
22
+ Twenty patterns that AI models generate repeatedly, that pass all static checks, and that silently destroy async performance under real load:
23
+
24
+ ### A. Event Loop Blocking
25
+
26
+ Synchronous calls inside `async def` — each stalls the event loop for its entire duration, serializing all concurrent requests.
27
+
28
+ | Rule | Pattern | Gate | Runtime effect |
29
+ |------|---------|------|----------------|
30
+ | PYVIBE-001 | `time.sleep()` inside `async def` | `async def` | Freezes entire event loop for sleep duration |
31
+ | PYVIBE-002 | `requests.*` inside `async def` | `async def` | Blocks OS thread, serializes all concurrent I/O |
32
+ | PYVIBE-007 | `subprocess.run/call/check_output/Popen` inside `async def` | `async def` | Blocks OS thread for entire subprocess duration |
33
+ | PYVIBE-008 | `sqlite3.connect()` inside `async def` | `async def` | Synchronous file I/O blocks the event loop |
34
+ | PYVIBE-009 | `open()` builtin inside `async def` | `async def` | Synchronous file I/O blocks the event loop |
35
+ | PYVIBE-010 | `httpx.get/post/put/…` inside `async def` | `async def` | httpx sync API blocks OS thread for full HTTP round-trip |
36
+ | PYVIBE-011 | `os.system/popen/waitpid` inside `async def` | `async def` | Blocking OS calls with no direct async equivalent |
37
+ | PYVIBE-016 | `httpx.Client()` instantiated inside `async def` | `async def` | Sync client blocks OS thread per request; `httpx.AsyncClient()` is excluded |
38
+
39
+ ### B. Async Lifecycle Misuse
40
+
41
+ Incorrect use of asyncio primitives that raise `RuntimeError` at runtime or silently discard tasks mid-execution.
42
+
43
+ | Rule | Pattern | Gate | Runtime effect |
44
+ |------|---------|------|----------------|
45
+ | PYVIBE-003 | `asyncio.run()` inside `async def` | `async def` | `RuntimeError: This event loop is already running` |
46
+ | PYVIBE-012 | `asyncio.create_task()` with discarded return value | `async def` | Task GC'd mid-execution; exceptions silently swallowed |
47
+ | PYVIBE-013 | `asyncio.gather()` without `return_exceptions=True` | `async def` | First exception leaks remaining tasks; no per-task error handling |
48
+ | PYVIBE-014 | `asyncio.ensure_future()` with discarded return value | `async def` | Same GC hazard as PYVIBE-012; pre-3.7 API still common in older codebases |
49
+ | PYVIBE-015 | `loop.run_until_complete()` inside `async def` | `async def` | `RuntimeError: This event loop is already running` |
50
+
51
+ ### C. Concurrency & State Hazards
52
+
53
+ Patterns that introduce data races, swallowed errors, or runaway loops under concurrent async load.
54
+
55
+ | Rule | Pattern | Gate | Runtime effect |
56
+ |------|---------|------|----------------|
57
+ | PYVIBE-004 | `threading.Lock/RLock/…` inside `async def` | `async def` | Blocks event loop under contention |
58
+ | PYVIBE-005 | `@app.task` / `@shared_task` without `soft_time_limit` or `time_limit` | decorator | Worker hangs forever if external call never returns |
59
+ | PYVIBE-006 | `ContextVar.set()` inside `async def` without `try/finally reset()` | `async def` | Context leaks into sibling async tasks |
60
+ | PYVIBE-017 | `except Exception: pass` / bare `except: pass` (empty body) | any | Swallows all errors silently; bare except also catches `KeyboardInterrupt`/`SystemExit` |
61
+ | PYVIBE-018 | `while True:` inside `async def` with no `await` in body | `async def` | Event loop blocked indefinitely; CPU hits 100% |
62
+ | PYVIBE-019 | retry `for`/`while` loop in `async def` with no backoff in `except` | `async def` | Tight retry loop on failure: thousands of failed requests/sec, cascading failures |
63
+ | PYVIBE-020 | `put_nowait()` without `asyncio.QueueFull` handler | any | `QueueFull` propagates unhandled; item is silently lost on bounded queues |
64
+
65
+ **Severity notes:**
66
+ - PYVIBE-005: `CRITICAL` — checks per-task decorator arguments only. **If your project sets a global `task_time_limit` via `app.conf.task_time_limit`, `app.conf.update(...)`, or in `celeryconfig.py` / `settings.py`, tasks already covered by that global limit will still be flagged.** Add per-task limits (self-documenting, immune to config drift) or suppress with `# noqa: PYVIBE-005`.
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
+ - PYVIBE-017: bare `except` → `CRITICAL` (catches `KeyboardInterrupt`/`SystemExit`); `except Exception` with empty body → `WARNING`. Specific exceptions (`except ValueError: pass`) are not flagged.
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.
70
+ - PYVIBE-019: `WARNING` — flags `except` that ends with `continue` or is solely `pass` with no sleep/backoff call. Suppressed when an escalation pattern (`if … : raise/break`) is present.
71
+ - PYVIBE-020: `WARNING` — fires in any function context (sync and async). Suppressed when the `put_nowait()` is inside a `try` whose handlers include `asyncio.QueueFull`, `QueueFull`, bare `except`, or `Exception`.
72
+
73
+ ---
74
+
75
+ ## Validation
76
+
77
+ ### 100-repo sweep (automated, v0.7.0)
78
+
79
+ Automated scan of 100 GitHub repos (`stars:>100`, updated last year, async Python keywords). Script: [`validation/run_massive_scan.py`](validation/run_massive_scan.py).
80
+
81
+ | Metric | Value |
82
+ |--------|-------|
83
+ | Repos scanned | 100 |
84
+ | .py files | 64,335 |
85
+ | Total violations | 3,639 |
86
+
87
+ **Rule prevalence (top 10):**
88
+
89
+ | Rule | Hits | Repos affected | % of repos |
90
+ |------|------|----------------|-----------|
91
+ | PYVIBE-017 `except Exception: pass` | 999 | 57 | **57%** |
92
+ | PYVIBE-013 `gather()` no return_exceptions | 766 | 47 | **47%** |
93
+ | PYVIBE-019 retry loop no backoff | 408 | 43 | **43%** |
94
+ | PYVIBE-009 `open()` in async def | 178 | 33 | **33%** |
95
+ | PYVIBE-005 `@task` no time_limit | 882 | 15 | 15% |
96
+ | PYVIBE-001 `time.sleep` in async | 55 | 16 | 16% |
97
+ | PYVIBE-012 orphaned `create_task()` | 58 | 13 | 13% |
98
+ | PYVIBE-008 `sqlite3` in async def | 121 | 10 | 10% |
99
+ | PYVIBE-018 `while True` no await | 36 | 10 | 10% |
100
+ | PYVIBE-006 ContextVar no reset | 19 | 10 | 10% |
101
+
102
+ Full data: [`validation/aggregate.json`](validation/aggregate.json) · [`validation/massive-results.md`](validation/massive-results.md)
103
+
104
+ ### Reference scan — 4 repos (reproducible)
105
+
106
+ | Repo | .py files | Violations |
107
+ |------|-----------|-----------|
108
+ | [fastapi/fastapi](https://github.com/tiangolo/fastapi) | 1 121 | 5 |
109
+ | [celery/celery](https://github.com/celery/celery) | 416 | 363 |
110
+ | [aio-libs/aiohttp](https://github.com/aio-libs/aiohttp) | 164 | 29 |
111
+ | [encode/httpx](https://github.com/encode/httpx) | 60 | 0 |
112
+ | **Total** | **1 761** | **397** |
113
+
114
+ Raw data: [`validation/breakdown.json`](validation/breakdown.json)
115
+
116
+ **Notable findings:**
117
+
118
+ - **home-assistant/core** — 418 violations across 17,702 files; 331 PYVIBE-013, 21 PYVIBE-018.
119
+ - **Celery** — 352 tasks without `time_limit`; workers can hang indefinitely on broker timeouts.
120
+ - **aiortc, anyio, uvicorn** — `while True` loops without `await` in production async I/O code (PYVIBE-018, 29 confirmed real hits after async-generator FP fix).
121
+ - **aiohttp examples** — `open()` in async WebSocket handlers; synchronous I/O in production-facing code.
122
+
123
+ ---
124
+
125
+ ## Installation
126
+
127
+ ```bash
128
+ pip install python-vibe-guard
129
+ ```
130
+
131
+ Or run without installing:
132
+
133
+ ```bash
134
+ pip install -e .
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Usage
140
+
141
+ ```bash
142
+ # Scan a file
143
+ python -m pyvibe path/to/file.py
144
+
145
+ # Scan a directory recursively
146
+ python -m pyvibe src/
147
+
148
+ # JSON output for CI/CD pipelines
149
+ python -m pyvibe src/ --json
150
+
151
+ # Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
152
+ python -m pyvibe src/ --exclude tests
153
+
154
+ # Exit code: 0 = clean, 1 = violations found, 2 = path error
155
+ ```
156
+
157
+ ### Example output
158
+
159
+ ```
160
+ python-vibe-guard
161
+ ─────────────────────────────────────────────
162
+
163
+ demo/bad_async.py
164
+
165
+ [CRITICAL] [PYVIBE-001] — line 14
166
+ Function : process_order()
167
+ Problem : time.sleep() blocks the entire event loop
168
+ Fix : Use `await asyncio.sleep(n)` instead
169
+
170
+ [CRITICAL] [PYVIBE-002] — line 20
171
+ Function : fetch_user()
172
+ Problem : requests.get() is synchronous — blocks the event loop
173
+ Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
174
+
175
+ [CRITICAL] [PYVIBE-003] — line 26
176
+ Function : orchestrate()
177
+ Problem : asyncio.run() inside async def raises RuntimeError at runtime
178
+ Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
179
+
180
+ [CRITICAL] [PYVIBE-004] — line 32
181
+ Function : update_counter()
182
+ Problem : threading.Lock() blocks the event loop under contention
183
+ Fix : Use `asyncio.Lock()` with `async with lock:` instead
184
+
185
+ ─────────────────────────────────────────────
186
+ 4 violation(s) in 1 file(s)
187
+ ```
188
+
189
+ ---
190
+
191
+ ## CI/CD integration
192
+
193
+ Add to your GitHub Actions workflow:
194
+
195
+ ```yaml
196
+ - name: python-vibe-guard scan
197
+ run: |
198
+ pip install python-vibe-guard
199
+ python -m pyvibe src/ --json
200
+ ```
201
+
202
+ The scanner exits with code `1` when violations are found, failing the CI job.
203
+
204
+ ---
205
+
206
+ ## Pre-commit integration
207
+
208
+ Add to your `.pre-commit-config.yaml`:
209
+
210
+ ```yaml
211
+ repos:
212
+ - repo: https://github.com/Joaquinriosheredia/python-vibe-guard
213
+ rev: v0.7.0
214
+ hooks:
215
+ - id: python-vibe-guard
216
+ ```
217
+
218
+ Then install the hook:
219
+
220
+ ```bash
221
+ pre-commit install
222
+ ```
223
+
224
+ The hook runs on every `git commit`, scans all Python files in the project, and blocks the commit if any violations are found.
225
+
226
+ ---
227
+
228
+ ## Design
229
+
230
+ - **Pure AST, zero runtime dependencies** — uses only Python's built-in `ast` module
231
+ - **Each rule is an independent `ast.NodeVisitor`** — easy to add, disable, or extend
232
+ - **Gate on `async def`** — most rules check `_current_async_func` before firing; PYVIBE-017 (silent except) fires in any function context
233
+ - **No import resolution** — works on any Python file without installing its dependencies
234
+ - **Test-aware severity** — PYVIBE-013 is automatically downgraded to WARNING in test files
235
+
236
+ ---
237
+
238
+ ## Run the demo
239
+
240
+ ```bash
241
+ python -m pyvibe demo/bad_async.py
242
+ ```
243
+
244
+ Expected: 20 findings (17 CRITICAL + 3 WARNING for PYVIBE-017 `except Exception`, PYVIBE-019 retry without backoff, and PYVIBE-020 `put_nowait` without handler), one per rule. `demo/bad_async.py` also contains a sync function that mirrors the async-specific patterns — those produce zero findings.
245
+
246
+ ---
247
+
248
+ ## Run the tests
249
+
250
+ ```bash
251
+ python -m pytest tests/ -v
252
+ # or
253
+ python tests/test_rules.py
254
+ ```
255
+
256
+ 123 tests: true positives + false-positive guards for every rule.
257
+
258
+ ---
259
+
260
+ ## Ecosystem
261
+
262
+ This project is part of the **vibe-guard** family of runtime anti-pattern scanners:
263
+
264
+ - [java-vibe-guard](https://github.com/jouninno/java-vibe-guard) — Spring Boot / async Java (blocking Kafka, @Transactional + Virtual Threads)
265
+ - **python-vibe-guard** — FastAPI / asyncio (this project)
266
+
267
+ ---
268
+
269
+ ## License
270
+
271
+ MIT
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-vibe-guard"
7
+ version = "0.7.1"
8
+ description = "Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ keywords = ["async", "linter", "fastapi", "asyncio", "static-analysis"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Topic :: Software Development :: Quality Assurance",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+
22
+ [project.scripts]
23
+ pyvibe = "pyvibe.cli:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["."]
27
+ include = ["pyvibe*"]