Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

👌 Handle external references pointing to object types #12133

Merged
merged 15 commits into from
Mar 19, 2024
Merged
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Deprecated
Features added
--------------

* #12133: Allow ``external`` roles to reference object types
(rather than role names). Patch by Chris Sewell.

* #11701: HTML Search: Adopt the new `<search>`_ element.
Patch by Bénédikt Tran.

Expand Down
53 changes: 38 additions & 15 deletions sphinx/ext/intersphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,13 +552,17 @@ def run(self) -> tuple[list[Node], list[system_message]]:
return result, messages

def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]:
"""Extract an inventory name (if any) and ``domain+name`` suffix from a role *name*.
and the domain+name suffix.

The role name is expected to be of one of the following forms:

- ``external+inv:name`` -- explicit inventory and name, any domain.
- ``external+inv:domain:name`` -- explicit inventory, domain and name.
- ``external:name`` -- any inventory and domain, explicit name.
- ``external:domain:name`` -- any inventory, explicit domain and name.
"""
assert name.startswith('external'), name
# either we have an explicit inventory name, i.e,
# :external+inv:role: or
# :external+inv:domain:role:
# or we look in all inventories, i.e.,
# :external:role: or
# :external:domain:role:
suffix = name[9:]
if name[8] == '+':
inv_name, suffix = suffix.split(':', 1)
Expand All @@ -570,34 +574,53 @@ def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]:
raise ValueError(msg)

def get_role_name(self, name: str) -> tuple[str, str] | None:
"""Find (if any) the corresponding ``(domain, role name)`` for *name*.

The *name* can be either a role name (e.g., ``py:function`` or ``function``)
given as ``domain:role`` or ``role``, or its corresponding object name (in
this case, ``py:func`` or ``func``) given as ``domain:objname`` or ``objname``.
return a tuple of the (domain, role name),
or None if no available domain/role can be found.

The ``name`` can be either a role name or an object name.
chrisjsewell marked this conversation as resolved.
Show resolved Hide resolved
"""
names = name.split(':')
if len(names) == 1:
# role
default_domain = self.env.temp_data.get('default_domain')
domain = default_domain.name if default_domain else None
role = names[0]
name = names[0]
elif len(names) == 2:
# domain:role:
domain = names[0]
role = names[1]
name = names[1]
else:
return None

if domain and self.is_existent_role(domain, role):
if domain and (role := self.is_existent_role(domain, name)):
return (domain, role)
elif self.is_existent_role('std', role):
elif (role := self.is_existent_role('std', name)):
picnixz marked this conversation as resolved.
Show resolved Hide resolved
return ('std', role)
else:
return None

def is_existent_role(self, domain_name: str, role_name: str) -> bool:
def is_existent_role(self, domain_name: str, role_or_obj_name: str) -> None | str:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
"""Check if the given role or object exists in the given domain,
and return the related role if it exists, otherwise return None.
"""
try:
domain = self.env.get_domain(domain_name)
return role_name in domain.roles
except ExtensionError:
return False
return None
if role_or_obj_name in domain.roles:
return role_or_obj_name
if (
(role_name := domain.role_for_objtype(role_or_obj_name))
and role_name in domain.roles
):
return role_name
return None

def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]:
"""Retrieve and invoke the (domain, role)."""
chrisjsewell marked this conversation as resolved.
Show resolved Hide resolved
domain = self.env.get_domain(role[0])
if domain:
role_func = domain.role(role[1])
Expand Down
2 changes: 1 addition & 1 deletion tests/roots/test-ext-intersphinx-role/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@


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

Expand Down
19 changes: 11 additions & 8 deletions tests/test_extensions/test_ext_intersphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
normalize_intersphinx_mapping,
)
from sphinx.ext.intersphinx import setup as intersphinx_setup
from sphinx.util.console import strip_colors
picnixz marked this conversation as resolved.
Show resolved Hide resolved

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

app.build()
content = (app.outdir / 'index.html').read_text(encoding='utf8')
wStr = warning.getvalue()
warnings = strip_colors(warning.getvalue()).replace(str(app.srcdir), "src").replace('\\', '/').splitlines()
picnixz marked this conversation as resolved.
Show resolved Hide resolved

assert warnings == [
'src/index.rst:21: WARNING: role for external cross-reference not found: py:nope',
'src/index.rst:28: WARNING: role for external cross-reference not found: nope',
'src/index.rst:39: WARNING: inventory for external cross-reference not found: invNope',
'src/index.rst:9: WARNING: external py:mod reference target not found: module3',
'src/index.rst:14: WARNING: external py:mod reference target not found: module10',
'src/index.rst:19: WARNING: external py:meth reference target not found: inv:Foo.bar',
]

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

assert html.format('sub/foo.html#module1.func') in content
assert "WARNING: external py:meth reference target not found: inv:Foo.bar" in wStr

assert "WARNING: role for external cross-reference not found: py:nope" in wStr

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

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

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

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