Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions src/sphinx_autodoc_typehints/_formats/_sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from docutils.parsers.rst import Directive, directives
from docutils.utils import new_document
from sphinx.parsers import RSTParser
from sphinx.util.docutils import sphinx_domains

from sphinx_autodoc_typehints._parser import _RstSnippetParser

Expand Down Expand Up @@ -109,13 +108,13 @@ def _safe_directive_lookup(
return cls, messages

doc = new_document("", settings=settings) # ty: ignore[invalid-argument-type]
with sphinx_domains(settings.env):
directives.directive = _safe_directive_lookup # type: ignore[assignment]
try:
parser = _RstSnippetParser()
parser.parse(inputstr, doc)
finally:
directives.directive = original_lookup
# The read phase already runs inside sphinx_domains; entering it again shadows the intersphinx
# dispatcher layered on top of it, losing the external+ roles (#753)
directives.directive = _safe_directive_lookup # type: ignore[assignment]
try:
_RstSnippetParser().parse(inputstr, doc)
finally:
directives.directive = original_lookup
return doc


Expand Down
25 changes: 25 additions & 0 deletions tests/roots/test-intersphinx-external-role/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import pathlib
import sys
import zlib

sys.path.insert(0, str(pathlib.Path(__file__).parent))

master_doc = "index"

extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx_autodoc_typehints",
]

# a hand-rolled inventory keeps the fixture offline
_INVENTORY = pathlib.Path(__file__).parent / "objects.inv"
_INVENTORY.write_bytes(
b"# Sphinx inventory version 2\n"
b"# Project: demo\n"
b"# Version: 1.0\n"
b"# The remainder of this file is compressed using zlib.\n" + zlib.compress(b"index std:doc -1 index.html Demo\n")
)
intersphinx_mapping = {"demo": ("https://example.org/demo/", str(_INVENTORY))}
10 changes: 10 additions & 0 deletions tests/roots/test-intersphinx-external-role/demo_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from __future__ import annotations


def probe(x: int) -> int:
"""
Summarize.

:param x: see :external+demo:doc:`the demo docs <index>`.
"""
return x
4 changes: 4 additions & 0 deletions tests/roots/test-intersphinx-external-role/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Demo
====

.. autofunction:: demo_module.probe
12 changes: 11 additions & 1 deletion tests/test_safe_parse.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests that snippet parsing doesn't trigger extension directive side-effects."""
"""Tests that the throwaway snippet parse leaves the real build untouched."""

from __future__ import annotations

Expand Down Expand Up @@ -68,3 +68,13 @@ class _TrackingDirective(Directive):
def run(self) -> list:
_TrackingDirective.executions.append(self.content[0] if self.content else "")
return []


@pytest.mark.sphinx("text", testroot="intersphinx-external-role")
def test_intersphinx_role_resolves_during_snippet_parse(
app: SphinxTestApp, status: StringIO, warning: StringIO
) -> None:
"""The snippet parse must not shadow the intersphinx role dispatcher (#753)."""
app.build()
assert "build succeeded" in status.getvalue()
assert "unknown role name" not in warning.getvalue()