1"""Parsing error value preservation."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING, Any, ClassVar
6
7from icalendar.error import BrokenCalendarProperty
8from icalendar.parser_tools import DEFAULT_ENCODING
9from icalendar.prop.text import vText
10
11if TYPE_CHECKING:
12 from icalendar.compatibility import Self
13 from icalendar.parser import Parameters
14
15
16class vBroken(vText):
17 """Property that failed to parse, preserving raw value as text.
18
19 Represents property values that failed to parse with their expected
20 type. The raw iCalendar string is preserved for round-trip serialization:
21 ical -> xcal -> ical
22 """
23
24 default_value: ClassVar[str] = "X-BROKEN"
25 """The default value is x-broken.
26
27 With this, we can identify broken properties in xCal round-trip.
28 """
29 __slots__ = ("expected_type", "parse_error", "property_name")
30
31 def __new__(
32 cls,
33 value: str | bytes,
34 encoding: str = DEFAULT_ENCODING,
35 /,
36 params: dict[str, Any] | None = None,
37 expected_type: str | None = None,
38 property_name: str | None = None,
39 parse_error: BaseException | None = None,
40 ) -> Self:
41 self = super().__new__(cls, value, encoding, params=params)
42 self.expected_type = expected_type
43 self.property_name = property_name
44 self.parse_error = parse_error
45 return self
46
47 def __getattr__(self, name: str):
48 """Raise BrokenCalendarProperty for attribute access.
49
50 Attributes like ``.dt`` that the expected type would have
51 raise :class:`~icalendar.error.BrokenCalendarProperty`
52 with the original parse error chained. Private attributes
53 (starting with ``_``) raise normal ``AttributeError`` to
54 preserve ``getattr(..., default)`` patterns.
55 """
56 if name.startswith("_"):
57 raise AttributeError(name)
58 raise BrokenCalendarProperty(
59 f"Cannot access {name!r} on broken property "
60 f"{self.property_name!r} (expected {self.expected_type!r}): "
61 f"{self.parse_error}"
62 ) from self.parse_error
63
64 def __repr__(self) -> str:
65 return (
66 f"{self.__class__.__name__}({str(self)!r}, "
67 f"expected_type={self.expected_type!r}, "
68 f"property_name={self.property_name!r})"
69 )
70
71 def to_jcal(self, name: str) -> list:
72 """Serialize the unparsed value as the :rfc:`7265#section-7.1` ``unknown`` type."""
73 return [name, self.params.to_jcal(), "unknown", str(self)]
74
75 @classmethod
76 def from_parse_error(
77 cls,
78 raw_value: str,
79 params: Parameters,
80 property_name: str,
81 expected_type: str,
82 error: Exception,
83 ) -> Self:
84 """Create vBroken from parse failure."""
85 return cls(
86 raw_value,
87 params=params,
88 expected_type=expected_type,
89 property_name=property_name,
90 parse_error=error,
91 )
92
93 @classmethod
94 def examples(cls) -> list[Self]:
95 """Examples of vBroken."""
96 return [
97 cls(
98 "INVALID-DATE",
99 expected_type="date-time",
100 property_name="DTSTART",
101 parse_error=ValueError("Invalid date format"),
102 )
103 ]
104
105
106__all__ = ["vBroken"]