Coverage for /usr/lib/python3/dist-packages/fontTools/misc/etree.py: 2%

263 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-14 15:55 +0200

1"""Shim module exporting the same ElementTree API for lxml and 

2xml.etree backends. 

3 

4When lxml is installed, it is automatically preferred over the built-in 

5xml.etree module. 

6On Python 2.7, the cElementTree module is preferred over the pure-python 

7ElementTree module. 

8 

9Besides exporting a unified interface, this also defines extra functions 

10or subclasses built-in ElementTree classes to add features that are 

11only availble in lxml, like OrderedDict for attributes, pretty_print and 

12iterwalk. 

13""" 

14from fontTools.misc.textTools import tostr 

15 

16 

17XML_DECLARATION = """<?xml version='1.0' encoding='%s'?>""" 

18 

19__all__ = [ 

20 # public symbols 

21 "Comment", 

22 "dump", 

23 "Element", 

24 "ElementTree", 

25 "fromstring", 

26 "fromstringlist", 

27 "iselement", 

28 "iterparse", 

29 "parse", 

30 "ParseError", 

31 "PI", 

32 "ProcessingInstruction", 

33 "QName", 

34 "SubElement", 

35 "tostring", 

36 "tostringlist", 

37 "TreeBuilder", 

38 "XML", 

39 "XMLParser", 

40 "register_namespace", 

41] 

42 

43try: 

44 from lxml.etree import * 

45 

46 _have_lxml = True 

47except ImportError: 

48 try: 

49 from xml.etree.cElementTree import * 

50 

51 # the cElementTree version of XML function doesn't support 

52 # the optional 'parser' keyword argument 

53 from xml.etree.ElementTree import XML 

54 except ImportError: # pragma: no cover 

55 from xml.etree.ElementTree import * 

56 _have_lxml = False 

57 

58 import sys 

59 

60 # dict is always ordered in python >= 3.6 and on pypy 

61 PY36 = sys.version_info >= (3, 6) 

62 try: 

63 import __pypy__ 

64 except ImportError: 

65 __pypy__ = None 

66 _dict_is_ordered = bool(PY36 or __pypy__) 

67 del PY36, __pypy__ 

68 

69 if _dict_is_ordered: 

70 _Attrib = dict 

71 else: 

72 from collections import OrderedDict as _Attrib 

73 

74 if isinstance(Element, type): 

75 _Element = Element 

76 else: 

77 # in py27, cElementTree.Element cannot be subclassed, so 

78 # we need to import the pure-python class 

79 from xml.etree.ElementTree import Element as _Element 

80 

81 class Element(_Element): 

82 """Element subclass that keeps the order of attributes.""" 

83 

84 def __init__(self, tag, attrib=_Attrib(), **extra): 

85 super(Element, self).__init__(tag) 

86 self.attrib = _Attrib() 

87 if attrib: 

88 self.attrib.update(attrib) 

89 if extra: 

90 self.attrib.update(extra) 

91 

92 def SubElement(parent, tag, attrib=_Attrib(), **extra): 

93 """Must override SubElement as well otherwise _elementtree.SubElement 

94 fails if 'parent' is a subclass of Element object. 

95 """ 

96 element = parent.__class__(tag, attrib, **extra) 

97 parent.append(element) 

98 return element 

99 

100 def _iterwalk(element, events, tag): 

101 include = tag is None or element.tag == tag 

102 if include and "start" in events: 

103 yield ("start", element) 

104 for e in element: 

105 for item in _iterwalk(e, events, tag): 

106 yield item 

107 if include: 

108 yield ("end", element) 

109 

110 def iterwalk(element_or_tree, events=("end",), tag=None): 

111 """A tree walker that generates events from an existing tree as 

112 if it was parsing XML data with iterparse(). 

113 Drop-in replacement for lxml.etree.iterwalk. 

114 """ 

115 if iselement(element_or_tree): 

116 element = element_or_tree 

117 else: 

118 element = element_or_tree.getroot() 

119 if tag == "*": 

120 tag = None 

121 for item in _iterwalk(element, events, tag): 

122 yield item 

123 

124 _ElementTree = ElementTree 

125 

126 class ElementTree(_ElementTree): 

127 """ElementTree subclass that adds 'pretty_print' and 'doctype' 

128 arguments to the 'write' method. 

129 Currently these are only supported for the default XML serialization 

130 'method', and not also for "html" or "text", for these are delegated 

131 to the base class. 

132 """ 

133 

134 def write( 

135 self, 

136 file_or_filename, 

137 encoding=None, 

138 xml_declaration=False, 

139 method=None, 

140 doctype=None, 

141 pretty_print=False, 

142 ): 

143 if method and method != "xml": 

144 # delegate to super-class 

145 super(ElementTree, self).write( 

146 file_or_filename, 

147 encoding=encoding, 

148 xml_declaration=xml_declaration, 

149 method=method, 

150 ) 

151 return 

152 

153 if encoding is not None and encoding.lower() == "unicode": 

154 if xml_declaration: 

155 raise ValueError( 

156 "Serialisation to unicode must not request an XML declaration" 

157 ) 

158 write_declaration = False 

159 encoding = "unicode" 

160 elif xml_declaration is None: 

161 # by default, write an XML declaration only for non-standard encodings 

162 write_declaration = encoding is not None and encoding.upper() not in ( 

163 "ASCII", 

164 "UTF-8", 

165 "UTF8", 

166 "US-ASCII", 

167 ) 

168 else: 

169 write_declaration = xml_declaration 

170 

171 if encoding is None: 

172 encoding = "ASCII" 

173 

174 if pretty_print: 

175 # NOTE this will modify the tree in-place 

176 _indent(self._root) 

177 

178 with _get_writer(file_or_filename, encoding) as write: 

179 if write_declaration: 

180 write(XML_DECLARATION % encoding.upper()) 

181 if pretty_print: 

182 write("\n") 

183 if doctype: 

184 write(_tounicode(doctype)) 

185 if pretty_print: 

186 write("\n") 

187 

188 qnames, namespaces = _namespaces(self._root) 

189 _serialize_xml(write, self._root, qnames, namespaces) 

190 

191 import io 

192 

193 def tostring( 

194 element, 

195 encoding=None, 

196 xml_declaration=None, 

197 method=None, 

198 doctype=None, 

199 pretty_print=False, 

200 ): 

201 """Custom 'tostring' function that uses our ElementTree subclass, with 

202 pretty_print support. 

203 """ 

204 stream = io.StringIO() if encoding == "unicode" else io.BytesIO() 

205 ElementTree(element).write( 

206 stream, 

207 encoding=encoding, 

208 xml_declaration=xml_declaration, 

209 method=method, 

210 doctype=doctype, 

211 pretty_print=pretty_print, 

212 ) 

213 return stream.getvalue() 

214 

215 # serialization support 

216 

217 import re 

218 

219 # Valid XML strings can include any Unicode character, excluding control 

220 # characters, the surrogate blocks, FFFE, and FFFF: 

221 # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] 

222 # Here we reversed the pattern to match only the invalid characters. 

223 # For the 'narrow' python builds supporting only UCS-2, which represent 

224 # characters beyond BMP as UTF-16 surrogate pairs, we need to pass through 

225 # the surrogate block. I haven't found a more elegant solution... 

226 UCS2 = sys.maxunicode < 0x10FFFF 

227 if UCS2: 

228 _invalid_xml_string = re.compile( 

229 "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uFFFE-\uFFFF]" 

230 ) 

231 else: 

232 _invalid_xml_string = re.compile( 

233 "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]" 

234 ) 

235 

236 def _tounicode(s): 

237 """Test if a string is valid user input and decode it to unicode string 

238 using ASCII encoding if it's a bytes string. 

239 Reject all bytes/unicode input that contains non-XML characters. 

240 Reject all bytes input that contains non-ASCII characters. 

241 """ 

242 try: 

243 s = tostr(s, encoding="ascii", errors="strict") 

244 except UnicodeDecodeError: 

245 raise ValueError( 

246 "Bytes strings can only contain ASCII characters. " 

247 "Use unicode strings for non-ASCII characters." 

248 ) 

249 except AttributeError: 

250 _raise_serialization_error(s) 

251 if s and _invalid_xml_string.search(s): 

252 raise ValueError( 

253 "All strings must be XML compatible: Unicode or ASCII, " 

254 "no NULL bytes or control characters" 

255 ) 

256 return s 

257 

258 import contextlib 

259 

260 @contextlib.contextmanager 

261 def _get_writer(file_or_filename, encoding): 

262 # returns text write method and release all resources after using 

263 try: 

264 write = file_or_filename.write 

265 except AttributeError: 

266 # file_or_filename is a file name 

267 f = open( 

268 file_or_filename, 

269 "w", 

270 encoding="utf-8" if encoding == "unicode" else encoding, 

271 errors="xmlcharrefreplace", 

272 ) 

273 with f: 

274 yield f.write 

275 else: 

276 # file_or_filename is a file-like object 

277 # encoding determines if it is a text or binary writer 

278 if encoding == "unicode": 

279 # use a text writer as is 

280 yield write 

281 else: 

282 # wrap a binary writer with TextIOWrapper 

283 detach_buffer = False 

284 if isinstance(file_or_filename, io.BufferedIOBase): 

285 buf = file_or_filename 

286 elif isinstance(file_or_filename, io.RawIOBase): 

287 buf = io.BufferedWriter(file_or_filename) 

288 detach_buffer = True 

289 else: 

290 # This is to handle passed objects that aren't in the 

291 # IOBase hierarchy, but just have a write method 

292 buf = io.BufferedIOBase() 

293 buf.writable = lambda: True 

294 buf.write = write 

295 try: 

296 # TextIOWrapper uses this methods to determine 

297 # if BOM (for UTF-16, etc) should be added 

298 buf.seekable = file_or_filename.seekable 

299 buf.tell = file_or_filename.tell 

300 except AttributeError: 

301 pass 

302 wrapper = io.TextIOWrapper( 

303 buf, 

304 encoding=encoding, 

305 errors="xmlcharrefreplace", 

306 newline="\n", 

307 ) 

308 try: 

309 yield wrapper.write 

310 finally: 

311 # Keep the original file open when the TextIOWrapper and 

312 # the BufferedWriter are destroyed 

313 wrapper.detach() 

314 if detach_buffer: 

315 buf.detach() 

316 

317 from xml.etree.ElementTree import _namespace_map 

318 

319 def _namespaces(elem): 

320 # identify namespaces used in this tree 

321 

322 # maps qnames to *encoded* prefix:local names 

323 qnames = {None: None} 

324 

325 # maps uri:s to prefixes 

326 namespaces = {} 

327 

328 def add_qname(qname): 

329 # calculate serialized qname representation 

330 try: 

331 qname = _tounicode(qname) 

332 if qname[:1] == "{": 

333 uri, tag = qname[1:].rsplit("}", 1) 

334 prefix = namespaces.get(uri) 

335 if prefix is None: 

336 prefix = _namespace_map.get(uri) 

337 if prefix is None: 

338 prefix = "ns%d" % len(namespaces) 

339 else: 

340 prefix = _tounicode(prefix) 

341 if prefix != "xml": 

342 namespaces[uri] = prefix 

343 if prefix: 

344 qnames[qname] = "%s:%s" % (prefix, tag) 

345 else: 

346 qnames[qname] = tag # default element 

347 else: 

348 qnames[qname] = qname 

349 except TypeError: 

350 _raise_serialization_error(qname) 

351 

352 # populate qname and namespaces table 

353 for elem in elem.iter(): 

354 tag = elem.tag 

355 if isinstance(tag, QName): 

356 if tag.text not in qnames: 

357 add_qname(tag.text) 

358 elif isinstance(tag, str): 

359 if tag not in qnames: 

360 add_qname(tag) 

361 elif tag is not None and tag is not Comment and tag is not PI: 

362 _raise_serialization_error(tag) 

363 for key, value in elem.items(): 

364 if isinstance(key, QName): 

365 key = key.text 

366 if key not in qnames: 

367 add_qname(key) 

368 if isinstance(value, QName) and value.text not in qnames: 

369 add_qname(value.text) 

370 text = elem.text 

371 if isinstance(text, QName) and text.text not in qnames: 

372 add_qname(text.text) 

373 return qnames, namespaces 

374 

375 def _serialize_xml(write, elem, qnames, namespaces, **kwargs): 

376 tag = elem.tag 

377 text = elem.text 

378 if tag is Comment: 

379 write("<!--%s-->" % _tounicode(text)) 

380 elif tag is ProcessingInstruction: 

381 write("<?%s?>" % _tounicode(text)) 

382 else: 

383 tag = qnames[_tounicode(tag) if tag is not None else None] 

384 if tag is None: 

385 if text: 

386 write(_escape_cdata(text)) 

387 for e in elem: 

388 _serialize_xml(write, e, qnames, None) 

389 else: 

390 write("<" + tag) 

391 if namespaces: 

392 for uri, prefix in sorted( 

393 namespaces.items(), key=lambda x: x[1] 

394 ): # sort on prefix 

395 if prefix: 

396 prefix = ":" + prefix 

397 write(' xmlns%s="%s"' % (prefix, _escape_attrib(uri))) 

398 attrs = elem.attrib 

399 if attrs: 

400 # try to keep existing attrib order 

401 if len(attrs) <= 1 or type(attrs) is _Attrib: 

402 items = attrs.items() 

403 else: 

404 # if plain dict, use lexical order 

405 items = sorted(attrs.items()) 

406 for k, v in items: 

407 if isinstance(k, QName): 

408 k = _tounicode(k.text) 

409 else: 

410 k = _tounicode(k) 

411 if isinstance(v, QName): 

412 v = qnames[_tounicode(v.text)] 

413 else: 

414 v = _escape_attrib(v) 

415 write(' %s="%s"' % (qnames[k], v)) 

416 if text is not None or len(elem): 

417 write(">") 

418 if text: 

419 write(_escape_cdata(text)) 

420 for e in elem: 

421 _serialize_xml(write, e, qnames, None) 

422 write("</" + tag + ">") 

423 else: 

424 write("/>") 

425 if elem.tail: 

426 write(_escape_cdata(elem.tail)) 

427 

428 def _raise_serialization_error(text): 

429 raise TypeError("cannot serialize %r (type %s)" % (text, type(text).__name__)) 

430 

431 def _escape_cdata(text): 

432 # escape character data 

433 try: 

434 text = _tounicode(text) 

435 # it's worth avoiding do-nothing calls for short strings 

436 if "&" in text: 

437 text = text.replace("&", "&amp;") 

438 if "<" in text: 

439 text = text.replace("<", "&lt;") 

440 if ">" in text: 

441 text = text.replace(">", "&gt;") 

442 return text 

443 except (TypeError, AttributeError): 

444 _raise_serialization_error(text) 

445 

446 def _escape_attrib(text): 

447 # escape attribute value 

448 try: 

449 text = _tounicode(text) 

450 if "&" in text: 

451 text = text.replace("&", "&amp;") 

452 if "<" in text: 

453 text = text.replace("<", "&lt;") 

454 if ">" in text: 

455 text = text.replace(">", "&gt;") 

456 if '"' in text: 

457 text = text.replace('"', "&quot;") 

458 if "\n" in text: 

459 text = text.replace("\n", "&#10;") 

460 return text 

461 except (TypeError, AttributeError): 

462 _raise_serialization_error(text) 

463 

464 def _indent(elem, level=0): 

465 # From http://effbot.org/zone/element-lib.htm#prettyprint 

466 i = "\n" + level * " " 

467 if len(elem): 

468 if not elem.text or not elem.text.strip(): 

469 elem.text = i + " " 

470 if not elem.tail or not elem.tail.strip(): 

471 elem.tail = i 

472 for elem in elem: 

473 _indent(elem, level + 1) 

474 if not elem.tail or not elem.tail.strip(): 

475 elem.tail = i 

476 else: 

477 if level and (not elem.tail or not elem.tail.strip()): 

478 elem.tail = i