1 #! /usr/bin/env python3
3 # Author: David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
7 Test module for nodes.py.
10 from pathlib
import Path
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
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)
35 e
[0] += nodes
.Element()
36 e
[0] += nodes
.TextElement()
37 e
[0][1] += nodes
.Text('some text')
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)),
46 self
.assertEqual(list(e
[0].findall(descend
=False, ascend
=True)),
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)),
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)),
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
)),
63 self
.testlist
.append(e
[0][1][0])
64 self
.assertEqual(list(e
[0].findall(condition
=self
.not_in_testlist
)),
66 self
.assertEqual(list(e
.findall(nodes
.TextElement
)), [e
[0][1]])
68 def test_findall_duplicate_texts(self
):
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)
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
):
89 e
[0] += nodes
.Element()
90 e
[0] += nodes
.TextElement()
91 e
[0][1] += nodes
.Text('some text')
94 self
.testlist
= [e
[0], e
[0][1], e
[1]]
95 compare
= [(e
, e
[0][0]),
97 (e
[0][0], e
[0][1][0]),
98 (e
[0][1], e
[0][1][0]),
102 for node
, next_node
in compare
:
103 self
.assertEqual(node
.next_node(self
.not_in_testlist
, ascend
=True),
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):
122 def test_Text_rawsource_deprection_warning(self
):
123 with self
.assertWarnsRegex(DeprecationWarning,
124 '"rawsource" is ignored'):
125 nodes
.Text('content', rawsource
='content')
128 self
.assertEqual(str(self
.text
), 'Line 1.\n\x00rad två.')
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 "
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/>')
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"/>')
176 self
.assertEqual(element
.pformat(), '<Element attr="1">\n')
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"/>')
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>')
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>')
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
214 e
+= nodes
.Text('sample')
216 e
+= nodes
.Text('other sample')
217 e
+= nodes
.Text('sample')
218 # return element's index for the first four children:
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
):
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
))
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')
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(),
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',
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',
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',
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',
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'])
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'])
382 self
.assertEqual(element1
['ll'], ['A', 'B'])
386 grandchild
= nodes
.Text('grandchild text')
387 child
= nodes
.emphasis('childtext', grandchild
, att
='child')
388 e
= nodes
.Element('raw text', child
, att
='e')
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
):
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')
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'])
429 child2
= nodes
.Element(ids
=['child2'])
430 twins
= [nodes
.Element(ids
=['twin%s' % i
]) for i
in (1, 2)]
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">
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'))
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", '):
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',
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", '):
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".'):
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 '):
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.
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(), [])
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 '):
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())
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;)+
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>,'
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
= []
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
):
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
))
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
):
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
):
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
):
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'],
864 def test_set_id_descriptive_auto_id(self
):
865 # Use name or tag-name for auto-id.
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'],
878 def test_set_id_custom_descriptive_auto_id(self
):
879 # Custom prefixes and name or tag-name for auto-id.
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'],
894 class NodeVisitorTests(unittest
.TestCase
):
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'),
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
):
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
))
1091 self
.fail(f
'{len(failures)} failures in {len(self.ids)} ids\n'
1092 + "\n".join(failures
))
1094 names
= [ # sample, whitespace_normalized, fully_normalized
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".'):
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']
1164 self
.assertEqual(nodes
.validate_identifier_list(l1
), l1
)
1165 self
.assertEqual(nodes
.validate_identifier_list(s1
), l1
)
1166 l2
= ['m8-b', 'm8_c']
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 ')
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
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__':