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

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

60 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 for line in self._content_lines_iterator: 

58 content_lines.append(line) 

59 if ( 

60 line[:4].upper() == "END:" 

61 and line[4:].strip().upper() == component_name 

62 ): 

63 break 

64 if self.component is None: 

65 raise ValueError( 

66 f"BEGIN:{component_name} encountered outside of a parent component." 

67 ) 

68 self.component.add_component( 

69 LazySubcomponent( 

70 component_name, 

71 self.get_subcomponent_parser(content_lines), 

72 ) 

73 ) 

74 

75 def get_subcomponent_parser( 

76 self, content_lines: list[Contentline] 

77 ) -> ComponentIcalParser: 

78 """Get the parser for a subcomponent. 

79 

80 Parameters: 

81 content_lines: The content lines of the subcomponent. 

82 """ 

83 return ComponentIcalParser( 

84 content_lines, self._component_factory, self._types_factory 

85 ) 

86 

87 def prepare_components(self): 

88 """Prepare the lazily parsed components.""" 

89 

90 

91class LazySubcomponent: 

92 """A subcomponent that is evaluated lazily. 

93 

94 This class holds the raw data of the subcomponent ready for parsing. 

95 """ 

96 

97 def __init__(self, name: str, parser: ComponentIcalParser): 

98 """Initialize the lazy subcomponent with the raw data.""" 

99 self._name = name 

100 self._parser = parser 

101 self._component: Component | None = None 

102 

103 @property 

104 def name(self) -> str: 

105 """The name of the subcomponent. 

106 

107 The name is uppercased, per :rfc:`5545#section-2.1`. 

108 """ 

109 return self._name 

110 

111 def is_parsed(self) -> bool: 

112 """Return whether the subcomponent is already parsed.""" 

113 return self._component is not None 

114 

115 def parse(self) -> Component: 

116 """Parse the raw data and return the component.""" 

117 if self._component is None: 

118 components = self._parser.parse() 

119 if len(components) != 1: 

120 raise ValueError( 

121 f"Expected exactly one component in the subcomponent, " 

122 f"but got {len(components)}." 

123 ) 

124 self._component = components[0] 

125 self._parser = None # free memory 

126 return self._component 

127 

128 def is_lazy(self) -> bool: 

129 """Return whether the subcomponents were accessed and parsed lazily. 

130 

131 Call :meth:`parse` to get the fully parsed component. 

132 """ 

133 return True 

134 

135 def __repr__(self) -> str: 

136 return f"LazySubcomponent(name={self._name}, parsed={self.is_parsed()})" 

137 

138 def walk(self, name: str) -> list[Component]: 

139 """Walk through this component. 

140 

141 This only parses the component if necessary. 

142 

143 Parameters: 

144 name: The name to match for all components in the calendar, 

145 then walk through and parse the resulting matches. 

146 """ 

147 if not isinstance(name, str): 

148 raise TypeError("name must be a string.") 

149 if name == self.name or ( 

150 self._parser and self._parser.contains_component(name) 

151 ): 

152 return self.parse().walk(name) 

153 return [] 

154 

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

156 """Return the components containing the given ``uid``. 

157 

158 This only parses the component if necessary. 

159 

160 Parameters: 

161 uid: The UID of the components. 

162 """ 

163 if self._parser and not self._parser.contains_uid(uid): 

164 return [] 

165 return self.parse().with_uid(uid) 

166 

167 

168__all__ = ["LazyCalendarIcalParser", "LazySubcomponent"]