forked from unslothai/unsloth
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify_comment_only_diff.py
More file actions
245 lines (213 loc) · 8.09 KB
/
Copy pathverify_comment_only_diff.py
File metadata and controls
245 lines (213 loc) · 8.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# Unsloth - 2x faster, 60% less VRAM LLM training and finetuning
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
"""Deterministic comment / docstring-only verifier.
Compares a list of changed files between two git refs and reports whether
each diff is strictly comments / docstrings (Python) or comments
(YAML / GitHub Actions). Useful for gating a "comment trim" /
"docstring refactor" PR against accidental code drift.
Per .py file: parse both revs into AST, strip module / class / function
docstrings, then compare ast.unparse output. Pure Python comments are
discarded by the parser by construction, so any post-strip diff is real
code. Per .yml file: yaml.safe_load both sides and compare the parsed
Python object; if scalar values differ, also strip shell comments inside
``run: |`` block bodies before comparing. Exit code 0 = all OK, 1 = at
least one file has a real (non-comment) diff or an error.
Usage:
python scripts/verify_comment_only_diff.py [--base REF] [--head REF] path ...
Defaults: --base origin/main, --head HEAD. Paths are repo-relative.
Example:
git diff --name-only origin/main..HEAD \\
| xargs python scripts/verify_comment_only_diff.py --base origin/main
"""
from __future__ import annotations
import argparse
import ast
import difflib
import subprocess
import sys
from typing import Any
import yaml
def _git_show(rev: str, path: str) -> str:
return subprocess.check_output(
["git", "show", f"{rev}:{path}"],
text = True,
stderr = subprocess.DEVNULL,
)
def _strip_docstrings(tree: ast.AST) -> ast.AST:
"""Remove docstrings; empty bodies become ``pass`` so unparse stays valid."""
for node in ast.walk(tree):
if isinstance(
node,
(ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef),
):
body = getattr(node, "body", None)
if not body:
continue
first = body[0]
if (
isinstance(first, ast.Expr)
and isinstance(first.value, ast.Constant)
and isinstance(first.value.value, str)
):
node.body = body[1:]
if not node.body:
node.body = [ast.Pass()]
return tree
def _normalize_py(src: str) -> str:
tree = ast.parse(src)
tree = _strip_docstrings(tree)
return ast.unparse(tree)
def _strip_shell_comments(s: str) -> str:
"""Strip shell comments and collapse blank lines. Heuristic: skips lines
with an odd quote count (open string)."""
out = []
for line in s.splitlines():
stripped = line.lstrip()
if stripped.startswith("#"):
continue
has_single = line.count("'") % 2 == 0
has_double = line.count('"') % 2 == 0
if has_single and has_double:
idx = line.find(" #")
if idx >= 0:
line = line[:idx].rstrip()
out.append(line)
norm = []
prev_blank = False
for line in out:
if line.strip() == "":
if prev_blank:
continue
prev_blank = True
else:
prev_blank = False
norm.append(line)
return "\n".join(norm).strip()
def _normalize_yaml_run_strings(obj: Any) -> Any:
"""Strip shell comments from any multi-line string (``run: |`` body)."""
if isinstance(obj, dict):
return {k: _normalize_yaml_run_strings(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_normalize_yaml_run_strings(x) for x in obj]
if isinstance(obj, str) and "\n" in obj:
return _strip_shell_comments(obj)
return obj
def _walk_yaml_diff(
b: Any,
a: Any,
prefix: str = "",
) -> None:
"""Print a path-keyed summary of the first structural / scalar diff."""
if type(b) is not type(a):
print(
f" type-diff at {prefix or '/'}: " f"{type(b).__name__} -> {type(a).__name__}",
)
return
if isinstance(b, dict):
keys = sorted((set(b.keys()) | set(a.keys())), key = lambda x: str(x))
for k in keys:
if k not in b:
print(f" added key {prefix}/{k}")
elif k not in a:
print(f" removed key {prefix}/{k}")
else:
_walk_yaml_diff(b[k], a[k], f"{prefix}/{k}")
elif isinstance(b, list):
if len(b) != len(a):
print(
f" list len at {prefix or '/'}: " f"{len(b)} -> {len(a)}",
)
for i, (bi, ai) in enumerate(zip(b, a)):
_walk_yaml_diff(bi, ai, f"{prefix}[{i}]")
elif b != a:
bs = repr(b)[:300]
as_ = repr(a)[:300]
print(f" scalar at {prefix or '/'}:")
print(f" before: {bs}")
print(f" after: {as_}")
def _verify_python(path: str, before: str, after: str) -> bool:
try:
norm_before = _normalize_py(before)
norm_after = _normalize_py(after)
except SyntaxError as exc:
print(f"FAIL {path}: SyntaxError parsing -- {exc}")
return False
if norm_before == norm_after:
print(f"OK {path} (AST identical after docstring strip)")
return True
diff = list(
difflib.unified_diff(
norm_before.splitlines(),
norm_after.splitlines(),
fromfile = f"{path}@before",
tofile = f"{path}@after",
n = 2,
)
)
print(f"FAIL {path}: AST differs after docstring strip:")
for line in diff[:40]:
print(f" {line}")
return False
def _verify_yaml(path: str, before: str, after: str) -> bool:
try:
raw_before = yaml.safe_load(before)
raw_after = yaml.safe_load(after)
except yaml.YAMLError as exc:
print(f"FAIL {path}: YAML parse error -- {exc}")
return False
if raw_before == raw_after:
print(f"OK {path} (YAML parsed object identical)")
return True
norm_before = _normalize_yaml_run_strings(raw_before)
norm_after = _normalize_yaml_run_strings(raw_after)
if norm_before == norm_after:
print(
f"OK {path} (YAML parsed object identical after "
f"stripping shell comments from run: bodies)",
)
return True
print(
f"FAIL {path}: YAML parsed objects still differ after stripping "
f"shell comments from `run:` bodies.",
)
_walk_yaml_diff(norm_before, norm_after)
return False
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description = "Verify each path's diff between BASE and HEAD is "
"strictly comments / docstrings.",
)
parser.add_argument("--base", default = "origin/main", help = "base git ref")
parser.add_argument("--head", default = "HEAD", help = "head git ref")
parser.add_argument("paths", nargs = "+", help = "repo-relative paths")
args = parser.parse_args(argv)
rc = 0
print(f"Comparing {len(args.paths)} files: {args.base} vs {args.head}\n")
for path in args.paths:
try:
before = _git_show(args.base, path)
after = _git_show(args.head, path)
except subprocess.CalledProcessError as exc:
print(f"SKIP {path}: {exc}")
continue
if path.endswith(".py"):
if not _verify_python(path, before, after):
rc = 1
elif path.endswith((".yml", ".yaml")):
if not _verify_yaml(path, before, after):
rc = 1
else:
print(f"NOTE {path}: not .py or .yaml -- skipped automated check.")
return rc
if __name__ == "__main__":
sys.exit(main())