Source code for triad.collections.dict

import copy
import json
import sys
from collections import OrderedDict
from typing import Any, Dict, List, Tuple, TypeVar, Union

from triad.exceptions import InvalidOperationError
from triad.utils.assertion import assert_arg_not_none
from triad.utils.convert import as_type
from triad.utils.iter import to_kv_iterable

KT = TypeVar("KT")
VT = TypeVar("VT")


[docs] class IndexedOrderedDict(OrderedDict, Dict[KT, VT]): """Subclass of OrderedDict that can get and set with index""" def __init__(self, *args: Any, **kwds: Any): self._readonly = False self._need_reindex = True self._key_index: Dict[Any, int] = {} self._index_key: List[Any] = [] super().__init__(*args, **kwds) @property def readonly(self) -> bool: """Whether this dict is readonly""" return self._readonly
[docs] def set_readonly(self) -> None: """Make this dict readonly""" self._readonly = True
[docs] def index_of_key(self, key: Any) -> int: """Get index of key :param key: key value :return: index of the key value """ self._build_index() return self._key_index[key]
[docs] def get_key_by_index(self, index: int) -> KT: """Get key by index :param index: index of the key :return: key value at the index """ self._build_index() return self._index_key[index]
[docs] def get_value_by_index(self, index: int) -> VT: """Get value by index :param index: index of the item :return: value at the index """ key = self.get_key_by_index(index) return self[key]
[docs] def get_item_by_index(self, index: int) -> Tuple[KT, VT]: """Get key value pair by index :param index: index of the item :return: key value tuple at the index """ key = self.get_key_by_index(index) return key, self[key]
[docs] def set_value_by_index(self, index: int, value: VT) -> None: """Set value by index :param index: index of the item :param value: new value """ key = self.get_key_by_index(index) self[key] = value
[docs] def pop_by_index(self, index: int) -> Tuple[KT, VT]: """Pop item at index :param index: index of the item :return: key value tuple at the index """ key = self.get_key_by_index(index) return key, self.pop(key)
[docs] def equals(self, other: Any, with_order: bool): """Compare with another object :param other: for possible types, see :func:`~triad.utils.iter.to_kv_iterable` :param with_order: whether to compare order :return: whether they equal """ if with_order: if isinstance(other, OrderedDict): return self == other return self == OrderedDict(to_kv_iterable(other)) else: if isinstance(other, OrderedDict) or not isinstance(other, Dict): return self == dict(to_kv_iterable(other)) return self == other
# ----------------------------------- Wrappers over OrderedDict def __setitem__( # type: ignore self, key: KT, value: VT, *args: Any, **kwds: Any ) -> None: self._pre_update("__setitem__", key not in self) super().__setitem__(key, value, *args, **kwds) # type: ignore def __delitem__(self, *args: Any, **kwds: Any) -> None: # type: ignore self._pre_update("__delitem__") super().__delitem__(*args, **kwds) # type: ignore
[docs] def clear(self) -> None: self._pre_update("clear") super().clear()
[docs] def copy(self) -> "IndexedOrderedDict": other = super().copy() assert isinstance(other, IndexedOrderedDict) other._need_reindex = self._need_reindex other._index_key = self._index_key.copy() other._key_index = self._key_index.copy() other._readonly = False return other
def __copy__(self) -> "IndexedOrderedDict": return self.copy() def __deepcopy__(self, arg: Any) -> "IndexedOrderedDict": it = [(copy.deepcopy(k), copy.deepcopy(v)) for k, v in self.items()] return IndexedOrderedDict(it)
[docs] def popitem(self, *args: Any, **kwds: Any) -> Tuple[KT, VT]: # type: ignore self._pre_update("popitem") return super().popitem(*args, **kwds) # type: ignore
[docs] def move_to_end(self, *args: Any, **kwds: Any) -> None: # type: ignore self._pre_update("move_to_end") super().move_to_end(*args, **kwds) # type: ignore
def __sizeof__(self) -> int: # pragma: no cover return super().__sizeof__() + sys.getsizeof(self._need_reindex)
[docs] def pop(self, *args: Any, **kwds: Any) -> VT: # type: ignore self._pre_update("pop") return super().pop(*args, **kwds) # type: ignore
def _build_index(self) -> None: if self._need_reindex: self._index_key = list(self.keys()) self._key_index = {x: i for i, x in enumerate(self._index_key)} self._need_reindex = False def _pre_update(self, op: str, need_reindex: bool = True) -> None: if self.readonly: raise InvalidOperationError("This dict is readonly") self._need_reindex = need_reindex
[docs] class ParamDict(IndexedOrderedDict[str, Any]): """Parameter dictionary, a subclass of ``IndexedOrderedDict``, keys must be string :param data: for possible types, see :func:`~triad.utils.iter.to_kv_iterable` :param deep: whether to deep copy ``data`` """ OVERWRITE = 0 THROW = 1 IGNORE = 2 def __init__(self, data: Any = None, deep: bool = True): super().__init__() self.update(data, deep=deep) def __setitem__( # type: ignore self, key: str, value: Any, *args: Any, **kwds: Any ) -> None: assert isinstance(key, str) super().__setitem__(key, value, *args, **kwds) # type: ignore def __getitem__(self, key: Union[str, int]) -> Any: # type: ignore if isinstance(key, int): key = self.get_key_by_index(key) return super().__getitem__(key) # type: ignore
[docs] def get(self, key: Union[int, str], default: Any) -> Any: # type: ignore """Get value by ``key``, and the value must be a subtype of the type of ``default``(which can't be None). If the ``key`` is not found, return ``default``. :param key: the key to search :raises NoneArgumentError: if default is None :raises TypeError: if the value can't be converted to the type of ``default`` :return: the value by ``key``, and the value must be a subtype of the type of ``default``. If ``key`` is not found, return `default` """ assert_arg_not_none(default, "default") if (isinstance(key, str) and key in self) or isinstance(key, int): return as_type(self[key], type(default)) return default
[docs] def get_or_none(self, key: Union[int, str], expected_type: type) -> Any: """Get value by `key`, and the value must be a subtype of ``expected_type`` :param key: the key to search :param expected_type: expected return value type :raises TypeError: if the value can't be converted to ``expected_type`` :return: if ``key`` is not found, None. Otherwise if the value can be converted to ``expected_type``, return the converted value, otherwise raise exception """ return self._get_or(key, expected_type, throw=False)
[docs] def get_or_throw(self, key: Union[int, str], expected_type: type) -> Any: """Get value by ``key``, and the value must be a subtype of ``expected_type``. If ``key`` is not found or value can't be converted to ``expected_type``, raise exception :param key: the key to search :param expected_type: expected return value type :raises KeyError: if ``key`` is not found :raises TypeError: if the value can't be converted to ``expected_type`` :return: only when ``key`` is found and can be converted to ``expected_type``, return the converted value """ return self._get_or(key, expected_type, throw=True)
[docs] def to_json(self, indent: bool = False) -> str: """Generate json expression string for the dictionary :param indent: whether to have indent :return: json string """ if not indent: return json.dumps(self, separators=(",", ":")) else: return json.dumps(self, indent=4)
[docs] def update( # type: ignore self, other: Any, on_dup: int = 0, deep: bool = True ) -> "ParamDict": """Update dictionary with another object (for possible types, see :func:`~triad.utils.iter.to_kv_iterable`) :param other: for possible types, see :func:`~triad.utils.iter.to_kv_iterable` :param on_dup: one of ``ParamDict.OVERWRITE``, ``ParamDict.THROW`` and ``ParamDict.IGNORE`` :raises KeyError: if using ``ParamDict.THROW`` and other contains existing keys :raises ValueError: if ``on_dup`` is invalid :return: itself """ self._pre_update("update", True) for k, v in to_kv_iterable(other): if on_dup == ParamDict.OVERWRITE or k not in self: self[k] = copy.deepcopy(v) if deep else v elif on_dup == ParamDict.THROW: raise KeyError(f"{k} exists in dict") elif on_dup == ParamDict.IGNORE: continue else: raise ValueError(f"{on_dup} is not supported") return self
def _get_or( self, key: Union[int, str], expected_type: type, throw: bool = True ) -> Any: if (isinstance(key, str) and key in self) or isinstance(key, int): return as_type(self[key], expected_type) if throw: raise KeyError(f"{key} not found") return None