Source code for ooodev.utils.helper.dot_dict

from __future__ import annotations
from typing import Any, TypeVar, Generic, Dict, cast, Generator
from collections import OrderedDict
from ooodev.utils.gen_util import NULL_OBJ

T = TypeVar("T")

# Protected attributes that should not be included in dictionary operations
_PROTECTED_ATTRIBS = ("_missing_attrib_value", "_internal_keys", "_is_protocol")


[docs]class DotDict(Generic[T]): """ Generic class for accessing dictionary keys as attributes or keys as attributes. Type Parameters: T: Value type Args: missing_attr_val (Any, optional): Value to return if attribute is not found. If omitted then AttributeError is raised if attribute is not found. kwargs (T): Keyword arguments. Note: It is possible to override class attributes such as keys, copy, and items attributes. This is not recommended. .. code-block:: python d = DotDict[str](a="hello", keys="world") assert d.keys == "world" Example: .. code-block:: python # String values d1 = DotDict[str](a="hello", b="world") # Integer values d2 = DotDict[int](a=1, b=2) # Mixed values with Union d3 = DotDict[Union[str, int]](a="hello", b=2) # Mixed values with object d4 = DotDict[object](a="hello", b=2) # Mixed values with no generic type d5 = DotDict(a="hello", b=2) # Mixed values missing attribute value d6 = DotDict[object](None, a="hello", b=2) assert d6.missing is None """
[docs] def __init__(self, missing_attr_val: Any = NULL_OBJ, **kwargs: T) -> None: """ Constructor Args: missing_attr_val (Any, optional): Value to return if attribute is not found. If omitted then AttributeError is raised. kwargs (T): Keyword arguments. """ self._missing_attrib_value = missing_attr_val self._internal_keys: OrderedDict[str, None] = OrderedDict() self.__dict__.update(cast(Dict[str, T], kwargs)) for key in kwargs: self._internal_keys[key] = None
def __bool__(self) -> bool: """Returns True if the dictionary is not empty.""" return len(self._internal_keys) > 0 def __getitem__(self, key: str) -> T: """Gets item by key using dictionary syntax.""" return self.__dict__[key] def __setitem__(self, key: str, value: T) -> None: """Sets item by key using dictionary syntax.""" self.__dict__[key] = value self._internal_keys[key] = None def __delitem__(self, key: str) -> None: """Deletes item by key using dictionary syntax.""" del self.__dict__[key] if key in self._internal_keys: del self._internal_keys[key] def __getattr__(self, key: str) -> T: """Gets item by key using attribute syntax.""" try: return self.__dict__[key] # type: ignore except KeyError: if self._missing_attrib_value is not NULL_OBJ: return self._missing_attrib_value # type: ignore raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'") def __setattr__(self, key: str, value: Any) -> None: """Sets item by key using attribute syntax.""" if key.startswith("__"): super().__setattr__(key, value) else: if key not in _PROTECTED_ATTRIBS: self._internal_keys[key] = None self.__dict__[key] = value # type: ignore def __delattr__(self, key: str) -> None: """Deletes item by key using attribute syntax.""" if key in self._internal_keys: del self._internal_keys[key] if key.startswith("__"): super().__delattr__(key) else: del self.__dict__[key] # type: ignore def __contains__(self, key: str) -> bool: """Returns True if key exists in dictionary.""" return key in self.__dict__ def __len__(self) -> int: """Returns the number of items in the dictionary.""" return len(self._internal_keys) def __copy__(self) -> DotDict[T]: """Returns a shallow copy of the dictionary.""" return self.copy()
[docs] def get(self, key: str, default: T | None = None) -> T | None: """ Get value from dictionary. Args: key (KT): Key to get value. default (T | None, optional): Default value if key not found. Defaults to None. Returns: T | None: Value of key or default value. """ return self.__dict__.get(key, default)
[docs] def items(self) -> Generator[tuple[str, T], None, None]: """Returns all items in the dictionary in a set like object.""" for key in self._internal_keys.keys(): if key not in self.__dict__: continue yield key, self.__dict__[key]
[docs] def keys(self) -> Generator[str, None, None]: """Returns all keys in the dictionary in a set like object.""" # filter out _PROTECTED_ATTRIBS and return a generator expression for key in self._internal_keys.keys(): yield key
[docs] def values(self) -> Generator[T, None, None]: """Returns an object providing a view on the dictionary's values.""" # filter out _PROTECTED_ATTRIBS and return a generator expression for key in self._internal_keys.keys(): if key not in self.__dict__: continue yield self.__dict__[key]
[docs] def update(self, other: Dict[str, T] | DotDict[T]) -> None: """ Update dictionary with another dictionary. Args: other (Dict[KT, T] | DotDict[KT, T]): Dictionary to update with. Raises: TypeError: If other is not a dict or DotDict """ if isinstance(other, DotDict): self.__dict__.update(other.__dict__) self._internal_keys.update(other._internal_keys) elif isinstance(other, dict): self.__dict__.update(other) for key in other.keys(): self._internal_keys[key] = None else: raise TypeError(f"Expected dict or DotDict, got {type(other)}")
[docs] def copy(self) -> DotDict[T]: """Returns a shallow copy of the dictionary.""" copy_dict = {} for key in self._internal_keys.keys(): if key not in self.__dict__: continue copy_dict[key] = self.__dict__[key] copy_dict["missing_attr_val"] = self._missing_attrib_value inst = DotDict[T](**copy_dict) return inst
[docs] def copy_dict(self) -> Dict[str, T]: """Returns a shallow copy as a standard dictionary.""" copy_dict = {} for key in self._internal_keys.keys(): if key not in self.__dict__: continue copy_dict[key] = self.__dict__[key] return copy_dict
[docs] def clear(self) -> None: """Clears all items from the dictionary while preserving protected attributes.""" protected = {} for attr in _PROTECTED_ATTRIBS: if attr in self.__dict__: protected[attr] = self.__dict__[attr] self._internal_keys.clear() self.__dict__.clear() self.__dict__.update(protected)