fix(repo): 修正 Scripts 目录索引大小写并补齐未入库脚本

问题
- 索引中该目录记为小写 `scripts/`,工作区实为 `Scripts/`。因 core.ignorecase=true,
  本地 `git diff -- Scripts/bin/x.py` 会**静默返回空**(看似无改动),
  且在大小写敏感环境(Linux CI / Docker 挂载)会检出 `scripts/`,
  与全仓所有 `Scripts/...` 引用不匹配。
- 该目录此前 10 个脚本(check_python_version.py、perf_snapshot.py、
  install_collector_launchd.py、mysql_toggle.sh 等)从未入库,
  而 CI 第 31 行正调用 `Scripts/bin/check_python_version.py --strict` ——
  全新检出必然失败。

改动
- 索引路径统一为 `Scripts/`(工作区文件未移动,仅索引改名)。
- 补齐上述 10 个未跟踪脚本及其测试。

影响
- 修正前索引 35 条(全为小写路径),修正后 45 条。
This commit is contained in:
FinDataHub committed 2026-09-18 22:37:32 +08:00
1 parent 2e019cd118
commit 11d36d1c1e
46 files changed
+3089 -149

No files matched your search

+253
View File
@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
Python 版本一致性检查 CLI
==========================
FinDataHub 全项目统一锁定 Python 3.12(见仓库根目录 .python-version)。
本脚本校验「声明」与「实际」是否都已对齐,供本地自查与 CI 使用。
检查项
------
1. 所有 pyproject.toml 的 requires-python 是否接受目标版本
2. 各服务 .venv 的实际 Python 版本是否为目标版本
3. 代码里是否残留硬编码的其它 Python 版本(python3.13 / python@3.13 等)
4. Scripts/bin 下脚本的 shebang 是否指向目标版本(或为环境无关的 env 形式)
用法
----
python3 check_python_version.py # 人类可读报告
python3 check_python_version.py --json # JSON 输出
python3 check_python_version.py --strict # 警告也视为失败(CI 用)
退出码
------
0 全部通过(--strict 下警告也算通过,只有 error 才失败)
1 存在 error
"""
import argparse
import json
import re
import sys
from pathlib import Path
# 仓库根目录:本脚本位于 <root>/Scripts/bin/
ROOT = Path(__file__).resolve().parent.parent.parent
VERSION_FILE = ROOT / ".python-version"
# 参与版本检查的服务(相对 ROOT)
SERVICES = [
"Services/akshare_fetcher",
"Services/tushare_fetcher",
"Services/kuaixun-service",
"Services/findatahub-core",
"Services/findatahub-client",
]
# 需要扫描硬编码版本引用的目录与文件后缀
SCAN_DIRS = ["Scripts", "App/Sources", "Services"]
SCAN_SUFFIX = {".py", ".swift", ".sh", ".toml", ".yml", ".yaml"}
# 这些文件是历史记录/报告,不参与检查
SCAN_EXCLUDE = {
"DEVLOG.md",
"FinDataHub-性能与资源分析.html",
"FinDataHub-能耗优化分析.html",
}
def read_target_version():
if not VERSION_FILE.exists():
return None, f"缺少 {VERSION_FILE}"
v = VERSION_FILE.read_text(encoding="utf-8").strip()
if not re.fullmatch(r"3\.\d+", v):
return None, f"{VERSION_FILE} 内容不合法:{v!r}(应为形如 3.12)"
return v, None
def parse_requires_python(text):
"""从 pyproject.toml 文本里取出 requires-python 字符串"""
m = re.search(r'^requires-python\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE)
return m.group(1) if m else None
def version_accepts(spec, target):
"""
判断 requires-python 说明符是否接受目标版本。
支持形如:>=3.12、>=3.12,<3.13、>=3.10、==3.12.*
"""
def key(s):
return tuple(int(x) for x in s.split("."))
t = key(target)
ok = True
for clause in [c.strip() for c in spec.split(",") if c.strip()]:
m = re.fullmatch(r"(==|>=|<=|>|<|!=|~=)\s*([0-9.*]+)", clause)
if not m:
continue # 无法解析的子句跳过,不误报
op, ver = m.group(1), m.group(2)
if "*" in ver: # ==3.12.*
base = key(ver.replace(".*", "").rstrip("."))
ok = ok and (op == "==" and t[:len(base)] == base) or (op == "!=" and t[:len(base)] != base)
continue
v = key(ver)
if op == ">=":
ok = ok and t >= v
elif op == "<=":
ok = ok and t <= v
elif op == ">":
ok = ok and t > v
elif op == "<":
ok = ok and t < v
elif op == "==":
ok = ok and t == v
elif op == "!=":
ok = ok and t != v
return ok
def check_pyprojects(target):
issues = []
for svc in SERVICES:
p = ROOT / svc / "pyproject.toml"
if not p.exists():
issues.append(("warn", f"{svc}: 缺少 pyproject.toml"))
continue
spec = parse_requires_python(p.read_text(encoding="utf-8"))
if spec is None:
issues.append(("warn", f"{svc}: 未声明 requires-python"))
continue
if not version_accepts(spec, target):
issues.append(("error", f"{svc}: requires-python = {spec!r} 不接受 {target}"))
return issues
def check_venvs(target):
issues = []
for svc in SERVICES:
venv = ROOT / svc / ".venv"
if not venv.exists():
issues.append(("warn", f"{svc}: 无 .venv(未初始化)"))
continue
cfg = venv / "pyvenv.cfg"
actual = None
if cfg.exists():
txt = cfg.read_text(encoding="utf-8")
m = re.search(r"^version(?:_info)?\s*=\s*([0-9.]+)", txt, re.MULTILINE)
if m:
actual = m.group(1)
if actual is None:
# 回退:从 lib/python3.X 目录推断
lib = venv / "lib"
vers = sorted(d.name.replace("python", "")
for d in lib.iterdir()
if d.name.startswith("python") and d.is_dir()) if lib.exists() else []
actual = vers[-1] if vers else None
if actual is None:
issues.append(("warn", f"{svc}: 无法判定 venv 版本"))
elif not actual.startswith(target):
issues.append(("error", f"{svc}: venv 实际版本 {actual},目标 {target}"))
return issues
def check_hardcoded(target):
"""
扫描代码中残留的其它 python3.X 硬编码引用。
只匹配 `python3.XX` / `python@3.XX` 两种形式(不含 ruff 的 `py312`——
那是 linter 配置,不是版本引用)。注释行、本脚本自身、以及测试里
刻意构造的「旧版本残留」数据均不计入。
"""
issues = []
pat = re.compile(r"python3\.(\d+)|python@3\.(\d+)")
self_name = Path(__file__).name
for d in SCAN_DIRS:
base = ROOT / d
if not base.exists():
continue
for f in base.rglob("*"):
if not f.is_file() or f.suffix not in SCAN_SUFFIX:
continue
if any(x in str(f) for x in (".venv", "node_modules", "__pycache__", ".build")):
continue
if f.name in SCAN_EXCLUDE or f.name == self_name:
continue
try:
txt = f.read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
for i, raw in enumerate(txt.splitlines(), 1):
line = raw.strip()
# 跳过注释(Swift / Python / Shell / YAML 通用)
if line.startswith(("#", "//", "///", "*")):
continue
# 跳过动态取版本的行
if "PYTHON_TARGET_VERSION" in line or "findPython" in line:
continue
# 测试文件里刻意构造的旧版本数据不算问题
if "tests/" in str(f).replace("\\", "/") and "tmp_path" in txt:
continue
for m in pat.finditer(line):
found = f"3.{m.group(1) or m.group(2)}"
if found != target:
rel = f.relative_to(ROOT)
issues.append((
"warn",
f"{rel}:{i} 硬编码 python {found}(目标 {target})— {line[:70]}"
))
break
return issues
def main():
ap = argparse.ArgumentParser(description="检查全项目 Python 版本一致性")
ap.add_argument("--json", action="store_true", help="JSON 输出")
ap.add_argument("--strict", action="store_true", help="警告也计入失败")
args = ap.parse_args()
target, err = read_target_version()
if err:
print(f"error: {err}", file=sys.stderr)
return 1
issues = []
issues += check_pyprojects(target)
issues += check_venvs(target)
issues += check_hardcoded(target)
errors = [m for lv, m in issues if lv == "error"]
warns = [m for lv, m in issues if lv == "warn"]
if args.json:
print(json.dumps({
"target_version": target,
"errors": errors,
"warnings": warns,
"passed": not errors and (not args.strict or not warns),
}, ensure_ascii=False, indent=2))
else:
print(f"目标版本:Python {target}(来源 {VERSION_FILE.relative_to(ROOT)})\n")
if errors:
print(f"✗ 错误 {len(errors)} 项:")
for m in errors:
print(f" {m}")
if warns:
print(f"⚠ 警告 {len(warns)} 项:")
for m in warns:
print(f" {m}")
if not errors and not warns:
print("✓ 全部检查通过:声明、venv、硬编码引用均已对齐。")
elif not errors:
print(f"\n✓ 无致命错误({len(warns)} 项警告)")
if errors:
return 1
if args.strict and warns:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())