From d5f8e468fb48575a3dd8acc17bde62cf67641023 Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Sun, 19 Feb 2023 12:08:45 +0100 Subject: [PATCH 1/6] set lineos to True for example --- sphinx/ext/viewcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 4bef39726b6..73b70e42cdd 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -250,7 +250,7 @@ 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) + highlighted = highlighter.highlight_block(code, lexer, linenos=True) # split the code into lines lines = highlighted.splitlines() # split off wrap markup from the first line of the actual code From f86f5cdefb6f35dabccabc93afc7abc2bcd08bcf Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Sun, 19 Feb 2023 17:45:28 +0100 Subject: [PATCH 2/6] add viwecode_show_lineos parameter --- sphinx/ext/viewcode.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 73b70e42cdd..6af85db3ad5 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -250,12 +250,15 @@ 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=True) + 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 + offset = next(i for i, line in enumerate(lines) if '' in line) if show_lineos else 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 +267,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 +327,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) From 0d962a5b7b0a2277f01316f56164d58e8151a4ea Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Sun, 19 Feb 2023 17:53:20 +0100 Subject: [PATCH 3/6] document viewcode_show_lineos usage --- doc/usage/extensions/viewcode.rst | 8 ++++++++ sphinx/ext/viewcode.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) 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 6af85db3ad5..970fe98f0ea 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -254,8 +254,9 @@ def collect_pages(app: Sphinx) -> Generator[tuple[str, dict[str, Any], str], Non highlighted = highlighter.highlight_block(code, lexer, linenos=show_lineos) # split the code into lines lines = highlighted.splitlines() - # find the first line of code - offset = next(i for i, line in enumerate(lines) if '' in line) if show_lineos else 0 + # 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[offset].split('
')
         lines[offset:offset+1] = [before + '
', after]

From 40484df7713078799cdd4a9963cf2e6c1e611ac3 Mon Sep 17 00:00:00 2001
From: Pierrick Rambaud 
Date: Sun, 19 Feb 2023 18:44:55 +0100
Subject: [PATCH 4/6] add test for viewcode lineos

---
 tests/test_ext_viewcode.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py
index 6f2c9ce8025..9fa209cb55c 100644
--- a/tests/test_ext_viewcode.py
+++ b/tests/test_ext_viewcode.py
@@ -124,3 +124,12 @@ def find_source(app, modname):
 
     assert result.count('href="_modules/not_a_package/submodule.html#not_a_package.submodule.Class3.class_attr"') == 1
     assert result.count('This is the class attribute class_attr') == 1
+
+@pytest.mark.sphinx(testroot='ext-viewcode', confoverrides={'viewcode_show_lineos': True})
+def test_viewcode_show_lineos(app, status, warning):
+    app.builder.build_all()
+
+    res = (app.outdir / '_modules/spam/mod1.html').read_text(encoding='utf8')
+    print(res)
+
+    assert '' in res

From 8a565ed8806ca7e94eb1dc84767d0070cc50dd24 Mon Sep 17 00:00:00 2001
From: Pierrick Rambaud 
Date: Sun, 19 Feb 2023 19:59:50 +0100
Subject: [PATCH 5/6] improve tests

---
 tests/roots/test-ext-viewcode-lineos/conf.py  |   9 +
 .../roots/test-ext-viewcode-lineos/index.rst  |  39 ++++
 .../test-ext-viewcode-lineos/objects.rst      | 169 ++++++++++++++++++
 .../test-ext-viewcode-lineos/spam/__init__.py |   2 +
 .../test-ext-viewcode-lineos/spam/mod1.py     |  30 ++++
 .../test-ext-viewcode-lineos/spam/mod2.py     |  22 +++
 .../test-ext-viewcode-lineos/spam/mod3.py     |   3 +
 tests/test_ext_viewcode.py                    |  57 ++++--
 tox.ini                                       |   2 +-
 9 files changed, 322 insertions(+), 11 deletions(-)
 create mode 100644 tests/roots/test-ext-viewcode-lineos/conf.py
 create mode 100644 tests/roots/test-ext-viewcode-lineos/index.rst
 create mode 100644 tests/roots/test-ext-viewcode-lineos/objects.rst
 create mode 100644 tests/roots/test-ext-viewcode-lineos/spam/__init__.py
 create mode 100644 tests/roots/test-ext-viewcode-lineos/spam/mod1.py
 create mode 100644 tests/roots/test-ext-viewcode-lineos/spam/mod2.py
 create mode 100644 tests/roots/test-ext-viewcode-lineos/spam/mod3.py

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 9fa209cb55c..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): @@ -124,12 +170,3 @@ def find_source(app, modname): assert result.count('href="_modules/not_a_package/submodule.html#not_a_package.submodule.Class3.class_attr"') == 1 assert result.count('This is the class attribute class_attr') == 1 - -@pytest.mark.sphinx(testroot='ext-viewcode', confoverrides={'viewcode_show_lineos': True}) -def test_viewcode_show_lineos(app, status, warning): - app.builder.build_all() - - res = (app.outdir / '_modules/spam/mod1.html').read_text(encoding='utf8') - print(res) - - assert '
' in res diff --git a/tox.ini b/tox.ini index 512aa5f74e5..77cd3c1c26f 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ setenv = PYTHONWARNINGS = error PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:} --color yes commands= - python -X dev -X warn_default_encoding -m pytest --durations 25 {posargs} + python -X dev -X warn_default_encoding -m pytest --durations 25 -vv {posargs} [testenv:docs] basepython = python3 From c696420a42a89110fce3448a75244666f2f94baf Mon Sep 17 00:00:00 2001 From: Pierrick Rambaud Date: Sun, 19 Feb 2023 20:01:26 +0100 Subject: [PATCH 6/6] remove -vv option from tox legacy addition from my side, I should have used posargs --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 77cd3c1c26f..512aa5f74e5 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ setenv = PYTHONWARNINGS = error PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:} --color yes commands= - python -X dev -X warn_default_encoding -m pytest --durations 25 -vv {posargs} + python -X dev -X warn_default_encoding -m pytest --durations 25 {posargs} [testenv:docs] basepython = python3