#!/usr/bin/env python3 # -*- coding: utf-8 -*- """第二处修复: 机器全局 PYTHONPATH 污染 .venv。 实测 (本机 PYTHONPATH="D:\\Program Files\\Python\\Lib\\site-packages;") : pip 在 venv 里看到系统 site-packages 的包, 判定 "Requirement already satisfied ... outside environment", 于是没有把传递依赖装进 .venv: venv 里缺 urllib3 / polars / pyyaml / jinja2 / python-dotenv 等, 并且 sys.path 里 PYTHONPATH 排在 venv site-packages **前面**, requests 被系统 2.33.0 顶掉了 pin 住的 2.34.2。 后果: 一旦换个没设 PYTHONPATH 的 shell (或别的机器用户), import requests/polars/yaml 直接 ModuleNotFoundError。 修复 (都不改变功能, 只要求 "跑的就是 .venv 里那一套"): D. install.ps1 / install.sh : 装依赖前摘掉 PYTHONPATH, 让 pip 把 requirements.txt 真正装进 .venv。 E. guanlan.py : 启动时把 PYTHONPATH 从 sys.path/b环境里摘掉 (check/serve/stop 与子进程都干净)。 幂等: 重复运行报 "已修过"。 """ from __future__ import annotations import pathlib import shutil import sys ROOT = pathlib.Path(r"F:\temp\guanlan-rudong-v2_0.2.0") GUANLAN = ROOT / "guanlan.py" INSTALL_PS1 = ROOT / "install.ps1" INSTALL_SH = ROOT / "install.sh" BAK = ".bak-pyfix" changed: list[str] = [] def fail(msg: str) -> None: print(" [X] " + msg) sys.exit(1) def backup(p: pathlib.Path) -> None: b = p.with_name(p.name + BAK) if not b.exists(): shutil.copy2(p, b) print(f" 备份 {p.name} -> {b.name}") def sub1(text: str, old: str, new: str, what: str) -> str: n = text.count(old) if n != 1: fail(f"{what}: 锚点命中 {n} 次 (期望 1)") return text.replace(old, new) # ------------------------------------------------------------------ D1. install.ps1 PS1_ANCHOR = '$ErrorActionPreference = "Stop"; $env:PYTHONUTF8 = "1"\n' PS1_INSERT = PS1_ANCHOR + ( '# 机器上若全局设了 PYTHONPATH (例如指向 D:\\Program Files\\Python\\Lib\\site-packages), pip 会把那里的包\n' '# 当成 "已满足", 于是不装进 .venv —— venv 里就缺 urllib3 / polars / pyyaml / jinja2 等传递依赖,\n' '# 换个没设 PYTHONPATH 的 shell 立刻 import 失败; 且 PYTHONPATH 排在 venv 之前会顶掉 pin 住的版本。\n' '# 先把环境变量摘掉, 让下面 pip 老老实实按 requirements.txt 装进 .venv。\n' 'Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue\n' ) def fix_install_ps1() -> None: raw = INSTALL_PS1.read_bytes() text = raw.decode("utf-8-sig").replace("\r\n", "\n") if "Remove-Item Env:PYTHONPATH" in text: print(" [=] install.ps1 已修过, 跳过") return backup(INSTALL_PS1) text = sub1(text, PS1_ANCHOR, PS1_INSERT, "install.ps1") INSTALL_PS1.write_bytes(text.replace("\r\n", "\n").replace("\n", "\r\n").encode("utf-8-sig")) changed.append("install.ps1: 装依赖前 Remove-Item Env:PYTHONPATH") print(" [OK] install.ps1 已修 (保持 UTF-8 BOM + CRLF)") # ------------------------------------------------------------------ D2. install.sh SH_ANCHOR = 'set -e\n' SH_INSERT = SH_ANCHOR + ( '# 同理: 全局 PYTHONPATH 会让 pip 误判依赖已满足而不装进 .venv (见 install.ps1 注释)\n' 'unset PYTHONPATH\n' ) def fix_install_sh() -> None: raw = INSTALL_SH.read_bytes() if raw.startswith(b"\xef\xbb\xbf"): fail("install.sh 不该有 BOM") text = raw.decode("utf-8") if "unset PYTHONPATH" in text: print(" [=] install.sh 已修过, 跳过") return backup(INSTALL_SH) text = sub1(text, SH_ANCHOR, SH_INSERT, "install.sh") INSTALL_SH.write_bytes(text.encode("utf-8")) # 保持 LF / 无 BOM changed.append("install.sh: 装依赖前 unset PYTHONPATH") print(" [OK] install.sh 已修 (保持 LF)") # ------------------------------------------------------------------ E. guanlan.py G_ANCHOR = 'WIN = os.name == "nt"\n' G_INSERT = G_ANCHOR + ''' def _hermetic(): """摘掉机器全局的 PYTHONPATH: 本项目依赖都装在 .venv 里, 而 PYTHONPATH 会被插到 sys.path 前面, 顶掉 venv 里 pin 住的版本 (实测 requests 2.33.0 顶掉 2.34.2), 还会让 pip 少装传递依赖。 子进程从 env() 继承的是摘干净之后的环境。""" for d in [x for x in os.environ.pop("PYTHONPATH", "").split(os.pathsep) if x]: while d in sys.path: sys.path.remove(d) _hermetic() ''' def fix_guanlan() -> None: raw = GUANLAN.read_bytes() text = raw.decode("utf-8") if "_hermetic()" in text: print(" [=] guanlan.py 已修过, 跳过") return backup(GUANLAN) text = sub1(text, G_ANCHOR, G_INSERT, "guanlan.py") compile(text, str(GUANLAN), "exec") GUANLAN.write_bytes(text.encode("utf-8")) changed.append("guanlan.py: 启动时摘掉 PYTHONPATH (自身与子进程都跑 .venv 的那套)") print(" [OK] guanlan.py 已修") def main() -> int: print(f"目标: {ROOT}") print("\n== D1 install.ps1"); fix_install_ps1() print("\n== D2 install.sh"); fix_install_sh() print("\n== E guanlan.py"); fix_guanlan() print("\n完成:") for c in changed or ["(无需改动, 之前已修)"]: print(" - " + c) return 0 if __name__ == "__main__": sys.exit(main())