SF.net SVN: docutils:[9737 ] trunk/docutils

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9737
          http://sourceforge.net/p/docutils/code/9737
Author:   milde
Date:     2024-06-06 14:02:08 +0000 (Thu, 06 Jun 2024)
Log Message:
-----------
xml-parser: test and fix handling of invalid input

Gracefully handle invalid attributes and text inserts:
Just generate (invalid) nodes without complaining.

With, e.g., `docutils --parser=xml myfile.xml`, the document tree
is validated by the `universal.Validate` transform by default.

Nodes returned from `parse_elememt()` can be easily validated
via their `validate()` method.

+ Allows parsing XML documents with "extended" document model
  for special applications.
+ Allows for cleanup operations between parsing and validation.
+ Simpler implementation (no duplicating of the reporting framework
  of the validator).

Modified Paths:
--------------
    trunk/docutils/docutils/parsers/docutils_xml.py
    trunk/docutils/test/test_parsers/test_docutils_xml/test_parse.py
    trunk/docutils/test/test_parsers/test_docutils_xml/test_parse_element.py

Modified: trunk/docutils/docutils/parsers/docutils_xml.py
===================================================================
--- trunk/docutils/docutils/parsers/docutils_xml.py	2024-06-06 14:01:54 UTC (rev 9736)
+++ trunk/docutils/docutils/parsers/docutils_xml.py	2024-06-06 14:02:08 UTC (rev 9737)
@@ -52,6 +52,11 @@
         self.finish_parse()
 
 
+class Unknown(nodes.Special, nodes.Inline, nodes.Element):
+    """An unknown element found by the XML parser."""
+    content_model = (((nodes.Element, nodes.Text), '*'),)  # no restrictions
+
+
 def parse_element(inputstring, document=None):
     """
     Parse `inputstring` as "Docutils XML", return `nodes.Element` instance.
@@ -76,11 +81,19 @@
     root = None
     parser = ET.XMLPullParser(events=('start',))
     for i, line in enumerate(inputstring.splitlines(keepends=True)):
-        parser.feed(line)
-        for event, element in parser.read_events():
-            if root is None:
-                root = element
-            element.attrib['source line'] = str(i+1)
+        try:
+            parser.feed(line)
+            for event, element in parser.read_events():
+                if root is None:
+                    root = element
+                element.attrib['source line'] = str(i+1)
+        except ET.ParseError as e:
+            if document is None:
+                raise
+            document.reporter.error(f'XML parse error: {e}.',
+                                    source=document.settings._source,
+                                    line=e.position[0])
+            break
     return element2node(root, document)
 
 
@@ -95,21 +108,38 @@
     if document is None:
         document = utils.new_document('xml input',
                                       frontend.get_default_settings(Parser))
+        document.source == 'xml input'
 
     # Get the corresponding `nodes.Element` instance:
-    nodeclass = getattr(nodes, element.tag)
+    try:
+        nodeclass = getattr(nodes, element.tag)
+        if not issubclass(nodeclass, nodes.Element):
+            nodeclass = Unknown
+    except AttributeError:
+        nodeclass = Unknown
     if nodeclass == nodes.document:
         node = document
+        document.source = document.source or document.settings._source
     else:
         node = nodeclass()
 
     node.line = int(element.get('source line'))
+    if isinstance(node, Unknown):
+        node.tagname = element.tag
+        document.reporter.warning(
+            f'Unknown element type <{element.tag}>.',
+            base_node=node)
 
     # Attributes: convert and add to `node.attributes`.
     for key, value in element.items():
         if key.startswith('{') or key == 'source line':
             continue  # skip duplicate attributes with namespace URL
-        node.attributes[key] = nodes.ATTRIBUTE_VALIDATORS[key](value)
+        try:
+            node.attributes[key] = nodes.ATTRIBUTE_VALIDATORS[key](value)
+        except (ValueError, KeyError):
+            if key in node.list_attributes:
+                value = value.split()
+            node.attributes[key] = value  # node becomes invalid!
 
     # Append text (wrapped in a `nodes.Text` instance)
     append_text(node, element.text)

Modified: trunk/docutils/test/test_parsers/test_docutils_xml/test_parse.py
===================================================================
--- trunk/docutils/test/test_parsers/test_docutils_xml/test_parse.py	2024-06-06 14:01:54 UTC (rev 9736)
+++ trunk/docutils/test/test_parsers/test_docutils_xml/test_parse.py	2024-06-06 14:02:08 UTC (rev 9737)
@@ -131,6 +131,63 @@
 """],
 ]
 
+totest['invalid'] = [
+["""\
+<document>
+    <tip>
+        spurious text
+        <paragraph>A paragraph.</paragraph>
+    </tip>
+</document>
+""",
+"""\
+<document source="test data">
+    <tip>
+        spurious text
+        <paragraph>
+            A paragraph.
+"""],
+["""\
+<document>
+    spurious text
+    <paragraph>A paragraph.</paragraph>
+</document>
+""",
+"""\
+<document source="test data">
+    spurious text
+    <paragraph>
+        A paragraph.
+"""],
+["""\
+<document>
+    <tip>
+        <paragraph>A paragraph.</paragraph>
+        spurious tailing text
+    </tip>
+</document>
+""",
+"""\
+<document source="test data">
+    <tip>
+        <paragraph>
+            A paragraph.
+        spurious tailing text
+"""],
+["""\
+<document>
+    <paragraph>A paragraph.</paragraph>
+    spurious tailing text
+</document>
+""",
+"""\
+<document source="test data">
+    <paragraph>
+        A paragraph.
+    spurious tailing text
+"""],
+]
 
+
 if __name__ == '__main__':
     unittest.main()

Modified: trunk/docutils/test/test_parsers/test_docutils_xml/test_parse_element.py
===================================================================
--- trunk/docutils/test/test_parsers/test_docutils_xml/test_parse_element.py	2024-06-06 14:01:54 UTC (rev 9736)
+++ trunk/docutils/test/test_parsers/test_docutils_xml/test_parse_element.py	2024-06-06 14:02:08 UTC (rev 9737)
@@ -21,23 +21,82 @@
     # so we import the local `docutils` package.
     sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
 
+from docutils import frontend, utils
 from docutils.parsers import docutils_xml
 
 
 class ParseElementTestCase(unittest.TestCase):
+    """Test the `docutils.xml.parse_element()` function."""
 
+    # supress warnings when passing `document` to `parse_element()`
+    settings = frontend.get_default_settings(docutils_xml.Parser)
+    settings.warning_stream = ''  # comment out to see warnings
+    document = utils.new_document('xml input', settings)
+
     def test_element_with_child_with_text(self):
         xml = '<tip><paragraph>some text</paragraph></tip>'
         node = docutils_xml.parse_element(xml)
         self.assertEqual(xml, str(node))
 
-    def test_tailing_text(self):
-        xml = '<strong>text</strong>trailing text'
-        with self.assertRaisesRegex(ET.ParseError,
-                                    'junk after document element'):
+    def test_tailing_text_after_root(self):
+        """etree.ElementTree does not accept tailing text in the input.
+        """
+        xml = '<strong>text</strong>tailing text'
+        with self.assertRaisesRegex(ET.ParseError, 'junk after document '):
             docutils_xml.parse_element(xml)
+        # If a document is provided, report via a "loose" error system message
+        # comment out ``settings.warning_stream = ''`` above to see it).
+        node = docutils_xml.parse_element(xml, self.document)
+        self.assertEqual('<strong>text</strong>', str(node))
 
+    def test_nonexistent_element_type(self):
+        xml = '<tip><p>some text</p></tip>'
+        node = docutils_xml.parse_element(xml, self.document)
+        self.assertEqual(xml, str(node))
+        # see test_misc.py for the warning
 
+    def test_junk_text(self):
+        # insert text also in nodes that are not TextElement instances
+        xml = '<tip>some text</tip>'
+        node = docutils_xml.parse_element(xml)
+        self.assertEqual(xml, str(node))
+        with self.assertRaisesRegex(ValueError,
+                                    'Expecting child of type <Body>,'
+                                    ' not text data "some text"'):
+            node.validate()
+
+    def test_tailing_junk_text(self):
+        # insert text also in nodes that are not TextElement instances
+        xml = '<tip><paragraph>some text</paragraph>tailing text</tip>'
+        node = docutils_xml.parse_element(xml)
+        self.assertEqual(xml, str(node))
+        with self.assertRaisesRegex(
+            ValueError, 'Spurious text: "tailing text"'):
+            node.validate()
+
+    def test_element_with_attributes(self):
+        xml = ('<image align="left" alt="a barking dog" height="3ex"'
+               ' loading="embed" scale="3" uri="dog.jpg" width="4cm"/>')
+        node = docutils_xml.parse_element(xml)
+        self.assertEqual(xml, str(node))
+
+    def test_element_with_invalid_attributes(self):
+        """Silently accept invalid attribute names and values.
+
+        Validation reports problems.
+        """
+        xml = ('<image breadth="3 cm" height="3 inch"/>')
+        node = docutils_xml.parse_element(xml)
+        self.assertEqual(xml, str(node))
+        with self.assertRaisesRegex(ValueError,
+                                    'Element <image breadth="3 cm".*invalid:\n'
+                                    '.*"breadth" not one of "ids",.*\n'
+                                    '.*"height" has invalid value "3 inch".\n'
+                                    '.*Valid units: em ex px in cm mm pt '
+                                    ):
+            node.validate()
+
+
 class XmlAttributesTestCase(unittest.TestCase):
     """
     Test correct parsing of the `supported element attributes`_.

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.