import inspect import json from typing import Any, Callable def stable_repr(val: Any) -> str: """Stable JSON representation for cache key components.""" return json.dumps(val, sort_keys=True, default=str) def get_canonical_name(item: Any) -> str: """Return canonical module.qualname for functions / callables.""" module = getattr( item, "__module__", item.__class__.__module__, ) qualname = getattr( item, "__qualname__", getattr(item, "__name__", item.__class__.__name__), ) return f"{module}.{qualname}" def build_cache_key( func: Callable, prefix: str | None, *args: Any, **kwargs: Any, ) -> str: """ Build a deterministic cache key for func called with args/kwargs. For bound methods, skips the first parameter if it's named 'self' or 'cls'. """ sig = inspect.signature(func) bound = sig.bind_partial(*args, **kwargs) bound.apply_defaults() params = list(sig.parameters.values()) arguments = list(bound.arguments.items()) # Detect methods: if first parameter name is 'self' or 'cls' and it's provided in bound args, # skip it when building the key. if params: first_name = params[0].name if first_name in ("self", "cls") and first_name in bound.arguments: arguments = arguments[1:] arguments_fmt = [ f"{name}={stable_repr(val)}" for name, val in sorted(arguments, key=lambda kv: kv[0]) ] key_parts = [ get_canonical_name(func), *arguments_fmt, ] if prefix is not None: key_parts = [prefix] + key_parts return ":".join(key_parts)