We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
web3.datastructures.AttributeDict.__hash__ fails if event logs contain list objects
* Version: 5.31.1
* Python: 3.8
* OS: linux
* pip freeze output omitted since not a package specific issue

### What was wrong?

Note: Not exactly an issue but something I had to circumvent myself when using the library and thinking it may be helpful if `web3.py` handled it instead directly.

I was trying to use functions requiring `__hash__` on event logs and the code failed saying lists are not hashable [here](https://github.com/ethereum/web3.py/blob/402d001b4f0a19720d859efe3c9e0ece39168224/web3/datastructures.py#L117). This happens if the event logs contain `list` objects nested inside them (which some smart contracts seem to return), as lists by default are mutable and thus not hashable in Python.

### How can it be fixed?

Would it make sense to (possibly recursively) cast any present lists to tuples inside the hash function to avoid crashes? Would some detection mechanism inside these pseudo-immutable classes be helpful to make sure they are indeed hashable (e.g., raising an error if a list is added)?

Any thoughts?

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `web3/datastructures.py`
Content:
```
1 from collections import (
2     OrderedDict,
3 )
4 from collections.abc import (
5     Hashable,
6 )
7 from typing import (
8     Any,
9     Callable,
10     Dict,
11     Iterator,
12     List,
13     Mapping,
14     MutableMapping,
15     Optional,
16     Sequence,
17     Type,
18     TypeVar,
19     Union,
20     cast,
21 )
22 
23 from eth_utils import (
24     is_integer,
25 )
26 
27 from web3._utils.formatters import (
28     recursive_map,
29 )
30 
31 # Hashable must be immutable:
32 # "the implementation of hashable collections requires that a
33 # key's hash value is immutable"
34 # https://docs.python.org/3/reference/datamodel.html#object.__hash__
35 
36 T = TypeVar("T")
37 TKey = TypeVar("TKey", bound=Hashable)
38 TValue = TypeVar("TValue")
39 
40 
41 class ReadableAttributeDict(Mapping[TKey, TValue]):
42     """
43     The read attributes for the AttributeDict types
44     """
45 
46     def __init__(
47         self, dictionary: Dict[TKey, TValue], *args: Any, **kwargs: Any
48     ) -> None:
49         # type ignored on 46/50 b/c dict() expects str index type not TKey
50         self.__dict__ = dict(dictionary)  # type: ignore
51         self.__dict__.update(dict(*args, **kwargs))
52 
53     def __getitem__(self, key: TKey) -> TValue:
54         return self.__dict__[key]  # type: ignore
55 
56     def __iter__(self) -> Iterator[Any]:
57         return iter(self.__dict__)
58 
59     def __len__(self) -> int:
60         return len(self.__dict__)
61 
62     def __repr__(self) -> str:
63         return self.__class__.__name__ + f"({self.__dict__!r})"
64 
65     def _repr_pretty_(self, builder: Any, cycle: bool) -> None:
66         """
67         Custom pretty output for the IPython console
68         https://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html#extending  # noqa: E501
69         """
70         builder.text(self.__class__.__name__ + "(")
71         if cycle:
72             builder.text("<cycle>")
73         else:
74             builder.pretty(self.__dict__)
75         builder.text(")")
76 
77     @classmethod
78     def _apply_if_mapping(cls: Type[T], value: TValue) -> Union[T, TValue]:
79         if isinstance(value, Mapping):
80             # error: Too many arguments for "object"
81             return cls(value)  # type: ignore
82         else:
83             return value
84 
85     @classmethod
86     def recursive(cls, value: TValue) -> "ReadableAttributeDict[TKey, TValue]":
87         return recursive_map(cls._apply_if_mapping, value)
88 
89 
90 class MutableAttributeDict(
91     MutableMapping[TKey, TValue], ReadableAttributeDict[TKey, TValue]
92 ):
93     def __setitem__(self, key: Any, val: Any) -> None:
94         self.__dict__[key] = val
95 
96     def __delitem__(self, key: Any) -> None:
97         del self.__dict__[key]
98 
99 
100 class AttributeDict(ReadableAttributeDict[TKey, TValue], Hashable):
101     """
102     This provides superficial immutability, someone could hack around it
103     """
104 
105     def __setattr__(self, attr: str, val: TValue) -> None:
106         if attr == "__dict__":
107             super().__setattr__(attr, val)
108         else:
109             raise TypeError(
110                 "This data is immutable -- create a copy instead of modifying"
111             )
112 
113     def __delattr__(self, key: str) -> None:
114         raise TypeError("This data is immutable -- create a copy instead of modifying")
115 
116     def __hash__(self) -> int:
117         return hash(tuple(sorted(self.items())))
118 
119     def __eq__(self, other: Any) -> bool:
120         if isinstance(other, Mapping):
121             return self.__dict__ == dict(other)
122         else:
123             return False
124 
125 
126 class NamedElementOnion(Mapping[TKey, TValue]):
127     """
128     Add layers to an onion-shaped structure. Optionally, inject to a specific layer.
129     This structure is iterable, where the outermost layer is first, and innermost
130     is last.
131     """
132 
133     def __init__(
134         self,
135         init_elements: Sequence[Any],
136         valid_element: Callable[..., bool] = callable,
137     ) -> None:
138         self._queue: "OrderedDict[Any, Any]" = OrderedDict()
139         for element in reversed(init_elements):
140             if valid_element(element):
141                 self.add(element)
142             else:
143                 self.add(*element)
144 
145     def add(self, element: TValue, name: Optional[TKey] = None) -> None:
146         if name is None:
147             name = cast(TKey, element)
148 
149         if name in self._queue:
150             if name is element:
151                 raise ValueError("You can't add the same un-named instance twice")
152             else:
153                 raise ValueError(
154                     "You can't add the same name again, use replace instead"
155                 )
156 
157         self._queue[name] = element
158 
159     def inject(
160         self, element: TValue, name: Optional[TKey] = None, layer: Optional[int] = None
161     ) -> None:
162         """
163         Inject a named element to an arbitrary layer in the onion.
164 
165         The current implementation only supports insertion at the innermost layer,
166         or at the outermost layer. Note that inserting to the outermost is equivalent
167         to calling :meth:`add` .
168         """
169         if not is_integer(layer):
170             raise TypeError("The layer for insertion must be an int.")
171         elif layer != 0 and layer != len(self._queue):
172             raise NotImplementedError(
173                 f"You can only insert to the beginning or end of a {type(self)}, "
174                 f"currently. You tried to insert to {layer}, but only 0 and "
175                 f"{len(self._queue)} are permitted. "
176             )
177 
178         self.add(element, name=name)
179 
180         if layer == 0:
181             if name is None:
182                 name = cast(TKey, element)
183             self._queue.move_to_end(name, last=False)
184         elif layer == len(self._queue):
185             return
186         else:
187             raise AssertionError(
188                 "Impossible to reach: earlier validation raises an error"
189             )
190 
191     def clear(self) -> None:
192         self._queue.clear()
193 
194     def replace(self, old: TKey, new: TKey) -> TValue:
195         if old not in self._queue:
196             raise ValueError(
197                 "You can't replace unless one already exists, use add instead"
198             )
199         to_be_replaced = self._queue[old]
200         if to_be_replaced is old:
201             # re-insert with new name in old slot
202             self._replace_with_new_name(old, new)
203         else:
204             self._queue[old] = new
205         return to_be_replaced
206 
207     def remove(self, old: TKey) -> None:
208         if old not in self._queue:
209             raise ValueError("You can only remove something that has been added")
210         del self._queue[old]
211 
212     @property
213     def middlewares(self) -> Sequence[Any]:
214         """
215         Returns middlewares in the appropriate order to be imported into a new Web3
216         instance (reversed _queue order) as a list of (middleware, name) tuples.
217         """
218         return [(val, key) for key, val in reversed(self._queue.items())]
219 
220     def _replace_with_new_name(self, old: TKey, new: TKey) -> None:
221         self._queue[new] = new
222         found_old = False
223         for key in list(self._queue.keys()):
224             if not found_old:
225                 if key == old:
226                     found_old = True
227                 continue
228             elif key != new:
229                 self._queue.move_to_end(key)
230         del self._queue[old]
231 
232     def __iter__(self) -> Iterator[TKey]:
233         elements = self._queue.values()
234         if not isinstance(elements, Sequence):
235             # type ignored b/c elements is set as _OrderedDictValuesView[Any] on 210
236             elements = list(elements)  # type: ignore
237         return iter(reversed(elements))
238 
239     def __add__(self, other: Any) -> "NamedElementOnion[TKey, TValue]":
240         if not isinstance(other, NamedElementOnion):
241             raise NotImplementedError(
242                 "You can only combine with another NamedElementOnion"
243             )
244         combined = self._queue.copy()
245         combined.update(other._queue)
246         return NamedElementOnion(cast(List[Any], combined.items()))
247 
248     def __contains__(self, element: Any) -> bool:
249         return element in self._queue
250 
251     def __getitem__(self, element: TKey) -> TValue:
252         return self._queue[element]
253 
254     def __len__(self) -> int:
255         return len(self._queue)
256 
257     def __reversed__(self) -> Iterator[TValue]:
258         elements = cast(List[Any], self._queue.values())
259         if not isinstance(elements, Sequence):
260             elements = list(elements)
261         return iter(elements)
262
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/web3/datastructures.py b/web3/datastructures.py
--- a/web3/datastructures.py
+++ b/web3/datastructures.py
@@ -114,7 +114,7 @@
         raise TypeError("This data is immutable -- create a copy instead of modifying")
 
     def __hash__(self) -> int:
-        return hash(tuple(sorted(self.items())))
+        return hash(tuple(sorted(tupleize_lists_nested(self).items())))
 
     def __eq__(self, other: Any) -> bool:
         if isinstance(other, Mapping):
@@ -123,6 +123,29 @@
             return False
 
 
+def tupleize_lists_nested(d: Mapping[TKey, TValue]) -> AttributeDict[TKey, TValue]:
+    """
+    Unhashable types inside dicts will throw an error if attempted to be hashed.
+    This method converts lists to tuples, rendering them hashable.
+    Other unhashable types found will raise a TypeError
+    """
+
+    def _to_tuple(lst: List[Any]) -> Any:
+        return tuple(_to_tuple(i) if isinstance(i, list) else i for i in lst)
+
+    ret = dict()
+    for k, v in d.items():
+        if isinstance(v, List):
+            ret[k] = _to_tuple(v)
+        elif isinstance(v, Mapping):
+            ret[k] = tupleize_lists_nested(v)
+        elif not isinstance(v, Hashable):
+            raise TypeError(f"Found unhashable type '{type(v).__name__}': {v}")
+        else:
+            ret[k] = v
+    return AttributeDict(ret)
+
+
 class NamedElementOnion(Mapping[TKey, TValue]):
     """
     Add layers to an onion-shaped structure. Optionally, inject to a specific layer.

{"golden_diff": "diff --git a/web3/datastructures.py b/web3/datastructures.py\n--- a/web3/datastructures.py\n+++ b/web3/datastructures.py\n@@ -114,7 +114,7 @@\n         raise TypeError(\"This data is immutable -- create a copy instead of modifying\")\n \n     def __hash__(self) -> int:\n-        return hash(tuple(sorted(self.items())))\n+        return hash(tuple(sorted(tupleize_lists_nested(self).items())))\n \n     def __eq__(self, other: Any) -> bool:\n         if isinstance(other, Mapping):\n@@ -123,6 +123,29 @@\n             return False\n \n \n+def tupleize_lists_nested(d: Mapping[TKey, TValue]) -> AttributeDict[TKey, TValue]:\n+    \"\"\"\n+    Unhashable types inside dicts will throw an error if attempted to be hashed.\n+    This method converts lists to tuples, rendering them hashable.\n+    Other unhashable types found will raise a TypeError\n+    \"\"\"\n+\n+    def _to_tuple(lst: List[Any]) -> Any:\n+        return tuple(_to_tuple(i) if isinstance(i, list) else i for i in lst)\n+\n+    ret = dict()\n+    for k, v in d.items():\n+        if isinstance(v, List):\n+            ret[k] = _to_tuple(v)\n+        elif isinstance(v, Mapping):\n+            ret[k] = tupleize_lists_nested(v)\n+        elif not isinstance(v, Hashable):\n+            raise TypeError(f\"Found unhashable type '{type(v).__name__}': {v}\")\n+        else:\n+            ret[k] = v\n+    return AttributeDict(ret)\n+\n+\n class NamedElementOnion(Mapping[TKey, TValue]):\n     \"\"\"\n     Add layers to an onion-shaped structure. Optionally, inject to a specific layer.\n", "issue": "web3.datastructures.AttributeDict.__hash__ fails if event logs contain list objects\n* Version: 5.31.1\r\n* Python: 3.8\r\n* OS: linux\r\n* pip freeze output omitted since not a package specific issue\r\n\r\n### What was wrong?\r\n\r\nNote: Not exactly an issue but something I had to circumvent myself when using the library and thinking it may be helpful if `web3.py` handled it instead directly.\r\n\r\nI was trying to use functions requiring `__hash__` on event logs and the code failed saying lists are not hashable [here](https://github.com/ethereum/web3.py/blob/402d001b4f0a19720d859efe3c9e0ece39168224/web3/datastructures.py#L117). This happens if the event logs contain `list` objects nested inside them (which some smart contracts seem to return), as lists by default are mutable and thus not hashable in Python.\r\n\r\n### How can it be fixed?\r\n\r\nWould it make sense to (possibly recursively) cast any present lists to tuples inside the hash function to avoid crashes? Would some detection mechanism inside these pseudo-immutable classes be helpful to make sure they are indeed hashable (e.g., raising an error if a list is added)?\r\n\r\nAny thoughts?\n", "before_files": [{"content": "from collections import (\n    OrderedDict,\n)\nfrom collections.abc import (\n    Hashable,\n)\nfrom typing import (\n    Any,\n    Callable,\n    Dict,\n    Iterator,\n    List,\n    Mapping,\n    MutableMapping,\n    Optional,\n    Sequence,\n    Type,\n    TypeVar,\n    Union,\n    cast,\n)\n\nfrom eth_utils import (\n    is_integer,\n)\n\nfrom web3._utils.formatters import (\n    recursive_map,\n)\n\n# Hashable must be immutable:\n# \"the implementation of hashable collections requires that a\n# key's hash value is immutable\"\n# https://docs.python.org/3/reference/datamodel.html#object.__hash__\n\nT = TypeVar(\"T\")\nTKey = TypeVar(\"TKey\", bound=Hashable)\nTValue = TypeVar(\"TValue\")\n\n\nclass ReadableAttributeDict(Mapping[TKey, TValue]):\n    \"\"\"\n    The read attributes for the AttributeDict types\n    \"\"\"\n\n    def __init__(\n        self, dictionary: Dict[TKey, TValue], *args: Any, **kwargs: Any\n    ) -> None:\n        # type ignored on 46/50 b/c dict() expects str index type not TKey\n        self.__dict__ = dict(dictionary)  # type: ignore\n        self.__dict__.update(dict(*args, **kwargs))\n\n    def __getitem__(self, key: TKey) -> TValue:\n        return self.__dict__[key]  # type: ignore\n\n    def __iter__(self) -> Iterator[Any]:\n        return iter(self.__dict__)\n\n    def __len__(self) -> int:\n        return len(self.__dict__)\n\n    def __repr__(self) -> str:\n        return self.__class__.__name__ + f\"({self.__dict__!r})\"\n\n    def _repr_pretty_(self, builder: Any, cycle: bool) -> None:\n        \"\"\"\n        Custom pretty output for the IPython console\n        https://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html#extending  # noqa: E501\n        \"\"\"\n        builder.text(self.__class__.__name__ + \"(\")\n        if cycle:\n            builder.text(\"<cycle>\")\n        else:\n            builder.pretty(self.__dict__)\n        builder.text(\")\")\n\n    @classmethod\n    def _apply_if_mapping(cls: Type[T], value: TValue) -> Union[T, TValue]:\n        if isinstance(value, Mapping):\n            # error: Too many arguments for \"object\"\n            return cls(value)  # type: ignore\n        else:\n            return value\n\n    @classmethod\n    def recursive(cls, value: TValue) -> \"ReadableAttributeDict[TKey, TValue]\":\n        return recursive_map(cls._apply_if_mapping, value)\n\n\nclass MutableAttributeDict(\n    MutableMapping[TKey, TValue], ReadableAttributeDict[TKey, TValue]\n):\n    def __setitem__(self, key: Any, val: Any) -> None:\n        self.__dict__[key] = val\n\n    def __delitem__(self, key: Any) -> None:\n        del self.__dict__[key]\n\n\nclass AttributeDict(ReadableAttributeDict[TKey, TValue], Hashable):\n    \"\"\"\n    This provides superficial immutability, someone could hack around it\n    \"\"\"\n\n    def __setattr__(self, attr: str, val: TValue) -> None:\n        if attr == \"__dict__\":\n            super().__setattr__(attr, val)\n        else:\n            raise TypeError(\n                \"This data is immutable -- create a copy instead of modifying\"\n            )\n\n    def __delattr__(self, key: str) -> None:\n        raise TypeError(\"This data is immutable -- create a copy instead of modifying\")\n\n    def __hash__(self) -> int:\n        return hash(tuple(sorted(self.items())))\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, Mapping):\n            return self.__dict__ == dict(other)\n        else:\n            return False\n\n\nclass NamedElementOnion(Mapping[TKey, TValue]):\n    \"\"\"\n    Add layers to an onion-shaped structure. Optionally, inject to a specific layer.\n    This structure is iterable, where the outermost layer is first, and innermost\n    is last.\n    \"\"\"\n\n    def __init__(\n        self,\n        init_elements: Sequence[Any],\n        valid_element: Callable[..., bool] = callable,\n    ) -> None:\n        self._queue: \"OrderedDict[Any, Any]\" = OrderedDict()\n        for element in reversed(init_elements):\n            if valid_element(element):\n                self.add(element)\n            else:\n                self.add(*element)\n\n    def add(self, element: TValue, name: Optional[TKey] = None) -> None:\n        if name is None:\n            name = cast(TKey, element)\n\n        if name in self._queue:\n            if name is element:\n                raise ValueError(\"You can't add the same un-named instance twice\")\n            else:\n                raise ValueError(\n                    \"You can't add the same name again, use replace instead\"\n                )\n\n        self._queue[name] = element\n\n    def inject(\n        self, element: TValue, name: Optional[TKey] = None, layer: Optional[int] = None\n    ) -> None:\n        \"\"\"\n        Inject a named element to an arbitrary layer in the onion.\n\n        The current implementation only supports insertion at the innermost layer,\n        or at the outermost layer. Note that inserting to the outermost is equivalent\n        to calling :meth:`add` .\n        \"\"\"\n        if not is_integer(layer):\n            raise TypeError(\"The layer for insertion must be an int.\")\n        elif layer != 0 and layer != len(self._queue):\n            raise NotImplementedError(\n                f\"You can only insert to the beginning or end of a {type(self)}, \"\n                f\"currently. You tried to insert to {layer}, but only 0 and \"\n                f\"{len(self._queue)} are permitted. \"\n            )\n\n        self.add(element, name=name)\n\n        if layer == 0:\n            if name is None:\n                name = cast(TKey, element)\n            self._queue.move_to_end(name, last=False)\n        elif layer == len(self._queue):\n            return\n        else:\n            raise AssertionError(\n                \"Impossible to reach: earlier validation raises an error\"\n            )\n\n    def clear(self) -> None:\n        self._queue.clear()\n\n    def replace(self, old: TKey, new: TKey) -> TValue:\n        if old not in self._queue:\n            raise ValueError(\n                \"You can't replace unless one already exists, use add instead\"\n            )\n        to_be_replaced = self._queue[old]\n        if to_be_replaced is old:\n            # re-insert with new name in old slot\n            self._replace_with_new_name(old, new)\n        else:\n            self._queue[old] = new\n        return to_be_replaced\n\n    def remove(self, old: TKey) -> None:\n        if old not in self._queue:\n            raise ValueError(\"You can only remove something that has been added\")\n        del self._queue[old]\n\n    @property\n    def middlewares(self) -> Sequence[Any]:\n        \"\"\"\n        Returns middlewares in the appropriate order to be imported into a new Web3\n        instance (reversed _queue order) as a list of (middleware, name) tuples.\n        \"\"\"\n        return [(val, key) for key, val in reversed(self._queue.items())]\n\n    def _replace_with_new_name(self, old: TKey, new: TKey) -> None:\n        self._queue[new] = new\n        found_old = False\n        for key in list(self._queue.keys()):\n            if not found_old:\n                if key == old:\n                    found_old = True\n                continue\n            elif key != new:\n                self._queue.move_to_end(key)\n        del self._queue[old]\n\n    def __iter__(self) -> Iterator[TKey]:\n        elements = self._queue.values()\n        if not isinstance(elements, Sequence):\n            # type ignored b/c elements is set as _OrderedDictValuesView[Any] on 210\n            elements = list(elements)  # type: ignore\n        return iter(reversed(elements))\n\n    def __add__(self, other: Any) -> \"NamedElementOnion[TKey, TValue]\":\n        if not isinstance(other, NamedElementOnion):\n            raise NotImplementedError(\n                \"You can only combine with another NamedElementOnion\"\n            )\n        combined = self._queue.copy()\n        combined.update(other._queue)\n        return NamedElementOnion(cast(List[Any], combined.items()))\n\n    def __contains__(self, element: Any) -> bool:\n        return element in self._queue\n\n    def __getitem__(self, element: TKey) -> TValue:\n        return self._queue[element]\n\n    def __len__(self) -> int:\n        return len(self._queue)\n\n    def __reversed__(self) -> Iterator[TValue]:\n        elements = cast(List[Any], self._queue.values())\n        if not isinstance(elements, Sequence):\n            elements = list(elements)\n        return iter(elements)\n", "path": "web3/datastructures.py"}], "after_files": [{"content": "from collections import (\n    OrderedDict,\n)\nfrom collections.abc import (\n    Hashable,\n)\nfrom typing import (\n    Any,\n    Callable,\n    Dict,\n    Iterator,\n    List,\n    Mapping,\n    MutableMapping,\n    Optional,\n    Sequence,\n    Type,\n    TypeVar,\n    Union,\n    cast,\n)\n\nfrom eth_utils import (\n    is_integer,\n)\n\nfrom web3._utils.formatters import (\n    recursive_map,\n)\n\n# Hashable must be immutable:\n# \"the implementation of hashable collections requires that a\n# key's hash value is immutable\"\n# https://docs.python.org/3/reference/datamodel.html#object.__hash__\n\nT = TypeVar(\"T\")\nTKey = TypeVar(\"TKey\", bound=Hashable)\nTValue = TypeVar(\"TValue\")\n\n\nclass ReadableAttributeDict(Mapping[TKey, TValue]):\n    \"\"\"\n    The read attributes for the AttributeDict types\n    \"\"\"\n\n    def __init__(\n        self, dictionary: Dict[TKey, TValue], *args: Any, **kwargs: Any\n    ) -> None:\n        # type ignored on 46/50 b/c dict() expects str index type not TKey\n        self.__dict__ = dict(dictionary)  # type: ignore\n        self.__dict__.update(dict(*args, **kwargs))\n\n    def __getitem__(self, key: TKey) -> TValue:\n        return self.__dict__[key]  # type: ignore\n\n    def __iter__(self) -> Iterator[Any]:\n        return iter(self.__dict__)\n\n    def __len__(self) -> int:\n        return len(self.__dict__)\n\n    def __repr__(self) -> str:\n        return self.__class__.__name__ + f\"({self.__dict__!r})\"\n\n    def _repr_pretty_(self, builder: Any, cycle: bool) -> None:\n        \"\"\"\n        Custom pretty output for the IPython console\n        https://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html#extending  # noqa: E501\n        \"\"\"\n        builder.text(self.__class__.__name__ + \"(\")\n        if cycle:\n            builder.text(\"<cycle>\")\n        else:\n            builder.pretty(self.__dict__)\n        builder.text(\")\")\n\n    @classmethod\n    def _apply_if_mapping(cls: Type[T], value: TValue) -> Union[T, TValue]:\n        if isinstance(value, Mapping):\n            # error: Too many arguments for \"object\"\n            return cls(value)  # type: ignore\n        else:\n            return value\n\n    @classmethod\n    def recursive(cls, value: TValue) -> \"ReadableAttributeDict[TKey, TValue]\":\n        return recursive_map(cls._apply_if_mapping, value)\n\n\nclass MutableAttributeDict(\n    MutableMapping[TKey, TValue], ReadableAttributeDict[TKey, TValue]\n):\n    def __setitem__(self, key: Any, val: Any) -> None:\n        self.__dict__[key] = val\n\n    def __delitem__(self, key: Any) -> None:\n        del self.__dict__[key]\n\n\nclass AttributeDict(ReadableAttributeDict[TKey, TValue], Hashable):\n    \"\"\"\n    This provides superficial immutability, someone could hack around it\n    \"\"\"\n\n    def __setattr__(self, attr: str, val: TValue) -> None:\n        if attr == \"__dict__\":\n            super().__setattr__(attr, val)\n        else:\n            raise TypeError(\n                \"This data is immutable -- create a copy instead of modifying\"\n            )\n\n    def __delattr__(self, key: str) -> None:\n        raise TypeError(\"This data is immutable -- create a copy instead of modifying\")\n\n    def __hash__(self) -> int:\n        return hash(tuple(sorted(tupleize_lists_nested(self).items())))\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, Mapping):\n            return self.__dict__ == dict(other)\n        else:\n            return False\n\n\ndef tupleize_lists_nested(d: Mapping[TKey, TValue]) -> AttributeDict[TKey, TValue]:\n    \"\"\"\n    Unhashable types inside dicts will throw an error if attempted to be hashed.\n    This method converts lists to tuples, rendering them hashable.\n    Other unhashable types found will raise a TypeError\n    \"\"\"\n\n    def _to_tuple(lst: List[Any]) -> Any:\n        return tuple(_to_tuple(i) if isinstance(i, list) else i for i in lst)\n\n    ret = dict()\n    for k, v in d.items():\n        if isinstance(v, List):\n            ret[k] = _to_tuple(v)\n        elif isinstance(v, Mapping):\n            ret[k] = tupleize_lists_nested(v)\n        elif not isinstance(v, Hashable):\n            raise TypeError(f\"Found unhashable type '{type(v).__name__}': {v}\")\n        else:\n            ret[k] = v\n    return AttributeDict(ret)\n\n\nclass NamedElementOnion(Mapping[TKey, TValue]):\n    \"\"\"\n    Add layers to an onion-shaped structure. Optionally, inject to a specific layer.\n    This structure is iterable, where the outermost layer is first, and innermost\n    is last.\n    \"\"\"\n\n    def __init__(\n        self,\n        init_elements: Sequence[Any],\n        valid_element: Callable[..., bool] = callable,\n    ) -> None:\n        self._queue: \"OrderedDict[Any, Any]\" = OrderedDict()\n        for element in reversed(init_elements):\n            if valid_element(element):\n                self.add(element)\n            else:\n                self.add(*element)\n\n    def add(self, element: TValue, name: Optional[TKey] = None) -> None:\n        if name is None:\n            name = cast(TKey, element)\n\n        if name in self._queue:\n            if name is element:\n                raise ValueError(\"You can't add the same un-named instance twice\")\n            else:\n                raise ValueError(\n                    \"You can't add the same name again, use replace instead\"\n                )\n\n        self._queue[name] = element\n\n    def inject(\n        self, element: TValue, name: Optional[TKey] = None, layer: Optional[int] = None\n    ) -> None:\n        \"\"\"\n        Inject a named element to an arbitrary layer in the onion.\n\n        The current implementation only supports insertion at the innermost layer,\n        or at the outermost layer. Note that inserting to the outermost is equivalent\n        to calling :meth:`add` .\n        \"\"\"\n        if not is_integer(layer):\n            raise TypeError(\"The layer for insertion must be an int.\")\n        elif layer != 0 and layer != len(self._queue):\n            raise NotImplementedError(\n                f\"You can only insert to the beginning or end of a {type(self)}, \"\n                f\"currently. You tried to insert to {layer}, but only 0 and \"\n                f\"{len(self._queue)} are permitted. \"\n            )\n\n        self.add(element, name=name)\n\n        if layer == 0:\n            if name is None:\n                name = cast(TKey, element)\n            self._queue.move_to_end(name, last=False)\n        elif layer == len(self._queue):\n            return\n        else:\n            raise AssertionError(\n                \"Impossible to reach: earlier validation raises an error\"\n            )\n\n    def clear(self) -> None:\n        self._queue.clear()\n\n    def replace(self, old: TKey, new: TKey) -> TValue:\n        if old not in self._queue:\n            raise ValueError(\n                \"You can't replace unless one already exists, use add instead\"\n            )\n        to_be_replaced = self._queue[old]\n        if to_be_replaced is old:\n            # re-insert with new name in old slot\n            self._replace_with_new_name(old, new)\n        else:\n            self._queue[old] = new\n        return to_be_replaced\n\n    def remove(self, old: TKey) -> None:\n        if old not in self._queue:\n            raise ValueError(\"You can only remove something that has been added\")\n        del self._queue[old]\n\n    @property\n    def middlewares(self) -> Sequence[Any]:\n        \"\"\"\n        Returns middlewares in the appropriate order to be imported into a new Web3\n        instance (reversed _queue order) as a list of (middleware, name) tuples.\n        \"\"\"\n        return [(val, key) for key, val in reversed(self._queue.items())]\n\n    def _replace_with_new_name(self, old: TKey, new: TKey) -> None:\n        self._queue[new] = new\n        found_old = False\n        for key in list(self._queue.keys()):\n            if not found_old:\n                if key == old:\n                    found_old = True\n                continue\n            elif key != new:\n                self._queue.move_to_end(key)\n        del self._queue[old]\n\n    def __iter__(self) -> Iterator[TKey]:\n        elements = self._queue.values()\n        if not isinstance(elements, Sequence):\n            # type ignored b/c elements is set as _OrderedDictValuesView[Any] on 210\n            elements = list(elements)  # type: ignore\n        return iter(reversed(elements))\n\n    def __add__(self, other: Any) -> \"NamedElementOnion[TKey, TValue]\":\n        if not isinstance(other, NamedElementOnion):\n            raise NotImplementedError(\n                \"You can only combine with another NamedElementOnion\"\n            )\n        combined = self._queue.copy()\n        combined.update(other._queue)\n        return NamedElementOnion(cast(List[Any], combined.items()))\n\n    def __contains__(self, element: Any) -> bool:\n        return element in self._queue\n\n    def __getitem__(self, element: TKey) -> TValue:\n        return self._queue[element]\n\n    def __len__(self) -> int:\n        return len(self._queue)\n\n    def __reversed__(self) -> Iterator[TValue]:\n        elements = cast(List[Any], self._queue.values())\n        if not isinstance(elements, Sequence):\n            elements = list(elements)\n        return iter(elements)\n", "path": "web3/datastructures.py"}]}