diff options
Diffstat (limited to 'sphinx/ext/viewcode.py')
-rw-r--r-- | sphinx/ext/viewcode.py | 137 |
1 files changed, 110 insertions, 27 deletions
diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index a2eeb7891..baf86dbbf 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -4,12 +4,15 @@ Add links to module code in Python object descriptions. - :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +import posixpath import traceback -from typing import Any, Dict, Iterable, Iterator, Set, Tuple +import warnings +from os import path +from typing import Any, Dict, Generator, Iterable, Optional, Set, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node @@ -17,17 +20,32 @@ from docutils.nodes import Element, Node import sphinx from sphinx import addnodes from sphinx.application import Sphinx +from sphinx.builders import Builder +from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer +from sphinx.transforms.post_transforms import SphinxPostTransform 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: +OUTPUT_DIRNAME = '_modules' + + +class viewcode_anchor(Element): + """Node for viewcode anchors. + + This node will be processed in the resolving phase. + For viewcode supported builders, they will be all converted to the anchors. + For not supported builders, they will be removed. + """ + + +def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> Optional[str]: try: return get_full_modname(modname, attribute) except AttributeError: @@ -45,14 +63,21 @@ def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> str: return None +def is_supported_builder(builder: Builder) -> bool: + if builder.format != 'html': + return False + elif builder.name == 'singlehtml': + return False + elif builder.name.startswith('epub') and not builder.config.viewcode_enable_epub: + return False + else: + return True + + 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: str, fullname: str, docname: str, refname: str) -> bool: entry = env._viewcode_modules.get(modname, None) # type: ignore @@ -109,13 +134,8 @@ def doctree_read(app: Sphinx, doctree: Node) -> None: # 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 + pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')) + signode += viewcode_anchor(reftarget=pagename, refid=fullname, refdoc=env.docname) def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str], @@ -129,22 +149,80 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str], env._viewcode_modules.update(other._viewcode_modules) # type: ignore +class ViewcodeAnchorTransform(SphinxPostTransform): + """Convert or remove viewcode_anchor nodes depends on builder.""" + default_priority = 100 + + def run(self, **kwargs: Any) -> None: + if is_supported_builder(self.app.builder): + self.convert_viewcode_anchors() + else: + self.remove_viewcode_anchors() + + def convert_viewcode_anchors(self) -> None: + for node in self.document.traverse(viewcode_anchor): + anchor = nodes.inline('', _('[source]'), classes=['viewcode-link']) + refnode = make_refnode(self.app.builder, node['refdoc'], node['reftarget'], + node['refid'], anchor) + node.replace_self(refnode) + + def remove_viewcode_anchors(self) -> None: + for node in self.document.traverse(viewcode_anchor): + node.parent.remove(node) + + def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node - ) -> Node: - if app.builder.format != 'html': - return None - elif node['reftype'] == 'viewcode': - # resolve our "viewcode" reference nodes -- they need special treatment + ) -> Optional[Node]: + # resolve our "viewcode" reference nodes -- they need special treatment + if node['reftype'] == 'viewcode': + warnings.warn('viewcode extension is no longer use pending_xref node. ' + 'Please update your extension.', RemovedInSphinx50Warning) 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]]: +def get_module_filename(app: Sphinx, modname: str) -> Optional[str]: + """Get module filename for *modname*.""" + source_info = app.emit_firstresult('viewcode-find-source', modname) + if source_info: + return None + else: + try: + filename, source = ModuleAnalyzer.get_module_source(modname) + return filename + except Exception: + return None + + +def should_generate_module_page(app: Sphinx, modname: str) -> bool: + """Check generation of module page is needed.""" + module_filename = get_module_filename(app, modname) + if module_filename is None: + # Always (re-)generate module page when module filename is not found. + return True + + builder = cast(StandaloneHTMLBuilder, app.builder) + basename = modname.replace('.', '/') + builder.out_suffix + page_filename = path.join(app.outdir, '_modules/', basename) + + try: + if path.getmtime(module_filename) <= path.getmtime(page_filename): + # generation is not needed if the HTML page is newer than module file. + return False + except IOError: + pass + + return True + + +def collect_pages(app: Sphinx) -> Generator[Tuple[str, Dict[str, Any], str], None, None]: env = app.builder.env if not hasattr(env, '_viewcode_modules'): return + if not is_supported_builder(app.builder): + return highlighter = app.builder.highlighter # type: ignore urito = app.builder.get_relative_uri @@ -157,9 +235,12 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: app.verbosity, lambda x: x[0]): if not entry: continue + if not should_generate_module_page(app, modname): + continue + code, tags, used, refname = entry # construct a page name for the highlighted source - pagename = '_modules/' + modname.replace('.', '/') + pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')) # highlight the source using the builder's highlighter if env.config.highlight_language in ('python3', 'default', 'none'): lexer = env.config.highlight_language @@ -191,10 +272,10 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: parent = parent.rsplit('.', 1)[0] if parent in modnames: parents.append({ - 'link': urito(pagename, '_modules/' + - parent.replace('.', '/')), + 'link': urito(pagename, + posixpath.join(OUTPUT_DIRNAME, parent.replace('.', '/'))), 'title': parent}) - parents.append({'link': urito(pagename, '_modules/index'), + parents.append({'link': urito(pagename, posixpath.join(OUTPUT_DIRNAME, 'index')), 'title': _('Module code')}) parents.reverse() # putting it all together @@ -223,7 +304,8 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: html.append('</ul>') stack.append(modname + '.') html.append('<li><a href="%s">%s</a></li>\n' % ( - urito('_modules/index', '_modules/' + modname.replace('.', '/')), + urito(posixpath.join(OUTPUT_DIRNAME, 'index'), + posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))), modname)) html.append('</ul>' * (len(stack) - 1)) context = { @@ -232,7 +314,7 @@ def collect_pages(app: Sphinx) -> Iterator[Tuple[str, Dict[str, Any], str]]: ''.join(html)), } - yield ('_modules/index', context, 'page.html') + yield (posixpath.join(OUTPUT_DIRNAME, 'index'), context, 'page.html') def setup(app: Sphinx) -> Dict[str, Any]: @@ -247,6 +329,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: # app.add_config_value('viewcode_exclude_modules', [], 'env') app.add_event('viewcode-find-source') app.add_event('viewcode-follow-imported') + app.add_post_transform(ViewcodeAnchorTransform) return { 'version': sphinx.__display_version__, 'env_version': 1, |