diff --git a/colour/utilities/data_structures.py b/colour/utilities/data_structures.py index c9fd40dc2..069f33cf8 100644 --- a/colour/utilities/data_structures.py +++ b/colour/utilities/data_structures.py @@ -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. @@ -281,6 +282,8 @@ 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 ---------- @@ -288,8 +291,12 @@ class CaseInsensitiveMapping(MutableMapping): 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 """ @@ -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]): """ @@ -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: """ @@ -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: """ @@ -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. diff --git a/colour/utilities/tests/test_data_structures.py b/colour/utilities/tests/test_data_structures.py index fd9f5dd38..0f4bd13ed 100644 --- a/colour/utilities/tests/test_data_structures.py +++ b/colour/utilities/tests/test_data_structures.py @@ -174,6 +174,8 @@ def test_required_methods(self): "copy", "lower_keys", "lower_items", + "slugified_keys", + "slugified_items", ) for method in required_methods: @@ -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.\ @@ -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.\ @@ -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.\ @@ -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") @@ -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): """ @@ -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")