labull-framework 0.1.5__tar.gz → 0.1.7__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.
Files changed (49) hide show
  1. {labull_framework-0.1.5/labull_framework.egg-info → labull_framework-0.1.7}/PKG-INFO +1 -1
  2. labull_framework-0.1.7/labull_framework/magic_block/blockpackage.py +191 -0
  3. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/deepcheck.py +70 -21
  4. labull_framework-0.1.7/labull_framework/magic_block/devkit.py +658 -0
  5. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/dispatch.py +14 -0
  6. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/export.py +19 -2
  7. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/registry.py +33 -22
  8. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/service.py +1 -1
  9. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/validate.py +68 -49
  10. {labull_framework-0.1.5 → labull_framework-0.1.7/labull_framework.egg-info}/PKG-INFO +1 -1
  11. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework.egg-info/SOURCES.txt +1 -0
  12. {labull_framework-0.1.5 → labull_framework-0.1.7}/pyproject.toml +1 -1
  13. labull_framework-0.1.5/labull_framework/magic_block/devkit.py +0 -584
  14. {labull_framework-0.1.5 → labull_framework-0.1.7}/LICENSE +0 -0
  15. {labull_framework-0.1.5 → labull_framework-0.1.7}/MANIFEST.in +0 -0
  16. {labull_framework-0.1.5 → labull_framework-0.1.7}/README-pypi.md +0 -0
  17. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/__init__.py +0 -0
  18. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/admin.py +0 -0
  19. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/apps.py +0 -0
  20. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/auth/__init__.py +0 -0
  21. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/auth/backend.py +0 -0
  22. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/auth/bootstrap.py +0 -0
  23. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/auth/sso.py +0 -0
  24. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/auth/views.py +0 -0
  25. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/__init__.py +0 -0
  26. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/data.py +0 -0
  27. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/gitinfo.py +0 -0
  28. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/static/canvas.css +0 -0
  29. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/static/canvas.js +0 -0
  30. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/static/page.html +0 -0
  31. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/canvas/views.py +0 -0
  32. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/env.py +0 -0
  33. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/jsonio.py +0 -0
  34. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/__init__.py +0 -0
  35. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/health.py +0 -0
  36. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/sdk.py +0 -0
  37. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/magic_block/views.py +0 -0
  38. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/management/__init__.py +0 -0
  39. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/management/commands/__init__.py +0 -0
  40. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/management/commands/labull_contract.py +0 -0
  41. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/migrations/0001_initial.py +0 -0
  42. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/migrations/__init__.py +0 -0
  43. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/models.py +0 -0
  44. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/settings.py +0 -0
  45. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework/urls.py +0 -0
  46. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework.egg-info/dependency_links.txt +0 -0
  47. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework.egg-info/requires.txt +0 -0
  48. {labull_framework-0.1.5 → labull_framework-0.1.7}/labull_framework.egg-info/top_level.txt +0 -0
  49. {labull_framework-0.1.5 → labull_framework-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labull-framework
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Labull Framework: a pluggable Django app with Authing OIDC login and magic blocks
5
5
  Author: Labull Framework
6
6
  License-Expression: MIT
@@ -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
- tree = ast.parse(source, filename='backend_open.py')
282
- except SyntaxError:
283
- return _unavailable('正文有语法错,引用深检没跑(语法那一条已经报了)')
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
- try:
287
- _static_check(tree, module_name, apps)
288
- except Exception as exc:
289
- # 判据自己崩了不是这个块的错,但要说出来(否则就是"静默地什么都没验")。
290
- _STATIC.append(_issue('backend', '引用深检自己失败了(不是这个块的问题):%s: %s'
291
- % (type(exc).__name__, exc)))
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
- """把这份正文加载一次,回 `[(路由名, 绑好的 handler, 建类时的异常)]`。
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
- namespace = {'__name__': module_name}
518
- try:
519
- exec(compile(source, 'backend_open.py', 'exec'), namespace)
520
- except Exception:
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 = dict((key, value) for key, value in namespace.items()
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)