1"""UNKNOWN values from :rfc:`7265`."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING, Any, ClassVar
6from xml.etree.ElementTree import Element, SubElement
7
8from icalendar.error import JCalParsingError
9from icalendar.parser import Parameters
10from icalendar.parser.xcal.string import _to_valid_xml_string
11from icalendar.parser.xcal.wrapper import from_xcal_wrapper
12from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode
13
14if TYPE_CHECKING:
15 from icalendar.compatibility import Self
16 from icalendar.parser.content_line import Contentline
17 from icalendar.parser.xcal.base import XCalParser
18
19
20class vUnknown(str):
21 """A property value of the :rfc:`7265#section-5` reserved UNKNOWN value data type.
22
23 .. versionchanged:: 7.2.0
24
25 Previously ``vUnknown`` inherited from ``vText``, which unescapes values.
26 Now ``vUnknown`` doesn't unescape its values, which is the correct behavior.
27
28 Unlike :class:`~icalendar.prop.text.vText`, the value is preserved verbatim
29 when imported from and exported to iCalendar data, without :rfc:`5545` escaping
30 or unescaping. When the value type of an unrecognized property is not known,
31 then no escaping rules can be applied, and the value must be preserved as is
32 round-trip.
33
34 For :rfc:`6321`, vUnknown plays an important role in preserving the VALUE parameter.
35
36 .. code-block:: pycon
37
38 >>> from icalendar import Calendar, vUnknown
39 >>> cal = Calendar()
40 >>> cal.add("X-PROP", vUnknown("lalala", params={"VALUE": "X-VALUE"}))
41 >>> print(cal.to_xcal(indent=2).decode("utf-8"))
42 <?xml version="1.0" encoding="UTF-8"?>
43 <icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
44 <vcalendar>
45 <properties>
46 <x-prop>
47 <x-value>lalala</x-value>
48 </x-prop>
49 </properties>
50 </vcalendar>
51 </icalendar>
52
53 See also:
54
55 :rfc:`7265#section-5.1`
56 """
57
58 default_value: ClassVar[str] = "UNKNOWN"
59 params: Parameters
60 __slots__ = ("encoding", "params")
61
62 def __new__(
63 cls,
64 value: ICAL_TYPE,
65 encoding: str = DEFAULT_ENCODING,
66 /,
67 params: dict[str, Any] | None = None,
68 ) -> Self:
69 value = to_unicode(value, encoding=encoding)
70 self = super().__new__(cls, value)
71 self.encoding = encoding
72 self.params = Parameters(params)
73 return self
74
75 def __repr__(self) -> str:
76 return f"{self.__class__.__name__}({self.to_ical()!r})"
77
78 def to_ical(self) -> bytes:
79 r"""Return the value verbatim, without :rfc:`5545` escaping.
80
81 This method's implementation is different from that in
82 :class:`~icalendar.prop.text.vText`, whose
83 :meth:`~icalendar.prop.text.vText.to_ical` method escapes ``;``, ``,``,
84 ``\``, and newlines.
85
86 Example:
87
88 The semicolon is kept verbatim for UNKNOWN, unlike a TEXT value
89 which would escape it as ``\\;``.
90
91 .. code-block:: pycon
92
93 >>> from icalendar.prop import vText, vUnknown
94 >>> vUnknown("a;b").to_ical()
95 b'a;b'
96 >>> vText("a;b").to_ical()
97 b'a\\;b'
98
99 See also:
100
101 :rfc:`7265#section-5.2`
102
103 """
104 return self.encode(self.encoding)
105
106 @classmethod
107 def from_ical(cls, ical: ICAL_TYPE) -> Self:
108 """Take the value verbatim, without unescaping."""
109 return cls(ical)
110
111 @classmethod
112 def get_value_from_content_line(cls, line: Contentline) -> str:
113 """Return this type's value from ``line``, taken verbatim.
114
115 Value types that must not have their value unescaped provide this
116 method, and the parser uses it to obtain the value instead of
117 :meth:`~icalendar.parser.content_line.Contentline.parts`. For
118 :rfc:`7265` ``UNKNOWN`` values, the escaping rules of the real value
119 type are not known, so no unescaping can be applied.
120 """
121 return line.raw_parts()[2]
122
123 @property
124 def ical_value(self) -> str:
125 """The string value of the property."""
126 return str(self)
127
128 from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE
129
130 def to_jcal(self, name: str) -> list:
131 """The jCal representation of this property, according to :rfc:`7265#section-5.1`.
132
133 If the property doesn't include a VALUE property parameter and its value
134 type is not known, then its value type is set to ``"unknown"``. Else the
135 property's value type is converted to lowercase.
136
137 The property's value is the unprocessed value text, aside from standard
138 JSON string escaping.
139 """
140 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)]
141
142 @classmethod
143 def examples(cls) -> list[Self]:
144 """Examples of vUnknown."""
145 return [cls("Some property text.")]
146
147 @classmethod
148 def from_jcal(cls, jcal_property: list) -> Self:
149 """Parse jCal from :rfc:`7265`, taking the value verbatim.
150
151 Parameters:
152 jcal_property: The jCal property to parse.
153
154 Raises:
155 ~error.JCalParsingError: If the provided jCal is invalid.
156 """
157 JCalParsingError.validate_property(jcal_property, cls)
158 string = jcal_property[3]
159 JCalParsingError.validate_value_type(string, str, cls, 3)
160 return cls(
161 string,
162 params=Parameters.from_jcal_property(jcal_property),
163 )
164
165 @classmethod
166 def parse_jcal_value(cls, jcal_value: Any) -> Self:
167 """Parse a jCal value into a vUnknown."""
168 JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls)
169 return cls(str(jcal_value))
170
171 def to_xcal(self, element: Element) -> None:
172 """Add the xCal representation of this property according to :rfc:`6321`."""
173 self.params.to_xcal(element)
174 value_parameter = self.params.value
175 if value_parameter is None:
176 value_parameter = self.default_value
177 element = SubElement(element, value_parameter.lower())
178 element.text = _to_valid_xml_string(self)
179
180 @classmethod
181 @from_xcal_wrapper
182 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
183 """Parse xCal from :rfc:`6321`.
184
185 Parameters:
186 xml: The XML to parse or a parser.
187
188 Raises:
189 ~error.XCalParsingError: If the provided xCal is invalid.
190 """
191 element = parser.parse_tag(cls.default_value)
192 params.value = cls.default_value # UNKNOWN is never the default type
193 return cls(element.get_xsd_string(), params=params)
194
195 @classmethod
196 @from_xcal_wrapper
197 def from_first_xcal_element(cls, parser: XCalParser, params: Parameters) -> Self:
198 """Parse xCal from :rfc:`6321`.
199
200 This class is used as a fallback if no other type can parse this.
201
202 Parameters:
203 xml: The XML to parse or a parser.
204
205 Raises:
206 ~error.XCalParsingError: If the provided xCal is invalid.
207 """
208 element = parser.parse_tag()
209 params.value = element.tag.upper() # set the VALUE parameter
210 return cls(element.get_xsd_string(), params=params)
211
212
213__all__ = ["vUnknown"]