Skip to content

Commit b982dcb

Browse files
committed
Add DIGEST command support with tests
1 parent 6bbc5c7 commit b982dcb

File tree

4 files changed

+100
-0
lines changed

4 files changed

+100
-0
lines changed

redis/commands/cluster.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"GEOPOS",
6262
"GEORADIUS",
6363
"GEORADIUSBYMEMBER",
64+
"DIGEST",
6465
"GET",
6566
"GETBIT",
6667
"GETRANGE",

redis/commands/core.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,6 +1835,21 @@ def expiretime(self, key: str) -> int:
18351835
"""
18361836
return self.execute_command("EXPIRETIME", key)
18371837

1838+
def digest(self, name: KeyT) -> Optional[str]:
1839+
"""
1840+
Return the digest of the value stored at the specified key.
1841+
1842+
Returns:
1843+
- None if the key does not exist
1844+
- (bulk string) the XXH3 digest of the value as a hex string
1845+
Raises:
1846+
- ResponseError if key exists but is not a string
1847+
1848+
For more information, see https://redis.io/commands/digest
1849+
"""
1850+
# Bulk string response is already handled (bytes/str based on decode_responses)
1851+
return self.execute_command("DIGEST", name)
1852+
18381853
def get(self, name: KeyT) -> ResponseT:
18391854
"""
18401855
Return the value at key ``name``, or None if the key doesn't exist

tests/test_asyncio/test_commands.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,48 @@ async def test_expireat_unixtime(self, r: redis.Redis):
11151115
assert await r.expireat("a", expire_at_seconds)
11161116
assert 0 < await r.ttl("a") <= 61
11171117

1118+
@skip_if_server_version_lt("8.3.224")
1119+
async def test_digest_nonexistent_returns_none(self, r):
1120+
assert await r.digest("no:such:key") is None
1121+
1122+
@skip_if_server_version_lt("8.3.224")
1123+
async def test_digest_wrong_type_raises(self, r):
1124+
await r.lpush("alist", "x")
1125+
with pytest.raises(Exception): # or redis.exceptions.ResponseError
1126+
await r.digest("alist")
1127+
1128+
@skip_if_server_version_lt("8.3.224")
1129+
@pytest.mark.parametrize(
1130+
"value", [b"", b"abc", b"The quick brown fox jumps over the lazy dog"]
1131+
)
1132+
async def test_digest_response_when_available(self, r, value):
1133+
key = "k:digest"
1134+
await r.delete(key)
1135+
await r.set(key, value)
1136+
1137+
res = await r.digest(key)
1138+
# got is str if decode_responses=True; ensure bytes->str for comparison
1139+
if isinstance(res, bytes):
1140+
res = res.decode()
1141+
assert res is not None
1142+
assert all(c in "0123456789abcdefABCDEF" for c in res)
1143+
1144+
assert len(res) == 16
1145+
1146+
@skip_if_server_version_lt("8.3.224")
1147+
async def test_pipeline_digest(self, r):
1148+
k1, k2 = "k:d1", "k:d2"
1149+
await r.mset({k1: b"A", k2: b"B"})
1150+
p = r.pipeline()
1151+
p.digest(k1)
1152+
p.digest(k2)
1153+
out = await p.execute()
1154+
assert len(out) == 2
1155+
for d in out:
1156+
if isinstance(d, bytes):
1157+
d = d.decode()
1158+
assert d is None or len(d) == 16
1159+
11181160
async def test_get_and_set(self, r: redis.Redis):
11191161
# get and set can't be tested independently of each other
11201162
assert await r.get("a") is None

tests/test_commands.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,6 +1666,48 @@ def test_expireat_option_lt(self, r):
16661666
expire_at = redis_server_time(r) + datetime.timedelta(minutes=1)
16671667
assert r.expireat("key", expire_at, lt=True) is True
16681668

1669+
@skip_if_server_version_lt("8.3.224")
1670+
def test_digest_nonexistent_returns_none(self, r):
1671+
assert r.digest("no:such:key") is None
1672+
1673+
@skip_if_server_version_lt("8.3.224")
1674+
def test_digest_wrong_type_raises(self, r):
1675+
r.lpush("alist", "x")
1676+
with pytest.raises(Exception): # or redis.exceptions.ResponseError
1677+
r.digest("alist")
1678+
1679+
@skip_if_server_version_lt("8.3.224")
1680+
@pytest.mark.parametrize(
1681+
"value", [b"", b"abc", b"The quick brown fox jumps over the lazy dog"]
1682+
)
1683+
def test_digest_response_when_available(self, r, value):
1684+
key = "k:digest"
1685+
r.delete(key)
1686+
r.set(key, value)
1687+
1688+
res = r.digest(key)
1689+
# got is str if decode_responses=True; ensure bytes->str for comparison
1690+
if isinstance(res, bytes):
1691+
res = res.decode()
1692+
assert res is not None
1693+
assert all(c in "0123456789abcdefABCDEF" for c in res)
1694+
1695+
assert len(res) == 16
1696+
1697+
@skip_if_server_version_lt("8.3.224")
1698+
def test_pipeline_digest(self, r):
1699+
k1, k2 = "k:d1", "k:d2"
1700+
r.mset({k1: b"A", k2: b"B"})
1701+
p = r.pipeline()
1702+
p.digest(k1)
1703+
p.digest(k2)
1704+
out = p.execute()
1705+
assert len(out) == 2
1706+
for d in out:
1707+
if isinstance(d, bytes):
1708+
d = d.decode()
1709+
assert d is None or len(d) == 16
1710+
16691711
def test_get_and_set(self, r):
16701712
# get and set can't be tested independently of each other
16711713
assert r.get("a") is None

0 commit comments

Comments
 (0)