Repository: redis/redis-py
Pull request #2980 (2023-10-05 09:38:58)

Make the connection callback methods public again, add documentation
### Pull Request check-list

_Please make sure to review and check all of these items:_

- [x] Do tests and lints pass with this change?
- [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)?
- [ ] Is there an example added to the examples folder (if applicable)?
- [x] Was the change added to CHANGES file?

_NOTE: these things are not required to open a PR and can be done
afterwards / while the PR is open._

### Description of change

In issue #2965 a case is made for the connection callback methods to be public.  These were previosly made semi-private in pr #2870.

This pr makes them public, and adds rudimentary documentation.


hiimdoublej-swag: Hello @chayim @dvora-h, can you help with the triage of this ? Thanks
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) Report
Attention: `2 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`0113034`)](https://app.codecov.io/gh/redis/redis-py/commit/0113034634c7b4968cfc34b42980f3ad86eb1fc6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) 91.32% compared to head [(`ab99e33`)](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) 77.71%.

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master    #2980       +/-   ##
===========================================
- Coverage   91.32%   77.71%   -13.62%     
===========================================
  Files         126      126               
  Lines       32719    32719               
===========================================
- Hits        29881    25427     -4454     
- Misses       2838     7292     +4454     
```


| [Files](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis) | Coverage Δ | |
|---|---|---|
| [redis/asyncio/connection.py](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvYXN5bmNpby9jb25uZWN0aW9uLnB5) | `83.41% <100.00%> (-0.50%)` | :arrow_down: |
| [redis/client.py](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvY2xpZW50LnB5) | `91.67% <100.00%> (-0.31%)` | :arrow_down: |
| [redis/connection.py](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvY29ubmVjdGlvbi5weQ==) | `81.40% <100.00%> (-0.92%)` | :arrow_down: |
| [redis/asyncio/client.py](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvYXN5bmNpby9jbGllbnQucHk=) | `91.99% <66.66%> (ø)` | |
| [redis/cluster.py](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis#diff-cmVkaXMvY2x1c3Rlci5weQ==) | `18.38% <0.00%> (-74.43%)` | :arrow_down: |

... and [22 files with indirect coverage changes](https://app.codecov.io/gh/redis/redis-py/pull/2980/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/redis/redis-py/pull/2980?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=redis).

hiimdoublej-swag: Hello @chayim @dvora-h, can we get some traction on this ? Thanks

Patch:
diff --git a/CHANGES b/CHANGES
index 191286d4..7c4b476e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,4 @@
+    * Connection.register_connect_callback() is made public.
     * Fix async `read_response` to use `disable_decoding`.
     * Add 'aclose()' methods to async classes, deprecate async close().
     * Fix #2831, add auto_close_connection_pool=True arg to asyncio.Redis.from_url()
@@ -57,6 +58,7 @@
     * Improve error output for master discovery 
     * Make `ClusterCommandsProtocol` an actual Protocol
     * Add `sum` to DUPLICATE_POLICY documentation of `TS.CREATE`, `TS.ADD` and `TS.ALTER`
+    * Prevent async ClusterPipeline instances from becoming "false-y" in case of empty command stack (#3061)
 
 * 4.1.3 (Feb 8, 2022)
   * Fix flushdb and flushall (#1926)
diff --git a/redis/asyncio/client.py b/redis/asyncio/client.py
index 8a8f54dc..de27da17 100644
--- a/redis/asyncio/client.py
+++ b/redis/asyncio/client.py
@@ -337,7 +337,10 @@ class Redis(
         self._single_conn_lock = asyncio.Lock()
 
     def __repr__(self):
-        return f"{self.__class__.__name__}<{self.connection_pool!r}>"
+        return (
+            f"<{self.__class__.__module__}.{self.__class__.__name__}"
+            f"({self.connection_pool!r})>"
+        )
 
     def __await__(self):
         return self.initialize().__await__()
@@ -785,7 +788,7 @@ class PubSub:
 
     def __del__(self):
         if self.connection:
-            self.connection._deregister_connect_callback(self.on_connect)
+            self.connection.deregister_connect_callback(self.on_connect)
 
     async def aclose(self):
         # In case a connection property does not yet exist
@@ -796,7 +799,7 @@ class PubSub:
         async with self._lock:
             if self.connection:
                 await self.connection.disconnect()
-                self.connection._deregister_connect_callback(self.on_connect)
+                self.connection.deregister_connect_callback(self.on_connect)
                 await self.connection_pool.release(self.connection)
                 self.connection = None
             self.channels = {}
@@ -859,7 +862,7 @@ class PubSub:
             )
             # register a callback that re-subscribes to any channels we
             # were listening to when we were disconnected
-            self.connection._register_connect_callback(self.on_connect)
+            self.connection.register_connect_callback(self.on_connect)
         else:
             await self.connection.connect()
         if self.push_handler_func is not None and not HIREDIS_AVAILABLE:
diff --git a/redis/asyncio/cluster.py b/redis/asyncio/cluster.py
index ebc7e4a4..6a1753ad 100644
--- a/redis/asyncio/cluster.py
+++ b/redis/asyncio/cluster.py
@@ -1429,7 +1429,8 @@ class ClusterPipeline(AbstractRedis, AbstractRedisCluster, AsyncRedisClusterComm
         self._command_stack = []
 
     def __bool__(self) -> bool:
-        return bool(self._command_stack)
+        "Pipeline instances should  always evaluate to True on Python 3+"
+        return True
 
     def __len__(self) -> int:
         return len(self._command_stack)
diff --git a/redis/asyncio/connection.py b/redis/asyncio/connection.py
index 77312211..bbd438fc 100644
--- a/redis/asyncio/connection.py
+++ b/redis/asyncio/connection.py
@@ -225,7 +225,7 @@ class AbstractConnection:
 
     def __repr__(self):
         repr_args = ",".join((f"{k}={v}" for k, v in self.repr_pieces()))
-        return f"{self.__class__.__name__}<{repr_args}>"
+        return f"<{self.__class__.__module__}.{self.__class__.__name__}({repr_args})>"
 
     @abstractmethod
     def repr_pieces(self):
@@ -235,12 +235,24 @@ class AbstractConnection:
     def is_connected(self):
         return self._reader is not None and self._writer is not None
 
-    def _register_connect_callback(self, callback):
+    def register_connect_callback(self, callback):
+        """
+        Register a callback to be called when the connection is established either
+        initially or reconnected.  This allows listeners to issue commands that
+        are ephemeral to the connection, for example pub/sub subscription or
+        key tracking.  The callback must be a _method_ and will be kept as
+        a weak reference.
+        """
         wm = weakref.WeakMethod(callback)
         if wm not in self._connect_callbacks:
             self._connect_callbacks.append(wm)
 
-    def _deregister_connect_callback(self, callback):
+    def deregister_connect_callback(self, callback):
+        """
+        De-register a previously registered callback.  It will no-longer receive
+        notifications on connection events.  Calling this is not required when the
+        listener goes away, since the callbacks are kept as weak methods.
+        """
         try:
             self._connect_callbacks.remove(weakref.WeakMethod(callback))
         except ValueError:
@@ -1031,8 +1043,8 @@ class ConnectionPool:
 
     def __repr__(self):
         return (
-            f"{self.__class__.__name__}"
-            f"<{self.connection_class(**self.connection_kwargs)!r}>"
+            f"<{self.__class__.__module__}.{self.__class__.__name__}"
+            f"({self.connection_class(**self.connection_kwargs)!r})>"
         )
 
     def reset(self):
diff --git a/redis/asyncio/sentinel.py b/redis/asyncio/sentinel.py
index 56f5e9d6..d88babc5 100644
--- a/redis/asyncio/sentinel.py
+++ b/redis/asyncio/sentinel.py
@@ -30,11 +30,14 @@ class SentinelManagedConnection(Connection):
 
     def __repr__(self):
         pool = self.connection_pool
-        s = f"{self.__class__.__name__}<service={pool.service_name}"
+        s = (
+            f"<{self.__class__.__module__}.{self.__class__.__name__}"
+            f"(service={pool.service_name}"
+        )
         if self.host:
             host_info = f",host={self.host},port={self.port}"
             s += host_info
-        return s + ">"
+        return s + ")>"
 
     async def connect_to(self, address):
         self.host, self.port = address
@@ -120,8 +123,8 @@ class SentinelConnectionPool(ConnectionPool):
 
     def __repr__(self):
         return (
-            f"{self.__class__.__name__}"
-            f"<service={self.service_name}({self.is_master and 'master' or 'slave'})>"
+            f"<{self.__class__.__module__}.{self.__class__.__name__}"
+            f"(service={self.service_name}({self.is_master and 'master' or 'slave'}))>"
         )
 
     def reset(self):
@@ -242,7 +245,10 @@ class Sentinel(AsyncSentinelCommands):
                 f"{sentinel.connection_pool.connection_kwargs['host']}:"
                 f"{sentinel.connection_pool.connection_kwargs['port']}"
             )
-        return f"{self.__class__.__name__}<sentinels=[{','.join(sentinel_addresses)}]>"
+        return (
+            f"<{self.__class__}.{self.__class__.__name__}"
+            f"(sentinels=[{','.join(sentinel_addresses)}])>"
+        )
 
     def check_master_state(self, state: dict, service_name: str) -> bool:
         if not state["is_master"] or state["is_sdown"] or state["is_odown"]:
diff --git a/redis/client.py b/redis/client.py
index 2ff3706d..0af7e050 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -334,7 +334,10 @@ class Redis(RedisModuleCommands, CoreCommands, SentinelCommands):
             self.cache_whitelist = cache_whitelist
 
     def __repr__(self) -> str:
-        return f"{type(self).__name__}<{repr(self.connection_pool)}>"
+        return (
+            f"<{type(self).__module__}.{type(self).__name__}"
+            f"({repr(self.connection_pool)})>"
+        )
 
     def get_encoder(self) -> "Encoder":
         """Get the connection pool's encoder"""
@@ -756,7 +759,7 @@ class PubSub:
     def reset(self) -> None:
         if self.connection:
             self.connection.disconnect()
-            self.connection._deregister_connect_callback(self.on_connect)
+            self.connection.deregister_connect_callback(self.on_connect)
             self.connection_pool.release(self.connection)
             self.connection = None
         self.health_check_response_counter = 0
@@ -814,7 +817,7 @@ class PubSub:
             )
             # register a callback that re-subscribes to any channels we
             # were listening to when we were disconnected
-            self.connection._register_connect_callback(self.on_connect)
+            self.connection.register_connect_callback(self.on_connect)
             if self.push_handler_func is not None and not HIREDIS_AVAILABLE:
                 self.connection._parser.set_push_handler(self.push_handler_func)
         connection = self.connection
diff --git a/redis/cluster.py b/redis/cluster.py
index 595efcb6..0405b054 100644
--- a/redis/cluster.py
+++ b/redis/cluster.py
@@ -1776,7 +1776,7 @@ class ClusterPubSub(PubSub):
             )
             # register a callback that re-subscribes to any channels we
             # were listening to when we were disconnected
-            self.connection._register_connect_callback(self.on_connect)
+            self.connection.register_connect_callback(self.on_connect)
             if self.push_handler_func is not None and not HIREDIS_AVAILABLE:
                 self.connection._parser.set_push_handler(self.push_handler_func)
         connection = self.connection
diff --git a/redis/commands/helpers.py b/redis/commands/helpers.py
index 324d981d..127141f6 100644
--- a/redis/commands/helpers.py
+++ b/redis/commands/helpers.py
@@ -64,6 +64,11 @@ def parse_list_to_dict(response):
     for i in range(0, len(response), 2):
         if isinstance(response[i], list):
             res["Child iterators"].append(parse_list_to_dict(response[i]))
+            try:
+                if isinstance(response[i + 1], list):
+                    res["Child iterators"].append(parse_list_to_dict(response[i + 1]))
+            except IndexError:
+                pass
         elif isinstance(response[i + 1], list):
             res["Child iterators"] = [parse_list_to_dict(response[i + 1])]
         else:
diff --git a/redis/connection.py b/redis/connection.py
index fead6135..c201224e 100644
--- a/redis/connection.py
+++ b/redis/connection.py
@@ -217,7 +217,7 @@ class AbstractConnection:
 
     def __repr__(self):
         repr_args = ",".join([f"{k}={v}" for k, v in self.repr_pieces()])
-        return f"{self.__class__.__name__}<{repr_args}>"
+        return f"<{self.__class__.__module__}.{self.__class__.__name__}({repr_args})>"
 
     @abstractmethod
     def repr_pieces(self):
@@ -237,12 +237,24 @@ class AbstractConnection:
         else:
             return PythonRespSerializer(self._buffer_cutoff, self.encoder.encode)
 
-    def _register_connect_callback(self, callback):
+    def register_connect_callback(self, callback):
+        """
+        Register a callback to be called when the connection is established either
+        initially or reconnected.  This allows listeners to issue commands that
+        are ephemeral to the connection, for example pub/sub subscription or
+        key tracking.  The callback must be a _method_ and will be kept as
+        a weak reference.
+        """
         wm = weakref.WeakMethod(callback)
         if wm not in self._connect_callbacks:
             self._connect_callbacks.append(wm)
 
-    def _deregister_connect_callback(self, callback):
+    def deregister_connect_callback(self, callback):
+        """
+        De-register a previously registered callback.  It will no-longer receive
+        notifications on connection events.  Calling this is not required when the
+        listener goes away, since the callbacks are kept as weak methods.
+        """
         try:
             self._connect_callbacks.remove(weakref.WeakMethod(callback))
         except ValueError:
@@ -1004,8 +1016,8 @@ class ConnectionPool:
 
     def __repr__(self) -> (str, str):
         return (
-            f"{type(self).__name__}"
-            f"<{repr(self.connection_class(**self.connection_kwargs))}>"
+            f"<{type(self).__module__}.{type(self).__name__}"
+            f"({repr(self.connection_class(**self.connection_kwargs))})>"
         )
 
     def reset(self) -> None:
diff --git a/redis/sentinel.py b/redis/sentinel.py
index a1ae5c52..dfcd8ff6 100644
--- a/redis/sentinel.py
+++ b/redis/sentinel.py
@@ -24,7 +24,10 @@ class SentinelManagedConnection(Connection):
 
     def __repr__(self):
         pool = self.connection_pool
-        s = f"{type(self).__name__}<service={pool.service_name}%s>"
+        s = (
+            f"<{type(self).__module__}.{type(self).__name__}"
+            f"(service={pool.service_name}%s)>"
+        )
         if self.host:
             host_info = f",host={self.host},port={self.port}"
             s = s % host_info
@@ -162,7 +165,10 @@ class SentinelConnectionPool(ConnectionPool):
 
     def __repr__(self):
         role = "master" if self.is_master else "slave"
-        return f"{type(self).__name__}<service={self.service_name}({role})"
+        return (
+            f"<{type(self).__module__}.{type(self).__name__}"
+            f"(service={self.service_name}({role}))>"
+        )
 
     def reset(self):
         super().reset()
@@ -262,7 +268,10 @@ class Sentinel(SentinelCommands):
             sentinel_addresses.append(
                 "{host}:{port}".format_map(sentinel.connection_pool.connection_kwargs)
             )
-        return f'{type(self).__name__}<sentinels=[{",".join(sentinel_addresses)}]>'
+        return (
+            f"<{type(self).__module__}.{type(self).__name__}"
+            f'(sentinels=[{",".join(sentinel_addresses)}])>'
+        )
 
     def check_master_state(self, state, service_name):
         if not state["is_master"] or state["is_sdown"] or state["is_odown"]:


Test patch:
diff --git a/tests/test_asyncio/test_connection_pool.py b/tests/test_asyncio/test_connection_pool.py
index ed90fc73..5e4d3f20 100644
--- a/tests/test_asyncio/test_connection_pool.py
+++ b/tests/test_asyncio/test_connection_pool.py
@@ -180,11 +180,8 @@ class TestConnectionPool:
         async with self.get_pool(
             connection_kwargs=connection_kwargs, connection_class=redis.Connection
         ) as pool:
-            expected = (
-                "ConnectionPool<Connection<"
-                "host=localhost,port=6379,db=1,client_name=test-client>>"
-            )
-            assert repr(pool) == expected
+            expected = "host=localhost,port=6379,db=1,client_name=test-client"
+            assert expected in repr(pool)
 
     async def test_repr_contains_db_info_unix(self):
         connection_kwargs = {"path": "/abc", "db": 1, "client_name": "test-client"}
@@ -192,11 +189,8 @@ class TestConnectionPool:
             connection_kwargs=connection_kwargs,
             connection_class=redis.UnixDomainSocketConnection,
         ) as pool:
-            expected = (
-                "ConnectionPool<UnixDomainSocketConnection<"
-                "path=/abc,db=1,client_name=test-client>>"
-            )
-            assert repr(pool) == expected
+            expected = "path=/abc,db=1,client_name=test-client"
+            assert expected in repr(pool)
 
 
 class TestBlockingConnectionPool:
@@ -293,11 +287,8 @@ class TestBlockingConnectionPool:
         pool = redis.ConnectionPool(
             host="localhost", port=6379, client_name="test-client"
         )
-        expected = (
-            "ConnectionPool<Connection<"
-            "host=localhost,port=6379,db=0,client_name=test-client>>"
-        )
-        assert repr(pool) == expected
+        expected = "host=localhost,port=6379,db=0,client_name=test-client"
+        assert expected in repr(pool)
 
     def test_repr_contains_db_info_unix(self):
         pool = redis.ConnectionPool(
@@ -305,11 +296,8 @@ class TestBlockingConnectionPool:
             path="abc",
             client_name="test-client",
         )
-        expected = (
-            "ConnectionPool<UnixDomainSocketConnection<"
-            "path=abc,db=0,client_name=test-client>>"
-        )
-        assert repr(pool) == expected
+        expected = "path=abc,db=0,client_name=test-client"
+        assert expected in repr(pool)
 
 
 class TestConnectionPoolURLParsing:
@@ -659,7 +647,10 @@ class TestConnection:
         connection = redis.Redis.from_url("redis://localhost")
         pool = connection.connection_pool
 
-        assert re.match("(.*)<(.*)<(.*)>>", repr(pool)).groups() == (
+        print(repr(pool))
+        assert re.match(
+            r"< .*?([^\.]+) \( < .*?([^\.]+) \( (.+) \) > \) >", repr(pool), re.VERBOSE
+        ).groups() == (
             "ConnectionPool",
             "Connection",
             "host=localhost,port=6379,db=0",
@@ -669,7 +660,9 @@ class TestConnection:
         connection = redis.Redis.from_url("unix:///path/to/socket")
         pool = connection.connection_pool
 
-        assert re.match("(.*)<(.*)<(.*)>>", repr(pool)).groups() == (
+        assert re.match(
+            r"< .*?([^\.]+) \( < .*?([^\.]+) \( (.+) \) > \) >", repr(pool), re.VERBOSE
+        ).groups() == (
             "ConnectionPool",
             "UnixDomainSocketConnection",
             "path=/path/to/socket,db=0",
diff --git a/tests/test_asyncio/test_pipeline.py b/tests/test_asyncio/test_pipeline.py
index 3d271bf1..4b29360d 100644
--- a/tests/test_asyncio/test_pipeline.py
+++ b/tests/test_asyncio/test_pipeline.py
@@ -7,7 +7,6 @@ from .conftest import wait_for_command
 
 
 class TestPipeline:
-    @pytest.mark.onlynoncluster
     async def test_pipeline_is_true(self, r):
         """Ensure pipeline instances are not false-y"""
         async with r.pipeline() as pipe:
diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py
index d1e984ee..dee7c554 100644
--- a/tests/test_connection_pool.py
+++ b/tests/test_connection_pool.py
@@ -95,11 +95,8 @@ class TestConnectionPool:
         pool = self.get_pool(
             connection_kwargs=connection_kwargs, connection_class=redis.Connection
         )
-        expected = (
-            "ConnectionPool<Connection<"
-            "host=localhost,port=6379,db=1,client_name=test-client>>"
-        )
-        assert repr(pool) == expected
+        expected = "host=localhost,port=6379,db=1,client_name=test-client"
+        assert expected in repr(pool)
 
     def test_repr_contains_db_info_unix(self):
         connection_kwargs = {"path": "/abc", "db": 1, "client_name": "test-client"}
@@ -107,11 +104,8 @@ class TestConnectionPool:
             connection_kwargs=connection_kwargs,
             connection_class=redis.UnixDomainSocketConnection,
         )
-        expected = (
-            "ConnectionPool<UnixDomainSocketConnection<"
-            "path=/abc,db=1,client_name=test-client>>"
-        )
-        assert repr(pool) == expected
+        expected = "path=/abc,db=1,client_name=test-client"
+        assert expected in repr(pool)
 
 
 class TestBlockingConnectionPool:
@@ -190,11 +184,8 @@ class TestBlockingConnectionPool:
         pool = redis.ConnectionPool(
             host="localhost", port=6379, client_name="test-client"
         )
-        expected = (
-            "ConnectionPool<Connection<"
-            "host=localhost,port=6379,db=0,client_name=test-client>>"
-        )
-        assert repr(pool) == expected
+        expected = "host=localhost,port=6379,db=0,client_name=test-client"
+        assert expected in repr(pool)
 
     def test_repr_contains_db_info_unix(self):
         pool = redis.ConnectionPool(
@@ -202,11 +193,8 @@ class TestBlockingConnectionPool:
             path="abc",
             client_name="test-client",
         )
-        expected = (
-            "ConnectionPool<UnixDomainSocketConnection<"
-            "path=abc,db=0,client_name=test-client>>"
-        )
-        assert repr(pool) == expected
+        expected = "path=abc,db=0,client_name=test-client"
+        assert expected in repr(pool)
 
 
 class TestConnectionPoolURLParsing:
@@ -579,7 +567,9 @@ class TestConnection:
         connection = redis.Redis.from_url("redis://localhost")
         pool = connection.connection_pool
 
-        assert re.match("(.*)<(.*)<(.*)>>", repr(pool)).groups() == (
+        assert re.match(
+            r"< .*?([^\.]+) \( < .*?([^\.]+) \( (.+) \) > \) >", repr(pool), re.VERBOSE
+        ).groups() == (
             "ConnectionPool",
             "Connection",
             "host=localhost,port=6379,db=0",
@@ -589,7 +579,9 @@ class TestConnection:
         connection = redis.Redis.from_url("unix:///path/to/socket")
         pool = connection.connection_pool
 
-        assert re.match("(.*)<(.*)<(.*)>>", repr(pool)).groups() == (
+        assert re.match(
+            r"< .*?([^\.]+) \( < .*?([^\.]+) \( (.+) \) > \) >", repr(pool), re.VERBOSE
+        ).groups() == (
             "ConnectionPool",
             "UnixDomainSocketConnection",
             "path=/path/to/socket,db=0",
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 57a94d2f..66ee1c53 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -41,6 +41,7 @@ def test_parse_to_dict():
                 "Child iterators",
                 ["Type", "bar", "Time", "0.0729", "Counter", 3],
                 ["Type", "barbar", "Time", "0.058", "Counter", 3],
+                ["Type", "barbarbar", "Time", "0.0234", "Counter", 3],
             ],
         ],
     ]
@@ -49,6 +50,7 @@ def test_parse_to_dict():
             "Child iterators": [
                 {"Counter": 3.0, "Time": 0.0729, "Type": "bar"},
                 {"Counter": 3.0, "Time": 0.058, "Type": "barbar"},
+                {"Counter": 3.0, "Time": 0.0234, "Type": "barbarbar"},
             ],
             "Counter": 3.0,
             "Time": 0.2089,