Skip to content

Commit dcb0455

Browse files
committed
Add a bit more documentation and tests for attrdict middlewares
1 parent 578b8e8 commit dcb0455

File tree

5 files changed

+178
-10
lines changed

5 files changed

+178
-10
lines changed

docs/examples.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ The script can be run with: ``python ./eventscanner.py <your JSON-RPC API URL>``
881881
882882
:param event: Symbolic dictionary of the event data
883883
884-
:return: Internal state structure that is the result of event tranformation.
884+
:return: Internal state structure that is the result of event transformation.
885885
"""
886886
887887
@abstractmethod

docs/middleware.rst

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ AttributeDict
2929

3030
.. py:method:: web3.middleware.attrdict_middleware
3131
32-
This middleware converts the output of a function from a dictionary to an ``AttributeDict``
33-
which enables dot-syntax access, like ``eth.get_block('latest').number``
34-
in addition to ``eth.get_block('latest')['number']``.
32+
This middleware recursively converts any dictionary type in the result of a call
33+
to an ``AttributeDict``. This enables dot-syntax access, like
34+
``eth.get_block('latest').number`` in addition to
35+
``eth.get_block('latest')['number']``.
36+
37+
.. note::
38+
Accessing a property via attribute breaks type hinting. For this reason, this
39+
feature is available as a middleware, which may be removed if desired.
3540

3641
.eth Name Resolution
3742
~~~~~~~~~~~~~~~~~~~~~
@@ -42,10 +47,10 @@ AttributeDict
4247
address that the name points to. For example :meth:`w3.eth.send_transaction <web3.eth.Eth.send_transaction>` will
4348
accept .eth names in the 'from' and 'to' fields.
4449

45-
.. note::
46-
This middleware only converts ENS names if invoked with the mainnet
47-
(where the ENS contract is deployed), for all other cases will result in an
48-
``InvalidAddress`` error
50+
.. note::
51+
This middleware only converts ENS names if invoked with the mainnet
52+
(where the ENS contract is deployed), for all other cases will result in an
53+
``InvalidAddress`` error
4954

5055
Pythonic
5156
~~~~~~~~~~~~

docs/providers.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ Supported Middleware
369369
- :meth:`Gas Price Strategy <web3.middleware.gas_price_strategy_middleware>`
370370
- :meth:`Buffered Gas Estimate Middleware <web3.middleware.buffered_gas_estimate_middleware>`
371371
- :meth:`Stalecheck Middleware <web3.middleware.make_stalecheck_middleware>`
372+
- :meth:`Attribute Dict Middleware <web3.middleware.attrdict_middleware>`
372373
- :meth:`Validation Middleware <web3.middleware.validation>`
373374
- :ref:`Geth POA Middleware <geth-poa>`
374375
- :meth:`Simple Cache Middleware <web3.middleware.simple_cache_middleware>`

docs/web3.eth.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ web3.eth API
1616
The ``web3.eth`` object exposes the following properties and methods to
1717
interact with the RPC APIs under the ``eth_`` namespace.
1818

19-
Often, when a property or method returns a mapping of keys to values, it
19+
By default, when a property or method returns a mapping of keys to values, it
2020
will return an ``AttributeDict`` which acts like a ``dict`` but you can
2121
access the keys as attributes and cannot modify its fields. For example,
2222
you can find the latest block number in these two ways:
@@ -39,6 +39,11 @@ you can find the latest block number in these two ways:
3939
Traceback # ... etc ...
4040
TypeError: This data is immutable -- create a copy instead of modifying
4141
42+
This feature is available via the ``attrdict_middleware`` which is a default middleware.
43+
Of note, accessing a property via attribute will break type hinting. If typing is
44+
crucial for your application, accessing via key / value, as well as removing the
45+
``attrdict_middleware`` altogether, may be desired.
46+
4247

4348
Properties
4449
----------
@@ -332,7 +337,8 @@ The following methods are available on the ``web3.eth`` namespace.
332337
333338
* Merkle proof verification using py-trie.
334339

335-
The following example verifies that the values returned in the AttributeDict are included in the state of given trie ``root``.
340+
The following example verifies that the values returned in the ``AttributeDict``
341+
are included in the state of given trie ``root``.
336342

337343
.. code-block:: python
338344
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import pytest
2+
3+
from web3 import (
4+
EthereumTesterProvider,
5+
Web3,
6+
)
7+
from web3.datastructures import (
8+
AttributeDict,
9+
)
10+
from web3.middleware import (
11+
async_attrdict_middleware,
12+
async_construct_result_generator_middleware,
13+
attrdict_middleware,
14+
construct_result_generator_middleware,
15+
)
16+
from web3.providers.eth_tester import (
17+
AsyncEthereumTesterProvider,
18+
)
19+
from web3.types import (
20+
RPCEndpoint,
21+
)
22+
23+
GENERATED_NESTED_DICT_RESULT = {
24+
"result": {
25+
"a": 1,
26+
"b": {
27+
"b1": 1,
28+
"b2": {"b2a": 1, "b2b": {"b2b1": 1, "b2b2": {"test": "fin"}}},
29+
},
30+
}
31+
}
32+
33+
34+
def _assert_dict_and_not_attrdict(value):
35+
assert not isinstance(value, AttributeDict)
36+
assert isinstance(value, dict)
37+
38+
39+
def test_attrdict_middleware_default_for_ethereum_tester_provider():
40+
w3 = Web3(EthereumTesterProvider())
41+
assert w3.middleware_onion.get("attrdict") == attrdict_middleware
42+
43+
44+
def test_attrdict_middleware_is_recursive(w3):
45+
w3.middleware_onion.inject(
46+
construct_result_generator_middleware(
47+
{RPCEndpoint("fake_endpoint"): lambda *_: GENERATED_NESTED_DICT_RESULT}
48+
),
49+
"result_gen",
50+
layer=0,
51+
)
52+
response = w3.manager.request_blocking("fake_endpoint", [])
53+
54+
result = response["result"]
55+
assert isinstance(result, AttributeDict)
56+
assert response.result == result
57+
58+
assert isinstance(result["b"], AttributeDict)
59+
assert result.b == result["b"]
60+
assert isinstance(result.b["b2"], AttributeDict)
61+
assert result.b.b2 == result.b["b2"]
62+
assert isinstance(result.b.b2["b2b"], AttributeDict)
63+
assert result.b.b2.b2b == result.b.b2["b2b"]
64+
assert isinstance(result.b.b2.b2b["b2b2"], AttributeDict)
65+
assert result.b.b2.b2b.b2b2 == result.b.b2.b2b["b2b2"]
66+
67+
# cleanup
68+
w3.middleware_onion.remove("result_gen")
69+
70+
71+
def test_no_attrdict_middleware_does_not_convert_dicts_to_attrdict():
72+
w3 = Web3(EthereumTesterProvider())
73+
74+
w3.middleware_onion.inject(
75+
construct_result_generator_middleware(
76+
{RPCEndpoint("fake_endpoint"): lambda *_: GENERATED_NESTED_DICT_RESULT}
77+
),
78+
"result_gen",
79+
layer=0,
80+
)
81+
82+
# remove attrdict middleware
83+
w3.middleware_onion.remove("attrdict")
84+
85+
response = w3.manager.request_blocking("fake_endpoint", [])
86+
87+
result = response["result"]
88+
89+
_assert_dict_and_not_attrdict(result)
90+
_assert_dict_and_not_attrdict(result["b"])
91+
_assert_dict_and_not_attrdict(result["b"]["b2"])
92+
_assert_dict_and_not_attrdict(result["b"]["b2"]["b2b"])
93+
_assert_dict_and_not_attrdict(result["b"]["b2"]["b2b"]["b2b2"])
94+
95+
96+
# --- async --- #
97+
98+
99+
@pytest.mark.asyncio
100+
async def test_async_attrdict_middleware_default_for_async_ethereum_tester_provider():
101+
async_w3 = Web3(AsyncEthereumTesterProvider())
102+
assert async_w3.middleware_onion.get("attrdict") == async_attrdict_middleware
103+
104+
105+
@pytest.mark.asyncio
106+
async def test_async_attrdict_middleware_is_recursive(async_w3):
107+
async_w3.middleware_onion.inject(
108+
await async_construct_result_generator_middleware(
109+
{RPCEndpoint("fake_endpoint"): lambda *_: GENERATED_NESTED_DICT_RESULT}
110+
),
111+
"result_gen",
112+
layer=0,
113+
)
114+
response = await async_w3.manager.coro_request("fake_endpoint", [])
115+
116+
result = response["result"]
117+
assert isinstance(result, AttributeDict)
118+
assert response.result == result
119+
120+
assert isinstance(result["b"], AttributeDict)
121+
assert result.b == result["b"]
122+
assert isinstance(result.b["b2"], AttributeDict)
123+
assert result.b.b2 == result.b["b2"]
124+
assert isinstance(result.b.b2["b2b"], AttributeDict)
125+
assert result.b.b2.b2b == result.b.b2["b2b"]
126+
assert isinstance(result.b.b2.b2b["b2b2"], AttributeDict)
127+
assert result.b.b2.b2b.b2b2 == result.b.b2.b2b["b2b2"]
128+
129+
# cleanup
130+
async_w3.middleware_onion.remove("result_gen")
131+
132+
133+
@pytest.mark.asyncio
134+
async def test_no_async_attrdict_middleware_does_not_convert_dicts_to_attrdict():
135+
async_w3 = Web3(AsyncEthereumTesterProvider())
136+
137+
async_w3.middleware_onion.inject(
138+
await async_construct_result_generator_middleware(
139+
{RPCEndpoint("fake_endpoint"): lambda *_: GENERATED_NESTED_DICT_RESULT}
140+
),
141+
"result_gen",
142+
layer=0,
143+
)
144+
145+
# remove attrdict middleware
146+
async_w3.middleware_onion.remove("attrdict")
147+
148+
response = await async_w3.manager.coro_request("fake_endpoint", [])
149+
150+
result = response["result"]
151+
152+
_assert_dict_and_not_attrdict(result)
153+
_assert_dict_and_not_attrdict(result["b"])
154+
_assert_dict_and_not_attrdict(result["b"]["b2"])
155+
_assert_dict_and_not_attrdict(result["b"]["b2"]["b2b"])
156+
_assert_dict_and_not_attrdict(result["b"]["b2"]["b2b"]["b2b2"])

0 commit comments

Comments
 (0)