python-vibe-guard 0.11.0__tar.gz → 0.12.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.
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/PKG-INFO +109 -3
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/README.md +107 -2
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyproject.toml +4 -1
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/PKG-INFO +109 -3
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/SOURCES.txt +8 -1
- python_vibe_guard-0.12.1/python_vibe_guard.egg-info/requires.txt +3 -0
- python_vibe_guard-0.12.1/pyvibe/__init__.py +1 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/analyzer.py +97 -8
- python_vibe_guard-0.12.1/pyvibe/audit.py +119 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/cli.py +211 -18
- python_vibe_guard-0.12.1/pyvibe/config.py +99 -0
- python_vibe_guard-0.12.1/pyvibe/suppressions.py +102 -0
- python_vibe_guard-0.12.1/tests/test_audit.py +274 -0
- python_vibe_guard-0.12.1/tests/test_config.py +268 -0
- python_vibe_guard-0.12.1/tests/test_suppressions.py +158 -0
- python_vibe_guard-0.11.0/pyvibe/__init__.py +0 -1
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/entry_points.txt +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/top_level.txt +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/__main__.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/autofix.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/baseline.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/diff.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/explain.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rule_docs.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/__init__.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/async_requests.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/async_sleep.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/asyncio_run.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/base.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/celery_time_limit.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/contextvar_cleanup.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/create_task_orphan.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/ensure_future_orphan.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/httpx_client_sync.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/httpx_sync.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/loop_run_until_complete.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/open_async.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/os_blocking.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/queue_put_nowait.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/retry_no_backoff.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/silent_except.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/sqlite_async.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/subprocess_async.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/threading_lock.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/rules/while_true_no_await.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/pyvibe/sarif.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/setup.cfg +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_baseline.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_diff.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_exclude.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_explain.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/tests/test_rules.py +0 -0
- {python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/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.
|
|
3
|
+
Version: 0.12.1
|
|
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
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
15
16
|
|
|
16
17
|
# python-vibe-guard
|
|
17
18
|
|
|
@@ -177,6 +178,14 @@ python -m pyvibe explain PYVIBE-002
|
|
|
177
178
|
python -m pyvibe baseline create src/ # snapshot current findings
|
|
178
179
|
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
179
180
|
|
|
181
|
+
# Show suppressed findings (inline comments + pyvibe.toml) alongside the report
|
|
182
|
+
python -m pyvibe src/ --verbose
|
|
183
|
+
|
|
184
|
+
# Audit inline suppressions: justification coverage + orphaned (unused) suppressions
|
|
185
|
+
python -m pyvibe audit src/
|
|
186
|
+
python -m pyvibe audit src/ --json
|
|
187
|
+
python -m pyvibe audit src/ --fail-on-unused --fail-on-unjustified
|
|
188
|
+
|
|
180
189
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
181
190
|
```
|
|
182
191
|
|
|
@@ -216,7 +225,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
|
|
|
216
225
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
217
226
|
|
|
218
227
|
─────────────────────────────────────────────
|
|
219
|
-
4
|
|
228
|
+
4 reported · 0 suppressed
|
|
220
229
|
```
|
|
221
230
|
|
|
222
231
|
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
@@ -291,6 +300,103 @@ produces exit code `0`.
|
|
|
291
300
|
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
292
301
|
regenerate its own baseline instead.
|
|
293
302
|
|
|
303
|
+
### Suppressing findings
|
|
304
|
+
|
|
305
|
+
For one-off exceptions, use an inline comment right in the source:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
# pyvibe: ignore PYVIBE-008
|
|
309
|
+
conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
|
|
310
|
+
|
|
311
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
|
|
312
|
+
|
|
313
|
+
time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
|
|
314
|
+
|
|
315
|
+
# pyvibe: ignore-next-line PYVIBE-008
|
|
316
|
+
conn = sqlite3.connect(...) # suppressed — always targets the next line
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
|
|
320
|
+
(treated as a regular comment).
|
|
321
|
+
|
|
322
|
+
Add an optional justification after `--` — it's free text, stored and shown in `pyvibe audit`:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 -- legacy sqlite wrapper
|
|
326
|
+
# pyvibe: ignore-next-line PYVIBE-008 -- startup only
|
|
327
|
+
conn = sqlite3.connect(...)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
|
|
331
|
+
from the scan target to find it, same convention as `pyproject.toml`):
|
|
332
|
+
|
|
333
|
+
```toml
|
|
334
|
+
[tool.pyvibe]
|
|
335
|
+
ignore = ["PYVIBE-019"]
|
|
336
|
+
exclude = ["tests/**", "examples/**", "docs/**"]
|
|
337
|
+
|
|
338
|
+
[tool.pyvibe.severity]
|
|
339
|
+
PYVIBE-008 = "warning"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- `ignore`: rule IDs suppressed everywhere, project-wide.
|
|
343
|
+
- `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
|
|
344
|
+
to skip entirely.
|
|
345
|
+
- `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
|
|
346
|
+
applied after the built-in test-file downgrade.
|
|
347
|
+
|
|
348
|
+
Both mechanisms feed into the same summary line:
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
4 reported · 2 suppressed
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Add `--verbose` to see exactly what was suppressed and why:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
Suppressed:
|
|
358
|
+
PYVIBE-008 app/db.py:42 (inline)
|
|
359
|
+
PYVIBE-019 legacy.py:81 (config)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `pyvibe audit`
|
|
363
|
+
|
|
364
|
+
Audits every inline `# pyvibe: ignore` comment in a codebase: how many carry a justification,
|
|
365
|
+
and how many are **orphaned** — the rule they name never actually fires on the target line,
|
|
366
|
+
usually because the underlying code changed since the suppression was added.
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
$ pyvibe audit src/
|
|
370
|
+
|
|
371
|
+
Suppressions audit
|
|
372
|
+
──────────────────
|
|
373
|
+
Total suppressions: 12
|
|
374
|
+
With justification: 9 (75%)
|
|
375
|
+
Without justification: 3
|
|
376
|
+
Unused (no violation found): 2
|
|
377
|
+
|
|
378
|
+
By rule:
|
|
379
|
+
PYVIBE-008 5
|
|
380
|
+
PYVIBE-019 4
|
|
381
|
+
PYVIBE-003 3
|
|
382
|
+
|
|
383
|
+
Unused suppressions:
|
|
384
|
+
app/legacy.py:41 # pyvibe: ignore PYVIBE-008
|
|
385
|
+
|
|
386
|
+
Without justification:
|
|
387
|
+
models/db.py:88 # pyvibe: ignore PYVIBE-019
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- `--json` — machine-readable output (`total`, `with_justification`, `without_justification`,
|
|
391
|
+
`unused`, `by_rule`, `unused_suppressions`, `without_justification_suppressions`).
|
|
392
|
+
- `--fail-on-unused` — exit `1` if any orphaned suppression is found.
|
|
393
|
+
- `--fail-on-unjustified` — exit `1` if any suppression is missing a justification.
|
|
394
|
+
- `--max-unused N` — exit `1` if more than `N` orphaned suppressions are found.
|
|
395
|
+
- `--exclude DIR` — same directory-exclusion convention as `pyvibe scan`.
|
|
396
|
+
|
|
397
|
+
With no flags, `pyvibe audit` is purely informational (exit `0`) — the flags above are what
|
|
398
|
+
you wire into CI to enforce a justification/cleanup policy over time.
|
|
399
|
+
|
|
294
400
|
---
|
|
295
401
|
|
|
296
402
|
## CI/CD integration
|
|
@@ -388,7 +494,7 @@ python -m pytest tests/ -v
|
|
|
388
494
|
python tests/test_rules.py
|
|
389
495
|
```
|
|
390
496
|
|
|
391
|
-
|
|
497
|
+
331 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, suppressions (inline comments + pyvibe.toml), and `pyvibe audit` (justifications + orphan detection) coverage.
|
|
392
498
|
|
|
393
499
|
---
|
|
394
500
|
|
|
@@ -162,6 +162,14 @@ python -m pyvibe explain PYVIBE-002
|
|
|
162
162
|
python -m pyvibe baseline create src/ # snapshot current findings
|
|
163
163
|
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
164
164
|
|
|
165
|
+
# Show suppressed findings (inline comments + pyvibe.toml) alongside the report
|
|
166
|
+
python -m pyvibe src/ --verbose
|
|
167
|
+
|
|
168
|
+
# Audit inline suppressions: justification coverage + orphaned (unused) suppressions
|
|
169
|
+
python -m pyvibe audit src/
|
|
170
|
+
python -m pyvibe audit src/ --json
|
|
171
|
+
python -m pyvibe audit src/ --fail-on-unused --fail-on-unjustified
|
|
172
|
+
|
|
165
173
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
166
174
|
```
|
|
167
175
|
|
|
@@ -201,7 +209,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
|
|
|
201
209
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
202
210
|
|
|
203
211
|
─────────────────────────────────────────────
|
|
204
|
-
4
|
|
212
|
+
4 reported · 0 suppressed
|
|
205
213
|
```
|
|
206
214
|
|
|
207
215
|
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
@@ -276,6 +284,103 @@ produces exit code `0`.
|
|
|
276
284
|
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
277
285
|
regenerate its own baseline instead.
|
|
278
286
|
|
|
287
|
+
### Suppressing findings
|
|
288
|
+
|
|
289
|
+
For one-off exceptions, use an inline comment right in the source:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
# pyvibe: ignore PYVIBE-008
|
|
293
|
+
conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
|
|
294
|
+
|
|
295
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
|
|
296
|
+
|
|
297
|
+
time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
|
|
298
|
+
|
|
299
|
+
# pyvibe: ignore-next-line PYVIBE-008
|
|
300
|
+
conn = sqlite3.connect(...) # suppressed — always targets the next line
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
|
|
304
|
+
(treated as a regular comment).
|
|
305
|
+
|
|
306
|
+
Add an optional justification after `--` — it's free text, stored and shown in `pyvibe audit`:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 -- legacy sqlite wrapper
|
|
310
|
+
# pyvibe: ignore-next-line PYVIBE-008 -- startup only
|
|
311
|
+
conn = sqlite3.connect(...)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
|
|
315
|
+
from the scan target to find it, same convention as `pyproject.toml`):
|
|
316
|
+
|
|
317
|
+
```toml
|
|
318
|
+
[tool.pyvibe]
|
|
319
|
+
ignore = ["PYVIBE-019"]
|
|
320
|
+
exclude = ["tests/**", "examples/**", "docs/**"]
|
|
321
|
+
|
|
322
|
+
[tool.pyvibe.severity]
|
|
323
|
+
PYVIBE-008 = "warning"
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- `ignore`: rule IDs suppressed everywhere, project-wide.
|
|
327
|
+
- `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
|
|
328
|
+
to skip entirely.
|
|
329
|
+
- `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
|
|
330
|
+
applied after the built-in test-file downgrade.
|
|
331
|
+
|
|
332
|
+
Both mechanisms feed into the same summary line:
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
4 reported · 2 suppressed
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Add `--verbose` to see exactly what was suppressed and why:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
Suppressed:
|
|
342
|
+
PYVIBE-008 app/db.py:42 (inline)
|
|
343
|
+
PYVIBE-019 legacy.py:81 (config)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### `pyvibe audit`
|
|
347
|
+
|
|
348
|
+
Audits every inline `# pyvibe: ignore` comment in a codebase: how many carry a justification,
|
|
349
|
+
and how many are **orphaned** — the rule they name never actually fires on the target line,
|
|
350
|
+
usually because the underlying code changed since the suppression was added.
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
$ pyvibe audit src/
|
|
354
|
+
|
|
355
|
+
Suppressions audit
|
|
356
|
+
──────────────────
|
|
357
|
+
Total suppressions: 12
|
|
358
|
+
With justification: 9 (75%)
|
|
359
|
+
Without justification: 3
|
|
360
|
+
Unused (no violation found): 2
|
|
361
|
+
|
|
362
|
+
By rule:
|
|
363
|
+
PYVIBE-008 5
|
|
364
|
+
PYVIBE-019 4
|
|
365
|
+
PYVIBE-003 3
|
|
366
|
+
|
|
367
|
+
Unused suppressions:
|
|
368
|
+
app/legacy.py:41 # pyvibe: ignore PYVIBE-008
|
|
369
|
+
|
|
370
|
+
Without justification:
|
|
371
|
+
models/db.py:88 # pyvibe: ignore PYVIBE-019
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- `--json` — machine-readable output (`total`, `with_justification`, `without_justification`,
|
|
375
|
+
`unused`, `by_rule`, `unused_suppressions`, `without_justification_suppressions`).
|
|
376
|
+
- `--fail-on-unused` — exit `1` if any orphaned suppression is found.
|
|
377
|
+
- `--fail-on-unjustified` — exit `1` if any suppression is missing a justification.
|
|
378
|
+
- `--max-unused N` — exit `1` if more than `N` orphaned suppressions are found.
|
|
379
|
+
- `--exclude DIR` — same directory-exclusion convention as `pyvibe scan`.
|
|
380
|
+
|
|
381
|
+
With no flags, `pyvibe audit` is purely informational (exit `0`) — the flags above are what
|
|
382
|
+
you wire into CI to enforce a justification/cleanup policy over time.
|
|
383
|
+
|
|
279
384
|
---
|
|
280
385
|
|
|
281
386
|
## CI/CD integration
|
|
@@ -373,7 +478,7 @@ python -m pytest tests/ -v
|
|
|
373
478
|
python tests/test_rules.py
|
|
374
479
|
```
|
|
375
480
|
|
|
376
|
-
|
|
481
|
+
331 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, suppressions (inline comments + pyvibe.toml), and `pyvibe audit` (justifications + orphan detection) coverage.
|
|
377
482
|
|
|
378
483
|
---
|
|
379
484
|
|
|
@@ -4,10 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-vibe-guard"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.12.1"
|
|
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"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
13
|
+
]
|
|
11
14
|
license = {text = "MIT"}
|
|
12
15
|
keywords = ["async", "linter", "fastapi", "asyncio", "static-analysis"]
|
|
13
16
|
classifiers = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-vibe-guard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.1
|
|
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
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Requires-Python: >=3.10
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
15
16
|
|
|
16
17
|
# python-vibe-guard
|
|
17
18
|
|
|
@@ -177,6 +178,14 @@ python -m pyvibe explain PYVIBE-002
|
|
|
177
178
|
python -m pyvibe baseline create src/ # snapshot current findings
|
|
178
179
|
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
179
180
|
|
|
181
|
+
# Show suppressed findings (inline comments + pyvibe.toml) alongside the report
|
|
182
|
+
python -m pyvibe src/ --verbose
|
|
183
|
+
|
|
184
|
+
# Audit inline suppressions: justification coverage + orphaned (unused) suppressions
|
|
185
|
+
python -m pyvibe audit src/
|
|
186
|
+
python -m pyvibe audit src/ --json
|
|
187
|
+
python -m pyvibe audit src/ --fail-on-unused --fail-on-unjustified
|
|
188
|
+
|
|
180
189
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
181
190
|
```
|
|
182
191
|
|
|
@@ -216,7 +225,7 @@ python -m pyvibe src/ --baseline # only reports findings NOT in the bas
|
|
|
216
225
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
217
226
|
|
|
218
227
|
─────────────────────────────────────────────
|
|
219
|
-
4
|
|
228
|
+
4 reported · 0 suppressed
|
|
220
229
|
```
|
|
221
230
|
|
|
222
231
|
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
@@ -291,6 +300,103 @@ produces exit code `0`.
|
|
|
291
300
|
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
292
301
|
regenerate its own baseline instead.
|
|
293
302
|
|
|
303
|
+
### Suppressing findings
|
|
304
|
+
|
|
305
|
+
For one-off exceptions, use an inline comment right in the source:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
# pyvibe: ignore PYVIBE-008
|
|
309
|
+
conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
|
|
310
|
+
|
|
311
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
|
|
312
|
+
|
|
313
|
+
time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
|
|
314
|
+
|
|
315
|
+
# pyvibe: ignore-next-line PYVIBE-008
|
|
316
|
+
conn = sqlite3.connect(...) # suppressed — always targets the next line
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
|
|
320
|
+
(treated as a regular comment).
|
|
321
|
+
|
|
322
|
+
Add an optional justification after `--` — it's free text, stored and shown in `pyvibe audit`:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 -- legacy sqlite wrapper
|
|
326
|
+
# pyvibe: ignore-next-line PYVIBE-008 -- startup only
|
|
327
|
+
conn = sqlite3.connect(...)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
|
|
331
|
+
from the scan target to find it, same convention as `pyproject.toml`):
|
|
332
|
+
|
|
333
|
+
```toml
|
|
334
|
+
[tool.pyvibe]
|
|
335
|
+
ignore = ["PYVIBE-019"]
|
|
336
|
+
exclude = ["tests/**", "examples/**", "docs/**"]
|
|
337
|
+
|
|
338
|
+
[tool.pyvibe.severity]
|
|
339
|
+
PYVIBE-008 = "warning"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
- `ignore`: rule IDs suppressed everywhere, project-wide.
|
|
343
|
+
- `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
|
|
344
|
+
to skip entirely.
|
|
345
|
+
- `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
|
|
346
|
+
applied after the built-in test-file downgrade.
|
|
347
|
+
|
|
348
|
+
Both mechanisms feed into the same summary line:
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
4 reported · 2 suppressed
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Add `--verbose` to see exactly what was suppressed and why:
|
|
355
|
+
|
|
356
|
+
```
|
|
357
|
+
Suppressed:
|
|
358
|
+
PYVIBE-008 app/db.py:42 (inline)
|
|
359
|
+
PYVIBE-019 legacy.py:81 (config)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `pyvibe audit`
|
|
363
|
+
|
|
364
|
+
Audits every inline `# pyvibe: ignore` comment in a codebase: how many carry a justification,
|
|
365
|
+
and how many are **orphaned** — the rule they name never actually fires on the target line,
|
|
366
|
+
usually because the underlying code changed since the suppression was added.
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
$ pyvibe audit src/
|
|
370
|
+
|
|
371
|
+
Suppressions audit
|
|
372
|
+
──────────────────
|
|
373
|
+
Total suppressions: 12
|
|
374
|
+
With justification: 9 (75%)
|
|
375
|
+
Without justification: 3
|
|
376
|
+
Unused (no violation found): 2
|
|
377
|
+
|
|
378
|
+
By rule:
|
|
379
|
+
PYVIBE-008 5
|
|
380
|
+
PYVIBE-019 4
|
|
381
|
+
PYVIBE-003 3
|
|
382
|
+
|
|
383
|
+
Unused suppressions:
|
|
384
|
+
app/legacy.py:41 # pyvibe: ignore PYVIBE-008
|
|
385
|
+
|
|
386
|
+
Without justification:
|
|
387
|
+
models/db.py:88 # pyvibe: ignore PYVIBE-019
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- `--json` — machine-readable output (`total`, `with_justification`, `without_justification`,
|
|
391
|
+
`unused`, `by_rule`, `unused_suppressions`, `without_justification_suppressions`).
|
|
392
|
+
- `--fail-on-unused` — exit `1` if any orphaned suppression is found.
|
|
393
|
+
- `--fail-on-unjustified` — exit `1` if any suppression is missing a justification.
|
|
394
|
+
- `--max-unused N` — exit `1` if more than `N` orphaned suppressions are found.
|
|
395
|
+
- `--exclude DIR` — same directory-exclusion convention as `pyvibe scan`.
|
|
396
|
+
|
|
397
|
+
With no flags, `pyvibe audit` is purely informational (exit `0`) — the flags above are what
|
|
398
|
+
you wire into CI to enforce a justification/cleanup policy over time.
|
|
399
|
+
|
|
294
400
|
---
|
|
295
401
|
|
|
296
402
|
## CI/CD integration
|
|
@@ -388,7 +494,7 @@ python -m pytest tests/ -v
|
|
|
388
494
|
python tests/test_rules.py
|
|
389
495
|
```
|
|
390
496
|
|
|
391
|
-
|
|
497
|
+
331 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, suppressions (inline comments + pyvibe.toml), and `pyvibe audit` (justifications + orphan detection) coverage.
|
|
392
498
|
|
|
393
499
|
---
|
|
394
500
|
|
{python_vibe_guard-0.11.0 → python_vibe_guard-0.12.1}/python_vibe_guard.egg-info/SOURCES.txt
RENAMED
|
@@ -4,17 +4,21 @@ python_vibe_guard.egg-info/PKG-INFO
|
|
|
4
4
|
python_vibe_guard.egg-info/SOURCES.txt
|
|
5
5
|
python_vibe_guard.egg-info/dependency_links.txt
|
|
6
6
|
python_vibe_guard.egg-info/entry_points.txt
|
|
7
|
+
python_vibe_guard.egg-info/requires.txt
|
|
7
8
|
python_vibe_guard.egg-info/top_level.txt
|
|
8
9
|
pyvibe/__init__.py
|
|
9
10
|
pyvibe/__main__.py
|
|
10
11
|
pyvibe/analyzer.py
|
|
12
|
+
pyvibe/audit.py
|
|
11
13
|
pyvibe/autofix.py
|
|
12
14
|
pyvibe/baseline.py
|
|
13
15
|
pyvibe/cli.py
|
|
16
|
+
pyvibe/config.py
|
|
14
17
|
pyvibe/diff.py
|
|
15
18
|
pyvibe/explain.py
|
|
16
19
|
pyvibe/rule_docs.py
|
|
17
20
|
pyvibe/sarif.py
|
|
21
|
+
pyvibe/suppressions.py
|
|
18
22
|
pyvibe/rules/__init__.py
|
|
19
23
|
pyvibe/rules/async_requests.py
|
|
20
24
|
pyvibe/rules/async_sleep.py
|
|
@@ -37,9 +41,12 @@ pyvibe/rules/sqlite_async.py
|
|
|
37
41
|
pyvibe/rules/subprocess_async.py
|
|
38
42
|
pyvibe/rules/threading_lock.py
|
|
39
43
|
pyvibe/rules/while_true_no_await.py
|
|
44
|
+
tests/test_audit.py
|
|
40
45
|
tests/test_baseline.py
|
|
46
|
+
tests/test_config.py
|
|
41
47
|
tests/test_diff.py
|
|
42
48
|
tests/test_exclude.py
|
|
43
49
|
tests/test_explain.py
|
|
44
50
|
tests/test_rules.py
|
|
45
|
-
tests/test_sarif.py
|
|
51
|
+
tests/test_sarif.py
|
|
52
|
+
tests/test_suppressions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.12.1"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import FrozenSet, List, Optional
|
|
3
|
+
from typing import FrozenSet, List, Optional, Tuple
|
|
4
4
|
|
|
5
|
+
from pyvibe.config import PyvibeConfig, is_excluded
|
|
5
6
|
from pyvibe.rules.base import Violation
|
|
7
|
+
from pyvibe.suppressions import parse_inline_suppressions
|
|
6
8
|
from pyvibe.rules.async_sleep import AsyncSleepRule
|
|
7
9
|
from pyvibe.rules.async_requests import AsyncRequestsRule
|
|
8
10
|
from pyvibe.rules.asyncio_run import AsyncioRunRule
|
|
@@ -90,22 +92,29 @@ def _is_test_file(filepath: str) -> bool:
|
|
|
90
92
|
)
|
|
91
93
|
|
|
92
94
|
|
|
93
|
-
def
|
|
95
|
+
def analyze_source_full(
|
|
94
96
|
source: str,
|
|
95
97
|
filepath: str = "<string>",
|
|
96
98
|
*,
|
|
97
99
|
downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
config: Optional[PyvibeConfig] = None,
|
|
101
|
+
) -> Tuple[List[Violation], List[Tuple[Violation, str]]]:
|
|
102
|
+
"""Parse source and run all rules. Returns (reported, suppressed) where
|
|
103
|
+
suppressed is a list of (Violation, reason) with reason "inline" (a
|
|
104
|
+
`# pyvibe: ignore ...` comment) or "config" (pyvibe.toml's [tool.pyvibe]
|
|
105
|
+
ignore list).
|
|
100
106
|
|
|
101
107
|
downgrade_in_tests: rule IDs whose severity is lowered to WARNING when
|
|
102
108
|
the file is detected as a test file. Pass frozenset() to disable all
|
|
103
109
|
downgrading, or ALL_RULE_IDS to downgrade every rule.
|
|
110
|
+
|
|
111
|
+
config: optional PyvibeConfig (see pyvibe/config.py) applying a global
|
|
112
|
+
ignore list and per-rule severity overrides on top of the above.
|
|
104
113
|
"""
|
|
105
114
|
try:
|
|
106
115
|
tree = ast.parse(source)
|
|
107
116
|
except SyntaxError:
|
|
108
|
-
return []
|
|
117
|
+
return [], []
|
|
109
118
|
|
|
110
119
|
source_lines = source.splitlines()
|
|
111
120
|
violations = []
|
|
@@ -124,8 +133,53 @@ def analyze_source(
|
|
|
124
133
|
if v.rule_id in downgrade_in_tests:
|
|
125
134
|
v.severity = "WARNING"
|
|
126
135
|
|
|
136
|
+
if config and config.severity:
|
|
137
|
+
for v in violations:
|
|
138
|
+
if v.rule_id in config.severity:
|
|
139
|
+
v.severity = config.severity[v.rule_id]
|
|
140
|
+
|
|
127
141
|
violations.sort(key=lambda v: v.line)
|
|
128
|
-
|
|
142
|
+
|
|
143
|
+
inline_suppressions = parse_inline_suppressions(source)
|
|
144
|
+
reported: List[Violation] = []
|
|
145
|
+
suppressed: List[Tuple[Violation, str]] = []
|
|
146
|
+
for v in violations:
|
|
147
|
+
if v.rule_id in inline_suppressions.get(v.line, frozenset()):
|
|
148
|
+
suppressed.append((v, "inline"))
|
|
149
|
+
elif config and v.rule_id in config.ignore:
|
|
150
|
+
suppressed.append((v, "config"))
|
|
151
|
+
else:
|
|
152
|
+
reported.append(v)
|
|
153
|
+
|
|
154
|
+
return reported, suppressed
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def analyze_source(
|
|
158
|
+
source: str,
|
|
159
|
+
filepath: str = "<string>",
|
|
160
|
+
*,
|
|
161
|
+
downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
|
|
162
|
+
config: Optional[PyvibeConfig] = None,
|
|
163
|
+
) -> List[Violation]:
|
|
164
|
+
"""Parse source and run all rules. Returns the reported (non-suppressed)
|
|
165
|
+
violations — see analyze_source_full() for the suppressed ones too.
|
|
166
|
+
"""
|
|
167
|
+
reported, _ = analyze_source_full(
|
|
168
|
+
source, filepath, downgrade_in_tests=downgrade_in_tests, config=config
|
|
169
|
+
)
|
|
170
|
+
return reported
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def analyze_file_full(
|
|
174
|
+
path: Path,
|
|
175
|
+
*,
|
|
176
|
+
downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
|
|
177
|
+
config: Optional[PyvibeConfig] = None,
|
|
178
|
+
) -> Tuple[List[Violation], List[Tuple[Violation, str]]]:
|
|
179
|
+
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
180
|
+
return analyze_source_full(
|
|
181
|
+
source, filepath=str(path), downgrade_in_tests=downgrade_in_tests, config=config
|
|
182
|
+
)
|
|
129
183
|
|
|
130
184
|
|
|
131
185
|
def analyze_file(
|
|
@@ -133,12 +187,15 @@ def analyze_file(
|
|
|
133
187
|
*,
|
|
134
188
|
downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
|
|
135
189
|
line_filter: Optional[FrozenSet[int]] = None,
|
|
190
|
+
config: Optional[PyvibeConfig] = None,
|
|
136
191
|
) -> List[Violation]:
|
|
137
192
|
"""line_filter: if given, only violations whose `line` is in this set
|
|
138
193
|
are returned (used by `pyvibe review` to report only diff-touched lines).
|
|
139
194
|
"""
|
|
140
195
|
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
141
|
-
violations = analyze_source(
|
|
196
|
+
violations = analyze_source(
|
|
197
|
+
source, filepath=str(path), downgrade_in_tests=downgrade_in_tests, config=config
|
|
198
|
+
)
|
|
142
199
|
if line_filter is not None:
|
|
143
200
|
violations = [v for v in violations if v.line in line_filter]
|
|
144
201
|
return violations
|
|
@@ -156,23 +213,55 @@ def analyze_directory(
|
|
|
156
213
|
*,
|
|
157
214
|
skip_test_files: bool = False,
|
|
158
215
|
downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
|
|
216
|
+
config: Optional[PyvibeConfig] = None,
|
|
159
217
|
) -> dict:
|
|
160
218
|
"""Walk directory and analyze all .py files. Returns {path: [violations]}.
|
|
161
219
|
|
|
162
220
|
Directories whose *name* appears in `exclude` are skipped entirely.
|
|
163
221
|
skip_test_files: if True, files matching test_*.py / *_test.py / tests/* are
|
|
164
222
|
omitted from results entirely rather than downgraded.
|
|
223
|
+
config: optional PyvibeConfig — files matching its exclude glob patterns
|
|
224
|
+
are also skipped entirely.
|
|
165
225
|
"""
|
|
166
226
|
results = {}
|
|
167
227
|
for py_file in _walk(root, exclude):
|
|
168
228
|
if skip_test_files and _is_test_file(str(py_file)):
|
|
169
229
|
continue
|
|
170
|
-
|
|
230
|
+
if config and is_excluded(py_file, config):
|
|
231
|
+
continue
|
|
232
|
+
violations = analyze_file(py_file, downgrade_in_tests=downgrade_in_tests, config=config)
|
|
171
233
|
if violations:
|
|
172
234
|
results[py_file] = violations
|
|
173
235
|
return results
|
|
174
236
|
|
|
175
237
|
|
|
238
|
+
def analyze_directory_full(
|
|
239
|
+
root: Path,
|
|
240
|
+
exclude: frozenset = DEFAULT_EXCLUDES,
|
|
241
|
+
*,
|
|
242
|
+
skip_test_files: bool = False,
|
|
243
|
+
downgrade_in_tests: FrozenSet[str] = TEST_FILE_DOWNGRADE,
|
|
244
|
+
config: Optional[PyvibeConfig] = None,
|
|
245
|
+
) -> Tuple[dict, List[Tuple[Path, Violation, str]]]:
|
|
246
|
+
"""Like analyze_directory(), but also returns the suppressed findings
|
|
247
|
+
(across every scanned file) as a flat list of (path, Violation, reason).
|
|
248
|
+
"""
|
|
249
|
+
results = {}
|
|
250
|
+
all_suppressed: List[Tuple[Path, Violation, str]] = []
|
|
251
|
+
for py_file in _walk(root, exclude):
|
|
252
|
+
if skip_test_files and _is_test_file(str(py_file)):
|
|
253
|
+
continue
|
|
254
|
+
if config and is_excluded(py_file, config):
|
|
255
|
+
continue
|
|
256
|
+
reported, suppressed = analyze_file_full(
|
|
257
|
+
py_file, downgrade_in_tests=downgrade_in_tests, config=config
|
|
258
|
+
)
|
|
259
|
+
if reported:
|
|
260
|
+
results[py_file] = reported
|
|
261
|
+
all_suppressed.extend((py_file, v, reason) for v, reason in suppressed)
|
|
262
|
+
return results, all_suppressed
|
|
263
|
+
|
|
264
|
+
|
|
176
265
|
def _walk(root: Path, exclude: frozenset):
|
|
177
266
|
"""Yield .py files under root, skipping any directory in exclude."""
|
|
178
267
|
for entry in sorted(root.iterdir()):
|