Skip to content

Commit 3bb7231

Browse files
authored
🐛 fix(resolver): resolve NamedTuple hints from class (#657)
Fixes #656 NamedTuples with non-builtin type annotations (e.g. `Path`) combined with `from __future__ import annotations` produce spurious "Cannot resolve forward reference" warnings since 3.9.6. 🐛 This happens because the `__new__` method generated for NamedTuples has its `__module__` set to a synthetic name like `namedtuple_Program` that doesn't exist in `sys.modules`, so `typing.get_type_hints()` can't resolve the stringified annotations. The fix passes the original class to `get_all_type_hints()` instead of `__new__` when dealing with NamedTuple-style classes (custom `__new__`, inherited `__init__`). The class has the correct `__module__` pointing to the real module where the imports live, while `__new__` continues to be used for signature extraction as intended by the 3.9.6 C extension support change.
1 parent eed675c commit 3bb7231

File tree

3 files changed

+33
-2
lines changed

3 files changed

+33
-2
lines changed

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,11 @@ def process_docstring( # noqa: PLR0913, PLR0917
152152
if inspect.isclass(obj):
153153
backfill_attrs_annotations(obj)
154154
use_class_for_signature = False
155+
cls_for_hints = None
155156
if inspect.isclass(obj):
156157
if obj.__init__ is object.__init__ and obj.__new__ is not object.__new__:
157158
use_class_for_signature = True
159+
cls_for_hints = obj
158160
obj = obj.__new__
159161
else:
160162
obj = obj.__init__
@@ -176,7 +178,7 @@ def process_docstring( # noqa: PLR0913, PLR0917
176178
if (env := getattr(app, "env", None)) is not None:
177179
deferred, eager_aliases = collect_documented_type_aliases(obj, module_prefix, env)
178180
localns.update(deferred)
179-
type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name, localns)
181+
type_hints = get_all_type_hints(app.config.autodoc_mock_imports, cls_for_hints or obj, name, localns)
180182
for param, hint in type_hints.items():
181183
if id(hint) in eager_aliases:
182184
type_hints[param] = eager_aliases[id(hint)]

tests/roots/test-dummy/dummy_module.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4-
from typing import NamedTuple
4+
from typing import TYPE_CHECKING, NamedTuple
5+
6+
if TYPE_CHECKING:
7+
from pathlib import Path
58

69

710
def undocumented_function(x: int) -> str:
@@ -23,6 +26,13 @@ class MyNamedTuple(NamedTuple):
2326
y: str = "hello"
2427

2528

29+
class MyNamedTupleWithPath(NamedTuple):
30+
"""A named tuple with non-builtin types."""
31+
32+
path: Path
33+
version: str
34+
35+
2636
@dataclass
2737
class DataClass:
2838
"""Class docstring."""

tests/test_sphinx_autodoc_typehints.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,25 @@ def test_namedtuple_new_no_warning(
166166
assert "NoneType" not in warning.getvalue()
167167

168168

169+
@pytest.mark.sphinx("text", testroot="dummy")
170+
def test_namedtuple_no_forward_ref_warning(
171+
app: SphinxTestApp,
172+
status: StringIO,
173+
warning: StringIO,
174+
write_rst: Callable[[str], None],
175+
) -> None:
176+
"""Regression test for #656: NamedTuple forward reference resolution fails."""
177+
write_rst("""\
178+
.. autoclass:: dummy_module.MyNamedTupleWithPath
179+
:undoc-members:
180+
""")
181+
182+
app.build()
183+
184+
assert "build succeeded" in status.getvalue()
185+
assert "Cannot resolve forward reference" not in warning.getvalue()
186+
187+
169188
@pytest.mark.sphinx("text", testroot="dummy")
170189
def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None:
171190
app.config.master_doc = "future_annotations" # create flag

0 commit comments

Comments
 (0)