Skip to content

Commit 5ce5acd

Browse files
committed
test: add redis-py 7.x and Python 3.13 compatibility tests (#124)
Add test suite verifying compatibility with redis-py 7.x and Python 3.13: - Test get_protocol_version() works with sync and async clients (this was the function failing on Python 3.13) - Test basic checkpointer operations (put/get) - Test basic store operations (put/get) - Log redis-py and Python versions for debugging
1 parent 8437004 commit 5ce5acd

File tree

1 file changed

+198
-0
lines changed

1 file changed

+198
-0
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""Tests for issue #124: Support redis-py 7.x (Python 3.13 compatibility).
2+
3+
This test verifies that the library works correctly with redis-py 7.x,
4+
which includes fixes for Python 3.13's stricter isinstance() behavior.
5+
6+
See: https://github.com/redis/redis-py/issues/3501
7+
See: https://github.com/redis/redis-py/pull/3510
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import sys
13+
from typing import AsyncIterator, Iterator
14+
15+
import pytest
16+
import redis
17+
import redis.asyncio as aredis
18+
from redis.commands.helpers import get_protocol_version
19+
20+
from langgraph.checkpoint.redis import RedisSaver
21+
from langgraph.checkpoint.redis.aio import AsyncRedisSaver
22+
from langgraph.store.redis import RedisStore
23+
from langgraph.store.redis.aio import AsyncRedisStore
24+
25+
26+
class TestRedis7Compatibility:
27+
"""Test suite for redis-py 7.x compatibility with Python 3.13+."""
28+
29+
def test_redis_version_info(self) -> None:
30+
"""Log redis-py version for debugging."""
31+
redis_version = redis.__version__
32+
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
33+
print(f"\nredis-py version: {redis_version}")
34+
print(f"Python version: {python_version}")
35+
36+
# Verify redis version is at least 5.2.1
37+
major, minor, patch = map(int, redis_version.split(".")[:3])
38+
assert major >= 5, f"redis-py {redis_version} is too old, need >= 5.2.1"
39+
40+
def test_get_protocol_version_sync_client(self) -> None:
41+
"""Test that get_protocol_version works with sync Redis client.
42+
43+
This was the function causing TypeError on Python 3.13 before the fix.
44+
"""
45+
client = redis.Redis()
46+
try:
47+
# This should not raise TypeError on Python 3.13
48+
version = get_protocol_version(client)
49+
assert version in (2, 3), f"Unexpected protocol version: {version}"
50+
finally:
51+
client.close()
52+
53+
def test_get_protocol_version_async_client(self) -> None:
54+
"""Test that get_protocol_version works with async Redis client.
55+
56+
This was the function causing TypeError on Python 3.13 before the fix.
57+
"""
58+
client = aredis.Redis()
59+
try:
60+
# This should not raise TypeError on Python 3.13
61+
version = get_protocol_version(client)
62+
assert version in (2, 3), f"Unexpected protocol version: {version}"
63+
finally:
64+
# Async client cleanup
65+
import asyncio
66+
67+
try:
68+
asyncio.get_event_loop().run_until_complete(client.aclose())
69+
except RuntimeError:
70+
# Event loop may not be available in sync test
71+
pass
72+
73+
74+
class TestRedis7WithCheckpointer:
75+
"""Test checkpointer functionality with redis-py 7.x compatibility."""
76+
77+
@pytest.fixture
78+
def saver(self, redis_url: str) -> Iterator[RedisSaver]:
79+
"""Create a RedisSaver for testing."""
80+
with RedisSaver.from_conn_string(redis_url) as saver:
81+
saver.setup()
82+
yield saver
83+
84+
@pytest.fixture
85+
async def async_saver(self, redis_url: str) -> AsyncIterator[AsyncRedisSaver]:
86+
"""Create an AsyncRedisSaver for testing."""
87+
async with AsyncRedisSaver.from_conn_string(redis_url) as saver:
88+
await saver.setup()
89+
yield saver
90+
91+
def test_sync_checkpointer_basic_operations(
92+
self, saver: RedisSaver, redis_url: str
93+
) -> None:
94+
"""Test basic sync checkpointer operations work with current redis-py."""
95+
from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata
96+
97+
# Create a test checkpoint
98+
config = {
99+
"configurable": {
100+
"thread_id": "test-redis7-sync",
101+
"checkpoint_ns": "",
102+
}
103+
}
104+
checkpoint = Checkpoint(
105+
v=1,
106+
id="test-checkpoint-1",
107+
ts="2024-01-01T00:00:00Z",
108+
channel_values={},
109+
channel_versions={},
110+
versions_seen={},
111+
pending_sends=[],
112+
)
113+
metadata = CheckpointMetadata()
114+
115+
# Put and get checkpoint
116+
result = saver.put(config, checkpoint, metadata, {})
117+
assert result is not None
118+
119+
# Verify we can retrieve it
120+
retrieved = saver.get_tuple(config)
121+
assert retrieved is not None
122+
assert retrieved.checkpoint["id"] == checkpoint["id"]
123+
124+
@pytest.mark.asyncio
125+
async def test_async_checkpointer_basic_operations(
126+
self, async_saver: AsyncRedisSaver
127+
) -> None:
128+
"""Test basic async checkpointer operations work with current redis-py."""
129+
from langgraph.checkpoint.base import Checkpoint, CheckpointMetadata
130+
131+
config = {
132+
"configurable": {
133+
"thread_id": "test-redis7-async",
134+
"checkpoint_ns": "",
135+
}
136+
}
137+
checkpoint = Checkpoint(
138+
v=1,
139+
id="test-checkpoint-2",
140+
ts="2024-01-01T00:00:00Z",
141+
channel_values={},
142+
channel_versions={},
143+
versions_seen={},
144+
pending_sends=[],
145+
)
146+
metadata = CheckpointMetadata()
147+
148+
result = await async_saver.aput(config, checkpoint, metadata, {})
149+
assert result is not None
150+
151+
retrieved = await async_saver.aget_tuple(config)
152+
assert retrieved is not None
153+
assert retrieved.checkpoint["id"] == checkpoint["id"]
154+
155+
156+
class TestRedis7WithStore:
157+
"""Test store functionality with redis-py 7.x compatibility."""
158+
159+
@pytest.fixture
160+
def store(self, redis_url: str) -> Iterator[RedisStore]:
161+
"""Create a RedisStore for testing."""
162+
with RedisStore.from_conn_string(redis_url) as store:
163+
store.setup()
164+
yield store
165+
166+
@pytest.fixture
167+
async def async_store(self, redis_url: str) -> AsyncIterator[AsyncRedisStore]:
168+
"""Create an AsyncRedisStore for testing."""
169+
async with AsyncRedisStore.from_conn_string(redis_url) as store:
170+
await store.setup()
171+
yield store
172+
173+
def test_sync_store_basic_operations(self, store: RedisStore) -> None:
174+
"""Test basic sync store operations work with current redis-py."""
175+
namespace = ("test", "redis7", "sync")
176+
key = "test-key"
177+
value = {"data": "test-value", "count": 42}
178+
179+
store.put(namespace, key, value)
180+
181+
item = store.get(namespace, key)
182+
assert item is not None
183+
assert item.value == value
184+
185+
@pytest.mark.asyncio
186+
async def test_async_store_basic_operations(
187+
self, async_store: AsyncRedisStore
188+
) -> None:
189+
"""Test basic async store operations work with current redis-py."""
190+
namespace = ("test", "redis7", "async")
191+
key = "test-key"
192+
value = {"data": "test-value", "count": 42}
193+
194+
await async_store.aput(namespace, key, value)
195+
196+
item = await async_store.aget(namespace, key)
197+
assert item is not None
198+
assert item.value == value

0 commit comments

Comments
 (0)