Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Combine multiple cache decorators to a single one #278

Merged
merged 1 commit into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 3 additions & 12 deletions denonavr/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

import attr
import httpx
from asyncstdlib import lru_cache
from defusedxml.ElementTree import fromstring

from .appcommand import AppCommandCmd
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
42 changes: 18 additions & 24 deletions denonavr/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
Loading