Skip to content

GenericModel calling get_args() which is not hashable #4551

@samuelcolvin

Description

@samuelcolvin

See #4482 (comment)

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

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.

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

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug V1Bug related to Pydantic V1.X

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions