openverify-code 0.1.1__py3-none-any.whl
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.
openverify_code/local.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Python code rules owned by openverify-code."""
|
|
2
|
+
# pyright: basic
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from openverify.relay.local import finding, run_rules
|
|
12
|
+
|
|
13
|
+
RULES = {
|
|
14
|
+
"ruff": "Ruff lint with project configuration",
|
|
15
|
+
"ruff-format": "Ruff format check with project configuration",
|
|
16
|
+
"pytest": "Configured project tests",
|
|
17
|
+
"ast-grep": "Configured structural code pattern",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tool(name: str) -> str | None:
|
|
22
|
+
sibling = Path(sys.executable).with_name(name + (".exe" if os.name == "nt" else ""))
|
|
23
|
+
return str(sibling) if sibling.is_file() else shutil.which(name)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run_command(argv: list[str], root: Path) -> subprocess.CompletedProcess[str]:
|
|
27
|
+
return subprocess.run(argv, cwd=root, text=True, capture_output=True, encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def code_rule(rule: str, root: Path, files: list[str], settings: dict) -> list[dict]:
|
|
31
|
+
for name in files:
|
|
32
|
+
if b"\0" in (root / name).read_bytes():
|
|
33
|
+
raise ValueError(f"Binary selected file: {name}")
|
|
34
|
+
if rule == "pytest":
|
|
35
|
+
tests = settings.get("tests")
|
|
36
|
+
if not tests:
|
|
37
|
+
return []
|
|
38
|
+
if any(not isinstance(t, str) or t.startswith("-") for t in tests):
|
|
39
|
+
raise ValueError("pytest tests must be explicit paths")
|
|
40
|
+
executable = settings.get("executable") or tool("pytest")
|
|
41
|
+
if not executable:
|
|
42
|
+
raise FileNotFoundError("pytest is required")
|
|
43
|
+
pytest_config = []
|
|
44
|
+
if settings.get("_staged"):
|
|
45
|
+
candidate = next(
|
|
46
|
+
(
|
|
47
|
+
root / name
|
|
48
|
+
for name in ("pytest.ini", "pyproject.toml", "tox.ini", "setup.cfg")
|
|
49
|
+
if (root / name).is_file()
|
|
50
|
+
),
|
|
51
|
+
None,
|
|
52
|
+
)
|
|
53
|
+
if candidate is None:
|
|
54
|
+
candidate = root / ".openverify-empty-pytest.ini"
|
|
55
|
+
candidate.write_text("[pytest]\n", encoding="utf-8")
|
|
56
|
+
pytest_config = ["-c", str(candidate)]
|
|
57
|
+
result = run_command([executable, "-q", *pytest_config, "--", *tests], root)
|
|
58
|
+
if result.returncode not in (0, 1):
|
|
59
|
+
raise RuntimeError(f"pytest exit {result.returncode}: {result.stderr}")
|
|
60
|
+
return (
|
|
61
|
+
[]
|
|
62
|
+
if result.returncode == 0
|
|
63
|
+
else [finding(rule, "", 0, 0, "Tests failed", stdout=result.stdout[-8000:])]
|
|
64
|
+
)
|
|
65
|
+
if not files:
|
|
66
|
+
return []
|
|
67
|
+
if rule in ("ruff", "ruff-format"):
|
|
68
|
+
executable = settings.get("executable") or tool("ruff")
|
|
69
|
+
if not executable:
|
|
70
|
+
raise FileNotFoundError("Ruff is required")
|
|
71
|
+
ruff_config = []
|
|
72
|
+
if settings.get("_staged"):
|
|
73
|
+
candidate = next(
|
|
74
|
+
(
|
|
75
|
+
root / name
|
|
76
|
+
for name in ("ruff.toml", ".ruff.toml", "pyproject.toml")
|
|
77
|
+
if (root / name).is_file()
|
|
78
|
+
),
|
|
79
|
+
None,
|
|
80
|
+
)
|
|
81
|
+
ruff_config = ["--config", str(candidate)] if candidate else ["--isolated"]
|
|
82
|
+
if rule == "ruff":
|
|
83
|
+
argv = [executable, "check", *ruff_config, "--output-format", "json"]
|
|
84
|
+
if settings.get("select"):
|
|
85
|
+
argv += ["--select", ",".join(settings["select"])]
|
|
86
|
+
result = run_command([*argv, "--", *files], root)
|
|
87
|
+
if result.returncode not in (0, 1):
|
|
88
|
+
raise RuntimeError(f"Ruff exit {result.returncode}: {result.stderr}")
|
|
89
|
+
return [
|
|
90
|
+
finding(
|
|
91
|
+
rule,
|
|
92
|
+
Path(item["filename"]).relative_to(root).as_posix(),
|
|
93
|
+
item["location"]["row"],
|
|
94
|
+
item["location"]["column"],
|
|
95
|
+
item["message"],
|
|
96
|
+
code=item["code"],
|
|
97
|
+
)
|
|
98
|
+
for item in json.loads(result.stdout)
|
|
99
|
+
]
|
|
100
|
+
findings = []
|
|
101
|
+
for name in files:
|
|
102
|
+
result = run_command([executable, "format", *ruff_config, "--check", "--", name], root)
|
|
103
|
+
if result.returncode not in (0, 1):
|
|
104
|
+
raise RuntimeError(f"Ruff format exit {result.returncode}: {result.stderr}")
|
|
105
|
+
if result.returncode == 1:
|
|
106
|
+
findings.append(finding(rule, name, 1, 1, "Formatting differs"))
|
|
107
|
+
return findings
|
|
108
|
+
executable = settings.get("executable") or tool("ast-grep")
|
|
109
|
+
if not executable:
|
|
110
|
+
raise FileNotFoundError("ast-grep is required")
|
|
111
|
+
if "rule_file" in settings:
|
|
112
|
+
argv = [executable, "scan", "--rule", settings["rule_file"]]
|
|
113
|
+
elif settings.get("pattern") and settings.get("language"):
|
|
114
|
+
argv = [executable, "run", "--lang", settings["language"], "--pattern", settings["pattern"]]
|
|
115
|
+
else:
|
|
116
|
+
return []
|
|
117
|
+
result = run_command([*argv, "--json=compact", "--", *files], root)
|
|
118
|
+
if result.returncode not in (0, 1):
|
|
119
|
+
raise RuntimeError(f"ast-grep exit {result.returncode}: {result.stderr}")
|
|
120
|
+
return [
|
|
121
|
+
finding(
|
|
122
|
+
rule,
|
|
123
|
+
item["file"],
|
|
124
|
+
item["range"]["start"]["line"] + 1,
|
|
125
|
+
item["range"]["start"]["column"] + 1,
|
|
126
|
+
"Pattern matched",
|
|
127
|
+
text=item["text"],
|
|
128
|
+
)
|
|
129
|
+
for item in json.loads(result.stdout or "[]")
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def check(root: Path, names: list[str], staged: bool = False) -> dict:
|
|
134
|
+
return run_rules("code", root, names, staged, RULES, code_rule)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openverify-code
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Local Python code checks for OpenVerify
|
|
5
|
+
Requires-Python: <3.14,>=3.13
|
|
6
|
+
Requires-Dist: openverify-relay==0.1.1
|
|
7
|
+
Provides-Extra: tools
|
|
8
|
+
Requires-Dist: ruff>=0.17; extra == "tools"
|
|
9
|
+
Requires-Dist: pytest>=8.3; extra == "tools"
|
|
10
|
+
Requires-Dist: ast-grep-cli>=0.50; extra == "tools"
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
openverify_code/__init__.py,sha256=Q3EAQpz_nmAZl5395qdQisrAR36sgXoUB-1p2kVcm_4,137
|
|
2
|
+
openverify_code/local.py,sha256=D7z0cSUwbS8XSIiIKMnDlUja9IQHjUxLp8yPnCXNzus,5323
|
|
3
|
+
openverify_code-0.1.1.dist-info/METADATA,sha256=XxrHswmwRY99zUj9qJJbxyLRwwbnvnAuyuqAM2JHiDk,340
|
|
4
|
+
openverify_code-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
5
|
+
openverify_code-0.1.1.dist-info/top_level.txt,sha256=t09MnzjD_Qcx2adx3POypDzEFWD6U1-f0c6XqLqZ_UA,16
|
|
6
|
+
openverify_code-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openverify_code
|