diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a8bb56..fee1cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,9 @@ `na_values` 默认吃下空串 / `NA` / `N/A` / `#N/A` / `null` / `-`。 - `data.save_table`:实现(原属 L1,顺手落地)。按扩展名分派,父目录不存在时自动创建, 返回实际写入的 `Path`。 +- `data.load_table` / `data.save_table`:docstring 补 `Parameters` / `Raises` + (`docs/02-architecture.md` §4.7 对每个公开函数的硬要求)。 +- `data.load_table` 的返回注解 `Any` → `pd.DataFrame`,与 docstring 的承诺对齐。 ### Added - `docs/02-architecture.md` §4.7「类型约定」:注解要写全、numpy 标量的判断写法、 diff --git a/STATUS.md b/STATUS.md index ae0aee6..a417c33 100644 --- a/STATUS.md +++ b/STATUS.md @@ -86,6 +86,8 @@ - `na_values` 默认吃下空串 / `NA` / `N/A` / `#N/A` / `null` / `-` - 不支持的扩展名抛 `ValueError`;文件不存在抛带路径的 `FileNotFoundError` - 测试:`tests/test_data.py` 由 1 条(且被模块级 skip 掉)扩到 **8 条** + - docstring 的 `Parameters` / `Raises` 已按 §4.7 补齐(Codex 代写,用户授权)—— + numpydoc 格式,对齐参考实现 `core/random.py` 的 `get_rng` - [ ] 剩余 9 项(10 个函数):`clean_missing` `detect_outliers` `normalize` `standardize` `describe` `correlation_matrix` `bootstrap_ci` `monte_carlo` `save_figure` @@ -155,17 +157,14 @@ | 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` 零覆盖,直到有人手动数才被发现 | +| 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 自动跳过 | ## 下一步 -1. **`data/io.py` 待你修两处**(`src/` 归你,Codex 不代写,见决策 25): - - `save_table` 把 `path.parent.mkdir(parents=True, exist_ok=True)` 放在了**后缀校验之前**, - 传个 `.txt` 会先建出一个空目录再抛 `ValueError`。 - 把校验(后缀 + `isinstance(df, pd.DataFrame)`)挪到 `mkdir` 前面即可。 - - 两个函数都缺 `Raises` 段落(`docs/02-architecture.md` §4.7 要求写全); - `load_table` 的返回注解建议从 `Any` 改成 `pd.DataFrame` —— docstring 已经承诺返回 DataFrame, - 注解写 `Any` 等于同一个契约写了两遍(`docs/dev/personal-pitfalls.md` 条目 3,本次已复发一次)。 +1. **`data/io.py` 还剩一处**:`isinstance(df, pd.DataFrame)` 已经挪到 `mkdir` 前面了, + 但**后缀校验还留在 `else` 分支里**,仍在 `mkdir` 之后。传 `sub/x.txt` 依旧会先建出 + `sub/` 再抛 `ValueError`(实测确认)。把后缀判断也提到 `mkdir` 前面即可。 2. 继续 L0,**下一个:`data/clean.py` 的 `clean_missing`**(L0 / A 档,直接能写)。 同文件的 `detect_outliers` 是 **S 档**,要先按 `docs/dev/function-card-template.md` 把函数卡片写出来再动手。 diff --git a/src/mcm_toolkit/data/io.py b/src/mcm_toolkit/data/io.py index 0c7568d..36b764a 100644 --- a/src/mcm_toolkit/data/io.py +++ b/src/mcm_toolkit/data/io.py @@ -14,8 +14,7 @@ import pandas as pd __all__ = ["load_table", "save_table"] - -def load_table(path: str | Path, **kwargs: Any) -> Any: +def load_table(path: str | Path, **kwargs: Any) -> pd.DataFrame: """按扩展名自动选择读取方式,返回 DataFrame。 约定: @@ -24,6 +23,27 @@ def load_table(path: str | Path, **kwargs: Any) -> Any: * `.xlsx` / `.xls` 需要 `openpyxl`(`pip install "mcm-toolkit[excel]"`); * `na_values` 默认包含空串、`"NA"`、`"N/A"`、`"#N/A"`、`"null"`、`"-"`。 + Parameters + ---------- + path: + 文件路径。扩展名决定用哪个读取器,大小写不敏感。 + **kwargs: + 原样转交给 `pandas.read_csv` / `pandas.read_excel`。 + `.csv` 传了 `encoding=` 就照办,不再探测;不传则按上面的顺序逐个试。 + + Raises + ------ + FileNotFoundError + 文件不存在。`.csv` 的自动探测分支会把原始路径包进中文提示里。 + ValueError + 后缀不在 `.csv` / `.xlsx` / `.xls` 里; + `.csv` 在候选编码下全部解码失败; + 或 pandas 自身的解析错误(`pandas.errors.ParserError` 也是 `ValueError` 的子类, + 报错信息原样抛出——它比转述的更有用)。 + ImportError + `.xlsx` / `.xls` 但缺可选依赖(`openpyxl`,读旧 `.xls` 另需 `xlrd`), + 见 docs/02-architecture.md §4.6:可选依赖的报错必须能直接看懂。 + Examples -------- >>> df = load_table("data.csv") # doctest: +SKIP @@ -52,14 +72,36 @@ def load_table(path: str | Path, **kwargs: Any) -> Any: def save_table(df: pd.DataFrame, path: str | Path, index: bool = False, **kwargs: Any) -> Path: - """保存 DataFrame,返回实际写入的路径。""" + """保存 DataFrame,返回实际写入的路径。 + + Parameters + ---------- + df: + 要写出的表。必须是 `pandas.DataFrame`——`Series` 也不行,先 `.to_frame()`。 + path: + 目标路径。扩展名决定用哪个写入器,大小写不敏感;父目录不存在时自动创建。 + index: + 是否把行索引一并写进文件,默认 `False`。 + **kwargs: + 原样转交给 `pandas.DataFrame.to_csv` / `to_excel`。 + + Raises + ------ + TypeError + `df` 不是 `pandas.DataFrame`。 + ValueError + 后缀不在 `.csv` / `.xlsx` / `.xls` 里。 + ImportError + 写 `.xlsx` / `.xls` 但缺 `openpyxl`。 + """ path = Path(path) - path.parent.mkdir(parents=True,exist_ok=True) suffix = path.suffix.lower() if not isinstance(df,pd.DataFrame): raise TypeError(f"df 必须是一个pandas DataFrame,但是你上传了{type(df)}") + path.parent.mkdir(parents=True,exist_ok=True) + if suffix == '.csv': df.to_csv(path,index=index,**kwargs) elif suffix in ['.xlsx','.xls']: