11from enum import Enum
2- from typing import Generic , TypeVar , List , Dict , Any , Union , Optional , get_origin , cast
2+ from typing import Generic , TypeVar , List , Dict , Any , Union , Optional , get_origin , get_args , cast
33from ..utils import JSONElement
44from .model_type import ModelType
55from .model_dictionary import ModelDictionary
@@ -15,21 +15,46 @@ class ModelCollection(ModelType[list[JSONElement]], Generic[T], list[T]):
1515 Useful for updating model object items from JSON data (patches)
1616 """
1717
18- def __init__ (self , item_constructor : type [T ], value : Optional [List [T ]] = None ) -> None :
18+ def __init__ (self , item_constructor : type [T ] | object , value : Optional [List [T ]] = None , allow_none : bool = False ) -> None :
1919 """
2020 :param item_constructor: Item constructor type that items must derive from
2121 :param value: Value used to initialize the list from
22+ :param allow_none: Whether list items may be None
2223 """
2324 from .utils import is_model_object
2425
2526 super ().__init__ ()
26- self ._item_constructor : type [T ] = item_constructor
27- self ._runtime_model_type = cast (type [object ], get_origin (item_constructor ) or item_constructor )
27+ self ._declared_item_constructor = item_constructor
28+ item_origin = get_origin (item_constructor )
29+ item_args = get_args (item_constructor )
30+ self ._allow_none = allow_none or (item_origin in (Union , getattr (__import__ ('types' ), 'UnionType' , Union )) and type (None ) in item_args )
31+
32+ resolved_constructor : object = item_constructor
33+ if item_origin in (Union , getattr (__import__ ('types' ), 'UnionType' , Union )):
34+ non_none_args = [arg for arg in item_args if arg is not type (None )]
35+ if len (non_none_args ) == 1 :
36+ resolved_constructor = non_none_args [0 ]
37+ self ._runtime_model_type = cast (type [object ], get_origin (non_none_args [0 ]) or non_none_args [0 ])
38+ else :
39+ self ._runtime_model_type = object
40+ else :
41+ self ._runtime_model_type = cast (type [object ], item_origin or item_constructor )
42+
43+ if isinstance (resolved_constructor , type ):
44+ self ._item_constructor : type [T ] = cast (type [T ], resolved_constructor )
45+ elif isinstance (self ._runtime_model_type , type ):
46+ self ._item_constructor = cast (type [T ], self ._runtime_model_type )
47+ else :
48+ self ._item_constructor = cast (type [T ], object )
2849
2950 if value is not None :
3051 self [:] = []
3152 for (_ , item ) in enumerate (value ):
32- if isinstance (item , self ._item_constructor ):
53+ if item is None :
54+ self .append (self ._coerce_item_value (item ))
55+ continue
56+
57+ if isinstance (item , self ._runtime_model_type ):
3358 self .append (item )
3459 else :
3560 ref_item = self ._item_constructor ()
@@ -45,6 +70,24 @@ def __init__(self, item_constructor: type[T], value: Optional[List[T]] = None) -
4570 def from_json (cls , data : list [JSONElement ]):
4671 raise RuntimeError ("from_json is not supported for ModelCollection. Use the constructor instead." )
4772
73+ def _coerce_item_value (self , value : JSONElement ) -> T :
74+ """Coerce scalar/enum values using the declared item constructor when possible."""
75+ if value is None :
76+ if self ._allow_none :
77+ return cast (T , None )
78+ raise TypeError (f"None is not allowed for collection of type { self ._declared_item_constructor } " )
79+
80+ if issubclass (self ._runtime_model_type , Enum ):
81+ try :
82+ return self ._item_constructor (value )
83+ except (TypeError , ValueError , KeyError ):
84+ raise ValueError (f"Invalid enum value { value } for collection of type { self ._runtime_model_type .__name__ } " )
85+
86+ try :
87+ return self ._item_constructor (value )
88+ except (TypeError , ValueError ):
89+ return cast (T , value )
90+
4891 def update_from_json (self , data : list [JSONElement ]) -> 'ModelCollection[T]' :
4992 """
5093 Update this instance from the given data
@@ -65,50 +108,49 @@ def update_from_json(self, data: list[JSONElement]) -> 'ModelCollection[T]':
65108 new_item_data = data [i ]
66109
67110 # If the new item data is null, set the current item to null (even if it was a model object before)
68- if new_item_data is None :
111+ if new_item_data is None and self . _allow_none :
69112 self [i ] = None
70113 continue
71114
72115 # If the current item is null then we need to create a new item
73116 if current_item is None :
74- if isinstance (new_item_data , self ._item_constructor ):
75- self [i ] = new_item_data
76- elif issubclass (self ._item_constructor , Enum ):
77- try :
78- enum_value = self ._item_constructor (new_item_data )
79- self .append (enum_value )
80- except KeyError :
81- raise ValueError (f"Invalid enum value { new_item_data } for collection of type { self ._item_constructor .__name__ } " )
117+ if isinstance (new_item_data , self ._runtime_model_type ):
118+ self [i ] = cast (T , new_item_data )
82119 else :
83- ref_item = self ._item_constructor ()
84- if not is_model_object (ref_item ):
85- raise TypeError (f"Item constructor for ModelCollection must inherit from type ModelType to update from a dict."
86- f" Got { type (ref_item ).__name__ } : { ref_item } " )
87- self [i ] = cast (ModelType [JSONElement ], ref_item ).update_from_json (new_item_data )
120+ ref_item : Optional [T ] = None
121+ try :
122+ ref_item = self ._item_constructor ()
123+ except TypeError :
124+ ref_item = None
125+
126+ if ref_item is not None and is_model_object (ref_item ):
127+ self [i ] = cast (T , cast (ModelType [JSONElement ], ref_item ).update_from_json (new_item_data ))
128+ else :
129+ self [i ] = self ._coerce_item_value (new_item_data )
88130 # Use the `update_from_json` method of the current item if it's a model object, otherwise replace it with the new data
89131 elif is_model_object (current_item ):
90- self [i ] = cast (ModelType [JSONElement ], current_item ).update_from_json (new_item_data )
132+ self [i ] = cast (T , cast ( ModelType [JSONElement ], current_item ).update_from_json (new_item_data ) )
91133 else :
92- self [i ] = new_item_data
134+ self [i ] = self . _coerce_item_value ( new_item_data )
93135
94136 # Add new items
95137 for i in range (len (self ), len (data )):
96138 item_to_add = data [i ]
97139 if item_to_add is None :
98- self .append (item_to_add )
99- elif issubclass (self ._item_constructor , Enum ):
100- try :
101- enum_value = self ._item_constructor (item_to_add )
102- self .append (enum_value )
103- except KeyError :
104- raise ValueError (f"Invalid enum value { item_to_add } for collection of type { self ._item_constructor .__name__ } " )
140+ self .append (self ._coerce_item_value (item_to_add ))
141+ elif isinstance (item_to_add , self ._runtime_model_type ):
142+ self .append (cast (T , item_to_add ))
105143 else :
106-
107- ref_item = self ._item_constructor ()
108- if is_model_object (ref_item ):
144+ ref_item : Optional [T ] = None
145+ try :
146+ ref_item = self ._item_constructor ()
147+ except TypeError :
148+ ref_item = None
149+
150+ if ref_item is not None and is_model_object (ref_item ):
109151 self .append (cast (ModelType [JSONElement ], ref_item ).update_from_json (item_to_add ))
110152 else :
111- self .append (item_to_add )
153+ self .append (self . _coerce_item_value ( item_to_add ) )
112154
113155
114156 return self
0 commit comments