diff --git a/cache/key.py b/cache/key.py index d06e57a..5ffa550 100755 --- a/cache/key.py +++ b/cache/key.py @@ -4,6 +4,8 @@ def _to_hashable(param: Any): """Recursive to hashable for stable keys (tuples/dicts/objs). - Tuples recursive. + - Lists: tuple(recursive). + - Sets/frozensets: sorted tuple(recursive). - Dicts: sorted items tuple. - Objs: str(sorted vars) (deterministic). - Else: str (fallback). @@ -11,6 +13,10 @@ def _to_hashable(param: Any): """ if isinstance(param, tuple): return tuple(map(_to_hashable, param)) + if isinstance(param, list): + return tuple(map(_to_hashable, param)) + if isinstance(param, (set, frozenset)): + return tuple(sorted(_to_hashable(item) for item in param)) if isinstance(param, dict): return tuple(sorted((k, _to_hashable(v)) for k, v in param.items())) elif hasattr(param, "__dict__"): @@ -32,7 +38,7 @@ class KEY: def __init__(self, args, kwargs): # args: tuple; kwargs cleaned/sorted for stability - self.args = args # tuple for hash/eq + self.args = _to_hashable(args) # copy + remove use_cache (decorator param) + sort for stability kw = dict(kwargs) # copy to avoid side-effect on caller kw.pop("use_cache", None) @@ -57,6 +63,8 @@ def __repr__(self): def _to_hashable(param: Any): """Recursive to hashable for stable keys (tuples/dicts/objs). - Tuples recursive. + - Lists: tuple(recursive). + - Sets/frozensets: sorted tuple(recursive). - Dicts: sorted items tuple. - Objs: str(vars) (deterministic repr). - Else: str (fallback). @@ -64,6 +72,10 @@ def _to_hashable(param: Any): """ if isinstance(param, tuple): return tuple(map(_to_hashable, param)) + if isinstance(param, list): + return tuple(map(_to_hashable, param)) + if isinstance(param, (set, frozenset)): + return tuple(sorted(_to_hashable(item) for item in param)) if isinstance(param, dict): return tuple(sorted((k, _to_hashable(v)) for k, v in param.items())) elif hasattr(param, "__dict__"): diff --git a/tests/test_cache_features.py b/tests/test_cache_features.py index 2a0a20e..56f702d 100755 --- a/tests/test_cache_features.py +++ b/tests/test_cache_features.py @@ -122,6 +122,20 @@ async def dummy(a, b, use_cache=True): pass # used in cache (e.g. herd/batch keys stable) # (implicit via other tests) + def test_key_with_list_args_is_hashable(self): + """Regression: list in args should not raise TypeError in KEY hash.""" + from cache.key import KEY, make_key + + k = KEY((['https://amzn.to/4rPPcFB'],), {}) + h = hash(k) + self.assertIsInstance(h, int) + + async def dummy(links, use_cache=True): + return links + + mk = make_key(dummy, (['https://amzn.to/4rPPcFB'],), {'use_cache': True}, skip_args=0) + self.assertIsNotNone(mk) + def test_lru_concurrent_eviction(self): """Test for concurrency bug in LRU re-runs with unique keys near maxsize. Pre-fix (race in move_to_end/evict): hits dropped weirdly (e.g., 3% for size=94 vs ~90% for 95).