diff --git a/CHANGES.md b/CHANGES.md index a9d6626..e39d16f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,13 @@ +### Breaking changes +- `BackendCommands` and `AsyncBackendCommands` are no longer decorated with `omit_exception`. +- added `omit_exception_async` to decorate async operations, instead of using `omit_exception` for both sync and async. +- `omit_exception` no longer supports async functions and generators. +- added `DecoratedBackendCommands` and `DecoratedAsyncBackendCommands` as commands decorated with `omit_exception` and `omit_exception_async`. +- `AsyncValkeyCache` and `ValkeyCache` no longer inherit from `BackendCommands` and `AsyncBackendCommands`, they inherit from `DecoratedBackendCommands` and `DecoratedAsyncBackendCommands` instead. + +### improvement +- removed the undecorator loop from cluster client + Version 0.3.2 ------------- diff --git a/django_valkey/async_cache/cache.py b/django_valkey/async_cache/cache.py index e12a97c..6f90297 100644 --- a/django_valkey/async_cache/cache.py +++ b/django_valkey/async_cache/cache.py @@ -1,11 +1,21 @@ from valkey.asyncio.client import Valkey as AValkey -from django_valkey.base import BaseValkeyCache, AsyncBackendCommands +from django_valkey.base import ( + BaseValkeyCache, + AsyncBackendCommands, + decorate_all_methods, + omit_exception_async, +) from django_valkey.async_cache.client.default import AsyncDefaultClient +@decorate_all_methods(omit_exception_async) +class DecoratedAsyncBackendCommands(AsyncBackendCommands): + pass + + class AsyncValkeyCache( - BaseValkeyCache[AsyncDefaultClient, AValkey], AsyncBackendCommands + BaseValkeyCache[AsyncDefaultClient, AValkey], DecoratedAsyncBackendCommands ): DEFAULT_CLIENT_CLASS = "django_valkey.async_cache.client.default.AsyncDefaultClient" is_async = True diff --git a/django_valkey/base.py b/django_valkey/base.py index fb40e08..d2be327 100644 --- a/django_valkey/base.py +++ b/django_valkey/base.py @@ -4,7 +4,6 @@ import inspect import logging from collections.abc import AsyncGenerator, Callable, Iterator -from inspect import iscoroutinefunction from typing import ( Any, TypeVar, @@ -32,7 +31,7 @@ def decorate_all_methods(decorator): def decorate(cls): - for attr in vars(cls): + for attr in dir(cls): # dunders and `get` should not be decorated # get is handled by `_get` if attr.startswith("__") or attr in {"get", "get_or_set"}: @@ -91,6 +90,34 @@ def _generator_decorator(self, *args, **kwargs): except ConnectionInterrupted as e: yield __handle_error(self, e) + # sync generators (iter_keys, sscan_iter) are only generator on client class + # in the backend they are just a function (that returns a generator) + # so inspect.isgeneratorfunction does not work + if not gen: + wrapper = _decorator + + # if method is a generator or async generator, it should be iterated over by this decorator + # generators don't error by simply being called, they need to be iterated over. + else: + wrapper = _generator_decorator + + return wrapper + + +def omit_exception_async( + method: Callable | None = None, return_value: Any | None = None, gen=False +): + if method is None: + return functools.partial(omit_exception, return_value=return_value) + + def __handle_error(self, e) -> Any | None: + if getattr(self, "_ignore_exceptions", None): + if getattr(self, "_log_ignored_exceptions", None): + self.logger.exception("Exception ignored") + + return return_value + raise e.__cause__ + @functools.wraps(method) async def _async_decorator(self, *args, **kwargs): try: @@ -106,22 +133,13 @@ async def _async_generator_decorator(self, *args, **kwargs): except ConnectionInterrupted as e: yield __handle_error(self, e) - # sync generators (iter_keys, sscan_iter) are only generator on client class - # in the backend they are just a function (that returns a generator) - # so inspect.isgeneratorfunction does not work - if not inspect.isasyncgenfunction(method) and not gen: - wrapper = _async_decorator if iscoroutinefunction(method) else _decorator - - # if method is a generator or async generator, it should be iterated over by this decorator - # generators don't error by simply being called, they need to be iterated over. + if inspect.isasyncgenfunction(method): + # if method is a generator or async generator, it should be iterated over by this decorator + # generators don't error by simply being called, they need to be iterated over. + wrapper = _async_generator_decorator else: - wrapper = ( - _async_generator_decorator - if inspect.isasyncgenfunction(method) - else _generator_decorator - ) + wrapper = _async_decorator - wrapper.original = method return wrapper @@ -199,7 +217,6 @@ def make_pattern(self, *args, **kwargs) -> bool: return self.client.make_pattern(*args, **kwargs) -@decorate_all_methods(omit_exception) class BackendCommands: def __contains__(self, item): return self.has_key(item) @@ -384,7 +401,6 @@ def hexists(self: BaseValkeyCache, *args, **kwargs) -> bool: return self.client.hexists(*args, **kwargs) -@decorate_all_methods(omit_exception) class AsyncBackendCommands: def __getattr__(self, item): if item.startswith("a"): diff --git a/django_valkey/cache.py b/django_valkey/cache.py index 2bd26ef..e7c9aa7 100644 --- a/django_valkey/cache.py +++ b/django_valkey/cache.py @@ -1,8 +1,18 @@ from valkey import Valkey -from django_valkey.base import BaseValkeyCache, BackendCommands +from django_valkey.base import ( + BaseValkeyCache, + BackendCommands, + decorate_all_methods, + omit_exception, +) from django_valkey.client import DefaultClient -class ValkeyCache(BaseValkeyCache[DefaultClient, Valkey], BackendCommands): +@decorate_all_methods(omit_exception) +class DecoratedBackendCommands(BackendCommands): + pass + + +class ValkeyCache(BaseValkeyCache[DefaultClient, Valkey], DecoratedBackendCommands): DEFAULT_CLIENT_CLASS = "django_valkey.client.DefaultClient" diff --git a/django_valkey/cluster_cache/cache.py b/django_valkey/cluster_cache/cache.py index 642ce52..e4a152a 100644 --- a/django_valkey/cluster_cache/cache.py +++ b/django_valkey/cluster_cache/cache.py @@ -36,8 +36,3 @@ class ClusterValkeyCache( BackendCommands, ): DEFAULT_CLIENT_CLASS = "django_valkey.cluster_cache.client.DefaultClusterClient" - - -for name, value in vars(BackendCommands).items(): - if original := getattr(value, "original", None): - setattr(ClusterValkeyCache, name, original)