Source code for cache.key

from typing import Any

_PRIMITIVES = (int, float, str, bool, bytes, type(None))


def _to_hashable(param: Any):
    """Recursive to hashable for stable, value-based cache keys.

    Rules:
    - Primitives: returned as-is.
    - list/tuple -> tuple(recursive)
    - dict -> tuple(sorted((k, v)))
    - set -> sorted tuple (deterministic)
    - objects -> (type, sorted __dict__)
    - fallback -> (type, repr)
    """

    # Only trust true primitives
    if isinstance(param, _PRIMITIVES):
        return param

    # named tuples (before generic tuple/list) — distinguish by type
    if isinstance(param, tuple) and hasattr(type(param), '_fields'):
        return (
            type(param).__qualname__,
            tuple(_to_hashable(p) for p in param),
        )

    # sequences
    if isinstance(param, (list, tuple)):
        return tuple(_to_hashable(p) for p in param)

    # dict
    if isinstance(param, dict):
        return tuple(
            sorted((k, _to_hashable(v)) for k, v in param.items())
        )

    # set
    if isinstance(param, set):
        return tuple(
            sorted(
                (_to_hashable(p) for p in param),
                key=lambda x: (type(x).__qualname__, repr(x)),
            )
        )

    # objects (value-based)
    if hasattr(param, "__dict__"):
        return (
            type(param).__module__,
            type(param).__qualname__,
            id(type(param)),
            tuple(
                sorted(
                    (k, _to_hashable(v)) for k, v in vars(param).items()
                )
            ),
        )

    # fallback (rare / edge types)
    return (type(param).__qualname__, repr(param))


[docs] class KEY: """Immutable, hash/eq-stable key for cache (args + kwargs). Fixes prior bugs: - __eq__ was hash-only (violated contract: a==b but hashes differ possible; collisions). - Hash unstable (dict.items() order pre-3.7, str(vars) arbitrary, kwargs.pop mutated caller!). - Now: frozen tuples, recursive _to_hashable (sorted dicts, stable obj repr), no mutation. Guarantees hash(a)==hash(b) iff a==b; stable across runs/Python versions. Used via make_key in decorators/AsyncCache. """ def __init__(self, args, kwargs): # args: tuple; kwargs cleaned/sorted for stability self.args = tuple(_to_hashable(arg) for arg in 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) # recursive hashable for canonical eq/hash self.kwargs = tuple((k, _to_hashable(v)) for k, v in sorted(kw.items())) def __eq__(self, obj): """Value equality: must match hash contract.""" if not isinstance(obj, KEY): return NotImplemented return self.args == obj.args and self.kwargs == obj.kwargs def __hash__(self): """Stable hash: tuple of recursive hashables (from frozen args/kwargs).""" # self.* already hashable tuples; ensures contract with __eq__ return hash((self.args, self.kwargs)) def __repr__(self): return f"KEY(args={self.args}, kwargs={self.kwargs})"
[docs] def make_key(func, args, kwargs, skip_args=0): """Reusable key: func name + sliced args + cleaned kwargs. Handles skip_args (e.g., 'self'); stable for complex types. """ func_name = getattr(func, "__qualname__", func.__name__) call_args = args[skip_args:] if skip_args else args # pass tuples for immutability inner = KEY(tuple(call_args), dict(kwargs)) # copy dict return (func_name, inner)