116 lines
4.6 KiB
Python
116 lines
4.6 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""重写 selfcheck.py 的 F 组:改为 import load_path 模块读取权威 PATHS 元组。
|
||
|
||
旧实现用正则扫描 load_path.py **源码文本**,会把生成器表达式里的模板串
|
||
"/api/pbl_learner/%s.dspy" 误当成注册路径(实际 PATHS 里是展开后的字面量),
|
||
导致「无通配符」检查假失败。改为 importlib 加载模块、直接读 PATHS 与 validate(),
|
||
以运行时真实值为准,彻底消除文本扫描误报。
|
||
"""
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
SC = os.path.join(ROOT, "scripts", "selfcheck.py")
|
||
|
||
src = open(SC, "r", encoding="utf-8").read()
|
||
bak = SC + ".fbak"
|
||
shutil.copyfile(SC, bak)
|
||
|
||
NEW_F = '''# ============================================================== F. RBAC 路径
|
||
def group_f() -> None:
|
||
"""以 import load_path 后的运行时 PATHS 元组为权威(不扫源码文本,避免把
|
||
生成器模板串 '/api/x/%s.dspy' 误判为通配路径)。"""
|
||
g = "F.RBAC路径"
|
||
check(g, os.path.isfile(LOAD_PATH), "load_path.py 存在", rel(LOAD_PATH))
|
||
|
||
mod = None
|
||
err = ""
|
||
try:
|
||
import importlib.util
|
||
spec = importlib.util.spec_from_file_location("_lp_m1a", LOAD_PATH)
|
||
mod = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(mod)
|
||
except Exception as e: # noqa: BLE001
|
||
err = "%s: %s" % (type(e).__name__, e)
|
||
check(g, mod is not None, "load_path.py 可导入执行", err or "导入成功")
|
||
if mod is None:
|
||
return
|
||
|
||
paths = list(getattr(mod, "PATHS", ()) or ())
|
||
check(g, len(paths) > 0, "RBAC 注册路径显式枚举(PATHS 元组)",
|
||
"路径数=%d" % len(paths))
|
||
|
||
wild = [p for p in paths if "%" in p or "*" in p]
|
||
check(g, not wild, "RBAC 路径无 %/* 通配符(运行时真实值)",
|
||
"通配路径=%s" % (wild[:5] or "无"))
|
||
|
||
nonstd = [p for p in paths
|
||
if not re.fullmatch(r"/api/[A-Za-z0-9_.-]+\\.dspy", p)]
|
||
check(g, not nonstd, "RBAC 路径均为 /api/<表>/<动作>.dspy 规范字面量",
|
||
"异常=%s" % (nonstd[:5] or "无"))
|
||
|
||
dup = sorted({p for p in paths if paths.count(p) > 1})
|
||
check(g, not dup, "RBAC 路径无重复", "重复=%s" % (dup[:5] or "无"))
|
||
|
||
# 覆盖 json/*.json editable 的 3×11 条(超集合法:支撑表含额外动作)
|
||
need = set()
|
||
if os.path.isdir(JSON_DIR):
|
||
for fn in sorted(os.listdir(JSON_DIR)):
|
||
if not fn.endswith(".json"):
|
||
continue
|
||
d, _e = load_json(os.path.join(JSON_DIR, fn))
|
||
for u in ((d or {}).get("editable") or []):
|
||
if isinstance(u, str) and u.startswith("api/"):
|
||
need.add("/" + u)
|
||
missing = sorted(need - set(paths))
|
||
check(g, not missing,
|
||
"load_path.py 覆盖 json/*.json editable 全部路径(超集)",
|
||
"load_path=%d json_editable=%d 未覆盖=%s"
|
||
% (len(paths), len(need), missing[:5] or "无"))
|
||
|
||
# 孤儿路径:每条路径的表名必须归属主清单 11 表或支撑表 9 表
|
||
main_t = set(getattr(mod, "MAIN_TABLES", ()) or ())
|
||
sup_t = set(getattr(mod, "SUPPORT_TABLES", ()) or ())
|
||
known = main_t | sup_t
|
||
check(g, len(main_t) == 11, "load_path.MAIN_TABLES = 设计定稿 11 表",
|
||
"实际=%d" % len(main_t))
|
||
orphan = []
|
||
for p in paths:
|
||
m = re.match(r"/api/([a-z0-9_]+)/", p)
|
||
if not m or (known and m.group(1) not in known):
|
||
orphan.append(p)
|
||
check(g, not orphan, "load_path.py 无孤儿路径(均归属已知表)",
|
||
"孤儿=%s" % (orphan[:5] or "无"))
|
||
|
||
# 模块自带 validate() 必须通过
|
||
vfn = getattr(mod, "validate", None)
|
||
if callable(vfn):
|
||
verrs = vfn() or []
|
||
check(g, not verrs, "load_path.validate() 自检通过",
|
||
"错误=%s" % (verrs[:3] or "无"))
|
||
else:
|
||
check(g, False, "load_path.validate() 存在", "未定义 validate()")
|
||
|
||
|
||
'''
|
||
|
||
pat = re.compile(r"# =+ F\. RBAC 路径\n.*?(?=# =+ G\.)", re.S)
|
||
if pat.search(src):
|
||
src = pat.sub(NEW_F, src, count=1)
|
||
open(SC, "w", encoding="utf-8").write(src)
|
||
r = subprocess.run([sys.executable, "-m", "py_compile", SC],
|
||
capture_output=True, text=True)
|
||
if r.returncode != 0:
|
||
shutil.move(bak, SC)
|
||
print("[ROLLBACK] 编译失败已回滚: %s" % (r.stderr or "")[-400:])
|
||
sys.exit(1)
|
||
os.remove(bak)
|
||
print("[FIX ] F 组已重写为 import load_path 读运行时 PATHS")
|
||
else:
|
||
os.remove(bak)
|
||
print("[ERR ] 未匹配到 F 组区块,未修改")
|
||
sys.exit(1)
|