生信入门/tools/datasets.py(148 行)+ conftest.py(61 行)+ tools/course.py(172 行)带 sha256 的数据下载(datasets.py 第 121-148 行):
def fetch(key: str, log=print) -> Path:
d = DATASETS[key]
dest = path_of(key)
if dest.is_file() and (not d.sha256 or sha256_of(dest) == d.sha256):
log(f" 已有 {d.path}")
else:
part = dest.with_suffix(dest.suffix + ".part") # 先下到 .part
with urllib.request.urlopen(req, timeout=120) as resp, open(part, "wb") as out:
shutil.copyfileobj(resp, out)
got = sha256_of(part)
if d.sha256 and got != d.sha256: # 校验
part.unlink() # 失败删除,不留坏文件
raise RuntimeError(f"{d.path} 校验失败:期望 sha256={d.sha256},实际 {got}。\n"
"可能是网络中断或上游文件变了,重新运行一次;仍失败请告诉助手。")
part.replace(dest) # 校验通过才改名
TODO 清单机制(conftest.py 第 45-61 行)——最值得学的一段:
def pytest_runtest_makereport(item, call):
# 模板里没填的函数会抛 NotImplementedError。记下来,结尾单独列一张"还没写"清单,
# 一眼看出"是还没写"而不是"写错了"。
if call.excinfo is not None and call.excinfo.errisinstance(NotImplementedError):
item.user_properties.append(("todo", str(call.excinfo.value)))
def pytest_terminal_summary(terminalreporter):
todos = []
for rep in terminalreporter.stats.get("failed", []) + terminalreporter.stats.get("error", []):
for key, value in getattr(rep, "user_properties", []):
if key == "todo":
todos.append((rep.nodeid.split("::")[-1], value))
if todos:
terminalreporter.section("还没写的 TODO(不是写错,是还没填)")
for test, msg in todos:
terminalreporter.write_line(f" {test:<40} {msg}")
三态实现开关(course.py 第 32、43-50、96-107 行):
IMPL_DIRS = {"work": "交付", "ref": "参考解", "template": "模板"}
def load_module(n: int, impl: str):
# 用 importlib 按路径加载指定实现,并起不同的模块名避免冲突
...
course check N --ref 应全过,--template 应全卡 TODO——课程开发者用它自检。
.part 后校验再改名:半截文件永不被当成正式数据(原子性,见第 19 课)user_properties + terminal_summary 钩子,把NotImplementedError 单独汇总——把产品体验做进测试输出
datasets.py:零依赖(标准库),函数级可直接用conftest.py TODO 机制:pytest 通用,可直接搬到任何"教学/练习"仓库checkable-dataset:登记数据(URL+sha256+解压)→ 一键获取与校验pytest-todo-stub(发 PyPI 也能用)--template 输出的截图dvc;你的实现是零依赖教学版第 20 课(数据分析实验工程化)