Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/parser/xcal/base.py: 33%

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

84 statements  

1"""xCal parser class to fully parse the content.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Sequence 

6from typing import TYPE_CHECKING 

7 

8from icalendar.error import XCalParsingError 

9from icalendar.parser.parameter import Parameters 

10from icalendar.parser.xcal.adapter import ChildElementAdapter, ElementAdapter 

11 

12if TYPE_CHECKING: 

13 from collections.abc import Sequence 

14 from xml.etree.ElementTree import Element 

15 

16 

17class InvalidParserState(Exception): 

18 """The parser is in an invalid state. 

19 

20 This exception should never be raised in production code. 

21 """ 

22 

23 

24class XCalParser: 

25 """Create a new XCalParser that takes care of the parsing state. 

26 

27 An element is passed to the parser. 

28 The element and its child elements can be inspected and marked as parsed. 

29 """ 

30 

31 def __init__(self, element: Element | ElementAdapter) -> None: 

32 self._element = ElementAdapter.with_element(element) 

33 self._children = self._element.children 

34 self._consumed = 0 

35 self._element_is_parsed = False 

36 

37 @property 

38 def element(self) -> ElementAdapter: 

39 """The element to parse.""" 

40 return self._element 

41 

42 def is_finished(self) -> bool: 

43 """Whether there are more elements left to parse.""" 

44 return len(self._children) == 0 

45 

46 @property 

47 def child(self) -> ChildElementAdapter: 

48 """Get the next child element. 

49 

50 Raises: 

51 InvalidParserState: If there are no more children. 

52 """ 

53 if self.is_finished(): 

54 raise InvalidParserState("No more children available.") 

55 return self._children[0] 

56 

57 def done(self): 

58 """Done with this child element. 

59 

60 Raises: 

61 InvalidParserState: If there are no more children. 

62 """ 

63 if self.is_finished(): 

64 raise InvalidParserState("No more children available.") 

65 self._children = self._children[1:] 

66 self._consumed += 1 

67 

68 @property 

69 def tag(self) -> str: 

70 """The tag of the element on which to work.""" 

71 return self._element.tag 

72 

73 @staticmethod 

74 def _sanitize_tags(tags: str | Sequence[str]) -> set[str]: 

75 """Return a set of tags to test.""" 

76 if isinstance(tags, str): 

77 return {tags.lower()} 

78 return {t.lower() for t in tags} 

79 

80 def parse_optional_tag(self, tag: str | Sequence[str]) -> ElementAdapter | None: 

81 """Find a child tag and return it if present, else ``None``. 

82 

83 This child is then considered parsed. 

84 """ 

85 tags = self._sanitize_tags(tag) 

86 return self._parse_tag(tags) 

87 

88 def parse_tag(self, tag: str | Sequence[str] | None = None) -> ElementAdapter: 

89 """Find a child tag and return it. 

90 

91 This child is then considered parsed. 

92 

93 Returns: 

94 The child element with the tag, or if no tag is given, then the first child. 

95 

96 Raises: 

97 XCalParsingError: If the tag is not found. 

98 """ 

99 if tag is None: 

100 child = self.child 

101 self.done() 

102 return child 

103 tags = self._sanitize_tags(tag) 

104 element = self._parse_tag(tags) 

105 

106 if element is None: 

107 raise XCalParsingError( 

108 f"Tag {' or '.join(tags)} not found", None, self._element 

109 ) 

110 return element 

111 

112 def _parse_tag(self, tags: set[str]) -> ElementAdapter | None: 

113 """Find a child tag and return it. 

114 

115 This child is then considered parsed. 

116 """ 

117 for i, child in enumerate(self._children): 

118 if child.tag in tags: 

119 del self._children[i] 

120 self._consumed += 1 

121 return child 

122 if not self._element_is_parsed and self._element.tag in tags: 

123 self._element_is_parsed = True 

124 self._consumed += 1 

125 return self._element 

126 return None 

127 

128 def parse_tags(self, tag: str | Sequence[str]) -> list[ElementAdapter]: 

129 """Find all child tags and return it. 

130 

131 These children is then considered parsed. 

132 """ 

133 tags = self._sanitize_tags(tag) 

134 children = [] 

135 while True: 

136 child = self._parse_tag(tags) 

137 if child is None: 

138 break 

139 children.append(child) 

140 return children 

141 

142 def ensure_tag_is_present(self, tag: str | Sequence[str]) -> str: 

143 """Check for the presence of a tag that has not yet been parsed. 

144 

145 This serves as a check before :meth:`parse_tag`. 

146 The returned tag is not marked as parsed. 

147 

148 Returns: 

149 The tag that is present. 

150 

151 Raises: 

152 XCalParsingError: If the tag is not found. 

153 """ 

154 tags = self._sanitize_tags(tag) 

155 if not self._element_is_parsed and self._element.tag in tags: 

156 return self._element.tag 

157 for child in self._children: 

158 if child.tag in tags: 

159 return child.tag 

160 raise XCalParsingError( 

161 f"Tag {' or '.join(tags)} not found", None, self._element 

162 ) 

163 

164 def parse_parameters(self) -> Parameters: 

165 """Return empty parameters. 

166 

167 This can be changed in subclasses. 

168 """ 

169 return Parameters() 

170 

171 

172__all__ = ["XCalParser"]