Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/parser/ical/component.py: 92%

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

140 statements  

1"""Parsing a component's iCalendar data.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, ClassVar 

6 

7from icalendar.error import ICalParsingError, InvalidToken 

8from icalendar.parser.content_line import Contentline, Contentlines 

9from icalendar.parser.property import split_on_unescaped_comma 

10from icalendar.prop import vBroken 

11from icalendar.timezone import tzp 

12 

13if TYPE_CHECKING: 

14 from icalendar.cal import Component, ComponentFactory 

15 from icalendar.parser.parameter import Parameters 

16 from icalendar.prop import VPROPERTY, TypesFactory 

17 

18 

19class ComponentIcalParser: 

20 """A parser for a component's iCalendar data. 

21 

22 This uses the template method pattern, where the main parsing 

23 logic can be refined in subclasses. 

24 """ 

25 

26 datetime_names: ClassVar[tuple[str, ...]] = ( 

27 "DTSTART", 

28 "DTEND", 

29 "RECURRENCE-ID", 

30 "DUE", 

31 "RDATE", 

32 "EXDATE", 

33 ) 

34 """Names to check for TZID parameter when parsing datetimes. 

35 

36 Their ``from_ical`` methods take an optional ``tzid`` argument, 

37 which is used if the property has a TZID parameter. 

38 """ 

39 

40 def __init__( 

41 self, 

42 data: bytes | str | list[Contentline], 

43 component_factory: ComponentFactory, 

44 types_factory: TypesFactory, 

45 ) -> None: 

46 """Initialize the parser with the raw data. 

47 

48 Parameters: 

49 data: The raw iCalendar data to parse, either as bytes 

50 or a list of content lines. 

51 component_factory: The factory to use for creating components. 

52 types_factory: The factory to use for creating property values. 

53 """ 

54 self._data = data 

55 self._component_factory = component_factory 

56 self._types_factory = types_factory 

57 self._tzp = tzp 

58 

59 _content_lines: list[Contentline] 

60 

61 def contains_component(self, name: str) -> bool: 

62 """Check if the parser contains a component.""" 

63 self.initialize_parsing() 

64 begin_line = "BEGIN:" + name.upper() 

65 return any( 

66 len(content_line) == len(begin_line) and content_line.upper() == begin_line 

67 for content_line in self._content_lines 

68 ) 

69 

70 def contains_uid(self, uid: str) -> bool: 

71 """Determines whether the component contains a ``uid``. 

72 

73 Returns: 

74 ``True`` if the component contains a ``uid``, else ``False``. 

75 """ 

76 self.initialize_parsing() 

77 return any(uid in line for line in self._content_lines) 

78 

79 def initialize_parsing(self): 

80 self._stack: list[Component] = [] 

81 self._components: list[Component] = [] 

82 self._data = self._content_lines = ( 

83 self._data 

84 if isinstance(self._data, list) 

85 else Contentlines.from_ical(self._data) 

86 ) 

87 self._content_lines_iterator = iter(self._content_lines) 

88 

89 def handle_line_parse_error(self, exception: Exception): 

90 """Handle a line parsing error.""" 

91 # if unable to parse a line within a component 

92 # that ignores exceptions, mark the component 

93 # as broken and skip the line. otherwise raise. 

94 component = self.component 

95 if not component or not component.ignore_exceptions: 

96 raise exception 

97 component.errors.append((None, str(exception))) 

98 

99 def get_component_class(self, name: str) -> type[Component]: 

100 """Get the component class for a name from the ``BEGIN`` line. 

101 

102 Parameters: 

103 name: Required. The component name from the ``BEGIN`` line. 

104 

105 Raises: 

106 ~icalendar.error.ICalParsingError: If the name is not a valid token. 

107 """ 

108 try: 

109 return self._component_factory.get_component_class(name) 

110 except InvalidToken as error: 

111 raise ICalParsingError( 

112 "The component name must be a valid iCalendar token", 

113 value=name, 

114 ) from error 

115 

116 def handle_begin_component(self, vals: str) -> None: 

117 """Handle the beginning of a component.""" 

118 # try and create one of the components defined in the spec, 

119 # otherwise get a general Components for robustness. 

120 c_name = vals.upper() 

121 c_class = self.get_component_class(vals) 

122 # If component factory cannot resolve ``c_name``, the generic 

123 # ``Component`` class is used which does not have the name set. 

124 # That's opposed to the usage of ``cls``, which represents a 

125 # more concrete subclass with a name set (e.g. VCALENDAR). 

126 component = c_class() 

127 if not getattr(component, "name", ""): # undefined components 

128 component.name = c_name 

129 self._stack.append(component) 

130 

131 def handle_end_component(self, vals: str) -> None: 

132 """Handle the end of a component.""" 

133 # we are done adding properties to this component 

134 # so pop it from the stack and add it to the new top. 

135 if not self._stack: 

136 # The stack is currently empty, the input must be invalid 

137 raise ValueError("END encountered without an accompanying BEGIN!") 

138 

139 component = self._stack.pop() 

140 if not self._stack: # we are at the end 

141 self._components.append(component) 

142 else: 

143 self._stack[-1].add_component(component) 

144 if vals.upper() == "VTIMEZONE" and "TZID" in component: 

145 tzp.cache_timezone_component(component) 

146 

147 def prepare_components(self) -> None: 

148 """Prepare the parsed components. 

149 

150 This is called when all components are parsed. 

151 """ 

152 

153 def parse(self) -> list[Component]: 

154 """Parse the raw data.""" 

155 self.initialize_parsing() 

156 self.parse_content_lines() 

157 self.prepare_components() 

158 return self._components 

159 

160 def parse_content_lines(self) -> None: 

161 """Parse the content lines.""" 

162 for line in self._content_lines_iterator: 

163 if not line: 

164 continue 

165 try: 

166 name, params, vals = line.parts() 

167 except ValueError as e: 

168 self.handle_line_parse_error(e) 

169 continue 

170 

171 uname = name.upper() 

172 if uname == "BEGIN": 

173 self.handle_begin_component(vals) 

174 elif uname == "END": 

175 self.handle_end_component(vals) 

176 else: 

177 self.handle_property(uname, params, vals, line) 

178 

179 @property 

180 def component(self) -> Component | None: 

181 return self._stack[-1] if self._stack else None 

182 

183 def get_factory_for_property(self, name: str, params: Parameters) -> VPROPERTY: 

184 """Get the factory for a property.""" 

185 return self._types_factory.for_property(name, params.value) 

186 

187 def handle_property( 

188 self, name: str, params: Parameters, vals: str, line: Contentline 

189 ) -> None: 

190 """Handle a property line. 

191 

192 Add properties to the top of the current stack. 

193 

194 Parameters: 

195 name: The name of the property, uppercased. 

196 params: The parameters of the property. 

197 vals: The value of the property. 

198 line: The original content line. 

199 """ 

200 # Extract VALUE parameter if present 

201 if not self.component: 

202 # only accept X-COMMENT at the end of the .ics file 

203 # ignore these components in parsing 

204 if name == "X-COMMENT": 

205 return 

206 raise ValueError(f'Property "{name}" does not have a parent component.') 

207 # Determine TZID for datetime properties 

208 tzid = params.get("TZID") if params and name in self.datetime_names else None 

209 

210 # Handle special cases for value list preparation 

211 if name == "CATEGORIES": 

212 if self.handle_categories(params, vals, line): 

213 return 

214 # Fallback to normal processing if we can't find colon 

215 vals_list = [vals] 

216 elif name == "FREEBUSY": 

217 # Handle FREEBUSY comma-separated values 

218 vals_list = vals.split(",") 

219 # Workaround broken ICS files with empty RDATE 

220 # (not EXDATE - let it parse and fail) 

221 elif name == "RDATE" and vals == "": 

222 vals_list = [] 

223 else: 

224 factory = self.get_factory_for_property(name, params) 

225 # Value types that must not be unescaped provide their own value. 

226 get_value_from_content_line = getattr( 

227 factory, "get_value_from_content_line", None 

228 ) 

229 if get_value_from_content_line is not None: 

230 vals = get_value_from_content_line(line) 

231 vals_list = [vals] 

232 

233 # Parse all properties eagerly 

234 for val in vals_list: 

235 self.parse_and_add_property(name, params, val, tzid, line) 

236 

237 def parse_and_add_property( 

238 self, 

239 name: str, 

240 params: Parameters, 

241 val: str, 

242 tzid: str | None, 

243 line: Contentline, 

244 ): 

245 """Parse a property value and add it to the current component.""" 

246 factory = self.get_factory_for_property(name, params) 

247 try: 

248 if tzid: 

249 parsed_val = factory.from_ical(val, tzid) 

250 else: 

251 parsed_val = factory.from_ical(val) 

252 except (ValueError, TypeError) as e: 

253 self.handle_property_parse_error(e, name, params, val, line) 

254 else: 

255 vals_inst = factory(parsed_val) 

256 vals_inst.params = params 

257 self.component.add(name, vals_inst, encode=False) 

258 

259 def handle_property_parse_error( 

260 self, 

261 exception: Exception, 

262 name: str, 

263 params: Parameters, 

264 val: str, 

265 line: Contentline, 

266 ): 

267 """Handle the parse error for a property.""" 

268 if not self.component.ignore_exceptions and not name[:2].upper() == "X-": 

269 raise exception 

270 # Error-tolerant mode: create vBroken 

271 factory = self.get_factory_for_property(name, params) 

272 expected_type = getattr(factory, "__name__", "unknown") 

273 broken_prop = vBroken.from_parse_error( 

274 raw_value=val, 

275 params=params, 

276 property_name=name, 

277 expected_type=expected_type, 

278 error=exception, 

279 ) 

280 self.component.errors.append((name, str(exception))) 

281 self.component.add(name, broken_prop, encode=0) 

282 

283 def handle_categories( 

284 self, params: Parameters, vals: str, line: Contentline 

285 ) -> bool: 

286 """Handle the special case of CATEGORIES property. 

287 

288 Returns: 

289 ``True`` if handled, else ``False``. 

290 """ 

291 # Special handling for CATEGORIES - need raw value 

292 # before unescaping to properly split on unescaped commas 

293 line_str = str(line) 

294 # The value separator is the first colon outside a quoted parameter 

295 # section. ``rfind`` here would pick a colon that belongs to the value 

296 # (``TEXT`` does not escape ``:``) and truncate the categories. 

297 colon_idx = line.value_separator_index() 

298 if colon_idx > 0: 

299 raw_value = line_str[colon_idx + 1 :] 

300 # Parse categories immediately (not lazily) for both 

301 # strict and tolerant components. 

302 # CATEGORIES needs special comma handling 

303 try: 

304 category_list = split_on_unescaped_comma(raw_value) 

305 factory = self.get_factory_for_property("CATEGORIES", params) 

306 vals_inst = factory(category_list) 

307 vals_inst.params = params 

308 self.component.add("CATEGORIES", vals_inst, encode=0) 

309 except ValueError as e: 

310 self.handle_property_parse_error( 

311 e, "CATEGORIES", params, raw_value, line 

312 ) 

313 return True 

314 return False