2525from enum import Enum
2626
2727from ..models .decode_options import DecodeOptions
28+ from ..models .overflow_dict import OverflowDict
2829from ..models .undefined import Undefined
2930
3031
32+ def _numeric_key_pairs (mapping : t .Mapping [t .Any , t .Any ]) -> t .List [t .Tuple [int , t .Any ]]:
33+ """Return (numeric_key, original_key) for keys that coerce to int.
34+
35+ Note: distinct keys like "01" and "1" both coerce to 1; downstream merges
36+ may overwrite earlier values when materializing numeric-keyed dicts.
37+ """
38+ pairs : t .List [t .Tuple [int , t .Any ]] = []
39+ for key in mapping .keys ():
40+ try :
41+ numeric_key = int (key )
42+ except (TypeError , ValueError ):
43+ continue
44+ pairs .append ((numeric_key , key ))
45+ return pairs
46+
47+
3148class Utils :
3249 """
3350 Namespace container for stateless utility routines.
@@ -143,6 +160,9 @@ def merge(
143160 target = list (target )
144161 target .append (source )
145162 elif isinstance (target , t .Mapping ):
163+ if Utils .is_overflow (target ):
164+ return Utils .combine (target , source , options )
165+
146166 # Target is a mapping but source is a sequence — coerce indices to string keys.
147167 if isinstance (source , (list , tuple )):
148168 _new = dict (target )
@@ -166,28 +186,58 @@ def merge(
166186 ** source ,
167187 }
168188
189+ if Utils .is_overflow (source ):
190+ source_of = t .cast (OverflowDict , source )
191+ sorted_pairs = sorted (_numeric_key_pairs (source_of ), key = lambda item : item [0 ])
192+ numeric_keys = {key for _ , key in sorted_pairs }
193+ result = OverflowDict ()
194+ offset = 0
195+ if not isinstance (target , Undefined ):
196+ result ["0" ] = target
197+ offset = 1
198+ for numeric_key , key in sorted_pairs :
199+ val = source_of [key ]
200+ if not isinstance (val , Undefined ):
201+ # Offset ensures target occupies index "0"; source indices shift up by 1
202+ result [str (numeric_key + offset )] = val
203+ for key , val in source_of .items ():
204+ if key in numeric_keys :
205+ continue
206+ if not isinstance (val , Undefined ):
207+ result [key ] = val
208+ return result
209+
169210 _res : t .List [t .Any ] = []
170211 _iter1 = target if isinstance (target , (list , tuple )) else [target ]
171212 for _el in _iter1 :
172213 if not isinstance (_el , Undefined ):
173214 _res .append (_el )
174- _iter2 = source if isinstance ( source , ( list , tuple )) else [source ]
215+ _iter2 = [source ]
175216 for _el in _iter2 :
176217 if not isinstance (_el , Undefined ):
177218 _res .append (_el )
178219 return _res
179220
180221 # Prepare a mutable copy of the target we can merge into.
222+ is_overflow_target = Utils .is_overflow (target )
181223 merge_target : t .Dict [str , t .Any ] = copy .deepcopy (target if isinstance (target , dict ) else dict (target ))
182224
183225 # For overlapping keys, merge recursively; otherwise, take the new value.
184- return {
226+ merged_updates : t .Dict [t .Any , t .Any ] = {}
227+ # Prefer exact key matches; fall back to string normalization only when needed.
228+ for key , value in source .items ():
229+ normalized_key = str (key )
230+ if key in merge_target :
231+ merged_updates [key ] = Utils .merge (merge_target [key ], value , options )
232+ elif normalized_key in merge_target :
233+ merged_updates [normalized_key ] = Utils .merge (merge_target [normalized_key ], value , options )
234+ else :
235+ merged_updates [key ] = value
236+ merged = {
185237 ** merge_target ,
186- ** {
187- str (key ): Utils .merge (merge_target [key ], value , options ) if key in merge_target else value
188- for key , value in source .items ()
189- },
238+ ** merged_updates ,
190239 }
240+ return OverflowDict (merged ) if is_overflow_target else merged
191241
192242 @staticmethod
193243 def compact (root : t .Dict [str , t .Any ]) -> t .Dict [str , t .Any ]:
@@ -324,17 +374,90 @@ def _dicts_are_equal(
324374 else :
325375 return d1 == d2
326376
377+ @staticmethod
378+ def is_overflow (obj : t .Any ) -> bool :
379+ """Check if an object is an OverflowDict."""
380+ return isinstance (obj , OverflowDict )
381+
327382 @staticmethod
328383 def combine (
329384 a : t .Union [t .List [t .Any ], t .Tuple [t .Any ], t .Any ],
330385 b : t .Union [t .List [t .Any ], t .Tuple [t .Any ], t .Any ],
331- ) -> t .List [t .Any ]:
386+ options : t .Optional [DecodeOptions ] = None ,
387+ ) -> t .Union [t .List [t .Any ], t .Dict [str , t .Any ]]:
332388 """
333- Concatenate two values, treating non‑sequences as singletons.
334-
335- Returns a new `list`; tuples are expanded but not preserved as tuples.
389+ Concatenate two values, treating non-sequences as singletons.
390+
391+ If `list_limit` is exceeded, converts the list to an `OverflowDict`
392+ (a dict with numeric keys) to prevent memory exhaustion.
393+ When `options` is provided, its ``list_limit`` controls when a list is
394+ converted into an :class:`OverflowDict` (a dict with numeric keys) to
395+ prevent unbounded growth. If ``options`` is ``None``, the default
396+ ``list_limit`` from :class:`DecodeOptions` is used.
397+ A negative ``list_limit`` is treated as "overflow immediately": any
398+ non-empty combined result will be converted to :class:`OverflowDict`.
399+ This helper never raises an exception when the limit is exceeded; even
400+ if :class:`DecodeOptions` has ``raise_on_limit_exceeded`` set to
401+ ``True``, ``combine`` will still handle overflow only by converting the
402+ list to :class:`OverflowDict`.
336403 """
337- return [* (a if isinstance (a , (list , tuple )) else [a ]), * (b if isinstance (b , (list , tuple )) else [b ])]
404+ if Utils .is_overflow (a ):
405+ # a is already an OverflowDict. Append b to a *copy* at the next numeric index.
406+ # We assume sequential keys; len(a_copy) gives the next index.
407+ orig_a = t .cast (OverflowDict , a )
408+ a_copy = OverflowDict ({k : v for k , v in orig_a .items () if not isinstance (v , Undefined )})
409+ # Use max key + 1 to handle sparse dicts safely, rather than len(a)
410+ key_pairs = _numeric_key_pairs (a_copy )
411+ idx = (max (key for key , _ in key_pairs ) + 1 ) if key_pairs else 0
412+
413+ if isinstance (b , (list , tuple )):
414+ for item in b :
415+ if not isinstance (item , Undefined ):
416+ a_copy [str (idx )] = item
417+ idx += 1
418+ elif Utils .is_overflow (b ):
419+ b = t .cast (OverflowDict , b )
420+ # Iterate in numeric key order to preserve list semantics
421+ for _ , k in sorted (_numeric_key_pairs (b ), key = lambda item : item [0 ]):
422+ val = b [k ]
423+ if not isinstance (val , Undefined ):
424+ a_copy [str (idx )] = val
425+ idx += 1
426+ else :
427+ if not isinstance (b , Undefined ):
428+ a_copy [str (idx )] = b
429+ return a_copy
430+
431+ # Normal combination: flatten lists/tuples
432+ # Flatten a
433+ if isinstance (a , (list , tuple )):
434+ list_a = [x for x in a if not isinstance (x , Undefined )]
435+ else :
436+ list_a = [a ] if not isinstance (a , Undefined ) else []
437+
438+ # Flatten b, handling OverflowDict as a list source
439+ if isinstance (b , (list , tuple )):
440+ list_b = [x for x in b if not isinstance (x , Undefined )]
441+ elif Utils .is_overflow (b ):
442+ b_of = t .cast (OverflowDict , b )
443+ list_b = [
444+ b_of [k ]
445+ for _ , k in sorted (_numeric_key_pairs (b_of ), key = lambda item : item [0 ])
446+ if not isinstance (b_of [k ], Undefined )
447+ ]
448+ else :
449+ list_b = [b ] if not isinstance (b , Undefined ) else []
450+
451+ res = [* list_a , * list_b ]
452+
453+ list_limit = options .list_limit if options else DecodeOptions ().list_limit
454+ if list_limit < 0 :
455+ return OverflowDict ({str (i ): x for i , x in enumerate (res )}) if res else res
456+ if len (res ) > list_limit :
457+ # Convert to OverflowDict
458+ return OverflowDict ({str (i ): x for i , x in enumerate (res )})
459+
460+ return res
338461
339462 @staticmethod
340463 def apply (
0 commit comments