pbl_blueprint/tools/m1b_fix_qc_round4.py
2026-09-17 16:59:58 +08:00

349 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""M1b QC 第 4 轮退回意见修复脚本(幂等,可重复执行)。
逐条对应 QC 意见:
#1 m1b/__init__.py 导出面缺 get_env → db.py compat 块整条 ImportError 被静默吞掉
修法:新增 pbl_blueprint/m1b/env.py已落盘本脚本把 get_env 等符号
接入 m1b/__init__.py 的 import 与 __all__。
#2 本模块包内 7 处 import 闭包断裂:
init.py:54 from pbl_common.crud_factory import _sor pbl_common 无 _sor
init.py:66 from pbl_common.dbutil import new_id, now_str, escdbutil 无这三个)
test_contract.py:439 from pbl_blueprint.init import API_PATHS, PAGE_PATHS, CRUD_ALIASES
api_blueprint.py:504 from pbl_template.offline import load_offline_template他模块
修法:本任务范围内的缺失符号在 pbl_blueprint 侧自给m1b 供给层 + init.py 导出面补齐);
pbl_template.offline 属他模块职责 → 改为「本地离线兜底优先 + 他模块可选增强」,
并在交付说明中冒泡 PM 明确边界。
#3 声称已修复而引擎实测仍断 23 处(含 pbl_common.* / pbl_agent_runtime 跨模块 19 处)
修法:本脚本末尾跑全量闭包核验,输出「本模块内 0 断裂」的实测证据;
跨模块pbl_common / pbl_agent_runtime断裂如实列出并冒泡不以 fallback 名义掩盖。
用法python3 tools/m1b_fix_qc_round4.py [--check]
"""
from __future__ import print_function
import io
import os
import re
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PKG = os.path.join(REPO, "pbl_blueprint")
M1B = os.path.join(PKG, "m1b")
MARK = "M1b-QC4"
def _read(path):
with io.open(path, encoding="utf-8") as f:
return f.read()
def _write(path, text):
d = os.path.dirname(path)
if d and not os.path.isdir(d):
os.makedirs(d)
with io.open(path, "w", encoding="utf-8") as f:
f.write(text)
def _patch(path, old, new, required=True):
"""把 old 替换为 new已含 new 则跳过(幂等)。返回是否发生写入。"""
txt = _read(path)
if new in txt:
return False
if old not in txt:
if required:
raise SystemExit("[FAIL] %s 未找到待替换片段:\n%s" % (path, old[:200]))
return False
_write(path, txt.replace(old, new, 1))
print("[patch] %s" % os.path.relpath(path, REPO))
return True
# --------------------------------------------------------------------------
# #1 m1b/__init__.py接入 env 子模块get_env 等)
# --------------------------------------------------------------------------
ENV_IMPORT = '''from .env import ( # noqa: F401
NullEnv,
get_env,
get_module_dbname,
has_env,
reset_env,
set_env,
)
'''
ENV_ALL = ''' # envQC#1db.py compat 块依赖 get_env缺失会让整条 import 静默失败)
"get_env", "set_env", "reset_env", "has_env", "env_or_none",
"get_module_dbname", "NullEnv", "ENV_ATTRS",
'''
def fix_m1b_init():
path = os.path.join(M1B, "__init__.py")
txt = _read(path)
changed = False
if "from .env import" not in txt:
# 插在 errors 导入之后errors 是 env 的依赖,必须先导入)
m = re.search(r"^from \.errors import \(.*?^\)\n", txt, re.S | re.M)
if not m:
raise SystemExit("[FAIL] m1b/__init__.py 未找到 .errors 导入块")
txt = txt[: m.end()] + ENV_IMPORT + txt[m.end():]
changed = True
if "ENV_ATTRS" not in txt:
# 补进 __all__插在 "new_id" 那一组之前,保持分组注释风格)
anchor = ' # util\n'
if anchor in txt:
txt = txt.replace(anchor, ENV_ALL + anchor, 1)
else:
txt = txt.rstrip()
assert txt.endswith("]"), "m1b/__init__.py __all__ 结尾异常"
txt = txt[:-1].rstrip("\n") + "\n" + ENV_ALL + "]\n"
changed = True
# env_or_none 由 env.py 导出__all__ 里已列出,这里补 import 面
if "env_or_none" in txt and " env_or_none,\n" not in txt and "from .env import" in txt:
txt = txt.replace(" get_env,\n", " env_or_none,\n get_env,\n", 1)
changed = True
if changed:
_write(path, txt)
print("[patch] pbl_blueprint/m1b/__init__.py接入 env 子模块)")
return changed
# --------------------------------------------------------------------------
# #2a init.py:54 from pbl_common.crud_factory import _sor
# --------------------------------------------------------------------------
OLD_SOR = """def ensure_tables(sor=None, dbname=None):
if sor is None:
from pbl_common.crud_factory import _sor
sor = _sor()
from pbl_common.dbutil import get_dbname
dbname = dbname or get_dbname(MODULE_NAME)"""
NEW_SOR = """def _resolve_sor():
\"\"\"取 sqlor 句柄QC#2pbl_common.crud_factory 无 _sor改为多源解析
解析顺序pbl_common.crud_factory._sor → pbl_common.api.sor →
sqlor 模块单例 → ServerEnv().sor。全部失败抛 PblError(PBL_DB_UNAVAILABLE)
不静默返回 None避免把缺库推迟成下游 AttributeError
\"\"\"
from pbl_blueprint.m1b import PblError, ErrorCode, get_env
try: # 1) pbl_common 兼容供给(若内核已补回 _sor
from pbl_common.crud_factory import _sor # noqa
got = _sor()
if got is not None:
return got
except Exception:
pass
try: # 2) pbl_common.api 契约面
from pbl_common import api as _capi
for attr in ("sor", "sqlor", "get_sor"):
obj = getattr(_capi, attr, None)
if callable(obj):
try:
obj = obj()
except Exception:
obj = None
if obj is not None:
return obj
except Exception:
pass
try: # 3) sqlor 模块单例
import sqlor as _sqlor
for attr in ("sor", "Sqlor", "instance"):
obj = getattr(_sqlor, attr, None)
if callable(obj) and attr != "sor":
try:
obj = obj()
except Exception:
obj = None
if obj is not None:
return obj
except Exception:
pass
try: # 4) ServerEnv
env = get_env(required=False)
obj = getattr(env, "sor", None)
if obj is not None:
return obj
except Exception:
pass
raise PblError(ErrorCode.PBL_DB_UNAVAILABLE,
"sqlor 句柄不可用ensure_tables 需要 sor请传入 sor= 或先挂载应用环境)")
def _resolve_dbname(default=None):
\"\"\"取模块库名QC#2pbl_common.dbutil.get_dbname 可能缺失,多源兜底)。\"\"\"
try:
from pbl_common.dbutil import get_dbname
name = get_dbname(MODULE_NAME)
if name:
return name
except Exception:
pass
from pbl_blueprint.m1b import get_module_dbname
return get_module_dbname(MODULE_NAME, default=default)
def ensure_tables(sor=None, dbname=None):
if sor is None:
sor = _resolve_sor()
dbname = dbname or _resolve_dbname()"""
# --------------------------------------------------------------------------
# #2b init.py:66 from pbl_common.dbutil import new_id, now_str, esc
# --------------------------------------------------------------------------
OLD_SEED = """ from pbl_common.dbutil import new_id, now_str, esc"""
NEW_SEED = """ # QC#2pbl_common.dbutil 导出面无 new_id/now_str/esc半迁移
# 改用本模块 m1b 自包含供给层 + 本地 esc杜绝跨模块符号断裂。
from pbl_blueprint.m1b import new_id, now_str
from pbl_blueprint.init import esc"""
# --------------------------------------------------------------------------
# #2c init.py 导出面补 API_PATHS / PAGE_PATHS / CRUD_ALIASES / esc
# --------------------------------------------------------------------------
INIT_TAIL = '''
# >>> M1b-QC4: init.py 导出面补齐QC#2 >>>
# test_contract.py:439 断言 `from pbl_blueprint.init import API_PATHS, PAGE_PATHS,
# CRUD_ALIASES`;此前这三个符号只存在于 api.pyinit.py 未转出 → import 即 ImportError。
# 这里做「单一事实来源在 api.pyinit.py 转出」的薄再导出,并在 api.py 缺失时给出
# 可用的空面(保证 load_path 注册链路不因符号缺失整体崩掉)。
def _esc(v):
"""SQL 字面量转义(本地实现,不依赖 pbl_common.dbutil.esc"""
if v is None:
return "NULL"
if isinstance(v, bool):
return "1" if v else "0"
if isinstance(v, (int, float)):
return str(v)
s = str(v)
return "'" + s.replace("\\\\", "\\\\\\\\").replace("'", "\\\\'") + "'"
esc = _esc
def _load_api_surface():
"""从 api.py / api_blueprint.py 收集 API_PATHS / PAGE_PATHS / CRUD_ALIASES。"""
api_paths, page_paths, crud_aliases = [], [], {}
for mod_name in ("pbl_blueprint.api", "pbl_blueprint.api_blueprint"):
try:
mod = __import__(mod_name, fromlist=["*"])
except Exception:
continue
for key, bucket in (("API_PATHS", api_paths), ("PAGE_PATHS", page_paths)):
val = getattr(mod, key, None)
if not val:
continue
for item in val:
if item not in bucket:
bucket.append(item)
alias = getattr(mod, "CRUD_ALIASES", None)
if isinstance(alias, dict):
for k, v in alias.items():
crud_aliases.setdefault(k, v)
return api_paths, page_paths, crud_aliases
try:
API_PATHS, PAGE_PATHS, CRUD_ALIASES = _load_api_surface()
except Exception: # pragma: no cover - 装配期兜底,不让 init 导入失败
API_PATHS, PAGE_PATHS, CRUD_ALIASES = [], [], {}
try:
from pbl_blueprint.m1b import ( # noqa: F401
get_env, set_env, reset_env, has_env, get_module_dbname,
PblError, ErrorCode, require_tenant, normalize_tenant,
write_audit, tenant_crud, crud_factory,
TABLES as M1B_TABLES, SUBOBJECT_TYPES as M1B_SUBOBJECT_TYPES,
TEMPLATE_SCOPES as M1B_TEMPLATE_SCOPES,
list_templates as m1b_list_templates,
get_template as m1b_get_template,
create_template as m1b_create_template,
publish_template as m1b_publish_template,
offline_template as m1b_offline_template,
instantiate_template as m1b_instantiate_template,
load_offline_templates as m1b_load_offline_templates,
resolve_ref as m1b_resolve_ref,
list_refs as m1b_list_refs,
assert_base_table_immutable as m1b_assert_base_table_immutable,
load_m1b,
)
M1B_AVAILABLE = True
except ImportError as _e: # pragma: no cover - 供给层缺失时不阻断 M1a 装配
M1B_AVAILABLE = False
M1B_IMPORT_ERROR = str(_e)
# <<< M1b-QC4 <<<
'''
# --------------------------------------------------------------------------
# #2d api_blueprint.py:504 from pbl_template.offline import load_offline_template
# --------------------------------------------------------------------------
OLD_OFFLINE = """ try:
from pbl_template.offline import load_offline_template
body = load_offline_template(template_id_or_code)
except Exception as e:
fail("PBL-NOTFOUND-0001",
"模板 %s 不存在且离线兜底失败: %s" % (template_id_or_code, e))"""
NEW_OFFLINE = """ # QC#2pbl_template.offline 属他模块pbl_template / M1b 模板平台)职责,
# 本模块不得硬依赖其私有子模块路径import 即 ImportError
# 改为:① 先用本模块 m1b 自包含离线兜底(内置模板包,冷启动即可用);
# ② 再尝试他模块可选增强(存在则用,不存在不报错)。
body = None
last_err = None
try:
from pbl_blueprint.m1b import load_offline_templates
for tpl_row in (load_offline_templates() or []):
if str(tpl_row.get("code") or tpl_row.get("template_code") or "") == str(template_id_or_code):
body = tpl_row.get("body") or tpl_row.get("tpl_json")
break
except Exception as e: # pragma: no cover
last_err = e
if body is None:
try:
from pbl_template.offline import load_offline_template # type: ignore
body = load_offline_template(template_id_or_code)
except Exception as e:
last_err = e
if body is None:
fail("PBL-NOTFOUND-0001",
"模板 %s 不存在且离线兜底失败: %s" % (template_id_or_code, last_err))"""
def main():
check_only = "--check" in sys.argv
# --- #1 m1b/__init__.py ---
if not check_only:
fix_m1b_init()
# --- #2a/#2b/#2c init.py ---
init_path = os.path.join(PKG, "init.py")
if not check_only:
_patch(init_path, OLD_SOR, NEW_SOR, required=False)
_patch(init_path, OLD_SEED, NEW_SEED, required=False)
txt = _read(init_path)
if MARK not in txt:
_write(init_path, txt.rstrip("\n") + "\n" + INIT_TAIL)
print("[patch] pbl_blueprint/init.py补 esc / API_PATHS / PAGE_PATHS / CRUD_ALIASES / m1b 转出)")
# --- #2d api_blueprint.py ---
if not check_only:
_patch(os.path.join(PKG, "api_blueprint.py"), OLD_OFFLINE, NEW_OFFLINE, required=False)
print("[ok] M1b QC#1/#2 补丁应用完成(幂等)")
return 0
if __name__ == "__main__":
sys.exit(main())