Skip to content

Commit 0a3f897

Browse files
authored
Merge pull request #8432 from tk0miya/7119_pending_xref_condition
Fix #7119: Show type hint names unqualified when resolving succeeded
2 parents 6886de2 + 7f0b13a commit 0a3f897

File tree

12 files changed

+183
-16
lines changed

12 files changed

+183
-16
lines changed

CHANGES

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Features added
6262
* #8924: autodoc: Support ``bound`` argument for TypeVar
6363
* #4826: py domain: Add ``:canonical:`` option to python directives to describe
6464
the location where the object is defined
65+
* #7199: py domain: Add :confval:`python_use_unqualified_type_names` to suppress
66+
the module name of the python reference if it can be resolved (experimental)
6567
* #7784: i18n: The alt text for image is translated by default (without
6668
:confval:`gettext_additional_targets` setting)
6769
* #2018: html: :confval:`html_favicon` and :confval:`html_logo` now accept URL
@@ -72,6 +74,8 @@ Features added
7274
* #8201: Emit a warning if toctree contains duplicated entries
7375
* #8326: ``master_doc`` is now renamed to :confval:`root_doc`
7476
* #8942: C++, add support for the C++20 spaceship operator, ``<=>``.
77+
* #7199: A new node, ``sphinx.addnodes.pending_xref_condition`` has been added.
78+
It can be used to choose appropriate content of the reference by conditions.
7579

7680
Bugs fixed
7781
----------

doc/extdev/nodes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ New inline nodes
3737

3838
.. autoclass:: index
3939
.. autoclass:: pending_xref
40+
.. autoclass:: pending_xref_condition
4041
.. autoclass:: literal_emphasis
4142
.. autoclass:: download_reference
4243

doc/usage/configuration.rst

+11
Original file line numberDiff line numberDiff line change
@@ -2714,6 +2714,17 @@ Options for the C++ domain
27142714

27152715
.. versionadded:: 1.5
27162716

2717+
Options for the Python domain
2718+
-----------------------------
2719+
2720+
.. confval:: python_use_unqualified_type_names
2721+
2722+
If true, suppress the module name of the python reference if it can be
2723+
resolved. The default is ``False``.
2724+
2725+
.. versionadded:: 4.0
2726+
2727+
.. note:: This configuration is still in experimental
27172728

27182729
Example of configuration file
27192730
=============================

sphinx/addnodes.py

+48
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,54 @@ class pending_xref(nodes.Inline, nodes.Element):
338338
"""
339339

340340

341+
class pending_xref_condition(nodes.Inline, nodes.TextElement):
342+
"""Node for cross-references that are used to choose appropriate
343+
content of the reference by conditions on the resolving phase.
344+
345+
When the :py:class:`pending_xref` node contains one or more
346+
**pending_xref_condition** nodes, the cross-reference resolver
347+
should choose the content of the reference using defined conditions
348+
in ``condition`` attribute of each pending_xref_condition nodes::
349+
350+
<pending_xref refdomain="py" reftarget="io.StringIO ...>
351+
<pending_xref_condition condition="resolved">
352+
<literal>
353+
StringIO
354+
<pending_xref_condition condition="*">
355+
<literal>
356+
io.StringIO
357+
358+
After the processing of cross-reference resolver, one of the content node
359+
under pending_xref_condition node is chosen by its condition and to be
360+
removed all of pending_xref_condition nodes::
361+
362+
# When resolved the cross-reference successfully
363+
<reference>
364+
<literal>
365+
StringIO
366+
367+
# When resolution is failed
368+
<reference>
369+
<literal>
370+
io.StringIO
371+
372+
.. note:: This node is only allowed to be placed under pending_xref node.
373+
It is not allows to place it under other nodes. In addition,
374+
pending_xref node must contain only pending_xref_condition
375+
nodes if it contains one or more pending_xref_condition nodes.
376+
377+
The pending_xref_condition node should have **condition** attribute.
378+
Domains can be store their individual conditions into the attribute to
379+
filter contents on resolving phase. As a reserved condition name,
380+
``condition="*"`` is used for the fallback of resolution failure.
381+
Additionally, as a recommended condition name, ``condition="resolved"``
382+
is used for the representation of resolstion success in the intersphinx
383+
module.
384+
385+
.. versionadded:: 4.0
386+
"""
387+
388+
341389
class number_reference(nodes.reference):
342390
"""Node for number references, similar to pending_xref."""
343391

sphinx/domains/python.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from docutils.parsers.rst import directives
2323

2424
from sphinx import addnodes
25-
from sphinx.addnodes import desc_signature, pending_xref
25+
from sphinx.addnodes import desc_signature, pending_xref, pending_xref_condition
2626
from sphinx.application import Sphinx
2727
from sphinx.builders import Builder
2828
from sphinx.deprecation import RemovedInSphinx50Warning
@@ -37,7 +37,7 @@
3737
from sphinx.util.docfields import Field, GroupedField, TypedField
3838
from sphinx.util.docutils import SphinxDirective
3939
from sphinx.util.inspect import signature_from_str
40-
from sphinx.util.nodes import make_id, make_refnode
40+
from sphinx.util.nodes import find_pending_xref_condition, make_id, make_refnode
4141
from sphinx.util.typing import TextlikeNode
4242

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

95-
return pending_xref('', nodes.Text(text),
95+
if env.config.python_use_unqualified_type_names:
96+
# Note: It would be better to use qualname to describe the object to support support
97+
# nested classes. But python domain can't access the real python object because this
98+
# module should work not-dynamically.
99+
shortname = text.split('.')[-1]
100+
contnodes = [pending_xref_condition('', shortname, condition='resolved'),
101+
pending_xref_condition('', text, condition='*')] # type: List[Node]
102+
else:
103+
contnodes = [nodes.Text(text)]
104+
105+
return pending_xref('', *contnodes,
96106
refdomain='py', reftype=reftype, reftarget=text, **kwargs)
97107

98108

@@ -1209,7 +1219,15 @@ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder
12091219
if obj[2] == 'module':
12101220
return self._make_module_refnode(builder, fromdocname, name, contnode)
12111221
else:
1212-
return make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name)
1222+
# determine the content of the reference by conditions
1223+
content = find_pending_xref_condition(node, 'resolved')
1224+
if content:
1225+
children = content.children
1226+
else:
1227+
# if not found, use contnode
1228+
children = [contnode]
1229+
1230+
return make_refnode(builder, fromdocname, obj[0], obj[1], children, name)
12131231

12141232
def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
12151233
target: str, node: pending_xref, contnode: Element
@@ -1226,9 +1244,17 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str, builder: Bui
12261244
self._make_module_refnode(builder, fromdocname,
12271245
name, contnode)))
12281246
else:
1247+
# determine the content of the reference by conditions
1248+
content = find_pending_xref_condition(node, 'resolved')
1249+
if content:
1250+
children = content.children
1251+
else:
1252+
# if not found, use contnode
1253+
children = [contnode]
1254+
12291255
results.append(('py:' + self.role_for_objtype(obj[2]),
12301256
make_refnode(builder, fromdocname, obj[0], obj[1],
1231-
contnode, name)))
1257+
children, name)))
12321258
return results
12331259

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

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

sphinx/ext/intersphinx.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,19 @@
3333
from urllib.parse import urlsplit, urlunsplit
3434

3535
from docutils import nodes
36-
from docutils.nodes import Element, TextElement
36+
from docutils.nodes import TextElement
3737
from docutils.utils import relative_path
3838

3939
import sphinx
40+
from sphinx.addnodes import pending_xref
4041
from sphinx.application import Sphinx
4142
from sphinx.builders.html import INVENTORY_FILENAME
4243
from sphinx.config import Config
4344
from sphinx.environment import BuildEnvironment
4445
from sphinx.locale import _, __
4546
from sphinx.util import logging, requests
4647
from sphinx.util.inventory import InventoryFile
48+
from sphinx.util.nodes import find_pending_xref_condition
4749
from sphinx.util.typing import Inventory
4850

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

259261

260-
def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: TextElement
261-
) -> nodes.reference:
262+
def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
263+
contnode: TextElement) -> nodes.reference:
262264
"""Attempt to resolve a missing reference via intersphinx references."""
263265
target = node['reftarget']
264266
inventories = InventoryAdapter(env)
@@ -284,6 +286,17 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnod
284286
if 'py:attribute' in objtypes:
285287
# Since Sphinx-2.1, properties are stored as py:method
286288
objtypes.append('py:method')
289+
290+
# determine the contnode by pending_xref_condition
291+
content = find_pending_xref_condition(node, 'resolved')
292+
if content:
293+
# resolved condition found.
294+
contnodes = content.children
295+
contnode = content.children[0] # type: ignore
296+
else:
297+
# not resolved. Use the given contnode
298+
contnodes = [contnode]
299+
287300
to_try = [(inventories.main_inventory, target)]
288301
if domain:
289302
full_qualified_name = env.get_domain(domain).get_full_qualified_name(node)
@@ -316,7 +329,7 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnod
316329
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
317330
if node.get('refexplicit'):
318331
# use whatever title was given
319-
newnode.append(contnode)
332+
newnode.extend(contnodes)
320333
elif dispname == '-' or \
321334
(domain == 'std' and node['reftype'] == 'keyword'):
322335
# use whatever title was given, but strip prefix
@@ -325,7 +338,7 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnod
325338
newnode.append(contnode.__class__(title[len(in_set) + 1:],
326339
title[len(in_set) + 1:]))
327340
else:
328-
newnode.append(contnode)
341+
newnode.extend(contnodes)
329342
else:
330343
# else use the given display name (used for :ref:)
331344
newnode.append(contnode.__class__(dispname, dispname))

sphinx/transforms/post_transforms/__init__.py

+20-3
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@
2222
from sphinx.transforms import SphinxTransform
2323
from sphinx.util import logging
2424
from sphinx.util.docutils import SphinxTranslator
25-
from sphinx.util.nodes import process_only_nodes
25+
from sphinx.util.nodes import find_pending_xref_condition, process_only_nodes
2626

2727
logger = logging.getLogger(__name__)
2828

29+
if False:
30+
# For type annotation
31+
from docutils.nodes import Node
32+
2933

3034
class SphinxPostTransform(SphinxTransform):
3135
"""A base class of post-transforms.
@@ -97,8 +101,21 @@ def run(self, **kwargs: Any) -> None:
97101
if newnode is None:
98102
self.warn_missing_reference(refdoc, typ, target, node, domain)
99103
except NoUri:
100-
newnode = contnode
101-
node.replace_self(newnode or contnode)
104+
newnode = None
105+
106+
if newnode:
107+
newnodes = [newnode] # type: List[Node]
108+
else:
109+
newnodes = [contnode]
110+
if newnode is None and isinstance(node[0], addnodes.pending_xref_condition):
111+
matched = find_pending_xref_condition(node, "*")
112+
if matched:
113+
newnodes = matched.children
114+
else:
115+
logger.warning(__('Could not determine the fallback text for the '
116+
'cross-reference. Might be a bug.'), location=node)
117+
118+
node.replace_self(newnodes)
102119

103120
def resolve_anyref(self, refdoc: str, node: pending_xref, contnode: Element) -> Element:
104121
"""Resolve reference generated by the "any" role."""

sphinx/util/nodes.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import re
1212
import unicodedata
13-
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Set, Tuple, Type, cast
13+
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Set, Tuple, Type, Union, cast
1414

1515
from docutils import nodes
1616
from docutils.nodes import Element, Node
@@ -531,8 +531,18 @@ def make_id(env: "BuildEnvironment", document: nodes.document,
531531
return node_id
532532

533533

534+
def find_pending_xref_condition(node: addnodes.pending_xref, condition: str) -> Element:
535+
"""Pick matched pending_xref_condition node up from the pending_xref."""
536+
for subnode in node:
537+
if (isinstance(subnode, addnodes.pending_xref_condition) and
538+
subnode.get('condition') == condition):
539+
return subnode
540+
else:
541+
return None
542+
543+
534544
def make_refnode(builder: "Builder", fromdocname: str, todocname: str, targetid: str,
535-
child: Node, title: str = None) -> nodes.reference:
545+
child: Union[Node, List[Node]], title: str = None) -> nodes.reference:
536546
"""Shortcut to create a reference node."""
537547
node = nodes.reference('', '', internal=True)
538548
if fromdocname == todocname and targetid:
@@ -545,7 +555,7 @@ def make_refnode(builder: "Builder", fromdocname: str, todocname: str, targetid:
545555
node['refuri'] = builder.get_relative_uri(fromdocname, todocname)
546556
if title:
547557
node['reftitle'] = title
548-
node.append(child)
558+
node += child
549559
return node
550560

551561

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python_use_unqualified_type_names = True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
domain-py-smart_reference
2+
=========================
3+
4+
.. py:class:: Name
5+
:module: foo
6+
7+
8+
.. py:function:: hello(name: foo.Name, age: foo.Age)

tests/test_domain_py.py

+19
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,25 @@ def test_noindexentry(app):
999999
assert_node(doctree[2], addnodes.index, entries=[])
10001000

10011001

1002+
@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names')
1003+
def test_python_python_use_unqualified_type_names(app, status, warning):
1004+
app.build()
1005+
content = (app.outdir / 'index.html').read_text()
1006+
assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
1007+
'<span class="pre">Name</span></a></span>' in content)
1008+
assert '<span class="n"><span class="pre">foo.Age</span></span>' in content
1009+
1010+
1011+
@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names',
1012+
confoverrides={'python_use_unqualified_type_names': False})
1013+
def test_python_python_use_unqualified_type_names_disabled(app, status, warning):
1014+
app.build()
1015+
content = (app.outdir / 'index.html').read_text()
1016+
assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">'
1017+
'<span class="pre">foo.Name</span></a></span>' in content)
1018+
assert '<span class="n"><span class="pre">foo.Age</span></span>' in content
1019+
1020+
10021021
@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning')
10031022
def test_warn_missing_reference(app, status, warning):
10041023
app.build()

tests/test_ext_intersphinx.py

+8
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ def test_missing_reference_pydomain(tempdir, app, status, warning):
196196
rn = missing_reference(app, app.env, node, contnode)
197197
assert rn.astext() == 'Foo.bar'
198198

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

200208
def test_missing_reference_stddomain(tempdir, app, status, warning):
201209
inv_file = tempdir / 'inventory'

0 commit comments

Comments
 (0)