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:
Georg Brandl 2014-09-19 12:59:18 +02:00
parent 430be0496a
commit c3eb669f8a
11 changed files with 230 additions and 26 deletions

View File

@ -18,6 +18,8 @@ Incompatible changes
* #1543: :confval:`templates_path` is automatically added to * #1543: :confval:`templates_path` is automatically added to
:confval:`exclude_patterns` to avoid reading autosummary rst templates in the :confval:`exclude_patterns` to avoid reading autosummary rst templates in the
templates directory. 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 Features added
-------------- --------------
@ -26,6 +28,9 @@ Features added
* Add support for docutils 0.12 * Add support for docutils 0.12
* Added ``sphinx.ext.napoleon`` extension for NumPy and Google style docstring * Added ``sphinx.ext.napoleon`` extension for NumPy and Google style docstring
support. 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. * Exception logs now contain the last 10 messages emitted by Sphinx.
* Added support for extension versions (a string returned by ``setup()``, these * Added support for extension versions (a string returned by ``setup()``, these
can be shown in the traceback log files). Version requirements for extensions can be shown in the traceback log files). Version requirements for extensions

View File

@ -12,7 +12,9 @@ They are written as ``:rolename:`content```.
The default role (```content```) has no special meaning by default. You are 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 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. 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 * 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 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 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. 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 Cross-referencing objects
------------------------- -------------------------

View File

@ -155,10 +155,13 @@ class Domain(object):
self._role_cache = {} self._role_cache = {}
self._directive_cache = {} self._directive_cache = {}
self._role2type = {} self._role2type = {}
self._type2role = {}
for name, obj in iteritems(self.object_types): for name, obj in iteritems(self.object_types):
for rolename in obj.roles: for rolename in obj.roles:
self._role2type.setdefault(rolename, []).append(name) self._role2type.setdefault(rolename, []).append(name)
self._type2role[name] = obj.roles[0] if obj.roles else ''
self.objtypes_for_role = self._role2type.get self.objtypes_for_role = self._role2type.get
self.role_for_objtype = self._type2role.get
def role(self, name): def role(self, name):
"""Return a role adapter function that always gives the registered """Return a role adapter function that always gives the registered
@ -220,6 +223,22 @@ class Domain(object):
""" """
pass 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): def get_objects(self):
"""Return an iterable of "object descriptions", which are tuples with """Return an iterable of "object descriptions", which are tuples with
five items: five items:

View File

@ -279,6 +279,17 @@ class CDomain(Domain):
return make_refnode(builder, fromdocname, obj[0], 'c.' + target, return make_refnode(builder, fromdocname, obj[0], 'c.' + target,
contnode, 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): def get_objects(self):
for refname, (docname, type) in list(self.data['objects'].items()): for refname, (docname, type) in list(self.data['objects'].items()):
yield (refname, refname, type, docname, 'c.' + refname, 1) yield (refname, refname, type, docname, 'c.' + refname, 1)

View File

@ -1838,18 +1838,18 @@ class CPPDomain(Domain):
if data[0] == docname: if data[0] == docname:
del self.data['objects'][fullname] del self.data['objects'][fullname]
def resolve_xref(self, env, fromdocname, builder, def _resolve_xref_inner(self, env, fromdocname, builder,
typ, target, node, contnode): target, node, contnode, warn=True):
def _create_refnode(nameAst): def _create_refnode(nameAst):
name = text_type(nameAst) name = text_type(nameAst)
if name not in self.data['objects']: if name not in self.data['objects']:
# try dropping the last template # try dropping the last template
name = nameAst.get_name_no_last_template() name = nameAst.get_name_no_last_template()
if name not in self.data['objects']: if name not in self.data['objects']:
return None return None, None
docname, objectType, id = self.data['objects'][name] docname, objectType, id = self.data['objects'][name]
return make_refnode(builder, fromdocname, docname, id, contnode, return make_refnode(builder, fromdocname, docname, id, contnode,
name) name), objectType
parser = DefinitionParser(target) parser = DefinitionParser(target)
try: try:
@ -1858,20 +1858,34 @@ class CPPDomain(Domain):
if not parser.eof: if not parser.eof:
raise DefinitionError('') raise DefinitionError('')
except DefinitionError: except DefinitionError:
env.warn_node('unparseable C++ definition: %r' % target, node) if warn:
return None env.warn_node('unparseable C++ definition: %r' % target, node)
return None, None
# try as is the name is fully qualified # try as is the name is fully qualified
refNode = _create_refnode(nameAst) res = _create_refnode(nameAst)
if refNode: if res[0]:
return refNode return res
# try qualifying it with the parent # try qualifying it with the parent
parent = node.get('cpp:parent', None) parent = node.get('cpp:parent', None)
if parent and len(parent) > 0: if parent and len(parent) > 0:
return _create_refnode(nameAst.prefix_nested_name(parent[-1])) return _create_refnode(nameAst.prefix_nested_name(parent[-1]))
else: 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): def get_objects(self):
for refname, (docname, type, theid) in iteritems(self.data['objects']): for refname, (docname, type, theid) in iteritems(self.data['objects']):

View File

@ -179,7 +179,7 @@ class JavaScriptDomain(Domain):
'attr': JSXRefRole(), 'attr': JSXRefRole(),
} }
initial_data = { initial_data = {
'objects': {}, # fullname -> docname, objtype 'objects': {}, # fullname -> docname, objtype
} }
def clear_doc(self, docname): def clear_doc(self, docname):
@ -214,6 +214,16 @@ class JavaScriptDomain(Domain):
return make_refnode(builder, fromdocname, obj[0], return make_refnode(builder, fromdocname, obj[0],
name.replace('$', '_S_'), contnode, name) 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): def get_objects(self):
for refname, (docname, type) in list(self.data['objects'].items()): for refname, (docname, type) in list(self.data['objects'].items()):
yield refname, refname, type, docname, \ yield refname, refname, type, docname, \

View File

@ -643,7 +643,10 @@ class PythonDomain(Domain):
newname = None newname = None
if searchmode == 1: 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 objtypes is not None:
if modname and classname: if modname and classname:
fullname = modname + '.' + classname + '.' + name fullname = modname + '.' + classname + '.' + name
@ -704,22 +707,44 @@ class PythonDomain(Domain):
name, obj = matches[0] name, obj = matches[0]
if obj[1] == 'module': if obj[1] == 'module':
# get additional info for modules return self._make_module_refnode(builder, fromdocname, name,
docname, synopsis, platform, deprecated = self.data['modules'][name] contnode)
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)
else: else:
return make_refnode(builder, fromdocname, obj[0], name, return make_refnode(builder, fromdocname, obj[0], name,
contnode, 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): def get_objects(self):
for modname, info in iteritems(self.data['modules']): for modname, info in iteritems(self.data['modules']):
yield (modname, modname, 'module', info[0], 'module-' + modname, 0) yield (modname, modname, 'module', info[0], 'module-' + modname, 0)

View File

@ -134,6 +134,19 @@ class ReSTDomain(Domain):
objtype + '-' + target, objtype + '-' + target,
contnode, target + ' ' + objtype) 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): def get_objects(self):
for (typ, name), docname in iteritems(self.data['objects']): for (typ, name), docname in iteritems(self.data['objects']):
yield name, name, typ, docname, typ + '-' + name, 1 yield name, name, typ, docname, typ + '-' + name, 1

View File

@ -628,6 +628,23 @@ class StandardDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
labelid, contnode) 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): def get_objects(self):
for (prog, option), info in iteritems(self.data['progoptions']): for (prog, option), info in iteritems(self.data['progoptions']):
yield (option, option, 'option', info[0], info[1], 1) yield (option, option, 'option', info[0], info[1], 1)

View File

@ -1352,6 +1352,8 @@ class BuildEnvironment:
newnode = domain.resolve_xref(self, refdoc, builder, newnode = domain.resolve_xref(self, refdoc, builder,
typ, target, node, contnode) typ, target, node, contnode)
# really hardwired reference types # really hardwired reference types
elif typ == 'any':
newnode = self._resolve_any_reference(builder, node, contnode)
elif typ == 'doc': elif typ == 'doc':
newnode = self._resolve_doc_reference(builder, node, contnode) newnode = self._resolve_doc_reference(builder, node, contnode)
elif typ == 'citation': elif typ == 'citation':
@ -1435,6 +1437,49 @@ class BuildEnvironment:
# transforms.CitationReference.apply. # transforms.CitationReference.apply.
del node['ids'][:] 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): def process_only_nodes(self, doctree, builder, fromdocname=None):
# A comment on the comment() nodes being inserted: replacing by [] would # A comment on the comment() nodes being inserted: replacing by [] would
# result in a "Losing ids" exception if there is a target node before # result in a "Losing ids" exception if there is a target node before

View File

@ -316,6 +316,8 @@ specific_docroles = {
'download': XRefRole(nodeclass=addnodes.download_reference), 'download': XRefRole(nodeclass=addnodes.download_reference),
# links to documents # links to documents
'doc': XRefRole(warn_dangling=True), 'doc': XRefRole(warn_dangling=True),
# links to anything
'any': XRefRole(warn_dangling=True),
'pep': indexmarkup_role, 'pep': indexmarkup_role,
'rfc': indexmarkup_role, 'rfc': indexmarkup_role,