mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
* Let xml.sax initialize pyexpat-backed parsers RustPython already exposes an Expat-like XML parser, but xml.sax expects a few setup APIs before parsing. Add compatible parser setup methods and constants without pretending to implement full Expat external entity behavior. Constraint: Lib/test changes only remove expectedFailure markers for tests that now pass; test bodies and assertions are unchanged. Rejected: Implementing full Expat parameter entity parsing | xml-rs does not expose that configuration and the immediate failure is the missing setup API. Confidence: high Scope-risk: narrow Directive: Keep SetParamEntityParsing as a compatibility shim unless the backend grows real Expat-style entity parsing support. Tested: PATH=/tmp/pyshim:$PATH prek run --all-files Tested: cargo run --quiet -- extra_tests/snippets/stdlib_xml.py Tested: cargo run --release -- -m test test_sax Tested: cargo run --release -- -m test test_pyexpat Tested: cargo test --workspace --exclude rustpython-capi --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --features threading --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env && (cd crates/capi && cargo test) Tested: PYO3_CONFIG_FILE=$PWD/crates/capi/pyo3-rustpython.config cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher Tested: cargo build --release --features sqlite && cd extra_tests && /tmp/rustpython-extra-tests-venv/bin/python -m pytest -v Tested: cargo clippy --workspace --exclude rustpython-capi --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --features threading --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env Assisted-by: Codex:gpt-5.5 * Keep XML follow-up tests aligned with passing behavior The pyexpat setup API compatibility fix also makes pulldom's basic parse smoke test pass, so keep RustPython expected-failure markers aligned with current behavior and apply the ruff import-spacing fix. Constraint: Test body and assertions remain unchanged. Confidence: high Scope-risk: narrow Tested: cargo run --release -- -m test test_pulldom -v Tested: cargo run --release -- -m test test_sax Tested: cargo run --release -- -m test test_pyexpat Tested: cargo run --quiet -- extra_tests/snippets/stdlib_xml.py Tested: PATH=/tmp/pyshim:/Users/kbm/.codex/tmp/arg0/codex-arg0ejsdGi:/Users/kbm/.omx-runs/run-20260703105454-4fab/.omx/runtime/bin:/Users/kbm/.nvm/versions/node/v24.15.0/bin:/Users/kbm/Library/Java/JavaVirtualMachines/temurin-21.0.9/Contents/Home/bin:/Users/kbm/.antigravity/antigravity/bin:/opt/homebrew/opt/mysql-client/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin:/var/folders/vq/1mhmtsbs2kj_03tsx74kd1mm0000gn/T//cmux-cli-shims/93BA47BD-630C-4ED3-8573-0FE6ABFAE824:/Applications/cmux.app/Contents/Resources/bin:/Users/kbm/.nvm/versions/node/v24.15.0/bin:/Users/kbm/Library/Java/JavaVirtualMachines/temurin-21.0.9/Contents/Home/bin:/Users/kbm/.antigravity/antigravity/bin:/opt/homebrew/opt/mysql-client/bin:/var/folders/vq/1mhmtsbs2kj_03tsx74kd1mm0000gn/T/cmux-cli-shims/93BA47BD-630C-4ED3-8573-0FE6ABFAE824:/Applications/Visual Studio Code.app/Contents/Resources/app/bin:/Applications/Visual Studio Code.app/Contents/Resources/app/bin:/Applications/Visual Studio Code.app/Contents/Resources/app/bin prek run --all-files Tested: cargo build --release --features sqlite && cd extra_tests && /tmp/rustpython-extra-tests-venv/bin/python -m pytest -v Tested: PYO3_CONFIG_FILE=/Users/kbm/IdeaProjects/RustPython/crates/capi/pyo3-rustpython.config cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher Tested: cargo clippy --workspace --exclude rustpython-capi --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --features threading --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env Assisted-by: Codex:gpt-5.5
353 lines
13 KiB
Python
Vendored
353 lines
13 KiB
Python
Vendored
import io
|
|
import unittest
|
|
import xml.sax
|
|
|
|
from xml.sax.xmlreader import AttributesImpl
|
|
from xml.sax.handler import feature_external_ges
|
|
from xml.dom import pulldom
|
|
|
|
from test.support import findfile
|
|
|
|
|
|
tstfile = findfile("test.xml", subdir="xmltestdata")
|
|
|
|
# A handy XML snippet, containing attributes, a namespace prefix, and a
|
|
# self-closing tag:
|
|
SMALL_SAMPLE = """<?xml version="1.0"?>
|
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xdc="http://www.xml.com/books">
|
|
<!-- A comment -->
|
|
<title>Introduction to XSL</title>
|
|
<hr/>
|
|
<p><xdc:author xdc:attrib="prefixed attribute" attrib="other attrib">A. Namespace</xdc:author></p>
|
|
</html>"""
|
|
|
|
|
|
class PullDOMTestCase(unittest.TestCase):
|
|
|
|
def test_parse(self):
|
|
"""Minimal test of DOMEventStream.parse()"""
|
|
|
|
# This just tests that parsing from a stream works. Actual parser
|
|
# semantics are tested using parseString with a more focused XML
|
|
# fragment.
|
|
|
|
# Test with a filename:
|
|
handler = pulldom.parse(tstfile)
|
|
self.addCleanup(handler.stream.close)
|
|
list(handler)
|
|
|
|
# Test with a file object:
|
|
with open(tstfile, "rb") as fin:
|
|
list(pulldom.parse(fin))
|
|
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON; implement DOM semantic
|
|
def test_parse_semantics(self):
|
|
"""Test DOMEventStream parsing semantics."""
|
|
|
|
items = pulldom.parseString(SMALL_SAMPLE)
|
|
evt, node = next(items)
|
|
# Just check the node is a Document:
|
|
self.assertHasAttr(node, "createElement")
|
|
self.assertEqual(pulldom.START_DOCUMENT, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.START_ELEMENT, evt)
|
|
self.assertEqual("html", node.tagName)
|
|
self.assertEqual(2, len(node.attributes))
|
|
self.assertEqual(node.attributes.getNamedItem("xmlns:xdc").value,
|
|
"http://www.xml.com/books")
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.CHARACTERS, evt) # Line break
|
|
evt, node = next(items)
|
|
# XXX - A comment should be reported here!
|
|
# self.assertEqual(pulldom.COMMENT, evt)
|
|
# Line break after swallowed comment:
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual("title", node.tagName)
|
|
title_node = node
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
self.assertEqual("Introduction to XSL", node.data)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
self.assertEqual("title", node.tagName)
|
|
self.assertTrue(title_node is node)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.START_ELEMENT, evt)
|
|
self.assertEqual("hr", node.tagName)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
self.assertEqual("hr", node.tagName)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.START_ELEMENT, evt)
|
|
self.assertEqual("p", node.tagName)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.START_ELEMENT, evt)
|
|
self.assertEqual("xdc:author", node.tagName)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
self.assertEqual("xdc:author", node.tagName)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
# XXX No END_DOCUMENT item is ever obtained:
|
|
#evt, node = next(items)
|
|
#self.assertEqual(pulldom.END_DOCUMENT, evt)
|
|
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON; pulldom.parseString(SMALL_SAMPLE) return iterator with tuple with 2 elements
|
|
def test_expandItem(self):
|
|
"""Ensure expandItem works as expected."""
|
|
items = pulldom.parseString(SMALL_SAMPLE)
|
|
# Loop through the nodes until we get to a "title" start tag:
|
|
for evt, item in items:
|
|
if evt == pulldom.START_ELEMENT and item.tagName == "title":
|
|
items.expandNode(item)
|
|
self.assertEqual(1, len(item.childNodes))
|
|
break
|
|
else:
|
|
self.fail("No \"title\" element detected in SMALL_SAMPLE!")
|
|
# Loop until we get to the next start-element:
|
|
for evt, node in items:
|
|
if evt == pulldom.START_ELEMENT:
|
|
break
|
|
self.assertEqual("hr", node.tagName,
|
|
"expandNode did not leave DOMEventStream in the correct state.")
|
|
# Attempt to expand a standalone element:
|
|
items.expandNode(node)
|
|
self.assertEqual(next(items)[0], pulldom.CHARACTERS)
|
|
evt, node = next(items)
|
|
self.assertEqual(node.tagName, "p")
|
|
items.expandNode(node)
|
|
next(items) # Skip character data
|
|
evt, node = next(items)
|
|
self.assertEqual(node.tagName, "html")
|
|
with self.assertRaises(StopIteration):
|
|
next(items)
|
|
items.clear()
|
|
self.assertIsNone(items.parser)
|
|
self.assertIsNone(items.stream)
|
|
|
|
@unittest.expectedFailure
|
|
def test_comment(self):
|
|
"""PullDOM does not receive "comment" events."""
|
|
items = pulldom.parseString(SMALL_SAMPLE)
|
|
for evt, _ in items:
|
|
if evt == pulldom.COMMENT:
|
|
break
|
|
else:
|
|
self.fail("No comment was encountered")
|
|
|
|
@unittest.expectedFailure
|
|
def test_end_document(self):
|
|
"""PullDOM does not receive "end-document" events."""
|
|
items = pulldom.parseString(SMALL_SAMPLE)
|
|
# Read all of the nodes up to and including </html>:
|
|
for evt, node in items:
|
|
if evt == pulldom.END_ELEMENT and node.tagName == "html":
|
|
break
|
|
try:
|
|
# Assert that the next node is END_DOCUMENT:
|
|
evt, node = next(items)
|
|
self.assertEqual(pulldom.END_DOCUMENT, evt)
|
|
except StopIteration:
|
|
self.fail(
|
|
"Ran out of events, but should have received END_DOCUMENT")
|
|
|
|
def test_external_ges_default(self):
|
|
parser = pulldom.parseString(SMALL_SAMPLE)
|
|
saxparser = parser.parser
|
|
ges = saxparser.getFeature(feature_external_ges)
|
|
self.assertEqual(ges, False)
|
|
|
|
|
|
class ThoroughTestCase(unittest.TestCase):
|
|
"""Test the hard-to-reach parts of pulldom."""
|
|
|
|
def test_thorough_parse(self):
|
|
"""Test some of the hard-to-reach parts of PullDOM."""
|
|
self._test_thorough(pulldom.parse(None, parser=SAXExerciser()))
|
|
|
|
@unittest.expectedFailure
|
|
def test_sax2dom_fail(self):
|
|
"""SAX2DOM can"t handle a PI before the root element."""
|
|
pd = SAX2DOMTestHelper(None, SAXExerciser(), 12)
|
|
self._test_thorough(pd)
|
|
|
|
def test_thorough_sax2dom(self):
|
|
"""Test some of the hard-to-reach parts of SAX2DOM."""
|
|
pd = SAX2DOMTestHelper(None, SAX2DOMExerciser(), 12)
|
|
self._test_thorough(pd, False)
|
|
|
|
def _test_thorough(self, pd, before_root=True):
|
|
"""Test some of the hard-to-reach parts of the parser, using a mock
|
|
parser."""
|
|
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.START_DOCUMENT, evt)
|
|
# Just check the node is a Document:
|
|
self.assertHasAttr(node, "createElement")
|
|
|
|
if before_root:
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.COMMENT, evt)
|
|
self.assertEqual("a comment", node.data)
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.PROCESSING_INSTRUCTION, evt)
|
|
self.assertEqual("target", node.target)
|
|
self.assertEqual("data", node.data)
|
|
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.START_ELEMENT, evt)
|
|
self.assertEqual("html", node.tagName)
|
|
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.COMMENT, evt)
|
|
self.assertEqual("a comment", node.data)
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.PROCESSING_INSTRUCTION, evt)
|
|
self.assertEqual("target", node.target)
|
|
self.assertEqual("data", node.data)
|
|
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.START_ELEMENT, evt)
|
|
self.assertEqual("p", node.tagName)
|
|
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.CHARACTERS, evt)
|
|
self.assertEqual("text", node.data)
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
self.assertEqual("p", node.tagName)
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.END_ELEMENT, evt)
|
|
self.assertEqual("html", node.tagName)
|
|
evt, node = next(pd)
|
|
self.assertEqual(pulldom.END_DOCUMENT, evt)
|
|
|
|
|
|
class SAXExerciser(object):
|
|
"""A fake sax parser that calls some of the harder-to-reach sax methods to
|
|
ensure it emits the correct events"""
|
|
|
|
def setContentHandler(self, handler):
|
|
self._handler = handler
|
|
|
|
def parse(self, _):
|
|
h = self._handler
|
|
h.startDocument()
|
|
|
|
# The next two items ensure that items preceding the first
|
|
# start_element are properly stored and emitted:
|
|
h.comment("a comment")
|
|
h.processingInstruction("target", "data")
|
|
|
|
h.startElement("html", AttributesImpl({}))
|
|
|
|
h.comment("a comment")
|
|
h.processingInstruction("target", "data")
|
|
|
|
h.startElement("p", AttributesImpl({"class": "paraclass"}))
|
|
h.characters("text")
|
|
h.endElement("p")
|
|
h.endElement("html")
|
|
h.endDocument()
|
|
|
|
def stub(self, *args, **kwargs):
|
|
"""Stub method. Does nothing."""
|
|
pass
|
|
setProperty = stub
|
|
setFeature = stub
|
|
|
|
|
|
class SAX2DOMExerciser(SAXExerciser):
|
|
"""The same as SAXExerciser, but without the processing instruction and
|
|
comment before the root element, because S2D can"t handle it"""
|
|
|
|
def parse(self, _):
|
|
h = self._handler
|
|
h.startDocument()
|
|
h.startElement("html", AttributesImpl({}))
|
|
h.comment("a comment")
|
|
h.processingInstruction("target", "data")
|
|
h.startElement("p", AttributesImpl({"class": "paraclass"}))
|
|
h.characters("text")
|
|
h.endElement("p")
|
|
h.endElement("html")
|
|
h.endDocument()
|
|
|
|
|
|
class SAX2DOMTestHelper(pulldom.DOMEventStream):
|
|
"""Allows us to drive SAX2DOM from a DOMEventStream."""
|
|
|
|
def reset(self):
|
|
self.pulldom = pulldom.SAX2DOM()
|
|
# This content handler relies on namespace support
|
|
self.parser.setFeature(xml.sax.handler.feature_namespaces, 1)
|
|
self.parser.setContentHandler(self.pulldom)
|
|
|
|
|
|
class SAX2DOMTestCase(unittest.TestCase):
|
|
|
|
def confirm(self, test, testname="Test"):
|
|
self.assertTrue(test, testname)
|
|
|
|
@unittest.expectedFailure # TODO: RUSTPYTHON; read from stream io
|
|
def test_basic(self):
|
|
"""Ensure SAX2DOM can parse from a stream."""
|
|
with io.StringIO(SMALL_SAMPLE) as fin:
|
|
sd = SAX2DOMTestHelper(fin, xml.sax.make_parser(),
|
|
len(SMALL_SAMPLE))
|
|
for evt, node in sd:
|
|
if evt == pulldom.START_ELEMENT and node.tagName == "html":
|
|
break
|
|
# Because the buffer is the same length as the XML, all the
|
|
# nodes should have been parsed and added:
|
|
self.assertGreater(len(node.childNodes), 0)
|
|
|
|
def testSAX2DOM(self):
|
|
"""Ensure SAX2DOM expands nodes as expected."""
|
|
sax2dom = pulldom.SAX2DOM()
|
|
sax2dom.startDocument()
|
|
sax2dom.startElement("doc", {})
|
|
sax2dom.characters("text")
|
|
sax2dom.startElement("subelm", {})
|
|
sax2dom.characters("text")
|
|
sax2dom.endElement("subelm")
|
|
sax2dom.characters("text")
|
|
sax2dom.endElement("doc")
|
|
sax2dom.endDocument()
|
|
|
|
doc = sax2dom.document
|
|
root = doc.documentElement
|
|
(text1, elm1, text2) = root.childNodes
|
|
text3 = elm1.childNodes[0]
|
|
|
|
self.assertIsNone(text1.previousSibling)
|
|
self.assertIs(text1.nextSibling, elm1)
|
|
self.assertIs(elm1.previousSibling, text1)
|
|
self.assertIs(elm1.nextSibling, text2)
|
|
self.assertIs(text2.previousSibling, elm1)
|
|
self.assertIsNone(text2.nextSibling)
|
|
self.assertIsNone(text3.previousSibling)
|
|
self.assertIsNone(text3.nextSibling)
|
|
|
|
self.assertIs(root.parentNode, doc)
|
|
self.assertIs(text1.parentNode, root)
|
|
self.assertIs(elm1.parentNode, root)
|
|
self.assertIs(text2.parentNode, root)
|
|
self.assertIs(text3.parentNode, elm1)
|
|
doc.unlink()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|