Coverage for src / minibook / exceptions.py: 100%

56 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-01-27 10:03 +0000

1"""Custom exceptions for MiniBook. 

2 

3This module defines a hierarchy of exceptions for better error handling 

4and more informative error messages throughout the MiniBook codebase. 

5""" 

6 

7 

8class MinibookError(Exception): 

9 """Base exception for all MiniBook errors. 

10 

11 All MiniBook-specific exceptions inherit from this class, 

12 allowing callers to catch all MiniBook errors with a single except clause. 

13 

14 Examples: 

15 >>> try: 

16 ... raise MinibookError("Something went wrong") 

17 ... except MinibookError as e: 

18 ... print(f"MiniBook error: {e}") 

19 MiniBook error: Something went wrong 

20 """ 

21 

22 

23class ValidationError(MinibookError): 

24 """Exception raised when input validation fails. 

25 

26 This exception is raised when user input doesn't meet 

27 the required format or constraints. 

28 

29 Attributes: 

30 field: The name of the field that failed validation 

31 value: The invalid value that was provided 

32 message: Description of what went wrong 

33 

34 Examples: 

35 >>> raise ValidationError("url", "javascript:alert(1)", "Invalid URL scheme") 

36 Traceback (most recent call last): 

37 ... 

38 minibook.exceptions.ValidationError: Invalid URL scheme (field: url, value: javascript:alert(1)) 

39 """ 

40 

41 def __init__(self, field: str, value: str | None = None, message: str = "Validation failed"): 

42 """Initialize validation error. 

43 

44 Args: 

45 field: The name of the field that failed validation. 

46 value: The invalid value that was provided. 

47 message: Description of what went wrong. 

48 """ 

49 self.field = field 

50 self.value = value 

51 self.message = message 

52 

53 if value is not None: 

54 super().__init__(f"{message} (field: {field}, value: {value})") 

55 else: 

56 super().__init__(f"{message} (field: {field})") 

57 

58 

59class URLValidationError(ValidationError): 

60 """Exception raised when URL validation fails. 

61 

62 A specialized validation error for URL-specific issues. 

63 

64 Examples: 

65 >>> raise URLValidationError("javascript:alert(1)", "Dangerous URL scheme") 

66 Traceback (most recent call last): 

67 ... 

68 minibook.exceptions.URLValidationError: Dangerous URL scheme (field: url, value: javascript:alert(1)) 

69 """ 

70 

71 def __init__(self, url: str, message: str = "Invalid URL"): 

72 """Initialize URL validation error. 

73 

74 Args: 

75 url: The invalid URL. 

76 message: Description of what went wrong. 

77 """ 

78 super().__init__(field="url", value=url, message=message) 

79 self.url = url 

80 

81 

82class LinkNameValidationError(ValidationError): 

83 """Exception raised when link name validation fails. 

84 

85 Examples: 

86 >>> raise LinkNameValidationError("", "Link name cannot be empty") 

87 Traceback (most recent call last): 

88 ... 

89 minibook.exceptions.LinkNameValidationError: Link name cannot be empty (field: name, value: ) 

90 """ 

91 

92 def __init__(self, name: str, message: str = "Invalid link name"): 

93 """Initialize link name validation error. 

94 

95 Args: 

96 name: The invalid link name. 

97 message: Description of what went wrong. 

98 """ 

99 super().__init__(field="name", value=name, message=message) 

100 self.name = name 

101 

102 

103class TemplateError(MinibookError): 

104 """Exception raised when template operations fail. 

105 

106 This exception covers template loading, parsing, and rendering errors. 

107 

108 Attributes: 

109 template_path: Path to the template that caused the error 

110 message: Description of what went wrong 

111 

112 Examples: 

113 >>> raise TemplateError("/path/to/missing.j2", "Template file not found") 

114 Traceback (most recent call last): 

115 ... 

116 minibook.exceptions.TemplateError: Template file not found: /path/to/missing.j2 

117 """ 

118 

119 def __init__(self, template_path: str | None = None, message: str = "Template error"): 

120 """Initialize template error. 

121 

122 Args: 

123 template_path: Path to the template that caused the error. 

124 message: Description of what went wrong. 

125 """ 

126 self.template_path = template_path 

127 

128 if template_path: 

129 super().__init__(f"{message}: {template_path}") 

130 else: 

131 super().__init__(message) 

132 

133 

134class TemplateNotFoundError(TemplateError): 

135 """Exception raised when a template file cannot be found. 

136 

137 Examples: 

138 >>> raise TemplateNotFoundError("/path/to/missing.j2") 

139 Traceback (most recent call last): 

140 ... 

141 minibook.exceptions.TemplateNotFoundError: Template file not found: /path/to/missing.j2 

142 """ 

143 

144 def __init__(self, template_path: str): 

145 """Initialize template not found error. 

146 

147 Args: 

148 template_path: Path to the template that was not found. 

149 """ 

150 super().__init__(template_path, "Template file not found") 

151 

152 

153class PluginError(MinibookError): 

154 """Exception raised when plugin operations fail. 

155 

156 This exception covers plugin loading, registration, and execution errors. 

157 

158 Attributes: 

159 plugin_name: Name of the plugin that caused the error 

160 message: Description of what went wrong 

161 

162 Examples: 

163 >>> raise PluginError("pdf", "Required dependency fpdf2 not installed") 

164 Traceback (most recent call last): 

165 ... 

166 minibook.exceptions.PluginError: Plugin 'pdf' error: Required dependency fpdf2 not installed 

167 """ 

168 

169 def __init__(self, plugin_name: str | None = None, message: str = "Plugin error"): 

170 """Initialize plugin error. 

171 

172 Args: 

173 plugin_name: Name of the plugin that caused the error. 

174 message: Description of what went wrong. 

175 """ 

176 self.plugin_name = plugin_name 

177 

178 if plugin_name: 

179 super().__init__(f"Plugin '{plugin_name}' error: {message}") 

180 else: 

181 super().__init__(message) 

182 

183 

184class PluginNotFoundError(PluginError): 

185 """Exception raised when a requested plugin is not found. 

186 

187 Examples: 

188 >>> raise PluginNotFoundError("unknown_format") 

189 Traceback (most recent call last): 

190 ... 

191 minibook.exceptions.PluginNotFoundError: Plugin 'unknown_format' error: Output format not found 

192 """ 

193 

194 def __init__(self, plugin_name: str): 

195 """Initialize plugin not found error. 

196 

197 Args: 

198 plugin_name: Name of the plugin that was not found. 

199 """ 

200 super().__init__(plugin_name, "Output format not found") 

201 

202 

203class PluginDependencyError(PluginError): 

204 """Exception raised when a plugin's dependencies are not installed. 

205 

206 Attributes: 

207 dependency: The missing dependency name 

208 install_command: Command to install the dependency 

209 

210 Examples: 

211 >>> raise PluginDependencyError("", "fpdf2", "pip install minibook[pdf]") 

212 Traceback (most recent call last): 

213 ... 

214 minibook.exceptions.PluginDependencyError: Missing dependency 'fpdf2'. Install with: pip install minibook[pdf] 

215 """ 

216 

217 def __init__(self, plugin_name: str, dependency: str, install_command: str | None = None): 

218 """Initialize plugin dependency error. 

219 

220 Args: 

221 plugin_name: Name of the plugin that caused the error. 

222 dependency: The missing dependency name. 

223 install_command: Command to install the dependency. 

224 """ 

225 self.dependency = dependency 

226 self.install_command = install_command 

227 

228 message = f"Missing dependency '{dependency}'" 

229 if install_command: 

230 message += f". Install with: {install_command}" 

231 

232 super().__init__(plugin_name, message) 

233 

234 

235class ParseError(MinibookError): 

236 """Exception raised when parsing input fails. 

237 

238 This exception is raised when JSON or other input formats cannot be parsed. 

239 

240 Attributes: 

241 input_type: The type of input being parsed (e.g., "JSON", "YAML") 

242 message: Description of what went wrong 

243 

244 Examples: 

245 >>> raise ParseError("JSON", "Invalid JSON syntax") 

246 Traceback (most recent call last): 

247 ... 

248 minibook.exceptions.ParseError: Failed to parse JSON: Invalid JSON syntax 

249 """ 

250 

251 def __init__(self, input_type: str = "input", message: str = "Parse error"): 

252 """Initialize parse error. 

253 

254 Args: 

255 input_type: The type of input being parsed (e.g., "JSON", "YAML"). 

256 message: Description of what went wrong. 

257 """ 

258 self.input_type = input_type 

259 super().__init__(f"Failed to parse {input_type}: {message}") 

260 

261 

262class JSONParseError(ParseError): 

263 """Exception raised when JSON parsing fails. 

264 

265 Examples: 

266 >>> raise JSONParseError("Unexpected token at position 5") 

267 Traceback (most recent call last): 

268 ... 

269 minibook.exceptions.JSONParseError: Failed to parse JSON: Unexpected token at position 5 

270 """ 

271 

272 def __init__(self, message: str = "Invalid JSON"): 

273 """Initialize JSON parse error. 

274 

275 Args: 

276 message: Description of what went wrong. 

277 """ 

278 super().__init__("JSON", message) 

279 

280 

281class OutputError(MinibookError): 

282 """Exception raised when output generation fails. 

283 

284 Attributes: 

285 output_path: Path where output was being written 

286 message: Description of what went wrong 

287 

288 Examples: 

289 >>> raise OutputError("/output/file.html", "Permission denied") 

290 Traceback (most recent call last): 

291 ... 

292 minibook.exceptions.OutputError: Failed to write output to /output/file.html: Permission denied 

293 """ 

294 

295 def __init__(self, output_path: str | None = None, message: str = "Output error"): 

296 """Initialize output error. 

297 

298 Args: 

299 output_path: Path where output was being written. 

300 message: Description of what went wrong. 

301 """ 

302 self.output_path = output_path 

303 

304 if output_path: 

305 super().__init__(f"Failed to write output to {output_path}: {message}") 

306 else: 

307 super().__init__(message)