Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/text.py: 65%
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"""TEXT values from :rfc:`5545`."""
3import re
4from typing import Any, ClassVar
5from xml.etree.ElementTree import Element, SubElement
7from icalendar.compatibility import Self
8from icalendar.error import JCalParsingError
9from icalendar.parser import Parameters, _escape_char
10from icalendar.parser.xcal.base import XCalParser
11from icalendar.parser.xcal.string import _to_valid_xml_string
12from icalendar.parser.xcal.wrapper import from_xcal_wrapper
13from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode
15# :rfc:`5545#section-3.3.11` defines TEXT as
16# ``*(TSAFE-CHAR / ":" / DQUOTE / ESCAPED-CHAR)`` where TSAFE-CHAR is in turn
17# defined by the following grammar in :rfc:`5545#section-3.1`.
18#
19# .. code-block:: text
20#
21# TSAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-5B /
22# %x5D-7E / NON-US-ASCII
23# ; Any character except CONTROLs not needed by the current
24# ; character set, DQUOTE, ";", ":", "\", ","
25#
26# CONTROL is defined in the same section as ``%x00-08 / %x0A-1F / %x7F``, so no
27# control character except the horizontal tab may appear in a TEXT value.
28# The line feed, ``\x0a``, is additionally accepted here because it is the
29# result of the escaped sequences ``\N`` and ``\n``.
30_UNSAFE_TEXT_CHARS = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
33def _strip_unsafe_text_chars(value: object) -> str:
34 r"""Remove CONTROL characters that :rfc:`5545#section-3.3.11` forbids in TEXT.
36 ``\r\n`` and a lone ``\r`` become ``\n`` first, so an intentional line
37 break is kept and escaped on serialize. Remaining matches of
38 :data:`_UNSAFE_TEXT_CHARS` (NUL, other C0 controls, DEL) are stripped.
39 HTAB and LF are left as-is.
41 :func:`~icalendar.parser_tools.to_unicode` leaves non-``str``/``bytes``
42 values unchanged, and callers such as :meth:`vUid.new` pass a
43 :class:`uuid.UUID`. Coerce those to ``str`` the same way ``str.__new__``
44 did before this filter ran.
46 .. seealso::
48 :func:`~icalendar.parser.xcal.string._to_valid_xml_string`
49 `Issue 1712 <https://github.com/collective/icalendar/issues/1712>`_,
50 `Issue 1888 <https://github.com/collective/icalendar/issues/1888>`_,
51 """
52 if not isinstance(value, str):
53 value = str(value)
54 value = value.replace("\r\n", "\n").replace("\r", "\n")
55 return _UNSAFE_TEXT_CHARS.sub("", value)
58class vText(str):
59 r"""vText is a data type that contains human-readable text values.
61 The vText property uses the :rfc:`5545#section-3.3.11` TEXT value type
62 in various icalendar properties to show free-form text that others can read.
63 This class can be created from Python strings, and can be used to add text
64 descriptions to calendar events.
66 To create a TEXT object, pass in the string you want when creating the
67 object.
69 To add a line break, use ``\n`` or ``\N``.
71 Use the LANGUAGE property parameter to set the language of the text.
73 When the TEXT object is created, CONTROL characters other than HTAB
74 are removed so both :meth:`to_ical` and :meth:`to_jcal` stay valid
75 :rfc:`5545#section-3.3.11` TEXT. Parsing does not raise; the value is corrected.
76 When the TEXT object is serialized to an icalendar stream, COMMA,
77 SEMICOLON, BACKSLASH, and line breaks are escaped.
79 Contrast TEXT with the UNKNOWN value data type specified in :rfc:`7265#section-5`.
80 UNKNOWN is implemented in the Python class :class:`~icalendar.prop.unknown.vUnknown`,
81 which does **not** apply this escaping and preserves its value verbatim,
82 because the escaping rules of an unrecognized value type are not known.
83 :class:`~icalendar.prop.unknown.vUnknown` deliberately does not inherit from
84 ``vText``, so the two don't share escaping behavior.
86 .. versionchanged:: 0.0.0
88 Remove CONTROL characters other than HTAB.
90 Examples:
92 vText property as a TEXT value type.
94 .. code-block:: text
96 Project XYZ Final Review\nConference Room - 3B\nCome Prepared.
98 Create a vText property, and display it in a readable format.
100 .. code-block:: pycon
102 >>> from icalendar.prop import vText
103 >>> desc = 'Project XYZ Final Review\nConference Room - 3B\nCome Prepared.'
104 >>> text = vText(desc)
105 >>> text
106 vText(b'Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared.')
107 >>> print(text.ical_value)
108 Project XYZ Final Review
109 Conference Room - 3B
110 Come Prepared.
112 Add a SUMMARY to an event, then display its value as a vText property then in a readable format:
114 .. code-block:: pycon
116 >>> from icalendar import Event
117 >>> event = Event()
118 >>> event.add('SUMMARY', desc)
119 >>> event['SUMMARY']
120 vText(b'Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared.')
121 >>> print(event.to_ical().decode())
122 BEGIN:VEVENT
123 SUMMARY:Project XYZ Final Review\nConference Room - 3B\nCome Prepared.
124 END:VEVENT
126 """
128 default_value: ClassVar[str] = "TEXT"
129 params: Parameters
130 __slots__ = ("encoding", "params")
132 def __new__(
133 cls,
134 value: ICAL_TYPE,
135 encoding: str = DEFAULT_ENCODING,
136 /,
137 params: dict[str, Any] | None = None,
138 ) -> Self:
139 value = _strip_unsafe_text_chars(to_unicode(value, encoding=encoding))
140 self = super().__new__(cls, value)
141 self.encoding = encoding
142 self.params = Parameters(params)
143 return self
145 def __repr__(self) -> str:
146 return f"{self.__class__.__name__}({self.to_ical()!r})"
148 def to_ical(self) -> bytes:
149 """Serialize this TEXT value with :rfc:`5545` escaping."""
150 return _escape_char(self).encode(self.encoding)
152 @classmethod
153 def from_ical(cls, ical: ICAL_TYPE) -> Self:
154 r"""Parse a TEXT value from its iCalendar representation.
156 Control characters that :rfc:`5545#section-3.3.11` does not allow
157 in TEXT get removed.
159 .. versionchanged:: 0.0.0
161 Don't raise for unsafe characters in TEXT, but instead remove them.
162 The parsed object can now be read and later serialized.
164 .. seealso::
166 - :issue:`1712`
167 - :pr:`1723`
168 """
169 return cls(ical)
171 @property
172 def ical_value(self) -> str:
173 """The string value of the text."""
174 return str(self)
176 from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE
178 def to_jcal(self, name: str) -> list:
179 """The jCal representation of this property according to :rfc:`7265`."""
180 if name == "request-status": # TODO: maybe add a vRequestStatus class?
181 return [name, {}, "text", self.split(";", 2)]
182 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)]
184 @classmethod
185 def examples(cls) -> list[Self]:
186 """Examples of vText."""
187 return [cls("Hello World!")]
189 @classmethod
190 def from_jcal(cls, jcal_property: list) -> Self:
191 """Parse jCal from :rfc:`7265`.
193 Parameters:
194 jcal_property: The jCal property to parse.
196 Raises:
197 ~error.JCalParsingError: If the provided jCal is invalid.
198 """
199 JCalParsingError.validate_property(jcal_property, cls)
200 name = jcal_property[0]
201 if name == "categories":
202 from icalendar.prop import vCategory
204 return vCategory.from_jcal(jcal_property)
205 string = jcal_property[3] # TODO: accept list or string but join with ;
206 if name == "request-status": # TODO: maybe add a vRequestStatus class?
207 JCalParsingError.validate_list_type(jcal_property[3], str, cls, 3)
208 string = ";".join(jcal_property[3])
209 JCalParsingError.validate_value_type(string, str, cls, 3)
210 return cls(
211 string,
212 params=Parameters.from_jcal_property(jcal_property),
213 )
215 @classmethod
216 def parse_jcal_value(cls, jcal_value: Any) -> Self:
217 """Parse a jCal value into a vText."""
218 JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls)
219 return cls(str(jcal_value))
221 def to_xcal(self, element: Element) -> None:
222 """Add the xCal representation of this property according to :rfc:`6321`."""
223 self.params.to_xcal(element)
224 element = SubElement(element, self.default_value.lower())
225 element.text = _to_valid_xml_string(self)
227 @classmethod
228 @from_xcal_wrapper
229 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self:
230 """Parse xCal from :rfc:`6321`.
232 Parameters:
233 xml: The XML to parse or a parser.
235 Raises:
236 ~error.XCalParsingError: If the provided xCal is invalid.
237 """
238 element = parser.parse_tag(cls.default_value)
239 return cls(element.get_xsd_string(), params=params)
241 @classmethod
242 @from_xcal_wrapper
243 def from_xcal_in_recur(cls, parser: XCalParser, _params: Parameters) -> Self:
244 """Parse xCal from :rfc:`6321`.
246 Parameters:
247 xml: The XML to parse or a parser.
249 Raises:
250 ~error.XCalParsingError: If the provided xCal is invalid.
251 """
252 element = parser.parse_tag()
253 return cls(element.get_xsd_string())
256__all__ = ["vText"]