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
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
12class InvalidCalendar(ValueError):
13 """The calendar given is not valid.
15 This calendar does not conform with RFC 5545 or breaks other RFCs.
16 """
19class ICalParsingError(InvalidCalendar):
20 """Could not parse an iCalendar."""
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
34 full_message = message
36 if value is not None:
37 full_message += f": {value!r}"
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})"
46 super().__init__(full_message)
49class BrokenCalendarProperty(InvalidCalendar):
50 """A property could not be parsed and its value is broken.
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 """
59class IncompleteComponent(ValueError):
60 """The component is missing attributes.
62 The attributes are not required, otherwise this would be
63 an InvalidCalendar. But in order to perform calculations,
64 this attribute is required.
66 This error is not raised in the UPPERCASE properties like .DTSTART,
67 only in the lowercase computations like .start.
68 """
71class IncompleteAlarmInformation(ValueError):
72 """The alarms cannot be calculated yet because information is missing."""
75class LocalTimezoneMissing(IncompleteAlarmInformation):
76 """We are missing the local timezone to compute the value.
78 Use Alarms.set_local_timezone().
79 """
82class ComponentEndMissing(IncompleteAlarmInformation):
83 """We are missing the end of a component that the alarm is for.
85 Use Alarms.set_end().
86 """
89class ComponentStartMissing(IncompleteAlarmInformation):
90 """We are missing the start of a component that the alarm is for.
92 Use Alarms.set_start().
93 """
96class FeatureWillBeRemovedInFutureVersion(DeprecationWarning):
97 """This feature will be removed in a future version."""
100class GloballyUniqueTZIDGuessed(UserWarning):
101 """A globally unique TZID was resolved by stripping the vendor prefix.
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 """
109def _repr_index(index: str | int) -> str:
110 """Create a JSON compatible representation for the index.
112 Parameters:
113 index: It is either a dict key (string) or a list position (integer).
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)
124class JCalParsingError(InvalidCalendar):
125 """Could not parse a part of the JCal."""
127 _default_value = object()
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.
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)
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.
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
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
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.
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.
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])
239 _type_names = {
240 str: "a string",
241 int: "an integer",
242 float: "a float",
243 bool: "a boolean",
244 }
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 )
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 )
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.
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.
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.
319 Raises:
320 ~error.JCalParsingError: If ``name`` is not a valid lowercase
321 iCalendar token.
323 See also:
324 :meth:`~icalendar.parser.string.validate_token`
325 """
326 from icalendar.parser.string import validate_token
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)
342__all__ = [
343 "BrokenCalendarProperty",
344 "ComponentEndMissing",
345 "ComponentStartMissing",
346 "FeatureWillBeRemovedInFutureVersion",
347 "ICalParsingError",
348 "IncompleteAlarmInformation",
349 "IncompleteComponent",
350 "InvalidCalendar",
351 "JCalParsingError",
352 "LocalTimezoneMissing",
353]