Coverage for gws-app/gws/lib/xmlx/element.py: 99%

147 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""XmlElement implementation.""" 

2 

3from typing import Iterable, Optional, cast 

4import xml.etree.ElementPath as ElementPath 

5 

6import gws 

7 

8from . import error, namespace, serializer 

9 

10 

11class XmlElement(gws.XmlElement): 

12 """XML element.""" 

13 

14 def __init__(self, tag: str, attrib: Optional[dict] = None, **extra): 

15 """Create an element. 

16 

17 Names are taken as they are, no namespace resolution is done. 

18 

19 Args: 

20 tag: Tag name, local or Clark. 

21 attrib: Attributes. 

22 **extra: Additional attributes. 

23 """ 

24 

25 self.tag = tag 

26 self.name = namespace.plain_name(tag) 

27 self.text = '' 

28 self.tail = '' 

29 self.namespaces = [] 

30 self._children = [] 

31 

32 self.attrib = {} 

33 if attrib: 

34 self.attrib.update(attrib) 

35 if extra: 

36 self.attrib.update(extra) 

37 

38 # ElementTree.Element implementations, copied from ElementTree.py 

39 

40 def __repr__(self): 

41 return f'<{self.__class__.__name__} {self.tag!r} at {id(self):#x}>' 

42 

43 def makeelement(self, tag, attrib): 

44 """Create a new element of the same class, as in ``ElementTree``. 

45 

46 The new element is not added to this element. 

47 

48 Args: 

49 tag: Tag name, local or Clark. 

50 attrib: Attributes. 

51 

52 Returns: 

53 A new element. 

54 """ 

55 

56 return self.__class__(tag, attrib) 

57 

58 def __copy__(self): 

59 """Shallow copy: the children are shared with the original.""" 

60 

61 elem = self.__class__(self.tag, self.attrib) 

62 elem.text = self.text 

63 elem.tail = self.tail 

64 elem.namespaces = list(self.namespaces) 

65 elem._children = list(self._children) 

66 return elem 

67 

68 def __len__(self): 

69 return len(self._children) 

70 

71 def __getitem__(self, index): 

72 return self._children[index] 

73 

74 def __setitem__(self, index, element): 

75 """Replace the child at the given index.""" 

76 

77 self._children[index] = element 

78 

79 def __delitem__(self, index): 

80 """Remove the child at the given index.""" 

81 

82 del self._children[index] 

83 

84 def append(self, subelement): 

85 self._children.append(subelement) 

86 

87 def extend(self, elements): 

88 for element in elements: 

89 self._children.append(element) 

90 

91 def insert(self, index, subelement): 

92 self._children.insert(index, subelement) 

93 

94 def remove(self, subelement): 

95 self._children.remove(subelement) 

96 

97 def find(self, path): 

98 return cast(Optional[gws.XmlElement], ElementPath.find(self, path)) 

99 

100 def findtext(self, path, default=''): 

101 return cast(str, ElementPath.findtext(self, path, default)) 

102 

103 def findall(self, path): 

104 return cast(list[gws.XmlElement], ElementPath.findall(self, path)) 

105 

106 def iterfind(self, path): 

107 return cast(Iterable[gws.XmlElement], ElementPath.iterfind(self, path)) 

108 

109 def clear(self): 

110 self.attrib = {} 

111 self.namespaces = [] 

112 self._children = [] 

113 self.text = self.tail = '' 

114 

115 def get(self, key, default=''): 

116 return self.attrib.get(key, default) 

117 

118 def set(self, key, value): 

119 self.attrib[key] = value 

120 

121 def keys(self): 

122 return self.attrib.keys() 

123 

124 def items(self): 

125 return self.attrib.items() 

126 

127 def iter(self, tag=None): 

128 if tag == '*': 

129 tag = None 

130 if tag is None or self.tag == tag: 

131 yield self 

132 for e in self._children: 

133 yield from e.iter(tag) 

134 

135 def itertext(self): 

136 t = self.text 

137 if t: 

138 yield t 

139 for e in self: 

140 yield from e.itertext() 

141 t = e.tail 

142 if t: 

143 yield t 

144 

145 ## extensions 

146 

147 def __bool__(self): 

148 """An element is always true, also when it has no children.""" 

149 

150 return True 

151 

152 def __iter__(self): 

153 return iter(self._children) 

154 

155 def require(self, path): 

156 el = self.find(path) 

157 if el is None: 

158 raise error.Error(f'XmlElement: required element not found: {path!r}') 

159 return el 

160 

161 def children(self): 

162 return self._children 

163 

164 def hasattr(self, key): 

165 return key in self.attrib 

166 

167 def isa(self, *names): 

168 n = self.name.lower() 

169 return any(n == s.lower() for s in names) 

170 

171 def to_dict(self): 

172 return { 

173 'tag': self.tag, 

174 'attrib': dict(self.attrib), 

175 'text': self.text, 

176 'tail': self.tail, 

177 'children': [c.to_dict() for c in self._children], 

178 } 

179 

180 def to_string(self, opts=None): 

181 ser = serializer.Serializer(self, opts=opts) 

182 return ser.to_string() 

183 

184 ## 

185 

186 def add(self, tag, attrib=None, **extra): 

187 el = self.__class__(tag, attrib or {}, **extra) 

188 self.append(el) 

189 return el 

190 

191 def declare(self, *namespaces): 

192 for ns in namespaces: 

193 if not any(n.uri == ns.uri and n.prefix == ns.prefix for n in self.namespaces): 

194 self.namespaces.append(ns) 

195 

196 def findfirst(self, *paths): 

197 if not paths: 

198 return self._children[0] if len(self._children) > 0 else None 

199 for path in paths: 

200 el = self.find(path) 

201 if el is not None: 

202 return el 

203 return None 

204 

205 def textof(self, *paths): 

206 for path in paths: 

207 el = self.find(path) 

208 if el is not None and el.text: 

209 return el.text 

210 return '' 

211 

212 def textlist(self, *paths, deep=False): 

213 ls = self._collect_tags_and_text(paths, deep) 

214 return [text for _, text in ls] 

215 

216 def textdict(self, *paths, deep=False): 

217 ls = self._collect_tags_and_text(paths, deep) 

218 return dict(ls) 

219 

220 def _collect_tags_and_text(self, paths, deep): 

221 """Collect ``(tag, stripped text)`` pairs for ``textlist`` and ``textdict``.""" 

222 

223 def walk(el): 

224 s = (el.text or '').strip() 

225 if s: 

226 ls.append((el.tag, s)) 

227 if deep: 

228 for c in el: 

229 walk(c) 

230 

231 ls = [] 

232 

233 if not paths: 

234 for el in self: 

235 walk(el) 

236 else: 

237 for path in paths: 

238 for el in self.findall(path): 

239 walk(el) 

240 

241 return ls