Replies: 1 comment
-
I want to provide some additional context to @adriangb's question. When creating a web api, it's common to have many types that have the same top-level fields, with a reused inner field. For example: from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel
class IntResponse(BaseModel):
value: Optional[int]
error_message: Optional[str] = None
class StrResponse(BaseModel):
value: Optional[str]
error_message: Optional[str] = None
# You could imagine other kinds of XyzResponse classes You can clearly see this looks ripe for some sort of generics-like approach. However, one important difference with the way generics work in the python typing module is that when you parametrize a typing-generics class, you don't retain the ability to have different underlying behavior at runtime as a function of the parameters. In particular, for the above (non-generic) classes, we have: IntResponse.model_validate({'value': 1}) # passes
StrResponse.model_validate({'value': 'a'}) # passes
# IntResponse.model_validate({'value': 'a'}) # would raise an error As far as I can tell, because of the way T = TypeVar("T")
class StandardGenericResponse(Generic[T]):
value: Optional[T]
error_message: Optional[str] = None
@classmethod
def model_validate(cls, data):
... # ???
... # any other methods/etc. as appropriate
StandardGenericResponse[int].model_validate({'value': 1}) # *should not* error at runtime
StandardGenericResponse[str].model_validate({'value': 'a'}) # *should not* error at runtime
StandardGenericResponse[int].model_validate({'value': 'a'}) # *should* raise an error at runtime However, through some elbow grease, we have largely been able to achieve a work-around for this in pydantic, by adding a bunch of custom T = TypeVar("T")
class ResponseModel(BaseModel, Generic[T]): # BaseModel has a custom implementation of __class_getitem__
value: Optional[T]
error_message: Optional[str] = None
ResponseModel[int].model_validate({'value': 1}) # passes
ResponseModel[str].model_validate({'value': 'a'}) # passes
# ResponseModel[int].model_validate({'value': 'a'}) # would raise an error However, there are two issues we've run into with this:
ResponseModelList = List[ResponseModel[T]]
# The following line would raise TypeError: typing.List[__main__.XModel] is not a generic class
# IntResponseModelList = ResponseModelList[int]
# Ideally, it would produce List[ResponseModel[int]]
List[StandardGenericResponse[T]][int] # no error, produces List[StandardGenericResponse[int]] With the above description as motivation, ultimately, I think the thing that would empower us to resolve all of these issues (and simplify our codebase significantly) would be for In particular, if the following code could run without an error on the final line, I think we'd be in good shape: from dataclasses import dataclass
from typing import Dict, List, Generic, TypeVar, Any
T = TypeVar("T")
@dataclass
class MyGenericAlias:
__origin__: Any
__parameters__: Any
__args__: Any
@staticmethod
def construct(cls):
return MyGenericAlias(cls.__origin__, cls.__parameters__, cls.__args__)
def __call__(self, *args, **kwargs):
# without this, we'll get:
# TypeError: Parameters to generic types must be types. Got MyGenericAlias(__origin__=<class '__main__.CustomGeneric'>, __parameters__=(~T,), __args__=(typing.D.
pass
class StandardGeneric(Generic[T]):
pass
class CustomGeneric(Generic[T]):
__origin__: Any
__parameters__: Any
__args__: Any
def __class_getitem__(cls, item) -> MyGenericAlias:
# return super().__class_getitem__(item)
# Only difference: rather than
return MyGenericAlias(
__origin__=CustomGeneric,
__parameters__=getattr(item, '__parameters__', None),
__args__=(item,)
)
print(MyGenericAlias.construct(StandardGeneric[Dict[int, T]]))
# the following looks basically the same, indicating same origin, parameters, args
print(MyGenericAlias.construct(CustomGeneric[Dict[int, T]]))
List[StandardGeneric[Dict[int, T]]][int]
# The next line raises:
# TypeError: typing.List[MyGenericAlias(__origin__=<class '__main__.CustomGeneric'>, __parameters__=(~T,), __args__=(typing.Dict[int, ~T],))] is not a generic class
List[CustomGeneric[Dict[int, T]]][int] (Happy to explain more if useful.) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
What are valid things to return from
__class_getitem__
? PEP 560 gives as an example a string of the formf"{cls.__name__}[{item.__name__}]"
. Subclasses oftyping.Generic
return atyping._GenericAlias
instance. I tried to write a class that tracks generic parameters so that they are available when it is instantiated by returning itself (code below) and am running into problems.The issue I've encountered is that the internals of
typing._GenericAlias
look for type vars in their arguments usingtyping._collect_type_vars
. SoList[Dict[K, V]]
->[~K, ~V]
becauseList
is a_GenericAlias
and thus_GenericAlias.__class_getitem__
->_GenericAlias.__init__
->typing._collect_type_vars
get called). In bothtyping.py
andtyping_extensions
_collect_type_vars
specifically checks if each type is an instance of_GenericAlias
and only collects type vars from it (by accessing__parameters__
) if it is (typing_extensions implementation). So for my class, which has or can have__parameters__
but is not a subclass of_GenericAlias
no type variables are collected. This eventually triggers this error if you try to do something likeList[Model[T]][int]
because the outerList
thinks it has no generic parameters.Would it be unreasonable for
_GenericAlias
to dogetattr(type, '__parameters__', ())
or something like that? Or is there some way I can restructure my implementation to avoid these issues?This is somewhat similar (in goal at least) to #1050 but I didn't want to hijack that.
Beta Was this translation helpful? Give feedback.
All reactions