From 7e601db22ab648501f893da4320200440f2db606 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sat, 5 Sep 2026 17:12:38 +0800 Subject: [PATCH 1/3] Read Doxygen XML independently of the system text encoding --- .../mkdocstrings_handlers/cxx/__init__.py | 6 +- support/python/tests/test_cxx.py | 102 ++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 support/python/tests/test_cxx.py diff --git a/support/python/mkdocstrings_handlers/cxx/__init__.py b/support/python/mkdocstrings_handlers/cxx/__init__.py index a55e85b2622c..7d3b61eaa5d0 100644 --- a/support/python/mkdocstrings_handlers/cxx/__init__.py +++ b/support/python/mkdocstrings_handlers/cxx/__init__.py @@ -302,7 +302,7 @@ def __init__( self._file_doxyxml: "ET.ElementTree[ET.Element[str]] | None" = None for h in headers: filename = h.replace(".h", "_8h.xml") - with open(os.path.join(self._doxyxml_dir, filename)) as f: + with open(os.path.join(self._doxyxml_dir, filename), "rb") as f: doxyxml = ET.parse(f) if self._file_doxyxml is None: self._file_doxyxml = doxyxml @@ -317,7 +317,7 @@ def collect_compound(self, identifier: str, cls: "list[ET.Element]") -> Definiti if refid is None: raise ValueError path = os.path.join(self._doxyxml_dir, refid + ".xml") - with open(path) as f: + with open(path, "rb") as f: xml = ET.parse(f) node = xml.find("compounddef") if node is None: @@ -371,7 +371,7 @@ def collect(self, identifier: str, options: "Mapping[str, Any]") -> Definition: doxyxml = self._ns2doxyxml.get(namespace) if doxyxml is None: path = f"namespace{namespace.replace('::', '_1_1')}.xml" - with open(os.path.join(self._doxyxml_dir, path)) as f: + with open(os.path.join(self._doxyxml_dir, path), "rb") as f: doxyxml = ET.parse(f) self._ns2doxyxml[namespace] = doxyxml diff --git a/support/python/tests/test_cxx.py b/support/python/tests/test_cxx.py new file mode 100644 index 000000000000..be2b3e659dd9 --- /dev/null +++ b/support/python/tests/test_cxx.py @@ -0,0 +1,102 @@ +"""XML-loading regressions for the documentation handler. + +Run with the documentation dependencies installed: +python -m unittest discover -s support/python/tests +""" + +import builtins +import sys +import tempfile +import unittest +import xml.etree.ElementTree as ET +from pathlib import Path +from unittest.mock import Mock, patch + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from mkdocstrings_handlers import cxx + + +def locale_open(file, mode="r", **kwargs): + if "b" not in mode and "encoding" not in kwargs: + kwargs["encoding"] = "cp1252" + return builtins.open(file, mode, **kwargs) + + +class XmlEncodingTest(unittest.TestCase): + def setUp(self): + directory = tempfile.TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.xml_dir = self.root / "build" / "doxyxml" + self.xml_dir.mkdir(parents=True) + self.description = "\u5341\u4e8c" + self.handler = object.__new__(cxx.CxxHandler) + self.handler._doxyxml_dir = str(self.xml_dir) + self.handler._ns2doxyxml = {} + self.handler._file_doxyxml = None + opener = patch.object(cxx, "open", locale_open, create=True) + opener.start() + self.addCleanup(opener.stop) + + def write_xml(self, filename, body): + xml = '' + xml += body + "" + (self.xml_dir / filename).write_bytes(xml.encode("utf-8")) + + def compound(self): + return ( + '' + + self.description + + "" + ) + + def test_header_xml(self): + headers = ( + "args", + "base", + "chrono", + "color", + "compile", + "enum", + "format", + "os", + "ostream", + "printf", + "ranges", + "std", + "xchar", + ) + for header in headers: + self.write_xml(header + "_8h.xml", self.compound()) + module_path = self.root / "support/python/mkdocstrings_handlers/cxx/__init__.py" + process = Mock(returncode=0) + process.communicate.return_value = (b"", None) + with ( + patch.object(cxx, "__file__", str(module_path)), + patch.object(cxx.BaseHandler, "__init__", return_value=None), + patch.object(cxx, "Popen", return_value=process), + ): + handler = cxx.CxxHandler({}, self.root) + paragraphs = handler._file_doxyxml.findall("compounddef/briefdescription/para") + self.assertEqual([p.text for p in paragraphs], [self.description] * len(headers)) + + def test_compound_xml(self): + self.write_xml("struct_example.xml", self.compound()) + reference = ET.Element("innerclass", refid="struct_example") + result = self.handler.collect_compound("example", [reference]) + self.assertEqual(result.desc[0].text, self.description) + + def test_namespace_xml(self): + self.write_xml( + "namespacefmt.xml", + '' + "int\nexample" + + self.description + + "", + ) + result = self.handler.collect("example", {}) + self.assertEqual(result.desc[0].text, self.description) + + +if __name__ == "__main__": + unittest.main() From 3fdcb234efd62c00464135ebe4ab781e46698fa5 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Sat, 5 Sep 2026 17:13:05 +0800 Subject: [PATCH 2/3] Format the XML regression assertion --- support/python/tests/test_cxx.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/support/python/tests/test_cxx.py b/support/python/tests/test_cxx.py index be2b3e659dd9..3450b87ef611 100644 --- a/support/python/tests/test_cxx.py +++ b/support/python/tests/test_cxx.py @@ -78,7 +78,9 @@ def test_header_xml(self): ): handler = cxx.CxxHandler({}, self.root) paragraphs = handler._file_doxyxml.findall("compounddef/briefdescription/para") - self.assertEqual([p.text for p in paragraphs], [self.description] * len(headers)) + self.assertEqual( + [p.text for p in paragraphs], [self.description] * len(headers) + ) def test_compound_xml(self): self.write_xml("struct_example.xml", self.compound()) From 83fc345f9d1e8a436211b3cbaa2ffa2352fa6e33 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Tue, 8 Sep 2026 21:10:07 +0800 Subject: [PATCH 3/3] Read Doxygen XML paths directly --- .../mkdocstrings_handlers/cxx/__init__.py | 91 ++++++++------- support/python/tests/test_cxx.py | 104 ------------------ 2 files changed, 44 insertions(+), 151 deletions(-) delete mode 100644 support/python/tests/test_cxx.py diff --git a/support/python/mkdocstrings_handlers/cxx/__init__.py b/support/python/mkdocstrings_handlers/cxx/__init__.py index 7d3b61eaa5d0..0d53903c9a20 100644 --- a/support/python/mkdocstrings_handlers/cxx/__init__.py +++ b/support/python/mkdocstrings_handlers/cxx/__init__.py @@ -302,14 +302,13 @@ def __init__( self._file_doxyxml: "ET.ElementTree[ET.Element[str]] | None" = None for h in headers: filename = h.replace(".h", "_8h.xml") - with open(os.path.join(self._doxyxml_dir, filename), "rb") as f: - doxyxml = ET.parse(f) - if self._file_doxyxml is None: - self._file_doxyxml = doxyxml - continue - root = self._file_doxyxml.getroot() - for node in doxyxml.getroot(): - root.append(node) + doxyxml = ET.parse(os.path.join(self._doxyxml_dir, filename)) + if self._file_doxyxml is None: + self._file_doxyxml = doxyxml + continue + root = self._file_doxyxml.getroot() + for node in doxyxml.getroot(): + root.append(node) def collect_compound(self, identifier: str, cls: "list[ET.Element]") -> Definition: """Collect a compound definition such as a struct.""" @@ -317,43 +316,42 @@ def collect_compound(self, identifier: str, cls: "list[ET.Element]") -> Definiti if refid is None: raise ValueError path = os.path.join(self._doxyxml_dir, refid + ".xml") - with open(path, "rb") as f: - xml = ET.parse(f) - node = xml.find("compounddef") - if node is None: + xml = ET.parse(path) + node = xml.find("compounddef") + if node is None: + raise ValueError + d = Definition(identifier, node=node) + d.template_params = convert_template_params(node) + d.desc = get_description(node) + d.members = [] + for m in node.findall( + 'sectiondef[@kind="public-attrib"]/memberdef' + ) + node.findall('sectiondef[@kind="public-func"]/memberdef'): + name = m.find("name") + if name is None or name.text is None: raise ValueError - d = Definition(identifier, node=node) - d.template_params = convert_template_params(node) - d.desc = get_description(node) - d.members = [] - for m in node.findall( - 'sectiondef[@kind="public-attrib"]/memberdef' - ) + node.findall('sectiondef[@kind="public-func"]/memberdef'): - name = m.find("name") - if name is None or name.text is None: - raise ValueError - name = name.text - # Doxygen incorrectly classifies members of private unnamed unions as - # public members of the containing class. - if name.endswith("_"): - continue - desc = get_description(m) - if len(desc) == 0: - continue - kind = m.get("kind") - member = Definition(name if name else "", kind=kind, is_member=True) - type_ = m.find("type") - if type_ is None: - raise ValueError - type_text = type_.text - member.type = type_text if type_text else "" - if kind == "function": - member.params = convert_params(m) - convert_return_type(member, m) - member.template_params = None - member.desc = desc - d.members.append(member) - return d + name = name.text + # Doxygen incorrectly classifies members of private unnamed unions as + # public members of the containing class. + if name.endswith("_"): + continue + desc = get_description(m) + if len(desc) == 0: + continue + kind = m.get("kind") + member = Definition(name if name else "", kind=kind, is_member=True) + type_ = m.find("type") + if type_ is None: + raise ValueError + type_text = type_.text + member.type = type_text if type_text else "" + if kind == "function": + member.params = convert_params(m) + convert_return_type(member, m) + member.template_params = None + member.desc = desc + d.members.append(member) + return d @override def collect(self, identifier: str, options: "Mapping[str, Any]") -> Definition: @@ -371,9 +369,8 @@ def collect(self, identifier: str, options: "Mapping[str, Any]") -> Definition: doxyxml = self._ns2doxyxml.get(namespace) if doxyxml is None: path = f"namespace{namespace.replace('::', '_1_1')}.xml" - with open(os.path.join(self._doxyxml_dir, path), "rb") as f: - doxyxml = ET.parse(f) - self._ns2doxyxml[namespace] = doxyxml + doxyxml = ET.parse(os.path.join(self._doxyxml_dir, path)) + self._ns2doxyxml[namespace] = doxyxml nodes = doxyxml.findall(f"compounddef/sectiondef/memberdef/name[.='{name}']/..") if len(nodes) == 0: diff --git a/support/python/tests/test_cxx.py b/support/python/tests/test_cxx.py deleted file mode 100644 index 3450b87ef611..000000000000 --- a/support/python/tests/test_cxx.py +++ /dev/null @@ -1,104 +0,0 @@ -"""XML-loading regressions for the documentation handler. - -Run with the documentation dependencies installed: -python -m unittest discover -s support/python/tests -""" - -import builtins -import sys -import tempfile -import unittest -import xml.etree.ElementTree as ET -from pathlib import Path -from unittest.mock import Mock, patch - -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from mkdocstrings_handlers import cxx - - -def locale_open(file, mode="r", **kwargs): - if "b" not in mode and "encoding" not in kwargs: - kwargs["encoding"] = "cp1252" - return builtins.open(file, mode, **kwargs) - - -class XmlEncodingTest(unittest.TestCase): - def setUp(self): - directory = tempfile.TemporaryDirectory() - self.addCleanup(directory.cleanup) - self.root = Path(directory.name) - self.xml_dir = self.root / "build" / "doxyxml" - self.xml_dir.mkdir(parents=True) - self.description = "\u5341\u4e8c" - self.handler = object.__new__(cxx.CxxHandler) - self.handler._doxyxml_dir = str(self.xml_dir) - self.handler._ns2doxyxml = {} - self.handler._file_doxyxml = None - opener = patch.object(cxx, "open", locale_open, create=True) - opener.start() - self.addCleanup(opener.stop) - - def write_xml(self, filename, body): - xml = '' - xml += body + "" - (self.xml_dir / filename).write_bytes(xml.encode("utf-8")) - - def compound(self): - return ( - '' - + self.description - + "" - ) - - def test_header_xml(self): - headers = ( - "args", - "base", - "chrono", - "color", - "compile", - "enum", - "format", - "os", - "ostream", - "printf", - "ranges", - "std", - "xchar", - ) - for header in headers: - self.write_xml(header + "_8h.xml", self.compound()) - module_path = self.root / "support/python/mkdocstrings_handlers/cxx/__init__.py" - process = Mock(returncode=0) - process.communicate.return_value = (b"", None) - with ( - patch.object(cxx, "__file__", str(module_path)), - patch.object(cxx.BaseHandler, "__init__", return_value=None), - patch.object(cxx, "Popen", return_value=process), - ): - handler = cxx.CxxHandler({}, self.root) - paragraphs = handler._file_doxyxml.findall("compounddef/briefdescription/para") - self.assertEqual( - [p.text for p in paragraphs], [self.description] * len(headers) - ) - - def test_compound_xml(self): - self.write_xml("struct_example.xml", self.compound()) - reference = ET.Element("innerclass", refid="struct_example") - result = self.handler.collect_compound("example", [reference]) - self.assertEqual(result.desc[0].text, self.description) - - def test_namespace_xml(self): - self.write_xml( - "namespacefmt.xml", - '' - "int\nexample" - + self.description - + "", - ) - result = self.handler.collect("example", {}) - self.assertEqual(result.desc[0].text, self.description) - - -if __name__ == "__main__": - unittest.main()