更新了保存图表模块
This commit is contained in:
1 parent
748affb7f0
commit
f99cf5c3c9
4 files changed
+228
-20
No files matched your search
@@ -6,6 +6,7 @@
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- `viz.save_figure`:L0 最后一项落地(Codex 写)。入口校验先于副作用,支持 `png` / `pdf` / `svg` / `eps` / `ps` / `jpg` / `jpeg` / `tif` / `tiff` / `webp`;`path` 带图片后缀时自动去后缀。`tests/test_viz.py` 去掉模块级 skip 并新增 8 条保存测试。
|
||||||
- 仓库骨架:`src/mcm_toolkit/` 7 个包(core / data / stats / models / solve / decision / viz)
|
- 仓库骨架:`src/mcm_toolkit/` 7 个包(core / data / stats / models / solve / decision / viz)
|
||||||
- 顶层 `__init__.py` 只导出 7 个子包 + 8 个快捷函数,`__all__` 即公开 API 清单
|
- 顶层 `__init__.py` 只导出 7 个子包 + 8 个快捷函数,`__all__` 即公开 API 清单
|
||||||
- `core.Result`:唯一的结果对象(替代原建议里的 5 个 Result 类)
|
- `core.Result`:唯一的结果对象(替代原建议里的 5 个 Result 类)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# STATUS — 当前进度
|
# STATUS — 当前进度
|
||||||
|
|
||||||
> 更新日期:2026-10-12
|
> 更新日期:2026-10-18
|
||||||
|
|
||||||
## 本次任务
|
## 本次任务
|
||||||
|
|
||||||
@@ -18,12 +18,12 @@
|
|||||||
- A 档**薄封装**(`rmse`/`mae`/`fit_linear`/`train_test_split`…)算工程,Codex 可直接实现。
|
- A 档**薄封装**(`rmse`/`mae`/`fit_linear`/`train_test_split`…)算工程,Codex 可直接实现。
|
||||||
- 分地盘按**文件**划,不按函数划(同一文件两人各写一半曾导致用户编辑器保存失败)。
|
- 分地盘按**文件**划,不按函数划(同一文件两人各写一半曾导致用户编辑器保存失败)。
|
||||||
|
|
||||||
当前 **L0 11/12**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
当前 **L0 12/12(已全部完成)**:`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
||||||
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
`detect_outliers`、`normalize`/`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
||||||
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
`monte_carlo`(**用户写**,2026-10-04);另**提前落地**了 L1 的 `confidence_interval` / `t_test` /
|
||||||
`monte_carlo_probability`、`monte_carlo_integral`、`random_walk`、`markov_simulate`、
|
`monte_carlo_probability`、`monte_carlo_integral`、`random_walk`、`markov_simulate`、
|
||||||
`markov_steady_state`、`mm1_metrics`(**均为用户写**)—— `solve` 层的 L1 清单**已全部落地**。
|
`markov_steady_state`、`mm1_metrics`(**均为用户写**)—— `solve` 层的 L1 清单**已全部落地**。
|
||||||
剩余 1 项:`save_figure`(Codex 写)。
|
`save_figure` 已于 2026-10-18 由 Codex 落地,L0 收官。
|
||||||
**`models/metrics.py` 已完成(第九~十轮)**:6 个指标(`rmse` / `mae` / `mape` / `smape` / `r2_score` /
|
**`models/metrics.py` 已完成(第九~十轮)**:6 个指标(`rmse` / `mae` / `mape` / `smape` / `r2_score` /
|
||||||
`adjusted_r2`)+ 共享入口 `verify_yt_yp` **由用户写**、Codex 审;`compare_models` 由 Codex 按 A 档落地
|
`adjusted_r2`)+ 共享入口 `verify_yt_yp` **由用户写**、Codex 审;`compare_models` 由 Codex 按 A 档落地
|
||||||
(含指标名**别名映射**)。`models` 下一步是 `regression.py`。
|
(含指标名**别名映射**)。`models` 下一步是 `regression.py`。
|
||||||
@@ -428,11 +428,11 @@
|
|||||||
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
|
只差 Python 补丁号(3.13.9 vs 3.13.15)。详见 `docs/04-environment.md` §5。
|
||||||
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
|
- [x] ~~Gitea 远程尚未配置~~ → **已配置并推送完成**,见上面 D 节与决策 19/20。
|
||||||
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
|
note:仓库当前是 **Public**(匿名 `git ls-remote` 可读),要改私有去 Gitea 仓库设置。
|
||||||
- [ ] L0 完成 **11/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
- [x] L0 完成 **12/12**(`Result`、`get_rng`/`set_seed`、`load_table`/`save_table`、`clean_missing`、
|
||||||
`detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
`detect_outliers`、`normalize`、`standardize`、`describe`、`correlation_matrix`、`bootstrap_ci`、
|
||||||
`monte_carlo`(用户写,2026-10-04));另 L1 的 `confidence_interval` / `t_test` /
|
`monte_carlo`(用户写,2026-10-04));另 L1 的 `confidence_interval` / `t_test` /
|
||||||
`monte_carlo_probability` / `monte_carlo_integral` 已提前落地;
|
`monte_carlo_probability` / `monte_carlo_integral` 已提前落地;
|
||||||
**L0 只剩 `save_figure`(Codex 写)**,详情见 E 节与"下一步"
|
**L0 已全部完成**(`save_figure` 于 2026-10-18 落地),详情见"下一步"第十一轮
|
||||||
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
|
- [ ] 本机 `exec_command` 在沙箱模式下**完全不可用**
|
||||||
(`helper_unknown_error: setup refresh had errors`),只有提权
|
(`helper_unknown_error: setup refresh had errors`),只有提权
|
||||||
(`require_escalated`)模式可用。沙箱模式只允许写 `D:\MCM&ICM_Toolkit`。
|
(`require_escalated`)模式可用。沙箱模式只允许写 `D:\MCM&ICM_Toolkit`。
|
||||||
@@ -650,6 +650,26 @@ Codex 侧:把这三条口径写进 7 处 `Raises`(**又是易错点 30**:
|
|||||||
测试 23 → 31 条:首列排序(升序 / 降序两条)、别名、坏指标名、坏 `models` / `cv`、每折样本数下限、`random_state` 可复现。
|
测试 23 → 31 条:首列排序(升序 / 降序两条)、别名、坏指标名、坏 `models` / `cv`、每折样本数下限、`random_state` 可复现。
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
|
### ⭐ 当前进度与待办(2026-10-18 第十一轮)
|
||||||
|
|
||||||
|
**本轮:L0 收官 —— `save_figure` 落地(Codex 写,A 档)。**
|
||||||
|
|
||||||
|
- `viz/export.py`:实现 `save_figure`。入口先校验 `fig` / `dpi` / `formats` / `bbox_inches` /
|
||||||
|
`transparent` / `path`,**全部校验通过后才建目录、写文件**;`formats` 支持大小写归一,
|
||||||
|
空 / 未知 / 重复格式一律报错;`path` 带图片后缀时自动去后缀(不会写出 `fig1.png.png`)。
|
||||||
|
- `tests/test_viz.py`:去掉违反决策 26 的模块级 `pytestmark`,改为逐条 skip;
|
||||||
|
`test_save_figure_writes_png_and_pdf` 不再依赖未实现的 `plot()`,改用 `plt.subplots()` 构造图。
|
||||||
|
新增 8 条:后缀剥离、自动建目录、格式归一与顺序、单个字符串 / 空 / 未知 / 重复格式、
|
||||||
|
校验先于副作用、坏 `dpi` / `bbox_inches` / `transparent` / 非 `Figure`。
|
||||||
|
- 三连:`pytest` **242 passed / 24 skipped**、`ruff` **0**、`pyright` **0**。
|
||||||
|
|
||||||
|
**下一步**
|
||||||
|
1. `models/regression.py` 的 `fit_linear` / `fit_polynomial`(A 档)。**开工前先定返回契约**:
|
||||||
|
`Result` 没有 `fit/predict/score`,与 `compare_models` 要求 `Model` 协议的入参对不上,
|
||||||
|
需要定成"`Result.data` 放已拟合模型 + `compare_models` 接受两种入参"之类的桥接方案。
|
||||||
|
2. `decision/entropy_weights`(S 档,用户写);Codex 先补函数卡片 / 测试骨架。
|
||||||
|
3. 集中拍板 `STATUS.md` 里挂着的小口径(`t_test` / `as_array` / `bootstrap_ci` / `markov_*` / `mm1` / 死代码)。
|
||||||
|
|
||||||
### ⭐ 当前进度与待办(2026-10-12 第十轮)
|
### ⭐ 当前进度与待办(2026-10-12 第十轮)
|
||||||
|
|
||||||
**本轮:你办掉第九轮那 4 条口径里的 3 条,Codex 落地 `compare_models`(别名映射)。**
|
**本轮:你办掉第九轮那 4 条口径里的 3 条,Codex 落地 `compare_models`(别名映射)。**
|
||||||
|
|||||||
@@ -9,36 +9,132 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
__all__ = ["save_figure", "save_table"]
|
__all__ = ["save_figure", "save_table"]
|
||||||
|
|
||||||
|
_ALLOWED_FORMATS = frozenset({"png", "pdf", "svg", "eps", "ps", "jpg", "jpeg", "tif", "tiff", "webp"})
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_formats(formats: Any) -> tuple[str, ...]:
|
||||||
|
"""校验并规范化输出格式;重复、空列表、单个字符串都在入口拦下。"""
|
||||||
|
if isinstance(formats, str):
|
||||||
|
raise TypeError("formats 必须是 tuple / list,不要传单个字符串(会把 'png' 拆成 p/n/g)")
|
||||||
|
if not isinstance(formats, (tuple, list)):
|
||||||
|
raise TypeError(f"formats 必须是 tuple / list,收到 {type(formats).__name__}")
|
||||||
|
if not formats:
|
||||||
|
raise ValueError("formats 不能为空")
|
||||||
|
|
||||||
|
normalized: list[str] = []
|
||||||
|
for fmt in formats:
|
||||||
|
if not isinstance(fmt, str):
|
||||||
|
raise TypeError(f"formats 里的每一项都必须是字符串,收到 {type(fmt).__name__}")
|
||||||
|
key = fmt.lower().lstrip(".")
|
||||||
|
if key not in _ALLOWED_FORMATS:
|
||||||
|
allowed = ", ".join(sorted(_ALLOWED_FORMATS))
|
||||||
|
raise ValueError(f"不支持的图片格式:{fmt!r};可选:{allowed}")
|
||||||
|
if key in normalized:
|
||||||
|
raise ValueError(f"formats 里有重复的格式:{key!r}")
|
||||||
|
normalized.append(key)
|
||||||
|
return tuple(normalized)
|
||||||
|
|
||||||
|
|
||||||
def save_figure(
|
def save_figure(
|
||||||
fig: Any,
|
fig: Any,
|
||||||
path: str | Path,
|
path: str | Path,
|
||||||
dpi: int = 300,
|
dpi: int | np.integer = 300,
|
||||||
formats: tuple[str, ...] = ("png", "pdf"),
|
formats: tuple[str, ...] | list[str] = ("png", "pdf"),
|
||||||
bbox_inches: str = "tight",
|
bbox_inches: str | None = "tight",
|
||||||
transparent: bool = False,
|
transparent: bool = False,
|
||||||
) -> list[Path]:
|
) -> list[Path]:
|
||||||
"""保存图,默认同时出 PNG(预览用)和 PDF(LaTeX 插图用)。
|
"""保存图,默认同时出 PNG(预览用)和 PDF(LaTeX 插图用)。
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
fig:
|
||||||
|
`matplotlib.figure.Figure`(`plt.subplots()` 的第一个返回值)。
|
||||||
|
path:
|
||||||
|
文件名**主干**。若带了 `png` / `pdf` 等图片后缀,会先去掉后缀,
|
||||||
|
再按 `formats` 逐个生成;父目录不存在时自动创建。
|
||||||
|
dpi:
|
||||||
|
输出分辨率,默认 300 —— 美赛送稿的 DPI 底线。
|
||||||
formats:
|
formats:
|
||||||
美赛论文如果用 LaTeX 排版,**矢量图(PDF/EPS)比 PNG 清晰得多**,
|
要写的格式,tuple / list,大小写不敏感,顺序决定返回顺序。
|
||||||
所以默认两种都出。
|
支持 png / pdf / svg / eps / ps / jpg / jpeg / tif / tiff / webp。
|
||||||
|
bbox_inches:
|
||||||
|
传给 `Figure.savefig`;默认 `"tight"` 去掉多余白边,`None` 用 matplotlib 默认值。
|
||||||
|
transparent:
|
||||||
|
是否透明背景。正文用图一般保持 `False`,叠加图例或封面图可以用 `True`。
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
实际写出的文件路径列表。
|
实际写出的文件路径列表,顺序与 `formats` 一致。
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
TypeError
|
||||||
|
`fig` 不是 `Figure`;`formats` 不是 tuple / list、含非字符串项;
|
||||||
|
`dpi` / `transparent` / `bbox_inches` 类型不对。
|
||||||
|
ValueError
|
||||||
|
`formats` 为空、含不支持的格式或重复格式;`dpi <= 0`;
|
||||||
|
`path` 为空或是一个目录。
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
|
所有校验都在建目录、写文件**之前**完成,传错格式不会留下空目录。
|
||||||
建议配合项目的 figure 目录约定:`figures/fig{序号}_{短名}.pdf`,
|
建议配合项目的 figure 目录约定:`figures/fig{序号}_{短名}.pdf`,
|
||||||
图号与正文引用一一对应,最后核对时不容易错位。
|
图号与正文引用一一对应,最后核对时不容易错位。
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> fig, ax = plt.subplots() # doctest: +SKIP
|
||||||
|
>>> paths = save_figure(fig, "figures/fig1")
|
||||||
|
>>> [p.suffix for p in paths]
|
||||||
|
['.png', '.pdf']
|
||||||
"""
|
"""
|
||||||
# TODO(L0)
|
if not isinstance(fig, Figure):
|
||||||
raise NotImplementedError
|
raise TypeError(f"fig 必须是 matplotlib 的 Figure,收到 {type(fig).__name__}")
|
||||||
|
|
||||||
|
if isinstance(dpi, bool) or not isinstance(dpi, (int, np.integer)):
|
||||||
|
raise TypeError(f"dpi 必须是正整数,收到 {type(dpi).__name__}")
|
||||||
|
if dpi <= 0:
|
||||||
|
raise ValueError(f"dpi 必须大于 0,收到 {dpi}")
|
||||||
|
|
||||||
|
if not isinstance(transparent, bool):
|
||||||
|
raise TypeError(f"transparent 必须是 bool,收到 {type(transparent).__name__}")
|
||||||
|
|
||||||
|
if bbox_inches is not None:
|
||||||
|
if not isinstance(bbox_inches, str):
|
||||||
|
raise TypeError(f"bbox_inches 必须是字符串或 None,收到 {type(bbox_inches).__name__}")
|
||||||
|
if not bbox_inches.strip():
|
||||||
|
raise ValueError("bbox_inches 不能是空字符串")
|
||||||
|
|
||||||
|
if not isinstance(path, (str, Path)):
|
||||||
|
raise TypeError(f"path 必须是 str 或 Path,收到 {type(path).__name__}")
|
||||||
|
base = Path(path)
|
||||||
|
if not base.name:
|
||||||
|
raise ValueError(f"path 不能为空:{path!r}")
|
||||||
|
if base.is_dir():
|
||||||
|
raise ValueError(f"path 是一个目录,请给文件名主干:{base}")
|
||||||
|
if base.suffix and base.suffix.lower().lstrip(".") in _ALLOWED_FORMATS:
|
||||||
|
base = base.with_suffix("")
|
||||||
|
|
||||||
|
normalized = _normalize_formats(formats)
|
||||||
|
base.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
written: list[Path] = []
|
||||||
|
for fmt in normalized:
|
||||||
|
target = base.with_suffix(f".{fmt}")
|
||||||
|
fig.savefig(
|
||||||
|
target,
|
||||||
|
dpi=int(dpi),
|
||||||
|
bbox_inches=bbox_inches,
|
||||||
|
transparent=transparent,
|
||||||
|
format=fmt,
|
||||||
|
)
|
||||||
|
written.append(target)
|
||||||
|
return written
|
||||||
|
|
||||||
|
|
||||||
def save_table(
|
def save_table(
|
||||||
|
|||||||
+98
-7
@@ -1,6 +1,7 @@
|
|||||||
"""viz 包测试。
|
"""viz 包测试。
|
||||||
|
|
||||||
绘图测试不比对像素——只验证"能画出来、能存成文件、尺寸符合美赛要求"。
|
绘图测试不比对像素——只验证"能画出来、能存成文件、尺寸符合美赛要求"。
|
||||||
|
未实现的函数按决策 26 **逐条** skip,不用模块级 `pytestmark`。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -11,18 +12,30 @@ matplotlib.use("Agg")
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
pytestmark = pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0/L1")
|
_NOT_IMPLEMENTED = "未实现:见 docs/03-roadmap.md L0/L1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def fig():
|
||||||
|
"""最小 Figure;测试结束统一关闭,避免 matplotlib 的"图开太多"告警。"""
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
figure, _ = plt.subplots(figsize=(2, 2))
|
||||||
|
yield figure
|
||||||
|
plt.close(figure)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
@pytest.mark.parametrize("kind", ["line", "scatter", "hist", "box", "bar"])
|
@pytest.mark.parametrize("kind", ["line", "scatter", "hist", "box", "bar"])
|
||||||
def test_plot_kinds_produce_figure(kind, rng):
|
def test_plot_kinds_produce_figure(kind, rng):
|
||||||
from mcm_toolkit.viz import plot
|
from mcm_toolkit.viz import plot
|
||||||
|
|
||||||
x = rng.normal(size=100)
|
x = rng.normal(size=100)
|
||||||
fig, ax = plot(x, x, kind=kind)
|
figure, ax = plot(x, x, kind=kind)
|
||||||
assert fig is not None and ax is not None
|
assert figure is not None and ax is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
def test_mcm_style_sets_300dpi():
|
def test_mcm_style_sets_300dpi():
|
||||||
from mcm_toolkit.viz import mcm_style
|
from mcm_toolkit.viz import mcm_style
|
||||||
|
|
||||||
@@ -31,15 +44,93 @@ def test_mcm_style_sets_300dpi():
|
|||||||
assert matplotlib.rcParams["font.size"] >= 8.0
|
assert matplotlib.rcParams["font.size"] >= 8.0
|
||||||
|
|
||||||
|
|
||||||
def test_save_figure_writes_png_and_pdf(tmp_path, rng):
|
def test_save_figure_writes_png_and_pdf(fig, tmp_path):
|
||||||
from mcm_toolkit.viz import plot, save_figure
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
fig, _ = plot(rng.normal(size=50), rng.normal(size=50), kind="scatter")
|
|
||||||
paths = save_figure(fig, tmp_path / "fig1", formats=("png", "pdf"))
|
paths = save_figure(fig, tmp_path / "fig1", formats=("png", "pdf"))
|
||||||
assert {p.suffix for p in paths} == {".png", ".pdf"}
|
assert [p.suffix for p in paths] == [".png", ".pdf"]
|
||||||
assert all(p.exists() and p.stat().st_size > 0 for p in paths)
|
assert all(p.exists() and p.stat().st_size > 0 for p in paths)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_strips_an_image_suffix_from_path(fig, tmp_path):
|
||||||
|
"""`fig1.png` 当主干用:不能写出 `fig1.png.png`。"""
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
paths = save_figure(fig, tmp_path / "fig1.png", formats=("pdf",))
|
||||||
|
assert paths == [tmp_path / "fig1.pdf"]
|
||||||
|
assert paths[0].exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_creates_parent_directories(fig, tmp_path):
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
target = tmp_path / "nested" / "deeper" / "fig1"
|
||||||
|
paths = save_figure(fig, target, formats=("png",))
|
||||||
|
assert paths[0] == tmp_path / "nested" / "deeper" / "fig1.png"
|
||||||
|
assert paths[0].exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_normalizes_format_case_and_keeps_order(fig, tmp_path):
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
paths = save_figure(fig, tmp_path / "fig1", formats=("PDF", "png"))
|
||||||
|
assert [p.suffix for p in paths] == [".pdf", ".png"]
|
||||||
|
assert all(p.exists() for p in paths)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_rejects_a_single_string_as_formats(fig, tmp_path):
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="不要传单个字符串"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", formats="png") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_rejects_bad_formats(fig, tmp_path):
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="不能为空"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", formats=())
|
||||||
|
with pytest.raises(ValueError, match="不支持"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", formats=("gif",))
|
||||||
|
with pytest.raises(ValueError, match="重复"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", formats=("png", "PNG"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_validates_before_creating_directories(fig, tmp_path):
|
||||||
|
"""格式错时不能先建目录留下副作用。"""
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
nested = tmp_path / "should_not_exist" / "fig1"
|
||||||
|
with pytest.raises(ValueError, match="不支持"):
|
||||||
|
save_figure(fig, nested, formats=("gif",))
|
||||||
|
assert not nested.parent.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_rejects_bad_render_options(fig, tmp_path):
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="dpi"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", dpi=300.0) # type: ignore[arg-type]
|
||||||
|
with pytest.raises(TypeError, match="dpi"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", dpi=True)
|
||||||
|
with pytest.raises(ValueError, match="dpi"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", dpi=0)
|
||||||
|
with pytest.raises(TypeError, match="bbox_inches"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", bbox_inches=3) # type: ignore[arg-type]
|
||||||
|
with pytest.raises(ValueError, match="bbox_inches"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", bbox_inches="")
|
||||||
|
with pytest.raises(TypeError, match="transparent"):
|
||||||
|
save_figure(fig, tmp_path / "fig1", transparent="yes") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_figure_rejects_non_figure(tmp_path):
|
||||||
|
from mcm_toolkit.viz import save_figure
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match="Figure"):
|
||||||
|
save_figure(object(), tmp_path / "fig1") # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason=_NOT_IMPLEMENTED)
|
||||||
def test_save_table_writes_latex_with_caption(tmp_path):
|
def test_save_table_writes_latex_with_caption(tmp_path):
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user