Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/error.py: 48%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

131 statements  

1"""Errors thrown by icalendar.""" 

2 

3from __future__ import annotations 

4 

5import contextlib 

6from typing import TYPE_CHECKING 

7 

8if TYPE_CHECKING: 

9 from collections.abc import Generator 

10 

11 from icalendar.parser.xcal.adapter import ElementAdapter 

12 

13 

14class InvalidCalendar(ValueError): 

15 """The calendar given is not valid. 

16 

17 This calendar does not conform with RFC 5545 or breaks other RFCs. 

18 """ 

19 

20 

21class InvalidToken(InvalidCalendar): 

22 """A name is not a valid iCalendar token. 

23 

24 .. versionadded:: 0.0.0 

25 """ 

26 

27 

28class ICalParsingError(InvalidCalendar): 

29 """Could not parse an iCalendar.""" 

30 

31 def __init__( 

32 self, 

33 message: str, 

34 line: str | None = None, 

35 line_number: int | None = None, 

36 value: object = None, 

37 ) -> None: 

38 self.message = message 

39 self.line = line 

40 self.line_number = line_number 

41 self.value = value 

42 

43 full_message = message 

44 

45 if value is not None: 

46 full_message += f": {value!r}" 

47 

48 if line_number is not None and line is not None: 

49 full_message += f" (line {line_number}: {line!r})" 

50 elif line_number is not None: 

51 full_message += f" (line {line_number})" 

52 elif line is not None: 

53 full_message += f" ({line!r})" 

54 

55 super().__init__(full_message) 

56 

57 

58class BrokenCalendarProperty(InvalidCalendar): 

59 """A property could not be parsed and its value is broken. 

60 

61 This error is raised when accessing attributes on a 

62 :class:`~icalendar.prop.vBroken` property that would normally 

63 be present on the expected type. The original parse error 

64 is chained as ``__cause__``. 

65 """ 

66 

67 

68class IncompleteComponent(ValueError): 

69 """The component is missing attributes. 

70 

71 The attributes are not required, otherwise this would be 

72 an InvalidCalendar. But in order to perform calculations, 

73 this attribute is required. 

74 

75 This error is not raised in the UPPERCASE properties like .DTSTART, 

76 only in the lowercase computations like .start. 

77 """ 

78 

79 

80class IncompleteAlarmInformation(ValueError): 

81 """The alarms cannot be calculated yet because information is missing.""" 

82 

83 

84class LocalTimezoneMissing(IncompleteAlarmInformation): 

85 """We are missing the local timezone to compute the value. 

86 

87 Use Alarms.set_local_timezone(). 

88 """ 

89 

90 

91class ComponentEndMissing(IncompleteAlarmInformation): 

92 """We are missing the end of a component that the alarm is for. 

93 

94 Use Alarms.set_end(). 

95 """ 

96 

97 

98class ComponentStartMissing(IncompleteAlarmInformation): 

99 """We are missing the start of a component that the alarm is for. 

100 

101 Use Alarms.set_start(). 

102 """ 

103 

104 

105class FeatureWillBeRemovedInFutureVersion(DeprecationWarning): 

106 """This feature will be removed in a future version.""" 

107 

108 

109class GloballyUniqueTZIDGuessed(UserWarning): 

110 """A globally unique TZID was resolved by stripping the vendor prefix. 

111 

112 Per :rfc:`5545#section-3.2.19`, the trailing component is convention only 

113 and not guaranteed to match a known Olson identifier. Suppress this warning 

114 if the resolved timezone is correct for your data. 

115 """ 

116 

117 

118def _repr_index(index: str | int) -> str: 

119 """Create a JSON compatible representation for the index. 

120 

121 Parameters: 

122 index: It is either a dict key (string) or a list position (integer). 

123 

124 Returns: 

125 The index as a quoted string if it's a string, else the string 

126 representation if it's an integer. 

127 """ 

128 if isinstance(index, str): 

129 return f'"{index}"' 

130 return str(index) 

131 

132 

133class JCalParsingError(InvalidCalendar): 

134 """Could not parse a part of the JCal.""" 

135 

136 _default_value = object() 

137 path: list[str | int] 

138 """The location in the jCal structure where the error occurred.""" 

139 parser: str 

140 """The parser class or its name where the error occurred.""" 

141 message: str 

142 """A description of the error that occurred while parsing.""" 

143 value: object 

144 """The value which caused the error, if available.""" 

145 

146 def __init__( 

147 self, 

148 message: str, 

149 parser: str | type = "", 

150 path: list[str | int] | None | str | int = None, 

151 value: object = _default_value, 

152 ) -> None: 

153 """Create a new JCalParsingError. 

154 

155 Parameters: 

156 message: A description of the error that occurred while parsing. 

157 parser: The parser class or its name where the error occurred. 

158 path: The location in the jCal structure where the error occurred. 

159 value: The value which caused the error, if available. 

160 """ 

161 self.path = self._get_path(path) 

162 if not isinstance(parser, str): 

163 parser = parser.__name__ 

164 self.parser = parser 

165 self.message = message 

166 self.value = value 

167 full_message = message 

168 repr_path = "" 

169 if self.path: 

170 repr_path = "".join([f"[{_repr_index(index)}]" for index in self.path]) 

171 full_message = f"{repr_path}: {full_message}" 

172 repr_path += " " 

173 if parser: 

174 full_message = f"{repr_path}in {parser}: {message}" 

175 if value is not self._default_value: 

176 full_message += f" Got value: {value!r}" 

177 super().__init__(full_message) 

178 

179 @classmethod 

180 @contextlib.contextmanager 

181 def reraise_with_path_added( 

182 cls, 

183 *path_components: int | str, 

184 ) -> Generator[None, None, None]: 

185 """Automatically re-raise the exception with path components added. 

186 

187 Raises: 

188 ~icalendar.error.JCalParsingError: If there was an exception in the context. 

189 """ 

190 try: 

191 yield 

192 except JCalParsingError as e: 

193 raise cls( 

194 path=list(path_components) + e.path, 

195 parser=e.parser, 

196 message=e.message, 

197 value=e.value, 

198 ).with_traceback(e.__traceback__) from e 

199 

200 @staticmethod 

201 def _get_path(path: list[str | int] | None | str | int) -> list[str | int]: 

202 """Return the path as a list.""" 

203 if path is None: 

204 path = [] 

205 elif not isinstance(path, list): 

206 path = [path] 

207 return path 

208 

209 @classmethod 

210 def validate_property( 

211 cls, 

212 jcal_property: list[object], 

213 parser: str | type, 

214 path: list[str | int] | None | str | int = None, 

215 ) -> None: 

216 """Validate a jCal property. 

217 

218 Parameters: 

219 jcal_property: A list with at least four items (name, 

220 parameters, value type, and value) which is the jCal property 

221 to be validated. 

222 parser: The parser class or its name where the error occurred. 

223 path: The location in the jCal structure where the error occurred. 

224 

225 Raises: 

226 ~icalendar.error.JCalParsingError: if the property is not valid. 

227 

228 .. versionchanged:: 7.3.0 

229 The name (first item of the list) is validated by 

230 :meth:`validate_jcal_token`. 

231 """ 

232 path = cls._get_path(path) 

233 if not isinstance(jcal_property, list) or len(jcal_property) < 4: 

234 raise JCalParsingError( 

235 "The property must be a list with at least 4 items.", 

236 parser, 

237 path, 

238 value=jcal_property, 

239 ) 

240 if not isinstance(jcal_property[0], str): 

241 raise JCalParsingError( 

242 "The name must be a string.", parser, path + [0], value=jcal_property[0] 

243 ) 

244 if not isinstance(jcal_property[1], dict): 

245 raise JCalParsingError( 

246 "The parameters must be a mapping.", 

247 parser, 

248 path + [1], 

249 value=jcal_property[1], 

250 ) 

251 if not isinstance(jcal_property[2], str): 

252 raise JCalParsingError( 

253 "The VALUE parameter must be a string.", 

254 parser, 

255 path + [2], 

256 value=jcal_property[2], 

257 ) 

258 cls.validate_jcal_token(jcal_property[0], "property name", parser, path + [0]) 

259 

260 _type_names = { 

261 str: "a string", 

262 int: "an integer", 

263 float: "a float", 

264 bool: "a boolean", 

265 } 

266 

267 @classmethod 

268 def validate_value_type( 

269 cls, 

270 jcal: object, 

271 expected_type: type[str | int | float | bool] 

272 | tuple[type[str | int | float | bool], ...], 

273 parser: str | type = "", 

274 path: list[str | int] | None | str | int = None, 

275 ) -> None: 

276 """Validate the type of a jCal value.""" 

277 if not isinstance(jcal, expected_type): 

278 type_name = ( 

279 cls._type_names[expected_type] 

280 if isinstance(expected_type, type) 

281 else " or ".join(cls._type_names[t] for t in expected_type) 

282 ) 

283 raise cls( 

284 f"The value must be {type_name}.", 

285 parser=parser, 

286 value=jcal, 

287 path=path, 

288 ) 

289 

290 @classmethod 

291 def validate_list_type( 

292 cls, 

293 jcal: object, 

294 expected_type: type[str | int | float | bool], 

295 parser: str | type = "", 

296 path: list[str | int] | None | str | int = None, 

297 ) -> None: 

298 """Validate the type of each item in a jCal list.""" 

299 path = cls._get_path(path) 

300 if not isinstance(jcal, list): 

301 raise cls( 

302 "The value must be a list.", 

303 parser=parser, 

304 value=jcal, 

305 path=path, 

306 ) 

307 for index, item in enumerate(jcal): 

308 if not isinstance(item, expected_type): 

309 type_name = cls._type_names[expected_type] 

310 raise cls( 

311 f"Each item in the list must be {type_name}.", 

312 parser=parser, 

313 value=item, 

314 path=path + [index], 

315 ) 

316 

317 @classmethod 

318 def validate_jcal_token( 

319 cls, 

320 name: str, 

321 kind: str, 

322 parser: str | type = "", 

323 path: list[str | int] | None | str | int = None, 

324 ) -> None: 

325 r"""Validate a jCal ``name`` as a lowercase iCalendar token. 

326 

327 jCal keeps a property name, parameter name, or ``RRULE`` part name 

328 verbatim and re-emits it into the content line on serialization, so a 

329 ``:``, ``;``, or lone carriage return in the name could inject 

330 parameters or a new content line. A valid name matches the iCalendar 

331 token pattern ``[\w.-]+`` and, per :rfc:`7265`, must be lowercase. 

332 

333 Parameters: 

334 name: The jCal name to validate. 

335 kind: Names the token in the error message, for example, 

336 ``"property name"``. 

337 parser: The parser or component to which the name belongs. 

338 path: The jCal path to ``name``, used to locate it in the error. 

339 

340 Raises: 

341 ~icalendar.error.JCalParsingError: If ``name`` is not a valid lowercase 

342 iCalendar token. 

343 

344 See also: 

345 :meth:`~icalendar.parser.string.validate_token` 

346 

347 .. versionadded:: 7.3.0 

348 """ 

349 from icalendar.parser.string import validate_token 

350 

351 try: 

352 validate_token(name) 

353 except InvalidToken: 

354 raise cls( 

355 rf"The {kind} must be a valid iCalendar token, matching the " 

356 rf"regular expression pattern `[\w.-]+`.", 

357 parser, 

358 path, 

359 value=name, 

360 ) from None 

361 if name != name.lower(): 

362 raise cls(f"The {kind} must be lowercase.", parser, path, value=name) 

363 

364 

365class XCalParsingError(InvalidCalendar): 

366 """Could not parse a part of the xCal.""" 

367 

368 message: str 

369 """A description of the error that occurred while parsing.""" 

370 

371 value: str | None 

372 """The value that caused the error.""" 

373 

374 element: ElementAdapter 

375 """The XML element that caused the error.""" 

376 

377 def __init__( 

378 self, 

379 message: str, 

380 value: str | None, 

381 element: ElementAdapter, 

382 ) -> None: 

383 """Create a new XCalParsingError. 

384 

385 Parameters: 

386 message: A description of the error that occurred while parsing. 

387 value: The value that caused the error. 

388 element: The XML element that caused the error. 

389 """ 

390 self.value = value 

391 self.element = element 

392 self.short_message = message 

393 if value is not None: 

394 self.message = f"{message}, got {value!r} in {element.get_xpath()}." 

395 else: 

396 self.message = f"{message} in {element.get_xpath()}." 

397 super().__init__(self.message) 

398 

399 

400__all__ = [ 

401 "BrokenCalendarProperty", 

402 "ComponentEndMissing", 

403 "ComponentStartMissing", 

404 "FeatureWillBeRemovedInFutureVersion", 

405 "ICalParsingError", 

406 "IncompleteAlarmInformation", 

407 "IncompleteComponent", 

408 "InvalidCalendar", 

409 "InvalidToken", 

410 "JCalParsingError", 

411 "LocalTimezoneMissing", 

412 "XCalParsingError", 

413]