Skip to content

Commit aae0b27

Browse files
authored
Add caseless attr dict (#9)
* updating version. * Running `blue`. * replacing `black` with `blue`. * adding caseless attribute dictionary. * adding more cases. * added `str_only`. * added `str_only` to examples. * Modifying examples and documentation. * Updating README.md to reflect the changes. * renaming file. * Fixing imports. * removing doctest. * removing doctest. * Removing un used import. * adding doctest modules back in.
1 parent 585f453 commit aae0b27

15 files changed

+1568
-324
lines changed

.github/workflows/pytest-workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ jobs:
2626
python -m pip install -r requirements-test.txt
2727
- name: Test
2828
run: |
29-
pytest --doctest-modules
29+
pytest tests/ --doctest-modules

README.md

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
2-
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
2+
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-0000ff.svg)](https://github.com/psf/blue)
33
[![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://opensource.org/licenses/MIT)
44
[![codecov](https://codecov.io/gh/tybruno/caseless-dictionary/branch/main/graph/badge.svg?token=ZO94EJFI3G)](https://codecov.io/gh/tybruno/caseless-dictionary)
5+
56
# caseless-dictionary
67

7-
A simple, fast, typed, and tested implementation for a python3.6+ case-insensitive dictionary. This class extends and
8-
maintains the original functionality of the builtin `dict`.
8+
A simple, fast, typed, and tested implementation for a python3.6+ case-insensitive and attribute case-insensitive
9+
dictionaries. This class extends and maintains the original functionality of the builtin `dict` while providing extra
10+
features.
911

1012
#### Key Features:
1113

1214
* **Easy**: If you don't care about the case of the key in a dictionary then this implementation is easy to use since it
13-
acts just like a `dict` obj.
15+
acts just like a `dict` obj.
16+
* **Attribute Access**: `CaselessAttrDict` allows attribute-style access to dictionary items, providing an alternative,
17+
often more readable way to access dictionary items.
1418
* **Great Developer Experience**: Being fully typed makes it great for editor support.
1519
* **Fully Tested**: Our test suit fully tests the functionality to ensure that `CaselessDict` runs as expected.
1620
* **There is More!!!**:
@@ -22,52 +26,96 @@ maintains the original functionality of the builtin `dict`.
2226

2327
`pip install caseless-dictionary`
2428

25-
## Simple CaselessDict Example
29+
## Caseless Dictionaries
30+
31+
| Class Name | Description | Example |
32+
|----------------------|----------------------------------------------------------------|------------------------------------------------------------------------------|
33+
| CaselessDict | A dictionary where keys that are strings are case-folded. | `CaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello world': 1}` |
34+
| CaseFoldCaselessDict | A dictionary where keys that are strings are case-folded. | `CaseFoldCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello world': 1}` |
35+
| LowerCaselessDict | A dictionary where keys that are strings are in lower case. | `LowerCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello world': 1}` |
36+
| UpperCaselessDict | A dictionary where keys that are strings are in upper case. | `UpperCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'HELLO WORLD': 1}` |
37+
| TitleCaselessDict | A dictionary where keys that are strings are in title case. | `TitleCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'Hello World': 1}` |
38+
| SnakeCaselessDict | A dictionary where keys that are strings are in snake case. | `SnakeCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello_world': 1}` |
39+
| KebabCaselessDict | A dictionary where keys that are strings are in kebab case. | `KebabCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello-world': 1}` |
40+
| ConstantCaselessDict | A dictionary where keys that are strings are in constant case. | `ConstantCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'HELLO_WORLD': 1}` |
41+
## Caseless Attribute Dictionaries
42+
43+
| Class Name | Description | Example |
44+
|--------------------------|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
45+
| SnakeCaselessAttrDict | A dictionary where keys that are strings are in snake case and can be accessed using attribute notation. | `SnakeCaselessAttrDict({" HeLLO WoRLD ": 1}).hello_world # Output: 1` |
46+
| ConstantCaselessAttrDict | A dictionary where keys that are strings are in constant case and can be accessed using attribute notation. | `ConstantCaselessAttrDict({" HeLLO WoRLD ": 1}).HELLO_WORLD # Output: 1` |
47+
48+
### Basic CaselessDict Example
2649

2750
```python
2851
from caseless_dictionary import CaselessDict
2952

30-
normal_dict: dict = {" CamelCase ": 1, " UPPER ": "TWO", 3: " Number as Key "}
53+
# Create a CaselessDict
54+
caseless_dict = CaselessDict({" HeLLO WoRLD ": 1, 2: "two"})
55+
56+
print(caseless_dict) # Output: {'hello world': 1, 2: 'two'}
57+
58+
# Accessing the value using different cases
59+
print(caseless_dict[" hello world "]) # Output: 1
60+
print(caseless_dict[" HELLO WORLD "]) # Output: 1
61+
62+
# Accessing non string value
63+
print(caseless_dict[2]) # Output: two
64+
```
3165

32-
caseless_dict: CaselessDict = CaselessDict(normal_dict)
66+
### Caseless Dictionary with Key as Str Only
3367

34-
print(caseless_dict) # {'camelcase': 1, 'upper': 'TWO', 3: 'Number as Key'}
68+
```python
69+
from caseless_dictionary import CaselessDict
3570

36-
print("CamelCase" in caseless_dict) # True
37-
print("camelcase" in caseless_dict) # True
71+
# Create a CaselessDict with key_is_str_only set to True
72+
CaselessDict.key_is_str_only = True
73+
caseless_dict = CaselessDict({" HeLLO WoRLD ": 1})
3874

39-
print(caseless_dict[" camelCASE "]) # 1
75+
# Attempt to set a non-string key
76+
try:
77+
caseless_dict[1] = 2
78+
except TypeError:
79+
print("TypeError raised as expected when key_is_str_only is True")
4080
```
4181

42-
## Simple UpperCaselessDict Example
82+
83+
### Basic SnakeCaselessAttrDict Example
4384

4485
```python
45-
from caseless_dictionary import UpperCaselessDict
46-
from typing import Iterable
86+
from caseless_dictionary import SnakeCaselessAttrDict
4787

48-
iterable: Iterable = [(" wArNIng", 0), ("deBug ", 10)]
49-
upper_caseless_dict: dict = UpperCaselessDict(iterable)
50-
print(upper_caseless_dict) # {'WARNING': 0, 'DEBUG': 10}
88+
# Create a SnakeCaselessAttrDict
89+
snake_caseless_attr_dict = SnakeCaselessAttrDict({" HeLLO WoRLD ": 1, 2: "two"})
90+
print(snake_caseless_attr_dict) # Output: {'hello_world': 1, 2: 'two'}
5191

52-
print("warning" in upper_caseless_dict) # True
92+
# Accessing the value using attribute notation
93+
print(snake_caseless_attr_dict.hello_world) # Output: 1
94+
print(snake_caseless_attr_dict.HELLO_WORLD) # Output: 1
95+
96+
# Accessing the value using Keys
97+
print(snake_caseless_attr_dict[" hello_world "]) # Output: 1
98+
print(snake_caseless_attr_dict[" HELLO WORLD "]) # Output: 1
99+
100+
# Accessing non string value
101+
print(snake_caseless_attr_dict[2]) # Output: two
53102

54-
upper_caseless_dict["WarninG"] = 30
55-
print(upper_caseless_dict) # {'WARNING': 30, 'DEBUG': 10}
56103
```
57104

58-
## Simple TitleCaselessDict Example
105+
### SnakeCaselessAttrDict with Key as Str Only
59106

60107
```python
61-
from caseless_dictionary import TitleCaselessDict
62-
from typing import Iterable
63-
64-
iterable: Iterable = {" Brave ": 1, " ProtonMail ": 2}
65-
title_caseless_dict: dict = TitleCaselessDict(iterable)
66-
print(title_caseless_dict) # {'Brave': 1, 'Protonmail': 2}
108+
from caseless_dictionary import SnakeCaselessAttrDict
67109

68-
title_caseless_dict.pop(" protonMAIL ")
110+
# Create a SnakeCaselessAttrDict with key_is_str_only set to True
111+
SnakeCaselessAttrDict.key_is_str_only = True
112+
snake_caseless_attr_dict = SnakeCaselessAttrDict({" HeLLO WoRLD ": 1})
69113

70-
print(title_caseless_dict) # {'Brave': 1}
114+
# Attempt to set a non-string key
115+
try:
116+
snake_caseless_attr_dict[1] = 2
117+
except TypeError:
118+
print("TypeError raised as expected when key_is_str_only is True")
71119
```
72120

73121
## Acknowledgments

caseless_dictionary/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
from caseless_dictionary.caseless_dictionary import (
1+
from caseless_dictionary.caseless_attribute_dict import (
2+
CaselessAttrDict,
3+
SnakeCaselessAttrDict,
4+
ConstantCaselessAttrDict,
5+
)
6+
from caseless_dictionary.caseless_dict import (
27
CaselessDict,
38
UpperCaselessDict,
49
TitleCaselessDict,
10+
SnakeCaselessDict,
11+
KebabCaselessDict,
12+
ConstantCaselessDict,
513
)
614

715
__all__ = (
816
CaselessDict.__name__,
917
UpperCaselessDict.__name__,
1018
TitleCaselessDict.__name__,
19+
SnakeCaselessDict.__name__,
20+
KebabCaselessDict.__name__,
21+
ConstantCaselessDict.__name__,
22+
CaselessAttrDict.__name__,
23+
SnakeCaselessAttrDict.__name__,
24+
ConstantCaselessAttrDict.__name__,
1125
)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
This module contains classes that implement case-insensitive attribute
3+
dictionaries.
4+
5+
Classes:
6+
- CaselessAttrDict: A case-insensitive AttrDict where keys that are
7+
strings are in snake case.
8+
- SnakeCaselessAttrDict: A case-insensitive AttrDict where keys that are
9+
strings are in snake case.
10+
- ConstantCaselessAttrDict: A case-insensitive AttrDict where keys that
11+
are strings are in constant case.
12+
13+
Each class inherits from ModifiableItemsAttrDict and overrides the
14+
_key_modifiers attribute to provide different case handling.
15+
"""
16+
from modifiable_items_dictionary.modifiable_items_attribute_dictionary import (
17+
ModifiableItemsAttrDict,
18+
)
19+
from modifiable_items_dictionary.modifiable_items_dictionary import Key, Value
20+
21+
from caseless_dictionary.cases import (
22+
snake_case,
23+
constant_case,
24+
)
25+
26+
27+
class CaselessAttrDict(ModifiableItemsAttrDict):
28+
"""
29+
Case-insensitive AttrDict where keys that are strings are in snake case.
30+
If key_is_str_only is set to True, keys must be of type str.
31+
32+
CaselessAttrDict() -> new empty caseless attribute dictionary
33+
CaselessAttrDict(mapping) -> new caseless attribute dictionary initialized
34+
from a mapping object's (key, value) pairs
35+
CaselessAttrDict(iterable) -> new caseless attribute dictionary initialized
36+
as if via:
37+
d = CaselessAttrDict()
38+
for k, v in iterable:
39+
d[k] = v
40+
CaselessAttrDict(**kwargs) -> new caseless attribute dictionary initialized
41+
with the name=value pairs in the keyword argument list.
42+
For example: CaselessAttrDict(one=1, two=2)
43+
44+
Example:
45+
>>> normal_dict: dict = {" sOmE WoRD ":1}
46+
>>> caseless_attr_dict = CaselessAttrDict(normal_dict)
47+
>>> caseless_attr_dict
48+
{'some_word': 1}
49+
>>> caseless_attr_dict["soME_WorD"]
50+
1
51+
>>> caseless_attr_dict.sOme_worD
52+
1
53+
"""
54+
55+
__slots__ = ()
56+
_key_modifiers = [snake_case]
57+
key_is_str_only = False
58+
59+
def __missing__(self, key: Key) -> None:
60+
"""Handle missing __key.
61+
Args:
62+
key: The Hashable __key that is missing.
63+
64+
Raises:
65+
*KeyError* with a more descriptive error for caseless keys.
66+
"""
67+
error = KeyError('Missing key of some case variant of ', key)
68+
69+
raise error
70+
71+
def __setitem__(self, key: Key, value: Value) -> None:
72+
"""Set the value of the key in the dictionary.
73+
Args:
74+
key: The Hashable key that will be set.
75+
value: The value that will be set for the key.
76+
77+
Raises:
78+
TypeError: If key_is_str_only is True and key is not a str.
79+
"""
80+
if self.key_is_str_only and not isinstance(key, str):
81+
raise TypeError('Key must be a str, not ', type(key).__name__)
82+
ModifiableItemsAttrDict.__setitem__(self, key, value)
83+
84+
85+
class SnakeCaselessAttrDict(CaselessAttrDict):
86+
"""
87+
Case-insensitive AttrDict where keys that are strings are in snake case.
88+
If key_is_str_only is set to True, keys must be of type str.
89+
90+
SnakeCaselessAttrDict() -> new empty snake caseless attribute dictionary
91+
SnakeCaselessAttrDict(mapping) -> new snake caseless attribute dictionary
92+
initialized from a mapping object's (key, value) pairs
93+
SnakeCaselessAttrDict(iterable) -> new snake caseless attribute dictionary
94+
initialized as if via:
95+
d = SnakeCaselessAttrDict()
96+
for k, v in iterable:
97+
d[k] = v
98+
SnakeCaselessAttrDict(**kwargs) -> new snake caseless attribute dictionary
99+
initialized with the name=value pairs in the keyword argument list.
100+
For example: SnakeCaselessAttrDict(one=1, two=2)
101+
102+
Example:
103+
>>> normal_dict: dict = {" sOmE WoRD ":1}
104+
>>> snake_caseless_attr_dict = SnakeCaselessAttrDict(normal_dict)
105+
>>> snake_caseless_attr_dict
106+
{'some_word': 1}
107+
>>> snake_caseless_attr_dict["soME_WorD"]
108+
1
109+
>>> snake_caseless_attr_dict.sOme_worD
110+
1
111+
"""
112+
113+
__slots__ = ()
114+
115+
116+
class ConstantCaselessAttrDict(CaselessAttrDict):
117+
"""
118+
Case-insensitive AttrDict where keys that are strings are in constant case.
119+
If key_is_str_only is set to True, keys must be of type str.
120+
121+
ConstantCaselessAttrDict() -> new empty constant caseless attribute dict
122+
ConstantCaselessAttrDict(mapping) -> new constant caseless attribute dict
123+
initialized from a mapping object's (key, value) pairs
124+
ConstantCaselessAttrDict(iterable) -> new constant caseless attribute dict
125+
initialized as if via:
126+
d = ConstantCaselessAttrDict()
127+
for k, v in iterable:
128+
d[k] = v
129+
ConstantCaselessAttrDict(**kwargs) -> new constant caseless attribute dict
130+
initialized with the name=value pairs in the keyword argument list.
131+
For example: ConstantCaselessAttrDict(one=1, two=2)
132+
133+
Example:
134+
>>> normal_dict: dict = {" sOmE WoRD ":1}
135+
>>> constant_caseless_attr_dict = ConstantCaselessAttrDict(normal_dict)
136+
>>> constant_caseless_attr_dict
137+
{'SOME_WORD': 1}
138+
>>> constant_caseless_attr_dict["soME_WorD"]
139+
1
140+
>>> constant_caseless_attr_dict.sOme_worD
141+
1
142+
"""
143+
144+
__slots__ = ()
145+
_key_modifiers = [constant_case]

0 commit comments

Comments
 (0)