diff --git a/denonavr/api.py b/denonavr/api.py index 1b1c219..b301e35 100644 --- a/denonavr/api.py +++ b/denonavr/api.py @@ -30,7 +30,6 @@ import attr import httpx -from asyncstdlib import lru_cache from defusedxml.ElementTree import fromstring from .appcommand import AppCommandCmd @@ -49,11 +48,7 @@ ZONE2, ZONE3, ) -from .decorators import ( - async_handle_receiver_exceptions, - cache_clear_on_exception, - set_cache_id, -) +from .decorators import async_handle_receiver_exceptions, cache_result from .exceptions import ( AvrIncompleteResponseError, AvrInvalidResponseError, @@ -180,9 +175,7 @@ async def async_get_command(self, request: str) -> str: # Return text return res.text - @set_cache_id - @cache_clear_on_exception - @lru_cache(maxsize=32) + @cache_result @async_handle_receiver_exceptions async def async_get_xml( self, request: str, cache_id: Hashable = None @@ -197,9 +190,7 @@ async def async_get_xml( # Return ElementTree element return xml_root - @set_cache_id - @cache_clear_on_exception - @lru_cache(maxsize=32) + @cache_result @async_handle_receiver_exceptions async def async_post_appcommand( self, request: str, cmds: Tuple[AppCommandCmd], cache_id: Hashable = None diff --git a/denonavr/decorators.py b/denonavr/decorators.py index 3fc7b5f..b84c0ca 100644 --- a/denonavr/decorators.py +++ b/denonavr/decorators.py @@ -16,6 +16,7 @@ from typing import Callable, Coroutine, TypeVar import httpx +from asyncstdlib import lru_cache from defusedxml import DefusedXmlException from defusedxml.ElementTree import ParseError @@ -85,44 +86,37 @@ async def wrapper(*args, **kwargs): return wrapper -def cache_clear_on_exception(func: Callable[..., AnyT]) -> Callable[..., AnyT]: +def cache_result(func: Callable[..., AnyT]) -> Callable[..., AnyT]: """ - Decorate a function to clear lru_cache if an exception occurs. + Decorate a function to cache its results with an lru_cache of maxsize 16. - The decorator must be placed right before the @lru_cache decorator. - It prevents memory leaks in home-assistant when receiver instances are - created and deleted right away in case the device is offline on setup. + This decorator also sets an "cache_id" keyword argument if it is not set yet. + When an exception occurs it clears lru_cache to prevent memory leaks in + home-assistant when receiver instances are created and deleted right + away in case the device is offline on setup. """ + if inspect.signature(func).parameters.get("cache_id") is None: + raise AttributeError( + f"Function {func} does not have a 'cache_id' keyword parameter" + ) + + lru_decorator = lru_cache(maxsize=16) + cached_func = lru_decorator(func) @wraps(func) async def wrapper(*args, **kwargs): + if kwargs.get("cache_id") is None: + kwargs["cache_id"] = time.time() try: - return await func(*args, **kwargs) + return await cached_func(*args, **kwargs) except Exception as err: _LOGGER.debug("Exception %s raised, clearing cache", err) - func.cache_clear() + cached_func.cache_clear() raise return wrapper -def set_cache_id(func: Callable[..., AnyT]) -> Callable[..., AnyT]: - """ - Decorate a function to add cache_id keyword argument if it is not present. - - The function must be called with a fix cache_id keyword argument to be able - to get cached data. This prevents accidental caching of a function result. - """ - - @wraps(func) - def wrapper(*args, **kwargs): - if kwargs.get("cache_id") is None: - kwargs["cache_id"] = time.time() - return func(*args, **kwargs) - - return wrapper - - def run_async_synchronously(async_func: Coroutine) -> Callable: """ Decorate to run the configured asynchronous function synchronously instead.