From d2738184b5296a333043621774a6259db38aadfe Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Thu, 24 Feb 2022 15:03:20 -0700 Subject: [PATCH 1/4] Minor cleanups in method class. --- web3/method.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/web3/method.py b/web3/method.py index 4f08807e40..dfc88800e9 100644 --- a/web3/method.py +++ b/web3/method.py @@ -61,14 +61,14 @@ def inner(args: Any) -> TReturn: return inner -def default_munger(module: "Module", *args: Any, **kwargs: Any) -> Tuple[()]: +def default_munger(_module: "Module", *args: Any, **kwargs: Any) -> Tuple[()]: if not args and not kwargs: return () else: raise TypeError("Parameters passed to method without parameter mungers defined.") -def default_root_munger(module: "Module", *args: Any) -> List[Any]: +def default_root_munger(_module: "Module", *args: Any) -> List[Any]: return [*args] @@ -115,15 +115,14 @@ def get_balance_root_munger(module, account, block_identifier=None): and the response formatters are applied to the output. """ def __init__( - self, - json_rpc_method: Optional[RPCEndpoint] = None, - mungers: Optional[Sequence[Munger]] = None, - request_formatters: Optional[Callable[..., TReturn]] = None, - result_formatters: Optional[Callable[..., TReturn]] = None, - error_formatters: Optional[Callable[..., TReturn]] = None, - null_result_formatters: Optional[Callable[..., TReturn]] = None, - method_choice_depends_on_args: Optional[Callable[..., RPCEndpoint]] = None, - w3: Optional["Web3"] = None): + self, + json_rpc_method: Optional[RPCEndpoint] = None, + mungers: Optional[Sequence[Munger]] = None, + request_formatters: Optional[Callable[..., TReturn]] = None, + result_formatters: Optional[Callable[..., TReturn]] = None, + null_result_formatters: Optional[Callable[..., TReturn]] = None, + method_choice_depends_on_args: Optional[Callable[..., RPCEndpoint]] = None, + ): self.json_rpc_method = json_rpc_method self.mungers = mungers or [default_munger] @@ -133,8 +132,9 @@ def __init__( self.null_result_formatters = null_result_formatters or get_null_result_formatters self.method_choice_depends_on_args = method_choice_depends_on_args - def __get__(self, obj: Optional["Module"] = None, - obj_type: Optional[Type["Module"]] = None) -> TFunc: + def __get__( + self, obj: Optional["Module"] = None, obj_type: Optional[Type["Module"]] = None + ) -> TFunc: if obj is None: raise TypeError( "Direct calls to methods are not supported. " @@ -204,7 +204,7 @@ def process_params( return request, response_formatters -class DeprecatedMethod(): +class DeprecatedMethod: def __init__(self, method: Method[Callable[..., Any]], old_name: str, new_name: str) -> None: self.method = method self.old_name = old_name From dd5bd98c3b10a4c1987996f6b14150cbde198af7 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Thu, 24 Feb 2022 15:05:42 -0700 Subject: [PATCH 2/4] Remove old reference to TestRPCProvider --- web3/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/web3/__init__.py b/web3/__init__.py index 5c8edfacef..8d86766529 100644 --- a/web3/__init__.py +++ b/web3/__init__.py @@ -45,7 +45,6 @@ "HTTPProvider", "IPCProvider", "WebsocketProvider", - "TestRPCProvider", "EthereumTesterProvider", "Account", "AsyncHTTPProvider", From e6f306cbc0f3d3803fcae504603f8bfea69986f2 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Thu, 24 Feb 2022 19:04:29 -0700 Subject: [PATCH 3/4] Add option to attach methods to classes extending from `Module` class * Refactor logic for attaching a `Method` class as a property rather than a method. Instead of implicitly setting `mungers=None`, explicitly set the `is_property` flag on `Method` to `True`. This also facilitates attaching new methods and properties to modules. * Fix up some tests in test_method.py that were falsely passing to actually test correctly. Add tests for new `is_property` flag for the `Method` class. * Create `test_module.py` and add tests for `attach_methods()` --- newsfragments/2383.breaking-change.rst | 1 + tests/core/method-class/test_method.py | 44 ++++++++++----- tests/core/module-class/test_module.py | 75 ++++++++++++++++++++++++++ web3/_utils/admin.py | 10 ++-- web3/_utils/miner.py | 6 +-- web3/_utils/personal.py | 4 +- web3/_utils/txpool.py | 6 +-- web3/eth.py | 22 ++++---- web3/method.py | 35 +++++++----- web3/module.py | 12 +++++ web3/parity.py | 6 +-- 11 files changed, 170 insertions(+), 51 deletions(-) create mode 100644 newsfragments/2383.breaking-change.rst create mode 100644 tests/core/module-class/test_module.py diff --git a/newsfragments/2383.breaking-change.rst b/newsfragments/2383.breaking-change.rst new file mode 100644 index 0000000000..a53354bbc7 --- /dev/null +++ b/newsfragments/2383.breaking-change.rst @@ -0,0 +1 @@ +Add ``attach_methods()`` to ``Module`` class to facilitate attaching methods to modules. \ No newline at end of file diff --git a/tests/core/method-class/test_method.py b/tests/core/method-class/test_method.py index 28b7f953aa..44e132f4b6 100644 --- a/tests/core/method-class/test_method.py +++ b/tests/core/method-class/test_method.py @@ -32,7 +32,7 @@ def test_method_accepts_callable_for_selector(): def test_method_selector_fn_accepts_str(): method = Method( - mungers=None, + is_property=True, json_rpc_method='eth_method', ) assert method.method_selector_fn() == 'eth_method' @@ -77,7 +77,7 @@ def test_input_munger_parameter_passthrough_matching_arity(): mungers=[lambda m, z, y: ['success']], json_rpc_method='eth_method', ) - method.input_munger(object(), ['first', 'second'], {}) == 'success' + assert method.input_munger(object(), ['first', 'second'], {}) == ['success'] def test_input_munger_parameter_passthrough_mismatch_arity(): @@ -94,16 +94,15 @@ def test_input_munger_falsy_config_result_in_default_munger(): mungers=[], json_rpc_method='eth_method', ) - method.input_munger(object(), [], {}) == [] + assert method.input_munger(object(), [], {}) == [] -def test_default_input_munger_with_input_parameters_exception(): +def test_default_input_munger_with_input_parameters(): method = Method( mungers=[], json_rpc_method='eth_method', ) - with pytest.raises(TypeError): - method.input_munger(object(), [1], {}) + assert method.input_munger(object(), [1], {}) == [1] @pytest.mark.parametrize( @@ -125,7 +124,7 @@ def test_default_input_munger_with_input_parameters_exception(): }, ['unexpected_argument'], {}, - TypeError, + IndexError, 2 ), ( @@ -184,6 +183,15 @@ def test_default_input_munger_with_input_parameters_exception(): ('eth_getBalance', ('0x0000000000000000000000000000000000000000', '0x3')), 2, ), + ( + { + 'json_rpc_method': 'eth_getBalance', + }, + ('0x0000000000000000000000000000000000000000', 3), + {}, + ('eth_getBalance', ('0x0000000000000000000000000000000000000000', '0x3')), + 2, + ), ( { 'mungers': [ @@ -206,7 +214,17 @@ def test_default_input_munger_with_input_parameters_exception(): {}, ('eth_chainId', ()), 2, - ) + ), + ( + { + 'is_property': True, + 'json_rpc_method': 'eth_chainId', + }, + [], + {}, + ('eth_chainId', ()), + 2, + ), ), ids=[ 'raises-error-no-rpc-method', @@ -215,9 +233,11 @@ def test_default_input_munger_with_input_parameters_exception(): 'test-rpc-method-as-callable', 'test-arg-munger', 'test-munger-wrong-length-arg', - 'test-request-formatters', + 'test-request-formatters-default-root-munger-explicit', + 'test-request-formatters-default-root-munger-implicit', 'test-mungers-and-request-formatters', 'test-response-formatters', + 'test-set-as-property-default-munger-implicit', ] ) def test_process_params( @@ -230,7 +250,7 @@ def test_process_params( if isclass(expected_request_result) and issubclass(expected_request_result, Exception): with pytest.raises(expected_request_result): method = Method(**method_config) - request_params, output_formatter = method.process_params(object(), *args, **kwargs) + method.process_params(object(), *args, **kwargs) else: method = Method(**method_config) request_params, output_formatter = method.process_params(object(), *args, **kwargs) @@ -248,8 +268,8 @@ class Success(Exception): pass -def return_exception_raising_formatter(method): - def formatter(params): +def return_exception_raising_formatter(_method): + def formatter(_params): raise Success() return compose(formatter) diff --git a/tests/core/module-class/test_module.py b/tests/core/module-class/test_module.py new file mode 100644 index 0000000000..3654a7df8d --- /dev/null +++ b/tests/core/module-class/test_module.py @@ -0,0 +1,75 @@ +import pytest + +from web3 import ( + EthereumTesterProvider, + Web3, +) +from web3.method import ( + Method, +) + + +@pytest.fixture +def web3_with_external_modules(module1, module2, module3): + return Web3( + EthereumTesterProvider(), + external_modules={ + 'module1': module1, + 'module2': (module2, { + 'submodule1': module3, + }), + } + ) + + +def test_attach_methods_to_module(web3_with_external_modules): + w3 = web3_with_external_modules + + w3.module1.attach_methods({ + # set `property1` on `module1` with `eth_chainId` RPC endpoint + 'property1': Method('eth_chainId', is_property=True), + # set `method1` on `module1` with `eth_getBalance` RPC endpoint + 'method1': Method('eth_getBalance'), + }) + + assert w3.eth.chain_id == 61 + assert w3.module1.property1 == 61 + + coinbase = w3.eth.coinbase + assert w3.eth.get_balance(coinbase, 'latest') == 1000000000000000000000000 + assert w3.module1.method1(coinbase, 'latest') == 1000000000000000000000000 + + w3.module2.submodule1.attach_methods({ + # set `method2` on `module2.submodule1` with `eth_blockNumber` RPC endpoint + 'method2': Method('eth_blockNumber', is_property=True) + }) + + assert w3.eth.block_number == 0 + assert w3.module2.submodule1.method2 == 0 + + w3.eth.attach_methods({'get_block2': Method('eth_getBlockByNumber')}) + + assert w3.eth.get_block('latest')['number'] == 0 + assert w3.eth.get_block('pending')['number'] == 1 + + assert w3.eth.get_block2('latest')['number'] == 0 + assert w3.eth.get_block2('pending')['number'] == 1 + + +def test_attach_methods_with_mungers(web3_with_external_modules): + w3 = web3_with_external_modules + + w3.module1.attach_methods({ + 'method1': Method('eth_getBlockByNumber', mungers=[ + lambda _method, block_id, f, _z: (block_id, f), + lambda _m, block_id, _f: (block_id - 1,), + ]), + }) + + assert w3.eth.get_block(0)['baseFeePerGas'] == 1000000000 + assert w3.eth.get_block(1)['baseFeePerGas'] == 875000000 + + # `method1` should take a higher block number than `eth_getBlockByNumber` due to mungers and no + # other params should matter + assert w3.module1.method1(1, False, '_is_never_used_')['baseFeePerGas'] == 1000000000 + assert w3.module1.method1(2, '_will_be_overridden_', None)['baseFeePerGas'] == 875000000 diff --git a/web3/_utils/admin.py b/web3/_utils/admin.py index 2505451d96..86e946d550 100644 --- a/web3/_utils/admin.py +++ b/web3/_utils/admin.py @@ -40,19 +40,19 @@ def admin_start_params_munger( datadir: Method[Callable[[], str]] = Method( RPC.admin_datadir, - mungers=None, + is_property=True, ) node_info: Method[Callable[[], NodeInfo]] = Method( RPC.admin_nodeInfo, - mungers=None, + is_property=True, ) peers: Method[Callable[[], List[Peer]]] = Method( RPC.admin_peers, - mungers=None, + is_property=True, ) @@ -77,13 +77,13 @@ def __call__( stop_rpc: Method[Callable[[], bool]] = Method( RPC.admin_stopRPC, - mungers=None, + is_property=True, ) stop_ws: Method[Callable[[], bool]] = Method( RPC.admin_stopWS, - mungers=None, + is_property=True, ) # diff --git a/web3/_utils/miner.py b/web3/_utils/miner.py index 873f296d95..f39c99953e 100644 --- a/web3/_utils/miner.py +++ b/web3/_utils/miner.py @@ -51,19 +51,19 @@ stop: Method[Callable[[], bool]] = Method( RPC.miner_stop, - mungers=None, + is_property=True, ) start_auto_dag: Method[Callable[[], bool]] = Method( RPC.miner_startAutoDag, - mungers=None, + is_property=True, ) stop_auto_dag: Method[Callable[[], bool]] = Method( RPC.miner_stopAutoDag, - mungers=None, + is_property=True, ) diff --git a/web3/_utils/personal.py b/web3/_utils/personal.py index 2bb2eda77d..d9690cc44a 100644 --- a/web3/_utils/personal.py +++ b/web3/_utils/personal.py @@ -44,13 +44,13 @@ list_accounts: Method[Callable[[], List[ChecksumAddress]]] = Method( RPC.personal_listAccounts, - mungers=None, + is_property=True, ) list_wallets: Method[Callable[[], List[GethWallet]]] = Method( RPC.personal_listWallets, - mungers=None, + is_property=True, ) diff --git a/web3/_utils/txpool.py b/web3/_utils/txpool.py index 052f8b35ca..7af244b52b 100644 --- a/web3/_utils/txpool.py +++ b/web3/_utils/txpool.py @@ -16,17 +16,17 @@ content: Method[Callable[[], TxPoolContent]] = Method( RPC.txpool_content, - mungers=None, + is_property=True, ) inspect: Method[Callable[[], TxPoolInspect]] = Method( RPC.txpool_inspect, - mungers=None, + is_property=True, ) status: Method[Callable[[], TxPoolStatus]] = Method( RPC.txpool_status, - mungers=None, + is_property=True, ) diff --git a/web3/eth.py b/web3/eth.py index ef1c134911..5c84b14a6c 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -119,7 +119,7 @@ class BaseEth(Module): _gas_price: Method[Callable[[], Wei]] = Method( RPC.eth_gasPrice, - mungers=None, + is_property=True, ) @property @@ -244,7 +244,7 @@ def estimate_gas_munger( _max_priority_fee: Method[Callable[..., Wei]] = Method( RPC.eth_maxPriorityFeePerGas, - mungers=None, + is_property=True, ) def get_block_munger( @@ -267,12 +267,12 @@ def get_block_munger( get_block_number: Method[Callable[[], BlockNumber]] = Method( RPC.eth_blockNumber, - mungers=None, + is_property=True, ) get_coinbase: Method[Callable[[], ChecksumAddress]] = Method( RPC.eth_coinbase, - mungers=None, + is_property=True, ) def block_id_munger( @@ -315,27 +315,27 @@ def call_munger( _get_accounts: Method[Callable[[], Tuple[ChecksumAddress]]] = Method( RPC.eth_accounts, - mungers=None, + is_property=True, ) _get_hashrate: Method[Callable[[], int]] = Method( RPC.eth_hashrate, - mungers=None, + is_property=True, ) _chain_id: Method[Callable[[], int]] = Method( RPC.eth_chainId, - mungers=None, + is_property=True, ) _is_mining: Method[Callable[[], bool]] = Method( RPC.eth_mining, - mungers=None, + is_property=True, ) _is_syncing: Method[Callable[[], Union[SyncStatus, bool]]] = Method( RPC.eth_syncing, - mungers=None, + is_property=True, ) _get_transaction_receipt: Method[Callable[[_Hash32], TxReceipt]] = Method( @@ -565,7 +565,7 @@ def icapNamereg(self) -> NoReturn: _protocol_version: Method[Callable[[], str]] = Method( RPC.eth_protocolVersion, - mungers=None, + is_property=True, ) @property @@ -983,7 +983,7 @@ def getCompilers(self) -> NoReturn: get_work: Method[Callable[[], List[HexBytes]]] = Method( RPC.eth_getWork, - mungers=None, + is_property=True, ) @deprecated_for("generate_gas_price") diff --git a/web3/method.py b/web3/method.py index dfc88800e9..ce94e75915 100644 --- a/web3/method.py +++ b/web3/method.py @@ -61,6 +61,14 @@ def inner(args: Any) -> TReturn: return inner +def _set_mungers(mungers: Optional[Sequence[Munger]], is_property: bool) -> Sequence[Any]: + return ( + mungers if mungers + else [default_munger] if is_property + else [default_root_munger] + ) + + def default_munger(_module: "Module", *args: Any, **kwargs: Any) -> Tuple[()]: if not args and not kwargs: return () @@ -81,7 +89,7 @@ class Method(Generic[TFunc]): Calls to the Method go through these steps: 1. input munging - includes normalization, parameter checking, early parameter - formatting. Any processing on the input parameters that need to happen before + formatting. Any processing on the input parameters that need to happen before json_rpc method string selection occurs. A note about mungers: The first (root) munger should reflect the desired @@ -122,15 +130,16 @@ def __init__( result_formatters: Optional[Callable[..., TReturn]] = None, null_result_formatters: Optional[Callable[..., TReturn]] = None, method_choice_depends_on_args: Optional[Callable[..., RPCEndpoint]] = None, + is_property: bool = False, ): - self.json_rpc_method = json_rpc_method - self.mungers = mungers or [default_munger] + self.mungers = _set_mungers(mungers, is_property) self.request_formatters = request_formatters or get_request_formatters self.result_formatters = result_formatters or get_result_formatters self.error_formatters = get_error_formatters self.null_result_formatters = null_result_formatters or get_null_result_formatters self.method_choice_depends_on_args = method_choice_depends_on_args + self.is_property = is_property def __get__( self, obj: Optional["Module"] = None, obj_type: Optional[Type["Module"]] = None @@ -167,8 +176,8 @@ def input_munger( root_munger = next(mungers_iter) munged_inputs = pipe( root_munger(module, *args, **kwargs), - *map(lambda m: _munger_star_apply(functools.partial(m, module)), mungers_iter)) - + *map(lambda m: _munger_star_apply(functools.partial(m, module)), mungers_iter) + ) return munged_inputs def process_params( @@ -194,13 +203,15 @@ def process_params( params = [] method = self.method_selector_fn() - response_formatters = (self.result_formatters(method, module), - self.error_formatters(method), - self.null_result_formatters(method),) - - request = (method, - _apply_request_formatters(params, self.request_formatters(method))) - + response_formatters = ( + self.result_formatters(method, module), + self.error_formatters(method), + self.null_result_formatters(method), + ) + request = ( + method, + _apply_request_formatters(params, self.request_formatters(method)) + ) return request, response_formatters diff --git a/web3/module.py b/web3/module.py index 49fdfb9209..de3343e5a8 100644 --- a/web3/module.py +++ b/web3/module.py @@ -3,6 +3,7 @@ Any, Callable, Coroutine, + Dict, TypeVar, Union, ) @@ -91,3 +92,14 @@ def __init__(self, w3: "Web3") -> None: self.retrieve_caller_fn = retrieve_blocking_method_call_fn(w3, self) self.w3 = w3 self.codec: ABICodec = w3.codec + + def attach_methods( + self, + methods: Dict[str, Method[Callable[..., Any]]], + ) -> None: + for method_name, method_class in methods.items(): + klass = ( + method_class.__get__(obj=self)() if method_class.is_property else + method_class.__get__(obj=self) + ) + setattr(self, method_name, klass) diff --git a/web3/parity.py b/web3/parity.py index 84bcaa5c44..4b47a40b21 100644 --- a/web3/parity.py +++ b/web3/parity.py @@ -94,7 +94,7 @@ class Parity(Module): enode: Method[Callable[[], str]] = Method( RPC.parity_enode, - mungers=None, + is_property=True, ) """ property default_block """ @@ -141,7 +141,7 @@ def list_storage_keys_munger( net_peers: Method[Callable[[], ParityNetPeers]] = Method( RPC.parity_netPeers, - mungers=None + is_property=True ) add_reserved_peer: Method[Callable[[EnodeURI], bool]] = Method( @@ -217,7 +217,7 @@ def trace_transactions_munger( mode: Method[Callable[[], ParityMode]] = Method( RPC.parity_mode, - mungers=None + is_property=True ) # Deprecated Methods From cb6b4a0b9f79bb12696ce95dfa40270e20a64761 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Thu, 10 Mar 2022 09:18:39 -0300 Subject: [PATCH 4/4] More tests for method class and cleanup from PR #2383 comments --- tests/core/method-class/test_method.py | 99 ++++++++++++++++++++------ tests/core/module-class/test_module.py | 19 ++--- web3/method.py | 16 +++-- 3 files changed, 99 insertions(+), 35 deletions(-) diff --git a/tests/core/method-class/test_method.py b/tests/core/method-class/test_method.py index 44e132f4b6..9e0bb64e0d 100644 --- a/tests/core/method-class/test_method.py +++ b/tests/core/method-class/test_method.py @@ -3,6 +3,9 @@ ) import pytest +from eth_utils import ( + ValidationError, +) from eth_utils.toolz import ( compose, ) @@ -69,7 +72,7 @@ def test_get_formatters_non_falsy_config_retrieval(): first_formatter = (method.request_formatters(method_name).first,) all_other_formatters = method.request_formatters(method_name).funcs assert len(first_formatter + all_other_formatters) == 2 - # assert method.request_formatters('eth_nonmatching') == 'nonmatch' + assert (method.request_formatters('eth_getBalance').first,) == first_formatter def test_input_munger_parameter_passthrough_matching_arity(): @@ -89,9 +92,17 @@ def test_input_munger_parameter_passthrough_mismatch_arity(): method.input_munger(object(), ['first', 'second', 'third'], {}) -def test_input_munger_falsy_config_result_in_default_munger(): +def test_default_input_munger_with_no_input_parameters(): method = Method( - mungers=[], + json_rpc_method='eth_method', + ) + assert method.input_munger(object(), [], {}) == [] + + +@pytest.mark.parametrize('empty', ([], (), None), ids=['empty-list', 'empty-tuple', 'None']) +def test_empty_input_munger_with_no_input_parameters(empty): + method = Method( + mungers=empty, json_rpc_method='eth_method', ) assert method.input_munger(object(), [], {}) == [] @@ -99,14 +110,69 @@ def test_input_munger_falsy_config_result_in_default_munger(): def test_default_input_munger_with_input_parameters(): method = Method( - mungers=[], json_rpc_method='eth_method', ) assert method.input_munger(object(), [1], {}) == [1] +@pytest.mark.parametrize('empty', ([], (), None), ids=['empty-list', 'empty-tuple', 'None']) +def test_empty_input_mungers_with_input_parameters(empty): + method = Method( + mungers=empty, + json_rpc_method='eth_method', + ) + assert method.input_munger(object(), [1], {}) == [1] + + +def test_default_munger_for_property_with_no_input_parameters(): + method = Method( + is_property=True, + json_rpc_method='eth_method', + ) + assert method.input_munger(object(), [], {}) == () + + +@pytest.mark.parametrize('empty', ([], (), None), ids=['empty-list', 'empty-tuple', 'None']) +def test_empty_mungers_for_property_with_no_input_parameters(empty): + method = Method( + is_property=True, + mungers=empty, + json_rpc_method='eth_method', + ) + assert method.input_munger(object(), [], {}) == () + + +def test_default_munger_for_property_with_input_parameters_raises_ValidationError(): + method = Method( + is_property=True, + json_rpc_method='eth_method', + ) + with pytest.raises(ValidationError, match='Parameters cannot be passed to a property'): + method.input_munger(object(), [1], {}) + + +@pytest.mark.parametrize('empty', ([], (), None), ids=['empty-list', 'empty-tuple', 'None']) +def test_empty_mungers_for_property_with_input_parameters_raises_ValidationError(empty): + method = Method( + is_property=True, + mungers=empty, + json_rpc_method='eth_method', + ) + with pytest.raises(ValidationError, match='Parameters cannot be passed to a property'): + method.input_munger(object(), [1], {}) + + +def test_property_with_mungers_raises_ValidationError(): + with pytest.raises(ValidationError, match='Mungers cannot be used with a property'): + Method( + is_property=True, + mungers=[lambda m, z, y: 'success'], + json_rpc_method='eth_method', + ) + + @pytest.mark.parametrize( - "method_config,args,kwargs,expected_request_result,expected_result_formatters_len", + "method_config,args,kwargs,expected_request_result", ( ( { @@ -115,17 +181,15 @@ def test_default_input_munger_with_input_parameters(): [], {}, ValueError, - 2 ), ( { 'mungers': [], 'json_rpc_method': 'eth_getBalance', }, - ['unexpected_argument'], + ['only_the_first_argument_but_expects_two'], {}, IndexError, - 2 ), ( { @@ -135,7 +199,6 @@ def test_default_input_munger_with_input_parameters(): ['0x0000000000000000000000000000000000000000', 3], {}, ('eth_getBalance', (('0x' + '00' * 20), "0x3")), - 2 ), ( { @@ -145,7 +208,6 @@ def test_default_input_munger_with_input_parameters(): ['0x0000000000000000000000000000000000000000', 3], {}, ('eth_getBalance', (('0x' + '00' * 20), "0x3")), - 2 ), ( { @@ -158,7 +220,6 @@ def test_default_input_munger_with_input_parameters(): [1, 2, 3, ('0x' + '00' * 20)], {}, ('eth_getBalance', (('0x' + '00' * 20), "1")), - 2, ), ( { @@ -171,7 +232,6 @@ def test_default_input_munger_with_input_parameters(): [1, 2, 3, 4], {}, TypeError, - 2, ), ( { @@ -181,7 +241,6 @@ def test_default_input_munger_with_input_parameters(): ('0x0000000000000000000000000000000000000000', 3), {}, ('eth_getBalance', ('0x0000000000000000000000000000000000000000', '0x3')), - 2, ), ( { @@ -190,7 +249,6 @@ def test_default_input_munger_with_input_parameters(): ('0x0000000000000000000000000000000000000000', 3), {}, ('eth_getBalance', ('0x0000000000000000000000000000000000000000', '0x3')), - 2, ), ( { @@ -203,7 +261,6 @@ def test_default_input_munger_with_input_parameters(): [('0x' + '00' * 20), 1, 2, 3], {}, ('eth_getBalance', (('0x' + '00' * 20), '1')), - 2, ), ( { @@ -213,7 +270,6 @@ def test_default_input_munger_with_input_parameters(): [], {}, ('eth_chainId', ()), - 2, ), ( { @@ -223,16 +279,15 @@ def test_default_input_munger_with_input_parameters(): [], {}, ('eth_chainId', ()), - 2, ), ), ids=[ 'raises-error-no-rpc-method', - 'test-unexpected-arg', + 'test-missing-argument', 'test-rpc-method-as-string', 'test-rpc-method-as-callable', 'test-arg-munger', - 'test-munger-wrong-length-arg', + 'test-munger-too-many-args', 'test-request-formatters-default-root-munger-explicit', 'test-request-formatters-default-root-munger-implicit', 'test-mungers-and-request-formatters', @@ -245,7 +300,7 @@ def test_process_params( args, kwargs, expected_request_result, - expected_result_formatters_len): +): if isclass(expected_request_result) and issubclass(expected_request_result, Exception): with pytest.raises(expected_request_result): @@ -257,7 +312,9 @@ def test_process_params( assert request_params == expected_request_result first_formatter = (output_formatter[0].first,) all_other_formatters = output_formatter[0].funcs - assert len(first_formatter + all_other_formatters) == expected_result_formatters_len + + # the expected result formatters length is 2 + assert len(first_formatter + all_other_formatters) == 2 def keywords(module, keyword_one, keyword_two): diff --git a/tests/core/module-class/test_module.py b/tests/core/module-class/test_module.py index 3654a7df8d..ec67fa8d46 100644 --- a/tests/core/module-class/test_module.py +++ b/tests/core/module-class/test_module.py @@ -59,17 +59,20 @@ def test_attach_methods_to_module(web3_with_external_modules): def test_attach_methods_with_mungers(web3_with_external_modules): w3 = web3_with_external_modules + # `method1` uses `eth_getBlockByNumber` but makes use of unique mungers w3.module1.attach_methods({ 'method1': Method('eth_getBlockByNumber', mungers=[ - lambda _method, block_id, f, _z: (block_id, f), - lambda _m, block_id, _f: (block_id - 1,), + lambda _method, block_id, full_transactions: (block_id, full_transactions), + # take the user-provided `block_id` and subtract 1 + lambda _method, block_id, full_transactions: (block_id - 1, full_transactions), ]), }) - assert w3.eth.get_block(0)['baseFeePerGas'] == 1000000000 - assert w3.eth.get_block(1)['baseFeePerGas'] == 875000000 + assert w3.eth.get_block(0, False)['baseFeePerGas'] == 1000000000 + assert w3.eth.get_block(1, False)['baseFeePerGas'] == 875000000 - # `method1` should take a higher block number than `eth_getBlockByNumber` due to mungers and no - # other params should matter - assert w3.module1.method1(1, False, '_is_never_used_')['baseFeePerGas'] == 1000000000 - assert w3.module1.method1(2, '_will_be_overridden_', None)['baseFeePerGas'] == 875000000 + # Testing the mungers work: + # `method1` also calls 'eth_getBlockByNumber' but subtracts 1 from the user-provided `block_id` + # due to the second munger. So, `0` from above is a `1` here and `1` is `2`. + assert w3.module1.method1(1, False)['baseFeePerGas'] == 1000000000 + assert w3.module1.method1(2, False)['baseFeePerGas'] == 875000000 diff --git a/web3/method.py b/web3/method.py index ce94e75915..9e0e6b822f 100644 --- a/web3/method.py +++ b/web3/method.py @@ -15,6 +15,9 @@ ) import warnings +from eth_utils import ( + ValidationError, +) from eth_utils.curried import ( to_tuple, ) @@ -62,6 +65,9 @@ def inner(args: Any) -> TReturn: def _set_mungers(mungers: Optional[Sequence[Munger]], is_property: bool) -> Sequence[Any]: + if is_property and mungers: + raise ValidationError("Mungers cannot be used with a property.") + return ( mungers if mungers else [default_munger] if is_property @@ -70,10 +76,9 @@ def _set_mungers(mungers: Optional[Sequence[Munger]], is_property: bool) -> Sequ def default_munger(_module: "Module", *args: Any, **kwargs: Any) -> Tuple[()]: - if not args and not kwargs: - return () - else: - raise TypeError("Parameters passed to method without parameter mungers defined.") + if args or kwargs: + raise ValidationError("Parameters cannot be passed to a property.") + return () def default_root_munger(_module: "Module", *args: Any) -> List[Any]: @@ -136,7 +141,6 @@ def __init__( self.mungers = _set_mungers(mungers, is_property) self.request_formatters = request_formatters or get_request_formatters self.result_formatters = result_formatters or get_result_formatters - self.error_formatters = get_error_formatters self.null_result_formatters = null_result_formatters or get_null_result_formatters self.method_choice_depends_on_args = method_choice_depends_on_args self.is_property = is_property @@ -205,7 +209,7 @@ def process_params( method = self.method_selector_fn() response_formatters = ( self.result_formatters(method, module), - self.error_formatters(method), + get_error_formatters(method), self.null_result_formatters(method), ) request = (