dsh-origin-bridge 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,12 +11,12 @@
11
11
  </p>
12
12
 
13
13
  <p align="center">
14
- <sub>27 个工具 · <b>1118 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
14
+ <sub>27 个工具 · <b>1126 例全链路测试全部通过</b> · 数据不出本机 · 支持工作站 <code>.txt</code> / <code>.bin</code></sub>
15
15
  </p>
16
16
 
17
17
  <p align="center">
18
18
  <a href="#安装"><img alt="platform" src="https://img.shields.io/badge/platform-Windows%20%2B%20Origin%202021%2B-blue"></a>
19
- <img alt="tests" src="https://img.shields.io/badge/tests-1118%2F1118%20passing-brightgreen">
19
+ <img alt="tests" src="https://img.shields.io/badge/tests-1126%2F1126%20passing-brightgreen">
20
20
  <img alt="license" src="https://img.shields.io/badge/license-MIT-lightgrey">
21
21
  </p>
22
22
 
@@ -35,7 +35,7 @@
35
35
  - **数据不出本机。** 没有 API key、没有云端组件,dsh 拉起一个本地 Python 进程,通过 OriginLab 官方的 [`originpro`](https://pypi.org/project/originpro/) 驱动你机器上的 Origin。
36
36
  - **每一步都有证据。** Origin 的 X-Function 普遍"返回成功但什么都没发生"。这里画图后读回轴范围与数据跨度比对,导出后校验文件存在/体积/文件头魔数,样式逐项读回——读不回的进 `failed` 列表,不会假装成功。
37
37
  - **不是 74 个工具,是 27 个。** 每个都有实测用途,工具描述里写着实测限制,模型不会踩坑。
38
- - **1000+ 次真实测试,不是自称。** **1118 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1118 通过 / 0 失败**:27 个工具每个至少 15 例、最多 134 例,按副作用分五层(L0 纯解析 296、L1 只读 COM 61、L2 改会话 520、L3 落盘 163、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **352 项**离线/单元/边界检查(asciiio 84、binio 30、lifecycle 37、smoke 129、edge 26、paths 28、json 18)**随包发布,你可以直接复跑**;那 1118 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
38
+ - **1000+ 次真实测试,不是自称。** **1126 例**全链路用例跑在真实 stdio 服务器 + 本机 OriginPro 上,**1126 通过 / 0 失败**:27 个工具每个至少 15 例、最多 134 例,按副作用分五层(L0 纯解析 301、L1 只读 COM 63、L2 改会话 521、L3 落盘 163、L4 进程级 78)。提示词全部按人话写,跑之前先用在线 `tools/list` schema 校验用例本身,所以失败只会是产品问题,不会是题目写错。另有 **364 项**离线/单元/边界检查(asciiio 84、binio 30、lifecycle 37、smoke 129、edge 26、paths 28、json 18、readfile 12)**随包发布,你可以直接复跑**;那 1126 例题库含提示词与模拟数据,是开发侧资产,不随包分发。originpro 的十几个文档陷阱全部记录在案并绕开。
39
39
 
40
40
  ## 它能做什么
41
41
 
@@ -50,7 +50,7 @@
50
50
  | "做一张 XPS 图" | `origin_chart_render`(40 个科研预设:轴标题/图类型/建议拟合) |
51
51
  | "热力图显示这个矩阵" | `origin_matrix`(heat_map / cmap / mesh / contline,Origin 自带图模) |
52
52
 
53
- 数据源支持:`.dat` `.csv` `.txt` `.tsv`(自动嗅探分隔符;编码覆盖 UTF-8 / GBK / **UTF-16(有无 BOM 都行)** / cp1252;多行表头、Fortran 指数、`NA`/`-`;还有 `<0.001` 检出限、`2.5±0.1`、`25 mV`、`1,234.5`、`(500)`、`2.5*` 这类被装饰过的数值——每解一种都会在 `warnings` 里写清代入了什么)、`.xls` `.xlsx`、电化学工作站(CH Instruments 等)导出的 `.txt`(几十行仪器说明自动收进 `metadata`,不会把首行日期当成列名),以及头部为 `80 F2 1B 00` 的电化学工作站二进制 `.bin`。
53
+ 数据源支持:`.dat` `.csv` `.txt` `.tsv`(`origin_read_file` 只读这些文本类;`.xls/.xlsx/.wks` 会明确报 `unsupported_file_type` 让你改用 `origin_import`。自动嗅探分隔符;编码覆盖 UTF-8 / GBK / **UTF-16(有无 BOM 都行)** / cp1252;多行表头、Fortran 指数、`NA`/`-`;还有 `<0.001` 检出限、`2.5±0.1`、`25 mV`、`1,234.5`、`(500)`、`2.5*` 这类被装饰过的数值——每解一种都会在 `warnings` 里写清代入了什么)、`.xls` `.xlsx`、电化学工作站(CH Instruments 等)导出的 `.txt`(几十行仪器说明自动收进 `metadata`,不会把首行日期当成列名),以及头部为 `80 F2 1B 00` 的电化学工作站二进制 `.bin`。
54
54
 
55
55
  ## 安装
56
56
 
@@ -87,6 +87,12 @@ dsh plugin --profile desktop add @caob23/dsh-origin-bridge # 转发壳,依赖
87
87
  pip install originpro numpy # 国内网络慢就加 -i https://pypi.tuna.tsinghua.edu.cn/simple
88
88
  ```
89
89
 
90
+ > **前置缺了会怎样,两种情况不一样**(2026-10-19 实测,别猜):
91
+ > - **解释器在、`originpro` 不在**:27 个工具**照常注册**,每一次调用都返回 `code=missing_originpro` 和原因(`No module named 'originpro'`),`isError: true`——看得见、知道修什么。
92
+ > - **PATH 上没有 `python`**(补丁默认命令就是 `python`):桥进程起不来,`failOnStartupError: false` 保证 dsh 照常启动,代价是那一轮**工具完全不注册**、也没有报错。这时先设 `ORIGIN_BRIDGE_PYTHON`。
93
+ >
94
+ > Origin 没开**不影响**注册:COM 连接是第一次工具调用时才建立的。
95
+
90
96
  包内补丁的默认取值是 `python`(PATH 上那个)和 `profiles/desktop`。依赖装在 venv 里、或者你用的是别的 profile,就设这两个用户环境变量(设完要重开终端、再彻底重启 dsh):
91
97
 
92
98
  ```powershell
@@ -127,9 +133,10 @@ pip install -r requirements.txt # 国内网络慢就加 -i https://pypi.tun
127
133
  .\.venv\Scripts\python.exe test_lifecycle.py # 37 项,不需要 Origin:实例分类/回收/掉线重连的安全边界
128
134
  .\.venv\Scripts\python.exe test_paths.py # 28 项,不需要 Origin:导出/保存目标路径的规范化与拒绝
129
135
  .\.venv\Scripts\python.exe test_json_safety.py # 18 项,不需要 Origin:返回值必须是严格 JSON
136
+ .\.venv\Scripts\python.exe test_readfile.py # 12 项,不需要 Origin:read_file 的 sample 与二进制表格拒绝
130
137
  ```
131
138
 
132
- 上面这 352 项随包发布,你可以自己复跑。另外那 **1118 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
139
+ 上面这 364 项随包发布,你可以自己复跑。另外那 **1126 例全链路题库是开发侧资产**(`cases/` + `run_cases.py`),跑的是真实 stdio 服务器 + 本机 Origin,用它的提示词和你的原始数据都不适合公开,所以没有进仓库、也没有进 npm 包;本文里的覆盖数字都来自它的实际运行结果。
133
140
 
134
141
  ### 不是 dsh,是别的 MCP 客户端
135
142
 
@@ -179,7 +186,7 @@ powershell -ExecutionPolicy Bypass -File .\install_dsh.ps1 -Profile desktop
179
186
  | 工具 | 用途 |
180
187
  |---|---|
181
188
  | `origin_status` | 连接检查、Origin 版本、已开页面。**动手前先调它** |
182
- | `origin_read_file` | 只解析文件不碰 Origin:编码、分隔符、表头行、每列长名/单位/有效行数;`.bin` 走另一条路线给出技术/点数/方法参数/轴来源 |
189
+ | `origin_read_file` | 只解析文件不碰 Origin:编码、分隔符、表头行、每列长名/单位/有效行数、**前 3 个原始值(`sample`,缺值为 null,用来抽查没串列)**;`.xls/.xlsx/.wks` 明确拒绝并让你改用 `origin_import`;`.bin` 走另一条路线给出技术/点数/方法参数/轴来源 |
183
190
  | `origin_import` | 导入 `.dat/.csv/.txt/.tsv/.xls/.xlsx/.wks` 和工作站二进制 `.bin`,返回 `ws-N` 句柄和列清单 |
184
191
  | `origin_write` | 内联数据建表:窄表 `columns={列名:[值…]}`,宽表 `headers`+`rows`(一次写完) |
185
192
  | `origin_formula` | 给列设计算公式(Fx)做派生列,读回算出的行数与首末值 |
@@ -267,6 +274,7 @@ Origin Bridge 的做法:
267
274
  | 编码候选顺序 | 仪器"另存为 Unicode 文本"导出的 UTF-16(有/无 BOM)此前直接报 `找不到数值数据行`;而 cp1252 写的 `µ`/`°` 会被 gb18030 **抢先**解成 `祍`/`癈`——它不报错,所以错误一路静默到列名 | `read_text` 先认 BOM,无 BOM 时按 NUL 占比猜 utf-16 并在 `encoding` 里写明是猜的;再用"高位字节成对=GBK、孤立单字节=cp1252"决定候选顺序(`high_byte_runs`) |
268
275
  | 装饰过的数值格子 | `<0.001`、`2.5±0.1`、`25 mV`、`1,234.5`、`2.5*`、`(500)`、`1e400` 这些形态此前让整列被判"全为空"直接丢弃——用户看到的是少一列数据,不是报错 | `cell_number` 逐形态解开并回一条 `warnings` 说明代入了什么;溢出/Infinity 一律按缺值,绝不把 inf 送出桥 |
269
276
  | 分隔符嗅探 | 欧陆 `Zeit;Signal / 0,0;1,5` 会被逗号拆开,而且错的拆法因为"列更多"在评分里胜出 | 评分改成:数值列数 → 行宽一致性 → 列数更少者优先;表头行不参与数值比 |
277
+ | `origin_open` 与句柄 | 开工程会替换所有页面,之前发出去的 `ws-/gr-` 句柄全部指向已销毁的对象;死对象上的 `save_fig` 会"成功"但什么都不写(1124 例跑里正是它让连续三个导出失败) | `open_project` 成功与失败都清空句柄表并在返回里给 `handles_reset: true`;旧句柄再用会得到 `worksheet_not_found`/`graph_not_found`,不会静默操作死页面 |
270
278
  | 非标准 JSON | `json.dumps` 默认把 inf/nan 写成裸 `Infinity`/`NaN`,那不是合法 JSON——dsh 侧整条响应解析失败,看起来像"桥挂了"而不是"一个数不对" | `sanitize()` 把 ±inf/nan 归 None;`server.py` 写出走 `allow_nan=False`,兜不住就降级成带原 id 的 JSON-RPC 错误;`smoke_test` 客户端用 `strict_loads` 拒绝,测试不会替产品掩盖它(`test_json_safety.py` 18 项) |
271
279
 
272
280
  ## 已知边界
@@ -315,10 +323,10 @@ dsh-origin-bridge/
315
323
  ├── icon.svg # 卡片图标(scripts/make_icon.py 生成)
316
324
  ├── locale/*.json # dsh 卡片显示名与描述
317
325
  ├── scripts/make_preview.py # 重新生成 docs/preview.png(关掉图例后才导出)
318
- └── test_*.py / smoke_test.py / edge_checks.py # 352 项离线与单元检查(随包发布)
326
+ └── test_*.py / smoke_test.py / edge_checks.py # 364 项离线与单元检查(随包发布)
319
327
  ```
320
328
 
321
- 开发侧另有 `cases/` + `run_cases.py` 的 1118 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
329
+ 开发侧另有 `cases/` + `run_cases.py` 的 1126 例全链路题库和 `render_realdata.py` 的真实数据逐文件出图脚本,两者都不随包发布(见上文)。
322
330
 
323
331
  重新生成图标:`python scripts/make_icon.py`(只需要 numpy,PNG 由 stdlib 手写)。
324
332
 
package/cordis.patch.yml CHANGED
@@ -17,8 +17,12 @@
17
17
  # manual install -> set ORIGIN_BRIDGE_HOME to the folder holding server.py
18
18
  # interpreter -> set ORIGIN_BRIDGE_PYTHON to your venv python.exe
19
19
  #
20
- # failOnStartupError: false keeps dsh bootable when Origin is missing or a modal
21
- # dialog stalls the COM server; the tools just do not register that session.
20
+ # failOnStartupError: false keeps dsh bootable when this entry cannot start at all —
21
+ # measured (2026-10-19) that means only a missing interpreter: the default command is
22
+ # `python` from PATH. A missing originpro does NOT hide the tools: all 27 still
23
+ # register and every call answers `missing_originpro` with the fix to run. Origin
24
+ # itself being closed is irrelevant to startup — the COM connection is made lazily
25
+ # inside the first tool call, not here.
22
26
 
23
27
  - insert:
24
28
  - id: mcp-origin-bridge
package/originlab.py CHANGED
@@ -669,7 +669,16 @@ def read_file(path, comment=None):
669
669
  path = os.path.abspath(os.path.expanduser(path))
670
670
  if not os.path.isfile(path):
671
671
  raise OriginError("file_not_found", "文件不存在:%s" % path)
672
- if os.path.splitext(path)[1].lower() == ".bin":
672
+ ext = os.path.splitext(path)[1].lower()
673
+ if ext in (".xls", ".xlsx", ".wks"):
674
+ # Decoding a zip/OLE container as text "succeeds" into mojibake and then reports
675
+ # "只有表头?", blaming the user's file for our mistake (measured 2026-10-19).
676
+ raise OriginError(
677
+ "unsupported_file_type",
678
+ "%s 是二进制表格,origin_read_file 只读文本类数据" % ext,
679
+ ["用 origin_import 直接导入(走 Origin 原生导入)",
680
+ "或先在 Excel 里另存为 .csv,再用 origin_read_file 预览结构"])
681
+ if ext == ".bin":
673
682
  try:
674
683
  decoded = binio.read_bin(path)
675
684
  except binio.BinFormatError as exc:
@@ -689,8 +698,14 @@ def read_file(path, comment=None):
689
698
  except ValueError as exc:
690
699
  raise OriginError("parse_failed", str(exc),
691
700
  ["确认文件里有数值行", "给 comment 指定实际的注释前缀"])
692
- parsed["columns"] = [{k: v for k, v in c.items() if k != "values"}
693
- for c in parsed["columns"]]
701
+ # Three raw values per column: enough for the model to spot a shifted column or a
702
+ # column that parsed to nothing, without putting the whole dataset in context.
703
+ parsed["columns"] = [
704
+ dict({k: v for k, v in c.items() if k != "values"},
705
+ sample=(c.get("values") or [])[:3])
706
+ for c in parsed["columns"]]
707
+ parsed["sample_note"] = ("sample 是每列前 3 个原始值(缺值为 null),只用来抽查解析是否正确;"
708
+ "完整数据用 origin_import 进 Origin")
694
709
  parsed["proof_level"] = "verified"
695
710
  return sanitize(parsed)
696
711
 
@@ -1662,12 +1677,26 @@ def open_project(path):
1662
1677
  def run():
1663
1678
  global _project_file
1664
1679
  op = origin()
1665
- if not op.open(path):
1680
+ # Opening a project replaces every page, so every handle handed out earlier is
1681
+ # dead — and a *refused* open can still have closed the current project. Both
1682
+ # ways the table has to go: leaving it behind makes later calls act on dead
1683
+ # pages, where originpro's wrapper "succeeds" and Origin quietly writes nothing
1684
+ # (that is how three exports failed in the 1124-case run).
1685
+ try:
1686
+ ok = bool(op.open(path))
1687
+ finally:
1688
+ _handles.clear()
1689
+ _names.clear()
1690
+ if not ok:
1691
+ _project_file = ""
1666
1692
  raise OriginError("open_failed", "Origin 拒绝打开这个工程:%s" % path,
1667
1693
  ["确认文件是 .opju/.opj 且没被另一个 Origin 实例锁住",
1668
- "Origin 版本低于该工程保存版本时也打不开"])
1694
+ "Origin 版本低于该工程保存版本时也打不开",
1695
+ "打开失败后旧句柄已作废,请重新 origin_inspect 看当前页面"])
1669
1696
  _project_file = path
1670
- return sanitize({"opened": path, "open_pages": [p.name for p in op.pages()]})
1697
+ return sanitize({"opened": path, "open_pages": [p.name for p in op.pages()],
1698
+ "handles_reset": True,
1699
+ "note": "之前拿到的 ws-/gr- 句柄全部作废(工程换了页面),请用这里的页面名或重新导入"})
1671
1700
 
1672
1701
  return call(run)
1673
1702
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-origin-bridge",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "在对话里驱动本机 Origin / OriginPro:导入 .dat/.csv/.txt 与电化学工作站 .bin,绘图、拟合、改样式,导出图片并保存可在 Origin 里继续编辑的 .opju 工程。dsh 插件,27 个 MCP 工具。第三方工具,与 OriginLab Corporation 无关联、未获认可。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,6 +52,7 @@
52
52
  "edge_checks.py",
53
53
  "test_paths.py",
54
54
  "test_json_safety.py",
55
+ "test_readfile.py",
55
56
  "test_asciiio.py",
56
57
  "test_binio.py",
57
58
  "test_lifecycle.py",
package/requirements.txt CHANGED
@@ -1,2 +1,4 @@
1
+ # 实测环境:originpro 1.1.15 + OriginPro 2024 SR1 (10.100178) + Python 3.12
2
+ # originpro 要求 Origin 2021+;2.x 没有验证过,暂不设上限。
1
3
  originpro>=1.1.0
2
4
  numpy>=1.24
package/server.py CHANGED
@@ -20,7 +20,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
20
  import originlab # noqa: E402
21
21
 
22
22
  SERVER_NAME = "origin-bridge"
23
- SERVER_VERSION = "0.4.6"
23
+ SERVER_VERSION = "0.4.7"
24
24
  PROTOCOL = "2024-11-05"
25
25
 
26
26
 
@@ -106,7 +106,7 @@ TOOLS = [
106
106
  },
107
107
  {
108
108
  "name": "origin_read_file",
109
- "description": "只解析数据文件、不碰 Origin:返回编码、分隔符、表头行数、每列长名/单位/有效行数,以及仪器参数 metadata。拿到陌生 .dat/.csv/.txt 先用它看清结构,再决定 origin_import 用哪几列。支持 #、//、!、; 注释、BOM、GBK、空格对齐、Fortran 的 1.5d+00 指数、NA/- 缺失值、Origin 三行表头(Long Name/Units/Comments),也支持电化学工作站(CH Instruments 等)导出的 txt:前面几十行参数说明会被跳进 metadata,紧贴数据的 Potential/V, Current/A 这类表头按斜杠拆出列名和单位。.bin 走另一条解析路线:返回技术标签(CV/LSV/IMP/i-t)、点数、两个 float32 数组的范围和 role_confidence。",
109
+ "description": "只解析数据文件、不碰 Origin:返回编码、分隔符、表头行数、每列长名/单位/有效行数,以及仪器参数 metadata。拿到陌生 .dat/.csv/.txt 先用它看清结构,再决定 origin_import 用哪几列。支持 #、//、!、; 注释、BOM、GBK、空格对齐、Fortran 的 1.5d+00 指数、NA/- 缺失值、Origin 三行表头(Long Name/Units/Comments),也支持电化学工作站(CH Instruments 等)导出的 txt:前面几十行参数说明会被跳进 metadata,紧贴数据的 Potential/V, Current/A 这类表头按斜杠拆出列名和单位。编码覆盖 UTF-8 / GBK / UTF-16(有无 BOM)/ cp1252;<0.001、2.5±0.1、25 mV、1,234.5、(500)、2.5* 这类被装饰的数值会解出来并在 warnings 里说明代入了什么。每列还带 sample(前 3 个原始值,缺值为 null),用来抽查解析对不对——完整数据仍然只进 Origin。二进制表格 .xls/.xlsx/.wks 不在它的范围内,会明确返回 unsupported_file_type,请用 origin_import。.bin 走另一条解析路线:返回技术标签(CV/LSV/IMP/i-t)、点数、两个 float32 数组的范围和 role_confidence。",
110
110
  "inputSchema": {
111
111
  "type": "object",
112
112
  "properties": {
@@ -217,7 +217,7 @@ TOOLS = [
217
217
  },
218
218
  {
219
219
  "name": "origin_open",
220
- "description": "打开已有的 .opju/.opj 工程,返回页面清单。",
220
+ "description": "打开已有的 .opju/.opj 工程,返回页面清单。打开会替换工程里的所有页面,所以之前拿到的 ws-/gr- 句柄全部作废(返回 handles_reset: true)——要用就用返回的页面名,或重新 origin_import/origin_plot 拿新句柄;打开失败时旧句柄同样作废。",
221
221
  "inputSchema": {
222
222
  "type": "object",
223
223
  "properties": {"path": {"type": "string"}},
@@ -0,0 +1,104 @@
1
+ """Offline checks for origin_read_file's shaping (no Origin needed).
2
+
3
+ Two behaviours that cost a real debugging cycle:
4
+ - a `.xlsx` used to be decoded as text and reported as "找不到数值数据行(只有表头?)",
5
+ which blames the user's file for our mistake;
6
+ - the response used to carry no values at all, so a model could not tell a correctly
7
+ parsed column from one whose values slid into the neighbouring column.
8
+ """
9
+
10
+ import os
11
+ import shutil
12
+ import sys
13
+ import tempfile
14
+
15
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
16
+ import originlab as ol # noqa: E402
17
+
18
+ PASSED = 0
19
+ FAILED = []
20
+
21
+
22
+ def ok(label, cond, detail=""):
23
+ global PASSED
24
+ if cond:
25
+ PASSED += 1
26
+ else:
27
+ FAILED.append("%s %s" % (label, detail))
28
+ print("%s %s%s" % ("PASS" if cond else "FAIL", label,
29
+ (" | " + str(detail)[:110]) if detail else ""))
30
+
31
+
32
+ def write(dirname, name, text, encoding="utf-8"):
33
+ path = os.path.join(dirname, name)
34
+ with open(path, "w", encoding=encoding, newline="") as fh:
35
+ fh.write(text)
36
+ return path
37
+
38
+
39
+ def code_of(fn):
40
+ try:
41
+ fn()
42
+ except ol.OriginError as exc:
43
+ return exc.code
44
+ except Exception as exc: # noqa: BLE001 — an unwrapped crash is the failure
45
+ return "UNWRAPPED:" + type(exc).__name__
46
+ return "no-raise"
47
+
48
+
49
+ def main():
50
+ work = tempfile.mkdtemp(prefix="readfile_checks_")
51
+ try:
52
+ csv = write(work, "a.csv", "x,y\n1,2\n3,\n5,6\n")
53
+ res = ol.read_file(csv)
54
+ ok("csv parses", res.get("rows") == 3, res.get("rows"))
55
+ ok("every column carries a 3-value sample",
56
+ [c["sample"] for c in res["columns"]] == [[1.0, 3.0, 5.0], [2.0, None, 6.0]],
57
+ [c["sample"] for c in res["columns"]])
58
+ ok("sample keeps nulls so a gap is visible", None in res["columns"][1]["sample"])
59
+ ok("sample is explained", "原始值" in (res.get("sample_note") or ""),
60
+ res.get("sample_note"))
61
+ ok("full values still stay out of the payload",
62
+ all("values" not in c for c in res["columns"]))
63
+
64
+ # the binary-spreadsheet guard
65
+ for name, head in (("b.xlsx", b"PK\x03\x04"), ("c.xls", b"\xd0\xcf\x11\xe0"),
66
+ ("d.wks", b"\x00\x00\x02\x00")):
67
+ path = os.path.join(work, name)
68
+ with open(path, "wb") as fh:
69
+ fh.write(head + b"\x00" * 64)
70
+ ok("%s is refused as a binary sheet" % name,
71
+ code_of(lambda p=path: ol.read_file(p)) == "unsupported_file_type",
72
+ code_of(lambda p=path: ol.read_file(p)))
73
+
74
+ # and the guard says what to do instead, with the file name in the message
75
+ path = os.path.join(work, "b.xlsx")
76
+ try:
77
+ ol.read_file(path)
78
+ except ol.OriginError as exc:
79
+ ok("message names the format", ".xlsx" in exc.message, exc.message)
80
+ ok("next action points at origin_import",
81
+ any("origin_import" in a for a in exc.next_actions), exc.next_actions)
82
+
83
+ # import_file must still accept Excel: only read_file refuses it
84
+ import inspect
85
+ src = inspect.getsource(ol.import_file)
86
+ ok("import_file still allows .xlsx", "二进制表格" not in src)
87
+
88
+ ok("a .bin is still routed to binio, not refused",
89
+ code_of(lambda: ol.read_file(os.path.join(
90
+ os.path.dirname(os.path.abspath(__file__)), "fixtures", "not_a_bin.bin")))
91
+ in ("parse_failed", "no-raise"), "binio path")
92
+
93
+ print("\n%d/%d read_file checks passed" % (PASSED, PASSED + len(FAILED)))
94
+ if FAILED:
95
+ for f in FAILED:
96
+ print(" FAILED:", f)
97
+ return 1
98
+ return 0
99
+ finally:
100
+ shutil.rmtree(work, ignore_errors=True)
101
+
102
+
103
+ if __name__ == "__main__":
104
+ sys.exit(main())