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
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
1"""Errors thrown by icalendar."""
3from __future__ import annotations
5import contextlib
6from typing import TYPE_CHECKING
8if TYPE_CHECKING:
9 from collections.abc import Generator
11 from icalendar.parser.xcal.adapter import ElementAdapter
14class InvalidCalendar(ValueError):
15 """The calendar given is not valid.
17 This calendar does not conform with RFC 5545 or breaks other RFCs.
18 """
21class ICalParsingError(InvalidCalendar):
22 """Could not parse an iCalendar."""
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
36 full_message = message
38 if value is not None:
39 full_message += f": {value!r}"
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})"
48 super().__init__(full_message)
51class BrokenCalendarProperty(InvalidCalendar):
52 """A property could not be parsed and its value is broken.
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 """
61class IncompleteComponent(ValueError):
62 """The component is missing attributes.
64 The attributes are not required, otherwise this would be
65 an InvalidCalendar. But in order to perform calculations,
66 this attribute is required.
68 This error is not raised in the UPPERCASE properties like .DTSTART,
69 only in the lowercase computations like .start.
70 """
73class IncompleteAlarmInformation(ValueError):
74 """The alarms cannot be calculated yet because information is missing."""
77class LocalTimezoneMissing(IncompleteAlarmInformation):
78 """We are missing the local timezone to compute the value.
80 Use Alarms.set_local_timezone().
81 """
84class ComponentEndMissing(IncompleteAlarmInformation):
85 """We are missing the end of a component that the alarm is for.
87 Use Alarms.set_end().
88 """
91class ComponentStartMissing(IncompleteAlarmInformation):
92 """We are missing the start of a component that the alarm is for.
94 Use Alarms.set_start().
95 """
98class FeatureWillBeRemovedInFutureVersion(DeprecationWarning):
99 """This feature will be removed in a future version."""
102class GloballyUniqueTZIDGuessed(UserWarning):
103 """A globally unique TZID was resolved by stripping the vendor prefix.
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 """
111def _repr_index(index: str | int) -> str:
112 """Create a JSON compatible representation for the index.
114 Parameters:
115 index: It is either a dict key (string) or a list position (integer).
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)
126class JCalParsingError(InvalidCalendar):
127 """Could not parse a part of the JCal."""
129 _default_value = object()
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.
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)
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.
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
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
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.
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.
210 Raises:
211 ~error.JCalParsingError: if the property is not valid.
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])
245 _type_names = {
246 str: "a string",
247 int: "an integer",
248 float: "a float",
249 bool: "a boolean",
250 }
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 )
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 )
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.
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.
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.
325 Raises:
326 ~error.JCalParsingError: If ``name`` is not a valid lowercase
327 iCalendar token.
329 See also:
330 :meth:`~icalendar.parser.string.validate_token`
332 .. versionadded:: 7.3.0
333 """
334 from icalendar.parser.string import validate_token
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)
350class XCalParsingError(InvalidCalendar):
351 """Could not parse a part of the xCal."""
353 message: str
354 """A description of the error that occurred while parsing."""
356 value: str | None
357 """The value that caused the error."""
359 element: ElementAdapter
360 """The XML element that caused the error."""
362 def __init__(
363 self,
364 message: str,
365 value: str | None,
366 element: ElementAdapter,
367 ) -> None:
368 """Create a new XCalParsingError.
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)
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]