Repository: cevans87/atools

Issue:
Memoize decorator keeps input objects alive while cached
The hash key created to memoize function calls is a tuple of the inputs. It should instead be a hash of that tuple so that none of the objects in the tuple are accidentally kept from being garbage collected.

Discussion:

Patch:
diff --git a/atools/memoize_decorator.py b/atools/memoize_decorator.py
index 1e54ea3..e757e35 100644
--- a/atools/memoize_decorator.py
+++ b/atools/memoize_decorator.py
@@ -6,7 +6,7 @@ from datetime import timedelta
 import inspect
 from time import time
 from threading import Lock as SyncLock
-from typing import Any, Optional, Tuple, Union
+from typing import Any, Optional, Union
 
 
 class _MemoZeroValue:
@@ -74,15 +74,15 @@ class _MemoizeState:
         self._memos = OrderedDict()
         self._expire_order = deque() if self._expire_order is not None else None
 
-    def make_key(self, *args, **kwargs) -> Tuple:
+    def make_key(self, *args, **kwargs) -> int:
         """Returns all params (args, kwargs, and missing default kwargs) for function as kwargs."""
         args_as_kwargs = {}
         for k, v in zip(self._default_kwargs, args):
             args_as_kwargs[k] = v
 
-        return tuple(ChainMap(args_as_kwargs, kwargs, self._default_kwargs).values())
+        return hash(tuple(ChainMap(args_as_kwargs, kwargs, self._default_kwargs).values()))
 
-    def get_memo(self, key) -> _Memo:
+    def get_memo(self, key: int) -> _Memo:
         try:
             memo = self._memos[key] = self._memos.pop(key)
             if self._duration is not None and memo.expire_time < time():
diff --git a/setup.py b/setup.py
index 3a2fd23..d029798 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
 
 setup(
     name='atools',
-    version='0.6.0',
+    version='0.6.1',
     packages=['', 'atools'],
     python_requires='>=3.6',
     url='https://github.com/cevans87/atools',


Test patch:
diff --git a/test/test_memoize_decorator.py b/test/test_memoize_decorator.py
index f1f515d..877f0a9 100644
--- a/test/test_memoize_decorator.py
+++ b/test/test_memoize_decorator.py
@@ -5,6 +5,7 @@ from atools import async_test_case, memoize
 from datetime import timedelta
 import unittest
 from unittest.mock import call, MagicMock, patch
+from weakref import ref
 
 
 @async_test_case
@@ -465,11 +466,21 @@ class TestMemoize(unittest.TestCase):
 
         self.assertEqual(foo_a, foo_b)
 
-    #async def test_memoize_does_not_stop_object_cleanup(self) -> None:
-    #    # TODO make an object, memoize a funciton with that object, and then convert local
-    #    #  variable to a weakref. Assert that the object is cleaned up.
+    async def test_memoize_does_not_stop_object_cleanup(self) -> None:
+        class Foo:
+            pass
+
+        @memoize
+        def foo(_: Foo) -> None:
+            ...
+
+        f = Foo()
+        foo(f)
 
-    #    raise Exception()
+        r = ref(f)
+        assert r() is not None
+        del f
+        assert r() is None
 
 
 if __name__ == '__main__':