Repository: redis/redis-py
Pull request #1595 (2021-10-08 14:19:17)

Update docs for multiple usernames for ACL DELUSER
### Pull Request check-list

_Please make sure to review and check all of these items:_

- [ ] Does `$ tox` pass with this change (including linting)?
- [ ] Do the CI tests pass with this change (enable it first in your forked repo and wait for the github action build to finish)?
- [ ] Is the new or changed code fully tested?
- [ ] Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

_NOTE: these things are not required to open a PR and can be done
afterwards / while the PR is open._

### Description of change

_Please provide a description of the change here._



Patch:
diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml
new file mode 100644
index 00000000..f17a2992
--- /dev/null
+++ b/.github/release-drafter-config.yml
@@ -0,0 +1,44 @@
+name-template: 'Version $NEXT_PATCH_VERSION'
+tag-template: 'v$NEXT_PATCH_VERSION'
+autolabeler:
+  - label: 'chore'
+    files:
+      - '*.md'
+      - '.github/*'
+  - label: 'bug'
+    branch:
+      - '/bug-.+'
+  - label: 'chore'
+    branch:
+      - '/chore-.+'
+  - label: 'feature'
+    branch:
+      - '/feature-.+'
+categories:
+  - title: 'Breaking Changes'
+    labels:
+      - 'breakingchange'
+  - title: '🚀 New Features'
+    labels:
+      - 'feature'
+      - 'enhancement'
+  - title: '🐛 Bug Fixes'
+    labels:
+      - 'fix'
+      - 'bugfix'
+      - 'bug'
+  - title: '🧰 Maintenance'
+    label: 'chore'
+change-template: '- $TITLE (#$NUMBER)'
+exclude-labels:
+  - 'skip-changelog'
+template: |
+  ## Changes
+
+  $CHANGES
+
+  ## Contributors
+  We'd like to thank all the contributors who worked on this release!
+
+  $CONTRIBUTORS
+
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
new file mode 100644
index 00000000..ec2d88bf
--- /dev/null
+++ b/.github/workflows/release-drafter.yml
@@ -0,0 +1,19 @@
+name: Release Drafter
+
+on:
+  push:
+    # branches to consider in the event; optional, defaults to all
+    branches:
+      - master
+
+jobs:
+  update_release_draft:
+    runs-on: ubuntu-latest
+    steps:
+      # Drafts your next Release notes as Pull Requests are merged into "master"
+      - uses: release-drafter/release-drafter@v5
+        with:
+          # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
+           config-name: release-drafter-config.yml
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CHANGES b/CHANGES
index 057423da..801e2f12 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,16 +4,27 @@
       urllib.parse.unquote. Prior versions of redis-py supported this by
       specifying the ``decode_components`` flag to the ``from_url`` functions.
       This is now done by default and cannot be disabled. #589
-    * POTENTIALLY INCOMPATIBLE: Redis commands were moved into a mixin 
-      (see commands.py). Anyone importing ``redis.client`` to access commands 
-      directly should import ``redis.commands``.
+    * POTENTIALLY INCOMPATIBLE: Redis commands were moved into a mixin
+      (see commands.py). Anyone importing ``redis.client`` to access commands
+      directly should import ``redis.commands``. #1534, #1550
+    * Removed technical debt on REDIS_6_VERSION placeholder. Thanks @chayim #1582.
+    * Various docus fixes. Thanks @Andrew-Chen-Wang #1585, #1586.
+    * Support for LOLWUT command, available since Redis 5.0.0.
+      Thanks @brainix #1568.
+    * Added support for CLIENT REPLY, available in Redis 3.2.0.
+      Thanks @chayim #1581.
+    * Support for Auto-reconnect PubSub on get_message. Thanks @luhn #1574.
+    * Fix RST syntax error in README/ Thanks @JanCBrammer #1451.
+    * IDLETIME and FREQ support for RESTORE. Thanks @chayim #1580.
+    * Supporting args with MODULE LOAD. Thanks @chayim #1579.
+    * Updating RedisLabs with Redis. Thanks @gkorland #1575.
     * Added support for ASYNC to SCRIPT FLUSH available in Redis 6.2.0.
       Thanks @chayim. #1567
-    * Added CLIENT LIST fix to support multiple client ids available in 
+    * Added CLIENT LIST fix to support multiple client ids available in
       Redis 2.8.12. Thanks @chayim #1563.
     * Added DISCARD support for pipelines available in Redis 2.0.0.
       Thanks @chayim #1565.
-    * Added ACL DELUSER support for deleting lists of users available in 
+    * Added ACL DELUSER support for deleting lists of users available in
       Redis 6.2.0. Thanks @chayim. #1562
     * Added CLIENT TRACKINFO support available in Redis 6.2.0.
       Thanks @chayim. #1560
@@ -31,10 +42,10 @@
       Thanks @ian28223 #1489.
     * Added support for STRALGO available in Redis 6.0.0.
       Thanks @AvitalFineRedis. #1528
-    * Addes support for ZMSCORE available in Redis 6.2.0. 
+    * Addes support for ZMSCORE available in Redis 6.2.0.
       Thanks @2014BDuck and @jiekun.zhu. #1437
-    * Support MINID and LIMIT on XADD available in Redis 6.2.0. 
-      Thanks @AvitalFineRedis. #1548  
+    * Support MINID and LIMIT on XADD available in Redis 6.2.0.
+      Thanks @AvitalFineRedis. #1548
     * Added sentinel commands FLUSHCONFIG, CKQUORUM, FAILOVER, and RESET
       available in Redis 2.8.12.
       Thanks @otherpirate. #834
diff --git a/README.rst b/README.rst
index 710b8a68..821a2538 100644
--- a/README.rst
+++ b/README.rst
@@ -3,14 +3,14 @@ redis-py
 
 The Python interface to the Redis key-value store.
 
-.. image:: https://github.com/andymccurdy/redis-py/workflows/CI/badge.svg?branch=master
-        :target: https://github.com/andymccurdy/redis-py/actions?query=workflow%3ACI+branch%3Amaster
+.. image:: https://github.com/redis/redis-py/workflows/CI/badge.svg?branch=master
+        :target: https://github.com/redis/redis-py/actions?query=workflow%3ACI+branch%3Amaster
 .. image:: https://readthedocs.org/projects/redis-py/badge/?version=stable&style=flat
         :target: https://redis-py.readthedocs.io/en/stable/
 .. image:: https://badge.fury.io/py/redis.svg
         :target: https://pypi.org/project/redis/
-.. image:: https://codecov.io/gh/andymccurdy/redis-py/branch/master/graph/badge.svg
-  :target: https://codecov.io/gh/andymccurdy/redis-py
+.. image:: https://codecov.io/gh/redis/redis-py/branch/master/graph/badge.svg
+  :target: https://codecov.io/gh/redis/redis-py
 
 
 Python 2 Compatibility Note
@@ -51,7 +51,7 @@ Contributing
 ------------
 
 Want to contribute a feature, bug report, or report an issue? Check out our `guide to
-contributing <https://github.com/andymccurdy/redis-py/blob/master/CONTRIBUTING.rst>`_.
+contributing <https://github.com/redis/redis-py/blob/master/CONTRIBUTING.rst>`_.
 
 
 Getting Started
@@ -127,7 +127,7 @@ SSL Connections
 
 redis-py 3.0 changes the default value of the `ssl_cert_reqs` option from
 `None` to `'required'`. See
-`Issue 1016 <https://github.com/andymccurdy/redis-py/issues/1016>`_. This
+`Issue 1016 <https://github.com/redis/redis-py/issues/1016>`_. This
 change enforces hostname validation when accepting a cert from a remote SSL
 terminator. If the terminator doesn't properly set the hostname on the cert
 this will cause redis-py 3.0 to raise a ConnectionError.
@@ -284,7 +284,7 @@ to the official command syntax. There are a few exceptions:
   will return a PubSub instance where you can subscribe to channels and listen
   for messages. You can only call PUBLISH from the Redis client (see
   `this comment on issue #151
-  <https://github.com/andymccurdy/redis-py/issues/151#issuecomment-1545015>`_
+  <https://github.com/redis/redis-py/issues/151#issuecomment-1545015>`_
   for details).
 * **SCAN/SSCAN/HSCAN/ZSCAN**: The \*SCAN commands are implemented as they
   exist in the Redis documentation. In addition, each command has an equivalent
@@ -952,7 +952,7 @@ Author
 ^^^^^^
 
 redis-py is developed and maintained by Andy McCurdy (sedrik@gmail.com).
-It can be found here: https://github.com/andymccurdy/redis-py
+It can be found here: https://github.com/redis/redis-py
 
 Special thanks to:
 
diff --git a/redis/__init__.py b/redis/__init__.py
index 9f67f51d..a3b5eb67 100644
--- a/redis/__init__.py
+++ b/redis/__init__.py
@@ -31,7 +31,7 @@ def int_or_str(value):
         return value
 
 
-__version__ = '3.5.3'
+__version__ = '4.0.0b1'
 VERSION = tuple(map(int_or_str, __version__.split('.')))
 
 __all__ = [
diff --git a/redis/client.py b/redis/client.py
index cbdb13d4..2e2a26a2 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -1862,100 +1862,3 @@ class Script:
             # Overwrite the sha just in case there was a discrepancy.
             self.sha = client.script_load(self.script)
             return client.evalsha(self.sha, len(keys), *args)
-
-
-class BitFieldOperation:
-    """
-    Command builder for BITFIELD commands.
-    """
-    def __init__(self, client, key, default_overflow=None):
-        self.client = client
-        self.key = key
-        self._default_overflow = default_overflow
-        self.reset()
-
-    def reset(self):
-        """
-        Reset the state of the instance to when it was constructed
-        """
-        self.operations = []
-        self._last_overflow = 'WRAP'
-        self.overflow(self._default_overflow or self._last_overflow)
-
-    def overflow(self, overflow):
-        """
-        Update the overflow algorithm of successive INCRBY operations
-        :param overflow: Overflow algorithm, one of WRAP, SAT, FAIL. See the
-            Redis docs for descriptions of these algorithmsself.
-        :returns: a :py:class:`BitFieldOperation` instance.
-        """
-        overflow = overflow.upper()
-        if overflow != self._last_overflow:
-            self._last_overflow = overflow
-            self.operations.append(('OVERFLOW', overflow))
-        return self
-
-    def incrby(self, fmt, offset, increment, overflow=None):
-        """
-        Increment a bitfield by a given amount.
-        :param fmt: format-string for the bitfield being updated, e.g. 'u8'
-            for an unsigned 8-bit integer.
-        :param offset: offset (in number of bits). If prefixed with a
-            '#', this is an offset multiplier, e.g. given the arguments
-            fmt='u8', offset='#2', the offset will be 16.
-        :param int increment: value to increment the bitfield by.
-        :param str overflow: overflow algorithm. Defaults to WRAP, but other
-            acceptable values are SAT and FAIL. See the Redis docs for
-            descriptions of these algorithms.
-        :returns: a :py:class:`BitFieldOperation` instance.
-        """
-        if overflow is not None:
-            self.overflow(overflow)
-
-        self.operations.append(('INCRBY', fmt, offset, increment))
-        return self
-
-    def get(self, fmt, offset):
-        """
-        Get the value of a given bitfield.
-        :param fmt: format-string for the bitfield being read, e.g. 'u8' for
-            an unsigned 8-bit integer.
-        :param offset: offset (in number of bits). If prefixed with a
-            '#', this is an offset multiplier, e.g. given the arguments
-            fmt='u8', offset='#2', the offset will be 16.
-        :returns: a :py:class:`BitFieldOperation` instance.
-        """
-        self.operations.append(('GET', fmt, offset))
-        return self
-
-    def set(self, fmt, offset, value):
-        """
-        Set the value of a given bitfield.
-        :param fmt: format-string for the bitfield being read, e.g. 'u8' for
-            an unsigned 8-bit integer.
-        :param offset: offset (in number of bits). If prefixed with a
-            '#', this is an offset multiplier, e.g. given the arguments
-            fmt='u8', offset='#2', the offset will be 16.
-        :param int value: value to set at the given position.
-        :returns: a :py:class:`BitFieldOperation` instance.
-        """
-        self.operations.append(('SET', fmt, offset, value))
-        return self
-
-    @property
-    def command(self):
-        cmd = ['BITFIELD', self.key]
-        for ops in self.operations:
-            cmd.extend(ops)
-        return cmd
-
-    def execute(self):
-        """
-        Execute the operation(s) in a single BITFIELD command. The return value
-        is a list of values corresponding to each operation. If the client
-        used to create this instance was a pipeline, the list of values
-        will be present within the pipeline's execute.
-        """
-        command = self.command
-        self.reset()
-        return self.client.execute_command(*command)
diff --git a/redis/commands.py b/redis/commands.py
index eb7cea54..f20acfe2 100644
--- a/redis/commands.py
+++ b/redis/commands.py
@@ -49,7 +49,7 @@ class Commands:
         return self.execute_command('ACL CAT', *pieces)
 
     def acl_deluser(self, *username):
-        "Delete the ACL for the specified ``username``"
+        "Delete the ACL for the specified ``username``s"
         return self.execute_command('ACL DELUSER', *username)
 
     def acl_genpass(self, bits=None):
@@ -310,10 +310,10 @@ class Commands:
         'master', 'slave' or 'pubsub'
         :param addr: Kills a client by its 'address:port'
         :param skipme: If True, then the client calling the command
-        :param laddr: Kills a client by its 'local (bind)  address:port'
-        :param user: Kills a client for a specific user name
         will not get killed even if it is identified by one of the filter
         options. If skipme is not provided, the server defaults to skipme=True
+        :param laddr: Kills a client by its 'local (bind) address:port'
+        :param user: Kills a client for a specific user name
         """
         args = []
         if _type is not None:
@@ -355,6 +355,7 @@ class Commands:
         If type of client specified, only that type will be returned.
         :param _type: optional. one of the client types (normal, master,
          replica, pubsub)
+        :param client_id: optional. a list of client ids
         """
         "Returns a list of currently connected clients"
         args = []
@@ -373,7 +374,7 @@ class Commands:
         return self.execute_command('CLIENT LIST', *args)
 
     def client_getname(self):
-        "Returns the current connection name"
+        """Returns the current connection name"""
         return self.execute_command('CLIENT GETNAME')
 
     def client_reply(self, reply):
@@ -396,11 +397,12 @@ class Commands:
         return self.execute_command("CLIENT REPLY", reply)
 
     def client_id(self):
-        "Returns the current connection id"
+        """Returns the current connection id"""
         return self.execute_command('CLIENT ID')
 
     def client_trackinginfo(self):
-        """Returns the information about the current client connection's
+        """
+        Returns the information about the current client connection's
         use of the server assisted client side cache.
         See https://redis.io/commands/client-trackinginfo
         """
@@ -438,15 +440,19 @@ class Commands:
         return self.execute_command('CLIENT UNPAUSE')
 
     def readwrite(self):
-        "Disables read queries for a connection to a Redis Cluster slave node"
+        """
+        Disables read queries for a connection to a Redis Cluster slave node.
+        """
         return self.execute_command('READWRITE')
 
     def readonly(self):
-        "Enables read queries for a connection to a Redis Cluster replica node"
+        """
+        Enables read queries for a connection to a Redis Cluster replica node.
+        """
         return self.execute_command('READONLY')
 
     def config_get(self, pattern="*"):
-        "Return a dictionary of configuration based on the ``pattern``"
+        """Return a dictionary of configuration based on the ``pattern``"""
         return self.execute_command('CONFIG GET', pattern)
 
     def config_set(self, name, value):
@@ -454,23 +460,25 @@ class Commands:
         return self.execute_command('CONFIG SET', name, value)
 
     def config_resetstat(self):
-        "Reset runtime statistics"
+        """Reset runtime statistics"""
         return self.execute_command('CONFIG RESETSTAT')
 
     def config_rewrite(self):
-        "Rewrite config file with the minimal change to reflect running config"
+        """
+        Rewrite config file with the minimal change to reflect running config.
+        """
         return self.execute_command('CONFIG REWRITE')
 
     def dbsize(self):
-        "Returns the number of keys in the current database"
+        """Returns the number of keys in the current database"""
         return self.execute_command('DBSIZE')
 
     def debug_object(self, key):
-        "Returns version specific meta information about a given key"
+        """Returns version specific meta information about a given key"""
         return self.execute_command('DEBUG OBJECT', key)
 
     def echo(self, value):
-        "Echo the string back from the server"
+        """Echo the string back from the server"""
         return self.execute_command('ECHO', value)
 
     def flushall(self, asynchronous=False):
@@ -524,7 +532,8 @@ class Commands:
         return self.execute_command('LASTSAVE')
 
     def lolwut(self, *version_numbers):
-        """Get the Redis version and a piece of generative computer art
+        """
+        Get the Redis version and a piece of generative computer art
         See: https://redis.io/commands/lolwut
         """
         if version_numbers:
@@ -568,13 +577,27 @@ class Commands:
                                     timeout, *pieces)
 
     def object(self, infotype, key):
-        "Return the encoding, idletime, or refcount about the key"
+        """Return the encoding, idletime, or refcount about the key"""
         return self.execute_command('OBJECT', infotype, key, infotype=infotype)
 
+    def memory_doctor(self):
+        raise NotImplementedError(
+            "MEMORY DOCTOR is intentionally not implemented in the client."
+        )
+
+    def memory_help(self):
+        raise NotImplementedError(
+            "MEMORY HELP is intentionally not implemented in the client."
+        )
+
     def memory_stats(self):
-        "Return a dictionary of memory stats"
+        """Return a dictionary of memory stats"""
         return self.execute_command('MEMORY STATS')
 
+    def memory_malloc_stats(self):
+        """Return an internal statistics report from the memory allocator."""
+        return self.execute_command('MEMORY MALLOC-STATS')
+
     def memory_usage(self, key, samples=None):
         """
         Return the total memory usage for key, its value and associated
@@ -590,15 +613,16 @@ class Commands:
         return self.execute_command('MEMORY USAGE', key, *args)
 
     def memory_purge(self):
-        "Attempts to purge dirty pages for reclamation by allocator"
+        """Attempts to purge dirty pages for reclamation by allocator"""
         return self.execute_command('MEMORY PURGE')
 
     def ping(self):
-        "Ping the Redis server"
+        """Ping the Redis server"""
         return self.execute_command('PING')
 
     def quit(self):
-        """Ask the server to close the connection.
+        """
+        Ask the server to close the connection.
         https://redis.io/commands/quit
         """
         return self.execute_command('QUIT')
@@ -1049,7 +1073,7 @@ class Commands:
         return self.execute_command("HRANDFIELD", key, *params)
 
     def randomkey(self):
-        "Returns the name of a random key"
+        """Returns the name of a random key"""
         return self.execute_command('RANDOMKEY')
 
     def rename(self, src, dst):
@@ -1059,7 +1083,7 @@ class Commands:
         return self.execute_command('RENAME', src, dst)
 
     def renamenx(self, src, dst):
-        "Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist"
+        """Rename key ``src`` to ``dst`` if ``dst`` doesn't already exist"""
         return self.execute_command('RENAMENX', src, dst)
 
     def restore(self, name, ttl, value, replace=False, absttl=False,
@@ -1122,7 +1146,7 @@ class Commands:
             (Available since Redis 6.0)
 
         ``get`` if True, set the value at key ``name`` to ``value`` and return
-            the old value stored at key, or None when key did not exist.
+            the old value stored at key, or None if the key did not exist.
             (Available since Redis 6.2)
 
         ``exat`` sets an expire flag on key ``name`` for ``ex`` seconds,
@@ -1521,32 +1545,25 @@ class Commands:
 
         pieces = [name]
         if by is not None:
-            pieces.append(b'BY')
-            pieces.append(by)
+            pieces.extend([b'BY', by])
         if start is not None and num is not None:
-            pieces.append(b'LIMIT')
-            pieces.append(start)
-            pieces.append(num)
+            pieces.extend([b'LIMIT', start, num])
         if get is not None:
             # If get is a string assume we want to get a single value.
             # Otherwise assume it's an interable and we want to get multiple
             # values. We can't just iterate blindly because strings are
             # iterable.
             if isinstance(get, (bytes, str)):
-                pieces.append(b'GET')
-                pieces.append(get)
+                pieces.extend([b'GET', get])
             else:
                 for g in get:
-                    pieces.append(b'GET')
-                    pieces.append(g)
+                    pieces.extend([b'GET', g])
         if desc:
             pieces.append(b'DESC')
         if alpha:
             pieces.append(b'ALPHA')
         if store is not None:
-            pieces.append(b'STORE')
-            pieces.append(store)
-
+            pieces.extend([b'STORE', store])
         if groups:
             if not get or isinstance(get, (bytes, str)) or len(get) < 2:
                 raise DataError('when using "groups" the "get" argument '
@@ -1705,15 +1722,15 @@ class Commands:
 
     # SET COMMANDS
     def sadd(self, name, *values):
-        "Add ``value(s)`` to set ``name``"
+        """Add ``value(s)`` to set ``name``"""
         return self.execute_command('SADD', name, *values)
 
     def scard(self, name):
-        "Return the number of elements in set ``name``"
+        """Return the number of elements in set ``name``"""
         return self.execute_command('SCARD', name)
 
     def sdiff(self, keys, *args):
-        "Return the difference of sets specified by ``keys``"
+        """Return the difference of sets specified by ``keys``"""
         args = list_or_args(keys, args)
         return self.execute_command('SDIFF', *args)
 
@@ -1726,7 +1743,7 @@ class Commands:
         return self.execute_command('SDIFFSTORE', dest, *args)
 
     def sinter(self, keys, *args):
-        "Return the intersection of sets specified by ``keys``"
+        """Return the intersection of sets specified by ``keys``"""
         args = list_or_args(keys, args)
         return self.execute_command('SINTER', *args)
 
@@ -1739,15 +1756,17 @@ class Commands:
         return self.execute_command('SINTERSTORE', dest, *args)
 
     def sismember(self, name, value):
-        "Return a boolean indicating if ``value`` is a member of set ``name``"
+        """
+        Return a boolean indicating if ``value`` is a member of set ``name``
+        """
         return self.execute_command('SISMEMBER', name, value)
 
     def smembers(self, name):
-        "Return all members of the set ``name``"
+        """Return all members of the set ``name``"""
         return self.execute_command('SMEMBERS', name)
 
     def smove(self, src, dst, value):
-        "Move ``value`` from set ``src`` to set ``dst`` atomically"
+        """Move ``value`` from set ``src`` to set ``dst`` atomically"""
         return self.execute_command('SMOVE', src, dst, value)
 
     def spop(self, name, count=None):
@@ -1826,8 +1845,7 @@ class Commands:
                 pieces.append(b'~')
             pieces.append(minid)
         if limit is not None:
-            pieces.append(b"LIMIT")
-            pieces.append(limit)
+            pieces.extend([b'LIMIT', limit])
         if nomkstream:
             pieces.append(b'NOMKSTREAM')
         pieces.append(id)
@@ -2189,7 +2207,7 @@ class Commands:
         """
         pieces = []
         if maxlen is not None and minid is not None:
-            raise DataError("Only one of ```maxlen``` or ```minid```",
+            raise DataError("Only one of ``maxlen`` or ``minid`` "
                             "may be specified")
 
         if maxlen is not None:
@@ -2416,41 +2434,113 @@ class Commands:
         keys.append(timeout)
         return self.execute_command('BZPOPMIN', *keys)
 
+    def _zrange(self, command, dest, name, start, end, desc=False,
+                byscore=False, bylex=False, withscores=False,
+                score_cast_func=float, offset=None, num=None):
+        if byscore and bylex:
+            raise DataError("``byscore`` and ``bylex`` can not be "
+                            "specified together.")
+        if (offset is not None and num is None) or \
+                (num is not None and offset is None):
+            raise DataError("``offset`` and ``num`` must both be specified.")
+        if bylex and withscores:
+            raise DataError("``withscores`` not supported in combination "
+                            "with ``bylex``.")
+        pieces = [command]
+        if dest:
+            pieces.append(dest)
+        pieces.extend([name, start, end])
+        if byscore:
+            pieces.append('BYSCORE')
+        if bylex:
+            pieces.append('BYLEX')
+        if desc:
+            pieces.append('REV')
+        if offset is not None and num is not None:
+            pieces.extend(['LIMIT', offset, num])
+        if withscores:
+            pieces.append('WITHSCORES')
+        options = {
+            'withscores': withscores,
+            'score_cast_func': score_cast_func
+        }
+        return self.execute_command(*pieces, **options)
+
     def zrange(self, name, start, end, desc=False, withscores=False,
-               score_cast_func=float):
+               score_cast_func=float, byscore=False, bylex=False,
+               offset=None, num=None):
         """
         Return a range of values from sorted set ``name`` between
         ``start`` and ``end`` sorted in ascending order.
 
         ``start`` and ``end`` can be negative, indicating the end of the range.
 
-        ``desc`` a boolean indicating whether to sort the results descendingly
+        ``desc`` a boolean indicating whether to sort the results in reversed
+        order.
 
         ``withscores`` indicates to return the scores along with the values.
+        The return type is a list of (value, score) pairs.
+
+        ``score_cast_func`` a callable used to cast the score return value.
+
+        ``byscore`` when set to True, returns the range of elements from the
+        sorted set having scores equal or between ``start`` and ``end``.
+
+        ``bylex`` when set to True, returns the range of elements from the
+        sorted set between the ``start`` and ``end`` lexicographical closed
+        range intervals.
+        Valid ``start`` and ``end`` must start with ( or [, in order to specify
+        whether the range interval is exclusive or inclusive, respectively.
+
+        ``offset`` and ``num`` are specified, then return a slice of the range.
+        Can't be provided when using ``bylex``.
+        """
+        return self._zrange('ZRANGE', None, name, start, end, desc, byscore,
+                            bylex, withscores, score_cast_func, offset, num)
+
+    def zrevrange(self, name, start, end, withscores=False,
+                  score_cast_func=float):
+        """
+        Return a range of values from sorted set ``name`` between
+        ``start`` and ``end`` sorted in descending order.
+
+        ``start`` and ``end`` can be negative, indicating the end of the range.
+
+        ``withscores`` indicates to return the scores along with the values
         The return type is a list of (value, score) pairs
 
         ``score_cast_func`` a callable used to cast the score return value
         """
-        if desc:
-            return self.zrevrange(name, start, end, withscores,
-                                  score_cast_func)
-        pieces = ['ZRANGE', name, start, end]
-        if withscores:
-            pieces.append(b'WITHSCORES')
-        options = {
-            'withscores': withscores,
-            'score_cast_func': score_cast_func
-        }
-        return self.execute_command(*pieces, **options)
+        return self.zrange(name, start, end, desc=True,
+                           withscores=withscores,
+                           score_cast_func=score_cast_func)
 
-    def zrangestore(self, dest, name, start, end):
+    def zrangestore(self, dest, name, start, end,
+                    byscore=False, bylex=False, desc=False,
+                    offset=None, num=None):
         """
         Stores in ``dest`` the result of a range of values from sorted set
         ``name`` between ``start`` and ``end`` sorted in ascending order.
 
         ``start`` and ``end`` can be negative, indicating the end of the range.
+
+        ``byscore`` when set to True, returns the range of elements from the
+        sorted set having scores equal or between ``start`` and ``end``.
+
+        ``bylex`` when set to True, returns the range of elements from the
+        sorted set between the ``start`` and ``end`` lexicographical closed
+        range intervals.
+        Valid ``start`` and ``end`` must start with ( or [, in order to specify
+        whether the range interval is exclusive or inclusive, respectively.
+
+        ``desc`` a boolean indicating whether to sort the results in reversed
+        order.
+
+        ``offset`` and ``num`` are specified, then return a slice of the range.
+        Can't be provided when using ``bylex``.
         """
-        return self.execute_command('ZRANGESTORE', dest, name, start, end)
+        return self._zrange('ZRANGESTORE', dest, name, start, end, desc,
+                            byscore, bylex, False, None, offset, num)
 
     def zrangebylex(self, name, min, max, start=None, num=None):
         """
@@ -2460,13 +2550,7 @@ class Commands:
         If ``start`` and ``num`` are specified, then return a slice of the
         range.
         """
-        if (start is not None and num is None) or \
-                (num is not None and start is None):
-            raise DataError("``start`` and ``num`` must both be specified")
-        pieces = ['ZRANGEBYLEX', name, min, max]
-        if start is not None and num is not None:
-            pieces.extend([b'LIMIT', start, num])
-        return self.execute_command(*pieces)
+        return self.zrange(name, min, max, bylex=True, offset=start, num=num)
 
     def zrevrangebylex(self, name, max, min, start=None, num=None):
         """
@@ -2476,13 +2560,8 @@ class Commands:
         If ``start`` and ``num`` are specified, then return a slice of the
         range.
         """
-        if (start is not None and num is None) or \
-                (num is not None and start is None):
-            raise DataError("``start`` and ``num`` must both be specified")
-        pieces = ['ZREVRANGEBYLEX', name, max, min]
-        if start is not None and num is not None:
-            pieces.extend([b'LIMIT', start, num])
-        return self.execute_command(*pieces)
+        return self.zrange(name, max, min, desc=True,
+                           bylex=True, offset=start, num=num)
 
     def zrangebyscore(self, name, min, max, start=None, num=None,
                       withscores=False, score_cast_func=float):
@@ -2498,19 +2577,29 @@ class Commands:
 
         `score_cast_func`` a callable used to cast the score return value
         """
-        if (start is not None and num is None) or \
-                (num is not None and start is None):
-            raise DataError("``start`` and ``num`` must both be specified")
-        pieces = ['ZRANGEBYSCORE', name, min, max]
-        if start is not None and num is not None:
-            pieces.extend([b'LIMIT', start, num])
-        if withscores:
-            pieces.append(b'WITHSCORES')
-        options = {
-            'withscores': withscores,
-            'score_cast_func': score_cast_func
-        }
-        return self.execute_command(*pieces, **options)
+        return self.zrange(name, min, max, byscore=True,
+                           offset=start, num=num,
+                           withscores=withscores,
+                           score_cast_func=score_cast_func)
+
+    def zrevrangebyscore(self, name, max, min, start=None, num=None,
+                         withscores=False, score_cast_func=float):
+        """
+        Return a range of values from the sorted set ``name`` with scores
+        between ``min`` and ``max`` in descending order.
+
+        If ``start`` and ``num`` are specified, then return a slice
+        of the range.
+
+        ``withscores`` indicates to return the scores along with the values.
+        The return type is a list of (value, score) pairs
+
+        ``score_cast_func`` a callable used to cast the score return value
+        """
+        return self.zrange(name, max, min, desc=True,
+                           byscore=True, offset=start,
+                           num=num, withscores=withscores,
+                           score_cast_func=score_cast_func)
 
     def zrank(self, name, value):
         """
@@ -2548,56 +2637,6 @@ class Commands:
         """
         return self.execute_command('ZREMRANGEBYSCORE', name, min, max)
 
-    def zrevrange(self, name, start, end, withscores=False,
-                  score_cast_func=float):
-        """
-        Return a range of values from sorted set ``name`` between
-        ``start`` and ``end`` sorted in descending order.
-
-        ``start`` and ``end`` can be negative, indicating the end of the range.
-
-        ``withscores`` indicates to return the scores along with the values
-        The return type is a list of (value, score) pairs
-
-        ``score_cast_func`` a callable used to cast the score return value
-        """
-        pieces = ['ZREVRANGE', name, start, end]
-        if withscores:
-            pieces.append(b'WITHSCORES')
-        options = {
-            'withscores': withscores,
-            'score_cast_func': score_cast_func
-        }
-        return self.execute_command(*pieces, **options)
-
-    def zrevrangebyscore(self, name, max, min, start=None, num=None,
-                         withscores=False, score_cast_func=float):
-        """
-        Return a range of values from the sorted set ``name`` with scores
-        between ``min`` and ``max`` in descending order.
-
-        If ``start`` and ``num`` are specified, then return a slice
-        of the range.
-
-        ``withscores`` indicates to return the scores along with the values.
-        The return type is a list of (value, score) pairs
-
-        ``score_cast_func`` a callable used to cast the score return value
-        """
-        if (start is not None and num is None) or \
-                (num is not None and start is None):
-            raise DataError("``start`` and ``num`` must both be specified")
-        pieces = ['ZREVRANGEBYSCORE', name, max, min]
-        if start is not None and num is not None:
-            pieces.extend([b'LIMIT', start, num])
-        if withscores:
-            pieces.append(b'WITHSCORES')
-        options = {
-            'withscores': withscores,
-            'score_cast_func': score_cast_func
-        }
-        return self.execute_command(*pieces, **options)
-
     def zrevrank(self, name, value):
         """
         Returns a 0-based value indicating the descending rank of
@@ -2842,7 +2881,7 @@ class Commands:
         See: https://redis.io/commands/script-flush
         """
         if sync_type not in ["SYNC", "ASYNC"]:
-            raise DataError("SCRIPT FLUSH defaults to SYNC or"
+            raise DataError("SCRIPT FLUSH defaults to SYNC or "
                             "accepts SYNC/ASYNC")
         pieces = [sync_type]
         return self.execute_command('SCRIPT FLUSH', *pieces)
@@ -2865,17 +2904,39 @@ class Commands:
         return Script(self, script)
 
     # GEO COMMANDS
-    def geoadd(self, name, *values):
+    def geoadd(self, name, values, nx=False, xx=False, ch=False):
         """
         Add the specified geospatial items to the specified key identified
         by the ``name`` argument. The Geospatial items are given as ordered
         members of the ``values`` argument, each item or place is formed by
         the triad longitude, latitude and name.
+
+        Note: You can use ZREM to remove elements.
+
+        ``nx`` forces ZADD to only create new elements and not to update
+        scores for elements that already exist.
+
+        ``xx`` forces ZADD to only update scores of elements that already
+        exist. New elements will not be added.
+
+        ``ch`` modifies the return value to be the numbers of elements changed.
+        Changed elements include new elements that were added and elements
+        whose scores changed.
         """
+        if nx and xx:
+            raise DataError("GEOADD allows either 'nx' or 'xx', not both")
         if len(values) % 3 != 0:
             raise DataError("GEOADD requires places with lon, lat and name"
                             " values")
-        return self.execute_command('GEOADD', name, *values)
+        pieces = [name]
+        if nx:
+            pieces.append('NX')
+        if xx:
+            pieces.append('XX')
+        if ch:
+            pieces.append('CH')
+        pieces.extend(values)
+        return self.execute_command('GEOADD', *pieces)
 
     def geodist(self, name, place1, place2, unit=None):
         """
@@ -2908,7 +2969,7 @@ class Commands:
 
     def georadius(self, name, longitude, latitude, radius, unit=None,
                   withdist=False, withcoord=False, withhash=False, count=None,
-                  sort=None, store=None, store_dist=None):
+                  sort=None, store=None, store_dist=None, any=False):
         """
         Return the members of the specified key identified by the
         ``name`` argument which are within the borders of the area specified
@@ -2942,11 +3003,12 @@ class Commands:
                                       unit=unit, withdist=withdist,
                                       withcoord=withcoord, withhash=withhash,
                                       count=count, sort=sort, store=store,
-                                      store_dist=store_dist)
+                                      store_dist=store_dist, any=any)
 
     def georadiusbymember(self, name, member, radius, unit=None,
                           withdist=False, withcoord=False, withhash=False,
-                          count=None, sort=None, store=None, store_dist=None):
+                          count=None, sort=None, store=None, store_dist=None,
+                          any=False):
         """
         This command is exactly like ``georadius`` with the sole difference
         that instead of taking, as the center of the area to query, a longitude
@@ -2958,7 +3020,7 @@ class Commands:
                                       withdist=withdist, withcoord=withcoord,
                                       withhash=withhash, count=count,
                                       sort=sort, store=store,
-                                      store_dist=store_dist)
+                                      store_dist=store_dist, any=any)
 
     def _georadiusgeneric(self, command, *args, **kwargs):
         pieces = list(args)
@@ -2969,21 +3031,26 @@ class Commands:
         else:
             pieces.append('m',)
 
+        if kwargs['any'] and kwargs['count'] is None:
+            raise DataError("``any`` can't be provided without ``count``")
+
         for arg_name, byte_repr in (
-                ('withdist', b'WITHDIST'),
-                ('withcoord', b'WITHCOORD'),
-                ('withhash', b'WITHHASH')):
+                ('withdist', 'WITHDIST'),
+                ('withcoord', 'WITHCOORD'),
+                ('withhash', 'WITHHASH')):
             if kwargs[arg_name]:
                 pieces.append(byte_repr)
 
-        if kwargs['count']:
-            pieces.extend([b'COUNT', kwargs['count']])
+        if kwargs['count'] is not None:
+            pieces.extend(['COUNT', kwargs['count']])
+            if kwargs['any']:
+                pieces.append('ANY')
 
         if kwargs['sort']:
             if kwargs['sort'] == 'ASC':
-                pieces.append(b'ASC')
+                pieces.append('ASC')
             elif kwargs['sort'] == 'DESC':
-                pieces.append(b'DESC')
+                pieces.append('DESC')
             else:
                 raise DataError("GEORADIUS invalid sort")
 
@@ -3116,7 +3183,8 @@ class Commands:
             if kwargs['any']:
                 pieces.append(b'ANY')
         elif kwargs['any']:
-            raise DataError("GEOSEARCH any can't be provided without count")
+            raise DataError("GEOSEARCH ``any`` can't be provided "
+                            "without count")
 
         # other properties
         for arg_name, byte_repr in (
@@ -3136,9 +3204,7 @@ class Commands:
         Passes all ``*args`` to the module, during loading.
         Raises ``ModuleError`` if a module is not found at ``path``.
         """
-        pieces = list(args)
-        pieces.insert(0, path)
-        return self.execute_command('MODULE LOAD', *pieces)
+        return self.execute_command('MODULE LOAD', path, *args)
 
     def module_unload(self, name):
         """
@@ -3154,6 +3220,11 @@ class Commands:
         """
         return self.execute_command('MODULE LIST')
 
+    def command_info(self):
+        raise NotImplementedError(
+            "COMMAND INFO is intentionally not implemented in the client."
+        )
+
     def command_count(self):
         return self.execute_command('COMMAND COUNT')
 
diff --git a/redis/connection.py b/redis/connection.py
index de30f0c6..55285890 100755
--- a/redis/connection.py
+++ b/redis/connection.py
@@ -880,7 +880,13 @@ class UnixDomainSocketConnection(Connection):
                  encoding_errors='strict', decode_responses=False,
                  retry_on_timeout=False,
                  parser_class=DefaultParser, socket_read_size=65536,
-                 health_check_interval=0, client_name=None):
+                 health_check_interval=0, client_name=None,
+                 retry=None):
+        """
+        Initialize a new UnixDomainSocketConnection.
+        To specify a retry policy, first set `retry_on_timeout` to `True`
+        then set `retry` to a valid `Retry` object
+        """
         self.pid = os.getpid()
         self.path = path
         self.db = db
@@ -889,6 +895,14 @@ class UnixDomainSocketConnection(Connection):
         self.password = password
         self.socket_timeout = socket_timeout
         self.retry_on_timeout = retry_on_timeout
+        if retry_on_timeout:
+            if retry is None:
+                self.retry = Retry(NoBackoff(), 1)
+            else:
+                # deep-copy the Retry object as it is mutable
+                self.retry = copy.deepcopy(retry)
+        else:
+            self.retry = Retry(NoBackoff(), 0)
         self.health_check_interval = health_check_interval
         self.next_health_check = 0
         self.encoder = Encoder(encoding, encoding_errors, decode_responses)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index b9091a96..3dc3ea14 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,12 +9,6 @@ from unittest.mock import Mock
 from urllib.parse import urlparse
 
 
-# redis 6 release candidates report a version number of 5.9.x. Use this
-# constant for skip_if decorators as a placeholder until 6.0.0 is officially
-# released
-REDIS_6_VERSION = '5.9.0'
-
-
 REDIS_INFO = {}
 default_redis_url = "redis://localhost:6379/9"
 
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 60b9ff15..802d8e44 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -11,7 +11,6 @@ from redis import exceptions
 
 from .conftest import (
     _get_client,
-    REDIS_6_VERSION,
     skip_if_server_version_gte,
     skip_if_server_version_lt,
     skip_unless_arch_bits,
@@ -68,19 +67,19 @@ class TestRedisCommands:
             r['a']
 
     # SERVER INFORMATION
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_cat_no_category(self, r):
         categories = r.acl_cat()
         assert isinstance(categories, list)
         assert 'read' in categories
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_cat_with_category(self, r):
         commands = r.acl_cat('read')
         assert isinstance(commands, list)
         assert 'get' in commands
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_deluser(self, r, request):
         username = 'redis-py-user'
 
@@ -104,7 +103,7 @@ class TestRedisCommands:
         assert r.acl_getuser(users[3]) is None
         assert r.acl_getuser(users[4]) is None
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_genpass(self, r):
         password = r.acl_genpass()
         assert isinstance(password, str)
@@ -117,7 +116,7 @@ class TestRedisCommands:
         r.acl_genpass(555)
         assert isinstance(password, str)
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_getuser_setuser(self, r, request):
         username = 'redis-py-user'
 
@@ -204,13 +203,13 @@ class TestRedisCommands:
                              hashed_passwords=['-' + hashed_password])
         assert len(r.acl_getuser(username)['passwords']) == 1
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_help(self, r):
         res = r.acl_help()
         assert isinstance(res, list)
         assert len(res) != 0
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_list(self, r, request):
         username = 'redis-py-user'
 
@@ -222,7 +221,7 @@ class TestRedisCommands:
         users = r.acl_list()
         assert len(users) == 2
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_log(self, r, request):
         username = 'redis-py-user'
 
@@ -257,7 +256,7 @@ class TestRedisCommands:
         assert 'client-info' in r.acl_log(count=1)[0]
         assert r.acl_log_reset()
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_setuser_categories_without_prefix_fails(self, r, request):
         username = 'redis-py-user'
 
@@ -268,7 +267,7 @@ class TestRedisCommands:
         with pytest.raises(exceptions.DataError):
             r.acl_setuser(username, categories=['list'])
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_setuser_commands_without_prefix_fails(self, r, request):
         username = 'redis-py-user'
 
@@ -279,7 +278,7 @@ class TestRedisCommands:
         with pytest.raises(exceptions.DataError):
             r.acl_setuser(username, commands=['get'])
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_setuser_add_passwords_and_nopass_fails(self, r, request):
         username = 'redis-py-user'
 
@@ -290,13 +289,13 @@ class TestRedisCommands:
         with pytest.raises(exceptions.DataError):
             r.acl_setuser(username, passwords='+mypass', nopass=True)
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_users(self, r):
         users = r.acl_users()
         assert isinstance(users, list)
         assert len(users) > 0
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_acl_whoami(self, r):
         username = r.acl_whoami()
         assert isinstance(username, str)
@@ -1137,7 +1136,7 @@ class TestRedisCommands:
         assert r.set('a', '1', xx=True, px=10000)
         assert 0 < r.ttl('a') <= 10
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_set_keepttl(self, r):
         r['a'] = 'val'
         assert r.set('a', '1', xx=True, px=10000)
@@ -1451,7 +1450,7 @@ class TestRedisCommands:
         _, keys = r.scan(match='a')
         assert set(keys) == {b'a'}
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_scan_type(self, r):
         r.sadd('a-set', 1)
         r.hset('a-hash', 'foo', 2)
@@ -1841,6 +1840,7 @@ class TestRedisCommands:
         r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3})
         assert r.zrange('a', 0, 1) == [b'a1', b'a2']
         assert r.zrange('a', 1, 2) == [b'a2', b'a3']
+        assert r.zrange('a', 0, 2, desc=True) == [b'a3', b'a2', b'a1']
 
         # withscores
         assert r.zrange('a', 0, 1, withscores=True) == \
@@ -1852,6 +1852,46 @@ class TestRedisCommands:
         assert r.zrange('a', 0, 1, withscores=True, score_cast_func=int) == \
                [(b'a1', 1), (b'a2', 2)]
 
+    def test_zrange_errors(self, r):
+        with pytest.raises(exceptions.DataError):
+            r.zrange('a', 0, 1, byscore=True, bylex=True)
+        with pytest.raises(exceptions.DataError):
+            r.zrange('a', 0, 1, bylex=True, withscores=True)
+        with pytest.raises(exceptions.DataError):
+            r.zrange('a', 0, 1, byscore=True, withscores=True, offset=4)
+        with pytest.raises(exceptions.DataError):
+            r.zrange('a', 0, 1, byscore=True, withscores=True, num=2)
+
+    @skip_if_server_version_lt('6.2.0')
+    def test_zrange_params(self, r):
+        # bylex
+        r.zadd('a', {'a': 0, 'b': 0, 'c': 0, 'd': 0, 'e': 0, 'f': 0, 'g': 0})
+        assert r.zrange('a', '[aaa', '(g', bylex=True) == \
+               [b'b', b'c', b'd', b'e', b'f']
+        assert r.zrange('a', '[f', '+', bylex=True) == [b'f', b'g']
+        assert r.zrange('a', '+', '[f', desc=True, bylex=True) == [b'g', b'f']
+        assert r.zrange('a', '-', '+', bylex=True, offset=3, num=2) == \
+               [b'd', b'e']
+        assert r.zrange('a', '+', '-', desc=True, bylex=True,
+                        offset=3, num=2) == \
+               [b'd', b'c']
+
+        # byscore
+        r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5})
+        assert r.zrange('a', 2, 4, byscore=True, offset=1, num=2) == \
+               [b'a3', b'a4']
+        assert r.zrange('a', 4, 2, desc=True, byscore=True,
+                        offset=1, num=2) == \
+               [b'a3', b'a2']
+        assert r.zrange('a', 2, 4, byscore=True, withscores=True) == \
+               [(b'a2', 2.0), (b'a3', 3.0), (b'a4', 4.0)]
+        assert r.zrange('a', 4, 2, desc=True, byscore=True,
+                        withscores=True, score_cast_func=int) == \
+               [(b'a4', 4), (b'a3', 3), (b'a2', 2)]
+
+        # rev
+        assert r.zrange('a', 0, 1, desc=True) == [b'a5', b'a4']
+
     @skip_if_server_version_lt('6.2.0')
     def test_zrangestore(self, r):
         r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3})
@@ -1861,6 +1901,16 @@ class TestRedisCommands:
         assert r.zrange('b', 0, -1) == [b'a2', b'a3']
         assert r.zrange('b', 0, -1, withscores=True) == \
                [(b'a2', 2), (b'a3', 3)]
+        # reversed order
+        assert r.zrangestore('b', 'a', 1, 2, desc=True)
+        assert r.zrange('b', 0, -1) == [b'a1', b'a2']
+        # by score
+        assert r.zrangestore('b', 'a', 1, 2, byscore=True,
+                             offset=0, num=1)
+        assert r.zrange('b', 0, -1) == [b'a1']
+        # by lex
+        assert r.zrange('a', '[a2', '(a3', bylex=True) == \
+               [b'a2']
 
     @skip_if_server_version_lt('2.8.9')
     def test_zrangebylex(self, r):
@@ -1886,16 +1936,12 @@ class TestRedisCommands:
     def test_zrangebyscore(self, r):
         r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5})
         assert r.zrangebyscore('a', 2, 4) == [b'a2', b'a3', b'a4']
-
         # slicing with start/num
         assert r.zrangebyscore('a', 2, 4, start=1, num=2) == \
                [b'a3', b'a4']
-
         # withscores
         assert r.zrangebyscore('a', 2, 4, withscores=True) == \
                [(b'a2', 2.0), (b'a3', 3.0), (b'a4', 4.0)]
-
-        # custom score function
         assert r.zrangebyscore('a', 2, 4, withscores=True,
                                score_cast_func=int) == \
                [(b'a2', 2), (b'a3', 3), (b'a4', 4)]
@@ -1959,15 +2005,12 @@ class TestRedisCommands:
     def test_zrevrangebyscore(self, r):
         r.zadd('a', {'a1': 1, 'a2': 2, 'a3': 3, 'a4': 4, 'a5': 5})
         assert r.zrevrangebyscore('a', 4, 2) == [b'a4', b'a3', b'a2']
-
         # slicing with start/num
         assert r.zrevrangebyscore('a', 4, 2, start=1, num=2) == \
                [b'a3', b'a2']
-
         # withscores
         assert r.zrevrangebyscore('a', 4, 2, withscores=True) == \
                [(b'a4', 4.0), (b'a3', 3.0), (b'a2', 2.0)]
-
         # custom score function
         assert r.zrevrangebyscore('a', 4, 2, withscores=True,
                                   score_cast_func=int) == \
@@ -2379,35 +2422,63 @@ class TestRedisCommands:
     def test_geoadd(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-
-        assert r.geoadd('barcelona', *values) == 2
+        assert r.geoadd('barcelona', values) == 2
         assert r.zcard('barcelona') == 2
 
+    @skip_if_server_version_lt('6.2.0')
+    def test_geoadd_nx(self, r):
+        values = (2.1909389952632, 41.433791470673, 'place1') + \
+                 (2.1873744593677, 41.406342043777, 'place2')
+        assert r.geoadd('a', values) == 2
+        values = (2.1909389952632, 41.433791470673, 'place1') + \
+                 (2.1873744593677, 41.406342043777, 'place2') + \
+                 (2.1804738294738, 41.405647879212, 'place3')
+        assert r.geoadd('a', values, nx=True) == 1
+        assert r.zrange('a', 0, -1) == [b'place3', b'place2', b'place1']
+
+    @skip_if_server_version_lt('6.2.0')
+    def test_geoadd_xx(self, r):
+        values = (2.1909389952632, 41.433791470673, 'place1')
+        assert r.geoadd('a', values) == 1
+        values = (2.1909389952632, 41.433791470673, 'place1') + \
+                 (2.1873744593677, 41.406342043777, 'place2')
+        assert r.geoadd('a', values, xx=True) == 0
+        assert r.zrange('a', 0, -1) == \
+               [b'place1']
+
+    @skip_if_server_version_lt('6.2.0')
+    def test_geoadd_ch(self, r):
+        values = (2.1909389952632, 41.433791470673, 'place1')
+        assert r.geoadd('a', values) == 1
+        values = (2.1909389952632, 31.433791470673, 'place1') + \
+                 (2.1873744593677, 41.406342043777, 'place2')
+        assert r.geoadd('a', values, ch=True) == 2
+        assert r.zrange('a', 0, -1) == \
+               [b'place1', b'place2']
+
     @skip_if_server_version_lt('3.2.0')
     def test_geoadd_invalid_params(self, r):
         with pytest.raises(exceptions.RedisError):
-            r.geoadd('barcelona', *(1, 2))
+            r.geoadd('barcelona', (1, 2))
 
     @skip_if_server_version_lt('3.2.0')
     def test_geodist(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-
-        assert r.geoadd('barcelona', *values) == 2
+        assert r.geoadd('barcelona', values) == 2
         assert r.geodist('barcelona', 'place1', 'place2') == 3067.4157
 
     @skip_if_server_version_lt('3.2.0')
     def test_geodist_units(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.geodist('barcelona', 'place1', 'place2', 'km') == 3.0674
 
     @skip_if_server_version_lt('3.2.0')
     def test_geodist_missing_one_member(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1')
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.geodist('barcelona', 'place1', 'missing_member', 'km') is None
 
     @skip_if_server_version_lt('3.2.0')
@@ -2419,8 +2490,7 @@ class TestRedisCommands:
     def test_geohash(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.geohash('barcelona', 'place1', 'place2', 'place3') == \
                ['sp3e9yg3kd0', 'sp3e9cbc3t0', None]
 
@@ -2429,8 +2499,7 @@ class TestRedisCommands:
     def test_geopos(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         # redis uses 52 bits precision, hereby small errors may be introduced.
         assert r.geopos('barcelona', 'place1', 'place2') == \
                [(2.19093829393386841, 41.43379028184083523),
@@ -2450,7 +2519,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, b'\x80place2') + \
                  (2.583333, 41.316667, 'place3')
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.geosearch('barcelona', longitude=2.191,
                            latitude=41.433, radius=1000) == [b'place1']
         assert r.geosearch('barcelona', longitude=2.187,
@@ -2472,7 +2541,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, b'\x80place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.geosearch('barcelona', member='place1', radius=4000) == \
                [b'\x80place2', b'place1']
         assert r.geosearch('barcelona', member='place1', radius=10) == \
@@ -2491,7 +2560,7 @@ class TestRedisCommands:
     def test_geosearch_sort(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.geosearch('barcelona', longitude=2.191,
                            latitude=41.433, radius=3000, sort='ASC') == \
                [b'place1', b'place2']
@@ -2504,7 +2573,7 @@ class TestRedisCommands:
     def test_geosearch_with(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
 
         # test a bunch of combinations to test the parse response
         # function.
@@ -2575,7 +2644,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         r.geosearchstore('places_barcelona', 'barcelona',
                          longitude=2.191, latitude=41.433, radius=1000)
         assert r.zrange('places_barcelona', 0, -1) == [b'place1']
@@ -2586,7 +2655,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         r.geosearchstore('places_barcelona', 'barcelona',
                          longitude=2.191, latitude=41.433,
                          radius=1000, storedist=True)
@@ -2598,7 +2667,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, b'\x80place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.georadius('barcelona', 2.191, 41.433, 1000) == [b'place1']
         assert r.georadius('barcelona', 2.187, 41.406, 1000) == [b'\x80place2']
 
@@ -2607,7 +2676,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.georadius('barcelona', 1, 2, 1000) == []
 
     @skip_if_server_version_lt('3.2.0')
@@ -2615,7 +2684,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.georadius('barcelona', 2.191, 41.433, 1, unit='km') == \
                [b'place1']
 
@@ -2625,7 +2694,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
 
         # test a bunch of combinations to test the parse response
         # function.
@@ -2653,16 +2722,19 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.georadius('barcelona', 2.191, 41.433, 3000, count=1) == \
                [b'place1']
+        assert r.georadius('barcelona', 2.191, 41.433, 3000,
+                           count=1, any=True) == \
+               [b'place2']
 
     @skip_if_server_version_lt('3.2.0')
     def test_georadius_sort(self, r):
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.georadius('barcelona', 2.191, 41.433, 3000, sort='ASC') == \
                [b'place1', b'place2']
         assert r.georadius('barcelona', 2.191, 41.433, 3000, sort='DESC') == \
@@ -2673,7 +2745,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         r.georadius('barcelona', 2.191, 41.433, 1000, store='places_barcelona')
         assert r.zrange('places_barcelona', 0, -1) == [b'place1']
 
@@ -2683,7 +2755,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, 'place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         r.georadius('barcelona', 2.191, 41.433, 1000,
                     store_dist='places_barcelona')
         # instead of save the geo score, the distance is saved.
@@ -2695,7 +2767,7 @@ class TestRedisCommands:
         values = (2.1909389952632, 41.433791470673, 'place1') + \
                  (2.1873744593677, 41.406342043777, b'\x80place2')
 
-        r.geoadd('barcelona', *values)
+        r.geoadd('barcelona', values)
         assert r.georadiusbymember('barcelona', 'place1', 4000) == \
                [b'\x80place2', b'place1']
         assert r.georadiusbymember('barcelona', 'place1', 10) == [b'place1']
@@ -2707,6 +2779,9 @@ class TestRedisCommands:
                  (2.187376320362091, 41.40634178640635)],
                 [b'place1', 0.0, 3471609698139488,
                  (2.1909382939338684, 41.433790281840835)]]
+        assert r.georadiusbymember('barcelona', 'place1', 4000,
+                                   count=1, any=True) == \
+               [b'\x80place2']
 
     @skip_if_server_version_lt('5.0.0')
     def test_xack(self, r):
@@ -3455,6 +3530,10 @@ class TestRedisCommands:
                 .execute())
         assert resp == [0, None, 255]
 
+    @skip_if_server_version_lt('4.0.0')
+    def test_memory_malloc_stats(self, r):
+        assert r.memory_malloc_stats()
+
     @skip_if_server_version_lt('4.0.0')
     def test_memory_stats(self, r):
         # put a key into the current db to make sure that "db.<current-db>"
diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py
index 7f9d0545..8d2ad041 100644
--- a/tests/test_connection_pool.py
+++ b/tests/test_connection_pool.py
@@ -7,7 +7,7 @@ from unittest import mock
 
 from threading import Thread
 from redis.connection import ssl_available, to_bool
-from .conftest import skip_if_server_version_lt, _get_client, REDIS_6_VERSION
+from .conftest import skip_if_server_version_lt, _get_client
 from .test_pubsub import wait_for_message
 
 
@@ -200,7 +200,7 @@ class TestConnectionPoolURLParsing:
             'port': 6380,
         }
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_username(self):
         pool = redis.ConnectionPool.from_url('redis://myuser:@localhost')
         assert pool.connection_class == redis.Connection
@@ -209,7 +209,7 @@ class TestConnectionPoolURLParsing:
             'username': 'myuser',
         }
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_quoted_username(self):
         pool = redis.ConnectionPool.from_url(
             'redis://%2Fmyuser%2F%2B name%3D%24+:@localhost')
@@ -236,7 +236,7 @@ class TestConnectionPoolURLParsing:
             'password': '/mypass/+ word=$+',
         }
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_username_and_password(self):
         pool = redis.ConnectionPool.from_url('redis://myuser:mypass@localhost')
         assert pool.connection_class == redis.Connection
@@ -349,7 +349,7 @@ class TestConnectionPoolUnixSocketURLParsing:
             'path': '/socket',
         }
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_username(self):
         pool = redis.ConnectionPool.from_url('unix://myuser:@/socket')
         assert pool.connection_class == redis.UnixDomainSocketConnection
@@ -358,7 +358,7 @@ class TestConnectionPoolUnixSocketURLParsing:
             'username': 'myuser',
         }
 
-    @skip_if_server_version_lt(REDIS_6_VERSION)
+    @skip_if_server_version_lt("6.0.0")
     def test_quoted_username(self):
         pool = redis.ConnectionPool.from_url(
             'unix://%2Fmyuser%2F%2B name%3D%24+:@/socket')
diff --git a/tests/test_retry.py b/tests/test_retry.py
index 24d9683f..535485ac 100644
--- a/tests/test_retry.py
+++ b/tests/test_retry.py
@@ -2,7 +2,7 @@ from redis.backoff import NoBackoff
 import pytest
 
 from redis.exceptions import ConnectionError
-from redis.connection import Connection
+from redis.connection import Connection, UnixDomainSocketConnection
 from redis.retry import Retry
 
 
@@ -20,20 +20,22 @@ class BackoffMock:
 
 
 class TestConnectionConstructorWithRetry:
-    "Test that the Connection constructor properly handles Retry objects"
+    "Test that the Connection constructors properly handles Retry objects"
 
     @pytest.mark.parametrize("retry_on_timeout", [False, True])
-    def test_retry_on_timeout_boolean(self, retry_on_timeout):
-        c = Connection(retry_on_timeout=retry_on_timeout)
+    @pytest.mark.parametrize("Class", [Connection, UnixDomainSocketConnection])
+    def test_retry_on_timeout_boolean(self, Class, retry_on_timeout):
+        c = Class(retry_on_timeout=retry_on_timeout)
         assert c.retry_on_timeout == retry_on_timeout
         assert isinstance(c.retry, Retry)
         assert c.retry._retries == (1 if retry_on_timeout else 0)
 
     @pytest.mark.parametrize("retries", range(10))
-    def test_retry_on_timeout_retry(self, retries):
+    @pytest.mark.parametrize("Class", [Connection, UnixDomainSocketConnection])
+    def test_retry_on_timeout_retry(self, Class, retries):
         retry_on_timeout = retries > 0
-        c = Connection(retry_on_timeout=retry_on_timeout,
-                       retry=Retry(NoBackoff(), retries))
+        c = Class(retry_on_timeout=retry_on_timeout,
+                  retry=Retry(NoBackoff(), retries))
         assert c.retry_on_timeout == retry_on_timeout
         assert isinstance(c.retry, Retry)
         assert c.retry._retries == retries