From a3d52f53d37159b704f037ed7e7496a34a0f4ba9 Mon Sep 17 00:00:00 2001 From: Felipe Selmo Date: Thu, 24 Feb 2022 19:04:29 -0700 Subject: [PATCH] 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()` --- 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 +-- 10 files changed, 169 insertions(+), 51 deletions(-) create mode 100644 tests/core/module-class/test_module.py 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