# -*- coding: utf-8 -*- r'''修所有「__future__ 不在文件最前面」的文件,并加一道永久防线。 == 为什么反复踩 == 我这个会话里已经**第三次**被同一件事咬: Python 要求 `from __future__ import ...` 必须是文件里 **第一条语句**(docstring 之后)。而我的补丁脚本为了给文件 加 `import os` / `import re`,图省事写成: m = re.search(r"^(import |from )", text, re.M) text = text[:m.start()] + "import os\n" + text[m.start():] —— 第一个 import 恰好就是 `from __future__`, 于是新 import 被塞到它前面,整个文件语法错: SyntaxError: from __future__ imports must occur at the beginning of the file 症状还特别隐蔽: * 源码直接跑 -> 启动就崩 * **打包成 exe 之后** -> 只在走到那个模块时才崩, 用户看到的是"双击没反应" == 这个脚本做两件事 == 1. 全仓扫一遍,把所有"__future__ 不在最前面"的文件修好 (把 __future__ 提到所有 import 之前,docstring 之后) 2. 把这条检查加进回归套件(_tests/check_future_import.py), 以后任何脚本再犯,全量回归立刻红 —— 不会再等到打包之后。 ''' from __future__ import annotations import ast import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PATTERNS = ["_tests/*.py", "_build/*.py", "relay/*.py", "windows/*.py", "windows/lanremote/*.py", "installer/*.py", "android/*.py"] def fix_file(path: Path) -> str: """返回 'ok' / 'fixed' / 'error:...'""" text = path.read_text(encoding="utf-8") try: # ⚠ 必须用 compile():ast.parse 不做 __future__ 位置检查, # 用它探测会误判成"没问题"(这一轮就是这么被骗了好几次)。 compile(text, str(path), "exec") return "ok" except SyntaxError as exc: if "__future__" not in (exc.msg or ""): return "ok" # 别的语法错不归这个脚本管 lines = text.splitlines() # 找到 __future__ 那几行 fut_idx = [i for i, ln in enumerate(lines) if re.match(r"^\s*from __future__ import ", ln)] if not fut_idx: return "ok" # 把它之前的所有 import 行摘出来,插到它后面 first = fut_idx[0] before = lines[:first] # docstring(如果第一段是字符串)要留在最前面 keep = [] moved = [] i = 0 # 跳过 shebang / coding / 注释 / docstring while i < len(before): s = before[i].strip() if (not s or s.startswith("#") or s.startswith('"""') or s.startswith("'''") or s.startswith('r"""') or s.startswith("r'''")): keep.append(before[i]) # docstring 可能是多行,简单处理:找到闭合 if s.startswith(('"""', "'''", 'r"""', "r'''")): quote = '"""' if '"""' in s else "'''" if s.count(quote) < 2: i += 1 while i < len(before): keep.append(before[i]) if quote in before[i]: break i += 1 i += 1 continue if re.match(r"^\s*(import |from )", before[i]): moved.append(before[i]) i += 1 continue # 其它(比如赋值)就停在 docstring 之后、import 之前 —— 不动它 break rest_before = before[i:] fut_block = lines[first:] # 找到 __future__ 连续块结束 end = 0 while end < len(fut_block) and ( re.match(r"^\s*from __future__ import ", fut_block[end]) or not fut_block[end].strip()): end += 1 fut_lines = fut_block[:end] after = fut_block[end:] new_lines = keep + fut_lines + ([""] if moved else []) + moved \ + rest_before + after new_text = "\n".join(new_lines) + "\n" try: compile(new_text, str(path), "exec") except SyntaxError as exc: return "error: 修完还是语法错:第 %s 行 %s" % (exc.lineno, exc.msg) path.write_text(new_text, encoding="utf-8", newline="\n") return "fixed" def main() -> int: files = [] for pat in PATTERNS: files += sorted(ROOT.glob(pat)) fixed = [] errors = [] checked = 0 for f in files: if not f.is_file(): continue checked += 1 r = fix_file(f) if r == "fixed": fixed.append(str(f.relative_to(ROOT))) elif r != "ok": errors.append("%s: %s" % (f.relative_to(ROOT), r)) print(" 检查 %d 个文件" % checked) if fixed: print(" 修好 %d 个:" % len(fixed)) for x in fixed: print(" %s" % x) else: print(" ✓ 没有需要修的") if errors: print(" ⚠ %d 个修不了:" % len(errors)) for x in errors: print(" %s" % x) return 1 # 复查 still = [] for f in files: if not f.is_file(): continue try: compile(f.read_text(encoding="utf-8"), str(f), "exec") except SyntaxError as exc: if "__future__" in (exc.msg or ""): still.append(str(f.relative_to(ROOT))) if still: print(" ✗ 还有 %s" % still) return 1 print(" ✓ 全仓 __future__ 位置都正确") return 0 if __name__ == "__main__": sys.exit(main())