fix(deps): 声明 httpx[http2] / h2,修复干净环境缺包启动失败
utils/api_client.py 用 httpx.AsyncClient(http2=True) 发请求,需要 h2 包。 旧 requirements 只写了 httpx,未声明 http2 extra;开发机碰巧已装 h2 所以 未暴露,干净 release 环境启动即报错: Using http2=True, but the 'h2' package is not installed. - requirements.txt: httpx -> httpx[http2] - requirements.lock.txt: httpx[http2]==0.28.1,并显式锁 h2==4.1.0 - scripts/gen_requirements_lock.py: 升级为可解析 extras,自动锁定 extra 引入的传递依赖(如 httpx[http2] -> h2),避免以后重生 lock 丢失 h2; 通用于任意 extra Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
582f292114
commit
04cdf964af
@ -13,7 +13,8 @@ flask==2.3.3
|
||||
flask-socketio==5.3.6
|
||||
flask-cors==3.0.10
|
||||
werkzeug==2.3.7
|
||||
httpx==0.28.1
|
||||
httpx[http2]==0.28.1
|
||||
h2==4.1.0 # 由 httpx[http2] 引入
|
||||
openai==2.8.1
|
||||
cryptography==43.0.3
|
||||
pillow==9.3.0
|
||||
|
||||
@ -4,7 +4,7 @@ flask
|
||||
flask-socketio
|
||||
flask-cors
|
||||
werkzeug
|
||||
httpx
|
||||
httpx[http2]
|
||||
openai
|
||||
cryptography
|
||||
pillow
|
||||
|
||||
@ -35,41 +35,93 @@ _LOCK_HEADER = """\
|
||||
"""
|
||||
|
||||
|
||||
def _parse_requirement_names(text: str) -> list[str]:
|
||||
"""从 requirements.txt 提取包名(忽略注释、空行、已带版本约束的尾巴)。"""
|
||||
names: list[str] = []
|
||||
def _parse_requirements(text: str) -> list[tuple[str, list[str]]]:
|
||||
"""从 requirements.txt 提取 (包名, extras 列表)。
|
||||
|
||||
忽略注释/空行;去掉版本约束;保留 extras。
|
||||
例: httpx[http2]>=0.28 -> ("httpx", ["http2"]) ; flask -> ("flask", [])
|
||||
"""
|
||||
items: list[tuple[str, list[str]]] = []
|
||||
for raw in text.splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
# 去掉版本约束/extras,仅留包名: flask[async]>=2 -> flask
|
||||
name = re.split(r"[<>=!~\[ ;]", line, 1)[0].strip()
|
||||
if name:
|
||||
names.append(name)
|
||||
return names
|
||||
# 先剥掉版本约束/环境标记: name[extra]>=1 ; marker
|
||||
head = re.split(r"[<>=!~ ;]", line, 1)[0].strip()
|
||||
m = re.match(r"^([A-Za-z0-9._-]+)(?:\[([^\]]*)\])?$", head)
|
||||
if not m:
|
||||
continue
|
||||
name = m.group(1)
|
||||
extras = [e.strip() for e in (m.group(2) or "").split(",") if e.strip()]
|
||||
items.append((name, extras))
|
||||
return items
|
||||
|
||||
|
||||
def _extra_dependency_names(pkg: str, extras: list[str]) -> list[str]:
|
||||
"""解析某包在指定 extras 下引入的依赖包名(用于一并锁定其传递依赖)。
|
||||
|
||||
例: httpx + ["http2"] -> ["h2"]。仅返回包名,版本由调用方查实际安装值。
|
||||
无法解析时返回空列表(静默降级)。
|
||||
"""
|
||||
if not extras:
|
||||
return []
|
||||
try:
|
||||
requires = md.requires(pkg) or []
|
||||
except md.PackageNotFoundError:
|
||||
return []
|
||||
wanted = {e.lower() for e in extras}
|
||||
found: list[str] = []
|
||||
for req in requires:
|
||||
# 仅取声明了 extra == '<name>' 且匹配我们启用的 extra 的依赖
|
||||
if "extra ==" not in req:
|
||||
continue
|
||||
if not any(f"extra == '{e}'" in req or f'extra == "{e}"' in req for e in wanted):
|
||||
continue
|
||||
dep = re.split(r"[<>=!~ ;]", req.strip(), 1)[0].strip()
|
||||
dep = re.sub(r"\[[^\]]*\]", "", dep) # 去掉 extra 自身可能带的 []
|
||||
if dep and dep not in found:
|
||||
found.append(dep)
|
||||
return found
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not REQ_PATH.exists():
|
||||
print(f"找不到 {REQ_PATH}", file=sys.stderr)
|
||||
return 1
|
||||
names = _parse_requirement_names(REQ_PATH.read_text(encoding="utf-8"))
|
||||
if not names:
|
||||
items = _parse_requirements(REQ_PATH.read_text(encoding="utf-8"))
|
||||
if not items:
|
||||
print("requirements.txt 中没有可锁定的依赖。", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
lines: list[str] = []
|
||||
missing: list[str] = []
|
||||
for name in names:
|
||||
seen: set[str] = set() # 去重(extra 依赖可能与直接依赖重复)
|
||||
|
||||
for name, extras in items:
|
||||
spec = f"{name}[{','.join(extras)}]" if extras else name
|
||||
try:
|
||||
version = md.version(name)
|
||||
lines.append(f"{name}=={version}")
|
||||
lines.append(f"{spec}=={version}")
|
||||
except md.PackageNotFoundError:
|
||||
missing.append(name)
|
||||
lines.append(f"# {name} (未安装,无法锁定版本)")
|
||||
lines.append(f"# {spec} (未安装,无法锁定版本)")
|
||||
seen.add(name.lower())
|
||||
|
||||
# 一并锁定该包在启用 extras 下引入的依赖(如 httpx[http2] -> h2),
|
||||
# 否则干净环境装 lock 时不会拉到这些可选依赖。
|
||||
for dep in _extra_dependency_names(name, extras):
|
||||
if dep.lower() in seen:
|
||||
continue
|
||||
seen.add(dep.lower())
|
||||
try:
|
||||
dep_ver = md.version(dep)
|
||||
lines.append(f"{dep}=={dep_ver} # 由 {name}[{','.join(extras)}] 引入")
|
||||
except md.PackageNotFoundError:
|
||||
missing.append(dep)
|
||||
lines.append(f"# {dep} (由 {name} extra 引入,但未安装)")
|
||||
|
||||
LOCK_PATH.write_text(_LOCK_HEADER + "\n" + "\n".join(lines) + "\n", encoding="utf-8")
|
||||
print(f"已写出 {LOCK_PATH}({len(names)} 项依赖)")
|
||||
print(f"已写出 {LOCK_PATH}({len(items)} 项直接依赖 + extra 传递依赖)")
|
||||
for line in lines:
|
||||
print(" " + line)
|
||||
if missing:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user