Coverage for C:\Python311\Lib\site-packages\persist_cache\persist_cache.py: 100%
55 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-03-14 21:04 +1100
« prev ^ index » next coverage.py v7.3.2, created at 2024-03-14 21:04 +1100
1import asyncio
2import inspect
3import os
4from datetime import timedelta
5from functools import wraps
6from typing import Any, Callable, Union
8from . import caching
9from .caching import NOT_IN_CACHE
10from .helpers import inflate_arguments, is_async, signaturize
13def cache(
14 name: Union[str, Callable, None] = None,
15 dir: Union[str, None] = None,
16 expiry: Union[int, float, timedelta, None] = None,
17 ) -> Callable:
18 """Persistently and locally cache the returns of a function.
20 The function to be cached must accept and return dillable objects only (with the exception of methods' `self` argument, which is always ignored). Additionally, for consistent caching across subsequent sessions, arguments and returns should also be hashable.
22 Arguments:
23 name (`str | Callable`, optional): The name of the cache (or, if `cache()` is being called as an argument-less decorator (ie, as `@cache` instead of `@cache(...)`), the function to be cached). Defaults to the hash of the qualified name of the function. If `dir` is set, this argument will be ignored.
25 dir (`str`, optional): The directory in which the cache should be stored. Defaults to a subdirectory bearing the name of the cache in a parent folder called '.persist_cache' in the current working directory.
27 expiry (`int | float | timedelta`, optional): How long, in seconds or as a `timedelta`, function calls should persist in the cache. Defaults to `None`.
29 Returns:
30 `Callable`: If `cache()` is called with arguments, a decorator that wraps the function to be cached, otherwise, the wrapped function itself. Once wrapped, the function will have the following methods attached to it:
31 - `set_expiry(value: int | float | timedelta) -> None`: Set the expiry of the cache.
32 - `flush_cache() -> None`: Flush out any expired cached returns.
33 - `clear_cache() -> None`: Clear out all cached returns.
34 - `delete_cache() -> None`: Delete the cache."""
36 def decorator(func: Callable) -> Callable:
37 nonlocal name, dir, expiry
39 # If the cache directory has not been set, and the name of the cache has, set it to a subdirectory by that name in a directory called '.persist_cache' in the current working directory, or, if the name of the cache has not been set, set the name of that subdirectory to the hash of the qualified name of the function.
40 if dir is None:
41 if name is not None:
42 dir = f'.persist_cache/{name}'
43 else:
44 dir = f'.persist_cache/{caching.hash(func.__qualname__)}'
46 # Create the cache directory and any other necessary directories if it does not exist.
47 if not os.path.exists(dir):
48 os.makedirs(dir, exist_ok=True)
50 # Flag whether the function is a method to enable the exclusion of the first argument (which will be the instance of the function's class) from being hashed to produce the cache key.
51 is_method = inspect.ismethod(func)
53 # Preserve a map of the function's arguments to their default values and the name and index of the args parameter if such a parameter exists to enable the remapping of positional arguments to their keywords, which thereby allows for the consistent caching of function calls where positional arguments are used on some occasions and keyword arguments are used on others.
54 signature, args_parameter, args_i = signaturize(func)
56 # Initialise a wrapper for synchronous functions.
57 def sync_wrapper(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any:
58 nonlocal dir, expiry, is_method
60 # Map arguments to their keywords or the keyword of the args parameter where necessary, filtering out the first argument if the function is a method, to enable the consistent caching of function calls where positional arguments are used on some occasions and keyword arguments are used on others.
61 arguments = inflate_arguments(signature, args_parameter, args_i, args[is_method:], kwargs)
63 # Hash the arguments to produce the cache key.
64 key = caching.hash(arguments)
66 # Get the value of the key from the cache if it is not expired, otherwise, call the function and set the value of the key in the cache to the result of that call.
67 if (value := caching.get(key, dir, expiry)) is NOT_IN_CACHE:
68 value = func(*args, **kwargs)
69 caching.set(key, value, dir)
71 return value
73 # Initialise a wrapper for asynchronous functions.
74 async def async_wrapper(*args: tuple[Any], **kwargs: dict[str, Any]) -> Any:
75 nonlocal dir, expiry, is_method
77 # Map arguments to their keywords or the keyword of the args parameter where necessary, filtering out the first argument if the function is a method, to enable the consistent caching of function calls where positional arguments are used on some occasions and keyword arguments are used on others.
78 arguments = inflate_arguments(signature, args_parameter, args_i, args[is_method:], kwargs)
80 # Hash the arguments to produce the cache key.
81 key = caching.hash(arguments)
83 # Get the value of the key from the cache if it is not expired, otherwise, call the function and set the value of the key in the cache to the result of that call.
84 if (value := caching.get(key, dir, expiry)) is NOT_IN_CACHE:
85 value = await func(*args, **kwargs)
86 caching.set(key, value, dir)
88 return value
90 # Identify the appropriate wrapper for the function by checking whether it is asynchronous or not.
91 wrapper = async_wrapper if is_async(func) else sync_wrapper
93 # Attach convenience functions to the wrapper for modifying the cache.
94 def delete_cache() -> None:
95 """Delete the cache."""
96 nonlocal dir
98 caching.delete(dir)
100 def clear_cache() -> None:
101 """Clear the cache."""
102 nonlocal dir
104 caching.clear(dir)
106 def flush_cache() -> None:
107 """Flush expired keys from the cache."""
108 nonlocal dir, expiry, is_method
110 caching.flush(dir, expiry)
112 def set_expiry(value: int) -> None:
113 """Set the expiry of the cache."""
114 nonlocal expiry
116 expiry = value
118 wrapper.delete_cache = delete_cache
119 wrapper.clear_cache = clear_cache
120 wrapper.cache_clear = wrapper.clear_cache # Add an alias for cache_clear which is used by lru_cache.
121 wrapper.flush_cache = flush_cache
122 wrapper.set_expiry = set_expiry
124 # Preserve the original function.
125 wrapper.__wrapped__ = func
127 # Preserve the function's original signature.
128 wrapper = wraps(func)(wrapper)
130 return wrapper
132 # If the first argument is a function and all of the other arguments are `None`, indicating that this decorator factory was invoked without passing any arguments, return the result of passing that function to the decorator while also emptying the first argument to avoid it being used by the decorator.
133 if callable(name) and dir is expiry is None:
134 func = name
135 name = None
137 return decorator(func)
139 return decorator