diff --git a/python/lib/core/dmod/core/common/collections/cache.py b/python/lib/core/dmod/core/common/collections/cache.py index 68ac72a6c..5f77eef51 100644 --- a/python/lib/core/dmod/core/common/collections/cache.py +++ b/python/lib/core/dmod/core/common/collections/cache.py @@ -1,5 +1,5 @@ """ -@TODO: Put a module wide description here +Define a cache that may contain and share data between processes while triggering operations upon alteration. """ from __future__ import annotations @@ -27,10 +27,58 @@ ON_ACCESS_KEY = "on_access" ON_UPDATE_KEY = "on_update" + +@typing.runtime_checkable +class ToDictProtocol(typing.Protocol): + """ + A type of object that has a `to_dict` method that converts itself into a of strings to values + """ + def to_dict(self, *args, **kwargs) -> typing.Dict[str, typing.Any]: + ... + + +@typing.runtime_checkable +class ToJsonProtocol(typing.Protocol): + """ + A type of object that has a `to_json` method that converts itself into a a string interpretation of a dictionary + """ + def to_json(self, *args, **kwargs) -> str: + ... + + T = typing.TypeVar("T") """Any sort of class that might indicate consistency""" -HashableType = typing.TypeVar("HashableType", bound=typing.Union[typing.Hashable, typing.Mapping, typing.Sequence[typing.Hashable]]) +HashableType = typing.TypeVar( + "HashableType", + bound=typing.Union[ + typing.Mapping[ + str, + typing.Union[ + typing.Hashable, + typing.Mapping[str, typing.ForwardRef("HashableType")], + typing.Iterable[typing.ForwardRef("HashableType")] + ] + ], + typing.Iterable[ + typing.Union[ + typing.Hashable, + typing.Mapping[ + str, + typing.Union[ + typing.Hashable, + typing.Mapping[str, typing.ForwardRef("HashableType")], + typing.Iterable[typing.ForwardRef("HashableType")] + ] + ], + typing.Iterable[typing.ForwardRef("HashableType")] + ] + ], + ToJsonProtocol, + ToDictProtocol, + typing.Hashable + ] +) """A type of item that may either be hashed or we have a method of hashing (such as `hash_hashable_map_sequence`)""" @@ -48,22 +96,41 @@ def hash_hashable_map_sequence(value: HashableType) -> int: Returns: The result of the hashing operation """ - if not isinstance(value, (str, bytes)) and isinstance(value, typing.Sequence): - return hash( - ( - hash_hashable_map_sequence(item) for item in value - ) - ) - elif isinstance(value, typing.Mapping): + def key_function(element: T) -> int: + """ + Function used to provided a value used to act as the key value for sorting + + Args: + element: The value serving as the initial key for sorting + + Returns: + A representation of that key value that may be used for comparisons + """ + if isinstance(element, typing.Hashable): + return hash(element) + else: + return hash(str(element)) + + if isinstance(value, typing.Mapping): return hash( - ( + tuple( ( key if isinstance(key, typing.Hashable) else hash_hashable_map_sequence(key), value if isinstance(value, typing.Hashable) else hash_hashable_map_sequence(value) ) - for key, value in sorted(value.items()) + for key, value in sorted(value.items(), key=key_function) + ) + ) + elif not isinstance(value, (str, bytes)) and isinstance(value, typing.Iterable): + return hash( + tuple( + hash_hashable_map_sequence(item) for item in value ) ) + elif isinstance(value, ToDictProtocol): + return hash_hashable_map_sequence(value.to_dict()) + elif isinstance(value, ToJsonProtocol): + return hash_hashable_map_sequence(value.to_json()) else: return hash(value) @@ -446,7 +513,8 @@ def __init__( Constructor Args: - max_size: The maximum number of items that this cache may contain. Non-positive numbers or None will store all data + max_size: The maximum number of items that this cache may contain. Values other than positive numbers will + cause the cache to become unbounded values: Preexisting data to add to the cache on_addition: Handlers to call when an item is added to the cache on_removal: Handlers to call when an item is removed from the cache diff --git a/python/lib/core/dmod/core/events/router.py b/python/lib/core/dmod/core/events/router.py index 08ee587c7..395ca08f9 100644 --- a/python/lib/core/dmod/core/events/router.py +++ b/python/lib/core/dmod/core/events/router.py @@ -127,7 +127,7 @@ async def fire(self, event: typing.Union[str, Event], *args, **kwargs): if isinstance(event, str): event = Event(event_name=event) - if self.__fail_on_missing_event and event.event_name not in self.__events: + if not (self.__fail_on_missing_event or event.event_name in self.__events): return elif event.event_name not in self.__events: raise ValueError(f"There are no registered handlers for the '{event.event_name}' event") diff --git a/python/lib/core/dmod/test/common/test_accesscache.py b/python/lib/core/dmod/test/common/test_accesscache.py index e46161493..b639a4337 100644 --- a/python/lib/core/dmod/test/common/test_accesscache.py +++ b/python/lib/core/dmod/test/common/test_accesscache.py @@ -14,6 +14,7 @@ from ...core.common import CacheEntry from ...core.common import AccessCache +from ...core.common.collections.cache import hash_hashable_map_sequence from ...core.events import Event from ...core.common.collections.constants import ValueType @@ -236,6 +237,15 @@ def compare_records(self, records_added: int, records_removed: int): self.assertEqual(len(self.removal_record), records_removed) self.assertEqual(len(self.access_record), records_added * 4 + records_removed * 5) + def test_hash_hashable_map_sequence(self): + input_1 = [{'one': 48}, {'two': 2}, {'three': 3}] + input_2 = {object(): 42, object(): 0} + + input_1_hash = hash_hashable_map_sequence(input_1) + input_2_hash = hash_hashable_map_sequence(input_2) + + self.assertNotEqual(input_1_hash, input_2_hash) + async def test_accesscache(self): records_added = 0 records_removed = 0