Skip to content

Commit

Permalink
Updated the sorting used when creating hashes for dictionaries and ad…
Browse files Browse the repository at this point in the history
…ded unit tests for them, and fixed a condition when firing asynchronous event handlers which would have returned errors when it was supposed to exit without worry.
  • Loading branch information
christophertubbs committed Feb 20, 2024
1 parent dd49925 commit 3f3e67a
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 13 deletions.
92 changes: 80 additions & 12 deletions python/lib/core/dmod/core/common/collections/cache.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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`)"""


Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion python/lib/core/dmod/core/events/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 10 additions & 0 deletions python/lib/core/dmod/test/common/test_accesscache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 3f3e67a

Please sign in to comment.