diff --git a/.coveragerc b/.coveragerc
index 0add24e..d64169d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -15,6 +15,7 @@ exclude_lines =
@(abc\.)?abstractmethod
if TYPE_CHECKING
if sys\.version_info
+ @overload
fail_under = 95
diff --git a/README.md b/README.md
index f6ce3be..dfa766a 100644
--- a/README.md
+++ b/README.md
@@ -24,22 +24,24 @@ Install and update using [pip](https://pypi.org/project/case-insensitive-diction
$ pip install -U case-insensitive-dictionary
```
+## API Reference
+
+| Method | Description |
+| :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------- |
+| clear() | Removes all elements from the dictionary. |
+| copy() | Returns a copy of the dictionary. |
+| get(key, default) | Returns the value (case-insensitively), of the item specified with the key.
Falls back to the default value if the specified key does not exist. |
+| fromkeys(iterable, value) | Returns a dictionary with the specified keys and the specified value. |
+| keys() | Returns the dictionary's keys. |
+| values() | Returns the dictionary's values. |
+| items() | Returns the key-value pairs. |
+| pop(key) | Remove the specified item (case-insensitively).
The value of the removed item is the return value. |
+| popitem() | Remove the last item that was inserted into the dictionary.
For Python version <3.7, popitem() removes a random item. |
+
## Example
CaseInsensitiveDict:
-```py
->>> from case_insensitive_dict import CaseInsensitiveDict
-
->>> case_insensitive_dict = CaseInsensitiveDict[str, str](data={"Aa": "b"})
->>> case_insensitive_dict.get("aa")
-'b'
->>> case_insensitive_dict.get("Aa")
-'b'
-```
-
-also supports generic keys:
-
```py
>>> from typing import Union
@@ -53,7 +55,7 @@ also supports generic keys:
```
-and json encoding/decoding:
+which also supports json encoding/decoding:
```py
>>> import json
diff --git a/pyproject.toml b/pyproject.toml
index 8d2328c..c620a33 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
name = "case-insensitive-dictionary"
-version = "0.1.3"
-description = "Case Insensitive Dictionary"
+version = "0.2.0"
+description = "Typed Python Case Insensitive Dictionary"
authors = ["rikhilrai"]
license = "MIT"
readme = "README.md"
diff --git a/src/case_insensitive_dict/__init__.py b/src/case_insensitive_dict/__init__.py
index d6a24cf..0c34020 100644
--- a/src/case_insensitive_dict/__init__.py
+++ b/src/case_insensitive_dict/__init__.py
@@ -8,7 +8,7 @@
from importlib_metadata import PackageNotFoundError # type: ignore[no-redef,misc]
try:
- __version__: str = version(__name__)
+ __version__: str = version('case-insensitive-dictionary')
except PackageNotFoundError:
__version__ = "unknown"
diff --git a/src/case_insensitive_dict/case_insensitive_dict.py b/src/case_insensitive_dict/case_insensitive_dict.py
index 1c762fe..a9685f2 100644
--- a/src/case_insensitive_dict/case_insensitive_dict.py
+++ b/src/case_insensitive_dict/case_insensitive_dict.py
@@ -6,11 +6,14 @@
from typing import Any
from typing import Dict
from typing import Generic
+from typing import Iterable
from typing import Iterator
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import TypeVar
+from typing import Union
+from typing import overload
KT = TypeVar('KT') # pylint: disable=invalid-name
VT = TypeVar('VT') # pylint: disable=invalid-name
@@ -23,13 +26,24 @@
class CaseInsensitiveDict(MutableMapping, Generic[KT, VT]):
+ @overload
def __init__(self, data: Optional[Mapping[KT, VT]] = None) -> None:
+ ...
+
+ @overload
+ def __init__(self, data: Optional[Iterable[Tuple[KT, VT]]] = None) -> None:
+ ...
+
+ def __init__(self, data: Optional[Union[Mapping[KT, VT], Iterable[Tuple[KT, VT]]]] = None) -> None:
# Mapping from lowercased key to tuple of (actual key, value)
self._data: Dict[KT, Tuple[KT, VT]] = {}
if data is None:
data = {}
self.update(data)
+ def __repr__(self) -> str:
+ return f'{self.__class__.__name__}({dict(self.items())!r})'
+
@staticmethod
def _convert_key(key: KT) -> KT:
if isinstance(key, str):
@@ -40,7 +54,10 @@ def __setitem__(self, key: KT, value: VT) -> None:
self._data[self._convert_key(key=key)] = (key, value)
def __getitem__(self, key: KT) -> VT:
- return self._data[self._convert_key(key=key)][1]
+ try:
+ return self._data[self._convert_key(key=key)][1]
+ except KeyError:
+ raise KeyError(f"Key: {key!r} not found.") from None
def __delitem__(self, key: KT) -> None:
del self._data[self._convert_key(key=key)]
@@ -63,8 +80,9 @@ def __eq__(self, other: Any) -> bool:
def copy(self) -> CaseInsensitiveDict[KT, VT]:
return CaseInsensitiveDict(data=dict(self._data.values()))
- def __repr__(self) -> str:
- return f'{self.__class__.__name__}({dict(self.items())!r})'
+ @classmethod
+ def fromkeys(cls, iterable: Iterable[KT], value: VT) -> CaseInsensitiveDict[KT, VT]:
+ return cls([(key, value) for key in iterable])
class CaseInsensitiveDictJSONEncoder(JSONEncoder):
diff --git a/src/tests/test_case_insensitive_dict.py b/src/tests/test_case_insensitive_dict.py
index 5d46436..38ccac7 100644
--- a/src/tests/test_case_insensitive_dict.py
+++ b/src/tests/test_case_insensitive_dict.py
@@ -19,52 +19,63 @@ class CaseInsensitiveDictTestCase:
class TestInit(CaseInsensitiveDictTestCase):
# check that the store is structured as expected
def test_store_written(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, str](data={"a": "b"})
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({"a": "b"})
assert case_insensitive_dict._data == {"a": ("a", "b")}
# check that the key in the store is lowered
def test_store_written_case_insensitive(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, str](data={"A": "b"})
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({"A": "b"})
assert case_insensitive_dict._data == {"a": ("A", "b")}
# check instantiated with an empty dict
def test_store_written_empty(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, str](data={})
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({})
assert isinstance(case_insensitive_dict._data, dict)
assert not case_insensitive_dict._data
# check instantiated with none
def test_store_written_none(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, str](data=None)
+ case_insensitive_dict = CaseInsensitiveDict[str, str](None)
assert isinstance(case_insensitive_dict._data, dict)
assert not case_insensitive_dict._data
# check instantiated with none value
def test_store_written_with_optional_value(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, Optional[str]](data={"A": None})
+ case_insensitive_dict = CaseInsensitiveDict[str, Optional[str]]({"A": None})
assert case_insensitive_dict._data == {"a": ("A", None)}
- # check instantiation with non-str key
- def test_init_with_non_str_key(self) -> None:
- case_insensitive_dict_int = CaseInsensitiveDict[int, str](data={1: "b"})
+ # check instantiation with non-str keys
+ def test_init_with_non_str_keys(self) -> None:
+ case_insensitive_dict_int = CaseInsensitiveDict[int, str]({1: "b"})
assert case_insensitive_dict_int._data == {1: (1, "b")}
- case_insensitive_dict_bool = CaseInsensitiveDict[bool, str](data={True: "b"})
+ case_insensitive_dict_bool = CaseInsensitiveDict[bool, str]({True: "b"})
assert case_insensitive_dict_bool._data == {True: (True, "b")}
+ # check picks the last key/value if instantiated with conflicting cases
+ def test_store_written_with_conflicting_cases(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({"a": "b", "A": "c"})
+ assert case_insensitive_dict._data == {"a": ("A", "c")}
+
+ # check instantiated with list of tuples
+ def test_store_written_with_list_of_tuples(self) -> None:
+ data = [("A", "b")]
+ case_insensitive_dict = CaseInsensitiveDict[str, str](data)
+ assert case_insensitive_dict._data == {"a": ("A", "b")}
+
class TestTyping(CaseInsensitiveDictTestCase):
# check valid typing
def test_valid_types(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, str](data={"a": "b"})
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({"a": "b"})
# keys
case_insensitive_dict['a'] # pylint: disable=pointless-statement
case_insensitive_dict.get('a')
# values
case_insensitive_dict['b'] = 'a'
- # check valid typing
- def test_valid_types_non_str_key(self) -> None:
- case_insensitive_dict_int = CaseInsensitiveDict[int, str](data={1: "b"})
+ # check valid typings
+ def test_valid_types_non_str_keys(self) -> None:
+ case_insensitive_dict_int = CaseInsensitiveDict[int, str]({1: "b"})
# keys
case_insensitive_dict_int[1] # pylint: disable=pointless-statement
case_insensitive_dict_int.get(1)
@@ -73,7 +84,7 @@ def test_valid_types_non_str_key(self) -> None:
# check valid with union
def test_valid_types_union(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[Union[str, int], Union[str, int, bool]](data={"a": "b", "b": 1, 1: "c"})
+ case_insensitive_dict = CaseInsensitiveDict[Union[str, int], Union[str, int, bool]]({"a": "b", "b": 1, 1: "c"})
# keys
case_insensitive_dict[1] # pylint: disable=pointless-statement
case_insensitive_dict.get(1)
@@ -89,7 +100,7 @@ def test_valid_types_union(self) -> None:
# check invalid types
def test_invalid_type(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, int](data={"a": "3"}) # type: ignore[dict-item]
+ case_insensitive_dict = CaseInsensitiveDict[str, int]({"a": "3"}) # type: ignore[dict-item]
case_insensitive_dict[1] = 2 # type: ignore[index]
case_insensitive_dict['b'] = "2" # type: ignore[assignment]
@@ -97,15 +108,15 @@ def test_invalid_type(self) -> None:
class TestContains(CaseInsensitiveDictTestCase):
# check that key in CaseInsensitiveDict check works as expected
def test_contains(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, str](data={"A": "b"})
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({"A": "b"})
assert 'A' in case_insensitive_dict
assert 'a' in case_insensitive_dict
- # check contains with non-str key
- def test_contains_with_non_str_key(self) -> None:
- case_insensitive_dict_int = CaseInsensitiveDict[int, str](data={1: "b"})
+ # check contains with non-str keys
+ def test_contains_with_non_str_keys(self) -> None:
+ case_insensitive_dict_int = CaseInsensitiveDict[int, str]({1: "b"})
assert 1 in case_insensitive_dict_int
- case_insensitive_dict_bool = CaseInsensitiveDict[bool, str](data={True: "b"})
+ case_insensitive_dict_bool = CaseInsensitiveDict[bool, str]({True: "b"})
assert True in case_insensitive_dict_bool
@@ -123,9 +134,9 @@ def test_value(self) -> None:
case_insensitive_dict["A"] = "c"
assert case_insensitive_dict._data == {"a": ("A", "c")}
- # check set item with non-str key
- def test_set_item_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[int, str](data={1: "b"})
+ # check set item with non-str keys
+ def test_set_item_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[int, str]({1: "b"})
assert case_insensitive_dict._data == {1: (1, "b")}
case_insensitive_dict[1] = "c"
assert case_insensitive_dict._data == {1: (1, "c")}
@@ -142,24 +153,29 @@ def test_value_returned(self) -> None:
def test_key_missing(self) -> None:
case_insensitive_dict = CaseInsensitiveDict[str, str]()
assert case_insensitive_dict.get("b") is None
- with pytest.raises(KeyError):
+ with pytest.raises(KeyError, match=r"Key: 'b' not found."):
assert case_insensitive_dict["b"]
+ # check behaviour when key is missing and default passed
+ def test_key_missing_with_default(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[str, str]()
+ assert case_insensitive_dict.get("b", 1) == 1
+
# check value returned using get
def test_value_returned_using_get(self) -> None:
case_insensitive_dict = CaseInsensitiveDict[str, str]({"a": "b"})
assert case_insensitive_dict.get("A") == "b"
assert case_insensitive_dict.get("a") == "b"
- # check get item with non-str key
- def test_get_item_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[int, str](data={1: "b"})
+ # check get item with non-str keys
+ def test_get_item_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[int, str]({1: "b"})
assert case_insensitive_dict.get(1) == "b"
assert case_insensitive_dict[1] == "b"
# check instantiated with none value
def test_store_written_with_optional_value(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[str, Optional[str]](data={"A": None})
+ case_insensitive_dict = CaseInsensitiveDict[str, Optional[str]]({"A": None})
assert case_insensitive_dict.get("a") is None
assert case_insensitive_dict["a"] is None
assert "a" in case_insensitive_dict
@@ -173,9 +189,9 @@ def test_value_removed(self) -> None:
del case_insensitive_dict["A"]
assert "a" not in case_insensitive_dict
- # check del item with non-str key
- def test_del_item_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[int, str](data={1: "b"})
+ # check del item with non-str keys
+ def test_del_item_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[int, str]({1: "b"})
assert case_insensitive_dict[1] == "b"
del case_insensitive_dict[1]
assert 1 not in case_insensitive_dict
@@ -187,9 +203,9 @@ def test_iter(self) -> None:
case_insensitive_dict = CaseInsensitiveDict[str, str]({"a": "b"})
assert list(case_insensitive_dict) == ["a"]
- # check iter with non-str key
- def test_iter_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str](data={1: "b", "a": "c"})
+ # check iter with non-str keys
+ def test_iter_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str]({1: "b", "a": "c"})
assert list(case_insensitive_dict) == [1, "a"]
@@ -217,9 +233,9 @@ def test_lower_empty(self) -> None:
assert isinstance(case_insensitive_dict.lower_items(), GeneratorType)
assert not list(case_insensitive_dict.lower_items())
- # check lower items with non-str key
- def test_lower_items_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str](data={1: "b", "a": "c"})
+ # check lower items with non-str keys
+ def test_lower_items_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str]({1: "b", "a": "c"})
assert list(case_insensitive_dict.lower_items()) == [(1, "b"), ("a", "c")]
@@ -244,9 +260,9 @@ def test_not_equality(self) -> None:
case_insensitive_dict = CaseInsensitiveDict[str, str]()
assert case_insensitive_dict != 1
- # check equality with non-str key
- def test_equality_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str](data={1: "b", "a": "c"})
+ # check equality with non-str keys
+ def test_equality_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str]({1: "b", "a": "c"})
assert case_insensitive_dict == {1: "b", "a": "c"}
@@ -262,9 +278,9 @@ def test_copy_ids(self) -> None:
case_insensitive_dict = CaseInsensitiveDict[str, str]({"A": "b"})
assert id(case_insensitive_dict) != id(case_insensitive_dict.copy())
- # check copy with non-str key
- def test_copy_with_non_str_key(self) -> None:
- case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str](data={1: "b", "a": "c"})
+ # check copy with non-str keys
+ def test_copy_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str]({1: "b", "a": "c"})
assert case_insensitive_dict.copy() == case_insensitive_dict
assert case_insensitive_dict == case_insensitive_dict.copy()
@@ -273,7 +289,7 @@ class TestJson(CaseInsensitiveDictTestCase):
# check to_json
def test_to_json(self) -> None:
data: Dict[Union[bool, str, int], Union[str, int, bool]] = {"A": "a", "b": 1, "c": False, 2: "a", True: 2}
- case_insensitive_dict = CaseInsensitiveDict[Union[bool, str, int], Union[str, int, bool]](data=data)
+ case_insensitive_dict = CaseInsensitiveDict[Union[bool, str, int], Union[str, int, bool]](data)
json_string = json.dumps(obj=case_insensitive_dict, cls=CaseInsensitiveDictJSONEncoder)
assert json_string == '{"A": "a", "b": 1, "c": false, "2": "a", "true": 2}'
assert json_string == json.dumps(data)
@@ -285,3 +301,121 @@ def test_from_json(self) -> None:
expected_case_insensitive_dict = CaseInsensitiveDict[Union[bool, str, int], Union[str, int, bool]]({"A": "a", "b": 1, "c": False, '2': "a", 'true': 2})
assert case_insensitive_dict == expected_case_insensitive_dict
assert case_insensitive_dict == json.loads(json_string)
+
+
+class TestStrAndRepr(CaseInsensitiveDictTestCase):
+ # check string and representation
+ def test_str_and_repr(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[str, str]({"A": "b"})
+ assert case_insensitive_dict.__str__() == "CaseInsensitiveDict({'A': 'b'})"
+ assert case_insensitive_dict.__repr__() == "CaseInsensitiveDict({'A': 'b'})"
+
+
+class TestFromKeys(CaseInsensitiveDictTestCase):
+ # check fromkeys
+ def test_fromkeys(self) -> None:
+ dictionary = dict.fromkeys(["A", "b"], "c")
+ assert dictionary == {"A": "c", "b": "c"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str].fromkeys(["A", "b"], "c")
+ assert case_insensitive_dict == dictionary
+
+
+class TestDictMethods(CaseInsensitiveDictTestCase):
+ # check dict
+ def test_dict(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert dict(case_insensitive_dict) == dict(dictionary)
+
+ # check dict with non-str keys
+ def test_dict_with_non_str_keys(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[Union[str, int], str]({1: "b", "a": "c"})
+ assert dict(case_insensitive_dict) == dict({1: "b", "a": "c"})
+
+ # check falsey dict
+ def test_falsey(self) -> None:
+ case_insensitive_dict = CaseInsensitiveDict[str, str]()
+ assert not case_insensitive_dict
+ assert bool(case_insensitive_dict) is False
+ assert not {}
+ assert bool({}) is False
+
+ # check truthy dict
+ def test_truthy(self) -> None:
+ dictionary = {"a": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert case_insensitive_dict
+ assert bool(case_insensitive_dict) is True
+ assert dictionary
+ assert bool(dictionary) is True
+
+ # check clear
+ def test_clear(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ case_insensitive_dict.clear()
+ assert not case_insensitive_dict
+ dictionary.clear()
+ assert not dictionary
+
+ # check reference to instantiated value not maintained
+ def test_reference(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert id(case_insensitive_dict._data) != id(dictionary)
+ case_insensitive_dict.pop("a")
+ assert dictionary == {"A": "b"}
+
+ # check pop
+ def test_pop(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert dictionary.pop("A") == case_insensitive_dict.pop("a") == "b"
+ assert not case_insensitive_dict
+ assert not dictionary
+
+ # check pop key not in dictionary
+ def test_pop_key_not_in_dictionary(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ with pytest.raises(KeyError):
+ case_insensitive_dict.pop("b")
+ with pytest.raises(KeyError):
+ dictionary.pop("b")
+
+ # check pop key not in dictionary with default
+ def test_pop_key_not_in_dictionary_with_default(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert dictionary.pop("A") == case_insensitive_dict.pop("a") == "b"
+ response = case_insensitive_dict.pop("b", "a")
+ assert response == 'a'
+
+ # check popitem
+ def test_pop_item(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert dictionary.popitem() == case_insensitive_dict.popitem() == ("A", "b")
+ with pytest.raises(KeyError):
+ case_insensitive_dict.popitem()
+ with pytest.raises(KeyError):
+ dictionary.popitem()
+
+ # check keys
+ def test_keys(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert dictionary.keys() == case_insensitive_dict.keys()
+ assert list(dictionary.keys()) == list(case_insensitive_dict.keys()) == ["A"]
+
+ # check values
+ def test_values(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert list(dictionary.values()) == list(case_insensitive_dict.values()) == ["b"]
+
+ # check items
+ def test_items(self) -> None:
+ dictionary = {"A": "b"}
+ case_insensitive_dict = CaseInsensitiveDict[str, str](dictionary)
+ assert list(dictionary.items()) == list(case_insensitive_dict.items()) == [("A", "b")]