Repository: redis/redis-py
Pull request #2327 (2022-08-08 13:51:30)

Document Redis(single_connection_client)
### Pull Request check-list

_Please make sure to review and check all of these items:_

- [X] Does `$ tox` pass with this change (including linting)?
- [X] Do the CI tests pass with this change (enable it first in your forked repo and wait for the github action build to finish)?
- [X] Is the new or changed code fully tested?
- [X] Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?
- [X] Is there an example added to the examples folder (if applicable)?
- [ ] Was the change added to CHANGES file?
   WON'T DO: too insignificant.


### Description of change

I spent a significant portion of time debugging concurrency issues not knowing that `Redis` is not thread safe with `single_connection_client=True`.
Found this comment on GitHub issues: https://github.com/redis/redis-py/issues/1426#issuecomment-740298873
Hopefully will save the trouble for others :)


codecov-commenter: # [Codecov](https://codecov.io/gh/redis/redis-py/pull/2327?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) Report
> Merging [#2327](https://codecov.io/gh/redis/redis-py/pull/2327?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) (a8a1166) into [master](https://codecov.io/gh/redis/redis-py/commit/2cea637b2e936265d2001043d2b2d4e62559dc17?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) (2cea637) will **decrease** coverage by `0.03%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##           master    #2327      +/-   ##
==========================================
- Coverage   92.21%   92.18%   -0.04%     
==========================================
  Files         111      111              
  Lines       28751    28751              
==========================================
- Hits        26513    26503      -10     
- Misses       2238     2248      +10     
```


| [Impacted Files](https://codecov.io/gh/redis/redis-py/pull/2327?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) | Coverage Δ | |
|---|---|---|
| [redis/client.py](https://codecov.io/gh/redis/redis-py/pull/2327/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvY2xpZW50LnB5) | `89.06% <ø> (ø)` | |
| [tests/conftest.py](https://codecov.io/gh/redis/redis-py/pull/2327/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-dGVzdHMvY29uZnRlc3QucHk=) | `84.94% <0.00%> (-0.78%)` | :arrow_down: |
| [tests/test\_cluster.py](https://codecov.io/gh/redis/redis-py/pull/2327/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-dGVzdHMvdGVzdF9jbHVzdGVyLnB5) | `96.73% <0.00%> (-0.35%)` | :arrow_down: |
| [tests/test\_asyncio/test\_search.py](https://codecov.io/gh/redis/redis-py/pull/2327/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-dGVzdHMvdGVzdF9hc3luY2lvL3Rlc3Rfc2VhcmNoLnB5) | `98.43% <0.00%> (-0.32%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis)


Patch:
diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml
index 5876c087..56dc286b 100644
--- a/.github/workflows/integration.yaml
+++ b/.github/workflows/integration.yaml
@@ -18,6 +18,15 @@ on:
 
 jobs:
 
+   dependency-audit:
+     name: Dependency audit
+     runs-on: ubuntu-latest
+     steps:
+       - uses: actions/checkout@v3
+       - uses: trailofbits/gh-action-pip-audit@v1.0.0
+         with:
+           inputs: requirements.txt dev_requirements.txt
+
    lint:
      name: Code linters
      runs-on: ubuntu-latest
diff --git a/LICENSE b/LICENSE
index 29a3fe38..00aee10d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,22 +1,21 @@
-Copyright (c) 2012 Andy McCurdy
+MIT License
 
- Permission is hereby granted, free of charge, to any person
- obtaining a copy of this software and associated documentation
- files (the "Software"), to deal in the Software without
- restriction, including without limitation the rights to use,
- copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the
- Software is furnished to do so, subject to the following
- conditions:
+Copyright (c) 2022, Redis, inc.
 
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
- OTHER DEALINGS IN THE SOFTWARE.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 80e2cc2e..f5ad9a41 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ Alternatively, you might want to look at [Async connections](https://redis.readt
 
 ### Redis Commands
 
-There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) except where a work (i.e del) is reserved by the language. The complete set of commands can be found [here](https://github.com/redis/redis-py/tree/master/redis/commands), or [the documentation](https://redis.readthedocs.io/en/stable/commands.html).
+There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) except where a word (i.e del) is reserved by the language. The complete set of commands can be found [here](https://github.com/redis/redis-py/tree/master/redis/commands), or [the documentation](https://redis.readthedocs.io/en/stable/commands.html).
 
 ## Advanced Topics
 
diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py
index 6823dcb8..db8c2408 100644
--- a/redis/asyncio/connection.py
+++ b/redis/asyncio/connection.py
@@ -87,6 +87,15 @@ MODULE_EXPORTS_DATA_TYPES_ERROR = (
     "exports one or more module-side data "
     "types, can't unload"
 )
+# user send an AUTH cmd to a server without authorization configured
+NO_AUTH_SET_ERROR = {
+    # Redis >= 6.0
+    "AUTH <password> called without any password "
+    "configured for the default user. Are you sure "
+    "your configuration is correct?": AuthenticationError,
+    # Redis < 6.0
+    "Client sent AUTH, but no password is set": AuthenticationError,
+}
 
 
 class _HiredisReaderArgs(TypedDict, total=False):
@@ -160,7 +169,9 @@ class BaseParser:
             MODULE_EXPORTS_DATA_TYPES_ERROR: ModuleError,
             NO_SUCH_MODULE_ERROR: ModuleError,
             MODULE_UNLOAD_NOT_POSSIBLE_ERROR: ModuleError,
+            **NO_AUTH_SET_ERROR,
         },
+        "WRONGPASS": AuthenticationError,
         "EXECABORT": ExecAbortError,
         "LOADING": BusyLoadingError,
         "NOSCRIPT": NoScriptError,
diff --git a/redis/client.py b/redis/client.py
index 39fbcdea..0662a99e 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -945,6 +945,12 @@ class Redis(AbstractRedis, RedisModuleCommands, CoreCommands, SentinelCommands):
         `retry_on_error` to a list of the error/s to retry on, then set
         `retry` to a valid `Retry` object.
         To retry on TimeoutError, `retry_on_timeout` can also be set to `True`.
+
+        Args:
+
+        single_connection_client:
+            if `True`, connection pool is not used. In that case `Redis`
+            instance use is not thread safe.
         """
         if not connection_pool:
             if charset is not None:
diff --git a/redis/commands/bf/commands.py b/redis/commands/bf/commands.py
index 0fe065d7..9c3c97c0 100644
--- a/redis/commands/bf/commands.py
+++ b/redis/commands/bf/commands.py
@@ -69,6 +69,8 @@ class BFCommands:
         self.append_no_scale(params, noScale)
         return self.execute_command(BF_RESERVE, *params)
 
+    reserve = create
+
     def add(self, key, item):
         """
         Add to a Bloom Filter `key` an `item`.
@@ -178,6 +180,8 @@ class CFCommands:
         self.append_max_iterations(params, max_iterations)
         return self.execute_command(CF_RESERVE, *params)
 
+    reserve = create
+
     def add(self, key, item):
         """
         Add an `item` to a Cuckoo Filter `key`.
@@ -351,7 +355,7 @@ class TDigestCommands:
         Allocate the memory and initialize the t-digest.
         For more information see `TDIGEST.CREATE <https://redis.io/commands/tdigest.create>`_.
         """  # noqa
-        return self.execute_command(TDIGEST_CREATE, key, compression)
+        return self.execute_command(TDIGEST_CREATE, key, "COMPRESSION", compression)
 
     def reset(self, key):
         """
diff --git a/redis/commands/core.py b/redis/commands/core.py
index 6cf04577..51f30d2e 100644
--- a/redis/commands/core.py
+++ b/redis/commands/core.py
@@ -1504,6 +1504,29 @@ class BasicKeyCommands(CommandsProtocol):
         """
         return BitFieldOperation(self, key, default_overflow=default_overflow)
 
+    def bitfield_ro(
+        self: Union["Redis", "AsyncRedis"],
+        key: KeyT,
+        encoding: str,
+        offset: BitfieldOffsetT,
+        items: Optional[list] = None,
+    ) -> ResponseT:
+        """
+        Return an array of the specified bitfield values
+        where the first value is found using ``encoding`` and ``offset``
+        parameters and remaining values are result of corresponding
+        encoding/offset pairs in optional list ``items``
+        Read-only variant of the BITFIELD command.
+
+        For more information see https://redis.io/commands/bitfield_ro
+        """
+        params = [key, "GET", encoding, offset]
+
+        items = items or []
+        for encoding, offset in items:
+            params.extend(["GET", encoding, offset])
+        return self.execute_command("BITFIELD_RO", *params)
+
     def bitop(self, operation: str, dest: KeyT, *keys: KeyT) -> ResponseT:
         """
         Perform a bitwise operation using ``operation`` between ``keys`` and
diff --git a/redis/connection.py b/redis/connection.py
index 3438bafe..3281b14d 100755
--- a/redis/connection.py
+++ b/redis/connection.py
@@ -80,6 +80,15 @@ MODULE_EXPORTS_DATA_TYPES_ERROR = (
     "exports one or more module-side data "
     "types, can't unload"
 )
+# user send an AUTH cmd to a server without authorization configured
+NO_AUTH_SET_ERROR = {
+    # Redis >= 6.0
+    "AUTH <password> called without any password "
+    "configured for the default user. Are you sure "
+    "your configuration is correct?": AuthenticationError,
+    # Redis < 6.0
+    "Client sent AUTH, but no password is set": AuthenticationError,
+}
 
 
 class Encoder:
@@ -127,7 +136,6 @@ class BaseParser:
     EXCEPTION_CLASSES = {
         "ERR": {
             "max number of clients reached": ConnectionError,
-            "Client sent AUTH, but no password is set": AuthenticationError,
             "invalid password": AuthenticationError,
             # some Redis server versions report invalid command syntax
             # in lowercase
@@ -141,7 +149,9 @@ class BaseParser:
             MODULE_EXPORTS_DATA_TYPES_ERROR: ModuleError,
             NO_SUCH_MODULE_ERROR: ModuleError,
             MODULE_UNLOAD_NOT_POSSIBLE_ERROR: ModuleError,
+            **NO_AUTH_SET_ERROR,
         },
+        "WRONGPASS": AuthenticationError,
         "EXECABORT": ExecAbortError,
         "LOADING": BusyLoadingError,
         "NOSCRIPT": NoScriptError,
@@ -1166,6 +1176,16 @@ URL_QUERY_ARGUMENT_PARSERS = {
 
 
 def parse_url(url):
+    if not (
+        url.startswith("redis://")
+        or url.startswith("rediss://")
+        or url.startswith("unix://")
+    ):
+        raise ValueError(
+            "Redis URL must specify one of the following "
+            "schemes (redis://, rediss://, unix://)"
+        )
+
     url = urlparse(url)
     kwargs = {}
 
@@ -1192,7 +1212,7 @@ def parse_url(url):
             kwargs["path"] = unquote(url.path)
         kwargs["connection_class"] = UnixDomainSocketConnection
 
-    elif url.scheme in ("redis", "rediss"):
+    else:  # implied:  url.scheme in ("redis", "rediss"):
         if url.hostname:
             kwargs["host"] = unquote(url.hostname)
         if url.port:
@@ -1208,11 +1228,6 @@ def parse_url(url):
 
         if url.scheme == "rediss":
             kwargs["connection_class"] = SSLConnection
-    else:
-        raise ValueError(
-            "Redis URL must specify one of the following "
-            "schemes (redis://, rediss://, unix://)"
-        )
 
     return kwargs
 
diff --git a/redis/typing.py b/redis/typing.py
index b572b0cf..8504c7de 100644
--- a/redis/typing.py
+++ b/redis/typing.py
@@ -16,7 +16,7 @@ EncodedT = Union[bytes, memoryview]
 DecodedT = Union[str, int, float]
 EncodableT = Union[EncodedT, DecodedT]
 AbsExpiryT = Union[int, datetime]
-ExpiryT = Union[float, timedelta]
+ExpiryT = Union[int, timedelta]
 ZScoreBoundT = Union[float, str]  # str allows for the [ or ( prefix
 BitfieldOffsetT = Union[int, str]  # str allows for #x syntax
 _StringLikeT = Union[bytes, str, memoryview]


Test patch:
diff --git a/tests/test_asyncio/test_bloom.py b/tests/test_asyncio/test_bloom.py
index 15b22b33..8e642d2f 100644
--- a/tests/test_asyncio/test_bloom.py
+++ b/tests/test_asyncio/test_bloom.py
@@ -366,21 +366,21 @@ async def test_tdigest_quantile(modclient: redis.Redis):
     # assert min min/max have same result as quantile 0 and 1
     assert (
         await modclient.tdigest().max("tDigest")
-        == (await modclient.tdigest().quantile("tDigest", 1.0))[1]
+        == (await modclient.tdigest().quantile("tDigest", 1.0))[0]
     )
     assert (
         await modclient.tdigest().min("tDigest")
-        == (await modclient.tdigest().quantile("tDigest", 0.0))[1]
+        == (await modclient.tdigest().quantile("tDigest", 0.0))[0]
     )
 
-    assert 1.0 == round((await modclient.tdigest().quantile("tDigest", 0.01))[1], 2)
-    assert 99.0 == round((await modclient.tdigest().quantile("tDigest", 0.99))[1], 2)
+    assert 1.0 == round((await modclient.tdigest().quantile("tDigest", 0.01))[0], 2)
+    assert 99.0 == round((await modclient.tdigest().quantile("tDigest", 0.99))[0], 2)
 
     # test multiple quantiles
     assert await modclient.tdigest().create("t-digest", 100)
     assert await modclient.tdigest().add("t-digest", [1, 2, 3, 4, 5], [1.0] * 5)
     res = await modclient.tdigest().quantile("t-digest", 0.5, 0.8)
-    assert [0.5, 3.0, 0.8, 5.0] == res
+    assert [3.0, 5.0] == res
 
 
 @pytest.mark.redismod
diff --git a/tests/test_asyncio/test_commands.py b/tests/test_asyncio/test_commands.py
index 4c252776..422328a0 100644
--- a/tests/test_asyncio/test_commands.py
+++ b/tests/test_asyncio/test_commands.py
@@ -2953,6 +2953,19 @@ class TestRedisCommands:
         )
         assert resp == [0, None, 255]
 
+    @skip_if_server_version_lt("6.0.0")
+    async def test_bitfield_ro(self, r: redis.Redis):
+        bf = r.bitfield("a")
+        resp = await bf.set("u8", 8, 255).execute()
+        assert resp == [0]
+
+        resp = await r.bitfield_ro("a", "u8", 0)
+        assert resp == [0]
+
+        items = [("u4", 8), ("u4", 12), ("u4", 13)]
+        resp = await r.bitfield_ro("a", "u8", 0, items)
+        assert resp == [0, 15, 15, 14]
+
     @skip_if_server_version_lt("4.0.0")
     async def test_memory_stats(self, r: redis.Redis):
         # put a key into the current db to make sure that "db.<current-db>"
diff --git a/tests/test_asyncio/test_graph.py b/tests/test_asyncio/test_graph.py
index 8a8f9cf9..0c011bd9 100644
--- a/tests/test_asyncio/test_graph.py
+++ b/tests/test_asyncio/test_graph.py
@@ -348,11 +348,11 @@ async def test_list_keys(modclient: redis.Redis):
     result = await modclient.graph().list_keys()
     assert result == []
 
-    await modclient.execute_command("GRAPH.EXPLAIN", "G", "RETURN 1")
+    await modclient.graph("G").query("CREATE (n)")
     result = await modclient.graph().list_keys()
     assert result == ["G"]
 
-    await modclient.execute_command("GRAPH.EXPLAIN", "X", "RETURN 1")
+    await modclient.graph("X").query("CREATE (m)")
     result = await modclient.graph().list_keys()
     assert result == ["G", "X"]
 
diff --git a/tests/test_asyncio/test_timeseries.py b/tests/test_asyncio/test_timeseries.py
index 182f6b5f..ef582269 100644
--- a/tests/test_asyncio/test_timeseries.py
+++ b/tests/test_asyncio/test_timeseries.py
@@ -240,6 +240,9 @@ async def test_range_advanced(modclient: redis.Redis):
     assert [(0, 5.0), (5, 6.0)] == await modclient.ts().range(
         1, 0, 10, aggregation_type="count", bucket_size_msec=10, align=5
     )
+    assert [(0, 2.5500000000000003), (10, 3.0)] == await modclient.ts().range(
+        1, 0, 10, aggregation_type="twa", bucket_size_msec=10
+    )
 
 
 @pytest.mark.redismod
diff --git a/tests/test_bloom.py b/tests/test_bloom.py
index 12a48399..2ff2ea79 100644
--- a/tests/test_bloom.py
+++ b/tests/test_bloom.py
@@ -38,6 +38,21 @@ def test_create(client):
     assert client.topk().reserve("topk", 5, 100, 5, 0.9)
 
 
+@pytest.mark.redismod
+def test_bf_reserve(client):
+    """Testing BF.RESERVE"""
+    assert client.bf().reserve("bloom", 0.01, 1000)
+    assert client.bf().reserve("bloom_e", 0.01, 1000, expansion=1)
+    assert client.bf().reserve("bloom_ns", 0.01, 1000, noScale=True)
+    assert client.cf().reserve("cuckoo", 1000)
+    assert client.cf().reserve("cuckoo_e", 1000, expansion=1)
+    assert client.cf().reserve("cuckoo_bs", 1000, bucket_size=4)
+    assert client.cf().reserve("cuckoo_mi", 1000, max_iterations=10)
+    assert client.cms().initbydim("cmsDim", 100, 5)
+    assert client.cms().initbyprob("cmsProb", 0.01, 0.01)
+    assert client.topk().reserve("topk", 5, 100, 5, 0.9)
+
+
 @pytest.mark.redismod
 @pytest.mark.experimental
 def test_tdigest_create(client):
@@ -365,17 +380,17 @@ def test_tdigest_quantile(client):
     )
     # assert min min/max have same result as quantile 0 and 1
     res = client.tdigest().quantile("tDigest", 1.0)
-    assert client.tdigest().max("tDigest") == res[1]
+    assert client.tdigest().max("tDigest") == res[0]
     res = client.tdigest().quantile("tDigest", 0.0)
-    assert client.tdigest().min("tDigest") == res[1]
+    assert client.tdigest().min("tDigest") == res[0]
 
-    assert 1.0 == round(client.tdigest().quantile("tDigest", 0.01)[1], 2)
-    assert 99.0 == round(client.tdigest().quantile("tDigest", 0.99)[1], 2)
+    assert 1.0 == round(client.tdigest().quantile("tDigest", 0.01)[0], 2)
+    assert 99.0 == round(client.tdigest().quantile("tDigest", 0.99)[0], 2)
 
     # test multiple quantiles
     assert client.tdigest().create("t-digest", 100)
     assert client.tdigest().add("t-digest", [1, 2, 3, 4, 5], [1.0] * 5)
-    assert [0.5, 3.0, 0.8, 5.0] == client.tdigest().quantile("t-digest", 0.5, 0.8)
+    assert [3.0, 5.0] == client.tdigest().quantile("t-digest", 0.5, 0.8)
 
 
 @pytest.mark.redismod
diff --git a/tests/test_commands.py b/tests/test_commands.py
index f9134d85..1c9a5c27 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -68,6 +68,14 @@ class TestResponseCallbacks:
 class TestRedisCommands:
     @skip_if_redis_enterprise()
     def test_auth(self, r, request):
+        # sending an AUTH command before setting a user/password on the
+        # server should return an AuthenticationError
+        with pytest.raises(exceptions.AuthenticationError):
+            r.auth("some_password")
+
+        with pytest.raises(exceptions.AuthenticationError):
+            r.auth("some_password", "some_user")
+
         # first, test for default user (`username` is supposed to be optional)
         default_username = "default"
         temp_pass = "temp_pass"
@@ -81,9 +89,19 @@ class TestRedisCommands:
 
         def teardown():
             try:
-                r.auth(temp_pass)
-            except exceptions.ResponseError:
-                r.auth("default", "")
+                # this is needed because after an AuthenticationError the connection
+                # is closed, and if we send an AUTH command a new connection is
+                # created, but in this case we'd get an "Authentication required"
+                # error when switching to the db 9 because we're not authenticated yet
+                # setting the password on the connection itself triggers the
+                # authentication in the connection's `on_connect` method
+                r.connection.password = temp_pass
+            except AttributeError:
+                # connection field is not set in Redis Cluster, but that's ok
+                # because the problem discussed above does not apply to Redis Cluster
+                pass
+
+            r.auth(temp_pass)
             r.config_set("requirepass", "")
             r.acl_deluser(username)
 
@@ -95,7 +113,7 @@ class TestRedisCommands:
 
         assert r.auth(username=username, password="strong_password") is True
 
-        with pytest.raises(exceptions.ResponseError):
+        with pytest.raises(exceptions.AuthenticationError):
             r.auth(username=username, password="wrong_password")
 
     def test_command_on_invalid_key_type(self, r):
@@ -4438,6 +4456,19 @@ class TestRedisCommands:
         )
         assert resp == [0, None, 255]
 
+    @skip_if_server_version_lt("6.0.0")
+    def test_bitfield_ro(self, r: redis.Redis):
+        bf = r.bitfield("a")
+        resp = bf.set("u8", 8, 255).execute()
+        assert resp == [0]
+
+        resp = r.bitfield_ro("a", "u8", 0)
+        assert resp == [0]
+
+        items = [("u4", 8), ("u4", 12), ("u4", 13)]
+        resp = r.bitfield_ro("a", "u8", 0, items)
+        assert resp == [0, 15, 15, 14]
+
     @skip_if_server_version_lt("4.0.0")
     def test_memory_help(self, r):
         with pytest.raises(NotImplementedError):
diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py
index a836f5b2..73eb6e19 100644
--- a/tests/test_connection_pool.py
+++ b/tests/test_connection_pool.py
@@ -339,6 +339,14 @@ class TestConnectionPoolURLParsing:
             "(redis://, rediss://, unix://)"
         )
 
+    def test_invalid_scheme_raises_error_when_double_slash_missing(self):
+        with pytest.raises(ValueError) as cm:
+            redis.ConnectionPool.from_url("redis:foo.bar.com:12345")
+        assert str(cm.value) == (
+            "Redis URL must specify one of the following schemes "
+            "(redis://, rediss://, unix://)"
+        )
+
 
 class TestConnectionPoolUnixSocketURLParsing:
     def test_defaults(self):
@@ -545,22 +553,40 @@ class TestConnection:
         )
 
     @skip_if_redis_enterprise()
-    def test_connect_no_auth_supplied_when_required(self, r):
+    def test_connect_no_auth_configured(self, r):
         """
-        AuthenticationError should be raised when the server requires a
-        password but one isn't supplied.
+        AuthenticationError should be raised when the server is not configured with auth
+        but credentials are supplied by the user.
         """
+        # Redis < 6
         with pytest.raises(redis.AuthenticationError):
             r.execute_command(
                 "DEBUG", "ERROR", "ERR Client sent AUTH, but no password is set"
             )
 
+        # Redis >= 6
+        with pytest.raises(redis.AuthenticationError):
+            r.execute_command(
+                "DEBUG",
+                "ERROR",
+                "ERR AUTH <password> called without any password "
+                "configured for the default user. Are you sure "
+                "your configuration is correct?",
+            )
+
     @skip_if_redis_enterprise()
-    def test_connect_invalid_password_supplied(self, r):
-        "AuthenticationError should be raised when sending the wrong password"
+    def test_connect_invalid_auth_credentials_supplied(self, r):
+        """
+        AuthenticationError should be raised when sending invalid username/password
+        """
+        # Redis < 6
         with pytest.raises(redis.AuthenticationError):
             r.execute_command("DEBUG", "ERROR", "ERR invalid password")
 
+        # Redis >= 6
+        with pytest.raises(redis.AuthenticationError):
+            r.execute_command("DEBUG", "ERROR", "WRONGPASS")
+
 
 @pytest.mark.onlynoncluster
 class TestMultiConnectionClient:
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 526308c6..a46c336d 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -366,11 +366,11 @@ def test_list_keys(client):
     result = client.graph().list_keys()
     assert result == []
 
-    client.execute_command("GRAPH.EXPLAIN", "G", "RETURN 1")
+    client.graph("G").query("CREATE (n)")
     result = client.graph().list_keys()
     assert result == ["G"]
 
-    client.execute_command("GRAPH.EXPLAIN", "X", "RETURN 1")
+    client.graph("X").query("CREATE (m)")
     result = client.graph().list_keys()
     assert result == ["G", "X"]
 
diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py
index b4b85e17..cb27a752 100644
--- a/tests/test_timeseries.py
+++ b/tests/test_timeseries.py
@@ -233,7 +233,7 @@ def test_range_advanced(client):
     assert [(0, 5.0), (5, 6.0)] == client.ts().range(
         1, 0, 10, aggregation_type="count", bucket_size_msec=10, align=5
     )
-    assert [(0, 2.5500000000000003), (10, 3.95)] == client.ts().range(
+    assert [(0, 2.5500000000000003), (10, 3.0)] == client.ts().range(
         1, 0, 10, aggregation_type="twa", bucket_size_msec=10
     )