--- /dev/null
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank@brehm-online.com
+@copyright: © 2020 by Frank Brehm, Berlin
+@summary: A module for providing a dict with case insensitive keys.
+"""
+from __future__ import absolute_import
+
+# Standard module
+import copy
+
+try:
+ from collections.abc import MutableMapping
+except ImportError:
+ from collections import MutableMapping
+
+# Third party modules
+
+# Own modules
+from fb_tools.common import pp
+from fb_tools.errors import FbError
+from fb_tools.obj import FbBaseObject
+
+__version__ = '0.1.0'
+
+LOG = logging.getLogger(__name__)
+
+
+# =============================================================================
+class WrongKeyTypeError(TypeError, FbError):
+
+ # -------------------------------------------------------------------------
+ def __init__(self, key):
+
+ self.key = key
+
+ # -------------------------------------------------------------------------
+ def __str__(self):
+
+ msg = "Key {key!r} must be of type 'str', but is of type {cls!r} instead."
+ return msg.format(key=key, cls=key.__class__.__name__)
+
+
+# =============================================================================
+class WrongCompareClassError(TypeError, FbError):
+
+ # -------------------------------------------------------------------------
+ def __init__(self, other):
+
+ self.other_class = other.__class__.__name__
+
+ # -------------------------------------------------------------------------
+ def __str__(self):
+
+ msg = "Object {!r} is not a CaseInsensitiveDict object."
+ return msg.format(self.other_class)
+
+
+# =============================================================================
+class CaseInsensitiveKeyError(KeyError, FbError):
+
+ # -------------------------------------------------------------------------
+ def __init__(self, key):
+
+ self.key = key
+
+ # -------------------------------------------------------------------------
+ def __str__(self):
+
+ msg = "Key {!r} not existing in CaseInsensitiveDict."
+ return msg.format(key)
+
+
+# =============================================================================
+class CaseInsensitiveDict(MutableMapping):
+ """
+ A dictionary, where the keys are insensitive strings.
+ The keys MUST be of type string!
+ It works like a dict.
+ """
+
+ wrong_type_msg = "Key {key!r} must be of type 'str', but is of type {cls!r} instead."
+
+ # -------------------------------------------------------------------------
+ def __init__(self, **kwargs):
+ '''Use the object dict'''
+ self._map = dict()
+
+ for key in kwargs:
+ self._set_item(key, kwargs[key])
+
+ # -------------------------------------------------------------------------
+ def _set_item(self, key, value):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ key = okey
+ break
+
+ self._map[key] = value
+
+ # -------------------------------------------------------------------------
+ def _get_item(self, key):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ return self._map[okey]
+
+ raise CaseInsensitiveKeyError(key)
+
+ # -------------------------------------------------------------------------
+ def get(self, key):
+ return self._get_item(key)
+
+ # -------------------------------------------------------------------------
+ def _del_item(self, key, strict=True):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ del self._map[okey]
+ return
+
+ if strict:
+ raise CaseInsensitiveKeyError(key)
+
+ return
+
+ # -------------------------------------------------------------------------
+ # The next five methods are requirements of the ABC.
+ def __setitem__(self, key, value):
+ self._set_item(key, value)
+
+ # -------------------------------------------------------------------------
+ def __getitem__(self, key):
+ return self._get_item(key)
+
+ # -------------------------------------------------------------------------
+ def __delitem__(self, key):
+ self._del_item(key)
+
+ # -------------------------------------------------------------------------
+ def __iter__(self):
+
+ for key in self.keys():
+ yield key
+
+ # -------------------------------------------------------------------------
+ def __len__(self):
+ return len(self._map)
+
+ # -------------------------------------------------------------------------
+ # The next methods aren't required, but nice for different purposes:
+ def __str__(self):
+ '''returns simple dict representation of the mapping'''
+ return str(self._map)
+
+ # -------------------------------------------------------------------------
+ def __repr__(self):
+ '''echoes class, id, & reproducible representation in the REPL'''
+ return '{}, {}({})'.format(
+ super(CaseInsensitiveDict, self).__repr__(),
+ self.__class__.__name__,
+ self._map)
+
+ # -------------------------------------------------------------------------
+ def __contains__(self, key):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ return True
+
+ return False
+
+ # -------------------------------------------------------------------------
+ def keys(self):
+
+ return sorted(self._map.keys(), key=str.lower)
+
+ # -------------------------------------------------------------------------
+ def items(self):
+
+ item_list = []
+
+ for key in sorted(self._map.keys(), key=str.lower):
+ value = self._map[key]
+ item_list.append((key, value))
+
+ return item_list
+
+ # -------------------------------------------------------------------------
+ def values(self):
+
+ value_list = []
+
+ for key in sorted(self._map.keys(), key=str.lower):
+ value_list.append(self._map[key])
+
+ return value_list
+
+ # -------------------------------------------------------------------------
+ def __eq__(self, other):
+
+ if not isinstance(other, CaseInsensitiveDict):
+ raise WrongCompareClassError(other)
+
+ if len(self) != len(other):
+ return False
+
+ # First compare keys
+ my_keys = []
+ other_keys = []
+
+ for key in self.keys():
+ my_keys.append(key)
+
+ for key in other.keys():
+ other_keys.append(key)
+
+ if my_keys != other_keys:
+ return False
+
+ # Now compare values
+ for key in self.keys():
+ if self[key] != other[key]:
+ return False
+
+ return True
+
+ # -------------------------------------------------------------------------
+ def __ne__(self, other):
+
+ if not isinstance(other, CaseInsensitiveDict):
+ raise WrongCompareClassError(other)
+
+ if self.__eq__(other):
+ return True
+
+ return False
+
+ # -------------------------------------------------------------------------
+ def pop(self, key, *args):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ key = okey
+ break
+
+ return self._map.pop(key, *args)
+
+ # -------------------------------------------------------------------------
+ def popitem(self):
+
+ if not len(self._map):
+ return None
+
+ key = self.keys()[0]
+ value = self._map[key]
+ del self._map[key]
+ return (key, value)
+
+ # -------------------------------------------------------------------------
+ def clear(self):
+ self._map = dict()
+
+ # -------------------------------------------------------------------------
+ def setdefault(self, key, default):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ return self._map[okey]
+
+ self._set_item(key, default)
+ return default
+
+ # -------------------------------------------------------------------------
+ def update(self, other):
+
+ if isinstance(other, CaseInsensitiveDict) or isinstance(other, dict):
+ for key in other.keys():
+ self._set_item(key, other[key])
+ return
+
+ for tokens in other:
+ key = tokens[0]
+ value = tokens[1]
+ self._set_item(key, value)
+
+ # -------------------------------------------------------------------------
+ def as_dict(self, short=True):
+
+ res = {}
+ for key in self._map.keys():
+ value = self._map[key]
+ if isinstance(value, FbBaseObject):
+ res[key] = value.as_dict(short=short)
+ else:
+ res[key] = copy.copy(value)
+
+ return res
+
+ # -------------------------------------------------------------------------
+ def set_key(self, key, *args):
+
+ if not isinstance(key, str):
+ raise WrongKeyTypeError(key)
+
+ value = None
+
+ if len(args) > 1:
+ msg = "Wrong arguments {!r} in calling set_key(), at most one optional argument "
+ msg += "may be given."
+ raise AttributeError(msg.format(args))
+ elif len(args) == 1:
+ value = args[0]
+
+ for okey in self._map.keys():
+ if okey.lower() == key.lower():
+ if okey == key:
+ if len(args) == 1:
+ self._map[key] = value
+ else:
+ if len(args) < 1:
+ # Taking the old value
+ value = self._map[okey]
+ del self._map[okey]
+ self._map[key] = value
+ return
+
+ # Given key not found
+ if len(args) == 1:
+ self._map[key] = value
+ else:
+ raise CaseInsensitiveKeyError(key)
+
+
+# =============================================================================
+if __name__ == "__main__":
+
+ pass
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list