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 classesYou 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 errorAs 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 runtimeHowever, 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 errorHowever, 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.Genericreturn atyping._GenericAliasinstance. 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._GenericAliaslook for type vars in their arguments usingtyping._collect_type_vars. SoList[Dict[K, V]]->[~K, ~V]becauseListis a_GenericAliasand thus_GenericAlias.__class_getitem__->_GenericAlias.__init__->typing._collect_type_varsget called). In bothtyping.pyandtyping_extensions_collect_type_varsspecifically checks if each type is an instance of_GenericAliasand 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_GenericAliasno type variables are collected. This eventually triggers this error if you try to do something likeList[Model[T]][int]because the outerListthinks it has no generic parameters.Would it be unreasonable for
_GenericAliasto 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