Skip to content

Commit 28ffe9d

Browse files
name-of-okjaclaude
andcommitted
Implement pyexpat ordered_attributes
When `ordered_attributes` is set, report attributes to the StartElementHandler as a flat `[name, value, ...]` list matching CPython's my_StartElementHandler, instead of always passing a dict. `xml.etree.ElementTree` and `xml.dom.minidom` (via expatbuilder) both enable this flag and rely on the flat-list contract, so parsing any element with attributes previously failed: the dict made `_start` do integer indexing, raising KeyError which was silently swallowed by the handler invoker, leaving the tree empty ("missing toplevel element"). Reference (CPython 3.14): https://github.com/python/cpython/blob/3.14/Modules/pyexpat.c#L439-L443 Remove `@unittest.expectedFailure` from tests that now pass (23 in test_xml_etree, 15 in test_minidom). AI assistance: implemented with Claude Code (claude-opus-4-8), which wrote the pyexpat change and the test-marker removals under my direction; I reviewed the full diff and verified the affected tests locally. Assisted-by: Claude:claude-opus-4-8 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1205fd2 commit 28ffe9d

3 files changed

Lines changed: 27 additions & 49 deletions

File tree

‎Lib/test/test_minidom.py‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,6 @@ def testAttributeRepr(self):
553553
self.assertEqual(str(node), repr(node))
554554
dom.unlink()
555555

556-
@unittest.expectedFailure # TODO: RUSTPYTHON
557556
def testWriteXML(self):
558557
str = '<?xml version="1.0" ?><a b="c"/>'
559558
dom = parseString(str)
@@ -601,7 +600,6 @@ def test_toxml_quote_attrib(self):
601600
'lflf="&#10;&#10;" '
602601
'ws="&#9;&#10;&#13; "/>')
603602

604-
@unittest.expectedFailure # TODO: RUSTPYTHON
605603
def testAltNewline(self):
606604
str = '<?xml version="1.0" ?>\n<a b="c"/>\n'
607605
dom = parseString(str)
@@ -705,7 +703,6 @@ def testAttrListKeys(self): pass
705703

706704
def testAttrListKeysNS(self): pass
707705

708-
@unittest.expectedFailure # TODO: RUSTPYTHON
709706
def testRemoveNamedItem(self):
710707
doc = parseString("<doc a=''/>")
711708
e = doc.documentElement
@@ -715,7 +712,6 @@ def testRemoveNamedItem(self):
715712
self.assertTrue(a1.isSameNode(a2))
716713
self.assertRaises(xml.dom.NotFoundErr, attrs.removeNamedItem, "a")
717714

718-
@unittest.expectedFailure # TODO: RUSTPYTHON
719715
def testRemoveNamedItemNS(self):
720716
doc = parseString("<doc xmlns:a='http://xml.python.org/' a:b=''/>")
721717
e = doc.documentElement
@@ -788,7 +784,6 @@ def _setupCloneElement(self, deep):
788784
root.setAttribute("added", "VALUE")
789785
return dom, clone
790786

791-
@unittest.expectedFailure # TODO: RUSTPYTHON
792787
def testCloneElementShallow(self):
793788
dom, clone = self._setupCloneElement(0)
794789
self.assertEqual(len(clone.childNodes), 0)
@@ -940,11 +935,9 @@ def check_clone_attribute(self, deep, testName):
940935
self.confirm(clone.specified,
941936
testName + ": cloned attribute must have specified == True")
942937

943-
@unittest.expectedFailure # TODO: RUSTPYTHON
944938
def testCloneAttributeShallow(self):
945939
self.check_clone_attribute(0, "testCloneAttributeShallow")
946940

947-
@unittest.expectedFailure # TODO: RUSTPYTHON
948941
def testCloneAttributeDeep(self):
949942
self.check_clone_attribute(1, "testCloneAttributeDeep")
950943

@@ -1336,7 +1329,6 @@ def checkRenameNodeSharedConstraints(self, doc, node):
13361329
self.assertRaises(xml.dom.WrongDocumentErr, doc2.renameNode, node,
13371330
xml.dom.EMPTY_NAMESPACE, "foo")
13381331

1339-
@unittest.expectedFailure # TODO: RUSTPYTHON
13401332
def testRenameAttribute(self):
13411333
doc = parseString("<doc a='v'/>")
13421334
elem = doc.documentElement
@@ -1541,7 +1533,6 @@ def setup():
15411533
self.confirm(text is None
15421534
and len(elem.childNodes) == 2)
15431535

1544-
@unittest.expectedFailure # TODO: RUSTPYTHON
15451536
def testSchemaType(self):
15461537
doc = parseString(
15471538
"<!DOCTYPE doc [\n"
@@ -1575,7 +1566,6 @@ def testSchemaType(self):
15751566
self.confirm(hasattr(t, "name")
15761567
and t.namespace == xml.dom.EMPTY_NAMESPACE)
15771568

1578-
@unittest.expectedFailure # TODO: RUSTPYTHON
15791569
def testSetIdAttribute(self):
15801570
doc = parseString("<doc a1='v' a2='w'/>")
15811571
e = doc.documentElement
@@ -1607,7 +1597,6 @@ def testSetIdAttribute(self):
16071597
self.confirm(e.isSameNode(doc.getElementById("w"))
16081598
and a2.isId)
16091599

1610-
@unittest.expectedFailure # TODO: RUSTPYTHON
16111600
def testSetIdAttributeNS(self):
16121601
NS1 = "http://xml.python.org/ns1"
16131602
NS2 = "http://xml.python.org/ns2"
@@ -1644,7 +1633,6 @@ def testSetIdAttributeNS(self):
16441633
self.confirm(e.isSameNode(doc.getElementById("w"))
16451634
and a2.isId)
16461635

1647-
@unittest.expectedFailure # TODO: RUSTPYTHON
16481636
def testSetIdAttributeNode(self):
16491637
NS1 = "http://xml.python.org/ns1"
16501638
NS2 = "http://xml.python.org/ns2"
@@ -1766,21 +1754,18 @@ def testProcessingInstructionNameError(self):
17661754
pi = doc.createProcessingInstruction("y", "z")
17671755
pi.nodeValue = "crash"
17681756

1769-
@unittest.expectedFailure # TODO: RUSTPYTHON
17701757
def test_minidom_attribute_order(self):
17711758
xml_str = '<?xml version="1.0" ?><curriculum status="public" company="example"/>'
17721759
doc = parseString(xml_str)
17731760
output = io.StringIO()
17741761
doc.writexml(output)
17751762
self.assertEqual(output.getvalue(), xml_str)
17761763

1777-
@unittest.expectedFailure # TODO: RUSTPYTHON
17781764
def test_toxml_with_attributes_ordered(self):
17791765
xml_str = '<?xml version="1.0" ?><curriculum status="public" company="example"/>'
17801766
doc = parseString(xml_str)
17811767
self.assertEqual(doc.toxml(), xml_str)
17821768

1783-
@unittest.expectedFailure # TODO: RUSTPYTHON
17841769
def test_toprettyxml_with_attributes_ordered(self):
17851770
xml_str = '<?xml version="1.0" ?><curriculum status="public" company="example"/>'
17861771
doc = parseString(xml_str)

‎Lib/test/test_xml_etree.py‎

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,6 @@ def test_cdata(self):
392392
self.serialize_check(ET.XML("<tag><![CDATA[hello]]></tag>"),
393393
'<tag>hello</tag>')
394394

395-
@unittest.expectedFailure # TODO: RUSTPYTHON
396395
def test_file_init(self):
397396
stringfile = io.BytesIO(SAMPLE_XML.encode("utf-8"))
398397
tree = ET.ElementTree(file=stringfile)
@@ -508,7 +507,6 @@ def test_makeelement(self):
508507
elem[:] = tuple([subelem])
509508
self.serialize_check(elem, '<tag><subtag key="value" /></tag>')
510509

511-
@unittest.expectedFailure # TODO: RUSTPYTHON
512510
def test_parsefile(self):
513511
# Test parsing from file.
514512

@@ -688,7 +686,6 @@ def test_initialize_parser_without_target(self):
688686
parser2 = ET.XMLParser()
689687
self.assertIsInstance(parser2.target, ET.TreeBuilder)
690688

691-
@unittest.expectedFailure # TODO: RUSTPYTHON
692689
def test_children(self):
693690
# Test Element children iteration
694691

@@ -1091,7 +1088,6 @@ def test_entity(self):
10911088
self.assertEqual(str(cm.exception),
10921089
'undefined entity &entity;: line 4, column 10')
10931090

1094-
@unittest.expectedFailure # TODO: RUSTPYTHON
10951091
def test_namespace(self):
10961092
# Test namespace issues.
10971093

@@ -1335,7 +1331,6 @@ def test_attlist_default(self):
13351331
class IterparseTest(unittest.TestCase):
13361332
# Test iterparse interface.
13371333

1338-
@unittest.expectedFailure # TODO: RUSTPYTHON
13391334
def test_basic(self):
13401335
iterparse = ET.iterparse
13411336

@@ -1361,7 +1356,6 @@ def test_basic(self):
13611356
])
13621357
it.close()
13631358

1364-
@unittest.expectedFailure # TODO: RUSTPYTHON
13651359
def test_external_file(self):
13661360
with open(SIMPLE_XMLFILE, 'rb') as source:
13671361
it = ET.iterparse(source)
@@ -1374,7 +1368,6 @@ def test_external_file(self):
13741368
])
13751369
self.assertEqual(it.root.tag, 'root')
13761370

1377-
@unittest.expectedFailure # TODO: RUSTPYTHON
13781371
def test_events(self):
13791372
iterparse = ET.iterparse
13801373

@@ -1475,7 +1468,6 @@ def test_nonexistent_file(self):
14751468
with self.assertRaises(FileNotFoundError):
14761469
ET.iterparse("nonexistent")
14771470

1478-
@unittest.expectedFailure # TODO: RUSTPYTHON
14791471
def test_resource_warnings_not_exhausted(self):
14801472
# Not exhausting the iterator still closes the underlying file (bpo-43292)
14811473
it = ET.iterparse(SIMPLE_XMLFILE)
@@ -1514,7 +1506,6 @@ def test_resource_warnings_exhausted(self):
15141506
del it
15151507
gc_collect()
15161508

1517-
@unittest.expectedFailure # TODO: RUSTPYTHON
15181509
def test_close_not_exhausted(self):
15191510
iterparse = ET.iterparse
15201511

@@ -2041,7 +2032,6 @@ def _my_loader(self, href, parse):
20412032
else:
20422033
return None
20432034

2044-
@unittest.expectedFailure # TODO: RUSTPYTHON
20452035
def test_xinclude_default(self):
20462036
from xml.etree import ElementInclude
20472037
doc = self.xinclude_loader('default.xml')
@@ -2056,7 +2046,6 @@ def test_xinclude_default(self):
20562046
'</root>\n'
20572047
'</document>')
20582048

2059-
@unittest.expectedFailure # TODO: RUSTPYTHON
20602049
def test_xinclude(self):
20612050
from xml.etree import ElementInclude
20622051

@@ -2121,15 +2110,13 @@ def test_xinclude(self):
21212110
' </ns0:include>\n'
21222111
'</div>') # C5
21232112

2124-
@unittest.expectedFailure # TODO: RUSTPYTHON
21252113
def test_xinclude_repeated(self):
21262114
from xml.etree import ElementInclude
21272115

21282116
document = self.xinclude_loader("include_c1_repeated.xml")
21292117
ElementInclude.include(document, self.xinclude_loader)
21302118
self.assertEqual(1+4*2, len(document.findall(".//p")))
21312119

2132-
@unittest.expectedFailure # TODO: RUSTPYTHON
21332120
def test_xinclude_failures(self):
21342121
from xml.etree import ElementInclude
21352122

@@ -2234,7 +2221,6 @@ def check(elem):
22342221
elem.set("123", 123)
22352222
check(elem) # attribute value
22362223

2237-
@unittest.expectedFailure # TODO: RUSTPYTHON
22382224
def test_bug_xmltoolkit25(self):
22392225
# typo in ElementTree.findtext
22402226

@@ -2258,7 +2244,6 @@ def test_bug_xmltoolkitX1(self):
22582244
ET.dump(tree)
22592245
self.assertEqual(stdout.getvalue(), '<doc><table><tbody /></table></doc>\n')
22602246

2261-
@unittest.expectedFailure # TODO: RUSTPYTHON
22622247
def test_bug_xmltoolkit39(self):
22632248
# non-ascii element and attribute names doesn't work
22642249

@@ -2345,7 +2330,6 @@ def xmltoolkit63():
23452330
xmltoolkit63()
23462331
self.assertEqual(sys.getrefcount(None), count)
23472332

2348-
@unittest.expectedFailure # TODO: RUSTPYTHON
23492333
def test_bug_200708_newline(self):
23502334
# Preserve newlines in attributes.
23512335

@@ -2461,7 +2445,6 @@ def test_issue6233(self):
24612445
b"<?xml version='1.0' encoding='ascii'?>\n"
24622446
b'<body>t&#227;g</body>')
24632447

2464-
@unittest.expectedFailure # TODO: RUSTPYTHON
24652448
def test_issue6565(self):
24662449
elem = ET.XML("<body><tag/></body>")
24672450
self.assertEqual(summarize_list(elem), ['tag'])
@@ -2533,7 +2516,6 @@ def check_expat224_utf8_bug(self, text):
25332516
root = ET.XML(xml)
25342517
self.assertEqual(root.get('b'), text.decode('utf-8'))
25352518

2536-
@unittest.expectedFailure # TODO: RUSTPYTHON
25372519
def test_expat224_utf8_bug(self):
25382520
# bpo-31170: Expat 2.2.3 had a bug in its UTF-8 decoder.
25392521
# Check that Expat 2.2.4 fixed the bug.
@@ -3355,7 +3337,6 @@ class MyElement(ET.Element):
33553337

33563338

33573339
class ElementFindTest(unittest.TestCase):
3358-
@unittest.expectedFailure # TODO: RUSTPYTHON
33593340
def test_find_simple(self):
33603341
e = ET.XML(SAMPLE_XML)
33613342
self.assertEqual(e.find('tag').tag, 'tag')
@@ -3379,7 +3360,6 @@ def test_find_simple(self):
33793360
# Issue #16922
33803361
self.assertEqual(ET.XML('<tag><empty /></tag>').findtext('empty'), '')
33813362

3382-
@unittest.expectedFailure # TODO: RUSTPYTHON
33833363
def test_find_xpath(self):
33843364
LINEAR_XML = '''
33853365
<body>
@@ -3402,7 +3382,6 @@ def test_find_xpath(self):
34023382
self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]')
34033383
self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]')
34043384

3405-
@unittest.expectedFailure # TODO: RUSTPYTHON
34063385
def test_findall(self):
34073386
e = ET.XML(SAMPLE_XML)
34083387
e[2] = ET.XML(SAMPLE_SECTION)
@@ -3591,7 +3570,6 @@ def test_bad_find(self):
35913570
with self.assertRaisesRegex(SyntaxError, 'cannot use absolute path'):
35923571
e.findall('/tag')
35933572

3594-
@unittest.expectedFailure # TODO: RUSTPYTHON
35953573
def test_find_through_ElementTree(self):
35963574
e = ET.XML(SAMPLE_XML)
35973575
self.assertEqual(ET.ElementTree(e).find('tag').tag, 'tag')
@@ -4576,7 +4554,6 @@ def test_correct_import_pyET(self):
45764554
# --------------------------------------------------------------------
45774555

45784556
class BoolTest(unittest.TestCase):
4579-
@unittest.expectedFailure # TODO: RUSTPYTHON
45804557
def test_warning(self):
45814558
e = ET.fromstring('<a style="new"></a>')
45824559
msg = (

‎crates/stdlib/src/pyexpat.rs‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -355,19 +355,35 @@ mod _pyexpat {
355355
XmlEvent::StartElement {
356356
name, attributes, ..
357357
} => {
358-
let dict = vm.ctx.new_dict();
359-
for attribute in attributes {
360-
let attr_name = self.make_name(&attribute.name);
361-
dict.set_item(
362-
attr_name.as_str(),
363-
vm.ctx.new_str(attribute.value).into(),
364-
vm,
365-
)
366-
.unwrap();
367-
}
358+
let ordered = self
359+
.ordered_attributes
360+
.read()
361+
.clone()
362+
.is_true(vm)
363+
.unwrap_or(false);
364+
// Build the container.
365+
let attrs: PyObjectRef = if ordered {
366+
let mut items = Vec::with_capacity(attributes.len() * 2);
367+
for attribute in attributes {
368+
items.push(vm.ctx.new_str(self.make_name(&attribute.name)).into());
369+
items.push(vm.ctx.new_str(attribute.value).into());
370+
}
371+
vm.ctx.new_list(items).into()
372+
} else {
373+
let dict = vm.ctx.new_dict();
374+
for attribute in attributes {
375+
dict.set_item(
376+
self.make_name(&attribute.name).as_str(),
377+
vm.ctx.new_str(attribute.value).into(),
378+
vm,
379+
)
380+
.unwrap();
381+
}
382+
dict.into()
383+
};
368384

369385
let name_str = PyStr::from(self.make_name(&name)).into_ref(&vm.ctx);
370-
invoke_handler(vm, &self.start_element, (name_str, dict));
386+
invoke_handler(vm, &self.start_element, (name_str, attrs));
371387
}
372388
XmlEvent::EndElement { name, .. } => {
373389
let name_str = PyStr::from(self.make_name(&name)).into_ref(&vm.ctx);

0 commit comments

Comments
 (0)