summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES1
-rw-r--r--doc/ext/viewcode.rst35
-rw-r--r--sphinx/ext/viewcode.py32
-rw-r--r--tests/roots/test-ext-viewcode-find/conf.py9
-rw-r--r--tests/roots/test-ext-viewcode-find/index.rst38
-rw-r--r--tests/roots/test-ext-viewcode-find/not_a_package/__init__.py3
-rw-r--r--tests/roots/test-ext-viewcode-find/not_a_package/submodule.py30
-rw-r--r--tests/roots/test-ext-viewcode/conf.py4
-rw-r--r--tests/test_ext_viewcode.py43
9 files changed, 174 insertions, 21 deletions
diff --git a/CHANGES b/CHANGES
index aa7d8cc65..c1a28ae67 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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
----------
diff --git a/doc/ext/viewcode.rst b/doc/ext/viewcode.rst
index 5823090f6..9ea1262fa 100644
--- a/doc/ext/viewcode.rst
+++ b/doc/ext/viewcode.rst
@@ -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.
diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py
index d20ad78d5..f1ae4bf57 100644
--- a/sphinx/ext/viewcode.py
+++ b/sphinx/ext/viewcode.py
@@ -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,
diff --git a/tests/roots/test-ext-viewcode-find/conf.py b/tests/roots/test-ext-viewcode-find/conf.py
new file mode 100644
index 000000000..c23c0762c
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find/conf.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+
+extensions = ['sphinx.ext.viewcode']
+master_doc = 'index'
+exclude_patterns = ['_build']
+viewcode_import = False
diff --git a/tests/roots/test-ext-viewcode-find/index.rst b/tests/roots/test-ext-viewcode-find/index.rst
new file mode 100644
index 000000000..7eb416ac3
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find/index.rst
@@ -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
diff --git a/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py b/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py
new file mode 100644
index 000000000..4a1d689e5
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find/not_a_package/__init__.py
@@ -0,0 +1,3 @@
+from __future__ import absolute_import
+
+from .submodule import func1, Class1 # NOQA
diff --git a/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py b/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py
new file mode 100644
index 000000000..fb697ab75
--- /dev/null
+++ b/tests/roots/test-ext-viewcode-find/not_a_package/submodule.py
@@ -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"""
diff --git a/tests/roots/test-ext-viewcode/conf.py b/tests/roots/test-ext-viewcode/conf.py
index 08522791b..53ce4f7ce 100644
--- a/tests/roots/test-ext-viewcode/conf.py
+++ b/tests/roots/test-ext-viewcode/conf.py
@@ -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']
diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py
index 3b7dbdafc..4676f488f 100644
--- a/tests/test_ext_viewcode.py
+++ b/tests/test_ext_viewcode.py
@@ -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