Files
ad/molodetz/gates.py
T

130 lines
5.0 KiB
Python

# retoor <retoor@molodetz.nl>
import ast
import re
import sys
from pathlib import Path
PKG_ROOT = Path(__file__).resolve().parent
REPO_ROOT = PKG_ROOT.parent
BREAKPOINTS = {360, 480, 768, 1024}
HTTP_CLIENT_ALLOWLIST = {"molodetz/stealth.py", "molodetz/curl_transport.py"}
CLIENT_RENDER_ALLOWLIST = {"molodetz/templates/admin/post_form.html"}
LOADING_BUTTON_MODULE = "molodetz/static/js/utils/LoadingButton.js"
_DASH_ENTITIES = ["&" + name + ";" for name in ("m" + "dash", "n" + "dash", "#8212", "#8211", "#x2014", "#x2013")]
DASH_PATTERN = re.compile("|".join(["\u2014", "\u2013", *_DASH_ENTITIES]), re.IGNORECASE)
DASH_SUFFIXES = {".py", ".html", ".md", ".js", ".css", ".toml", ".txt", ".sh", ".cfg", ".ini"}
DASH_EXEMPT_DIRS = ("molodetz/static/vendor", "molodetz/static/js/generated")
def _rel(path):
return path.resolve().relative_to(REPO_ROOT).as_posix()
def _files(base, pattern):
return sorted((REPO_ROOT / base).rglob(pattern))
def gate_template_environment():
offenders = set()
for path in _files("molodetz", "*.py"):
text = path.read_text(encoding="utf-8")
if re.search(r"\b(Jinja2Templates|Environment)\(", text) and _rel(path) != "molodetz/templating.py":
offenders.add(_rel(path))
return offenders
def gate_breakpoints():
found = set()
for path in _files("molodetz/static/css", "*.css"):
for match in re.finditer(r"@media[^{]*\((?:max|min)-width:\s*(\d+)px\)", path.read_text(encoding="utf-8")):
found.add(int(match.group(1)))
return found
def gate_http_clients():
found = set()
pattern = re.compile(r"\b(httpx\.(Async)?Client|AsyncSession|aiohttp\.ClientSession|urllib\.request\.urlopen)\(")
for path in _files("molodetz", "*.py"):
if pattern.search(path.read_text(encoding="utf-8")):
found.add(_rel(path))
return found
def gate_client_render_templates():
found = set()
for path in _files("molodetz/templates", "*.html"):
if "/templates/docs/" in path.as_posix():
continue
if re.search(r"<dp-(content|title)\b", path.read_text(encoding="utf-8")):
found.add(_rel(path))
return found
def gate_bare_static():
offenders = []
for path in _files("molodetz/templates", "*.html"):
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if re.search(r"""(href|src|srcset|content)\s*=\s*["']/static/""", line):
offenders.append(f"{_rel(path)}:{number}")
return offenders
def gate_loading_toggles():
offenders = []
for path in _files("molodetz/static/js", "*.js"):
if _rel(path) == LOADING_BUTTON_MODULE:
continue
if "is-loading" in path.read_text(encoding="utf-8"):
offenders.append(_rel(path))
return offenders
def gate_em_dashes():
offenders = []
for path in sorted(REPO_ROOT.rglob("*")):
if not path.is_file() or path.suffix not in DASH_SUFFIXES:
continue
rel = _rel(path)
if rel.startswith((".venv", "data", "out", ".git")) or rel.startswith(DASH_EXEMPT_DIRS):
continue
if DASH_PATTERN.search(path.read_text(encoding="utf-8", errors="ignore")):
offenders.append(rel)
return offenders
def gate_json_error_order():
offenders = []
for path in _files("molodetz", "*.py"):
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.Call) and getattr(node.func, "id", getattr(node.func, "attr", None)) == "json_error":
first = node.args[0] if node.args else None
is_status = isinstance(first, ast.Constant) and isinstance(first.value, int)
is_name = isinstance(first, (ast.Name, ast.Attribute)) and "status" in ast.unparse(first).lower()
if not (is_status or is_name):
offenders.append(f"{_rel(path)}:{node.lineno}")
return offenders
def run_all():
results = {
"1 template environment only in templating.py": (not gate_template_environment(), gate_template_environment()),
"2 breakpoints equal 360 480 768 1024": (gate_breakpoints() == BREAKPOINTS, sorted(gate_breakpoints())),
"3 http client files equal allowlist": (gate_http_clients() == HTTP_CLIENT_ALLOWLIST, sorted(gate_http_clients())),
"4 client-render templates equal allowlist": (gate_client_render_templates() == CLIENT_RENDER_ALLOWLIST, sorted(gate_client_render_templates())),
"5 no bare /static/ in templates": (not gate_bare_static(), gate_bare_static()),
"6 no is-loading outside LoadingButton": (not gate_loading_toggles(), gate_loading_toggles()),
"em-dash scan": (not gate_em_dashes(), gate_em_dashes()),
"json_error argument order": (not gate_json_error_order(), gate_json_error_order()),
}
failed = 0
for name, (ok, detail) in results.items():
print(f"{'PASS' if ok else 'FAIL'} {name} {detail if not ok else ''}".rstrip())
failed += 0 if ok else 1
return failed
if __name__ == "__main__":
sys.exit(1 if run_all() else 0)