From c3eb669f8aa67afff8aaf6f069b186869bd31158 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Fri, 19 Sep 2014 12:59:18 +0200 Subject: [PATCH] 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. --- CHANGES | 5 ++++ doc/markup/inline.rst | 47 +++++++++++++++++++++++++++++++-- sphinx/domains/__init__.py | 19 ++++++++++++++ sphinx/domains/c.py | 11 ++++++++ sphinx/domains/cpp.py | 34 +++++++++++++++++------- sphinx/domains/javascript.py | 12 ++++++++- sphinx/domains/python.py | 51 +++++++++++++++++++++++++++--------- sphinx/domains/rst.py | 13 +++++++++ sphinx/domains/std.py | 17 ++++++++++++ sphinx/environment.py | 45 +++++++++++++++++++++++++++++++ sphinx/roles.py | 2 ++ 11 files changed, 230 insertions(+), 26 deletions(-) diff --git a/CHANGES b/CHANGES index 57d285add..7146251c2 100644 --- a/CHANGES +++ b/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 diff --git a/doc/markup/inline.rst b/doc/markup/inline.rst index 0cc97f43f..7d83e3178 100644 --- a/doc/markup/inline.rst +++ b/doc/markup/inline.rst @@ -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 ------------------------- diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 51b886fdf..cfba9e913 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -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: diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 4d12c141a..a9938c256 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -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) diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 778a36bff..8992991d3 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -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']): diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 2718b8727..dc65b2a39 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -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, \ diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index a7a93cb1e..17609692d 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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) diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index e213211ab..6a4e390f1 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -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 diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index b5cc81b93..4910a52f8 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -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) diff --git a/sphinx/environment.py b/sphinx/environment.py index fbc56b3da..6a3f1c68d 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -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 diff --git a/sphinx/roles.py b/sphinx/roles.py index b99b22795..b253097e3 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -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,