Repository: ethereum/web3.py (python)

Issue:
Deprecate get_uncle* endpoints
### What feature should we add?

Use the `DeprecatedMethod` class to communicate to users that these methods will be removed in v8.

Pull request:
Deprecate eth.get_uncle* methods
### What was wrong?

Closes #3683

### How was it fixed?
Warn users that eth.get_uncle* will be dropped in v8.

### Todo:

- [x] Clean up commit history
- [x] Add or update documentation related to these changes
- [x] Add entry to the [release notes](https://github.com/ethereum/web3.py/blob/main/newsfragments/README.md)

#### Cute Animal Picture

![t18AHHmB](https://github.com/user-attachments/assets/ad8ce9ba-71f6-439a-bf20-b4b7ddb6fd20)


Patch:
diff --git a/docs/web3.eth.rst b/docs/web3.eth.rst
index ce716ffe..9d391938 100644
--- a/docs/web3.eth.rst
+++ b/docs/web3.eth.rst
@@ -398,6 +398,8 @@ The following methods are available on the ``web3.eth`` namespace.
 
 .. py:method:: Eth.get_uncle_by_block(block_identifier, uncle_index)
 
+    .. warning:: Deprecated. Will be removed in v8.
+
     * Delegates to ``eth_getUncleByBlockHashAndIndex`` or
       ``eth_getUncleByBlockNumberAndIndex`` RPC methods
 
@@ -444,6 +446,8 @@ The following methods are available on the ``web3.eth`` namespace.
 
 .. py:method:: Eth.get_uncle_count(block_identifier)
 
+    .. warning:: Deprecated. Will be removed in v8.
+
     * Delegates to ``eth_getUncleCountByBlockHash`` or
       ``eth_getUncleCountByBlockNumber`` RPC methods
 
diff --git a/newsfragments/3683.deprecation.rst b/newsfragments/3683.deprecation.rst
new file mode 100644
index 00000000..dbdb9bcc
--- /dev/null
+++ b/newsfragments/3683.deprecation.rst
@@ -0,0 +1,1 @@
+Deprecate eth.get_uncle* methods. Will be removed in v8.
diff --git a/newsfragments/3690.bugfix.rst b/newsfragments/3690.bugfix.rst
new file mode 100644
index 00000000..e587bde8
--- /dev/null
+++ b/newsfragments/3690.bugfix.rst
@@ -0,0 +1,1 @@
+Properly handle ``PersistentConnectionClosedOK`` for ``subscription_manager.handle_subscriptions()``.
diff --git a/web3/_utils/decorators.py b/web3/_utils/decorators.py
index 346ac5b0..2922bc16 100644
--- a/web3/_utils/decorators.py
+++ b/web3/_utils/decorators.py
@@ -43,7 +43,7 @@ def deprecated_for(replace_message: str) -> Callable[..., Any]:
     """
     Decorate a deprecated function, with info about what to use instead, like:
 
-    @deprecated_for("to_bytes()")
+    @deprecated_for("use to_bytes() instead")
     def toAscii(arg):
         ...
     """
@@ -52,7 +52,7 @@ def deprecated_for(replace_message: str) -> Callable[..., Any]:
         @functools.wraps(to_wrap)
         def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
             warnings.warn(
-                f"{to_wrap.__name__} is deprecated in favor of {replace_message}",
+                f"{to_wrap.__name__} is deprecated: {replace_message}",
                 category=DeprecationWarning,
                 stacklevel=2,
             )
diff --git a/web3/eth/async_eth.py b/web3/eth/async_eth.py
index a1d3adda..2339ac3b 100644
--- a/web3/eth/async_eth.py
+++ b/web3/eth/async_eth.py
@@ -39,6 +39,9 @@ from web3._utils.blocks import (
 from web3._utils.compat import (
     Unpack,
 )
+from web3._utils.decorators import (
+    deprecated_for,
+)
 from web3._utils.fee_utils import (
     async_fee_history_priority_fee,
 )
@@ -669,6 +672,7 @@ class AsyncEth(BaseEth):
         mungers=[default_root_munger],
     )
 
+    @deprecated_for("all get_uncle* methods will be removed in v8")
     async def get_uncle_count(self, block_identifier: BlockIdentifier) -> int:
         return await self._get_uncle_count(block_identifier)
 
diff --git a/web3/eth/eth.py b/web3/eth/eth.py
index b901ed50..d459f964 100644
--- a/web3/eth/eth.py
+++ b/web3/eth/eth.py
@@ -69,6 +69,7 @@ from web3.exceptions import (
     Web3ValueError,
 )
 from web3.method import (
+    DeprecatedMethod,
     Method,
     default_root_munger,
 )
@@ -566,7 +567,7 @@ class Eth(BaseEth):
     # eth_getUncleCountByBlockHash
     # eth_getUncleCountByBlockNumber
 
-    get_uncle_count: Method[Callable[[BlockIdentifier], int]] = Method(
+    _get_uncle_count: Method[Callable[[BlockIdentifier], int]] = Method(
         method_choice_depends_on_args=select_method_for_block_identifier(
             if_predefined=RPC.eth_getUncleCountByBlockNumber,
             if_hash=RPC.eth_getUncleCountByBlockHash,
@@ -574,11 +575,17 @@ class Eth(BaseEth):
         ),
         mungers=[default_root_munger],
     )
+    get_uncle_count = DeprecatedMethod(
+        _get_uncle_count,
+        old_name="_get_uncle_count",
+        new_name="get_uncle_count",
+        msg="All get_uncle* methods have been deprecated",
+    )
 
     # eth_getUncleByBlockHashAndIndex
     # eth_getUncleByBlockNumberAndIndex
 
-    get_uncle_by_block: Method[Callable[[BlockIdentifier, int], Uncle]] = Method(
+    _get_uncle_by_block: Method[Callable[[BlockIdentifier, int], Uncle]] = Method(
         method_choice_depends_on_args=select_method_for_block_identifier(
             if_predefined=RPC.eth_getUncleByBlockNumberAndIndex,
             if_hash=RPC.eth_getUncleByBlockHashAndIndex,
@@ -586,6 +593,12 @@ class Eth(BaseEth):
         ),
         mungers=[default_root_munger],
     )
+    get_uncle_by_block = DeprecatedMethod(
+        _get_uncle_by_block,
+        old_name="_get_uncle_by_block",
+        new_name="get_uncle_by_block",
+        msg="All get_uncle* methods have been deprecated",
+    )
 
     def replace_transaction(
         self, transaction_hash: _Hash32, new_transaction: TxParams
diff --git a/web3/method.py b/web3/method.py
index f67828f9..c9ced51f 100644
--- a/web3/method.py
+++ b/web3/method.py
@@ -241,17 +241,25 @@ class Method(Generic[TFunc]):
 
 class DeprecatedMethod:
     def __init__(
-        self, method: Method[Callable[..., Any]], old_name: str, new_name: str
+        self,
+        method: Method[Callable[..., Any]],
+        old_name: Optional[str] = None,
+        new_name: Optional[str] = None,
+        msg: Optional[str] = None,
     ) -> None:
         self.method = method
         self.old_name = old_name
         self.new_name = new_name
+        self.msg = msg
 
     def __get__(
         self, obj: Optional["Module"] = None, obj_type: Optional[Type["Module"]] = None
     ) -> Any:
+        message = f"{self.old_name} is deprecated in favor of {self.new_name}"
+        if self.msg is not None:
+            message = self.msg
         warnings.warn(
-            f"{self.old_name} is deprecated in favor of {self.new_name}",
+            message,
             category=DeprecationWarning,
             stacklevel=2,
         )
diff --git a/web3/providers/persistent/async_ipc.py b/web3/providers/persistent/async_ipc.py
index 583c9ae4..ad182382 100644
--- a/web3/providers/persistent/async_ipc.py
+++ b/web3/providers/persistent/async_ipc.py
@@ -110,7 +110,9 @@ class AsyncIPCProvider(PersistentConnectionProvider):
             raise
 
         if not data:
-            raise PersistentConnectionClosedOK("Socket reader received end of stream.")
+            raise PersistentConnectionClosedOK(
+                user_message="Socket reader received end of stream."
+            )
         return self.decode_rpc_response(data)
 
     # -- private methods -- #
diff --git a/web3/providers/persistent/subscription_manager.py b/web3/providers/persistent/subscription_manager.py
index e3305e4b..c5879f22 100644
--- a/web3/providers/persistent/subscription_manager.py
+++ b/web3/providers/persistent/subscription_manager.py
@@ -298,6 +298,7 @@ class SubscriptionManager:
                     "Message listener background task for the provider has stopped "
                     "unexpectedly. Stopping subscription handling."
                 )
+                break
 
         # no active handler subscriptions, clear the handler subscription queue
         self._provider._request_processor._reset_handler_subscription_queue()
diff --git a/web3/utils/abi.py b/web3/utils/abi.py
index 10d30d51..e3cc5555 100644
--- a/web3/utils/abi.py
+++ b/web3/utils/abi.py
@@ -682,7 +682,7 @@ def check_if_arguments_can_be_encoded(
     )
 
 
-@deprecated_for("get_abi_element")
+@deprecated_for("use get_abi_element instead")
 def get_event_abi(
     abi: ABI,
     event_name: str,


Test patch:
diff --git a/tests/core/providers/test_websocket_provider.py b/tests/core/providers/test_websocket_provider.py
index 10f16d42..e20555a8 100644
--- a/tests/core/providers/test_websocket_provider.py
+++ b/tests/core/providers/test_websocket_provider.py
@@ -397,7 +397,7 @@ async def test_async_iterator_pattern_exception_handling_for_subscriptions():
 
 
 @pytest.mark.asyncio
-async def test_connection_closed_ok_breaks_message_iteration():
+async def test_connection_closed_ok_breaks_process_subscriptions_iteration():
     with patch(
         "web3.providers.persistent.websocket.connect",
         new=lambda *_1, **_2: WebSocketMessageStreamMock(
@@ -409,6 +409,22 @@ async def test_connection_closed_ok_breaks_message_iteration():
             pytest.fail("Should not reach this point.")
 
 
+@pytest.mark.asyncio
+async def test_connection_closed_ok_breaks_handle_subscriptions_iteration():
+    with patch(
+        "web3.providers.persistent.websocket.connect",
+        new=lambda *_1, **_2: WebSocketMessageStreamMock(
+            raise_exception=ConnectionClosedOK(None, None)
+        ),
+    ):
+        w3 = await AsyncWeb3(WebSocketProvider("ws://mocked"))
+        # would fail with a ``TimeoutError`` if the iteration did not break properly
+        # on ``ConnectionClosedOK``
+        await asyncio.wait_for(
+            w3.subscription_manager.handle_subscriptions(run_forever=True), timeout=1
+        )
+
+
 @pytest.mark.asyncio
 async def test_listener_task_breaks_out_of_stream_when_cancelled():
     with patch(
diff --git a/tests/core/utilities/test_abi.py b/tests/core/utilities/test_abi.py
index 5cd35f1f..baebbec2 100644
--- a/tests/core/utilities/test_abi.py
+++ b/tests/core/utilities/test_abi.py
@@ -829,7 +829,7 @@ def test_get_event_abi(event_name: str, input_args: Sequence[ABIComponent]) -> N
 
     with pytest.warns(
         DeprecationWarning,
-        match="get_event_abi is deprecated in favor of get_abi_element",
+        match="get_event_abi is deprecated: use get_abi_element instead",
     ):
         assert (
             get_event_abi(contract_abi, event_name, input_names) == expected_event_abi
diff --git a/web3/_utils/module_testing/eth_module.py b/web3/_utils/module_testing/eth_module.py
index 71958482..99f4f546 100644
--- a/web3/_utils/module_testing/eth_module.py
+++ b/web3/_utils/module_testing/eth_module.py
@@ -2221,19 +2221,31 @@ class AsyncEthModuleTest:
     async def test_eth_getUncleCountByBlockHash(
         self, async_w3: "AsyncWeb3", async_empty_block: BlockData
     ) -> None:
-        uncle_count = await async_w3.eth.get_uncle_count(async_empty_block["hash"])
+        with pytest.warns(
+            DeprecationWarning,
+            match=r"get_uncle_count is deprecated: all get_uncle\* "
+            r"methods will be removed in v8",
+        ):
+            uncle_count = await async_w3.eth.get_uncle_count(async_empty_block["hash"])
 
-        assert is_integer(uncle_count)
-        assert uncle_count == 0
+            assert is_integer(uncle_count)
+            assert uncle_count == 0
 
     @pytest.mark.asyncio
     async def test_eth_getUncleCountByBlockNumber(
         self, async_w3: "AsyncWeb3", async_empty_block: BlockData
     ) -> None:
-        uncle_count = await async_w3.eth.get_uncle_count(async_empty_block["number"])
+        with pytest.warns(
+            DeprecationWarning,
+            match=r"get_uncle_count is deprecated: all get_uncle\* "
+            r"methods will be removed in v8",
+        ):
+            uncle_count = await async_w3.eth.get_uncle_count(
+                async_empty_block["number"]
+            )
 
-        assert is_integer(uncle_count)
-        assert uncle_count == 0
+            assert is_integer(uncle_count)
+            assert uncle_count == 0
 
     @pytest.mark.asyncio
     async def test_eth_getBlockTransactionCountByNumber_block_with_txn(
@@ -2843,18 +2855,24 @@ class EthModuleTest:
     def test_eth_getUncleCountByBlockHash(
         self, w3: "Web3", empty_block: BlockData
     ) -> None:
-        uncle_count = w3.eth.get_uncle_count(empty_block["hash"])
+        with pytest.warns(
+            DeprecationWarning, match=r"All get_uncle\* methods have been deprecated"
+        ):
+            uncle_count = w3.eth.get_uncle_count(empty_block["hash"])
 
-        assert is_integer(uncle_count)
-        assert uncle_count == 0
+            assert is_integer(uncle_count)
+            assert uncle_count == 0
 
     def test_eth_getUncleCountByBlockNumber(
         self, w3: "Web3", empty_block: BlockData
     ) -> None:
-        uncle_count = w3.eth.get_uncle_count(empty_block["number"])
+        with pytest.warns(
+            DeprecationWarning, match=r"All get_uncle\* methods have been deprecated"
+        ):
+            uncle_count = w3.eth.get_uncle_count(empty_block["number"])
 
-        assert is_integer(uncle_count)
-        assert uncle_count == 0
+            assert is_integer(uncle_count)
+            assert uncle_count == 0
 
     def test_eth_get_code(
         self, w3: "Web3", math_contract_address: ChecksumAddress