From acbea714a3c3b9c61304c4ba2f4d5884fdacc104 Mon Sep 17 00:00:00 2001 From: MrNK2107 Date: Mon, 19 Oct 2026 00:21:41 +0530 Subject: [PATCH] fix(idempotency): stop CachePersistenceLayer from emitting the Redis deprecation warning CachePersistenceLayer, CacheConnection and CacheClientProtocol were TypeAliases of the deprecated Redis* classes, so they were the same object and @deprecated fired for both. The implementation now lives under the Cache* names and the Redis* names are thin @deprecated subclasses, so only the old names warn. Also fixes the RedisClientProtocol warning text, which named RedisPersistenceLayer/CacheProtocol. Fixes #8574 --- .../idempotency/persistence/redis.py | 35 ++++++++------ .../idempotency/_redis/test_redis_layer.py | 48 +++++++++++++++++++ 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py index d06f5cb64fa..07f74b62af8 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py @@ -5,7 +5,7 @@ import logging from contextlib import contextmanager from datetime import timedelta -from typing import Any, Literal, Protocol, TypeAlias, cast +from typing import Any, Literal, Protocol, cast import redis from typing_extensions import deprecated @@ -26,8 +26,7 @@ logger = logging.getLogger(__name__) -@deprecated("RedisPersistenceLayer will be removed in v4.0.0. Please use CacheProtocol instead.") -class RedisClientProtocol(Protocol): +class CacheClientProtocol(Protocol): """ Protocol class defining the interface for a Cache client. @@ -80,8 +79,7 @@ def delete(self, *names: bytes | str | memoryview) -> Any: raise NotImplementedError -@deprecated("RedisConnection will be removed in v4.0.0. Please use CacheConnection instead.") -class RedisConnection: +class CacheConnection: def __init__( self, url: str = "", @@ -172,7 +170,7 @@ def create_subscription_payment(event: dict) -> Payment: self.ssl = ssl self.mode = mode - def _init_client(self) -> RedisClientProtocol: + def _init_client(self) -> CacheClientProtocol: logger.debug(f"Trying to connect to Redis: {self.host}") client: type[redis.Redis | redis.cluster.RedisCluster] if self.mode == "standalone": @@ -185,7 +183,7 @@ def _init_client(self) -> RedisClientProtocol: try: if self.url: logger.debug(f"Using URL format to connect to Cache: {self.host}") - return cast(RedisClientProtocol, client.from_url(url=self.url)) + return cast(CacheClientProtocol, client.from_url(url=self.url)) else: # Cache in cluster mode doesn't support db parameter extra_param_connection: dict[str, Any] = {} @@ -194,7 +192,7 @@ def _init_client(self) -> RedisClientProtocol: logger.debug(f"Using arguments to connect to Cache: {self.host}") return cast( - RedisClientProtocol, + CacheClientProtocol, client( host=self.host, port=self.port, @@ -210,8 +208,7 @@ def _init_client(self) -> RedisClientProtocol: raise IdempotencyPersistenceConnectionError("Could not to connect to Cache endpoint", exc) from exc -@deprecated("RedisCachePersistenceLayer will be removed in v4.0.0. Please use CachePersistenceLayer instead.") -class RedisCachePersistenceLayer(BasePersistenceLayer): +class CachePersistenceLayer(BasePersistenceLayer): def __init__( self, url: str = "", @@ -222,7 +219,7 @@ def __init__( db_index: int = 0, mode: Literal["standalone", "cluster"] = "standalone", ssl: bool = True, - client: RedisClientProtocol | None = None, + client: CacheClientProtocol | None = None, in_progress_expiry_attr: str = "in_progress_expiration", expiry_attr: str = "expiration", status_attr: str = "status", @@ -297,7 +294,7 @@ def lambda_handler(event: dict, context: LambdaContext): # Initialize Cache client with cache config if no client is passed in if client is None: - self.client = RedisConnection( + self.client = CacheConnection( host=host, port=port, username=username, @@ -517,6 +514,14 @@ def _delete_record(self, data_record: DataRecord) -> None: self.client.delete(data_record.idempotency_key) -CachePersistenceLayer: TypeAlias = RedisCachePersistenceLayer -CacheClientProtocol: TypeAlias = RedisClientProtocol -CacheConnection: TypeAlias = RedisConnection +# Deprecated Redis* names are thin subclasses, so only they emit the DeprecationWarning. +@deprecated("RedisClientProtocol will be removed in v4.0.0. Please use CacheClientProtocol instead.") +class RedisClientProtocol(CacheClientProtocol, Protocol): ... + + +@deprecated("RedisConnection will be removed in v4.0.0. Please use CacheConnection instead.") +class RedisConnection(CacheConnection): ... + + +@deprecated("RedisCachePersistenceLayer will be removed in v4.0.0. Please use CachePersistenceLayer instead.") +class RedisCachePersistenceLayer(CachePersistenceLayer): ... diff --git a/tests/functional/idempotency/_redis/test_redis_layer.py b/tests/functional/idempotency/_redis/test_redis_layer.py index 6adb97a64a4..45f2cad75ee 100644 --- a/tests/functional/idempotency/_redis/test_redis_layer.py +++ b/tests/functional/idempotency/_redis/test_redis_layer.py @@ -3,6 +3,7 @@ import datetime import json import time as t +import warnings from threading import Event, Lock as ThreadLock, Thread from unittest import mock @@ -728,3 +729,50 @@ def lambda_handler(event, context): p2.join() # Then only one handler will actually run assert redis_client.cache["exec_count"] == 1 + + +def test_cache_names_do_not_emit_deprecation_warning(): + # GIVEN the non-deprecated Cache* names + from aws_lambda_powertools.utilities.idempotency.persistence.cache import CacheClientProtocol, CacheConnection + + # WHEN they are used + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + CachePersistenceLayer(client=MockRedis(host="localhost", port="63005")) + CacheConnection(host="localhost") + + class MyClient(CacheClientProtocol): ... + + # THEN no DeprecationWarning is raised + + +@pytest.mark.parametrize( + "deprecated_name, kwargs", + [ + ("RedisCachePersistenceLayer", {"client": MockRedis(host="localhost", port="63005")}), + ("RedisConnection", {"host": "localhost"}), + ], +) +def test_redis_names_still_emit_deprecation_warning(deprecated_name, kwargs): + # GIVEN a deprecated Redis* name + from aws_lambda_powertools.utilities.idempotency.persistence import redis as redis_module + + deprecated_cls = getattr(redis_module, deprecated_name) + + # WHEN it is instantiated + # THEN a DeprecationWarning pointing to the Cache* name is raised + with pytest.warns(DeprecationWarning, match=f"{deprecated_name} will be removed in v4.0.0"): + instance = deprecated_cls(**kwargs) + + # AND it is still usable wherever a Cache* object is expected + from aws_lambda_powertools.utilities.idempotency.persistence.cache import CacheConnection + + assert isinstance(instance, (CachePersistenceLayer, CacheConnection)) + + +def test_redis_client_protocol_still_emits_deprecation_warning_on_subclass(): + from aws_lambda_powertools.utilities.idempotency.persistence.redis import RedisClientProtocol + + with pytest.warns(DeprecationWarning, match="RedisClientProtocol will be removed in v4.0.0"): + + class MyClient(RedisClientProtocol): ...