Skip to content

Commit 565d410

Browse files
👌 Handle external references pointing to object types (sphinx-doc#12133)
This commit fixes the issue of `objects.inv` denoting object names, whilst the `external` role only allows for role names. As an example, take the `objects.inv` for the sphinx documentation, which contains: ``` py:function compile : usage/domains/python.html#compile ``` A user might understandably expect that they could reference this using `` :external:py:function:`compile` ``, but actually this would previously error with: ``` WARNING: role for external cross-reference not found: py:function ``` this is because, `function` is the object type, yet `external` expects the related role name `func`. It should not be necessary for the user to know about this distinction, so in this commit, we add logic, to first look if the name relates to a role name (as previous, to not be back-breaking) but, if not, then also look if the name relates to an object that has a known role and, if so, use that. Co-authored-by: Bénédikt Tran <[email protected]>
1 parent b9b0ad8 commit 565d410

File tree

5 files changed

+57
-24
lines changed

5 files changed

+57
-24
lines changed

‎CHANGES.rst‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Deprecated
2222
Features added
2323
--------------
2424

25+
* #12133: Allow ``external`` roles to reference object types
26+
(rather than role names). Patch by Chris Sewell.
27+
2528
* #12131: Added :confval:`show_warning_types` configuration option.
2629
Patch by Chris Sewell.
2730

‎doc/usage/extensions/intersphinx.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ The Intersphinx extension provides the following role.
218218
e.g., ``:external:py:class:`zipfile.ZipFile```, or
219219
- ``:external:reftype:`target```,
220220
e.g., ``:external:doc:`installation```.
221+
With this shorthand, the domain is assumed to be ``std``.
221222

222223
If you would like to constrain the lookup to a specific external project,
223224
then the key of the project, as specified in :confval:`intersphinx_mapping`,

‎sphinx/ext/intersphinx.py‎

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -552,13 +552,17 @@ def run(self) -> tuple[list[Node], list[system_message]]:
552552
return result, messages
553553

554554
def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]:
555+
"""Extract an inventory name (if any) and ``domain+name`` suffix from a role *name*.
556+
and the domain+name suffix.
557+
558+
The role name is expected to be of one of the following forms:
559+
560+
- ``external+inv:name`` -- explicit inventory and name, any domain.
561+
- ``external+inv:domain:name`` -- explicit inventory, domain and name.
562+
- ``external:name`` -- any inventory and domain, explicit name.
563+
- ``external:domain:name`` -- any inventory, explicit domain and name.
564+
"""
555565
assert name.startswith('external'), name
556-
# either we have an explicit inventory name, i.e,
557-
# :external+inv:role: or
558-
# :external+inv:domain:role:
559-
# or we look in all inventories, i.e.,
560-
# :external:role: or
561-
# :external:domain:role:
562566
suffix = name[9:]
563567
if name[8] == '+':
564568
inv_name, suffix = suffix.split(':', 1)
@@ -570,34 +574,56 @@ def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]:
570574
raise ValueError(msg)
571575

572576
def get_role_name(self, name: str) -> tuple[str, str] | None:
577+
"""Find (if any) the corresponding ``(domain, role name)`` for *name*.
578+
579+
The *name* can be either a role name (e.g., ``py:function`` or ``function``)
580+
given as ``domain:role`` or ``role``, or its corresponding object name
581+
(in this case, ``py:func`` or ``func``) given as ``domain:objname`` or ``objname``.
582+
583+
If no domain is given, or the object/role name is not found for the requested domain,
584+
the 'std' domain is used.
585+
"""
573586
names = name.split(':')
574587
if len(names) == 1:
575-
# role
576588
default_domain = self.env.temp_data.get('default_domain')
577589
domain = default_domain.name if default_domain else None
578-
role = names[0]
590+
name = names[0]
579591
elif len(names) == 2:
580-
# domain:role:
581592
domain = names[0]
582-
role = names[1]
593+
name = names[1]
583594
else:
584595
return None
585596

586-
if domain and self.is_existent_role(domain, role):
597+
if domain and (role := self.get_role_name_from_domain(domain, name)):
587598
return (domain, role)
588-
elif self.is_existent_role('std', role):
599+
elif (role := self.get_role_name_from_domain('std', name)):
589600
return ('std', role)
590601
else:
591602
return None
592603

593-
def is_existent_role(self, domain_name: str, role_name: str) -> bool:
604+
def is_existent_role(self, domain_name: str, role_or_obj_name: str) -> bool:
605+
"""Check if the given role or object exists in the given domain."""
606+
return self.get_role_name_from_domain(domain_name, role_or_obj_name) is not None
607+
608+
def get_role_name_from_domain(self, domain_name: str, role_or_obj_name: str) -> str | None:
609+
"""Check if the given role or object exists in the given domain,
610+
and return the related role name if it exists, otherwise return None.
611+
"""
594612
try:
595613
domain = self.env.get_domain(domain_name)
596-
return role_name in domain.roles
597614
except ExtensionError:
598-
return False
615+
return None
616+
if role_or_obj_name in domain.roles:
617+
return role_or_obj_name
618+
if (
619+
(role_name := domain.role_for_objtype(role_or_obj_name))
620+
and role_name in domain.roles
621+
):
622+
return role_name
623+
return None
599624

600625
def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]:
626+
"""Invoke the role described by a ``(domain, role name)`` pair."""
601627
domain = self.env.get_domain(role[0])
602628
if domain:
603629
role_func = domain.role(role[1])

‎tests/roots/test-ext-intersphinx-role/index.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636

3737
- a function with explicit inventory:
38-
:external+inv:c:func:`CFunc`
38+
:external+inv:c:func:`CFunc` or :external+inv:c:function:`CFunc`
3939
- a class with explicit non-existing inventory, which also has upper-case in name:
4040
:external+invNope:cpp:class:`foo::Bar`
4141

‎tests/test_extensions/test_ext_intersphinx.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
normalize_intersphinx_mapping,
1919
)
2020
from sphinx.ext.intersphinx import setup as intersphinx_setup
21+
from sphinx.util.console import strip_colors
2122

2223
from tests.test_util.test_util_inventory import inventory_v2, inventory_v2_not_having_version
2324
from tests.utils import http_server
@@ -551,30 +552,32 @@ def test_intersphinx_role(app, warning):
551552

552553
app.build()
553554
content = (app.outdir / 'index.html').read_text(encoding='utf8')
554-
wStr = warning.getvalue()
555+
warnings = strip_colors(warning.getvalue()).splitlines()
556+
index_path = app.srcdir / 'index.rst'
557+
assert warnings == [
558+
f'{index_path}:21: WARNING: role for external cross-reference not found: py:nope',
559+
f'{index_path}:28: WARNING: role for external cross-reference not found: nope',
560+
f'{index_path}:39: WARNING: inventory for external cross-reference not found: invNope',
561+
f'{index_path}:9: WARNING: external py:mod reference target not found: module3',
562+
f'{index_path}:14: WARNING: external py:mod reference target not found: module10',
563+
f'{index_path}:19: WARNING: external py:meth reference target not found: inv:Foo.bar',
564+
]
555565

556566
html = '<a class="reference external" href="https://example.org/{}" title="(in foo v2.0)">'
557567
assert html.format('foo.html#module-module1') in content
558568
assert html.format('foo.html#module-module2') in content
559-
assert "WARNING: external py:mod reference target not found: module3" in wStr
560-
assert "WARNING: external py:mod reference target not found: module10" in wStr
561569

562570
assert html.format('sub/foo.html#module1.func') in content
563-
assert "WARNING: external py:meth reference target not found: inv:Foo.bar" in wStr
564-
565-
assert "WARNING: role for external cross-reference not found: py:nope" in wStr
566571

567572
# default domain
568573
assert html.format('index.html#std_uint8_t') in content
569-
assert "WARNING: role for external cross-reference not found: nope" in wStr
570574

571575
# std roles without domain prefix
572576
assert html.format('docname.html') in content
573577
assert html.format('index.html#cmdoption-ls-l') in content
574578

575579
# explicit inventory
576580
assert html.format('cfunc.html#CFunc') in content
577-
assert "WARNING: inventory for external cross-reference not found: invNope" in wStr
578581

579582
# explicit title
580583
assert html.format('index.html#foons') in content

0 commit comments

Comments
 (0)