:mod:~sphinx.ext.viewcode support imported function/class aliases. Closes #623

This commit is contained in:
Takayuki Shimizukawa 2014-08-22 11:38:56 +09:00
parent de0b87be3e
commit e8b870de0c
10 changed files with 180 additions and 6 deletions

View File

@ -55,6 +55,7 @@ Features added
* Automatically compile ``*.mo`` files from ``*.po`` files when
:confval:`gettext_auto_build` is True (default) and ``*.po`` is newer than
``*.mo`` file.
* #623: :mod:`~sphinx.ext.viewcode` support imported function/class aliases.
Bugs fixed
----------

View File

@ -18,3 +18,23 @@ from the source to the description will also be inserted.
There are currently no configuration values for this extension; you just need to
add ``'sphinx.ext.viewcode'`` to your :confval:`extensions` value for it to
work.
There is also an additional config value:
.. confval:: viewcode_import
If this is ``True``, viewcode extension will follow alias objects that
imported from another module such as functions, classes and attributes.
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

View File

@ -9,21 +9,43 @@
:license: BSD, see LICENSE for details.
"""
import traceback
from six import iteritems, text_type
from docutils import nodes
from sphinx import addnodes
from sphinx.locale import _
from sphinx.pycode import ModuleAnalyzer
from sphinx.util import get_full_modname
from sphinx.util.nodes import make_refnode
def _get_full_modname(app, modname, attribute):
try:
return get_full_modname(modname, attribute)
except AttributeError:
# sphinx.ext.viewcode can't follow class instance attribute
# then AttributeError logging output only verbose mode.
app.verbose('Didn\'t find %s in %s' % (attribute, modname))
return None
except Exception as e:
# sphinx.ext.viewcode follow python domain directives.
# because of that, if there are no real modules exists that specified
# by py:function or other directives, viewcode emits a lot of warnings.
# It should be displayed only verbose mode.
app.verbose(traceback.format_exc().rstrip())
app.verbose('viewcode can\'t import %s, failed with error "%s"' %
(modname, e))
return None
def doctree_read(app, doctree):
env = app.builder.env
if not hasattr(env, '_viewcode_modules'):
env._viewcode_modules = {}
def has_tag(modname, fullname, docname):
def has_tag(modname, fullname, docname, refname):
entry = env._viewcode_modules.get(modname, None)
try:
analyzer = ModuleAnalyzer.for_module(modname)
@ -36,11 +58,11 @@ def doctree_read(app, doctree):
code = analyzer.code
if entry is None or entry[0] != code:
analyzer.find_tags()
entry = code, analyzer.tags, {}
entry = code, analyzer.tags, {}, refname
env._viewcode_modules[modname] = entry
elif entry is False:
return
code, tags, used = entry
_, tags, used, _ = entry
if fullname in tags:
used[fullname] = docname
return True
@ -53,10 +75,14 @@ def doctree_read(app, doctree):
if not isinstance(signode, addnodes.desc_signature):
continue
modname = signode.get('module')
fullname = signode.get('fullname')
refname = modname
if env.config.viewcode_import:
modname = _get_full_modname(app, modname, fullname)
if not modname:
continue
fullname = signode.get('fullname')
if not has_tag(modname, fullname, env.docname):
if not has_tag(modname, fullname, env.docname, refname):
continue
if fullname in names:
# only one link per name, please
@ -95,7 +121,7 @@ def collect_pages(app):
for modname, entry in iteritems(env._viewcode_modules):
if not entry:
continue
code, tags, used = entry
code, tags, used, refname = entry
# construct a page name for the highlighted source
pagename = '_modules/' + modname.replace('.', '/')
# highlight the source using the builder's highlighter
@ -112,7 +138,7 @@ def collect_pages(app):
maxindex = len(lines) - 1
for name, docname in iteritems(used):
type, start, end = tags[name]
backlink = urito(pagename, docname) + '#' + modname + '.' + name
backlink = urito(pagename, docname) + '#' + refname + '.' + name
lines[start] = (
'<div class="viewcode-block" id="%s"><a class="viewcode-back" '
'href="%s">%s</a>' % (name, backlink, _('[docs]'))
@ -171,6 +197,7 @@ def collect_pages(app):
def setup(app):
app.add_config_value('viewcode_import', True, False)
app.connect('doctree-read', doctree_read)
app.connect('html-collect-pages', collect_pages)
app.connect('missing-reference', missing_reference)

View File

@ -238,6 +238,20 @@ def get_module_source(modname):
return 'file', filename
def get_full_modname(modname, attribute):
__import__(modname)
module = sys.modules[modname]
# Allow an attribute to have multiple parts and incidentially allow
# repeated .s in the attribute.
value = module
for attr in attribute.split('.'):
if attr:
value = getattr(value, attr)
return getattr(value, '__module__', None)
# a regex to recognize coding cookies
_coding_re = re.compile(r'coding[:=]\s*([-\w.]+)')

View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
import sys
import os
sys.path.insert(0, os.path.abspath('.'))
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
master_doc = 'index'

View File

@ -0,0 +1,29 @@
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

View File

@ -0,0 +1,5 @@
from mod1 import func1, Class1
from mod2 import (
func2,
Class2,
)

View File

@ -0,0 +1,15 @@
"""
mod1
"""
def func1(a, b):
"""
this is func1
"""
return a, b
class Class1(object):
"""
this is Class1
"""

View File

@ -0,0 +1,15 @@
"""
mod2
"""
def func2(a, b):
"""
this is func2
"""
return a, b
class Class2(object):
"""
this is Class2
"""

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
test_ext_viewcode
~~~~~~~~~~~~~~~~~
Test sphinx.ext.viewcode extension.
:copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
from StringIO import StringIO
from util import test_roots, with_app
warnfile = StringIO()
root = test_roots / 'test-ext-viewcode'
doctreedir = root / '_build' / 'doctree'
def teardown_module():
(root / '_build').rmtree(True)
@with_app(srcdir=root, warning=warnfile)
def test_simple(app):
app.builder.build_all()
warnings = re.sub(r'\\+', '/', warnfile.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').text(encoding='utf-8')
assert result.count('href="_modules/spam/mod1.html#func1"') == 2
assert result.count('href="_modules/spam/mod2.html#func2"') == 2