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

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

77 statements  

1from __future__ import annotations 

2 

3import re 

4from collections import defaultdict 

5from xml.etree.ElementTree import Element 

6 

7from icalendar.error import XCalParsingError 

8from icalendar.parser.xcal.string import string_to_xsd_float 

9 

10REGEX_WHITESPACE = re.compile(r"\s+", re.MULTILINE) 

11 

12 

13class ElementAdapter: 

14 """An adapter class with convenience methods for XML elements. 

15 

16 This is a read-only access adapter. 

17 All operations on ElementAdapter are reproducible and 

18 do not change the underlying element. 

19 """ 

20 

21 @classmethod 

22 def with_element(cls, element: Element | ElementAdapter) -> ElementAdapter: 

23 """Create a new element adapter or return the present one.""" 

24 if isinstance(element, ElementAdapter): 

25 return element 

26 return cls(element) 

27 

28 def __init__(self, element: Element) -> None: 

29 self._element = element 

30 self._tag = self._element.tag.split("}")[-1].lower() 

31 

32 @property 

33 def element(self) -> Element: 

34 """The underlying element.""" 

35 return self._element 

36 

37 @property 

38 def tag(self) -> str: 

39 """A sanitized element tag.""" 

40 return self._tag 

41 

42 @property 

43 def is_extension(self) -> bool: 

44 """Whether the element is in a namespace other than the iCalendar namespace. 

45 

46 Elements without a namespace belong to iCalendar. 

47 Extension elements directly in ``properties`` are XML properties. 

48 

49 See also: 

50 :rfc:`6321#section-4.1`. 

51 

52 Returns: 

53 ``True`` for an element outside the iCalendar namespace, 

54 or ``False`` for an element either in the iCalendar namespace 

55 or without a namespace. 

56 """ 

57 tag = self._element.tag 

58 return tag.startswith("{") and not tag.startswith( 

59 "{urn:ietf:params:xml:ns:icalendar-2.0}" 

60 ) 

61 

62 @property 

63 def children(self) -> list[ChildElementAdapter]: 

64 """A list of child elements.""" 

65 tags = defaultdict(int) 

66 children = [] 

67 for child_element in self._element: 

68 tag = child_element.tag 

69 tags[tag] += 1 

70 children.append(ChildElementAdapter(child_element, self, tags[tag])) 

71 return children 

72 

73 def get_child_with_tag(self, tag: str) -> ChildElementAdapter | None: 

74 """Get the first child element with the given tag.""" 

75 tag = tag.lower() 

76 for child in self.children: 

77 if child.tag == tag: 

78 return child 

79 return None 

80 

81 def parse_tag(self, tag: str): 

82 """Get the first child element with the given tag. 

83 

84 Returns: 

85 The first child element with the given tag. 

86 

87 Raises: 

88 XCalParsingError: If the tag is not found. 

89 """ 

90 child = self.get_child_with_tag(tag) 

91 if child is None: 

92 raise XCalParsingError(f"Tag {tag} not found", None, self) 

93 return child 

94 

95 def get_xsd_string(self) -> str: 

96 """Return the element's text as ``xsd:string``. 

97 

98 This is the only datatype that leaves all the whitespace. 

99 

100 .. seealso:: 

101 `xmlschemata.org <https://books.xmlschemata.org/relaxng/ch19-77303.html>`__ 

102 """ 

103 return self._element.text or "" 

104 

105 def get_xsd_token(self) -> str: 

106 """Return the element's text as ``xsd:token``. 

107 

108 ``xsd:token`` is the most appropriate datatype to use for strings 

109 that don't care about whitespace. 

110 

111 .. seealso:: 

112 `xmlschemata.org <https://books.xmlschemata.org/relaxng/ch19-77319.html>`__ 

113 """ 

114 return REGEX_WHITESPACE.sub(" ", self.get_xsd_string()).strip() 

115 

116 def get_text_without_whitespace(self) -> str: 

117 """Return the element's text with all whitespace removed.""" 

118 return REGEX_WHITESPACE.sub("", self.get_xsd_string()) 

119 

120 def get_xpath(self) -> str: 

121 """Return the path in the XML file where this element occurs.""" 

122 return f"/{self.tag}" 

123 

124 def get_xsd_float(self) -> float: 

125 """Return the element's text parsed as ``xsd:float``. 

126 

127 Returns: 

128 The parsed float value. 

129 

130 Raises: 

131 XCalParsingError: If the value is not a float. 

132 """ 

133 try: 

134 return string_to_xsd_float(self.get_xsd_token()) 

135 except (TypeError, ValueError) as e: 

136 raise XCalParsingError( 

137 "Expected xsd:float", 

138 self.get_xsd_token(), 

139 self, 

140 ) from e 

141 

142 def __repr__(self) -> str: 

143 """Return the text representation of this element.""" 

144 return f"Element@{self.get_xpath()}" 

145 

146 def make_parent(self, tag: str) -> ElementAdapter: 

147 """Return an element adapter for a parent.""" 

148 parent = Element(tag) 

149 parent.append(self._element) 

150 return ElementAdapter(parent) 

151 

152 def get_inner_text(self) -> str: 

153 """Return the inner text. 

154 

155 This is useful when there is a broken property. 

156 """ 

157 return self.get_xsd_string() 

158 

159 

160class ChildElementAdapter(ElementAdapter): 

161 """An adapter class with convenience methods for child elements. 

162 

163 Parameters: 

164 child: The child element. 

165 index: The index for building the XPath in the parent. 

166 parent: The parent element adapter. 

167 """ 

168 

169 def __init__(self, child: Element, parent: ElementAdapter, index: int = 1) -> None: 

170 super().__init__(child) 

171 self._parent = parent 

172 self._index = index 

173 

174 @property 

175 def parent(self) -> ElementAdapter: 

176 """The parent element.""" 

177 return self._parent 

178 

179 def get_xpath(self) -> str: 

180 """Return the path in the XML file where this element occurs.""" 

181 return f"{self.parent.get_xpath()}/{self.tag}[{self._index}]"