#! /usr/bin/env python3 # $Id: test_nodes.py 10134 2025-05-19 21:12:34Z milde $ # Author: David Goodger # Copyright: This module has been placed in the public domain. """ Test module for nodes.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])) from docutils import nodes, utils debug = False class NodeTests(unittest.TestCase): def not_in_testlist(self, x): # function to use in `condition` argument in findall() and next_node() return x not in self.testlist def test_findall(self): # `findall()` is defined in class Node, # we test with a tree of Element instances (simpler setup) e = nodes.Element() e += nodes.Element() e[0] += nodes.Element() e[0] += nodes.TextElement() e[0][1] += nodes.Text('some text') e += nodes.Element() e += nodes.Element() self.assertEqual(list(e.findall()), [e, e[0], e[0][0], e[0][1], e[0][1][0], e[1], e[2]]) self.assertEqual(list(e.findall(include_self=False)), [e[0], e[0][0], e[0][1], e[0][1][0], e[1], e[2]]) self.assertEqual(list(e.findall(descend=False)), [e]) self.assertEqual(list(e[0].findall(descend=False, ascend=True)), [e[0], e[1], e[2]]) self.assertEqual(list(e[0][0].findall(descend=False, ascend=True)), [e[0][0], e[0][1], e[1], e[2]]) self.assertEqual(list(e[0][0].findall(descend=False, siblings=True)), [e[0][0], e[0][1]]) self.testlist = e[0:2] self.assertEqual(list(e.findall(condition=self.not_in_testlist)), [e, e[0][0], e[0][1], e[0][1][0], e[2]]) # Return siblings despite siblings=False because ascend is true. self.assertEqual(list(e[1].findall(ascend=True, siblings=False)), [e[1], e[2]]) self.assertEqual(list(e[0].findall()), [e[0], e[0][0], e[0][1], e[0][1][0]]) self.testlist = [e[0][0], e[0][1]] self.assertEqual(list(e[0].findall(condition=self.not_in_testlist)), [e[0], e[0][1][0]]) self.testlist.append(e[0][1][0]) self.assertEqual(list(e[0].findall(condition=self.not_in_testlist)), [e[0]]) self.assertEqual(list(e.findall(nodes.TextElement)), [e[0][1]]) def test_findall_duplicate_texts(self): e = nodes.Element() e += nodes.TextElement() e[0] += nodes.Text('one') # e[0][0] e[0] += nodes.Text('two') # e[0][1] e[0] += nodes.Text('three') # e[0][2] e[0] += nodes.Text('two') # e[0][3] same value as e[0][1] e[0] += nodes.Text('five') # e[0][4] full_list = list(e[0][0].findall(siblings=True)) self.assertEqual(len(full_list), 5) for i in range(5): self.assertIs(full_list[i], e[0][i]) partial_list = list(e[0][3].findall(siblings=True)) self.assertEqual(len(partial_list), 2) self.assertIs(partial_list[0], e[0][3]) self.assertIs(partial_list[1], e[0][4]) def test_next_node(self): e = nodes.Element() e += nodes.Element() e[0] += nodes.Element() e[0] += nodes.TextElement() e[0][1] += nodes.Text('some text') e += nodes.Element() e += nodes.Element() self.testlist = [e[0], e[0][1], e[1]] compare = [(e, e[0][0]), (e[0], e[0][0]), (e[0][0], e[0][1][0]), (e[0][1], e[0][1][0]), (e[0][1][0], e[2]), (e[1], e[2]), (e[2], None)] for node, next_node in compare: self.assertEqual(node.next_node(self.not_in_testlist, ascend=True), next_node) self.assertEqual(e[0][0].next_node(ascend=True), e[0][1]) self.assertEqual(e[2].next_node(), None) class TextTests(unittest.TestCase): text = nodes.Text('Line 1.\n\x00rad två.') longtext = nodes.Text('Mary had a little lamb ' 'whose fleece was white as snow ' 'and everwhere that Mary went ' 'the lamb was sure to go.') def test_value_type_check(self): # data must by `str` instance, no `bytes` allowed with self.assertRaises(TypeError): nodes.Text(b'hol') def test_Text_rawsource_deprection_warning(self): with self.assertWarnsRegex(DeprecationWarning, '"rawsource" is ignored'): nodes.Text('content', rawsource='content') def test_str(self): self.assertEqual(str(self.text), 'Line 1.\n\x00rad två.') def test_repr(self): self.assertEqual(repr(self.text), r"<#text: 'Line 1.\n\x00rad två.'>") self.assertEqual(self.text.shortrepr(), r"<#text: 'Line 1.\n\x00rad två.'>") def test_repr_long_text(self): self.assertEqual(repr(self.longtext), r"<#text: 'Mary had a " r"little lamb whose fleece was white as snow " r"and everwh ...'>") self.assertEqual(self.longtext.shortrepr(), r"<#text: 'Mary had a lit ...'>") def test_astext(self): self.assertEqual(self.text.astext(), 'Line 1.\nrad två.') def test_pformat(self): self.assertTrue(isinstance(self.text.pformat(), str)) self.assertEqual(self.text.pformat(), 'Line 1.\nrad två.\n') def test_strip(self): text = nodes.Text(' was noch ') stripped = text.lstrip().rstrip() stripped2 = text.lstrip(' wahn').rstrip(' wahn') self.assertEqual(stripped, 'was noch') self.assertEqual(stripped2, 's noc') def test_comparison(self): # Text nodes are compared by value self.assertEqual(self.text, nodes.Text('Line 1.\n\x00rad två.')) class ElementTests(unittest.TestCase): def test_empty(self): element = nodes.Element() self.assertEqual(repr(element), '') self.assertEqual(str(element), '') dom = element.asdom() self.assertEqual(dom.toxml(), '') dom.unlink() element['attr'] = '1' self.assertEqual(repr(element), '') self.assertEqual(str(element), '') dom = element.asdom() self.assertEqual(dom.toxml(), '') dom.unlink() self.assertEqual(element.pformat(), '\n') del element['attr'] element['mark'] = '\u2022' self.assertEqual(repr(element), '') self.assertEqual(str(element), '') dom = element.asdom() self.assertEqual(dom.toxml(), '') dom.unlink() element['names'] = ['nobody', 'имя', 'näs'] self.assertEqual(repr(element), '') self.assertTrue(isinstance(repr(element), str)) def test_withtext(self): element = nodes.Element('text\nmore', nodes.Text('text\nmore')) uelement = nodes.Element('grün', nodes.Text('grün')) self.assertEqual(repr(element), r">") self.assertEqual(repr(uelement), ">") self.assertTrue(isinstance(repr(uelement), str)) self.assertEqual(str(element), 'text\nmore') self.assertEqual(str(uelement), 'gr\xfcn') dom = element.asdom() self.assertEqual(dom.toxml(), 'text\nmore') dom.unlink() element['attr'] = '1' self.assertEqual(repr(element), r">") self.assertEqual(str(element), 'text\nmore') dom = element.asdom() self.assertEqual(dom.toxml(), 'text\nmore') dom.unlink() self.assertEqual(element.pformat(), '\n text\n more\n') def test_index(self): # Element.index() behaves like list.index() on the element's children e = nodes.Element() e += nodes.Element() e += nodes.Text('sample') e += nodes.Element() e += nodes.Text('other sample') e += nodes.Text('sample') # return element's index for the first four children: for i in range(4): self.assertEqual(e.index(e[i]), i) # Caution: mismatches are possible for Text nodes # as they are compared by value (like `str` instances) self.assertEqual(e.index(e[4]), 1) self.assertEqual(e.index(e[4], start=2), 4) def test_previous_sibling(self): e = nodes.Element() c1 = nodes.Element() c2 = nodes.Element() e += [c1, c2] # print(c1 == c2) self.assertEqual(e.previous_sibling(), None) self.assertEqual(c1.previous_sibling(), None) self.assertEqual(c2.previous_sibling(), c1) def test_clear(self): element = nodes.Element() element += nodes.Element() self.assertTrue(len(element)) element.clear() self.assertTrue(not len(element)) def test_get_language_code(self): # Return language tag from node or parents parent = nodes.Element(classes=['parental', 'language-pt-BR']) self.assertEqual(parent.get_language_code('en'), 'pt-BR') child = nodes.Element(classes=['small']) self.assertEqual(child.get_language_code('en'), 'en') parent.append(child) self.assertEqual(child.get_language_code('en'), 'pt-BR') def test_normal_attributes(self): element = nodes.Element() self.assertTrue('foo' not in element) self.assertRaises(KeyError, element.__getitem__, 'foo') element['foo'] = 'sometext' self.assertEqual(element['foo'], 'sometext') del element['foo'] self.assertRaises(KeyError, element.__getitem__, 'foo') def test_default_attributes(self): element = nodes.Element() self.assertEqual(element['ids'], []) self.assertEqual(element.non_default_attributes(), {}) self.assertTrue(not element.is_not_default('ids')) self.assertTrue(element['ids'] is not nodes.Element()['ids']) element['ids'].append('someid') self.assertEqual(element['ids'], ['someid']) self.assertEqual(element.non_default_attributes(), {'ids': ['someid']}) self.assertTrue(element.is_not_default('ids')) def test_update_basic_atts(self): element1 = nodes.Element(ids=['foo', 'bar'], test=['test1']) element2 = nodes.Element(ids=['baz', 'qux'], test=['test2']) element1.update_basic_atts(element2) # 'ids' are appended because 'ids' is a basic attribute. self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux']) # 'test' is not overwritten because it is not a basic attribute. self.assertEqual(element1['test'], ['test1']) def test_update_all_atts(self): # Note: Also tests is_not_list_attribute and is_not_known_attribute # and various helpers # Test for full attribute replacement element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent', all_nodes='mom') element2 = nodes.Element(ids=['baz', 'qux'], child_only='child', all_nodes='dad', source='source') # Test for when same fields are replaced as well as source... element1.update_all_atts_consistantly(element2, True, True) # 'ids' are appended because 'ids' is a basic attribute. self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux']) # 'parent_only' should remain unaffected. self.assertEqual(element1['parent_only'], 'parent') # 'all_nodes' is overwritten due to the second parameter == True. self.assertEqual(element1['all_nodes'], 'dad') # 'child_only' should have been added. self.assertEqual(element1['child_only'], 'child') # 'source' is also overwritten due to the third parameter == True. self.assertEqual(element1['source'], 'source') # Test for when same fields are replaced but not source... element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent', all_nodes='mom') element1.update_all_atts_consistantly(element2) # 'ids' are appended because 'ids' is a basic attribute. self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux']) # 'parent_only' should remain unaffected. self.assertEqual(element1['parent_only'], 'parent') # 'all_nodes' is overwritten due to the second parameter default True. self.assertEqual(element1['all_nodes'], 'dad') # 'child_only' should have been added. self.assertEqual(element1['child_only'], 'child') # 'source' remains unset due to the third parameter default of False. self.assertEqual(element1.get('source'), None) # Test for when fields are NOT replaced but source is... element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent', all_nodes='mom') element1.update_all_atts_consistantly(element2, False, True) # 'ids' are appended because 'ids' is a basic attribute. self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux']) # 'parent_only' should remain unaffected. self.assertEqual(element1['parent_only'], 'parent') # 'all_nodes' is preserved due to the second parameter == False. self.assertEqual(element1['all_nodes'], 'mom') # 'child_only' should have been added. self.assertEqual(element1['child_only'], 'child') # 'source' is added due to the third parameter == True. self.assertEqual(element1['source'], 'source') element1 = nodes.Element(source='destination') element1.update_all_atts_consistantly(element2, False, True) # 'source' remains unchanged due to the second parameter == False. self.assertEqual(element1['source'], 'destination') # Test for when same fields are replaced but not source... element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent', all_nodes='mom') element1.update_all_atts_consistantly(element2, False) # 'ids' are appended because 'ids' is a basic attribute. self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux']) # 'parent_only' should remain unaffected. self.assertEqual(element1['parent_only'], 'parent') # 'all_nodes' is preserved due to the second parameter == False. self.assertEqual(element1['all_nodes'], 'mom') # 'child_only' should have been added. self.assertEqual(element1['child_only'], 'child') # 'source' remains unset due to the third parameter default of False. self.assertEqual(element1.get('source'), None) # Test for List attribute merging # Attribute Concatination element1 = nodes.Element(ss='a', sl='1', ls=['I'], ll=['A']) element2 = nodes.Element(ss='b', sl=['2'], ls='II', ll=['B']) element1.update_all_atts_concatenating(element2) # 'ss' is replaced because non-list self.assertEqual(element1['ss'], 'b') # 'sl' is replaced because they are both not lists self.assertEqual(element1['sl'], ['2']) # 'ls' is replaced because they are both not lists self.assertEqual(element1['ls'], 'II') # 'll' is extended because they are both lists self.assertEqual(element1['ll'], ['A', 'B']) # Attribute Coercion element1 = nodes.Element(ss='a', sl='1', ls=['I'], ll=['A']) element2 = nodes.Element(ss='b', sl=['2'], ls='II', ll=['B']) element1.update_all_atts_coercion(element2) # 'ss' is replaced because non-list self.assertEqual(element1['ss'], 'b') # 'sl' is converted to a list and appended because element2 has a list self.assertEqual(element1['sl'], ['1', '2']) # 'ls' has element2's value appended to the list self.assertEqual(element1['ls'], ['I', 'II']) # 'll' is extended because they are both lists self.assertEqual(element1['ll'], ['A', 'B']) # Attribute Conversion element1 = nodes.Element(ss='a', sl='1', ls=['I'], ll=['A']) element2 = nodes.Element(ss='b', sl=['2'], ls='II', ll=['B']) element1.update_all_atts_convert(element2) # 'ss' is converted to a list with the values from each element self.assertEqual(element1['ss'], ['a', 'b']) # 'sl' is converted to a list and appended self.assertEqual(element1['sl'], ['1', '2']) # 'ls' has element2's value appended to the list self.assertEqual(element1['ls'], ['I', 'II']) # 'll' is extended self.assertEqual(element1['ll'], ['A', 'B']) def test_copy(self): # Shallow copy: grandchild = nodes.Text('grandchild text') child = nodes.emphasis('childtext', grandchild, att='child') e = nodes.Element('raw text', child, att='e') e_copy = e.copy() self.assertTrue(e is not e_copy) # Internal attributes (like `rawsource`) are also copied. self.assertEqual(e.rawsource, 'raw text') self.assertEqual(e_copy.rawsource, e.rawsource) self.assertEqual(e_copy['att'], 'e') self.assertEqual(e_copy.document, e.document) self.assertEqual(e_copy.source, e.source) self.assertEqual(e_copy.line, e.line) # Children are not copied. self.assertEqual(len(e_copy), 0) def test_deepcopy(self): # Deep copy: grandchild = nodes.Text('grandchild text') child = nodes.emphasis('childtext', grandchild, att='child') e = nodes.Element('raw text', child, att='e') e_deepcopy = e.deepcopy() self.assertEqual(e_deepcopy.rawsource, e.rawsource) self.assertEqual(e_deepcopy['att'], 'e') # Children are copied recursively. self.assertEqual(e_deepcopy[0][0], grandchild) self.assertTrue(e_deepcopy[0][0] is not grandchild) self.assertEqual(e_deepcopy[0]['att'], 'child') def test_system_message_copy(self): e = nodes.system_message('mytext', att='e', rawsource='raw text') # Shallow copy: e_copy = e.copy() self.assertTrue(e is not e_copy) # Internal attributes (like `rawsource`) are also copied. self.assertEqual(e.rawsource, 'raw text') self.assertEqual(e_copy.rawsource, e.rawsource) self.assertEqual(e_copy['att'], 'e') def test_replace_self(self): parent = nodes.Element(ids=['parent']) child1 = nodes.Element(ids=['child1']) grandchild = nodes.Element(ids=['grandchild']) child1 += grandchild child2 = nodes.Element(ids=['child2']) twins = [nodes.Element(ids=['twin%s' % i]) for i in (1, 2)] child2 += twins child3 = nodes.Element(ids=['child3']) child4 = nodes.Element(ids=['child4']) parent += [child1, child2, child3, child4] self.assertEqual(parent.pformat(), """\ """) # Replace child1 with the grandchild. child1.replace_self(child1[0]) self.assertEqual(parent[0], grandchild) # Assert that 'ids' have been updated. self.assertEqual(grandchild['ids'], ['grandchild', 'child1']) # Replace child2 with its children. child2.replace_self(child2[:]) self.assertEqual(parent[1:3], twins) # Assert that 'ids' have been propagated to first child. self.assertEqual(twins[0]['ids'], ['twin1', 'child2']) self.assertEqual(twins[1]['ids'], ['twin2']) # Replace child3 with new child. newchild = nodes.Element(ids=['newchild']) child3.replace_self(newchild) self.assertEqual(parent[3], newchild) self.assertEqual(newchild['ids'], ['newchild', 'child3']) # Crazy but possible case: Substitute child4 for itself. child4.replace_self(child4) # Make sure the 'child4' ID hasn't been duplicated. self.assertEqual(child4['ids'], ['child4']) self.assertEqual(len(parent), 5) class ColspecTests(unittest.TestCase): def test_propwidth(self): # Return colwidth attribute value if it is a proportional measure. colspec = nodes.colspec() colspec['colwidth'] = '8.2*' # value + '*' self.assertEqual(colspec.propwidth(), 8.2) colspec['colwidth'] = '2' # in Docutils < 2.0, default unit is '*' self.assertEqual(colspec.propwidth(), 2) colspec['colwidth'] = '20%' # percentual values not supported with self.assertRaisesRegex(ValueError, '"20%" is no proportional me'): colspec.propwidth() colspec['colwidth'] = '2em' # fixed values not supported with self.assertRaisesRegex(ValueError, '"2em" is no proportional me'): colspec.propwidth() colspec['colwidth'] = '0*' # value must be positive with self.assertRaisesRegex(ValueError, r'"0\*" is no proportional '): colspec.propwidth() # for backwards compatibility, numerical values are accepted colspec['colwidth'] = 8.2 self.assertEqual(colspec.propwidth(), 8.2) colspec['colwidth'] = 2 self.assertEqual(colspec.propwidth(), 2) class ElementValidationTests(unittest.TestCase): def test_validate(self): """Valid node: validation should simply pass.""" node = nodes.paragraph('', 'plain text', classes='my test classes') node.append(nodes.emphasis('', 'emphasised text', ids='emphtext')) node.validate() def test_validate_invalid_descendent(self): paragraph = nodes.paragraph('', 'plain text') tip = nodes.tip('', paragraph) paragraph.append(nodes.strong('doll', id='missing-es')) tip.validate(recursive=False) with self.assertRaisesRegex(nodes.ValidationError, 'Element invalid:\n' ' Attribute "id" not one of "ids", '): tip.validate() def test_validate_attributes(self): # Convert to expected data-type, normalize values, # cf. AttributeTypeTests below for attribute validating function tests. node = nodes.image(classes='my test-classes', names='My teST\n\\ \xA0classes', width='30 mm') node.validate_attributes() self.assertEqual(node['classes'], ['my', 'test-classes']) self.assertEqual(node['names'], ['My', 'teST classes']) self.assertEqual(node['width'], '30mm') def test_validate_wrong_attribute(self): node = nodes.paragraph('', 'text', id='test-paragraph') with self.assertRaisesRegex(nodes.ValidationError, 'Element invalid:\n' ' Attribute "id" not one of "ids", '): node.validate() def test_validate_wrong_attribute_value(self): node = nodes.image(uri='test.png', width='1in 3pt') with self.assertRaisesRegex(nodes.ValidationError, 'Element invalid:\n' '.*"width" has invalid value "1in 3pt".'): node.validate() def test_validate_spurious_element(self): label = nodes.label('', '*') label.append(nodes.strong()) with self.assertRaisesRegex(nodes.ValidationError, 'Element