Skip to content

Commit 14d49e0

Browse files
committed
Add DriverInfo support for Redis client identification
1 parent b2143ef commit 14d49e0

2 files changed

Lines changed: 185 additions & 0 deletions

File tree

fastapi_cache/backends/redis.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class RedisBackend(Backend):
1010
def __init__(self, redis: Union["Redis[bytes]", "RedisCluster[bytes]"]):
1111
self.redis = redis
1212
self.is_cluster: bool = isinstance(redis, RedisCluster)
13+
# Add driver identification for redis-py
14+
self._add_driver_info()
1315

1416
async def get_with_ttl(self, key: str) -> Tuple[int, Optional[bytes]]:
1517
async with self.redis.pipeline(transaction=not self.is_cluster) as pipe:
@@ -28,3 +30,37 @@ async def clear(self, namespace: Optional[str] = None, key: Optional[str] = None
2830
elif key:
2931
return await self.redis.delete(key) # type: ignore[union-attr]
3032
return 0
33+
34+
def _add_driver_info(self) -> None:
35+
"""Add driver identification to Redis connection.
36+
37+
Uses DriverInfo class if available, or falls back to
38+
lib_name/lib_version for older versions.
39+
"""
40+
from typing import Any
41+
42+
from fastapi_cache import __version__
43+
44+
# Get connection pool from the redis client
45+
connection_pool: Any = getattr(self.redis, "connection_pool", None)
46+
if connection_pool is None:
47+
return
48+
49+
# Try to use DriverInfo class
50+
try:
51+
from redis import DriverInfo
52+
53+
driver_info = DriverInfo().add_upstream_driver("fastapi-cache", __version__)
54+
connection_pool.connection_kwargs["driver_info"] = driver_info
55+
except (ImportError, AttributeError):
56+
# Fallback: use lib_name/lib_version
57+
# Format: lib_name='redis-py(fastapi-cache_v{version})'
58+
connection_pool.connection_kwargs["lib_name"] = f"redis-py(fastapi-cache_v{__version__})"
59+
# lib_version should be the redis client version
60+
try:
61+
import redis
62+
63+
redis_version = redis.__version__
64+
except (ImportError, AttributeError):
65+
redis_version = "unknown"
66+
connection_pool.connection_kwargs["lib_version"] = redis_version

tests/test_redis_backend.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from typing import Any, Dict, Optional
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from fastapi_cache.backends.redis import RedisBackend
7+
8+
9+
class MockConnectionPool:
10+
"""Mock Redis connection pool."""
11+
12+
def __init__(self) -> None:
13+
self.connection_kwargs: Dict[str, Any] = {}
14+
15+
16+
class MockRedisClient:
17+
"""Mock Redis client."""
18+
19+
def __init__(self, has_pool: bool = True) -> None:
20+
self.connection_pool: Optional[MockConnectionPool] = (
21+
MockConnectionPool() if has_pool else None
22+
)
23+
24+
25+
@pytest.fixture
26+
def mock_redis_client() -> MockRedisClient:
27+
"""Create a mock Redis client with connection pool."""
28+
return MockRedisClient(has_pool=True)
29+
30+
31+
@pytest.fixture
32+
def mock_redis_client_no_pool() -> MockRedisClient:
33+
"""Create a mock Redis client without connection pool."""
34+
return MockRedisClient(has_pool=False)
35+
36+
37+
def test_add_driver_info_with_driver_info_class(mock_redis_client: MockRedisClient) -> None:
38+
"""Test _add_driver_info when DriverInfo class is available."""
39+
mock_driver_info_instance = MagicMock()
40+
mock_driver_info_instance.add_upstream_driver.return_value = mock_driver_info_instance
41+
mock_driver_info_class = MagicMock(return_value=mock_driver_info_instance)
42+
43+
with patch("redis.DriverInfo", mock_driver_info_class, create=True):
44+
with patch("fastapi_cache.__version__", "0.2.2"):
45+
RedisBackend(mock_redis_client) # type: ignore[arg-type]
46+
47+
# Verify DriverInfo was instantiated
48+
mock_driver_info_class.assert_called_once()
49+
mock_driver_info_instance.add_upstream_driver.assert_called_once_with(
50+
"fastapi-cache", "0.2.2"
51+
)
52+
53+
# Verify driver_info was set in connection_kwargs
54+
assert "driver_info" in mock_redis_client.connection_pool.connection_kwargs # type: ignore[union-attr]
55+
assert (
56+
mock_redis_client.connection_pool.connection_kwargs["driver_info"] # type: ignore[union-attr]
57+
== mock_driver_info_instance
58+
)
59+
60+
61+
def test_add_driver_info_fallback_without_driver_info(
62+
mock_redis_client: MockRedisClient,
63+
) -> None:
64+
"""Test _add_driver_info fallback when DriverInfo is not available."""
65+
with patch("redis.DriverInfo", side_effect=ImportError, create=True):
66+
with patch("fastapi_cache.__version__", "0.2.2"):
67+
with patch("redis.__version__", "5.0.0"):
68+
RedisBackend(mock_redis_client) # type: ignore[arg-type]
69+
70+
# Verify fallback to lib_name/lib_version
71+
assert "lib_name" in mock_redis_client.connection_pool.connection_kwargs # type: ignore[union-attr]
72+
assert "lib_version" in mock_redis_client.connection_pool.connection_kwargs # type: ignore[union-attr]
73+
74+
lib_name = mock_redis_client.connection_pool.connection_kwargs["lib_name"] # type: ignore[union-attr]
75+
assert lib_name == "redis-py(fastapi-cache_v0.2.2)"
76+
assert (
77+
mock_redis_client.connection_pool.connection_kwargs["lib_version"] # type: ignore[union-attr]
78+
== "5.0.0"
79+
)
80+
81+
82+
def test_add_driver_info_fallback_unknown_redis_version(
83+
mock_redis_client: MockRedisClient,
84+
) -> None:
85+
"""Test _add_driver_info fallback when redis version is unknown."""
86+
with patch("redis.DriverInfo", side_effect=ImportError, create=True):
87+
with patch("fastapi_cache.__version__", "0.2.2"):
88+
# Delete __version__ from redis module to trigger AttributeError
89+
import redis
90+
original_version = getattr(redis, "__version__", None)
91+
try:
92+
if hasattr(redis, "__version__"):
93+
delattr(redis, "__version__")
94+
95+
RedisBackend(mock_redis_client) # type: ignore[arg-type]
96+
97+
# Verify fallback with unknown version
98+
assert "lib_version" in mock_redis_client.connection_pool.connection_kwargs # type: ignore[union-attr]
99+
assert (
100+
mock_redis_client.connection_pool.connection_kwargs["lib_version"] # type: ignore[union-attr]
101+
== "unknown"
102+
)
103+
finally:
104+
# Restore original version
105+
if original_version is not None:
106+
redis.__version__ = original_version # type: ignore[attr-defined]
107+
108+
109+
def test_add_driver_info_no_connection_pool(
110+
mock_redis_client_no_pool: MockRedisClient,
111+
) -> None:
112+
"""Test _add_driver_info when connection pool is not available."""
113+
# Should not raise an error, just return early
114+
backend = RedisBackend(mock_redis_client_no_pool) # type: ignore[arg-type]
115+
116+
# Verify no error was raised and backend was created
117+
assert backend.redis == mock_redis_client_no_pool
118+
119+
120+
def test_add_driver_info_attribute_error_fallback(
121+
mock_redis_client: MockRedisClient,
122+
) -> None:
123+
"""Test _add_driver_info fallback when DriverInfo raises AttributeError."""
124+
with patch("redis.DriverInfo", side_effect=AttributeError, create=True):
125+
with patch("fastapi_cache.__version__", "0.2.2"):
126+
with patch("redis.__version__", "4.5.0"):
127+
RedisBackend(mock_redis_client) # type: ignore[arg-type]
128+
129+
# Verify fallback to lib_name/lib_version
130+
assert "lib_name" in mock_redis_client.connection_pool.connection_kwargs # type: ignore[union-attr]
131+
assert (
132+
mock_redis_client.connection_pool.connection_kwargs["lib_version"] # type: ignore[union-attr]
133+
== "4.5.0"
134+
)
135+
136+
137+
def test_redis_backend_is_cluster_false(mock_redis_client: MockRedisClient) -> None:
138+
"""Test that is_cluster is False for regular Redis client."""
139+
backend = RedisBackend(mock_redis_client) # type: ignore[arg-type]
140+
assert backend.is_cluster is False
141+
142+
143+
def test_redis_backend_initialization(mock_redis_client: MockRedisClient) -> None:
144+
"""Test RedisBackend initialization."""
145+
backend = RedisBackend(mock_redis_client) # type: ignore[arg-type]
146+
147+
assert backend.redis == mock_redis_client
148+
assert backend.is_cluster is False
149+

0 commit comments

Comments
 (0)