Coverage for src/epublib/package/resource.py: 97%
71 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 16:07 -0300
1from pathlib import Path
2from typing import IO, cast, override
3from zipfile import ZipInfo
5import bs4
7from epublib.exceptions import EPUBError
8from epublib.identifier import EPUBId
9from epublib.mediatype import MediaType, guess_file_type
10from epublib.package.manifest import (
11 BookManifest,
12 ManifestItem,
13 detect_manifest_properties,
14)
15from epublib.package.metadata import BookMetadata
16from epublib.package.spine import BookSpine
17from epublib.resources import (
18 ContentDocument,
19 PublicationResource,
20 Resource,
21 XMLResource,
22)
23from epublib.soup import PackageDocumentSoup
26class PackageDocument(XMLResource[PackageDocumentSoup]):
27 """The package document of the EPUB file, sometimes known as the 'content.opf' file."""
29 soup_class: type[PackageDocumentSoup] = PackageDocumentSoup
31 def __init__(self, file: IO[bytes] | bytes, info: ZipInfo | str | Path) -> None:
32 super().__init__(file, info)
33 self._manifest: BookManifest | None = None
34 self._metadata: BookMetadata | None = None
35 self._spine: BookSpine | None = None
37 @property
38 def manifest(self):
39 if self._manifest is None:
40 self._manifest = BookManifest(self.soup.manifest, self.filename)
41 return self._manifest
43 @property
44 def metadata(self):
45 if self._metadata is None:
46 self._metadata = BookMetadata(self.soup.metadata)
47 return self._metadata
49 @property
50 def spine(self):
51 if self._spine is None:
52 self._spine = BookSpine(self.soup.spine)
53 return self._spine
55 def remove(self, filename: str):
56 item = self.manifest[filename]
57 spine_item = self.spine.get(item.id)
58 if spine_item:
59 self.spine.remove_item(spine_item)
60 self.manifest.remove_item(item)
62 def on_soup_change(self):
63 del self._manifest
64 del self._metadata
65 del self._spine
66 self._manifest = None
67 self._metadata = None
68 self._spine = None
70 @override
71 def on_content_change(self):
72 super().on_content_change()
73 self.on_soup_change()
76def resource_to_manifest_item(
77 resource: Resource,
78 package: PackageDocument,
79 identifier: EPUBId | str | None = None,
80 media_type: str | MediaType | None = None,
81 fallback: str | None = None,
82 media_overlay: str | None = None,
83 is_nav: bool = False,
84 is_cover: bool = False,
85 properties: list[str] | None = None,
86 detect_properties: bool = True,
87):
88 name = resource.filename
90 if identifier is None:
91 identifier = package.manifest.get_new_id(resource.filename)
92 elif package.manifest.get(identifier) is not None:
93 raise EPUBError(f"Identifier '{identifier}' is already used in the manifest")
95 if media_type is None:
96 media_type = (
97 resource.media_type
98 if isinstance(resource, PublicationResource)
99 else guess_file_type(resource.filename)
100 )
102 if not media_type:
103 raise EPUBError(f"Can't determine media type of file {resource.filename}")
105 if detect_properties or is_nav or is_cover:
106 properties = properties if properties is not None else []
108 if detect_properties and isinstance(resource, ContentDocument):
109 properties += detect_manifest_properties(
110 cast(ContentDocument[bs4.BeautifulSoup], resource).soup
111 )
113 if is_nav:
114 properties.append("nav")
116 if is_cover:
117 properties.append("cover-image")
119 properties = list(set(properties))
121 return ManifestItem(
122 name=name,
123 id=EPUBId(identifier),
124 media_type=str(media_type),
125 media_overlay=media_overlay,
126 fallback=fallback,
127 properties=properties,
128 manifest_filename=package.filename,
129 )