labull-framework 0.1.5__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labull_framework-0.1.5/labull_framework.egg-info → labull_framework-0.1.6}/PKG-INFO +1 -1
- labull_framework-0.1.6/labull_framework/magic_block/blockpackage.py +191 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/deepcheck.py +70 -21
- labull_framework-0.1.6/labull_framework/magic_block/devkit.py +629 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/export.py +7 -1
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/registry.py +33 -22
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/service.py +1 -1
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/validate.py +68 -49
- {labull_framework-0.1.5 → labull_framework-0.1.6/labull_framework.egg-info}/PKG-INFO +1 -1
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework.egg-info/SOURCES.txt +1 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/pyproject.toml +1 -1
- labull_framework-0.1.5/labull_framework/magic_block/devkit.py +0 -584
- {labull_framework-0.1.5 → labull_framework-0.1.6}/LICENSE +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/MANIFEST.in +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/README-pypi.md +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/admin.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/apps.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/auth/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/auth/backend.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/auth/bootstrap.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/auth/sso.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/auth/views.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/data.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/gitinfo.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/static/canvas.css +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/static/canvas.js +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/static/page.html +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/canvas/views.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/env.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/jsonio.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/dispatch.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/health.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/sdk.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/magic_block/views.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/management/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/management/commands/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/management/commands/labull_contract.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/migrations/0001_initial.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/migrations/__init__.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/models.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/settings.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework/urls.py +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework.egg-info/dependency_links.txt +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework.egg-info/requires.txt +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/labull_framework.egg-info/top_level.txt +0 -0
- {labull_framework-0.1.5 → labull_framework-0.1.6}/setup.cfg +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""块包里的**每一个 `.py` 都当入口独立加载**(不止 `backend_open.py`)。
|
|
2
|
+
|
|
3
|
+
🔴 这是块包"能拆文件"的全部机制,只有两条:
|
|
4
|
+
① **包内每个 `.py` 各自独立执行**(宿主是 `exec(compile(正文), 文件名, 'exec')`);
|
|
5
|
+
② **文件之间不许互相引用** —— 谁都不 import 包里的另一个文件(`sibling_imports()` 查这一条)。
|
|
6
|
+
于是不需要模块注册、不需要依赖排序、不需要清理别名:每个文件就是一个独立模块,
|
|
7
|
+
与 `backend_open.py` 的待遇完全一样。
|
|
8
|
+
|
|
9
|
+
🔴 为什么要"不许互相引用"这条规矩:块后端的正文**在库里**,不在磁盘上、也不在 `sys.path` 上。
|
|
10
|
+
要想让 `from helper import x` 成立,就得先把包内文件注册成真模块(注册壳 → 依赖排序 →
|
|
11
|
+
别名共享 → 用完摘干净)——那套能跑通,但每一步都有坑(见 `probe-module-loader.py` 的血泪)。
|
|
12
|
+
而"各写各的"把这一整类复杂度消掉了:想分文件就分,但每个文件要能独立站住
|
|
13
|
+
(公共逻辑各写一份,或者放进**项目**里的公共模块 —— 那是普通 Python,随便 import)。
|
|
14
|
+
|
|
15
|
+
🔴 路由判据要扫**所有 `.py`**:`api.open.d.ts`、权限、404 报文都照它。
|
|
16
|
+
所以 `blockpackage.route_names_of()` 与运行期 `registry` 用的是同一份扫描。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
#: 块后端的入口文件(**契约**:这个名字不变;它同时是"主文件")。
|
|
26
|
+
ENTRY_FILE = 'backend_open.py'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def python_files(files: dict[str, str] | None) -> list[tuple[str, str]]:
|
|
30
|
+
"""包内全部 `.py`:`[(包内相对路径, 正文)]`,按路径排序(结果可复现)。"""
|
|
31
|
+
out: list[tuple[str, str]] = []
|
|
32
|
+
for name, body in sorted((files or {}).items()):
|
|
33
|
+
if isinstance(body, str) and str(name).endswith('.py'):
|
|
34
|
+
out.append((str(name), body))
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LoadedBlock:
|
|
40
|
+
"""一次加载的结果。
|
|
41
|
+
|
|
42
|
+
· `namespaces`:每个 `.py` 各一份命名空间(键是包内相对路径);
|
|
43
|
+
· `classes`:**包里所有文件**里定义的类(`类名 → 类`)—— 路由判据要它;
|
|
44
|
+
· `errors`:逐条"哪个文件怎么了"(作者一眼看得出是哪一份的问题)。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
namespaces: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
48
|
+
classes: dict[str, type] = field(default_factory=dict)
|
|
49
|
+
errors: list[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def ok(self) -> bool:
|
|
53
|
+
return not self.errors
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def namespace(self) -> dict[str, Any]:
|
|
57
|
+
"""主文件那份命名空间(老调用点用得多;主文件不在包里就是空的)。"""
|
|
58
|
+
return self.namespaces.get(ENTRY_FILE, {})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def load_block(files: dict[str, str], tag: str = '') -> LoadedBlock:
|
|
62
|
+
"""把包内每个 `.py` 各执行一次(互不影响),收集它们的类。
|
|
63
|
+
|
|
64
|
+
:param tag: 只用来给每份正文起个可读的 `__name__`(块 uuid 最好)。
|
|
65
|
+
🔴 **一个文件炸了不影响别的文件**:错误收进 `result.errors`(带文件名),其余照常加载 ——
|
|
66
|
+
否则"有个小笔误"会把整个块的路由一起弄没。
|
|
67
|
+
"""
|
|
68
|
+
result = LoadedBlock()
|
|
69
|
+
for relative, source in python_files(files):
|
|
70
|
+
namespace: dict[str, Any] = {'__name__': f'{relative[:-3].replace("/", ".") or tag or "block"}'}
|
|
71
|
+
try:
|
|
72
|
+
exec(compile(source, relative, 'exec'), namespace)
|
|
73
|
+
except BaseException as exc: # noqa: BLE001 —— 逐文件报,不让它带崩别的文件
|
|
74
|
+
result.errors.append(f'{relative} 加载失败:{type(exc).__name__}: {exc}')
|
|
75
|
+
continue
|
|
76
|
+
result.namespaces[relative] = namespace
|
|
77
|
+
for name, value in namespace.items():
|
|
78
|
+
# 只看**这个文件自己定义**的公开类(`import` 进来的名字也在命名空间里,但它们
|
|
79
|
+
# 属于别的文件 —— 那样同一个类会被扫两遍、还可能把项目里的类当成路由类)。
|
|
80
|
+
if isinstance(value, type) and not name.startswith('_') and value.__module__ == namespace['__name__']:
|
|
81
|
+
result.classes.setdefault(name, value)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ──────────────────────────── 静态那一半:不执行也能列出路由 / 查规矩 ────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _decorator_name(node: ast.AST) -> str:
|
|
89
|
+
"""装饰器叫什么(认 `@MagicBackendAPI(...)` 与 `@sdk.MagicBackendAPI(...)` 两种写法)。"""
|
|
90
|
+
target = node.func if isinstance(node, ast.Call) else node
|
|
91
|
+
if isinstance(target, ast.Name):
|
|
92
|
+
return target.id
|
|
93
|
+
if isinstance(target, ast.Attribute):
|
|
94
|
+
return target.attr
|
|
95
|
+
return ''
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def route_names_of(files: dict[str, str]) -> list[str]:
|
|
99
|
+
"""**不执行任何代码**,把包内所有 `.py` 里带装饰器的 `类名.方法名` 列出来。
|
|
100
|
+
|
|
101
|
+
🔴 与运行期那份路由表必须**说同一件事**(`api.open.d.ts`、权限、404 报文都照它)——
|
|
102
|
+
所以这里扫的是同样那批文件:**每一个 `.py`**。
|
|
103
|
+
🔴 只扫**顶层类**:`routes_of_namespace` 也是这个口径(类体里再嵌套的类不算路由)。
|
|
104
|
+
"""
|
|
105
|
+
out: list[str] = []
|
|
106
|
+
for relative, source in python_files(files):
|
|
107
|
+
try:
|
|
108
|
+
tree = ast.parse(source, filename=relative)
|
|
109
|
+
except SyntaxError:
|
|
110
|
+
continue # 语法那一条由 `check_backend` 报
|
|
111
|
+
for cls in [node for node in tree.body if isinstance(node, ast.ClassDef)]:
|
|
112
|
+
for node in cls.body:
|
|
113
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
114
|
+
continue
|
|
115
|
+
if any(_decorator_name(item) == 'MagicBackendAPI' for item in node.decorator_list):
|
|
116
|
+
route = f'{cls.name}.{node.name}'
|
|
117
|
+
if route not in out:
|
|
118
|
+
out.append(route)
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def route_meta_of(files: dict[str, str]) -> dict[str, str]:
|
|
123
|
+
"""`类名.方法名 → 声明的权限`(只看包内文件;这是 `api.open.d.ts` 与权限表的同一来源)。"""
|
|
124
|
+
out: dict[str, str] = {}
|
|
125
|
+
for relative, source in python_files(files):
|
|
126
|
+
try:
|
|
127
|
+
tree = ast.parse(source, filename=relative)
|
|
128
|
+
except SyntaxError:
|
|
129
|
+
continue
|
|
130
|
+
for cls in [node for node in tree.body if isinstance(node, ast.ClassDef)]:
|
|
131
|
+
for node in cls.body:
|
|
132
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
133
|
+
continue
|
|
134
|
+
if not any(_decorator_name(item) == 'MagicBackendAPI' for item in node.decorator_list):
|
|
135
|
+
continue
|
|
136
|
+
permission = ''
|
|
137
|
+
for decorator in node.decorator_list:
|
|
138
|
+
if _decorator_name(decorator) != 'MagicBackendAPI' or not isinstance(decorator, ast.Call):
|
|
139
|
+
continue
|
|
140
|
+
for keyword in decorator.keywords:
|
|
141
|
+
if keyword.arg == 'permission' and isinstance(keyword.value, ast.Constant):
|
|
142
|
+
permission = str(keyword.value.value or '')
|
|
143
|
+
out[f'{cls.name}.{node.name}'] = permission
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def sibling_imports(files: dict[str, str]) -> list[tuple[str, str, str]]:
|
|
148
|
+
"""包内文件**互相引用**的那些 import:`[(哪个文件, import 的名字, 它指向包里的哪个文件)]`。
|
|
149
|
+
|
|
150
|
+
🔴 这条是规矩("文件和文件之间不能互相引用"),不是风格建议:块后端的正文在库里、
|
|
151
|
+
不在磁盘上,跨文件 import 要靠一整套模块注册与依赖排序才成立,而那套东西脆弱得多。
|
|
152
|
+
与其让作者踩,不如**在保存 / 体检 / 提交前就报出来**。
|
|
153
|
+
🔴 判据只认"指向包内某个 `.py`"的 import:`orders.models`(项目里)与 `json`(标准库)不在此列。
|
|
154
|
+
⚠️ 只按**名字**判(不执行),所以 `import helper` 与 `from helper import x` 都算。
|
|
155
|
+
"""
|
|
156
|
+
#: 包内所有 `.py` 的"模块候选名":`helper.py` ⇒ `helper`,`lib/util.py` ⇒ `lib.util`。
|
|
157
|
+
known: dict[str, str] = {}
|
|
158
|
+
for relative, _body in python_files(files):
|
|
159
|
+
stem = relative[:-3]
|
|
160
|
+
if stem.endswith('/__init__'):
|
|
161
|
+
stem = stem[: -len('/__init__')]
|
|
162
|
+
if not stem:
|
|
163
|
+
continue
|
|
164
|
+
known[stem] = relative
|
|
165
|
+
known[stem.split('/')[-1]] = relative
|
|
166
|
+
|
|
167
|
+
out: list[tuple[str, str, str]] = []
|
|
168
|
+
for relative, source in python_files(files):
|
|
169
|
+
try:
|
|
170
|
+
tree = ast.parse(source, filename=relative)
|
|
171
|
+
except SyntaxError:
|
|
172
|
+
continue
|
|
173
|
+
for node in ast.walk(tree):
|
|
174
|
+
if isinstance(node, ast.ImportFrom):
|
|
175
|
+
if node.level:
|
|
176
|
+
continue # 相对 import(`from . import x`)在块里本来就不成立,另有判据
|
|
177
|
+
candidates = [node.module or '']
|
|
178
|
+
candidates.extend(f'{node.module}.{alias.name}' if node.module else alias.name
|
|
179
|
+
for alias in node.names if alias.name != '*')
|
|
180
|
+
elif isinstance(node, ast.Import):
|
|
181
|
+
candidates = [alias.name for alias in node.names]
|
|
182
|
+
else:
|
|
183
|
+
continue
|
|
184
|
+
for name in candidates:
|
|
185
|
+
hit = known.get(name) or known.get(name.split('.')[0])
|
|
186
|
+
if hit is not None and hit != relative:
|
|
187
|
+
# 同一个 import 会给出两个候选(`helper` 与 `helper.VALUE`)⇒ 按目标去重,
|
|
188
|
+
# 不然作者会看到同一件事报两遍。
|
|
189
|
+
if not any(item[0] == relative and item[2] == hit for item in out):
|
|
190
|
+
out.append((relative, name, hit))
|
|
191
|
+
return out
|
|
@@ -277,24 +277,57 @@ def main():
|
|
|
277
277
|
return _unavailable('Django 设置没配好,这一块这一次没验')
|
|
278
278
|
|
|
279
279
|
source = json.loads(os.environ['LABULL_DEEP_SOURCE'])
|
|
280
|
+
# 🔴 整包一起验(块包可以拆文件):`LABULL_DEEP_FILES` 是 `{包内相对路径: 正文}`。
|
|
281
|
+
# ⚠️ **子进程里没有父进程模块级的那些名字、也没有 wrapper 里的 `_parent`**:
|
|
282
|
+
# 要什么就自己 import(这一段顶上只 import 了 `...sdk` 那几个)。
|
|
283
|
+
# 写一个不存在的名字会让整层深检**静默失效**(`NameError` 被 `_unavailable` 吞成一句说明,
|
|
284
|
+
# 保存于是全放行)—— 这里真踩过,而且是"改动前能报的错、改动后不报了"这种最难发现的形态。
|
|
285
|
+
from labull_framework.magic_block import blockpackage as _blockpackage
|
|
286
|
+
_main_file = os.environ.get('LABULL_DEEP_MAIN_FILE') or 'backend_open.py'
|
|
280
287
|
try:
|
|
281
|
-
|
|
282
|
-
except
|
|
283
|
-
|
|
288
|
+
raw_package = json.loads(os.environ.get('LABULL_DEEP_FILES') or '{}')
|
|
289
|
+
except ValueError:
|
|
290
|
+
raw_package = {}
|
|
291
|
+
package: dict[str, str] = (
|
|
292
|
+
{str(name): str(body) for name, body in raw_package.items()} if isinstance(raw_package, dict) else {}
|
|
293
|
+
)
|
|
294
|
+
package.setdefault(_main_file, source)
|
|
295
|
+
# 🔴 包内文件**互相引用**要先报出来(规矩,不是风格):块后端的正文在库里、不在磁盘上,
|
|
296
|
+
# `from helper import x` 那种写法在块里**不成立**(平台按"每个 .py 各自独立加载"来跑)。
|
|
297
|
+
# 不报的话症状是运行期 `ModuleNotFoundError`,而那时人已经不知道该看哪儿了。
|
|
298
|
+
for where, name, target in _blockpackage.sibling_imports(package):
|
|
299
|
+
_STATIC.append(_issue(
|
|
300
|
+
'backend',
|
|
301
|
+
f'{where} 里 import 了包内的 {name}(就是 {target}):**块里文件之间不能互相引用** —— '
|
|
302
|
+
f'平台把每个 .py 各自独立加载(谁都不认识谁)。要么把那段逻辑复制到这份文件里,'
|
|
303
|
+
f'要么放进**项目**里的公共模块再 import(那是普通 Python,随便用)。',
|
|
304
|
+
))
|
|
284
305
|
|
|
285
306
|
module_name = str(os.environ.get('LABULL_DEEP_MODULE') or 'backend_open')
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
307
|
+
|
|
308
|
+
# 🔴 **每一份 `.py` 都静态过一遍**(不只主文件):拆出去的文件里 `Order.no` 这种写法
|
|
309
|
+
# 同样要在保存时现形。`_imported_models` 只对"导入了模型"的文件起作用,
|
|
310
|
+
# 其余文件白跑一遍也不报 —— 所以这里不必挑文件。
|
|
311
|
+
for relative, body in sorted(package.items()):
|
|
312
|
+
if not relative.endswith('.py'):
|
|
313
|
+
continue
|
|
314
|
+
try:
|
|
315
|
+
tree = ast.parse(body, filename=relative)
|
|
316
|
+
except SyntaxError:
|
|
317
|
+
continue # 语法那一条已经由上层报了
|
|
318
|
+
try:
|
|
319
|
+
_static_check(tree, module_name, apps)
|
|
320
|
+
except Exception as exc:
|
|
321
|
+
# 判据自己崩了不是这个块的错,但要说出来(否则就是"静默地什么都没验")。
|
|
322
|
+
_STATIC.append(_issue('backend', '引用深检自己失败了(不是这个块的问题):%s: %s'
|
|
323
|
+
% (type(exc).__name__, exc)))
|
|
324
|
+
break
|
|
292
325
|
|
|
293
326
|
# 🔴 试跑那一层会用到的几个函数(撒样例行 / 取主键 / 错该算谁头上)是**从外面注入**的
|
|
294
327
|
# (见 `deep_check()` 里那个 wrapper):它们只有一份实现(本模块模块级那几个),
|
|
295
328
|
# 这里**不许再抄一份** —— 抄一份的下场是"保存时说合法、体检时说非法"。
|
|
296
329
|
if str(os.environ.get('LABULL_DEEP_PROBE') or '') == '1':
|
|
297
|
-
probe(source, module_name, apps)
|
|
330
|
+
probe(source, module_name, apps, package)
|
|
298
331
|
return _result()
|
|
299
332
|
|
|
300
333
|
|
|
@@ -444,7 +477,7 @@ def _label(value):
|
|
|
444
477
|
# 是块自己的代码炸了"时才归给块。
|
|
445
478
|
|
|
446
479
|
|
|
447
|
-
def probe(source, module_name, apps):
|
|
480
|
+
def probe(source, module_name, apps, package=None):
|
|
448
481
|
# 🔴 **换库必须在加载那份正文之前**(真踩过):块正文一跑,它 import 的东西就可能已经把
|
|
449
482
|
# `default` 那条连接建起来了(Django 的 ORM 是懒连接,但 import 期就可能碰它),
|
|
450
483
|
# 之后再换库就会留下一条"连着真库"的连接 —— 建表于是撞在真库上、探针整层静默失效。
|
|
@@ -452,9 +485,9 @@ def probe(source, module_name, apps):
|
|
|
452
485
|
for note in _mirror_database():
|
|
453
486
|
_NOTES.append(note)
|
|
454
487
|
return
|
|
455
|
-
handlers = _handlers(source, module_name)
|
|
488
|
+
handlers = _handlers(source, module_name, package, _main_file)
|
|
456
489
|
if handlers is None:
|
|
457
|
-
_NOTES.append('
|
|
490
|
+
_NOTES.append('这一包加载不起来,试跑那一层没跑(静态那几条照样算)')
|
|
458
491
|
return
|
|
459
492
|
if not handlers:
|
|
460
493
|
return
|
|
@@ -505,22 +538,26 @@ def _mirror_database():
|
|
|
505
538
|
return []
|
|
506
539
|
|
|
507
540
|
|
|
508
|
-
def _handlers(source, module_name):
|
|
509
|
-
"""
|
|
541
|
+
def _handlers(source, module_name, package=None, main_file='backend_open.py'):
|
|
542
|
+
"""把这一**包**加载一次,回 `[(路由名, 绑好的 handler, 建类时的异常)]`。
|
|
510
543
|
|
|
544
|
+
:param package: `{包内相对路径: 正文}`(包内每个 `.py` 都会被加载一次)。
|
|
545
|
+
:param main_file: 主文件的相对路径(**不许在这里写死**:本函数会被编译进子进程执行,
|
|
546
|
+
而父进程模块级的那个常量在子进程里不存在 —— 写死/引用都会 `NameError`,
|
|
547
|
+
整层深检于是静默失效,真踩过)。
|
|
511
548
|
⚠️ 由调用方保证**在这之前已经换过库**(顺序是判据,见 `probe()`)。
|
|
512
549
|
"""
|
|
513
550
|
try:
|
|
551
|
+
from labull_framework.magic_block import blockpackage
|
|
514
552
|
from labull_framework.magic_block.sdk import routes_of_namespace
|
|
515
553
|
except Exception:
|
|
516
554
|
return None
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
555
|
+
files = {str(name): str(body) for name, body in (package or {}).items()}
|
|
556
|
+
files.setdefault(main_file, source)
|
|
557
|
+
loaded = blockpackage.load_block(files, tag=module_name or 'probe')
|
|
558
|
+
if loaded.errors:
|
|
521
559
|
return None
|
|
522
|
-
classes =
|
|
523
|
-
if isinstance(value, type) and not key.startswith('_'))
|
|
560
|
+
classes = loaded.classes
|
|
524
561
|
try:
|
|
525
562
|
meta = routes_of_namespace(classes)
|
|
526
563
|
except Exception:
|
|
@@ -680,6 +717,9 @@ def deep_check(
|
|
|
680
717
|
'for _name in ("_seed_rows", "_filled", "_blamable", "_needs_params", "_failure_message",\n'
|
|
681
718
|
' "_route_models"):\n'
|
|
682
719
|
' space[_name] = getattr(_parent, _name)\n'
|
|
720
|
+
# ⚠️ `_main_file` 也要注入:`probe()` 是**父进程的函数**、在子进程的命名空间里执行,
|
|
721
|
+
# 所以它只能读子进程的全局(先前漏了这一条 ⇒ `NameError` ⇒ 试跑那一层静默不跑)。
|
|
722
|
+
'space["_main_file"] = os.environ.get("LABULL_DEEP_MAIN_FILE") or "backend_open.py"\n'
|
|
683
723
|
'exec(compile(os.environ["LABULL_DEEP_CHECK"], "labull_deepcheck.py", "exec"), space)\n'
|
|
684
724
|
'print("LABULL_DEEP_RESULT" + json.dumps(space["_RESULT_"], ensure_ascii=False))\n'
|
|
685
725
|
)
|
|
@@ -690,6 +730,15 @@ def deep_check(
|
|
|
690
730
|
env['LABULL_DEEP_PATH'] = env['PYTHONPATH']
|
|
691
731
|
env['LABULL_DEEP_CHECK'] = _CHECK
|
|
692
732
|
env['LABULL_DEEP_SOURCE'] = json.dumps(src, ensure_ascii=False)
|
|
733
|
+
# 🔴 主文件名由父进程告诉子进程(子进程里**没有**父进程模块级的 `BLOCK_BACKEND_FILE`)。
|
|
734
|
+
env['LABULL_DEEP_MAIN_FILE'] = BLOCK_BACKEND_FILE
|
|
735
|
+
# 🔴 **整包**一起递(不只主文件):拆过文件的块要靠兄弟文件才加载得起来,
|
|
736
|
+
# 否则保存时说"加载不起来",而它明明在包里。
|
|
737
|
+
env['LABULL_DEEP_FILES'] = json.dumps(
|
|
738
|
+
{name: body for name, body in (files or {}).items()
|
|
739
|
+
if isinstance(body, str) and name.endswith('.py')},
|
|
740
|
+
ensure_ascii=False,
|
|
741
|
+
)
|
|
693
742
|
env['LABULL_DEEP_PROBE'] = '1' if probe else '0'
|
|
694
743
|
env['LABULL_DEEP_PROBE_LIMIT'] = str(PROBE_LIMIT)
|
|
695
744
|
params = (files or {}).get(PROBE_PARAMS_FILE)
|