Skip to content

Commit a830157

Browse files
authored
Merge branch 'master' into command-list
2 parents 43ed1fa + fa7b3f6 commit a830157

25 files changed

+7058
-47
lines changed

.github/workflows/integration.yaml

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,21 @@ jobs:
2525
uses: actions/setup-python@v3
2626
with:
2727
python-version: 3.9
28+
cache: 'pip'
2829
- name: run code linters
2930
run: |
3031
pip install -r dev_requirements.txt
3132
invoke linters
3233
3334
run-tests:
3435
runs-on: ubuntu-latest
35-
continue-on-error: ${{ matrix.experimental }}
3636
timeout-minutes: 30
3737
strategy:
3838
max-parallel: 15
3939
matrix:
4040
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.7']
4141
test-type: ['standalone', 'cluster']
4242
connection-type: ['hiredis', 'plain']
43-
experimental: [false]
44-
include:
45-
- python-version: 3.11.0-alpha.6
46-
experimental: true
47-
test-type: standalone
48-
connection-type: plain
4943
env:
5044
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
5145
name: Python ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} tests
@@ -55,6 +49,7 @@ jobs:
5549
uses: actions/setup-python@v3
5650
with:
5751
python-version: ${{ matrix.python-version }}
52+
cache: 'pip'
5853
- name: run tests
5954
run: |
6055
pip install -U setuptools wheel
@@ -83,22 +78,18 @@ jobs:
8378
bash .github/workflows/install_and_test.sh ${{ matrix.extension }}
8479
8580
install_package_from_commit:
86-
continue-on-error: ${{ matrix.experimental }}
8781
name: Install package from commit hash
8882
runs-on: ubuntu-latest
8983
strategy:
9084
matrix:
9185
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.7']
92-
experimental: [false]
93-
include:
94-
- python-version: 3.11.0-alpha.5
95-
- experimental: true
9686
steps:
9787
- uses: actions/checkout@v2
9888
- name: install python ${{ matrix.python-version }}
9989
uses: actions/setup-python@v3
10090
with:
10191
python-version: ${{ matrix.python-version }}
92+
cache: 'pip'
10293
- name: install from pip
10394
run: |
10495
pip install --quiet git+${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git@${GITHUB_SHA}

CHANGES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
* Allow negative `retries` for `Retry` class to retry forever
23
* Add `items` parameter to `hset` signature
34
* Create codeql-analysis.yml (#1988). Thanks @chayim
45
* Add limited support for Lua scripting with RedisCluster
@@ -7,6 +8,7 @@
78
* Fix scan_iter for RedisCluster
89
* Remove verbose logging when initializing ClusterPubSub, ClusterPipeline or RedisCluster
910
* Fix broken connection writer lock-up for asyncio (#2065)
11+
* Fix auth bug when provided with no username (#2086)
1012

1113
* 4.1.3 (Feb 8, 2022)
1214
* Fix flushdb and flushall (#1926)

docker/redis7/master/redis.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
port 6379
2+
save ""
3+
enable-debug-command yes
4+
enable-module-command yes

redis/asyncio/client.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@
4141
)
4242
from redis.commands import (
4343
AsyncCoreCommands,
44+
AsyncRedisModuleCommands,
4445
AsyncSentinelCommands,
45-
RedisModuleCommands,
4646
list_or_args,
4747
)
4848
from redis.compat import Protocol, TypedDict
@@ -81,7 +81,7 @@ async def __call__(self, response: Any, **kwargs):
8181

8282

8383
class Redis(
84-
AbstractRedis, RedisModuleCommands, AsyncCoreCommands, AsyncSentinelCommands
84+
AbstractRedis, AsyncRedisModuleCommands, AsyncCoreCommands, AsyncSentinelCommands
8585
):
8686
"""
8787
Implementation of the Redis protocol.
@@ -693,16 +693,24 @@ async def execute_command(self, *args: EncodableT):
693693
# legitimate message off the stack if the connection is already
694694
# subscribed to one or more channels
695695

696+
await self.connect()
697+
connection = self.connection
698+
kwargs = {"check_health": not self.subscribed}
699+
await self._execute(connection, connection.send_command, *args, **kwargs)
700+
701+
async def connect(self):
702+
"""
703+
Ensure that the PubSub is connected
704+
"""
696705
if self.connection is None:
697706
self.connection = await self.connection_pool.get_connection(
698707
"pubsub", self.shard_hint
699708
)
700709
# register a callback that re-subscribes to any channels we
701710
# were listening to when we were disconnected
702711
self.connection.register_connect_callback(self.on_connect)
703-
connection = self.connection
704-
kwargs = {"check_health": not self.subscribed}
705-
await self._execute(connection, connection.send_command, *args, **kwargs)
712+
else:
713+
await self.connection.connect()
706714

707715
async def _disconnect_raise_connect(self, conn, error):
708716
"""
@@ -962,6 +970,7 @@ async def run(
962970
if handler is None:
963971
raise PubSubError(f"Pattern: '{pattern}' has no handler registered")
964972

973+
await self.connect()
965974
while True:
966975
try:
967976
await self.get_message(

redis/asyncio/lock.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import threading
44
import uuid
55
from types import SimpleNamespace
6-
from typing import TYPE_CHECKING, Awaitable, NoReturn, Optional, Union
6+
from typing import TYPE_CHECKING, Awaitable, Optional, Union
77

88
from redis.exceptions import LockError, LockNotOwnedError
99

@@ -243,15 +243,15 @@ async def owned(self) -> bool:
243243
stored_token = encoder.encode(stored_token)
244244
return self.local.token is not None and stored_token == self.local.token
245245

246-
def release(self) -> Awaitable[NoReturn]:
246+
def release(self) -> Awaitable[None]:
247247
"""Releases the already acquired lock"""
248248
expected_token = self.local.token
249249
if expected_token is None:
250250
raise LockError("Cannot release an unlocked lock")
251251
self.local.token = None
252252
return self.do_release(expected_token)
253253

254-
async def do_release(self, expected_token: bytes):
254+
async def do_release(self, expected_token: bytes) -> None:
255255
if not bool(
256256
await self.lua_release(
257257
keys=[self.name], args=[expected_token], client=self.redis

redis/asyncio/retry.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(
2727
"""
2828
Initialize a `Retry` object with a `Backoff` object
2929
that retries a maximum of `retries` times.
30+
`retries` can be negative to retry forever.
3031
You can specify the types of supported errors which trigger
3132
a retry with the `supported_errors` parameter.
3233
"""
@@ -51,7 +52,7 @@ async def call_with_retry(
5152
except self._supported_errors as error:
5253
failures += 1
5354
await fail(error)
54-
if failures > self._retries:
55+
if self._retries >= 0 and failures > self._retries:
5556
raise error
5657
backoff = self._backoff.compute(failures)
5758
if backoff > 0:

redis/cluster.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,10 @@ def set_response_callback(self, command, callback):
867867
self.cluster_response_callbacks[command] = callback
868868

869869
def _determine_nodes(self, *args, **kwargs):
870-
command = args[0]
870+
command = args[0].upper()
871+
if len(args) >= 2 and f"{args[0]} {args[1]}".upper() in self.command_flags:
872+
command = f"{args[0]} {args[1]}".upper()
873+
871874
nodes_flag = kwargs.pop("nodes_flag", None)
872875
if nodes_flag is not None:
873876
# nodes flag passed by the user

redis/commands/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .core import AsyncCoreCommands, CoreCommands
33
from .helpers import list_or_args
44
from .parser import CommandsParser
5-
from .redismodules import RedisModuleCommands
5+
from .redismodules import AsyncRedisModuleCommands, RedisModuleCommands
66
from .sentinel import AsyncSentinelCommands, SentinelCommands
77

88
__all__ = [
@@ -11,6 +11,7 @@
1111
"AsyncCoreCommands",
1212
"CoreCommands",
1313
"list_or_args",
14+
"AsyncRedisModuleCommands",
1415
"RedisModuleCommands",
1516
"AsyncSentinelCommands",
1617
"SentinelCommands",

redis/commands/cluster.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,15 @@ def replicaof(self, *args, **kwargs):
189189
190190
For more information see https://redis.io/commands/replicaof
191191
"""
192-
raise RedisClusterException("REPLICAOF is not supported in cluster" " mode")
192+
raise RedisClusterException("REPLICAOF is not supported in cluster mode")
193193

194194
def swapdb(self, *args, **kwargs):
195195
"""
196196
Swaps two Redis databases.
197197
198198
For more information see https://redis.io/commands/swapdb
199199
"""
200-
raise RedisClusterException("SWAPDB is not supported in cluster" " mode")
200+
raise RedisClusterException("SWAPDB is not supported in cluster mode")
201201

202202

203203
class ClusterDataAccessCommands(DataAccessCommands):
@@ -310,7 +310,6 @@ class RedisClusterCommands(
310310
target specific nodes. By default, if target_nodes is not specified, the
311311
command will be executed on the default cluster node.
312312
313-
314313
:param :target_nodes: type can be one of the followings:
315314
- nodes flag: ALL_NODES, PRIMARIES, REPLICAS, RANDOM
316315
- 'ClusterNode'
@@ -323,7 +322,7 @@ class RedisClusterCommands(
323322

324323
def cluster_myid(self, target_node):
325324
"""
326-
Returns the nodes id.
325+
Returns the node's id.
327326
328327
:target_node: 'ClusterNode'
329328
The node to execute the command on

redis/commands/core.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,11 @@ def auth(self, password, username=None, **kwargs):
376376
authenticate for the given user.
377377
For more information see https://redis.io/commands/auth
378378
"""
379-
if username:
380-
return self.execute_command("AUTH", username, password, **kwargs)
381-
return self.execute_command
379+
pieces = []
380+
if username is not None:
381+
pieces.append(username)
382+
pieces.append(password)
383+
return self.execute_command("AUTH", *pieces, **kwargs)
382384

383385
def bgrewriteaof(self, **kwargs):
384386
"""Tell the Redis server to rewrite the AOF file from data in memory.
@@ -1090,6 +1092,15 @@ def memory_purge(self, **kwargs) -> ResponseT:
10901092
"""
10911093
return self.execute_command("MEMORY PURGE", **kwargs)
10921094

1095+
def latency_histogram(self, *args):
1096+
"""
1097+
This function throws a NotImplementedError since it is intentionally
1098+
not supported.
1099+
"""
1100+
raise NotImplementedError(
1101+
"LATENCY HISTOGRAM is intentionally not implemented in the client."
1102+
)
1103+
10931104
def ping(self, **kwargs) -> ResponseT:
10941105
"""
10951106
Ping the Redis server
@@ -3522,6 +3533,7 @@ def xgroup_create(
35223533
groupname: GroupT,
35233534
id: StreamIdT = "$",
35243535
mkstream: bool = False,
3536+
entries_read: Optional[int] = None,
35253537
) -> ResponseT:
35263538
"""
35273539
Create a new consumer group associated with a stream.
@@ -3534,6 +3546,9 @@ def xgroup_create(
35343546
pieces: list[EncodableT] = ["XGROUP CREATE", name, groupname, id]
35353547
if mkstream:
35363548
pieces.append(b"MKSTREAM")
3549+
if entries_read is not None:
3550+
pieces.extend(["ENTRIESREAD", entries_read])
3551+
35373552
return self.execute_command(*pieces)
35383553

35393554
def xgroup_delconsumer(
@@ -3589,6 +3604,7 @@ def xgroup_setid(
35893604
name: KeyT,
35903605
groupname: GroupT,
35913606
id: StreamIdT,
3607+
entries_read: Optional[int] = None,
35923608
) -> ResponseT:
35933609
"""
35943610
Set the consumer group last delivered ID to something else.
@@ -3598,7 +3614,10 @@ def xgroup_setid(
35983614
35993615
For more information see https://redis.io/commands/xgroup-setid
36003616
"""
3601-
return self.execute_command("XGROUP SETID", name, groupname, id)
3617+
pieces = [name, groupname, id]
3618+
if entries_read is not None:
3619+
pieces.extend(["ENTRIESREAD", entries_read])
3620+
return self.execute_command("XGROUP SETID", *pieces)
36023621

36033622
def xinfo_consumers(self, name: KeyT, groupname: GroupT) -> ResponseT:
36043623
"""
@@ -3845,7 +3864,7 @@ def xrevrange(
38453864
def xtrim(
38463865
self,
38473866
name: KeyT,
3848-
maxlen: int,
3867+
maxlen: Union[int, None],
38493868
approximate: bool = True,
38503869
minid: Union[StreamIdT, None] = None,
38513870
limit: Union[int, None] = None,

redis/commands/redismodules.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,13 @@ def graph(self, index_name="idx"):
8181

8282
g = Graph(client=self, name=index_name)
8383
return g
84+
85+
86+
class AsyncRedisModuleCommands(RedisModuleCommands):
87+
def ft(self, index_name="idx"):
88+
"""Access the search namespace, providing support for redis search."""
89+
90+
from .search import AsyncSearch
91+
92+
s = AsyncSearch(client=self, index_name=index_name)
93+
return s

0 commit comments

Comments
 (0)