Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop/text.py: 67%

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

75 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 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)] 

181 

182 @classmethod 

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

184 """Examples of vText.""" 

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

186 

187 @classmethod 

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

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

190 

191 Parameters: 

192 jcal_property: The jCal property to parse. 

193 

194 Raises: 

195 ~icalendar.error.JCalParsingError: If the provided jCal is invalid. 

196 """ 

197 JCalParsingError.validate_property(jcal_property, cls) 

198 name = jcal_property[0] 

199 if name == "categories": 

200 from icalendar.prop import vCategory 

201 

202 return vCategory.from_jcal(jcal_property) 

203 if name == "request-status": 

204 from .request_status import vRequestStatus 

205 

206 return vRequestStatus.from_jcal(jcal_property) 

207 string = jcal_property[3] # TODO: accept list or string but join with ; 

208 JCalParsingError.validate_value_type(string, str, cls, 3) 

209 return cls( 

210 string, 

211 params=Parameters.from_jcal_property(jcal_property), 

212 ) 

213 

214 @classmethod 

215 def parse_jcal_value(cls, jcal_value: Any) -> Self: 

216 """Parse a jCal value into a vText.""" 

217 JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls) 

218 return cls(str(jcal_value)) 

219 

220 def to_xcal(self, element: Element) -> None: 

221 """Add the xCal representation of this property according to :rfc:`6321`.""" 

222 self.params.to_xcal(element) 

223 element = SubElement(element, self.default_value.lower()) 

224 element.text = _to_valid_xml_string(self) 

225 

226 @classmethod 

227 @from_xcal_wrapper 

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

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

230 

231 Parameters: 

232 xml: The XML to parse or a parser. 

233 

234 Raises: 

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

236 """ 

237 element = parser.parse_tag(cls.default_value) 

238 return cls(element.get_xsd_string(), params=params) 

239 

240 @classmethod 

241 @from_xcal_wrapper 

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

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

244 

245 Parameters: 

246 xml: The XML to parse or a parser. 

247 

248 Raises: 

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

250 """ 

251 element = parser.parse_tag() 

252 return cls(element.get_xsd_string()) 

253 

254 

255__all__ = ["vText"]