-
Notifications
You must be signed in to change notification settings - Fork 26
Fix KEY hashing regression for list args in 2.0.0 #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,13 +4,19 @@ | |
| 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). | ||
| Fixes old unstable str(dict.items())/vars. | ||
| """ | ||
| 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,13 +63,19 @@ def __repr__(self): | |
| def _to_hashable(param: Any): | ||
| """Recursive to hashable for stable keys (tuples/dicts/objs). | ||
| - Tuples recursive. | ||
|
Comment on lines
63
to
65
|
||
| - Lists: tuple(recursive). | ||
| - Sets/frozensets: sorted tuple(recursive). | ||
| - Dicts: sorted items tuple. | ||
| - Objs: str(vars) (deterministic repr). | ||
| - Else: str (fallback). | ||
| Prevents instability vs. old str/dict.items(). | ||
| """ | ||
| 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__"): | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+136
|
||||||||||||||||||||||||||||||||||
| 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) | |
| k = KEY((['https://example.com/shortlink'],), {}) | |
| h = hash(k) | |
| self.assertIsInstance(h, int) | |
| async def dummy(links, use_cache=True): | |
| return links | |
| mk = make_key(dummy, (['https://example.com/shortlink'],), {'use_cache': True}, skip_args=0) |
Copilot
AI
Mar 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This regression test claims to cover make_key, but it only asserts that mk is not None. To actually catch the historical failure mode, the test should exercise hashing/equality on the returned key (e.g., hash(mk) or hash(mk[1])) and assert it does not raise.
| self.assertIsNotNone(mk) | |
| h2 = hash(mk) | |
| self.assertIsInstance(h2, int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new set/frozenset handling uses
sorted(_to_hashable(item) for item in param)._to_hashablecan return different types (e.g.,strfor ints,tuplefor tuples/lists), and sorting a mix ofstrandtupleraisesTypeErrorin Python 3. Consider sorting by a stable key (e.g.,key=repr/key=str) while preserving the converted items, or normalizing the converted set elements to a single comparable type before sorting.