diff --git a/doc/usage/extensions/viewcode.rst b/doc/usage/extensions/viewcode.rst index d3c3c44fbd5..4030b92a1c0 100644 --- a/doc/usage/extensions/viewcode.rst +++ b/doc/usage/extensions/viewcode.rst @@ -73,6 +73,14 @@ Configuration `epubcheck `_'s score becomes worse even if the reader supports. +.. confval:: viewcode_show_lineos + + If this is ``True``, viewcode extension will display line numbers on the page. + + The default is ``False``. + + .. versionadded:: 6.2 + .. event:: viewcode-find-source (app, modname) .. versionadded:: 1.8 diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 4bef39726b6..970fe98f0ea 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -250,12 +250,16 @@ def collect_pages(app: Sphinx) -> Generator[tuple[str, dict[str, Any], str], Non lexer = env.config.highlight_language else: lexer = 'python' - highlighted = highlighter.highlight_block(code, lexer, linenos=False) + show_lineos = env.config.viewcode_show_lineos + highlighted = highlighter.highlight_block(code, lexer, linenos=show_lineos) # split the code into lines lines = highlighted.splitlines() + # find the first line of code. 0 if it's not nested in a table + # skip all row number and search for 2nd if lineos are shown (class="code") + offset = next((i for i, line in enumerate(lines) if '' in line), 0) # split off wrap markup from the first line of the actual code - before, after = lines[0].split('
')
-        lines[0:1] = [before + '
', after]
+        before, after = lines[offset].split('
')
+        lines[offset:offset+1] = [before + '
', after]
         # nothing to do for the last line; it always starts with 
anyway # now that we have code lines (starting at index 1), insert anchors for # the collected tags (HACK: this only works if the tag boundaries are @@ -264,11 +268,11 @@ def collect_pages(app: Sphinx) -> Generator[tuple[str, dict[str, Any], str], Non for name, docname in used.items(): type, start, end = tags[name] backlink = urito(pagename, docname) + '#' + refname + '.' + name - lines[start] = ( + lines[start+offset] = ( '
%s' % (name, backlink, _('[docs]')) + - lines[start]) - lines[min(end, maxindex)] += '
' + lines[start+offset]) + lines[min(end+offset, maxindex)] += '' # try to find parents (for submodules) parents = [] parent = modname @@ -324,6 +328,7 @@ def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value('viewcode_import', None, False) app.add_config_value('viewcode_enable_epub', False, False) app.add_config_value('viewcode_follow_imported_members', True, False) + app.add_config_value('viewcode_show_lineos', False, False) app.connect('doctree-read', doctree_read) app.connect('env-merge-info', env_merge_info) app.connect('env-purge-doc', env_purge_doc) diff --git a/tests/roots/test-ext-viewcode-lineos/conf.py b/tests/roots/test-ext-viewcode-lineos/conf.py new file mode 100644 index 00000000000..2f142f813b3 --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/conf.py @@ -0,0 +1,9 @@ +import os +import sys + +source_dir = os.path.abspath('.') +if source_dir not in sys.path: + sys.path.insert(0, source_dir) +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +exclude_patterns = ['_build'] +viewcode_show_lineos = True diff --git a/tests/roots/test-ext-viewcode-lineos/index.rst b/tests/roots/test-ext-viewcode-lineos/index.rst new file mode 100644 index 00000000000..e7956e723b7 --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/index.rst @@ -0,0 +1,39 @@ +viewcode +======== + +.. py:module:: spam + +.. autofunction:: func1 + +.. autofunction:: func2 + +.. autofunction:: spam.mod1.func1 + +.. autofunction:: spam.mod2.func2 + +.. autofunction:: Class1 + +.. autofunction:: Class2 + +.. autofunction:: spam.mod1.Class1 + +.. autofunction:: spam.mod2.Class2 + + +.. literalinclude:: spam/__init__.py + :language: python + :pyobject: func1 + +.. literalinclude:: spam/mod1.py + :language: python + :pyobject: func1 + +.. autoclass:: spam.mod3.Class3 + :members: + +.. automodule:: spam.mod3 + :members: + +.. toctree:: + + objects diff --git a/tests/roots/test-ext-viewcode-lineos/objects.rst b/tests/roots/test-ext-viewcode-lineos/objects.rst new file mode 100644 index 00000000000..42f9047667e --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/objects.rst @@ -0,0 +1,169 @@ +Testing object descriptions +=========================== + +.. function:: func_without_module(a, b, *c[, d]) + + Does something. + +.. function:: func_without_body() + +.. function:: func_noindex + :noindex: + +.. function:: func_with_module + :module: foolib + +Referring to :func:`func with no index `. +Referring to :func:`nothing <>`. + +.. module:: mod + :synopsis: Module synopsis. + :platform: UNIX + +.. function:: func_in_module + +.. class:: Cls + + .. method:: meth1 + + .. staticmethod:: meths + + .. attribute:: attr + +.. explicit class given +.. method:: Cls.meth2 + +.. explicit module given +.. exception:: Error(arg1, arg2) + :module: errmod + +.. data:: var + + +.. currentmodule:: None + +.. function:: func_without_module2() -> annotation + +.. object:: long(parameter, \ + list) + another one + +.. class:: TimeInt + + Has only one parameter (triggers special behavior...) + + :param moo: |test| + :type moo: |test| + +.. |test| replace:: Moo + +.. class:: Time(hour, minute, isdst) + + :param year: The year. + :type year: TimeInt + :param TimeInt minute: The minute. + :param isdst: whether it's DST + :type isdst: * some complex + * expression + :returns: a new :class:`Time` instance + :rtype: :class:`Time` + :raises ValueError: if the values are out of range + :ivar int hour: like *hour* + :ivar minute: like *minute* + :vartype minute: int + :param hour: Some parameter + :type hour: DuplicateType + :param hour: Duplicate param. Should not lead to crashes. + :type hour: DuplicateType + :param .Cls extcls: A class from another module. + + +C items +======= + +.. c:function:: Sphinx_DoSomething() + +.. c:member:: SphinxStruct.member + +.. c:macro:: SPHINX_USE_PYTHON + +.. c:type:: SphinxType + +.. c:var:: sphinx_global + + +Javascript items +================ + +.. js:function:: foo() + +.. js:data:: bar + +.. documenting the method of any object +.. js:function:: bar.baz(href, callback[, errback]) + + :param string href: The location of the resource. + :param callback: Gets called with the data returned by the resource. + :throws InvalidHref: If the `href` is invalid. + :returns: `undefined` + +.. js:attribute:: bar.spam + +References +========== + +Referencing :class:`mod.Cls` or :Class:`mod.Cls` should be the same. + +With target: :c:func:`Sphinx_DoSomething()` (parentheses are handled), +:c:member:`SphinxStruct.member`, :c:macro:`SPHINX_USE_PYTHON`, +:c:type:`SphinxType *` (pointer is handled), :c:data:`sphinx_global`. + +Without target: :c:func:`CFunction`. :c:func:`!malloc`. + +:js:func:`foo()` +:js:func:`foo` + +:js:data:`bar` +:js:func:`bar.baz()` +:js:func:`bar.baz` +:js:func:`~bar.baz()` + +:js:attr:`bar.baz` + + +Others +====== + +.. envvar:: HOME + +.. program:: python + +.. cmdoption:: -c command + +.. program:: perl + +.. cmdoption:: -c + +.. option:: +p + +Link to :option:`perl +p`. + + +User markup +=========== + +.. userdesc:: myobj:parameter + + Description of userdesc. + + +Referencing :userdescrole:`myobj`. + + +CPP domain +========== + +.. cpp:class:: n::Array + + .. cpp:function:: T& operator[]( unsigned j ) + const T& operator[]( unsigned j ) const diff --git a/tests/roots/test-ext-viewcode-lineos/spam/__init__.py b/tests/roots/test-ext-viewcode-lineos/spam/__init__.py new file mode 100644 index 00000000000..62190421f26 --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/spam/__init__.py @@ -0,0 +1,2 @@ +from .mod1 import Class1, func1 +from .mod2 import Class2, func2 diff --git a/tests/roots/test-ext-viewcode-lineos/spam/mod1.py b/tests/roots/test-ext-viewcode-lineos/spam/mod1.py new file mode 100644 index 00000000000..a078328c283 --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/spam/mod1.py @@ -0,0 +1,30 @@ +""" +mod1 +""" + + +def decorator(f): + return f + + +@decorator +def func1(a, b): + """ + this is func1 + """ + return a, b + + +@decorator +class Class1: + """ + this is Class1 + """ + + +class Class3: + """ + this is Class3 + """ + class_attr = 42 + """this is the class attribute class_attr""" diff --git a/tests/roots/test-ext-viewcode-lineos/spam/mod2.py b/tests/roots/test-ext-viewcode-lineos/spam/mod2.py new file mode 100644 index 00000000000..72cb0897815 --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/spam/mod2.py @@ -0,0 +1,22 @@ +""" +mod2 +""" + + +def decorator(f): + return f + + +@decorator +def func2(a, b): + """ + this is func2 + """ + return a, b + + +@decorator +class Class2: + """ + this is Class2 + """ diff --git a/tests/roots/test-ext-viewcode-lineos/spam/mod3.py b/tests/roots/test-ext-viewcode-lineos/spam/mod3.py new file mode 100644 index 00000000000..812c9b58643 --- /dev/null +++ b/tests/roots/test-ext-viewcode-lineos/spam/mod3.py @@ -0,0 +1,3 @@ +from spam.mod1 import Class3 + +__all__ = ('Class3',) diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py index 6f2c9ce8025..231245ae1ef 100644 --- a/tests/test_ext_viewcode.py +++ b/tests/test_ext_viewcode.py @@ -5,9 +5,54 @@ import pygments import pytest - @pytest.mark.sphinx(testroot='ext-viewcode') def test_viewcode(app, status, warning): + + app.builder.build_all() + + warnings = re.sub(r'\\+', '/', warning.getvalue()) + assert re.findall( + r"index.rst:\d+: WARNING: Object named 'func1' not found in include " + + r"file .*/spam/__init__.py'", + warnings, + ) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert result.count('href="_modules/spam/mod1.html#func1"') == 2 + assert result.count('href="_modules/spam/mod2.html#func2"') == 2 + assert result.count('href="_modules/spam/mod1.html#Class1"') == 2 + assert result.count('href="_modules/spam/mod2.html#Class2"') == 2 + assert result.count('@decorator') == 1 + + # test that the class attribute is correctly documented + assert result.count('this is Class3') == 2 + assert 'this is the class attribute class_attr' in result + # the next assert fails, until the autodoc bug gets fixed + assert result.count('this is the class attribute class_attr') == 2 + + result = (app.outdir / '_modules/spam/mod1.html').read_text(encoding='utf8') + result = re.sub('', '', result) # filter pygments classes + if pygments.__version__ >= '2.14.0': + assert ('
[docs]' + '@decorator\n' + 'class Class1:\n' + ' """\n' + ' this is Class1\n' + ' """
\n') in result + else: + assert ('
[docs]' + '@decorator\n' + 'class Class1:\n' + ' """\n' + ' this is Class1\n' + ' """
\n') in result + +@pytest.mark.sphinx(testroot='ext-viewcode-lineos') +def test_viewcode_lineos(app, status, warning): + + assert app.config["viewcode_show_lineos"] is True app.builder.build_all() warnings = re.sub(r'\\+', '/', warning.getvalue()) @@ -49,6 +94,7 @@ def test_viewcode(app, status, warning): ' this is Class1\n' ' """\n') in result + assert '' in result @pytest.mark.sphinx('epub', testroot='ext-viewcode') def test_viewcode_epub_default(app, status, warning):