mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 14:34:18 +08:00
release: v1.29.2 — 可编辑安装失效友好报错(#58):入口守卫 _editable_guard 经 force-include 存活于失效态
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [1.29.2] — 2026-09-02
|
||||
|
||||
**可编辑安装失效时给出友好报错**([#58](https://github.com/handsomejustin/easy_tdx/discussions/58))——`pip install -e .` 会在 site-packages 写入 `_editable_impl_easy_tdx.pth`(内容为仓库 `src/` 绝对路径)。仓库目录被移动/重命名/重新 clone 或该文件丢失后,`easy-tdx` 只会抛出一句无法定位的 `No module named 'easy_tdx.cli'`:`easy_tdx` 本体因 site-packages 里的 `web/dist` 命名空间碎片仍可导入,报错极具误导性(实测复现,失效态 `easy_tdx.__path__` 只剩 site-packages 碎片路径)。
|
||||
|
||||
### 新增
|
||||
|
||||
- **入口守卫模块 `easy_tdx._editable_guard`**——控制台脚本入口从 `easy_tdx.cli:cli` 改为经 `main()` 转发(正常态行为完全不变);`easy_tdx.cli` 不可导入时打印中文修复指引(失效原因说明 + `python -c "import easy_tdx; print(easy_tdx.__path__)"` 排查命令 + 免重建 venv 的修复命令 `pip uninstall easy-tdx -y && pip install -e . --no-deps` + issue 链接),退出码 1。守卫文件经 `force-include` 同时复制进 site-packages 的碎片目录——失效态下 `src/` 代码全部不可达,唯有该副本可导入,这正是守卫能"在坏掉时还活着"的关键(`__init__.py` 途径不可行:失效态解析到的是无 `__init__.py` 的命名空间碎片)。
|
||||
- 单测 `tests/unit/test_editable_guard.py`(2 例):meta_path 阻断器模拟 `ModuleNotFoundError` → 断言提示内容与退出码;正常态断言转发调用 click 组。已本地实测三种安装形态:可编辑健康态 `--help` 正常、模拟失效态输出指引且退出码 1、wheel 安装态正常(hatchling 对包内文件 + force-include 同路径映射自动去重,guard 在 wheel 中恰好一份)。
|
||||
|
||||
## [1.29.1] — 2026-09-02
|
||||
|
||||
**中金所成交持仓排名采集(ccpm,独立数据源)**——散户能免费看到的**最接近"主力动向"的公开数据**:每个交易日收盘后约 16:15,中金所官网公布各期货品种「成交量 / 持买单量(多单)/ 持卖单量(空单)」各前 20 名期货公司会员排名。新增 `easy_tdx.ccpm` 模块并三端接入(CLI / Web API / WebUI),零第三方依赖(标准库 urllib)。
|
||||
|
||||
+7
-3
@@ -4,14 +4,16 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.29.1"
|
||||
version = "1.29.2"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["pandas>=2.0,<3", "tzdata>=2024.1", "click>=8.0,<9"]
|
||||
|
||||
[project.scripts]
|
||||
easy-tdx = "easy_tdx.cli:cli" # cli/__init__.py exposes the click group
|
||||
# 经由守卫转发:可编辑安装失效(仓库被移动/.pth 丢失)时打印修复指引(#58),
|
||||
# 机制见 _editable_guard.py 模块 docstring。
|
||||
easy-tdx = "easy_tdx._editable_guard:main"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov", "mypy>=1.9", "ruff>=0.4", "scipy>=1.10,<1.16", "httpx>=0.27", "duckdb>=1.0"]
|
||||
@@ -34,7 +36,9 @@ artifacts = [
|
||||
packages = ["src/easy_tdx"]
|
||||
# 前端 dist 映射到包内 easy_tdx/web/dist/。CI(ci.yml)、publish(publish.yml)
|
||||
# 和 release(release.yml)都会在 pip install / build 前先 npm run build。
|
||||
force-include = { "web-ui/dist" = "easy_tdx/web/dist" }
|
||||
# _editable_guard.py 一并映射进 site-packages 的碎片目录:可编辑安装失效时
|
||||
# src/ 代码全部不可达,唯有这份副本可导入,入口守卫才能打印修复指引(#58)。
|
||||
force-include = { "web-ui/dist" = "easy_tdx/web/dist", "src/easy_tdx/_editable_guard.py" = "easy_tdx/_editable_guard.py" }
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""easy-tdx 控制台入口守卫——可编辑安装失效时给出可操作的修复指引。
|
||||
|
||||
背景(#58):``pip install -e .``(hatchling 可编辑安装)会在 site-packages
|
||||
留下两样东西:
|
||||
|
||||
1. ``_editable_impl_easy_tdx.pth`` —— 内容是仓库 ``src/`` 目录的**绝对路径**,
|
||||
Python 靠它把 clone 的源码挂进 ``sys.path``;
|
||||
2. 一个真实的 ``easy_tdx/`` 目录(pyproject 的 ``force-include`` 把
|
||||
``web-ui/dist`` 前端产物映射进来),**没有 ``__init__.py``**,是 PEP 420
|
||||
命名空间包的一个"碎片"。
|
||||
|
||||
当仓库目录被移动/重命名/重新 clone、或 .pth 丢失时,``src/`` 从
|
||||
``sys.path`` 消失,但碎片目录还在:``import easy_tdx`` 依旧成功(解析为
|
||||
只剩静态资源的命名空间包),``import easy_tdx.cli`` 才失败——pip 生成的
|
||||
控制台脚本会把 ``ModuleNotFoundError: No module named 'easy_tdx.cli'``
|
||||
原样抛出,用户完全无法定位。
|
||||
|
||||
本模块的存活机制:它同时通过 ``force-include`` 被复制进 site-packages 的
|
||||
碎片目录。失效态下 ``src/`` 里的代码一行都不可达,唯独这份副本仍可导入,
|
||||
``easy-tdx`` 入口指向这里的 :func:`main`,就能在坏掉时打印修复指引。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
_HINT = """\
|
||||
easy-tdx: 无法导入 easy_tdx.cli —— 可编辑安装(pip install -e .)的注册信息很可能已失效。
|
||||
|
||||
可编辑安装在 site-packages 生成 _editable_impl_easy_tdx.pth,内容是仓库 src/ 目录的
|
||||
绝对路径。仓库目录被移动/重命名/重新 clone,或该文件丢失后,easy_tdx 只剩
|
||||
site-packages 里的静态资源碎片,源码全部不可达(所以 easy_tdx 本体仍能导入,
|
||||
偏偏 cli 导不进来)。
|
||||
|
||||
排查:
|
||||
python -c "import easy_tdx; print(easy_tdx.__path__)"
|
||||
# 正常应包含你仓库的 src/easy_tdx 绝对路径;只剩 site-packages 路径即失效
|
||||
|
||||
修复(无需删除重建虚拟环境):
|
||||
pip uninstall easy-tdx -y && pip install -e . --no-deps
|
||||
|
||||
若以上排查不符合你的情况,请带报错提 issue:
|
||||
https://github.com/handsomejustin/easy_tdx/issues
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""``easy-tdx`` 控制台入口:转发到 click 组;导入失败时打印修复指引。"""
|
||||
try:
|
||||
from .cli import cli
|
||||
except ModuleNotFoundError:
|
||||
print(_HINT, file=sys.stderr)
|
||||
raise SystemExit(1) from None
|
||||
cli()
|
||||
@@ -0,0 +1,55 @@
|
||||
"""_editable_guard 入口守卫测试:失效态提示 + 正常态转发(#58)。
|
||||
|
||||
失效态无法在测试里真实构造(需要破坏 site-packages 的 .pth),用
|
||||
meta_path 阻断器让 ``easy_tdx.cli`` 的导入抛 ``ModuleNotFoundError``。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
from easy_tdx import _editable_guard
|
||||
|
||||
|
||||
class _CliImportBlocker:
|
||||
"""meta_path finder:``easy_tdx.cli`` 一律抛 ModuleNotFoundError,其余放行。"""
|
||||
|
||||
def find_spec(self, fullname, path=None, target=None): # noqa: ARG002
|
||||
if fullname == "easy_tdx.cli":
|
||||
raise ModuleNotFoundError(f"No module named {fullname!r}", name=fullname)
|
||||
return None
|
||||
|
||||
|
||||
def test_broken_editable_prints_hint_and_exits(capsys, monkeypatch):
|
||||
"""easy_tdx.cli 不可导入时:打印修复指引并以退出码 1 结束。"""
|
||||
# easy_tdx.cli 可能已被 conftest/其他用例真实导入,先清缓存并阻断再次加载,
|
||||
# 否则 import 系统直接命中 sys.modules 短路,阻断器根本不会被咨询。
|
||||
monkeypatch.delitem(sys.modules, "easy_tdx.cli", raising=False)
|
||||
monkeypatch.setattr(sys, "meta_path", [_CliImportBlocker(), *sys.meta_path])
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
_editable_guard.main()
|
||||
|
||||
assert exc_info.value.code == 1
|
||||
err = capsys.readouterr().err
|
||||
assert "easy_tdx.cli" in err
|
||||
# 指引必须点名失效根源与两条修复命令,否则用户无从下手
|
||||
assert "_editable_impl_easy_tdx.pth" in err
|
||||
assert "pip uninstall easy-tdx -y && pip install -e . --no-deps" in err
|
||||
assert "easy_tdx.__path__" in err
|
||||
|
||||
|
||||
def test_healthy_path_forwards_to_click_group(monkeypatch):
|
||||
"""正常态:main() 直接转发调用 easy_tdx.cli.cli。"""
|
||||
called: list[bool] = []
|
||||
|
||||
stub = types.ModuleType("easy_tdx.cli")
|
||||
stub.cli = lambda: called.append(True) # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "easy_tdx.cli", stub)
|
||||
|
||||
_editable_guard.main()
|
||||
|
||||
assert called == [True]
|
||||
Reference in New Issue
Block a user