Coverage for src/epublib/package/spine.py: 95%
62 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-30 17:43 -0300
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-30 17:43 -0300
1from dataclasses import dataclass
2from typing import Annotated, ClassVar, override
4from epublib.identifier import EPUBId
5from epublib.xml_element import XMLAttribute, XMLElement, XMLParent
8@dataclass(kw_only=True)
9class SpineItemRef(XMLElement):
10 """An item reference in the EPUB spine."""
12 idref: Annotated[EPUBId, XMLAttribute()]
13 id: Annotated[str | None, XMLAttribute()] = None
14 linear: Annotated[bool | None, XMLAttribute()] = None
15 properties: Annotated[str | None, XMLAttribute()] = None
17 tag_name: ClassVar[str] = "itemref"
19 @property
20 def pk(self) -> EPUBId:
21 return self.idref
23 @override
24 def create_tag(self) -> None:
25 super().create_tag()
26 if self.linear:
27 del self.tag["linear"]
29 @override
30 def __post_init__(self):
31 super().__post_init__()
32 self.idref = EPUBId(self.idref)
35class BookSpine(XMLParent[SpineItemRef]):
36 """The EPUB spine, which defines the linear reading order of the book."""
38 tag_name: str | None = "spine"
39 default_item_type: type[SpineItemRef] = SpineItemRef
41 @override
42 def add( # type: ignore[reportIncompatibleMethodOverride]
43 self,
44 idref: str | EPUBId,
45 id: str | None = None,
46 linear: bool | None = None,
47 properties: str | None = None,
48 ):
49 __ = self.add(idref=idref, id=id, linear=linear, properties=properties)
51 @override
52 def insert( # type: ignore[reportIncompatibleMethodOverride]
53 self,
54 position: int,
55 idref: str | EPUBId,
56 id: str | None = None,
57 linear: bool | None = None,
58 properties: str | None = None,
59 ):
60 __ = self.insert(
61 position,
62 idref,
63 id,
64 linear,
65 properties,
66 )
68 def get_position(self, idref: str | EPUBId) -> int | None:
69 return next(
70 (i for i, item in enumerate(self.items) if item.idref == idref), None
71 )
73 @override
74 def remove(self, idref: str | EPUBId): # type: ignore[reportIncompatibleMethodOverride]
75 self.remove_item(self[idref])
77 def _move_tag(self, item: SpineItemRef, new_position: int):
78 tags = list(self.tag.find_all("itemref"))
79 successor = tags[new_position]
80 actual_new_position = self.tag.index(successor)
82 __ = item.tag.extract()
83 __ = self.tag.insert(actual_new_position, item.tag)
85 def move_item(self, item: int | str | SpineItemRef, new_position: int):
86 if isinstance(item, (str, int)):
87 item = self[item]
88 else:
89 if item not in self.items:
90 raise ValueError(f"Item {item} not in spine")
92 self._items.remove(item)
93 self._items.insert(new_position, item)
94 self._move_tag(item, new_position)
96 def reorder(self, items: list[SpineItemRef]):
97 new_items_set = {item.idref for item in items}
98 curr_items_set = {item.idref for item in self.items}
100 if len(new_items_set) != len(items):
101 raise ValueError("Duplicate items in new order")
103 if new_items_set != curr_items_set:
104 raise ValueError("Items do not match current spine items")
106 self._items: list[SpineItemRef] = items
108 self.tag.clear()
109 for item in items:
110 __ = self.tag.append(item.tag)