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

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

122 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 ICalParsingError(InvalidCalendar): 

22 """Could not parse an iCalendar.""" 

23 

24 def __init__( 

25 self, 

26 message: str, 

27 line: str | None = None, 

28 line_number: int | None = None, 

29 value: object = None, 

30 ) -> None: 

31 self.message = message 

32 self.line = line 

33 self.line_number = line_number 

34 self.value = value 

35 

36 full_message = message 

37 

38 if value is not None: 

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

40 

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

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

43 elif line_number is not None: 

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

45 elif line is not None: 

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

47 

48 super().__init__(full_message) 

49 

50 

51class BrokenCalendarProperty(InvalidCalendar): 

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

53 

54 This error is raised when accessing attributes on a 

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

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

57 is chained as ``__cause__``. 

58 """ 

59 

60 

61class IncompleteComponent(ValueError): 

62 """The component is missing attributes. 

63 

64 The attributes are not required, otherwise this would be 

65 an InvalidCalendar. But in order to perform calculations, 

66 this attribute is required. 

67 

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

69 only in the lowercase computations like .start. 

70 """ 

71 

72 

73class IncompleteAlarmInformation(ValueError): 

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

75 

76 

77class LocalTimezoneMissing(IncompleteAlarmInformation): 

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

79 

80 Use Alarms.set_local_timezone(). 

81 """ 

82 

83 

84class ComponentEndMissing(IncompleteAlarmInformation): 

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

86 

87 Use Alarms.set_end(). 

88 """ 

89 

90 

91class ComponentStartMissing(IncompleteAlarmInformation): 

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

93 

94 Use Alarms.set_start(). 

95 """ 

96 

97 

98class FeatureWillBeRemovedInFutureVersion(DeprecationWarning): 

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

100 

101 

102class GloballyUniqueTZIDGuessed(UserWarning): 

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

104 

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

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

107 if the resolved timezone is correct for your data. 

108 """ 

109 

110 

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

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

113 

114 Parameters: 

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

116 

117 Returns: 

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

119 representation if it's an integer. 

120 """ 

121 if isinstance(index, str): 

122 return f'"{index}"' 

123 return str(index) 

124 

125 

126class JCalParsingError(InvalidCalendar): 

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

128 

129 _default_value = object() 

130 

131 def __init__( 

132 self, 

133 message: str, 

134 parser: str | type = "", 

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

136 value: object = _default_value, 

137 ) -> None: 

138 """Create a new JCalParsingError. 

139 

140 Parameters: 

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

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

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

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

145 """ 

146 self.path = self._get_path(path) 

147 if not isinstance(parser, str): 

148 parser = parser.__name__ 

149 self.parser = parser 

150 self.message = message 

151 self.value = value 

152 full_message = message 

153 repr_path = "" 

154 if self.path: 

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

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

157 repr_path += " " 

158 if parser: 

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

160 if value is not self._default_value: 

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

162 super().__init__(full_message) 

163 

164 @classmethod 

165 @contextlib.contextmanager 

166 def reraise_with_path_added( 

167 cls, 

168 *path_components: int | str, 

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

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

171 

172 Raises: 

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

174 """ 

175 try: 

176 yield 

177 except JCalParsingError as e: 

178 raise cls( 

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

180 parser=e.parser, 

181 message=e.message, 

182 value=e.value, 

183 ).with_traceback(e.__traceback__) from e 

184 

185 @staticmethod 

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

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

188 if path is None: 

189 path = [] 

190 elif not isinstance(path, list): 

191 path = [path] 

192 return path 

193 

194 @classmethod 

195 def validate_property( 

196 cls, 

197 jcal_property: list[object], 

198 parser: str | type, 

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

200 ) -> None: 

201 """Validate a jCal property. 

202 

203 Parameters: 

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

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

206 to be validated. 

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

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

209 

210 Raises: 

211 ~error.JCalParsingError: if the property is not valid. 

212 

213 .. versionchanged:: 7.3.0 

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

215 :meth:`validate_jcal_token`. 

216 """ 

217 path = cls._get_path(path) 

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

219 raise JCalParsingError( 

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

221 parser, 

222 path, 

223 value=jcal_property, 

224 ) 

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

226 raise JCalParsingError( 

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

228 ) 

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

230 raise JCalParsingError( 

231 "The parameters must be a mapping.", 

232 parser, 

233 path + [1], 

234 value=jcal_property[1], 

235 ) 

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

237 raise JCalParsingError( 

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

239 parser, 

240 path + [2], 

241 value=jcal_property[2], 

242 ) 

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

244 

245 _type_names = { 

246 str: "a string", 

247 int: "an integer", 

248 float: "a float", 

249 bool: "a boolean", 

250 } 

251 

252 @classmethod 

253 def validate_value_type( 

254 cls, 

255 jcal: object, 

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

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

258 parser: str | type = "", 

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

260 ) -> None: 

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

262 if not isinstance(jcal, expected_type): 

263 type_name = ( 

264 cls._type_names[expected_type] 

265 if isinstance(expected_type, type) 

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

267 ) 

268 raise cls( 

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

270 parser=parser, 

271 value=jcal, 

272 path=path, 

273 ) 

274 

275 @classmethod 

276 def validate_list_type( 

277 cls, 

278 jcal: object, 

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

280 parser: str | type = "", 

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

282 ) -> None: 

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

284 path = cls._get_path(path) 

285 if not isinstance(jcal, list): 

286 raise cls( 

287 "The value must be a list.", 

288 parser=parser, 

289 value=jcal, 

290 path=path, 

291 ) 

292 for index, item in enumerate(jcal): 

293 if not isinstance(item, expected_type): 

294 type_name = cls._type_names[expected_type] 

295 raise cls( 

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

297 parser=parser, 

298 value=item, 

299 path=path + [index], 

300 ) 

301 

302 @classmethod 

303 def validate_jcal_token( 

304 cls, 

305 name: str, 

306 kind: str, 

307 parser: str | type = "", 

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

309 ) -> None: 

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

311 

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

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

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

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

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

317 

318 Parameters: 

319 name: The jCal name to validate. 

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

321 ``"property name"``. 

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

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

324 

325 Raises: 

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

327 iCalendar token. 

328 

329 See also: 

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

331 

332 .. versionadded:: 7.3.0 

333 """ 

334 from icalendar.parser.string import validate_token 

335 

336 try: 

337 validate_token(name) 

338 except ValueError: 

339 raise cls( 

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

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

342 parser, 

343 path, 

344 value=name, 

345 ) from None 

346 if name != name.lower(): 

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

348 

349 

350class XCalParsingError(InvalidCalendar): 

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

352 

353 message: str 

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

355 

356 value: str | None 

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

358 

359 element: ElementAdapter 

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

361 

362 def __init__( 

363 self, 

364 message: str, 

365 value: str | None, 

366 element: ElementAdapter, 

367 ) -> None: 

368 """Create a new XCalParsingError. 

369 

370 Parameters: 

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

372 value: The value that caused the error. 

373 element: The XML element that caused the error. 

374 """ 

375 self.value = value 

376 self.element = element 

377 self.short_message = message 

378 if value is not None: 

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

380 else: 

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

382 super().__init__(self.message) 

383 

384 

385__all__ = [ 

386 "BrokenCalendarProperty", 

387 "ComponentEndMissing", 

388 "ComponentStartMissing", 

389 "FeatureWillBeRemovedInFutureVersion", 

390 "ICalParsingError", 

391 "IncompleteAlarmInformation", 

392 "IncompleteComponent", 

393 "InvalidCalendar", 

394 "JCalParsingError", 

395 "LocalTimezoneMissing", 

396 "XCalParsingError", 

397]