Source code for ClearMap.config.config_adjusters.patch_ops

from __future__ import annotations

from copy import deepcopy
from typing import Tuple, Any, Iterable, Mapping, Dict

from ClearMap.Utils.utilities import DELETE, _REPLACE
from ClearMap.config.config_adjusters.dict_ops import ensure_path_dict
from ClearMap.config.config_adjusters.type_hints import KeysPath
from ClearMap.Utils.exceptions import PatchConflictError


[docs] def merge_patches(dst: dict, src: dict, *, _path: Tuple[str, ...] = ()) -> dict: """ Combine two patch dicts without applying them. Composition rules: - DELETE and _REPLACE are strong operations. - dict values recurse (merge). - scalars overwrite. Conflict rules: - dst already has _REPLACE and src has *different* _REPLACE => raise. - dst already has _REPLACE and src provides a non-sentinel update => raise (prevents silent downgrade of authoritative replacement). """ for k, v in src.items(): path = _path + (k,) dv = dst.get(k) # DELETE always wins if v is DELETE: dst[k] = DELETE continue elif dv is DELETE: # If destination already deleted this key, do not allow resurrection raise PatchConflictError(path, v, dv, 'non-DELETE update would resurrect a DELETE') # REPLACE: detect conflicting REPLACE-vs-REPLACE if isinstance(v, _REPLACE): if isinstance(dv, _REPLACE) and dv.payload != v.payload: raise PatchConflictError(path, v, dv, 'two different REPLACE operations for the same path') dst[k] = v continue # If destination already asserts REPLACE, absorb dict updates into payload if isinstance(dv, _REPLACE): if isinstance(v, dict) and isinstance(dv.payload, dict): merge_patches(dv.payload, v, _path=path) # note: path stays same for good errors continue raise PatchConflictError(path, v, dv, 'non-dict update would downgrade an existing REPLACE') # Recurse only into plain dicts if isinstance(v, dict) and isinstance(dv, dict): merge_patches(dv, v, _path=path) else: dst[k] = deepcopy(v) return dst
def _strip_template_like_keys_from_entry(entry: dict[str, Any]) -> None: entry.pop('templates', None) for k in list(entry.keys()): if isinstance(k, str) and k.endswith('_template'): entry.pop(k, None) def _strip_template_like_keys_from_container(container: dict[str, Any]) -> None: container.pop('templates', None) for _, v in container.items(): if isinstance(v, dict): _strip_template_like_keys_from_entry(v) def _stable_order_keys(*, current_keys: Iterable[str], target_keys: Iterable[str]) -> list[str]: target_set = set(target_keys) ordered = [k for k in current_keys if k in target_set] for k in target_keys: if k not in ordered: ordered.append(k) return ordered def _restrict_entry_to_template_keys(entry: dict[str, Any], template_base: dict[str, Any]) -> dict[str, Any]: keep = set(template_base.keys()) return {k: v for k, v in entry.items() if k in keep} def _raise_if_rename_map_collides(rename_map: Mapping[str, str]) -> None: inv: dict[str, list[str]] = {} for old, new in rename_map.items(): inv.setdefault(new, []).append(old) collisions = {new: olds for new, olds in inv.items() if len(olds) > 1} if collisions: msg = ', '.join(f'{new}<-{olds}' for new, olds in collisions.items()) raise ValueError(f'renamed_channels has collisions: {msg}') # FIXME: merge with below using return_items arg def _iter_patch_paths(p: Any, *, _path: tuple[str, ...] = ()) -> Iterable[tuple[str, ...]]: """ Yield *assignment* paths in a patch dict, including sentinel leaves. We treat any non-dict value (including DELETE/_REPLACE/scalar/list) as a leaf assignment. """ if not isinstance(p, dict): yield _path return for k, v in p.items(): path = _path + (k,) if isinstance(v, dict): # recurse into dict patches yield from _iter_patch_paths(v, _path=path) else: # leaf assignment (DELETE, _REPLACE, scalar, list, etc.) yield path
[docs] def iter_patch_items(p: Any, *, _path: tuple[str, ...] = ()): """ Yield (path, value) for leaf assignments in a patch dict. A leaf is any non-dict value (including DELETE/_REPLACE/scalar/list). """ if not isinstance(p, dict): yield _path, p return for k, v in p.items(): path = _path + (k,) if isinstance(v, dict): yield from iter_patch_items(v, _path=path) else: yield path, v
[docs] def set_patch_at_path(patch: Dict[str, Any], path: KeysPath, value: Any) -> None: parent = ensure_path_dict(patch, path[:-1]) if len(path) > 1 else patch parent[path[-1]] = value