Merge pull request #6203 from tk0miya/refactor_citations

Add CitationDomain for citation processing
This commit is contained in:
Takeshi KOMIYA
2019-04-06 14:46:32 +09:00
committed by GitHub
7 changed files with 244 additions and 96 deletions

View File

@@ -33,11 +33,17 @@ Deprecated
* ``sphinx.directives.TabularColumns``
* ``sphinx.directives.TocTree``
* ``sphinx.directives.VersionChange``
* ``sphinx.domains.std.StandardDomain._resolve_citation_xref()``
* ``sphinx.domains.std.StandardDomain.note_citations()``
* ``sphinx.domains.std.StandardDomain.note_citation_refs()``
* ``sphinx.domains.std.StandardDomain.note_labels()``
* ``sphinx.environment.NoUri``
* ``sphinx.ext.autodoc.importer.MockFinder``
* ``sphinx.ext.autodoc.importer.MockLoader``
* ``sphinx.ext.autodoc.importer.mock()``
* ``sphinx.ext.autosummary.autolink_role()``
* ``sphinx.transforms.CitationReferences``
* ``sphinx.transforms.SmartQuotesSkipper``
* ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()``
* ``sphinx.util.node.find_source_node()``
* ``sphinx.util.i18n.find_catalog()``

View File

@@ -116,6 +116,26 @@ The following is a list of deprecated interfaces.
- 4.0
- ``sphinx.directives.other.VersionChange``
* - ``sphinx.domains.std.StandardDomain._resolve_citation_xref()``
- 2.1
- 4.0
- ``sphinx.domains.citation.CitationDomain.resolve_xref()``
* - ``sphinx.domains.std.StandardDomain.note_citations()``
- 2.1
- 4.0
- ``sphinx.domains.citation.CitationDomain.note_citation()``
* - ``sphinx.domains.std.StandardDomain.note_citation_refs()``
- 2.1
- 4.0
- ``sphinx.domains.citation.CitationDomain.note_citation_reference()``
* - ``sphinx.domains.std.StandardDomain.note_labels()``
- 2.1
- 4.0
- ``sphinx.domains.std.StandardDomain.process_doc()``
* - ``sphinx.environment.NoUri``
- 2.1
- 4.0
@@ -141,6 +161,16 @@ The following is a list of deprecated interfaces.
- 4.0
- ``sphinx.ext.autosummary.AutoLink``
* - ``sphinx.transforms.CitationReferences``
- 2.1
- 4.0
- ``sphinx.domains.citation.CitationReferenceTransform``
* - ``sphinx.transforms.SmartQuotesSkipper``
- 2.1
- 4.0
- ``sphinx.domains.citation.CitationDefinitionTransform``
* - ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()``
- 2.1
- 4.0

View File

@@ -76,6 +76,7 @@ builtin_extensions = (
'sphinx.config',
'sphinx.domains.c',
'sphinx.domains.changeset',
'sphinx.domains.citation',
'sphinx.domains.cpp',
'sphinx.domains.javascript',
'sphinx.domains.math',

View File

@@ -16,6 +16,7 @@ from sphinx import addnodes
from sphinx.builders.latex.nodes import (
captioned_literal_block, footnotemark, footnotetext, math_reference, thebibliography
)
from sphinx.domains.citation import CitationDomain
from sphinx.transforms import SphinxTransform
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util.nodes import NodeMatcher
@@ -545,10 +546,10 @@ class CitationReferenceTransform(SphinxPostTransform):
def run(self, **kwargs):
# type: (Any) -> None
matcher = NodeMatcher(addnodes.pending_xref, refdomain='std', reftype='citation')
citations = self.env.get_domain('std').data['citations']
domain = cast(CitationDomain, self.env.get_domain('citation'))
matcher = NodeMatcher(addnodes.pending_xref, refdomain='citation', reftype='ref')
for node in self.document.traverse(matcher): # type: addnodes.pending_xref
docname, labelid, _ = citations.get(node['reftarget'], ('', '', 0))
docname, labelid, _ = domain.citations.get(node['reftarget'], ('', '', 0))
if docname:
citation_ref = nodes.citation_reference('', '', *node.children,
docname=docname, refname=labelid)

167
sphinx/domains/citation.py Normal file
View File

@@ -0,0 +1,167 @@
"""
sphinx.domains.citation
~~~~~~~~~~~~~~~~~~~~~~~
The citation domain.
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from typing import cast
from docutils import nodes
from sphinx import addnodes
from sphinx.domains import Domain
from sphinx.locale import __
from sphinx.transforms import SphinxTransform
from sphinx.util import logging
from sphinx.util.nodes import copy_source_info, make_refnode
if False:
# For type annotation
from typing import Any, Dict, List, Set, Tuple, Union # NOQA
from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA
from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__)
class CitationDomain(Domain):
"""Domain for citations."""
name = 'citation'
label = 'citation'
dangling_warnings = {
'ref': 'citation not found: %(target)s',
}
@property
def citations(self):
# type: () -> Dict[str, Tuple[str, str, int]]
return self.data.setdefault('citations', {})
@property
def citation_refs(self):
# type: () -> Dict[str, Set[str]]
return self.data.setdefault('citation_refs', {})
def clear_doc(self, docname):
# type: (str) -> None
for key, (fn, _l, lineno) in list(self.citations.items()):
if fn == docname:
del self.citations[key]
for key, docnames in list(self.citation_refs.items()):
if docnames == {docname}:
del self.citation_refs[key]
elif docname in docnames:
docnames.remove(docname)
def merge_domaindata(self, docnames, otherdata):
# type: (List[str], Dict) -> None
# XXX duplicates?
for key, data in otherdata['citations'].items():
if data[0] in docnames:
self.citations[key] = data
for key, data in otherdata['citation_refs'].items():
citation_refs = self.citation_refs.setdefault(key, set())
for docname in data:
if docname in docnames:
citation_refs.add(docname)
def note_citation(self, node):
# type: (nodes.citation) -> None
label = node[0].astext()
if label in self.citations:
path = self.env.doc2path(self.citations[label][0])
logger.warning(__('duplicate citation %s, other instance in %s'), label, path,
location=node, type='ref', subtype='citation')
self.citations[label] = (node['docname'], node['ids'][0], node.line)
def note_citation_reference(self, node):
# type: (addnodes.pending_xref) -> None
docnames = self.citation_refs.setdefault(node['reftarget'], set())
docnames.add(self.env.docname)
def check_consistency(self):
# type: () -> None
for name, (docname, labelid, lineno) in self.citations.items():
if name not in self.citation_refs:
logger.warning(__('Citation [%s] is not referenced.'), name,
type='ref', subtype='citation', location=(docname, lineno))
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA
docname, labelid, lineno = self.citations.get(target, ('', '', 0))
if not docname:
return None
return make_refnode(builder, fromdocname, docname,
labelid, contnode)
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
# type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA
refnode = self.resolve_xref(env, fromdocname, builder, 'ref', target, node, contnode)
if refnode is None:
return []
else:
return [('ref', refnode)]
class CitationDefinitionTransform(SphinxTransform):
"""Mark citation definition labels as not smartquoted."""
default_priority = 619
def apply(self, **kwargs):
# type: (Any) -> None
domain = cast(CitationDomain, self.env.get_domain('citation'))
for node in self.document.traverse(nodes.citation):
# register citation node to domain
node['docname'] = self.env.docname
domain.note_citation(node)
# mark citation labels as not smartquoted
label = cast(nodes.label, node[0])
label['support_smartquotes'] = False
class CitationReferenceTransform(SphinxTransform):
"""
Replace citation references by pending_xref nodes before the default
docutils transform tries to resolve them.
"""
default_priority = 619
def apply(self, **kwargs):
# type: (Any) -> None
domain = cast(CitationDomain, self.env.get_domain('citation'))
for node in self.document.traverse(nodes.citation_reference):
target = node.astext()
ref = addnodes.pending_xref(target, refdomain='citation', reftype='ref',
reftarget=target, refwarn=True,
support_smartquotes=False,
ids=node["ids"],
classes=node.get('classes', []))
ref += nodes.inline(target, '[%s]' % target)
copy_source_info(node, ref)
node.replace_self(ref)
# register reference node to domain
domain.note_citation_reference(ref)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_domain(CitationDomain)
app.add_transform(CitationDefinitionTransform)
app.add_transform(CitationReferenceTransform)
return {
'version': 'builtin',
'env_version': 1,
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@@ -19,7 +19,7 @@ from docutils.parsers.rst import directives
from docutils.statemachine import StringList
from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType
from sphinx.errors import NoUri
@@ -499,8 +499,6 @@ class StandardDomain(Domain):
initial_data = {
'progoptions': {}, # (program, name) -> docname, labelid
'objects': {}, # (type, name) -> docname, labelid
'citations': {}, # citation_name -> docname, labelid, lineno
'citation_refs': {}, # citation_name -> list of docnames
'labels': { # labelname -> docname, labelid, sectionname
'genindex': ('genindex', '', _('Index')),
'modindex': ('py-modindex', '', _('Module Index')),
@@ -521,7 +519,6 @@ class StandardDomain(Domain):
'keyword': 'unknown keyword: %(target)s',
'doc': 'unknown document: %(target)s',
'option': 'unknown option: %(target)s',
'citation': 'citation not found: %(target)s',
}
enumerable_nodes = { # node_class -> (figtype, title_getter)
@@ -568,14 +565,6 @@ class StandardDomain(Domain):
for key, (fn, _l) in list(self.objects.items()):
if fn == docname:
del self.objects[key]
for key, (fn, _l, lineno) in list(self.data['citations'].items()):
if fn == docname:
del self.data['citations'][key]
for key, docnames in list(self.data['citation_refs'].items()):
if docnames == [docname]:
del self.data['citation_refs'][key]
elif docname in docnames:
docnames.remove(docname)
for key, (fn, _l, _l) in list(self.labels.items()):
if fn == docname:
del self.labels[key]
@@ -592,14 +581,6 @@ class StandardDomain(Domain):
for key, data in otherdata['objects'].items():
if data[0] in docnames:
self.objects[key] = data
for key, data in otherdata['citations'].items():
if data[0] in docnames:
self.data['citations'][key] = data
for key, data in otherdata['citation_refs'].items():
citation_refs = self.data['citation_refs'].setdefault(key, [])
for docname in data:
if docname in docnames:
citation_refs.append(docname)
for key, data in otherdata['labels'].items():
if data[0] in docnames:
self.labels[key] = data
@@ -608,31 +589,6 @@ class StandardDomain(Domain):
self.anonlabels[key] = data
def process_doc(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
self.note_citations(env, docname, document)
self.note_citation_refs(env, docname, document)
self.note_labels(env, docname, document)
def note_citations(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
for node in document.traverse(nodes.citation):
node['docname'] = docname
label = cast(nodes.label, node[0]).astext()
if label in self.data['citations']:
path = env.doc2path(self.data['citations'][label][0])
logger.warning(__('duplicate citation %s, other instance in %s'), label, path,
location=node, type='ref', subtype='citation')
self.data['citations'][label] = (docname, node['ids'][0], node.line)
def note_citation_refs(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
for node in document.traverse(addnodes.pending_xref):
if node['refdomain'] == 'std' and node['reftype'] == 'citation':
label = node['reftarget']
citation_refs = self.data['citation_refs'].setdefault(label, [])
citation_refs.append(docname)
def note_labels(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
for name, explicit in document.nametypes.items():
if not explicit:
@@ -682,14 +638,6 @@ class StandardDomain(Domain):
# type: (str, str, str, str) -> None
self.progoptions[program, name] = (docname, labelid)
def check_consistency(self):
# type: () -> None
for name, (docname, labelid, lineno) in self.data['citations'].items():
if name not in self.data['citation_refs']:
logger.warning(__('Citation [%s] is not referenced.'), name,
type='ref', subtype='citation',
location=(docname, lineno))
def build_reference_node(self, fromdocname, builder, docname, labelid,
sectname, rolename, **options):
# type: (str, Builder, str, str, str, str, Any) -> nodes.Element
@@ -728,7 +676,10 @@ class StandardDomain(Domain):
elif typ == 'option':
resolver = self._resolve_option_xref
elif typ == 'citation':
resolver = self._resolve_citation_xref
warnings.warn('pending_xref(domain=std, type=citation) is deprecated: %r' % node,
RemovedInSphinx40Warning)
domain = env.get_domain('citation')
return domain.resolve_xref(env, fromdocname, builder, typ, target, node, contnode)
else:
resolver = self._resolve_obj_xref
@@ -860,6 +811,8 @@ class StandardDomain(Domain):
def _resolve_citation_xref(self, env, fromdocname, builder, typ, target, node, contnode):
# type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA
warnings.warn('StandardDomain._resolve_citation_xref() is deprecated.',
RemovedInSphinx30Warning)
docname, labelid, lineno = self.data['citations'].get(target, ('', '', 0))
if not docname:
if 'ids' in node:
@@ -1027,6 +980,21 @@ class StandardDomain(Domain):
else:
return None
def note_citations(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
warnings.warn('StandardDomain.note_citations() is deprecated.',
RemovedInSphinx40Warning)
def note_citation_refs(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
warnings.warn('StandardDomain.note_citation_refs() is deprecated.',
RemovedInSphinx40Warning)
def note_labels(self, env, docname, document):
# type: (BuildEnvironment, str, nodes.document) -> None
warnings.warn('StandardDomain.note_labels() is deprecated.',
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]

View File

@@ -9,7 +9,6 @@
"""
import re
from typing import cast
from docutils import nodes
from docutils.transforms import Transform, Transformer
@@ -19,13 +18,12 @@ from docutils.utils import normalize_language_tag
from docutils.utils.smartquotes import smartchars
from sphinx import addnodes
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.docutils import new_document
from sphinx.util.i18n import format_date
from sphinx.util.nodes import (
NodeMatcher, apply_source_workaround, copy_source_info, is_smartquotable
)
from sphinx.util.nodes import NodeMatcher, apply_source_workaround, is_smartquotable
if False:
# For type annotation
@@ -200,39 +198,6 @@ class SortIds(SphinxTransform):
node['ids'] = node['ids'][1:] + [node['ids'][0]]
class SmartQuotesSkipper(SphinxTransform):
"""Mark specific nodes as not smartquoted."""
default_priority = 619
def apply(self, **kwargs):
# type: (Any) -> None
# citation labels
for node in self.document.traverse(nodes.citation):
label = cast(nodes.label, node[0])
label['support_smartquotes'] = False
class CitationReferences(SphinxTransform):
"""
Replace citation references by pending_xref nodes before the default
docutils transform tries to resolve them.
"""
default_priority = 619
def apply(self, **kwargs):
# type: (Any) -> None
for node in self.document.traverse(nodes.citation_reference):
target = node.astext()
ref = addnodes.pending_xref(target, refdomain='std', reftype='citation',
reftarget=target, refwarn=True,
support_smartquotes=False,
ids=node["ids"],
classes=node.get('classes', []))
ref += nodes.inline(target, '[%s]' % target)
copy_source_info(node, ref)
node.replace_self(ref)
TRANSLATABLE_NODES = {
'literal-block': nodes.literal_block,
'doctest-block': nodes.doctest_block,
@@ -440,12 +405,22 @@ class ManpageLink(SphinxTransform):
node.attributes.update(info)
from sphinx.domains.citation import ( # NOQA
CitationDefinitionTransform, CitationReferenceTransform
)
deprecated_alias('sphinx.transforms',
{
'CitationReferences': CitationReferenceTransform,
'SmartQuotesSkipper': CitationDefinitionTransform,
},
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_transform(ApplySourceWorkaround)
app.add_transform(ExtraTranslatableNodes)
app.add_transform(SmartQuotesSkipper)
app.add_transform(CitationReferences)
app.add_transform(DefaultSubstitutions)
app.add_transform(MoveModuleTargets)
app.add_transform(HandleCodeBlocks)