updated with functional changes, testing

This commit is contained in:
Rory Hinnen
2026-07-10 21:02:34 -05:00
parent 48b9dee6d6
commit c14a97331c
11 changed files with 931 additions and 381 deletions
View File
+71
View File
@@ -0,0 +1,71 @@
from epubmaker.models import Name, Series, Image, Section, Book, BuildContext
def test_name_defaults():
n = Name()
assert n.first == ""
assert n.last == ""
def test_series_defaults():
s = Series()
assert s.number == ""
assert s.name == ""
def test_image_defaults():
img = Image()
assert img.name == ""
assert img.type == ""
assert img.alt == ""
assert img.caption == ""
def test_section_defaults():
s = Section()
assert s.type == ""
assert s.name == ""
assert s.title == ""
assert s.body == ""
def test_book_defaults():
b = Book()
assert b.filename == ""
assert b.title == ""
assert b.titlesort == ""
assert b.publisher == ""
assert b.isbn == ""
assert b.uuid == ""
assert b.author == []
assert b.images == []
assert b.sections == []
def test_book_nested_objects():
b = Book()
assert isinstance(b.series, Series)
assert isinstance(b.artist, Name)
assert isinstance(b.editor, Name)
assert isinstance(b.cover, Image)
def test_book_author_lists_are_independent():
b1 = Book()
b2 = Book()
b1.author.append(Name(first="Jane", last="Doe"))
assert len(b2.author) == 0
def test_build_context_defaults():
ctx = BuildContext()
assert isinstance(ctx.book, Book)
assert ctx.verbose is False
assert ctx.in_section is False
def test_build_context_books_are_independent():
ctx1 = BuildContext()
ctx2 = BuildContext()
ctx1.book.title = "Book One"
assert ctx2.book.title == ""
+249
View File
@@ -0,0 +1,249 @@
import pytest
from epubmaker.models import BuildContext
from epubmaker.parser import extract, identify, parse_book
@pytest.fixture
def ctx():
return BuildContext()
def test_extract_returns_inner_content():
assert extract("<!-- Title: Foo Bar -->") == "Title: Foo Bar"
def test_extract_with_newline():
assert extract("<!-- Title: Foo Bar -->\n") == "Title: Foo Bar"
def test_extract_returns_none_for_non_comment():
assert extract("<p>Not a comment</p>") is None
def test_identify_directory(ctx):
identify("<!-- Directory: mybook -->", ctx)
assert ctx.book.directory == "mybook"
assert ctx.in_section is False
def test_identify_title_plain(ctx):
identify("<!-- Title: My Book -->", ctx)
assert ctx.book.title == "My Book"
assert ctx.book.titlesort == ""
def test_identify_title_the(ctx):
identify("<!-- Title: The Great Book -->", ctx)
assert ctx.book.title == "The Great Book"
assert ctx.book.titlesort == "Great Book, The"
def test_identify_title_a(ctx):
identify("<!-- Title: A Good Story -->", ctx)
assert ctx.book.title == "A Good Story"
assert ctx.book.titlesort == "Good Story, A"
def test_identify_title_an(ctx):
identify("<!-- Title: An Old Tale -->", ctx)
assert ctx.book.title == "An Old Tale"
assert ctx.book.titlesort == "Old Tale, An"
def test_identify_author(ctx):
identify("<!-- Author: Smith, John -->", ctx)
assert len(ctx.book.author) == 1
assert ctx.book.author[0].last == "Smith"
assert ctx.book.author[0].first == "John"
def test_identify_multiple_authors(ctx):
identify("<!-- Author: Smith, John -->", ctx)
identify("<!-- Author: Jones, Jane -->", ctx)
assert len(ctx.book.author) == 2
assert ctx.book.author[1].last == "Jones"
def test_identify_editor(ctx):
identify("<!-- Editor: Brown, Alice -->", ctx)
assert ctx.book.editor.last == "Brown"
assert ctx.book.editor.first == "Alice"
def test_identify_publisher(ctx):
identify("<!-- Publisher: Acme Press -->", ctx)
assert ctx.book.publisher == "Acme Press"
def test_identify_copyright(ctx):
identify("<!-- Copyright: 2024 Author -->", ctx)
assert ctx.book.copyright == "2024 Author"
def test_identify_isbn(ctx):
identify("<!-- ISBN: 978-0-000-00000-0 -->", ctx)
assert ctx.book.isbn == "978-0-000-00000-0"
def test_identify_uuid_explicit(ctx):
identify("<!-- UUID: abc-123 -->", ctx)
assert ctx.book.uuid == "abc-123"
def test_identify_uuid_empty_generates_uuid(ctx):
identify("<!-- UUID: -->", ctx)
assert ctx.book.uuid != ""
assert ctx.book.uuid is not None
def test_identify_source(ctx):
identify("<!-- Source: Some Original Work -->", ctx)
assert ctx.book.source == "Some Original Work"
def test_identify_subjects(ctx):
identify("<!-- Subjects: Fiction, Mystery -->", ctx)
assert ctx.book.subjects == "Fiction, Mystery"
def test_identify_series(ctx):
identify("<!-- Series: 1 My Series Name -->", ctx)
assert ctx.book.series.number == "1"
assert ctx.book.series.name == "My Series Name"
def test_identify_cover(ctx):
identify("<!-- Cover: cover.jpeg -->", ctx)
assert ctx.book.cover.name == "cover"
assert ctx.book.cover.type == "jpeg"
def test_identify_text_section(ctx):
identify("<!-- Text: chapter1 Chapter One -->", ctx)
assert ctx.in_section is True
assert len(ctx.book.sections) == 1
assert ctx.book.sections[0].type == "text"
assert ctx.book.sections[0].name == "chapter1"
assert ctx.book.sections[0].title == "Chapter One"
def test_identify_toc_section(ctx):
identify("<!-- TOC: toc Table of Contents -->", ctx)
assert ctx.in_section is True
assert ctx.book.sections[0].type == "toc"
def test_identify_title_page(ctx):
identify("<!-- TitlePage: tp Title Page -->", ctx)
assert ctx.book.sections[0].type == "title"
def test_identify_copyright_page(ctx):
identify("<!-- CopyrightPage: cp Copyright -->", ctx)
assert ctx.book.sections[0].type == "copyright-page"
def test_identify_dedication(ctx):
identify("<!-- Dedication: ded For someone -->", ctx)
assert ctx.book.sections[0].type == "dedication"
def test_identify_foreward(ctx):
identify("<!-- Foreward: fw Foreword -->", ctx)
assert ctx.book.sections[0].type == "forward"
def test_identify_notes(ctx):
identify("<!-- Notes: notes Notes -->", ctx)
assert ctx.book.sections[0].type == "notes"
def test_identify_acknowledgement(ctx):
identify("<!-- Acknowledgement: ack Acknowledgements -->", ctx)
assert ctx.book.sections[0].type == "acknowledgement"
def test_identify_metadata_clears_in_section(ctx):
identify("<!-- Text: ch1 Chapter One -->", ctx)
assert ctx.in_section is True
identify("<!-- Title: My Book -->", ctx)
assert ctx.in_section is False
def test_identify_image_jpeg(ctx):
identify("<!-- Text: ch1 Chapter One -->", ctx)
identify("<!-- Image: photo.jpg Alt text::Caption text -->", ctx)
assert len(ctx.book.images) == 1
assert ctx.book.images[0].name == "photo.jpg"
assert ctx.book.images[0].type == "jpeg"
assert ctx.book.images[0].alt == "Alt text"
assert ctx.book.images[0].caption == "Caption text"
def test_identify_image_png(ctx):
identify("<!-- Text: ch1 Chapter One -->", ctx)
identify("<!-- Image: photo.png Alt text::Caption -->", ctx)
assert ctx.book.images[0].type == "png"
def test_identify_image_appends_to_section_body(ctx):
identify("<!-- Text: ch1 Chapter One -->", ctx)
identify("<!-- Image: photo.jpg Alt::Caption -->", ctx)
assert "photo.jpg" in ctx.book.sections[-1].body
def test_identify_image_with_caption_uses_div(ctx):
identify("<!-- Text: ch1 Chapter One -->", ctx)
identify("<!-- Image: photo.jpg Alt::My Caption -->", ctx)
assert '<div class="center group">' in ctx.book.sections[-1].body
assert "My Caption" in ctx.book.sections[-1].body
def test_identify_image_without_caption_uses_img(ctx):
identify("<!-- Text: ch1 Chapter One -->", ctx)
identify("<!-- Image: photo.jpg Alt:: -->", ctx)
body = ctx.book.sections[-1].body
assert "<img" in body
assert '<div class="center group">' not in body
def test_identify_unknown_key_ignored(ctx):
identify("<!-- UnknownKey: some value -->", ctx)
assert ctx.book.title == ""
assert ctx.book.directory == ""
def test_parse_book_populates_book(tmp_path):
source = tmp_path / "book.html"
source.write_text(
"<!-- Directory: mybook -->\n"
"<!-- Title: Test Book -->\n"
"<!-- Author: Doe, Jane -->\n"
"<!-- Text: ch1 Chapter One -->\n"
"<p>Body content</p>\n"
)
ctx = BuildContext()
ctx.book.filename = str(source)
parse_book(ctx)
assert ctx.book.title == "Test Book"
assert ctx.book.directory == "mybook"
assert len(ctx.book.author) == 1
assert ctx.book.author[0].last == "Doe"
assert len(ctx.book.sections) == 1
assert "<p>Body content</p>\n" in ctx.book.sections[0].body
def test_parse_book_body_not_captured_outside_section(tmp_path):
source = tmp_path / "book.html"
source.write_text(
"<!-- Title: Test Book -->\n"
"<p>This is outside any section</p>\n"
"<!-- Text: ch1 Chapter One -->\n"
"<p>Inside section</p>\n"
)
ctx = BuildContext()
ctx.book.filename = str(source)
parse_book(ctx)
assert len(ctx.book.sections) == 1
assert "outside any section" not in ctx.book.sections[0].body
assert "Inside section" in ctx.book.sections[0].body
+243
View File
@@ -0,0 +1,243 @@
import os
import pytest
from unittest.mock import patch
from epubmaker.models import BuildContext, Book, Section, Image, Name
@pytest.fixture
def ctx(tmp_path):
context = BuildContext()
context.book.title = "Test Book"
context.book.uuid = "test-uuid-1234"
context.book.isbn = "978-0-000-00000-0"
context.book.publisher = "Test Publisher"
context.book.copyright = "2024 Test"
context.book.subjects = "Fiction"
context.book.source = "Original Source"
context.book.publish_date = "2024-1-1"
context.book.directory = str(tmp_path / "testbook")
context.book.filename = str(tmp_path / "source" / "book.html")
context.book.author.append(Name(first="Jane", last="Doe"))
return context
from epubmaker.writer import (
write_structure, write_misc, write_ncx, write_toc,
write_cover, write_contents, write_sections, write_illustrations,
)
# --- write_structure ---
def test_write_structure_creates_epub_directories(ctx):
write_structure(ctx, overwrite=False)
for subdir in ["META-INF", "OEBPS", "OEBPS/Images", "OEBPS/Styles", "OEBPS/Text"]:
assert os.path.isdir(os.path.join(ctx.book.directory, subdir))
def test_write_structure_exits_if_exists(ctx):
os.makedirs(ctx.book.directory)
with pytest.raises(SystemExit):
write_structure(ctx, overwrite=False)
def test_write_structure_overwrite_replaces_existing(ctx):
os.makedirs(ctx.book.directory)
sentinel = os.path.join(ctx.book.directory, "old_file.txt")
open(sentinel, "w").close()
write_structure(ctx, overwrite=True)
assert os.path.isdir(ctx.book.directory)
assert not os.path.exists(sentinel)
# --- write_misc ---
def test_write_misc_mimetype_content(ctx):
write_structure(ctx, overwrite=False)
write_misc(ctx)
with open(os.path.join(ctx.book.directory, "mimetype")) as f:
assert f.read() == "application/epub+zip"
def test_write_misc_container_xml_references_opf(ctx):
write_structure(ctx, overwrite=False)
write_misc(ctx)
with open(os.path.join(ctx.book.directory, "META-INF", "container.xml")) as f:
assert 'full-path="OEBPS/content.opf"' in f.read()
def test_write_misc_ibooks_display_options_created(ctx):
write_structure(ctx, overwrite=False)
write_misc(ctx)
path = os.path.join(ctx.book.directory, "META-INF", "com.apple.ibooks.display-options.xml")
assert os.path.isfile(path)
# --- write_ncx ---
def test_write_ncx_contains_title_and_uuid(ctx):
write_structure(ctx, overwrite=False)
write_ncx(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
content = f.read()
assert ctx.book.title in content
assert ctx.book.uuid in content
def test_write_ncx_includes_non_toc_sections(ctx):
ctx.book.sections.append(Section(type="text", name="chapter1", title="Chapter One"))
write_structure(ctx, overwrite=False)
write_ncx(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
content = f.read()
assert "chapter1" in content
assert "Chapter One" in content
def test_write_ncx_excludes_toc_type_sections(ctx):
ctx.book.sections.append(Section(type="toc", name="toc", title="Table of Contents"))
write_structure(ctx, overwrite=False)
write_ncx(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
content = f.read()
assert 'id ="toc"' not in content
def test_write_ncx_play_order_increments(ctx):
for i in range(3):
ctx.book.sections.append(Section(type="text", name=f"ch{i}", title=f"Chapter {i}"))
write_structure(ctx, overwrite=False)
write_ncx(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
content = f.read()
assert 'playOrder="2"' in content
assert 'playOrder="3"' in content
assert 'playOrder="4"' in content
# --- write_toc ---
def test_write_toc_writes_section_body(ctx):
section = Section(body="<p>Contents here</p>\n")
write_structure(ctx, overwrite=False)
write_toc(ctx, section)
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.xhtml")) as f:
content = f.read()
assert "<p>Contents here</p>" in content
assert "Table of Contents" in content
# --- write_cover ---
def test_write_cover_creates_xhtml_with_title(ctx, tmp_path):
source_images = tmp_path / "source" / "Images"
source_images.mkdir(parents=True)
(source_images / "cover.jpeg").write_bytes(b"fake")
write_structure(ctx, overwrite=False)
write_cover(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "Text", "cover.xhtml")) as f:
content = f.read()
assert ctx.book.title in content
assert 'src="../Images/cover.jpeg"' in content
def test_write_cover_copies_image_to_output(ctx, tmp_path):
source_images = tmp_path / "source" / "Images"
source_images.mkdir(parents=True)
(source_images / "cover.jpeg").write_bytes(b"image-data")
write_structure(ctx, overwrite=False)
write_cover(ctx)
assert os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Images", "cover.jpeg"))
# --- write_sections ---
def test_write_sections_creates_xhtml_per_section(ctx):
ctx.book.sections.append(Section(type="text", name="chapter1", title="Chapter One", body="<p>Content</p>\n"))
write_structure(ctx, overwrite=False)
write_sections(ctx)
path = os.path.join(ctx.book.directory, "OEBPS", "Text", "chapter1.xhtml")
assert os.path.isfile(path)
with open(path) as f:
content = f.read()
assert "Chapter One" in content
assert "<p>Content</p>" in content
def test_write_sections_routes_toc_to_toc_xhtml(ctx):
ctx.book.sections.append(Section(type="toc", name="toc", title="Table of Contents", body="<p>toc</p>\n"))
write_structure(ctx, overwrite=False)
write_sections(ctx)
assert os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "toc.xhtml"))
assert not os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Text", "toc.xhtml"))
def test_write_sections_multiple_sections(ctx):
for i in range(3):
ctx.book.sections.append(Section(type="text", name=f"ch{i}", title=f"Chapter {i}", body=f"<p>ch{i}</p>\n"))
write_structure(ctx, overwrite=False)
write_sections(ctx)
for i in range(3):
assert os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Text", f"ch{i}.xhtml"))
# --- write_illustrations ---
def test_write_illustrations_skips_when_no_images(ctx):
write_structure(ctx, overwrite=False)
write_illustrations(ctx)
assert not os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Text", "illustrations.xhtml"))
def test_write_illustrations_creates_file_with_image_links(ctx):
ctx.book.images.append(Image(name="photo.jpg", caption="A scenic photo"))
write_structure(ctx, overwrite=False)
write_illustrations(ctx)
path = os.path.join(ctx.book.directory, "OEBPS", "Text", "illustrations.xhtml")
assert os.path.isfile(path)
with open(path) as f:
content = f.read()
assert "photo.jpg" in content
assert "A scenic photo" in content
# --- write_contents ---
def test_write_contents_creates_opf_with_metadata(ctx):
write_structure(ctx, overwrite=False)
write_contents(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
content = f.read()
assert ctx.book.title in content
assert ctx.book.publisher in content
assert ctx.book.isbn in content
def test_write_contents_includes_section_in_manifest(ctx):
ctx.book.sections.append(Section(type="text", name="chapter1", title="Chapter One"))
write_structure(ctx, overwrite=False)
write_contents(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
content = f.read()
assert "chapter1.xhtml" in content
def test_write_contents_includes_series_metadata(ctx):
ctx.book.series.name = "My Series"
ctx.book.series.number = "1"
write_structure(ctx, overwrite=False)
write_contents(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
content = f.read()
assert "My Series" in content
assert "calibre:series" in content
def test_write_contents_includes_illustrations_when_images_present(ctx):
ctx.book.images.append(Image(name="photo.jpg", type="jpeg", caption="caption"))
write_structure(ctx, overwrite=False)
with patch("epubmaker.writer.copy"):
write_contents(ctx)
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
content = f.read()
assert "illustrations.xhtml" in content