Skip to content

Commit

Permalink
Merge pull request #8432 from tk0miya/7119_pending_xref_condition
Browse files Browse the repository at this point in the history
Fix #7119: Show type hint names unqualified when resolving succeeded
  • Loading branch information
tk0miya committed Mar 6, 2021
2 parents 6886de2 + 7f0b13a commit 0a3f897
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Expand Up @@ -62,6 +62,8 @@ Features added
* #8924: autodoc: Support ``bound`` argument for TypeVar
* #4826: py domain: Add ``:canonical:`` option to python directives to describe
the location where the object is defined
* #7199: py domain: Add :confval:`python_use_unqualified_type_names` to suppress
the module name of the python reference if it can be resolved (experimental)
* #7784: i18n: The alt text for image is translated by default (without
:confval:`gettext_additional_targets` setting)
* #2018: html: :confval:`html_favicon` and :confval:`html_logo` now accept URL
Expand All @@ -72,6 +74,8 @@ Features added
* #8201: Emit a warning if toctree contains duplicated entries
* #8326: ``master_doc`` is now renamed to :confval:`root_doc`
* #8942: C++, add support for the C++20 spaceship operator, ``<=>``.
* #7199: A new node, ``sphinx.addnodes.pending_xref_condition`` has been added.
It can be used to choose appropriate content of the reference by conditions.

Bugs fixed
----------
Expand Down
1 change: 1 addition & 0 deletions doc/extdev/nodes.rst
Expand Up @@ -37,6 +37,7 @@ New inline nodes

.. autoclass:: index
.. autoclass:: pending_xref
.. autoclass:: pending_xref_condition
.. autoclass:: literal_emphasis
.. autoclass:: download_reference

Expand Down
11 changes: 11 additions & 0 deletions doc/usage/configuration.rst
Expand Up @@ -2714,6 +2714,17 @@ Options for the C++ domain

.. versionadded:: 1.5

Options for the Python domain
-----------------------------

.. confval:: python_use_unqualified_type_names

If true, suppress the module name of the python reference if it can be
resolved. The default is ``False``.

.. versionadded:: 4.0

.. note:: This configuration is still in experimental

Example of configuration file
=============================
Expand Down
48 changes: 48 additions & 0 deletions sphinx/addnodes.py
Expand Up @@ -338,6 +338,54 @@ class pending_xref(nodes.Inline, nodes.Element):
"""


class pending_xref_condition(nodes.Inline, nodes.TextElement):
"""Node for cross-references that are used to choose appropriate
content of the reference by conditions on the resolving phase.
When the :py:class:`pending_xref` node contains one or more
**pending_xref_condition** nodes, the cross-reference resolver
should choose the content of the reference using defined conditions
in ``condition`` attribute of each pending_xref_condition nodes::
<pending_xref refdomain="py" reftarget="io.StringIO ...>
<pending_xref_condition condition="resolved">
<literal>
StringIO
<pending_xref_condition condition="*">
<literal>
io.StringIO
After the processing of cross-reference resolver, one of the content node
under pending_xref_condition node is chosen by its condition and to be
removed all of pending_xref_condition nodes::
# When resolved the cross-reference successfully
<reference>
<literal>
StringIO
# When resolution is failed
<reference>
<literal>
io.StringIO
.. note:: This node is only allowed to be placed under pending_xref node.
It is not allows to place it under other nodes. In addition,
pending_xref node must contain only pending_xref_condition
nodes if it contains one or more pending_xref_condition nodes.
The pending_xref_condition node should have **condition** attribute.
Domains can be store their individual conditions into the attribute to
filter contents on resolving phase. As a reserved condition name,
``condition="*"`` is used for the fallback of resolution failure.
Additionally, as a recommended condition name, ``condition="resolved"``
is used for the representation of resolstion success in the intersphinx
module.
.. versionadded:: 4.0
"""


class number_reference(nodes.reference):
"""Node for number references, similar to pending_xref."""

Expand Down
37 changes: 32 additions & 5 deletions sphinx/domains/python.py
Expand Up @@ -22,7 +22,7 @@
from docutils.parsers.rst import directives

from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.addnodes import desc_signature, pending_xref, pending_xref_condition
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.deprecation import RemovedInSphinx50Warning
Expand All @@ -37,7 +37,7 @@
from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.docutils import SphinxDirective
from sphinx.util.inspect import signature_from_str
from sphinx.util.nodes import make_id, make_refnode
from sphinx.util.nodes import find_pending_xref_condition, make_id, make_refnode
from sphinx.util.typing import TextlikeNode

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -92,7 +92,17 @@ def type_to_xref(text: str, env: BuildEnvironment = None) -> addnodes.pending_xr
else:
kwargs = {}

return pending_xref('', nodes.Text(text),
if env.config.python_use_unqualified_type_names:
# Note: It would be better to use qualname to describe the object to support support
# nested classes. But python domain can't access the real python object because this
# module should work not-dynamically.
shortname = text.split('.')[-1]
contnodes = [pending_xref_condition('', shortname, condition='resolved'),
pending_xref_condition('', text, condition='*')] # type: List[Node]
else:
contnodes = [nodes.Text(text)]

return pending_xref('', *contnodes,
refdomain='py', reftype=reftype, reftarget=text, **kwargs)


Expand Down Expand Up @@ -1209,7 +1219,15 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder
if obj[2] == 'module':
return self._make_module_refnode(builder, fromdocname, name, contnode)
else:
return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name)
# determine the content of the reference by conditions
content = find_pending_xref_condition(node, 'resolved')
if content:
children = content.children
else:
# if not found, use contnode
children = [contnode]

return make_refnode(builder, fromdocname, obj[0], obj[1], children, name)

def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
target: str, node: pending_xref, contnode: Element
Expand All @@ -1226,9 +1244,17 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui
self._make_module_refnode(builder, fromdocname,
name, contnode)))
else:
# determine the content of the reference by conditions
content = find_pending_xref_condition(node, 'resolved')
if content:
children = content.children
else:
# if not found, use contnode
children = [contnode]

results.append(('py:' + self.role_for_objtype(obj[2]),
make_refnode(builder, fromdocname, obj[0], obj[1],
contnode, name)))
children, name)))
return results

def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str,
Expand Down Expand Up @@ -1295,6 +1321,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.setup_extension('sphinx.directives')

app.add_domain(PythonDomain)
app.add_config_value('python_use_unqualified_type_names', False, 'env')
app.connect('object-description-transform', filter_meta_fields)
app.connect('missing-reference', builtin_resolver, priority=900)

Expand Down
23 changes: 18 additions & 5 deletions sphinx/ext/intersphinx.py
Expand Up @@ -33,17 +33,19 @@
from urllib.parse import urlsplit, urlunsplit

from docutils import nodes
from docutils.nodes import Element, TextElement
from docutils.nodes import TextElement
from docutils.utils import relative_path

import sphinx
from sphinx.addnodes import pending_xref
from sphinx.application import Sphinx
from sphinx.builders.html import INVENTORY_FILENAME
from sphinx.config import Config
from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __
from sphinx.util import logging, requests
from sphinx.util.inventory import InventoryFile
from sphinx.util.nodes import find_pending_xref_condition
from sphinx.util.typing import Inventory

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -257,8 +259,8 @@ def load_mappings(app: Sphinx) -> None:
inventories.main_inventory.setdefault(type, {}).update(objects)


def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: TextElement
) -> nodes.reference:
def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
contnode: TextElement) -> nodes.reference:
"""Attempt to resolve a missing reference via intersphinx references."""
target = node['reftarget']
inventories = InventoryAdapter(env)
Expand All @@ -284,6 +286,17 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnod
if 'py:attribute' in objtypes:
# Since Sphinx-2.1, properties are stored as py:method
objtypes.append('py:method')

# determine the contnode by pending_xref_condition
content = find_pending_xref_condition(node, 'resolved')
if content:
# resolved condition found.
contnodes = content.children
contnode = content.children[0] # type: ignore
else:
# not resolved. Use the given contnode
contnodes = [contnode]

to_try = [(inventories.main_inventory, target)]
if domain:
full_qualified_name = env.get_domain(domain).get_full_qualified_name(node)
Expand Down Expand Up @@ -316,7 +329,7 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnod
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
if node.get('refexplicit'):
# use whatever title was given
newnode.append(contnode)
newnode.extend(contnodes)
elif dispname == '-' or \
(domain == 'std' and node['reftype'] == 'keyword'):
# use whatever title was given, but strip prefix
Expand All @@ -325,7 +338,7 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnod
newnode.append(contnode.__class__(title[len(in_set) + 1:],
title[len(in_set) + 1:]))
else:
newnode.append(contnode)
newnode.extend(contnodes)
else:
# else use the given display name (used for :ref:)
newnode.append(contnode.__class__(dispname, dispname))
Expand Down
23 changes: 20 additions & 3 deletions sphinx/transforms/post_transforms/__init__.py
Expand Up @@ -22,10 +22,14 @@
from sphinx.transforms import SphinxTransform
from sphinx.util import logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.nodes import process_only_nodes
from sphinx.util.nodes import find_pending_xref_condition, process_only_nodes

logger = logging.getLogger(__name__)

if False:
# For type annotation
from docutils.nodes import Node


class SphinxPostTransform(SphinxTransform):
"""A base class of post-transforms.
Expand Down Expand Up @@ -97,8 +101,21 @@ def run(self, **kwargs: Any) -> None:
if newnode is None:
self.warn_missing_reference(refdoc, typ, target, node, domain)
except NoUri:
newnode = contnode
node.replace_self(newnode or contnode)
newnode = None

if newnode:
newnodes = [newnode] # type: List[Node]
else:
newnodes = [contnode]
if newnode is None and isinstance(node[0], addnodes.pending_xref_condition):
matched = find_pending_xref_condition(node, "*")
if matched:
newnodes = matched.children
else:
logger.warning(__('Could not determine the fallback text for the '
'cross-reference. Might be a bug.'), location=node)

node.replace_self(newnodes)

def resolve_anyref(self, refdoc: str, node: pending_xref, contnode: Element) -> Element:
"""Resolve reference generated by the "any" role."""
Expand Down
16 changes: 13 additions & 3 deletions sphinx/util/nodes.py
Expand Up @@ -10,7 +10,7 @@

import re
import unicodedata
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Set, Tuple, Type, cast
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Set, Tuple, Type, Union, cast

from docutils import nodes
from docutils.nodes import Element, Node
Expand Down Expand Up @@ -531,8 +531,18 @@ def make_id(env: "BuildEnvironment", document: nodes.document,
return node_id


def find_pending_xref_condition(node: addnodes.pending_xref, condition: str) -> Element:
"""Pick matched pending_xref_condition node up from the pending_xref."""
for subnode in node:
if (isinstance(subnode, addnodes.pending_xref_condition) and
subnode.get('condition') == condition):
return subnode
else:
return None


def make_refnode(builder: "Builder", fromdocname: str, todocname: str, targetid: str,
child: Node, title: str = None) -> nodes.reference:
child: Union[Node, List[Node]], title: str = None) -> nodes.reference:
"""Shortcut to create a reference node."""
node = nodes.reference('', '', internal=True)
if fromdocname == todocname and targetid:
Expand All @@ -545,7 +555,7 @@ def make_refnode(builder: "Builder", fromdocname: str, todocname: str, targetid:
node['refuri'] = builder.get_relative_uri(fromdocname, todocname)
if title:
node['reftitle'] = title
node.append(child)
node += child
return node


Expand Down
@@ -0,0 +1 @@
python_use_unqualified_type_names = True
@@ -0,0 +1,8 @@
domain-py-smart_reference
=========================

.. py:class:: Name
:module: foo


.. py:function:: hello(name: foo.Name, age: foo.Age)
19 changes: 19 additions & 0 deletions tests/test_domain_py.py
Expand Up @@ -999,6 +999,25 @@ def test_noindexentry(app):
assert_node(doctree[2], addnodes.index, entries=[])


@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names')
def test_python_python_use_unqualified_type_names(app, status, warning):
app.build()
content = (app.outdir / 'index.html').read_text()
assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
'<span class="pre">Name</span></a></span>' in content)
assert '<span class="n"><span class="pre">foo.Age</span></span>' in content


@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names',
confoverrides={'python_use_unqualified_type_names': False})
def test_python_python_use_unqualified_type_names_disabled(app, status, warning):
app.build()
content = (app.outdir / 'index.html').read_text()
assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
'<span class="pre">foo.Name</span></a></span>' in content)
assert '<span class="n"><span class="pre">foo.Age</span></span>' in content


@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning')
def test_warn_missing_reference(app, status, warning):
app.build()
Expand Down
8 changes: 8 additions & 0 deletions tests/test_ext_intersphinx.py
Expand Up @@ -196,6 +196,14 @@ def test_missing_reference_pydomain(tempdir, app, status, warning):
rn = missing_reference(app, app.env, node, contnode)
assert rn.astext() == 'Foo.bar'

# pending_xref_condition="resolved"
node = addnodes.pending_xref('', reftarget='Foo.bar', refdomain='py', reftype='attr')
node['py:module'] = 'module1'
node += addnodes.pending_xref_condition('', 'Foo.bar', condition='resolved')
node += addnodes.pending_xref_condition('', 'module1.Foo.bar', condition='*')
rn = missing_reference(app, app.env, node, nodes.Text('dummy-cont-node'))
assert rn.astext() == 'Foo.bar'


def test_missing_reference_stddomain(tempdir, app, status, warning):
inv_file = tempdir / 'inventory'
Expand Down

0 comments on commit 0a3f897

Please sign in to comment.