from collections.abc import MutableMapping, MutableSet
from typing import Any, Dict, Optional
from pprint import pformat
[docs]
class CaseInsensitiveDict(MutableMapping):
"""
Python dictionary where the keys are case-insensitive.
Note that this assumes the keys are strings, and indeed will fail if you try to
create an instance where keys are not strings.
All common Python dictionary operations are supported, and additional operations
can be added easily.
In order to preserve capitalization on key initialization, the implementation relies on storing
a dictionary of mappings, which are used to check any new keys against existing keys and compare them
in a case-insensitive fashion.
Attributes
----------
data : dict
The equivalent case-sensitive dictionary. This stores the actual values.
map : dict
Dictionary of mappings between the lowercase representation and the initial capitalization.
Warnings
--------
This container preserves the initial capitalization, such that
any operation which operates on an existing entry will not modify it.
This means that for example :meth:`__setitem__()` will NOT update the original capitalization.
"""
def __init__(self, *args, **kwargs):
self.data: dict = dict(*args, **kwargs)
if not all([isinstance(i, str) for i in self.data]):
raise TypeError("All keys must be strings!")
self.map: Dict[str, str] = {k.lower(): k for k in self.data.keys()}
def _getKey(self, key: str, raiseError=False):
"""
This function checks if the input key already exists.
Note that this check is case insensitive
Parameters
----------
key : str
the key to check
raiseError : bool
if true, raise KeyError if ``key`` is not found.
Returns
-------
str, None
Returns the original key if it exists. Otherwise returns None.
Raises
------
KeyError
If ``raiseError`` and key is not found.
"""
if key.lower() in self.map:
return self.map[key.lower()]
else:
if raiseError:
raise KeyError(f"Key '{key}' not found.")
return None
def __setitem__(self, key: str, value: Any):
if not isinstance(key, str):
raise TypeError("All keys must be strings.")
existingKey = self._getKey(key)
if existingKey:
key = existingKey
self.data[key] = value
self.map[key.lower()] = key
def __getitem__(self, key: str) -> Any:
existingKey = self._getKey(key, raiseError=True)
return self.data[existingKey]
def __delitem__(self, key: str):
existingKey = self._getKey(key, raiseError=True)
self.map.pop(existingKey.lower())
self.data.pop(existingKey)
def __iter__(self):
return iter(self.data)
def __len__(self) -> int:
return len(self.data)
def __eq__(self, other) -> bool:
"""We convert both to regular dict, and compare their lower case values"""
selfLower = {k.lower(): v for k, v in self.items()}
otherLower = {k.lower(): v for k, v in other.items()}
return selfLower.__eq__(otherLower)
def __repr__(self):
return pformat(self.data)
[docs]
class CaseInsensitiveSet(MutableSet):
"""
Python set where the elements are case-insensitive.
Note that this assumes the elements are strings, and indeed will fail if you try to
create an instance where elements are not strings.
All common Python set operations are supported, and additional operations
can be added easily.
In order to preserve capitalization on key initialization, the implementation relies on storing
a dictionary of mappings which are used to check any new keys against existing keys and compare them
in a case-insensitive fashion.
Attributes
----------
data : set
The equivalent case-sensitive set.
map : dict
Dictionary of mappings between the lowercase representation and the initial capitalization.
Warnings
--------
This container preserves the initial capitalization, such that
any operation which operates on an existing entry will not modify it.
This means that :meth:`add()` and :meth:`update()` will NOT update the original capitalization.
"""
def __init__(self, *args, **kwargs):
self.data: set = set(*args, **kwargs)
if not all([isinstance(i, str) for i in self.data]):
raise TypeError("All items must be strings!")
self.map: Dict[str, str] = {k.lower(): k for k in list(self)}
def _getItem(self, item: str) -> Optional[str]:
"""
This function checks if the input item already exists.
Note that this check is case insensitive
Parameters
----------
item : str
the item to check
Returns
-------
str, None
Returns the original item if it exists. Otherwise returns None.
"""
if item in self:
return self.map[item.lower()]
else:
return None
[docs]
def add(self, item: str):
if not isinstance(item, str):
raise TypeError("All keys must be strings.")
existingItem = self._getItem(item)
# don't do anything if it exists
if not existingItem:
self.map[item.lower()] = item
self.data.add(item)
def __contains__(self, item) -> bool:
if not isinstance(item, str):
raise TypeError("All keys must be strings.")
return item.lower() in self.map.keys()
def __eq__(self, other) -> bool:
"""We convert both to regular set, and compare their lower case values"""
if not all([isinstance(i, str) for i in other]):
raise TypeError("All items must be strings!")
a = {s.lower() for s in list(self)}
b = {o.lower() for o in list(other)}
return a.__eq__(b)
def __len__(self) -> int:
return len(self.data)
def __iter__(self):
return iter(self.data)
[docs]
def discard(self, item: str):
existingItem = self._getItem(item)
if existingItem:
self.map.pop(existingItem.lower())
self.data.discard(existingItem)
def union(self, d):
# make a copy of this object
new_set = CaseInsensitiveSet(self.data)
for item in d:
existingItem = new_set._getItem(item)
if not existingItem:
new_set.add(item)
return new_set
[docs]
def update(self, d):
"""Just call :meth:`add()` iteratively"""
for item in d:
self.add(item)
[docs]
def issubset(self, other) -> bool:
"""We convert both to regular set, and compare their lower case values"""
lowerSelf = {s.lower() for s in self}
lowerOther = {s.lower() for s in other}
return lowerSelf.issubset(lowerOther)
def __repr__(self):
return pformat(self.data)