Source code for dictutils.mergedict

from __future__ import annotations

from collections.abc import Mapping
from typing import Any


[docs] def mergedict(*args, path=None, update: bool = True) -> dict[Any, Any] | None: """ Merge multiple nested mappings into a single dict. If the first argument is a plain dict, it is updated in-place and returned. If the first argument is None or a non-dict Mapping, a new dict is created. Args: *args: Mappings to merge (first one is modified in-place if possible) path: Internal use for error reporting update: If True, values in later dicts override earlier ones Returns: The merged dict (same object as first argument when possible) Raises: TypeError: If no arguments provided or arguments are not mappings """ if not args: raise TypeError("mergedict requires at least one mapping") head, *tail = args if not tail: # Single argument - return it directly if it's a valid mapping if head is None: return None if not isinstance(head, Mapping): raise TypeError("All arguments must be mappings or None") return head if isinstance(head, dict) else dict(head) result = _coerce_to_dict(head) for d in tail: result = _mergedict(result, d, path, update) return result
def _coerce_to_dict(m: Mapping | None) -> dict[Any, Any] | None: """Convert a mapping to a dict, with type checking.""" if m is None: return None if not isinstance(m, Mapping): raise TypeError("All arguments must be mappings or None") return dict(m) def _handle_none_inputs( a: dict[Any, Any] | None, b: Mapping | None ) -> dict[Any, Any] | None: """Handle cases where a or b is None.""" if a is None and b is None: return None if a is None: return dict(b) if b is not None else None if b is None: return a return None # Indicates normal processing should continue def _merge_existing_key( a: dict, b: Mapping, key: Any, path: list, update: bool ) -> None: """Handle merging when key already exists in target dict.""" if isinstance(a[key], dict) and isinstance(b[key], Mapping): # Recursive merge for nested dicts _mergedict(a[key], b[key], path + [str(key)], update=update) elif a[key] == b[key]: # Same leaf value - no action needed pass elif isinstance(a[key], list) and isinstance(b[key], list): # Concatenate arrays a[key].extend(b[key]) elif update: # Update value when update=True a[key] = b[key] # When update=False, keep original a[key] def _copy_new_key(a: dict, b: Mapping, key: Any) -> None: """Copy a new key from source to target, converting Mappings to dicts.""" a[key] = b[key] if not isinstance(b[key], Mapping) else dict(b[key]) def _mergedict( a: dict[Any, Any] | None, b: Mapping | None, path=None, update: bool = True ) -> dict[Any, Any] | None: """ Internal merge function that handles the recursive merging logic. Args: a: Target dict (modified in-place) b: Source mapping to merge into a path: Current path for error reporting update: If True, values in b override values in a Returns: The merged dict a (or a new dict if a was None) """ # Handle None inputs result = _handle_none_inputs(a, b) if result is not None or (a is None and b is None): return result if not isinstance(b, Mapping): raise TypeError("Both a and b must be dicts or mappings") if path is None: path = [] # At this point, a is guaranteed to be non-None dict assert a is not None for key in b: if key in a: _merge_existing_key(a, b, key, path, update) else: _copy_new_key(a, b, key) return a