python-vibe-guard 0.7.1__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/PKG-INFO +90 -6
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/README.md +89 -5
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyproject.toml +1 -1
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/PKG-INFO +90 -6
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/SOURCES.txt +7 -1
- python_vibe_guard-0.9.0/pyvibe/__init__.py +1 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/analyzer.py +13 -0
- python_vibe_guard-0.9.0/pyvibe/autofix.py +31 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/cli.py +50 -0
- python_vibe_guard-0.9.0/pyvibe/explain.py +157 -0
- python_vibe_guard-0.9.0/pyvibe/rule_docs.py +64 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/async_requests.py +14 -2
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/async_sleep.py +13 -1
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/asyncio_run.py +15 -1
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/base.py +1 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/sqlite_async.py +11 -2
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/subprocess_async.py +43 -1
- python_vibe_guard-0.9.0/pyvibe/sarif.py +95 -0
- python_vibe_guard-0.9.0/tests/test_explain.py +112 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/tests/test_rules.py +69 -0
- python_vibe_guard-0.9.0/tests/test_sarif.py +116 -0
- python_vibe_guard-0.7.1/pyvibe/__init__.py +0 -1
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/entry_points.txt +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/top_level.txt +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/__main__.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/__init__.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/celery_time_limit.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/contextvar_cleanup.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/create_task_orphan.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/ensure_future_orphan.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/httpx_client_sync.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/httpx_sync.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/loop_run_until_complete.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/open_async.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/os_blocking.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/queue_put_nowait.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/retry_no_backoff.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/silent_except.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/threading_lock.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/while_true_no_await.py +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/setup.cfg +0 -0
- {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/tests/test_exclude.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-vibe-guard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: async,linter,fastapi,asyncio,static-analysis
|
|
@@ -163,9 +163,16 @@ python -m pyvibe src/
|
|
|
163
163
|
# JSON output for CI/CD pipelines
|
|
164
164
|
python -m pyvibe src/ --json
|
|
165
165
|
|
|
166
|
+
# SARIF 2.1.0 output for GitHub Code Scanning (writes results.sarif)
|
|
167
|
+
python -m pyvibe src/ --sarif
|
|
168
|
+
python -m pyvibe src/ --sarif --sarif-output custom-path.sarif
|
|
169
|
+
|
|
166
170
|
# Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
|
|
167
171
|
python -m pyvibe src/ --exclude tests
|
|
168
172
|
|
|
173
|
+
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
174
|
+
python -m pyvibe explain PYVIBE-002
|
|
175
|
+
|
|
169
176
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
170
177
|
```
|
|
171
178
|
|
|
@@ -177,22 +184,29 @@ python -m pyvibe src/ --exclude tests
|
|
|
177
184
|
|
|
178
185
|
demo/bad_async.py
|
|
179
186
|
|
|
180
|
-
[CRITICAL] [PYVIBE-001] — line
|
|
187
|
+
[CRITICAL] [PYVIBE-001] — line 27
|
|
181
188
|
Function : process_order()
|
|
182
189
|
Problem : time.sleep() blocks the entire event loop
|
|
183
190
|
Fix : Use `await asyncio.sleep(n)` instead
|
|
191
|
+
Suggested fix:
|
|
192
|
+
await asyncio.sleep(2)
|
|
184
193
|
|
|
185
|
-
[CRITICAL] [PYVIBE-002] — line
|
|
194
|
+
[CRITICAL] [PYVIBE-002] — line 33
|
|
186
195
|
Function : fetch_user()
|
|
187
196
|
Problem : requests.get() is synchronous — blocks the event loop
|
|
188
197
|
Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
|
|
198
|
+
Suggested fix:
|
|
199
|
+
async with httpx.AsyncClient() as client:
|
|
200
|
+
response = await client.get(f"https://api.example.com/users/{user_id}")
|
|
189
201
|
|
|
190
|
-
[CRITICAL] [PYVIBE-003] — line
|
|
202
|
+
[CRITICAL] [PYVIBE-003] — line 39
|
|
191
203
|
Function : orchestrate()
|
|
192
204
|
Problem : asyncio.run() inside async def raises RuntimeError at runtime
|
|
193
205
|
Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
|
|
206
|
+
Suggested fix:
|
|
207
|
+
await process_order(42)
|
|
194
208
|
|
|
195
|
-
[CRITICAL] [PYVIBE-004] — line
|
|
209
|
+
[CRITICAL] [PYVIBE-004] — line 45
|
|
196
210
|
Function : update_counter()
|
|
197
211
|
Problem : threading.Lock() blocks the event loop under contention
|
|
198
212
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
@@ -201,6 +215,46 @@ python -m pyvibe src/ --exclude tests
|
|
|
201
215
|
4 violation(s) in 1 file(s)
|
|
202
216
|
```
|
|
203
217
|
|
|
218
|
+
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
219
|
+
printed to the terminal / JSON output only and are never written back to your files.
|
|
220
|
+
Currently available for PYVIBE-001, 002, 003, 007, and 008.
|
|
221
|
+
|
|
222
|
+
### `pyvibe explain`
|
|
223
|
+
|
|
224
|
+
Every rule's accuracy claims come from `research/accepted/PYVIBE-XXX.md` — a per-rule
|
|
225
|
+
evidence file with repo-sweep data, an evidence-level grade, and a hit-by-hit precision
|
|
226
|
+
audit. `pyvibe explain` surfaces that in the terminal instead of making you go dig for it:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
python -m pyvibe explain PYVIBE-002
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
PYVIBE-002 — requests.* inside async def
|
|
234
|
+
───────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
Problema:
|
|
237
|
+
requests.* inside async def
|
|
238
|
+
|
|
239
|
+
Por qué ocurre:
|
|
240
|
+
`requests` is a synchronous HTTP library. Calling it inside an async function
|
|
241
|
+
blocks the OS thread running the event loop. Under concurrent load this
|
|
242
|
+
serialises all I/O and eliminates any benefit of async.
|
|
243
|
+
|
|
244
|
+
Visto en: 4.0% (10/250 repos, sweep-250 dataset)
|
|
245
|
+
Nivel de evidencia: B
|
|
246
|
+
Precisión auditada: ~83%
|
|
247
|
+
Falsos positivos conocidos: EXECUTOR_WRAPPER; INNER_SYNC_FUNCTION_EXECUTOR; ...
|
|
248
|
+
|
|
249
|
+
Fix sugerido:
|
|
250
|
+
use `httpx.AsyncClient` or `aiohttp.ClientSession` with await.
|
|
251
|
+
|
|
252
|
+
Full report: research/accepted/PYVIBE-002.md
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
256
|
+
and exits non-zero — it never invents data.
|
|
257
|
+
|
|
204
258
|
---
|
|
205
259
|
|
|
206
260
|
## CI/CD integration
|
|
@@ -216,6 +270,36 @@ Add to your GitHub Actions workflow:
|
|
|
216
270
|
|
|
217
271
|
The scanner exits with code `1` when violations are found, failing the CI job.
|
|
218
272
|
|
|
273
|
+
### GitHub Code Scanning (SARIF)
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
- name: python-vibe-guard scan
|
|
277
|
+
run: |
|
|
278
|
+
pip install python-vibe-guard
|
|
279
|
+
python -m pyvibe src/ --sarif
|
|
280
|
+
continue-on-error: true # let the upload step surface results in the PR instead
|
|
281
|
+
|
|
282
|
+
- name: Upload SARIF
|
|
283
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
284
|
+
with:
|
|
285
|
+
sarif_file: results.sarif
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Findings then show up as annotations on the PR diff and in the repo's Security tab,
|
|
289
|
+
each linking back to its `research/accepted/PYVIBE-XXX.md` evidence file via `helpUri`.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## GitHub Action
|
|
294
|
+
|
|
295
|
+
- name: python-vibe-guard
|
|
296
|
+
uses: joaquinriosheredia/python-vibe-guard-action@v1
|
|
297
|
+
with:
|
|
298
|
+
path: src/
|
|
299
|
+
|
|
300
|
+
Violations appear directly in GitHub Security tab (SARIF).
|
|
301
|
+
Link: https://github.com/Joaquinriosheredia/python-vibe-guard-action
|
|
302
|
+
|
|
219
303
|
---
|
|
220
304
|
|
|
221
305
|
## Pre-commit integration
|
|
@@ -268,7 +352,7 @@ python -m pytest tests/ -v
|
|
|
268
352
|
python tests/test_rules.py
|
|
269
353
|
```
|
|
270
354
|
|
|
271
|
-
|
|
355
|
+
226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
|
|
272
356
|
|
|
273
357
|
---
|
|
274
358
|
|
|
@@ -148,9 +148,16 @@ python -m pyvibe src/
|
|
|
148
148
|
# JSON output for CI/CD pipelines
|
|
149
149
|
python -m pyvibe src/ --json
|
|
150
150
|
|
|
151
|
+
# SARIF 2.1.0 output for GitHub Code Scanning (writes results.sarif)
|
|
152
|
+
python -m pyvibe src/ --sarif
|
|
153
|
+
python -m pyvibe src/ --sarif --sarif-output custom-path.sarif
|
|
154
|
+
|
|
151
155
|
# Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
|
|
152
156
|
python -m pyvibe src/ --exclude tests
|
|
153
157
|
|
|
158
|
+
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
159
|
+
python -m pyvibe explain PYVIBE-002
|
|
160
|
+
|
|
154
161
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
155
162
|
```
|
|
156
163
|
|
|
@@ -162,22 +169,29 @@ python -m pyvibe src/ --exclude tests
|
|
|
162
169
|
|
|
163
170
|
demo/bad_async.py
|
|
164
171
|
|
|
165
|
-
[CRITICAL] [PYVIBE-001] — line
|
|
172
|
+
[CRITICAL] [PYVIBE-001] — line 27
|
|
166
173
|
Function : process_order()
|
|
167
174
|
Problem : time.sleep() blocks the entire event loop
|
|
168
175
|
Fix : Use `await asyncio.sleep(n)` instead
|
|
176
|
+
Suggested fix:
|
|
177
|
+
await asyncio.sleep(2)
|
|
169
178
|
|
|
170
|
-
[CRITICAL] [PYVIBE-002] — line
|
|
179
|
+
[CRITICAL] [PYVIBE-002] — line 33
|
|
171
180
|
Function : fetch_user()
|
|
172
181
|
Problem : requests.get() is synchronous — blocks the event loop
|
|
173
182
|
Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
|
|
183
|
+
Suggested fix:
|
|
184
|
+
async with httpx.AsyncClient() as client:
|
|
185
|
+
response = await client.get(f"https://api.example.com/users/{user_id}")
|
|
174
186
|
|
|
175
|
-
[CRITICAL] [PYVIBE-003] — line
|
|
187
|
+
[CRITICAL] [PYVIBE-003] — line 39
|
|
176
188
|
Function : orchestrate()
|
|
177
189
|
Problem : asyncio.run() inside async def raises RuntimeError at runtime
|
|
178
190
|
Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
|
|
191
|
+
Suggested fix:
|
|
192
|
+
await process_order(42)
|
|
179
193
|
|
|
180
|
-
[CRITICAL] [PYVIBE-004] — line
|
|
194
|
+
[CRITICAL] [PYVIBE-004] — line 45
|
|
181
195
|
Function : update_counter()
|
|
182
196
|
Problem : threading.Lock() blocks the event loop under contention
|
|
183
197
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
@@ -186,6 +200,46 @@ python -m pyvibe src/ --exclude tests
|
|
|
186
200
|
4 violation(s) in 1 file(s)
|
|
187
201
|
```
|
|
188
202
|
|
|
203
|
+
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
204
|
+
printed to the terminal / JSON output only and are never written back to your files.
|
|
205
|
+
Currently available for PYVIBE-001, 002, 003, 007, and 008.
|
|
206
|
+
|
|
207
|
+
### `pyvibe explain`
|
|
208
|
+
|
|
209
|
+
Every rule's accuracy claims come from `research/accepted/PYVIBE-XXX.md` — a per-rule
|
|
210
|
+
evidence file with repo-sweep data, an evidence-level grade, and a hit-by-hit precision
|
|
211
|
+
audit. `pyvibe explain` surfaces that in the terminal instead of making you go dig for it:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python -m pyvibe explain PYVIBE-002
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
PYVIBE-002 — requests.* inside async def
|
|
219
|
+
───────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
Problema:
|
|
222
|
+
requests.* inside async def
|
|
223
|
+
|
|
224
|
+
Por qué ocurre:
|
|
225
|
+
`requests` is a synchronous HTTP library. Calling it inside an async function
|
|
226
|
+
blocks the OS thread running the event loop. Under concurrent load this
|
|
227
|
+
serialises all I/O and eliminates any benefit of async.
|
|
228
|
+
|
|
229
|
+
Visto en: 4.0% (10/250 repos, sweep-250 dataset)
|
|
230
|
+
Nivel de evidencia: B
|
|
231
|
+
Precisión auditada: ~83%
|
|
232
|
+
Falsos positivos conocidos: EXECUTOR_WRAPPER; INNER_SYNC_FUNCTION_EXECUTOR; ...
|
|
233
|
+
|
|
234
|
+
Fix sugerido:
|
|
235
|
+
use `httpx.AsyncClient` or `aiohttp.ClientSession` with await.
|
|
236
|
+
|
|
237
|
+
Full report: research/accepted/PYVIBE-002.md
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
241
|
+
and exits non-zero — it never invents data.
|
|
242
|
+
|
|
189
243
|
---
|
|
190
244
|
|
|
191
245
|
## CI/CD integration
|
|
@@ -201,6 +255,36 @@ Add to your GitHub Actions workflow:
|
|
|
201
255
|
|
|
202
256
|
The scanner exits with code `1` when violations are found, failing the CI job.
|
|
203
257
|
|
|
258
|
+
### GitHub Code Scanning (SARIF)
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
- name: python-vibe-guard scan
|
|
262
|
+
run: |
|
|
263
|
+
pip install python-vibe-guard
|
|
264
|
+
python -m pyvibe src/ --sarif
|
|
265
|
+
continue-on-error: true # let the upload step surface results in the PR instead
|
|
266
|
+
|
|
267
|
+
- name: Upload SARIF
|
|
268
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
269
|
+
with:
|
|
270
|
+
sarif_file: results.sarif
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Findings then show up as annotations on the PR diff and in the repo's Security tab,
|
|
274
|
+
each linking back to its `research/accepted/PYVIBE-XXX.md` evidence file via `helpUri`.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## GitHub Action
|
|
279
|
+
|
|
280
|
+
- name: python-vibe-guard
|
|
281
|
+
uses: joaquinriosheredia/python-vibe-guard-action@v1
|
|
282
|
+
with:
|
|
283
|
+
path: src/
|
|
284
|
+
|
|
285
|
+
Violations appear directly in GitHub Security tab (SARIF).
|
|
286
|
+
Link: https://github.com/Joaquinriosheredia/python-vibe-guard-action
|
|
287
|
+
|
|
204
288
|
---
|
|
205
289
|
|
|
206
290
|
## Pre-commit integration
|
|
@@ -253,7 +337,7 @@ python -m pytest tests/ -v
|
|
|
253
337
|
python tests/test_rules.py
|
|
254
338
|
```
|
|
255
339
|
|
|
256
|
-
|
|
340
|
+
226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
|
|
257
341
|
|
|
258
342
|
---
|
|
259
343
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-vibe-guard"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-vibe-guard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: async,linter,fastapi,asyncio,static-analysis
|
|
@@ -163,9 +163,16 @@ python -m pyvibe src/
|
|
|
163
163
|
# JSON output for CI/CD pipelines
|
|
164
164
|
python -m pyvibe src/ --json
|
|
165
165
|
|
|
166
|
+
# SARIF 2.1.0 output for GitHub Code Scanning (writes results.sarif)
|
|
167
|
+
python -m pyvibe src/ --sarif
|
|
168
|
+
python -m pyvibe src/ --sarif --sarif-output custom-path.sarif
|
|
169
|
+
|
|
166
170
|
# Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
|
|
167
171
|
python -m pyvibe src/ --exclude tests
|
|
168
172
|
|
|
173
|
+
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
174
|
+
python -m pyvibe explain PYVIBE-002
|
|
175
|
+
|
|
169
176
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
170
177
|
```
|
|
171
178
|
|
|
@@ -177,22 +184,29 @@ python -m pyvibe src/ --exclude tests
|
|
|
177
184
|
|
|
178
185
|
demo/bad_async.py
|
|
179
186
|
|
|
180
|
-
[CRITICAL] [PYVIBE-001] — line
|
|
187
|
+
[CRITICAL] [PYVIBE-001] — line 27
|
|
181
188
|
Function : process_order()
|
|
182
189
|
Problem : time.sleep() blocks the entire event loop
|
|
183
190
|
Fix : Use `await asyncio.sleep(n)` instead
|
|
191
|
+
Suggested fix:
|
|
192
|
+
await asyncio.sleep(2)
|
|
184
193
|
|
|
185
|
-
[CRITICAL] [PYVIBE-002] — line
|
|
194
|
+
[CRITICAL] [PYVIBE-002] — line 33
|
|
186
195
|
Function : fetch_user()
|
|
187
196
|
Problem : requests.get() is synchronous — blocks the event loop
|
|
188
197
|
Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
|
|
198
|
+
Suggested fix:
|
|
199
|
+
async with httpx.AsyncClient() as client:
|
|
200
|
+
response = await client.get(f"https://api.example.com/users/{user_id}")
|
|
189
201
|
|
|
190
|
-
[CRITICAL] [PYVIBE-003] — line
|
|
202
|
+
[CRITICAL] [PYVIBE-003] — line 39
|
|
191
203
|
Function : orchestrate()
|
|
192
204
|
Problem : asyncio.run() inside async def raises RuntimeError at runtime
|
|
193
205
|
Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
|
|
206
|
+
Suggested fix:
|
|
207
|
+
await process_order(42)
|
|
194
208
|
|
|
195
|
-
[CRITICAL] [PYVIBE-004] — line
|
|
209
|
+
[CRITICAL] [PYVIBE-004] — line 45
|
|
196
210
|
Function : update_counter()
|
|
197
211
|
Problem : threading.Lock() blocks the event loop under contention
|
|
198
212
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
@@ -201,6 +215,46 @@ python -m pyvibe src/ --exclude tests
|
|
|
201
215
|
4 violation(s) in 1 file(s)
|
|
202
216
|
```
|
|
203
217
|
|
|
218
|
+
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
219
|
+
printed to the terminal / JSON output only and are never written back to your files.
|
|
220
|
+
Currently available for PYVIBE-001, 002, 003, 007, and 008.
|
|
221
|
+
|
|
222
|
+
### `pyvibe explain`
|
|
223
|
+
|
|
224
|
+
Every rule's accuracy claims come from `research/accepted/PYVIBE-XXX.md` — a per-rule
|
|
225
|
+
evidence file with repo-sweep data, an evidence-level grade, and a hit-by-hit precision
|
|
226
|
+
audit. `pyvibe explain` surfaces that in the terminal instead of making you go dig for it:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
python -m pyvibe explain PYVIBE-002
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
PYVIBE-002 — requests.* inside async def
|
|
234
|
+
───────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
Problema:
|
|
237
|
+
requests.* inside async def
|
|
238
|
+
|
|
239
|
+
Por qué ocurre:
|
|
240
|
+
`requests` is a synchronous HTTP library. Calling it inside an async function
|
|
241
|
+
blocks the OS thread running the event loop. Under concurrent load this
|
|
242
|
+
serialises all I/O and eliminates any benefit of async.
|
|
243
|
+
|
|
244
|
+
Visto en: 4.0% (10/250 repos, sweep-250 dataset)
|
|
245
|
+
Nivel de evidencia: B
|
|
246
|
+
Precisión auditada: ~83%
|
|
247
|
+
Falsos positivos conocidos: EXECUTOR_WRAPPER; INNER_SYNC_FUNCTION_EXECUTOR; ...
|
|
248
|
+
|
|
249
|
+
Fix sugerido:
|
|
250
|
+
use `httpx.AsyncClient` or `aiohttp.ClientSession` with await.
|
|
251
|
+
|
|
252
|
+
Full report: research/accepted/PYVIBE-002.md
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
256
|
+
and exits non-zero — it never invents data.
|
|
257
|
+
|
|
204
258
|
---
|
|
205
259
|
|
|
206
260
|
## CI/CD integration
|
|
@@ -216,6 +270,36 @@ Add to your GitHub Actions workflow:
|
|
|
216
270
|
|
|
217
271
|
The scanner exits with code `1` when violations are found, failing the CI job.
|
|
218
272
|
|
|
273
|
+
### GitHub Code Scanning (SARIF)
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
- name: python-vibe-guard scan
|
|
277
|
+
run: |
|
|
278
|
+
pip install python-vibe-guard
|
|
279
|
+
python -m pyvibe src/ --sarif
|
|
280
|
+
continue-on-error: true # let the upload step surface results in the PR instead
|
|
281
|
+
|
|
282
|
+
- name: Upload SARIF
|
|
283
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
284
|
+
with:
|
|
285
|
+
sarif_file: results.sarif
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Findings then show up as annotations on the PR diff and in the repo's Security tab,
|
|
289
|
+
each linking back to its `research/accepted/PYVIBE-XXX.md` evidence file via `helpUri`.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## GitHub Action
|
|
294
|
+
|
|
295
|
+
- name: python-vibe-guard
|
|
296
|
+
uses: joaquinriosheredia/python-vibe-guard-action@v1
|
|
297
|
+
with:
|
|
298
|
+
path: src/
|
|
299
|
+
|
|
300
|
+
Violations appear directly in GitHub Security tab (SARIF).
|
|
301
|
+
Link: https://github.com/Joaquinriosheredia/python-vibe-guard-action
|
|
302
|
+
|
|
219
303
|
---
|
|
220
304
|
|
|
221
305
|
## Pre-commit integration
|
|
@@ -268,7 +352,7 @@ python -m pytest tests/ -v
|
|
|
268
352
|
python tests/test_rules.py
|
|
269
353
|
```
|
|
270
354
|
|
|
271
|
-
|
|
355
|
+
226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
|
|
272
356
|
|
|
273
357
|
---
|
|
274
358
|
|
|
@@ -8,7 +8,11 @@ python_vibe_guard.egg-info/top_level.txt
|
|
|
8
8
|
pyvibe/__init__.py
|
|
9
9
|
pyvibe/__main__.py
|
|
10
10
|
pyvibe/analyzer.py
|
|
11
|
+
pyvibe/autofix.py
|
|
11
12
|
pyvibe/cli.py
|
|
13
|
+
pyvibe/explain.py
|
|
14
|
+
pyvibe/rule_docs.py
|
|
15
|
+
pyvibe/sarif.py
|
|
12
16
|
pyvibe/rules/__init__.py
|
|
13
17
|
pyvibe/rules/async_requests.py
|
|
14
18
|
pyvibe/rules/async_sleep.py
|
|
@@ -32,4 +36,6 @@ pyvibe/rules/subprocess_async.py
|
|
|
32
36
|
pyvibe/rules/threading_lock.py
|
|
33
37
|
pyvibe/rules/while_true_no_await.py
|
|
34
38
|
tests/test_exclude.py
|
|
35
|
-
tests/
|
|
39
|
+
tests/test_explain.py
|
|
40
|
+
tests/test_rules.py
|
|
41
|
+
tests/test_sarif.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.0"
|
|
@@ -49,6 +49,17 @@ ALL_RULES = [
|
|
|
49
49
|
|
|
50
50
|
ALL_RULE_IDS: FrozenSet[str] = frozenset(r.RULE_ID for r in ALL_RULES)
|
|
51
51
|
|
|
52
|
+
# Rules that build a concrete "Suggested fix" snippet from the real source
|
|
53
|
+
# text (see pyvibe/autofix.py) and therefore need the full source passed
|
|
54
|
+
# into their constructor.
|
|
55
|
+
_NEEDS_SOURCE: FrozenSet[type] = frozenset({
|
|
56
|
+
AsyncSleepRule,
|
|
57
|
+
AsyncRequestsRule,
|
|
58
|
+
AsyncioRunRule,
|
|
59
|
+
SubprocessAsyncRule,
|
|
60
|
+
SqliteAsyncRule,
|
|
61
|
+
})
|
|
62
|
+
|
|
52
63
|
# Rules downgraded CRITICAL → WARNING when the violation is inside a test file.
|
|
53
64
|
# time.sleep in fixtures, subprocess in service-startup helpers, open() in
|
|
54
65
|
# async test helpers (e.g. asyncssh, aiofiles own test suites), and Celery task
|
|
@@ -101,6 +112,8 @@ def analyze_source(
|
|
|
101
112
|
for RuleClass in ALL_RULES:
|
|
102
113
|
if RuleClass is SilentExceptRule:
|
|
103
114
|
visitor = RuleClass(source_lines)
|
|
115
|
+
elif RuleClass in _NEEDS_SOURCE:
|
|
116
|
+
visitor = RuleClass(source)
|
|
104
117
|
else:
|
|
105
118
|
visitor = RuleClass()
|
|
106
119
|
visitor.visit(tree)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Shared helpers for building concrete "Suggested fix" snippets from real source context.
|
|
2
|
+
|
|
3
|
+
Rules that support autofix suggestions receive the original source text and use
|
|
4
|
+
`ast.get_source_segment` to pull the exact expressions the developer wrote
|
|
5
|
+
(variable names, literals, kwargs) into the suggested replacement, instead of
|
|
6
|
+
a synthetic placeholder like `...`.
|
|
7
|
+
"""
|
|
8
|
+
import ast
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def render_node(source: str, node: ast.AST) -> Optional[str]:
|
|
13
|
+
"""Return the literal source text of an AST node, or None if unavailable."""
|
|
14
|
+
return ast.get_source_segment(source, node)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def render_call_args(source: str, node: ast.Call) -> str:
|
|
18
|
+
"""Reconstruct the literal argument-list text of a Call node from source."""
|
|
19
|
+
parts = []
|
|
20
|
+
for arg in node.args:
|
|
21
|
+
if isinstance(arg, ast.Starred):
|
|
22
|
+
seg = render_node(source, arg.value)
|
|
23
|
+
parts.append(f"*{seg}" if seg else "*args")
|
|
24
|
+
else:
|
|
25
|
+
seg = render_node(source, arg)
|
|
26
|
+
parts.append(seg if seg is not None else "...")
|
|
27
|
+
for kw in node.keywords:
|
|
28
|
+
seg = render_node(source, kw.value)
|
|
29
|
+
seg = seg if seg is not None else "..."
|
|
30
|
+
parts.append(f"**{seg}" if kw.arg is None else f"{kw.arg}={seg}")
|
|
31
|
+
return ", ".join(parts)
|
|
@@ -5,8 +5,10 @@ python-vibe-guard — runtime anti-pattern scanner for async Python
|
|
|
5
5
|
Usage:
|
|
6
6
|
python -m pyvibe <path> # file or directory
|
|
7
7
|
python -m pyvibe <path> --json # machine-readable output
|
|
8
|
+
python -m pyvibe <path> --sarif # SARIF 2.1.0 -> results.sarif
|
|
8
9
|
python -m pyvibe <path> --no-test-files # skip test files entirely
|
|
9
10
|
python -m pyvibe <path> --downgrade-in-tests # WARNING instead of CRITICAL in all test files
|
|
11
|
+
python -m pyvibe explain PYVIBE-002 # show research evidence for a rule
|
|
10
12
|
"""
|
|
11
13
|
import argparse
|
|
12
14
|
import json
|
|
@@ -25,6 +27,10 @@ from pyvibe.analyzer import (
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def main():
|
|
30
|
+
if len(sys.argv) > 1 and sys.argv[1] == "explain":
|
|
31
|
+
_main_explain(sys.argv[2:])
|
|
32
|
+
return
|
|
33
|
+
|
|
28
34
|
parser = argparse.ArgumentParser(
|
|
29
35
|
prog="pyvibe",
|
|
30
36
|
description="Detect runtime anti-patterns in async Python code",
|
|
@@ -32,6 +38,17 @@ def main():
|
|
|
32
38
|
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
33
39
|
parser.add_argument("path", help="File or directory to scan")
|
|
34
40
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--sarif",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Also write SARIF 2.1.0 output (for GitHub Code Scanning)",
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--sarif-output",
|
|
48
|
+
metavar="PATH",
|
|
49
|
+
default="results.sarif",
|
|
50
|
+
help="Path to write SARIF output to (default: results.sarif)",
|
|
51
|
+
)
|
|
35
52
|
parser.add_argument(
|
|
36
53
|
"--exclude",
|
|
37
54
|
metavar="DIR",
|
|
@@ -97,6 +114,12 @@ def main():
|
|
|
97
114
|
total_violations = sum(len(v) for v in file_results.values())
|
|
98
115
|
total_files = sum(1 for v in file_results.values() if v)
|
|
99
116
|
|
|
117
|
+
if args.sarif:
|
|
118
|
+
from pyvibe.sarif import write_sarif
|
|
119
|
+
|
|
120
|
+
write_sarif(file_results, args.sarif_output)
|
|
121
|
+
print(f"SARIF results written to {args.sarif_output}")
|
|
122
|
+
|
|
100
123
|
if args.json:
|
|
101
124
|
output = []
|
|
102
125
|
for path, violations in file_results.items():
|
|
@@ -109,6 +132,7 @@ def main():
|
|
|
109
132
|
"function": v.function_name,
|
|
110
133
|
"message": v.message,
|
|
111
134
|
"evidence": v.evidence,
|
|
135
|
+
"suggested_fix": v.suggested_fix,
|
|
112
136
|
})
|
|
113
137
|
print(json.dumps(output, indent=2))
|
|
114
138
|
else:
|
|
@@ -137,6 +161,10 @@ def _print_human(file_results: dict, total_violations: int, total_files: int):
|
|
|
137
161
|
print(f" Function : {v.function_name}()")
|
|
138
162
|
print(f" Problem : {v.message}")
|
|
139
163
|
print(f" Fix : {v.evidence}")
|
|
164
|
+
if v.suggested_fix:
|
|
165
|
+
print(" Suggested fix:")
|
|
166
|
+
for line in v.suggested_fix.splitlines():
|
|
167
|
+
print(f" {line}")
|
|
140
168
|
print()
|
|
141
169
|
|
|
142
170
|
print(" ─────────────────────────────────────────────")
|
|
@@ -144,5 +172,27 @@ def _print_human(file_results: dict, total_violations: int, total_files: int):
|
|
|
144
172
|
print()
|
|
145
173
|
|
|
146
174
|
|
|
175
|
+
def _main_explain(argv):
|
|
176
|
+
parser = argparse.ArgumentParser(
|
|
177
|
+
prog="pyvibe explain",
|
|
178
|
+
description="Show the research evidence behind a python-vibe-guard rule",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument("rule_id", help="Rule ID, e.g. PYVIBE-002")
|
|
181
|
+
args = parser.parse_args(argv)
|
|
182
|
+
|
|
183
|
+
from pyvibe.explain import EvidenceNotFoundError, explain_rule
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
text = explain_rule(args.rule_id)
|
|
187
|
+
except EvidenceNotFoundError as e:
|
|
188
|
+
print(str(e))
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
|
|
191
|
+
print()
|
|
192
|
+
print(text)
|
|
193
|
+
print()
|
|
194
|
+
sys.exit(0)
|
|
195
|
+
|
|
196
|
+
|
|
147
197
|
if __name__ == "__main__":
|
|
148
198
|
main()
|