Repository: ethereum/web3.py

Issue:
web3 version 7.0.0: what happened to web3.middleware construct_sign_and_send_raw_middleware and pythonic_middleware?
### What happened?

Today we tried updating our Python project's requirements.txt files to use the latest version of 3rd party dependencies.

One of the dependencies that got updated was web3 went from version 6.20.2 --> 7.0.0

Our code had this import line that used to work in version 6.20.2:
`from web3.middleware import construct_sign_and_send_raw_middleware, geth_poa_middleware, pythonic_middleware
`

**After the update to version 7.0.0, PyCharm says that all 3 of those items on that import line cannot be found.**

[From this section of the v6 to v7 Migration Guide](https://web3py.readthedocs.io/en/stable/migration.html#middleware-renaming-and-removals), I see that I need to replace
`geth_poa_middleware --> ExtraDataToPOAMiddleware`

_That works._

**The problem is those other 2 middleware:**
`construct_sign_and_send_raw_middleware    and    pythonic_middleware`

I can find no mention of them in the v6 to v7 Migration Guide, nor did a web search find anything related to how they changed in version 7.0.0.

_What happened to them?  Renamed?  Totally removed?_

This change has broke my code.

At a minimum, the v6 to v7 Migration Guide needs to tell me how to cope, and it is a documentation bug that it is currently unmentioned.

### Code that produced the error

```python
from web3.middleware import construct_sign_and_send_raw_middleware, geth_poa_middleware, pythonic_middleware
```


### Full error output

_No response_

### Fill this section in if you know how this could or should be fixed

Update the v6 to v7 Migration Guide

### web3 Version

7.0.0

### Python Version

3.12.4

### Operating System

win

### Output from `pip freeze`

```shell
I do not feel comfortable posting my project's complete list of dependencies here.

I hope that it is sufficient to note that this is what pip freeze prints regarding web3:

web3==7.0.0
```


Discussion:

Patch:
diff --git a/newsfragments/3463.bugfix.rst b/newsfragments/3463.bugfix.rst
new file mode 100644
index 00000000..40c73c44
--- /dev/null
+++ b/newsfragments/3463.bugfix.rst
@@ -0,0 +1,1 @@
+Specify a unique ``__hash__()`` for unhashable ``Web3Middleware`` types and use this hash as the middleware onion key when a name is not provided for the middleware. This fixes a bug where different middleware were given the same name and therefore raised errors.
diff --git a/web3/datastructures.py b/web3/datastructures.py
index 7b6ea087..90932aa4 100644
--- a/web3/datastructures.py
+++ b/web3/datastructures.py
@@ -182,7 +182,7 @@ class NamedElementOnion(Mapping[TKey, TValue]):
         if name is None:
             name = cast(TKey, element)
 
-        name = self._repr_if_not_hashable(name)
+        name = self._build_name(name)
 
         if name in self._queue:
             if name is element:
@@ -219,7 +219,7 @@ class NamedElementOnion(Mapping[TKey, TValue]):
             if name is None:
                 name = cast(TKey, element)
 
-            name = self._repr_if_not_hashable(name)
+            name = self._build_name(name)
 
             self._queue.move_to_end(name, last=False)
         elif layer == len(self._queue):
@@ -233,7 +233,7 @@ class NamedElementOnion(Mapping[TKey, TValue]):
         self._queue.clear()
 
     def replace(self, old: TKey, new: TKey) -> TValue:
-        old_name = self._repr_if_not_hashable(old)
+        old_name = self._build_name(old)
 
         if old_name not in self._queue:
             raise Web3ValueError(
@@ -248,15 +248,25 @@ class NamedElementOnion(Mapping[TKey, TValue]):
             self._queue[old_name] = new
         return to_be_replaced
 
-    def _repr_if_not_hashable(self, value: TKey) -> TKey:
+    @staticmethod
+    def _build_name(value: TKey) -> TKey:
         try:
             value.__hash__()
+            return value
         except TypeError:
-            value = cast(TKey, repr(value))
-        return value
+            # unhashable, unnamed elements
+            if not callable(value):
+                raise Web3TypeError(
+                    f"Expected a callable or hashable type, got {type(value)}"
+                )
+            # This will either be ``Web3Middleware`` class or the ``build`` method of a
+            # ``Web3MiddlewareBuilder``. Instantiate with empty ``Web3`` and use a
+            # unique identifier with the ``__hash__()`` as the name.
+            v = value(None)
+            return cast(TKey, f"{v.__class__}<{v.__hash__()}>")
 
     def remove(self, old: TKey) -> None:
-        old_name = self._repr_if_not_hashable(old)
+        old_name = self._build_name(old)
         if old_name not in self._queue:
             raise Web3ValueError("You can only remove something that has been added")
         del self._queue[old_name]
@@ -270,8 +280,8 @@ class NamedElementOnion(Mapping[TKey, TValue]):
         return [(val, key) for key, val in reversed(self._queue.items())]
 
     def _replace_with_new_name(self, old: TKey, new: TKey) -> None:
-        old_name = self._repr_if_not_hashable(old)
-        new_name = self._repr_if_not_hashable(new)
+        old_name = self._build_name(old)
+        new_name = self._build_name(new)
 
         self._queue[new_name] = new
         found_old = False
@@ -293,11 +303,11 @@ class NamedElementOnion(Mapping[TKey, TValue]):
         return NamedElementOnion(cast(List[Any], combined.items()))
 
     def __contains__(self, element: Any) -> bool:
-        element_name = self._repr_if_not_hashable(element)
+        element_name = self._build_name(element)
         return element_name in self._queue
 
     def __getitem__(self, element: TKey) -> TValue:
-        element_name = self._repr_if_not_hashable(element)
+        element_name = self._build_name(element)
         return self._queue[element_name]
 
     def __len__(self) -> int:
diff --git a/web3/middleware/base.py b/web3/middleware/base.py
index 565d2f14..590fce58 100644
--- a/web3/middleware/base.py
+++ b/web3/middleware/base.py
@@ -40,6 +40,14 @@ class Web3Middleware:
     def __init__(self, w3: Union["AsyncWeb3", "Web3"]) -> None:
         self._w3 = w3
 
+    def __hash__(self) -> int:
+        return hash(f"{self.__class__.__name__}({str(self.__dict__)})")
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, Web3Middleware):
+            return False
+        return self.__hash__() == other.__hash__()
+
     # -- sync -- #
 
     def wrap_make_request(self, make_request: "MakeRequestFn") -> "MakeRequestFn":


Test patch:
diff --git a/tests/core/middleware/test_middleware.py b/tests/core/middleware/test_middleware.py
new file mode 100644
index 00000000..39a23658
--- /dev/null
+++ b/tests/core/middleware/test_middleware.py
@@ -0,0 +1,81 @@
+import pytest
+
+from web3 import (
+    Web3,
+)
+from web3.exceptions import (
+    Web3ValueError,
+)
+from web3.middleware import (
+    FormattingMiddlewareBuilder,
+    Web3Middleware,
+)
+
+
+class TestMiddleware(Web3Middleware):
+    def response_processor(self, method, response):
+        if method == "eth_blockNumber":
+            response["result"] = 1234
+
+        return response
+
+
+class TestMiddleware2(Web3Middleware):
+    def response_processor(self, method, response):
+        if method == "eth_blockNumber":
+            response["result"] = 4321
+
+        return response
+
+
+def test_middleware_class_eq_magic_method():
+    w3_a = Web3()
+    w3_b = Web3()
+
+    mw1w3_a = TestMiddleware(w3_a)
+    assert mw1w3_a is not None
+    assert mw1w3_a != ""
+
+    mw1w3_a_equal = TestMiddleware(w3_a)
+    assert mw1w3_a == mw1w3_a_equal
+
+    mw2w3_a = TestMiddleware2(w3_a)
+    mw1w3_b = TestMiddleware(w3_b)
+    assert mw1w3_a != mw2w3_a
+    assert mw1w3_a != mw1w3_b
+
+
+def test_unnamed_middleware_are_given_unique_keys(w3):
+    request_formatting_middleware = FormattingMiddlewareBuilder.build(
+        request_formatters={lambda x: x}
+    )
+    request_formatting_middleware2 = FormattingMiddlewareBuilder.build(
+        request_formatters={lambda x: x if x else None}
+    )
+    result_formatting_middleware = FormattingMiddlewareBuilder.build(
+        result_formatters={lambda x: x}
+    )
+    error_formatting_middleware = FormattingMiddlewareBuilder.build(
+        error_formatters={lambda x: x}
+    )
+
+    # adding different middleware should not cause an error
+    w3.middleware_onion.add(request_formatting_middleware)
+    w3.middleware_onion.add(request_formatting_middleware2)
+    w3.middleware_onion.add(result_formatting_middleware)
+    w3.middleware_onion.add(error_formatting_middleware)
+    assert isinstance(w3.eth.block_number, int)
+
+    with pytest.raises(Web3ValueError):
+        # adding the same middleware again should cause an error
+        w3.middleware_onion.add(request_formatting_middleware)
+
+
+def test_unnamed_class_middleware_are_given_unique_keys(w3):
+    w3.middleware_onion.add(TestMiddleware)
+    w3.middleware_onion.add(TestMiddleware2)
+    assert isinstance(w3.eth.block_number, int)
+
+    with pytest.raises(Web3ValueError):
+        # adding the same middleware again should cause an error
+        w3.middleware_onion.add(TestMiddleware)