Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/lazy.py: 41%

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

116 statements  

1"""Components for lazy parsing of components.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any, Literal 

6 

7from icalendar.cal.component_factory import ComponentFactory 

8from icalendar.parser.ical.lazy import LazyCalendarIcalParser 

9 

10from .calendar import Calendar 

11 

12if TYPE_CHECKING: 

13 from collections.abc import Callable 

14 

15 from icalendar.cal import Component 

16 from icalendar.parser.ical.component import ComponentIcalParser 

17 from icalendar.parser.ical.lazy import LazySubcomponent 

18 

19 

20class ParsedSubcomponentsStrategy: 

21 """All the subcomponents are parsed and available as a list.""" 

22 

23 def __init__(self) -> None: 

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

25 

26 def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: 

27 """Get the parsed subcomponents of the calendar. 

28 

29 Returns: 

30 A tuple of this strategy and the list of parsed subcomponents. 

31 """ 

32 return self, self._components 

33 

34 def set_components( 

35 self, components: list[Component] 

36 ) -> ParsedSubcomponentsStrategy: 

37 """Set the subcomponents of the calendar. 

38 

39 Parameters: 

40 components: The parsed subcomponents to store. 

41 

42 Returns: 

43 This strategy with the subcomponents stored. 

44 """ 

45 self._components = components 

46 return self 

47 

48 def add_component(self, component: Component) -> ParsedSubcomponentsStrategy: 

49 """Add a component to the calendar, parsing it immediately. 

50 

51 Parameters: 

52 component: The component to add. 

53 

54 Returns: 

55 This strategy with the component added. 

56 """ 

57 self._components.append(component.parse()) 

58 return self 

59 

60 def is_lazy(self) -> Literal[False]: 

61 """Return ``False`` because subcomponents are not lazily parsed.""" 

62 return False 

63 

64 def walk(self, name: str) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: 

65 """Get the subcomponents of the calendar with the given name. 

66 

67 Parameters: 

68 name: The component name to filter by, for example, ``"VEVENT"``. 

69 

70 Returns: 

71 A tuple of this strategy and the matching subcomponents. 

72 """ 

73 result = [] 

74 for component in self._components: 

75 result += component.walk(name) 

76 return self, result 

77 

78 def with_uid( 

79 self, name: str 

80 ) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: 

81 """Get the subcomponents of the calendar with the given UID. 

82 

83 Parameters: 

84 name: The UID to search for. 

85 

86 Returns: 

87 A tuple of this strategy and the matching subcomponents. 

88 """ 

89 result = [] 

90 for component in self._components: 

91 result += component.with_uid(name) 

92 return self, result 

93 

94 

95class LazySubcomponentsStrategy: 

96 """Parse subcomponents only when accessed.""" 

97 

98 initial_components_to_parse: tuple[str, ...] = ("VTIMEZONE",) 

99 """Parse these subcomponents before any others.""" 

100 

101 def __init__(self) -> None: 

102 self._components: list[LazySubcomponent | Component] = [] 

103 self._initial_parsed: bool = False 

104 

105 @property 

106 def as_parsed(self) -> ParsedSubcomponentsStrategy: 

107 """Return a parsed strategy with all subcomponents parsed. 

108 

109 Returns: 

110 A :class:`ParsedSubcomponentsStrategy` with all subcomponents. 

111 """ 

112 return ParsedSubcomponentsStrategy().set_components( 

113 [component.parse() for component in self._components] 

114 ) 

115 

116 def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]: 

117 """Get the subcomponents of the calendar, parsing all of them. 

118 

119 Returns: 

120 A tuple of a parsed strategy and the list of subcomponents. 

121 """ 

122 self.parse_initial_components() 

123 return self.as_parsed.get_all_components() 

124 

125 def set_components( 

126 self, components: list[Component] 

127 ) -> ParsedSubcomponentsStrategy: 

128 """Set the subcomponents of the calendar. 

129 

130 Parameters: 

131 components: The subcomponents to store. 

132 

133 Returns: 

134 A :class:`ParsedSubcomponentsStrategy` holding the components. 

135 """ 

136 return ParsedSubcomponentsStrategy().set_components(components) 

137 

138 def add_component( 

139 self, component: Component | LazySubcomponent 

140 ) -> LazySubcomponentsStrategy: 

141 """Add a component to the calendar without parsing it. 

142 

143 Parameters: 

144 component: The component to add. 

145 

146 Returns: 

147 This strategy with the component added. 

148 """ 

149 self._components.append(component) 

150 return self 

151 

152 def is_lazy(self) -> bool: 

153 """Return whether the subcomponents may be lazily parsed.""" 

154 return True 

155 

156 def parse_initial_components(self) -> None: 

157 """Parse the components that are required by other components. 

158 

159 This mainly concerns the timezone components. 

160 They are required by other components that have a TZID parameter. 

161 """ 

162 if self._initial_parsed: 

163 return 

164 self._initial_parsed = True 

165 for component in self._components: 

166 if component.name in self.initial_components_to_parse: 

167 component.parse() 

168 

169 def walk( 

170 self, name: str | None 

171 ) -> tuple[LazySubcomponentsStrategy, list[Component]]: 

172 """Get the subcomponents of the calendar with the given name. 

173 

174 Parse only the minimal number of subcomponents. 

175 

176 Parameters: 

177 name: The component name to filter by, or ``None`` for all. 

178 

179 Returns: 

180 A tuple of this strategy and the matching subcomponents. 

181 """ 

182 if name is None: 

183 return self.as_parsed.walk(name) 

184 self.parse_initial_components() 

185 result = [] 

186 for component in self._components: 

187 result += component.walk(name) 

188 return self, result 

189 

190 def with_uid(self, uid: str) -> tuple[LazySubcomponentsStrategy, list[Component]]: 

191 """Get the subcomponents of the calendar with the given ``uid``. 

192 

193 Parse only the minimal number of subcomponents. 

194 

195 Parameters: 

196 uid: The UID to search for. 

197 

198 Returns: 

199 A tuple of this strategy and the matching subcomponents. 

200 """ 

201 self.parse_initial_components() 

202 result = [] 

203 for component in self._components: 

204 result += component.with_uid(uid) 

205 return self, result 

206 

207 

208class InitialSubcomponentsStrategy: 

209 """Initial strategy for the calendar. 

210 

211 No subcomponents. 

212 """ 

213 

214 def set_components(self, components: list[Component]) -> LazySubcomponentsStrategy: 

215 """Set the subcomponents, switching to lazy parsing. 

216 

217 Parameters: 

218 components: The subcomponents to store. Must be empty for an 

219 uninitialised calendar. 

220 

221 Raises: 

222 ValueError: If ``components`` is not empty. Parse the calendar 

223 first or use :meth:`LazyCalendar.add_component` instead. 

224 """ 

225 if components: 

226 raise ValueError( 

227 "Cannot set subcomponents on an uninitialised LazyCalendar. " 

228 "Parse it first or add components via add_component()." 

229 ) 

230 return LazySubcomponentsStrategy() 

231 

232 

233class LazyCalendar(Calendar): 

234 """A calendar that parses subcomponents lazily for memory efficiency. 

235 

236 Subcomponents of this calendar are parsed only when accessed, 

237 allowing the calendar to handle large files without consuming 

238 too much memory or time. All calendar-level properties are parsed 

239 immediately; subcomponents and their properties are deferred. 

240 

241 Examples: 

242 

243 By accessing the :attr:`~icalendar.cal.calendar.Calendar.events` of the calendar, 

244 only :class:`~icalendar.cal.event.Event` and 

245 :class:`~icalendar.cal.timezone.Timezone` are immediately parsed. 

246 

247 .. code-block:: pycon 

248 

249 >>> from icalendar import LazyCalendar 

250 >>> calendar = LazyCalendar.example("issue_1050_all_components") 

251 >>> len(calendar.events) == 1 

252 True 

253 

254 The calendar's subcomponents were not parsed because they were not accessed. 

255 The calendar is still lazy. 

256 

257 >>> calendar.is_lazy() 

258 True 

259 

260 When you access all :attr:`~icalendar.cal.component.Component.subcomponents` of the calendar, 

261 for example by getting their count, the entire calendar is 

262 parsed and becomes not lazy. 

263 

264 >>> len(calendar.subcomponents) 

265 5 

266 >>> calendar.is_lazy() 

267 False 

268 

269 See also: 

270 :meth:`ComponentIcalParser.parse <icalendar.parser.ical.component.ComponentIcalParser.parse>` 

271 """ 

272 

273 _subcomponents: ( 

274 LazySubcomponentsStrategy 

275 | ParsedSubcomponentsStrategy 

276 | InitialSubcomponentsStrategy 

277 ) 

278 """The strategy pattern for subcomponents of the calendar.""" 

279 

280 def __init__(self, *args: Any, **kwargs: Any) -> None: 

281 """Initialize the calendar.""" 

282 self._subcomponents = InitialSubcomponentsStrategy() 

283 super().__init__(*args, **kwargs) 

284 

285 @property 

286 def subcomponents(self) -> list[Component]: 

287 """Parse and return all subcomponents of this calendar. 

288 

289 Accessing this property triggers the parsing of all deferred 

290 subcomponents. Once accessed, the calendar is no longer lazy. 

291 

292 You can manipulate the returned list or set it to replace all 

293 subcomponents. Setting the list does not re-enable lazy parsing. 

294 

295 Returns: 

296 A list of parsed subcomponents. 

297 

298 See also: 

299 - :attr:`~icalendar.cal.component.Component.subcomponents` 

300 - :meth:`is_lazy` 

301 

302 """ 

303 self._subcomponents, result = self._subcomponents.get_all_components() 

304 return result 

305 

306 @subcomponents.setter 

307 def subcomponents(self, value: list[Component]) -> None: 

308 """Set the subcomponents of the calendar.""" 

309 self._subcomponents = self._subcomponents.set_components(value) 

310 

311 @classmethod 

312 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser: 

313 """Get the iCal parser for the given input string.""" 

314 return LazyCalendarIcalParser( 

315 st, cls._get_component_factory(), cls.types_factory 

316 ) 

317 

318 @classmethod 

319 def _get_component_factory(cls) -> ComponentFactory: 

320 """Get the component factory for this calendar.""" 

321 factory = ComponentFactory() 

322 factory.add_component_class(cls) 

323 return factory 

324 

325 def add_component(self, component: Component) -> None: 

326 """Add a component to this calendar. 

327 

328 This adds a subcomponent without parsing the entire calendar. 

329 Use this, instead of appending to 

330 :attr:`~icalendar.cal.lazy.LazyCalendar.subcomponents` 

331 which forces all subcomponents to be parsed first. 

332 

333 Parameters: 

334 component: The component to add as a subcomponent. 

335 

336 See also: 

337 :meth:`Component.add_component <icalendar.cal.component.Component.add_component>` 

338 """ 

339 self._subcomponents = self._subcomponents.add_component(component) 

340 

341 def is_lazy(self) -> bool: 

342 """Whether the subcomponents are still deferred and not yet parsed. 

343 

344 Returns ``True`` if subcomponents have not been accessed yet. 

345 Returns ``False`` once all subcomponents have been parsed, 

346 for example, by accessing :attr:`subcomponents`. 

347 

348 .. note:: If you believe the calendar parses more subcomponents than 

349 it should, please `open an issue 

350 <https://github.com/collective/icalendar/issues/new?template=bug_report.md>`_. 

351 

352 Returns: 

353 ``True`` if subcomponent parsing is deferred. 

354 ``False`` if all subcomponents have been parsed. 

355 """ 

356 return self._subcomponents.is_lazy() 

357 

358 def _walk( 

359 self, name: str | None, select: Callable[[Component], bool] 

360 ) -> list[Component]: 

361 self._subcomponents, result = self._subcomponents.walk(name) 

362 result = [component for component in result if select(component)] 

363 if (name is None or self.name == name) and select(self): 

364 result.insert(0, self) 

365 return result 

366 

367 def with_uid(self, uid: str) -> list[Component]: 

368 """Return subcomponents matching the given UID without parsing all subcomponents. 

369 

370 This searches lazily, parsing only the minimal subcomponents 

371 needed to find matches. If this calendar's own UID matches, 

372 it is included as the first element. 

373 

374 Parameters: 

375 uid: The UID to search for. 

376 

377 Returns: 

378 A list of components whose UID matches, with the calendar itself 

379 first if it matches. 

380 

381 See also: 

382 :meth:`Component.with_uid <icalendar.cal.component.Component.with_uid>` 

383 """ 

384 self._subcomponents, result = self._subcomponents.with_uid(uid) 

385 if self.uid == uid: 

386 result.insert(0, self) 

387 return result 

388 

389 

390__all__ = ["LazyCalendar"]