Skip to content

Commit

Permalink
Give mungers module access
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjw committed Jan 13, 2019
1 parent 874fbc6 commit 8612dbf
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 67 deletions.
76 changes: 65 additions & 11 deletions tests/core/method-class/test_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
pipe,
)

from web3 import (
EthereumTesterProvider,
Web3,
)
from web3.method import (
Method,
)
from web3.module import (
ModuleV2,
)


def test_method_accepts_callable_for_selector():
Expand Down Expand Up @@ -70,21 +77,21 @@ def formatter_lookup_fn(method):

def test_input_munger_parameter_passthrough_matching_arity():
method = Method(
mungers=[lambda z, y: 'success'],
mungers=[lambda m, z, y: (m, ['success'])],
json_rpc_method='eth_method',
formatter_lookup_fn=''
)
method.input_munger((['first', 'second'], {})) == 'success'
method.input_munger((object(), ['first', 'second'], {})) == 'success'


def test_input_munger_parameter_passthrough_mismatch_arity():
method = Method(
mungers=[lambda z, y: 'success'],
mungers=[lambda m, z, y: 'success'],
json_rpc_method='eth_method',
formatter_lookup_fn=''
)
with pytest.raises(TypeError):
method.input_munger((['first', 'second', 'third'], {}))
method.input_munger((object(), ['first', 'second', 'third'], {}))


def test_input_munger_falsy_config_result_in_default_munger():
Expand All @@ -93,7 +100,7 @@ def test_input_munger_falsy_config_result_in_default_munger():
json_rpc_method='eth_method',
formatter_lookup_fn=''
)
method.input_munger(([], {})) == []
method.input_munger((object(), [], {})) == []


def test_default_input_munger_with_input_parameters_exception():
Expand All @@ -103,7 +110,7 @@ def test_default_input_munger_with_input_parameters_exception():
formatter_lookup_fn=''
)
with pytest.raises(TypeError):
method.input_munger(([1], {}))
method.input_munger((object(), [1], {}))


def get_test_formatters(method):
Expand Down Expand Up @@ -159,7 +166,9 @@ def formatter(params):
(
{
'mungers': [
lambda x, y, z: [x, y], lambda x, y: [x], lambda x: [str(x)]],
lambda m, x, y, z: (m, [x, y]),
lambda m, x, y: (m, [x]),
lambda m, x: (m, [str(x)])],
'json_rpc_method': 'eth_method',
'formatter_lookup_fn': ''
},
Expand All @@ -170,7 +179,9 @@ def formatter(params):
(
{
'mungers': [
lambda x, y, z: [x, y], lambda x, y: [x], lambda x: [str(x)]],
lambda m, x, y, z: (m, [x, y]),
lambda m, x, y: (m, [x]),
lambda m, x: (m, [str(x)])],
'json_rpc_method': 'eth_method',
'formatter_lookup_fn': ''
},
Expand Down Expand Up @@ -201,7 +212,9 @@ def formatter(params):
(
{
'mungers': [
lambda x, y, z: [x, y], lambda x, y: [x], lambda x: [str(x)]],
lambda m, x, y, z: (m, [x, y]),
lambda m, x, y: (m, [x]),
lambda m, x: (m, [str(x)])],
'json_rpc_method': 'eth_method',
'formatter_lookup_fn': get_test_formatters
},
Expand All @@ -220,8 +233,49 @@ def test_process_params(
if isclass(expected_result) and issubclass(expected_result, Exception):
with pytest.raises(expected_result):
method = Method(**method_config)
req_params, output_formatter = method.process_params(*args, **kwargs)
req_params, output_formatter = method.process_params(object(), *args, **kwargs)
else:
method = Method(**method_config)
req_params, output_formatter = method.process_params(*args, **kwargs)
req_params, output_formatter = method.process_params(object(), *args, **kwargs)
assert req_params == expected_result


def keywords(module, keyword_one, keyword_two):
return module, [keyword_one, keyword_two]


class Success(Exception):
pass


def return_exception_raising_formatter(method):
def formatter(params):
raise Success()
return ([formatter], [])


class FakeModule(ModuleV2):
method = Method(
'eth_method',
mungers=[keywords],
formatter_lookup_fn=return_exception_raising_formatter)


@pytest.fixture
def dummy_w3():
return Web3(
EthereumTesterProvider(),
modules={'fake': FakeModule},
middlewares=[])


def test_munger_class_method_access_raises_friendly_error():
with pytest.raises(Exception):
FakeModule.method(1, 2)


def test_munger_arguments_by_keyword(dummy_w3):
with pytest.raises(Success):
dummy_w3.fake.method(keyword_one=1, keyword_two=2)
with pytest.raises(Success):
dummy_w3.fake.method(1, keyword_two=2)
60 changes: 23 additions & 37 deletions web3/method.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@
)


def star_apply(fn):
def _munger_star_apply(fn):
@functools.wraps(fn)
def inner(args):
return fn(*args)
def inner(module_args):
module, args = module_args
return fn(module, *args)
return inner


def get_default_formatters(*args, **kwargs):
return ([identity], [identity],)


def default_munger(*args, **kwargs):
def default_munger(module, *args, **kwargs):
if not args and not kwargs:
return list()
return module, list()
else:
raise TypeError("Parameters passed to method without parameter mungers defined.")

Expand All @@ -39,16 +40,20 @@ class Method:
A note about mungers: The first (root) munger should reflect the desired
api function arguments. In other words, if the api function wants to
behave as: `getBalance(account, block_identifier=None)`, the root munger
should accept these same arguments, e.g.:
should accept these same arguments, with the addition of the module as
the first argument e.g.:
```
def getBalance_root_munger(account, block_identifier=None):
def getBalance_root_munger(module, account, block_identifier=None):
if block_identifier is None:
block_identifier = DEFAULT_BLOCK
return [account, block_identifier]
return module, [account, block_identifier]
```
all mungers should return a list.
all mungers should return a 2-tuple of the module and the argument list.
if no munger is provided, a default munger expecting no method arguments
will be used.
2. method selection - after input munging if a callable is provided for the
json_rpc_method config, the inputs are passed to the callable and a method
Expand All @@ -67,14 +72,12 @@ def getBalance_root_munger(account, block_identifier=None):
and the reponse formatters are applied to the output.
"""
def __init__(
self, *,
name=None,
self,
json_rpc_method=None,
mungers=None,
formatter_lookup_fn=None,
web3=None):

self.name = name
self.json_rpc_method = json_rpc_method
self.mungers = mungers or [default_munger]
self.formatter_lookup_fn = formatter_lookup_fn or get_default_formatters
Expand All @@ -101,25 +104,25 @@ def get_formatters(self, method_string):
formatters = self.formatter_lookup_fn(method_string)
return formatters or get_default_formatters()

def input_munger(self, args_kwargs):
def input_munger(self, val):
try:
args, kwargs = args_kwargs
module, args, kwargs = val
except TypeError:
raise ValueError("input_munger expects a 2-tuple")
raise ValueError("input_munger expects a 3-tuple")

# TODO: Create friendly error output.
mungers_iter = iter(self.mungers)
root_munger = next(mungers_iter)
munged_inputs = pipe(
root_munger(*args, **kwargs),
*map(star_apply, mungers_iter))
_, munged_inputs = pipe(
root_munger(module, *args, **kwargs),
*map(_munger_star_apply, mungers_iter))

return munged_inputs

def process_params(self, *args, **kwargs):
def process_params(self, module, *args, **kwargs):
# takes in input params, steps 1-3
params, method, (req_formatters, ret_formatters) = _pipe_and_accumulate(
(args, kwargs,),
(module, args, kwargs,),
[self.input_munger, self.method_selector_fn, self.get_formatters])

return (method, pipe(params, *req_formatters)), ret_formatters
Expand All @@ -139,20 +142,3 @@ def _pipe_and_accumulate(val, fns):
for fn in fns:
val = fn(val)
yield val


def retrieve_blocking_method_call_fn(module, method):
def caller(*args, **kwargs):
(method_str, params), output_formatters = method.process_params(*args, **kwargs)
return pipe(
module.web3.manager.request_blocking(method_str, params),
*output_formatters)
return caller


def retrieve_async_method_call_fn(module, method):
async def caller(*args, **kwargs):
(method_str, params), output_formatters = method.process_params(*args, **kwargs)
raw_result = await module.web3.manager.coro_request(method_str, params)
return pipe(raw_result, *output_formatters)
return caller
41 changes: 41 additions & 0 deletions web3/module.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
from eth_utils.toolz import (
curry,
pipe,
)


@curry
def retrieve_blocking_method_call_fn(w3, module, method):
def caller(*args, **kwargs):
(method_str, params), output_formatters = method.process_params(module, *args, **kwargs)
return pipe(
w3.manager.request_blocking(method_str, params),
*output_formatters)
return caller


@curry
def retrieve_async_method_call_fn(w3, module, method):
async def caller(*args, **kwargs):
(method_str, params), output_formatters = method.process_params(module, *args, **kwargs)
raw_result = await w3.manager.coro_request(method_str, params)
return pipe(raw_result, *output_formatters)
return caller


# TODO: Replace this with ModuleV2 when ready.
class Module:
web3 = None

def __init__(self, web3):
self.web3 = web3

Expand All @@ -23,3 +50,17 @@ def attach(cls, target, module_name=None):
web3 = target

setattr(target, module_name, cls(web3))


# Module should no longer have access to the full web3 api.
# Only the calling functions need access to the request methods.
# Any "re-entrant" shinanigans can go in the middlewares, which do
# have web3 access.
class ModuleV2(Module):
is_async = False

def __init__(self, web3):
if self.is_async:
self.retrieve_caller_fn = retrieve_async_method_call_fn(web3, self)
else:
self.retrieve_caller_fn = retrieve_blocking_method_call_fn(web3, self)
25 changes: 6 additions & 19 deletions web3/version.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
from web3.method import (
Method,
retrieve_async_method_call_fn,
retrieve_blocking_method_call_fn,
)
from web3.module import (
Module,
ModuleV2,
)


class BaseVersion(Module):
class BaseVersion(ModuleV2):
retrieve_caller_fn = None

_get_node_version = Method(
name='node',
json_rpc_method='web3_clientVersion'
)

_get_net_version = Method(
name='network',
json_rpc_method='net_version'
)
_get_protocol_version = Method(
name='ethereum',
json_rpc_method='eth_protocolVersion'
)
_get_node_version = Method('web3_clientVersion')
_get_net_version = Method('net_version')
_get_protocol_version = Method('eth_protocolVersion')

@property
def api(self):
Expand All @@ -32,7 +21,7 @@ def api(self):


class AsyncVersion(BaseVersion):
retrieve_caller_fn = retrieve_async_method_call_fn
is_async = True

@property
async def node(self):
Expand All @@ -48,8 +37,6 @@ async def ethereum(self):


class BlockingVersion(BaseVersion):
retrieve_caller_fn = retrieve_blocking_method_call_fn

@property
def node(self):
return self._get_node_version()
Expand Down

0 comments on commit 8612dbf

Please sign in to comment.