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
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
1"""Components for lazy parsing of components."""
3from __future__ import annotations
5from typing import TYPE_CHECKING, Any, Literal
7from icalendar.cal.component_factory import ComponentFactory
8from icalendar.parser.ical.lazy import LazyCalendarIcalParser
10from .calendar import Calendar
12if TYPE_CHECKING:
13 from collections.abc import Callable
15 from icalendar.cal import Component
16 from icalendar.parser.ical.component import ComponentIcalParser
17 from icalendar.parser.ical.lazy import LazySubcomponent
20class ParsedSubcomponentsStrategy:
21 """All the subcomponents are parsed and available as a list."""
23 def __init__(self) -> None:
24 self._components: list[Component] = []
26 def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
27 """Get the parsed subcomponents of the calendar.
29 Returns:
30 A tuple of this strategy and the list of parsed subcomponents.
31 """
32 return self, self._components
34 def set_components(
35 self, components: list[Component]
36 ) -> ParsedSubcomponentsStrategy:
37 """Set the subcomponents of the calendar.
39 Parameters:
40 components: The parsed subcomponents to store.
42 Returns:
43 This strategy with the subcomponents stored.
44 """
45 self._components = components
46 return self
48 def add_component(self, component: Component) -> ParsedSubcomponentsStrategy:
49 """Add a component to the calendar, parsing it immediately.
51 Parameters:
52 component: The component to add.
54 Returns:
55 This strategy with the component added.
56 """
57 self._components.append(component.parse())
58 return self
60 def is_lazy(self) -> Literal[False]:
61 """Return ``False`` because subcomponents are not lazily parsed."""
62 return False
64 def walk(self, name: str) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
65 """Get the subcomponents of the calendar with the given name.
67 Parameters:
68 name: The component name to filter by, for example, ``"VEVENT"``.
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
78 def with_uid(
79 self, name: str
80 ) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
81 """Get the subcomponents of the calendar with the given UID.
83 Parameters:
84 name: The UID to search for.
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
95class LazySubcomponentsStrategy:
96 """Parse subcomponents only when accessed."""
98 initial_components_to_parse: tuple[str, ...] = ("VTIMEZONE",)
99 """Parse these subcomponents before any others."""
101 def __init__(self) -> None:
102 self._components: list[LazySubcomponent | Component] = []
103 self._initial_parsed: bool = False
105 @property
106 def as_parsed(self) -> ParsedSubcomponentsStrategy:
107 """Return a parsed strategy with all subcomponents parsed.
109 Returns:
110 A :class:`ParsedSubcomponentsStrategy` with all subcomponents.
111 """
112 return ParsedSubcomponentsStrategy().set_components(
113 [component.parse() for component in self._components]
114 )
116 def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
117 """Get the subcomponents of the calendar, parsing all of them.
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()
125 def set_components(
126 self, components: list[Component]
127 ) -> ParsedSubcomponentsStrategy:
128 """Set the subcomponents of the calendar.
130 Parameters:
131 components: The subcomponents to store.
133 Returns:
134 A :class:`ParsedSubcomponentsStrategy` holding the components.
135 """
136 return ParsedSubcomponentsStrategy().set_components(components)
138 def add_component(
139 self, component: Component | LazySubcomponent
140 ) -> LazySubcomponentsStrategy:
141 """Add a component to the calendar without parsing it.
143 Parameters:
144 component: The component to add.
146 Returns:
147 This strategy with the component added.
148 """
149 self._components.append(component)
150 return self
152 def is_lazy(self) -> bool:
153 """Return whether the subcomponents may be lazily parsed."""
154 return True
156 def parse_initial_components(self) -> None:
157 """Parse the components that are required by other components.
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()
169 def walk(
170 self, name: str | None
171 ) -> tuple[LazySubcomponentsStrategy, list[Component]]:
172 """Get the subcomponents of the calendar with the given name.
174 Parse only the minimal number of subcomponents.
176 Parameters:
177 name: The component name to filter by, or ``None`` for all.
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
190 def with_uid(self, uid: str) -> tuple[LazySubcomponentsStrategy, list[Component]]:
191 """Get the subcomponents of the calendar with the given ``uid``.
193 Parse only the minimal number of subcomponents.
195 Parameters:
196 uid: The UID to search for.
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
208class InitialSubcomponentsStrategy:
209 """Initial strategy for the calendar.
211 No subcomponents.
212 """
214 def set_components(self, components: list[Component]) -> LazySubcomponentsStrategy:
215 """Set the subcomponents, switching to lazy parsing.
217 Parameters:
218 components: The subcomponents to store. Must be empty for an
219 uninitialised calendar.
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()
233class LazyCalendar(Calendar):
234 """A calendar that parses subcomponents lazily for memory efficiency.
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.
241 Examples:
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.
247 .. code-block:: pycon
249 >>> from icalendar import LazyCalendar
250 >>> calendar = LazyCalendar.example("issue_1050_all_components")
251 >>> len(calendar.events) == 1
252 True
254 The calendar's subcomponents were not parsed because they were not accessed.
255 The calendar is still lazy.
257 >>> calendar.is_lazy()
258 True
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.
264 >>> len(calendar.subcomponents)
265 5
266 >>> calendar.is_lazy()
267 False
269 See also:
270 :meth:`ComponentIcalParser.parse <icalendar.parser.ical.component.ComponentIcalParser.parse>`
271 """
273 _subcomponents: (
274 LazySubcomponentsStrategy
275 | ParsedSubcomponentsStrategy
276 | InitialSubcomponentsStrategy
277 )
278 """The strategy pattern for subcomponents of the calendar."""
280 def __init__(self, *args: Any, **kwargs: Any) -> None:
281 """Initialize the calendar."""
282 self._subcomponents = InitialSubcomponentsStrategy()
283 super().__init__(*args, **kwargs)
285 @property
286 def subcomponents(self) -> list[Component]:
287 """Parse and return all subcomponents of this calendar.
289 Accessing this property triggers the parsing of all deferred
290 subcomponents. Once accessed, the calendar is no longer lazy.
292 You can manipulate the returned list or set it to replace all
293 subcomponents. Setting the list does not re-enable lazy parsing.
295 Returns:
296 A list of parsed subcomponents.
298 See also:
299 - :attr:`~icalendar.cal.component.Component.subcomponents`
300 - :meth:`is_lazy`
302 """
303 self._subcomponents, result = self._subcomponents.get_all_components()
304 return result
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)
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 )
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
325 def add_component(self, component: Component) -> None:
326 """Add a component to this calendar.
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.
333 Parameters:
334 component: The component to add as a subcomponent.
336 See also:
337 :meth:`Component.add_component <icalendar.cal.component.Component.add_component>`
338 """
339 self._subcomponents = self._subcomponents.add_component(component)
341 def is_lazy(self) -> bool:
342 """Whether the subcomponents are still deferred and not yet parsed.
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`.
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>`_.
352 Returns:
353 ``True`` if subcomponent parsing is deferred.
354 ``False`` if all subcomponents have been parsed.
355 """
356 return self._subcomponents.is_lazy()
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
367 def with_uid(self, uid: str) -> list[Component]:
368 """Return subcomponents matching the given UID without parsing all subcomponents.
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.
374 Parameters:
375 uid: The UID to search for.
377 Returns:
378 A list of components whose UID matches, with the calendar itself
379 first if it matches.
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
390__all__ = ["LazyCalendar"]