Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GenericModel calling get_args() which is not hashable #4551

Closed
Tracked by #4552
samuelcolvin opened this issue Sep 22, 2022 · 1 comment
Closed
Tracked by #4552

GenericModel calling get_args() which is not hashable #4551

samuelcolvin opened this issue Sep 22, 2022 · 1 comment
Labels
bug V1 Bug related to Pydantic V1.X

Comments

@samuelcolvin
Copy link
Member

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.

@samuelcolvin samuelcolvin added the bug V1 Bug related to Pydantic V1.X label Sep 22, 2022
mfulgo added a commit to mfulgo/pydantic that referenced this issue Oct 24, 2022
Introduced in 1.10.2, a TypeError would be raised upon creation of a
GenericModel class that used a Callable type parameter. This would
happen when `typing.get_args` returned a list for the first type
agruments in a Callable and pydantic would try to use the value as a
dictionary key. To avoid the issue, we convert the list to a tuple
before using it as a key.

The possible approach of modifying pydantic's `get_args` function to
return a tuple instead of a list didn't work out because the return
values are used in more places, some of which expect the list and not a
tuple.

Fixes pydantic#4551
samuelcolvin added a commit that referenced this issue Oct 31, 2022
* Fix TypeError for GenericModel with Callable param

Introduced in 1.10.2, a TypeError would be raised upon creation of a
GenericModel class that used a Callable type parameter. This would
happen when `typing.get_args` returned a list for the first type
agruments in a Callable and pydantic would try to use the value as a
dictionary key. To avoid the issue, we convert the list to a tuple
before using it as a key.

The possible approach of modifying pydantic's `get_args` function to
return a tuple instead of a list didn't work out because the return
values are used in more places, some of which expect the list and not a
tuple.

Fixes #4551

* change as markdown

Co-authored-by: Samuel Colvin <samcolvin@gmail.com>
@samuelcolvin
Copy link
Member Author

Fixed by #4653

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V1 Bug related to Pydantic V1.X
Projects
None yet
Development

No branches or pull requests

1 participant