Plugins can find source code for viewcode

Fixes #4035
This commit is contained in:
Ashley Whetter
2018-04-05 15:40:09 -07:00
parent 0a0803fd45
commit 44da51a564
9 changed files with 174 additions and 21 deletions

View File

@@ -74,6 +74,7 @@ Features added
* Improve warning messages during including (refs: #4818)
* LaTeX: separate customizability of :rst:role:`guilabel` and
:rst:role:`menuselection` (refs: #4830)
* Add :event:`viewcode-find-source` event to viewcode extension.
Bugs fixed
----------

View File

@@ -15,6 +15,18 @@ a highlighted version of the source code, and a link will be added to all object
descriptions that leads to the source code of the described object. A link back
from the source to the description will also be inserted.
.. warning::
If :confval:`viewcode_import` is True,
or if the :event:`viewcode-find-source` event does not find source code
for the given module,
``viewcode`` will import the modules being linked to.
If any modules have side effects on import, these will be
executed by ``viewcode`` when ``sphinx-build`` is run.
If you document scripts (as opposed to library modules), make sure their
main routine is protected by a ``if __name__ == '__main__'`` condition.
This extension works only on HTML related builders like ``html``,
``applehelp``, ``devhelp``, ``htmlhelp``, ``qthelp`` and so on except
``singlehtml``. By default ``epub`` builder doesn't
@@ -29,15 +41,6 @@ There is an additional config value:
As side effects, this option
else they produce nothing. The default is ``True``.
.. warning::
:confval:`viewcode_import` **imports** the modules to be followed real
location. If any modules have side effects on import, these will be
executed by ``viewcode`` when ``sphinx-build`` is run.
If you document scripts (as opposed to library modules), make sure their
main routine is protected by a ``if __name__ == '__main__'`` condition.
.. versionadded:: 1.3
.. confval:: viewcode_enable_epub
@@ -62,3 +65,17 @@ There is an additional config value:
Some reader's rendering result are corrupted and
`epubcheck <https://github.com/IDPF/epubcheck>`_'s score
becomes worse even if the reader supports.
.. event:: viewcode-find-source (app, modname)
.. versionadded:: 1.8
Find the source code for a module.
An event handler for this event should return
a tuple of the source code itself and a dictionary of tags.
The dictionary maps the name of a class, function, attribute, etc
to a tuple of its type, the start line number, and the end line number.
The type should be one of "class", "def", or "other".
:param app: The Sphinx application object.
:param modname: The name of the module to find source code for.

View File

@@ -61,20 +61,29 @@ def doctree_read(app, doctree):
def has_tag(modname, fullname, docname, refname):
entry = env._viewcode_modules.get(modname, None) # type: ignore
try:
analyzer = ModuleAnalyzer.for_module(modname)
except Exception:
env._viewcode_modules[modname] = False # type: ignore
return
if not isinstance(analyzer.code, text_type):
code = analyzer.code.decode(analyzer.encoding)
else:
code = analyzer.code
if entry is False:
return
elif entry is None or entry[0] != code:
code_tags = app.emit_firstresult('viewcode-find-source', modname)
if code_tags is None:
try:
analyzer = ModuleAnalyzer.for_module(modname)
except Exception:
env._viewcode_modules[modname] = False # type: ignore
return
if not isinstance(analyzer.code, text_type):
code = analyzer.code.decode(analyzer.encoding)
else:
code = analyzer.code
analyzer.find_tags()
entry = code, analyzer.tags, {}, refname
tags = analyzer.tags
else:
code, tags = code_tags
if entry is None or entry[0] != code:
entry = code, tags, {}, refname
env._viewcode_modules[modname] = entry # type: ignore
_, tags, used, _ = entry
if fullname in tags:
@@ -240,6 +249,7 @@ def setup(app):
app.connect('missing-reference', missing_reference)
# app.add_config_value('viewcode_include_modules', [], 'env')
# app.add_config_value('viewcode_exclude_modules', [], 'env')
app.add_event('viewcode-find-source')
return {
'version': sphinx.__display_version__,
'env_version': 1,

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
import os
import sys
extensions = ['sphinx.ext.viewcode']
master_doc = 'index'
exclude_patterns = ['_build']
viewcode_import = False

View File

@@ -0,0 +1,38 @@
viewcode
========
.. py:module:: not_a_package
.. py:function:: func1(a, b)
This is func1
.. py:function:: not_a_package.submodule.func1(a, b)
This is func1
.. py:module:: not_a_package.submodule
.. py:class:: Class1
This is Class1
.. py:class:: Class3
This is Class3
.. py:class:: not_a_package.submodule.Class1
This is Class1
.. literalinclude:: not_a_package/__init__.py
:language: python
:pyobject: func1
.. literalinclude:: not_a_package/submodule.py
:language: python
:pyobject: func1
.. py:attribute:: not_a_package.submodule.Class3.class_attr
This is the class attribute class_attr

View File

@@ -0,0 +1,3 @@
from __future__ import absolute_import
from .submodule import func1, Class1 # NOQA

View File

@@ -0,0 +1,30 @@
"""
submodule
"""
raise RuntimeError('This module should not get imported')
def decorator(f):
return f
@decorator
def func1(a, b):
"""
this is func1
"""
return a, b
@decorator
class Class1(object):
"""
this is Class1
"""
class Class3(object):
"""
this is Class3
"""
class_attr = 42
"""this is the class attribute class_attr"""

View File

@@ -3,7 +3,9 @@
import os
import sys
sys.path.insert(0, os.path.abspath('.'))
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']
master_doc = 'index'
exclude_patterns = ['_build']

View File

@@ -60,3 +60,46 @@ def test_linkcode(app, status, warning):
assert 'http://foobar/js/' in stuff
assert 'http://foobar/c/' in stuff
assert 'http://foobar/cpp/' in stuff
@pytest.mark.sphinx(testroot='ext-viewcode-find')
def test_local_source_files(app, status, warning):
def find_source(app, modname):
if modname == 'not_a_package':
source = (app.srcdir / 'not_a_package/__init__.py').text()
tags = {
'func1': ('def', 3, 3),
'Class1': ('class', 3, 3),
'not_a_package.submodule.func1': ('def', 3, 3),
'not_a_package.submodule.Class1': ('class', 3, 3),
}
else:
source = (app.srcdir / 'not_a_package/submodule.py').text()
tags = {
'not_a_package.submodule.func1': ('def', 11, 15),
'Class1': ('class', 19, 22),
'not_a_package.submodule.Class1': ('class', 19, 22),
'Class3': ('class', 25, 30),
'not_a_package.submodule.Class3.class_attr': ('other', 29, 29),
}
return (source, tags)
app.connect('viewcode-find-source', find_source)
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 .*/not_a_package/__init__.py'",
warnings
)
result = (app.outdir / 'index.html').text(encoding='utf-8')
assert result.count('href="_modules/not_a_package.html#func1"') == 1
assert result.count('href="_modules/not_a_package.html#not_a_package.submodule.func1"') == 1
assert result.count('href="_modules/not_a_package/submodule.html#Class1"') == 1
assert result.count('href="_modules/not_a_package/submodule.html#Class3"') == 1
assert result.count('href="_modules/not_a_package/submodule.html#not_a_package.submodule.Class1"') == 1
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