Repository: tox-dev/sphinx-autodoc-typehints

Issue:
Warning for __new__ of NamedTuple
We subclass NamedTuple and want to automatically document the `__new__` method of classes.
We get this warning:

```
WARNING: error while formatting signature for icalendar.cal.alarm.Alarm.Triggers.__new__: Handler <function process_signature at 0x105f32de0> for event 'autodoc-process-signature' threw an exception (exception: 'NoneType' object has no attribute 'Triggers') [autodoc]
WARNING: error while formatting signature for icalendar.prop.AdrFields.__new__: Handler <function process_signature at 0x105f32de0> for event 'autodoc-process-signature' threw an exception (exception: 'NoneType' object has no attribute 'AdrFields') [autodoc]
WARNING: error while formatting signature for icalendar.prop.NFields.__new__: Handler <function process_signature at 0x105f32de0> for event 'autodoc-process-signature' threw an exception (exception: 'NoneType' object has no attribute 'NFields') [autodoc]
```

I can reproduce that the module is not properly set. The last line contains the correct module.

```
~/icalendar 1079-document-init-methods$ python
Python 3.13.2 (main, Feb  5 2025, 19:11:32) [Clang 19.1.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import icalendar
>>> icalendar.NFields.__new__.__module__
'namedtuple_NFields'
>>> icalendar.NFields.__init__.__module__
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    icalendar.NFields.__init__.__module__
AttributeError: 'wrapper_descriptor' object has no attribute '__module__'. Did you mean: '__reduce__'?
>>> icalendar.NFields.__module__
'icalendar.prop' 
```

Code to reproduce:

```python
from typing import NamedTuple
from datetime import timedelta

class Triggers(NamedTuple):
        """The computed times of alarm triggers.
        """

        start: tuple[timedelta]
        end: tuple[timedelta]
        absolute: tuple[datetime]

```

```python
autodoc_default_options = {
    "ignore-module-all": True,
    "members": True,
    "show-inheritance": True,
    "special-members": "__init__,__new__",
    "undoc-members": True,
}
```

docs:

```
Module contents
---------------

.. automodule:: icalendar.timezone
   :members:
   :show-inheritance:
   :undoc-members:
```

Adding these lines changes the error message:

```
Triggers.__new__.__module__ = __module__
Triggers.__new__.__qualname__ = f"{Triggers.__qualname__}.__new__"
```
Error:
```
WARNING: Cannot resolve forward reference in type annotations of "icalendar.cal.alarm.Alarm.Triggers.__new__": name 'tuple' is not defined [sphinx_autodoc_typehints.forward_reference]
```


Related:

- https://github.com/collective/icalendar/pull/1105

Discussion:

Fix (patch):
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index ec18772..e3999e8 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -364,7 +364,7 @@ def remove_prefix(text: str, prefix: str) -> str:
     return "\n".join(aligned_prefix + aligned_suffix)
 
 
-def process_signature(  # noqa: C901, PLR0912, PLR0913, PLR0917
+def process_signature(  # noqa: C901, PLR0911, PLR0912, PLR0913, PLR0917
     app: Sphinx,
     what: str,
     name: str,
@@ -435,8 +435,11 @@ def _get_formatted_annotation(annotation: TypeVar) -> TypeVar:
                 )
                 return None
             outer = inspect.getmodule(obj)
+            if outer is None:
+                return None
             for class_name in obj.__qualname__.split(".")[:-1]:
-                outer = getattr(outer, class_name)
+                if (outer := getattr(outer, class_name, None)) is None:
+                    return None
             method_name = obj.__name__
             if method_name.startswith("__") and not method_name.endswith("__"):
                 # when method starts with double underscore Python applies mangling -> prepend the class name


Test changes:
diff --git a/tests/roots/test-dummy/dummy_module.py b/tests/roots/test-dummy/dummy_module.py
index 0ced791..1dd98a5 100644
--- a/tests/roots/test-dummy/dummy_module.py
+++ b/tests/roots/test-dummy/dummy_module.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from dataclasses import dataclass
+from typing import NamedTuple
 
 
 def undocumented_function(x: int) -> str:
@@ -15,6 +16,13 @@ def undocumented_function_with_defaults(x: int, y: str = "hello") -> str:
     return str(x) + y
 
 
+class MyNamedTuple(NamedTuple):
+    """A named tuple."""
+
+    x: int
+    y: str = "hello"
+
+
 @dataclass
 class DataClass:
     """Class docstring."""
diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py
index 0242292..e92b310 100644
--- a/tests/test_sphinx_autodoc_typehints.py
+++ b/tests/test_sphinx_autodoc_typehints.py
@@ -666,6 +666,30 @@ def test_always_document_param_types_with_defaults_braces_after(
     assert "build succeeded" in status.getvalue()
 
 
+@pytest.mark.sphinx("text", testroot="dummy")
+@patch("sphinx.writers.text.MAXWIDTH", 2000)
+def test_namedtuple_new_no_warning(
+    app: SphinxTestApp,
+    status: StringIO,
+    warning: StringIO,
+) -> None:
+    """Regression test for #601: NamedTuple __new__ causes 'NoneType' attribute error."""
+    set_python_path()
+
+    for rst_file in Path(app.srcdir).glob("*.rst"):
+        rst_file.unlink()
+    index_content = """\
+        .. autoclass:: dummy_module.MyNamedTuple
+            :special-members: __new__
+    """
+    (Path(app.srcdir) / "index.rst").write_text(dedent(index_content))
+
+    app.build()
+
+    assert "build succeeded" in status.getvalue()
+    assert "NoneType" not in warning.getvalue()
+
+
 @pytest.mark.sphinx("text", testroot="dummy")
 @patch("sphinx.writers.text.MAXWIDTH", 2000)
 def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: