mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Added the any
role that can be used to find a cross-reference of
*any* type in *any* domain. Custom domains should implement the new `~Domain.resolve_any_xref` method to make this work properly.
This commit is contained in:
parent
430be0496a
commit
c3eb669f8a
5
CHANGES
5
CHANGES
@ -18,6 +18,8 @@ Incompatible changes
|
||||
* #1543: :confval:`templates_path` is automatically added to
|
||||
:confval:`exclude_patterns` to avoid reading autosummary rst templates in the
|
||||
templates directory.
|
||||
* Custom domains should implement the new :meth:`~Domain.resolve_any_xref`
|
||||
method to make the :rst:role:`any` role work properly.
|
||||
|
||||
Features added
|
||||
--------------
|
||||
@ -26,6 +28,9 @@ Features added
|
||||
* Add support for docutils 0.12
|
||||
* Added ``sphinx.ext.napoleon`` extension for NumPy and Google style docstring
|
||||
support.
|
||||
* Added the :rst:role:`any` role that can be used to find a cross-reference of
|
||||
*any* type in *any* domain. Custom domains should implement the new
|
||||
:meth:`~Domain.resolve_any_xref` method to make this work properly.
|
||||
* Exception logs now contain the last 10 messages emitted by Sphinx.
|
||||
* Added support for extension versions (a string returned by ``setup()``, these
|
||||
can be shown in the traceback log files). Version requirements for extensions
|
||||
|
@ -12,7 +12,9 @@ They are written as ``:rolename:`content```.
|
||||
|
||||
The default role (```content```) has no special meaning by default. You are
|
||||
free to use it for anything you like, e.g. variable names; use the
|
||||
:confval:`default_role` config value to set it to a known role.
|
||||
:confval:`default_role` config value to set it to a known role -- the
|
||||
:rst:role:`any` role to find anything or the :rst:role:`py:obj` role to find
|
||||
Python objects are very useful for this.
|
||||
|
||||
See :ref:`domains` for roles added by domains.
|
||||
|
||||
@ -38,12 +40,53 @@ more versatile:
|
||||
|
||||
* If you prefix the content with ``~``, the link text will only be the last
|
||||
component of the target. For example, ``:py:meth:`~Queue.Queue.get``` will
|
||||
refer to ``Queue.Queue.get`` but only display ``get`` as the link text.
|
||||
refer to ``Queue.Queue.get`` but only display ``get`` as the link text. This
|
||||
does not work with all cross-reference roles, but is domain specific.
|
||||
|
||||
In HTML output, the link's ``title`` attribute (that is e.g. shown as a
|
||||
tool-tip on mouse-hover) will always be the full target name.
|
||||
|
||||
|
||||
.. _any-role:
|
||||
|
||||
Cross-referencing anything
|
||||
--------------------------
|
||||
|
||||
.. rst:role:: any
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
This convenience role tries to do its best to find a valid target for its
|
||||
reference text.
|
||||
|
||||
* First, it tries standard cross-reference targets that would be referenced
|
||||
by :rst:role:`doc`, :rst:role:`ref` or :rst:role:`option`.
|
||||
|
||||
Custom objects added to the standard domain by extensions (see
|
||||
:meth:`.add_object_type`) are also searched.
|
||||
|
||||
* Then, it looks for objects (targets) in all loaded domains. It is up to
|
||||
the domains how specific a match must be. For example, in the Python
|
||||
domain a reference of ``:any:`Builder``` would match the
|
||||
``sphinx.builders.Builder`` class.
|
||||
|
||||
If none or multiple targets are found, a warning will be emitted. In the
|
||||
case of multiple targets, you can change "any" to a specific role.
|
||||
|
||||
This role is a good candidate for setting :confval:`default_role`. If you
|
||||
do, you can write cross-references without a lot of markup overhead. For
|
||||
example, in this Python function documentation ::
|
||||
|
||||
.. function:: install()
|
||||
|
||||
This function installs a `handler` for every signal known by the
|
||||
`signal` module. See the section `about-signals` for more information.
|
||||
|
||||
there could be references to a glossary term (usually ``:term:`handler```), a
|
||||
Python module (usually ``:py:mod:`signal``` or ``:mod:`signal```) and a
|
||||
section (usually ``:ref:`about-signals```).
|
||||
|
||||
|
||||
Cross-referencing objects
|
||||
-------------------------
|
||||
|
||||
|
@ -155,10 +155,13 @@ class Domain(object):
|
||||
self._role_cache = {}
|
||||
self._directive_cache = {}
|
||||
self._role2type = {}
|
||||
self._type2role = {}
|
||||
for name, obj in iteritems(self.object_types):
|
||||
for rolename in obj.roles:
|
||||
self._role2type.setdefault(rolename, []).append(name)
|
||||
self._type2role[name] = obj.roles[0] if obj.roles else ''
|
||||
self.objtypes_for_role = self._role2type.get
|
||||
self.role_for_objtype = self._type2role.get
|
||||
|
||||
def role(self, name):
|
||||
"""Return a role adapter function that always gives the registered
|
||||
@ -220,6 +223,22 @@ class Domain(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
|
||||
"""Resolve the pending_xref *node* with the given *target*.
|
||||
|
||||
The reference comes from an "any" or similar role, which means that we
|
||||
don't know the type. Otherwise, the arguments are the same as for
|
||||
:meth:`resolve_xref`.
|
||||
|
||||
The method must return a list (potentially empty) of tuples
|
||||
``('domain:role', newnode)``, where ``'domain:role'`` is the name of a
|
||||
role that could have created the same reference, e.g. ``'py:func'``.
|
||||
``newnode`` is what :meth:`resolve_xref` would return.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_objects(self):
|
||||
"""Return an iterable of "object descriptions", which are tuples with
|
||||
five items:
|
||||
|
@ -279,6 +279,17 @@ class CDomain(Domain):
|
||||
return make_refnode(builder, fromdocname, obj[0], 'c.' + target,
|
||||
contnode, target)
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target,
|
||||
node, contnode):
|
||||
# strip pointer asterisk
|
||||
target = target.rstrip(' *')
|
||||
if target not in self.data['objects']:
|
||||
return []
|
||||
obj = self.data['objects'][target]
|
||||
return [('c:' + self.role_for_objtype(obj[1]),
|
||||
make_refnode(builder, fromdocname, obj[0], 'c.' + target,
|
||||
contnode, target))]
|
||||
|
||||
def get_objects(self):
|
||||
for refname, (docname, type) in list(self.data['objects'].items()):
|
||||
yield (refname, refname, type, docname, 'c.' + refname, 1)
|
||||
|
@ -1838,18 +1838,18 @@ class CPPDomain(Domain):
|
||||
if data[0] == docname:
|
||||
del self.data['objects'][fullname]
|
||||
|
||||
def resolve_xref(self, env, fromdocname, builder,
|
||||
typ, target, node, contnode):
|
||||
def _resolve_xref_inner(self, env, fromdocname, builder,
|
||||
target, node, contnode, warn=True):
|
||||
def _create_refnode(nameAst):
|
||||
name = text_type(nameAst)
|
||||
if name not in self.data['objects']:
|
||||
# try dropping the last template
|
||||
name = nameAst.get_name_no_last_template()
|
||||
if name not in self.data['objects']:
|
||||
return None
|
||||
return None, None
|
||||
docname, objectType, id = self.data['objects'][name]
|
||||
return make_refnode(builder, fromdocname, docname, id, contnode,
|
||||
name)
|
||||
name), objectType
|
||||
|
||||
parser = DefinitionParser(target)
|
||||
try:
|
||||
@ -1858,20 +1858,34 @@ class CPPDomain(Domain):
|
||||
if not parser.eof:
|
||||
raise DefinitionError('')
|
||||
except DefinitionError:
|
||||
env.warn_node('unparseable C++ definition: %r' % target, node)
|
||||
return None
|
||||
if warn:
|
||||
env.warn_node('unparseable C++ definition: %r' % target, node)
|
||||
return None, None
|
||||
|
||||
# try as is the name is fully qualified
|
||||
refNode = _create_refnode(nameAst)
|
||||
if refNode:
|
||||
return refNode
|
||||
res = _create_refnode(nameAst)
|
||||
if res[0]:
|
||||
return res
|
||||
|
||||
# try qualifying it with the parent
|
||||
parent = node.get('cpp:parent', None)
|
||||
if parent and len(parent) > 0:
|
||||
return _create_refnode(nameAst.prefix_nested_name(parent[-1]))
|
||||
else:
|
||||
return None
|
||||
return None, None
|
||||
|
||||
def resolve_xref(self, env, fromdocname, builder,
|
||||
typ, target, node, contnode):
|
||||
return self._resolve_xref_inner(env, fromdocname, builder, target, node,
|
||||
contnode)[0]
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target,
|
||||
node, contnode):
|
||||
node, objtype = self._resolve_xref_inner(env, fromdocname, builder,
|
||||
target, node, contnode, warn=False)
|
||||
if node:
|
||||
return [('cpp:' + self.role_for_objtype(objtype), node)]
|
||||
return []
|
||||
|
||||
def get_objects(self):
|
||||
for refname, (docname, type, theid) in iteritems(self.data['objects']):
|
||||
|
@ -179,7 +179,7 @@ class JavaScriptDomain(Domain):
|
||||
'attr': JSXRefRole(),
|
||||
}
|
||||
initial_data = {
|
||||
'objects': {}, # fullname -> docname, objtype
|
||||
'objects': {}, # fullname -> docname, objtype
|
||||
}
|
||||
|
||||
def clear_doc(self, docname):
|
||||
@ -214,6 +214,16 @@ class JavaScriptDomain(Domain):
|
||||
return make_refnode(builder, fromdocname, obj[0],
|
||||
name.replace('$', '_S_'), contnode, name)
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target, node,
|
||||
contnode):
|
||||
objectname = node.get('js:object') # not likely
|
||||
name, obj = self.find_obj(env, objectname, target, None, 1)
|
||||
if not obj:
|
||||
return []
|
||||
return [('js:' + self.role_for_objtype(obj[1]),
|
||||
make_refnode(builder, fromdocname, obj[0],
|
||||
name.replace('$', '_S_'), contnode, name))]
|
||||
|
||||
def get_objects(self):
|
||||
for refname, (docname, type) in list(self.data['objects'].items()):
|
||||
yield refname, refname, type, docname, \
|
||||
|
@ -643,7 +643,10 @@ class PythonDomain(Domain):
|
||||
|
||||
newname = None
|
||||
if searchmode == 1:
|
||||
objtypes = self.objtypes_for_role(type)
|
||||
if type is None:
|
||||
objtypes = list(self.object_types)
|
||||
else:
|
||||
objtypes = self.objtypes_for_role(type)
|
||||
if objtypes is not None:
|
||||
if modname and classname:
|
||||
fullname = modname + '.' + classname + '.' + name
|
||||
@ -704,22 +707,44 @@ class PythonDomain(Domain):
|
||||
name, obj = matches[0]
|
||||
|
||||
if obj[1] == 'module':
|
||||
# get additional info for modules
|
||||
docname, synopsis, platform, deprecated = self.data['modules'][name]
|
||||
assert docname == obj[0]
|
||||
title = name
|
||||
if synopsis:
|
||||
title += ': ' + synopsis
|
||||
if deprecated:
|
||||
title += _(' (deprecated)')
|
||||
if platform:
|
||||
title += ' (' + platform + ')'
|
||||
return make_refnode(builder, fromdocname, docname,
|
||||
'module-' + name, contnode, title)
|
||||
return self._make_module_refnode(builder, fromdocname, name,
|
||||
contnode)
|
||||
else:
|
||||
return make_refnode(builder, fromdocname, obj[0], name,
|
||||
contnode, name)
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target,
|
||||
node, contnode):
|
||||
modname = node.get('py:module') # it is not likely we have these
|
||||
clsname = node.get('py:class')
|
||||
results = []
|
||||
|
||||
# always search in "refspecific" mode with the :any: role
|
||||
matches = self.find_obj(env, modname, clsname, target, None, 1)
|
||||
for name, obj in matches:
|
||||
if obj[1] == 'module':
|
||||
results.append(('py:mod',
|
||||
self._make_module_refnode(builder, fromdocname,
|
||||
name, contnode)))
|
||||
else:
|
||||
results.append(('py:' + self.role_for_objtype(obj[1]),
|
||||
make_refnode(builder, fromdocname, obj[0], name,
|
||||
contnode, name)))
|
||||
return results
|
||||
|
||||
def _make_module_refnode(self, builder, fromdocname, name, contnode):
|
||||
# get additional info for modules
|
||||
docname, synopsis, platform, deprecated = self.data['modules'][name]
|
||||
title = name
|
||||
if synopsis:
|
||||
title += ': ' + synopsis
|
||||
if deprecated:
|
||||
title += _(' (deprecated)')
|
||||
if platform:
|
||||
title += ' (' + platform + ')'
|
||||
return make_refnode(builder, fromdocname, docname,
|
||||
'module-' + name, contnode, title)
|
||||
|
||||
def get_objects(self):
|
||||
for modname, info in iteritems(self.data['modules']):
|
||||
yield (modname, modname, 'module', info[0], 'module-' + modname, 0)
|
||||
|
@ -134,6 +134,19 @@ class ReSTDomain(Domain):
|
||||
objtype + '-' + target,
|
||||
contnode, target + ' ' + objtype)
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target,
|
||||
node, contnode):
|
||||
objects = self.data['objects']
|
||||
results = []
|
||||
for objtype in self.object_types:
|
||||
if (objtype, target) in self.data['objects']:
|
||||
results.append(('rst:' + self.role_for_objtype(objtype),
|
||||
make_refnode(builder, fromdocname,
|
||||
objects[objtype, target],
|
||||
objtype + '-' + target,
|
||||
contnode, target + ' ' + objtype)))
|
||||
return results
|
||||
|
||||
def get_objects(self):
|
||||
for (typ, name), docname in iteritems(self.data['objects']):
|
||||
yield name, name, typ, docname, typ + '-' + name, 1
|
||||
|
@ -628,6 +628,23 @@ class StandardDomain(Domain):
|
||||
return make_refnode(builder, fromdocname, docname,
|
||||
labelid, contnode)
|
||||
|
||||
def resolve_any_xref(self, env, fromdocname, builder, target,
|
||||
node, contnode):
|
||||
results = []
|
||||
for role in ('ref', 'option'): # do not try "keyword"
|
||||
res = self.resolve_xref(env, fromdocname, builder, target,
|
||||
role, node, contnode)
|
||||
if res:
|
||||
results.append(('std:ref', res))
|
||||
# all others
|
||||
for objtype in self.object_types:
|
||||
if (objtype, target) in self.data['objects']:
|
||||
docname, labelid = self.data['objects'][objtype, target]
|
||||
results.append(('std:' + self.role_for_objtype(objtype),
|
||||
make_refnode(builder, fromdocname, docname,
|
||||
labelid, contnode)))
|
||||
return results
|
||||
|
||||
def get_objects(self):
|
||||
for (prog, option), info in iteritems(self.data['progoptions']):
|
||||
yield (option, option, 'option', info[0], info[1], 1)
|
||||
|
@ -1352,6 +1352,8 @@ class BuildEnvironment:
|
||||
newnode = domain.resolve_xref(self, refdoc, builder,
|
||||
typ, target, node, contnode)
|
||||
# really hardwired reference types
|
||||
elif typ == 'any':
|
||||
newnode = self._resolve_any_reference(builder, node, contnode)
|
||||
elif typ == 'doc':
|
||||
newnode = self._resolve_doc_reference(builder, node, contnode)
|
||||
elif typ == 'citation':
|
||||
@ -1435,6 +1437,49 @@ class BuildEnvironment:
|
||||
# transforms.CitationReference.apply.
|
||||
del node['ids'][:]
|
||||
|
||||
def _resolve_any_reference(self, builder, node, contnode):
|
||||
"""Resolve reference generated by the "any" role."""
|
||||
refdoc = node['refdoc']
|
||||
target = node['reftarget']
|
||||
results = []
|
||||
# first, try resolving as :doc:
|
||||
doc_ref = self._resolve_doc_reference(builder, node, contnode)
|
||||
if doc_ref:
|
||||
results.append(('doc', doc_ref))
|
||||
# next, do the standard domain (makes this a priority)
|
||||
results.extend(self.domains['std'].resolve_any_xref(
|
||||
self, refdoc, builder, target, node, contnode))
|
||||
for domain in self.domains.values():
|
||||
if domain.name == 'std':
|
||||
continue # we did this one already
|
||||
try:
|
||||
results.extend(domain.resolve_any_xref(self, refdoc, builder,
|
||||
target, node, contnode))
|
||||
except NotImplementedError:
|
||||
# the domain doesn't yet support the new interface
|
||||
# we have to manually collect possible references (SLOW)
|
||||
for role in domain.roles:
|
||||
res = domain.resolve_xref(self, refdoc, builder, role, target,
|
||||
node, contnode)
|
||||
if res:
|
||||
results.append(('%s:%s' % (domain.name, role), res))
|
||||
# now, see how many matches we got...
|
||||
if not results:
|
||||
return None
|
||||
if len(results) > 1:
|
||||
nice_results = ' or '.join(':%s:' % r[0] for r in results)
|
||||
self.warn_node('more than one target found for \'any\' cross-'
|
||||
'reference %r: could be %s' % (target, nice_results),
|
||||
node)
|
||||
res_role, newnode = results[0]
|
||||
# Override "any" class with the actual role type to get the styling
|
||||
# approximately correct.
|
||||
res_domain = res_role.split(':')[0]
|
||||
if newnode and newnode[0].get('classes'):
|
||||
newnode[0]['classes'].append(res_domain)
|
||||
newnode[0]['classes'].append(res_role.replace(':', '-'))
|
||||
return newnode
|
||||
|
||||
def process_only_nodes(self, doctree, builder, fromdocname=None):
|
||||
# A comment on the comment() nodes being inserted: replacing by [] would
|
||||
# result in a "Losing ids" exception if there is a target node before
|
||||
|
@ -316,6 +316,8 @@ specific_docroles = {
|
||||
'download': XRefRole(nodeclass=addnodes.download_reference),
|
||||
# links to documents
|
||||
'doc': XRefRole(warn_dangling=True),
|
||||
# links to anything
|
||||
'any': XRefRole(warn_dangling=True),
|
||||
|
||||
'pep': indexmarkup_role,
|
||||
'rfc': indexmarkup_role,
|
||||
|
Loading…
Reference in New Issue
Block a user