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

59 statements  

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

2 

3import re 

4from typing import Any, ClassVar 

5 

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 

10 

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

27 

28 

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

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

31 

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. 

36 

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) 

46 

47 

48class vText(str): 

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

50 

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. 

55 

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

57 object. 

58 

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

60 

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

62 

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. 

68 

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. 

75 

76 .. versionchanged:: 0.0.0 

77 

78 Remove CONTROL characters other than HTAB. 

79 

80 Examples: 

81 

82 vText property as a TEXT value type. 

83 

84 .. code-block:: text 

85 

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

87 

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

89 

90 .. code-block:: pycon 

91 

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. 

101 

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

103 

104 .. code-block:: pycon 

105 

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 

115 

116 """ 

117 

118 default_value: ClassVar[str] = "TEXT" 

119 params: Parameters 

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

121 

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 

134 

135 def __repr__(self) -> str: 

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

137 

138 def to_ical(self) -> bytes: 

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

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

141 

142 @classmethod 

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

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

145 

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

147 in TEXT get removed. 

148 

149 .. versionchanged:: 0.0.0 

150 

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

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

153 

154 .. seealso:: 

155 

156 - :issue:`1712` 

157 - :pr:`1723` 

158 """ 

159 return cls(ical) 

160 

161 @property 

162 def ical_value(self) -> str: 

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

164 return str(self) 

165 

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

167 

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

173 

174 @classmethod 

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

176 """Examples of vText.""" 

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

178 

179 @classmethod 

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

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

182 

183 Parameters: 

184 jcal_property: The jCal property to parse. 

185 

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 

193 

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 ) 

204 

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

210 

211 

212__all__ = ["vText"]