diff --git a/.github/workflows/pytest-workflow.yml b/.github/workflows/pytest-workflow.yml index c934d6f..226383f 100644 --- a/.github/workflows/pytest-workflow.yml +++ b/.github/workflows/pytest-workflow.yml @@ -26,4 +26,4 @@ jobs: python -m pip install -r requirements-test.txt - name: Test run: | - pytest --doctest-modules + pytest tests/ --doctest-modules diff --git a/README.md b/README.md index 83e859e..4cbd698 100644 --- a/README.md +++ b/README.md @@ -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!!!**: @@ -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 diff --git a/caseless_dictionary/__init__.py b/caseless_dictionary/__init__.py index 1ad4126..4587d1a 100644 --- a/caseless_dictionary/__init__.py +++ b/caseless_dictionary/__init__.py @@ -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__, ) diff --git a/caseless_dictionary/caseless_attribute_dict.py b/caseless_dictionary/caseless_attribute_dict.py new file mode 100644 index 0000000..9c06338 --- /dev/null +++ b/caseless_dictionary/caseless_attribute_dict.py @@ -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] diff --git a/caseless_dictionary/caseless_dict.py b/caseless_dictionary/caseless_dict.py new file mode 100644 index 0000000..109503e --- /dev/null +++ b/caseless_dictionary/caseless_dict.py @@ -0,0 +1,293 @@ +""" +Caseless Dictionary and related objects. + +Objects provided by this module: + `CaselessDict` - Keys are case-folded case. + `TitleCaselessDict` - Keys are in title case. + `UpperCaselessDict` - Keys are in upper case. + `SnakeCaselessDict` - Keys are in snake case. + `KebabCaselessDict` - Keys are in kebab case. + `ConstantCaselessDict` - Keys are in constant case. +""" +from modifiable_items_dictionary.modifiable_items_dictionary import ( + ModifiableItemsDict, + Key, + Value, +) + +from caseless_dictionary.cases import ( + case_fold, + upper, + lower, + title, + snake_case, + kebab_case, + constant_case, +) + + +class CaselessDict(ModifiableItemsDict): + """ + Case-insensitive Dictionary class where the keys that are strings are + casefolded. If key_is_str_only is set to True, keys must be of type str. + + CaselessDict() -> new empty caseless dictionary + CaselessDict(mapping) -> new caseless dictionary initialized from a + mapping object's (key, value) pairs + CaselessDict(iterable) -> new caseless dictionary initialized as if via: + d = CaselessDict() + for k, v in iterable: + d[k] = v + CaselessDict(**kwargs) -> new caseless dictionary initialized with + the name=value pairs in the keyword argument list. + For example: CaselessDict(one=1, two=2) + + + Example: + >>> normal_dict: dict = {" sOmE WoRD ": 1} + >>> caseless_dict = CaselessDict(normal_dict) + >>> caseless_dict + {'some word': 1} + >>> caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [case_fold] + 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__) + + ModifiableItemsDict.__setitem__(self, key, value) + + +class CaseFoldCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + case-folded. If key_is_str_only is True, keys must be str. + + CaseFoldCaselessDict() -> new empty case-folded caseless dictionary + CaseFoldCaselessDict(mapping) -> new case-folded caseless dictionary + initialized from a mapping object's (key, value) pairs + CaseFoldCaselessDict(iterable) -> new case-folded caseless dictionary + initialized as if via: + d = CaseFoldCaselessDict() + for k, v in iterable: + d[k] = v + CaseFoldCaselessDict(**kwargs) -> new case-folded caseless dictionary + initialized with the name=value pairs in the keyword argument list. + For example: CaseFoldCaselessDict(one=1, two=2) + + Example: + >>> normal_dict: dict = {" sOmE WoRD ": 1} + >>> case_fold_caseless_dict = CaseFoldCaselessDict(normal_dict) + >>> case_fold_caseless_dict + {'some word': 1} + >>> case_fold_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + + +class LowerCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + in lower case. If key_is_str_only is True, keys must be str. + + LowerCaselessDict() -> new empty lower caseless dictionary + LowerCaselessDict(mapping) -> new lower caseless dictionary + initialized from a mapping object's (key, value) pairs + LowerCaselessDict(iterable) -> new lower caseless dictionary + initialized as if via: + d = LowerCaselessDict() + for k, v in iterable: + d[k] = v + LowerCaselessDict(**kwargs) -> new lower caseless dictionary + initialized with the name=value pairs in the keyword argument list. + For example: LowerCaselessDict(one=1, two=2) + + Example: + >>> normal_dict: dict = {" sOmE WoRD ": 1} + >>> lower_caseless_dict = LowerCaselessDict(normal_dict) + >>> lower_caseless_dict + {'some word': 1} + >>> lower_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [lower] + + +class UpperCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + in upper case. If key_is_str_only is True, keys must be str. + + UpperCaselessDict() -> new empty upper caseless dictionary + UpperCaselessDict(mapping) -> new upper caseless dictionary initialized + from a mapping object's (key, value) pairs + UpperCaselessDict(iterable) -> new upper caseless dictionary initialized + as if via: + d = UpperCaselessDict() + for k, v in iterable: + d[k] = v + UpperCaselessDict(**kwargs) -> new upper caseless dictionary initialized + with the name=value pairs in the keyword argument list. + For example: UpperCaselessDict(one=1, two=2) + + + Example: + >>> normal_dict: dict = {" sOmE WoRD ": 1} + >>> upper_caseless_dict = UpperCaselessDict(normal_dict) + >>> upper_caseless_dict + {'SOME WORD': 1} + >>> upper_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [upper] + + +class TitleCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + in Title Case. If key_is_str_only is True, keys must be str. + + TitleCaselessDict() -> new empty title caseless dictionary + TitleCaselessDict(mapping) -> new title caseless dictionary initialized + from a mapping object's (key, value) pairs. + TitleCaselessDict(iterable) -> new title caseless dictionary initialized + as if via: + d = TitleCaselessDict() + for k, v in iterable: + d[k] = v + TitleCaselessDict(**kwargs) -> new title caseless dictionary initialized + with the name=value pairs in the keyword argument list. + For example: TitleCaselessDict(one=1, two=2) + + Example: + >>> normal_dict: dict = {" sOmE WoRD ": 1} + >>> title_caseless_dict = TitleCaselessDict(normal_dict) + >>> title_caseless_dict + {'Some Word': 1} + >>> title_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [title] + + +class SnakeCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + in Snake Case. If key_is_str_only is True, keys must be str. + + SnakeCaselessDict() -> new empty snake caseless dictionary + SnakeCaselessDict(mapping) -> new snake caseless dictionary initialized + from a mapping object's (key, value) pairs + SnakeCaselessDict(iterable) -> new snake caseless dictionary initialized + as if via: + d = SnakeCaselessDict() + for k, v in iterable: + d[k] = v + SnakeCaselessDict(**kwargs) -> new snake caseless dictionary initialized + with the name=value pairs in the keyword argument list. + For example: SnakeCaselessDict(one=1, two=2) + + Example: + >>> normal_dict = {" SomE wORd ": 1} + >>> snake_caseless_dict = SnakeCaselessDict(normal_dict) + >>> snake_caseless_dict + {'some_word': 1} + >>> snake_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [snake_case] + + +class KebabCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + in Kebab Case. If key_is_str_only is True, keys must be str. + + KebabCaselessDict() -> new empty kebab caseless dictionary + KebabCaselessDict(mapping) -> new kebab caseless dictionary initialized + from a mapping object's (key, value) pairs + KebabCaselessDict(iterable) -> new kebab caseless dictionary initialized + as if via: + d = KebabCaselessDict() + for k, v in iterable: + d[k] = v + KebabCaselessDict(**kwargs) -> new kebab caseless dictionary initialized + with the name=value pairs in the keyword argument list. + For example: KebabCaselessDict(one=1, two=2) + + Example: + >>> normal_dict: dict = {" SomE wORd ": 1} + >>> kebab_caseless_dict = KebabCaselessDict(normal_dict) + >>> kebab_caseless_dict + {'some-word': 1} + >>> kebab_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [kebab_case] + + +class ConstantCaselessDict(CaselessDict): + """ + Case-insensitive Dictionary class where keys that are strings are + in Constant Case. If key_is_str_only is True, keys must be str. + + ConstantCaselessDict() -> new empty constant caseless dictionary + ConstantCaselessDict(mapping) -> new constant caseless dictionary + initialized from a mapping object's (key, value) pairs + ConstantCaselessDict(iterable) -> new constant caseless dictionary + initialized as if via: + d = ConstantCaselessDict() + for k, v in iterable: + d[k] = v + ConstantCaselessDict(**kwargs) -> new constant caseless dictionary + initialized with the name=value pairs in the keyword argument list. + For example: ConstantCaselessDict(one=1, two=2) + + Example: + >>> normal_dict: dict = {" SomE wORd ": 1} + >>> constant_caseless_dict = ConstantCaselessDict(normal_dict) + >>> constant_caseless_dict + {'SOME_WORD': 1} + >>> constant_caseless_dict[" SomE Word "] + 1 + """ + + __slots__ = () + _key_modifiers = [constant_case] diff --git a/caseless_dictionary/caseless_dictionary.py b/caseless_dictionary/caseless_dictionary.py deleted file mode 100644 index 937d942..0000000 --- a/caseless_dictionary/caseless_dictionary.py +++ /dev/null @@ -1,167 +0,0 @@ -""" Caseless Dictionary and related objects. - -Objects provided by this module: - `CaselessDict` - Keys are case-folded case. - `TitleCaselessDict` - Keys are in title case. - `UpperCaselessDict` - Keys are in upper case. - -""" -import typing - -import modifiable_items_dictionary - - -def _case_fold(value: typing.Any): - """strip then casefold a *str* - - Example: - >>> _case_fold(" CamelCase ") - 'camelcase' - >>> _case_fold(2) # Not a *str* - 2 - - Args: - value: If an instance of a string strip then casefold the *v* - - Returns: - The stripped and casefolded *str*. If not an instance of *str* return the v unchanged. - - """ - if isinstance(value, str): - _stripped_value: str = value.strip() - _value_case_folded: str = _stripped_value.casefold() - return _value_case_folded - return value - - -def _upper(value: typing.Any): - """strip the string then convert to uppercase. - - Example: - >>> _upper(" CamelCase ") - 'CAMELCASE' - >>> _upper(["not of type *str*"]) # Not a *str* - ['not of type *str*'] - - Args: - value: If an instance of a string strip then convert the *v* to an uppercase *str* - - Returns: - The stripped and uppercased *str*. If not an instance of *str* return the v unchanged. - """ - if isinstance(value, str): - _stripped_value: str = value.strip() - _value_upper: str = _stripped_value.upper() - return _value_upper - return value - - -def _title(value: typing.Any): - """strip the string then convert to title. - - Example: - >>> _title(" lower UPPER CamelCase ") - 'Lower Upper Camelcase' - >>> _title(["not of type *str*"]) # Not a *str* - ['not of type *str*'] - - Args: - value: If an instance of a string strip then convert the *v* to an uppercase *str* - - Returns: - The stripped and uppercased *str*. If not an instance of *str* return the v unchanged. - """ - if isinstance(value, str): - _stripped_value: str = value.strip() - _value_title: str = _stripped_value.title() - return _value_title - return value - - -class CaselessDict(modifiable_items_dictionary.ModifiableItemsDict): - """Case-insensitive Dictionary class where the keys thar are strings are casefolded. - - CaselessDict() -> new empty caseless dictionary - CaselessDict(mapping) -> new caseless dictionary initialized from a mapping object's - (__key, v) pairs - CaselessDict(__m) -> new caseless dictionary initialized as if via: - d = CaselessDict() - for k, v in __m: - d[k] = v - CaselessDict(**kwargs) -> new caseless dictionary initialized with the name=v pairs - in the keyword argument list. For example: CaselessDict(one=1, two=2) - - Example: - >>> normal_dict: dict = {" lower ": 1, "UPPER ": 2, "CamelCase": 3} - >>> caseless_dict = CaselessDict(normal_dict) - >>> caseless_dict - {'lower': 1, 'upper': 2, 'camelcase': 3} - >>> caseless_dict[" UPpeR "] - 2 - """ - - _key_modifiers = staticmethod(_case_fold) - - def __missing__(self, key: typing.Hashable) -> None: - """Handle missing __key. - Args: - key: The Hashable __key that is missing. - - Raises: - *KeyError* with a more descriptive error for caseless keys. - """ - error: KeyError = KeyError( - "Missing key of some case variant of ", key - ) - - raise error - - -class UpperCaselessDict(CaselessDict): - """Case-insensitive Dictionary class where the keys that are strings are in upper case. - - UpperCaselessDict() -> new empty upper caseless dictionary - UpperCaselessDict(mapping) -> new upper caseless dictionary initialized from a mapping object's - (__key, v) pairs - UpperCaselessDict(__m) -> new upper caseless dictionary initialized as if via: - d = UpperCaselessDict() - for k, v in __m: - d[k] = v - UpperCaselessDict(**kwargs) -> new upper caseless dictionary initialized with the name=v pairs - in the keyword argument list. For example: UpperCaselessDict(one=1, two=2) - - Example: - >>> normal_dict: dict = {" lower ": 1, "UPPER ": 2, "CamelCase": 3} - >>> upper_caseless_dict = UpperCaselessDict(normal_dict) - >>> upper_caseless_dict - {'LOWER': 1, 'UPPER': 2, 'CAMELCASE': 3} - >>> "CAmelCase " in upper_caseless_dict - True - """ - - _key_modifiers = staticmethod(_upper) - - -class TitleCaselessDict(CaselessDict): - """Case-insensitive Dictionary class where the keys that are strings are in Title Case. - - TitleCaselessDict() -> new empty title caseless dictionary - TitleCaselessDict(mapping) -> new title caseless dictionary initialized from a mapping object's - (__key, v) pairs - TitleCaselessDict(__m) -> new title caseless dictionary initialized as if via: - d = TitleCaselessDict() - for k, v in __m: - d[k] = v - TitleCaselessDict(**kwargs) -> new title caseless dictionary initialized with the name=v pairs - in the keyword argument list. For example: TitleCaselessDict(one=1, two=2) - - Example: - >>> normal_dict: dict = {" lower ": 1, "UPPER ": 2, "CamelCase": 3} - >>> upper_caseless_dict = TitleCaselessDict(lower=1, UPPER=2, CamelCase=3) - >>> upper_caseless_dict - {'Lower': 1, 'Upper': 2, 'Camelcase': 3} - >>> " lOwEr " in upper_caseless_dict - True - """ - - _key_modifiers = staticmethod(_title) diff --git a/caseless_dictionary/cases.py b/caseless_dictionary/cases.py new file mode 100644 index 0000000..7679318 --- /dev/null +++ b/caseless_dictionary/cases.py @@ -0,0 +1,198 @@ +""" +This module contains a collection of functions that modify the case of a string. + +Each function takes an input value and, if the value is a string, it strips +the string and then modifies the case. If the input value is not a string, it +returns the value unchanged. + +Functions: + case_fold(value: Any) -> Any: + Strips the string and then applies case folding. + + upper(value: Any) -> Any: + Strips the string and then converts it to uppercase. + + lower(value: Any) -> Any: + Strips the string and then converts it to lowercase. + + title(value: Any) -> Any: + Strips the string and then converts it to title case. + + snake_case(value: Any) -> Any: + Strips the string and then converts it to snake case. + + kebab_case(value: Any) -> Any: + Strips the string and then converts it to kebab case. + + constant_case(value: Any) -> Any: + Strips the string and then converts it to constant case. +""" +from typing import Any + + +def case_fold(value: Any): + """strip then casefold a *str* + + Example: + >>> case_fold(" sOme WoRd ") + 'some word' + >>> case_fold(2) # Not a *str* + 2 + + Args: + value: If an instance of a string strip then casefold the *v* + + Returns: + The stripped and casefolded *str*. If not an instance of *str* return + the v unchanged. + + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_case_folded = _stripped_value.casefold() + return value_case_folded + return value + + +def upper(value: Any): + """strip the string then convert to uppercase. + + Example: + >>> upper(" sOme WoRd ") + 'SOME WORD' + >>> upper(["not of type *str*"]) # Not a *str* + ['not of type *str*'] + + Args: + value: If an instance of a string strip then convert the *v* to an + uppercase *str* + + Returns: + The stripped and uppercased *str*. If not an instance of *str* return + the v unchanged. + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_upper_case = _stripped_value.upper() + return value_upper_case + return value + + +def lower(value: Any): + """strip the string then convert to lowercase. + + Example: + >>> lower(" sOme WoRd ") + 'some word' + >>> lower(["not of type *str*"]) # Not a *str* + ['not of type *str*'] + + Args: + value: If an instance of a string strip then convert the *v* to a + lowercase *str* + + Returns: + The stripped and lowercased *str*. If not an instance of *str* return + the v unchanged. + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_lower_case = _stripped_value.lower() + return value_lower_case + return value + + +def title(value: Any): + """strip the string then convert to title. + + Example: + >>> title(" lower UPPER CamelCase ") + 'Lower Upper Camelcase' + >>> title(["not of type *str*"]) # Not a *str* + ['not of type *str*'] + + Args: + value: If an instance of a string strip then convert the *v* to an + uppercase *str* + + Returns: + The stripped and uppercased *str*. If not an instance of *str* return + the v unchanged. + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_title_case = _stripped_value.title() + return value_title_case + return value + + +def snake_case(value: Any): + """strip the string then convert to snake case. + + Example: + >>> snake_case(" sOme WoRd ") + 'some_word' + >>> snake_case(["not of type *str*"]) # Not a *str* + ['not of type *str*'] + + Args: + value: If an instance of a string strip then convert the *v* to a + snake case *str* + + Returns: + The stripped and snake cased *str*. If not an instance of *str* return + the v unchanged. + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_snake_case = _stripped_value.replace(' ', '_').casefold() + return value_snake_case + return value + + +def kebab_case(value: Any): + """strip the string then convert to kebab case. + + Example: + >>> kebab_case(" sOme WoRd ") + 'some-word' + >>> kebab_case(["not of type *str*"]) # Not a *str* + ['not of type *str*'] + + Args: + value: If an instance of a string strip then convert the *v* to a + kebab case *str* + + Returns: + The stripped and kebab cased *str*. If not an instance of *str* return + the v unchanged. + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_kebab_case = _stripped_value.replace(' ', '-').casefold() + return value_kebab_case + return value + + +def constant_case(value: Any): + """strip the string then convert to constant case. + + Example: + >>> constant_case(" sOme WoRd ") + 'SOME_WORD' + >>> constant_case(["not of type *str*"]) # Not a *str* + ['not of type *str*'] + + Args: + value: If an instance of a string strip then convert the *v* to a + constant case *str* + + Returns: + The stripped and constant cased *str*. If not an instance of *str* + return the v unchanged. + """ + if isinstance(value, str): + _stripped_value = value.strip() + value_constant_case = _stripped_value.replace(' ', '_').upper() + return value_constant_case + return value diff --git a/examples/caseless_attribute_dictionary_examples.py b/examples/caseless_attribute_dictionary_examples.py new file mode 100644 index 0000000..60b7437 --- /dev/null +++ b/examples/caseless_attribute_dictionary_examples.py @@ -0,0 +1,60 @@ +""" +This module contains examples of how to use the caseless attribute dictionary +classes. + +Each function creates a caseless attribute dictionary of a different type and +prints its contents. + +Functions: + example_caseless_attr_dict(): + Creates a SnakeCaselessAttrDict and prints its contents. + + example_constant_caseless_attr_dict(): + Creates a ConstantCaselessAttrDict and prints its contents. +""" +from caseless_dictionary.caseless_attribute_dict import ( + SnakeCaselessAttrDict, + ConstantCaselessAttrDict, +) + + +def example_caseless_attr_dict(): + """ + This function creates a SnakeCaselessAttrDict with 'Hello World' and + 'WORLD WIDE' as keys. It then prints the dictionary and the value of the + 'Hello World' key accessed using attribute notation. If a non-string key + is attempted to be set, a TypeError is raised. + """ + # Keys can only be strings + SnakeCaselessAttrDict.key_is_str_only = True + caseless_attr_dict = SnakeCaselessAttrDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print(caseless_attr_dict) # Output: {'hello world': 1, 'world wide': 2} + print(caseless_attr_dict.hEllo_wOrld) # Output: 1 + try: + caseless_attr_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') + + +def example_constant_caseless_attr_dict(): + """ + This function creates a ConstantCaselessAttrDict with 'Hello World' and + 'WORLD WIDE' as keys. It then prints the dictionary and the value of the + 'Hello World' key accessed using attribute notation. If a non-string key + is attempted to be set, a TypeError is raised. + """ + # Keys can only be strings + ConstantCaselessAttrDict.key_is_str_only = True + constant_caseless_attr_dict = ConstantCaselessAttrDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print( + constant_caseless_attr_dict + ) # Output: {'HELLO_WORLD': 1, 'WORLD_WIDE': 2} + print(constant_caseless_attr_dict.HELLO_WORLD) # Output: 1 + try: + constant_caseless_attr_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') diff --git a/examples/caseless_dictionary_examples.py b/examples/caseless_dictionary_examples.py new file mode 100644 index 0000000..986c456 --- /dev/null +++ b/examples/caseless_dictionary_examples.py @@ -0,0 +1,131 @@ +""" +This module contains examples of how to use the caseless dictionary classes. + +Each function creates a caseless dictionary of a different type and prints +its contents. + +Functions: + example_caseless_dict(): + Creates a CaselessDict and prints its contents. + + example_upper_caseless_dict(): + Creates an UpperCaselessDict and prints its contents. + + example_title_caseless_dict(): + Creates a TitleCaselessDict and prints its contents. + + example_snake_caseless_dict(): + Creates a SnakeCaselessDict and prints its contents. + + example_kebab_caseless_dict(): + Creates a KebabCaselessDict and prints its contents. + + example_constant_caseless_dict(): + Creates a ConstantCaselessDict and prints its contents. +""" +from caseless_dictionary.caseless_dict import ( + CaselessDict, + UpperCaselessDict, + TitleCaselessDict, + SnakeCaselessDict, + KebabCaselessDict, + ConstantCaselessDict, +) + + +def example_caseless_dict(): + """ + Create a CaselessDict and print its contents. The dictionary is created + with 'Hello World' and 'WORLD WIDE' as keys. + """ + CaselessDict.key_is_str_only = True # Keys can only be strings + caseless_dict = CaselessDict({' Hello World ': 1, ' WORLD WIDE ': 2}) + print(caseless_dict) # Output: {'hello world': 1, 'world wide': 2} + try: + caseless_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') + + +def example_upper_caseless_dict(): + """ + Create an UpperCaselessDict and print its contents. The dictionary is + created with 'Hello World' and 'WORLD WIDE' as keys. + """ + UpperCaselessDict.key_is_str_only = True # Keys can only be strings + upper_caseless_dict = UpperCaselessDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print(upper_caseless_dict) # Output: {'HELLO WORLD': 1, 'WORLD WIDE': 2} + try: + upper_caseless_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') + + +def example_title_caseless_dict(): + """ + Create a TitleCaselessDict and print its contents. The dictionary is + created with 'Hello World' and 'WORLD WIDE' as keys. + """ + TitleCaselessDict.key_is_str_only = True # Keys can only be strings + title_caseless_dict = TitleCaselessDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print(title_caseless_dict) # Output: {'Hello World': 1, 'World Wide': 2} + try: + title_caseless_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') + + +def example_snake_caseless_dict(): + """ + Create a SnakeCaselessDict and print its contents. The dictionary is + created with 'Hello World' and 'WORLD WIDE' as keys. + """ + SnakeCaselessDict.key_is_str_only = True # Keys can only be strings + snake_caseless_dict = SnakeCaselessDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print(snake_caseless_dict) # Output: {'hello_world': 1, 'world_wide': 2} + try: + snake_caseless_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') + + +def example_kebab_caseless_dict(): + """ + Create a KebabCaselessDict and print its contents. The dictionary is + created with 'Hello World' and 'WORLD WIDE' as keys. + """ + KebabCaselessDict.key_is_str_only = True # Keys can only be strings + kebab_caseless_attr_dict = KebabCaselessDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print( + kebab_caseless_attr_dict + ) # Output: {'hello-world': 1, 'world-wide': 2} + try: + kebab_caseless_attr_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') + + +def example_constant_caseless_dict(): + """ + Create a ConstantCaselessDict and print its contents. The dictionary is + created with 'Hello World' and 'WORLD WIDE' as keys. + """ + ConstantCaselessDict.key_is_str_only = True # Keys can only be strings + constant_caseless_attr_dict = ConstantCaselessDict( + {' Hello World ': 1, ' WORLD WIDE ': 2} + ) + print( + constant_caseless_attr_dict + ) # Output: {'HELLO_WORLD': 1, 'WORLD_WIDE': 2} + try: + constant_caseless_attr_dict[1] = 2 # Raises TypeError + except TypeError: + print('TypeError raised as expected when str_only is True') diff --git a/requirements-test.txt b/requirements-test.txt index cc9e866..dc39e58 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,3 +1,4 @@ # Test Requirements pytest -pytest-cov \ No newline at end of file +pytest-cov +blue \ No newline at end of file diff --git a/setup.py b/setup.py index 4252182..5c67649 100644 --- a/setup.py +++ b/setup.py @@ -4,39 +4,42 @@ setup, ) -__version__ = "v1.0.3" -__author__ = "Tyler Bruno" -DESCRIPTION = "Typed and Tested Case Insensitive Dictionary which was inspired by Raymond Hettinger" -INSTALL_REQUIRES = ("modifiable-items-dictionary >= 2.0.0",) +__version__ = 'v2.0.0' +__author__ = 'Tyler Bruno' +DESCRIPTION = ( + 'Typed and Tested Case Insensitive Dictionary which was ' + 'inspired by Raymond Hettinger' +) +INSTALL_REQUIRES = ('modifiable-items-dictionary >= 3.0.0',) -with open("README.md", "r", encoding="utf-8") as file: +with open('README.md', 'r', encoding='utf-8') as file: README = file.read() setup( - name="caseless-dictionary", + name='caseless-dictionary', version=__version__, author=__author__, description=DESCRIPTION, long_description=README, - long_description_content_type="text/markdown", - keywords="python dict dictionary", - url="https://github.com/tybruno/caseless-dictionary", - license="MIT", - package_data={"caseless-dictionary": ["py.typed"]}, + long_description_content_type='text/markdown', + keywords='python dict dictionary', + url='https://github.com/tybruno/caseless-dictionary', + license='MIT', + package_data={'caseless-dictionary': ['py.typed']}, packages=find_packages(), install_requires=INSTALL_REQUIRES, classifiers=[ - "License :: OSI Approved :: MIT License", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Software Development :: " "Libraries :: Python Modules", + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX :: Linux', + 'Operating System :: MacOS', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Software Development :: ' 'Libraries :: Python Modules', ], - python_requires=">=3.6", + python_requires='>=3.6', ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..eb00645 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,114 @@ +from typing import Mapping, Any, Callable, Hashable, NamedTuple, Type, Union + +import pytest + +from caseless_dictionary import SnakeCaselessAttrDict, \ + ConstantCaselessAttrDict, CaselessAttrDict, KebabCaselessDict, \ + CaselessDict, ConstantCaselessDict, UpperCaselessDict, SnakeCaselessDict, \ + TitleCaselessDict + + +class _TestingClass(NamedTuple): + cls: Union[ + Type[CaselessDict], + Type[CaselessAttrDict], + ] + key_modifier: Callable[[Any], Hashable] + + +def _case_fold(value: Any): + if isinstance(value, str): + value = value.strip().casefold() + return value + + +def _upper(value: Any): + if isinstance(value, str): + value = value.strip().upper() + return value + + +def _title(value: Any): + if isinstance(value, str): + value = value.strip().title() + return value + + +def _lower(value: Any): + if isinstance(value, str): + value = value.strip().lower() + return value + + +def _snake_case(value: Any): + if isinstance(value, str): + value = value.strip().replace(' ', '_').casefold() + return value + + +def _kebab_case(value: Any): + if isinstance(value, str): + value = value.strip().replace(' ', '-').casefold() + return value + + +def _constant_case(value: Any): + if isinstance(value, str): + value = value.strip().replace(' ', '_').upper() + return value + + +@pytest.fixture( + params=( + { + 'CamelCase': 1, + 'lower': 2, + 'UPPER': 3, + 'snake_case': 4, + 5.56: 'Five', + True: 'True', + }, + {1: 2, ('hello', 'Goodbye'): 2}, + {'kebab-case': 5, 'CONSTANT_CASE': 6, 'Title Case': 7}, + {False: 'False', 3.14: 'Pi', 'pascalCase': 8}, + ) +) +def valid_mapping(request) -> Mapping: + inputs: Mapping = request.param + return inputs + + +@pytest.fixture( + params=( + _TestingClass(CaselessDict, _case_fold), + _TestingClass(UpperCaselessDict, _upper), + _TestingClass(TitleCaselessDict, _title), + _TestingClass(SnakeCaselessDict, _snake_case), + _TestingClass(KebabCaselessDict, _kebab_case), + _TestingClass( + ConstantCaselessDict, _constant_case + ), + ) +) +def caseless_class(request) -> _TestingClass: + _caseless_class: _TestingClass = request.param + return _caseless_class + + +@pytest.fixture( + params=( + _TestingClass(SnakeCaselessAttrDict, _snake_case), + _TestingClass( + ConstantCaselessAttrDict, _constant_case + ), + ) +) +def caseless_attr_class(request) -> _TestingClass: + _caseless_attr_class: _TestingClass = request.param + return _caseless_attr_class + + +@pytest.fixture(params=(set(), list(), dict())) +def unhashable_type(request): + unhashable_type = request.param + return unhashable_type diff --git a/tests/test_caseless_attribute_dictionary.py b/tests/test_caseless_attribute_dictionary.py new file mode 100644 index 0000000..8fe9ac9 --- /dev/null +++ b/tests/test_caseless_attribute_dictionary.py @@ -0,0 +1,397 @@ +"""Tests for the CaselessAttributeDictionary class. + +This module contains tests for the CaselessAttributeDictionary class. + +Classes: + TestCaselessAttributeDictionary: Test case for the + CaselessAttributeDictionary class. +""" +import contextlib +from copy import deepcopy +from typing import Mapping + +import pytest + + +class TestCaselessAttributeDictionary: + """Test case for the CaselessAttributeDictionary class. + + This class contains test cases for the CaselessAttributeDictionary class. + + tests: + test__init__mapping: Test the __init__ method with a mapping. + test__init__kwargs: Test the __init__ method with keyword arguments. + test__init__iterable_and_kwargs: Test the __init__ method with an + iterable and keyword arguments. + test__init__iterable: Test the __init__ method with an iterable. + test__init__invalid_type: Test the __init__ method with an invalid + type. + test_fromkeys: Test the fromkeys method. + test_fromkeys_with_invalid_type: Test the fromkeys method with an + invalid type. + test__setitem__: Test the __setitem__ method. + test___setitem__bad_key_type: Test the __setitem__ method with an + invalid key type. + test__getitem__: Test the __getitem__ method. + test__getitem__missing_key: Test the __getitem__ method with a + missing key. + test__delitem__: Test the __delitem__ method. + test__delitem__missing_key: Test the __delitem__ method with a + missing key. + test_get: Test the get method. + test_get_missing_key: Test the get method with a missing key. + test_get_unhashable_key: Test the get method with an unhashable key. + test_pop: Test the pop method. + test_pop_missing_key: Test the pop method with a missing key. + test_pop_unhashable_type: Test the pop method with an unhashable type. + test_setdefault: Test the setdefault method. + test_setdefault_unhashable_type: Test the setdefault method with an + unhashable type. + test_update_using_mapping: Test the update method using a mapping. + test_update_using_sequence: Test the update method using a sequence. + + + """ + + def test__init__mapping( + self, valid_mapping: Mapping, caseless_attr_class + ): + _class, _key_operation = caseless_attr_class + caseless_attr_dict = _class(valid_mapping) + expected = { + _key_operation(key): value for key, value in valid_mapping.items() + } + assert caseless_attr_dict == expected + + @pytest.mark.parametrize( + 'valid_kwargs', + ({'a': 1, 'B': 2}, {'lower': 3, 'UPPER': 4, 'MiXeD': 5}), + ) + def test__init__kwargs(self, valid_kwargs, caseless_attr_class): + _class, _key_operation = caseless_attr_class + + caseless_attr_dict = _class(**valid_kwargs) + expected = { + _key_operation(key): value for key, value in valid_kwargs.items() + } + assert caseless_attr_dict == expected + + @pytest.mark.parametrize( + 'mapping_and_kwargs', + [ + ({'a': 1, 'B': 2}, {'lower': 3, 'UPPER': 4, 'MiXeD': 5}), + ], + ) + def test__init__iterable_and_kwargs( + self, mapping_and_kwargs, caseless_attr_class + ): + _class, _key_operation = caseless_attr_class + args, kwargs = mapping_and_kwargs + expected = { + _key_operation(key): value + for key, value in dict(**args, **kwargs).items() + } + caseless_attr_dict = _class(args, **kwargs) + assert caseless_attr_dict == expected + + @pytest.mark.parametrize( + 'iterables', + [ + zip(['one', 'TwO', 'ThrEE'], [1, 2, 3]), + [('TwO', 2), ('one', 1), ('ThrEE', 3), (4, 'FouR')], + ], + ) + def test__init__iterable(self, iterables, caseless_attr_class): + _class, _key_operation = caseless_attr_class + iterables_copy = deepcopy(iterables) + caseless_attr_dict = _class(iterables) + expected: Mapping = { + _key_operation(key): value for key, value in iterables_copy + } + assert caseless_attr_dict == expected + assert repr(caseless_attr_dict) == repr(expected) + + @pytest.mark.parametrize('invalid_type', ([1], {2}, True, 1)) + def test__init__invalid_type(self, invalid_type, caseless_attr_class): + _class, _ = caseless_attr_class + + with pytest.raises(TypeError): + _class(invalid_type) + + @pytest.mark.parametrize('iterable', [[('1', 1), ('two', 2, 2)]]) + def test__init__bad_iterable_elements(self, iterable, caseless_attr_class): + _class, _ = caseless_attr_class + + with pytest.raises(ValueError): + _class(iterable) + + @pytest.mark.parametrize( + 'keys', + (['lower', 'Title', 'UPPER', 'CamelCase', 'snake_case', object()],), + ) + def test_fromkeys(self, keys, caseless_attr_class): + _class, _key_operation = caseless_attr_class + + value = None + expected: Mapping = {_key_operation(key): value for key in keys} + + caseless_attr_dict = _class.fromkeys(keys, value) + assert caseless_attr_dict == expected + assert isinstance(caseless_attr_dict, _class) + + for key in keys: + assert key in caseless_attr_dict + + @pytest.mark.parametrize('invalid_type', (True, 1)) + def test_fromkeys_with_invalid_type( + self, invalid_type, caseless_attr_class + ): + _class, _ = caseless_attr_class + + with pytest.raises(TypeError): + _class.fromkeys(invalid_type) + + def test__setitem__(self, valid_mapping, caseless_attr_class): + _class, _key_operation = caseless_attr_class + + caseless_attr_dict: _class = _class() + expected: dict = dict() + for key, item in valid_mapping.items(): + expected[_key_operation(key)] = item + caseless_attr_dict[key] = item + assert caseless_attr_dict == expected + assert repr(caseless_attr_dict) == repr(expected) + + def test___setitem__bad_key_type( + self, caseless_attr_class, unhashable_type + ): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class() + with pytest.raises(TypeError): + caseless_attr_dict[unhashable_type] = 0 + + def test__getitem__(self, valid_mapping, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class(valid_mapping) + for key, value in valid_mapping.items(): + assert caseless_attr_dict[key] == value + + def test__getitem__missing_key(self, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class() + assert caseless_attr_dict == dict() + + # make unique __key which will not be in dict + _missing_key = object() + + with pytest.raises(KeyError): + _ = caseless_attr_dict[_missing_key] + + def test__delitem__(self, valid_mapping, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class(valid_mapping) + for key, value in valid_mapping.items(): + del caseless_attr_dict[key] + assert key not in caseless_attr_dict + + def test__delitem__missing_key(self, valid_mapping, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class(valid_mapping) + with contextlib.suppress(KeyError): + del caseless_attr_dict['missing_key'] + + def test_get(self, valid_mapping, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class(valid_mapping) + for key, value in valid_mapping.items(): + assert caseless_attr_dict.get(key) == value + assert caseless_attr_dict.get(key, None) == value + + def test_get_missing_key(self, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class() + + # make unique __key which will not be in dict + _missing_key = object() + _default = '__default v' + + assert caseless_attr_dict.get(_missing_key) is None + assert caseless_attr_dict.get(_missing_key, _default) == _default + + def test_get_unhashable_key(self, caseless_attr_class, unhashable_type): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class() + + _default = '__default v' + + with pytest.raises(TypeError): + caseless_attr_dict.get(unhashable_type) + caseless_attr_dict.get(unhashable_type, _default) + + def test_pop(self, valid_mapping, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class(valid_mapping) + for key, value in valid_mapping.items(): + assert caseless_attr_dict.pop(key) == value + assert key not in caseless_attr_dict + + def test_pop_missing_key(self, caseless_attr_class): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class() + + # make unique __key which will not be in dict + _missing_key = object() + + with pytest.raises(KeyError): + caseless_attr_dict.pop(_missing_key) + + def test_pop_unhashable_type(self, caseless_attr_class, unhashable_type): + _class, _ = caseless_attr_class + + caseless_attr_dict: _class = _class() + + with pytest.raises(KeyError): + caseless_attr_dict.pop(unhashable_type) + + def test_setdefault(self, valid_mapping, caseless_attr_class): + _class, _key_operation = caseless_attr_class + + caseless_attr_dict: _class = _class() + expected: dict = dict() + for key, item in valid_mapping.items(): + expected.setdefault(_key_operation(key), item) + caseless_attr_dict.setdefault(key, item) + assert caseless_attr_dict == expected + assert repr(caseless_attr_dict) == repr(expected) + + def test_setdefault_unhashable_type( + self, caseless_attr_class, unhashable_type + ): + _class, _key_operation = caseless_attr_class + + caseless_attr_dict: _class = _class() + + with pytest.raises(TypeError): + caseless_attr_dict.setdefault(unhashable_type) + + @pytest.mark.parametrize( + 'starting_data', ({'start_lower': 1, 'START_UPPER': 2, '__key': 1},) + ) + @pytest.mark.parametrize( + 'args', ({'UPPER': 1, 'lower': 2, 'CamelCase': 3, 'Key': 2},) + ) + @pytest.mark.parametrize('kwargs', ({'UP': 1, 'down': 2, '__key': 3},)) + def test_update_using_mapping( + self, caseless_attr_class, starting_data, args, kwargs + ): + _class, _key_operation = caseless_attr_class + caseless_attr_dict: _class = _class(starting_data) + + expected = dict( + { + _key_operation(key): value + for key, value in starting_data.items() + } + ) + + assert caseless_attr_dict == expected + + expected.update( + {_key_operation(key): value for key, value in args.items()}, + **{_key_operation(key): value for key, value in kwargs.items()} + ) + + caseless_attr_dict.update(args, **kwargs) + + assert caseless_attr_dict == expected + + @pytest.mark.parametrize( + 'starting_data', ({'start_lower': 1, 'START_UPPER': 2, '__key': 1},) + ) + @pytest.mark.parametrize( + 'args', + ([('UPPER', 1), ('lower', 2), ('CamelCase', 3), ('__key', 2)],), + ) + @pytest.mark.parametrize('kwargs', ({'UP': 1, 'down': 2, '__key': 3},)) + def test_update_using_sequence( + self, caseless_attr_class, starting_data, args, kwargs + ): + _class, _key_operation = caseless_attr_class + caseless_attr_dict: _class = _class(starting_data) + + expected = dict( + { + _key_operation(key): value + for key, value in starting_data.items() + } + ) + + assert caseless_attr_dict == expected + + expected.update( + {_key_operation(key): value for key, value in args}, + **{_key_operation(key): value for key, value in kwargs.items()} + ) + + caseless_attr_dict.update(args, **kwargs) + + assert caseless_attr_dict == expected + + def test_add_attribute(self, caseless_attr_class): + _class, _key_operation = caseless_attr_class + caseless_attr_dict: _class = _class() + + caseless_attr_dict.new_attr = 'value' + assert caseless_attr_dict.new_attr == 'value' + assert caseless_attr_dict[_key_operation('new_attr')] == 'value' + + def test_get_attribute(self, caseless_attr_class): + _class, _key_operation = caseless_attr_class + caseless_attr_dict: _class = _class() + + caseless_attr_dict['new_attr'] = 'value' + assert caseless_attr_dict.new_attr == 'value' + + def test_delete_attribute(self, caseless_attr_class): + _class, _key_operation = caseless_attr_class + caseless_attr_dict: _class = _class() + + caseless_attr_dict.new_attr = 'value' + del caseless_attr_dict.new_attr + with pytest.raises(AttributeError): + _ = caseless_attr_dict.new_attr + with pytest.raises(KeyError): + _ = caseless_attr_dict[_key_operation('new_attr')] + + def test_str_only(self, caseless_attr_class): + _class, _key_operation = caseless_attr_class + caseless_attr_dict: _class = _class() + + # Initially, str_only should be False and we should be able to add any hashable key + assert caseless_attr_dict.key_is_str_only == False + caseless_attr_dict[1] = 'one' + assert caseless_attr_dict[1] == 'one' + + # Set str_only to True + _class.key_is_str_only = True + caseless_attr_dict: _class = _class() + + assert caseless_attr_dict.key_is_str_only == True + + # Now, trying to add a non-string key should raise a TypeError + with pytest.raises(TypeError): + caseless_attr_dict[2] = 'two' + + # But we should still be able to add string keys + caseless_attr_dict['two'] = 2 + assert caseless_attr_dict['two'] == 2 diff --git a/tests/test_caseless_dictionary.py b/tests/test_caseless_dictionary.py index 7fd8a4d..d32b14d 100644 --- a/tests/test_caseless_dictionary.py +++ b/tests/test_caseless_dictionary.py @@ -1,74 +1,13 @@ import contextlib from copy import deepcopy -from typing import Mapping, Any, Callable, Hashable, NamedTuple, Type +from typing import Mapping import pytest -import caseless_dictionary - - -class _TestingClass(NamedTuple): - cls: Type[caseless_dictionary.CaselessDict] - key_modifier: Callable[[Any], Hashable] - - -def _case_fold(value: Any): - if isinstance(value, str): - value = value.strip().casefold() - return value - - -def _upper(value: Any): - if isinstance(value, str): - value = value.strip().upper() - return value - - -def _title(value: Any): - if isinstance(value, str): - value = value.strip().title() - return value - - -@pytest.fixture( - params=( - { - "CamelCase": 1, - "lower": 2, - "UPPER": 3, - "snake_case": 4, - 5.56: "Five", - True: "True", - }, - {1: 2, ("hello", "Goodbye"): 2}, - ) -) -def valid_mapping(request) -> Mapping: - inputs: Mapping = request.param - return inputs - - -@pytest.fixture( - params=( - _TestingClass(caseless_dictionary.CaselessDict, _case_fold), - _TestingClass(caseless_dictionary.UpperCaselessDict, _upper), - _TestingClass(caseless_dictionary.TitleCaselessDict, _title), - ) -) -def caseless_class(request) -> _TestingClass: - _caseless_class: _TestingClass = request.param - return _caseless_class - - -@pytest.fixture(params=(set(), list(), dict())) -def unhashable_type(request): - unhashable_type = request.param - return unhashable_type - class TestCaselessDictionary: def test__init__mapping( - self, valid_mapping: Mapping, caseless_class: _TestingClass + self, valid_mapping: Mapping, caseless_class ): _class, _key_operation = caseless_class caseless_dict = _class(valid_mapping) @@ -78,8 +17,8 @@ def test__init__mapping( assert caseless_dict == expected @pytest.mark.parametrize( - "valid_kwargs", - ({"a": 1, "B": 2}, {"lower": 3, "UPPER": 4, "MiXeD": 5}), + 'valid_kwargs', + ({'a': 1, 'B': 2}, {'lower': 3, 'UPPER': 4, 'MiXeD': 5}), ) def test__init__kwargs(self, valid_kwargs, caseless_class): _class, _key_operation = caseless_class @@ -91,13 +30,13 @@ def test__init__kwargs(self, valid_kwargs, caseless_class): assert caseless_dict == expected @pytest.mark.parametrize( - "mapping_and_kwargs", + 'mapping_and_kwargs', [ - ({"a": 1, "B": 2}, {"lower": 3, "UPPER": 4, "MiXeD": 5}), + ({'a': 1, 'B': 2}, {'lower': 3, 'UPPER': 4, 'MiXeD': 5}), ], ) def test__init__iterable_and_kwargs( - self, mapping_and_kwargs, caseless_class + self, mapping_and_kwargs, caseless_class ): _class, _key_operation = caseless_class args, kwargs = mapping_and_kwargs @@ -109,10 +48,10 @@ def test__init__iterable_and_kwargs( assert caseless_dict == expected @pytest.mark.parametrize( - "iterables", + 'iterables', [ - zip(["one", "TwO", "ThrEE"], [1, 2, 3]), - [("TwO", 2), ("one", 1), ("ThrEE", 3), (4, "FouR")], + zip(['one', 'TwO', 'ThrEE'], [1, 2, 3]), + [('TwO', 2), ('one', 1), ('ThrEE', 3), (4, 'FouR')], ], ) def test__init__iterable(self, iterables, caseless_class): @@ -125,14 +64,14 @@ def test__init__iterable(self, iterables, caseless_class): assert caseless_dict == expected assert repr(caseless_dict) == repr(expected) - @pytest.mark.parametrize("invalid_type", ([1], {2}, True, 1)) + @pytest.mark.parametrize('invalid_type', ([1], {2}, True, 1)) def test__init__invalid_type(self, invalid_type, caseless_class): _class, _ = caseless_class with pytest.raises(TypeError): _class(invalid_type) - @pytest.mark.parametrize("iterable", [[("1", 1), ("two", 2, 2)]]) + @pytest.mark.parametrize('iterable', [[('1', 1), ('two', 2, 2)]]) def test__init__bad_iterable_elements(self, iterable, caseless_class): _class, _ = caseless_class @@ -140,8 +79,8 @@ def test__init__bad_iterable_elements(self, iterable, caseless_class): _class(iterable) @pytest.mark.parametrize( - "keys", - (["lower", "Title", "UPPER", "CamelCase", "snake_case", object()],), + 'keys', + (['lower', 'Title', 'UPPER', 'CamelCase', 'snake_case', object()],), ) def test_fromkeys(self, keys, caseless_class): _class, _key_operation = caseless_class @@ -156,7 +95,7 @@ def test_fromkeys(self, keys, caseless_class): for key in keys: assert key in caseless_dict - @pytest.mark.parametrize("invalid_type", (True, 1)) + @pytest.mark.parametrize('invalid_type', (True, 1)) def test_fromkeys_with_invalid_type(self, invalid_type, caseless_class): _class, _ = caseless_class @@ -213,7 +152,7 @@ def test__delitem__missing_key(self, valid_mapping, caseless_class): caseless_dict: _class = _class(valid_mapping) with contextlib.suppress(KeyError): - del caseless_dict["missing_key"] + del caseless_dict['missing_key'] def test_get(self, valid_mapping, caseless_class): _class, _ = caseless_class @@ -230,7 +169,7 @@ def test_get_missing_key(self, caseless_class): # make unique __key which will not be in dict _missing_key = object() - _default = "__default v" + _default = '__default v' assert caseless_dict.get(_missing_key) is None assert caseless_dict.get(_missing_key, _default) == _default @@ -240,7 +179,7 @@ def test_get_unhashable_key(self, caseless_class, unhashable_type): caseless_dict: _class = _class() - _default = "__default v" + _default = '__default v' with pytest.raises(TypeError): caseless_dict.get(unhashable_type) @@ -284,9 +223,7 @@ def test_setdefault(self, valid_mapping, caseless_class): assert caseless_dict == expected assert repr(caseless_dict) == repr(expected) - def test_setdefault_unhashable_type( - self, caseless_class, unhashable_type - ): + def test_setdefault_unhashable_type(self, caseless_class, unhashable_type): _class, _key_operation = caseless_class caseless_dict: _class = _class() @@ -295,14 +232,14 @@ def test_setdefault_unhashable_type( caseless_dict.setdefault(unhashable_type) @pytest.mark.parametrize( - "starting_data", ({"start_lower": 1, "START_UPPER": 2, "__key": 1},) + 'starting_data', ({'start_lower': 1, 'START_UPPER': 2, '__key': 1},) ) @pytest.mark.parametrize( - "args", ({"UPPER": 1, "lower": 2, "CamelCase": 3, "Key": 2},) + 'args', ({'UPPER': 1, 'lower': 2, 'CamelCase': 3, 'Key': 2},) ) - @pytest.mark.parametrize("kwargs", ({"UP": 1, "down": 2, "__key": 3},)) + @pytest.mark.parametrize('kwargs', ({'UP': 1, 'down': 2, '__key': 3},)) def test_update_using_mapping( - self, caseless_class, starting_data, args, kwargs + self, caseless_class, starting_data, args, kwargs ): _class, _key_operation = caseless_class caseless_dict: _class = _class(starting_data) @@ -326,15 +263,15 @@ def test_update_using_mapping( assert caseless_dict == expected @pytest.mark.parametrize( - "starting_data", ({"start_lower": 1, "START_UPPER": 2, "__key": 1},) + 'starting_data', ({'start_lower': 1, 'START_UPPER': 2, '__key': 1},) ) @pytest.mark.parametrize( - "args", - ([("UPPER", 1), ("lower", 2), ("CamelCase", 3), ("__key", 2)],), + 'args', + ([('UPPER', 1), ('lower', 2), ('CamelCase', 3), ('__key', 2)],), ) - @pytest.mark.parametrize("kwargs", ({"UP": 1, "down": 2, "__key": 3},)) + @pytest.mark.parametrize('kwargs', ({'UP': 1, 'down': 2, '__key': 3},)) def test_update_using_sequence( - self, caseless_class, starting_data, args, kwargs + self, caseless_class, starting_data, args, kwargs ): _class, _key_operation = caseless_class caseless_dict: _class = _class(starting_data) @@ -365,3 +302,12 @@ def test_update_unhashable_key(self, caseless_class, unhashable_type): with pytest.raises(TypeError): caseless_dict.update(iterable) + + def test_str_only(self, caseless_class): + _class, _ = caseless_class + + _class.key_is_str_only = True + caseless_dict: _class = _class() + + with pytest.raises(TypeError): + caseless_dict[1] = 2 diff --git a/tests/test_change_case.py b/tests/test_change_case.py index 206d994..f7dc899 100644 --- a/tests/test_change_case.py +++ b/tests/test_change_case.py @@ -1,20 +1,41 @@ +"""Test cases for the change_case module. + +This module contains test cases for the change_case module. It tests the +functions that change the case of a string. + +Classes: + TestCaseFold: Test case for the case_fold function. + TestTitle: Test case for the title function. + TestUpper: Test case for the upper function. + TestLowerCase: Test case for the lower function. + TestSnakeCase: Test case for the snake_case function. + TestKebabCase: Test case for the kebab_case function. +""" import typing import pytest -from caseless_dictionary.caseless_dictionary import _case_fold, _upper, _title +from caseless_dictionary.cases import ( + case_fold, + upper, + title, + snake_case, + kebab_case, + lower, + constant_case, +) @pytest.fixture( params=( - 1, - 5.56, - True, - " Tittle ", - "lower ", - " UPPER", - "CamelCase ", - ["NotTouched"], + 1, + 5.56, + True, + ' Tittle ', + 'lower ', + ' UPPER', + 'CamelCase ', + ['NotTouched'], ) ) def data(request) -> typing.Any: @@ -28,7 +49,7 @@ def test_case_fold(self, data): if isinstance(data, str): expected = data.strip().casefold() - actual = _case_fold(data) + actual = case_fold(data) assert actual == expected @@ -38,7 +59,7 @@ def test_case_fold(self, data): if isinstance(data, str): expected = data.strip().title() - actual = _title(data) + actual = title(data) assert actual == expected @@ -48,5 +69,45 @@ def test_case_fold(self, data): if isinstance(data, str): expected = data.strip().upper() - actual = _upper(data) + actual = upper(data) + assert actual == expected + + +class TestSnakeCase: + def test_snake_case(self, data): + expected = data + if isinstance(data, str): + expected = data.strip().replace(' ', '_').casefold() + + actual = snake_case(data) + assert actual == expected + + +class TestKebabCase: + def test_kebab_case(self, data): + expected = data + if isinstance(data, str): + expected = data.strip().replace(' ', '-').casefold() + + actual = kebab_case(data) + assert actual == expected + + +class TestConstantCase: + def test_constant_case(self, data): + expected = data + if isinstance(data, str): + expected = data.strip().replace(' ', '_').upper() + + actual = constant_case(data) + assert actual == expected + + +class TestLowerCase: + def test_lower_case(self, data): + expected = data + if isinstance(data, str): + expected = data.strip().lower() + + actual = lower(data) assert actual == expected