diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:20:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:20:58 +0000 |
commit | ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8 (patch) | |
tree | 3c64877dd20ad1141111c77b3463e95686002b39 /tests/test_directives | |
parent | Adding debian version 7.2.6-8. (diff) | |
download | sphinx-ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8.tar.xz sphinx-ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8.zip |
Merging upstream version 7.3.7.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/test_directives')
-rw-r--r-- | tests/test_directives/__init__.py | 0 | ||||
-rw-r--r-- | tests/test_directives/test_directive_code.py | 595 | ||||
-rw-r--r-- | tests/test_directives/test_directive_object_description.py | 59 | ||||
-rw-r--r-- | tests/test_directives/test_directive_only.py | 46 | ||||
-rw-r--r-- | tests/test_directives/test_directive_option.py | 40 | ||||
-rw-r--r-- | tests/test_directives/test_directive_other.py | 195 | ||||
-rw-r--r-- | tests/test_directives/test_directive_patch.py | 110 | ||||
-rw-r--r-- | tests/test_directives/test_directives_no_typesetting.py | 108 |
8 files changed, 1153 insertions, 0 deletions
diff --git a/tests/test_directives/__init__.py b/tests/test_directives/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_directives/__init__.py diff --git a/tests/test_directives/test_directive_code.py b/tests/test_directives/test_directive_code.py new file mode 100644 index 0000000..2783d8f --- /dev/null +++ b/tests/test_directives/test_directive_code.py @@ -0,0 +1,595 @@ +"""Test the code-block directive.""" + +import os.path + +import pytest +from docutils import nodes + +from sphinx.config import Config +from sphinx.directives.code import LiteralIncludeReader +from sphinx.testing.util import etree_parse + +DUMMY_CONFIG = Config({}, {}) + + +@pytest.fixture(scope='module') +def testroot(rootdir): + testroot_path = rootdir / 'test-directive-code' + return testroot_path + + +@pytest.fixture(scope='module') +def literal_inc_path(testroot): + return testroot / 'literal.inc' + + +def test_LiteralIncludeReader(literal_inc_path): + options = {'lineno-match': True} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == literal_inc_path.read_text(encoding='utf8') + assert lines == 13 + assert reader.lineno_start == 1 + + +def test_LiteralIncludeReader_lineno_start(literal_inc_path): + options = {'lineno-start': 4} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == literal_inc_path.read_text(encoding='utf8') + assert lines == 13 + assert reader.lineno_start == 4 + + +def test_LiteralIncludeReader_pyobject1(literal_inc_path): + options = {'lineno-match': True, 'pyobject': 'Foo'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("class Foo:\n" + " pass\n") + assert reader.lineno_start == 5 + + +def test_LiteralIncludeReader_pyobject2(literal_inc_path): + options = {'pyobject': 'Bar'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("class Bar:\n" + " def baz():\n" + " pass\n") + assert reader.lineno_start == 1 # no lineno-match + + +def test_LiteralIncludeReader_pyobject3(literal_inc_path): + options = {'pyobject': 'Bar.baz'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == (" def baz():\n" + " pass\n") + + +def test_LiteralIncludeReader_pyobject_and_lines(literal_inc_path): + options = {'pyobject': 'Bar', 'lines': '2-'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == (" def baz():\n" + " pass\n") + + +def test_LiteralIncludeReader_lines1(literal_inc_path): + options = {'lines': '1-3'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("# Literally included file using Python highlighting\n" + "\n" + "foo = \"Including Unicode characters: üöä\"\n") + + +def test_LiteralIncludeReader_lines2(literal_inc_path): + options = {'lines': '1,3,5'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("# Literally included file using Python highlighting\n" + "foo = \"Including Unicode characters: üöä\"\n" + "class Foo:\n") + + +def test_LiteralIncludeReader_lines_and_lineno_match1(literal_inc_path): + options = {'lines': '3-5', 'lineno-match': True} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("foo = \"Including Unicode characters: üöä\"\n" + "\n" + "class Foo:\n") + assert reader.lineno_start == 3 + + +@pytest.mark.sphinx() # init locale for errors +def test_LiteralIncludeReader_lines_and_lineno_match2(literal_inc_path, app, status, warning): + options = {'lines': '0,3,5', 'lineno-match': True} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + with pytest.raises(ValueError, match='Cannot use "lineno-match" with a disjoint set of "lines"'): + reader.read() + + +@pytest.mark.sphinx() # init locale for errors +def test_LiteralIncludeReader_lines_and_lineno_match3(literal_inc_path, app, status, warning): + options = {'lines': '100-', 'lineno-match': True} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + with pytest.raises(ValueError, match="Line spec '100-': no lines pulled from include file"): + reader.read() + + +def test_LiteralIncludeReader_start_at(literal_inc_path): + options = {'lineno-match': True, 'start-at': 'Foo', 'end-at': 'Bar'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("class Foo:\n" + " pass\n" + "\n" + "class Bar:\n") + assert reader.lineno_start == 5 + + +def test_LiteralIncludeReader_start_after(literal_inc_path): + options = {'lineno-match': True, 'start-after': 'Foo', 'end-before': 'Bar'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == (" pass\n" + "\n") + assert reader.lineno_start == 6 + + +def test_LiteralIncludeReader_start_after_and_lines(literal_inc_path): + options = {'lineno-match': True, 'lines': '6-', + 'start-after': 'Literally', 'end-before': 'comment'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("\n" + "class Bar:\n" + " def baz():\n" + " pass\n" + "\n") + assert reader.lineno_start == 7 + + +def test_LiteralIncludeReader_start_at_and_lines(literal_inc_path): + options = {'lines': '2, 3, 5', 'start-at': 'foo', 'end-before': '#'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("\n" + "class Foo:\n" + "\n") + assert reader.lineno_start == 1 + + +def test_LiteralIncludeReader_missing_start_and_end(literal_inc_path): + options = {'start-at': 'NOTHING'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + with pytest.raises(ValueError, match='start-at pattern not found: NOTHING'): + reader.read() + + options = {'end-at': 'NOTHING'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + with pytest.raises(ValueError, match='end-at pattern not found: NOTHING'): + reader.read() + + options = {'start-after': 'NOTHING'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + with pytest.raises(ValueError, match='start-after pattern not found: NOTHING'): + reader.read() + + options = {'end-before': 'NOTHING'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + with pytest.raises(ValueError, match='end-before pattern not found: NOTHING'): + reader.read() + + +def test_LiteralIncludeReader_end_before(literal_inc_path): + options = {'end-before': 'nclud'} # *nclud* matches first and third lines. + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("# Literally included file using Python highlighting\n" + "\n") + + +def test_LiteralIncludeReader_prepend(literal_inc_path): + options = {'lines': '1', 'prepend': 'Hello', 'append': 'Sphinx'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("Hello\n" + "# Literally included file using Python highlighting\n" + "Sphinx\n") + + +def test_LiteralIncludeReader_dedent(literal_inc_path): + # dedent: 2 + options = {'lines': '9-11', 'dedent': 2} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == (" def baz():\n" + " pass\n" + "\n") + + # dedent: 4 + options = {'lines': '9-11', 'dedent': 4} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("def baz():\n" + " pass\n" + "\n") + + # dedent: 6 + options = {'lines': '9-11', 'dedent': 6} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("f baz():\n" + " pass\n" + "\n") + + # dedent: None + options = {'lines': '9-11', 'dedent': None} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("def baz():\n" + " pass\n" + "\n") + + +def test_LiteralIncludeReader_dedent_and_append_and_prepend(literal_inc_path): + # dedent: 2 + options = {'lines': '9-11', 'dedent': 2, 'prepend': 'class Foo:', 'append': '# comment'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("class Foo:\n" + " def baz():\n" + " pass\n" + "\n" + "# comment\n") + + +def test_LiteralIncludeReader_tabwidth(testroot): + # tab-width: 4 + options = {'tab-width': 4, 'pyobject': 'Qux'} + reader = LiteralIncludeReader(testroot / 'target.py', options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("class Qux:\n" + " def quux(self):\n" + " pass\n") + + # tab-width: 8 + options = {'tab-width': 8, 'pyobject': 'Qux'} + reader = LiteralIncludeReader(testroot / 'target.py', options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("class Qux:\n" + " def quux(self):\n" + " pass\n") + + +def test_LiteralIncludeReader_tabwidth_dedent(testroot): + options = {'tab-width': 4, 'dedent': 4, 'pyobject': 'Qux.quux'} + reader = LiteralIncludeReader(testroot / 'target.py', options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("def quux(self):\n" + " pass\n") + + +def test_LiteralIncludeReader_diff(testroot, literal_inc_path): + options = {'diff': testroot / 'literal-diff.inc'} + reader = LiteralIncludeReader(literal_inc_path, options, DUMMY_CONFIG) + content, lines = reader.read() + assert content == ("--- " + os.path.join(testroot, 'literal-diff.inc') + "\n" + "+++ " + os.path.join(testroot, 'literal.inc') + "\n" + "@@ -6,8 +6,8 @@\n" + " pass\n" + " \n" + " class Bar:\n" + "- def baz(self):\n" + "+ def baz():\n" + " pass\n" + " \n" + "-# comment after Bar class\n" + "+# comment after Bar class definition\n" + " def bar(): pass\n") + + +@pytest.mark.sphinx('xml', testroot='directive-code') +def test_code_block(app, status, warning): + app.build(filenames=[app.srcdir / 'index.rst']) + et = etree_parse(app.outdir / 'index.xml') + secs = et.findall('./section/section') + code_block = secs[0].findall('literal_block') + assert len(code_block) > 0 + actual = code_block[0].text + expect = ( + " def ruby?\n" + + " false\n" + + " end" + ) + assert actual == expect + + +@pytest.mark.sphinx('html', testroot='directive-code') +def test_force_option(app, status, warning): + app.build(filenames=[app.srcdir / 'force.rst']) + assert 'force.rst' not in warning.getvalue() + + +@pytest.mark.sphinx('html', testroot='directive-code') +def test_code_block_caption_html(app, status, warning): + app.build(filenames=[app.srcdir / 'caption.rst']) + html = (app.outdir / 'caption.html').read_text(encoding='utf8') + caption = ('<div class="code-block-caption">' + '<span class="caption-number">Listing 1 </span>' + '<span class="caption-text">caption <em>test</em> rb' + '</span><a class="headerlink" href="#id1" ' + 'title="Link to this code">\xb6</a></div>') + assert caption in html + + +@pytest.mark.sphinx('latex', testroot='directive-code') +def test_code_block_caption_latex(app, status, warning): + app.build(force_all=True) + latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstyleemphasis{test} rb}' + label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id1}}}' + link = '\\hyperref[\\detokenize{caption:name-test-rb}]' \ + '{Listing \\ref{\\detokenize{caption:name-test-rb}}}' + assert caption in latex + assert label in latex + assert link in latex + + +@pytest.mark.sphinx('latex', testroot='directive-code') +def test_code_block_namedlink_latex(app, status, warning): + app.build(force_all=True) + latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-rb}}}' + link1 = '\\hyperref[\\detokenize{caption:name-test-rb}]'\ + '{\\sphinxcrossref{\\DUrole{std,std-ref}{Ruby}}' + label2 = ('\\def\\sphinxLiteralBlockLabel' + '{\\label{\\detokenize{namedblocks:some-ruby-code}}}') + link2 = '\\hyperref[\\detokenize{namedblocks:some-ruby-code}]'\ + '{\\sphinxcrossref{\\DUrole{std,std-ref}{the ruby code}}}' + assert label1 in latex + assert link1 in latex + assert label2 in latex + assert link2 in latex + + +@pytest.mark.sphinx('latex', testroot='directive-code') +def test_code_block_emphasize_latex(app, status, warning): + app.build(filenames=[app.srcdir / 'emphasize.rst']) + latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n') + includes = '\\fvset{hllines={, 5, 6, 13, 14, 15, 24, 25, 26,}}%\n' + assert includes in latex + includes = '\\end{sphinxVerbatim}\n\\sphinxresetverbatimhllines\n' + assert includes in latex + + +@pytest.mark.sphinx('xml', testroot='directive-code') +def test_literal_include(app, status, warning): + app.build(filenames=[app.srcdir / 'index.rst']) + et = etree_parse(app.outdir / 'index.xml') + secs = et.findall('./section/section') + literal_include = secs[1].findall('literal_block') + literal_src = (app.srcdir / 'literal.inc').read_text(encoding='utf8') + assert len(literal_include) > 0 + actual = literal_include[0].text + assert actual == literal_src + + +@pytest.mark.sphinx('xml', testroot='directive-code') +def test_literal_include_block_start_with_comment_or_brank(app, status, warning): + app.build(filenames=[app.srcdir / 'python.rst']) + et = etree_parse(app.outdir / 'python.xml') + secs = et.findall('./section/section') + literal_include = secs[0].findall('literal_block') + assert len(literal_include) > 0 + actual = literal_include[0].text + expect = ( + 'def block_start_with_comment():\n' + ' # Comment\n' + ' return 1\n' + ) + assert actual == expect + + actual = literal_include[1].text + expect = ( + 'def block_start_with_blank():\n' + '\n' + ' return 1\n' + ) + assert actual == expect + + +@pytest.mark.sphinx('html', testroot='directive-code') +def test_literal_include_linenos(app, status, warning): + app.build(filenames=[app.srcdir / 'linenos.rst']) + html = (app.outdir / 'linenos.html').read_text(encoding='utf8') + + # :linenos: + assert ('<span class="linenos"> 1</span><span class="c1">' + '# Literally included file using Python highlighting</span>' in html) + + # :lineno-start: + assert ('<span class="linenos">200</span><span class="c1">' + '# Literally included file using Python highlighting</span>' in html) + + # :lines: 5-9 + assert ('<span class="linenos">5</span><span class="k">class</span> ' + '<span class="nc">Foo</span><span class="p">:</span>' in html) + + +@pytest.mark.sphinx('latex', testroot='directive-code') +def test_literalinclude_file_whole_of_emptyline(app, status, warning): + app.build(force_all=True) + latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n') + includes = ( + '\\begin{sphinxVerbatim}' + '[commandchars=\\\\\\{\\},numbers=left,firstnumber=1,stepnumber=1]\n' + '\n' + '\n' + '\n' + '\\end{sphinxVerbatim}\n') + assert includes in latex + + +@pytest.mark.sphinx('html', testroot='directive-code') +def test_literalinclude_caption_html(app, status, warning): + app.build(force_all=True) + html = (app.outdir / 'caption.html').read_text(encoding='utf8') + caption = ('<div class="code-block-caption">' + '<span class="caption-number">Listing 2 </span>' + '<span class="caption-text">caption <strong>test</strong> py' + '</span><a class="headerlink" href="#id2" ' + 'title="Link to this code">\xb6</a></div>') + assert caption in html + + +@pytest.mark.sphinx('latex', testroot='directive-code') +def test_literalinclude_caption_latex(app, status, warning): + app.build(filenames='index') + latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstylestrong{test} py}' + label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id2}}}' + link = '\\hyperref[\\detokenize{caption:name-test-py}]' \ + '{Listing \\ref{\\detokenize{caption:name-test-py}}}' + assert caption in latex + assert label in latex + assert link in latex + + +@pytest.mark.sphinx('latex', testroot='directive-code') +def test_literalinclude_namedlink_latex(app, status, warning): + app.build(filenames='index') + latex = (app.outdir / 'python.tex').read_text(encoding='utf8') + label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-py}}}' + link1 = '\\hyperref[\\detokenize{caption:name-test-py}]'\ + '{\\sphinxcrossref{\\DUrole{std,std-ref}{Python}}' + label2 = ('\\def\\sphinxLiteralBlockLabel' + '{\\label{\\detokenize{namedblocks:some-python-code}}}') + link2 = '\\hyperref[\\detokenize{namedblocks:some-python-code}]'\ + '{\\sphinxcrossref{\\DUrole{std,std-ref}{the python code}}}' + assert label1 in latex + assert link1 in latex + assert label2 in latex + assert link2 in latex + + +@pytest.mark.sphinx('xml', testroot='directive-code') +def test_literalinclude_classes(app, status, warning): + app.build(filenames=[app.srcdir / 'classes.rst']) + et = etree_parse(app.outdir / 'classes.xml') + secs = et.findall('./section/section') + + code_block = secs[0].findall('literal_block') + assert len(code_block) > 0 + assert code_block[0].get('classes') == 'foo bar' + assert code_block[0].get('names') == 'code_block' + + literalinclude = secs[1].findall('literal_block') + assert len(literalinclude) > 0 + assert literalinclude[0].get('classes') == 'bar baz' + assert literalinclude[0].get('names') == 'literal_include' + + +@pytest.mark.sphinx('xml', testroot='directive-code') +def test_literalinclude_pydecorators(app, status, warning): + app.build(filenames=[app.srcdir / 'py-decorators.rst']) + et = etree_parse(app.outdir / 'py-decorators.xml') + secs = et.findall('./section/section') + + literal_include = secs[0].findall('literal_block') + assert len(literal_include) == 3 + + actual = literal_include[0].text + expect = ( + '@class_decorator\n' + '@other_decorator()\n' + 'class TheClass(object):\n' + '\n' + ' @method_decorator\n' + ' @other_decorator()\n' + ' def the_method():\n' + ' pass\n' + ) + assert actual == expect + + actual = literal_include[1].text + expect = ( + ' @method_decorator\n' + ' @other_decorator()\n' + ' def the_method():\n' + ' pass\n' + ) + assert actual == expect + + actual = literal_include[2].text + expect = ( + '@function_decorator\n' + '@other_decorator()\n' + 'def the_function():\n' + ' pass\n' + ) + assert actual == expect + + +@pytest.mark.sphinx('dummy', testroot='directive-code') +def test_code_block_highlighted(app, status, warning): + app.build(filenames=[app.srcdir / 'highlight.rst']) + doctree = app.env.get_doctree('highlight') + codeblocks = list(doctree.findall(nodes.literal_block)) + + assert codeblocks[0]['language'] == 'default' + assert codeblocks[1]['language'] == 'python2' + assert codeblocks[2]['language'] == 'python3' + assert codeblocks[3]['language'] == 'python2' + + +@pytest.mark.sphinx('html', testroot='directive-code') +def test_linenothreshold(app, status, warning): + app.build(filenames=[app.srcdir / 'linenothreshold.rst']) + html = (app.outdir / 'linenothreshold.html').read_text(encoding='utf8') + + # code-block using linenothreshold + assert ('<span class="linenos">1</span><span class="k">class</span> ' + '<span class="nc">Foo</span><span class="p">:</span>' in html) + + # code-block not using linenothreshold (no line numbers) + assert '<span></span><span class="c1"># comment</span>' in html + + # literal include using linenothreshold + assert ('<span class="linenos"> 1</span><span class="c1">' + '# Literally included file using Python highlighting</span>' in html) + + # literal include not using linenothreshold (no line numbers) + assert ('<span></span><span class="c1"># Very small literal include ' + '(linenothreshold check)</span>' in html) + + +@pytest.mark.sphinx('dummy', testroot='directive-code') +def test_code_block_dedent(app, status, warning): + app.build(filenames=[app.srcdir / 'dedent.rst']) + doctree = app.env.get_doctree('dedent') + codeblocks = list(doctree.findall(nodes.literal_block)) + # Note: comparison string should not have newlines at the beginning or end + text_0_indent = '''First line +Second line + Third line +Fourth line''' + text_2_indent = ''' First line + Second line + Third line + Fourth line''' + text_4_indent = ''' First line + Second line + Third line + Fourth line''' + + assert codeblocks[0].astext() == text_0_indent + assert codeblocks[1].astext() == text_0_indent + assert codeblocks[2].astext() == text_4_indent + assert codeblocks[3].astext() == text_2_indent + assert codeblocks[4].astext() == text_4_indent + assert codeblocks[5].astext() == text_0_indent diff --git a/tests/test_directives/test_directive_object_description.py b/tests/test_directives/test_directive_object_description.py new file mode 100644 index 0000000..f2c9f9d --- /dev/null +++ b/tests/test_directives/test_directive_object_description.py @@ -0,0 +1,59 @@ +"""Test object description directives.""" + +import docutils.utils +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.io import create_publisher +from sphinx.testing import restructuredtext +from sphinx.util.docutils import sphinx_domains + + +def _doctree_for_test(builder, docname: str) -> nodes.document: + builder.env.prepare_settings(docname) + publisher = create_publisher(builder.app, 'restructuredtext') + with sphinx_domains(builder.env): + publisher.set_source(source_path=builder.env.doc2path(docname)) + publisher.publish() + return publisher.document + + +@pytest.mark.sphinx('text', testroot='object-description-sections') +def test_object_description_sections(app): + doctree = _doctree_for_test(app.builder, 'index') + # <document> + # <index> + # <desc> + # <desc_signature> + # <desc_name> + # func + # <desc_parameterlist> + # <desc_content> + # <section> + # <title> + # Overview + # <paragraph> + # Lorem ipsum dolar sit amet + + assert isinstance(doctree[0], addnodes.index) + assert isinstance(doctree[1], addnodes.desc) + assert isinstance(doctree[1][0], addnodes.desc_signature) + assert isinstance(doctree[1][1], addnodes.desc_content) + assert isinstance(doctree[1][1][0], nodes.section) + assert isinstance(doctree[1][1][0][0], nodes.title) + assert doctree[1][1][0][0][0] == 'Overview' + assert isinstance(doctree[1][1][0][1], nodes.paragraph) + assert doctree[1][1][0][1][0] == 'Lorem ipsum dolar sit amet' + + +def test_object_description_content_line_number(app): + text = (".. py:function:: foo(bar)\n" + + "\n" + + " Some link here: :ref:`abc`\n") + doc = restructuredtext.parse(app, text) + xrefs = list(doc.findall(condition=addnodes.pending_xref)) + assert len(xrefs) == 1 + source, line = docutils.utils.get_source_line(xrefs[0]) + assert 'index.rst' in source + assert line == 3 diff --git a/tests/test_directives/test_directive_only.py b/tests/test_directives/test_directive_only.py new file mode 100644 index 0000000..bf03c7b --- /dev/null +++ b/tests/test_directives/test_directive_only.py @@ -0,0 +1,46 @@ +"""Test the only directive with the test root.""" + +import re + +import pytest +from docutils import nodes + + +@pytest.mark.sphinx('text', testroot='directive-only') +def test_sectioning(app, status, warning): + + def getsects(section): + if not isinstance(section, nodes.section): + return [getsects(n) for n in section.children] + title = section.next_node(nodes.title).astext().strip() + subsects = [] + children = section.children[:] + while children: + node = children.pop(0) + if isinstance(node, nodes.section): + subsects.append(node) + continue + children = list(node.children) + children + return [title, [getsects(subsect) for subsect in subsects]] + + def testsects(prefix, sects, indent=0): + title = sects[0] + parent_num = title.split()[0] + assert prefix == parent_num, \ + 'Section out of place: %r' % title + for i, subsect in enumerate(sects[1]): + num = subsect[0].split()[0] + assert re.match('[0-9]+[.0-9]*[.]', num), \ + 'Unnumbered section: %r' % subsect[0] + testsects(prefix + str(i + 1) + '.', subsect, indent + 4) + + app.build(filenames=[app.srcdir / 'only.rst']) + doctree = app.env.get_doctree('only') + app.env.apply_post_transforms(doctree, 'only') + + parts = [getsects(n) + for n in [_n for _n in doctree.children if isinstance(_n, nodes.section)]] + for i, s in enumerate(parts): + testsects(str(i + 1) + '.', s, 4) + assert len(parts) == 4, 'Expected 4 document level headings, got:\n%s' % \ + '\n'.join(p[0] for p in parts) diff --git a/tests/test_directives/test_directive_option.py b/tests/test_directives/test_directive_option.py new file mode 100644 index 0000000..76448cd --- /dev/null +++ b/tests/test_directives/test_directive_option.py @@ -0,0 +1,40 @@ +import pytest + + +@pytest.mark.sphinx('html', testroot='root', + confoverrides={'option_emphasise_placeholders': True}) +def test_option_emphasise_placeholders(app, status, warning): + app.build() + content = (app.outdir / 'objects.html').read_text(encoding='utf8') + assert '<em><span class="pre">TYPE</span></em>' in content + assert '{TYPE}' not in content + assert ('<em><span class="pre">WHERE</span></em>' + '<span class="pre">-</span>' + '<em><span class="pre">COUNT</span></em>' in content) + assert '<span class="pre">{{value}}</span>' in content + assert ('<span class="pre">--plugin.option</span></span>' + '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content + + +@pytest.mark.sphinx('html', testroot='root') +def test_option_emphasise_placeholders_default(app, status, warning): + app.build() + content = (app.outdir / 'objects.html').read_text(encoding='utf8') + assert '<span class="pre">={TYPE}</span>' in content + assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content + assert '<span class="pre">{client_name}</span>' in content + assert ('<span class="pre">--plugin.option</span></span>' + '<span class="sig-prename descclassname"></span>' + '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content + + +@pytest.mark.sphinx('html', testroot='root') +def test_option_reference_with_value(app, status, warning): + app.build() + content = (app.outdir / 'objects.html').read_text(encoding='utf-8') + assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">' + '</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content + assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content + assert ('<a class="reference internal" href="#cmdoption-git-commit-mapi">' + '<code class="xref std std-option docutils literal notranslate"><span class="pre">-mapi[=xxx]</span></code></a>') in content + assert '<span class="pre">-mapi</span> <span class="pre">with_space</span>' in content diff --git a/tests/test_directives/test_directive_other.py b/tests/test_directives/test_directive_other.py new file mode 100644 index 0000000..1feb251 --- /dev/null +++ b/tests/test_directives/test_directive_other.py @@ -0,0 +1,195 @@ +"""Test the other directives.""" +from pathlib import Path + +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_toctree(app): + text = (".. toctree::\n" + "\n" + " foo\n" + " bar/index\n" + " baz\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'foo'), (None, 'bar/index'), (None, 'baz')], + includefiles=['foo', 'bar/index', 'baz']) + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_relative_toctree(app): + text = (".. toctree::\n" + "\n" + " bar_1\n" + " bar_2\n" + " bar_3\n" + " ../quux\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'bar/index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'bar/bar_1'), (None, 'bar/bar_2'), (None, 'bar/bar_3'), + (None, 'quux')], + includefiles=['bar/bar_1', 'bar/bar_2', 'bar/bar_3', 'quux']) + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_toctree_urls_and_titles(app): + text = (".. toctree::\n" + "\n" + " Sphinx <https://www.sphinx-doc.org/>\n" + " https://readthedocs.org/\n" + " The BAR <bar/index>\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[('Sphinx', 'https://www.sphinx-doc.org/'), + (None, 'https://readthedocs.org/'), + ('The BAR', 'bar/index')], + includefiles=['bar/index']) + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_toctree_glob(app): + text = (".. toctree::\n" + " :glob:\n" + "\n" + " *\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'baz'), (None, 'foo'), (None, 'quux')], + includefiles=['baz', 'foo', 'quux']) + + # give both docname and glob (case1) + text = (".. toctree::\n" + " :glob:\n" + "\n" + " foo\n" + " *\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'foo'), (None, 'baz'), (None, 'quux')], + includefiles=['foo', 'baz', 'quux']) + + # give both docname and glob (case2) + text = (".. toctree::\n" + " :glob:\n" + "\n" + " *\n" + " foo\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'baz'), (None, 'foo'), (None, 'quux'), (None, 'foo')], + includefiles=['baz', 'foo', 'quux', 'foo']) + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_toctree_glob_and_url(app): + text = (".. toctree::\n" + " :glob:\n" + "\n" + " https://example.com/?q=sphinx\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'https://example.com/?q=sphinx')], + includefiles=[]) + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_reversed_toctree(app): + text = (".. toctree::\n" + " :reversed:\n" + "\n" + " foo\n" + " bar/index\n" + " baz\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'baz'), (None, 'bar/index'), (None, 'foo')], + includefiles=['baz', 'bar/index', 'foo']) + + +@pytest.mark.sphinx(testroot='toctree-glob') +def test_toctree_twice(app): + text = (".. toctree::\n" + "\n" + " foo\n" + " foo\n") + + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) + assert_node(doctree[0][0], + entries=[(None, 'foo'), (None, 'foo')], + includefiles=['foo', 'foo']) + + +@pytest.mark.sphinx(testroot='directive-include') +def test_include_include_read_event(app): + sources_reported = [] + + def source_read_handler(_app, relative_path, parent_docname, source): + sources_reported.append((relative_path, parent_docname, source[0])) + + app.connect("include-read", source_read_handler) + text = """\ +.. include:: baz/baz.rst + :start-line: 4 +.. include:: text.txt + :literal: +.. include:: bar.txt +""" + app.env.find_files(app.config, app.builder) + restructuredtext.parse(app, text, 'index') + + included_files = {filename.as_posix() + for filename, p, s in sources_reported} + assert 'index.rst' not in included_files # sources don't emit 'include-read' + assert 'baz/baz.rst' in included_files + assert 'text.txt' not in included_files # text was included as literal, no rst parsing + assert 'bar.txt' in included_files # suffix not in source-suffixes + assert (Path('baz/baz.rst'), 'index', '\nBaz was here.') in sources_reported + + +@pytest.mark.sphinx(testroot='directive-include') +def test_include_include_read_event_nested_includes(app): + + def source_read_handler(_app, _relative_path, _parent_docname, source): + text = source[0].replace("#magical", "amazing") + source[0] = text + + app.connect("include-read", source_read_handler) + text = ".. include:: baz/baz.rst\n" + app.env.find_files(app.config, app.builder) + doctree = restructuredtext.parse(app, text, 'index') + assert_node(doctree, addnodes.document) + assert len(doctree.children) == 3 + assert_node(doctree.children[1], nodes.paragraph) + assert doctree.children[1].rawsource == "The amazing foo." diff --git a/tests/test_directives/test_directive_patch.py b/tests/test_directives/test_directive_patch.py new file mode 100644 index 0000000..f4eb8f9 --- /dev/null +++ b/tests/test_directives/test_directive_patch.py @@ -0,0 +1,110 @@ +"""Test the patched directives.""" + +import pytest +from docutils import nodes + +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +def test_code_directive(app): + # normal case + text = ('.. code::\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="default", highlight_args={}) + + # with language + text = ('.. code:: python\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", highlight_args={}) + + # :number-lines: option + text = ('.. code:: python\n' + ' :number-lines:\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", linenos=True, highlight_args={}) + + # :number-lines: option + text = ('.. code:: python\n' + ' :number-lines: 5\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", linenos=True, highlight_args={'linenostart': 5}) + + +@pytest.mark.sphinx(testroot='directive-csv-table') +def test_csv_table_directive(app): + # relative path from current document + text = ('.. csv-table::\n' + ' :file: example.csv\n') + doctree = restructuredtext.parse(app, text, docname="subdir/index") + assert_node(doctree, + ([nodes.table, nodes.tgroup, (nodes.colspec, + nodes.colspec, + nodes.colspec, + [nodes.tbody, nodes.row])],)) + assert_node(doctree[0][0][3][0], + ([nodes.entry, nodes.paragraph, "FOO"], + [nodes.entry, nodes.paragraph, "BAR"], + [nodes.entry, nodes.paragraph, "BAZ"])) + + # absolute path from source directory + text = ('.. csv-table::\n' + ' :file: /example.csv\n') + doctree = restructuredtext.parse(app, text, docname="subdir/index") + assert_node(doctree, + ([nodes.table, nodes.tgroup, (nodes.colspec, + nodes.colspec, + nodes.colspec, + [nodes.tbody, nodes.row])],)) + assert_node(doctree[0][0][3][0], + ([nodes.entry, nodes.paragraph, "foo"], + [nodes.entry, nodes.paragraph, "bar"], + [nodes.entry, nodes.paragraph, "baz"])) + + +def test_math_directive(app): + # normal case + text = '.. math:: E = mc^2' + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.math_block, 'E = mc^2\n\n']) + + # :name: option + text = ('.. math:: E = mc^2\n' + ' :name: eq1\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, (nodes.target, + [nodes.math_block, "E = mc^2\n\n"])]) + assert_node(doctree[1], nodes.math_block, docname='index', label="eq1", number=1) + + # :label: option + text = ('.. math:: E = mc^2\n' + ' :label: eq2\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, (nodes.target, + [nodes.math_block, 'E = mc^2\n\n'])]) + assert_node(doctree[1], nodes.math_block, docname='index', label="eq2", number=2) + + # :label: option without value + text = ('.. math:: E = mc^2\n' + ' :label:\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, (nodes.target, + [nodes.math_block, 'E = mc^2\n\n'])]) + assert_node(doctree[1], nodes.math_block, ids=['equation-index-0'], + docname='index', label="index:0", number=3) diff --git a/tests/test_directives/test_directives_no_typesetting.py b/tests/test_directives/test_directives_no_typesetting.py new file mode 100644 index 0000000..fd101fb --- /dev/null +++ b/tests/test_directives/test_directives_no_typesetting.py @@ -0,0 +1,108 @@ +"""Tests the directives""" + +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + +DOMAINS = [ + # directive, no-index, no-index-entry, signature of f, signature of g, index entry of g + ('c:function', False, True, 'void f()', 'void g()', ('single', 'g (C function)', 'c.g', '', None)), + ('cpp:function', False, True, 'void f()', 'void g()', ('single', 'g (C++ function)', '_CPPv41gv', '', None)), + ('js:function', True, True, 'f()', 'g()', ('single', 'g() (built-in function)', 'g', '', None)), + ('py:function', True, True, 'f()', 'g()', ('pair', 'built-in function; g()', 'g', '', None)), + ('rst:directive', True, False, 'f', 'g', ('single', 'g (directive)', 'directive-g', '', None)), + ('cmdoption', True, False, 'f', 'g', ('pair', 'command line option; g', 'cmdoption-arg-g', '', None)), + ('envvar', True, False, 'f', 'g', ('single', 'environment variable; g', 'envvar-g', '', None)), +] + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + text = (f'.. {directive}:: {sig_f}\n' + f' :no-typesetting:\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, nodes.target)) + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting_twice(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + text = (f'.. {directive}:: {sig_f}\n' + f' :no-typesetting:\n' + f'.. {directive}:: {sig_g}\n' + f' :no-typesetting:\n') + doctree = restructuredtext.parse(app, text) + # Note that all index nodes come before the target nodes + assert_node(doctree, (addnodes.index, addnodes.index, nodes.target, nodes.target)) + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting_noindex_orig(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + if not no_index: + pytest.skip(f'{directive} does not support :no-index: option') + text = (f'.. {directive}:: {sig_f}\n' + f' :no-index:\n' + f'.. {directive}:: {sig_g}\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, addnodes.desc, addnodes.index, addnodes.desc)) + assert_node(doctree[2], addnodes.index, entries=[index_g]) + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting_noindex(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + if not no_index: + pytest.skip(f'{directive} does not support :no-index: option') + text = (f'.. {directive}:: {sig_f}\n' + f' :no-index:\n' + f' :no-typesetting:\n' + f'.. {directive}:: {sig_g}\n' + f' :no-typesetting:\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, addnodes.index, nodes.target)) + assert_node(doctree[0], addnodes.index, entries=[]) + assert_node(doctree[1], addnodes.index, entries=[index_g]) + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting_no_index_entry(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + if not no_index_entry: + pytest.skip(f'{directive} does not support :no-index-entry: option') + text = (f'.. {directive}:: {sig_f}\n' + f' :no-index-entry:\n' + f' :no-typesetting:\n' + f'.. {directive}:: {sig_g}\n' + f' :no-typesetting:\n') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, addnodes.index, nodes.target, nodes.target)) + assert_node(doctree[0], addnodes.index, entries=[]) + assert_node(doctree[1], addnodes.index, entries=[index_g]) + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting_code(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + text = (f'.. {directive}:: {sig_f}\n' + f' :no-typesetting:\n' + f'.. {directive}:: {sig_g}\n' + f' :no-typesetting:\n' + f'.. code::\n' + f'\n' + f' code\n') + doctree = restructuredtext.parse(app, text) + # Note that all index nodes come before the targets + assert_node(doctree, (addnodes.index, addnodes.index, nodes.target, nodes.target, nodes.literal_block)) + + +@pytest.mark.parametrize(('directive', 'no_index', 'no_index_entry', 'sig_f', 'sig_g', 'index_g'), DOMAINS) +def test_object_description_no_typesetting_heading(app, directive, no_index, no_index_entry, sig_f, sig_g, index_g): + text = (f'.. {directive}:: {sig_f}\n' + f' :no-typesetting:\n' + f'.. {directive}:: {sig_g}\n' + f' :no-typesetting:\n' + f'\n' + f'Heading\n' + f'=======\n') + doctree = restructuredtext.parse(app, text) + # Note that all index nodes come before the targets and the heading is floated before those. + assert_node(doctree, (nodes.title, addnodes.index, addnodes.index, nodes.target, nodes.target)) |