11from collections .abc import Callable
22from functools import wraps
3- from typing import Any , Protocol , TypeVar
4-
5- T = TypeVar ("T" )
6- R = TypeVar ("R" )
7- P_co = TypeVar ("P_co" , bound = Callable [..., Any ], covariant = True )
8-
9-
10- class ClearableFunction (Protocol [P_co ]):
11- """Protocol for a function with a clear_cache method."""
12-
13- def __call__ (self , * args : object , ** kwargs : object ) -> object :
14- """Call the function."""
15- ...
3+ from typing import Any
4+
5+
6+ class CachedFunction [** P , R ]:
7+ """Wrapper class for a cached function with a clear_cache method."""
8+
9+ def __init__ (self , func : Callable [P , R ], cache : Any ) -> None :
10+ """Initialize the cached function wrapper.
11+
12+ Args:
13+ func: The function to wrap.
14+ cache: The cache instance to use.
15+ """
16+ self ._func = func
17+ self ._cache = cache
18+ # Preserve function metadata
19+ wraps (func )(self )
20+
21+ def __get__ (self , obj : object , objtype : type | None = None ) -> CachedFunction [P , R ]:
22+ """Support instance methods by implementing descriptor protocol."""
23+ if obj is None :
24+ return self
25+ # Return a bound method-like callable
26+ from functools import partial
27+
28+ bound_call = partial (self .__call__ , obj )
29+ # Create a new CachedFunction instance that wraps the bound method
30+ # This ensures clear_cache is available on the bound method
31+ bound_cached = CachedFunction (bound_call , self ._cache )
32+ return bound_cached
33+
34+ def __call__ (self , * args : P .args , ** kwargs : P .kwargs ) -> R :
35+ """Call the cached function.
36+
37+ Args:
38+ *args: Positional arguments to pass to the function.
39+ **kwargs: Keyword arguments to pass to the function.
40+
41+ Returns:
42+ The result of the function call (from cache or fresh).
43+ """
44+ # Create a key based on function name, args, and kwargs
45+ func_name = getattr (self ._func , "__name__" , "unknown" )
46+ key_parts = [func_name ]
47+ # Skip first arg if it looks like 'self' (for instance methods)
48+ # We check if args[0] has __dict__ which indicates it's likely an instance
49+ if args and hasattr (args [0 ], "__dict__" ):
50+ key_parts .extend (str (arg ) for arg in args [1 :])
51+ else :
52+ key_parts .extend (str (arg ) for arg in args )
53+ key_parts .extend (f"{ k } :{ v } " for k , v in sorted (kwargs .items ()))
54+ key = ":" .join (key_parts )
55+
56+ # Check if result is in cache
57+ if key in self ._cache :
58+ return self ._cache [key ]
59+
60+ # Call the function and cache the result
61+ result = self ._func (* args , ** kwargs )
62+ self ._cache [key ] = result
63+ return result
1664
1765 def clear_cache (self ) -> None :
1866 """Clear the cache."""
19- ...
67+ self . _cache . clear ()
2068
2169
22- def ttl_cache_decorator (
70+ def ttl_cache_decorator [ ** P , R ] (
2371 ttl_seconds : int = 300 ,
2472 maxsize : int = 100 ,
25- ) -> Callable [[Callable [..., Any ]], ClearableFunction [ Callable [..., Any ] ]]:
73+ ) -> Callable [[Callable [P , R ]], CachedFunction [ P , R ]]:
2674 """Decorator that provides a TTL cache for methods.
2775
2876 Args:
@@ -36,31 +84,7 @@ def ttl_cache_decorator(
3684
3785 cache : TTLCache = TTLCache (maxsize = maxsize , ttl = ttl_seconds )
3886
39- def decorator (func : Callable [..., Any ]) -> ClearableFunction [Callable [..., Any ]]:
40- @wraps (func )
41- def wrapper (* args : object , ** kwargs : object ) -> object :
42- # Create a key based on function name, args, and kwargs
43- func_name = getattr (func , "__name__" , "unknown" )
44- key_parts = [func_name ]
45- key_parts .extend (str (arg ) for arg in args [1 :]) # Skip self
46- key_parts .extend (f"{ k } :{ v } " for k , v in sorted (kwargs .items ()))
47- key = ":" .join (key_parts )
48-
49- # Check if result is in cache
50- if key in cache :
51- return cache [key ]
52-
53- # Call the function and cache the result
54- result = func (* args , ** kwargs )
55- cache [key ] = result
56- return result
57-
58- # Add a method to clear the cache
59- def clear_cache () -> None :
60- cache .clear ()
61-
62- # Add clear_cache method to wrapper using setattr for dynamic attribute
63- wrapper .clear_cache = clear_cache
64- return wrapper
87+ def decorator (func : Callable [P , R ]) -> CachedFunction [P , R ]:
88+ return CachedFunction (func , cache )
6589
6690 return decorator
0 commit comments