Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/text.py: 66%
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
6from icalendar.compatibility import Self
7from icalendar.error import JCalParsingError
8from icalendar.parser import Parameters, _escape_char
9from icalendar.parser_tools import DEFAULT_ENCODING, ICAL_TYPE, to_unicode
11# :rfc:`5545#section-3.3.11` defines TEXT as
12# ``*(TSAFE-CHAR / ":" / DQUOTE / ESCAPED-CHAR)`` where TSAFE-CHAR is in turn
13# defined by the following grammar in :rfc:`5545#section-3.1`.
14#
15# .. code-block:: text
16#
17# TSAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-5B /
18# %x5D-7E / NON-US-ASCII
19# ; Any character except CONTROLs not needed by the current
20# ; character set, DQUOTE, ";", ":", "\", ","
21#
22# CONTROL is defined in the same section as ``%x00-08 / %x0A-1F / %x7F``, so no
23# control character except the horizontal tab may appear in a TEXT value.
24# The line feed, ``\x0a``, is additionally accepted here because it is the
25# result of the escaped sequences ``\N`` and ``\n``.
26_UNSAFE_TEXT_CHARS = re.compile(r"[\x00-\x08\x0b-\x1f\x7f]")
29def _strip_unsafe_text_chars(value: object) -> str:
30 r"""Remove CONTROL characters that :rfc:`5545#section-3.3.11` forbids in TEXT.
32 ``\r\n`` and a lone ``\r`` become ``\n`` first, so an intentional line
33 break is kept and escaped on serialize. Remaining matches of
34 :data:`_UNSAFE_TEXT_CHARS` (NUL, other C0 controls, DEL) are stripped.
35 HTAB and LF are left as-is.
37 :func:`~icalendar.parser_tools.to_unicode` leaves non-``str``/``bytes``
38 values unchanged, and callers such as :meth:`vUid.new` pass a
39 :class:`uuid.UUID`. Coerce those to ``str`` the same way ``str.__new__``
40 did before this filter ran.
41 """
42 if not isinstance(value, str):
43 value = str(value)
44 value = value.replace("\r\n", "\n").replace("\r", "\n")
45 return _UNSAFE_TEXT_CHARS.sub("", value)
48class vText(str):
49 r"""vText is a data type that contains human-readable text values.
51 The vText property uses the :rfc:`5545#section-3.3.11` TEXT value type
52 in various icalendar properties to show free-form text that others can read.
53 This class can be created from Python strings, and can be used to add text
54 descriptions to calendar events.
56 To create a TEXT object, pass in the string you want when creating the
57 object.
59 To add a line break, use ``\n`` or ``\N``.
61 Use the LANGUAGE property parameter to set the language of the text.
63 When the TEXT object is created, CONTROL characters other than HTAB
64 are removed so both :meth:`to_ical` and :meth:`to_jcal` stay valid
65 :rfc:`5545#section-3.3.11` TEXT. Parsing does not raise; the value is corrected.
66 When the TEXT object is serialized to an icalendar stream, COMMA,
67 SEMICOLON, BACKSLASH, and line breaks are escaped.
69 Contrast TEXT with the UNKNOWN value data type specified in :rfc:`7265#section-5`.
70 UNKNOWN is implemented in the Python class :class:`~icalendar.prop.unknown.vUnknown`,
71 which does **not** apply this escaping and preserves its value verbatim,
72 because the escaping rules of an unrecognized value type are not known.
73 :class:`~icalendar.prop.unknown.vUnknown` deliberately does not inherit from
74 ``vText``, so the two don't share escaping behavior.
76 .. versionchanged:: 0.0.0
78 Remove CONTROL characters other than HTAB.
80 Examples:
82 vText property as a TEXT value type.
84 .. code-block:: text
86 Project XYZ Final Review\nConference Room - 3B\nCome Prepared.
88 Create a vText property, and display it in a readable format.
90 .. code-block:: pycon
92 >>> from icalendar.prop import vText
93 >>> desc = 'Project XYZ Final Review\nConference Room - 3B\nCome Prepared.'
94 >>> text = vText(desc)
95 >>> text
96 vText(b'Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared.')
97 >>> print(text.ical_value)
98 Project XYZ Final Review
99 Conference Room - 3B
100 Come Prepared.
102 Add a SUMMARY to an event, then display its value as a vText property then in a readable format:
104 .. code-block:: pycon
106 >>> from icalendar import Event
107 >>> event = Event()
108 >>> event.add('SUMMARY', desc)
109 >>> event['SUMMARY']
110 vText(b'Project XYZ Final Review\\nConference Room - 3B\\nCome Prepared.')
111 >>> print(event.to_ical().decode())
112 BEGIN:VEVENT
113 SUMMARY:Project XYZ Final Review\nConference Room - 3B\nCome Prepared.
114 END:VEVENT
116 """
118 default_value: ClassVar[str] = "TEXT"
119 params: Parameters
120 __slots__ = ("encoding", "params")
122 def __new__(
123 cls,
124 value: ICAL_TYPE,
125 encoding: str = DEFAULT_ENCODING,
126 /,
127 params: dict[str, Any] | None = None,
128 ) -> Self:
129 value = _strip_unsafe_text_chars(to_unicode(value, encoding=encoding))
130 self = super().__new__(cls, value)
131 self.encoding = encoding
132 self.params = Parameters(params)
133 return self
135 def __repr__(self) -> str:
136 return f"{self.__class__.__name__}({self.to_ical()!r})"
138 def to_ical(self) -> bytes:
139 """Serialize this TEXT value with :rfc:`5545` escaping."""
140 return _escape_char(self).encode(self.encoding)
142 @classmethod
143 def from_ical(cls, ical: ICAL_TYPE) -> Self:
144 r"""Parse a TEXT value from its iCalendar representation.
146 Control characters that :rfc:`5545#section-3.3.11` does not allow
147 in TEXT get removed.
149 .. versionchanged:: 0.0.0
151 Don't raise for unsafe characters in TEXT, but instead remove them.
152 The parsed object can now be read and later serialized.
154 .. seealso::
156 - :issue:`1712`
157 - :pr:`1723`
158 """
159 return cls(ical)
161 @property
162 def ical_value(self) -> str:
163 """The string value of the text."""
164 return str(self)
166 from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE
168 def to_jcal(self, name: str) -> list:
169 """The jCal representation of this property according to :rfc:`7265`."""
170 if name == "request-status": # TODO: maybe add a vRequestStatus class?
171 return [name, {}, "text", self.split(";", 2)]
172 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)]
174 @classmethod
175 def examples(cls) -> list[Self]:
176 """Examples of vText."""
177 return [cls("Hello World!")]
179 @classmethod
180 def from_jcal(cls, jcal_property: list) -> Self:
181 """Parse jCal from :rfc:`7265`.
183 Parameters:
184 jcal_property: The jCal property to parse.
186 Raises:
187 ~error.JCalParsingError: If the provided jCal is invalid.
188 """
189 JCalParsingError.validate_property(jcal_property, cls)
190 name = jcal_property[0]
191 if name == "categories":
192 from icalendar.prop import vCategory
194 return vCategory.from_jcal(jcal_property)
195 string = jcal_property[3] # TODO: accept list or string but join with ;
196 if name == "request-status": # TODO: maybe add a vRequestStatus class?
197 JCalParsingError.validate_list_type(jcal_property[3], str, cls, 3)
198 string = ";".join(jcal_property[3])
199 JCalParsingError.validate_value_type(string, str, cls, 3)
200 return cls(
201 string,
202 params=Parameters.from_jcal_property(jcal_property),
203 )
205 @classmethod
206 def parse_jcal_value(cls, jcal_value: Any) -> Self:
207 """Parse a jCal value into a vText."""
208 JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls)
209 return cls(str(jcal_value))
212__all__ = ["vText"]