Repository: pydantic/pydantic

Issue:
`GenericModel` calling `get_args()` which is not hashable
See https://github.com/pydantic/pydantic/pull/4482#issuecomment-1253289404

From @pepastach 

> We recently bumped Pydantic from 1.10.1 to 1.10.2 and our tests started failing. After some lengthy investigation I found this change to be the root cause. I'd like to ask you for your opinions.
> 
> We have a Pydantic model with a field such as
> 
> ```python
> f: MyGenericModel[Callable[[Task, Dict[str, Any]], Iterable[Result]]] = Field(...)
> ```
> 
> Just importing Python module with this code gives me:
> 
> ```
> cls = <class 'foo.MyGenericModel'>
> params = typing.Callable[[foo.Task, typing.Dict[str, typing.Any]], typing.Iterable[foo.Result]]
> 
>     def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[Type[Any], ...]]) -> Type[Any]:
>         """Instantiates a new class from a generic class `cls` and type variables `params`.
>     
>         :param params: Tuple of types the class . Given a generic class
>             `Model` with 2 type variables and a concrete model `Model[str, int]`,
>             the value `(str, int)` would be passed to `params`.
>         :return: New model class inheriting from `cls` with instantiated
>             types described by `params`. If no parameters are given, `cls` is
>             returned as is.
>     
>         """
>     
>         def _cache_key(_params: Any) -> Tuple[Type[GenericModelT], Any, Tuple[Any, ...]]:
>             return cls, _params, get_args(_params)
>     
> >       cached = _generic_types_cache.get(_cache_key(params))
> E       TypeError: unhashable type: 'list'
> ```
> 
> The problem seems to be that the function `GenericModel._cache_key()` now calls `get_args()` which in turns calls Python's `typing.get_args()` -> and this function returns a tuple with a list in it. And that makes it unhashable.
> 
> ```python
> def get_args(tp):
>     """Get type arguments with all substitutions performed.
> 
>     For unions, basic simplifications used by Union constructor are performed.
>     Examples::
>         get_args(Dict[str, int]) == (str, int)
>         get_args(int) == ()
>         get_args(Union[int, Union[T, int], str][int]) == (int, str)
>         get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
>         get_args(Callable[[], T][int]) == ([], int)
>     """
>     if isinstance(tp, _GenericAlias) and not tp._special:
>         res = tp.__args__
>         if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis:
>             res = (list(res[:-1]), res[-1])             # <======== this list is a problem
>         return res
>     return ()
> ```
> 
> If I skip the callable typing and only define the field as
> 
> ```python
> f: MyGenericModel[Callable] = Field(...)
> ```
> 
> then it runs okay.
> 
> Thanks in advance!

---

Reply from @sveinugu

> @pepastach Hmm... Seems we did not think of that. Surprised that typing uses lists and not tuples for Callable arguments, but there is probably a good reason for this (or it might be just that it would be too much a nuisance to switch between brackets and parentheses...). In any case, there might be other examples of unhashable return values from `get_params`, so a simple solution could be to just catch `TypeError` and in that case default to the previous _cache_key generation.
> 
> I am new as contributor to pydantic, but I assume it would be cleanest if you could create another issue for this.



Discussion:

Resolving patch:
diff --git a/pydantic/generics.py b/pydantic/generics.py
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -64,7 +64,11 @@ def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[T
         """
 
         def _cache_key(_params: Any) -> Tuple[Type[GenericModelT], Any, Tuple[Any, ...]]:
-            return cls, _params, get_args(_params)
+            args = get_args(_params)
+            # python returns a list for Callables, which is not hashable
+            if len(args) == 2 and isinstance(args[0], list):
+                args = (tuple(args[0]), args[1])
+            return cls, _params, args
 
         cached = _generic_types_cache.get(_cache_key(params))
         if cached is not None:


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -7,6 +7,7 @@
     ClassVar,
     Dict,
     Generic,
+    Iterable,
     List,
     Mapping,
     Optional,
@@ -234,6 +235,32 @@ class Model(GenericModel, Generic[T]):
     assert len(_generic_types_cache) == cache_size + 2
 
 
+def test_cache_keys_are_hashable():
+    cache_size = len(_generic_types_cache)
+    T = TypeVar('T')
+    C = Callable[[str, Dict[str, Any]], Iterable[str]]
+
+    class MyGenericModel(GenericModel, Generic[T]):
+        t: T
+
+    # Callable's first params get converted to a list, which is not hashable.
+    # Make sure we can handle that special case
+    Simple = MyGenericModel[Callable[[int], str]]
+    assert len(_generic_types_cache) == cache_size + 2
+    # Nested Callables
+    MyGenericModel[Callable[[C], Iterable[str]]]
+    assert len(_generic_types_cache) == cache_size + 4
+    MyGenericModel[Callable[[Simple], Iterable[int]]]
+    assert len(_generic_types_cache) == cache_size + 6
+    MyGenericModel[Callable[[MyGenericModel[C]], Iterable[int]]]
+    assert len(_generic_types_cache) == cache_size + 10
+
+    class Model(BaseModel):
+        x: MyGenericModel[Callable[[C], Iterable[str]]] = Field(...)
+
+    assert len(_generic_types_cache) == cache_size + 10
+
+
 def test_generic_config():
     data_type = TypeVar('data_type')