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 path = cls._get_path(path) 

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

213 raise JCalParsingError( 

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

215 parser, 

216 path, 

217 value=jcal_property, 

218 ) 

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

220 raise JCalParsingError( 

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

222 ) 

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

224 raise JCalParsingError( 

225 "The parameters must be a mapping.", 

226 parser, 

227 path + [1], 

228 value=jcal_property[1], 

229 ) 

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

231 raise JCalParsingError( 

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

233 parser, 

234 path + [2], 

235 value=jcal_property[2], 

236 ) 

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

238 

239 _type_names = { 

240 str: "a string", 

241 int: "an integer", 

242 float: "a float", 

243 bool: "a boolean", 

244 } 

245 

246 @classmethod 

247 def validate_value_type( 

248 cls, 

249 jcal: object, 

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

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

252 parser: str | type = "", 

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

254 ) -> None: 

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

256 if not isinstance(jcal, expected_type): 

257 type_name = ( 

258 cls._type_names[expected_type] 

259 if isinstance(expected_type, type) 

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

261 ) 

262 raise cls( 

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

264 parser=parser, 

265 value=jcal, 

266 path=path, 

267 ) 

268 

269 @classmethod 

270 def validate_list_type( 

271 cls, 

272 jcal: object, 

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

274 parser: str | type = "", 

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

276 ) -> None: 

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

278 path = cls._get_path(path) 

279 if not isinstance(jcal, list): 

280 raise cls( 

281 "The value must be a list.", 

282 parser=parser, 

283 value=jcal, 

284 path=path, 

285 ) 

286 for index, item in enumerate(jcal): 

287 if not isinstance(item, expected_type): 

288 type_name = cls._type_names[expected_type] 

289 raise cls( 

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

291 parser=parser, 

292 value=item, 

293 path=path + [index], 

294 ) 

295 

296 @classmethod 

297 def validate_jcal_token( 

298 cls, 

299 name: str, 

300 kind: str, 

301 parser: str | type = "", 

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

303 ) -> None: 

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

305 

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

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

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

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

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

311 

312 Parameters: 

313 name: The jCal name to validate. 

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

315 ``"property name"``. 

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

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

318 

319 Raises: 

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

321 iCalendar token. 

322 

323 See also: 

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

325 """ 

326 from icalendar.parser.string import validate_token 

327 

328 try: 

329 validate_token(name) 

330 except ValueError: 

331 raise cls( 

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

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

334 parser, 

335 path, 

336 value=name, 

337 ) from None 

338 if name != name.lower(): 

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

340 

341 

342__all__ = [ 

343 "BrokenCalendarProperty", 

344 "ComponentEndMissing", 

345 "ComponentStartMissing", 

346 "FeatureWillBeRemovedInFutureVersion", 

347 "ICalParsingError", 

348 "IncompleteAlarmInformation", 

349 "IncompleteComponent", 

350 "InvalidCalendar", 

351 "JCalParsingError", 

352 "LocalTimezoneMissing", 

353]