266 lines
9.0 KiB
Python
266 lines
9.0 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import contextlib
|
|
import importlib.util
|
|
import io
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
MODULE_PATH = REPO_ROOT / "tools" / "import-content.py"
|
|
SPEC = importlib.util.spec_from_file_location("import_content", MODULE_PATH)
|
|
assert SPEC is not None and SPEC.loader is not None
|
|
import_content = importlib.util.module_from_spec(SPEC)
|
|
sys.modules[SPEC.name] = import_content
|
|
SPEC.loader.exec_module(import_content)
|
|
|
|
|
|
def args_for(**overrides: object) -> argparse.Namespace:
|
|
values: dict[str, object] = {
|
|
"type": "page",
|
|
"title_prefix": None,
|
|
"date": None,
|
|
"tags": None,
|
|
"author": None,
|
|
"field": [],
|
|
"collection": None,
|
|
"overwrite": False,
|
|
}
|
|
values.update(overrides)
|
|
return argparse.Namespace(**values)
|
|
|
|
|
|
class ImportContentTests(unittest.TestCase):
|
|
def test_author_override_uses_authors_list(self) -> None:
|
|
doc = import_content.Document(
|
|
"Title\n\nBody", {"authors": ["Previous Author"]}
|
|
)
|
|
|
|
import_content.apply_schema(
|
|
[doc], args_for(author="Ada Lovelace")
|
|
)
|
|
|
|
self.assertEqual(doc.meta["authors"], ["Ada Lovelace"])
|
|
|
|
def test_duplicate_output_destinations_are_rejected(self) -> None:
|
|
docs = [
|
|
import_content.Document("First", {"title": "Same"}),
|
|
import_content.Document("Second", {"title": "Same"}),
|
|
]
|
|
|
|
with self.assertRaisesRegex(
|
|
import_content.ContentImportError,
|
|
"duplicate output destination content/same.md",
|
|
):
|
|
import_content.assemble_file_entries(docs, args_for())
|
|
|
|
def test_explicit_source_slugs_determine_output_paths(self) -> None:
|
|
docs = [
|
|
import_content.Document(
|
|
"First", {"title": "Same", "slug": "source-one"}
|
|
),
|
|
import_content.Document(
|
|
"Second", {"title": "Same", "slug": "source-two"}
|
|
),
|
|
]
|
|
|
|
entries = import_content.assemble_file_entries(docs, args_for())
|
|
|
|
self.assertEqual(
|
|
{path.as_posix() for path, _content, _label in entries},
|
|
{"content/source-one.md", "content/source-two.md"},
|
|
)
|
|
|
|
def test_collections_support_every_content_type(self) -> None:
|
|
expected = {
|
|
"essay": (
|
|
"content/essays/cycle",
|
|
"/essays/cycle/",
|
|
),
|
|
"blog": (
|
|
"content/blog/cycle",
|
|
"/blog/cycle/",
|
|
),
|
|
"fiction": (
|
|
"content/fiction/cycle",
|
|
"/fiction/cycle/",
|
|
),
|
|
"poetry": (
|
|
"content/poetry/cycle",
|
|
"/poetry/cycle/",
|
|
),
|
|
"page": (
|
|
"content/cycle",
|
|
"/cycle/",
|
|
),
|
|
}
|
|
|
|
for content_type, (directory, collection_url) in expected.items():
|
|
with self.subTest(content_type=content_type):
|
|
doc = import_content.Document("Body", {"title": "Piece"})
|
|
entries = import_content.assemble_file_entries(
|
|
[doc],
|
|
args_for(type=content_type, collection="Cycle"),
|
|
)
|
|
|
|
self.assertEqual(
|
|
{path.as_posix() for path, _content, _label in entries},
|
|
{f"{directory}/piece.md", f"{directory}/index.md"},
|
|
)
|
|
self.assertEqual(
|
|
doc.meta["collection-url"], collection_url
|
|
)
|
|
|
|
def test_page_collection_rejects_reserved_section_slug(self) -> None:
|
|
docs = [import_content.Document("Body", {"title": "Piece"})]
|
|
|
|
with self.assertRaisesRegex(
|
|
import_content.ContentImportError,
|
|
"conflicts with a reserved content directory",
|
|
):
|
|
import_content.assemble_file_entries(
|
|
docs, args_for(type="page", collection="Fiction")
|
|
)
|
|
|
|
def test_writing_types_require_valid_iso_dates(self) -> None:
|
|
with self.assertRaisesRegex(
|
|
import_content.ContentImportError,
|
|
"a date in YYYY-MM-DD format is required",
|
|
):
|
|
import_content.apply_schema(
|
|
[import_content.Document("Body", {"title": "Story"})],
|
|
args_for(type="fiction"),
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
import_content.ContentImportError, "invalid date"
|
|
):
|
|
import_content.apply_schema(
|
|
[import_content.Document("Body", {"title": "Story"})],
|
|
args_for(type="fiction", date="2026-02-30"),
|
|
)
|
|
|
|
def test_opening_prose_is_not_consumed_as_a_title(self) -> None:
|
|
body = "This is the opening sentence.\nThe paragraph continues here."
|
|
doc = import_content.Document(body)
|
|
|
|
import_content.apply_schema([doc], args_for())
|
|
|
|
self.assertEqual(doc.meta["title"], "Untitled")
|
|
self.assertEqual(doc.body, body)
|
|
self.assertEqual(
|
|
doc.meta["abstract"],
|
|
"This is the opening sentence. The paragraph continues here.",
|
|
)
|
|
|
|
def test_invalid_regex_exits_without_writing(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
source = root / "source.txt"
|
|
source.write_text("Title\n\nBody\n", encoding="utf-8")
|
|
stdout = io.StringIO()
|
|
stderr = io.StringIO()
|
|
|
|
with contextlib.chdir(root), contextlib.redirect_stdout(stdout), \
|
|
contextlib.redirect_stderr(stderr):
|
|
status = import_content.main([
|
|
str(source), "--splitter", "regex", "--regex", "(",
|
|
])
|
|
|
|
self.assertEqual(status, 2)
|
|
self.assertIn("error: invalid regex pattern", stderr.getvalue())
|
|
self.assertFalse((root / "content").exists())
|
|
|
|
def test_consecutive_headings_keep_the_nonempty_section_title(self) -> None:
|
|
docs = import_content.split_heading_1(
|
|
import_content.Document("# First\n# Second\nSecond body"), {}
|
|
)
|
|
|
|
self.assertEqual(len(docs), 1)
|
|
self.assertEqual(docs[0].meta, {"number": 1, "title": "Second"})
|
|
self.assertEqual(docs[0].body, "Second body")
|
|
|
|
def test_page_break_handles_form_feed_and_preserves_no_match(self) -> None:
|
|
split = import_content.split_page_break(
|
|
import_content.Document("first\fsecond"), {}
|
|
)
|
|
untouched = import_content.Document("Body only")
|
|
|
|
self.assertEqual(
|
|
[(doc.meta, doc.body) for doc in split],
|
|
[({"number": 1}, "first"), ({"number": 2}, "second")],
|
|
)
|
|
self.assertEqual(
|
|
import_content.split_page_break(untouched, {}), [untouched]
|
|
)
|
|
|
|
def test_frontmatter_must_be_a_yaml_mapping(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
source = root / "source.md"
|
|
source.write_text(
|
|
"---\nOrdinary paragraph.\n---\nThis survives.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with contextlib.chdir(root), self.assertRaisesRegex(
|
|
import_content.ContentImportError,
|
|
"frontmatter must be a YAML mapping",
|
|
):
|
|
import_content.read_file_per_document(["source.md"])
|
|
|
|
def test_structured_fields_are_validated(self) -> None:
|
|
with self.assertRaisesRegex(
|
|
import_content.ContentImportError,
|
|
"json entry 0 field 'body' must be a string",
|
|
):
|
|
import_content._documents_from_records(
|
|
[{"title": "Bad", "body": 42}], Path("source.json"), "json"
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
import_content.ContentImportError,
|
|
"field 'number' must be a positive integer",
|
|
):
|
|
import_content.apply_schema(
|
|
[
|
|
import_content.Document(
|
|
"Body", {"title": "Bad", "number": "1"}
|
|
)
|
|
],
|
|
args_for(),
|
|
)
|
|
|
|
def test_collection_index_frontmatter_is_valid_yaml(self) -> None:
|
|
doc = import_content.Document(
|
|
"Body",
|
|
{
|
|
"title": "Piece",
|
|
"date": "2026-07-17",
|
|
"tags": ["poetry"],
|
|
"poet": "Ada Lovelace",
|
|
},
|
|
)
|
|
|
|
rendered = import_content.generate_collection_index(
|
|
[doc], "Essays: 2026"
|
|
)
|
|
metadata = import_content.yaml.safe_load(rendered.split("---", 2)[1])
|
|
|
|
self.assertEqual(metadata["title"], "Essays: 2026")
|
|
self.assertEqual(metadata["tags"], ["poetry"])
|
|
|
|
def test_abstract_respects_maximum_length_without_spaces(self) -> None:
|
|
abstract = import_content.auto_abstract("x" * 250, max_chars=200)
|
|
|
|
self.assertEqual(len(abstract), 200)
|
|
self.assertTrue(abstract.endswith(" …"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|