Skip to content

Commit 5c8a60f

Browse files
committed
Applying review comments.
1 parent 7c5791d commit 5c8a60f

File tree

3 files changed

+149
-91
lines changed

3 files changed

+149
-91
lines changed

redis/commands/core.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import datetime
44
import hashlib
55
import warnings
6+
from enum import Enum
67
from typing import (
78
TYPE_CHECKING,
89
Any,
@@ -4907,6 +4908,16 @@ def pfmerge(self, dest: KeyT, *sources: KeyT) -> ResponseT:
49074908
AsyncHyperlogCommands = HyperlogCommands
49084909

49094910

4911+
class HashDataPersistOptions(Enum):
4912+
# set the value for each provided key to each
4913+
# provided value only if all do not already exist.
4914+
FNX = "FNX"
4915+
4916+
# set the value for each provided key to each
4917+
# provided value only if all already exist.
4918+
FXX = "FXX"
4919+
4920+
49104921
class HashCommands(CommandsProtocol):
49114922
"""
49124923
Redis commands for Hash data type.
@@ -4949,7 +4960,9 @@ def hgetall(self, name: str) -> Union[Awaitable[dict], dict]:
49494960

49504961
def hgetdel(
49514962
self, name: str, *keys: str
4952-
) -> Union[Awaitable[Optional[str]], Optional[str]]:
4963+
) -> Union[
4964+
Awaitable[Optional[List[Union[str, bytes]]]], Optional[List[Union[str, bytes]]]
4965+
]:
49534966
"""
49544967
Return the value of ``key`` within the hash ``name`` and
49554968
delete the field in the hash.
@@ -4967,15 +4980,18 @@ def hgetdel(
49674980
def hgetex(
49684981
self,
49694982
name: KeyT,
4970-
*keys: str,
4983+
key: Optional[str] = None,
4984+
keys: Optional[list] = None,
49714985
ex: Optional[ExpiryT] = None,
49724986
px: Optional[ExpiryT] = None,
49734987
exat: Optional[AbsExpiryT] = None,
49744988
pxat: Optional[AbsExpiryT] = None,
49754989
persist: bool = False,
4976-
) -> Union[Awaitable[Optional[str]], Optional[str]]:
4990+
) -> Union[
4991+
Awaitable[Optional[List[Union[str, bytes]]]], Optional[List[Union[str, bytes]]]
4992+
]:
49774993
"""
4978-
Return the values of ``keys`` within the hash ``name``
4994+
Return the values of ``key`` and ``keys`` within the hash ``name``
49794995
and optionally set their expiration.
49804996
49814997
``ex`` sets an expire flag on ``kyes`` for ``ex`` seconds.
@@ -4993,8 +5009,7 @@ def hgetex(
49935009
Available since Redis 8.0
49945010
For more information see https://redis.io/commands/hgetex
49955011
"""
4996-
4997-
if len(keys) == 0:
5012+
if key is None and not keys:
49985013
raise DataError("'hgetex' should have at least one key provided")
49995014

50005015
opset = {ex, px, exat, pxat}
@@ -5003,14 +5018,24 @@ def hgetex(
50035018
"``ex``, ``px``, ``exat``, ``pxat``, "
50045019
"and ``persist`` are mutually exclusive."
50055020
)
5021+
keys_to_request = []
5022+
if key is not None:
5023+
keys_to_request.append(key)
5024+
if keys:
5025+
keys_to_request.extend(keys)
50065026

50075027
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
50085028

50095029
if persist:
50105030
exp_options.append("PERSIST")
50115031

50125032
return self.execute_command(
5013-
"HGETEX", name, *exp_options, "FIELDS", len(keys), *keys
5033+
"HGETEX",
5034+
name,
5035+
*exp_options,
5036+
"FIELDS",
5037+
len(keys_to_request),
5038+
*keys_to_request,
50145039
)
50155040

50165041
def hincrby(
@@ -5093,8 +5118,7 @@ def hsetex(
50935118
px: Optional[ExpiryT] = None,
50945119
exat: Optional[AbsExpiryT] = None,
50955120
pxat: Optional[AbsExpiryT] = None,
5096-
fnx: bool = False,
5097-
fxx: bool = False,
5121+
data_persist_option: Optional[HashDataPersistOptions] = None,
50985122
keepttl: bool = False,
50995123
) -> Union[Awaitable[int], int]:
51005124
"""
@@ -5116,11 +5140,12 @@ def hsetex(
51165140
``pxat`` sets an expire flag on ``keys`` for ``ex`` milliseconds,
51175141
specified in unix time.
51185142
5119-
``fnx`` if set to True, set the value for each provided key to each
5120-
provided value only if all do not already exist.
5121-
5122-
``fxx`` if set to True, set the value for each provided key to each
5123-
provided value only if all already exist.
5143+
``data_persist_option`` can be set to ``FNX`` or ``FXX`` to control the
5144+
behavior of the command.
5145+
``FNX`` will set the value for each provided key to each
5146+
provided value only if all do not already exist.
5147+
``FXX`` will set the value for each provided key to each
5148+
provided value only if all already exist.
51245149
51255150
``keepttl`` if True, retain the time to live associated with the keys.
51265151
@@ -5145,14 +5170,10 @@ def hsetex(
51455170
"and ``keepttl`` are mutually exclusive."
51465171
)
51475172

5148-
if fnx and fxx:
5149-
raise DataError("``fnx`` and ``fxx`` are mutually exclusive.")
5150-
51515173
exp_options: list[EncodableT] = extract_expire_flags(ex, px, exat, pxat)
5152-
if fnx:
5153-
exp_options.append("FNX")
5154-
if fxx:
5155-
exp_options.append("FXX")
5174+
if data_persist_option:
5175+
exp_options.append(data_persist_option.value)
5176+
51565177
if keepttl:
51575178
exp_options.append("KEEPTTL")
51585179

tests/test_asyncio/test_hash.py

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from redis import exceptions
8+
from redis.commands.core import HashDataPersistOptions
89
from tests.conftest import skip_if_server_version_lt
910
from tests.test_asyncio.test_utils import redis_server_time
1011

@@ -327,7 +328,9 @@ async def test_hgetex_no_expiration(r):
327328
"b", "foo", "bar", mapping={"1": 1, "2": 2, "3": "three", "4": b"four"}
328329
)
329330

330-
assert await r.hgetex("b", "foo", "1", "4") == [b"bar", b"1", b"four"]
331+
assert await r.hgetex("b", keys=["foo", "1", "4"]) == [b"bar", b"1", b"four"]
332+
assert await r.hgetex("b", "foo", keys=["1", "4"]) == [b"bar", b"1", b"four"]
333+
assert await r.hgetex("b", "foo") == [b"bar"]
331334
assert await r.httl("b", "foo", "1", "4") == [-1, -1, -1]
332335

333336

@@ -338,66 +341,76 @@ async def test_hgetex_expiration_configs(r):
338341
"test:hash", "foo", "bar", mapping={"1": 1, "3": "three", "4": b"four"}
339342
)
340343

344+
test_keys = ["foo", "1", "4"]
341345
# test get with multiple fields with expiration set through 'ex'
342-
assert await r.hgetex("test:hash", "foo", "1", "4", ex=10) == [
346+
assert await r.hgetex("test:hash", keys=test_keys, ex=10) == [
343347
b"bar",
344348
b"1",
345349
b"four",
346350
]
347-
assert await r.httl("test:hash", "foo", "1", "4") == [10, 10, 10]
351+
ttls = await r.httl("test:hash", *test_keys)
352+
for ttl in ttls:
353+
assert pytest.approx(ttl) == 10
348354

349355
# test get with multiple fields removing expiration settings with 'persist'
350-
assert await r.hgetex("test:hash", "foo", "1", "4", persist=True) == [
356+
assert await r.hgetex("test:hash", "foo", keys=["1", "4"], persist=True) == [
351357
b"bar",
352358
b"1",
353359
b"four",
354360
]
355-
assert await r.httl("test:hash", "foo", "1", "4") == [-1, -1, -1]
361+
assert await r.httl("test:hash", *test_keys) == [-1, -1, -1]
356362

357363
# test get with multiple fields with expiration set through 'px'
358-
assert await r.hgetex("test:hash", "foo", "1", "4", px=6000) == [
364+
assert await r.hgetex("test:hash", keys=test_keys, px=6000) == [
359365
b"bar",
360366
b"1",
361367
b"four",
362368
]
363-
assert await r.httl("test:hash", "foo", "1", "4") == [6, 6, 6]
369+
ttls = await r.httl("test:hash", *test_keys)
370+
for ttl in ttls:
371+
assert pytest.approx(ttl) == 6
364372

365373
# test get single field with expiration set through 'pxat'
366374
expire_at = await redis_server_time(r) + timedelta(minutes=1)
367375
assert await r.hgetex("test:hash", "foo", pxat=expire_at) == [b"bar"]
368376
assert (await r.httl("test:hash", "foo"))[0] <= 61
369377

378+
# test get single field with expiration set through 'exat'
379+
expire_at = await redis_server_time(r) + timedelta(seconds=10)
380+
assert await r.hgetex("test:hash", "foo", exat=expire_at) == [b"bar"]
381+
assert (await r.httl("test:hash", "foo"))[0] <= 10
382+
370383

371384
@skip_if_server_version_lt("7.9.0")
372-
async def test_hgetex_validate_expired_foields_removed(r):
385+
async def test_hgetex_validate_expired_fields_removed(r):
373386
await r.delete("test:hash")
374387
await r.hset(
375388
"test:hash", "foo", "bar", mapping={"1": 1, "3": "three", "4": b"four"}
376389
)
377390

378391
# test get multiple fields with expiration set
379392
# validate that expired fields are removed
380-
assert await r.hgetex("test:hash", "foo", "1", "3", ex=1) == [
393+
assert await r.hgetex("test:hash", keys=["foo", "1", "3"], ex=1) == [
381394
b"bar",
382395
b"1",
383396
b"three",
384397
]
385398
await asyncio.sleep(1.1)
386-
assert await r.hgetex("test:hash", "foo", "1", "3") == [None, None, None]
399+
assert await r.hgetex("test:hash", "foo", keys=["1", "3"]) == [None, None, None]
387400
assert await r.httl("test:hash", "foo", "1", "3") == [-2, -2, -2]
388401
assert await r.hgetex("test:hash", "4") == [b"four"]
389402

390403

391404
@skip_if_server_version_lt("7.9.0")
392405
async def test_hgetex_invalid_inputs(r):
393406
with pytest.raises(exceptions.DataError):
394-
await r.hgetex("b", "foo", "1", "3", ex=10, persist=True)
407+
await r.hgetex("b", "foo", ex=10, persist=True)
395408

396409
with pytest.raises(exceptions.DataError):
397-
await r.hgetex("b", "foo", "1", "3", ex=10.0, persist=True)
410+
await r.hgetex("b", "foo", ex=10.0, persist=True)
398411

399412
with pytest.raises(exceptions.DataError):
400-
await r.hgetex("b", "foo", "1", "3", ex=10, px=6000)
413+
await r.hgetex("b", "foo", ex=10, px=6000)
401414

402415
with pytest.raises(exceptions.DataError):
403416
await r.hgetex("b", ex=10)
@@ -430,14 +443,12 @@ async def test_hsetex_expiration_ex_and_keepttl(r):
430443
)
431444
== 1
432445
)
433-
assert await r.httl("test:hash", "foo", "1", "2", "i1", "i2") == [
434-
10,
435-
10,
436-
10,
437-
10,
438-
10,
439-
]
440-
assert await r.hgetex("test:hash", "foo", "1", "2", "i1", "i2") == [
446+
test_keys = ["foo", "1", "2", "i1", "i2"]
447+
ttls = await r.httl("test:hash", *test_keys)
448+
for ttl in ttls:
449+
assert pytest.approx(ttl) == 10
450+
451+
assert await r.hgetex("test:hash", keys=test_keys) == [
441452
b"bar",
442453
b"1",
443454
b"2",
@@ -447,7 +458,7 @@ async def test_hsetex_expiration_ex_and_keepttl(r):
447458
await asyncio.sleep(1.1)
448459
# validate keepttl
449460
assert await r.hsetex("test:hash", "foo", "bar1", keepttl=True) == 1
450-
assert (await r.httl("test:hash", "foo"))[0] < 10
461+
assert 0 < (await r.httl("test:hash", "foo"))[0] < 10
451462

452463

453464
@skip_if_server_version_lt("7.9.0")
@@ -459,8 +470,12 @@ async def test_hsetex_expiration_px(r):
459470
await r.hsetex("test:hash", "foo", "bar", mapping={"1": 1, "2": "2"}, px=60000)
460471
== 1
461472
)
462-
assert await r.httl("test:hash", "foo", "1", "2") == [60, 60, 60]
463-
assert await r.hgetex("test:hash", "foo", "1", "2") == [b"bar", b"1", b"2"]
473+
test_keys = ["foo", "1", "2"]
474+
ttls = await r.httl("test:hash", *test_keys)
475+
for ttl in ttls:
476+
assert pytest.approx(ttl) == 60
477+
478+
assert await r.hgetex("test:hash", keys=test_keys) == [b"bar", b"1", b"2"]
464479

465480

466481
@skip_if_server_version_lt("7.9.0")
@@ -474,30 +489,35 @@ async def test_hsetex_expiration_pxat_and_fnx(r):
474489
expire_at = await redis_server_time(r) + timedelta(minutes=1)
475490
assert (
476491
await r.hsetex(
477-
"test:hash", "foo", "bar1", mapping={"new": "ok"}, pxat=expire_at, fnx=True
492+
"test:hash",
493+
"foo",
494+
"bar1",
495+
mapping={"new": "ok"},
496+
pxat=expire_at,
497+
data_persist_option=HashDataPersistOptions.FNX,
478498
)
479499
== 0
480500
)
481501
ttls = await r.httl("test:hash", "foo", "new")
482502
assert ttls[0] <= 30
483503
assert ttls[1] == -2
484504

485-
assert await r.hgetex("test:hash", "foo", "1", "new") == [b"bar", b"1", None]
505+
assert await r.hgetex("test:hash", keys=["foo", "1", "new"]) == [b"bar", b"1", None]
486506
assert (
487507
await r.hsetex(
488508
"test:hash",
489509
"foo_new",
490510
"bar1",
491511
mapping={"new": "ok"},
492512
pxat=expire_at,
493-
fnx=True,
513+
data_persist_option=HashDataPersistOptions.FNX,
494514
)
495515
== 1
496516
)
497517
ttls = await r.httl("test:hash", "foo", "new")
498518
for ttl in ttls:
499519
assert ttl <= 61
500-
assert await r.hgetex("test:hash", "foo", "foo_new", "new") == [
520+
assert await r.hgetex("test:hash", keys=["foo", "foo_new", "new"]) == [
501521
b"bar",
502522
b"bar1",
503523
b"ok",
@@ -520,27 +540,27 @@ async def test_hsetex_expiration_exat_and_fxx(r):
520540
"bar1",
521541
mapping={"new": "ok"},
522542
exat=expire_at,
523-
fxx=True,
543+
data_persist_option=HashDataPersistOptions.FXX,
524544
)
525545
== 0
526546
)
527547
ttls = await r.httl("test:hash", "foo", "new")
528548
assert 10 < ttls[0] <= 30
529549
assert ttls[1] == -2
530550

531-
assert await r.hgetex("test:hash", "foo", "1", "new") == [b"bar", b"1", None]
551+
assert await r.hgetex("test:hash", keys=["foo", "1", "new"]) == [b"bar", b"1", None]
532552
assert (
533553
await r.hsetex(
534554
"test:hash",
535555
"foo",
536556
"bar1",
537557
mapping={"1": "new_value"},
538558
exat=expire_at,
539-
fxx=True,
559+
data_persist_option=HashDataPersistOptions.FXX,
540560
)
541561
== 1
542562
)
543-
assert await r.hgetex("test:hash", "foo", "1") == [b"bar1", b"new_value"]
563+
assert await r.hgetex("test:hash", keys=["foo", "1"]) == [b"bar1", b"new_value"]
544564

545565

546566
@skip_if_server_version_lt("7.9.0")
@@ -556,6 +576,3 @@ async def test_hsetex_invalid_inputs(r):
556576

557577
with pytest.raises(exceptions.DataError):
558578
await r.hsetex("b", "foo", "bar", ex=10, keepttl=True)
559-
560-
with pytest.raises(exceptions.DataError):
561-
await r.hsetex("b", "foo", "bar", ex=10, fxx=True, fnx=True)

0 commit comments

Comments
 (0)