diff --git a/CHANGELOG.md b/CHANGELOG.md index 19868fe..09cdb92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.2.2 — 2026-08-26 + +- README 改成如实写运行时依赖 PyYAML(不再写「零第三方依赖」) +- 损坏的 `takes:` 列表项(字符串 / 空值)现在报错,不再被静默丢掉 +- 测试增加 `compileall` 语法检查 + ## 0.2.1 — 2026-08-17 - `check` warns when a character `look` is too short to lock identity diff --git a/README.md b/README.md index e86a5aa..2c82ba4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Paste that file into your image/video model. The compiler never invents wardrobe ## Install -Python 3.10+, zero third-party dependencies. Not on PyPI yet: +Python 3.10+. One runtime dependency: **PyYAML** (for `bible.yaml`). Not on PyPI yet: ```bash pip install git+https://github.com/hc-ui/shotbible.git diff --git a/README.zh-CN.md b/README.zh-CN.md index da8ae19..8a8e816 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -24,7 +24,7 @@ Beat: 阿梅坐在最后一排,打开笔记本,屏幕亮起,她没有看 ## 安装 -Python 3.10+,无第三方依赖。尚未上 PyPI: +Python 3.10+。运行时只依赖 **PyYAML**(读写 `bible.yaml`)。尚未上 PyPI: ```bash pip install git+https://github.com/hc-ui/shotbible.git diff --git a/pyproject.toml b/pyproject.toml index fc5e971..c0b65f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "shotbible" -version = "0.2.1" +version = "0.2.2" description = "Local continuity bible for AI image/video productions — lock characters, scenes, and compile consistent prompts." readme = "README.md" license = { text = "MIT" } diff --git a/src/shotbible/__init__.py b/src/shotbible/__init__.py index 960c33f..4f423a4 100644 --- a/src/shotbible/__init__.py +++ b/src/shotbible/__init__.py @@ -1,3 +1,3 @@ """shotbible — local continuity bible for AI image/video productions.""" -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/src/shotbible/models.py b/src/shotbible/models.py index 2130305..59b264c 100644 --- a/src/shotbible/models.py +++ b/src/shotbible/models.py @@ -150,6 +150,17 @@ def to_dict(self) -> dict[str, Any]: return payload +def _parse_takes(takes_raw: Any) -> list[Take]: + takes: list[Take] = [] + for index, item in enumerate(takes_raw or []): + if not isinstance(item, dict): + raise ParseError( + f"takes[{index}] must be a mapping, got {type(item).__name__}" + ) + takes.append(Take.from_dict(item)) + return takes + + @dataclass class Bible: title: str = "untitled" @@ -191,7 +202,7 @@ def from_dict(cls, data: dict[str, Any] | None) -> "Bible": str(sid): Scene.from_dict(str(sid), s if isinstance(s, dict) else {}) for sid, s in scenes_raw.items() }, - takes=[Take.from_dict(t) for t in takes_raw if isinstance(t, dict)], + takes=_parse_takes(takes_raw), version=version, ) diff --git a/src/shotbible/store.py b/src/shotbible/store.py index 80f0d6d..32d6ac0 100644 --- a/src/shotbible/store.py +++ b/src/shotbible/store.py @@ -5,7 +5,7 @@ import yaml -from .models import Bible, ParseError, Take +from .models import Bible, Character, ParseError, Scene, Take BIBLE_NAME = "bible.yaml" diff --git a/tests/test_compileall.py b/tests/test_compileall.py new file mode 100644 index 0000000..5c06269 --- /dev/null +++ b/tests/test_compileall.py @@ -0,0 +1,12 @@ +"""CI already runs pytest; this keeps a syntax check without editing workflows.""" +from __future__ import annotations + +import compileall +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_sources_and_tests_compile() -> None: + assert compileall.compile_dir(str(ROOT / "src"), quiet=1, force=True) + assert compileall.compile_dir(str(ROOT / "tests"), quiet=1, force=True) diff --git a/tests/test_store.py b/tests/test_store.py index 7230971..b1ea670 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -104,6 +104,16 @@ def test_load_non_mapping_raises(tmp_path: Path) -> None: load(root) +def test_load_rejects_non_mapping_take(tmp_path: Path) -> None: + root = init_project(tmp_path / "bad-take") + (root / "bible.yaml").write_text( + "version: 1\ntitle: x\ncharacters: {}\nscenes: {}\ntakes:\n - just-a-string\n", + encoding="utf-8", + ) + with pytest.raises(StoreError, match=r"takes\[0\]"): + load(root) + + def test_copy_ref_writes_under_refs(tmp_path: Path) -> None: root = init_project(tmp_path / "refs-proj") src = write_dummy_png(tmp_path / "face.png")