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

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

65 statements  

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"]