Fix #8704: viewcode: anchors are generated in incremental build

The anchors for viewcode was generated in the reading phase only if
supported builder is used.  It causes anchors are missing on the
incremental build after the build for non supported builder.

This introduces `viewcode_anchor` node to insert the anchor even if non
supported builders.  They will be converted to the anchor tag in the
resolving phase for supported builders.  Or, they will be removed for
non supported builders.
This commit is contained in:
Takeshi KOMIYA 2021-01-19 03:18:39 +09:00
parent d5d072bc3a
commit 5260143afe
4 changed files with 64 additions and 13 deletions

View File

@ -10,6 +10,7 @@ Incompatible changes
Deprecated Deprecated
---------- ----------
* pending_xref node for viewcode extension
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.good``
* ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected`` * ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.redirected``
@ -69,6 +70,7 @@ Bugs fixed
* #8094: texinfo: image files on the different directory with document are not * #8094: texinfo: image files on the different directory with document are not
copied copied
* #8720: viewcode: module pages are generated for epub on incremental build * #8720: viewcode: module pages are generated for epub on incremental build
* #8704: viewcode: anchors are generated in incremental build after singlehtml
* #8671: :confval:`highlight_options` is not working * #8671: :confval:`highlight_options` is not working
* #8341: C, fix intersphinx lookup types for names in declarations. * #8341: C, fix intersphinx lookup types for names in declarations.
* C, C++: in general fix intersphinx and role lookup types. * C, C++: in general fix intersphinx and role lookup types.

View File

@ -26,6 +26,11 @@ The following is a list of deprecated interfaces.
- (will be) Removed - (will be) Removed
- Alternatives - Alternatives
* - pending_xref node for viewcode extension
- 3.5
- 5.0
- ``sphinx.ext.viewcode.viewcode_anchor``
* - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken`` * - ``sphinx.builders.linkcheck.CheckExternalLinksBuilder.broken``
- 3.5 - 3.5
- 5.0 - 5.0

View File

@ -10,6 +10,7 @@
import posixpath import posixpath
import traceback import traceback
import warnings
from os import path from os import path
from typing import Any, Dict, Generator, Iterable, Optional, Set, Tuple, cast from typing import Any, Dict, Generator, Iterable, Optional, Set, Tuple, cast
@ -19,10 +20,13 @@ from docutils.nodes import Element, Node
import sphinx import sphinx
from sphinx import addnodes from sphinx import addnodes
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer 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 import get_full_modname, logging, status_iterator
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
@ -32,6 +36,15 @@ logger = logging.getLogger(__name__)
OUTPUT_DIRNAME = '_modules' 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]: def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> Optional[str]:
try: try:
return get_full_modname(modname, attribute) return get_full_modname(modname, attribute)
@ -50,14 +63,21 @@ def _get_full_modname(app: Sphinx, modname: str, attribute: str) -> Optional[str
return None 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: def doctree_read(app: Sphinx, doctree: Node) -> None:
env = app.builder.env env = app.builder.env
if not hasattr(env, '_viewcode_modules'): if not hasattr(env, '_viewcode_modules'):
env._viewcode_modules = {} # type: ignore 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: def has_tag(modname: str, fullname: str, docname: str, refname: str) -> bool:
entry = env._viewcode_modules.get(modname, None) # type: ignore entry = env._viewcode_modules.get(modname, None) # type: ignore
@ -115,12 +135,7 @@ def doctree_read(app: Sphinx, doctree: Node) -> None:
continue continue
names.add(fullname) names.add(fullname)
pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/')) pagename = posixpath.join(OUTPUT_DIRNAME, modname.replace('.', '/'))
inline = nodes.inline('', _('[source]'), classes=['viewcode-link']) signode += viewcode_anchor(reftarget=pagename, refid=fullname, refdoc=env.docname)
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], def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
@ -134,10 +149,34 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str],
env._viewcode_modules.update(other._viewcode_modules) # type: ignore 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 def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node
) -> Optional[Node]: ) -> Optional[Node]:
# resolve our "viewcode" reference nodes -- they need special treatment # resolve our "viewcode" reference nodes -- they need special treatment
if node['reftype'] == 'viewcode': 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'], return make_refnode(app.builder, node['refdoc'], node['reftarget'],
node['refid'], contnode) node['refid'], contnode)
@ -182,9 +221,7 @@ def collect_pages(app: Sphinx) -> Generator[Tuple[str, Dict[str, Any], str], Non
env = app.builder.env env = app.builder.env
if not hasattr(env, '_viewcode_modules'): if not hasattr(env, '_viewcode_modules'):
return return
if app.builder.name == "singlehtml": if not is_supported_builder(app.builder):
return
if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub:
return return
highlighter = app.builder.highlighter # type: ignore highlighter = app.builder.highlighter # type: ignore
urito = app.builder.get_relative_uri urito = app.builder.get_relative_uri
@ -292,6 +329,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
# app.add_config_value('viewcode_exclude_modules', [], 'env') # app.add_config_value('viewcode_exclude_modules', [], 'env')
app.add_event('viewcode-find-source') app.add_event('viewcode-find-source')
app.add_event('viewcode-follow-imported') app.add_event('viewcode-follow-imported')
app.add_post_transform(ViewcodeAnchorTransform)
return { return {
'version': sphinx.__display_version__, 'version': sphinx.__display_version__,
'env_version': 1, 'env_version': 1,

View File

@ -55,6 +55,9 @@ def test_viewcode_epub_default(app, status, warning):
assert not (app.outdir / '_modules/spam/mod1.xhtml').exists() assert not (app.outdir / '_modules/spam/mod1.xhtml').exists()
result = (app.outdir / 'index.xhtml').read_text()
assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 0
@pytest.mark.sphinx('epub', testroot='ext-viewcode', @pytest.mark.sphinx('epub', testroot='ext-viewcode',
confoverrides={'viewcode_enable_epub': True}) confoverrides={'viewcode_enable_epub': True})
@ -63,6 +66,9 @@ def test_viewcode_epub_enabled(app, status, warning):
assert (app.outdir / '_modules/spam/mod1.xhtml').exists() assert (app.outdir / '_modules/spam/mod1.xhtml').exists()
result = (app.outdir / 'index.xhtml').read_text()
assert result.count('href="_modules/spam/mod1.xhtml#func1"') == 2
@pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode']) @pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode'])
def test_linkcode(app, status, warning): def test_linkcode(app, status, warning):