1 .. #!/usr/bin/env python
2 # -*- coding: iso-8859-1 -*-
6 Test pylit.py Python Module
7 +++++++++++++++++++++++++++
9 :Date: $Date: 2007-05-17 $
10 :Version: SVN-Revision $Revision: 45 $
11 :URL: $URL: svn+ssh://svn.berlios.de/svnroot/repos/pylit/trunk/test/pylit_test.py $
12 :Copyright: 2006 Guenter Milde.
13 Released under the terms of the GNU General Public License
22 from file:///home/milde/Texte/Doc/Programmierung/Software-Carpentry/lec/unit.html
24 * Numbers: zero, largest, smallest magnitude, most negative
25 * Structures: empty, exactly one element, maximum number of elements
26 - Duplicate elements (e.g., letter "J" appears three times in a string)
27 - Aliased elements (e.g., a list contains two references to another list)
28 - Circular structures (e.g., a list that contains a reference to itself)
29 * Searching: no match found, one match found, multiple matches found,
31 - Code like x = find_all(structure)[0] is almost always wrong
32 - Should also check aliased matches (same thing found multiple times)
36 """pylit_test.py: test the "literal python" module"""
38 from pprint import pprint
43 Text <-> Code conversion
44 ========================
49 Example of text, code and stripped code with typical features"::
51 text = """.. #!/usr/bin/env python
52 # -*- coding: iso-8859-1 -*-
56 in several paragraphs followed by a literal block::
58 block1 = 'first block'
60 Some more text and the next block. ::
62 block2 = 'second block'
69 The converter expects the data in separate lines (iterator or list)
70 with trailing newlines. We use the `splitlines` string method with
73 textdata = text.splitlines(True)
76 If a "code" source is converted with the `strip` option, only text blocks
77 are extracted, which leads to::
79 stripped_text = """Leading text
81 in several paragraphs followed by a literal block:
83 Some more text and the next block.
88 The code corresponding to the text test string.
90 Using a triple-quoted string for the code (and stripped_code) can create
91 problems with the conversion of this test by pylit (as the text parts
92 would be converted to text).
93 A workaround is using a different comment string for the text blocks and
94 converting with e.g. ``pylit --comment-string='## ' pylit_test.py``.
98 code = """#!/usr/bin/env python
99 # -*- coding: iso-8859-1 -*-
103 # in several paragraphs followed by a literal block::
105 block1 = 'first block'
107 # Some more text and the next block. ::
109 block2 = 'second block'
116 codedata = code.splitlines(True)
118 Converting the text teststring with the `strip` option leads to::
120 stripped_code = """#!/usr/bin/env python
121 # -*- coding: iso-8859-1 -*-
123 block1 = 'first block'
125 block2 = 'second block'
131 pprint(stripped_code.splitlines(True))
133 Containers for special case examples:
136 ``textsamples["what"] = (<text data>, <output>, <output (with `strip`)``
142 ``codesamples["what"] = (<code data>, <output>, <output (with `strip`)``
147 Auxiliary function to test the textsamples and codesamples::
149 def check_converter(key, converter, output):
151 extract = converter()
153 outstr = "".join(extract)
154 print "soll:", repr(output)
155 print "ist: ", repr(outstr)
156 assert output == outstr
158 Test generator for textsample tests::
160 def test_Text2Code_samples():
161 for key, sample in textsamples.iteritems():
162 yield (check_converter, key,
163 Text2Code(sample[0].splitlines(True)), sample[1])
165 yield (check_converter, key,
166 Text2Code(sample[0].splitlines(True), strip=True),
169 Test generator for codesample tests::
171 def test_Code2Text_samples():
172 for key, sample in codesamples.iteritems():
173 yield (check_converter, key,
174 Code2Text(sample[0].splitlines(True)), sample[1])
176 yield (check_converter, key,
177 Code2Text(sample[0].splitlines(True), strip=True),
181 Pre and postprocessing filters (for testing the filter hooks)
185 def r2l_filter(data):
186 print "applying r2l filter"
188 yield line.replace("r", "l")
192 defaults.preprocessors["rl2text"] = r2l_filter
196 def l2r_filter(data):
197 print "applying l2r filter"
199 yield line.replace("l", "r")
203 defaults.preprocessors["text2rl"] = l2r_filter
207 def x2u_filter(data):
208 print "applying x2u filter"
210 yield line.replace("x", "u")
214 defaults.postprocessors["x2text"] = x2u_filter
218 def u2x_filter(data):
219 print "applying u2x filter"
221 yield line.replace("u", "x")
225 defaults.postprocessors["text2x"] = u2x_filter
229 def test_x2u_filter():
230 soll = text.replace("x", "u")
231 result = "".join([line for line in x2u_filter(textdata)])
232 print "soll", repr(text)
233 print "ist", repr(result)
234 assert soll == result
243 class test_TextCodeConverter(object):
244 """Test the TextCodeConverter parent class
249 def check_marker_regexp_true(self, sample, converter):
250 match = converter.marker_regexp.search(sample)
251 print 'marker: %r; sample %r' %(converter.code_block_marker, sample)
252 print 'match %r'%match
253 assert match is not None
257 def check_marker_regexp_false(self, sample, converter):
258 print 'marker: %r; sample %r' %(converter.code_block_marker, sample)
259 assert converter.marker_regexp.search(sample) is None
263 def test_marker_regexp(self):
272 ' indented text :: ',
274 'a .. directive:: somewhere::']
275 directives = ['.. code-block:: python',
276 ' .. code-block:: python',
277 '.. code-block:: python listings',
278 ' .. code-block:: python listings']
279 misses = ['.. comment string ::',
282 # default code_block_marker ('::')
283 self.converter = TextCodeConverter(textdata)
284 assert self.converter.code_block_marker == '::'
285 # self.converter is not seen by the check_marker_regexp_true() method
286 for sample in literal:
287 yield (self.check_marker_regexp_true, sample, self.converter)
288 for sample in directives+misses:
289 yield (self.check_marker_regexp_false, sample, self.converter)
290 # code-block directive as marker
291 self.converter = TextCodeConverter(textdata,
292 code_block_marker='.. code-block::')
293 assert self.converter.code_block_marker == '.. code-block::'
294 for sample in directives:
295 yield (self.check_marker_regexp_true, sample, self.converter)
296 for sample in literal+misses:
297 yield (self.check_marker_regexp_false, sample, self.converter)
301 def test_get_indent(self):
302 converter = TextCodeConverter(textdata)
303 assert converter.get_indent("foo") == 0
304 assert converter.get_indent(" foo") == 1
305 assert converter.get_indent(" foo") == 2
309 def test_collect_blocks(self):
310 converter = TextCodeConverter(textdata)
311 textblocks = [block for block in collect_blocks(textdata)]
313 assert len(textblocks) == 7, "text sample has 7 blocks"
314 assert reduce(operator.__add__, textblocks) == textdata
321 class test_Text2Code(object):
322 """Test the Text2Code class converting rst->code"""
327 self.converter = Text2Code(textdata)
331 def test_set_state_empty(self):
333 self.converter.set_state([])
334 raise AssertionError, "should raise StopIteration"
335 except StopIteration:
338 def test_set_state_header(self):
339 """test for "header" or "documentation" for first block"""
340 self.converter.state = "" # normally set by the `convert` method
341 self.converter.set_state([".. header", " block"])
342 assert self.converter.state == "header"
343 self.converter.state = "" # normally set by the `convert` method
344 self.converter.set_state(["documentation", "block"])
345 assert self.converter.state == "documentation"
347 def test_set_state_code_block(self):
348 """test for "header" or "documentation" for "code_block" """
349 # normally set by the `convert` method
350 self.converter._textindent = 0
351 self.converter.state = "code_block"
352 self.converter.set_state(["documentation", " block"])
353 assert self.converter.state == "documentation"
355 self.converter.state = "code_block"
356 self.converter.set_state([" documentation", "block"])
357 assert self.converter.state == "documentation"
359 self.converter.state = "code_block"
360 self.converter.set_state([" code", " block"])
361 print self.converter.state
362 assert self.converter.state == "code_block"
364 def test_header_handler(self):
365 """should strip header-string from header"""
366 self.converter._codeindent = 0
367 sample = [".. header", " block"]
368 lines = [line for line in self.converter.header_handler(sample)]
370 assert lines == ["header", "block"]
372 def test_documentation_handler(self):
373 """should add comment string to documentation"""
374 sample = ["doc", "block", ""]
375 lines = [line for line
376 in self.converter.documentation_handler(sample)]
378 assert lines == ["# doc", "# block", "# "]
380 def test_documentation_handler_set_state(self):
381 """should add comment string to documentation"""
382 sample = ["doc", "block::", ""]
383 lines = [line for line
384 in self.converter.documentation_handler(sample)]
386 assert lines == ["# doc", "# block::", ""]
387 assert self.converter.state == "code_block"
389 def test_code_block_handler(self):
390 """should un-indent code-blocks"""
391 self.converter._codeindent = 0 # normally set in `convert`
392 sample = [" code", " block", ""]
393 lines = [line for line
394 in self.converter.code_block_handler(sample)]
396 assert lines == ["code", "block", ""]
399 base tests on the "long" test data ::
402 """Calling a Text2Code instance should return the converted data as list of lines"""
403 output = self.converter()
406 assert codedata == output
408 def test_call_strip(self):
409 """strip=True should strip text parts"""
410 self.converter.strip = True
411 output = self.converter()
412 print repr(stripped_code.splitlines(True))
414 assert stripped_code.splitlines(True) == output
417 outstr = str(self.converter)
420 assert code == outstr
422 def test_str_strip1(self):
423 """strip=True should strip text parts.
425 Version 1 with `strip` given as optional argument"""
426 outstr = str(Text2Code(textdata, strip=True))
427 print "ist ", repr(outstr)
428 print "soll", repr(stripped_code)
430 assert stripped_code == outstr
432 def test_str_strip2(self):
433 """strip=True should strip text parts
435 Version 2 with `strip` set after instantiation"""
436 self.converter.strip = True
437 outstr = str(self.converter)
438 print "ist ", repr(outstr)
439 print "soll", repr(stripped_code)
441 assert stripped_code == outstr
443 def test_malindented_code_line(self):
444 """raise error if code line is less indented than code-indent"""
445 data1 = [".. #!/usr/bin/env python\n", # indent == 4 * " "
447 " print 'hello world'"] # indent == 2 * " "
448 data2 = ["..\t#!/usr/bin/env python\n", # indent == 8 * " "
450 " print 'hello world'"] # indent == 2 * " "
451 for data in (data1, data2):
453 blocks = Text2Code(data)()
454 assert False, "wrong indent did not raise ValueError"
458 def test_str_different_comment_string(self):
459 """Convert only comments with the specified comment string to text
461 data = [".. #!/usr/bin/env python\n",
463 '::\n', # leading code block as header
465 " block1 = 'first block'\n",
468 soll = "\n".join(["#!/usr/bin/env python",
472 "block1 = 'first block'",
476 outstr = str(Text2Code(data, comment_string="##"))
477 print "soll:", repr(soll)
478 print "ist: ", repr(outstr)
479 assert outstr == soll
481 # Filters: test pre- and postprocessing of data
483 def test_get_filter_preprocessor(self):
484 """should return filter from filter_set for language"""
485 preprocessor = self.converter.get_filter("preprocessors", "rl")
487 assert preprocessor == l2r_filter
489 def test_get_filter_postprocessor(self):
490 """should return filter from filter_set for language"""
491 postprocessor = self.converter.get_filter("postprocessors", "x")
493 assert postprocessor == u2x_filter
495 def test_get_css_postprocessor(self):
496 """should return filter from filter_set for language"""
497 postprocessor = self.converter.get_filter("postprocessors", "css")
499 assert postprocessor == dumb_c_postprocessor
501 def test_get_filter_nonexisting_language_filter(self):
502 """should return identity_filter if language has no filter in set"""
503 preprocessor = self.converter.get_filter("preprocessors", "foo")
505 assert preprocessor == identity_filter
507 def test_get_filter_nonexisting_filter_set(self):
508 """should return identity_filter if filter_set does not exist"""
509 processor = self.converter.get_filter("foo_filters", "foo")
511 assert processor == identity_filter
513 def test_preprocessor(self):
514 """Preprocess data with registered preprocessor for language"""
515 output = Text2Code(textdata, language="x", comment_string="# ")()
516 soll = [line for line in u2x_filter(codedata)]
517 print "soll: ", repr(soll)
518 print "ist: ", repr(output)
519 assert output == soll
521 def test_postprocessor(self):
522 """Preprocess data with registered postprocessor for language"""
523 output = Text2Code(textdata, language="x", comment_string="# ")()
524 soll = [line for line in u2x_filter(codedata)]
525 print "soll:", repr(soll)
526 print "ist: ", repr(output)
527 assert output == soll
532 Code follows text block without blank line
533 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
535 End of text block detected ('::') but no paragraph separator (blank line)
538 It is an reStructuredText syntax error, if a "literal block
539 marker" is not followed by a blank line.
541 Assuming that no double colon at end of line occurs accidentally,
542 pylit could fix this and issue a warning::
544 # Do we need this feature? (Complicates code a lot)
545 # textsamples["ensure blank line after text"] = (
546 # """text followed by a literal block::
547 # block1 = 'first block'
549 # """# text followed by a literal block::
551 # block1 = 'first block'
554 Text follows code block without blank line
555 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
557 End of code block detected (a line not more indented than the preceding text
560 reStructuredText syntax demands a paragraph separator (blank line) before
563 Assuming that the unindent is not accidental, pylit could fix this and
566 # Do we need this feature? (Complicates code)
567 # textsamples["ensure blank line after code"] = (
570 # block1 = 'first block'
575 # block1 = 'first block'
580 A double colon on a line on its own
581 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
583 As a double colon is added by the Code2Text conversion after a text block
584 (if not already present), it could be removed by the Text2Code conversion
585 to keep the source small and pretty.
587 However, this would put the text and code source line numbers out of sync,
588 which is bad for error reporting, failing doctests, and the JED editor
589 support with the `pylit_buffer()` function in
590 http://jedmodes.sourceforge.net/mode/pylit.
592 Maybe this could be left to a post-processing filter::
594 # textsamples["remove single double colon"] = (
595 # ["text followed by a literal block\n",
599 # " foo = 'first'\n"]
600 # ["", # empty header
601 # "# text followed by a literal block\n\n",
606 Convert a leading reStructured text comment (variant: only if there is
607 content on the first line) to a leading code block. Return an empty list,
608 if there is no header. ::
610 textsamples["simple header"] = (".. print 'hello world'",
611 "print 'hello world'")
613 textsamples["no header (start with text)"] = (
614 """a classical example without header::
618 """# a classical example without header::
624 textsamples["no header (start with blank line)"] = (
626 a classical example without header::
631 # a classical example without header::
637 textsamples["standard header, followed by text"] = (
638 """.. #!/usr/bin/env python
639 # -*- coding: iso-8859-1 -*-
641 a classical example with header::
645 """#!/usr/bin/env python
646 # -*- coding: iso-8859-1 -*-
648 # a classical example with header::
653 textsamples["standard header, followed by code"] = (
654 """.. #!/usr/bin/env python
658 """#!/usr/bin/env python
663 textsamples["null string"] = ("", "", "")
670 class test_Code2Text(object):
673 self.converter = Code2Text(codedata)
675 ## Code2Text.strip_literal_marker
677 ## * strip `::`-line as well as preceding blank line if on a line on its own
678 ## * strip `::` if it is preceded by whitespace.
679 ## * convert `::` to a single colon if preceded by text
682 def check_strip_code_block_marker(self, sample):
683 """test Code2Text.strip_code_block_marker"""
684 ist = sample[0].splitlines(True)
685 soll = sample[1].splitlines(True)
687 converter = Code2Text(codedata)
688 converter.strip_code_block_marker(ist)
689 print "soll:", repr(soll)
690 print "ist: ", repr(ist)
694 def test_strip_code_block_marker(self):
695 samples = (("text\n\n::\n\n", "text\n\n"),
696 ("text\n::\n\n", "text\n\n"),
697 ("text ::\n\n", "text\n\n"),
698 ("text::\n\n", "text:\n\n"),
699 ("text:\n\n", "text:\n\n"),
700 ("text\n\n", "text\n\n"),
703 for sample in samples:
704 yield (self.check_strip_code_block_marker, sample)
709 def test_set_state(self):
710 samples = (("code_block", ["code_block\n"], "code_block"),
711 ("code_block", ["#code_block\n"], "code_block"),
712 ("code_block", ["## code_block\n"], "code_block"),
713 ("code_block", ["# documentation\n"], "documentation"),
714 ("code_block", ["# documentation\n"], "documentation"),
715 ("code_block", ["# \n"], "documentation"),
716 ("code_block", ["#\n"], "documentation"),
717 ("code_block", ["\n"], "documentation"),
718 ("", ["code_block\n"], "header"),
719 ("", ["# documentation\n"], "documentation"),
720 ("documentation", ["code_block\n"], "code_block"),
721 ("documentation", ["# documentation\n"], "documentation"),
723 print "comment string", repr(self.converter.comment_string)
724 for (old_state, lines, soll) in samples:
725 self.converter.state = old_state
726 self.converter.set_state(lines)
727 print repr(lines), "old state", old_state
728 print "soll", repr(soll),
729 print "result", repr(self.converter.state)
730 assert soll == self.converter.state
732 base tests on the "long" test strings ::
735 output = self.converter()
738 assert textdata == output
740 def test_call_strip(self):
741 output = Code2Text(codedata, strip=True)()
742 print repr(stripped_text.splitlines(True))
744 assert stripped_text.splitlines(True) == output
747 """Test Code2Text class converting code->text"""
748 outstr = str(self.converter)
750 print "soll:", repr(text)
751 print "ist: ", repr(outstr)
752 assert text == outstr
754 def test_str_strip(self):
755 """Test Code2Text class converting code->rst with strip=True
757 Should strip code blocks
759 outstr = str(Code2Text(codedata, strip=True))
760 print repr(stripped_text)
762 assert stripped_text == outstr
764 def test_str_different_comment_string(self):
765 """Convert only comments with the specified comment string to text
767 outstr = str(Code2Text(codedata, comment_string="##", strip=True))
772 "block1 = 'first block'\n",
775 soll = "\n".join(['.. # ::', # leading code block as header
777 " block1 = 'first block'",
779 ' more text'] # keep space (not part of comment string)
781 outstr = str(Code2Text(data, comment_string="##"))
782 print "soll:", repr(soll)
783 print "ist: ", repr(outstr)
784 assert outstr == soll
786 def test_call_different_code_block_marker(self):
787 """recognize specified code-block marker
789 data = ["# .. code-block:: python\n",
791 "block1 = 'first block'\n",
794 soll = ['.. code-block:: python\n',
796 " block1 = 'first block'\n",
798 ' more text\n'] # keep space (not part of comment string)
800 converter = Code2Text(data, code_block_marker='.. code-block::')
802 print "soll:", repr(soll)
803 print "ist: ", repr(output)
804 assert output == soll
806 # Filters: test pre- and postprocessing of Code2Text data conversion
808 def test_get_filter_preprocessor(self):
809 """should return Code2Text preprocessor for language"""
810 preprocessor = self.converter.get_filter("preprocessors", "rl")
812 assert preprocessor == r2l_filter
814 def test_get_css_preprocessor(self):
815 """should return filter from filter_set for language"""
816 preprocessor = self.converter.get_filter("preprocessors", "css")
818 assert preprocessor == dumb_c_preprocessor
820 def test_get_filter_postprocessor(self):
821 """should return Code2Text postprocessor for language"""
822 postprocessor = self.converter.get_filter("postprocessors", "x")
824 assert postprocessor == x2u_filter
826 def test_get_filter_nonexisting_language_filter(self):
827 """should return identity_filter if language has no filter in set"""
828 preprocessor = self.converter.get_filter("preprocessors", "foo")
830 assert preprocessor == identity_filter
832 def test_get_filter_nonexisting_filter_set(self):
833 """should return identity_filter if filter_set does not exist"""
834 processor = self.converter.get_filter("foo_filters", "foo")
836 assert processor == identity_filter
838 def test_preprocessor(self):
839 """Preprocess data with registered preprocessor for language"""
840 converter = Code2Text(codedata, language="rl", comment_string="# ")
841 print "preprocessor", converter.preprocessor
842 print "postprocessor", converter.postprocessor
844 soll = [line.replace("r", "l") for line in textdata]
845 print "ist: ", repr(output)
846 print "soll:", repr(soll)
847 assert output == soll
849 def test_postprocessor(self):
850 """Postprocess data with registered postprocessor for language"""
851 output = Code2Text(codedata, language="x", comment_string="# ")()
852 soll = [line.replace("x", "u") for line in textdata]
853 print "soll:", repr(soll)
854 print "ist: ", repr(output)
855 assert output == soll
864 Normally, whitespace in the comment string is significant, i.e. with
865 ``comment_string = "# "``, a line ``"#something\n"`` will count as code.
867 However, if a comment line is blank, trailing whitespace in the comment
868 string should be ignored, i.e. ``#\n`` is recognised as a blank text line::
870 codesamples["ignore trailing whitespace in comment string for blank line"] = (
873 block1 = 'first block'
880 block1 = 'first block'
886 No blank line after text
887 ~~~~~~~~~~~~~~~~~~~~~~~~
889 If a matching comment precedes or follows a code line (i.e. any line
890 without matching comment) without a blank line in between, it counts as code
893 This will keep small inline comments close to the code they comment on. It
894 will also keep blocks together where one commented line does not match the
895 comment string (the whole block will be kept as commented code)
898 codesamples["comment before code (without blank line)"] = (
913 codesamples["comment block before code (without blank line)"] = (
914 """# no text (watch the comment sign in the next line)::
919 """.. # no text (watch the comment sign in the next line)::
926 codesamples["comment after code (without blank line)"] = (
929 block1 = 'first block'
936 block1 = 'first block'
945 codesamples["comment block after code (without blank line)"] = (
948 block1 = 'first block'
955 block1 = 'first block'
963 missing literal block marker
964 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
966 If text (with matching comment string) is followed by code (line(s) without
967 matching comment string), but there is no double colon at the end, back
968 conversion would not recognise the end of text!
970 Therefore, pylit adds a paragraph containing only ``::`` -- the literal
971 block marker in expanded form. (While it would in many cases be nicer to
972 add the double colon to the last text line, this is not always valid rst
973 syntax, e.g. after a section header or a list. Therefore the automatic
974 insertion will use the save form, feel free to correct this by hand.)::
976 codesamples["insert missing double colon after text block"] = (
977 """# text followed by code without double colon
981 """text followed by code without double colon
987 """text followed by code without double colon
994 Convert a header (leading code block) to a reStructured text comment. ::
996 codesamples["no matching comment, just code"] = (
997 """print 'hello world'
1001 """.. print 'hello world'
1006 codesamples["empty header (start with matching comment)"] = (
1007 """# a classical example without header::
1011 """a classical example without header::
1015 """a classical example without header:
1019 codesamples["standard header, followed by text"] = (
1020 """#!/usr/bin/env python
1021 # -*- coding: iso-8859-1 -*-
1023 # a classical example with header::
1027 """.. #!/usr/bin/env python
1028 # -*- coding: iso-8859-1 -*-
1030 a classical example with header::
1034 """a classical example with header:
1038 codesamples["standard header, followed by code"] = (
1039 """#!/usr/bin/env python
1043 """.. #!/usr/bin/env python
1055 css_code = ['/* import the default Docutils style sheet */\n',
1056 '/* --------------------------------------- */\n',
1061 '@import url("html4css1.css"); /* style */\n']
1065 css_filtered_code = ['// import the default Docutils style sheet\n',
1066 '// ---------------------------------------\n',
1071 '@import url("html4css1.css"); /* style */\n']
1075 def test_dumb_c_preprocessor():
1076 """convert `C` to `C++` comments"""
1077 output = [line for line in dumb_c_preprocessor(css_code)]
1078 print "ist: %r"%output
1079 print "soll: %r"%css_filtered_code
1080 assert output == css_filtered_code
1084 def test_dumb_c_postprocessor():
1085 """convert `C++` to `C` comments"""
1086 output = [line for line in dumb_c_postprocessor(css_filtered_code)]
1087 print "ist: %r"%output
1088 print "soll: %r"%css_code
1089 assert output == css_code
1095 if __name__ == "__main__":
1096 nose.runmodule() # requires nose 0.9.1