summaryrefslogtreecommitdiff
path: root/sphinx/ext/viewcode.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/ext/viewcode.py')
-rw-r--r--sphinx/ext/viewcode.py137
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,