from __future__ import annotations from collections.abc import ( ItemsView, Iterator, KeysView, Mapping, MutableMapping, ValuesView, ) from typing import Any, cast from typing_extensions import Self, TypeVar from benedict.core.clone import clone as _clone _K = TypeVar("_K", default=str) _V = TypeVar("_V", default=Any) class BaseDict(dict[_K, _V]): _dict: dict[_K, _V] | None _frozen: bool # ids of mappings currently being unwrapped by `_get_dict_or_value`, # anywhere on the current call stack (not just in directly nested calls # of this method: unwrapping a value can re-enter this method through # __init__/__setitem__/_cast). Used to detect self-referential # (cyclic) structures and abort unwrapping them instead of recursing # forever. fix #592 _unwrapping_ids: set[int] = set() @classmethod def _get_dict_or_value(cls, value: Any) -> Any: value = value.dict() if isinstance(value, cls) else value if isinstance(value, MutableMapping): value_id = id(value) if value_id in cls._unwrapping_ids: # value is already being unwrapped higher up the call stack, # ie. it (directly or indirectly) contains itself. Raise # immediately instead of unwrapping it again, to unwind the # whole unwrap/cast chain before the still-cyclic structure # can reach unrelated code with no cycle protection of its # own (eg. keypath separator validation), which would also # recurse infinitely but with an uncontrolled RecursionError. raise ValueError( "Cannot assign a dict that contains itself " "(directly or indirectly): self-referential " "(cyclic) structures are not supported." ) cls._unwrapping_ids.add(value_id) try: for key in value.keys(): key_val = value[key] if isinstance(key_val, cls): key_val = cls._get_dict_or_value(value[key]) value[key] = key_val finally: cls._unwrapping_ids.discard(value_id) return value def __new__(cls, *args: Any, **kwargs: Any) -> Self: obj = super().__new__(cls) # bypass subclass __setattr__ (e.g. KeyattrDict) which may access _dict before it exists object.__setattr__(obj, "_dict", None) object.__setattr__(obj, "_frozen", False) return obj def __init__(self, *args: Any, **kwargs: Any) -> None: if len(args) == 1 and isinstance(args[0], Mapping): # bypass subclass __setattr__ (e.g. KeyattrDict) which may treat _dict as a key d = self._get_dict_or_value(args[0]) object.__setattr__(self, "_dict", d) super().__init__(d) return super().__init__(*args, **kwargs) def __bool__(self) -> bool: if self._dict is not None: return bool(self._dict) return len(self.keys()) > 0 def __contains__(self, o: object) -> bool: if self._dict is not None: return o in self._dict return super().__contains__(o) def __deepcopy__(self, memo: dict[int, Any]) -> Self: obj = self.__class__() for key, value in self.items(): obj[key] = _clone(value, memo=memo) if self._frozen: obj.freeze() return obj def __delitem__(self, key: _K) -> None: self._check_frozen() if self._dict is not None: del self._dict[key] return super().__delitem__(key) def __eq__(self, other: object) -> bool: if self._dict is not None: return self._dict == other return super().__eq__(other) def __getitem__(self, key: _K) -> _V: if self._dict is not None: return self._dict[key] return super().__getitem__(key) def __ior__(self, other: Any) -> Self: # type: ignore[misc,override] self._check_frozen() if self._dict is not None: return cast("Self", self._dict.__ior__(other)) return super().__ior__(other) def __iter__(self) -> Iterator[_K]: if self._dict is not None: return iter(self._dict) return super().__iter__() def __len__(self) -> int: if self._dict is not None: return len(self._dict) return super().__len__() def __or__(self, other: dict[_K, _V]) -> Self: # type: ignore[override] if self._dict is not None: return cast("Self", self._dict.__or__(other)) return cast("Self", super().__or__(other)) def __repr__(self) -> str: if self._dict is not None: return repr(self._dict) return super().__repr__() def __setitem__(self, key: _K, value: _V) -> None: self._check_frozen() value = self._get_dict_or_value(value) if self._dict is not None: is_dict_item = key in self._dict and isinstance(self._dict[key], dict) is_dict_value = isinstance(value, dict) if is_dict_item and is_dict_value: if self._dict[key] is value: # prevent clearing dict instance when assigning value to itself. fix #294 return self._dict[key].clear() # type: ignore[attr-defined] self._dict[key].update(value) # type: ignore[attr-defined] return self._dict[key] = value return super().__setitem__(key, value) def __setstate__(self, state: Mapping[str, Any]) -> None: object.__setattr__(self, "_dict", state.get("_dict", None)) object.__setattr__(self, "_frozen", state.get("_frozen", False)) def __str__(self) -> str: if self._dict is not None: return str(self._dict) return super().__str__() def _check_frozen(self) -> None: if self._frozen: raise TypeError( f"{self.__class__.__name__!r} object is frozen and cannot be modified." ) @property def frozen(self) -> bool: return self._frozen def freeze(self) -> Self: object.__setattr__(self, "_frozen", True) return self def unfreeze(self) -> Self: object.__setattr__(self, "_frozen", False) return self def clear(self) -> None: self._check_frozen() if self._dict is not None: self._dict.clear() return super().clear() def copy(self) -> Self: obj = self.__class__() for key, value in self.items(): obj[key] = value if self._frozen: obj.freeze() return obj def dict(self) -> Self: if self._dict is not None: return cast("Self", self._dict) return self def get(self, key: _K, default: _V | None = None) -> _V | None: # type: ignore[override] if self._dict is not None: return self._dict.get(key, default) return super().get(key, default) def items(self) -> ItemsView[_K, _V]: # type: ignore[override] if self._dict is not None: return self._dict.items() return super().items() def keys(self) -> KeysView[_K]: # type: ignore[override] if self._dict is not None: return self._dict.keys() return super().keys() def pop(self, key: _K, *args: Any) -> _V: self._check_frozen() if self._dict is not None: return self._dict.pop(key, *args) # type: ignore[no-any-return] return super().pop(key, *args) # type: ignore[no-any-return] def setdefault(self, key: _K, default: _V | None = None) -> _V: self._check_frozen() default = self._get_dict_or_value(default) assert default is not None if self._dict is not None: return self._dict.setdefault(key, default) return super().setdefault(key, default) def update(self, other: Any) -> None: self._check_frozen() other = self._get_dict_or_value(other) if self._dict is not None: self._dict.update(other) return super().update(other) def values(self) -> ValuesView[_V]: # type: ignore[override] if self._dict is not None: return self._dict.values() return super().values()