Skip to content

Commit

Permalink
Add caseless attr dict (#9)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
tybruno authored Jun 3, 2024
1 parent 585f453 commit aae0b27
Show file tree
Hide file tree
Showing 15 changed files with 1,568 additions and 324 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pytest-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ jobs:
python -m pip install -r requirements-test.txt
- name: Test
run: |
pytest --doctest-modules
pytest tests/ --doctest-modules
106 changes: 77 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity)
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-0000ff.svg)](https://github.com/psf/blue)
[![License: MIT](https://img.shields.io/badge/License-MIT-blueviolet.svg)](https://opensource.org/licenses/MIT)
[![codecov](https://codecov.io/gh/tybruno/caseless-dictionary/branch/main/graph/badge.svg?token=ZO94EJFI3G)](https://codecov.io/gh/tybruno/caseless-dictionary)

# caseless-dictionary

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

#### Key Features:

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

`pip install caseless-dictionary`

## Simple CaselessDict Example
## Caseless Dictionaries

| Class Name | Description | Example |
|----------------------|----------------------------------------------------------------|------------------------------------------------------------------------------|
| CaselessDict | A dictionary where keys that are strings are case-folded. | `CaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello world': 1}` |
| CaseFoldCaselessDict | A dictionary where keys that are strings are case-folded. | `CaseFoldCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello world': 1}` |
| LowerCaselessDict | A dictionary where keys that are strings are in lower case. | `LowerCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello world': 1}` |
| UpperCaselessDict | A dictionary where keys that are strings are in upper case. | `UpperCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'HELLO WORLD': 1}` |
| TitleCaselessDict | A dictionary where keys that are strings are in title case. | `TitleCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'Hello World': 1}` |
| SnakeCaselessDict | A dictionary where keys that are strings are in snake case. | `SnakeCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello_world': 1}` |
| KebabCaselessDict | A dictionary where keys that are strings are in kebab case. | `KebabCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'hello-world': 1}` |
| ConstantCaselessDict | A dictionary where keys that are strings are in constant case. | `ConstantCaselessDict({" HeLLO WoRLD ": 1}) # Output: {'HELLO_WORLD': 1}` |
## Caseless Attribute Dictionaries

| Class Name | Description | Example |
|--------------------------|-------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| 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` |
| 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` |

### Basic CaselessDict Example

```python
from caseless_dictionary import CaselessDict

normal_dict: dict = {" CamelCase ": 1, " UPPER ": "TWO", 3: " Number as Key "}
# Create a CaselessDict
caseless_dict = CaselessDict({" HeLLO WoRLD ": 1, 2: "two"})

print(caseless_dict) # Output: {'hello world': 1, 2: 'two'}

# Accessing the value using different cases
print(caseless_dict[" hello world "]) # Output: 1
print(caseless_dict[" HELLO WORLD "]) # Output: 1

# Accessing non string value
print(caseless_dict[2]) # Output: two
```

caseless_dict: CaselessDict = CaselessDict(normal_dict)
### Caseless Dictionary with Key as Str Only

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

print("CamelCase" in caseless_dict) # True
print("camelcase" in caseless_dict) # True
# Create a CaselessDict with key_is_str_only set to True
CaselessDict.key_is_str_only = True
caseless_dict = CaselessDict({" HeLLO WoRLD ": 1})

print(caseless_dict[" camelCASE "]) # 1
# Attempt to set a non-string key
try:
caseless_dict[1] = 2
except TypeError:
print("TypeError raised as expected when key_is_str_only is True")
```

## Simple UpperCaselessDict Example

### Basic SnakeCaselessAttrDict Example

```python
from caseless_dictionary import UpperCaselessDict
from typing import Iterable
from caseless_dictionary import SnakeCaselessAttrDict

iterable: Iterable = [(" wArNIng", 0), ("deBug ", 10)]
upper_caseless_dict: dict = UpperCaselessDict(iterable)
print(upper_caseless_dict) # {'WARNING': 0, 'DEBUG': 10}
# Create a SnakeCaselessAttrDict
snake_caseless_attr_dict = SnakeCaselessAttrDict({" HeLLO WoRLD ": 1, 2: "two"})
print(snake_caseless_attr_dict) # Output: {'hello_world': 1, 2: 'two'}

print("warning" in upper_caseless_dict) # True
# Accessing the value using attribute notation
print(snake_caseless_attr_dict.hello_world) # Output: 1
print(snake_caseless_attr_dict.HELLO_WORLD) # Output: 1

# Accessing the value using Keys
print(snake_caseless_attr_dict[" hello_world "]) # Output: 1
print(snake_caseless_attr_dict[" HELLO WORLD "]) # Output: 1

# Accessing non string value
print(snake_caseless_attr_dict[2]) # Output: two

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

## Simple TitleCaselessDict Example
### SnakeCaselessAttrDict with Key as Str Only

```python
from caseless_dictionary import TitleCaselessDict
from typing import Iterable

iterable: Iterable = {" Brave ": 1, " ProtonMail ": 2}
title_caseless_dict: dict = TitleCaselessDict(iterable)
print(title_caseless_dict) # {'Brave': 1, 'Protonmail': 2}
from caseless_dictionary import SnakeCaselessAttrDict

title_caseless_dict.pop(" protonMAIL ")
# Create a SnakeCaselessAttrDict with key_is_str_only set to True
SnakeCaselessAttrDict.key_is_str_only = True
snake_caseless_attr_dict = SnakeCaselessAttrDict({" HeLLO WoRLD ": 1})

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

## Acknowledgments
Expand Down
16 changes: 15 additions & 1 deletion caseless_dictionary/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
from caseless_dictionary.caseless_dictionary import (
from caseless_dictionary.caseless_attribute_dict import (
CaselessAttrDict,
SnakeCaselessAttrDict,
ConstantCaselessAttrDict,
)
from caseless_dictionary.caseless_dict import (
CaselessDict,
UpperCaselessDict,
TitleCaselessDict,
SnakeCaselessDict,
KebabCaselessDict,
ConstantCaselessDict,
)

__all__ = (
CaselessDict.__name__,
UpperCaselessDict.__name__,
TitleCaselessDict.__name__,
SnakeCaselessDict.__name__,
KebabCaselessDict.__name__,
ConstantCaselessDict.__name__,
CaselessAttrDict.__name__,
SnakeCaselessAttrDict.__name__,
ConstantCaselessAttrDict.__name__,
)
145 changes: 145 additions & 0 deletions caseless_dictionary/caseless_attribute_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
This module contains classes that implement case-insensitive attribute
dictionaries.
Classes:
- CaselessAttrDict: A case-insensitive AttrDict where keys that are
strings are in snake case.
- SnakeCaselessAttrDict: A case-insensitive AttrDict where keys that are
strings are in snake case.
- ConstantCaselessAttrDict: A case-insensitive AttrDict where keys that
are strings are in constant case.
Each class inherits from ModifiableItemsAttrDict and overrides the
_key_modifiers attribute to provide different case handling.
"""
from modifiable_items_dictionary.modifiable_items_attribute_dictionary import (
ModifiableItemsAttrDict,
)
from modifiable_items_dictionary.modifiable_items_dictionary import Key, Value

from caseless_dictionary.cases import (
snake_case,
constant_case,
)


class CaselessAttrDict(ModifiableItemsAttrDict):
"""
Case-insensitive AttrDict where keys that are strings are in snake case.
If key_is_str_only is set to True, keys must be of type str.
CaselessAttrDict() -> new empty caseless attribute dictionary
CaselessAttrDict(mapping) -> new caseless attribute dictionary initialized
from a mapping object's (key, value) pairs
CaselessAttrDict(iterable) -> new caseless attribute dictionary initialized
as if via:
d = CaselessAttrDict()
for k, v in iterable:
d[k] = v
CaselessAttrDict(**kwargs) -> new caseless attribute dictionary initialized
with the name=value pairs in the keyword argument list.
For example: CaselessAttrDict(one=1, two=2)
Example:
>>> normal_dict: dict = {" sOmE WoRD ":1}
>>> caseless_attr_dict = CaselessAttrDict(normal_dict)
>>> caseless_attr_dict
{'some_word': 1}
>>> caseless_attr_dict["soME_WorD"]
1
>>> caseless_attr_dict.sOme_worD
1
"""

__slots__ = ()
_key_modifiers = [snake_case]
key_is_str_only = False

def __missing__(self, key: Key) -> None:
"""Handle missing __key.
Args:
key: The Hashable __key that is missing.
Raises:
*KeyError* with a more descriptive error for caseless keys.
"""
error = KeyError('Missing key of some case variant of ', key)

raise error

def __setitem__(self, key: Key, value: Value) -> None:
"""Set the value of the key in the dictionary.
Args:
key: The Hashable key that will be set.
value: The value that will be set for the key.
Raises:
TypeError: If key_is_str_only is True and key is not a str.
"""
if self.key_is_str_only and not isinstance(key, str):
raise TypeError('Key must be a str, not ', type(key).__name__)
ModifiableItemsAttrDict.__setitem__(self, key, value)


class SnakeCaselessAttrDict(CaselessAttrDict):
"""
Case-insensitive AttrDict where keys that are strings are in snake case.
If key_is_str_only is set to True, keys must be of type str.
SnakeCaselessAttrDict() -> new empty snake caseless attribute dictionary
SnakeCaselessAttrDict(mapping) -> new snake caseless attribute dictionary
initialized from a mapping object's (key, value) pairs
SnakeCaselessAttrDict(iterable) -> new snake caseless attribute dictionary
initialized as if via:
d = SnakeCaselessAttrDict()
for k, v in iterable:
d[k] = v
SnakeCaselessAttrDict(**kwargs) -> new snake caseless attribute dictionary
initialized with the name=value pairs in the keyword argument list.
For example: SnakeCaselessAttrDict(one=1, two=2)
Example:
>>> normal_dict: dict = {" sOmE WoRD ":1}
>>> snake_caseless_attr_dict = SnakeCaselessAttrDict(normal_dict)
>>> snake_caseless_attr_dict
{'some_word': 1}
>>> snake_caseless_attr_dict["soME_WorD"]
1
>>> snake_caseless_attr_dict.sOme_worD
1
"""

__slots__ = ()


class ConstantCaselessAttrDict(CaselessAttrDict):
"""
Case-insensitive AttrDict where keys that are strings are in constant case.
If key_is_str_only is set to True, keys must be of type str.
ConstantCaselessAttrDict() -> new empty constant caseless attribute dict
ConstantCaselessAttrDict(mapping) -> new constant caseless attribute dict
initialized from a mapping object's (key, value) pairs
ConstantCaselessAttrDict(iterable) -> new constant caseless attribute dict
initialized as if via:
d = ConstantCaselessAttrDict()
for k, v in iterable:
d[k] = v
ConstantCaselessAttrDict(**kwargs) -> new constant caseless attribute dict
initialized with the name=value pairs in the keyword argument list.
For example: ConstantCaselessAttrDict(one=1, two=2)
Example:
>>> normal_dict: dict = {" sOmE WoRD ":1}
>>> constant_caseless_attr_dict = ConstantCaselessAttrDict(normal_dict)
>>> constant_caseless_attr_dict
{'SOME_WORD': 1}
>>> constant_caseless_attr_dict["soME_WorD"]
1
>>> constant_caseless_attr_dict.sOme_worD
1
"""

__slots__ = ()
_key_modifiers = [constant_case]
Loading

0 comments on commit aae0b27

Please sign in to comment.