From 8612dbf9fe3b1f98cb451701431767a219d31fce Mon Sep 17 00:00:00 2001 From: Dylan Wilson Date: Sun, 13 Jan 2019 14:20:34 +0800 Subject: [PATCH] Give mungers module access --- tests/core/method-class/test_method.py | 76 ++++++++++++++++++++++---- web3/method.py | 60 ++++++++------------ web3/module.py | 41 ++++++++++++++ web3/version.py | 25 ++------- 4 files changed, 135 insertions(+), 67 deletions(-) diff --git a/tests/core/method-class/test_method.py b/tests/core/method-class/test_method.py index 8a8c15ed6a..1e8b39f5c3 100644 --- a/tests/core/method-class/test_method.py +++ b/tests/core/method-class/test_method.py @@ -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(): @@ -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(): @@ -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(): @@ -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): @@ -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': '' }, @@ -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': '' }, @@ -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 }, @@ -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) diff --git a/web3/method.py b/web3/method.py index 66b1064e18..c341fd4ae2 100644 --- a/web3/method.py +++ b/web3/method.py @@ -9,10 +9,11 @@ ) -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 @@ -20,9 +21,9 @@ 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.") @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/web3/module.py b/web3/module.py index 010bd8ca4f..45d3b3bd98 100644 --- a/web3/module.py +++ b/web3/module.py @@ -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 @@ -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) diff --git a/web3/version.py b/web3/version.py index 21bd9e011d..1f50ac3d32 100644 --- a/web3/version.py +++ b/web3/version.py @@ -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): @@ -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): @@ -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()