diff options
author | Ashley Whetter <asw@dneg.com> | 2018-04-05 15:40:09 -0700 |
---|---|---|
committer | Ashley Whetter <asw@dneg.com> | 2018-04-24 14:45:16 -0700 |
commit | 44da51a5641caad93200a1cec6b276d0211f4e1f (patch) | |
tree | 74197077fea744a2cbbcd39fe77a990b2be5627e | |
parent | 0a0803fd450ec618f24a03cf9ef4309b9db1d36b (diff) | |
download | sphinx-git-44da51a5641caad93200a1cec6b276d0211f4e1f.tar.gz |
Plugins can find source code for viewcode
Fixes #4035
-rw-r--r-- | CHANGES | 1 | ||||
-rw-r--r-- | doc/ext/viewcode.rst | 35 | ||||
-rw-r--r-- | sphinx/ext/viewcode.py | 32 | ||||
-rw-r--r-- | tests/roots/test-ext-viewcode-find/conf.py | 9 | ||||
-rw-r--r-- | tests/roots/test-ext-viewcode-find/index.rst | 38 | ||||
-rw-r--r-- | tests/roots/test-ext-viewcode-find/not_a_package/__init__.py | 3 | ||||
-rw-r--r-- | tests/roots/test-ext-viewcode-find/not_a_package/submodule.py | 30 | ||||
-rw-r--r-- | tests/roots/test-ext-viewcode/conf.py | 4 | ||||
-rw-r--r-- | tests/test_ext_viewcode.py | 43 |
9 files changed, 174 insertions, 21 deletions
@@ -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 |