python-vibe-guard 0.10.0__tar.gz → 0.11.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.11.0}/PKG-INFO +38 -2
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/README.md +37 -1
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyproject.toml +1 -1
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/PKG-INFO +38 -2
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/SOURCES.txt +2 -0
- python_vibe_guard-0.11.0/pyvibe/__init__.py +1 -0
- python_vibe_guard-0.11.0/pyvibe/baseline.py +73 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/cli.py +190 -16
- python_vibe_guard-0.11.0/tests/test_baseline.py +234 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/tests/test_diff.py +5 -0
- python_vibe_guard-0.10.0/pyvibe/__init__.py +0 -1
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/entry_points.txt +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/top_level.txt +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/__main__.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/analyzer.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/autofix.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/diff.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/explain.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rule_docs.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/__init__.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/async_requests.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/async_sleep.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/asyncio_run.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/base.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/celery_time_limit.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/contextvar_cleanup.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/create_task_orphan.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/ensure_future_orphan.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/httpx_client_sync.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/httpx_sync.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/loop_run_until_complete.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/open_async.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/os_blocking.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/queue_put_nowait.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/retry_no_backoff.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/silent_except.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/sqlite_async.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/subprocess_async.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/threading_lock.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/while_true_no_await.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/sarif.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/setup.cfg +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/tests/test_exclude.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/tests/test_explain.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/tests/test_rules.py +0 -0
- {python_vibe_guard-0.10.0 → python_vibe_guard-0.11.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.11.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
|
|
@@ -173,6 +173,10 @@ python -m pyvibe src/ --exclude tests
|
|
|
173
173
|
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
174
174
|
python -m pyvibe explain PYVIBE-002
|
|
175
175
|
|
|
176
|
+
# Baseline mode: suppress pre-existing findings, only fail on new ones
|
|
177
|
+
python -m pyvibe baseline create src/ # snapshot current findings
|
|
178
|
+
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
179
|
+
|
|
176
180
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
177
181
|
```
|
|
178
182
|
|
|
@@ -255,6 +259,38 @@ python -m pyvibe explain PYVIBE-002
|
|
|
255
259
|
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
256
260
|
and exits non-zero — it never invents data.
|
|
257
261
|
|
|
262
|
+
### Baseline mode
|
|
263
|
+
|
|
264
|
+
Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
|
|
265
|
+
findings you can't fix before CI needs to go green. Baseline mode snapshots the current
|
|
266
|
+
findings once, then only fails on genuinely **new** ones:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Snapshot every current finding into .pyvibe-baseline.json
|
|
270
|
+
python -m pyvibe baseline create src/
|
|
271
|
+
|
|
272
|
+
# Refuses to run if a baseline already exists — use `update` to overwrite it
|
|
273
|
+
python -m pyvibe baseline update src/
|
|
274
|
+
|
|
275
|
+
# Full scan, but only reports findings NOT already in the baseline
|
|
276
|
+
python -m pyvibe src/ --baseline
|
|
277
|
+
python -m pyvibe src/ --baseline --json
|
|
278
|
+
python -m pyvibe src/ --baseline --sarif
|
|
279
|
+
|
|
280
|
+
# `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
|
|
281
|
+
python -m pyvibe scan src/ --baseline
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A finding is considered "already known" on an exact match of `(file path, rule ID, line
|
|
285
|
+
number)`. Anything that doesn't match — a new violation, or an existing one that moved to
|
|
286
|
+
a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
|
|
287
|
+
produces exit code `0`.
|
|
288
|
+
|
|
289
|
+
`.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
|
|
290
|
+
`.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
|
|
291
|
+
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
292
|
+
regenerate its own baseline instead.
|
|
293
|
+
|
|
258
294
|
---
|
|
259
295
|
|
|
260
296
|
## CI/CD integration
|
|
@@ -352,7 +388,7 @@ python -m pytest tests/ -v
|
|
|
352
388
|
python tests/test_rules.py
|
|
353
389
|
```
|
|
354
390
|
|
|
355
|
-
|
|
391
|
+
268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode coverage.
|
|
356
392
|
|
|
357
393
|
---
|
|
358
394
|
|
|
@@ -158,6 +158,10 @@ 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
|
+
|
|
161
165
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
162
166
|
```
|
|
163
167
|
|
|
@@ -240,6 +244,38 @@ python -m pyvibe explain PYVIBE-002
|
|
|
240
244
|
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
241
245
|
and exits non-zero — it never invents data.
|
|
242
246
|
|
|
247
|
+
### Baseline mode
|
|
248
|
+
|
|
249
|
+
Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
|
|
250
|
+
findings you can't fix before CI needs to go green. Baseline mode snapshots the current
|
|
251
|
+
findings once, then only fails on genuinely **new** ones:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Snapshot every current finding into .pyvibe-baseline.json
|
|
255
|
+
python -m pyvibe baseline create src/
|
|
256
|
+
|
|
257
|
+
# Refuses to run if a baseline already exists — use `update` to overwrite it
|
|
258
|
+
python -m pyvibe baseline update src/
|
|
259
|
+
|
|
260
|
+
# Full scan, but only reports findings NOT already in the baseline
|
|
261
|
+
python -m pyvibe src/ --baseline
|
|
262
|
+
python -m pyvibe src/ --baseline --json
|
|
263
|
+
python -m pyvibe src/ --baseline --sarif
|
|
264
|
+
|
|
265
|
+
# `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
|
|
266
|
+
python -m pyvibe scan src/ --baseline
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
A finding is considered "already known" on an exact match of `(file path, rule ID, line
|
|
270
|
+
number)`. Anything that doesn't match — a new violation, or an existing one that moved to
|
|
271
|
+
a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
|
|
272
|
+
produces exit code `0`.
|
|
273
|
+
|
|
274
|
+
`.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
|
|
275
|
+
`.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
|
|
276
|
+
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
277
|
+
regenerate its own baseline instead.
|
|
278
|
+
|
|
243
279
|
---
|
|
244
280
|
|
|
245
281
|
## CI/CD integration
|
|
@@ -337,7 +373,7 @@ python -m pytest tests/ -v
|
|
|
337
373
|
python tests/test_rules.py
|
|
338
374
|
```
|
|
339
375
|
|
|
340
|
-
|
|
376
|
+
268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode coverage.
|
|
341
377
|
|
|
342
378
|
---
|
|
343
379
|
|
|
@@ -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.11.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.11.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
|
|
@@ -173,6 +173,10 @@ python -m pyvibe src/ --exclude tests
|
|
|
173
173
|
# Show the research evidence behind a rule (accuracy, false positives, sources)
|
|
174
174
|
python -m pyvibe explain PYVIBE-002
|
|
175
175
|
|
|
176
|
+
# Baseline mode: suppress pre-existing findings, only fail on new ones
|
|
177
|
+
python -m pyvibe baseline create src/ # snapshot current findings
|
|
178
|
+
python -m pyvibe src/ --baseline # only reports findings NOT in the baseline
|
|
179
|
+
|
|
176
180
|
# Exit code: 0 = clean, 1 = violations found, 2 = path error
|
|
177
181
|
```
|
|
178
182
|
|
|
@@ -255,6 +259,38 @@ python -m pyvibe explain PYVIBE-002
|
|
|
255
259
|
If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
|
|
256
260
|
and exits non-zero — it never invents data.
|
|
257
261
|
|
|
262
|
+
### Baseline mode
|
|
263
|
+
|
|
264
|
+
Adopting python-vibe-guard on an existing codebase usually means hundreds of pre-existing
|
|
265
|
+
findings you can't fix before CI needs to go green. Baseline mode snapshots the current
|
|
266
|
+
findings once, then only fails on genuinely **new** ones:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# Snapshot every current finding into .pyvibe-baseline.json
|
|
270
|
+
python -m pyvibe baseline create src/
|
|
271
|
+
|
|
272
|
+
# Refuses to run if a baseline already exists — use `update` to overwrite it
|
|
273
|
+
python -m pyvibe baseline update src/
|
|
274
|
+
|
|
275
|
+
# Full scan, but only reports findings NOT already in the baseline
|
|
276
|
+
python -m pyvibe src/ --baseline
|
|
277
|
+
python -m pyvibe src/ --baseline --json
|
|
278
|
+
python -m pyvibe src/ --baseline --sarif
|
|
279
|
+
|
|
280
|
+
# `pyvibe scan` is an equivalent, explicit subcommand form of the same flags
|
|
281
|
+
python -m pyvibe scan src/ --baseline
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A finding is considered "already known" on an exact match of `(file path, rule ID, line
|
|
285
|
+
number)`. Anything that doesn't match — a new violation, or an existing one that moved to
|
|
286
|
+
a different line — is reported and fails the scan (exit code `1`); an unchanged baseline
|
|
287
|
+
produces exit code `0`.
|
|
288
|
+
|
|
289
|
+
`.pyvibe-baseline.json` is a plain JSON file — whether to commit it or add it to
|
|
290
|
+
`.gitignore` is up to your team. Commit it if you want the accepted-debt snapshot shared
|
|
291
|
+
and reviewed like any other file; gitignore it if each contributor/CI run should
|
|
292
|
+
regenerate its own baseline instead.
|
|
293
|
+
|
|
258
294
|
---
|
|
259
295
|
|
|
260
296
|
## CI/CD integration
|
|
@@ -352,7 +388,7 @@ python -m pytest tests/ -v
|
|
|
352
388
|
python tests/test_rules.py
|
|
353
389
|
```
|
|
354
390
|
|
|
355
|
-
|
|
391
|
+
268 tests: true positives + false-positive guards for every rule, plus SARIF output, `pyvibe explain`, `pyvibe review`, and baseline mode coverage.
|
|
356
392
|
|
|
357
393
|
---
|
|
358
394
|
|
{python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/SOURCES.txt
RENAMED
|
@@ -9,6 +9,7 @@ pyvibe/__init__.py
|
|
|
9
9
|
pyvibe/__main__.py
|
|
10
10
|
pyvibe/analyzer.py
|
|
11
11
|
pyvibe/autofix.py
|
|
12
|
+
pyvibe/baseline.py
|
|
12
13
|
pyvibe/cli.py
|
|
13
14
|
pyvibe/diff.py
|
|
14
15
|
pyvibe/explain.py
|
|
@@ -36,6 +37,7 @@ pyvibe/rules/sqlite_async.py
|
|
|
36
37
|
pyvibe/rules/subprocess_async.py
|
|
37
38
|
pyvibe/rules/threading_lock.py
|
|
38
39
|
pyvibe/rules/while_true_no_await.py
|
|
40
|
+
tests/test_baseline.py
|
|
39
41
|
tests/test_diff.py
|
|
40
42
|
tests/test_exclude.py
|
|
41
43
|
tests/test_explain.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.0"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Baseline support — `pyvibe baseline create/update` and `pyvibe scan --baseline`.
|
|
2
|
+
|
|
3
|
+
A baseline is a snapshot of existing findings ({filepath: [{rule_id, line,
|
|
4
|
+
message}, ...]}) that a team can choose to commit or gitignore (see README).
|
|
5
|
+
Once created, `pyvibe scan --baseline` only reports genuinely NEW
|
|
6
|
+
violations — anything already in the baseline is suppressed — so a
|
|
7
|
+
codebase can adopt python-vibe-guard incrementally instead of having to
|
|
8
|
+
fix every existing hit before CI goes green.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Set, Tuple
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASELINE_PATH = ".pyvibe-baseline.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaselineNotFoundError(Exception):
|
|
18
|
+
"""Raised when `pyvibe scan --baseline` can't find/parse the baseline file."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_baseline(file_results: Dict) -> dict:
|
|
22
|
+
"""file_results: {path: [Violation, ...]} as produced by analyze_file /
|
|
23
|
+
analyze_directory. Returns the JSON-serializable baseline:
|
|
24
|
+
{filepath: [{rule_id, line, message}, ...]}.
|
|
25
|
+
"""
|
|
26
|
+
return {
|
|
27
|
+
str(path): [
|
|
28
|
+
{"rule_id": v.rule_id, "line": v.line, "message": v.message}
|
|
29
|
+
for v in violations
|
|
30
|
+
]
|
|
31
|
+
for path, violations in file_results.items()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def write_baseline(file_results: Dict, output_path=DEFAULT_BASELINE_PATH) -> None:
|
|
36
|
+
baseline = build_baseline(file_results)
|
|
37
|
+
Path(output_path).write_text(json.dumps(baseline, indent=2), encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_baseline(path=DEFAULT_BASELINE_PATH) -> dict:
|
|
41
|
+
baseline_path = Path(path)
|
|
42
|
+
if not baseline_path.exists():
|
|
43
|
+
raise BaselineNotFoundError(
|
|
44
|
+
f"No baseline found at {path}. Run `pyvibe baseline create` first."
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
return json.loads(baseline_path.read_text(encoding="utf-8"))
|
|
48
|
+
except json.JSONDecodeError as e:
|
|
49
|
+
raise BaselineNotFoundError(f"Baseline at {path} is not valid JSON: {e}") from e
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _baseline_keys(baseline: dict) -> Set[Tuple[str, str, int]]:
|
|
53
|
+
"""Flatten the baseline into (filepath, rule_id, line) triples for exact,
|
|
54
|
+
O(1) membership checks.
|
|
55
|
+
"""
|
|
56
|
+
return {
|
|
57
|
+
(filepath, entry["rule_id"], entry["line"])
|
|
58
|
+
for filepath, entries in baseline.items()
|
|
59
|
+
for entry in entries
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def filter_new_violations(file_results: Dict, baseline: dict) -> Dict:
|
|
64
|
+
"""Return only the violations in file_results NOT present in baseline,
|
|
65
|
+
matched exactly on (filepath, rule_id, line).
|
|
66
|
+
"""
|
|
67
|
+
known = _baseline_keys(baseline)
|
|
68
|
+
new_results = {}
|
|
69
|
+
for path, violations in file_results.items():
|
|
70
|
+
new = [v for v in violations if (str(path), v.rule_id, v.line) not in known]
|
|
71
|
+
if new:
|
|
72
|
+
new_results[path] = new
|
|
73
|
+
return new_results
|
|
@@ -11,6 +11,10 @@ Usage:
|
|
|
11
11
|
python -m pyvibe explain PYVIBE-002 # show research evidence for a rule
|
|
12
12
|
python -m pyvibe review # PR review: only diff-touched lines (git diff HEAD~1)
|
|
13
13
|
python -m pyvibe review --base main # diff against another ref
|
|
14
|
+
python -m pyvibe baseline create [path] # snapshot existing findings to .pyvibe-baseline.json
|
|
15
|
+
python -m pyvibe baseline update [path] # overwrite the existing baseline
|
|
16
|
+
python -m pyvibe <path> --baseline # scan, suppressing findings already in the baseline
|
|
17
|
+
python -m pyvibe scan <path> --baseline # equivalent, explicit subcommand form
|
|
14
18
|
"""
|
|
15
19
|
import argparse
|
|
16
20
|
import json
|
|
@@ -26,6 +30,7 @@ from pyvibe.analyzer import (
|
|
|
26
30
|
TEST_FILE_DOWNGRADE,
|
|
27
31
|
_is_test_file,
|
|
28
32
|
)
|
|
33
|
+
from pyvibe.baseline import DEFAULT_BASELINE_PATH
|
|
29
34
|
|
|
30
35
|
|
|
31
36
|
def main():
|
|
@@ -37,6 +42,14 @@ def main():
|
|
|
37
42
|
_main_review(sys.argv[2:])
|
|
38
43
|
return
|
|
39
44
|
|
|
45
|
+
if len(sys.argv) > 1 and sys.argv[1] == "baseline":
|
|
46
|
+
_main_baseline(sys.argv[2:])
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if len(sys.argv) > 1 and sys.argv[1] == "scan":
|
|
50
|
+
_main_scan(sys.argv[2:])
|
|
51
|
+
return
|
|
52
|
+
|
|
40
53
|
parser = argparse.ArgumentParser(
|
|
41
54
|
prog="pyvibe",
|
|
42
55
|
description="Detect runtime anti-patterns in async Python code",
|
|
@@ -82,8 +95,34 @@ def main():
|
|
|
82
95
|
"Default: only PYVIBE-001, PYVIBE-007, and PYVIBE-013 are downgraded."
|
|
83
96
|
),
|
|
84
97
|
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--baseline",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help="Only report findings not already present in the baseline (see `pyvibe baseline create`)",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--baseline-path",
|
|
105
|
+
metavar="PATH",
|
|
106
|
+
default=DEFAULT_BASELINE_PATH,
|
|
107
|
+
help=f"Path to the baseline file (default: {DEFAULT_BASELINE_PATH})",
|
|
108
|
+
)
|
|
85
109
|
args = parser.parse_args()
|
|
86
110
|
|
|
111
|
+
file_results = _resolve_file_results(args)
|
|
112
|
+
file_results = _apply_baseline_filter(args, file_results)
|
|
113
|
+
total_violations = sum(len(v) for v in file_results.values())
|
|
114
|
+
total_files = sum(1 for v in file_results.values() if v)
|
|
115
|
+
|
|
116
|
+
_emit_scan_output(args, file_results, total_violations, total_files)
|
|
117
|
+
|
|
118
|
+
sys.exit(1 if total_violations > 0 else 0)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_file_results(args) -> dict:
|
|
122
|
+
"""Shared by the bare `pyvibe <path>` command and `pyvibe scan`.
|
|
123
|
+
|
|
124
|
+
Expects args.path/.exclude/.no_test_files/.downgrade_in_tests.
|
|
125
|
+
"""
|
|
87
126
|
target = Path(args.path)
|
|
88
127
|
if not target.exists():
|
|
89
128
|
print(f"Error: {target} does not exist", file=sys.stderr)
|
|
@@ -104,22 +143,42 @@ def main():
|
|
|
104
143
|
|
|
105
144
|
if target.is_file():
|
|
106
145
|
if target.suffix != ".py":
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
146
|
+
return {}
|
|
147
|
+
if skip_test_files and _is_test_file(str(target)):
|
|
148
|
+
return {}
|
|
149
|
+
return {target: analyze_file(target, downgrade_in_tests=downgrade_in_tests)}
|
|
150
|
+
|
|
151
|
+
return analyze_directory(
|
|
152
|
+
target,
|
|
153
|
+
exclude=exclude,
|
|
154
|
+
skip_test_files=skip_test_files,
|
|
155
|
+
downgrade_in_tests=downgrade_in_tests,
|
|
156
|
+
)
|
|
119
157
|
|
|
120
|
-
total_violations = sum(len(v) for v in file_results.values())
|
|
121
|
-
total_files = sum(1 for v in file_results.values() if v)
|
|
122
158
|
|
|
159
|
+
def _apply_baseline_filter(args, file_results: dict) -> dict:
|
|
160
|
+
"""Shared by the bare `pyvibe <path>` command and `pyvibe scan`.
|
|
161
|
+
|
|
162
|
+
Expects args.baseline/.baseline_path. No-op when --baseline wasn't passed.
|
|
163
|
+
"""
|
|
164
|
+
if not args.baseline:
|
|
165
|
+
return file_results
|
|
166
|
+
|
|
167
|
+
from pyvibe.baseline import BaselineNotFoundError, filter_new_violations, load_baseline
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
baseline = load_baseline(args.baseline_path)
|
|
171
|
+
except BaselineNotFoundError as e:
|
|
172
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
173
|
+
sys.exit(2)
|
|
174
|
+
return filter_new_violations(file_results, baseline)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _emit_scan_output(args, file_results: dict, total_violations: int, total_files: int):
|
|
178
|
+
"""Shared by the bare `pyvibe <path>` command and `pyvibe scan`.
|
|
179
|
+
|
|
180
|
+
Expects args.sarif/.sarif_output/.json.
|
|
181
|
+
"""
|
|
123
182
|
if args.sarif:
|
|
124
183
|
from pyvibe.sarif import write_sarif
|
|
125
184
|
|
|
@@ -144,8 +203,6 @@ def main():
|
|
|
144
203
|
else:
|
|
145
204
|
_print_human(file_results, total_violations, total_files)
|
|
146
205
|
|
|
147
|
-
sys.exit(1 if total_violations > 0 else 0)
|
|
148
|
-
|
|
149
206
|
|
|
150
207
|
def _print_human(file_results: dict, total_violations: int, total_files: int):
|
|
151
208
|
print()
|
|
@@ -285,6 +342,123 @@ def _print_review(file_results: dict, changed: dict, total_violations: int):
|
|
|
285
342
|
print()
|
|
286
343
|
|
|
287
344
|
|
|
345
|
+
def _main_scan(argv):
|
|
346
|
+
parser = argparse.ArgumentParser(
|
|
347
|
+
prog="pyvibe scan",
|
|
348
|
+
description="Scan for anti-patterns (same as bare `pyvibe <path>`), with optional baseline filtering",
|
|
349
|
+
)
|
|
350
|
+
parser.add_argument("path", help="File or directory to scan")
|
|
351
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
352
|
+
parser.add_argument(
|
|
353
|
+
"--sarif",
|
|
354
|
+
action="store_true",
|
|
355
|
+
help="Also write SARIF 2.1.0 output (for GitHub Code Scanning)",
|
|
356
|
+
)
|
|
357
|
+
parser.add_argument(
|
|
358
|
+
"--sarif-output",
|
|
359
|
+
metavar="PATH",
|
|
360
|
+
default="results.sarif",
|
|
361
|
+
help="Path to write SARIF output to (default: results.sarif)",
|
|
362
|
+
)
|
|
363
|
+
parser.add_argument(
|
|
364
|
+
"--exclude",
|
|
365
|
+
metavar="DIR",
|
|
366
|
+
action="append",
|
|
367
|
+
default=[],
|
|
368
|
+
help=(
|
|
369
|
+
"Directory name to exclude (can be repeated). "
|
|
370
|
+
"Added on top of the built-in defaults: "
|
|
371
|
+
+ ", ".join(sorted(DEFAULT_EXCLUDES))
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
parser.add_argument("--no-test-files", action="store_true", help="Exclude test files from the scan entirely")
|
|
375
|
+
parser.add_argument(
|
|
376
|
+
"--downgrade-in-tests",
|
|
377
|
+
action="store_true",
|
|
378
|
+
help="Downgrade ALL violations in test files from CRITICAL to WARNING",
|
|
379
|
+
)
|
|
380
|
+
parser.add_argument(
|
|
381
|
+
"--baseline",
|
|
382
|
+
action="store_true",
|
|
383
|
+
help="Only report findings not already present in the baseline (see `pyvibe baseline create`)",
|
|
384
|
+
)
|
|
385
|
+
parser.add_argument(
|
|
386
|
+
"--baseline-path",
|
|
387
|
+
metavar="PATH",
|
|
388
|
+
default=DEFAULT_BASELINE_PATH,
|
|
389
|
+
help=f"Path to the baseline file (default: {DEFAULT_BASELINE_PATH})",
|
|
390
|
+
)
|
|
391
|
+
args = parser.parse_args(argv)
|
|
392
|
+
|
|
393
|
+
file_results = _resolve_file_results(args)
|
|
394
|
+
file_results = _apply_baseline_filter(args, file_results)
|
|
395
|
+
|
|
396
|
+
total_violations = sum(len(v) for v in file_results.values())
|
|
397
|
+
total_files = sum(1 for v in file_results.values() if v)
|
|
398
|
+
|
|
399
|
+
_emit_scan_output(args, file_results, total_violations, total_files)
|
|
400
|
+
|
|
401
|
+
sys.exit(1 if total_violations > 0 else 0)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _main_baseline(argv):
|
|
405
|
+
parser = argparse.ArgumentParser(
|
|
406
|
+
prog="pyvibe baseline",
|
|
407
|
+
description="Snapshot existing findings so future scans only report new ones",
|
|
408
|
+
)
|
|
409
|
+
parser.add_argument("action", choices=["create", "update"], help="create: refuses to overwrite an existing baseline; update: always overwrites")
|
|
410
|
+
parser.add_argument("path", nargs="?", default=".", help="File or directory to scan (default: current directory)")
|
|
411
|
+
parser.add_argument(
|
|
412
|
+
"--baseline-path",
|
|
413
|
+
metavar="PATH",
|
|
414
|
+
default=DEFAULT_BASELINE_PATH,
|
|
415
|
+
help=f"Path to write the baseline to (default: {DEFAULT_BASELINE_PATH})",
|
|
416
|
+
)
|
|
417
|
+
parser.add_argument(
|
|
418
|
+
"--exclude",
|
|
419
|
+
metavar="DIR",
|
|
420
|
+
action="append",
|
|
421
|
+
default=[],
|
|
422
|
+
help="Directory name to exclude (can be repeated)",
|
|
423
|
+
)
|
|
424
|
+
parser.add_argument("--no-test-files", action="store_true", help="Exclude test files from the baseline entirely")
|
|
425
|
+
args = parser.parse_args(argv)
|
|
426
|
+
|
|
427
|
+
if args.action == "create" and Path(args.baseline_path).exists():
|
|
428
|
+
print(
|
|
429
|
+
f"Error: {args.baseline_path} already exists. "
|
|
430
|
+
"Use `pyvibe baseline update` to overwrite it.",
|
|
431
|
+
file=sys.stderr,
|
|
432
|
+
)
|
|
433
|
+
sys.exit(2)
|
|
434
|
+
|
|
435
|
+
target = Path(args.path)
|
|
436
|
+
if not target.exists():
|
|
437
|
+
print(f"Error: {target} does not exist", file=sys.stderr)
|
|
438
|
+
sys.exit(2)
|
|
439
|
+
|
|
440
|
+
exclude = DEFAULT_EXCLUDES | frozenset(args.exclude)
|
|
441
|
+
|
|
442
|
+
if target.is_file():
|
|
443
|
+
if target.suffix != ".py":
|
|
444
|
+
file_results = {}
|
|
445
|
+
elif args.no_test_files and _is_test_file(str(target)):
|
|
446
|
+
file_results = {}
|
|
447
|
+
else:
|
|
448
|
+
file_results = {target: analyze_file(target)}
|
|
449
|
+
else:
|
|
450
|
+
file_results = analyze_directory(target, exclude=exclude, skip_test_files=args.no_test_files)
|
|
451
|
+
|
|
452
|
+
from pyvibe.baseline import write_baseline
|
|
453
|
+
|
|
454
|
+
total_findings = sum(len(v) for v in file_results.values())
|
|
455
|
+
write_baseline(file_results, args.baseline_path)
|
|
456
|
+
|
|
457
|
+
verb = "created" if args.action == "create" else "updated"
|
|
458
|
+
print(f"Baseline {verb}: {total_findings} findings saved to {args.baseline_path}")
|
|
459
|
+
sys.exit(0)
|
|
460
|
+
|
|
461
|
+
|
|
288
462
|
def _main_explain(argv):
|
|
289
463
|
parser = argparse.ArgumentParser(
|
|
290
464
|
prog="pyvibe explain",
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Tests for baseline support (pyvibe/baseline.py + `pyvibe baseline` /
|
|
2
|
+
`pyvibe scan --baseline` CLI subcommands)."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from pyvibe.analyzer import analyze_source
|
|
14
|
+
from pyvibe.baseline import (
|
|
15
|
+
BaselineNotFoundError,
|
|
16
|
+
build_baseline,
|
|
17
|
+
filter_new_violations,
|
|
18
|
+
load_baseline,
|
|
19
|
+
write_baseline,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
|
|
24
|
+
VIOLATION_SRC = """\
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
async def handler():
|
|
28
|
+
time.sleep(5)
|
|
29
|
+
|
|
30
|
+
async def other():
|
|
31
|
+
time.sleep(9)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ─── build_baseline() / write_baseline() / load_baseline() unit tests ──────
|
|
36
|
+
|
|
37
|
+
def test_build_baseline_shapes_output():
|
|
38
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
39
|
+
baseline = build_baseline({Path("app.py"): violations})
|
|
40
|
+
|
|
41
|
+
assert baseline == {
|
|
42
|
+
"app.py": [
|
|
43
|
+
{"rule_id": "PYVIBE-001", "line": 4, "message": violations[0].message},
|
|
44
|
+
{"rule_id": "PYVIBE-001", "line": 7, "message": violations[1].message},
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_write_and_load_baseline_roundtrip(tmp_path):
|
|
50
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
51
|
+
out = tmp_path / "baseline.json"
|
|
52
|
+
|
|
53
|
+
write_baseline({Path("app.py"): violations}, out)
|
|
54
|
+
loaded = load_baseline(out)
|
|
55
|
+
|
|
56
|
+
assert loaded == build_baseline({Path("app.py"): violations})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_load_baseline_missing_file_raises(tmp_path):
|
|
60
|
+
with pytest.raises(BaselineNotFoundError, match="No baseline found"):
|
|
61
|
+
load_baseline(tmp_path / "nope.json")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_load_baseline_invalid_json_raises(tmp_path):
|
|
65
|
+
bad = tmp_path / "bad.json"
|
|
66
|
+
bad.write_text("not json{{{")
|
|
67
|
+
with pytest.raises(BaselineNotFoundError, match="not valid JSON"):
|
|
68
|
+
load_baseline(bad)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ─── filter_new_violations() unit tests ────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def test_filter_new_violations_excludes_exact_match():
|
|
74
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
75
|
+
baseline = build_baseline({Path("app.py"): violations})
|
|
76
|
+
|
|
77
|
+
result = filter_new_violations({Path("app.py"): violations}, baseline)
|
|
78
|
+
|
|
79
|
+
assert result == {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_filter_new_violations_reports_new_line():
|
|
83
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
84
|
+
baseline = build_baseline({Path("app.py"): [violations[0]]}) # only line 4 known
|
|
85
|
+
|
|
86
|
+
result = filter_new_violations({Path("app.py"): violations}, baseline)
|
|
87
|
+
|
|
88
|
+
assert list(result.keys()) == [Path("app.py")]
|
|
89
|
+
assert [v.line for v in result[Path("app.py")]] == [7]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_filter_new_violations_reports_new_rule_on_same_line():
|
|
93
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
94
|
+
baseline = {"app.py": [{"rule_id": "PYVIBE-999", "line": 4, "message": "unrelated"}]}
|
|
95
|
+
|
|
96
|
+
result = filter_new_violations({Path("app.py"): violations}, baseline)
|
|
97
|
+
|
|
98
|
+
assert {v.line for v in result[Path("app.py")]} == {4, 7}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_filter_new_violations_empty_baseline_returns_everything():
|
|
102
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
103
|
+
|
|
104
|
+
result = filter_new_violations({Path("app.py"): violations}, {})
|
|
105
|
+
|
|
106
|
+
assert result == {Path("app.py"): violations}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_filter_new_violations_drops_file_with_no_remaining_new_violations():
|
|
110
|
+
violations = analyze_source(VIOLATION_SRC, filepath="app.py")
|
|
111
|
+
baseline = build_baseline({Path("app.py"): violations})
|
|
112
|
+
|
|
113
|
+
clean_file_results = {Path("app.py"): violations, Path("clean.py"): []}
|
|
114
|
+
result = filter_new_violations(clean_file_results, baseline)
|
|
115
|
+
|
|
116
|
+
assert result == {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ─── `pyvibe baseline` / `pyvibe scan --baseline` CLI (subprocess) ─────────
|
|
120
|
+
|
|
121
|
+
def _run_cli(args, cwd):
|
|
122
|
+
# cwd is a throwaway tmp_path, not REPO_ROOT, so `-m pyvibe` needs
|
|
123
|
+
# PYTHONPATH set explicitly to find the package in a test environment
|
|
124
|
+
# where it isn't pip-installed (see the review-subcommand CI fix).
|
|
125
|
+
env = {**os.environ, "PYTHONPATH": str(REPO_ROOT)}
|
|
126
|
+
return subprocess.run(
|
|
127
|
+
[sys.executable, "-m", "pyvibe", *args],
|
|
128
|
+
cwd=cwd,
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
env=env,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_cli_baseline_create_writes_file_and_message(tmp_path):
|
|
136
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
137
|
+
|
|
138
|
+
result = _run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
139
|
+
|
|
140
|
+
assert result.returncode == 0
|
|
141
|
+
assert "Baseline created: 2 findings saved to .pyvibe-baseline.json" in result.stdout
|
|
142
|
+
baseline_file = tmp_path / ".pyvibe-baseline.json"
|
|
143
|
+
assert baseline_file.exists()
|
|
144
|
+
assert len(json.loads(baseline_file.read_text())["app.py"]) == 2
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_cli_baseline_create_refuses_to_overwrite_existing(tmp_path):
|
|
148
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
149
|
+
_run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
150
|
+
|
|
151
|
+
result = _run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
152
|
+
|
|
153
|
+
assert result.returncode == 2
|
|
154
|
+
assert "already exists" in result.stderr
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_cli_baseline_update_overwrites_existing(tmp_path):
|
|
158
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
159
|
+
_run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
160
|
+
|
|
161
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC + "\nasync def third():\n time.sleep(1)\n")
|
|
162
|
+
result = _run_cli(["baseline", "update", "."], cwd=tmp_path)
|
|
163
|
+
|
|
164
|
+
assert result.returncode == 0
|
|
165
|
+
assert "Baseline updated: 3 findings saved to .pyvibe-baseline.json" in result.stdout
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_cli_scan_baseline_reports_only_new_finding(tmp_path):
|
|
169
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
170
|
+
_run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
171
|
+
|
|
172
|
+
(tmp_path / "app.py").write_text(
|
|
173
|
+
VIOLATION_SRC + "\nasync def third():\n time.sleep(1)\n"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
result = _run_cli(["scan", ".", "--baseline", "--json"], cwd=tmp_path)
|
|
177
|
+
output = json.loads(result.stdout)
|
|
178
|
+
|
|
179
|
+
assert result.returncode == 1
|
|
180
|
+
assert len(output) == 1
|
|
181
|
+
assert output[0]["line"] == 10
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_cli_scan_baseline_clean_when_no_new_findings(tmp_path):
|
|
185
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
186
|
+
_run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
187
|
+
|
|
188
|
+
result = _run_cli(["scan", ".", "--baseline"], cwd=tmp_path)
|
|
189
|
+
|
|
190
|
+
assert result.returncode == 0
|
|
191
|
+
assert "No violations found" in result.stdout
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_cli_scan_baseline_missing_file_exits_2(tmp_path):
|
|
195
|
+
(tmp_path / "app.py").write_text("print('clean')\n")
|
|
196
|
+
|
|
197
|
+
result = _run_cli(["scan", ".", "--baseline"], cwd=tmp_path)
|
|
198
|
+
|
|
199
|
+
assert result.returncode == 2
|
|
200
|
+
assert "No baseline found" in result.stderr
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_cli_scan_without_baseline_flag_matches_bare_invocation(tmp_path):
|
|
204
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
205
|
+
|
|
206
|
+
scan_result = _run_cli(["scan", ".", "--json"], cwd=tmp_path)
|
|
207
|
+
bare_result = _run_cli([".", "--json"], cwd=tmp_path)
|
|
208
|
+
|
|
209
|
+
assert json.loads(scan_result.stdout) == json.loads(bare_result.stdout)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_cli_bare_command_supports_baseline_flag(tmp_path):
|
|
213
|
+
(tmp_path / "app.py").write_text(VIOLATION_SRC)
|
|
214
|
+
_run_cli(["baseline", "create", "."], cwd=tmp_path)
|
|
215
|
+
|
|
216
|
+
(tmp_path / "app.py").write_text(
|
|
217
|
+
VIOLATION_SRC + "\nasync def third():\n time.sleep(1)\n"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
result = _run_cli([".", "--baseline", "--json"], cwd=tmp_path)
|
|
221
|
+
output = json.loads(result.stdout)
|
|
222
|
+
|
|
223
|
+
assert result.returncode == 1
|
|
224
|
+
assert len(output) == 1
|
|
225
|
+
assert output[0]["line"] == 10
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_cli_bare_command_baseline_missing_file_exits_2(tmp_path):
|
|
229
|
+
(tmp_path / "app.py").write_text("print('clean')\n")
|
|
230
|
+
|
|
231
|
+
result = _run_cli([".", "--baseline"], cwd=tmp_path)
|
|
232
|
+
|
|
233
|
+
assert result.returncode == 2
|
|
234
|
+
assert "No baseline found" in result.stderr
|
|
@@ -176,11 +176,16 @@ def test_analyze_file_without_line_filter_returns_everything(tmp_path):
|
|
|
176
176
|
# ─── `pyvibe review` CLI (subprocess, end-to-end against a real git repo) ──
|
|
177
177
|
|
|
178
178
|
def _run_cli(args, cwd):
|
|
179
|
+
# cwd here is a throwaway temp git repo, not REPO_ROOT, so `-m pyvibe`
|
|
180
|
+
# can't rely on cwd-insertion to find the package — it must be told
|
|
181
|
+
# explicitly via PYTHONPATH (pyvibe isn't installed in the test env).
|
|
182
|
+
env = {**os.environ, "PYTHONPATH": str(REPO_ROOT)}
|
|
179
183
|
return subprocess.run(
|
|
180
184
|
[sys.executable, "-m", "pyvibe", *args],
|
|
181
185
|
cwd=cwd,
|
|
182
186
|
capture_output=True,
|
|
183
187
|
text=True,
|
|
188
|
+
env=env,
|
|
184
189
|
)
|
|
185
190
|
|
|
186
191
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.10.0"
|
|
File without changes
|
{python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/python_vibe_guard.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/gather_no_return_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_vibe_guard-0.10.0 → python_vibe_guard-0.11.0}/pyvibe/rules/loop_run_until_complete.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|