Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/cal/lazy.py: 38%
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, 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.parser.ical.component import ComponentIcalParser
16 from icalendar.parser.ical.lazy import LazySubcomponent
18 from .component import Component
21class ParsedSubcomponentsStrategy:
22 """All the subcomponents are parsed and available as a list."""
24 def __init__(self):
25 self._components: list[Component] = []
27 def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
28 """Get the subcomponents of the calendar."""
29 return self, self._components
31 def set_components(
32 self, components: list[Component]
33 ) -> ParsedSubcomponentsStrategy:
34 """Set the subcomponents of the calendar."""
35 self._components = components
36 return self
38 def add_component(self, component: Component) -> ParsedSubcomponentsStrategy:
39 """Add a component to the calendar."""
40 self._components.append(component.parse())
41 return self
43 def is_lazy(self) -> Literal[False]:
44 """Returns ``False`` because subcomponents are not lazily parsed."""
45 return False
47 def walk(self, name: str) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
48 """Get the subcomponents of the calendar with the given name."""
49 result = []
50 for component in self._components:
51 result += component.walk(name)
52 return self, result
54 def with_uid(
55 self, name: str
56 ) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
57 """Get the subcomponents of the calendar with the given uid."""
58 result = []
59 for component in self._components:
60 result += component.with_uid(name)
61 return self, result
64class LazySubcomponentsStrategy:
65 """Parse subcomponents only when accessed."""
67 initial_components_to_parse: tuple[str, ...] = ("VTIMEZONE",)
68 """Parse these subcomponents before any others."""
70 def __init__(self):
71 self._components: list[LazySubcomponent | Component] = []
72 self._initial_parsed: bool = False
74 @property
75 def as_parsed(self) -> ParsedSubcomponentsStrategy:
76 """Return the parsed components."""
77 return ParsedSubcomponentsStrategy().set_components(
78 [component.parse() for component in self._components]
79 )
81 def get_all_components(self) -> tuple[ParsedSubcomponentsStrategy, list[Component]]:
82 """Get the subcomponents of the calendar.
84 Parse all subcomponents.
85 """
86 self.parse_initial_components()
87 return self.as_parsed.get_all_components()
89 def set_components(
90 self, components: list[Component]
91 ) -> ParsedSubcomponentsStrategy:
92 """Set the subcomponents of the calendar."""
93 return ParsedSubcomponentsStrategy().set_components(components)
95 def add_component(
96 self, component: Component | LazySubcomponent
97 ) -> LazySubcomponentsStrategy:
98 """Add a component to the calendar."""
99 self._components.append(component)
100 return self
102 def is_lazy(self) -> bool:
103 """Return whether the subcomponents may be lazily parsed."""
104 return True
106 def parse_initial_components(self) -> None:
107 """Parse the components that are required by other components.
109 This mainly concerns the timezone components.
110 They are required by other components that have a TZID parameter.
111 """
112 if self._initial_parsed:
113 return
114 self._initial_parsed = True
115 for component in self._components:
116 if component.name in self.initial_components_to_parse:
117 component.parse()
119 def walk(
120 self, name: str | None
121 ) -> tuple[LazySubcomponentsStrategy, list[Component]]:
122 """Get the subcomponents of the calendar with the given name.
124 Parse only the minimal number of subcomponents.
125 """
126 if name is None:
127 return self.as_parsed.walk(name)
128 self.parse_initial_components()
129 result = []
130 for component in self._components:
131 result += component.walk(name)
132 return self, result
134 def with_uid(self, uid: str) -> tuple[LazySubcomponentsStrategy, list[Component]]:
135 """Get the subcomponents of the calendar with the given ``uid``.
137 Parse only the minimal number of subcomponents.
138 """
139 self.parse_initial_components()
140 result = []
141 for component in self._components:
142 result += component.with_uid(uid)
143 return self, result
146class InitialSubcomponentsStrategy:
147 """Initial strategy for the calendar.
149 No subcomponents.
150 """
152 def set_components(self, components: list[Component]) -> LazySubcomponentsStrategy:
153 if components:
154 raise ValueError(
155 "Cannot set subcomponents on an uninitialised LazyCalendar. "
156 "Parse it first or add components via add_component()."
157 )
158 return LazySubcomponentsStrategy()
161class LazyCalendar(Calendar):
162 """A calendar that can handle big files.
164 Subcomponents of this calendar are evaluated lazily,
165 meaning that they are not parsed until they are accessed.
166 This allows the calendar to handle large files without
167 consuming too much memory or time.
169 All properties of the calendar component are parsed immediately.
170 Subcomponents and their properties are parsed lazily.
172 Examples:
174 By accessing the :attr:`~icalendar.cal.calendar.Calendar.events` of the calendar,
175 only :class:`~icalendar.cal.event.Event` and
176 :class:`~icalendar.cal.timezone.Timezone` are immediately parsed.
178 .. code-block:: pycon
180 >>> from icalendar import LazyCalendar
181 >>> calendar = LazyCalendar.example("issue_1050_all_components")
182 >>> len(calendar.events) == 1
183 True
185 The calendar's subcomponents were not parsed because they were not accessed.
186 The calendar is still lazy.
188 >>> calendar.is_lazy()
189 True
191 When you access all :attr:`subcomponents` of the calendar,
192 for example by getting their count, the entire calendar is
193 parsed and becomes not lazy.
195 >>> len(calendar.subcomponents)
196 5
197 >>> calendar.is_lazy()
198 False
200 """
202 _subcomponents: (
203 LazySubcomponentsStrategy
204 | ParsedSubcomponentsStrategy
205 | InitialSubcomponentsStrategy
206 )
207 """The strategy pattern for subcomponents of the calendar."""
209 def __init__(self, *args, **kwargs):
210 """Initialize the calendar."""
211 self._subcomponents = InitialSubcomponentsStrategy()
212 super().__init__(*args, **kwargs)
214 @property
215 def subcomponents(self) -> list[Component]:
216 """The subcomponents of the calendar.
218 Parse all subcomponents of the calendar and return them as a list.
220 You can manipulate this list or set it.
221 It has the same behavior as in :class:`~icalendar.cal.calendar.Calendar`.
222 """
223 self._subcomponents, result = self._subcomponents.get_all_components()
224 return result
226 @subcomponents.setter
227 def subcomponents(self, value: list[Component]) -> None:
228 """Set the subcomponents of the calendar."""
229 self._subcomponents = self._subcomponents.set_components(value)
231 @classmethod
232 def _get_ical_parser(cls, st: str | bytes) -> ComponentIcalParser:
233 """Get the iCal parser for the given input string."""
234 return LazyCalendarIcalParser(
235 st, cls._get_component_factory(), cls.types_factory
236 )
238 @classmethod
239 def _get_component_factory(cls) -> ComponentFactory:
240 """Get the component factory for this calendar."""
241 factory = ComponentFactory()
242 factory.add_component_class(cls)
243 return factory
245 def add_component(self, component: Component) -> None:
246 """Add a component to the calendar.
248 Use this instead of appending to
249 :attr:`~icalendar.cal.lazy.LazyCalendar.subcomponents`,
250 as the latter does not parse the whole calendar.
251 """
252 self._subcomponents = self._subcomponents.add_component(component)
254 def is_lazy(self) -> bool:
255 """Whether the subcomponents will be parsed lazily.
257 .. note:: If you believe that the calendar parses more than it should,
258 please `open an issue <https://github.com/collective/icalendar/issues/new?template=bug_report.md>`_.
260 Returns:
261 ``True`` if subcomponents are deferred and not yet parsed.
262 ``False`` if all subcomponents have been parsed.
263 """
264 return self._subcomponents.is_lazy()
266 def _walk(
267 self, name: str | None, select: Callable[[Component], bool]
268 ) -> list[Component]:
269 self._subcomponents, result = self._subcomponents.walk(name)
270 result = [component for component in result if select(component)]
271 if (name is None or self.name == name) and select(self):
272 result.insert(0, self)
273 return result
275 def with_uid(self, uid: str) -> list[Component]:
276 self._subcomponents, result = self._subcomponents.with_uid(uid)
277 if self.uid == uid:
278 result.insert(0, self)
279 return result
281 with_uid.__doc__ = Calendar.with_uid.__doc__
284__all__ = ["LazyCalendar"]
286if __name__ == "__main__":
287 import timeit
289 calendar = Calendar.example("issue_1050_all_components")
290 COUNT = 10000
291 calendar.subcomponents *= COUNT
292 ics = calendar.to_ical()
294 def _benchmark(cal: type[Calendar]):
295 """Check out how fast this is."""
296 cal = cal.from_ical(ics)
297 assert len(cal.events) == COUNT
299 for cal in [Calendar, LazyCalendar]:
300 print("Benchmarking:", cal.__name__) # noqa: T201
301 print(timeit.timeit("_benchmark(cal)", globals=locals(), number=1)) # noqa: T201
303 # Benchmarking: Calendar
304 # 12.277852076000272
305 # Benchmarking: LazyCalendar
306 # 5.738950790999297