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

77 statements  

1"""TEXT values from :rfc:`5545`.""" 

2 

3import re 

4from typing import Any, ClassVar 

5from xml.etree.ElementTree import Element, SubElement 

6 

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 

14 

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]") 

31 

32 

33def _strip_unsafe_text_chars(value: object) -> str: 

34 r"""Remove CONTROL characters that :rfc:`5545#section-3.3.11` forbids in TEXT. 

35 

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. 

40 

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. 

45 

46 .. seealso:: 

47 

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) 

56 

57 

58class vText(str): 

59 r"""vText is a data type that contains human-readable text values. 

60 

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. 

65 

66 To create a TEXT object, pass in the string you want when creating the 

67 object. 

68 

69 To add a line break, use ``\n`` or ``\N``. 

70 

71 Use the LANGUAGE property parameter to set the language of the text. 

72 

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. 

78 

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. 

85 

86 .. versionchanged:: 0.0.0 

87 

88 Remove CONTROL characters other than HTAB. 

89 

90 Examples: 

91 

92 vText property as a TEXT value type. 

93 

94 .. code-block:: text 

95 

96 Project XYZ Final Review\nConference Room - 3B\nCome Prepared. 

97 

98 Create a vText property, and display it in a readable format. 

99 

100 .. code-block:: pycon 

101 

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. 

111 

112 Add a SUMMARY to an event, then display its value as a vText property then in a readable format: 

113 

114 .. code-block:: pycon 

115 

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 

125 

126 """ 

127 

128 default_value: ClassVar[str] = "TEXT" 

129 params: Parameters 

130 __slots__ = ("encoding", "params") 

131 

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 

144 

145 def __repr__(self) -> str: 

146 return f"{self.__class__.__name__}({self.to_ical()!r})" 

147 

148 def to_ical(self) -> bytes: 

149 """Serialize this TEXT value with :rfc:`5545` escaping.""" 

150 return _escape_char(self).encode(self.encoding) 

151 

152 @classmethod 

153 def from_ical(cls, ical: ICAL_TYPE) -> Self: 

154 r"""Parse a TEXT value from its iCalendar representation. 

155 

156 Control characters that :rfc:`5545#section-3.3.11` does not allow 

157 in TEXT get removed. 

158 

159 .. versionchanged:: 0.0.0 

160 

161 Don't raise for unsafe characters in TEXT, but instead remove them. 

162 The parsed object can now be read and later serialized. 

163 

164 .. seealso:: 

165 

166 - :issue:`1712` 

167 - :pr:`1723` 

168 """ 

169 return cls(ical) 

170 

171 @property 

172 def ical_value(self) -> str: 

173 """The string value of the text.""" 

174 return str(self) 

175 

176 from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE 

177 

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)] 

183 

184 @classmethod 

185 def examples(cls) -> list[Self]: 

186 """Examples of vText.""" 

187 return [cls("Hello World!")] 

188 

189 @classmethod 

190 def from_jcal(cls, jcal_property: list) -> Self: 

191 """Parse jCal from :rfc:`7265`. 

192 

193 Parameters: 

194 jcal_property: The jCal property to parse. 

195 

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 

203 

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 ) 

214 

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)) 

220 

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) 

226 

227 @classmethod 

228 @from_xcal_wrapper 

229 def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self: 

230 """Parse xCal from :rfc:`6321`. 

231 

232 Parameters: 

233 xml: The XML to parse or a parser. 

234 

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) 

240 

241 @classmethod 

242 @from_xcal_wrapper 

243 def from_xcal_in_recur(cls, parser: XCalParser, _params: Parameters) -> Self: 

244 """Parse xCal from :rfc:`6321`. 

245 

246 Parameters: 

247 xml: The XML to parse or a parser. 

248 

249 Raises: 

250 ~error.XCalParsingError: If the provided xCal is invalid. 

251 """ 

252 element = parser.parse_tag() 

253 return cls(element.get_xsd_string()) 

254 

255 

256__all__ = ["vText"]