Coverage for src/epublib/package/resource.py: 95%

82 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-06 15:17 -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.media_type import MediaType 

10from epublib.package.guide import BookGuide 

11from epublib.package.manifest import ( 

12 BookManifest, 

13 ManifestItem, 

14 detect_manifest_properties, 

15) 

16from epublib.package.metadata import BookMetadata, ValuedMetadataItem 

17from epublib.package.spine import BookSpine 

18from epublib.resources import ( 

19 ContentDocument, 

20 PublicationResource, 

21 Resource, 

22 XMLResource, 

23) 

24from epublib.soup import PackageDocumentSoup 

25 

26 

27class PackageDocument(XMLResource[PackageDocumentSoup]): 

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

29 

30 soup_class: type[PackageDocumentSoup] = PackageDocumentSoup 

31 

32 def __init__(self, file: IO[bytes] | bytes, info: ZipInfo | str | Path) -> None: 

33 super().__init__(file, info) 

34 self._manifest: BookManifest | None = None 

35 self._metadata: BookMetadata | None = None 

36 self._spine: BookSpine | None = None 

37 self._guide: BookGuide | None = None 

38 

39 @property 

40 def manifest(self): 

41 if self._manifest is None: 

42 self._manifest = BookManifest( 

43 self.soup, 

44 self.soup.manifest, 

45 own_filename=self.filename, 

46 ) 

47 return self._manifest 

48 

49 @property 

50 def metadata(self): 

51 if self._metadata is None: 

52 self._metadata = BookMetadata(self.soup, self.soup.metadata) 

53 return self._metadata 

54 

55 @property 

56 def spine(self): 

57 if self._spine is None: 

58 self._spine = BookSpine(self.soup, self.soup.spine) 

59 return self._spine 

60 

61 @property 

62 def guide(self): 

63 if self._guide is None and self.soup.guide: 

64 self._guide = BookGuide( 

65 self.soup, 

66 self.soup.guide, 

67 own_filename=self.filename, 

68 ) 

69 return self._guide 

70 

71 def remove(self, filename: str): 

72 item = self.manifest[filename] 

73 spine_item = self.spine.get(item.id) 

74 if spine_item: 

75 self.spine.remove_item(spine_item) 

76 self.manifest.remove_item(item) 

77 if item.has_property("cover-image"): 

78 metadata_item = self.metadata.get("cover", ValuedMetadataItem) 

79 if metadata_item and metadata_item.value == item.id: 

80 self.metadata.remove_item(metadata_item) 

81 

82 def on_soup_change(self): 

83 del self._manifest 

84 del self._metadata 

85 del self._spine 

86 self._manifest = None 

87 self._metadata = None 

88 self._spine = None 

89 

90 @override 

91 def on_content_change(self): 

92 super().on_content_change() 

93 self.on_soup_change() 

94 

95 

96def resource_to_manifest_item( 

97 resource: Resource, 

98 package: PackageDocument, 

99 identifier: EPUBId | str | None = None, 

100 media_type: MediaType | str | None = None, 

101 fallback: str | None = None, 

102 media_overlay: str | None = None, 

103 is_nav: bool = False, 

104 is_cover: bool = False, 

105 properties: list[str] | None = None, 

106 detect_properties: bool = True, 

107): 

108 filename = resource.filename 

109 

110 if identifier is None: 

111 identifier = package.manifest.get_new_id(resource.filename) 

112 elif package.manifest.get(identifier) is not None: 

113 raise EPUBError(f"Identifier '{identifier}' is already used in the manifest") 

114 

115 if media_type is None: 

116 media_type = ( 

117 resource.media_type 

118 if isinstance(resource, PublicationResource) 

119 else MediaType.from_filename(resource.filename) 

120 ) 

121 

122 if not media_type: 

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

124 

125 if detect_properties or is_nav or is_cover: 

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

127 

128 if detect_properties and isinstance(resource, ContentDocument): 

129 properties += detect_manifest_properties( 

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

131 ) 

132 

133 if is_nav: 

134 properties.append("nav") 

135 

136 if is_cover: 

137 properties.append("cover-image") 

138 

139 properties = list(set(properties)) if properties else None 

140 

141 return ManifestItem( 

142 soup=package.soup, 

143 filename=filename, 

144 id=EPUBId(identifier), 

145 media_type=str(media_type), 

146 media_overlay=media_overlay, 

147 fallback=fallback, 

148 properties=properties, 

149 own_filename=package.filename, 

150 )