mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Support xml.sax pyexpat setup APIs (#8238)
* 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
This commit is contained in:
1 parent
d3f0729fdf
commit
5f9139a3f8
4 files changed
+81
-10
No files matched your search
Vendored
-1
@@ -24,7 +24,6 @@ SMALL_SAMPLE = """<?xml version="1.0"?>
|
||||
|
||||
class PullDOMTestCase(unittest.TestCase):
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; FileNotFoundError: [Errno 2] No such file or directory (os error 2): 'xmltestdata/test.xml' -> 'None'
|
||||
def test_parse(self):
|
||||
"""Minimal test of DOMEventStream.parse()"""
|
||||
|
||||
|
||||
Vendored
-8
@@ -190,7 +190,6 @@ class ParseTest(unittest.TestCase):
|
||||
with self.assertRaises(SAXException):
|
||||
self.check_parse(f)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_parse_path_object(self):
|
||||
make_xml_file(self.data, 'utf-8', None)
|
||||
self.check_parse(FakePath(TESTFN))
|
||||
@@ -1018,7 +1017,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
resolver.entities, [(None, 'unsupported://non-existing')]
|
||||
)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_expat_external_dtd_default(self):
|
||||
parser = create_parser()
|
||||
resolver = self.TestEntityRecorder()
|
||||
@@ -1084,7 +1082,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
def startElementNS(self, name, qname, attrs):
|
||||
self._attrs = attrs
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_expat_attrs_empty(self):
|
||||
parser = create_parser()
|
||||
gather = self.AttrGatherer()
|
||||
@@ -1095,7 +1092,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
|
||||
self.verify_empty_attrs(gather._attrs)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_expat_attrs_wattr(self):
|
||||
parser = create_parser()
|
||||
gather = self.AttrGatherer()
|
||||
@@ -1106,7 +1102,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
|
||||
self.verify_attrs_wattr(gather._attrs)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_expat_nsattrs_empty(self):
|
||||
parser = create_parser(1)
|
||||
gather = self.AttrGatherer()
|
||||
@@ -1300,7 +1295,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
|
||||
# ===== Locator support
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_expat_locator_noinfo(self):
|
||||
result = BytesIO()
|
||||
xmlgen = XMLGenerator(result)
|
||||
@@ -1315,7 +1309,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
self.assertEqual(parser.getPublicId(), None)
|
||||
self.assertEqual(parser.getLineNumber(), 1)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
def test_expat_locator_withinfo(self):
|
||||
result = BytesIO()
|
||||
xmlgen = XMLGenerator(result)
|
||||
@@ -1326,7 +1319,6 @@ class ExpatReaderTest(XmlTestBase):
|
||||
self.assertEqual(parser.getSystemId(), TEST_XMLFILE)
|
||||
self.assertEqual(parser.getPublicId(), None)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'xmlparser' object has no attribute 'SetParamEntityParsing'
|
||||
@requires_nonascii_filenames
|
||||
def test_expat_locator_withinfo_nonascii(self):
|
||||
fname = os_helper.TESTFN_UNICODE
|
||||
|
||||
@@ -43,7 +43,7 @@ mod _pyexpat {
|
||||
Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine,
|
||||
builtins::{PyBytesRef, PyException, PyModule, PyStr, PyStrRef, PyType, PyUtf8StrRef},
|
||||
extend_module,
|
||||
function::{ArgBytesLike, Either, IntoFuncArgs, OptionalArg},
|
||||
function::{ArgBytesLike, ArgPrimitiveIndex, Either, IntoFuncArgs, OptionalArg},
|
||||
types::Constructor,
|
||||
};
|
||||
use rustpython_common::lock::PyRwLock;
|
||||
@@ -70,6 +70,13 @@ mod _pyexpat {
|
||||
#[pyattr(name = "version_info")]
|
||||
pub(super) const VERSION_INFO: (u32, u32, u32) = (2, 7, 1);
|
||||
|
||||
#[pyattr]
|
||||
const XML_PARAM_ENTITY_PARSING_NEVER: i32 = 0;
|
||||
#[pyattr]
|
||||
const XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE: i32 = 1;
|
||||
#[pyattr]
|
||||
const XML_PARAM_ENTITY_PARSING_ALWAYS: i32 = 2;
|
||||
|
||||
#[pyattr]
|
||||
#[pyattr(name = "XMLParserType")]
|
||||
#[pyclass(name = "xmlparser", module = false, traverse)]
|
||||
@@ -77,6 +84,8 @@ mod _pyexpat {
|
||||
pub(super) struct PyExpatLikeXmlParser {
|
||||
#[pytraverse(skip)]
|
||||
namespace_separator: Option<String>,
|
||||
#[pytraverse(skip)]
|
||||
base: PyRwLock<Option<String>>,
|
||||
start_element: MutableObject,
|
||||
end_element: MutableObject,
|
||||
character_data: MutableObject,
|
||||
@@ -128,6 +137,7 @@ mod _pyexpat {
|
||||
let intern_dict = intern.unwrap_or_else(|| vm.ctx.new_dict().into());
|
||||
Self {
|
||||
namespace_separator,
|
||||
base: PyRwLock::new(None),
|
||||
start_element: MutableObject::new(vm.ctx.none()),
|
||||
end_element: MutableObject::new(vm.ctx.none()),
|
||||
character_data: MutableObject::new(vm.ctx.none()),
|
||||
@@ -297,6 +307,29 @@ mod _pyexpat {
|
||||
.whitespace_to_characters(true)
|
||||
}
|
||||
|
||||
#[pymethod(name = "SetParamEntityParsing")]
|
||||
fn set_param_entity_parsing(&self, _flag: ArgPrimitiveIndex<i32>) -> i32 {
|
||||
// Compatibility shim: xml.sax requires this setup API, but xml-rs
|
||||
// does not expose Expat parameter entity parsing configuration.
|
||||
1
|
||||
}
|
||||
|
||||
#[pymethod(name = "SetBase")]
|
||||
fn set_base(&self, base: PyStrRef) {
|
||||
// Store-only compatibility state for xml.sax locator APIs. The
|
||||
// xml-rs backend still does not perform Expat-style base URI
|
||||
// resolution for external entities.
|
||||
*self.base.write() = Some(AsRef::<str>::as_ref(&base).to_owned());
|
||||
}
|
||||
|
||||
#[pymethod(name = "GetBase")]
|
||||
fn get_base(&self, vm: &VirtualMachine) -> PyObjectRef {
|
||||
self.base.read().as_ref().map_or_else(
|
||||
|| vm.ctx.none(),
|
||||
|base| vm.ctx.new_str(base.as_str()).into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Construct element name with namespace if separator is set
|
||||
fn make_name(&self, name: &xml::name::OwnedName) -> String {
|
||||
match (&self.namespace_separator, &name.namespace) {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import xml.sax
|
||||
from xml.parsers import expat
|
||||
|
||||
from testutils import assert_raises
|
||||
|
||||
assert expat.XML_PARAM_ENTITY_PARSING_NEVER == 0
|
||||
assert expat.XML_PARAM_ENTITY_PARSING_UNLESS_STANDALONE == 1
|
||||
assert expat.XML_PARAM_ENTITY_PARSING_ALWAYS == 2
|
||||
|
||||
parser = expat.ParserCreate()
|
||||
for value in (0, 1, 2, 3, -1, True):
|
||||
assert parser.SetParamEntityParsing(value) == 1
|
||||
|
||||
for value in ("x", None):
|
||||
with assert_raises(TypeError):
|
||||
parser.SetParamEntityParsing(value)
|
||||
|
||||
with assert_raises(OverflowError):
|
||||
parser.SetParamEntityParsing(2**100)
|
||||
|
||||
assert parser.GetBase() is None
|
||||
assert parser.SetBase("example.xml") is None
|
||||
assert parser.GetBase() == "example.xml"
|
||||
for value in (b"example.xml", None, 123):
|
||||
with assert_raises(TypeError):
|
||||
parser.SetBase(value)
|
||||
|
||||
|
||||
class Handler(xml.sax.handler.ContentHandler):
|
||||
def __init__(self):
|
||||
self.events = []
|
||||
|
||||
def startElement(self, name, attrs):
|
||||
self.events.append(("start", name))
|
||||
|
||||
def endElement(self, name):
|
||||
self.events.append(("end", name))
|
||||
|
||||
|
||||
handler = Handler()
|
||||
xml.sax.parseString("<main><child /></main>", handler)
|
||||
assert handler.events == [
|
||||
("start", "main"),
|
||||
("start", "child"),
|
||||
("end", "child"),
|
||||
("end", "main"),
|
||||
]
|
||||
Reference in new issue
Block a user