3737
3838TZ_UTC = timezone .utc
3939_T = typing .TypeVar ("_T" )
40+ _NONE_TYPE = type (None )
4041
4142
4243def _timedelta_as_isostr (td : timedelta ) -> str :
@@ -171,6 +172,21 @@ def default(self, o): # pylint: disable=too-many-return-statements
171172 r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s\d{4}\s\d{2}:\d{2}:\d{2}\sGMT"
172173)
173174
175+ _ARRAY_ENCODE_MAPPING = {
176+ "pipeDelimited" : "|" ,
177+ "spaceDelimited" : " " ,
178+ "commaDelimited" : "," ,
179+ "newlineDelimited" : "\n " ,
180+ }
181+
182+
183+ def _deserialize_array_encoded (delimit : str , attr ):
184+ if isinstance (attr , str ):
185+ if attr == "" :
186+ return []
187+ return attr .split (delimit )
188+ return attr
189+
174190
175191def _deserialize_datetime (attr : typing .Union [str , datetime ]) -> datetime :
176192 """Deserialize ISO-8601 formatted string into Datetime object.
@@ -202,7 +218,7 @@ def _deserialize_datetime(attr: typing.Union[str, datetime]) -> datetime:
202218 test_utc = date_obj .utctimetuple ()
203219 if test_utc .tm_year > 9999 or test_utc .tm_year < 1 :
204220 raise OverflowError ("Hit max or min date" )
205- return date_obj
221+ return date_obj # type: ignore[no-any-return]
206222
207223
208224def _deserialize_datetime_rfc7231 (attr : typing .Union [str , datetime ]) -> datetime :
@@ -256,7 +272,7 @@ def _deserialize_time(attr: typing.Union[str, time]) -> time:
256272 """
257273 if isinstance (attr , time ):
258274 return attr
259- return isodate .parse_time (attr )
275+ return isodate .parse_time (attr ) # type: ignore[no-any-return]
260276
261277
262278def _deserialize_bytes (attr ):
@@ -315,6 +331,8 @@ def _deserialize_int_as_str(attr):
315331def get_deserializer (annotation : typing .Any , rf : typing .Optional ["_RestField" ] = None ):
316332 if annotation is int and rf and rf ._format == "str" :
317333 return _deserialize_int_as_str
334+ if annotation is str and rf and rf ._format in _ARRAY_ENCODE_MAPPING :
335+ return functools .partial (_deserialize_array_encoded , _ARRAY_ENCODE_MAPPING [rf ._format ])
318336 if rf and rf ._format :
319337 return _DESERIALIZE_MAPPING_WITHFORMAT .get (rf ._format )
320338 return _DESERIALIZE_MAPPING .get (annotation ) # pyright: ignore
@@ -353,9 +371,39 @@ def __contains__(self, key: typing.Any) -> bool:
353371 return key in self ._data
354372
355373 def __getitem__ (self , key : str ) -> typing .Any :
374+ # If this key has been deserialized (for mutable types), we need to handle serialization
375+ if hasattr (self , "_attr_to_rest_field" ):
376+ cache_attr = f"_deserialized_{ key } "
377+ if hasattr (self , cache_attr ):
378+ rf = _get_rest_field (getattr (self , "_attr_to_rest_field" ), key )
379+ if rf :
380+ value = self ._data .get (key )
381+ if isinstance (value , (dict , list , set )):
382+ # For mutable types, serialize and return
383+ # But also update _data with serialized form and clear flag
384+ # so mutations via this returned value affect _data
385+ serialized = _serialize (value , rf ._format )
386+ # If serialized form is same type (no transformation needed),
387+ # return _data directly so mutations work
388+ if isinstance (serialized , type (value )) and serialized == value :
389+ return self ._data .get (key )
390+ # Otherwise return serialized copy and clear flag
391+ try :
392+ object .__delattr__ (self , cache_attr )
393+ except AttributeError :
394+ pass
395+ # Store serialized form back
396+ self ._data [key ] = serialized
397+ return serialized
356398 return self ._data .__getitem__ (key )
357399
358400 def __setitem__ (self , key : str , value : typing .Any ) -> None :
401+ # Clear any cached deserialized value when setting through dictionary access
402+ cache_attr = f"_deserialized_{ key } "
403+ try :
404+ object .__delattr__ (self , cache_attr )
405+ except AttributeError :
406+ pass
359407 self ._data .__setitem__ (key , value )
360408
361409 def __delitem__ (self , key : str ) -> None :
@@ -483,6 +531,8 @@ def _is_model(obj: typing.Any) -> bool:
483531
484532def _serialize (o , format : typing .Optional [str ] = None ): # pylint: disable=too-many-return-statements
485533 if isinstance (o , list ):
534+ if format in _ARRAY_ENCODE_MAPPING and all (isinstance (x , str ) for x in o ):
535+ return _ARRAY_ENCODE_MAPPING [format ].join (o )
486536 return [_serialize (x , format ) for x in o ]
487537 if isinstance (o , dict ):
488538 return {k : _serialize (v , format ) for k , v in o .items ()}
@@ -638,6 +688,10 @@ def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self:
638688 if not rf ._rest_name_input :
639689 rf ._rest_name_input = attr
640690 cls ._attr_to_rest_field : dict [str , _RestField ] = dict (attr_to_rest_field .items ())
691+ cls ._backcompat_attr_to_rest_field : dict [str , _RestField ] = {
692+ Model ._get_backcompat_attribute_name (cls ._attr_to_rest_field , attr ): rf
693+ for attr , rf in cls ._attr_to_rest_field .items ()
694+ }
641695 cls ._calculated .add (f"{ cls .__module__ } .{ cls .__qualname__ } " )
642696
643697 return super ().__new__ (cls )
@@ -647,6 +701,16 @@ def __init_subclass__(cls, discriminator: typing.Optional[str] = None) -> None:
647701 if hasattr (base , "__mapping__" ):
648702 base .__mapping__ [discriminator or cls .__name__ ] = cls # type: ignore
649703
704+ @classmethod
705+ def _get_backcompat_attribute_name (cls , attr_to_rest_field : dict [str , "_RestField" ], attr_name : str ) -> str :
706+ rest_field_obj = attr_to_rest_field .get (attr_name ) # pylint: disable=protected-access
707+ if rest_field_obj is None :
708+ return attr_name
709+ original_tsp_name = getattr (rest_field_obj , "_original_tsp_name" , None ) # pylint: disable=protected-access
710+ if original_tsp_name :
711+ return original_tsp_name
712+ return attr_name
713+
650714 @classmethod
651715 def _get_discriminator (cls , exist_discriminators ) -> typing .Optional ["_RestField" ]:
652716 for v in cls .__dict__ .values ():
@@ -758,6 +822,14 @@ def _deserialize_multiple_sequence(
758822 return type (obj )(_deserialize (deserializer , entry , module ) for entry , deserializer in zip (obj , entry_deserializers ))
759823
760824
825+ def _is_array_encoded_deserializer (deserializer : functools .partial ) -> bool :
826+ return (
827+ isinstance (deserializer , functools .partial )
828+ and isinstance (deserializer .args [0 ], functools .partial )
829+ and deserializer .args [0 ].func == _deserialize_array_encoded # pylint: disable=comparison-with-callable
830+ )
831+
832+
761833def _deserialize_sequence (
762834 deserializer : typing .Optional [typing .Callable ],
763835 module : typing .Optional [str ],
@@ -767,6 +839,19 @@ def _deserialize_sequence(
767839 return obj
768840 if isinstance (obj , ET .Element ):
769841 obj = list (obj )
842+
843+ # encoded string may be deserialized to sequence
844+ if isinstance (obj , str ) and isinstance (deserializer , functools .partial ):
845+ # for list[str]
846+ if _is_array_encoded_deserializer (deserializer ):
847+ return deserializer (obj )
848+
849+ # for list[Union[...]]
850+ if isinstance (deserializer .args [0 ], list ):
851+ for sub_deserializer in deserializer .args [0 ]:
852+ if _is_array_encoded_deserializer (sub_deserializer ):
853+ return sub_deserializer (obj )
854+
770855 return type (obj )(_deserialize (deserializer , entry , module ) for entry in obj )
771856
772857
@@ -817,16 +902,16 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur
817902
818903 # is it optional?
819904 try :
820- if any (a for a in annotation .__args__ if a == type ( None ) ): # pyright: ignore
905+ if any (a is _NONE_TYPE for a in annotation .__args__ ): # pyright: ignore
821906 if len (annotation .__args__ ) <= 2 : # pyright: ignore
822907 if_obj_deserializer = _get_deserialize_callable_from_annotation (
823- next (a for a in annotation .__args__ if a != type ( None ) ), module , rf # pyright: ignore
908+ next (a for a in annotation .__args__ if a is not _NONE_TYPE ), module , rf # pyright: ignore
824909 )
825910
826911 return functools .partial (_deserialize_with_optional , if_obj_deserializer )
827912 # the type is Optional[Union[...]], we need to remove the None type from the Union
828913 annotation_copy = copy .copy (annotation )
829- annotation_copy .__args__ = [a for a in annotation_copy .__args__ if a != type ( None ) ] # pyright: ignore
914+ annotation_copy .__args__ = [a for a in annotation_copy .__args__ if a is not _NONE_TYPE ] # pyright: ignore
830915 return _get_deserialize_callable_from_annotation (annotation_copy , module , rf )
831916 except AttributeError :
832917 pass
@@ -972,6 +1057,7 @@ def _failsafe_deserialize_xml(
9721057 return None
9731058
9741059
1060+ # pylint: disable=too-many-instance-attributes
9751061class _RestField :
9761062 def __init__ (
9771063 self ,
@@ -984,6 +1070,7 @@ def __init__(
9841070 format : typing .Optional [str ] = None ,
9851071 is_multipart_file_input : bool = False ,
9861072 xml : typing .Optional [dict [str , typing .Any ]] = None ,
1073+ original_tsp_name : typing .Optional [str ] = None ,
9871074 ):
9881075 self ._type = type
9891076 self ._rest_name_input = name
@@ -995,10 +1082,15 @@ def __init__(
9951082 self ._format = format
9961083 self ._is_multipart_file_input = is_multipart_file_input
9971084 self ._xml = xml if xml is not None else {}
1085+ self ._original_tsp_name = original_tsp_name
9981086
9991087 @property
10001088 def _class_type (self ) -> typing .Any :
1001- return getattr (self ._type , "args" , [None ])[0 ]
1089+ result = getattr (self ._type , "args" , [None ])[0 ]
1090+ # type may be wrapped by nested functools.partial so we need to check for that
1091+ if isinstance (result , functools .partial ):
1092+ return getattr (result , "args" , [None ])[0 ]
1093+ return result
10021094
10031095 @property
10041096 def _rest_name (self ) -> str :
@@ -1009,14 +1101,37 @@ def _rest_name(self) -> str:
10091101 def __get__ (self , obj : Model , type = None ): # pylint: disable=redefined-builtin
10101102 # by this point, type and rest_name will have a value bc we default
10111103 # them in __new__ of the Model class
1012- item = obj .get (self ._rest_name )
1104+ # Use _data.get() directly to avoid triggering __getitem__ which clears the cache
1105+ item = obj ._data .get (self ._rest_name )
10131106 if item is None :
10141107 return item
10151108 if self ._is_model :
10161109 return item
1017- return _deserialize (self ._type , _serialize (item , self ._format ), rf = self )
1110+
1111+ # For mutable types, we want mutations to directly affect _data
1112+ # Check if we've already deserialized this value
1113+ cache_attr = f"_deserialized_{ self ._rest_name } "
1114+ if hasattr (obj , cache_attr ):
1115+ # Return the value from _data directly (it's been deserialized in place)
1116+ return obj ._data .get (self ._rest_name )
1117+
1118+ deserialized = _deserialize (self ._type , _serialize (item , self ._format ), rf = self )
1119+
1120+ # For mutable types, store the deserialized value back in _data
1121+ # so mutations directly affect _data
1122+ if isinstance (deserialized , (dict , list , set )):
1123+ obj ._data [self ._rest_name ] = deserialized
1124+ object .__setattr__ (obj , cache_attr , True ) # Mark as deserialized
1125+ return deserialized
1126+
1127+ return deserialized
10181128
10191129 def __set__ (self , obj : Model , value ) -> None :
1130+ # Clear the cached deserialized object when setting a new value
1131+ cache_attr = f"_deserialized_{ self ._rest_name } "
1132+ if hasattr (obj , cache_attr ):
1133+ object .__delattr__ (obj , cache_attr )
1134+
10201135 if value is None :
10211136 # we want to wipe out entries if users set attr to None
10221137 try :
@@ -1046,6 +1161,7 @@ def rest_field(
10461161 format : typing .Optional [str ] = None ,
10471162 is_multipart_file_input : bool = False ,
10481163 xml : typing .Optional [dict [str , typing .Any ]] = None ,
1164+ original_tsp_name : typing .Optional [str ] = None ,
10491165) -> typing .Any :
10501166 return _RestField (
10511167 name = name ,
@@ -1055,6 +1171,7 @@ def rest_field(
10551171 format = format ,
10561172 is_multipart_file_input = is_multipart_file_input ,
10571173 xml = xml ,
1174+ original_tsp_name = original_tsp_name ,
10581175 )
10591176
10601177
@@ -1184,7 +1301,7 @@ def _get_wrapped_element(
11841301 _get_element (v , exclude_readonly , meta , wrapped_element )
11851302 else :
11861303 wrapped_element .text = _get_primitive_type_value (v )
1187- return wrapped_element
1304+ return wrapped_element # type: ignore[no-any-return]
11881305
11891306
11901307def _get_primitive_type_value (v ) -> str :
@@ -1197,7 +1314,9 @@ def _get_primitive_type_value(v) -> str:
11971314 return str (v )
11981315
11991316
1200- def _create_xml_element (tag , prefix = None , ns = None ):
1317+ def _create_xml_element (
1318+ tag : typing .Any , prefix : typing .Optional [str ] = None , ns : typing .Optional [str ] = None
1319+ ) -> ET .Element :
12011320 if prefix and ns :
12021321 ET .register_namespace (prefix , ns )
12031322 if ns :
0 commit comments