From 73215df2e8c415c1b19c64cd99b277254db92b80 Mon Sep 17 00:00:00 2001 From: Daniel Fichtinger Date: Sat, 21 Jun 2025 16:53:24 -0400 Subject: [PATCH] add better metadata handling --- src/zona/builder.py | 38 +++++++++++++++++++++++++++--------- src/zona/models.py | 2 +- tests/test_builder.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/zona/builder.py b/src/zona/builder.py index c60599b..01b4785 100644 --- a/src/zona/builder.py +++ b/src/zona/builder.py @@ -3,18 +3,38 @@ from zona import markdown as zmd from zona import util from pathlib import Path import frontmatter -from dacite import from_dict +from dacite import Config, from_dict, DaciteError +from datetime import date +from yaml import YAMLError def split_metadata(path: Path) -> tuple[Metadata, str]: - post = frontmatter.load(str(path)) - # TODO: handle missing metadata - meta = post.metadata - # TODO: handle malformed metadata - metadata = from_dict( - data_class=Metadata, - data=meta, - ) + """ + Parses a file and returns parsed Metadata and its content. Defaults + are applied for missing fields. If there is no metadata, a Metadata + with default values is returned. + + Raises: + ValueError: If the metadata block is malformed in any way. + """ + try: + post = frontmatter.load(str(path)) + except YAMLError as e: + raise ValueError(f"YAML frontmatter error in {path}: {e}") + raw_meta = post.metadata or {} + defaults = { + "title": util.filename_to_title(path), + "date": date.fromtimestamp(path.stat().st_mtime), + } + meta = {**defaults, **raw_meta} + try: + metadata = from_dict( + data_class=Metadata, + data=meta, + config=Config(check_types=True, strict=True), + ) + except DaciteError as e: + raise ValueError(f"Malformed metadata in {path}: {e}") return metadata, post.content diff --git a/src/zona/models.py b/src/zona/models.py index 2351abd..29dafee 100644 --- a/src/zona/models.py +++ b/src/zona/models.py @@ -9,7 +9,7 @@ from typing import Literal class Metadata: title: str date: date - description: str + description: str | None # add more options later... diff --git a/tests/test_builder.py b/tests/test_builder.py index 199d36f..f61f7ee 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,3 +1,4 @@ +import pytest from datetime import date from zona.models import Metadata from zona.builder import split_metadata, discover, build @@ -24,6 +25,50 @@ description: This is a test. assert content == "# Hello World" +def test_no_metadata(tmp_path: Path): + content = "# Hello World" + test_file = tmp_path / "hello-world.md" + test_file.write_text(content) + meta, content = split_metadata(test_file) + + assert isinstance(meta, Metadata) + assert meta.title == "Hello World" + assert meta.description is None + assert meta.date == date.today() + assert content == "# Hello World" + + +def test_malformed_metadata(tmp_path: Path): + with pytest.raises(ValueError): + tests = { + """ +--- +title: Test Post +date: not a date!!! +description: This is a test. +--- + """, + """ +--- +title: Test Post +foobar: + something: what??? +description: This is a test. +--- + """, + """ +--- +title Test Post +description: This is a test. +--- + """, + } + for i, content in enumerate(tests): + test_file = tmp_path / str(i) + test_file.write_text(content) + split_metadata(test_file) + + def test_discover(tmp_path: Path): contentd = tmp_path / "content" staticd = contentd / "static"