From 97a2f95066192e800dbac3ee15acc9aebac84800 Mon Sep 17 00:00:00 2001 From: guzhujushi Date: Fri, 2 Oct 2026 13:36:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(env):=20=E9=92=89=E6=AD=BB=20pyright=20?= =?UTF-8?q?=E7=9A=84=E5=88=86=E6=9E=90=E7=9B=AE=E6=A0=87=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=8E=89"=E6=97=A0=E6=B3=95=E8=A7=A3=E6=9E=90=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=20numpy/pandas"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:编辑器 / pyright 满屏 reportMissingImports,但 .venv 与 conda 环境里 import numpy, pandas 都正常,pytest 也全过。 根因两层: 1. pyright 没配 venvPath / pythonPath 时会去 PATH 上找 python。本机 PATH 上 唯一的 python 是 agent-tools 的 shim(Codex 运行时,Python 3.14),没有科学栈。 pyright --verbose 显示它选了 3.14.6,Search paths 里一个 Anaconda 路径都没有。 注意持久化的 User/Machine PATH 并没有这一项,是 Codex 进程临时注入的——所以 "我的终端没问题"和"工具有问题"可以同时成立。 2. .venv 是 --system-site-packages 建的,三方包在 D:\anaconda\Lib\site-packages, 而 pyright / Pylance 不认 pyvenv.cfg 的 include-system-site-packages, 只去 .venv\Lib\site-packages 找(那里只有 pip)。 pyright 不读 pyvenv.cfg,但**认 .pth**。 改动: - pyproject.toml [tool.pyright]:venvPath = "." / venv = ".venv" / pythonVersion = "3.13",并注释了为什么必须写 - scripts/setup-venv.ps1:新增第 [2/4] 步,自动往 .venv 写 _mcm_base_site_packages.pth 指向该机 base 的 site-packages,换机重建即可 - tests/test_data.py:3 处 isna().all() 断言外面套 bool()(它的类型是 Series | bool,直接当条件 pyright 报"条件操作数无效"); 2 处故意传错类型的用例加 type: ignore[arg-type] 结果:pyright 17 errors -> 0 errors / 0 warnings;pytest 两套环境均 40 passed, 44 skipped;ruff All checks passed;setup-venv.ps1 通过 AST 解析 (纯 ASCII + LF)。 文档:docs/04-environment.md 新增坑 7(含 pyright --verbose 诊断口诀与 VS Code / PyCharm 的操作步骤)、STATUS 决策 29、CHANGELOG。 --- CHANGELOG.md | 10 +++++++ STATUS.md | 6 +++- docs/04-environment.md | 64 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 7 +++++ scripts/setup-venv.ps1 | 17 +++++++++-- tests/test_data.py | 12 ++++---- 6 files changed, 107 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79806b2..36f25c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,16 @@ - `docs/dev/personal-pitfalls.md`:个人易错点清单,**只追加、不通读** ### Fixed +- 编辑器 / pyright 报"无法解析导入 numpy、pandas":分析器会去 PATH 上找 python, + 本机 PATH 上只有 Codex 运行时的 3.14 shim(无科学栈),且 pyright 不认 + `pyvenv.cfg` 的 `include-system-site-packages`。 + 改为 `[tool.pyright]` 指定 `venvPath = "."` / `venv = ".venv"` / + `pythonVersion = "3.13"`,并让 `scripts/setup-venv.ps1` 往 `.venv` 里写一个 + 指向 base site-packages 的 `.pth`。pyright 17 errors → **0**。 + 详见 `docs/04-environment.md` 坑 7。 +- `tests/test_data.py`:`df["b"].isna().all()` 的类型是 `Series | bool`, + 直接当断言条件 pyright 会报"条件操作数无效",外面套 `bool()`; + 两条故意传错类型的用例加 `# type: ignore[arg-type]`。 - `data.save_table` 的后缀校验排在 `path.parent.mkdir(...)` **之后**,传 `.txt` 会先留下一个 空目录再抛 `ValueError`。改为**先校验、后副作用**,并加回归测试。 - `data/io.py` 里 `__all__` 与下一个 `def` 之间只有 1 个空行(PEP 8 要 2 个)。 diff --git a/STATUS.md b/STATUS.md index b56b582..2fb7da5 100644 --- a/STATUS.md +++ b/STATUS.md @@ -104,7 +104,10 @@ | `scripts/doctor.ps1` | 跑通 | — | | SALib / pymoo / pyright | 缺失 | 已装 | -- base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致) +- `pyright`(分析目标 = 工作区 `.venv`):**0 errors, 0 warnings** + (修配置之前是 17 errors,全是"无法解析导入 numpy / pandas",见决策 29 与 + `docs/04-environment.md` 坑 7) +- - base 亦已复核:numpy 2.4.6 / scipy 1.18.1 / pandas 3.0.6(与两套环境一致) - skip 的都是等实现的算法行为测试;通过的 39 条 = 20 条公开 API 契约测试 (`tests/test_api_surface.py`)+ 11 条 `core` 行为测试(`tests/test_core.py`) + 9 条 `data` 行为测试(`tests/test_data.py`) @@ -159,6 +162,7 @@ | 24 | `get_rng(None)` **复用同一个模块级生成器**(会推进),不每次新建 | **用户本人提出**,取代 Codex 原来"每次新建 `default_rng(全局种子)`"的写法——那样两个都省略 `random_state` 的函数会抽到完全相同的数。共享单例既保证重跑一致,又让流自然推进 | | 25 | `src/` 下的实现默认由**用户本人**写,Codex 只改 `tests/` 与 `docs/` | 目标是"一边学一边自己写"。曾出现 Codex 直接落盘 `random.py`、与用户编辑器缓冲区冲突导致保存失败。需要代写时用户会明说 | | 26 | 未实现的算法用**逐条** `@pytest.mark.skip`,禁用模块级 `pytestmark` | 模块级 skip 会把同文件里**已经写好**的函数一起跳过。本次 `test_data.py` 就这样让 `load_table` 零覆盖,直到有人手动数才被发现 | +| 29 | 静态分析器钉死到工作区 `.venv`,并把 base 的 site-packages 用 `.pth` 链进去 | pyright / Pylance **不认** `pyvenv.cfg` 的 `include-system-site-packages`,`.venv` 自己只有 `pip`;不钉的话它们会去 PATH 上找 python,本机 PATH 上那个是 Codex 运行时的 3.14 shim,没有科学栈,于是报"无法解析导入"。`.pth` 是唯一被 pyright 认的转发手段 | | 28 | 不启用 ruff 的 preview 规则集 | `E302`(函数间要 2 个空行)在 ruff 0.16 里是 preview 规则,`select = ["E"]` 默认**不包含**它——所以少一个空行 ruff 照样报 "All checks passed"。行数空行得自己盯,别指望 lint | | 27 | base 里的 zstandard 残骸目录**改名禁用**,不删除、不重装 | 删掉不可逆;`pip install zstandard` 会给 base 塞一个 conda 记录里根本没有的包,下次 `conda update --all` 又要重新判断。改名后 `import` 变回正常 `ModuleNotFoundError`,pandas 自动跳过 | diff --git a/docs/04-environment.md b/docs/04-environment.md index df82a50..d32e6b6 100644 --- a/docs/04-environment.md +++ b/docs/04-environment.md @@ -164,6 +164,70 @@ Move-Item -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' ` 改名后 `import zstandard` 变成正常的 `ModuleNotFoundError`,pandas 会安静地跳过它。 本次已按此处理,`.venv` 的 `pd.read_csv` 恢复正常。 +**坑 7 —— 编辑器报"无法解析导入 numpy / pandas",但解释器明明是对的。** + +现象:`.venv` 与 conda 环境里 `import numpy, pandas` 都正常,pytest 也全过, +只有编辑器/pyright 满屏 `无法解析导入"numpy"`。 + +原因是**静态分析器自己挑了一个 python**,跟你运行代码用的那个没关系。本机上 +`pyright --verbose` 一跑就露馅: + +```text +Assuming Python version 3.14.6.final.0 + Search paths: + c:\Users\guzhu\AppData\Roaming\Python\Python314\site-packages + c:\Users\guzhu\AppData\Local\Python\pythoncore-3.14-64\Lib\site-packages + ... <- 一个 Anaconda 路径都没有 +``` + +那个 3.14 来自 PATH 上的 `python`,而 PATH 上的 `python` 是本机唯一的 python: + +```text +where python -> C:\Users\guzhu\agent-tools\bin\python.cmd + \-> C:\Users\guzhu\.cache\codex-runtimes\...\python\python.exe +``` + +**Codex 运行时的 shim**(Python 3.14,没有 numpy/pandas)。分析器从 PATH 找到它, +自然解析不到科学栈。注意持久化的 User/Machine PATH 里**并没有**这一项, +它是被 Codex 进程临时注入的——所以"我的终端里没问题"和"工具有问题"可以同时成立。 + +还有第二层原因:`.venv` 是 `--system-site-packages` 建的,三方包都在 +`D:\anaconda\Lib\site-packages`,而 **pyright / Pylance 不认 `pyvenv.cfg` 里的 +`include-system-site-packages`**,只会去 `.venv\Lib\site-packages` 找—— +那里只有 `pip`,于是照样报解析不到。 + +修法(两件事都要做): + +1. 在 `pyproject.toml` 里把分析器钉到工作区 venv,别让它去 PATH 上碰运气: + +```toml +[tool.pyright] +venvPath = "." +venv = ".venv" +pythonVersion = "3.13" # 不写就跟着 PATH 上那个 3.14 走 +``` + +2. 在 `.venv\Lib\site-packages` 放一个 `.pth` 指向 base 的 site-packages。 + pyright **不读** `pyvenv.cfg`,但**会读 `.pth`**: + +```powershell +& 'D:\anaconda\python.exe' -c "import sysconfig; print(sysconfig.get_paths()['purelib'])" +# 把输出的那一行原样写进 .venv\Lib\site-packages\_mcm_base_site_packages.pth +``` + +`scripts/setup-venv.ps1` 已经自动做这一步(第 [2/4] 步),换机重建 `.venv` 不用手动补。 + +改完 pyright 从 `17 errors` 掉到 `0 errors`。IDE 侧还要各做一件事: + +* **VS Code / Cursor**:`Ctrl+Shift+P` → Python: Select Interpreter → 选 + `D:\anaconda\envs\mcm-toolkit\python.exe`(或 `.venv\Scripts\python.exe`), + 然后 Developer: Reload Window。Pylance 有缓存,不重载不生效。 +* **PyCharm**:Settings → Project → Python Interpreter 选同一个, + 再 File → Invalidate Caches / Restart。 + +**一句诊断口诀**:编辑器说"解析不到包"时,先跑 `pyright --verbose` 看它的 +`Search paths`,而不是先去 `pip install`。 + --- ## 五、两套环境的实际版本(已对齐) diff --git a/pyproject.toml b/pyproject.toml index 76442d2..69e5114 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,3 +62,10 @@ filterwarnings = ["error::RuntimeWarning"] [tool.pyright] include = ["src", "tests"] typeCheckingMode = "basic" +# 不写 venvPath/venv 的话,pyright 会去 PATH 上找 python;本机 PATH 上的 +# python 是 agent-tools 的 shim(Codex 运行时,Python 3.14,没有 numpy/pandas), +# 于是全部报 "无法解析导入 numpy/pandas"。指到工作区的 .venv 才稳定。 +venvPath = "." +venv = ".venv" +# 与两套环境一致(venv 3.13.9 / conda 3.13.15);不写会跟着 PATH 上那个 3.14 走。 +pythonVersion = "3.13" diff --git a/scripts/setup-venv.ps1 b/scripts/setup-venv.ps1 index 59d7517..adc64e3 100644 --- a/scripts/setup-venv.ps1 +++ b/scripts/setup-venv.ps1 @@ -52,18 +52,29 @@ if (Test-Path -LiteralPath $venvDir) { } if (-not (Test-Path -LiteralPath $venvDir)) { - Write-Host '[1/3] creating .venv (--system-site-packages, reuses Anaconda base) ...' + Write-Host '[1/4] creating .venv (--system-site-packages, reuses Anaconda base) ...' & $Python -m venv --system-site-packages --prompt 'mcm-toolkit' $venvDir if ($LASTEXITCODE -ne 0) { throw 'venv creation failed' } } $venvPy = Join-Path $venvDir 'Scripts\python.exe' +$venvSitePackages = Join-Path $venvDir 'Lib\site-packages' -Write-Host '[2/3] editable install of mcm-toolkit (--no-deps --no-build-isolation) ...' +Write-Host '[2/4] linking base site-packages for static analyzers (.pth) ...' +# pyright / Pylance do NOT honour "include-system-site-packages" in pyvenv.cfg, +# but they DO read *.pth files found in site-packages. Without this file they see +# only .venv\Lib\site-packages (which holds just pip), so every "import numpy" +# in the editor is reported as unresolved even though the interpreter is right. +$baseSitePackages = (& $Python -c "import sysconfig; print(sysconfig.get_paths()['purelib'])").Trim() +if (-not (Test-Path -LiteralPath $baseSitePackages)) { throw "base site-packages not found: $baseSitePackages" } +Set-Content -LiteralPath (Join-Path $venvSitePackages '_mcm_base_site_packages.pth') -Value $baseSitePackages -Encoding ASCII +Write-Host " -> $baseSitePackages" + +Write-Host '[3/4] editable install of mcm-toolkit (--no-deps --no-build-isolation) ...' & $venvPy -m pip install -e $repoRoot --no-deps --no-build-isolation --disable-pip-version-check if ($LASTEXITCODE -ne 0) { throw 'pip install -e failed' } -Write-Host '[3/3] verifying ...' +Write-Host '[4/4] verifying ...' & $venvPy -c "import mcm_toolkit, numpy, scipy, pandas, matplotlib; print('mcm_toolkit', mcm_toolkit.__version__); print('numpy', numpy.__version__)" Write-Host '' diff --git a/tests/test_data.py b/tests/test_data.py index 7d476b0..6349822 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -47,9 +47,11 @@ def test_load_table_maps_na_like_tokens_to_nan(tmp_path): p = tmp_path / "na.csv" p.write_text("a,b,c\n1,NA,\n2,-,#N/A\n", encoding="utf-8") df = load_table(p) - assert df["b"].isna().all() - assert df["c"].isna().all() - assert df["a"].notna().all() + # 外面套 bool():df["b"] 的类型是 Series | DataFrame,直接当条件用 pyright 会报 + # "条件操作数无效";套上 bool() 既满足类型检查,又能在它真返回 Series 时当场炸掉。 + assert bool(df["b"].isna().all()) + assert bool(df["c"].isna().all()) + assert bool(df["a"].notna().all()) def test_load_table_respects_explicit_encoding(tmp_path): @@ -102,7 +104,7 @@ def test_save_table_rejects_non_dataframe(tmp_path): from mcm_toolkit.data import save_table with pytest.raises(TypeError): - save_table([1, 2, 3], tmp_path / "bad.csv") + save_table([1, 2, 3], tmp_path / "bad.csv") # type: ignore[arg-type] # 就是要传错 def test_save_table_validates_before_touching_disk(tmp_path): @@ -112,7 +114,7 @@ def test_save_table_validates_before_touching_disk(tmp_path): with pytest.raises(ValueError, match=r"\.txt"): save_table(pd.DataFrame({"a": [1]}), tmp_path / "d1" / "x.txt") with pytest.raises(TypeError): - save_table([1, 2, 3], tmp_path / "d2" / "x.csv") + save_table([1, 2, 3], tmp_path / "d2" / "x.csv") # type: ignore[arg-type] # 就是要传错 assert sorted(p.name for p in tmp_path.iterdir()) == []