1"""Special parsing for calendar components."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING, ClassVar
6
7from icalendar.parser.content_line import Contentline
8
9from .component import ComponentIcalParser
10
11if TYPE_CHECKING:
12 from icalendar.cal.component import Component
13
14
15class LazyCalendarIcalParser(ComponentIcalParser):
16 """A parser for calendar components.
17
18 Instead of parsing the components, LazyComponents are created.
19 Parsing can happen on demand.
20
21 A calendar may grow over time, with its subcomponents greatly increasing
22 in number, and requiring more memory and time to fully parse. This
23 optimization lazily parses calendar files without consuming more memory
24 than necessary, reducing the initial time it takes to access meta data.
25 """
26
27 parse_instantly: ClassVar[tuple[str, ...]] = ("VCALENDAR",)
28 """Parse these components immediately, instead of lazily.
29
30 All other components are parsed lazily.
31 """
32
33 def handle_begin_component(self, vals):
34 """Begin a new component.
35
36 This may be the first component.
37 """
38 c_name = vals.upper()
39 if (
40 c_name in self.parse_instantly
41 or not self.component
42 or not self.component.is_lazy()
43 ):
44 # these components are parsed immediately
45 super().handle_begin_component(vals)
46 else:
47 self.handle_lazy_begin_component(c_name)
48
49 def handle_lazy_begin_component(self, component_name: str) -> None:
50 """Begin a new component, but do not parse it yet.
51
52 Parameters:
53 component_name:
54 The upper case name of the component, for example, ``"VEVENT"``.
55 """
56 content_lines = [Contentline(f"BEGIN:{component_name}")]
57 # components with the same name can be nested
58 depth = 1
59 for line in self._content_lines_iterator:
60 content_lines.append(line)
61 if (
62 line[:6].upper() == "BEGIN:"
63 and line[6:].strip().upper() == component_name
64 ):
65 depth += 1
66 elif (
67 line[:4].upper() == "END:"
68 and line[4:].strip().upper() == component_name.strip()
69 ):
70 depth -= 1
71 if depth == 0:
72 break
73 if self.component is None:
74 raise ValueError(
75 f"BEGIN:{component_name} encountered outside of a parent component."
76 )
77 self.component.add_component(
78 LazySubcomponent(
79 component_name,
80 self.get_subcomponent_parser(content_lines),
81 )
82 )
83
84 def get_subcomponent_parser(
85 self, content_lines: list[Contentline]
86 ) -> ComponentIcalParser:
87 """Get the parser for a subcomponent.
88
89 Parameters:
90 content_lines: The content lines of the subcomponent.
91 """
92 return ComponentIcalParser(
93 content_lines, self._component_factory, self._types_factory
94 )
95
96 def prepare_components(self):
97 """Prepare the lazily parsed components."""
98
99
100class LazySubcomponent:
101 """A subcomponent that is evaluated lazily.
102
103 This class holds the raw data of the subcomponent ready for parsing.
104 """
105
106 def __init__(self, name: str, parser: ComponentIcalParser) -> None:
107 """Initialize the lazy subcomponent with the raw data."""
108 self._name = name
109 self._parser = parser
110 self._component: Component | None = None
111
112 @property
113 def name(self) -> str:
114 """The name of the subcomponent.
115
116 The name is uppercased, per :rfc:`5545#section-2.1`.
117 """
118 return self._name
119
120 def is_parsed(self) -> bool:
121 """Return whether the subcomponent is already parsed."""
122 return self._component is not None
123
124 def parse(self) -> Component:
125 """Parse the raw data and return the component."""
126 if self._component is None:
127 components = self._parser.parse()
128 if len(components) != 1:
129 raise ValueError(
130 f"Expected exactly one component in the subcomponent, "
131 f"but got {len(components)}."
132 )
133 self._component = components[0]
134 self._parser = None # free memory
135 return self._component
136
137 def is_lazy(self) -> bool:
138 """Return whether the subcomponents were accessed and parsed lazily.
139
140 Call :meth:`parse` to get the fully parsed component.
141 """
142 return True
143
144 def __repr__(self) -> str:
145 return f"LazySubcomponent(name={self._name}, parsed={self.is_parsed()})"
146
147 def walk(self, name: str) -> list[Component]:
148 """Walk through this component.
149
150 This only parses the component if necessary.
151
152 Parameters:
153 name: The name to match for all components in the calendar,
154 then walk through and parse the resulting matches.
155 """
156 if not isinstance(name, str):
157 raise TypeError("name must be a string.")
158 if name == self.name or (
159 self._parser and self._parser.contains_component(name)
160 ):
161 return self.parse().walk(name)
162 return []
163
164 def with_uid(self, uid: str) -> list[Component]:
165 """Return the components containing the given ``uid``.
166
167 This only parses the component if necessary.
168
169 Parameters:
170 uid: The UID of the components.
171 """
172 if self._parser and not self._parser.contains_uid(uid):
173 return []
174 return self.parse().with_uid(uid)
175
176
177__all__ = ["LazyCalendarIcalParser", "LazySubcomponent"]