将 .py 和 .pyi 后缀名改为了 .vp 和 .vpi 后缀名

This commit is contained in:
2026-07-30 16:33:56 +08:00
parent f79c8ca643
commit cfc30d735c
322 changed files with 246 additions and 31772 deletions

View File

@@ -146,7 +146,7 @@ def main() -> None:
parser.add_argument('--clean', action='store_true', help='清理 output 和 temp 目录')
parser.add_argument('--run', action='store_true', help='编译成功后立即执行生成的可执行文件')
parser.add_argument('--rebuild-includes', action='store_true', help='删除 includes.binary 预编译缓存并重新编译所有 includes')
parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.pyi/.stub.ll/.doc.json')
parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.vpi/.stub.ll/.doc.json')
args: argparse.Namespace = parser.parse_args()

View File

@@ -119,7 +119,7 @@ class Phase1Generator:
for root, dirs, files in os.walk(includes_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py') or fname.endswith('.pyi'):
if fname.endswith('.vp') or fname.endswith('.py') or fname.endswith('.vpi') or fname.endswith('.pyi'):
src_path: str = os.path.join(root, fname)
rel_from_inc: str = os.path.relpath(src_path, includes_dir)
ModulePath: str = rel_from_inc.replace(os.sep, '.').replace('/', '.')
@@ -127,10 +127,10 @@ class Phase1Generator:
info: tuple[str, str, str] = (src_path, rel_from_inc, includes_dir)
include_file_map[module_name] = info
top_pkg: str = module_name.split('.')[0]
# __init__.py 是包入口,应优先作为 top_pkg 的代表
# __init__.vp/__init__.py 是包入口,应优先作为 top_pkg 的代表
# (参考 find_reachable_files_from_entries 的优先级逻辑),
# 否则若 os/path.py 先被遍历,'os' 会错误指向 path.py
# 而非 os/__init__.py,导致包入口 SHA1 未注册到 sha1_map
# 否则若 os/path.vp 先被遍历,'os' 会错误指向 path.vp
# 而非 os/__init__.vp导致包入口 SHA1 未注册到 sha1_map
if module_name == f"{top_pkg}.__init__":
include_file_map[top_pkg] = info
elif top_pkg not in include_file_map:
@@ -208,12 +208,15 @@ class Phase1Generator:
return needed_infos
def run(self) -> None:
"""扫描源目录,只处理入口文件可达的 .py 文件(导入遍历)"""
"""扫描源目录,只处理入口文件可达的 .vp/.py 文件(导入遍历)"""
py_files: list[str]
if self.entry_files:
py_files = list(self.entry_files)
else:
main_py: str = os.path.join(self.src_root, 'main.py')
main_py: str = os.path.join(self.src_root, 'main.vp')
if not os.path.exists(main_py):
# 回退到 main.py向后兼容
main_py = os.path.join(self.src_root, 'main.py')
if os.path.exists(main_py):
py_files = [main_py]
else:
@@ -221,7 +224,7 @@ class Phase1Generator:
for root, dirs, files in os.walk(self.src_root):
dirs[:] = [d for d in dirs if d not in ('__pycache__',)]
for file in files:
if file.endswith('.py'):
if file.endswith('.vp') or file.endswith('.py'):
py_files.append(os.path.join(root, file))
if not py_files:
@@ -286,7 +289,7 @@ class Phase1Generator:
for root, dirs, files in os.walk(includes_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py') or fname.endswith('.pyi'):
if fname.endswith('.vp') or fname.endswith('.py') or fname.endswith('.vpi') or fname.endswith('.pyi'):
src_path: str = os.path.join(root, fname)
rel_from_inc: str = os.path.relpath(src_path, includes_dir)
include_py_files.append((src_path, rel_from_inc, includes_dir))
@@ -315,14 +318,14 @@ class Phase1Generator:
self.include_py_map[top_module] = sha1
self.include_py_map[module_name] = sha1
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.pyi")
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.vpi")
if os.path.isfile(sig_path):
_vlog().info(f" 缓存命中: {rel_from_inc} -> {sha1}.pyi")
_vlog().info(f" 缓存命中: {rel_from_inc} -> {sha1}.vpi")
continue
# 全局缓存检查(不被 --clean 清除)
if _TryGlobalCache(sha1, 'pyi', sig_path):
_vlog().info(f" 缓存命中(全局): {rel_from_inc} -> {sha1}.pyi")
_vlog().info(f" 缓存命中(全局): {rel_from_inc} -> {sha1}.vpi")
continue
# 缓存未命中,需读取 content 生成 .pyi
@@ -332,7 +335,7 @@ class Phase1Generator:
with open(sig_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(sig_content)
_SaveToGlobalCache(sha1, 'pyi', sig_path)
_vlog().info(f" 生成签名: {rel_from_inc} -> {sha1}.pyi")
_vlog().info(f" 生成签名: {rel_from_inc} -> {sha1}.vpi")
except Exception as e:
_vlog().error(f"处理 include 文件失败 {rel_from_inc}: {e}")
@@ -341,7 +344,7 @@ class Phase1Generator:
if not os.path.isdir(self.temp_dir):
return typedef_map
for fname in os.listdir(self.temp_dir):
if not fname.endswith('.pyi'):
if not (fname.endswith('.vpi') or fname.endswith('.pyi')):
continue
pyi_path: str = os.path.join(self.temp_dir, fname)
try:
@@ -474,7 +477,7 @@ class Phase1Generator:
except Exception:
pass
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.pyi")
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.vpi")
if not os.path.isfile(sig_path):
_TryGlobalCache(sha1, 'pyi', sig_path)
with open(sig_path, 'r', encoding='utf-8') as f:
@@ -530,9 +533,9 @@ class Phase1Generator:
return
self.sha1_map[sha1] = rel_path.replace(os.sep, '/').replace(chr(92), '/')
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.pyi")
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.vpi")
if os.path.isfile(sig_path):
_vlog().info(f" -> {sha1}.pyi (缓存)")
_vlog().info(f" -> {sha1}.vpi (缓存)")
# 缓存命中时补全缺失的 .doc.json
doc_path: str = os.path.join(self.temp_dir, f"{sha1}.doc.json")
if not os.path.isfile(doc_path):
@@ -551,7 +554,7 @@ class Phase1Generator:
# 全局缓存检查
if _TryGlobalCache(sha1, 'pyi', sig_path):
_vlog().info(f" -> {sha1}.pyi (全局缓存)")
_vlog().info(f" -> {sha1}.vpi (全局缓存)")
doc_path: str = os.path.join(self.temp_dir, f"{sha1}.doc.json")
if not os.path.isfile(doc_path):
_TryGlobalCache(sha1, 'doc.json', doc_path)
@@ -566,7 +569,7 @@ class Phase1Generator:
with open(sig_path, 'w', encoding='utf-8', newline='\n') as f:
f.write(sig_content)
_SaveToGlobalCache(sha1, 'pyi', sig_path)
_vlog().info(f" -> {sha1}.pyi (签名)")
_vlog().info(f" -> {sha1}.vpi (签名)")
# 提取 docstring 写入 {sha1}.doc.json供 Phase2 跨模块 __doc__ 加载
docs: dict[str, str] = self._extract_docstrings(content)
@@ -588,7 +591,7 @@ class Phase1Generator:
_vlog().info(f" -> {sha1}.stub.ll (缓存)")
return
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.pyi")
sig_path: str = os.path.join(self.temp_dir, f"{sha1}.vpi")
with open(sig_path, 'r', encoding='utf-8') as f:
sig_content: str = f.read()
@@ -613,9 +616,9 @@ class Phase1Generator:
ctype_marker_names.add(attr_name)
valid_sha1_keys: set[str] = set(self.sha1_map.keys())
for fname in os.listdir(self.temp_dir):
if not fname.endswith('.pyi'):
if not (fname.endswith('.vpi') or fname.endswith('.pyi')):
continue
sha1_key: str = fname.replace('.pyi', '')
sha1_key: str = fname.replace('.vpi', '').replace('.pyi', '')
if sha1_key not in valid_sha1_keys:
continue
pyi_path: str = os.path.join(self.temp_dir, fname)

View File

@@ -217,19 +217,19 @@ class Phase2Translator:
fpath = os.path.join(self.temp_dir, fname)
if fname.startswith('_'):
continue
if fname.endswith('.pyi'):
if fname.endswith('.vpi') or fname.endswith('.pyi'):
sha1 = fname[:-4]
self.sig_files[sha1] = fpath
elif fname.endswith('.stub.ll'):
sha1 = fname[:-8]
self.stub_files[sha1] = fpath
# 从全局缓存补充 temp_dir 中缺失的 .pyi / .stub.ll
# 从全局缓存补充 temp_dir 中缺失的 .vpi / .stub.ll
if os.path.isdir(_GLOBAL_CACHE_DIR):
for fname in os.listdir(_GLOBAL_CACHE_DIR):
if fname.startswith('_'):
continue
if fname.endswith('.pyi'):
if fname.endswith('.vpi') or fname.endswith('.pyi'):
sha1 = fname[:-4]
if sha1 not in self.sig_files:
self.sig_files[sha1] = os.path.join(_GLOBAL_CACHE_DIR, fname)
@@ -256,7 +256,7 @@ class Phase2Translator:
def _build_struct_sha1_map(self) -> dict[str, str]:
struct_sha1_map = {}
valid_sha1_keys = set(self.sha1_map.keys())
# 收集 temp_dir 和全局缓存中的 .pyi 文件
# 收集 temp_dir 和全局缓存中的 .vpi/.pyi 文件
pyi_dirs: list[str] = [self.temp_dir]
if os.path.isdir(_GLOBAL_CACHE_DIR):
pyi_dirs.append(_GLOBAL_CACHE_DIR)
@@ -264,9 +264,9 @@ class Phase2Translator:
if not os.path.isdir(scan_dir):
continue
for fname in os.listdir(scan_dir):
if not fname.endswith('.pyi'):
if not (fname.endswith('.vpi') or fname.endswith('.pyi')):
continue
sha1_key = fname.replace('.pyi', '')
sha1_key = fname.replace('.vpi', '').replace('.pyi', '')
if sha1_key not in valid_sha1_keys:
continue
if sha1_key in struct_sha1_map:
@@ -287,7 +287,10 @@ class Phase2Translator:
if self.entry_files:
py_files = list(self.entry_files)
else:
main_py = os.path.join(self.src_root, 'main.py')
main_py = os.path.join(self.src_root, 'main.vp')
if not os.path.exists(main_py):
# 回退到 main.py向后兼容
main_py = os.path.join(self.src_root, 'main.py')
if os.path.exists(main_py):
py_files = [main_py]
else:
@@ -295,7 +298,7 @@ class Phase2Translator:
for root, dirs, files in os.walk(self.src_root):
dirs[:] = [d for d in dirs if d not in ('__pycache__',)]
for file in files:
if file.endswith('.py'):
if file.endswith('.vp') or file.endswith('.py'):
py_files.append(os.path.join(root, file))
if not py_files:
@@ -525,7 +528,10 @@ class Phase2Translator:
for inc_dir in self.include_dirs:
if os.path.isdir(inc_dir):
py_path = os.path.join(inc_dir, rel_path[len('includes/'):].replace(os.sep, '/'))
py_path = os.path.splitext(py_path)[0] + '.py'
py_path = os.path.splitext(py_path)[0] + '.vp'
if not os.path.isfile(py_path):
# 回退到 .py向后兼容
py_path = os.path.splitext(py_path)[0] + '.py'
if os.path.isfile(py_path):
with open(py_path, 'r', encoding='utf-8') as f:
py_sha1 = compute_sha1(f.read())
@@ -982,12 +988,12 @@ class Phase2Translator:
for includes_dir in self.include_dirs:
if os.path.isdir(includes_dir):
for pyi_file in os.listdir(includes_dir):
if pyi_file.endswith('.pyi'):
if pyi_file.endswith('.vpi') or pyi_file.endswith('.pyi'):
pyi_path = os.path.join(includes_dir, pyi_file)
module_name = os.path.splitext(pyi_file)[0]
trans.translator.SymbolTable.LoadModuleSymbols(pyi_path, module_name, lineno=0)
for py_file in os.listdir(includes_dir):
if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'):
if (py_file.endswith('.vp') or py_file.endswith('.py')) and (not py_file.startswith('_') or py_file == '_list.vp' or py_file == '_list.py' or py_file == '_dict.vp' or py_file == '_dict.py'):
module_name = os.path.splitext(py_file)[0]
sha1_key = self.include_py_map.get(module_name)
if sha1_key and sha1_key in self.sig_files:
@@ -1635,7 +1641,7 @@ class Phase2Translator:
for root, dirs, files in os.walk(includes_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if not fname.endswith('.py'):
if not (fname.endswith('.vp') or fname.endswith('.py')):
continue
py_path: str = os.path.join(root, fname)
code, tree = parse_python_file(py_path)
@@ -1691,7 +1697,7 @@ class Phase2Translator:
for root, dirs, files in os.walk(includes_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py'):
if fname.endswith('.vp') or fname.endswith('.py'):
py_index.add(os.path.normcase(os.path.join(root, fname)))
return py_index
@@ -1714,7 +1720,7 @@ class Phase2Translator:
for root, dirs, files in os.walk(module_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py'):
if fname.endswith('.vp') or fname.endswith('.py'):
py_path = os.path.join(root, fname)
if py_path not in self.extra_py_files:
self.extra_py_files.append(py_path)
@@ -1732,7 +1738,10 @@ class Phase2Translator:
self.extra_compile_files.append(src_path)
_vlog().info(f" [includes] 发现源文件: {os.path.relpath(src_path, includes_dir)}")
py_path = os.path.join(includes_dir, module_name + '.py')
py_path = os.path.join(includes_dir, module_name + '.vp')
if os.path.normcase(py_path) not in py_index:
# 回退到 .py向后兼容
py_path = os.path.join(includes_dir, module_name + '.py')
if os.path.normcase(py_path) in py_index:
self.extra_py_files.append(py_path)
_vlog().info(f" [includes] 发现 Python 源文件: {os.path.relpath(py_path, includes_dir)}")
@@ -1790,15 +1799,18 @@ class Phase2Translator:
py_index 非空时用 set 查询替代 os.path.isfileO(1) vs 24ms/次)。
"""
# 候选路径子包路径w32/win32base.py、顶层模块w32.py、子包 __init__.py
# 候选路径子包路径w32/win32base.vp/.py、顶层模块w32.vp/.py、子包 __init__.vp/__init__.py
candidates: list[str] = []
# 子包路径w32.win32base → w32/win32base.py
# 子包路径w32.win32base → w32/win32base.vp / .py
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep) + '.vp'))
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep) + '.py'))
# 子包 __init__.pyw32.win32base → w32/win32base/__init__.py
# 子包 __init__.vp/__init__.pyw32.win32base → w32/win32base/__init__.vp / .py
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep), '__init__.vp'))
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep), '__init__.py'))
# 顶层模块:取第一个组件 → w32.py
# 顶层模块:取第一个组件 → w32.vp / .py
top_module = module_name.split('.')[0]
if top_module != module_name:
candidates.append(os.path.join(includes_dir, top_module + '.vp'))
candidates.append(os.path.join(includes_dir, top_module + '.py'))
for py_path in candidates:
@@ -2423,7 +2435,7 @@ class Phase2Translator:
if not os.path.isdir(includes_dir):
continue
for filename in os.listdir(includes_dir):
if not filename.endswith('.py'):
if not (filename.endswith('.vp') or filename.endswith('.py')):
continue
src_path = os.path.join(includes_dir, filename)
try:
@@ -2533,7 +2545,7 @@ class Phase2Translator:
pass
# 1. 生成 .pyi 签名文件(如果还没有的话)
sig_path = os.path.join(self.temp_dir, f"{sha1}.pyi")
sig_path = os.path.join(self.temp_dir, f"{sha1}.vpi")
if not os.path.isfile(sig_path):
# 全局缓存检查(不被 --clean 清除)
_TryGlobalCache(sha1, 'pyi', sig_path)
@@ -2554,7 +2566,7 @@ class Phase2Translator:
# 注册到 sig_files 和 _source_module_sig_files
if os.path.isfile(sig_path):
self.sig_files[sha1] = sig_path
self.sha1_map[sha1] = f"includes/{mod_full.replace('.', '/')}.py"
self.sha1_map[sha1] = f"includes/{mod_full.replace('.', '/')}.vp"
if self._shared_source_module_sig_files is not None:
self._shared_source_module_sig_files[mod_full] = sig_path
self._shared_source_module_sig_files[module_name] = sig_path
@@ -2611,10 +2623,13 @@ class Phase2Translator:
for root, dirs, files in os.walk(module_dir):
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
for fname in files:
if fname.endswith('.py'):
if fname.endswith('.vp') or fname.endswith('.py'):
src_path = os.path.join(root, fname)
self._collect_inline_symbols(src_path)
py_path = os.path.join(includes_dir, module_name + '.py')
py_path = os.path.join(includes_dir, module_name + '.vp')
if not os.path.isfile(py_path):
# 回退到 .py向后兼容
py_path = os.path.join(includes_dir, module_name + '.py')
if os.path.isfile(py_path):
self._collect_inline_symbols(py_path)
for ef in self.extra_py_files:
@@ -2677,12 +2692,12 @@ class Phase2Translator:
for includes_dir in self.include_dirs:
if os.path.isdir(includes_dir):
for pyi_file in os.listdir(includes_dir):
if pyi_file.endswith('.pyi'):
if pyi_file.endswith('.vpi') or pyi_file.endswith('.pyi'):
pyi_path = os.path.join(includes_dir, pyi_file)
module_name = os.path.splitext(pyi_file)[0]
base_trans.translator.SymbolTable.LoadModuleSymbols(pyi_path, module_name, lineno=0)
for py_file in os.listdir(includes_dir):
if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'):
if (py_file.endswith('.vp') or py_file.endswith('.py')) and (not py_file.startswith('_') or py_file == '_list.vp' or py_file == '_list.py' or py_file == '_dict.vp' or py_file == '_dict.py'):
module_name = os.path.splitext(py_file)[0]
sha1_key = self.include_py_map.get(module_name)
if sha1_key and sha1_key in self.sig_files:
@@ -2775,7 +2790,7 @@ class Phase2Translator:
if not os.path.isdir(abs_includes):
abs_includes = includes_dir
for py_file in os.listdir(abs_includes):
if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'):
if (py_file.endswith('.vp') or py_file.endswith('.py')) and (not py_file.startswith('_') or py_file == '_list.vp' or py_file == '_list.py' or py_file == '_dict.vp' or py_file == '_dict.py'):
py_path = os.path.join(abs_includes, py_file)
try:
# 复用 parse_python_file 的 AST 缓存

View File

@@ -127,19 +127,19 @@ def clear_file_deps_cache() -> None:
def find_reachable_files_from_entries(src_root: str, entry_files: list[str]) -> set[str]:
"""从入口文件出发,找出所有可达的 .py 文件(通过导入关系)"""
"""从入口文件出发,找出所有可达的 .vp/.py 文件(通过导入关系)"""
all_files: dict[str, str] = {} # module_name -> filepath
for root, dirs, files in os.walk(src_root):
dirs[:] = [d for d in dirs if d not in ('__pycache__',)]
for file in files:
if file.endswith('.py'):
if file.endswith('.vp') or file.endswith('.py'):
filepath: str = os.path.join(root, file)
rel: str = os.path.relpath(filepath, src_root)
module: str = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
if filepath.endswith('__init__.py'):
if filepath.endswith('__init__.vp') or filepath.endswith('__init__.py'):
all_files[module] = filepath
parent_module: str = module.rsplit('.', 1)[0] if '.' in module else module
if parent_module not in all_files or not all_files[parent_module].endswith('__init__.py'):
if parent_module not in all_files or not (all_files[parent_module].endswith('__init__.vp') or all_files[parent_module].endswith('__init__.py')):
all_files[parent_module] = filepath
elif module not in all_files:
all_files[module] = filepath