1818 Self ,
1919)
2020
21+ import copyreg
2122import inspect
2223import itertools
2324import types
@@ -395,7 +396,11 @@ def _process_pydantic_fields(
395396
396397 for fn , field in fields .items ():
397398 ptr = cls .__gel_reflection__ .pointers .get (fn )
398- if (ptr is None or ptr .computed ) and fn != "__linkprops__" :
399+ if (
400+ (ptr is None or ptr .computed )
401+ and fn != "__linkprops__"
402+ and fn not in _abstract .LITERAL_TAG_FIELDS
403+ ):
399404 # Regarding `fn != "__linkprops__"`: see MergedModelMeta --
400405 # it renames `linkprops____` to `__linkprops__` to circumvent
401406 # Pydantic's restriction on fields starting with `_`.
@@ -588,6 +593,8 @@ def _process_pydantic_fields(
588593 validate_assignment = True ,
589594 defer_build = True ,
590595 extra = "forbid" ,
596+ serialize_by_alias = True ,
597+ validate_by_name = False ,
591598)
592599
593600
@@ -659,13 +666,19 @@ def __gel_model_construct__(cls, __dict__: dict[str, Any] | None) -> Self:
659666 ll_setattr (self , "__pydantic_private__" , None )
660667 ll_setattr (self , "__gel_changed_fields__" , None )
661668
669+ ll_setattr (self , "tname__" , str (cls .__gel_reflection__ .name ))
670+
662671 if cls .__gel_has_id_field__ :
663672 mid = self .__dict__ .get ("id" , _unset )
664673 assert mid is not UNSET_UUID
665674 ll_setattr (self , "__gel_new__" , mid is _unset )
666675
667676 return self
668677
678+ @property
679+ def __tname__ (self ) -> str :
680+ return self .tname__ # type: ignore[no-any-return, attr-defined]
681+
669682 @classmethod
670683 def model_construct (
671684 cls ,
@@ -890,7 +903,24 @@ def __delattr__(self, name: str) -> None:
890903
891904 @classmethod
892905 def __gel_validate__ (cls , value : Any ) -> GelSourceModel :
893- return cls .model_validate (value )
906+ # __gel_validate__ is called when validating a gel type
907+ # reached by a link, and Gel links may point to subtypes of
908+ # the declared type. We dispatch on that ourselves, using a
909+ # __tname__ field in the dict.
910+ ccls = cls
911+ key = '__tname__'
912+ if isinstance (value , dict ) and key in value :
913+ ncls : Any = cls .get_class_by_name (value [key ])
914+ if ccls is not ncls :
915+ # For proxy models, we need an appropriate proxy.
916+ if issubclass (ccls , ProxyModel ):
917+ ccls = ccls ._get_subtype_proxy (ncls )
918+ else :
919+ ccls = ncls
920+
921+ res = ccls .model_validate (value )
922+
923+ return res
894924
895925
896926class GelModel (
@@ -950,6 +980,7 @@ def __new__(
950980
951981 validator = cls .__pydantic_validator__
952982 for arg , value in kwargs .items ():
983+ arg = _pydantic_utils .GEL_ALIASES .get (arg , arg ) # noqa: PLW2901 # ignoring it in ruff.toml failed?
953984 # Faster than setattr()
954985 validator .validate_assignment (self , arg , value )
955986
@@ -1216,6 +1247,7 @@ def __build_custom_serializer(
12161247 "__get_pydantic_core_schema__" : classmethod (
12171248 lambda cls , source_type , handler : handler (source_type )
12181249 ),
1250+ "__gel_is_custom_serializer__" : True ,
12191251 },
12201252 )
12211253
@@ -1363,6 +1395,18 @@ def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
13631395 return pydantic .BaseModel .model_dump (self , * args , ** kwargs )
13641396
13651397
1398+ def _pickle_dynamic_proxy_model (cls : Any ) -> Any :
1399+ # See discussion in _typing_parametric. We do the same tricks as
1400+ # PickleableClassParametricType, basically.
1401+ if "__gel_dynamic_proxy_base__" in cls .__dict__ :
1402+ base = cls .__dict__ ["__gel_dynamic_proxy_base__" ]
1403+ return base ._get_subtype_proxy , (cls .__proxy_of__ ,)
1404+ else :
1405+ # If it is not one of our dynamic things, we return the
1406+ # classname and pickle goes and does something useful.
1407+ return cls .__qualname__
1408+
1409+
13661410class ProxyModel (
13671411 GelModel ,
13681412 _abstract .AbstractGelProxyModel [_MT_co , GelLinkModel ],
@@ -1373,6 +1417,12 @@ class ProxyModel(
13731417 if TYPE_CHECKING :
13741418 __gel_proxy_merged_model_cache__ : ClassVar [type [_MergedModelBase ]]
13751419
1420+ # A cache for dynamically generated proxies where the __proxy_of__
1421+ # class is a *subtype*. Generated by _get_subtype_proxy.
1422+ __gel_subtype_proxy_cache__ : ClassVar [
1423+ dict [type [GelModel ], type [ProxyModel [Any ]]] | None
1424+ ] = None
1425+
13761426 # NB: __linkprops__ is not in slots because it is managed by
13771427 # GelLinkModelDescriptor.
13781428
@@ -1406,9 +1456,56 @@ def __init__(
14061456 # We want ProxyModel to be a trasparent wrapper, so we
14071457 # forward the constructor arguments to the wrapped object.
14081458 wrapped = self .__proxy_of__ (** kwargs )
1459+ assert not isinstance (wrapped , ProxyModel )
14091460 ll_setattr (self , "_p__obj__" , wrapped )
14101461 # __linkprops__ is written into __dict__ by GelLinkModelDescriptor
14111462
1463+ def __init_subclass__ (cls ) -> None :
1464+ super ().__init_subclass__ ()
1465+
1466+ # Register a custom reducer for every *metaclass*
1467+ # of ProxyModels. We need this to deal with our dynamic classes.
1468+ # Shouldn't be many.
1469+ copyreg .pickle (
1470+ type (cls ),
1471+ _pickle_dynamic_proxy_model , # pyright: ignore [reportArgumentType]
1472+ )
1473+
1474+ @classmethod
1475+ def _get_subtype_proxy (cls , ncls : type [GelModel ]) -> type [Self ]:
1476+ """Generate a subtype that has ncls as its __proxy_of__"""
1477+
1478+ if ncls is cls .__proxy_of__ :
1479+ return cls
1480+
1481+ if cls .__gel_subtype_proxy_cache__ is None :
1482+ cls .__gel_subtype_proxy_cache__ = {}
1483+
1484+ if cached := cls .__gel_subtype_proxy_cache__ .get (ncls ):
1485+ return cast ('type[Self]' , cached )
1486+
1487+ core_schema = ProxyModel .__dict__ ['__get_pydantic_core_schema__' ]
1488+ new_proxy = type (cls )( # type: ignore[misc]
1489+ f'{ cls .__name__ } [{ ncls .__name__ } ]' ,
1490+ (ncls , ProxyModel [ncls ], cls ), # type: ignore[valid-type]
1491+ {
1492+ k : cls .__dict__ [k ]
1493+ for k in (
1494+ '__module__' ,
1495+ '__qualname__' ,
1496+ )
1497+ if k in cls .__dict__
1498+ }
1499+ | {
1500+ # This gets overridden in some cases, and we need to restore it
1501+ '__get_pydantic_core_schema__' : core_schema ,
1502+ '__gel_dynamic_proxy_base__' : cls ,
1503+ },
1504+ )
1505+ new_proxy .model_rebuild ()
1506+ cls .__gel_subtype_proxy_cache__ [ncls ] = new_proxy
1507+ return cast ('type[Self]' , new_proxy )
1508+
14121509 @classmethod
14131510 def link (cls , obj : _MT_co , / , ** link_props : Any ) -> Self : # type: ignore [misc]
14141511 proxy_of = ll_type_getattr (cls , "__proxy_of__" )
@@ -1439,9 +1536,12 @@ def link(cls, obj: _MT_co, /, **link_props: Any) -> Self: # type: ignore [misc]
14391536 f"are allowed, got { type (obj ).__name__ } " ,
14401537 )
14411538
1442- self = cls .__new__ (cls )
1443- lprops = cls .__linkprops__ (** link_props )
1539+ ncls = cls ._get_subtype_proxy (type (obj ))
1540+
1541+ self = ncls .__new__ (ncls )
1542+ lprops = ncls .__linkprops__ (** link_props )
14441543 ll_setattr (self , "__linkprops__" , lprops )
1544+ assert not isinstance (obj , ProxyModel )
14451545 ll_setattr (self , "_p__obj__" , obj )
14461546
14471547 # Treat newly created link props as if they had all their values
@@ -1521,6 +1621,7 @@ def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
15211621 generic_meta = cls .__pydantic_generic_metadata__
15221622 if generic_meta ["origin" ] is ProxyModel and generic_meta ["args" ]:
15231623 cls .__proxy_of__ = generic_meta ["args" ][0 ]
1624+ assert issubclass (cls .__proxy_of__ , _abstract .AbstractGelModel )
15241625
15251626 @classmethod
15261627 def __make_merged_model (cls ) -> type [_MergedModelBase ]:
@@ -1543,18 +1644,22 @@ def __make_merged_model(cls) -> type[_MergedModelBase]:
15431644 # _MergedModelBase has a custom metaclass, so we must
15441645 # create a common subclass of that and whatever the
15451646 # metaclass of the real type is.
1546- metaclass = types .new_class (
1547- f"{ cls .__name__ } Meta" ,
1548- (_MergedModelMeta , type (cls .__proxy_of__ )),
1549- )
1647+ pmcls = type (cls .__proxy_of__ )
1648+ if issubclass (pmcls , _MergedModelMeta ):
1649+ metaclass = pmcls
1650+ else :
1651+ metaclass = types .new_class (
1652+ f"{ cls .__name__ } Meta" ,
1653+ (_MergedModelMeta , pmcls ),
1654+ )
15501655
15511656 merged = cast (
15521657 "type[_MergedModelBase]" ,
15531658 pydantic .create_model (
15541659 cls .__name__ ,
15551660 __base__ = (
1556- _MergedModelBase ,
15571661 cls .__proxy_of__ ,
1662+ _MergedModelBase ,
15581663 ), # inherit all wrapped fields
15591664 __config__ = DEFAULT_MODEL_CONFIG ,
15601665 __cls_kwargs__ = {"metaclass" : metaclass },
@@ -1632,11 +1737,14 @@ def __gel_proxy_construct__(
16321737 * ,
16331738 linked : bool = False ,
16341739 ) -> Self :
1635- pnv = cls .__gel_model_construct__ (None )
1740+ ncls = cls ._get_subtype_proxy (type (obj ))
1741+
1742+ pnv = ncls .__gel_model_construct__ (None )
1743+ assert not isinstance (obj , ProxyModel )
16361744 ll_setattr (pnv , "_p__obj__" , obj )
16371745
16381746 if type (lprops ) is dict :
1639- lp_obj = cls .__linkprops__ .__gel_model_construct__ (lprops )
1747+ lp_obj = ncls .__linkprops__ .__gel_model_construct__ (lprops )
16401748 else :
16411749 lp_obj = lprops # type: ignore [assignment]
16421750
0 commit comments