Skip to content

Commit 97adc5f

Browse files
committed
Add basic configurable Method class with attr lookup
- Demonstrates limited feature set using the Version module
1 parent c028368 commit 97adc5f

File tree

6 files changed

+290
-0
lines changed

6 files changed

+290
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
3+
from web3.module import Module
4+
5+
6+
class DummyWeb3:
7+
def __init__(self):
8+
Module.attach(self, 'module')
9+
10+
11+
@pytest.fixture(scope='module')
12+
def dw3():
13+
w3 = DummyWeb3()
14+
return w3
15+
16+
17+
def test_module_lookup_method(dw3):
18+
with pytest.raises(AttributeError):
19+
assert dw3.module.blahblahblah
20+
dw3.module.lookup_method_fn = lambda *_: True
21+
assert dw3.module.blahblahblah is True
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
3+
from web3 import Web3, EthereumTesterProvider
4+
from web3.providers.eth_tester.main import AsyncEthereumTesterProvider
5+
from web3.version import BlockingVersion, AsyncVersion, Version
6+
7+
8+
@pytest.fixture
9+
def blocking_w3():
10+
return Web3(
11+
EthereumTesterProvider(),
12+
modules={
13+
'blocking_version': BlockingVersion,
14+
'legacy_version': Version
15+
})
16+
17+
18+
@pytest.fixture
19+
def async_w3():
20+
return Web3(
21+
AsyncEthereumTesterProvider(),
22+
middlewares=[],
23+
modules={
24+
'async_version': AsyncVersion,
25+
})
26+
27+
28+
def test_blocking_version(blocking_w3):
29+
assert blocking_w3.blocking_version.api == blocking_w3.legacy_version.api
30+
assert blocking_w3.blocking_version.node == blocking_w3.legacy_version.node
31+
assert blocking_w3.blocking_version.network == blocking_w3.legacy_version.network
32+
assert blocking_w3.blocking_version.ethereum == blocking_w3.legacy_version.ethereum
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_async_blocking_version(async_w3, blocking_w3):
37+
# This seems a little awkward. How do we know if something is an awaitable
38+
# or a static method?
39+
assert async_w3.async_version.api == blocking_w3.legacy_version.api
40+
41+
assert await async_w3.async_version.node == blocking_w3.legacy_version.node
42+
with pytest.raises(
43+
ValueError,
44+
message="RPC Endpoint has not been implemented: net_version"
45+
):
46+
# net_version is provided through a middleware
47+
assert await async_w3.async_version.network == blocking_w3.legacy_version.network
48+
with pytest.raises(
49+
ValueError,
50+
message="RPC Endpoint has not been implemented: eth_protocolVersion"
51+
):
52+
assert await async_w3.async_version.ethereum == blocking_w3.legacy_version.ethereum

web3/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,7 @@ class TimeExhausted(Exception):
114114
Raised when a method has not retrieved the desired result within a specified timeout.
115115
"""
116116
pass
117+
118+
119+
class UndefinedMethodError(Exception):
120+
pass

web3/method.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
from eth_utils import (
2+
to_tuple,
3+
)
4+
from eth_utils.toolz import (
5+
identity,
6+
pipe,
7+
)
8+
9+
from web3.exceptions import (
10+
UndefinedMethodError,
11+
)
12+
13+
TEST_METHOD_CONFIG = {
14+
'name': "signInBlood",
15+
'mungers': [],
16+
'json_rpc_method': "eth_signTransactionInBlood",
17+
}
18+
19+
20+
def lookup_method(module, module_config, method_class, attr_name):
21+
try:
22+
method = module_config[attr_name]
23+
except KeyError:
24+
raise UndefinedMethodError("No method named {0}".format(attr_name))
25+
return method_class(module.web3, method)
26+
27+
28+
class DummyRequestManager:
29+
def request_blocking(method, params):
30+
return (method, params)
31+
32+
33+
class DummyWeb3:
34+
manager = DummyRequestManager
35+
36+
37+
def test_sync_method_config_loading():
38+
signInBlood = BlockingMethod(DummyWeb3(), TEST_METHOD_CONFIG)
39+
signInBlood.input_munger = lambda *_: {}
40+
assert signInBlood.method_selector
41+
assert ('eth_signTransactionInBlood', {}) == signInBlood()
42+
43+
44+
class BaseMethod:
45+
"""BaseMethod for web3 module methods
46+
47+
Calls to the Method go through these steps:
48+
49+
1. input munging ;) - includes normalization, parameter checking, formatters.
50+
Any processing on the input parameters that need to happen before json_rpc
51+
method string selection occurs.
52+
53+
2. method selection - function that selects the correct rpc_method. accepts a
54+
function or an string.
55+
56+
3. constructing formatter middlewares - takes the rpc_method and looks up the
57+
corresponding input/output formatters. these are the middlewares migrated here.
58+
59+
4. making the request through the middleware (pipeline)? wrapped request
60+
function.
61+
"""
62+
def __init__(self, web3, method_config):
63+
self.__name__ = method_config.get('name', 'anonymous')
64+
self.__doc__ = method_config.get('doc', '')
65+
self.is_property = method_config.get('is_property', False)
66+
self.web3 = web3
67+
self.input_munger = self._construct_input_pipe(
68+
method_config.get('mungers')) or identity
69+
self.method_selector = self._method_selector(
70+
method_config.get('json_rpc_method'))
71+
# TODO: Write formatter lookup.
72+
self.lookup_formatter = None
73+
74+
def _construct_input_pipe(self, formatters):
75+
formatters = formatters or [identity]
76+
77+
def _method_selector(self, selector):
78+
"""Method selector can just be the method string.
79+
"""
80+
if isinstance(selector, (str,)):
81+
return lambda _: selector
82+
else:
83+
return selector
84+
85+
def get_formatters(self, method_string):
86+
"""Lookup the request formatters for the rpc_method"""
87+
if not self.lookup_formatter:
88+
return ([identity], [identity],)
89+
else:
90+
raise NotImplementedError()
91+
92+
def prep_for_call(self, *args, **kwargs):
93+
# takes in input params, steps 1-3
94+
params, method, (req_formatters, ret_formatters) = pipe_appends(
95+
[self.input_munger, self.method_selector, self.get_formatters],
96+
(args, kwargs,))
97+
return pipe((method, params,), *req_formatters), ret_formatters
98+
99+
def __call__(self):
100+
raise NotImplementedError()
101+
102+
103+
@to_tuple
104+
def pipe_appends(fns, val):
105+
"""pipes val through a list of fns while appending the result to the
106+
tuple output
107+
108+
e.g.:
109+
110+
>>> pipe_appends([lambda x: x**2, lambda x: x*10], 5)
111+
(25, 250)
112+
113+
"""
114+
for fn in fns:
115+
val = fn(val)
116+
yield val
117+
118+
119+
class BlockingMethod(BaseMethod):
120+
def __call__(self, *args, **kwargs):
121+
(method, params), output_formatters = self.prep_for_call(*args, **kwargs)
122+
return pipe(
123+
self.web3.manager.request_blocking(method, params),
124+
*output_formatters)
125+
126+
def __get__(self, obj, objType):
127+
# allow methods to be configured for property access
128+
if self.is_property is True:
129+
return self.__call__()
130+
else:
131+
return self
132+
133+
134+
class AsyncMethod(BaseMethod):
135+
async def __call__(self, *args, **kwargs):
136+
(method, params), output_formatters = self.prep_for_call(*args, **kwargs)
137+
raw_result = await self.web3.manager.request_async(method, params)
138+
return pipe(raw_result, *output_formatters)
139+
140+
async def __get__(self, obj, objType):
141+
if self.is_property is True:
142+
return await self.__call__()
143+
else:
144+
return self

web3/module.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,38 @@
1+
from web3.exceptions import (
2+
UndefinedMethodError,
3+
)
4+
from web3.method import (
5+
lookup_method,
6+
)
7+
8+
19
class Module:
210
web3 = None
11+
method_class = None
12+
module_config = None
13+
lookup_method_fn = lookup_method
314

415
def __init__(self, web3):
516
self.web3 = web3
17+
if self.module_config is None:
18+
self.module_config = dict()
19+
20+
def __getattr__(self, attr):
21+
# Method lookup magic
22+
try:
23+
method = self.lookup_method_fn(
24+
self.module_config,
25+
self.method_class,
26+
attr)
27+
except UndefinedMethodError:
28+
raise AttributeError
29+
30+
# Emulate descriptor behavior to allow methods to control if
31+
# they are properties or not.
32+
if hasattr(method, '__get__'):
33+
return method.__get__(None, self)
34+
35+
return method
636

737
@classmethod
838
def attach(cls, target, module_name=None):

web3/version.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,46 @@
1+
from web3.method import (
2+
AsyncMethod,
3+
BlockingMethod,
4+
)
15
from web3.module import (
26
Module,
37
)
48

9+
VERSION_MODULE_CONFIG = {
10+
'node': {
11+
'name': 'node',
12+
'json_rpc_method': 'web3_clientVersion',
13+
'is_property': True
14+
},
15+
'network': {
16+
'name': 'network',
17+
'json_rpc_method': 'net_version',
18+
'is_property': True
19+
},
20+
'ethereum': {
21+
'name': 'ethereum',
22+
'json_rpc_method': 'eth_protocolVersion',
23+
'is_property': True
24+
},
25+
}
26+
27+
28+
class BaseVersion(Module):
29+
module_config = VERSION_MODULE_CONFIG
30+
31+
@property
32+
def api(self):
33+
from web3 import __version__
34+
return __version__
35+
36+
37+
class AsyncVersion(BaseVersion):
38+
method_class = AsyncMethod
39+
40+
41+
class BlockingVersion(BaseVersion):
42+
method_class = BlockingMethod
43+
544

645
class Version(Module):
746
@property

0 commit comments

Comments
 (0)