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
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 InvalidToken(InvalidCalendar):
22 """A name is not a valid iCalendar token.
24 .. versionadded:: 0.0.0
25 """
28class ICalParsingError(InvalidCalendar):
29 """Could not parse an iCalendar."""
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
43 full_message = message
45 if value is not None:
46 full_message += f": {value!r}"
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})"
55 super().__init__(full_message)
58class BrokenCalendarProperty(InvalidCalendar):
59 """A property could not be parsed and its value is broken.
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 """
68class IncompleteComponent(ValueError):
69 """The component is missing attributes.
71 The attributes are not required, otherwise this would be
72 an InvalidCalendar. But in order to perform calculations,
73 this attribute is required.
75 This error is not raised in the UPPERCASE properties like .DTSTART,
76 only in the lowercase computations like .start.
77 """
80class IncompleteAlarmInformation(ValueError):
81 """The alarms cannot be calculated yet because information is missing."""
84class LocalTimezoneMissing(IncompleteAlarmInformation):
85 """We are missing the local timezone to compute the value.
87 Use Alarms.set_local_timezone().
88 """
91class ComponentEndMissing(IncompleteAlarmInformation):
92 """We are missing the end of a component that the alarm is for.
94 Use Alarms.set_end().
95 """
98class ComponentStartMissing(IncompleteAlarmInformation):
99 """We are missing the start of a component that the alarm is for.
101 Use Alarms.set_start().
102 """
105class FeatureWillBeRemovedInFutureVersion(DeprecationWarning):
106 """This feature will be removed in a future version."""
109class GloballyUniqueTZIDGuessed(UserWarning):
110 """A globally unique TZID was resolved by stripping the vendor prefix.
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 """
118def _repr_index(index: str | int) -> str:
119 """Create a JSON compatible representation for the index.
121 Parameters:
122 index: It is either a dict key (string) or a list position (integer).
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)
133class JCalParsingError(InvalidCalendar):
134 """Could not parse a part of the JCal."""
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."""
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.
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)
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.
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
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
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.
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.
225 Raises:
226 ~icalendar.error.JCalParsingError: if the property is not valid.
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])
260 _type_names = {
261 str: "a string",
262 int: "an integer",
263 float: "a float",
264 bool: "a boolean",
265 }
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 )
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 )
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.
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.
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.
340 Raises:
341 ~icalendar.error.JCalParsingError: If ``name`` is not a valid lowercase
342 iCalendar token.
344 See also:
345 :meth:`~icalendar.parser.string.validate_token`
347 .. versionadded:: 7.3.0
348 """
349 from icalendar.parser.string import validate_token
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)
365class XCalParsingError(InvalidCalendar):
366 """Could not parse a part of the xCal."""
368 message: str
369 """A description of the error that occurred while parsing."""
371 value: str | None
372 """The value that caused the error."""
374 element: ElementAdapter
375 """The XML element that caused the error."""
377 def __init__(
378 self,
379 message: str,
380 value: str | None,
381 element: ElementAdapter,
382 ) -> None:
383 """Create a new XCalParsingError.
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)
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]