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

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

106 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 

12class InvalidCalendar(ValueError): 

13 """The calendar given is not valid. 

14 

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

16 """ 

17 

18 

19class ICalParsingError(InvalidCalendar): 

20 """Could not parse an iCalendar.""" 

21 

22 def __init__( 

23 self, 

24 message: str, 

25 line: str | None = None, 

26 line_number: int | None = None, 

27 value: object = None, 

28 ) -> None: 

29 self.message = message 

30 self.line = line 

31 self.line_number = line_number 

32 self.value = value 

33 

34 full_message = message 

35 

36 if value is not None: 

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

38 

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

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

41 elif line_number is not None: 

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

43 elif line is not None: 

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

45 

46 super().__init__(full_message) 

47 

48 

49class BrokenCalendarProperty(InvalidCalendar): 

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

51 

52 This error is raised when accessing attributes on a 

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

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

55 is chained as ``__cause__``. 

56 """ 

57 

58 

59class IncompleteComponent(ValueError): 

60 """The component is missing attributes. 

61 

62 The attributes are not required, otherwise this would be 

63 an InvalidCalendar. But in order to perform calculations, 

64 this attribute is required. 

65 

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

67 only in the lowercase computations like .start. 

68 """ 

69 

70 

71class IncompleteAlarmInformation(ValueError): 

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

73 

74 

75class LocalTimezoneMissing(IncompleteAlarmInformation): 

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

77 

78 Use Alarms.set_local_timezone(). 

79 """ 

80 

81 

82class ComponentEndMissing(IncompleteAlarmInformation): 

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

84 

85 Use Alarms.set_end(). 

86 """ 

87 

88 

89class ComponentStartMissing(IncompleteAlarmInformation): 

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

91 

92 Use Alarms.set_start(). 

93 """ 

94 

95 

96class FeatureWillBeRemovedInFutureVersion(DeprecationWarning): 

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

98 

99 

100class GloballyUniqueTZIDGuessed(UserWarning): 

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

102 

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

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

105 if the resolved timezone is correct for your data. 

106 """ 

107 

108 

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

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

111 

112 Parameters: 

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

114 

115 Returns: 

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

117 representation if it's an integer. 

118 """ 

119 if isinstance(index, str): 

120 return f'"{index}"' 

121 return str(index) 

122 

123 

124class JCalParsingError(InvalidCalendar): 

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

126 

127 _default_value = object() 

128 

129 def __init__( 

130 self, 

131 message: str, 

132 parser: str | type = "", 

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

134 value: object = _default_value, 

135 ) -> None: 

136 """Create a new JCalParsingError. 

137 

138 Parameters: 

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

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

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

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

143 """ 

144 self.path = self._get_path(path) 

145 if not isinstance(parser, str): 

146 parser = parser.__name__ 

147 self.parser = parser 

148 self.message = message 

149 self.value = value 

150 full_message = message 

151 repr_path = "" 

152 if self.path: 

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

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

155 repr_path += " " 

156 if parser: 

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

158 if value is not self._default_value: 

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

160 super().__init__(full_message) 

161 

162 @classmethod 

163 @contextlib.contextmanager 

164 def reraise_with_path_added( 

165 cls, 

166 *path_components: int | str, 

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

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

169 

170 Raises: 

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

172 """ 

173 try: 

174 yield 

175 except JCalParsingError as e: 

176 raise cls( 

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

178 parser=e.parser, 

179 message=e.message, 

180 value=e.value, 

181 ).with_traceback(e.__traceback__) from e 

182 

183 @staticmethod 

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

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

186 if path is None: 

187 path = [] 

188 elif not isinstance(path, list): 

189 path = [path] 

190 return path 

191 

192 @classmethod 

193 def validate_property( 

194 cls, 

195 jcal_property: list[object], 

196 parser: str | type, 

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

198 ) -> None: 

199 """Validate a jCal property. 

200 

201 Parameters: 

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

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

204 to be validated. 

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

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

207 

208 Raises: 

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

210 

211 .. versionchanged:: 7.3.0 

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

213 :meth:`validate_jcal_token`. 

214 """ 

215 path = cls._get_path(path) 

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

217 raise JCalParsingError( 

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

219 parser, 

220 path, 

221 value=jcal_property, 

222 ) 

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

224 raise JCalParsingError( 

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

226 ) 

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

228 raise JCalParsingError( 

229 "The parameters must be a mapping.", 

230 parser, 

231 path + [1], 

232 value=jcal_property[1], 

233 ) 

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

235 raise JCalParsingError( 

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

237 parser, 

238 path + [2], 

239 value=jcal_property[2], 

240 ) 

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

242 

243 _type_names = { 

244 str: "a string", 

245 int: "an integer", 

246 float: "a float", 

247 bool: "a boolean", 

248 } 

249 

250 @classmethod 

251 def validate_value_type( 

252 cls, 

253 jcal: object, 

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

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

256 parser: str | type = "", 

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

258 ) -> None: 

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

260 if not isinstance(jcal, expected_type): 

261 type_name = ( 

262 cls._type_names[expected_type] 

263 if isinstance(expected_type, type) 

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

265 ) 

266 raise cls( 

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

268 parser=parser, 

269 value=jcal, 

270 path=path, 

271 ) 

272 

273 @classmethod 

274 def validate_list_type( 

275 cls, 

276 jcal: object, 

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

278 parser: str | type = "", 

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

280 ) -> None: 

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

282 path = cls._get_path(path) 

283 if not isinstance(jcal, list): 

284 raise cls( 

285 "The value must be a list.", 

286 parser=parser, 

287 value=jcal, 

288 path=path, 

289 ) 

290 for index, item in enumerate(jcal): 

291 if not isinstance(item, expected_type): 

292 type_name = cls._type_names[expected_type] 

293 raise cls( 

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

295 parser=parser, 

296 value=item, 

297 path=path + [index], 

298 ) 

299 

300 @classmethod 

301 def validate_jcal_token( 

302 cls, 

303 name: str, 

304 kind: str, 

305 parser: str | type = "", 

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

307 ) -> None: 

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

309 

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

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

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

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

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

315 

316 Parameters: 

317 name: The jCal name to validate. 

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

319 ``"property name"``. 

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

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

322 

323 Raises: 

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

325 iCalendar token. 

326 

327 See also: 

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

329 

330 .. versionadded:: 7.3.0 

331 """ 

332 from icalendar.parser.string import validate_token 

333 

334 try: 

335 validate_token(name) 

336 except ValueError: 

337 raise cls( 

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

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

340 parser, 

341 path, 

342 value=name, 

343 ) from None 

344 if name != name.lower(): 

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

346 

347 

348__all__ = [ 

349 "BrokenCalendarProperty", 

350 "ComponentEndMissing", 

351 "ComponentStartMissing", 

352 "FeatureWillBeRemovedInFutureVersion", 

353 "ICalParsingError", 

354 "IncompleteAlarmInformation", 

355 "IncompleteComponent", 

356 "InvalidCalendar", 

357 "JCalParsingError", 

358 "LocalTimezoneMissing", 

359]