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

1from pathlib import Path 

2from typing import IO, cast, override 

3from zipfile import ZipInfo 

4 

5import bs4 

6 

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 

24 

25 

26class PackageDocument(XMLResource[PackageDocumentSoup]): 

27 """The package document of the EPUB file, sometimes known as the 'content.opf' file.""" 

28 

29 soup_class: type[PackageDocumentSoup] = PackageDocumentSoup 

30 

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 

36 

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 

42 

43 @property 

44 def metadata(self): 

45 if self._metadata is None: 

46 self._metadata = BookMetadata(self.soup.metadata) 

47 return self._metadata 

48 

49 @property 

50 def spine(self): 

51 if self._spine is None: 

52 self._spine = BookSpine(self.soup.spine) 

53 return self._spine 

54 

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) 

61 

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 

69 

70 @override 

71 def on_content_change(self): 

72 super().on_content_change() 

73 self.on_soup_change() 

74 

75 

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 

89 

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") 

94 

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 ) 

101 

102 if not media_type: 

103 raise EPUBError(f"Can't determine media type of file {resource.filename}") 

104 

105 if detect_properties or is_nav or is_cover: 

106 properties = properties if properties is not None else [] 

107 

108 if detect_properties and isinstance(resource, ContentDocument): 

109 properties += detect_manifest_properties( 

110 cast(ContentDocument[bs4.BeautifulSoup], resource).soup 

111 ) 

112 

113 if is_nav: 

114 properties.append("nav") 

115 

116 if is_cover: 

117 properties.append("cover-image") 

118 

119 properties = list(set(properties)) 

120 

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 )