Repository: ethereum/web3.py

Issue:
Add optional user message to Web3Exception
### What was wrong?

It would be nice to add an optional user message on the `Web3Exception` class that would allow us to communicate info about the exception to users. It came up in the context of #2796. In that case, we only return the info from the node, but we get a lot of questions about what the node error info means. It would be nice if we could include the error message from the node along with a hint to the users.







Discussion:

Resolving patch:
diff --git a/web3/exceptions.py b/web3/exceptions.py
--- a/web3/exceptions.py
+++ b/web3/exceptions.py
@@ -30,6 +30,16 @@ class Web3Exception(Exception):
             # deal with other exceptions
     """
 
+    def __init__(
+        self,
+        *args: Any,
+        user_message: Optional[str] = None,
+    ):
+        super().__init__(*args)
+
+        # Assign properties of Web3Exception
+        self.user_message = user_message
+
 
 class BadFunctionCallOutput(Web3Exception):
     """
@@ -274,6 +284,7 @@ def __init__(
         message: Optional[str] = None,
         data: Optional[Union[str, Dict[str, str]]] = None,
     ):
+        super().__init__(message, data)
         self.message = message
         self.data = data
 
diff --git a/web3/manager.py b/web3/manager.py
--- a/web3/manager.py
+++ b/web3/manager.py
@@ -246,7 +246,11 @@ def formatted_response(
             if not isinstance(code, int):
                 _raise_bad_response_format(response, "error['code'] must be an integer")
             elif code == METHOD_NOT_FOUND:
-                raise MethodUnavailable(error)
+                raise MethodUnavailable(
+                    error,
+                    user_message="Check your node provider's API docs to see what "
+                    "methods are supported",
+                )
 
             # Errors must include a message
             if not isinstance(error.get("message"), str):


Test patch:
diff --git a/tests/core/core/block-utils/test_select_method_for_block_identifier.py b/tests/core/block-utils/test_select_method_for_block_identifier.py
similarity index 100%
rename from tests/core/core/block-utils/test_select_method_for_block_identifier.py
rename to tests/core/block-utils/test_select_method_for_block_identifier.py
diff --git a/tests/core/exceptions/test_exceptions.py b/tests/core/exceptions/test_exceptions.py
new file mode 100644
--- /dev/null
+++ b/tests/core/exceptions/test_exceptions.py
@@ -0,0 +1,32 @@
+import pytest
+
+from web3.exceptions import (
+    Web3Exception,
+)
+
+
+def test_web3exception_with_user_message():
+    with pytest.raises(Web3Exception) as exception:
+        raise Web3Exception(user_message="This failed!")
+    assert exception.type is Web3Exception
+    assert exception.value.user_message == "This failed!"
+
+
+def test_web3exception_with_kwargs():
+    with pytest.raises(TypeError) as exception:
+        raise Web3Exception(data={"message": "Unable to fulfill your request."})
+
+    # For Python > 3.9, str exception includes 'Web3Exception.'
+    expected = "__init__() got an unexpected keyword argument 'data'"
+    actual = str(exception.value)
+    assert exception.type is TypeError
+    assert hasattr(exception.value, "data") is False
+    assert expected in actual
+
+
+def test_web3exception_with_args():
+    with pytest.raises(Web3Exception) as exception:
+        raise Web3Exception("failed")
+    assert exception.type is Web3Exception
+    assert exception.value.user_message is None
+    assert exception.value.args[0] == "failed"