mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
:mod:~sphinx.ext.viewcode
support imported function/class aliases. Closes #623
This commit is contained in:
parent
de0b87be3e
commit
e8b870de0c
1
CHANGES
1
CHANGES
@ -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
|
||||
----------
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.]+)')
|
||||
|
||||
|
8
tests/roots/test-ext-viewcode/conf.py
Normal file
8
tests/roots/test-ext-viewcode/conf.py
Normal 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'
|
29
tests/roots/test-ext-viewcode/index.rst
Normal file
29
tests/roots/test-ext-viewcode/index.rst
Normal 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
|
5
tests/roots/test-ext-viewcode/spam/__init__.py
Normal file
5
tests/roots/test-ext-viewcode/spam/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from mod1 import func1, Class1
|
||||
from mod2 import (
|
||||
func2,
|
||||
Class2,
|
||||
)
|
15
tests/roots/test-ext-viewcode/spam/mod1.py
Normal file
15
tests/roots/test-ext-viewcode/spam/mod1.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
mod1
|
||||
"""
|
||||
|
||||
def func1(a, b):
|
||||
"""
|
||||
this is func1
|
||||
"""
|
||||
return a, b
|
||||
|
||||
|
||||
class Class1(object):
|
||||
"""
|
||||
this is Class1
|
||||
"""
|
15
tests/roots/test-ext-viewcode/spam/mod2.py
Normal file
15
tests/roots/test-ext-viewcode/spam/mod2.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""
|
||||
mod2
|
||||
"""
|
||||
|
||||
def func2(a, b):
|
||||
"""
|
||||
this is func2
|
||||
"""
|
||||
return a, b
|
||||
|
||||
|
||||
class Class2(object):
|
||||
"""
|
||||
this is Class2
|
||||
"""
|
40
tests/test_ext_viewcode.py
Normal file
40
tests/test_ext_viewcode.py
Normal 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
|
Loading…
Reference in New Issue
Block a user