Repository: tox-dev/sphinx-autodoc-typehints (python)

Issue:
Event 'autodoc-process-docstring' threw an exception (exception: unhashable type: 'ParamSpecArgs')
Autodoc 1.21.8 fails for a ParamSpec

```python
HINT_CB_P = ParamSpec('HINT_CB_P')
HINT_CB: TypeAlias = Callable[HINT_CB_P, Any]


class MyClass):
    def func(self, callback: HINT_CB, *args: HINT_CB_P.args, **kwargs: HINT_CB_P.kwargs):
        pass
```

Error:
```
    if annotation in _TYPES_DICT:
TypeError: unhashable type: 'ParamSpecArgs'
```

https://github.com/tox-dev/sphinx-autodoc-typehints/blob/d9c4a06b654f4415d61e5ce23f46c8356a6ab92a/src/sphinx_autodoc_typehints/__init__.py#L39

Pull request:
FIX TypeError: unhashable type
Attempt to fix #318.
The access to `_TYPES_DICT` should be wrapped so the error for unhashable type is captured.
This is just a quick hack that fixes the issue for me and there sure is a more elegant way of doing it ...

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4087e17..2ffac0b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## 1.22
 
 - Allow Sphinx explicitly to write in parallel.
+- Fixed crash when documenting ParamSpecArgs
 
 ## 1.21.7
 
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index 60705ed..af60b9a 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -35,13 +35,25 @@ _TYPES_DICT = {getattr(types, name): name for name in types.__all__}
 _TYPES_DICT[types.FunctionType] = "FunctionType"
 
 
+def _get_types_type(obj: Any) -> str | None:
+    try:
+        return _TYPES_DICT.get(obj)
+    except Exception:
+        # e.g. exception: unhashable type
+        return None
+
+
 def get_annotation_module(annotation: Any) -> str:
-    if annotation in _TYPES_DICT:
-        return "types"
     if annotation is None:
         return "builtins"
+    if _get_types_type(annotation) is not None:
+        return "types"
     is_new_type = sys.version_info >= (3, 10) and isinstance(annotation, NewType)
-    if is_new_type or isinstance(annotation, TypeVar) or type(annotation).__name__ == "ParamSpec":
+    if (
+        is_new_type
+        or isinstance(annotation, TypeVar)
+        or type(annotation).__name__ in ("ParamSpec", "ParamSpecArgs", "ParamSpecKwargs")
+    ):
         return "typing"
     if hasattr(annotation, "__module__"):
         return annotation.__module__  # type: ignore # deduced Any
@@ -63,8 +75,9 @@ def get_annotation_class_name(annotation: Any, module: str) -> str:
         return "None"
     if annotation is AnyStr:
         return "AnyStr"
-    if annotation in _TYPES_DICT:
-        return _TYPES_DICT[annotation]
+    val = _get_types_type(annotation)
+    if val is not None:
+        return val
     if _is_newtype(annotation):
         return "NewType"
 


Test patch:
diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py
index a25a6e6..f9c3a0b 100644
--- a/tests/test_sphinx_autodoc_typehints.py
+++ b/tests/test_sphinx_autodoc_typehints.py
@@ -58,6 +58,8 @@ Z = TypeVar("Z", bound="A")
 S = TypeVar("S", bound="miss")  # type: ignore # miss not defined on purpose # noqa: F821
 W = NewType("W", str)
 P = typing_extensions.ParamSpec("P")
+P_args = P.args  # type:ignore[attr-defined]
+P_kwargs = P.kwargs  # type:ignore[attr-defined]
 P_co = typing_extensions.ParamSpec("P_co", covariant=True)  # type: ignore
 P_contra = typing_extensions.ParamSpec("P_contra", contravariant=True)  # type: ignore
 P_bound = typing_extensions.ParamSpec("P_bound", bound=str)  # type: ignore
@@ -163,6 +165,9 @@ else:
         pytest.param(Match[str], "typing", "Match", (str,), id="Match_parametrized"),
         pytest.param(IO, "typing", "IO", (), id="IO"),
         pytest.param(W, "typing", "NewType", (str,), id="W"),
+        pytest.param(P, "typing", "ParamSpec", (), id="P"),
+        pytest.param(P_args, "typing", "ParamSpecArgs", (), id="P_args"),
+        pytest.param(P_kwargs, "typing", "ParamSpecKwargs", (), id="P_kwargs"),
         pytest.param(Metaclass, __name__, "Metaclass", (), id="Metaclass"),
         pytest.param(Slotted, __name__, "Slotted", (), id="Slotted"),
         pytest.param(A, __name__, "A", (), id="A"),