mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
262 lines
10 KiB
Python
262 lines
10 KiB
Python
"""
|
|
sphinx.ext.viewcode
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
Add links to module code in Python object descriptions.
|
|
|
|
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
|
|
import traceback
|
|
import warnings
|
|
from typing import Any, Dict, Iterable, Iterator, Set, Tuple
|
|
|
|
from docutils import nodes
|
|
from docutils.nodes import Element, Node
|
|
|
|
import sphinx
|
|
from sphinx import addnodes
|
|
from sphinx.application import Sphinx
|
|
from sphinx.config import Config
|
|
from sphinx.deprecation import RemovedInSphinx30Warning
|
|
from sphinx.environment import BuildEnvironment
|
|
from sphinx.locale import _, __
|
|
from sphinx.pycode import ModuleAnalyzer
|
|
from sphinx.util import get_full_modname, logging, status_iterator
|
|
from sphinx.util.nodes import make_refnode
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str:
|
|
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.
|
|
logger.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.
|
|
logger.verbose(traceback.format_exc().rstrip())
|
|
logger.verbose('viewcode can\'t import %s, failed with error "%s"', modname, e)
|
|
return None
|
|
|
|
|
|
def doctree_read(app: Sphinx, doctree: Node) -> None:
|
|
env = app.builder.env
|
|
if not hasattr(env, '_viewcode_modules'):
|
|
env._viewcode_modules = {} # type: ignore
|
|
if app.builder.name == "singlehtml":
|
|
return
|
|
if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub:
|
|
return
|
|
|
|
def has_tag(modname, fullname, docname, refname):
|
|
entry = env._viewcode_modules.get(modname, None) # type: ignore
|
|
if entry is False:
|
|
return
|
|
|
|
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
|
|
|
|
analyzer.find_tags()
|
|
code = analyzer.code
|
|
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:
|
|
used[fullname] = docname
|
|
return True
|
|
|
|
for objnode in doctree.traverse(addnodes.desc):
|
|
if objnode.get('domain') != 'py':
|
|
continue
|
|
names = set() # type: Set[str]
|
|
for signode in objnode:
|
|
if not isinstance(signode, addnodes.desc_signature):
|
|
continue
|
|
modname = signode.get('module')
|
|
fullname = signode.get('fullname')
|
|
refname = modname
|
|
if env.config.viewcode_follow_imported_members:
|
|
new_modname = app.emit_firstresult(
|
|
'viewcode-follow-imported', modname, fullname,
|
|
)
|
|
if not new_modname:
|
|
new_modname = _get_full_modname(app, modname, fullname)
|
|
modname = new_modname
|
|
if not modname:
|
|
continue
|
|
fullname = signode.get('fullname')
|
|
if not has_tag(modname, fullname, env.docname, refname):
|
|
continue
|
|
if fullname in names:
|
|
# only one link per name, please
|
|
continue
|
|
names.add(fullname)
|
|
pagename = '_modules/' + modname.replace('.', '/')
|
|
inline = nodes.inline('', _('[source]'), classes=['viewcode-link'])
|
|
onlynode = addnodes.only(expr='html')
|
|
onlynode += addnodes.pending_xref('', inline, reftype='viewcode', refdomain='std',
|
|
refexplicit=False, reftarget=pagename,
|
|
refid=fullname, refdoc=env.docname)
|
|
signode += onlynode
|
|
|
|
|
|
def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
|
|
other: BuildEnvironment) -> None:
|
|
if not hasattr(other, '_viewcode_modules'):
|
|
return
|
|
# create a _viewcode_modules dict on the main environment
|
|
if not hasattr(env, '_viewcode_modules'):
|
|
env._viewcode_modules = {} # type: ignore
|
|
# now merge in the information from the subprocess
|
|
env._viewcode_modules.update(other._viewcode_modules) # type: ignore
|
|
|
|
|
|
def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node
|
|
) -> Node:
|
|
# resolve our "viewcode" reference nodes -- they need special treatment
|
|
if node['reftype'] == 'viewcode':
|
|
return make_refnode(app.builder, node['refdoc'], node['reftarget'],
|
|
node['refid'], contnode)
|
|
|
|
return None
|
|
|
|
|
|
def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]:
|
|
env = app.builder.env
|
|
if not hasattr(env, '_viewcode_modules'):
|
|
return
|
|
highlighter = app.builder.highlighter # type: ignore
|
|
urito = app.builder.get_relative_uri
|
|
|
|
modnames = set(env._viewcode_modules) # type: ignore
|
|
|
|
for modname, entry in status_iterator(
|
|
sorted(env._viewcode_modules.items()), # type: ignore
|
|
__('highlighting module code... '), "blue",
|
|
len(env._viewcode_modules), # type: ignore
|
|
app.verbosity, lambda x: x[0]):
|
|
if not entry:
|
|
continue
|
|
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
|
|
if env.config.highlight_language in ('python3', 'default', 'none'):
|
|
lexer = env.config.highlight_language
|
|
else:
|
|
lexer = 'python'
|
|
highlighted = highlighter.highlight_block(code, lexer, linenos=False)
|
|
# split the code into lines
|
|
lines = highlighted.splitlines()
|
|
# split off wrap markup from the first line of the actual code
|
|
before, after = lines[0].split('<pre>')
|
|
lines[0:1] = [before + '<pre>', after]
|
|
# nothing to do for the last line; it always starts with </pre> anyway
|
|
# now that we have code lines (starting at index 1), insert anchors for
|
|
# the collected tags (HACK: this only works if the tag boundaries are
|
|
# properly nested!)
|
|
maxindex = len(lines) - 1
|
|
for name, docname in used.items():
|
|
type, start, end = tags[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]')) +
|
|
lines[start])
|
|
lines[min(end, maxindex)] += '</div>'
|
|
# try to find parents (for submodules)
|
|
parents = []
|
|
parent = modname
|
|
while '.' in parent:
|
|
parent = parent.rsplit('.', 1)[0]
|
|
if parent in modnames:
|
|
parents.append({
|
|
'link': urito(pagename, '_modules/' +
|
|
parent.replace('.', '/')),
|
|
'title': parent})
|
|
parents.append({'link': urito(pagename, '_modules/index'),
|
|
'title': _('Module code')})
|
|
parents.reverse()
|
|
# putting it all together
|
|
context = {
|
|
'parents': parents,
|
|
'title': modname,
|
|
'body': (_('<h1>Source code for %s</h1>') % modname +
|
|
'\n'.join(lines)),
|
|
}
|
|
yield (pagename, context, 'page.html')
|
|
|
|
if not modnames:
|
|
return
|
|
|
|
html = ['\n']
|
|
# the stack logic is needed for using nested lists for submodules
|
|
stack = ['']
|
|
for modname in sorted(modnames):
|
|
if modname.startswith(stack[-1]):
|
|
stack.append(modname + '.')
|
|
html.append('<ul>')
|
|
else:
|
|
stack.pop()
|
|
while not modname.startswith(stack[-1]):
|
|
stack.pop()
|
|
html.append('</ul>')
|
|
stack.append(modname + '.')
|
|
html.append('<li><a href="%s">%s</a></li>\n' % (
|
|
urito('_modules/index', '_modules/' + modname.replace('.', '/')),
|
|
modname))
|
|
html.append('</ul>' * (len(stack) - 1))
|
|
context = {
|
|
'title': _('Overview: module code'),
|
|
'body': (_('<h1>All modules for which code is available</h1>') +
|
|
''.join(html)),
|
|
}
|
|
|
|
yield ('_modules/index', context, 'page.html')
|
|
|
|
|
|
def migrate_viewcode_import(app: Sphinx, config: Config) -> None:
|
|
if config.viewcode_import is not None:
|
|
warnings.warn('viewcode_import was renamed to viewcode_follow_imported_members. '
|
|
'Please update your configuration.',
|
|
RemovedInSphinx30Warning, stacklevel=2)
|
|
|
|
|
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
|
app.add_config_value('viewcode_import', None, False)
|
|
app.add_config_value('viewcode_enable_epub', False, False)
|
|
app.add_config_value('viewcode_follow_imported_members', True, False)
|
|
app.connect('config-inited', migrate_viewcode_import)
|
|
app.connect('doctree-read', doctree_read)
|
|
app.connect('env-merge-info', env_merge_info)
|
|
app.connect('html-collect-pages', collect_pages)
|
|
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')
|
|
app.add_event('viewcode-follow-imported')
|
|
return {
|
|
'version': sphinx.__display_version__,
|
|
'env_version': 1,
|
|
'parallel_read_safe': True
|
|
}
|