Files
MCM-ICM_Toolkit/docs/04-environment.md
T
guzhujushi 97a2f95066 fix(env): 钉死 pyright 的分析目标,修掉"无法解析导入 numpy/pandas"
现象:编辑器 / 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。
2026-10-02 13:36:16 +08:00

338 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 环境配置
> 记录本机(Windows / PowerShell)上 conda 与 venv 两套环境的**实际**配置方式、
> 实测数据、以及踩过的坑。相关脚本在 `scripts/`。
>
> 状态:**两套环境均已建好、版本已对齐、验证通过**(2026-10-01)。
---
## 一、结论速查
| | venv | conda env |
|---|---|---|
| 路径 | `D:\MCM&ICM_Toolkit\.venv` | `D:\anaconda\envs\mcm-toolkit` |
| 解释器 | Anaconda base,Python **3.13.9** | Python **3.13.15** |
| 占用 | 10.5 MB | 1.55 GB(+ 共用 pkgs 缓存) |
| 来源 | `--system-site-packages` 复用 base | conda 从 defaults 通道安装 |
| 用途 | 轻量开发 / 离线兜底 | **跑题主力环境** |
| 缺什么 | SALib、pymoo、pyright | 无缺口 |
**两套的数值计算栈已完全一致**(见 §5),只差 Python 补丁号。可以放心在两套之间切换。
---
## 二、venv(轻量、离线可用)
```powershell
D:\anaconda\python.exe -m venv --system-site-packages --prompt "mcm-toolkit" .venv
.venv\Scripts\python.exe -m pip install -e . --no-deps --no-build-isolation
```
一键:`./scripts/setup-venv.ps1`
### 为什么用 `--system-site-packages`
让 venv 直接复用 Anaconda base 里已有的包,**零下载**就有可用的科学栈,
而且只有 10.5 MB,可以安心放在项目目录里。
已验证可导入(版本随 base 走,2026-10-01 base 升级后为):
`text
numpy 2.4.6 scipy 1.18.1 pandas 3.0.6 matplotlib 3.11.2
scikit-learn 1.9.1 statsmodels 0.15.0 networkx 3.6.1 sympy 1.14.0
`
### `--no-deps --no-build-isolation` 是必须的
* `--no-deps`:依赖已由 base 提供,不必联网解析;
* `--no-build-isolation`:否则 pip 会新建隔离环境去下载 setuptools 构建后端。
---
## 三、conda 环境 `mcm-toolkit`(跑题主力)
```powershell
# 一键(推荐)
.\scripts\setup-conda.ps1
# 等价的手工命令
$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes' # 见 §4 坑 1
conda create -n mcm-toolkit -y python=3.13 ^
numpy scipy pandas matplotlib scikit-learn statsmodels ^
networkx sympy openpyxl pytest ruff pip
```
**实测:约 14.5 分钟(868 秒),下载约 3 GB,环境本体 1.55 GB。**
建完后补装 pip-only 的依赖并安装本项目:
```powershell
$py = 'D:\anaconda\envs\mcm-toolkit\python.exe'
& $py -m pip install SALib pymoo pyright
& $py -m pip install -e . --no-deps --no-build-isolation
```
这些 `setup-conda.ps1` 已全部自动完成。
### 为什么不推荐 `--clone base`
原以为 `conda create -n X --clone base` 是"本地复制、秒级完成、不用下载"。
**实测不是。** 在 conda 25.11 上它会重放 base 的完整包列表并逐个下载安装
(连 pandoc、m2-msys2-runtime、anaconda-navigator 这些都用不上的也照下),
跑 253 秒才下完约 2 GB 就已被中止,按比例算远慢于精准列表。
所以 `setup-conda.ps1` 的默认路径是**精准包列表**;`-CloneBase` 只在你确实想
要一份 base 的完整副本时才用。
### 断网怎么办
**提前建好环境**,并保留 `D:\anaconda\pkgs` 缓存(现在 8.4 GB)。
包已在缓存里时 `conda create --offline` 才能工作;否则会失败。
比赛现场不要指望临时建环境,走 venv(它 100% 离线可用)。
---
## 四、踩过的坑
**坑 1 —— Anaconda ToS 插件要联网。**
Anaconda 25.x 自带 `conda_anaconda_tos` 插件,任何 `create/install` 之前都会去
`repo.anaconda.com` 拉 ToS 元数据。断网时它不是报"没网",而是抛一长串 traceback。
```powershell
$env:CONDA_PLUGINS_AUTO_ACCEPT_TOS = 'yes' # 推荐:跳过联网,保留 libmamba 求解器
conda --no-plugins create ... # 彻底关插件(退化成经典求解器,慢)
```
**坑 2 —— `envs_dirs` / `pkgs_dirs` 必须可写。**
不可写时报 `NotWritableError` 或
`Error encountered while attempting to create cache directory`。排查:
```powershell
conda info --json | Select-String "envs_dirs","pkgs_dirs"
conda config --show envs_dirs pkgs_dirs
```
**坑 3 —— 被中止的 `conda create` 用 `conda env remove` 删不干净。**
本次中止后 `conda env remove -n mcm-toolkit -y` 跑完,`D:\anaconda\envs\mcm-toolkit`
**仍然存在**且 `conda env list` 里已看不到它。要手工删目录:
```powershell
Remove-Item -LiteralPath 'D:\anaconda\envs\mcm-toolkit' -Recurse -Force
```
**坑 4 —— 别把 conda env 建在项目目录里。**
`conda create -p ./.conda` 会把 GB 级目录放进仓库,脏 `.gitignore` 也容易误提交。
**坑 5 —— 版本漂移。**
本机 base 是 2025-10 装的,defaults 通道已是 2026-10 的版本,于是新建的 conda
环境拿到的是更新的版本(pandas 从 2.3.3 跳到 **3.0.6**,是 major 升级)。
见 §5,写代码时注意别依赖两套环境都有的行为。
**坑 6 —— conda 升级留下的 `*.conda_trash` 残骸会被 Python 当成包导入。**
base 从 2025.12 升到 2026.07 后,`D:\anaconda\Lib\site-packages\zstandard\` 里只剩一个
`backend_c.cp313-win_amd64.pyd.c~.conda_trash`。目录没了 `__init__.py`,Python 就把它当作
**namespace package** 导入成功,于是:
```text
>>> import zstandard
>>> zstandard.__file__ # None —— 导入"成功"了,但什么都没拿到
>>> zstandard.__version__
AttributeError
```
pandas 3.0 的 `import_optional_dependency("zstandard")` 拿到这种半残包时,抛的不是
`ModuleNotFoundError`(那会被正常忽略),而是
`ImportError: Can't determine version for zstandard`,直接**打断 `pd.read_csv`**。
症状很迷惑:同一个脚本今天能跑明天不能跑,报错还指向 csv 读取。
**怎么查**(关键是 `__file__` 和 `__version__` 两项一起看):
```powershell
& 'D:\anaconda\python.exe' -c "import zstandard; print(zstandard.__file__, getattr(zstandard,'__version__','MISSING'))"
Get-ChildItem -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' -Force
```
**怎么修**:确认 conda 记录里本来就没有这个包(`conda list -n base | findstr zstandard`
只该看到 `backports.zstd` / `zstd`),再把这个残骸目录改名禁用(比删掉可逆):
```powershell
Move-Item -LiteralPath 'D:\anaconda\Lib\site-packages\zstandard' `
-Destination 'D:\anaconda\Lib\site-packages\zstandard.disabled-conda-trash'
```
改名后 `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`。
---
## 五、两套环境的实际版本(已对齐)
**2026-10-01 已把 base 从 Anaconda 2025.12 升到 2026.07,两套环境版本已对齐。**
| 包 | venv(继承 base) | conda `mcm-toolkit` | |
|---|---|---|---|
| Python | 3.13.9 | 3.13.15 | 补丁号,可忽略 |
| numpy | 2.4.6 | 2.4.6 | ✅ 一致 |
| scipy | 1.18.1 | 1.18.1 | ✅ 一致(连 build 号都相同) |
| pandas | 3.0.6 | 3.0.6 | ✅ 一致 |
| matplotlib | 3.11.2 | 3.11.2 | ✅ 一致 |
| scikit-learn | 1.9.1 | 1.9.1 | ✅ 一致 |
| statsmodels | 0.15.0 | 0.15.0 | ✅ 一致 |
| networkx | 3.6.1 | 3.6.1 | ✅ 一致 |
| sympy | 1.14.0 | 1.14.0 | ✅ 一致 |
| pytest | 9.1.1 | 9.1.1 | ✅ 一致 |
| SALib / pymoo / pyright | 缺失 | 已装 | venv 本来就只做轻量开发 |
| BLAS 后端 | **MKL** | **MKL** | ✅ 一致 |
### base 升级记录
[FENCE]powershell
conda update -n base --all -y # 365 个包更新,下载 638 MB,耗时 1033 秒
conda install -n mcm-toolkit numpy=2.4.6 -y # 把项目环境对齐到 base 的 numpy
[FENCE]
* `conda` 本身从 25.11.1 升到 **26.7.3**。
* 升级前 `conda list -n base --revisions` 里已留有 revision 记录,可回滚:
`conda install -n base --revision <N>`。
* **踩过的坑**:base 想装 `numpy=2.5.3` 时求解器会顺手把 BLAS 从 **MKL 换成 OpenBLAS**
(`_anaconda_depends 2026.07 ..._mkl_1 → ..._openblas_1`、`blas 1.0-mkl → 1.0-openblas`)。
`_anaconda_depends` 把 base 的 numpy 钉在 2.4.6,硬升会牵动整套线性代数后端,
所以选择把项目环境的 numpy **降到 2.4.6** 来对齐,而不是动 base。
* 升级期间遇到 `repo.anaconda.com` 读超时(默认 9.15 秒太短),已把
`remote_read_timeout_secs` 调到 120、`remote_connect_timeout_secs` 调到 30、
`remote_max_retries` 调到 5。这几项写在 `~/.condarc` 里。
## 六、国内镜像(需要装新包时)
```powershell
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
conda config --set show_channel_urls yes
# pip(对 venv 和 conda 环境都生效)
python -m pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
```
> 本机当前通过 `http://127.0.0.1:7993` 的 UniClash 代理直连,未配镜像。镜像只影响**下载**,
> 不影响坑 1 的 ToS 行为。
---
## 七、可选依赖
核心 4 个包(`numpy scipy pandas matplotlib`)两套环境都有。其余按需:
```powershell
python -m pip install -e ".[ml]" # sklearn statsmodels 已有
python -m pip install -e ".[graph]" # networkx 已有
python -m pip install -e ".[sensitivity]" # SALib conda 环境已有
python -m pip install -e ".[symbolic]" # sympy 已有
python -m pip install -e ".[moo]" # pymoo conda 环境已有
python -m pip install -e ".[excel]" # openpyxl 已有
python -m pip install -e ".[dev]" # pytest ruff pyright
```
缺依赖时对应函数应当给出可读的安装提示,而不是裸抛 `ImportError`
(写成约定,见 `docs/02-architecture.md` §4.6)。
---
## 八、脚本的编码约定(踩过的坑)
`scripts/` 下的 `.ps1` **必须是纯 ASCII + UTF-8 无 BOM + LF**。
原因是 Windows PowerShell 5.1 **在没有 BOM 时按 ANSI(本机 GBK)解析 `.ps1`**,
中文注释和中文消息会被拆成非法字节,报出来的却是一堆莫名其妙的
`Missing closing '}' in statement block` / `The string is missing the terminator`。
(实测:带中文的 `doctor.ps1` 直接语法错误;改成纯 ASCII 后立刻正常。)
两种可行方案,本项目选了第二种:
1. 保留中文 → 必须存成 **UTF-8 with BOM**(违反 AGENTS.md 的"无 BOM"约定);
2. **纯 ASCII,注释与输出用英文** → 符合 AGENTS.md,且任何编码下都能解析。← 采用
另一个相关的坑:**PowerShell 5.1 向原生程序传参时会吃掉双引号**,
所以 `doctor.ps1` 里传给 `python -c` 的探测代码只能用单引号字符串。
---
## 九、已知的沙箱相关现象
如果在受限环境(文件系统只读 / 无网络)里跑 matplotlib,会看到:
```text
Matplotlib created a temporary cache directory at ... because there was an issue with
the default path (C:\Users\guzhu\.matplotlib)
```
这不是配置问题,是 `~\.matplotlib` 不可写。正常终端里不会出现;
真遇到时设置 `MPLCONFIGDIR` 指向可写目录即可:
```powershell
$env:MPLCONFIGDIR = "$env:TEMP\matplotlib"
```