Revise `HTMLTranslator.prepare_svg()`.
[docutils.git] / docutils / test / test_nodes.py
blobd9379070af0927eb26324c27e8aa730675591957
1 #! /usr/bin/env python3
2 # $Id$
3 # Author: David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
6 """
7 Test module for nodes.py.
8 """
10 from pathlib import Path
11 import sys
12 import unittest
14 if __name__ == '__main__':
15 # prepend the "docutils root" to the Python library path
16 # so we import the local `docutils` package.
17 sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
19 from docutils import nodes, utils
21 debug = False
24 class NodeTests(unittest.TestCase):
26 def not_in_testlist(self, x):
27 # function to use in `condition` argument in findall() and next_node()
28 return x not in self.testlist
30 def test_findall(self):
31 # `findall()` is defined in class Node,
32 # we test with a tree of Element instances (simpler setup)
33 e = nodes.Element()
34 e += nodes.Element()
35 e[0] += nodes.Element()
36 e[0] += nodes.TextElement()
37 e[0][1] += nodes.Text('some text')
38 e += nodes.Element()
39 e += nodes.Element()
40 self.assertEqual(list(e.findall()),
41 [e, e[0], e[0][0], e[0][1], e[0][1][0], e[1], e[2]])
42 self.assertEqual(list(e.findall(include_self=False)),
43 [e[0], e[0][0], e[0][1], e[0][1][0], e[1], e[2]])
44 self.assertEqual(list(e.findall(descend=False)),
45 [e])
46 self.assertEqual(list(e[0].findall(descend=False, ascend=True)),
47 [e[0], e[1], e[2]])
48 self.assertEqual(list(e[0][0].findall(descend=False, ascend=True)),
49 [e[0][0], e[0][1], e[1], e[2]])
50 self.assertEqual(list(e[0][0].findall(descend=False, siblings=True)),
51 [e[0][0], e[0][1]])
52 self.testlist = e[0:2]
53 self.assertEqual(list(e.findall(condition=self.not_in_testlist)),
54 [e, e[0][0], e[0][1], e[0][1][0], e[2]])
55 # Return siblings despite siblings=False because ascend is true.
56 self.assertEqual(list(e[1].findall(ascend=True, siblings=False)),
57 [e[1], e[2]])
58 self.assertEqual(list(e[0].findall()),
59 [e[0], e[0][0], e[0][1], e[0][1][0]])
60 self.testlist = [e[0][0], e[0][1]]
61 self.assertEqual(list(e[0].findall(condition=self.not_in_testlist)),
62 [e[0], e[0][1][0]])
63 self.testlist.append(e[0][1][0])
64 self.assertEqual(list(e[0].findall(condition=self.not_in_testlist)),
65 [e[0]])
66 self.assertEqual(list(e.findall(nodes.TextElement)), [e[0][1]])
68 def test_findall_duplicate_texts(self):
69 e = nodes.Element()
70 e += nodes.TextElement()
71 e[0] += nodes.Text('one') # e[0][0]
72 e[0] += nodes.Text('two') # e[0][1]
73 e[0] += nodes.Text('three') # e[0][2]
74 e[0] += nodes.Text('two') # e[0][3] same value as e[0][1]
75 e[0] += nodes.Text('five') # e[0][4]
76 full_list = list(e[0][0].findall(siblings=True))
77 self.assertEqual(len(full_list), 5)
78 for i in range(5):
79 self.assertIs(full_list[i], e[0][i])
81 partial_list = list(e[0][3].findall(siblings=True))
82 self.assertEqual(len(partial_list), 2)
83 self.assertIs(partial_list[0], e[0][3])
84 self.assertIs(partial_list[1], e[0][4])
86 def test_next_node(self):
87 e = nodes.Element()
88 e += nodes.Element()
89 e[0] += nodes.Element()
90 e[0] += nodes.TextElement()
91 e[0][1] += nodes.Text('some text')
92 e += nodes.Element()
93 e += nodes.Element()
94 self.testlist = [e[0], e[0][1], e[1]]
95 compare = [(e, e[0][0]),
96 (e[0], e[0][0]),
97 (e[0][0], e[0][1][0]),
98 (e[0][1], e[0][1][0]),
99 (e[0][1][0], e[2]),
100 (e[1], e[2]),
101 (e[2], None)]
102 for node, next_node in compare:
103 self.assertEqual(node.next_node(self.not_in_testlist, ascend=True),
104 next_node)
105 self.assertEqual(e[0][0].next_node(ascend=True), e[0][1])
106 self.assertEqual(e[2].next_node(), None)
109 class TextTests(unittest.TestCase):
111 text = nodes.Text('Line 1.\n\x00rad två.')
112 longtext = nodes.Text('Mary had a little lamb '
113 'whose fleece was white as snow '
114 'and everwhere that Mary went '
115 'the lamb was sure to go.')
117 def test_value_type_check(self):
118 # data must by `str` instance, no `bytes` allowed
119 with self.assertRaises(TypeError):
120 nodes.Text(b'hol')
122 def test_Text_rawsource_deprection_warning(self):
123 with self.assertWarnsRegex(DeprecationWarning,
124 '"rawsource" is ignored'):
125 nodes.Text('content', rawsource='content')
127 def test_str(self):
128 self.assertEqual(str(self.text), 'Line 1.\n\x00rad två.')
130 def test_repr(self):
131 self.assertEqual(repr(self.text), r"<#text: 'Line 1.\n\x00rad två.'>")
132 self.assertEqual(self.text.shortrepr(),
133 r"<#text: 'Line 1.\n\x00rad två.'>")
135 def test_repr_long_text(self):
136 self.assertEqual(repr(self.longtext), r"<#text: 'Mary had a "
137 r"little lamb whose fleece was white as snow "
138 r"and everwh ...'>")
139 self.assertEqual(self.longtext.shortrepr(),
140 r"<#text: 'Mary had a lit ...'>")
142 def test_astext(self):
143 self.assertEqual(self.text.astext(), 'Line 1.\nrad två.')
145 def test_pformat(self):
146 self.assertTrue(isinstance(self.text.pformat(), str))
147 self.assertEqual(self.text.pformat(), 'Line 1.\nrad två.\n')
149 def test_strip(self):
150 text = nodes.Text(' was noch ')
151 stripped = text.lstrip().rstrip()
152 stripped2 = text.lstrip(' wahn').rstrip(' wahn')
153 self.assertEqual(stripped, 'was noch')
154 self.assertEqual(stripped2, 's noc')
156 def test_comparison(self):
157 # Text nodes are compared by value
158 self.assertEqual(self.text, nodes.Text('Line 1.\n\x00rad två.'))
161 class ElementTests(unittest.TestCase):
163 def test_empty(self):
164 element = nodes.Element()
165 self.assertEqual(repr(element), '<Element: >')
166 self.assertEqual(str(element), '<Element/>')
167 dom = element.asdom()
168 self.assertEqual(dom.toxml(), '<Element/>')
169 dom.unlink()
170 element['attr'] = '1'
171 self.assertEqual(repr(element), '<Element: >')
172 self.assertEqual(str(element), '<Element attr="1"/>')
173 dom = element.asdom()
174 self.assertEqual(dom.toxml(), '<Element attr="1"/>')
175 dom.unlink()
176 self.assertEqual(element.pformat(), '<Element attr="1">\n')
177 del element['attr']
178 element['mark'] = '\u2022'
179 self.assertEqual(repr(element), '<Element: >')
180 self.assertEqual(str(element), '<Element mark="\u2022"/>')
181 dom = element.asdom()
182 self.assertEqual(dom.toxml(), '<Element mark="\u2022"/>')
183 dom.unlink()
184 element['names'] = ['nobody', 'имя', 'näs']
185 self.assertEqual(repr(element), '<Element "nobody; имя; näs": >')
186 self.assertTrue(isinstance(repr(element), str))
188 def test_withtext(self):
189 element = nodes.Element('text\nmore', nodes.Text('text\nmore'))
190 uelement = nodes.Element('grün', nodes.Text('grün'))
191 self.assertEqual(repr(element), r"<Element: <#text: 'text\nmore'>>")
192 self.assertEqual(repr(uelement), "<Element: <#text: 'grün'>>")
193 self.assertTrue(isinstance(repr(uelement), str))
194 self.assertEqual(str(element), '<Element>text\nmore</Element>')
195 self.assertEqual(str(uelement), '<Element>gr\xfcn</Element>')
196 dom = element.asdom()
197 self.assertEqual(dom.toxml(), '<Element>text\nmore</Element>')
198 dom.unlink()
199 element['attr'] = '1'
200 self.assertEqual(repr(element), r"<Element: <#text: 'text\nmore'>>")
201 self.assertEqual(str(element),
202 '<Element attr="1">text\nmore</Element>')
203 dom = element.asdom()
204 self.assertEqual(dom.toxml(),
205 '<Element attr="1">text\nmore</Element>')
206 dom.unlink()
207 self.assertEqual(element.pformat(),
208 '<Element attr="1">\n text\n more\n')
210 def test_index(self):
211 # Element.index() behaves like list.index() on the element's children
212 e = nodes.Element()
213 e += nodes.Element()
214 e += nodes.Text('sample')
215 e += nodes.Element()
216 e += nodes.Text('other sample')
217 e += nodes.Text('sample')
218 # return element's index for the first four children:
219 for i in range(4):
220 self.assertEqual(e.index(e[i]), i)
221 # Caution: mismatches are possible for Text nodes
222 # as they are compared by value (like `str` instances)
223 self.assertEqual(e.index(e[4]), 1)
224 self.assertEqual(e.index(e[4], start=2), 4)
226 def test_previous_sibling(self):
227 e = nodes.Element()
228 c1 = nodes.Element()
229 c2 = nodes.Element()
230 e += [c1, c2]
231 # print(c1 == c2)
232 self.assertEqual(e.previous_sibling(), None)
233 self.assertEqual(c1.previous_sibling(), None)
234 self.assertEqual(c2.previous_sibling(), c1)
236 def test_clear(self):
237 element = nodes.Element()
238 element += nodes.Element()
239 self.assertTrue(len(element))
240 element.clear()
241 self.assertTrue(not len(element))
243 def test_normal_attributes(self):
244 element = nodes.Element()
245 self.assertTrue('foo' not in element)
246 self.assertRaises(KeyError, element.__getitem__, 'foo')
247 element['foo'] = 'sometext'
248 self.assertEqual(element['foo'], 'sometext')
249 del element['foo']
250 self.assertRaises(KeyError, element.__getitem__, 'foo')
252 def test_default_attributes(self):
253 element = nodes.Element()
254 self.assertEqual(element['ids'], [])
255 self.assertEqual(element.non_default_attributes(), {})
256 self.assertTrue(not element.is_not_default('ids'))
257 self.assertTrue(element['ids'] is not nodes.Element()['ids'])
258 element['ids'].append('someid')
259 self.assertEqual(element['ids'], ['someid'])
260 self.assertEqual(element.non_default_attributes(),
261 {'ids': ['someid']})
262 self.assertTrue(element.is_not_default('ids'))
264 def test_update_basic_atts(self):
265 element1 = nodes.Element(ids=['foo', 'bar'], test=['test1'])
266 element2 = nodes.Element(ids=['baz', 'qux'], test=['test2'])
267 element1.update_basic_atts(element2)
268 # 'ids' are appended because 'ids' is a basic attribute.
269 self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux'])
270 # 'test' is not overwritten because it is not a basic attribute.
271 self.assertEqual(element1['test'], ['test1'])
273 def test_update_all_atts(self):
274 # Note: Also tests is_not_list_attribute and is_not_known_attribute
275 # and various helpers
276 # Test for full attribute replacement
277 element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent',
278 all_nodes='mom')
279 element2 = nodes.Element(ids=['baz', 'qux'], child_only='child',
280 all_nodes='dad', source='source')
282 # Test for when same fields are replaced as well as source...
283 element1.update_all_atts_consistantly(element2, True, True)
284 # 'ids' are appended because 'ids' is a basic attribute.
285 self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux'])
286 # 'parent_only' should remain unaffected.
287 self.assertEqual(element1['parent_only'], 'parent')
288 # 'all_nodes' is overwritten due to the second parameter == True.
289 self.assertEqual(element1['all_nodes'], 'dad')
290 # 'child_only' should have been added.
291 self.assertEqual(element1['child_only'], 'child')
292 # 'source' is also overwritten due to the third parameter == True.
293 self.assertEqual(element1['source'], 'source')
295 # Test for when same fields are replaced but not source...
296 element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent',
297 all_nodes='mom')
298 element1.update_all_atts_consistantly(element2)
299 # 'ids' are appended because 'ids' is a basic attribute.
300 self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux'])
301 # 'parent_only' should remain unaffected.
302 self.assertEqual(element1['parent_only'], 'parent')
303 # 'all_nodes' is overwritten due to the second parameter default True.
304 self.assertEqual(element1['all_nodes'], 'dad')
305 # 'child_only' should have been added.
306 self.assertEqual(element1['child_only'], 'child')
307 # 'source' remains unset due to the third parameter default of False.
308 self.assertEqual(element1.get('source'), None)
310 # Test for when fields are NOT replaced but source is...
311 element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent',
312 all_nodes='mom')
313 element1.update_all_atts_consistantly(element2, False, True)
314 # 'ids' are appended because 'ids' is a basic attribute.
315 self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux'])
316 # 'parent_only' should remain unaffected.
317 self.assertEqual(element1['parent_only'], 'parent')
318 # 'all_nodes' is preserved due to the second parameter == False.
319 self.assertEqual(element1['all_nodes'], 'mom')
320 # 'child_only' should have been added.
321 self.assertEqual(element1['child_only'], 'child')
322 # 'source' is added due to the third parameter == True.
323 self.assertEqual(element1['source'], 'source')
324 element1 = nodes.Element(source='destination')
325 element1.update_all_atts_consistantly(element2, False, True)
326 # 'source' remains unchanged due to the second parameter == False.
327 self.assertEqual(element1['source'], 'destination')
329 # Test for when same fields are replaced but not source...
330 element1 = nodes.Element(ids=['foo', 'bar'], parent_only='parent',
331 all_nodes='mom')
332 element1.update_all_atts_consistantly(element2, False)
333 # 'ids' are appended because 'ids' is a basic attribute.
334 self.assertEqual(element1['ids'], ['foo', 'bar', 'baz', 'qux'])
335 # 'parent_only' should remain unaffected.
336 self.assertEqual(element1['parent_only'], 'parent')
337 # 'all_nodes' is preserved due to the second parameter == False.
338 self.assertEqual(element1['all_nodes'], 'mom')
339 # 'child_only' should have been added.
340 self.assertEqual(element1['child_only'], 'child')
341 # 'source' remains unset due to the third parameter default of False.
342 self.assertEqual(element1.get('source'), None)
344 # Test for List attribute merging
345 # Attribute Concatination
346 element1 = nodes.Element(ss='a', sl='1', ls=['I'], ll=['A'])
347 element2 = nodes.Element(ss='b', sl=['2'], ls='II', ll=['B'])
348 element1.update_all_atts_concatenating(element2)
349 # 'ss' is replaced because non-list
350 self.assertEqual(element1['ss'], 'b')
351 # 'sl' is replaced because they are both not lists
352 self.assertEqual(element1['sl'], ['2'])
353 # 'ls' is replaced because they are both not lists
354 self.assertEqual(element1['ls'], 'II')
355 # 'll' is extended because they are both lists
356 self.assertEqual(element1['ll'], ['A', 'B'])
358 # Attribute Coercion
359 element1 = nodes.Element(ss='a', sl='1', ls=['I'], ll=['A'])
360 element2 = nodes.Element(ss='b', sl=['2'], ls='II', ll=['B'])
361 element1.update_all_atts_coercion(element2)
362 # 'ss' is replaced because non-list
363 self.assertEqual(element1['ss'], 'b')
364 # 'sl' is converted to a list and appended because element2 has a list
365 self.assertEqual(element1['sl'], ['1', '2'])
366 # 'ls' has element2's value appended to the list
367 self.assertEqual(element1['ls'], ['I', 'II'])
368 # 'll' is extended because they are both lists
369 self.assertEqual(element1['ll'], ['A', 'B'])
371 # Attribute Conversion
372 element1 = nodes.Element(ss='a', sl='1', ls=['I'], ll=['A'])
373 element2 = nodes.Element(ss='b', sl=['2'], ls='II', ll=['B'])
374 element1.update_all_atts_convert(element2)
375 # 'ss' is converted to a list with the values from each element
376 self.assertEqual(element1['ss'], ['a', 'b'])
377 # 'sl' is converted to a list and appended
378 self.assertEqual(element1['sl'], ['1', '2'])
379 # 'ls' has element2's value appended to the list
380 self.assertEqual(element1['ls'], ['I', 'II'])
381 # 'll' is extended
382 self.assertEqual(element1['ll'], ['A', 'B'])
384 def test_copy(self):
385 # Shallow copy:
386 grandchild = nodes.Text('grandchild text')
387 child = nodes.emphasis('childtext', grandchild, att='child')
388 e = nodes.Element('raw text', child, att='e')
389 e_copy = e.copy()
390 self.assertTrue(e is not e_copy)
391 # Internal attributes (like `rawsource`) are also copied.
392 self.assertEqual(e.rawsource, 'raw text')
393 self.assertEqual(e_copy.rawsource, e.rawsource)
394 self.assertEqual(e_copy['att'], 'e')
395 self.assertEqual(e_copy.document, e.document)
396 self.assertEqual(e_copy.source, e.source)
397 self.assertEqual(e_copy.line, e.line)
398 # Children are not copied.
399 self.assertEqual(len(e_copy), 0)
401 def test_deepcopy(self):
402 # Deep copy:
403 grandchild = nodes.Text('grandchild text')
404 child = nodes.emphasis('childtext', grandchild, att='child')
405 e = nodes.Element('raw text', child, att='e')
406 e_deepcopy = e.deepcopy()
407 self.assertEqual(e_deepcopy.rawsource, e.rawsource)
408 self.assertEqual(e_deepcopy['att'], 'e')
409 # Children are copied recursively.
410 self.assertEqual(e_deepcopy[0][0], grandchild)
411 self.assertTrue(e_deepcopy[0][0] is not grandchild)
412 self.assertEqual(e_deepcopy[0]['att'], 'child')
414 def test_system_message_copy(self):
415 e = nodes.system_message('mytext', att='e', rawsource='raw text')
416 # Shallow copy:
417 e_copy = e.copy()
418 self.assertTrue(e is not e_copy)
419 # Internal attributes (like `rawsource`) are also copied.
420 self.assertEqual(e.rawsource, 'raw text')
421 self.assertEqual(e_copy.rawsource, e.rawsource)
422 self.assertEqual(e_copy['att'], 'e')
424 def test_replace_self(self):
425 parent = nodes.Element(ids=['parent'])
426 child1 = nodes.Element(ids=['child1'])
427 grandchild = nodes.Element(ids=['grandchild'])
428 child1 += grandchild
429 child2 = nodes.Element(ids=['child2'])
430 twins = [nodes.Element(ids=['twin%s' % i]) for i in (1, 2)]
431 child2 += twins
432 child3 = nodes.Element(ids=['child3'])
433 child4 = nodes.Element(ids=['child4'])
434 parent += [child1, child2, child3, child4]
435 self.assertEqual(parent.pformat(), """\
436 <Element ids="parent">
437 <Element ids="child1">
438 <Element ids="grandchild">
439 <Element ids="child2">
440 <Element ids="twin1">
441 <Element ids="twin2">
442 <Element ids="child3">
443 <Element ids="child4">
444 """)
445 # Replace child1 with the grandchild.
446 child1.replace_self(child1[0])
447 self.assertEqual(parent[0], grandchild)
448 # Assert that 'ids' have been updated.
449 self.assertEqual(grandchild['ids'], ['grandchild', 'child1'])
450 # Replace child2 with its children.
451 child2.replace_self(child2[:])
452 self.assertEqual(parent[1:3], twins)
453 # Assert that 'ids' have been propagated to first child.
454 self.assertEqual(twins[0]['ids'], ['twin1', 'child2'])
455 self.assertEqual(twins[1]['ids'], ['twin2'])
456 # Replace child3 with new child.
457 newchild = nodes.Element(ids=['newchild'])
458 child3.replace_self(newchild)
459 self.assertEqual(parent[3], newchild)
460 self.assertEqual(newchild['ids'], ['newchild', 'child3'])
461 # Crazy but possible case: Substitute child4 for itself.
462 child4.replace_self(child4)
463 # Make sure the 'child4' ID hasn't been duplicated.
464 self.assertEqual(child4['ids'], ['child4'])
465 self.assertEqual(len(parent), 5)
468 class ElementValidationTests(unittest.TestCase):
470 def test_validate(self):
471 """Valid node: validation should simply pass."""
472 node = nodes.paragraph('', 'plain text', classes='my test classes')
473 node.append(nodes.emphasis('', 'emphasised text', ids='emphtext'))
474 node.validate()
476 def test_validate_invalid_descendent(self):
477 paragraph = nodes.paragraph('', 'plain text')
478 tip = nodes.tip('', paragraph)
479 paragraph.append(nodes.strong('doll', id='missing-es'))
480 tip.validate(recursive=False)
481 with self.assertRaisesRegex(nodes.ValidationError,
482 'Element <strong id=.*> invalid:\n'
483 ' Attribute "id" not one of "ids", '):
484 tip.validate()
486 def test_validate_attributes(self):
487 # Convert to expected data-type, normalize values,
488 # cf. AttributeTypeTests below for attribute validating function tests.
489 node = nodes.image(classes='my test-classes',
490 names='My teST\n\\ \xA0classes',
491 width='30 mm')
492 node.validate_attributes()
493 self.assertEqual(node['classes'], ['my', 'test-classes'])
494 self.assertEqual(node['names'], ['My', 'teST classes'])
495 self.assertEqual(node['width'], '30mm')
497 def test_validate_wrong_attribute(self):
498 node = nodes.paragraph('', 'text', id='test-paragraph')
499 with self.assertRaisesRegex(nodes.ValidationError,
500 'Element <paragraph id=.*> invalid:\n'
501 ' Attribute "id" not one of "ids", '):
502 node.validate()
504 def test_validate_wrong_attribute_value(self):
505 node = nodes.image(uri='test.png', width='1in 3pt')
506 with self.assertRaisesRegex(nodes.ValidationError,
507 'Element <image.*> invalid:\n'
508 '.*"width" has invalid value "1in 3pt".'):
509 node.validate()
511 def test_validate_spurious_element(self):
512 label = nodes.label('', '*')
513 label.append(nodes.strong())
514 with self.assertRaisesRegex(nodes.ValidationError,
515 'Element <label> invalid:\n'
516 ' Child element <strong> not allowed '):
517 label.validate()
519 def test_validate_content(self):
520 """Check, whether an element's children fit into its content model.
522 Return empty lists for valid elements,
523 lists with warnings and spurious children if children don't match.
525 # sample elements
526 inline = nodes.inline() # inline element
527 text = nodes.Text('explanation') # <#text>
528 hint = nodes.hint() # body element
530 # empty element: (EMPTY)
531 image = nodes.image('')
532 self.assertEqual(image.validate_content(), [])
533 image.append(text)
534 self.assertEqual(image.validate_content(), [text])
535 # ValueError, "Spurious Element <#text: 'explanation'>"):
537 # TextElement: (#PCDATA | %inline.elements;)*
538 paragraph = nodes.paragraph() # empty element
539 self.assertEqual(paragraph.validate_content(), [])
540 paragraph = nodes.paragraph('', 'text') # just text
541 self.assertEqual(paragraph.validate_content(), [])
542 paragraph.extend([inline, nodes.Text('text 2'), nodes.math()])
543 self.assertEqual(paragraph.validate_content(), [])
544 paragraph.append(hint) # body element (sic!)
545 paragraph.append(text)
546 self.assertEqual(paragraph.validate_content(), [hint, text])
547 # validate() reports "relics" as ValueError:
548 with self.assertRaisesRegex(nodes.ValidationError,
549 '<paragraph> invalid:\n'
550 ' Child element <hint> not allowed '):
551 paragraph.validate()
553 # PureTextElement: (#PCDATA)
554 label = nodes.label() # empty element
555 self.assertEqual(label.validate_content(), [])
556 label = nodes.label('', '†')
557 self.assertEqual(label.validate_content(), [])
558 label.append(inline) # sic!
559 self.assertEqual(label.validate_content(), [inline])
561 # docinfo: (%bibliographic.elements;)+
562 docinfo = nodes.docinfo() # empty element (sic!)
563 with self.assertRaisesRegex(nodes.ValidationError,
564 'Missing child of type <Bibliographic>.'):
565 docinfo.validate_content()
566 docinfo.append(nodes.paragraph())
567 with self.assertRaisesRegex(nodes.ValidationError,
568 'Expecting .* <Bibliographic>, not '):
569 docinfo.validate_content()
570 docinfo = nodes.docinfo('', nodes.authors(), nodes.contact())
571 self.assertEqual(docinfo.validate_content(), [])
572 docinfo.append(hint) # sic!
573 self.assertEqual(docinfo.validate_content(), [hint])
575 # decoration: (header?, footer?)
576 decoration = nodes.decoration() # empty element
577 self.assertEqual(decoration.validate_content(), [])
578 decoration = nodes.decoration('', nodes.header(), nodes.footer())
579 self.assertEqual(decoration.validate_content(), [])
580 header = nodes.header()
581 decoration.append(header) # 3rd element (sic!)
582 self.assertEqual(decoration.validate_content(), [header])
583 decoration = nodes.decoration('', nodes.footer())
584 self.assertEqual(decoration.validate_content(), [])
585 decoration.append(header) # wrong order!
586 self.assertEqual(decoration.validate_content(), [header])
588 # Body elements have a range of different content models.
590 # container: (%body.elements;)+
591 container = nodes.container() # empty (sic!)
592 with self.assertRaisesRegex(nodes.ValidationError,
593 'Missing child of type <Body>.'):
594 container.validate_content()
595 container.append(inline) # sic!
596 with self.assertRaisesRegex(nodes.ValidationError,
597 'Expecting child of type <Body>, not <in'):
598 container.validate_content()
599 container = nodes.container('', nodes.paragraph()) # one body element
600 self.assertEqual(container.validate_content(), []) # valid
601 container.append(nodes.tip()) # more body elements
602 self.assertEqual(container.validate_content(), []) # valid
603 container.append(inline) # sic!
604 self.assertEqual(container.validate_content(), [inline])
606 # block_quote: ((%body.elements;)+, attribution?)
607 block_quote = nodes.block_quote('', hint, nodes.table())
608 self.assertEqual(block_quote.validate_content(), [])
609 block_quote.append(nodes.attribution())
610 self.assertEqual(block_quote.validate_content(), [])
611 block_quote.append(hint) # element after attribution (sic!)
612 self.assertEqual(block_quote.validate_content(), [hint])
614 # list item (%body.elements;)*
615 list_item = nodes.list_item() # empty list item is valid
616 self.assertEqual(list_item.validate_content(), [])
617 list_item.append(nodes.bullet_list()) # lists may be nested
618 list_item.append(paragraph)
619 self.assertEqual(list_item.validate_content(), [])
620 list_item.append(inline) # sic!
621 self.assertEqual(list_item.validate_content(), [inline])
623 # bullet_list, enumerated_list: (list_item+)
624 bullet_list = nodes.bullet_list() # empty (sic!)
625 with self.assertRaisesRegex(nodes.ValidationError,
626 'Missing child of type <list_item>.'):
627 bullet_list.validate_content()
628 bullet_list.extend([list_item, list_item, list_item])
629 self.assertEqual(bullet_list.validate_content(), [])
630 bullet_list.append(hint) # must nest in <list_item>
631 self.assertEqual(bullet_list.validate_content(), [hint])
633 # definition_list_item: (term, classifier*, definition)
634 definition_list_item = nodes.definition_list_item()
635 with self.assertRaisesRegex(nodes.ValidationError,
636 'Element <definition_list_item> invalid:\n'
637 ' Missing child of type <term>.'):
638 definition_list_item.validate_content(),
639 definition_list_item.append(nodes.term())
640 definition_list_item.append(nodes.definition())
641 self.assertEqual(definition_list_item.validate_content(), [])
642 definition_list_item.children.insert(1, nodes.classifier())
643 definition_list_item.children.insert(1, nodes.classifier())
644 self.assertEqual(definition_list_item.validate_content(), [])
646 # field: (field_name, field_body)
647 field = nodes.field()
648 with self.assertRaisesRegex(nodes.ValidationError,
649 'Missing child of type <field_name>.'):
650 field.validate_content()
651 field.extend([nodes.field_name(), nodes.field_body()])
652 self.assertEqual(field.validate_content(), [])
653 field = nodes.field('', nodes.field_body(), nodes.field_name())
654 # wrong order!
655 with self.assertRaisesRegex(nodes.ValidationError,
656 'Expecting child of type <field_name>,'
657 ' not <field_body>.'):
658 field.validate_content()
660 # option: (option_string, option_argument*)
661 option = nodes.option()
662 with self.assertRaisesRegex(nodes.ValidationError,
663 'Missing child of type <option_string>.'):
664 option.validate_content()
665 option.append(nodes.paragraph()) # sic!
666 with self.assertRaisesRegex(nodes.ValidationError,
667 'Expecting child of type <option_string>,'
668 ' not <paragraph>.'):
669 option.validate_content()
670 option = nodes.option('', nodes.option_string())
671 self.assertEqual(option.validate_content(), [])
672 option.append(nodes.option_argument())
673 self.assertEqual(option.validate_content(), [])
675 # line_block: (line | line_block)+
676 line_block = nodes.line_block() # sic!
677 with self.assertRaisesRegex(nodes.ValidationError,
678 ' child of type <line> or <line_block>.'):
679 line_block.validate_content()
680 line_block.append(nodes.line_block())
681 self.assertEqual(line_block.validate_content(), [])
682 line_block = nodes.line_block('', nodes.paragraph(), nodes.line())
683 with self.assertRaisesRegex(nodes.ValidationError,
684 'Expecting child of type <line> or '
685 '<line_block>, not <paragraph>.'):
686 line_block.validate_content()
688 # admonition: (title, (%body.elements;)+)
689 admonition = nodes.admonition('', nodes.paragraph())
690 with self.assertRaisesRegex(nodes.ValidationError,
691 'Expecting child of type <title>,'
692 ' not <paragraph>.'):
693 admonition.validate_content()
694 admonition = nodes.admonition('', nodes.title(), nodes.paragraph())
695 self.assertEqual(admonition.validate_content(), [])
697 # specific admonitions: (%body.elements;)+
698 note = nodes.note()
699 with self.assertRaisesRegex(nodes.ValidationError,
700 'Missing child of type <Body>.'):
701 note.validate_content()
702 note.append(nodes.enumerated_list())
703 self.assertEqual(note.validate_content(), [])
705 # footnote: (label?, (%body.elements;)+)
706 # TODO: use case for footnote without label (make it required?)
707 # rST parser can generate footnotes without body elements!
708 footnote = nodes.footnote('', hint)
709 self.assertEqual(footnote.validate_content(), [])
711 # citation: (label, (%body.elements;)+)
712 # TODO: rST parser allows empty citation
713 # (see test_rst/test_citations.py). Is this sensible?
714 citation = nodes.citation('', hint)
715 with self.assertRaisesRegex(nodes.ValidationError,
716 'Expecting child of type <label>,'
717 ' not <hint>.'):
718 citation.validate_content()
720 # Table group: (colspec*, thead?, tbody)
721 tgroup = nodes.tgroup() # empty (sic!)
722 with self.assertRaisesRegex(nodes.ValidationError,
723 'Missing child of type <tbody>.'):
724 tgroup.validate_content()
725 tgroup = nodes.tgroup('', nodes.colspec(), nodes.colspec(),
726 nodes.thead(), nodes.tbody())
727 self.assertEqual(tgroup.validate_content(), [])
728 thead = nodes.thead()
729 tgroup = nodes.tgroup('', nodes.tbody(), thead) # wrong order!
730 self.assertEqual(tgroup.validate_content(), [thead])
732 def test_validate_content_authors(self):
733 """Return empty list for valid elements, raise ValidationError else.
735 Specific method for `authors` instances: complex content model
736 requires repeated application of `authors.content_model`.
738 authors = nodes.authors()
739 with self.assertRaisesRegex(nodes.ValidationError,
740 'Missing child of type <author>.'):
741 authors.validate_content()
742 authors.extend([nodes.author(), nodes.address(), nodes.contact()])
743 self.assertEqual(authors.validate_content(), [])
744 authors.append(nodes.hint())
745 with self.assertRaisesRegex(nodes.ValidationError,
746 ' child of type <author>, not <hint>.'):
747 authors.validate_content()
748 authors.extend([nodes.author(), nodes.tip(), nodes.contact()])
749 with self.assertRaisesRegex(nodes.ValidationError,
750 ' child of type <author>, not <hint>.'):
751 authors.validate_content()
753 def test_validate_content_subtitle(self):
754 """<subtitle> must follow a <title>.
756 subtitle = nodes.subtitle()
757 paragraph = nodes.paragraph()
758 sidebar = nodes.sidebar('', subtitle, paragraph)
759 with self.assertRaisesRegex(nodes.ValidationError,
760 '<subtitle> only allowed after <title>.'):
761 sidebar.validate_content()
763 def test_validate_content_transition(self):
764 """Test additional constraints on <transition> placement:
765 Not at begin or end of a section or document,
766 not after another transition.
768 transition = nodes.transition()
769 paragraph = nodes.paragraph()
770 section = nodes.section('', nodes.title(), transition, paragraph)
771 with self.assertRaisesRegex(nodes.ValidationError,
772 '<transition> may not begin a section '):
773 section.validate_content()
774 section = nodes.section('', nodes.title(), paragraph, transition)
775 with self.assertRaisesRegex(nodes.ValidationError,
776 '<transition> may not end a section '):
777 section.validate_content()
778 section = nodes.section('', nodes.title(), paragraph,
779 nodes.transition(), transition)
780 with self.assertRaisesRegex(nodes.ValidationError,
781 'Element <section> invalid:\n'
782 ' <transition> may not end .*\n'
783 ' <transition> may not directly '):
784 section.validate_content()
787 class MiscTests(unittest.TestCase):
789 def test_node_class_names(self):
790 node_class_names = []
791 for x in dir(nodes):
792 c = getattr(nodes, x)
793 if (isinstance(c, type)
794 and issubclass(c, nodes.Node)
795 and len(c.__bases__) > 1):
796 node_class_names.append(x)
797 node_class_names.sort()
798 nodes.node_class_names.sort()
799 self.assertEqual(node_class_names, nodes.node_class_names)
802 class TreeCopyVisitorTests(unittest.TestCase):
804 def setUp(self):
805 document = utils.new_document('test data')
806 document += nodes.paragraph('', 'Paragraph 1.')
807 blist = nodes.bullet_list()
808 for i in range(1, 6):
809 item = nodes.list_item()
810 for j in range(1, 4):
811 item += nodes.paragraph('', 'Item %s, paragraph %s.' % (i, j))
812 blist += item
813 document += blist
814 self.document = document
816 def compare_trees(self, one, two):
817 self.assertEqual(one.__class__, two.__class__)
818 self.assertNotEqual(id(one), id(two))
819 self.assertEqual(len(one.children), len(two.children))
820 for i in range(len(one.children)):
821 self.compare_trees(one.children[i], two.children[i])
823 def test_copy_whole(self):
824 visitor = nodes.TreeCopyVisitor(self.document)
825 self.document.walkabout(visitor)
826 newtree = visitor.get_tree_copy()
827 self.assertEqual(self.document.pformat(), newtree.pformat())
828 self.compare_trees(self.document, newtree)
831 class SetIdTests(unittest.TestCase):
833 def setUp(self):
834 self.document = utils.new_document('test')
835 self.elements = [nodes.Element(names=['test']),
836 nodes.section(), # Name empty
837 nodes.section(names=['Test']), # duplicate id
838 nodes.footnote(names=['2019-10-30']), # id empty
841 def test_set_id_default(self):
842 # Default prefixes.
843 for element in self.elements:
844 self.document.set_id(element)
845 ids = [element['ids'] for element in self.elements]
846 self.assertEqual(ids, [['test'], ['section-1'],
847 ['test-1'], ['footnote-1']])
849 def test_set_id_custom(self):
850 # Custom prefixes.
852 # Change settings.
853 self.document.settings.id_prefix = 'P-'
854 self.document.settings.auto_id_prefix = 'auto'
856 for element in self.elements:
857 self.document.set_id(element)
858 ids = [element['ids'] for element in self.elements]
859 self.assertEqual(ids, [['P-test'],
860 ['P-auto1'],
861 ['P-auto2'],
862 ['P-2019-10-30']])
864 def test_set_id_descriptive_auto_id(self):
865 # Use name or tag-name for auto-id.
867 # Change setting.
868 self.document.settings.auto_id_prefix = '%'
870 for element in self.elements:
871 self.document.set_id(element)
872 ids = [element['ids'] for element in self.elements]
873 self.assertEqual(ids, [['test'],
874 ['section-1'],
875 ['test-1'],
876 ['footnote-1']])
878 def test_set_id_custom_descriptive_auto_id(self):
879 # Custom prefixes and name or tag-name for auto-id.
881 # Change settings.
882 self.document.settings.id_prefix = 'P:'
883 self.document.settings.auto_id_prefix = 'a-%'
885 for element in self.elements:
886 self.document.set_id(element)
887 ids = [element['ids'] for element in self.elements]
888 self.assertEqual(ids, [['P:test'],
889 ['P:a-section-1'],
890 ['P:test-1'],
891 ['P:2019-10-30']])
894 class NodeVisitorTests(unittest.TestCase):
895 def setUp(self):
896 self.document = utils.new_document('test')
897 self.element = nodes.Element()
898 self.visitor = nodes.NodeVisitor(self.document)
900 def test_dispatch_visit_unknown(self):
901 # raise exception if no visit/depart methods are defined for node class
902 with self.assertRaises(NotImplementedError):
903 self.visitor.dispatch_visit(self.element)
905 def test_dispatch_visit_optional(self):
906 # silently skip nodes of a calss in tuple nodes.NodeVisitor.optional
907 rv = self.visitor.dispatch_visit(nodes.meta())
908 self.assertIsNone(rv)
911 class MiscFunctionTests(unittest.TestCase):
913 ids = [('a', 'a'), ('A', 'a'), ('', ''), ('a b \n c', 'a-b-c'),
914 ('a.b.c', 'a-b-c'), (' - a - b - c - ', 'a-b-c'), (' - ', ''),
915 ('\u2020\u2066', ''), ('a \xa7 b \u2020 c', 'a-b-c'),
916 ('1', ''), ('1abc', 'abc'),
919 ids_unicode_all = [
920 ('\u00f8 o with stroke', 'o-o-with-stroke'),
921 ('\u0111 d with stroke', 'd-d-with-stroke'),
922 ('\u0127 h with stroke', 'h-h-with-stroke'),
923 ('\u0131 dotless i', 'i-dotless-i'),
924 ('\u0142 l with stroke', 'l-l-with-stroke'),
925 ('\u0167 t with stroke', 't-t-with-stroke'),
926 # From Latin Extended-B
927 ('\u0180 b with stroke', 'b-b-with-stroke'),
928 ('\u0183 b with topbar', 'b-b-with-topbar'),
929 ('\u0188 c with hook', 'c-c-with-hook'),
930 ('\u018c d with topbar', 'd-d-with-topbar'),
931 ('\u0192 f with hook', 'f-f-with-hook'),
932 ('\u0199 k with hook', 'k-k-with-hook'),
933 ('\u019a l with bar', 'l-l-with-bar'),
934 ('\u019e n with long right leg', 'n-n-with-long-right-leg'),
935 ('\u01a5 p with hook', 'p-p-with-hook'),
936 ('\u01ab t with palatal hook', 't-t-with-palatal-hook'),
937 ('\u01ad t with hook', 't-t-with-hook'),
938 ('\u01b4 y with hook', 'y-y-with-hook'),
939 ('\u01b6 z with stroke', 'z-z-with-stroke'),
940 ('\u01e5 g with stroke', 'g-g-with-stroke'),
941 ('\u0225 z with hook', 'z-z-with-hook'),
942 ('\u0234 l with curl', 'l-l-with-curl'),
943 ('\u0235 n with curl', 'n-n-with-curl'),
944 ('\u0236 t with curl', 't-t-with-curl'),
945 ('\u0237 dotless j', 'j-dotless-j'),
946 ('\u023c c with stroke', 'c-c-with-stroke'),
947 ('\u023f s with swash tail', 's-s-with-swash-tail'),
948 ('\u0240 z with swash tail', 'z-z-with-swash-tail'),
949 ('\u0247 e with stroke', 'e-e-with-stroke'),
950 ('\u0249 j with stroke', 'j-j-with-stroke'),
951 ('\u024b q with hook tail', 'q-q-with-hook-tail'),
952 ('\u024d r with stroke', 'r-r-with-stroke'),
953 ('\u024f y with stroke', 'y-y-with-stroke'),
954 # From Latin-1 Supplements
955 ('\u00e0: a with grave', 'a-a-with-grave'),
956 ('\u00e1 a with acute', 'a-a-with-acute'),
957 ('\u00e2 a with circumflex', 'a-a-with-circumflex'),
958 ('\u00e3 a with tilde', 'a-a-with-tilde'),
959 ('\u00e4 a with diaeresis', 'a-a-with-diaeresis'),
960 ('\u00e5 a with ring above', 'a-a-with-ring-above'),
961 ('\u00e7 c with cedilla', 'c-c-with-cedilla'),
962 ('\u00e8 e with grave', 'e-e-with-grave'),
963 ('\u00e9 e with acute', 'e-e-with-acute'),
964 ('\u00ea e with circumflex', 'e-e-with-circumflex'),
965 ('\u00eb e with diaeresis', 'e-e-with-diaeresis'),
966 ('\u00ec i with grave', 'i-i-with-grave'),
967 ('\u00ed i with acute', 'i-i-with-acute'),
968 ('\u00ee i with circumflex', 'i-i-with-circumflex'),
969 ('\u00ef i with diaeresis', 'i-i-with-diaeresis'),
970 ('\u00f1 n with tilde', 'n-n-with-tilde'),
971 ('\u00f2 o with grave', 'o-o-with-grave'),
972 ('\u00f3 o with acute', 'o-o-with-acute'),
973 ('\u00f4 o with circumflex', 'o-o-with-circumflex'),
974 ('\u00f5 o with tilde', 'o-o-with-tilde'),
975 ('\u00f6 o with diaeresis', 'o-o-with-diaeresis'),
976 ('\u00f9 u with grave', 'u-u-with-grave'),
977 ('\u00fa u with acute', 'u-u-with-acute'),
978 ('\u00fb u with circumflex', 'u-u-with-circumflex'),
979 ('\u00fc u with diaeresis', 'u-u-with-diaeresis'),
980 ('\u00fd y with acute', 'y-y-with-acute'),
981 ('\u00ff y with diaeresis', 'y-y-with-diaeresis'),
982 # From Latin Extended-A
983 ('\u0101 a with macron', 'a-a-with-macron'),
984 ('\u0103 a with breve', 'a-a-with-breve'),
985 ('\u0105 a with ogonek', 'a-a-with-ogonek'),
986 ('\u0107 c with acute', 'c-c-with-acute'),
987 ('\u0109 c with circumflex', 'c-c-with-circumflex'),
988 ('\u010b c with dot above', 'c-c-with-dot-above'),
989 ('\u010d c with caron', 'c-c-with-caron'),
990 ('\u010f d with caron', 'd-d-with-caron'),
991 ('\u0113 e with macron', 'e-e-with-macron'),
992 ('\u0115 e with breve', 'e-e-with-breve'),
993 ('\u0117 e with dot above', 'e-e-with-dot-above'),
994 ('\u0119 e with ogonek', 'e-e-with-ogonek'),
995 ('\u011b e with caron', 'e-e-with-caron'),
996 ('\u011d g with circumflex', 'g-g-with-circumflex'),
997 ('\u011f g with breve', 'g-g-with-breve'),
998 ('\u0121 g with dot above', 'g-g-with-dot-above'),
999 ('\u0123 g with cedilla', 'g-g-with-cedilla'),
1000 ('\u0125 h with circumflex', 'h-h-with-circumflex'),
1001 ('\u0129 i with tilde', 'i-i-with-tilde'),
1002 ('\u012b i with macron', 'i-i-with-macron'),
1003 ('\u012d i with breve', 'i-i-with-breve'),
1004 ('\u012f i with ogonek', 'i-i-with-ogonek'),
1005 ('\u0133 ligature ij', 'ij-ligature-ij'),
1006 ('\u0135 j with circumflex', 'j-j-with-circumflex'),
1007 ('\u0137 k with cedilla', 'k-k-with-cedilla'),
1008 ('\u013a l with acute', 'l-l-with-acute'),
1009 ('\u013c l with cedilla', 'l-l-with-cedilla'),
1010 ('\u013e l with caron', 'l-l-with-caron'),
1011 ('\u0140 l with middle dot', 'l-l-with-middle-dot'),
1012 ('\u0144 n with acute', 'n-n-with-acute'),
1013 ('\u0146 n with cedilla', 'n-n-with-cedilla'),
1014 ('\u0148 n with caron', 'n-n-with-caron'),
1015 ('\u014d o with macron', 'o-o-with-macron'),
1016 ('\u014f o with breve', 'o-o-with-breve'),
1017 ('\u0151 o with double acute', 'o-o-with-double-acute'),
1018 ('\u0155 r with acute', 'r-r-with-acute'),
1019 ('\u0157 r with cedilla', 'r-r-with-cedilla'),
1020 ('\u0159 r with caron', 'r-r-with-caron'),
1021 ('\u015b s with acute', 's-s-with-acute'),
1022 ('\u015d s with circumflex', 's-s-with-circumflex'),
1023 ('\u015f s with cedilla', 's-s-with-cedilla'),
1024 ('\u0161 s with caron', 's-s-with-caron'),
1025 ('\u0163 t with cedilla', 't-t-with-cedilla'),
1026 ('\u0165 t with caron', 't-t-with-caron'),
1027 ('\u0169 u with tilde', 'u-u-with-tilde'),
1028 ('\u016b u with macron', 'u-u-with-macron'),
1029 ('\u016d u with breve', 'u-u-with-breve'),
1030 ('\u016f u with ring above', 'u-u-with-ring-above'),
1031 ('\u0171 u with double acute', 'u-u-with-double-acute'),
1032 ('\u0173 u with ogonek', 'u-u-with-ogonek'),
1033 ('\u0175 w with circumflex', 'w-w-with-circumflex'),
1034 ('\u0177 y with circumflex', 'y-y-with-circumflex'),
1035 ('\u017a z with acute', 'z-z-with-acute'),
1036 ('\u017c z with dot above', 'z-z-with-dot-above'),
1037 ('\u017e z with caron', 'z-z-with-caron'),
1038 # From Latin Extended-B
1039 ('\u01a1 o with horn', 'o-o-with-horn'),
1040 ('\u01b0 u with horn', 'u-u-with-horn'),
1041 ('\u01c6 dz with caron', 'dz-dz-with-caron'),
1042 ('\u01c9 lj', 'lj-lj'),
1043 ('\u01cc nj', 'nj-nj'),
1044 ('\u01ce a with caron', 'a-a-with-caron'),
1045 ('\u01d0 i with caron', 'i-i-with-caron'),
1046 ('\u01d2 o with caron', 'o-o-with-caron'),
1047 ('\u01d4 u with caron', 'u-u-with-caron'),
1048 ('\u01e7 g with caron', 'g-g-with-caron'),
1049 ('\u01e9 k with caron', 'k-k-with-caron'),
1050 ('\u01eb o with ogonek', 'o-o-with-ogonek'),
1051 ('\u01ed o with ogonek and macron', 'o-o-with-ogonek-and-macron'),
1052 ('\u01f0 j with caron', 'j-j-with-caron'),
1053 ('\u01f3 dz', 'dz-dz'),
1054 ('\u01f5 g with acute', 'g-g-with-acute'),
1055 ('\u01f9 n with grave', 'n-n-with-grave'),
1056 ('\u0201 a with double grave', 'a-a-with-double-grave'),
1057 ('\u0203 a with inverted breve', 'a-a-with-inverted-breve'),
1058 ('\u0205 e with double grave', 'e-e-with-double-grave'),
1059 ('\u0207 e with inverted breve', 'e-e-with-inverted-breve'),
1060 ('\u0209 i with double grave', 'i-i-with-double-grave'),
1061 ('\u020b i with inverted breve', 'i-i-with-inverted-breve'),
1062 ('\u020d o with double grave', 'o-o-with-double-grave'),
1063 ('\u020f o with inverted breve', 'o-o-with-inverted-breve'),
1064 ('\u0211 r with double grave', 'r-r-with-double-grave'),
1065 ('\u0213 r with inverted breve', 'r-r-with-inverted-breve'),
1066 ('\u0215 u with double grave', 'u-u-with-double-grave'),
1067 ('\u0217 u with inverted breve', 'u-u-with-inverted-breve'),
1068 ('\u0219 s with comma below', 's-s-with-comma-below'),
1069 ('\u021b t with comma below', 't-t-with-comma-below'),
1070 ('\u021f h with caron', 'h-h-with-caron'),
1071 ('\u0227 a with dot above', 'a-a-with-dot-above'),
1072 ('\u0229 e with cedilla', 'e-e-with-cedilla'),
1073 ('\u022f o with dot above', 'o-o-with-dot-above'),
1074 ('\u0233 y with macron', 'y-y-with-macron'),
1075 # digraphs From Latin-1 Supplements
1076 ('\u00df: ligature sz', 'sz-ligature-sz'),
1077 ('\u00e6 ae', 'ae-ae'),
1078 ('\u0153 ligature oe', 'oe-ligature-oe'),
1079 ('\u0238 db digraph', 'db-db-digraph'),
1080 ('\u0239 qp digraph', 'qp-qp-digraph'),
1083 def test_make_id(self):
1084 failures = []
1085 tests = self.ids + self.ids_unicode_all
1086 for input_, expect in tests:
1087 output = nodes.make_id(input_)
1088 if expect != output:
1089 failures.append("'%s' != '%s'" % (expect, output))
1090 if failures:
1091 self.fail(f'{len(failures)} failures in {len(self.ids)} ids\n'
1092 + "\n".join(failures))
1094 names = [ # sample, whitespace_normalized, fully_normalized
1095 ('a', 'a', 'a'),
1096 ('A', 'A', 'a'),
1097 ('A a A ', 'A a A', 'a a a'),
1098 ('A a A a', 'A a A a', 'a a a a'),
1099 (' AaA\n\r\naAa\tAaA\t\t', 'AaA aAa AaA', 'aaa aaa aaa')
1102 def test_whitespace_normalize_name(self):
1103 for (sample, ws, full) in self.names:
1104 result = nodes.whitespace_normalize_name(sample)
1105 self.assertEqual(result, ws)
1107 def test_fully_normalize_name(self):
1108 for (sample, ws, fully) in self.names:
1109 result = nodes.fully_normalize_name(sample)
1110 self.assertEqual(result, fully)
1112 def test_split_name_list(self):
1113 self.assertEqual(nodes.split_name_list(r'a\ n\ame two\\ n\\ames'),
1114 ['a name', 'two\\', r'n\ames'])
1116 def test_parse_measure(self):
1117 # measure is number + optional unit (letter(s) or percentage)
1118 self.assertEqual(nodes.parse_measure('8ex'), (8, 'ex'))
1119 self.assertEqual(nodes.parse_measure('2.5'), (2.5, ''))
1120 self.assertEqual(nodes.parse_measure('-2s'), (-2, 's'))
1121 self.assertEqual(nodes.parse_measure('2 µF'), (2, 'µF'))
1122 self.assertEqual(nodes.parse_measure('10 EUR'), (10, 'EUR'))
1123 self.assertEqual(nodes.parse_measure('.5 %'), (.5, '%'))
1124 # scientific notation not supported
1125 with self.assertRaisesRegex(ValueError, '"3e-4 mm" is no valid '):
1126 nodes.parse_measure('3e-4 mm')
1127 # unit must follow the number
1128 with self.assertRaisesRegex(ValueError, '"EUR 23" is no valid '):
1129 nodes.parse_measure('EUR 23')
1130 # only single percent sign allowed
1131 with self.assertRaisesRegex(ValueError, '"2%%" is no valid measure'):
1132 nodes.parse_measure('2%%')
1135 class AttributeTypeTests(unittest.TestCase):
1136 """Test validator functions for the supported `attribute data types`__
1138 See also test_parsers/test_docutils_xml/test_parse_element.py.
1140 __ https://docutils.sourceforge.io/docs/ref/doctree.html#attribute-types
1143 def test_validate_enumerated_type(self):
1144 # function factory for "choice validators"
1145 food = nodes.validate_enumerated_type('ham', 'spam')
1146 self.assertEqual(food('ham'), 'ham')
1147 with self.assertRaisesRegex(ValueError,
1148 '"bacon" is not one of "ham", "spam".'):
1149 food('bacon')
1151 def test_validate_identifier(self):
1152 # Identifiers must start with an ASCII letter and may contain
1153 # letters, digits and the hyphen
1154 # https://docutils.sourceforge.io/docs/ref/doctree.html#idref-type
1155 self.assertEqual(nodes.validate_identifier('mo-8b'), 'mo-8b')
1156 with self.assertRaisesRegex(ValueError, '"8b-mo" is no valid id'):
1157 nodes.validate_identifier('8b-mo')
1159 def test_validate_identifier_list(self):
1160 # list of identifiers (cf. above)
1161 # or a `str` of space-separated identifiers.
1162 l1 = ['m8-b', 'm8-c']
1163 s1 = 'm8-b m8-c'
1164 self.assertEqual(nodes.validate_identifier_list(l1), l1)
1165 self.assertEqual(nodes.validate_identifier_list(s1), l1)
1166 l2 = ['m8-b', 'm8_c']
1167 s2 = 'm8-b #8c'
1168 with self.assertRaises(ValueError):
1169 nodes.validate_identifier_list(l2)
1170 with self.assertRaises(ValueError):
1171 nodes.validate_identifier_list(s2)
1173 def test_validate_measure(self):
1174 # number (may be decimal fraction) + optional unit
1175 self.assertEqual(nodes.validate_measure('8ex'), '8ex')
1176 self.assertEqual(nodes.validate_measure('2'), '2')
1177 # internal whitespace is removed
1178 self.assertEqual(nodes.validate_measure('3.5 %'), '3.5%')
1179 # padding whitespace is not valid
1180 with self.assertRaisesRegex(ValueError, '"8ex " is no valid measure'):
1181 nodes.validate_measure('8ex ')
1182 # Negative numbers:
1183 # * ``doctree.txt`` does not mention negative numbers,
1184 # * in rST, negative numbers are not valid.
1185 # Provisional: currently valid but may become invalid!
1186 # self.assertEqual(nodes.validate_measure('-2'), '-2')
1188 def test_validate_NMTOKEN(self):
1189 # str with ASCII-letters, digits, hyphen, underscore, and full-stop.
1190 self.assertEqual(nodes.validate_NMTOKEN('-8x_.'), '-8x_.')
1191 with self.assertRaises(ValueError):
1192 nodes.validate_NMTOKEN('why me')
1194 def test_validate_NMTOKENS(self):
1195 # list of NMTOKENS or string with space-separated NMTOKENS
1196 l1 = ['8_b', '8.c']
1197 s1 = '8_b 8.c'
1198 l2 = ['8_b', '8/c']
1199 s2 = '8_b #8'
1200 self.assertEqual(nodes.validate_NMTOKENS(l1), l1)
1201 self.assertEqual(nodes.validate_NMTOKENS(s1), l1)
1202 with self.assertRaises(ValueError):
1203 nodes.validate_NMTOKENS(l2)
1204 with self.assertRaises(ValueError):
1205 nodes.validate_NMTOKENS(s2)
1207 def test_validate_refname_list(self):
1208 # list or string of "reference names".
1209 l1 = ['*:@', r'"more"\ & \x!']
1210 s1 = r'*:@ \"more"\\\ &\ \\x!' # unescaped backslash is ignored
1211 self.assertEqual(nodes.validate_refname_list(l1), l1)
1212 self.assertEqual(nodes.validate_refname_list(s1), l1)
1213 # whitspace is normalized, case is not normalized
1214 l2 = ['LARGE', 'a\t \tc']
1215 s2 = r'LARGE a\ \ \c'
1216 normalized = ['LARGE', 'a c']
1218 self.assertEqual(nodes.validate_refname_list(l2), normalized)
1219 self.assertEqual(nodes.validate_refname_list(s2), normalized)
1221 def test_validate_yesorno(self):
1222 # False if '0', else bool
1223 # TODO: The docs say '0' is false:
1224 # * Also return `True` for values that evaluate to `False`?
1225 # Even for `False` and `None`?
1226 # * Also return `False` for 'false', 'off', 'no'
1227 # like boolean config settings?
1228 self.assertFalse(nodes.validate_yesorno('0'))
1229 self.assertFalse(nodes.validate_yesorno(0))
1230 self.assertTrue(nodes.validate_yesorno('*'))
1231 self.assertTrue(nodes.validate_yesorno(1))
1232 # self.assertFalse(nodes.validate_yesorno('no'))
1235 if __name__ == '__main__':
1236 unittest.main()