tabfmt.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. # -*- coding: utf-8 -*-
  2. r"""Markdown 表格渲染(**零可选依赖**)。
  3. 为什么需要它(2026-09-19 实逮,一次真实事故):
  4. `src/windcms/report.py` 用 `DataFrame.to_markdown()` 渲染"L4 过闸谱线"表 —— 而 pandas 的
  5. `to_markdown` 依赖**可选包 `tabulate`**,本交付包没有它。此前 `model_run_l6.parquet` 是空的
  6. (六层链那两步没实现),`len(l6)` 为 0 走的是 `'无'` 分支 ⇒ **没人踩到**;2026-09-19 我把
  7. `model_run` 按口径重建、L6 真的有 14 行之后,重算链在 `report` 步当场炸:
  8. ImportError: `Import tabulate` failed. Use pip or conda to install the tabulate package.
  9. at src/windcms/report.py:504 l6[cols].to_markdown(index=False)
  10. 后果不是"少一张表",而是**整条重算链停在第 4 步**:后面的 ⑥重启 / ⑦本体(`ontology/objects.json` 等)
  11. 全不执行 ⇒ 页面上"决策链/本体/报告"一片空白。教训与 memory `optional-dep-in-hot-path` 同型:
  12. **热路径不许依赖可选包**,何况交付包是离线整包(不能 pip)。
  13. 这里用纯 Python 渲染同样的 Markdown 表(列宽按显示宽度补空格,中文按 2 列宽算)。
  14. """
  15. from __future__ import annotations
  16. def _w(s) -> int:
  17. """显示宽度: CJK 与全角标点算 2, 其余算 1(对齐用, 不追求像素级)。"""
  18. n = 0
  19. for ch in str(s):
  20. n += 2 if ('\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f'
  21. or '\uff00' <= ch <= '\uffef') else 1
  22. return n
  23. def to_md(rows: list[list], header: list[str] | None = None) -> str:
  24. """[[单元格…], …] → Markdown 表(首行可作表头)。空数据返回 ''。"""
  25. if not rows:
  26. return ''
  27. esc = lambda x: ('' if x is None else str(x)).replace('|', '\\|') # 竖线要转义, 否则表被拆列
  28. body = [[esc(c) for c in r] for r in rows]
  29. if header:
  30. cols = [str(h) for h in header]
  31. body = [cols] + body
  32. ncol = max(len(r) for r in body)
  33. body = [r + [''] * (ncol - len(r)) for r in body]
  34. width = [max(_w(r[i]) for r in body) for i in range(ncol)]
  35. out = []
  36. for i, r in enumerate(body):
  37. out.append('| ' + ' | '.join(str(c) + ' ' * (width[j] - _w(c)) for j, c in enumerate(r)) + ' |')
  38. if i == 0:
  39. out.append('|' + '|'.join('-' * (width[j] + 2) for j in range(ncol)) + '|')
  40. return '\n'.join(out)
  41. def df_to_md(df, cols=None, index: bool = False) -> str:
  42. """DataFrame → Markdown 表(`to_markdown(index=False)` 的零依赖替代)。"""
  43. if df is None or len(df) == 0:
  44. return ''
  45. d = df[cols] if cols else df
  46. head = ([d.index.name or ''] if index else []) + [str(c) for c in d.columns]
  47. rows = []
  48. for idx, r in zip(d.index, d.itertuples(index=False)):
  49. rows.append(([idx] if index else []) + list(r))
  50. return to_md(rows, header=head)