Skip to content

Fix KEY hashing regression for list args in 2.0.0#29

Open
kshubham506 wants to merge 1 commit intoiamsinghrajat:masterfrom
kshubham506:fix-key-hashable-list-args
Open

Fix KEY hashing regression for list args in 2.0.0#29
kshubham506 wants to merge 1 commit intoiamsinghrajat:masterfrom
kshubham506:fix-key-hashable-list-args

Conversation

@kshubham506
Copy link
Copy Markdown

Fix KEY hashing regression for list args in 2.0.0

Summary

This PR fixes a regression where decorator keys crash with:

TypeError: unhashable type: 'list'

when cached functions receive list arguments.

Root cause

KEY.__hash__ in cache/key.py hashes self.args directly, but self.args currently stores raw args. If args contain list values, hash fails.

Changes

  • Normalize args in KEY.__init__ using _to_hashable.
  • Extend _to_hashable to explicitly support:
    • list -> tuple(recursive)
    • set/frozenset -> sorted tuple(recursive)
  • Add regression test in tests/test_cache_features.py to ensure list args are hashable via both KEY and make_key.

Backward compatibility

No API changes. Only key canonicalization/hashing is hardened.

Validation

  • New regression test covers the failing case.
  • Existing key stability behavior remains intact.

Copilot AI review requested due to automatic review settings March 4, 2026 04:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a regression in decorator cache key hashing where list-valued positional arguments could cause TypeError: unhashable type: 'list' by hardening key canonicalization in cache/key.py and adding a regression test.

Changes:

  • Normalize KEY.args using _to_hashable to avoid unhashable list arguments during hashing.
  • Extend _to_hashable to support list → tuple and set/frozenset → sorted tuple.
  • Add a regression test intended to cover list arguments through both KEY and make_key.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
cache/key.py Adds list/set canonicalization and normalizes positional args before hashing to prevent unhashable list failures.
tests/test_cache_features.py Adds a regression test targeting the list-arg hashing failure case.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +19
if isinstance(param, (set, frozenset)):
return tuple(sorted(_to_hashable(item) for item in param))
Copy link

Copilot AI Mar 4, 2026

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_hashable can return different types (e.g., str for ints, tuple for tuples/lists), and sorting a mix of str and tuple raises TypeError in 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.

Copilot uses AI. Check for mistakes.
return links

mk = make_key(dummy, (['https://amzn.to/4rPPcFB'],), {'use_cache': True}, skip_args=0)
self.assertIsNotNone(mk)
Copy link

Copilot AI Mar 4, 2026

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.

Suggested change
self.assertIsNotNone(mk)
h2 = hash(mk)
self.assertIsInstance(h2, int)

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test data uses a real amzn.to shortlink string. To keep tests neutral and avoid embedding real/affiliate URLs in the repo, consider replacing it with a generic placeholder like https://example.com/... while still exercising the list-arg hashing behavior.

Suggested change
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 uses AI. Check for mistakes.
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 link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalizing args via _to_hashable(args) changes key semantics for all positional args, not just lists: _to_hashable falls back to str(param), so e.g. 1 and '1' both become '1' and will now collide in cache keys (previously self.args = args preserved type distinctions). Consider adjusting _to_hashable to return already-hashable primitives as-is (or generally, param when hash(param) succeeds) and only convert truly-unhashable/unstable containers (list/set/dict/obj).

Copilot uses AI. Check for mistakes.
Comment on lines 63 to 65
def _to_hashable(param: Any):
"""Recursive to hashable for stable keys (tuples/dicts/objs).
- Tuples recursive.
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache/key.py defines _to_hashable twice (at the top of the module and again starting here). The second definition overwrites the first at import time, which is confusing and makes future edits error-prone. Please keep a single _to_hashable implementation and remove the duplicate definition (and ensure all call sites still reference the intended one).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants