MAINT-20 part 2: same-code script checks -OO code, added/removed files, allowed paths, docstring list
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
d4c01daca5
commit
0fb4c2b5e2
@@ -5,12 +5,19 @@
|
||||
|
||||
For each FILE (default: every goodwe_controller/app/*.py that differs from
|
||||
BASE_REF, default release/1.0) the ast.dump() of the tree with docstrings
|
||||
removed must be identical at BASE_REF and in the working tree. Also prints
|
||||
code lines vs comment-only + docstring lines (whole lines, via tokenize) before
|
||||
and after. Run from the repository root. Exit 1 on any code difference.
|
||||
removed must be identical at BASE_REF and in the working tree, and so must
|
||||
the `python -OO` compiled code (a wholly removed docstring leaves a no-op
|
||||
there). An added or removed app/*.py file is a code difference. The diff
|
||||
against BASE_REF may touch only goodwe_controller/app/*.py,
|
||||
goodwe_controller/DECISIONS/*.md and this script. Also prints code lines vs
|
||||
comment-only + docstring lines (whole lines, via tokenize) before and after,
|
||||
and the docstrings removed entirely or added. Run from the repository root.
|
||||
Exit 1 on any difference.
|
||||
"""
|
||||
import ast
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tokenize
|
||||
@@ -53,29 +60,87 @@ def counts(src):
|
||||
return len(code), len(doc) + len(com - code - doc)
|
||||
|
||||
|
||||
def qualnames(src):
|
||||
out = {}
|
||||
|
||||
def walk(node, prefix):
|
||||
for n in ast.iter_child_nodes(node):
|
||||
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef,
|
||||
ast.ClassDef)):
|
||||
q = prefix + n.name
|
||||
out[q] = docstring(n) is not None
|
||||
walk(n, q + ".")
|
||||
out["<module>"] = docstring(ast.parse(src)) is not None
|
||||
walk(ast.parse(src), "")
|
||||
return out
|
||||
|
||||
|
||||
def code_sig(co):
|
||||
consts = tuple(code_sig(c) if hasattr(c, "co_code") else c
|
||||
for c in co.co_consts)
|
||||
return (co.co_name, co.co_code, consts, co.co_names, co.co_varnames,
|
||||
co.co_freevars, co.co_cellvars, co.co_flags, co.co_argcount,
|
||||
co.co_stacksize, co.co_exceptiontable)
|
||||
|
||||
|
||||
def oo_same(old, new):
|
||||
return (code_sig(compile(old, "f", "exec", optimize=2))
|
||||
== code_sig(compile(new, "f", "exec", optimize=2)))
|
||||
|
||||
|
||||
def git(*a):
|
||||
return subprocess.run(["git", *a], check=True, capture_output=True,
|
||||
text=True).stdout
|
||||
|
||||
|
||||
def exists(base, f):
|
||||
return subprocess.run(["git", "cat-file", "-e", f"{base}:{f}"],
|
||||
capture_output=True).returncode == 0
|
||||
|
||||
|
||||
ALLOWED = re.compile(r"^goodwe_controller/(app/[^/]+\.py|DECISIONS/[^/]+\.md|"
|
||||
r"tools/maint20_same_code\.py)$")
|
||||
|
||||
|
||||
def ratio(m, c):
|
||||
return f"{m / c:.2f}" if c else "n/a"
|
||||
|
||||
|
||||
def main(argv):
|
||||
base = argv[0] if argv else "release/1.0"
|
||||
files = argv[1:] or [
|
||||
f for f in git("diff", "--name-only", base, "--",
|
||||
"goodwe_controller/app").split()
|
||||
if f.endswith(".py")]
|
||||
bad = 0
|
||||
print(f"base {base} = {git('rev-parse', base).strip()}")
|
||||
changed = git("diff", "--name-only", base).split()
|
||||
for f in changed:
|
||||
if not ALLOWED.match(f):
|
||||
print(f"FAIL: diff touches {f}, outside app/*.py, DECISIONS/*.md "
|
||||
"and this script")
|
||||
bad += 1
|
||||
files = argv[1:] or [f for f in changed if re.match(
|
||||
r"^goodwe_controller/app/[^/]+\.py$", f)]
|
||||
for f in files:
|
||||
in_base, in_head = exists(base, f), os.path.exists(f)
|
||||
if in_base != in_head:
|
||||
print(f"CODE DIFFERS {f} "
|
||||
f"{'removed' if in_base else 'added'} file")
|
||||
bad += 1
|
||||
continue
|
||||
old = git("show", f"{base}:{f}")
|
||||
new = open(f, encoding="utf-8").read()
|
||||
same = dump(old) == dump(new)
|
||||
oo = oo_same(old, new)
|
||||
(c0, m0), (c1, m1) = counts(old), counts(new)
|
||||
print(f"{'SAME CODE' if same else 'CODE DIFFERS'} {f} "
|
||||
f"code {c0}->{c1} comment+doc {m0}->{m1} "
|
||||
f"ratio {m0 / c0:.2f}->{m1 / c1:.2f}")
|
||||
bad += not same
|
||||
print("OK: no code change" if not bad else f"FAIL: {bad} file(s) differ")
|
||||
f"ratio {ratio(m0, c0)}->{ratio(m1, c1)} "
|
||||
f"-OO {'same' if oo else 'DIFFERS'}")
|
||||
bad += (not same) + (not oo)
|
||||
qo, qn = qualnames(old), qualnames(new)
|
||||
for q in sorted(q for q in qo if qo[q] and not qn.get(q, True)):
|
||||
print(f" docstring REMOVED: {f}:{q}")
|
||||
for q in sorted(q for q in qn if qn[q] and not qo.get(q, True)):
|
||||
print(f" docstring ADDED: {f}:{q}")
|
||||
print("OK: no code change" if not bad else f"FAIL: {bad} problem(s)")
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user