Update xml and test_xml_etree

This commit is contained in:
CPython Developers authored and Jeong YunWon committed 2022-08-15 01:08:58 +09:00
1 parent 628c4c0edf
commit 2eec919416
13 files changed
+358 -168

No files matched your search

+77 -25
View File
@@ -10,7 +10,6 @@ import functools
import html
import io
import itertools
import locale
import operator
import os
import pickle
@@ -130,6 +129,9 @@ def checkwarnings(*filters, quiet=False):
return newtest
return decorator
def convlinesep(data):
return data.replace(b'\n', os.linesep.encode())
class ModuleTest(unittest.TestCase):
def test_sanity(self):
@@ -1023,17 +1025,15 @@ class ElementTreeTest(unittest.TestCase):
@unittest.expectedFailure
def test_tostring_xml_declaration_unicode_encoding(self):
elem = ET.XML('<body><tag/></body>')
preferredencoding = locale.getpreferredencoding()
self.assertEqual(
f"<?xml version='1.0' encoding='{preferredencoding}'?>\n<body><tag /></body>",
ET.tostring(elem, encoding='unicode', xml_declaration=True)
ET.tostring(elem, encoding='unicode', xml_declaration=True),
"<?xml version='1.0' encoding='utf-8'?>\n<body><tag /></body>"
)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_tostring_xml_declaration_cases(self):
elem = ET.XML('<body><tag>ø</tag></body>')
preferredencoding = locale.getpreferredencoding()
TESTCASES = [
# (expected_retval, encoding, xml_declaration)
# ... xml_declaration = None
@@ -1060,7 +1060,7 @@ class ElementTreeTest(unittest.TestCase):
b"<body><tag>&#248;</tag></body>", 'US-ASCII', True),
(b"<?xml version='1.0' encoding='ISO-8859-1'?>\n"
b"<body><tag>\xf8</tag></body>", 'ISO-8859-1', True),
(f"<?xml version='1.0' encoding='{preferredencoding}'?>\n"
("<?xml version='1.0' encoding='utf-8'?>\n"
"<body><tag>ø</tag></body>", 'unicode', True),
]
@@ -1102,11 +1102,10 @@ class ElementTreeTest(unittest.TestCase):
b"<?xml version='1.0' encoding='us-ascii'?>\n<body><tag /></body>"
)
preferredencoding = locale.getpreferredencoding()
stringlist = ET.tostringlist(elem, encoding='unicode', xml_declaration=True)
self.assertEqual(
''.join(stringlist),
f"<?xml version='1.0' encoding='{preferredencoding}'?>\n<body><tag /></body>"
"<?xml version='1.0' encoding='utf-8'?>\n<body><tag /></body>"
)
self.assertRegex(stringlist[0], r"^<\?xml version='1.0' encoding='.+'?>")
self.assertEqual(['<body', '>', '<tag', ' />', '</body>'], stringlist[1:])
@@ -3914,38 +3913,91 @@ class IOTest(unittest.TestCase):
@unittest.expectedFailure
def test_write_to_filename(self):
self.addCleanup(os_helper.unlink, TESTFN)
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
tree.write(TESTFN)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site />''')
self.assertEqual(f.read(), b'''<site>&#248;</site>''')
def test_write_to_filename_with_encoding(self):
self.addCleanup(os_helper.unlink, TESTFN)
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
tree.write(TESTFN, encoding='utf-8')
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site>\xc3\xb8</site>''')
tree.write(TESTFN, encoding='ISO-8859-1')
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), convlinesep(
b'''<?xml version='1.0' encoding='ISO-8859-1'?>\n'''
b'''<site>\xf8</site>'''))
def test_write_to_filename_as_unicode(self):
self.addCleanup(os_helper.unlink, TESTFN)
with open(TESTFN, 'w') as f:
encoding = f.encoding
os_helper.unlink(TESTFN)
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
tree.write(TESTFN, encoding='unicode')
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b"<site>\xc3\xb8</site>")
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_text_file(self):
self.addCleanup(os_helper.unlink, TESTFN)
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
with open(TESTFN, 'w', encoding='utf-8') as f:
tree.write(f, encoding='unicode')
self.assertFalse(f.closed)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site />''')
self.assertEqual(f.read(), b'''<site>\xc3\xb8</site>''')
with open(TESTFN, 'w', encoding='ascii', errors='xmlcharrefreplace') as f:
tree.write(f, encoding='unicode')
self.assertFalse(f.closed)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site>&#248;</site>''')
with open(TESTFN, 'w', encoding='ISO-8859-1') as f:
tree.write(f, encoding='unicode')
self.assertFalse(f.closed)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site>\xf8</site>''')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_binary_file(self):
self.addCleanup(os_helper.unlink, TESTFN)
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
with open(TESTFN, 'wb') as f:
tree.write(f)
self.assertFalse(f.closed)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site />''')
self.assertEqual(f.read(), b'''<site>&#248;</site>''')
def test_write_to_binary_file_with_encoding(self):
self.addCleanup(os_helper.unlink, TESTFN)
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
with open(TESTFN, 'wb') as f:
tree.write(f, encoding='utf-8')
self.assertFalse(f.closed)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(), b'''<site>\xc3\xb8</site>''')
with open(TESTFN, 'wb') as f:
tree.write(f, encoding='ISO-8859-1')
self.assertFalse(f.closed)
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(),
b'''<?xml version='1.0' encoding='ISO-8859-1'?>\n'''
b'''<site>\xf8</site>''')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_binary_file_with_bom(self):
self.addCleanup(os_helper.unlink, TESTFN)
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
# test BOM writing to buffered file
with open(TESTFN, 'wb') as f:
tree.write(f, encoding='utf-16')
@@ -3953,7 +4005,7 @@ class IOTest(unittest.TestCase):
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(),
'''<?xml version='1.0' encoding='utf-16'?>\n'''
'''<site />'''.encode("utf-16"))
'''<site>\xf8</site>'''.encode("utf-16"))
# test BOM writing to non-buffered file
with open(TESTFN, 'wb', buffering=0) as f:
tree.write(f, encoding='utf-16')
@@ -3961,7 +4013,7 @@ class IOTest(unittest.TestCase):
with open(TESTFN, 'rb') as f:
self.assertEqual(f.read(),
'''<?xml version='1.0' encoding='utf-16'?>\n'''
'''<site />'''.encode("utf-16"))
'''<site>\xf8</site>'''.encode("utf-16"))
# TODO: RUSTPYTHON
@unittest.expectedFailure
@@ -3974,10 +4026,10 @@ class IOTest(unittest.TestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_stringio(self):
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
stream = io.StringIO()
tree.write(stream, encoding='unicode')
self.assertEqual(stream.getvalue(), '''<site />''')
self.assertEqual(stream.getvalue(), '''<site>\xf8</site>''')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@@ -3990,10 +4042,10 @@ class IOTest(unittest.TestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_bytesio(self):
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
raw = io.BytesIO()
tree.write(raw)
self.assertEqual(raw.getvalue(), b'''<site />''')
self.assertEqual(raw.getvalue(), b'''<site>&#248;</site>''')
class dummy:
pass
@@ -4011,12 +4063,12 @@ class IOTest(unittest.TestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_user_text_writer(self):
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
stream = io.StringIO()
writer = self.dummy()
writer.write = stream.write
tree.write(writer, encoding='unicode')
self.assertEqual(stream.getvalue(), '''<site />''')
self.assertEqual(stream.getvalue(), '''<site>\xf8</site>''')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@@ -4032,12 +4084,12 @@ class IOTest(unittest.TestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_write_to_user_binary_writer(self):
tree = ET.ElementTree(ET.XML('''<site />'''))
tree = ET.ElementTree(ET.XML('''<site>\xf8</site>'''))
raw = io.BytesIO()
writer = self.dummy()
writer.write = raw.write
tree.write(writer)
self.assertEqual(raw.getvalue(), b'''<site />''')
self.assertEqual(raw.getvalue(), b'''<site>&#248;</site>''')
# TODO: RUSTPYTHON
@unittest.expectedFailure
+4 -4
View File
@@ -204,11 +204,11 @@ class ExpatBuilder:
buffer = file.read(16*1024)
if not buffer:
break
parser.Parse(buffer, 0)
parser.Parse(buffer, False)
if first_buffer and self.document.documentElement:
self._setup_subset(buffer)
first_buffer = False
parser.Parse("", True)
parser.Parse(b"", True)
except ParseEscape:
pass
doc = self.document
@@ -637,7 +637,7 @@ class FragmentBuilder(ExpatBuilder):
nsattrs = self._getNSattrs() # get ns decls from node's ancestors
document = _FRAGMENT_BUILDER_TEMPLATE % (ident, subset, nsattrs)
try:
parser.Parse(document, 1)
parser.Parse(document, True)
except:
self.reset()
raise
@@ -697,7 +697,7 @@ class FragmentBuilder(ExpatBuilder):
self.fragment = self.document.createDocumentFragment()
self.curNode = self.fragment
try:
parser.Parse(self._source, 1)
parser.Parse(self._source, True)
finally:
self.curNode = old_cur_node
self.document = old_document
+43 -13
View File
@@ -43,10 +43,11 @@ class Node(xml.dom.Node):
def __bool__(self):
return True
def toxml(self, encoding=None):
return self.toprettyxml("", "", encoding)
def toxml(self, encoding=None, standalone=None):
return self.toprettyxml("", "", encoding, standalone)
def toprettyxml(self, indent="\t", newl="\n", encoding=None):
def toprettyxml(self, indent="\t", newl="\n", encoding=None,
standalone=None):
if encoding is None:
writer = io.StringIO()
else:
@@ -56,7 +57,7 @@ class Node(xml.dom.Node):
newline='\n')
if self.nodeType == Node.DOCUMENT_NODE:
# Can pass encoding only to document, to put it into XML header
self.writexml(writer, "", indent, newl, encoding)
self.writexml(writer, "", indent, newl, encoding, standalone)
else:
self.writexml(writer, "", indent, newl)
if encoding is None:
@@ -718,6 +719,14 @@ class Element(Node):
Node.unlink(self)
def getAttribute(self, attname):
"""Returns the value of the specified attribute.
Returns the value of the element's attribute named attname as
a string. An empty string is returned if the element does not
have such an attribute. Note that an empty string may also be
returned as an explicitly given attribute value, use the
hasAttribute method to distinguish these two cases.
"""
if self._attrs is None:
return ""
try:
@@ -823,10 +832,16 @@ class Element(Node):
# Restore this since the node is still useful and otherwise
# unlinked
node.ownerDocument = self.ownerDocument
return node
removeAttributeNodeNS = removeAttributeNode
def hasAttribute(self, name):
"""Checks whether the element has an attribute with the specified name.
Returns True if the element has an attribute with the specified name.
Otherwise, returns False.
"""
if self._attrs is None:
return False
return name in self._attrs
@@ -837,6 +852,11 @@ class Element(Node):
return (namespaceURI, localName) in self._attrsNS
def getElementsByTagName(self, name):
"""Returns all descendant elements with the given tag name.
Returns the list of all descendant elements (not direct children
only) with the specified tag name.
"""
return _get_elements_by_tagName_helper(self, name, NodeList())
def getElementsByTagNameNS(self, namespaceURI, localName):
@@ -847,22 +867,27 @@ class Element(Node):
return "<DOM Element: %s at %#x>" % (self.tagName, id(self))
def writexml(self, writer, indent="", addindent="", newl=""):
"""Write an XML element to a file-like object
Write the element to the writer object that must provide
a write method (e.g. a file or StringIO object).
"""
# indent = current indentation
# addindent = indentation to add to higher levels
# newl = newline string
writer.write(indent+"<" + self.tagName)
attrs = self._get_attributes()
a_names = sorted(attrs.keys())
for a_name in a_names:
for a_name in attrs.keys():
writer.write(" %s=\"" % a_name)
_write_data(writer, attrs[a_name].value)
writer.write("\"")
if self.childNodes:
writer.write(">")
if (len(self.childNodes) == 1 and
self.childNodes[0].nodeType == Node.TEXT_NODE):
self.childNodes[0].nodeType in (
Node.TEXT_NODE, Node.CDATA_SECTION_NODE)):
self.childNodes[0].writexml(writer, '', '', '')
else:
writer.write(newl)
@@ -1786,12 +1811,17 @@ class Document(Node, DocumentLS):
raise xml.dom.NotSupportedErr("cannot import document type nodes")
return _clone_node(node, deep, self)
def writexml(self, writer, indent="", addindent="", newl="", encoding=None):
if encoding is None:
writer.write('<?xml version="1.0" ?>'+newl)
else:
writer.write('<?xml version="1.0" encoding="%s"?>%s' % (
encoding, newl))
def writexml(self, writer, indent="", addindent="", newl="", encoding=None,
standalone=None):
declarations = []
if encoding:
declarations.append(f'encoding="{encoding}"')
if standalone is not None:
declarations.append(f'standalone="{"yes" if standalone else "no"}"')
writer.write(f'<?xml version="1.0" {" ".join(declarations)}?>{newl}')
for node in self.childNodes:
node.writexml(writer, indent, addindent, newl)
+7
View File
@@ -217,6 +217,13 @@ class DOMEventStream:
self.parser.setContentHandler(self.pulldom)
def __getitem__(self, pos):
import warnings
warnings.warn(
"DOMEventStream's __getitem__ method ignores 'pos' parameter. "
"Use iterator protocol instead.",
DeprecationWarning,
stacklevel=2
)
rc = self.getEvent()
if rc:
return rc
+1 -24
View File
@@ -1,7 +1,6 @@
"""Implementation of the DOM Level 3 'LS-Load' feature."""
import copy
import warnings
import xml.dom
from xml.dom.NodeFilter import NodeFilter
@@ -80,7 +79,7 @@ class DOMBuilder:
settings = self._settings[(_name_xform(name), state)]
except KeyError:
raise xml.dom.NotSupportedErr(
"unsupported feature: %r" % (name,))
"unsupported feature: %r" % (name,)) from None
else:
for name, value in settings:
setattr(self._options, name, value)
@@ -332,29 +331,10 @@ class DOMBuilderFilter:
del NodeFilter
class _AsyncDeprecatedProperty:
def warn(self, cls):
clsname = cls.__name__
warnings.warn(
"{cls}.async is deprecated; use {cls}.async_".format(cls=clsname),
DeprecationWarning)
def __get__(self, instance, cls):
self.warn(cls)
if instance is not None:
return instance.async_
return False
def __set__(self, instance, value):
self.warn(type(instance))
setattr(instance, 'async_', value)
class DocumentLS:
"""Mixin to create documents that conform to the load/save spec."""
async_ = False
locals()['async'] = _AsyncDeprecatedProperty() # Avoid DeprecationWarning
def _get_async(self):
return False
@@ -384,9 +364,6 @@ class DocumentLS:
return snode.toxml()
del _AsyncDeprecatedProperty
class DOMImplementationLS:
MODE_SYNCHRONOUS = 1
MODE_ASYNCHRONOUS = 2
+50 -8
View File
@@ -42,7 +42,7 @@
# --------------------------------------------------------------------
# Licensed to PSF under a Contributor Agreement.
# See http://www.python.org/psf/license for licensing details.
# See https://www.python.org/psf/license for licensing details.
##
# Limited XInclude support for the ElementTree package.
@@ -50,18 +50,28 @@
import copy
from . import ElementTree
from urllib.parse import urljoin
XINCLUDE = "{http://www.w3.org/2001/XInclude}"
XINCLUDE_INCLUDE = XINCLUDE + "include"
XINCLUDE_FALLBACK = XINCLUDE + "fallback"
# For security reasons, the inclusion depth is limited to this read-only value by default.
DEFAULT_MAX_INCLUSION_DEPTH = 6
##
# Fatal include error.
class FatalIncludeError(SyntaxError):
pass
class LimitedRecursiveIncludeError(FatalIncludeError):
pass
##
# Default loader. This loader reads an included resource from disk.
#
@@ -92,13 +102,33 @@ def default_loader(href, parse, encoding=None):
# @param loader Optional resource loader. If omitted, it defaults
# to {@link default_loader}. If given, it should be a callable
# that implements the same interface as <b>default_loader</b>.
# @param base_url The base URL of the original file, to resolve
# relative include file references.
# @param max_depth The maximum number of recursive inclusions.
# Limited to reduce the risk of malicious content explosion.
# Pass a negative value to disable the limitation.
# @throws LimitedRecursiveIncludeError If the {@link max_depth} was exceeded.
# @throws FatalIncludeError If the function fails to include a given
# resource, or if the tree contains malformed XInclude elements.
# @throws OSError If the function fails to load a given resource.
# @throws IOError If the function fails to load a given resource.
# @returns the node or its replacement if it was an XInclude node
def include(elem, loader=None):
def include(elem, loader=None, base_url=None,
max_depth=DEFAULT_MAX_INCLUSION_DEPTH):
if max_depth is None:
max_depth = -1
elif max_depth < 0:
raise ValueError("expected non-negative depth or None for 'max_depth', got %r" % max_depth)
if hasattr(elem, 'getroot'):
elem = elem.getroot()
if loader is None:
loader = default_loader
_include(elem, loader, base_url, max_depth, set())
def _include(elem, loader, base_url, max_depth, _parent_hrefs):
# look for xinclude elements
i = 0
while i < len(elem):
@@ -106,14 +136,24 @@ def include(elem, loader=None):
if e.tag == XINCLUDE_INCLUDE:
# process xinclude directive
href = e.get("href")
if base_url:
href = urljoin(base_url, href)
parse = e.get("parse", "xml")
if parse == "xml":
if href in _parent_hrefs:
raise FatalIncludeError("recursive include of %s" % href)
if max_depth == 0:
raise LimitedRecursiveIncludeError(
"maximum xinclude depth reached when including file %s" % href)
_parent_hrefs.add(href)
node = loader(href, parse)
if node is None:
raise FatalIncludeError(
"cannot load %r as %r" % (href, parse)
)
node = copy.copy(node)
node = copy.copy(node) # FIXME: this makes little sense with recursive includes
_include(node, loader, href, max_depth - 1, _parent_hrefs)
_parent_hrefs.remove(href)
if e.tail:
node.tail = (node.tail or "") + e.tail
elem[i] = node
@@ -123,11 +163,13 @@ def include(elem, loader=None):
raise FatalIncludeError(
"cannot load %r as %r" % (href, parse)
)
if e.tail:
text += e.tail
if i:
node = elem[i-1]
node.tail = (node.tail or "") + text + (e.tail or "")
node.tail = (node.tail or "") + text
else:
elem.text = (elem.text or "") + text + (e.tail or "")
elem.text = (elem.text or "") + text
del elem[i]
continue
else:
@@ -139,5 +181,5 @@ def include(elem, loader=None):
"xi:fallback tag must be child of xi:include (%r)" % e.tag
)
else:
include(e, loader)
i = i + 1
_include(e, loader, base_url, max_depth, _parent_hrefs)
i += 1
+25 -9
View File
@@ -48,7 +48,7 @@
# --------------------------------------------------------------------
# Licensed to PSF under a Contributor Agreement.
# See http://www.python.org/psf/license for licensing details.
# See https://www.python.org/psf/license for licensing details.
##
# Implementation module for XPath support. There's usually no reason
@@ -65,8 +65,9 @@ xpath_tokenizer_re = re.compile(
r"//?|"
r"\.\.|"
r"\(\)|"
r"!=|"
r"[/.*:\[\]\(\)@=])|"
r"((?:\{[^}]+\})?[^/\[\]\(\)@=\s]+)|"
r"((?:\{[^}]+\})?[^/\[\]\(\)@!=\s]+)|"
r"\s+"
)
@@ -225,7 +226,6 @@ def prepare_parent(next, token):
def prepare_predicate(next, token):
# FIXME: replace with real parser!!! refs:
# http://effbot.org/zone/simple-iterator-parser.htm
# http://javascript.crockford.com/tdop/tdop.html
signature = []
predicate = []
@@ -253,15 +253,19 @@ def prepare_predicate(next, token):
if elem.get(key) is not None:
yield elem
return select
if signature == "@-='":
# [@attribute='value']
if signature == "@-='" or signature == "@-!='":
# [@attribute='value'] or [@attribute!='value']
key = predicate[1]
value = predicate[-1]
def select(context, result):
for elem in result:
if elem.get(key) == value:
yield elem
return select
def select_negated(context, result):
for elem in result:
if (attr_value := elem.get(key)) is not None and attr_value != value:
yield elem
return select_negated if '!=' in signature else select
if signature == "-" and not re.match(r"\-?\d+$", predicate[0]):
# [tag]
tag = predicate[0]
@@ -270,8 +274,10 @@ def prepare_predicate(next, token):
if elem.find(tag) is not None:
yield elem
return select
if signature == ".='" or (signature == "-='" and not re.match(r"\-?\d+$", predicate[0])):
# [.='value'] or [tag='value']
if signature == ".='" or signature == ".!='" or (
(signature == "-='" or signature == "-!='")
and not re.match(r"\-?\d+$", predicate[0])):
# [.='value'] or [tag='value'] or [.!='value'] or [tag!='value']
tag = predicate[0]
value = predicate[-1]
if tag:
@@ -281,12 +287,22 @@ def prepare_predicate(next, token):
if "".join(e.itertext()) == value:
yield elem
break
def select_negated(context, result):
for elem in result:
for e in elem.iterfind(tag):
if "".join(e.itertext()) != value:
yield elem
break
else:
def select(context, result):
for elem in result:
if "".join(elem.itertext()) == value:
yield elem
return select
def select_negated(context, result):
for elem in result:
if "".join(elem.itertext()) != value:
yield elem
return select_negated if '!=' in signature else select
if signature == "-" or signature == "-()" or signature == "-()-":
# [index] or [last()] or [last()-index]
if signature == "-":
+90 -70
View File
@@ -35,7 +35,7 @@
#---------------------------------------------------------------------
# Licensed to PSF under a Contributor Agreement.
# See http://www.python.org/psf/license for licensing details.
# See https://www.python.org/psf/license for licensing details.
#
# ElementTree
# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved.
@@ -76,7 +76,7 @@ __all__ = [
"dump",
"Element", "ElementTree",
"fromstring", "fromstringlist",
"iselement", "iterparse",
"indent", "iselement", "iterparse",
"parse", "ParseError",
"PI", "ProcessingInstruction",
"QName",
@@ -195,6 +195,13 @@ class Element:
original tree.
"""
warnings.warn(
"elem.copy() is deprecated. Use copy.copy(elem) instead.",
DeprecationWarning
)
return self.__copy__()
def __copy__(self):
elem = self.makeelement(self.tag, self.attrib)
elem.text = self.text
elem.tail = self.tail
@@ -273,19 +280,6 @@ class Element:
# assert iselement(element)
self._children.remove(subelement)
def getchildren(self):
"""(Deprecated) Return all subelements.
Elements are returned in document order.
"""
warnings.warn(
"This method will be removed in future versions. "
"Use 'list(elem)' or iteration over elem instead.",
DeprecationWarning, stacklevel=2
)
return self._children
def find(self, path, namespaces=None):
"""Find first matching element by tag name or path.
@@ -409,15 +403,6 @@ class Element:
for e in self._children:
yield from e.iter(tag)
# compatibility
def getiterator(self, tag=None):
warnings.warn(
"This method will be removed in future versions. "
"Use 'elem.iter()' or 'list(elem.iter())' instead.",
DeprecationWarning, stacklevel=2
)
return list(self.iter(tag))
def itertext(self):
"""Create text iterator.
@@ -617,15 +602,6 @@ class ElementTree:
# assert self._root is not None
return self._root.iter(tag)
# compatibility
def getiterator(self, tag=None):
warnings.warn(
"This method will be removed in future versions. "
"Use 'tree.iter()' or 'list(tree.iter())' instead.",
DeprecationWarning, stacklevel=2
)
return list(self.iter(tag))
def find(self, path, namespaces=None):
"""Find first matching element by tag name or path.
@@ -752,16 +728,11 @@ class ElementTree:
encoding = "utf-8"
else:
encoding = "us-ascii"
enc_lower = encoding.lower()
with _get_writer(file_or_filename, enc_lower) as write:
with _get_writer(file_or_filename, encoding) as (write, declared_encoding):
if method == "xml" and (xml_declaration or
(xml_declaration is None and
enc_lower not in ("utf-8", "us-ascii", "unicode"))):
declared_encoding = encoding
if enc_lower == "unicode":
# Retrieve the default encoding for the xml declaration
import locale
declared_encoding = locale.getpreferredencoding()
encoding.lower() != "unicode" and
declared_encoding.lower() not in ("utf-8", "us-ascii"))):
write("<?xml version='1.0' encoding='%s'?>\n" % (
declared_encoding,))
if method == "text":
@@ -786,19 +757,17 @@ def _get_writer(file_or_filename, encoding):
write = file_or_filename.write
except AttributeError:
# file_or_filename is a file name
if encoding == "unicode":
file = open(file_or_filename, "w")
else:
file = open(file_or_filename, "w", encoding=encoding,
errors="xmlcharrefreplace")
with file:
yield file.write
if encoding.lower() == "unicode":
encoding="utf-8"
with open(file_or_filename, "w", encoding=encoding,
errors="xmlcharrefreplace") as file:
yield file.write, encoding
else:
# file_or_filename is a file-like object
# encoding determines if it is a text or binary writer
if encoding == "unicode":
if encoding.lower() == "unicode":
# use a text writer as is
yield write
yield write, getattr(file_or_filename, "encoding", None) or "utf-8"
else:
# wrap a binary writer with TextIOWrapper
with contextlib.ExitStack() as stack:
@@ -829,7 +798,7 @@ def _get_writer(file_or_filename, encoding):
# Keep the original file open when the TextIOWrapper is
# destroyed
stack.callback(file.detach)
yield file.write
yield file.write, encoding
def _namespaces(elem, default_namespace=None):
# identify namespaces used in this tree
@@ -1081,15 +1050,15 @@ def _escape_attrib(text):
text = text.replace(">", "&gt;")
if "\"" in text:
text = text.replace("\"", "&quot;")
# The following business with carriage returns is to satisfy
# Section 2.11 of the XML specification, stating that
# CR or CR LN should be replaced with just LN
# Although section 2.11 of the XML specification states that CR or
# CR LN should be replaced with just LN, it applies only to EOLNs
# which take part of organizing file into lines. Within attributes,
# we are replacing these with entity numbers, so they do not count.
# http://www.w3.org/TR/REC-xml/#sec-line-ends
if "\r\n" in text:
text = text.replace("\r\n", "\n")
# The current solution, contained in following six lines, was
# discussed in issue 17582 and 39011.
if "\r" in text:
text = text.replace("\r", "\n")
#The following four lines are issue 17582
text = text.replace("\r", "&#13;")
if "\n" in text:
text = text.replace("\n", "&#10;")
if "\t" in text:
@@ -1185,6 +1154,57 @@ def dump(elem):
if not tail or tail[-1] != "\n":
sys.stdout.write("\n")
def indent(tree, space=" ", level=0):
"""Indent an XML document by inserting newlines and indentation space
after elements.
*tree* is the ElementTree or Element to modify. The (root) element
itself will not be changed, but the tail text of all elements in its
subtree will be adapted.
*space* is the whitespace to insert for each indentation level, two
space characters by default.
*level* is the initial indentation level. Setting this to a higher
value than 0 can be used for indenting subtrees that are more deeply
nested inside of a document.
"""
if isinstance(tree, ElementTree):
tree = tree.getroot()
if level < 0:
raise ValueError(f"Initial indentation level must be >= 0, got {level}")
if not len(tree):
return
# Reduce the memory consumption by reusing indentation strings.
indentations = ["\n" + level * space]
def _indent_children(elem, level):
# Start a new indentation level for the first child.
child_level = level + 1
try:
child_indentation = indentations[child_level]
except IndexError:
child_indentation = indentations[level] + space
indentations.append(child_indentation)
if not elem.text or not elem.text.strip():
elem.text = child_indentation
for child in elem:
if len(child):
_indent_children(child, child_level)
if not child.tail or not child.tail.strip():
child.tail = child_indentation
# Dedent after the last child by overwriting the previous indentation.
if not child.tail.strip():
child.tail = indentations[level]
_indent_children(tree, 0)
# --------------------------------------------------------------------
# parsing
@@ -1221,8 +1241,14 @@ def iterparse(source, events=None, parser=None):
# Use the internal, undocumented _parser argument for now; When the
# parser argument of iterparse is removed, this can be killed.
pullparser = XMLPullParser(events=events, _parser=parser)
def iterator():
def iterator(source):
close_source = False
try:
if not hasattr(source, "read"):
source = open(source, "rb")
close_source = True
yield None
while True:
yield from pullparser.read_events()
# load event buffer
@@ -1238,16 +1264,12 @@ def iterparse(source, events=None, parser=None):
source.close()
class IterParseIterator(collections.abc.Iterator):
__next__ = iterator().__next__
__next__ = iterator(source).__next__
it = IterParseIterator()
it.root = None
del iterator, IterParseIterator
close_source = False
if not hasattr(source, "read"):
source = open(source, "rb")
close_source = True
next(it)
return it
@@ -1256,7 +1278,7 @@ class XMLPullParser:
def __init__(self, events=None, *, _parser=None):
# The _parser argument is for internal use only and must not be relied
# upon in user code. It will be removed in a future release.
# See http://bugs.python.org/issue17741 for more details.
# See https://bugs.python.org/issue17741 for more details.
self._events_queue = collections.deque()
self._parser = _parser or XMLParser(target=TreeBuilder())
@@ -1533,7 +1555,6 @@ class XMLParser:
# Configure pyexpat: buffering, new-style attribute handling.
parser.buffer_text = 1
parser.ordered_attributes = 1
parser.specified_attributes = 1
self._doctype = None
self.entity = {}
try:
@@ -1553,7 +1574,6 @@ class XMLParser:
for event_name in events_to_report:
if event_name == "start":
parser.ordered_attributes = 1
parser.specified_attributes = 1
def handler(tag, attrib_in, event=event_name, append=append,
start=self._start):
append((event, start(tag, attrib_in)))
@@ -1690,14 +1710,14 @@ class XMLParser:
def feed(self, data):
"""Feed encoded data to parser."""
try:
self.parser.Parse(data, 0)
self.parser.Parse(data, False)
except self._error as v:
self._raiseerror(v)
def close(self):
"""Finish feeding data to parser and return element structure."""
try:
self.parser.Parse("", 1) # end of data
self.parser.Parse(b"", True) # end of data
except self._error as v:
self._raiseerror(v)
try:
+1 -1
View File
@@ -30,4 +30,4 @@
# --------------------------------------------------------------------
# Licensed to PSF under a Contributor Agreement.
# See http://www.python.org/psf/license for licensing details.
# See https://www.python.org/psf/license for licensing details.
+5 -5
View File
@@ -67,18 +67,18 @@ if sys.platform[:4] == "java" and sys.registry.containsKey(_key):
default_parser_list = sys.registry.getProperty(_key).split(",")
def make_parser(parser_list = []):
def make_parser(parser_list=()):
"""Creates and returns a SAX parser.
Creates the first parser it is able to instantiate of the ones
given in the list created by doing parser_list +
default_parser_list. The lists must contain the names of Python
given in the iterable created by chaining parser_list and
default_parser_list. The iterables must contain the names of Python
modules containing both a SAX parser and a create_parser function."""
for parser_name in parser_list + default_parser_list:
for parser_name in list(parser_list) + default_parser_list:
try:
return _create_parser(parser_name)
except ImportError as e:
except ImportError:
import sys
if parser_name in sys.modules:
# The parser module was found, but importing it
+7 -7
View File
@@ -93,7 +93,7 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
self._parser = None
self._namespaces = namespaceHandling
self._lex_handler_prop = None
self._parsing = 0
self._parsing = False
self._entity_stack = []
self._external_ges = 0
self._interning = None
@@ -203,10 +203,10 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
# IncrementalParser methods
def feed(self, data, isFinal = 0):
def feed(self, data, isFinal=False):
if not self._parsing:
self.reset()
self._parsing = 1
self._parsing = True
self._cont_handler.startDocument()
try:
@@ -237,13 +237,13 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
# If we are completing an external entity, do nothing here
return
try:
self.feed("", isFinal = 1)
self.feed(b"", isFinal=True)
self._cont_handler.endDocument()
self._parsing = 0
self._parsing = False
# break cycle created by expat handlers pointing to our methods
self._parser = None
finally:
self._parsing = 0
self._parsing = False
if self._parser is not None:
# Keep ErrorColumnNumber and ErrorLineNumber after closing.
parser = _ClosedParser()
@@ -307,7 +307,7 @@ class ExpatParser(xmlreader.IncrementalParser, xmlreader.Locator):
self._parser.SetParamEntityParsing(
expat.XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE)
self._parsing = 0
self._parsing = False
self._entity_stack = []
# Locator methods
+45
View File
@@ -340,3 +340,48 @@ all_properties = [property_lexical_handler,
property_xml_string,
property_encoding,
property_interning_dict]
class LexicalHandler:
"""Optional SAX2 handler for lexical events.
This handler is used to obtain lexical information about an XML
document, that is, information about how the document was encoded
(as opposed to what it contains, which is reported to the
ContentHandler), such as comments and CDATA marked section
boundaries.
To set the LexicalHandler of an XMLReader, use the setProperty
method with the property identifier
'http://xml.org/sax/properties/lexical-handler'."""
def comment(self, content):
"""Reports a comment anywhere in the document (including the
DTD and outside the document element).
content is a string that holds the contents of the comment."""
def startDTD(self, name, public_id, system_id):
"""Report the start of the DTD declarations, if the document
has an associated DTD.
A startEntity event will be reported before declaration events
from the external DTD subset are reported, and this can be
used to infer from which subset DTD declarations derive.
name is the name of the document element type, public_id the
public identifier of the DTD (or None if none were supplied)
and system_id the system identfier of the external subset (or
None if none were supplied)."""
def endDTD(self):
"""Signals the end of DTD declarations."""
def startCDATA(self):
"""Reports the beginning of a CDATA marked section.
The contents of the CDATA marked section will be reported
through the characters event."""
def endCDATA(self):
"""Reports the end of a CDATA marked section."""
+3 -2
View File
@@ -56,8 +56,7 @@ def quoteattr(data, entities={}):
the optional entities parameter. The keys and values must all be
strings; each key will be replaced with its corresponding value.
"""
entities = entities.copy()
entities.update({'\n': '&#10;', '\r': '&#13;', '\t':'&#9;'})
entities = {**entities, '\n': '&#10;', '\r': '&#13;', '\t':'&#9;'}
data = escape(data, entities)
if '"' in data:
if "'" in data:
@@ -340,6 +339,8 @@ def prepare_input_source(source, base=""):
"""This function takes an InputSource and an optional base URL and
returns a fully resolved InputSource object ready for reading."""
if isinstance(source, os.PathLike):
source = os.fspath(source)
if isinstance(source, str):
source = xmlreader.InputSource(source)
elif hasattr(source, "read"):