xml.dom is two questions, and neither one is worth doing yet
- Type: feature (shim) — Track B. Measured 2026-08-17 by frank3.
- Measured against:
pinnedv347 (f5da30bc9). - Recommendation: file and stop. The code is not the deliverable here; the measurement is, and it says don't.
The row, and what is actually behind it
missing module: xml_dom is row 3 of the corrected ladder table
(tools/nilpy_ladder.py), 4 files. Only three of them import xml.dom at
all — the fourth inherits the wall transitively:
| file | imports | needs |
|---|---|---|
html5lib/treewalkers/base.py |
from xml.dom import Node |
Node constants only |
html5lib/treewalkers/dom.py |
from xml.dom import Node |
Node constants only |
html5lib/treewalkers/etree.py |
nothing — inherits via from . import base |
— |
html5lib/treebuilders/dom.py |
from xml.dom import minidom, Node |
a real DOM |
Worth noting for anyone reading a ladder row as a work estimate: a row counts transitive victims. A grep of the corpus source finds three importers; the table says four. Both are right, and only the table answers "how many files does this unblock".
Question 1 — Node: tiny, closed, and complete
Read off CPython rather than off the call sites (a table built from call sites does not announce what it omitted):
>>> [x for x in dir(xml.dom.Node) if not x.startswith('_') and callable(...)]
[]
xml.dom.Node has twelve integer constants and zero methods. It is the DOM
spec's nodeType enumeration and nothing else, so a shim of it is not a
partial approximation — it is the whole class, fixed by a published spec, with
no version drift to track. About 20 lines.
The corpus uses it exactly as expected: Node.DOCUMENT_NODE, Node.TEXT_NODE,
Node.ELEMENT_NODE, Node.COMMENT_NODE, Node.DOCUMENT_TYPE_NODE,
Node.ENTITY_NODE, Node.CDATA_SECTION_NODE, Node.DOCUMENT_FRAGMENT_NODE —
8 of the 12, all as comparison operands.
Question 2 — minidom: a project, not a shim
treebuilders/dom.py builds and mutates a document. The surface it touches:
appendChild attributes childNodes cloneNode createComment createDocumentFragment
createElement createElementNS createTextNode firstChild hasAttributes
hasChildNodes insertBefore namespaceURI nodeName nodeType nodeValue normalize
ownerDocument publicId removeChild setAttribute setAttributeNS systemId
getDOMImplementation createDocument createDocumentType
...plus, decisively, a reach into a private minidom internal:
# html5lib/treebuilders/dom.py:170
# HACK: allow text nodes as children of the document node
if hasattr(self.dom, '_child_node_types'):
# pylint:disable=protected-access
if Node.TEXT_NODE not in self.dom._child_node_types:
self.dom._child_node_types = list(self.dom._child_node_types)
self.dom._child_node_types.append(Node.TEXT_NODE)
html5lib is patching minidom's private class-level list of permitted child
types. A shim would have to reproduce not just the DOM API but a specific
CPython implementation detail that the caller guards with hasattr — i.e. the
caller already knows it is reaching into someone's internals.
This is a real DOM implementation and should be ranked as one, not as a shim. It is the honest answer the dispatch asked for: these files want a DOM, and that is a project.
Why not even do question 1 — two measurements, either sufficient
It unblocks nothing
A 20-line Node-only shim was written to scratch and measured. All four files
clear the xml_dom wall, and land immediately on:
| file | next wall |
|---|---|
treewalkers/base.py |
undefined variable (digits) |
treewalkers/dom.py |
undefined variable (digits) |
treewalkers/etree.py |
undefined variable (digits) |
treebuilders/dom.py |
missing module: weakref |
So xml_dom is not a wall — it is in front of a wall. Three of the four files
collapse into row 1 (digits,
[[bug-n-assigning-to-a-name-that-collides-with-a-pascal-shim-attribute-fails]]),
which is already frank2's ground, and the fourth trades one missing module for
another. Net files unblocked by shimming xml.dom today: zero. Same
ranking discipline as
[[bug-n-a-unicode-identifier-is-rejected-by-the-lexer]] — rank on measured
unblock, not on how tractable the work looks.
And it would be silently wrong if written
This is the part that could not have been reasoned to. A class whose definition is not followed by a module-level statement currently loses its attribute initialisers and reads every one back as 0 ([[bug-n-the-last-class-in-a-module-reads-every-attribute-as-zero]], found while measuring this row). A constants-only shim is exactly that shape: one class, nothing after it.
Note this is not avoidable by writing the shim more carefully. Putting a
module-level statement after class Node would mask it — and masking it is
worse than hitting it, because the shim would then be correct only by virtue of
a trailing line nobody could explain, and would silently break the day someone
tidied it away.
So the obvious mimic_xml_dom.py — 12 constants, no module-level code — would
compile, import, and give Node.TEXT_NODE == 0, Node.ELEMENT_NODE == 0,
Node.DOCUMENT_NODE == 0. Every nodeType comparison in html5lib's treewalkers
would then be a comparison of 0 == 0: every node would take the first
branch, silently, and the walker would emit structurally wrong output with no
error anywhere.
That is exactly the "looks present and fails deep inside a caller" failure the work was scoped to avoid, arriving by a mechanism nobody predicted — and the only reason it was caught is that the measurement came before the code. A half-shim is not the only way to build something that looks present; a complete shim on a broken substrate does it too.
When to revisit
Do question 1 when
[[bug-n-the-last-class-in-a-module-reads-every-attribute-as-zero]] is fixed AND
the digits bug is fixed — at which point it is 20 lines and genuinely
complete, and there will be files behind it to unblock. Verify with an assertion
on an actual constant value (Node.TEXT_NODE == 3), never merely that the
import resolves, since resolving is precisely what it does while returning zero.
Question 2 should be re-filed as its own ranked item if a real DOM is ever wanted. Do not let it ride along with question 1 under one row.
Unblocked and reranked 15 -> 55 by the coordinator, 2026-08-18
Both blockers are now resolved, and the more important one was fixed today:
bug-n-the-last-class-in-a-module-reads-every-attribute-as-zero (12275b26f) and
bug-n-assigning-to-a-name-that-collides-with-a-pascal-shim-attribute-fails are both in
done/.
That first fix is why this ticket was correctly refused twice. A constants class is
exactly the shape that was broken — a module whose trailing class carries only
class-level attributes read every one of them back as zero, silently, with no
diagnostic. mimic_xml_dom is precisely that shape: xml.dom.Node is twelve integer
constants and no methods. Verified fixed here against CPython:
from nodemod import Node
print(Node.ELEMENT_NODE, Node.TEXT_NODE, Node.DOCUMENT_NODE)
pxx 1 3 9 CPython 1 3 9 (was 0 0 0)
Keep the campaign's scar attached to this ticket, because this is the file it was
written about: a ~20-line, spec-exact, COMPLETE mimic_xml_dom written before that fix
would have compiled, imported cleanly, and made every nodeType comparison 0 == 0, so
every node takes the first branch and the walker emits structurally wrong output with no
error anywhere. Completeness was never the protection — the substrate was. Re-run the
ladder after it lands and check the VALUES, not just that it imports.
Rerank rationale: p15 predates the corpus evidence. xml_dom is 4 files on the ladder
and xml_etree_elementtree a further 4, which puts it just under
[[feature-b-module-shims-for-the-html5lib-corpus]] (p60) rather than at the bottom of the
board. The refusal recorded in that ticket ("my own earlier refusal, still correct on
v347") was correct when written and is now spent.
Status 2026-08-18 (frank3-fc): question 2 split out, question 1 BLOCKED one
step short of landing
Measured against pinned v349 (596799fd9c6e, pin commit a6e8e763e) —
the pin carrying today's last-class-hoist and shim-class-visibility fixes.
Question 2 is settled and gone
Re-filed as [[feature-b-a-real-minidom-is-an-implementation-not-a-shim]] (B,
p15), exactly as this ticket instructed. Nothing about the answer changed: it
is ~25 DOM methods plus weakref.proxy plus a patch of minidom's private
_child_node_types, for one optional tree backend. An implementation, not a
shim, and it no longer rides along under this row.
Question 1 is written and correct, and cannot land
lib/rtl/mimic_xml_dom.py exists: twelve nodeType constants and the four
namespace URIs, checked programmatically against CPython's xml.dom rather
than typed from memory — zero mismatched, zero missing.
test/lib_mimic_xml_dom.npy is a differential asserting all twelve by VALUE
plus the distinctness and comparison shapes the treewalkers actually depend on;
it passes under CPython (20 checks).
It does not compile under pxx, for a reason that is not this ticket's:
[[bug-n-from-a-shim-import-a-class-loses-its-class-level-attributes]] (N, p75).
from <shim> import Class then Class.CONSTANT is undefined variable, while
the byte-identical file as a plain module answers correctly, the same shim by
its literal mimic_ filename answers correctly, and import shim; shim.Class.CONSTANT answers correctly.
The blocked spelling is the corpus's own. treewalkers/base.py and
dom.py do from xml.dom import Node and then compare Node.TEXT_NODE. So
there is no shim-side rewrite available and none should be attempted — the
qualified form works, but it is html5lib's source that would have to change,
and compiling existing source unchanged is the mission.
Why this ticket's own refusal held a third time
The refusal was always "do not write it on this substrate", and the substrate has now been wrong in three different ways in two days: the constants read zero, then the fix was not in the pin Track B ships against, and now the from-import binding drops the attributes. Each was found by measuring the VALUES rather than the import — which is the discipline this ticket exists to record.
The score for this ticket, stated the way the campaign asks
Past the wall: 0. Onto the next wall: 4. The three treewalker files moved
from missing module: xml_dom to undefined variable (DOCUMENT_NODE) — the
p75 blocker, where they now correctly attribute — and treebuilders/dom.py
moved to missing module: weakref. Compile count unchanged.
So this ticket bought attribution and a verified shim, not a compile. That
is the honest reading and it should not be dressed as anything else: a reader
of the board who sees the xml_dom row disappear must not conclude four files
were unblocked.
What remains, when the blocker lands
- Compile
test/lib_mimic_xml_dom.npyon a pin carrying the fix; the 20 checks must pass by value, not merely import. - Wire it into
make lib-testnext to the othermimic_*differentials. - Re-run
tools/nilpy_ladder.pyand report past-vs-onto. The fourxml_domfiles were previously measured to land ondigits(now fixed) andweakref; that measurement predates two pins and must be re-taken, not quoted.
LANDED 2026-08-18 (frank3-fc) — pinned v350 (66f59112e1a9, pin 46a6189a9)
The blocker
([[bug-n-from-a-shim-import-a-class-loses-its-class-level-attributes]]) was
fixed in 6cd63b836 and reached Track B in v350. Question 1 is in.
Verified by VALUE, which is the whole point of this ticket.
test/lib_mimic_xml_dom.npy reads all twelve nodeType constants back and diffs
them against the numbers CPython gives — 20 checks green, plus the distinctness
and comparison shapes html5lib's treewalkers actually branch on. Wired into
make lib-test (green, exit 0). A test asserting the import resolved would have
passed during both compiler bugs while the shim answered zero; this one would
not have.
The cause was in neither place we looked
ConsumeUnitQualifier was eating Node as a unit qualifier and then looking
TEXT_NODE up as a bare symbol — which is exactly why the diagnostic named the
ATTRIBUTE and read as a class-attribute fault to everyone. The guard that skips
that for a class looked the unit up by the name the SOURCE wrote, so for a shim
it searched the bare module name, found nothing, and stood down for every shim.
One identifier. Worth recording because the symptom description in the bug
ticket was accurate and still pointed at the wrong subsystem.
Score: past the wall 0, onto the next wall 3
| file | was (v349) | now (v350) |
|---|---|---|
treewalkers/base.py |
undefined variable (DOCUMENT_NODE) | undefined variable (yield) |
treewalkers/dom.py |
undefined variable (DOCUMENT_NODE) | undefined variable (yield) |
treewalkers/etree.py |
undefined variable (DOCUMENT_NODE) | undefined variable (yield) |
treebuilders/dom.py |
missing module: weakref | unchanged — question 2 |
Compile count 6/48 → 6/48. This ticket did not move the compile number and
should not be read as having done so: it moved three files off a shim wall onto
yield, which is now the top row at 14 files.
Attribution, since v350 carries more than this ticket:
html5lib/_tokenizer.py also moved (missing module: sys →
missing module: six_moves). That is
[[bug-n-from-sys-import-fails-while-import-sys-works]] being fixed — frank2's
work, not this ticket's. Nothing else changed.
What remains under this row
Question 2 only, split out as [[feature-b-a-real-minidom-is-an-implementation-not-a-shim]]
(B, p15), and treebuilders/dom.py still sits on missing module: weakref
exactly where it was measured.
Log
- 2026-08-18 — resolved, commit ac136d7aa.