Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/et_xmlfile/incremental_tree.py: 31%

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

324 statements  

1# Code modified from cPython's Lib/xml/etree/ElementTree.py 

2# The write() code is modified to allow specifying a particular namespace 

3# uri -> prefix mapping. 

4# 

5# --------------------------------------------------------------------- 

6# Licensed to PSF under a Contributor Agreement. 

7# See https://www.python.org/psf/license for licensing details. 

8# 

9# ElementTree 

10# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved. 

11# 

12# fredrik@pythonware.com 

13# http://www.pythonware.com 

14# -------------------------------------------------------------------- 

15# The ElementTree toolkit is 

16# 

17# Copyright (c) 1999-2008 by Fredrik Lundh 

18# 

19# By obtaining, using, and/or copying this software and/or its 

20# associated documentation, you agree that you have read, understood, 

21# and will comply with the following terms and conditions: 

22# 

23# Permission to use, copy, modify, and distribute this software and 

24# its associated documentation for any purpose and without fee is 

25# hereby granted, provided that the above copyright notice appears in 

26# all copies, and that both that copyright notice and this permission 

27# notice appear in supporting documentation, and that the name of 

28# Secret Labs AB or the author not be used in advertising or publicity 

29# pertaining to distribution of the software without specific, written 

30# prior permission. 

31# 

32# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD 

33# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- 

34# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR 

35# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY 

36# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 

37# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 

38# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 

39# OF THIS SOFTWARE. 

40# -------------------------------------------------------------------- 

41import contextlib 

42import io 

43 

44import xml.etree.ElementTree as ET 

45 

46 

47def current_global_nsmap(): 

48 return { 

49 prefix: uri for uri, prefix in ET._namespace_map.items() 

50 } 

51 

52 

53class IncrementalTree(ET.ElementTree): 

54 

55 def write( 

56 self, 

57 file_or_filename, 

58 encoding=None, 

59 xml_declaration=None, 

60 default_namespace=None, 

61 method=None, 

62 *, 

63 short_empty_elements=True, 

64 nsmap=None, 

65 root_ns_only=False, 

66 minimal_ns_only=False, 

67 ): 

68 """Write element tree to a file as XML. 

69 

70 Arguments: 

71 *file_or_filename* -- file name or a file object opened for writing 

72 

73 *encoding* -- the output encoding (default: US-ASCII) 

74 

75 *xml_declaration* -- bool indicating if an XML declaration should be 

76 added to the output. If None, an XML declaration 

77 is added if encoding IS NOT either of: 

78 US-ASCII, UTF-8, or Unicode 

79 

80 *default_namespace* -- sets the default XML namespace (for "xmlns"). 

81 Takes precedence over any default namespace 

82 provided in nsmap or 

83 xml.etree.ElementTree.register_namespace(). 

84 

85 *method* -- either "xml" (default), "html, "text", or "c14n" 

86 

87 *short_empty_elements* -- controls the formatting of elements 

88 that contain no content. If True (default) 

89 they are emitted as a single self-closed 

90 tag, otherwise they are emitted as a pair 

91 of start/end tags 

92 

93 *nsmap* -- a mapping of namespace prefixes to URIs. These take 

94 precedence over any mappings registered using 

95 xml.etree.ElementTree.register_namespace(). The 

96 default_namespace argument, if supplied, takes precedence 

97 over any default namespace supplied in nsmap. All supplied 

98 namespaces will be declared on the root element, even if 

99 unused in the document. 

100 

101 *root_ns_only* -- bool indicating namespace declrations should only 

102 be written on the root element. This requires two 

103 passes of the xml tree adding additional time to 

104 the writing process. This is primarily meant to 

105 mimic xml.etree.ElementTree's behaviour. 

106 

107 *minimal_ns_only* -- bool indicating only namespaces that were used 

108 to qualify elements or attributes should be 

109 declared. All namespace declarations will be 

110 written on the root element regardless of the 

111 value of the root_ns_only arg. Requires two 

112 passes of the xml tree adding additional time to 

113 the writing process. 

114 

115 """ 

116 if not method: 

117 method = "xml" 

118 elif method not in ("text", "xml", "html"): 

119 raise ValueError("unknown method %r" % method) 

120 if not encoding: 

121 encoding = "us-ascii" 

122 

123 with _get_writer(file_or_filename, encoding) as (write, declared_encoding): 

124 if method == "xml" and ( 

125 xml_declaration 

126 or ( 

127 xml_declaration is None 

128 and encoding.lower() != "unicode" 

129 and declared_encoding.lower() not in ("utf-8", "us-ascii") 

130 ) 

131 ): 

132 write("<?xml version='1.0' encoding='%s'?>\n" % (declared_encoding,)) 

133 if method == "text": 

134 ET._serialize_text(write, self._root) 

135 else: 

136 if method == "xml": 

137 is_html = False 

138 else: 

139 is_html = True 

140 if nsmap: 

141 if None in nsmap: 

142 raise ValueError( 

143 'Found None as default nsmap prefix in nsmap. ' 

144 'Use "" as the default namespace prefix.' 

145 ) 

146 new_nsmap = nsmap.copy() 

147 else: 

148 new_nsmap = {} 

149 if default_namespace: 

150 new_nsmap[""] = default_namespace 

151 if root_ns_only or minimal_ns_only: 

152 # _namespaces returns a mapping of only the namespaces that 

153 # were used. 

154 new_nsmap = _namespaces( 

155 self._root, 

156 default_namespace, 

157 new_nsmap, 

158 ) 

159 if not minimal_ns_only: 

160 if nsmap: 

161 # We want all namespaces defined in the provided 

162 # nsmap to be declared regardless of whether 

163 # they've been used. 

164 new_nsmap.update(nsmap) 

165 if default_namespace: 

166 new_nsmap[""] = default_namespace 

167 global_nsmap = { 

168 prefix: uri for uri, prefix in ET._namespace_map.items() 

169 } 

170 if None in global_nsmap: 

171 raise ValueError( 

172 'Found None as default nsmap prefix in nsmap registered with ' 

173 'register_namespace. Use "" for the default namespace prefix.' 

174 ) 

175 nsmap_scope = {} 

176 _serialize_ns_xml( 

177 write, 

178 self._root, 

179 nsmap_scope, 

180 global_nsmap, 

181 is_html=is_html, 

182 is_root=True, 

183 short_empty_elements=short_empty_elements, 

184 new_nsmap=new_nsmap, 

185 ) 

186 

187 

188def _make_new_ns_prefix( 

189 nsmap_scope, 

190 global_prefixes, 

191 local_nsmap=None, 

192 default_namespace=None, 

193): 

194 i = len(nsmap_scope) 

195 if default_namespace is not None and "" not in nsmap_scope: 

196 # Keep the same numbering scheme as python which assumes the default 

197 # namespace is present if supplied. 

198 i += 1 

199 

200 while True: 

201 prefix = f"ns{i}" 

202 if ( 

203 prefix not in nsmap_scope 

204 and prefix not in global_prefixes 

205 and ( 

206 not local_nsmap or prefix not in local_nsmap 

207 ) 

208 ): 

209 return prefix 

210 i += 1 

211 

212 

213def _get_or_create_prefix( 

214 uri, 

215 nsmap_scope, 

216 global_nsmap, 

217 new_namespace_prefixes, 

218 uri_to_prefix, 

219 for_default_namespace_attr_prefix=False, 

220): 

221 """Find a prefix that doesn't conflict with the ns scope or create a new prefix 

222 

223 This function mutates nsmap_scope, global_nsmap, new_namespace_prefixes and 

224 uri_to_prefix. It is intended to keep state in _serialize_ns_xml consistent 

225 while deduplicating the house keeping code or updating these dictionaries. 

226 """ 

227 # Check if we can reuse an existing (global) prefix within the current 

228 # namespace scope. There maybe many prefixes pointing to a single URI by 

229 # this point and we need to select a prefix that is not in use in the 

230 # current scope. 

231 for global_prefix, global_uri in global_nsmap.items(): 

232 if uri == global_uri and global_prefix not in nsmap_scope: 

233 prefix = global_prefix 

234 break 

235 else: # no break 

236 # We couldn't find a suitable existing prefix for this namespace scope, 

237 # let's create a new one. 

238 prefix = _make_new_ns_prefix(nsmap_scope, global_prefixes=global_nsmap) 

239 global_nsmap[prefix] = uri 

240 nsmap_scope[prefix] = uri 

241 if not for_default_namespace_attr_prefix: 

242 # Don't override the actual default namespace prefix 

243 uri_to_prefix[uri] = prefix 

244 if prefix != "xml": 

245 new_namespace_prefixes.add(prefix) 

246 return prefix 

247 

248 

249def _find_default_namespace_attr_prefix( 

250 default_namespace, 

251 nsmap, 

252 local_nsmap, 

253 global_prefixes, 

254 provided_default_namespace=None, 

255): 

256 # Search the provided nsmap for any prefixes for this uri that aren't the 

257 # default namespace "" 

258 for prefix, uri in nsmap.items(): 

259 if uri == default_namespace and prefix != "": 

260 return prefix 

261 

262 for prefix, uri in local_nsmap.items(): 

263 if uri == default_namespace and prefix != "": 

264 return prefix 

265 

266 # _namespace_map is a 1:1 mapping of uri -> prefix 

267 prefix = ET._namespace_map.get(default_namespace) 

268 if prefix and prefix not in nsmap: 

269 return prefix 

270 

271 return _make_new_ns_prefix( 

272 nsmap, 

273 global_prefixes, 

274 local_nsmap, 

275 provided_default_namespace, 

276 ) 

277 

278 

279def process_attribs( 

280 elem, 

281 is_nsmap_scope_changed, 

282 default_ns_attr_prefix, 

283 nsmap_scope, 

284 global_nsmap, 

285 new_namespace_prefixes, 

286 uri_to_prefix, 

287): 

288 item_parts = [] 

289 for k, v in elem.items(): 

290 if isinstance(k, ET.QName): 

291 k = k.text 

292 try: 

293 if k[:1] == "{": 

294 uri_and_name = k[1:].rsplit("}", 1) 

295 try: 

296 prefix = uri_to_prefix[uri_and_name[0]] 

297 except KeyError: 

298 if not is_nsmap_scope_changed: 

299 # We're about to mutate the these dicts so 

300 # let's copy them first. We don't have to 

301 # recompute other mappings as we're looking up 

302 # or creating a new prefix 

303 nsmap_scope = nsmap_scope.copy() 

304 uri_to_prefix = uri_to_prefix.copy() 

305 is_nsmap_scope_changed = True 

306 prefix = _get_or_create_prefix( 

307 uri_and_name[0], 

308 nsmap_scope, 

309 global_nsmap, 

310 new_namespace_prefixes, 

311 uri_to_prefix, 

312 ) 

313 

314 if not prefix: 

315 if default_ns_attr_prefix: 

316 prefix = default_ns_attr_prefix 

317 else: 

318 for prefix, known_uri in nsmap_scope.items(): 

319 if known_uri == uri_and_name[0] and prefix != "": 

320 default_ns_attr_prefix = prefix 

321 break 

322 else: # no break 

323 if not is_nsmap_scope_changed: 

324 # We're about to mutate the these dicts so 

325 # let's copy them first. We don't have to 

326 # recompute other mappings as we're looking up 

327 # or creating a new prefix 

328 nsmap_scope = nsmap_scope.copy() 

329 uri_to_prefix = uri_to_prefix.copy() 

330 is_nsmap_scope_changed = True 

331 prefix = _get_or_create_prefix( 

332 uri_and_name[0], 

333 nsmap_scope, 

334 global_nsmap, 

335 new_namespace_prefixes, 

336 uri_to_prefix, 

337 for_default_namespace_attr_prefix=True, 

338 ) 

339 default_ns_attr_prefix = prefix 

340 k = f"{prefix}:{uri_and_name[1]}" 

341 except TypeError: 

342 ET._raise_serialization_error(k) 

343 

344 if isinstance(v, ET.QName): 

345 if v.text[:1] != "{": 

346 v = v.text 

347 else: 

348 uri_and_name = v.text[1:].rsplit("}", 1) 

349 try: 

350 prefix = uri_to_prefix[uri_and_name[0]] 

351 except KeyError: 

352 if not is_nsmap_scope_changed: 

353 # We're about to mutate the these dicts so 

354 # let's copy them first. We don't have to 

355 # recompute other mappings as we're looking up 

356 # or creating a new prefix 

357 nsmap_scope = nsmap_scope.copy() 

358 uri_to_prefix = uri_to_prefix.copy() 

359 is_nsmap_scope_changed = True 

360 prefix = _get_or_create_prefix( 

361 uri_and_name[0], 

362 nsmap_scope, 

363 global_nsmap, 

364 new_namespace_prefixes, 

365 uri_to_prefix, 

366 ) 

367 v = f"{prefix}:{uri_and_name[1]}" 

368 item_parts.append((k, v)) 

369 return item_parts, default_ns_attr_prefix, nsmap_scope 

370 

371 

372def write_elem_start( 

373 write, 

374 elem, 

375 nsmap_scope, 

376 global_nsmap, 

377 short_empty_elements, 

378 is_html, 

379 is_root=False, 

380 uri_to_prefix=None, 

381 default_ns_attr_prefix=None, 

382 new_nsmap=None, 

383 **kwargs, 

384): 

385 """Write the opening tag (including self closing) and element text. 

386 

387 Refer to _serialize_ns_xml for description of arguments. 

388 

389 nsmap_scope should be an empty dictionary on first call. All nsmap prefixes 

390 must be strings with the default namespace prefix represented by "". 

391 

392 eg. 

393 - <foo attr1="one"> (returns tag = 'foo') 

394 - <foo attr1="one">text (returns tag = 'foo') 

395 - <foo attr1="one" /> (returns tag = None) 

396 

397 Returns: 

398 tag: 

399 The tag name to be closed or None if no closing required. 

400 nsmap_scope: 

401 The current nsmap after any prefix to uri additions from this 

402 element. This is the input dict if unmodified or an updated copy. 

403 default_ns_attr_prefix: 

404 The prefix for the default namespace to use with attrs. 

405 uri_to_prefix: 

406 The current uri to prefix map after any uri to prefix additions 

407 from this element. This is the input dict if unmodified or an 

408 updated copy. 

409 next_remains_root: 

410 A bool indicating if the child element(s) should be treated as 

411 their own roots. 

412 """ 

413 tag = elem.tag 

414 text = elem.text 

415 

416 if tag is ET.Comment: 

417 write("<!--%s-->" % text) 

418 tag = None 

419 next_remains_root = False 

420 elif tag is ET.ProcessingInstruction: 

421 write("<?%s?>" % text) 

422 tag = None 

423 next_remains_root = False 

424 else: 

425 if new_nsmap: 

426 is_nsmap_scope_changed = True 

427 nsmap_scope = nsmap_scope.copy() 

428 nsmap_scope.update(new_nsmap) 

429 new_namespace_prefixes = set(new_nsmap.keys()) 

430 new_namespace_prefixes.discard("xml") 

431 # We need to recompute the uri to prefixes 

432 uri_to_prefix = None 

433 default_ns_attr_prefix = None 

434 else: 

435 is_nsmap_scope_changed = False 

436 new_namespace_prefixes = set() 

437 

438 if uri_to_prefix is None: 

439 if None in nsmap_scope: 

440 raise ValueError( 

441 'Found None as a namespace prefix. Use "" as the default namespace prefix.' 

442 ) 

443 uri_to_prefix = {uri: prefix for prefix, uri in nsmap_scope.items()} 

444 if "" in nsmap_scope: 

445 # There may be multiple prefixes for the default namespace but 

446 # we want to make sure we preferentially use "" (for elements) 

447 uri_to_prefix[nsmap_scope[""]] = "" 

448 

449 if tag is None: 

450 # tag supression where tag is set to None 

451 # Don't change is_root so namespaces can be passed down 

452 next_remains_root = is_root 

453 if text: 

454 write(ET._escape_cdata(text)) 

455 else: 

456 next_remains_root = False 

457 if isinstance(tag, ET.QName): 

458 tag = tag.text 

459 try: 

460 # These splits / fully qualified tag creationg are the 

461 # bottleneck in this implementation vs the python 

462 # implementation. 

463 # The following split takes ~42ns with no uri and ~85ns if a 

464 # prefix is present. If the uri was present, we then need to 

465 # look up a prefix (~14ns) and create the fully qualified 

466 # string (~41ns). This gives a total of ~140ns where a uri is 

467 # present. 

468 # Python's implementation needs to preprocess the tree to 

469 # create a dict of qname -> tag by traversing the tree which 

470 # takes a bit of extra time but it quickly makes that back by 

471 # only having to do a dictionary look up (~14ns) for each tag / 

472 # attrname vs our splitting (~140ns). 

473 # So here we have the flexibility of being able to redefine the 

474 # uri a prefix points to midway through serialisation at the 

475 # expense of performance (~10% slower for a 1mb file on my 

476 # machine). 

477 if tag[:1] == "{": 

478 uri_and_name = tag[1:].rsplit("}", 1) 

479 try: 

480 prefix = uri_to_prefix[uri_and_name[0]] 

481 except KeyError: 

482 if not is_nsmap_scope_changed: 

483 # We're about to mutate the these dicts so let's 

484 # copy them first. We don't have to recompute other 

485 # mappings as we're looking up or creating a new 

486 # prefix 

487 nsmap_scope = nsmap_scope.copy() 

488 uri_to_prefix = uri_to_prefix.copy() 

489 is_nsmap_scope_changed = True 

490 prefix = _get_or_create_prefix( 

491 uri_and_name[0], 

492 nsmap_scope, 

493 global_nsmap, 

494 new_namespace_prefixes, 

495 uri_to_prefix, 

496 ) 

497 if prefix: 

498 tag = f"{prefix}:{uri_and_name[1]}" 

499 else: 

500 tag = uri_and_name[1] 

501 elif "" in nsmap_scope: 

502 raise ValueError( 

503 "cannot use non-qualified names with default_namespace option" 

504 ) 

505 except TypeError: 

506 ET._raise_serialization_error(tag) 

507 

508 write("<" + tag) 

509 

510 if elem.attrib: 

511 item_parts, default_ns_attr_prefix, nsmap_scope = process_attribs( 

512 elem, 

513 is_nsmap_scope_changed, 

514 default_ns_attr_prefix, 

515 nsmap_scope, 

516 global_nsmap, 

517 new_namespace_prefixes, 

518 uri_to_prefix, 

519 ) 

520 else: 

521 item_parts = [] 

522 if new_namespace_prefixes: 

523 ns_attrs = [] 

524 for k in sorted(new_namespace_prefixes): 

525 v = nsmap_scope[k] 

526 if k: 

527 k = "xmlns:" + k 

528 else: 

529 k = "xmlns" 

530 ns_attrs.append((k, v)) 

531 if is_html: 

532 write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in ns_attrs])) 

533 else: 

534 write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in ns_attrs])) 

535 if item_parts: 

536 if is_html: 

537 write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in item_parts])) 

538 else: 

539 write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in item_parts])) 

540 if is_html: 

541 write(">") 

542 ltag = tag.lower() 

543 if text: 

544 if ltag == "script" or ltag == "style": 

545 write(text) 

546 else: 

547 write(ET._escape_cdata(text)) 

548 if ltag in ET.HTML_EMPTY: 

549 tag = None 

550 elif text or len(elem) or not short_empty_elements: 

551 write(">") 

552 if text: 

553 write(ET._escape_cdata(text)) 

554 else: 

555 tag = None 

556 write(" />") 

557 return ( 

558 tag, 

559 nsmap_scope, 

560 default_ns_attr_prefix, 

561 uri_to_prefix, 

562 next_remains_root, 

563 ) 

564 

565 

566def _serialize_ns_xml( 

567 write, 

568 elem, 

569 nsmap_scope, 

570 global_nsmap, 

571 short_empty_elements, 

572 is_html, 

573 is_root=False, 

574 uri_to_prefix=None, 

575 default_ns_attr_prefix=None, 

576 new_nsmap=None, 

577 **kwargs, 

578): 

579 """Serialize an element or tree using 'write' for output. 

580 

581 Args: 

582 write: 

583 A function to write the xml to its destination. 

584 elem: 

585 The element to serialize. 

586 nsmap_scope: 

587 The current prefix to uri mapping for this element. This should be 

588 an empty dictionary for the root element. Additional namespaces are 

589 progressively added using the new_nsmap arg. 

590 global_nsmap: 

591 A dict copy of the globally registered _namespace_map in uri to 

592 prefix form 

593 short_empty_elements: 

594 Controls the formatting of elements that contain no content. If True 

595 (default) they are emitted as a single self-closed tag, otherwise 

596 they are emitted as a pair of start/end tags. 

597 is_html: 

598 Set to True to serialize as HTML otherwise XML. 

599 is_root: 

600 Boolean indicating if this is a root element. 

601 uri_to_prefix: 

602 Current state of the mapping of uri to prefix. 

603 default_ns_attr_prefix: 

604 new_nsmap: 

605 New prefix -> uri mapping to be applied to this element. 

606 """ 

607 ( 

608 tag, 

609 nsmap_scope, 

610 default_ns_attr_prefix, 

611 uri_to_prefix, 

612 next_remains_root, 

613 ) = write_elem_start( 

614 write, 

615 elem, 

616 nsmap_scope, 

617 global_nsmap, 

618 short_empty_elements, 

619 is_html, 

620 is_root, 

621 uri_to_prefix, 

622 default_ns_attr_prefix, 

623 new_nsmap=new_nsmap, 

624 ) 

625 for e in elem: 

626 _serialize_ns_xml( 

627 write, 

628 e, 

629 nsmap_scope, 

630 global_nsmap, 

631 short_empty_elements, 

632 is_html, 

633 next_remains_root, 

634 uri_to_prefix, 

635 default_ns_attr_prefix, 

636 new_nsmap=None, 

637 ) 

638 if tag: 

639 write(f"</{tag}>") 

640 if elem.tail: 

641 write(ET._escape_cdata(elem.tail)) 

642 

643 

644def _qnames_iter(elem): 

645 """Iterate through all the qualified names in elem""" 

646 seen_el_qnames = set() 

647 seen_other_qnames = set() 

648 for this_elem in elem.iter(): 

649 tag = this_elem.tag 

650 if isinstance(tag, str): 

651 if tag not in seen_el_qnames: 

652 seen_el_qnames.add(tag) 

653 yield tag, True 

654 elif isinstance(tag, ET.QName): 

655 tag = tag.text 

656 if tag not in seen_el_qnames: 

657 seen_el_qnames.add(tag) 

658 yield tag, True 

659 elif ( 

660 tag is not None 

661 and tag is not ET.ProcessingInstruction 

662 and tag is not ET.Comment 

663 ): 

664 ET._raise_serialization_error(tag) 

665 

666 for key, value in this_elem.items(): 

667 if isinstance(key, ET.QName): 

668 key = key.text 

669 if key not in seen_other_qnames: 

670 seen_other_qnames.add(key) 

671 yield key, False 

672 

673 if isinstance(value, ET.QName): 

674 if value.text not in seen_other_qnames: 

675 seen_other_qnames.add(value.text) 

676 yield value.text, False 

677 

678 text = this_elem.text 

679 if isinstance(text, ET.QName): 

680 if text.text not in seen_other_qnames: 

681 seen_other_qnames.add(text.text) 

682 yield text.text, False 

683 

684 

685def _namespaces( 

686 elem, 

687 default_namespace=None, 

688 nsmap=None, 

689): 

690 """Find all namespaces used in the document and return a prefix to uri map""" 

691 if nsmap is None: 

692 nsmap = {} 

693 

694 out_nsmap = {} 

695 

696 seen_uri_to_prefix = {} 

697 # Multiple prefixes may be present for a single uri. This will select the 

698 # last prefix found in nsmap for a given uri. 

699 local_prefix_map = {uri: prefix for prefix, uri in nsmap.items()} 

700 if default_namespace is not None: 

701 local_prefix_map[default_namespace] = "" 

702 elif "" in nsmap: 

703 # but we make sure the default prefix always take precedence 

704 local_prefix_map[nsmap[""]] = "" 

705 

706 global_prefixes = set(ET._namespace_map.values()) 

707 has_unqual_el = False 

708 default_namespace_attr_prefix = None 

709 for qname, is_el in _qnames_iter(elem): 

710 try: 

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

712 uri_and_name = qname[1:].rsplit("}", 1) 

713 

714 prefix = seen_uri_to_prefix.get(uri_and_name[0]) 

715 if prefix is None: 

716 prefix = local_prefix_map.get(uri_and_name[0]) 

717 if prefix is None or prefix in out_nsmap: 

718 prefix = ET._namespace_map.get(uri_and_name[0]) 

719 if prefix is None or prefix in out_nsmap: 

720 prefix = _make_new_ns_prefix( 

721 out_nsmap, 

722 global_prefixes, 

723 nsmap, 

724 default_namespace, 

725 ) 

726 if prefix or is_el: 

727 out_nsmap[prefix] = uri_and_name[0] 

728 seen_uri_to_prefix[uri_and_name[0]] = prefix 

729 

730 if not is_el and not prefix and not default_namespace_attr_prefix: 

731 # Find the alternative prefix to use with non-element 

732 # names 

733 default_namespace_attr_prefix = _find_default_namespace_attr_prefix( 

734 uri_and_name[0], 

735 out_nsmap, 

736 nsmap, 

737 global_prefixes, 

738 default_namespace, 

739 ) 

740 out_nsmap[default_namespace_attr_prefix] = uri_and_name[0] 

741 # Don't add this uri to prefix mapping as it might override 

742 # the uri -> "" default mapping. We'll fix this up at the 

743 # end of the fn. 

744 # local_prefix_map[uri_and_name[0]] = default_namespace_attr_prefix 

745 else: 

746 if is_el: 

747 has_unqual_el = True 

748 except TypeError: 

749 ET._raise_serialization_error(qname) 

750 

751 if "" in out_nsmap and has_unqual_el: 

752 # FIXME: can this be handled in XML 1.0? 

753 raise ValueError( 

754 "cannot use non-qualified names with default_namespace option" 

755 ) 

756 

757 # The xml prefix doesn't need to be declared but may have been used to 

758 # prefix names. Let's remove it if it has been used 

759 out_nsmap.pop("xml", None) 

760 return out_nsmap 

761 

762 

763def tostring( 

764 element, 

765 encoding=None, 

766 method=None, 

767 *, 

768 xml_declaration=None, 

769 default_namespace=None, 

770 short_empty_elements=True, 

771 nsmap=None, 

772 root_ns_only=False, 

773 minimal_ns_only=False, 

774 tree_cls=IncrementalTree, 

775): 

776 """Generate string representation of XML element. 

777 

778 All subelements are included. If encoding is "unicode", a string 

779 is returned. Otherwise a bytestring is returned. 

780 

781 *element* is an Element instance, *encoding* is an optional output 

782 encoding defaulting to US-ASCII, *method* is an optional output which can 

783 be one of "xml" (default), "html", "text" or "c14n", *default_namespace* 

784 sets the default XML namespace (for "xmlns"). 

785 

786 Returns an (optionally) encoded string containing the XML data. 

787 

788 """ 

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

790 tree_cls(element).write( 

791 stream, 

792 encoding, 

793 xml_declaration=xml_declaration, 

794 default_namespace=default_namespace, 

795 method=method, 

796 short_empty_elements=short_empty_elements, 

797 nsmap=nsmap, 

798 root_ns_only=root_ns_only, 

799 minimal_ns_only=minimal_ns_only, 

800 ) 

801 return stream.getvalue() 

802 

803 

804def tostringlist( 

805 element, 

806 encoding=None, 

807 method=None, 

808 *, 

809 xml_declaration=None, 

810 default_namespace=None, 

811 short_empty_elements=True, 

812 nsmap=None, 

813 root_ns_only=False, 

814 minimal_ns_only=False, 

815 tree_cls=IncrementalTree, 

816): 

817 lst = [] 

818 stream = ET._ListDataStream(lst) 

819 tree_cls(element).write( 

820 stream, 

821 encoding, 

822 xml_declaration=xml_declaration, 

823 default_namespace=default_namespace, 

824 method=method, 

825 short_empty_elements=short_empty_elements, 

826 nsmap=nsmap, 

827 root_ns_only=root_ns_only, 

828 minimal_ns_only=minimal_ns_only, 

829 ) 

830 return lst 

831 

832 

833def compat_tostring( 

834 element, 

835 encoding=None, 

836 method=None, 

837 *, 

838 xml_declaration=None, 

839 default_namespace=None, 

840 short_empty_elements=True, 

841 nsmap=None, 

842 root_ns_only=True, 

843 minimal_ns_only=False, 

844 tree_cls=IncrementalTree, 

845): 

846 """tostring with options that produce the same results as xml.etree.ElementTree.tostring 

847 

848 root_ns_only=True is a bit slower than False as it needs to traverse the 

849 tree one more time to collect all the namespaces. 

850 """ 

851 return tostring( 

852 element, 

853 encoding=encoding, 

854 method=method, 

855 xml_declaration=xml_declaration, 

856 default_namespace=default_namespace, 

857 short_empty_elements=short_empty_elements, 

858 nsmap=nsmap, 

859 root_ns_only=root_ns_only, 

860 minimal_ns_only=minimal_ns_only, 

861 tree_cls=tree_cls, 

862 ) 

863 

864 

865# -------------------------------------------------------------------- 

866# serialization support 

867 

868@contextlib.contextmanager 

869def _get_writer(file_or_filename, encoding): 

870 # Copied from Python 3.12 

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

872 try: 

873 write = file_or_filename.write 

874 except AttributeError: 

875 # file_or_filename is a file name 

876 if encoding.lower() == "unicode": 

877 encoding = "utf-8" 

878 with open(file_or_filename, "w", encoding=encoding, 

879 errors="xmlcharrefreplace") as file: 

880 yield file.write, encoding 

881 else: 

882 # file_or_filename is a file-like object 

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

884 if encoding.lower() == "unicode": 

885 # use a text writer as is 

886 yield write, getattr(file_or_filename, "encoding", None) or "utf-8" 

887 else: 

888 # wrap a binary writer with TextIOWrapper 

889 with contextlib.ExitStack() as stack: 

890 if isinstance(file_or_filename, io.BufferedIOBase): 

891 file = file_or_filename 

892 elif isinstance(file_or_filename, io.RawIOBase): 

893 file = io.BufferedWriter(file_or_filename) 

894 # Keep the original file open when the BufferedWriter is 

895 # destroyed 

896 stack.callback(file.detach) 

897 else: 

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

899 # IOBase hierarchy, but just have a write method 

900 file = io.BufferedIOBase() 

901 file.writable = lambda: True 

902 file.write = write 

903 try: 

904 # TextIOWrapper uses this methods to determine 

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

906 file.seekable = file_or_filename.seekable 

907 file.tell = file_or_filename.tell 

908 except AttributeError: 

909 pass 

910 file = io.TextIOWrapper(file, 

911 encoding=encoding, 

912 errors="xmlcharrefreplace", 

913 newline="\n") 

914 # Keep the original file open when the TextIOWrapper is 

915 # destroyed 

916 stack.callback(file.detach) 

917 yield file.write, encoding