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

milde--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9531
          http://sourceforge.net/p/docutils/code/9531
Author:   milde
Date:     2024-02-01 13:03:12 +0000 (Thu, 01 Feb 2024)
Log Message:
-----------
Unit tests for MathML elements.

Replace the comments with doctest-tests with a new test module.

Modified Paths:
--------------
    trunk/docutils/docutils/utils/math/mathml_elements.py

Added Paths:
-----------
    trunk/docutils/test/test_utils/test_math/test_mathml_elements.py

Modified: trunk/docutils/docutils/utils/math/mathml_elements.py
===================================================================
--- trunk/docutils/docutils/utils/math/mathml_elements.py	2024-02-01 13:03:02 UTC (rev 9530)
+++ trunk/docutils/docutils/utils/math/mathml_elements.py	2024-02-01 13:03:12 UTC (rev 9531)
@@ -114,9 +114,9 @@
             args = ', '.join(f'{child}' for child in self)
         return f'{self.__class__.__name__}({args})'
 
-# Emulate dictionary access methods for attributes
-# and list-like interface to the child elements
-# (differs from `docutils.nodes.Element` dict/list interface).
+    # Emulate dictionary access methods for attributes
+    # and list-like interface to the child elements
+    # (differs from `docutils.nodes.Element` dict/list interface).
 
     def get(self, key, default=None):
         return self.attrib.get(key, default)
@@ -128,9 +128,9 @@
         return self.attrib.items()
 
     def iter(self):
-        """Return iterator over all subnodes, including nested ones."""
+        """Return iterator over self and all subnodes, including nested."""
+        yield self
         for child in self.children:
-            yield child
             yield from child.iter()
 
     def __len__(self):
@@ -139,9 +139,15 @@
     def __getitem__(self, key):
         return self.children.__getitem__(key)
 
-    def __setitem__(self, key, element):
-        element.parent = self
-        self.children.__setitem__(key, element)
+    def __setitem__(self, key, value):
+        if self.nchildren == 0:
+            raise TypeError(f'Element "{self}" does not take children.')
+        if isinstance(value, MathElement):
+            value.parent = self
+        else:  # value may be an iterable
+            for e in value:
+                e.parent = self
+        self.children.__setitem__(key, value)
 
     def __delitem__(self, key):
         self.children.__delitem__(key)
@@ -233,43 +239,11 @@
             xml.extend(['\n', '  ' * level])
         return xml
 
-# >>> n2 = math(mn(2))
-# >>> n2
-# math(mn('2'))
-# >>> n2.toprettyxml()
-# '<math>\n  <mn>2</mn>\n</math>'
-# >>> len(n2)
-# 1
-# >>> n2.extend([mo('!')])
-# math(mn('2'), mo('!'))
-# >>> eq3 = math(id='eq3', display='block')
-# >>> eq3
-# math(id='eq3', display='block')
-# >>> eq3.toprettyxml()
-# '<math id="eq3" display="block"></math>'
-# >>> len(eq3)
-# 0
-# >>> math(CLASS='bold').xml_starttag()
-# '<math class="bold">'
-# >>> n2.in_block()
-# False
-# >>> node = n2.append(mrow())
-# >>> node.in_block()
-# False
-# >>> eq3.in_block()
-# True
-# >>> node = eq3.append(mrow())
-# >>> node.in_block()
-# True
-# >>> nested = math(math(math(CLASS='three'), CLASS='two'), CLASS='one')
-# >>> [node for node in nested.iter()]
-# [math(math(class='three'), class='two'), math(class='three')]
 
-
 # Group sub-expressions in a horizontal row
 #
 # The elements <msqrt>, <mstyle>, <merror>, <mpadded>, <mphantom>,
-# <menclose>, <mtd>, <mscarry>, and <math> treat their contents
+# <menclose>, <mtd>, [...], and <math> treat their contents
 # as a single inferred mrow formed from all their children.
 # (https://www.w3.org/TR/mathml4/#presm_inferredmrow)
 #
@@ -286,6 +260,7 @@
 
     The special attribute `switch` indicates that the last two child
     elements are in reversed order and must be switched before XML-export.
+    See `msub` for an example.
     """
     nchildren = 2
 
@@ -301,18 +276,7 @@
             self.switch = False
         return current_node
 
-# >>> MathSchema(switch=True, display=True)
-# MathSchema(switch=True, display='true')
-# >>> MathSchema(MathElement(), switch=True)
-# MathSchema(MathElement(), switch=True)
-# >>> MathSchema(MathElement(id='c1'), MathElement(id='c2'), switch=True)
-# MathSchema(MathElement(id='c2'), MathElement(id='c1'))
-# >>> MathSchema(mrow(), mrow(), mrow())  # doctest: +ELLIPSIS
-# Traceback (most recent call last):
-# ...
-# TypeError: Element "MathSchema(...)" takes only 2 children.
 
-
 # Token elements represent the smallest units of mathematical notation which
 # carry meaning.
 
@@ -351,36 +315,30 @@
 class mi(MathToken):
     """Identifier, such as a function name, variable or symbolic constant."""
 
-# >>> mi(mtext('out'))
-# Traceback (most recent call last):
-# ...
-# ValueError: MathToken element expects `str` or number, not "mtext('out')".
 
-
 class mn(MathToken):
     """Numeric literal.
 
+    >>> mn(3.41).toprettyxml()
+    '<mn>3.41</mn>'
+
     Normally a sequence of digits with a possible separator (a dot or a comma).
+    (Values with comma must be specified as `str`.)
     """
 
-# >>> mn(3.41)
-# mn('3.41')
 
-
 class mo(MathToken):
     """Operator, Fence, Separator, or Accent.
 
+    >>> mo('<').toprettyxml()
+    '<mo>&lt;</mo>'
+
     Besides operators in strict mathematical meaning, this element also
     includes "operators" like parentheses, separators like comma and
     semicolon, or "absolute value" bars.
     """
 
-# >>> mo('<')
-# mo('<')
-# >>> mo('<').toprettyxml()
-# '<mo>&lt;</mo>'
 
-
 class mspace(MathElement):
     """Blank space, whose size is set by its attributes.
 
@@ -391,14 +349,7 @@
     """
     nchildren = 0
 
-# >>> mspace(width='2em').toprettyxml()
-# '<mspace width="2em"></mspace>'
-# >>> mspace(mn(3))
-# Traceback (most recent call last):
-# ...
-# TypeError: Element "mspace()" does not take children.
 
-
 # General Layout Schemata
 # ~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -438,24 +389,7 @@
             self.transfer_attributes(child)
         return super().close()
 
-# Examples:
-#
-# >>> e = mrow(mn('3.5'), mo('<'), mi('x'))
-# >>> e
-# mrow(mn('3.5'), mo('<'), mi('x'))
-# >>> e.toprettyxml()
-# '<mrow>\n  <mn>3.5</mn>\n  <mo>&lt;</mo>\n  <mi>x</mi>\n</mrow>'
-# >>> mo('∫', movablelimits=True).toprettyxml()
-# '<mo movablelimits="true">∫</mo>'
-# >>> mtext('comments or annotations').text
-# 'comments or annotations'
 
-# >>> row = mrow(mi('i', CLASS='boldmath'), mathvariant='normal', CLASS='test')
-# >>> tree = math(row)
-# >>> row.close()
-# math(mi('i', class='boldmath test', mathvariant='normal'))
-
-
 class mfrac(MathSchema):
     """Fractions or fraction-like objects such as binomial coefficients."""
 
@@ -473,8 +407,8 @@
     """Style Change.
 
     In modern browsers, <mstyle> is equivalent to an <mrow> element.
-    However, <mstyle> may still be relevant for compatibility with MathML
-    implementations outside browsers.
+    However, <mstyle> may still be relevant for compatibility with
+    MathML implementations outside browsers.
     """
 
 
@@ -538,21 +472,6 @@
     """Attach accents or limits both under and over an expression."""
 
 
-# >>> munder(mi('lim'), mo('-'), accent='false')
-# munder(mi('lim'), mo('-'), accent='false')
-# >>> mu = munder(mo('-'), accent='false', switch=True)
-# >>> mu
-# munder(mo('-'), switch=True, accent='false')
-# >>> mu.append(mi('lim'))
-# >>> mu
-# munder(mi('lim'), mo('-'), accent='false')
-# >>> mu.append(mi('lim'))
-# Traceback (most recent call last):
-# TypeError: Element "munder(mi('lim'), mo('-'))" takes only 2 children.
-# >>> munder(mo('-'), mi('lim'), accent='false', switch=True).toprettyxml()
-# '<munder accent="false">\n  <mi>lim</mi>\n  <mo>-</mo>\n</munder>'
-
-
 # Tabular Math
 # ~~~~~~~~~~~~
 
@@ -566,7 +485,3 @@
 
 class mtd(MathRow):
     """Cell in a table or a matrix"""
-
-# >>> t = mtable(displaystyle=True)
-# >>> math(t).toprettyxml()
-# '<math>\n  <mtable displaystyle="true"></mtable>\n</math>'

Added: trunk/docutils/test/test_utils/test_math/test_mathml_elements.py
===================================================================
--- trunk/docutils/test/test_utils/test_math/test_mathml_elements.py	                        (rev 0)
+++ trunk/docutils/test/test_utils/test_math/test_mathml_elements.py	2024-02-01 13:03:12 UTC (rev 9531)
@@ -0,0 +1,375 @@
+#! /usr/bin/env python3
+# :Copyright: © 2024 Günter Milde.
+# :License: Released under the terms of the `2-Clause BSD license`_, in short:
+#
+#    Copying and distribution of this file, with or without modification,
+#    are permitted in any medium without royalty provided the copyright
+#    notice and this notice are preserved.
+#    This file is offered as-is, without any warranty.
+#
+# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
+
+
+"""
+Test module for utils/__init__.py.
+"""
+
+from pathlib import Path
+import sys
+import unittest
+
+if __name__ == '__main__':
+    # prepend the "docutils root" to the Python library path
+    # so we import the local `docutils` package.
+    sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+
+import docutils.utils.math.mathml_elements as mml
+
+TEST_ROOT = Path(__file__).parent  # ./test/ from the docutils root
+
+# The index of MathML elements
+# on https://developer.mozilla.org/en-US/docs/Web/MathML/Element
+#   Element     Nr. of children    Description                comment
+mathml_elements = """
+    <maction>         * Bound actions to sub-expressions      DEPRECATED
+    <annotation>      0 Data annotations                      NOT IMPLEMENTED
+    <annotation-xml>  * XML annotations                       NOT IMPLEMENTED
+    <math>            * Top-level element
+    <menclose>        1 Enclosed contents (non-standard)
+    <merror>          * Enclosed syntax error messages
+    <mfenced>         * Parentheses (non-standard)            DEPRECATED
+    <mfrac>           2 Fraction
+    <mi>              0 Identifier
+    <mmultiscripts>   * Prescripts and tensor indices         NOT IMPLEMENTED
+    <mn>              0 Number
+    <mo>              0 Operator (and similar)
+    <mover>           2 Overscript
+    <mpadded>         * Extra padding                         NOT IMPLEMENTED
+    <mphantom>        1 Invisible content reserving space
+    <mroot>           2 Radical with specified index
+    <mrow>            * Grouped sub-expressions
+    <ms>              0 String literal                        NOT IMPLEMENTED
+    <mspace>          0 Space
+    <msqrt>           1 Square root without an index
+    <mstyle>          * Style change (for compatibility)
+    <msub>            2 Subscript
+    <msup>            2 Superscrip
+    <msubsup>         3 Subscript-superscript pair
+    <mtable>          * Table or matrix
+    <mtd>             * Cell in a table or a matrix
+    <mtext>           0 Text
+    <mtr>             * Row in a table or a matrix
+    <munder>          2 Underscript
+    <munderover>      3 Underscript-overscript pair
+    <semantics>       * annotations (e.g. text source)       NOT IMPLEMENTED
+    """
+
+
+class MathElementTests(unittest.TestCase):
+    """Test the <MathElement> base class."""
+
+    prettyXML = """\
+<math id="root">
+  <math id="c1"></math>
+  <math id="c2">
+    <math id="cc1"></math>
+  </math>
+</math>"""
+
+    def test__init__(self):
+        """Instantiate a MathElement object and test its properties."""
+
+        # No arguments required, the class name is used as XML tag:
+        e1 = mml.MathElement()
+
+        # Positional arguments are stored as children,
+        # named arguments are stored as element attributes:
+        # * argument names are downcased,
+        # * attribute values must be `str`, `boolean`, or numerical
+        #   and are stored as `str` (cf. test_a_str() below).
+        e0 = mml.MathElement(e1, CLASS='root', scriptlevel=1)
+        # len() returns the number of children
+        self.assertEqual(len(e0), 1)
+        # Attributes can be accessed using get(), set(), items(), and keys().
+        self.assertEqual(e0.get('class'), 'root')
+        e0.set('scriptlevel', 2)
+        e0.set('displaystyle', True)
+        self.assertEqual(e0.attrib, {'class': 'root',
+                                     'scriptlevel': '2',
+                                     'displaystyle': 'true'})
+
+    def test_children(self):
+        # positional arguments are stored as children.
+        e4 = mml.MathElement(id='e4')
+        e3 = mml.MathElement(id='e3')
+        e2 = mml.MathElement(e3, e4, id='e2')
+        e1 = mml.MathElement(id='e1')
+        e0 = mml.MathElement(e1, e2, id='e0')
+        # xml.etree uses list syntax to access them
+        self.assertEqual(e0[0], e1)
+        self.assertEqual(e0[1][-1], e4)
+        self.assertEqual(e0[:], [e1, e2])  # list of e0's children
+        # The default iterator yields the immediate children
+        self.assertEqual(tuple(e0), (e1, e2))
+        # The `iter()` method returns `self` and all descendants
+        self.assertEqual(tuple(e0.iter()), (e0, e1, e2, e3, e4))
+
+        # more list-like functions are tested below...
+
+    def test_a_str(self):
+        # test the XML attribute normalisation
+        #
+        # Values are converted to `str`:
+        self.assertEqual(mml.MathElement.a_str(-2), '-2')
+        self.assertEqual(mml.MathElement.a_str(1), '1')
+        self.assertEqual(mml.MathElement.a_str(0), '0')
+        # Booleans are downcased:
+        self.assertEqual(mml.MathElement.a_str(True), 'true')
+        self.assertEqual(mml.MathElement.a_str(False), 'false')
+        # Strings are left as-is:
+        self.assertEqual(mml.MathElement.a_str('+1'), '+1')
+        self.assertEqual(mml.MathElement.a_str('True'), 'True')
+        self.assertEqual(mml.MathElement.a_str(''), '')
+
+    def test__repr__(self):
+        """The representation should match the code to instantiate."""
+        # Watch the idempotent normalisation:
+        # * attribute names are downcased,
+        # * XML attribute values are converted to strings
+        e1 = mml.MathElement(mml.MathElement(level=2), CLASS='root')
+        self.assertEqual(repr(e1),
+                         "MathElement(MathElement(level='2'), class='root')")
+        # Also report non-default number of children: (as `int`, not `str`)
+        e1[0].nchildren = 1
+        self.assertEqual(repr(e1[0]), "MathElement(nchildren=1, level='2')")
+
+    def test__str__(self):
+        """Informal string representation for debugging and error reports."""
+        e1 = mml.MathElement(mml.MathElement(level=2), CLASS='root')
+        self.assertEqual(str(e1), 'MathElement(MathElement())')
+
+    def test__setitem__(self):
+        """Setting an item also sets the `parent` attribute on the child."""
+        e1 = mml.MathElement()
+        e1[:] = (mml.MathElement(id=1), mml.MathElement(id=2))
+        self.assertEqual(e1[0].parent, e1)
+        self.assertEqual(e1[0].get('id'), '1')
+        self.assertEqual(e1[1].get('id'), '2')
+        e1[0] = mml.MathElement()
+        self.assertEqual(e1[0].get('id'), None)
+        self.assertEqual(e1[0].parent, e1)
+
+    def test_is_full(self):
+        # A node is "full", if the number of children equals or exceeds
+        # the number of accepted children given in the "nchildren" attribute.
+        e1 = mml.MathElement()
+        self.assertTrue(e1.nchildren is None)  # no limit to number of children
+        self.assertFalse(e1.is_full())
+        e1.nchildren = 1  # element expects/accepts one child.
+        self.assertFalse(e1.is_full())
+        e1.nchildren = 0  # element expects/accepts no children.
+        self.assertTrue(e1.is_full())
+
+    def test_close(self):
+        """Closing an element returns first non-closed anchestor or None."""
+        e1 = mml.MathElement(mml.MathElement())
+        self.assertEqual(e1[0].close(), e1)
+        self.assertTrue(e1.close() is None)
+        # When all anchestors are closed, return None as well
+        self.assertTrue(e1[0].close() is None)
+
+    def test_append(self):
+        """Test special features of the "append()" method."""
+        e1 = mml.MathElement()
+        e1.nchildren = 2  # element expects/accepts two children.
+
+        # Appending to an element sets the internal "parent" attribute ...
+        result = e1.append(mml.MathElement(id='c1'))
+        self.assertEqual(e1[0].parent, e1)
+        # ... which is hidden in XML ...
+        self.assertEqual(e1[0].toprettyxml(),
+                         '<MathElement id="c1"></MathElement>')
+        # ... and returns the new "insertion point".
+        # If more children may be appended, return self
+        self.assertEqual(result, e1)
+        # If the element is "full" after appending the child,
+        # a non-full anchestor or None is returned:
+        result = e1.append(mml.MathElement(id='c2'))
+        self.assertTrue(result is None)
+
+        # Trying to append to an element that is "full" returns an error:
+        with self.assertRaises(TypeError) as cm:
+            result = e1.append(mml.MathElement())
+        self.assertEqual(str(cm.exception),
+                         'Element "MathElement(MathElement(), MathElement())"'
+                         ' takes only 2 children.')
+
+    def test_extend(self):
+        e1 = mml.MathElement()
+        c1 = mml.MathElement(id='c1')
+        c2 = mml.MathElement(id='c2')
+        e1.extend([c1, c2])
+        self.assertEqual([*e1], [c1, c2])
+
+    def test_pop(self):
+        c1 = mml.MathElement(id='c1')
+        c2 = mml.MathElement(id='c2')
+        e1 = mml.MathElement(c1, c2)
+        last_element = e1.pop()
+        self.assertEqual(last_element, c2)
+        self.assertEqual([*e1], [c1])
+        first_element = e1.pop(0)
+        self.assertEqual(first_element, c1)
+        with self.assertRaises(IndexError):
+            e1.pop()
+
+    def test_in_block(self):
+        # Return True, if `self` or an anchestor is a block-level element:
+        e1 = mml.math(display='block')
+        e2 = mml.math(mml.math(id='e3'), id='e2')
+        self.assertTrue(e1.in_block())
+        self.assertFalse(e2.in_block())
+        self.assertFalse(e2[0].in_block())
+        e1.append(e2)
+        self.assertTrue(e2.in_block())
+        self.assertTrue(e2[0].in_block())
+
+    def test_toprettyxml(self):
+        """XML representation of the element/subtree as `str`."""
+        e1 = mml.math(mml.math(level=2), CLASS='root')
+        self.assertEqual(e1.toprettyxml(),
+                         '<math class="root">\n'
+                         '  <math level="2"></math>\n'
+                         '</math>')
+
+
+class MathSchemaTests(unittest.TestCase):
+    """Test `MathSchema` and derived element classes."""
+
+    def test__init__(self):
+        # the optional `switch` argument is stored as internal attribute
+        ms1 = mml.MathSchema(switch=True, id='ms1')
+        self.assertEqual(repr(ms1), "MathSchema(switch=True, id='ms1')")
+        # internal attributes are not exported to XML.
+        self.assertEqual(ms1.toprettyxml(),
+                         '<MathSchema id="ms1"></MathSchema>')
+        # the default value is dropped from ``repr()``
+        ms1.switch = False
+        self.assertEqual(repr(ms1), "MathSchema(id='ms1')")
+
+    def test__init__full(self):
+        # when initialized with `nchildren` children and `switch` True,
+        # the children are switched and `switch` is reset:
+        ms2 = mml.MathSchema(mml.mn(1), mml.mn(2), switch=True)
+        self.assertEqual(repr(ms2), "MathSchema(mn('2'), mn('1'))")
+        self.assertEqual(
+            ms2.toprettyxml(),
+            '<MathSchema>\n  <mn>2</mn>\n  <mn>1</mn>\n</MathSchema>')
+
+    def test_append(self):
+        # appending normalizes the order before switching
+        ms1 = mml.MathSchema(mml.mn(1), switch=True)
+        ms1.append(mml.mn(2))
+        self.assertEqual(repr(ms1), "MathSchema(mn('2'), mn('1'))")
+        # appending to a closed element raises TypeError
+        with self.assertRaises(TypeError) as cm:
+            ms1.append(mml.mn(3))
+        self.assertIn('takes only 2 children', str(cm.exception))
+
+
+class MathTokenTests(unittest.TestCase):
+    """Test MathToken and derived element classes."""
+
+    def test__init__(self):
+        # requires one argument `text` that is stored as the element's text
+        self.assertEqual(repr(mml.mi('x')), "mi('x')")
+        # the argument is converted to a `str`
+        self.assertEqual(repr(mml.mn(42)), "mn('42')")
+        # only one positional argument allowed:
+        with self.assertRaises(TypeError):
+            mml.mo('[', 'stretchy')
+        # text must be `str` or numerical value:
+        with self.assertRaises(ValueError) as cm:
+            mml.mi(mml.mtext('out'))
+        self.assertIn('MathToken element expects `str` or number',
+                      str(cm.exception))
+
+        # optional named arguments become XML attributes
+        e1 = mml.mo('[', stretchy=False)
+        self.assertEqual(e1.toprettyxml(), '<mo stretchy="false">[</mo>')
+
+    def test_append(self):
+        # MathTokens don't take child elements.
+        # Appending to MathTokens raises an exception.
+        e1 = mml.mo('[')
+        self.assertEqual(e1.nchildren, 0)
+        with self.assertRaises(TypeError) as cm:
+            e1.append(mml.mi('x'))
+        self.assertEqual(str(cm.exception),
+                         'Element "mo(\'[\')" does not take children.')
+        with self.assertRaises(TypeError) as cm:
+            e1[:] = [mml.mn(3)]
+        self.assertEqual(str(cm.exception),
+                         'Element "mo(\'[\')" does not take children.')
+
+
+class mrowTests(unittest.TestCase):
+    """Test the `mrow` element class."""
+
+    def test_transfer_attributes(self):
+        e1 = mml.mrow(level=1, CLASS='cls1', style='rule1')
+        e2 = mml.mrow(level=2, CLASS='cls2')
+        e3 = mml.mrow(level=3, style='rule3')
+        # e1.attrib -> e2.attrib
+        e1.transfer_attributes(e2)
+        self.assertEqual(e2.get('level'), '1')
+        self.assertEqual(e2.get('class'), 'cls2 cls1')
+        self.assertEqual(e2.get('style'), 'rule1')
+        # e3.attrib -> e1.attrib
+        e3.transfer_attributes(e1)
+        self.assertEqual(e1.get('level'), '3')
+        self.assertEqual(e1.get('class'), 'cls1')
+        self.assertEqual(e1.get('style'), 'rule1; rule3')
+
+    def test_close_only_child(self):
+        # Remove <mrow> if it has a parent and only one child element.
+        # NOTE: this feature may be removed.
+        c1 = mml.math(CLASS='c1')
+        row1 = mml.mrow(c1, CLASS='row1')
+        # if possible, attach child to parent
+        row1.close()  # no parent, no removal
+        self.assertEqual(c1.parent, row1)
+
+        root = mml.math(row1)  # provide a parent
+        row1.close()  # try again
+        self.assertEqual(c1.parent, root)
+        self.assertEqual(root.toprettyxml(),
+                         '<math>\n  <math class="c1 row1"></math>\n</math>')
+
+
+class MathMLElementTests(unittest.TestCase):
+    """Test classes for "MathML Core" elements."""
+
+    def test_mathml_elements(self):
+        for line in mathml_elements.strip().splitlines():
+            element, nchildren, description = line.split(maxsplit=2)
+            element = element.strip('<>')
+            if 'DEPRECATED' in description or 'NOT IMPLEMENTED' in description:
+                continue
+            cls = getattr(mml, element)
+            if issubclass(cls, mml.MathToken):
+                e = cls('x')
+                self.assertEqual(e.toprettyxml(), f'<{element}>x</{element}>')
+            else:
+                e = cls()
+                self.assertEqual(e.toprettyxml(), f'<{element}></{element}>')
+            if nchildren == '*':
+                self.assertTrue(e.nchildren is None,
+                                f'{element}.nchildren == {e.nchildren}')
+            else:
+                self.assertEqual(e.nchildren, int(nchildren),
+                                 f'{element}.nchildren == {e.nchildren}')
+
+
+if __name__ == '__main__':
+    unittest.main()


Property changes on: trunk/docutils/test/test_utils/test_math/test_mathml_elements.py
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: svn:keywords
## -0,0 +1 ##
+Author Date Id Revision
\ No newline at end of property
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.



_______________________________________________
Docutils-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/docutils-checkins
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.