modict 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
modict/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from ._modict import modict
2
+ from ._modict_meta import modictConfig, Field, Factory, Computed, Check
3
+ from ._typechecker import check_type, coerce, typechecked, TypeChecker, TypeCheckError, TypeCheckException, TypeCheckFailureError, TypeMismatchError, Coercer, CoercionError
modict/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Main module for modict.
3
+ """
4
+
5
+ def main():
6
+ """
7
+ Main entry point for the application.
8
+ """
9
+ print("Hello from modict!")
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -0,0 +1,664 @@
1
+ """Collections utilities for nested data structures.
2
+
3
+ This module provides a comprehensive set of tools for working with nested data structures
4
+ in Python, particularly focusing on Mappings (like dictionaries) and Sequences (like lists).
5
+ It enables deep traversal, comparison, merging, and manipulation of nested structures
6
+ while maintaining type safety and providing clear error handling.
7
+
8
+ Key Features:
9
+ - Path-based access: Access nested values using string paths ("a.b.0.c") or key tuples
10
+ - Deep operations: Compare, merge, and modify nested structures
11
+ - Type-safe: Comprehensive type hints and runtime type checking
12
+ - Container views: Create custom views over container data
13
+ - Structure traversal: Walk through nested structures with custom callbacks and filters
14
+
15
+ Main Components:
16
+ - View: Base class for creating custom container views
17
+ - MISSING: Sentinel value for distinguishing missing values from None
18
+ - Container operations: get_nested(), set_nested(), del_nested(), has_nested()
19
+ - Deep operations: deep_merge(), deep_equals(), diff_nested()
20
+ - Traversal: walk(), walked() for recursive container traversal
21
+
22
+ Typical Usage:
23
+ >>> from collections_utils import get_nested, set_nested, walk
24
+
25
+ # Access nested values with string paths
26
+ >>> data = {"users": [{"name": "Alice", "age": 30}]}
27
+ >>> get_nested(data, "users.0.name")
28
+ 'Alice'
29
+
30
+ # Modify nested structures
31
+ >>> set_nested(data, "users.0.email", "alice@example.com")
32
+ >>> data
33
+ {"users": [{"name": "Alice", "age": 30, "email": "alice@example.com"}]}
34
+
35
+ # Walk through nested structures
36
+ >>> for path, value in walk(data):
37
+ ... print(f"{path}: {value}")
38
+ users.0.name: Alice
39
+ users.0.age: 30
40
+ users.0.email: alice@example.com
41
+
42
+ Type Definitions:
43
+ - Key: Union[int, str] - Valid container keys/indices
44
+ - Path: Union[str, Tuple[Key, ...]] - Path to nested values
45
+ - Container: Union[Mapping, Sequence] - Any container type
46
+ - MutableContainer: Union[MutableMapping, MutableSequence] - Mutable containers
47
+
48
+ Notes:
49
+ - String paths use dots as separators: "a.0.b"
50
+ - Numeric path components are converted to integers: "users.0" -> ("users", 0)
51
+ - Container operations maintain the original container types
52
+ - The MISSING sentinel distinguishes missing values from None values
53
+ """
54
+
55
+ from collections.abc import Collection,Mapping,Sequence,MutableMapping,MutableSequence
56
+ from typing import Any, Union, Dict, TypeVar, Tuple, Type, Optional, Set, Callable, TypeAlias, Generic, Iterator
57
+ from itertools import islice
58
+
59
+ # Type definitions for improved clarity and type safety
60
+ Key: TypeAlias = Union[int, str] # Valid key types for containers
61
+ Path: TypeAlias = Union[str, Tuple[Key, ...]] # Path can be string ("a.b.c") or tuple of keys
62
+ Container: TypeAlias = Union[Mapping, Sequence] # Any immutable container
63
+ MutableContainer: TypeAlias = Union[MutableMapping, MutableSequence] # Any mutable container
64
+ T = TypeVar('T') # Generic type for values
65
+ C = TypeVar('C', bound=Container) # Generic type constrained to Containers
66
+ MC = TypeVar('MC', bound=MutableContainer) # Generic type for mutable containers
67
+ CallbackFn = Callable[[Any], Any] # Type for callback functions
68
+ FilterFn = Callable[[str, Any], bool] # Type for filter predicates
69
+
70
+ class _MISSING:
71
+ """Sentinel class representing a missing value.
72
+
73
+ Used instead of None when None is a valid value. This helps distinguish
74
+ between "no value set" and "value explicitly set to None".
75
+ """
76
+ def __str__(self)->str:
77
+ return "MISSING"
78
+
79
+ def __repr__(self)->str:
80
+ return "MISSING"
81
+
82
+ def __bool__(self)->bool:
83
+ return False
84
+
85
+ # Sentinel instance
86
+ MISSING=_MISSING()
87
+
88
+ class View(Collection[T], Generic[C, T]):
89
+
90
+ """Base View class for creating custom views over any Mapping or Sequence.
91
+
92
+ Provides a read-only view over container data with custom element access logic.
93
+ Subclasses must implement _get_element(key) to determine how elements are accessed.
94
+
95
+ Type Parameters:
96
+ C: The container type (must be Mapping or Sequence)
97
+ T: The type of elements in the view
98
+
99
+ Args:
100
+ data: The container to create a view over
101
+
102
+ Raises:
103
+ TypeError: If data is not a Mapping or Sequence
104
+
105
+ Examples:
106
+ >>> class Keys(View[Mapping, Key]):
107
+ ... def _get_element(self, key: Key) -> Key:
108
+ ... return key
109
+ >>> class Values(View[Mapping, T]):
110
+ ... def _get_element(self, key: Key) -> T:
111
+ ... return self.data[key]
112
+ """
113
+
114
+ def __init__(self, data:C) -> None:
115
+ if not isinstance(data,(Mapping,Sequence)):
116
+ raise TypeError(f"The data on which a View is defined is expected to be a Mapping or Sequence. Got {type(data)}")
117
+ self._data:C = data
118
+ self._nmax: int = 10 # max number of elements to show in repr
119
+
120
+ @property
121
+ def data(self) -> C:
122
+ return self._data
123
+
124
+ def _get_element(self,key:Key) -> T:
125
+ """Takes a key and returns the corresponding view element"""
126
+ raise NotImplementedError()
127
+
128
+ def __iter__(self) -> Iterator[T]:
129
+ """Return an iterator over view elements"""
130
+ return iter(self._get_element(key) for key in keys(self.data))
131
+
132
+ def __len__(self) -> int:
133
+ """Return the number of elements in the view."""
134
+ return len(self.data)
135
+
136
+ def __repr__(self) -> str:
137
+ """String representation of the view"""
138
+ content=', '.join(repr(self._get_element(key)) for key in islice(keys(self.data),self._nmax))
139
+ if len(self.data)>self._nmax:
140
+ content+=", ..."
141
+ return f"{self.__class__.__name__}({content})"
142
+
143
+ def __contains__(self, item:Any) -> bool:
144
+ """Check if an element is in the view."""
145
+ return any(item==self._get_element(key) for key in keys(self.data))
146
+
147
+ def join_path(path_tuple:Tuple[Key,...])-> str:
148
+ """
149
+ joins a path tuple into a path str : ('a',0,'b') -> "a.0.b"
150
+ """
151
+ return '.'.join(str(k) for k in path_tuple)
152
+
153
+ def split_path(path_str:str)-> Tuple[Key,...]:
154
+ """
155
+ splits a path str into a path tuple : "a.0.b" -> ('a',0,'b')
156
+ """
157
+ def format(key:str)-> Key:
158
+ try:
159
+ return int(key)
160
+ except ValueError:
161
+ return key
162
+ return tuple(format(k) for k in path_str.split('.'))
163
+
164
+ def keys(obj:Container)-> Iterator[Key]:
165
+ """Yield possible keys or indices of a container.
166
+
167
+ Args:
168
+ obj: Mapping or Sequence to get keys from
169
+
170
+ Yields:
171
+ Keys for Mapping, indices for Sequence
172
+
173
+ Raises:
174
+ TypeError: If obj is neither Mapping nor Sequence
175
+ """
176
+ if isinstance(obj,Mapping):
177
+ yield from obj.keys()
178
+ elif isinstance(obj,Sequence):
179
+ yield from range(len(obj))
180
+ else:
181
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
182
+
183
+ def has_key(obj:Container,key:Key)->bool:
184
+ """Check if a key/index exists in a container.
185
+
186
+ Args:
187
+ obj: Container to check
188
+ key: Key (for Mapping) or index (for Sequence) to look for
189
+
190
+ Returns:
191
+ True if key exists, False otherwise
192
+
193
+ Raises:
194
+ TypeError: If obj is neither Mapping nor Sequence
195
+ """
196
+ if isinstance(obj,Mapping):
197
+ return key in obj
198
+ elif isinstance(obj,Sequence):
199
+ return isinstance(key,int) and 0<=key<len(obj)
200
+ else:
201
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
202
+
203
+ def set_key(obj:MutableContainer,key:Key,value:Any):
204
+ if not is_mutable_container(obj):
205
+ raise TypeError(f"Expected a MutableMapping or MutableSequence container. Got {type(obj)}")
206
+ if isinstance(obj,MutableMapping):
207
+ obj[key]=value
208
+ elif isinstance(obj,MutableSequence):
209
+ if isinstance(key,int):
210
+ while len(obj)<=key:
211
+ obj.append(MISSING)
212
+ obj[key]=value
213
+ else:
214
+ raise IndexError(f"Invalid key type. Expected int, got {type(key)}")
215
+
216
+ def is_container(obj:Any, excluded:Optional[Tuple[Type,...]]=None)->bool:
217
+ """Test if an object is a container (but not an excluded type).
218
+
219
+ Args:
220
+ obj: Object to test
221
+ excluded: Types to not consider as containers (default: str, bytes, bytearray)
222
+
223
+ Returns:
224
+ True if obj is a non-excluded container
225
+ """
226
+ excluded= excluded if excluded is not None else (str,bytes,bytearray)
227
+ return (isinstance(obj,Mapping) or isinstance(obj,Sequence)) and not isinstance(obj,excluded)
228
+
229
+ def is_mutable_container(obj:Any)->bool:
230
+ """Test if an object is a mutable container.
231
+
232
+ Args:
233
+ obj: Object to test
234
+
235
+ Returns:
236
+ True if obj is MutableMapping or MutableSequence
237
+ """
238
+ return isinstance(obj,MutableMapping) or isinstance(obj,MutableSequence)
239
+
240
+ def unroll(obj: Container) -> Iterator[Tuple[Key, Any]]:
241
+ """Yield (key, value) pairs from a container.
242
+
243
+ Args:
244
+ obj: Container to unroll
245
+
246
+ Yields:
247
+ Tuple of (key, value) for each element
248
+
249
+ Raises:
250
+ TypeError: If obj is not a container
251
+ """
252
+ if not is_container(obj):
253
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
254
+ for key in keys(obj):
255
+ yield key,obj[key]
256
+
257
+
258
+ def get_nested(obj: Container, path: Path, default: Any = MISSING) -> Any:
259
+ """Retrieve a nested value using a path.
260
+
261
+ Args:
262
+ obj: Nested structure to traverse
263
+ path: Either dot-separated string ("a.0.b") or tuple of keys
264
+ default: Value to return if path doesn't exist
265
+
266
+ Returns:
267
+ Value at path or default if provided
268
+
269
+ Raises:
270
+ TypeError: If obj is not a container
271
+ KeyError: If path doesn't exist and no default provided
272
+
273
+ Examples:
274
+ >>> data = {"a": {"b": [1, 2, {"c": 3}]}}
275
+ >>> get_nested(data, "a.b.2.c")
276
+ 3
277
+ >>> get_nested(data, ("a", "b", 2, "c"))
278
+ 3
279
+ >>> get_nested(data, "x.y.z", default=None)
280
+ None
281
+ """
282
+ if not is_container(obj):
283
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
284
+
285
+ if isinstance(path,str):
286
+ keys = split_path(path)
287
+ else:
288
+ keys = path
289
+
290
+ value=obj
291
+ for key in keys:
292
+ if is_container(value) and has_key(value,key):
293
+ value = value[key]
294
+ else:
295
+ if default is not MISSING:
296
+ return default
297
+ else:
298
+ raise KeyError(f"Path {path!r} not found in container")
299
+ return value
300
+
301
+ def set_nested(obj:Container, path: Path, value):
302
+ """Set a nested value, creating intermediate containers as needed.
303
+
304
+ Creates missing containers (dict for string keys, list for integer keys)
305
+ along the path if they don't exist.
306
+
307
+ Args:
308
+ obj: Container to modify
309
+ path: Path to set value at
310
+ value: Value to set
311
+
312
+ Raises:
313
+ TypeError: If obj is not a container or if any container we attempt to write in is immutable
314
+
315
+ Examples:
316
+ >>> data = {}
317
+ >>> set_nested(data, "a.b.0.c", 42)
318
+ >>> data
319
+ {'a': {'b': [{'c': 42}]}}
320
+ """
321
+ if not is_container(obj):
322
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
323
+
324
+ if isinstance(path, str):
325
+ path = split_path(path)
326
+ current = obj
327
+ for i,key in enumerate(path):
328
+ if i==len(path)-1:
329
+ #terminal key reached, we set the value and return
330
+ set_key(current,key,value)
331
+ return
332
+ elif not has_key(current,key) or current[key] is MISSING:
333
+ if isinstance(path[i+1],int):
334
+ set_key(current,key,[])
335
+ else:# str
336
+ set_key(current,key,{})
337
+ current = current[key]
338
+ else:
339
+ current = current[key]
340
+
341
+ def pop_nested(obj:Container, path:Path, default=MISSING):
342
+ """deletes a nested key/index and returns the value (if found).
343
+ If not found, returns default if provided, otherwise raises an error.
344
+ If provided, default will be returned in ANY case of failure.
345
+ This includes these cases:
346
+ - the path doesn't exist or doesn't make sense in the structure
347
+ - the path actually exists but ends in an immutable container in which we can't pop
348
+
349
+ Args:
350
+ obj: Container to modify
351
+ path: Path to the value to delete
352
+
353
+ Raises:
354
+ TypeError: If obj is not a container, or we attempt to modify an immutable container
355
+ KeyError: If path doesn't exist
356
+ """
357
+ if not is_container(obj):
358
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
359
+
360
+ if isinstance(path, str):
361
+ path = split_path(path)
362
+
363
+ current = obj
364
+ try:
365
+ for i, key in enumerate(path):
366
+ if is_container(current) and has_key(current,key):
367
+ if i == len(path) - 1:
368
+ if is_mutable_container(current):
369
+ value=current[key]
370
+ del current[key]
371
+ return value
372
+ else:
373
+ raise TypeError(f"Can't delete in an immutable container. Attempt to delete key={key!r} in {current} of immutable type {type(current)} caused this error.")
374
+ current = current[key]
375
+ else:
376
+ raise KeyError(f"Path {path!r} not found in container")
377
+
378
+ except (KeyError,TypeError):
379
+ if default is not MISSING:
380
+ return default
381
+ else:
382
+ raise
383
+
384
+ def del_nested(obj:Container, path:Path):
385
+ """deletes a nested key/index (if found).
386
+
387
+ Args:
388
+ obj: Container to modify
389
+ path: Path to the value to delete
390
+
391
+ Raises:
392
+ TypeError: If obj is not a container, or we attempt to modify an immutable container
393
+ KeyError: If path doesn't exist
394
+
395
+
396
+ """
397
+ # delegate the logic to pop_nested, we just don't return the output
398
+ pop_nested(obj,path)
399
+
400
+ def has_nested(obj:Container, path: Path):
401
+ """Check if a nested path exists.
402
+
403
+ Args:
404
+ obj: Container to check
405
+ path: Path to check for existence
406
+
407
+ Returns:
408
+ True if path exists, False otherwise
409
+ """
410
+ if not is_container(obj):
411
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
412
+ try:
413
+ get_nested(obj,path)
414
+ return True
415
+ except KeyError:
416
+ return False
417
+
418
+
419
+ def extract(obj: C, *extracted_keys: Key) -> Iterator[Tuple[Key, Any]]:
420
+ """
421
+ Extract specified keys from a container, preserving the original order.
422
+
423
+ Args:
424
+ obj (Mapping or Sequence): The source container.
425
+ *extracted_keys (str): Keys to extract.
426
+
427
+ Returns:
428
+ Iterator[Tuple[Key, Any]]: A generator of (key, value) pairs.
429
+ """
430
+ if not is_container(obj):
431
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
432
+
433
+ ek = set(extracted_keys)
434
+ return ((key, obj[key]) for key in keys(obj) if key in ek)
435
+
436
+
437
+ def exclude(obj: C, *excluded_keys: Key) -> Iterator[Tuple[Key, Any]]:
438
+ """
439
+ Exclude specified keys from a container, preserving the original order.
440
+
441
+ Args:
442
+ obj (Mapping or Sequence): The source container.
443
+ *excluded_keys (str): Keys to exclude.
444
+
445
+ Returns:
446
+ Iterator[Tuple[Key, Any]]: A generator of (key, value) pairs.
447
+ """
448
+ if not is_container(obj):
449
+ raise TypeError(f"Expected a Mapping or Sequence container, got {type(obj)}")
450
+
451
+ ek = set(excluded_keys)
452
+ return ((key, obj[key]) for key in keys(obj) if key not in ek)
453
+
454
+ def walk(
455
+ obj: Container,
456
+ callback: Optional[CallbackFn] = None,
457
+ filter: Optional[FilterFn] = None,
458
+ excluded: Optional[Tuple[Type, ...]] = None
459
+ ) -> Iterator[Tuple[str, Any]]:
460
+ """Walk through a nested container yielding (path, value) pairs.
461
+
462
+ Recursively traverses the container, yielding paths and values for leaf nodes.
463
+ Leaves can be transformed by callback and filtered by the filter predicate.
464
+
465
+ Args:
466
+ obj: Container to traverse
467
+ callback: Optional function to transform leaf values
468
+ filter: Optional predicate to filter paths/values (receives path and value)
469
+ excluded: Container types to treat as leaves (default: str, bytes, bytearray)
470
+
471
+ Yields:
472
+ Tuples of (path_string, value) for each leaf node
473
+ If callback provided, value is transformed by callback
474
+ If filter provided, only yields pairs that pass filter(path, value)
475
+
476
+ Examples:
477
+ >>> data = {"a": [1, {"b": 2}], "c": 3}
478
+ >>> list(walk(data))
479
+ [('a.0', 1), ('a.1.b', 2), ('c', 3)]
480
+ >>> list(walk(data, callback=str))
481
+ [('a.0', '1'), ('a.1.b', '2'), ('c', '3')]
482
+ """
483
+
484
+ def _walk(obj: Any, path: Tuple[Key, ...]) -> Iterator[Tuple[str, Any]]:
485
+ if is_container(obj,excluded=excluded):
486
+ for k, v in unroll(obj):
487
+ yield from _walk(v, path + (k,))
488
+ else:
489
+ joined_path=join_path(path)
490
+ if filter is None or filter(joined_path,obj):
491
+ yield joined_path, callback(obj) if callback is not None else obj
492
+
493
+ yield from _walk(obj, ())
494
+
495
+ def walked(
496
+ obj: Container,
497
+ callback: Optional[CallbackFn] = None,
498
+ filter: Optional[FilterFn] = None,
499
+ excluded: Optional[Tuple[Type, ...]] = None
500
+ ) -> Dict[str, Any]:
501
+ """Return a flattened dictionary of path:value pairs from a nested container.
502
+
503
+ Similar to walk(), but returns a dictionary instead of an iterator.
504
+
505
+ Args:
506
+ obj: Container to traverse
507
+ callback: Optional function to transform leaf values
508
+ filter: Optional predicate to filter paths/values
509
+ excluded: Container types to treat as leaves
510
+
511
+ Returns:
512
+ Dictionary mapping path strings to leaf values
513
+
514
+ Examples:
515
+ >>> data = {"a": [1, {"b": 2}], "c": 3}
516
+ >>> walked(data)
517
+ {'a.0': 1, 'a.1.b': 2, 'c': 3}
518
+ """
519
+ return dict(walk(obj,callback=callback,filter=filter,excluded=excluded))
520
+
521
+
522
+ def first_keys(walked:Dict[str,Any])->Set[Key]:
523
+ """
524
+ Return all the first keys encountered in walked paths
525
+ """
526
+ return set(split_path(p)[0] for p in walked)
527
+
528
+ def is_seq_based(walked:Dict[str,Any])->bool:
529
+ """
530
+ Determines if the walked structure was initially a Sequence
531
+ """
532
+ fk=first_keys(walked)
533
+ return fk==set(range(len(fk)))
534
+
535
+ def unwalk(walked:Dict[str,Any])->MutableContainer:
536
+ """
537
+ Recontructs a nested structure from a flattened dict.
538
+ Args:
539
+ walked: (Dict[str,Any]) a path:value flattened dictionary
540
+ Returns:
541
+ (MutableContainer) : Reconstructed Nested list / dict structure
542
+ """
543
+ if is_seq_based(walked):
544
+ base=[]
545
+ else:
546
+ base={}
547
+
548
+ for path,value in walked.items():
549
+ set_nested(base,path,value)
550
+
551
+ return base
552
+
553
+ def deep_equals(obj1:Container,obj2:Container,excluded:Optional[Tuple[Type,...]]=None)->bool:
554
+ """
555
+ Compares two nested structures deeply by comparing their walked dicts
556
+ """
557
+ return walked(obj1,excluded=excluded)==walked(obj2,excluded=excluded)
558
+
559
+ def diff_nested(
560
+ obj1: Container,
561
+ obj2: Container,
562
+ path: Tuple[Key, ...] = ()
563
+ ) -> Dict[str, Tuple[Any, Any]]:
564
+ """Compare two nested structures and return their differences.
565
+
566
+ Recursively compares two containers and returns a dictionary of differences.
567
+ Keys are paths where values differ, values are tuples of (obj1_value, obj2_value).
568
+
569
+ Args:
570
+ obj1: First container to compare
571
+ obj2: Second container to compare
572
+ path: Current path in recursion (used internally)
573
+
574
+ Returns:
575
+ Dictionary mapping paths to value pairs that differ
576
+ MISSING is used when a key exists in one container but not the other
577
+
578
+ Examples:
579
+ >>> a = {"x": 1, "y": {"z": 2}}
580
+ >>> b = {"x": 1, "y": {"z": 3}, "w": 4}
581
+ >>> diff_nested(a, b)
582
+ {'y.z': (2, 3), 'w': (MISSING, 4)}
583
+ """
584
+ diffs: Dict[str, Tuple[Any, Any]] = {}
585
+
586
+ if is_container(obj1) and is_container(obj2):
587
+ keys1 = set(keys(obj1))
588
+ keys2 = set(keys(obj2))
589
+ all_keys = keys1.union(keys2)
590
+ for key in all_keys:
591
+ new_path = path + (key,)
592
+ in_obj1 = has_key(obj1, key)
593
+ in_obj2 = has_key(obj2, key)
594
+ if in_obj1 and in_obj2:
595
+ val1, val2 = obj1[key], obj2[key]
596
+ if is_container(val1) and is_container(val2):
597
+ diffs.update(diff_nested(val1, val2, new_path))
598
+ else:
599
+ if val1 != val2:
600
+ diffs[join_path(new_path)] = (val1, val2)
601
+ elif in_obj1:
602
+ diffs[join_path(new_path)] = (obj1[key], MISSING)
603
+ elif in_obj2:
604
+ diffs[join_path(new_path)] = (MISSING, obj2[key])
605
+ else:
606
+ if obj1 != obj2:
607
+ diffs[join_path(path)] = (obj1, obj2)
608
+
609
+ return diffs
610
+
611
+ def deep_merge(
612
+ target: MutableContainer,
613
+ src: Container,
614
+ conflict_resolver: Optional[Callable[[Any, Any], Any]] = None
615
+ ) -> None:
616
+ """Deeply merge source container into target, modifying target in-place.
617
+
618
+ For mappings:
619
+ - If a key exists in both and both values are containers, merge recursively
620
+ - Otherwise, src value overwrites target value (or uses conflict_resolver)
621
+
622
+ For sequences:
623
+ - Elements are merged by index
624
+ - If src has more elements, they are appended to target
625
+
626
+ Args:
627
+ target: Mutable container to merge into
628
+ src: Container to merge from
629
+ conflict_resolver: Optional function to resolve value conflicts
630
+ Takes (target_value, src_value), returns resolved value
631
+
632
+ Raises:
633
+ TypeError: If target and src are incompatible container types
634
+
635
+ Examples:
636
+ >>> target = {"a": 1, "b": {"x": 1}}
637
+ >>> src = {"b": {"y": 2}, "c": 3}
638
+ >>> deep_merge(target, src)
639
+ >>> target
640
+ {'a': 1, 'b': {'x': 1, 'y': 2}, 'c': 3}
641
+ """
642
+ if isinstance(target, MutableMapping) and isinstance(src, Mapping):
643
+ for key, src_value in src.items():
644
+ if has_key(target, key):
645
+ target_value = target[key]
646
+ if is_container(target_value) and is_container(src_value):
647
+ deep_merge(target_value, src_value, conflict_resolver)
648
+ else:
649
+ target[key] = conflict_resolver(target_value, src_value) if conflict_resolver else src_value
650
+ else:
651
+ target[key] = src_value
652
+
653
+ elif isinstance(target, MutableSequence) and isinstance(src, Sequence):
654
+ for idx, src_value in enumerate(src):
655
+ if idx < len(target):
656
+ target_value = target[idx]
657
+ if is_container(target_value) and is_container(src_value):
658
+ deep_merge(target_value, src_value, conflict_resolver)
659
+ else:
660
+ target[idx] = conflict_resolver(target_value, src_value) if conflict_resolver else src_value
661
+ else:
662
+ target.append(src_value)
663
+ else:
664
+ raise TypeError("Types of 'target' and 'src' aren't compatibles for deep merging.")