Skip to content

Commit

Permalink
Implement support for slugs to "colour.utilities.CaseInsensitiveMappi…
Browse files Browse the repository at this point in the history
…ng" class.
  • Loading branch information
KelSolaar committed Jun 1, 2022
1 parent 9bc7f13 commit f8475dc
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 27 deletions.
80 changes: 63 additions & 17 deletions colour/utilities/data_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,10 @@ def first_key_from_value(self, value: Any) -> Any:

class CaseInsensitiveMapping(MutableMapping):
"""
Implement a case-insensitive :class:`dict`-like object.
Implement a case-insensitive :class:`dict`-like object with support for
slugs, i.e. *SEO* friendly and human-readable version of the keys.
Allows value retrieving from key while ignoring the key case.
Allow value retrieving by key while ignoring the key case.
The keys are expected to be str or :class:`str`-like objects supporting the
:meth:`str.lower` method.
Expand Down Expand Up @@ -281,15 +282,21 @@ class CaseInsensitiveMapping(MutableMapping):
- :meth:`~colour.utilities.CaseInsensitiveMapping.copy`
- :meth:`~colour.utilities.CaseInsensitiveMapping.lower_keys`
- :meth:`~colour.utilities.CaseInsensitiveMapping.lower_items`
- :meth:`~colour.utilities.CaseInsensitiveMapping.slugified_keys`
- :meth:`~colour.utilities.CaseInsensitiveMapping.slugified_items`
References
----------
:cite:`Reitza`
Examples
--------
>>> methods = CaseInsensitiveMapping({'McCamy': 1, 'Hernandez': 2})
>>> methods['mccamy']
>>> methods = CaseInsensitiveMapping({'McCamy 1992': 1, 'Hernandez 1999': 2})
>>> methods['mccamy 1992']
1
>>> methods['MCCAMY 1992']
1
>>> methods['mccamy-1992']
1
"""

Expand Down Expand Up @@ -373,10 +380,14 @@ def __getitem__(self, item: Union[str, Any]) -> Any:
Notes
-----
- The item value is retrieved by using its lower-case variant.
- The item value can be retrieved by using either its lower-case or
slugified variant.
"""

return self._data[self._lower_key(item)][1]
try:
return self._data[self._lower_key(item)][1]
except KeyError:
return self[dict(zip(self.slugified_keys(), self.keys()))[item]]

def __delitem__(self, item: Union[str, Any]):
"""
Expand All @@ -389,10 +400,14 @@ def __delitem__(self, item: Union[str, Any]):
Notes
-----
- The item is deleted by using its lower-case variant.
- The item can be deleted by using either its lower-case or slugified
variant.
"""

del self._data[self._lower_key(item)]
try:
del self._data[self._lower_key(item)]
except KeyError:
del self[dict(zip(self.slugified_keys(), self.keys()))[item]]

def __contains__(self, item: Union[str, Any]) -> bool:
"""
Expand All @@ -410,9 +425,20 @@ def __contains__(self, item: Union[str, Any]) -> bool:
:class:`bool`
Whether given item is in the case-insensitive :class:`dict`-like
object.
Notes
-----
- The item presence can be checked by using either its lower-case or
slugified variant.
"""

return self._lower_key(item) in self._data
if (
self._lower_key(item) in self._data
or item in self.slugified_keys()
):
return True
else:
return False

def __iter__(self) -> Generator:
"""
Expand Down Expand Up @@ -552,24 +578,44 @@ def lower_items(self) -> Generator:
------
Generator
Item generator.
Notes
-----
- The iterated items are the lower-case items.
"""

yield from ((item, value[1]) for (item, value) in self._data.items())

def slugified_keys(self) -> Generator:
"""
Iterate over the slugified keys of the case-insensitive
:class:`dict`-like object.
Yields
------
Generator
Item generator.
"""

from colour.utilities import slugify

yield from (slugify(key) for key in self.lower_keys())

def slugified_items(self) -> Generator:
"""
Iterate over the slugified items of the case-insensitive
:class:`dict`-like object.
Yields
------
Generator
Item generator.
"""

yield from zip(self.slugified_keys(), self.values())


class LazyCaseInsensitiveMapping(CaseInsensitiveMapping):
"""
Implement a lazy case-insensitive :class:`dict`-like object inheriting
from :class:`CaseInsensitiveMapping` class.
Allows lazy value retrieving from key while ignoring the key case.
The keys are expected to be str or :class:`str`-like objects supporting the
:meth:`str.lower` method.
The lazy retrieval is performed as follows: If the value is a callable,
then it is evaluated and its return value is stored in place of the current
value.
Expand Down
68 changes: 58 additions & 10 deletions colour/utilities/tests/test_data_structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def test_required_methods(self):
"copy",
"lower_keys",
"lower_items",
"slugified_keys",
"slugified_items",
)

for method in required_methods:
Expand Down Expand Up @@ -224,17 +226,21 @@ def test__getitem__(self):
mapping = CaseInsensitiveMapping(John="Doe", Jane="Doe")

self.assertEqual(mapping["John"], "Doe")

self.assertEqual(mapping["john"], "Doe")

self.assertEqual(mapping["Jane"], "Doe")

self.assertEqual(mapping["jane"], "Doe")

mapping = CaseInsensitiveMapping({1: "Foo", 2: "Bar"})

self.assertEqual(mapping[1], "Foo")

mapping = CaseInsensitiveMapping(
{"McCamy 1992": 1, "Hernandez 1999": 2}
)

self.assertEqual(mapping["mccamy-1992"], 1)
self.assertEqual(mapping["hernandez-1999"], 2)

def test__delitem__(self):
"""
Test :meth:`colour.utilities.data_structures.\
Expand All @@ -250,6 +256,18 @@ def test__delitem__(self):
self.assertNotIn("jane", mapping)
self.assertEqual(len(mapping), 0)

mapping = CaseInsensitiveMapping(
{"McCamy 1992": 1, "Hernandez 1999": 2}
)

del mapping["mccamy-1992"]
self.assertNotIn("McCamy 1992", mapping)

del mapping["hernandez-1999"]
self.assertNotIn("Hernandez 1999", mapping)

self.assertEqual(len(mapping), 0)

def test__contains__(self):
"""
Test :meth:`colour.utilities.data_structures.\
Expand All @@ -259,13 +277,17 @@ def test__contains__(self):
mapping = CaseInsensitiveMapping(John="Doe", Jane="Doe")

self.assertIn("John", mapping)

self.assertIn("john", mapping)

self.assertIn("Jane", mapping)

self.assertIn("jane", mapping)

mapping = CaseInsensitiveMapping(
{"McCamy 1992": 1, "Hernandez 1999": 2}
)

self.assertIn("mccamy-1992", mapping)
self.assertIn("hernandez-1999", mapping)

def test__iter__(self):
"""
Test :meth:`colour.utilities.data_structures.\
Expand Down Expand Up @@ -356,7 +378,7 @@ def test_copy(self):
def test_lower_keys(self):
"""
Test :meth:`colour.utilities.data_structures.\
CaseInsensitiveMapping.lowerlower_keys` method.
CaseInsensitiveMapping.lower_keys` method.
"""

mapping = CaseInsensitiveMapping(John="Doe", Jane="Doe")
Expand All @@ -379,6 +401,35 @@ def test_lower_items(self):
[("jane", "Doe"), ("john", "Doe")],
)

def test_slugified_keys(self):
"""
Test :meth:`colour.utilities.data_structures.\
CaseInsensitiveMapping.slugified_keys` method.
"""

mapping = CaseInsensitiveMapping(
{"McCamy 1992": 1, "Hernandez 1999": 2}
)

self.assertListEqual(
sorted(item for item in mapping.slugified_keys()),
["hernandez-1999", "mccamy-1992"],
)

def test_slugified_items(self):
"""
Test :meth:`colour.utilities.data_structures.\
CaseInsensitiveMapping.slugified_items` method.
"""

mapping = CaseInsensitiveMapping(
{"McCamy 1992": 1, "Hernandez 1999": 2}
)
self.assertListEqual(
sorted(item for item in mapping.slugified_items()),
[("hernandez-1999", 2), ("mccamy-1992", 1)],
)


class TestLazyCaseInsensitiveMapping(unittest.TestCase):
"""
Expand Down Expand Up @@ -411,11 +462,8 @@ def test__getitem__(self):
mapping = LazyCaseInsensitiveMapping(John="Doe", Jane=lambda: "Doe")

self.assertEqual(mapping["John"], "Doe")

self.assertEqual(mapping["john"], "Doe")

self.assertEqual(mapping["Jane"], "Doe")

self.assertEqual(mapping["jane"], "Doe")


Expand Down

0 comments on commit f8475dc

Please sign in to comment.