python-vibe-guard 0.10.0__tar.gz → 0.12.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.10.0 → python_vibe_guard-0.12.0}/PKG-INFO +94 -3
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/README.md +92 -2
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyproject.toml +4 -1
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/PKG-INFO +94 -3
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/SOURCES.txt +8 -1
- python_vibe_guard-0.12.0/python_vibe_guard.egg-info/requires.txt +3 -0
- python_vibe_guard-0.12.0/pyvibe/__init__.py +1 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/analyzer.py +97 -8
- python_vibe_guard-0.12.0/pyvibe/baseline.py +73 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/cli.py +253 -20
- python_vibe_guard-0.12.0/pyvibe/config.py +99 -0
- python_vibe_guard-0.12.0/pyvibe/suppressions.py +53 -0
- python_vibe_guard-0.12.0/tests/test_baseline.py +234 -0
- python_vibe_guard-0.12.0/tests/test_config.py +268 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_diff.py +5 -0
- python_vibe_guard-0.12.0/tests/test_suppressions.py +158 -0
- python_vibe_guard-0.10.0/pyvibe/__init__.py +0 -1
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/entry_points.txt +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/top_level.txt +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/__main__.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/autofix.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/diff.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/explain.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rule_docs.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/__init__.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/async_requests.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/async_sleep.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/asyncio_run.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/base.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/celery_time_limit.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/contextvar_cleanup.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/create_task_orphan.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/ensure_future_orphan.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/httpx_client_sync.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/httpx_sync.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/loop_run_until_complete.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/open_async.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/os_blocking.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/queue_put_nowait.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/retry_no_backoff.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/silent_except.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/sqlite_async.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/subprocess_async.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/threading_lock.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/rules/while_true_no_await.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/pyvibe/sarif.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/setup.cfg +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_exclude.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_explain.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/tests/test_rules.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/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.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
|
|
@@ -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
|
|
|
@@ -173,6 +174,13 @@ python -m pyvibe src/ --exclude tests
|
|
|
173
174
|
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
174
175
|
python -m pyvibe explain PYVIBE-002
|
|
175
176
|
|
|
177
|
+
# Baseline mode: suppress pre-existing findings, only fail on new ones
|
|
178
|
+
python -m pyvibe baseline create src/ # snapshot current findings
|
|
179
|
+
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
180
|
+
|
|
181
|
+
# Show suppressed findings (inline comments + pyvibe.toml) alongside the report
|
|
182
|
+
python -m pyvibe src/ --verbose
|
|
183
|
+
|
|
176
184
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
177
185
|
```
|
|
178
186
|
|
|
@@ -212,7 +220,7 @@ python -m pyvibe explain PYVIBE-002
|
|
|
212
220
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
213
221
|
|
|
214
222
|
─────────────────────────────────────────────
|
|
215
|
-
4
|
|
223
|
+
4 reported · 0 suppressed
|
|
216
224
|
```
|
|
217
225
|
|
|
218
226
|
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
@@ -255,6 +263,89 @@ python -m pyvibe explain PYVIBE-002
|
|
|
255
263
|
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
256
264
|
and exits non-zero — it never invents data.
|
|
257
265
|
|
|
266
|
+
### Baseline mode
|
|
267
|
+
|
|
268
|
+
Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
|
|
269
|
+
findings you can't fix before CI needs to go green. Baseline mode snapshots the current
|
|
270
|
+
findings once, then only fails on genuinely **new** ones:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Snapshot every current finding into .pyvibe-baseline.json
|
|
274
|
+
python -m pyvibe baseline create src/
|
|
275
|
+
|
|
276
|
+
# Refuses to run if a baseline already exists — use `update` to overwrite it
|
|
277
|
+
python -m pyvibe baseline update src/
|
|
278
|
+
|
|
279
|
+
# Full scan, but only reports findings NOT already in the baseline
|
|
280
|
+
python -m pyvibe src/ --baseline
|
|
281
|
+
python -m pyvibe src/ --baseline --json
|
|
282
|
+
python -m pyvibe src/ --baseline --sarif
|
|
283
|
+
|
|
284
|
+
# `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
|
|
285
|
+
python -m pyvibe scan src/ --baseline
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A finding is considered "already known" on an exact match of `(file path, rule ID, line
|
|
289
|
+
number)`. Anything that doesn't match — a new violation, or an existing one that moved to
|
|
290
|
+
a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
|
|
291
|
+
produces exit code `0`.
|
|
292
|
+
|
|
293
|
+
`.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
|
|
294
|
+
`.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
|
|
295
|
+
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
296
|
+
regenerate its own baseline instead.
|
|
297
|
+
|
|
298
|
+
### Suppressing findings
|
|
299
|
+
|
|
300
|
+
For one-off exceptions, use an inline comment right in the source:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
# pyvibe: ignore PYVIBE-008
|
|
304
|
+
conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
|
|
305
|
+
|
|
306
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
|
|
307
|
+
|
|
308
|
+
time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
|
|
309
|
+
|
|
310
|
+
# pyvibe: ignore-next-line PYVIBE-008
|
|
311
|
+
conn = sqlite3.connect(...) # suppressed — always targets the next line
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
|
|
315
|
+
(treated as a regular comment).
|
|
316
|
+
|
|
317
|
+
For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
|
|
318
|
+
from the scan target to find it, same convention as `pyproject.toml`):
|
|
319
|
+
|
|
320
|
+
```toml
|
|
321
|
+
[tool.pyvibe]
|
|
322
|
+
ignore = ["PYVIBE-019"]
|
|
323
|
+
exclude = ["tests/**", "examples/**", "docs/**"]
|
|
324
|
+
|
|
325
|
+
[tool.pyvibe.severity]
|
|
326
|
+
PYVIBE-008 = "warning"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `ignore`: rule IDs suppressed everywhere, project-wide.
|
|
330
|
+
- `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
|
|
331
|
+
to skip entirely.
|
|
332
|
+
- `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
|
|
333
|
+
applied after the built-in test-file downgrade.
|
|
334
|
+
|
|
335
|
+
Both mechanisms feed into the same summary line:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
4 reported · 2 suppressed
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Add `--verbose` to see exactly what was suppressed and why:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
Suppressed:
|
|
345
|
+
PYVIBE-008 app/db.py:42 (inline)
|
|
346
|
+
PYVIBE-019 legacy.py:81 (config)
|
|
347
|
+
```
|
|
348
|
+
|
|
258
349
|
---
|
|
259
350
|
|
|
260
351
|
## CI/CD integration
|
|
@@ -352,7 +443,7 @@ python -m pytest tests/ -v
|
|
|
352
443
|
python tests/test_rules.py
|
|
353
444
|
```
|
|
354
445
|
|
|
355
|
-
|
|
446
|
+
309 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, and suppressions (inline comments + pyvibe.toml) coverage.
|
|
356
447
|
|
|
357
448
|
---
|
|
358
449
|
|
|
@@ -158,6 +158,13 @@ python -m pyvibe src/ --exclude tests
|
|
|
158
158
|
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
159
159
|
python -m pyvibe explain PYVIBE-002
|
|
160
160
|
|
|
161
|
+
# Baseline mode: suppress pre-existing findings, only fail on new ones
|
|
162
|
+
python -m pyvibe baseline create src/ # snapshot current findings
|
|
163
|
+
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
164
|
+
|
|
165
|
+
# Show suppressed findings (inline comments + pyvibe.toml) alongside the report
|
|
166
|
+
python -m pyvibe src/ --verbose
|
|
167
|
+
|
|
161
168
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
162
169
|
```
|
|
163
170
|
|
|
@@ -197,7 +204,7 @@ python -m pyvibe explain PYVIBE-002
|
|
|
197
204
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
198
205
|
|
|
199
206
|
─────────────────────────────────────────────
|
|
200
|
-
4
|
|
207
|
+
4 reported · 0 suppressed
|
|
201
208
|
```
|
|
202
209
|
|
|
203
210
|
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
@@ -240,6 +247,89 @@ python -m pyvibe explain PYVIBE-002
|
|
|
240
247
|
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
241
248
|
and exits non-zero — it never invents data.
|
|
242
249
|
|
|
250
|
+
### Baseline mode
|
|
251
|
+
|
|
252
|
+
Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
|
|
253
|
+
findings you can't fix before CI needs to go green. Baseline mode snapshots the current
|
|
254
|
+
findings once, then only fails on genuinely **new** ones:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Snapshot every current finding into .pyvibe-baseline.json
|
|
258
|
+
python -m pyvibe baseline create src/
|
|
259
|
+
|
|
260
|
+
# Refuses to run if a baseline already exists — use `update` to overwrite it
|
|
261
|
+
python -m pyvibe baseline update src/
|
|
262
|
+
|
|
263
|
+
# Full scan, but only reports findings NOT already in the baseline
|
|
264
|
+
python -m pyvibe src/ --baseline
|
|
265
|
+
python -m pyvibe src/ --baseline --json
|
|
266
|
+
python -m pyvibe src/ --baseline --sarif
|
|
267
|
+
|
|
268
|
+
# `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
|
|
269
|
+
python -m pyvibe scan src/ --baseline
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
A finding is considered "already known" on an exact match of `(file path, rule ID, line
|
|
273
|
+
number)`. Anything that doesn't match — a new violation, or an existing one that moved to
|
|
274
|
+
a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
|
|
275
|
+
produces exit code `0`.
|
|
276
|
+
|
|
277
|
+
`.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
|
|
278
|
+
`.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
|
|
279
|
+
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
280
|
+
regenerate its own baseline instead.
|
|
281
|
+
|
|
282
|
+
### Suppressing findings
|
|
283
|
+
|
|
284
|
+
For one-off exceptions, use an inline comment right in the source:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
# pyvibe: ignore PYVIBE-008
|
|
288
|
+
conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
|
|
289
|
+
|
|
290
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
|
|
291
|
+
|
|
292
|
+
time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
|
|
293
|
+
|
|
294
|
+
# pyvibe: ignore-next-line PYVIBE-008
|
|
295
|
+
conn = sqlite3.connect(...) # suppressed — always targets the next line
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
`pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
|
|
299
|
+
(treated as a regular comment).
|
|
300
|
+
|
|
301
|
+
For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
|
|
302
|
+
from the scan target to find it, same convention as `pyproject.toml`):
|
|
303
|
+
|
|
304
|
+
```toml
|
|
305
|
+
[tool.pyvibe]
|
|
306
|
+
ignore = ["PYVIBE-019"]
|
|
307
|
+
exclude = ["tests/**", "examples/**", "docs/**"]
|
|
308
|
+
|
|
309
|
+
[tool.pyvibe.severity]
|
|
310
|
+
PYVIBE-008 = "warning"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- `ignore`: rule IDs suppressed everywhere, project-wide.
|
|
314
|
+
- `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
|
|
315
|
+
to skip entirely.
|
|
316
|
+
- `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
|
|
317
|
+
applied after the built-in test-file downgrade.
|
|
318
|
+
|
|
319
|
+
Both mechanisms feed into the same summary line:
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
4 reported · 2 suppressed
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Add `--verbose` to see exactly what was suppressed and why:
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
Suppressed:
|
|
329
|
+
PYVIBE-008 app/db.py:42 (inline)
|
|
330
|
+
PYVIBE-019 legacy.py:81 (config)
|
|
331
|
+
```
|
|
332
|
+
|
|
243
333
|
---
|
|
244
334
|
|
|
245
335
|
## CI/CD integration
|
|
@@ -337,7 +427,7 @@ python -m pytest tests/ -v
|
|
|
337
427
|
python tests/test_rules.py
|
|
338
428
|
```
|
|
339
429
|
|
|
340
|
-
|
|
430
|
+
309 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, and suppressions (inline comments + pyvibe.toml) coverage.
|
|
341
431
|
|
|
342
432
|
---
|
|
343
433
|
|
|
@@ -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.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"
|
|
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.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
|
|
@@ -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
|
|
|
@@ -173,6 +174,13 @@ python -m pyvibe src/ --exclude tests
|
|
|
173
174
|
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
174
175
|
python -m pyvibe explain PYVIBE-002
|
|
175
176
|
|
|
177
|
+
# Baseline mode: suppress pre-existing findings, only fail on new ones
|
|
178
|
+
python -m pyvibe baseline create src/ # snapshot current findings
|
|
179
|
+
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
180
|
+
|
|
181
|
+
# Show suppressed findings (inline comments + pyvibe.toml) alongside the report
|
|
182
|
+
python -m pyvibe src/ --verbose
|
|
183
|
+
|
|
176
184
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
177
185
|
```
|
|
178
186
|
|
|
@@ -212,7 +220,7 @@ python -m pyvibe explain PYVIBE-002
|
|
|
212
220
|
Fix : Use `asyncio.Lock()` with `async with lock:` instead
|
|
213
221
|
|
|
214
222
|
─────────────────────────────────────────────
|
|
215
|
-
4
|
|
223
|
+
4 reported · 0 suppressed
|
|
216
224
|
```
|
|
217
225
|
|
|
218
226
|
`Suggested fix:` blocks are generated from the real code on the flagged line — they are
|
|
@@ -255,6 +263,89 @@ python -m pyvibe explain PYVIBE-002
|
|
|
255
263
|
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
256
264
|
and exits non-zero — it never invents data.
|
|
257
265
|
|
|
266
|
+
### Baseline mode
|
|
267
|
+
|
|
268
|
+
Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
|
|
269
|
+
findings you can't fix before CI needs to go green. Baseline mode snapshots the current
|
|
270
|
+
findings once, then only fails on genuinely **new** ones:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Snapshot every current finding into .pyvibe-baseline.json
|
|
274
|
+
python -m pyvibe baseline create src/
|
|
275
|
+
|
|
276
|
+
# Refuses to run if a baseline already exists — use `update` to overwrite it
|
|
277
|
+
python -m pyvibe baseline update src/
|
|
278
|
+
|
|
279
|
+
# Full scan, but only reports findings NOT already in the baseline
|
|
280
|
+
python -m pyvibe src/ --baseline
|
|
281
|
+
python -m pyvibe src/ --baseline --json
|
|
282
|
+
python -m pyvibe src/ --baseline --sarif
|
|
283
|
+
|
|
284
|
+
# `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
|
|
285
|
+
python -m pyvibe scan src/ --baseline
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A finding is considered "already known" on an exact match of `(file path, rule ID, line
|
|
289
|
+
number)`. Anything that doesn't match — a new violation, or an existing one that moved to
|
|
290
|
+
a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
|
|
291
|
+
produces exit code `0`.
|
|
292
|
+
|
|
293
|
+
`.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
|
|
294
|
+
`.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
|
|
295
|
+
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
296
|
+
regenerate its own baseline instead.
|
|
297
|
+
|
|
298
|
+
### Suppressing findings
|
|
299
|
+
|
|
300
|
+
For one-off exceptions, use an inline comment right in the source:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
# pyvibe: ignore PYVIBE-008
|
|
304
|
+
conn = sqlite3.connect(...) # suppressed — standalone comment targets the next line
|
|
305
|
+
|
|
306
|
+
conn = sqlite3.connect(...) # pyvibe: ignore PYVIBE-008 # suppressed — trailing comment targets its own line
|
|
307
|
+
|
|
308
|
+
time.sleep(n) # pyvibe: ignore PYVIBE-001, PYVIBE-003 # multiple rules, comma-separated
|
|
309
|
+
|
|
310
|
+
# pyvibe: ignore-next-line PYVIBE-008
|
|
311
|
+
conn = sqlite3.connect(...) # suppressed — always targets the next line
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`pyvibe:` is case-insensitive. A directive with no recognizable `PYVIBE-XXX` id is ignored
|
|
315
|
+
(treated as a regular comment).
|
|
316
|
+
|
|
317
|
+
For project-wide rules, add a `pyvibe.toml` next to your code (python-vibe-guard walks up
|
|
318
|
+
from the scan target to find it, same convention as `pyproject.toml`):
|
|
319
|
+
|
|
320
|
+
```toml
|
|
321
|
+
[tool.pyvibe]
|
|
322
|
+
ignore = ["PYVIBE-019"]
|
|
323
|
+
exclude = ["tests/**", "examples/**", "docs/**"]
|
|
324
|
+
|
|
325
|
+
[tool.pyvibe.severity]
|
|
326
|
+
PYVIBE-008 = "warning"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
- `ignore`: rule IDs suppressed everywhere, project-wide.
|
|
330
|
+
- `exclude`: glob patterns (relative to the `pyvibe.toml` directory) for files/directories
|
|
331
|
+
to skip entirely.
|
|
332
|
+
- `[tool.pyvibe.severity]`: per-rule severity override (`"critical"` or `"warning"`),
|
|
333
|
+
applied after the built-in test-file downgrade.
|
|
334
|
+
|
|
335
|
+
Both mechanisms feed into the same summary line:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
4 reported · 2 suppressed
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Add `--verbose` to see exactly what was suppressed and why:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
Suppressed:
|
|
345
|
+
PYVIBE-008 app/db.py:42 (inline)
|
|
346
|
+
PYVIBE-019 legacy.py:81 (config)
|
|
347
|
+
```
|
|
348
|
+
|
|
258
349
|
---
|
|
259
350
|
|
|
260
351
|
## CI/CD integration
|
|
@@ -352,7 +443,7 @@ python -m pytest tests/ -v
|
|
|
352
443
|
python tests/test_rules.py
|
|
353
444
|
```
|
|
354
445
|
|
|
355
|
-
|
|
446
|
+
309 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, baseline mode, and suppressions (inline comments + pyvibe.toml) coverage.
|
|
356
447
|
|
|
357
448
|
---
|
|
358
449
|
|
{python_vibe_guard-0.10.0 → python_vibe_guard-0.12.0}/python_vibe_guard.egg-info/SOURCES.txt
RENAMED
|
@@ -4,16 +4,20 @@ 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
|
|
11
12
|
pyvibe/autofix.py
|
|
13
|
+
pyvibe/baseline.py
|
|
12
14
|
pyvibe/cli.py
|
|
15
|
+
pyvibe/config.py
|
|
13
16
|
pyvibe/diff.py
|
|
14
17
|
pyvibe/explain.py
|
|
15
18
|
pyvibe/rule_docs.py
|
|
16
19
|
pyvibe/sarif.py
|
|
20
|
+
pyvibe/suppressions.py
|
|
17
21
|
pyvibe/rules/__init__.py
|
|
18
22
|
pyvibe/rules/async_requests.py
|
|
19
23
|
pyvibe/rules/async_sleep.py
|
|
@@ -36,8 +40,11 @@ pyvibe/rules/sqlite_async.py
|
|
|
36
40
|
pyvibe/rules/subprocess_async.py
|
|
37
41
|
pyvibe/rules/threading_lock.py
|
|
38
42
|
pyvibe/rules/while_true_no_await.py
|
|
43
|
+
tests/test_baseline.py
|
|
44
|
+
tests/test_config.py
|
|
39
45
|
tests/test_diff.py
|
|
40
46
|
tests/test_exclude.py
|
|
41
47
|
tests/test_explain.py
|
|
42
48
|
tests/test_rules.py
|
|
43
|
-
tests/test_sarif.py
|
|
49
|
+
tests/test_sarif.py
|
|
50
|
+
tests/test_suppressions.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.12.0"
|
|
@@ -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()):
|