https://github.com/python/cpython/commit/f802d2d246ba9b8ed53aa6e760faad68fd490710
commit: f802d2d246ba9b8ed53aa6e760faad68fd490710
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-13T09:05:20Z
summary:

gh-157406: Report all document type declarations in the Python XMLParser 
(GH-157408)

The Python implementation of XMLParser only reported a document type
declaration with an external identifier.  Use the Expat handler, like
the C implementation does.

files:
A Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst
M Lib/test/test_xml_etree.py
M Lib/xml/etree/ElementTree.py

diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index f9ff8c4c3541eda..90e556ec95308bc 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -4211,6 +4211,21 @@ def close(self):
             ('html', '-//W3C//DTD XHTML 1.0 Transitional//EN',
              'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'))
 
+        for doctype, expected in [
+            ('<!DOCTYPE html>', ('html', None, None)),
+            ('<!DOCTYPE html [<!ENTITY e "v">]>', ('html', None, None)),
+            ('<!DOCTYPE html SYSTEM "a.dtd">', ('html', None, 'a.dtd')),
+            ('<!DOCTYPE html SYSTEM "a.dtd" [<!ENTITY e "v">]>',
+             ('html', None, 'a.dtd')),
+            ('<!DOCTYPE html PUBLIC "-//P" "a.dtd">', ('html', '-//P', 
'a.dtd')),
+            ("<!DOCTYPE\nhtml\nPUBLIC\n'-//P'\n'a.dtd'\n>",
+             ('html', '-//P', 'a.dtd')),
+        ]:
+            with self.subTest(doctype=doctype):
+                parser = ET.XMLParser(target=DoctypeParser())
+                parser.feed(doctype + '<html/>')
+                self.assertEqual(parser.close(), expected)
+
     def test_builder_lookup_errors(self):
         class RaisingBuilder:
             def __init__(self, raise_in=None, what=ValueError):
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index bed8c27df5a3845..ce98e4dc24a0d3a 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -1591,10 +1591,10 @@ def __init__(self, *, target=None, encoding=None):
             parser.CommentHandler = target.comment
         if hasattr(target, 'pi'):
             parser.ProcessingInstructionHandler = target.pi
+        parser.StartDoctypeDeclHandler = self._start_doctype
         # Configure pyexpat: buffering, new-style attribute handling.
         parser.buffer_text = 1
         parser.ordered_attributes = 1
-        self._doctype = None
         self.entity = {}
         try:
             self.version = "Expat %d.%d.%d" % expat.version_info
@@ -1713,38 +1713,15 @@ def _default(self, text):
                 err.lineno = self.parser.ErrorLineNumber
                 err.offset = self.parser.ErrorColumnNumber
                 raise err
-        elif prefix == "<" and text[:9] == "<!DOCTYPE":
-            self._doctype = [] # inside a doctype declaration
-        elif self._doctype is not None:
-            # parse doctype contents
-            if prefix == ">":
-                self._doctype = None
-                return
-            text = text.strip(_XML_WHITESPACE)
-            if not text:
-                return
-            self._doctype.append(text)
-            n = len(self._doctype)
-            if n > 2:
-                type = self._doctype[1]
-                if type == "PUBLIC" and n == 4:
-                    name, type, pubid, system = self._doctype
-                    if pubid:
-                        pubid = pubid[1:-1]
-                elif type == "SYSTEM" and n == 3:
-                    name, type, system = self._doctype
-                    pubid = None
-                else:
-                    return
-                if hasattr(self.target, "doctype"):
-                    self.target.doctype(name, pubid, system[1:-1])
-                elif hasattr(self, "doctype"):
-                    warnings.warn(
-                        "The doctype() method of XMLParser is ignored.  "
-                        "Define doctype() method on the TreeBuilder target.",
-                        RuntimeWarning)
-
-                self._doctype = None
+
+    def _start_doctype(self, name, system, pubid, has_internal_subset):
+        if hasattr(self.target, "doctype"):
+            self.target.doctype(name, pubid, system)
+        elif hasattr(self, "doctype"):
+            warnings.warn(
+                "The doctype() method of XMLParser is ignored.  "
+                "Define doctype() method on the TreeBuilder target.",
+                RuntimeWarning)
 
     def feed(self, data):
         """Feed encoded data to parser."""
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst 
b/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst
new file mode 100644
index 000000000000000..53f2bf561eefbb7
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-13-10-30-00.gh-issue-157406.a3kZq7.rst
@@ -0,0 +1,4 @@
+Fix the Python implementation of :class:`xml.etree.ElementTree.XMLParser`:
+the ``doctype()`` method of the target is now called for a document type
+declaration without an external identifier, like ``<!DOCTYPE html>``,
+as in the C implementation.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to