"Fluent Python, 2nd Edition" is one book with a field for the edition. Left in the title it also splits the library, since the second edition never looks like the first. Only a numbered statement is moved, so "Catch 22" keeps its number and "Global Edition" — which has nowhere to go in an integer column — stays put.
175 lines
6.8 KiB
Python
175 lines
6.8 KiB
Python
import pytest
|
|
from pathlib import Path
|
|
from datetime import date
|
|
from chitai.services.metadata_extractor import (
|
|
EpubExtractor,
|
|
Extractor,
|
|
PdfExtractor,
|
|
split_edition,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
class TestEpubExtractor:
|
|
async def test_extraction_by_path(self):
|
|
path = Path("tests/data_files/Moby Dick; Or, The Whale - Herman Melville.epub")
|
|
|
|
metadata = await EpubExtractor.extract_metadata(path)
|
|
|
|
assert metadata["title"] == "Moby Dick; Or, The Whale"
|
|
assert metadata["authors"] == ["Herman Melville"]
|
|
assert metadata["published_date"] == date(year=2001, month=7, day=1)
|
|
|
|
|
|
|
|
EPUB = Path("tests/data_files/Metamorphosis - Franz Kafka.epub")
|
|
PDF = Path("tests/data_files/Calculus Made Easy - Silvanus Thompson.pdf")
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
class TestIdentifierMerging:
|
|
"""A book's formats each contribute identifiers; none of them replaces the rest."""
|
|
|
|
async def test_every_format_contributes(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""
|
|
Identifiers are a collection, not a single value.
|
|
|
|
Merging the whole dict meant the last format to report won outright: an EPUB
|
|
declaring an ASIN, a Google volume id and a Calibre id kept none of them once
|
|
a PDF contributed one ISBN.
|
|
"""
|
|
|
|
async def epub(_file):
|
|
return {
|
|
"title": "How Linux Works",
|
|
"identifiers": {"isbn-13": "9781718500419", "asin": "1718500408"},
|
|
}
|
|
|
|
async def pdf(_file):
|
|
return {"identifiers": {"isbn-13": "9781718500402", "isbn-10": "1593270356"}}
|
|
|
|
monkeypatch.setattr(EpubExtractor, "extract_metadata", epub)
|
|
monkeypatch.setattr(PdfExtractor, "extract_metadata", pdf)
|
|
|
|
metadata = await Extractor.extract_metadata(
|
|
[Path("How Linux Works.epub"), Path("How Linux Works.pdf")]
|
|
)
|
|
|
|
assert metadata["identifiers"] == {
|
|
# Declared by the publisher's toolchain, so it outranks the PDF's, which
|
|
# was scraped off a copyright page that also prints the print edition's.
|
|
"isbn-13": "9781718500419",
|
|
"asin": "1718500408",
|
|
"isbn-10": "1593270356",
|
|
}
|
|
|
|
async def test_a_second_format_that_finds_nothing_erases_nothing(self) -> None:
|
|
"""The PDF fixture carries no ISBN, so it must leave the EPUB's alone."""
|
|
metadata = await Extractor.extract_metadata([EPUB, PDF])
|
|
|
|
assert metadata["identifiers"] == {"id": "http://www.gutenberg.org/5200"}
|
|
|
|
async def test_one_format_on_its_own_is_unaffected(self) -> None:
|
|
metadata = await Extractor.extract_metadata([EPUB])
|
|
|
|
assert metadata["identifiers"] == {"id": "http://www.gutenberg.org/5200"}
|
|
|
|
async def test_no_identifiers_anywhere_leaves_the_field_absent(self) -> None:
|
|
"""An empty dict would count as extracted metadata and overwrite nothing."""
|
|
metadata = await Extractor.extract_metadata([PDF])
|
|
|
|
assert "identifiers" not in metadata
|
|
|
|
|
|
class TestSplitEdition:
|
|
"""An edition is a field on the book, not part of what the book is called."""
|
|
|
|
@pytest.mark.parametrize(
|
|
("title", "stripped", "edition"),
|
|
[
|
|
# Every form below is one that turned up in a real library.
|
|
("Fluent Python, 2nd Edition", "Fluent Python", 2),
|
|
("Building Microservices, 2E", "Building Microservices", 2),
|
|
("Digital Image Processing, 4e", "Digital Image Processing", 4),
|
|
(
|
|
"Network Security Essentials: Applications and Standards/6e",
|
|
"Network Security Essentials: Applications and Standards",
|
|
6,
|
|
),
|
|
(
|
|
"Refactoring: Improving the Design of Existing Code (2nd edition)",
|
|
"Refactoring: Improving the Design of Existing Code",
|
|
2,
|
|
),
|
|
# Ordinal words, including a qualifier sitting inside the statement.
|
|
(
|
|
"The Art of Computer Programming: Volume 1 / Fundamental Algorithms, Third Edition",
|
|
"The Art of Computer Programming: Volume 1 / Fundamental Algorithms",
|
|
3,
|
|
),
|
|
(
|
|
"Introduction to the Theory of Computation, Third International Edition",
|
|
"Introduction to the Theory of Computation",
|
|
3,
|
|
),
|
|
# Mid-title, before a subtitle and before a trailing author.
|
|
(
|
|
"How Linux Works, 3rd Edition: What Every Superuser Should Know",
|
|
"How Linux Works: What Every Superuser Should Know",
|
|
3,
|
|
),
|
|
(
|
|
"Code Complete, 2nd Edition - Steve McConnell",
|
|
"Code Complete - Steve McConnell",
|
|
2,
|
|
),
|
|
# An underscore between the number and the "e", beside an unnumbered
|
|
# qualifier that has nowhere to go in an integer column and so stays put.
|
|
(
|
|
"Cryptography and Network Security, Global Edition, 8_e - Stallings",
|
|
"Cryptography and Network Security, Global Edition - Stallings",
|
|
8,
|
|
),
|
|
],
|
|
)
|
|
def test_editions_are_split_out(self, title: str, stripped: str, edition: int) -> None:
|
|
assert split_edition(title) == (stripped, edition)
|
|
|
|
@pytest.mark.parametrize(
|
|
"title",
|
|
[
|
|
# A number alone is never an edition — these are titles.
|
|
"Catch 22",
|
|
"Fahrenheit 451",
|
|
"Blade Runner 2049",
|
|
"1984",
|
|
"Apollo 13",
|
|
"Slaughterhouse 5",
|
|
"The Art of Computer Programming: Volume 1",
|
|
# "Edition" with no number cannot be stored, so it stays where it can
|
|
# still be read.
|
|
"Cryptography and Network Security: Principles and Practice, Global Edition",
|
|
"Building Microservices",
|
|
],
|
|
)
|
|
def test_titles_are_left_alone(self, title: str) -> None:
|
|
assert split_edition(title) == (title, None)
|
|
|
|
def test_a_title_that_is_only_an_edition_is_kept(self) -> None:
|
|
"""Stripping must never leave a book with no title at all."""
|
|
assert split_edition("2nd Edition") == ("2nd Edition", None)
|
|
|
|
@pytest.mark.parametrize("title", ["", None])
|
|
def test_nothing_yields_nothing(self, title: str | None) -> None:
|
|
assert split_edition(title) == (title, None)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
class TestEditionFromFiles:
|
|
async def test_extraction_moves_the_edition_off_the_title(self) -> None:
|
|
"""The PDF fixture calls itself a 2nd edition in its own metadata title."""
|
|
metadata = await Extractor.extract_metadata([PDF])
|
|
|
|
assert metadata["title"] == "The Project Gutenberg eBook #33283: Calculus Made Easy"
|
|
assert metadata["edition"] == 2
|