Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better support for custom methods for modules #2383

Merged
merged 4 commits into from Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/2383.breaking-change.rst
@@ -0,0 +1 @@
Add ``attach_methods()`` to ``Module`` class to facilitate attaching methods to modules.
137 changes: 107 additions & 30 deletions tests/core/method-class/test_method.py
Expand Up @@ -3,6 +3,9 @@
)
import pytest

from eth_utils import (
ValidationError,
)
from eth_utils.toolz import (
compose,
)
Expand Down Expand Up @@ -32,7 +35,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'
Expand Down Expand Up @@ -69,15 +72,15 @@ 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
kclowes marked this conversation as resolved.
Show resolved Hide resolved


def test_input_munger_parameter_passthrough_matching_arity():
method = Method(
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']
fselmo marked this conversation as resolved.
Show resolved Hide resolved


def test_input_munger_parameter_passthrough_mismatch_arity():
Expand All @@ -89,25 +92,87 @@ 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',
)
method.input_munger(object(), [], {}) == []
assert method.input_munger(object(), [], {}) == []


def test_default_input_munger_with_input_parameters_exception():
@pytest.mark.parametrize('empty', ([], (), None), ids=['empty-list', 'empty-tuple', 'None'])
def test_empty_input_munger_with_no_input_parameters(empty):
method = Method(
mungers=[],
mungers=empty,
json_rpc_method='eth_method',
)
with pytest.raises(TypeError):
assert method.input_munger(object(), [], {}) == []


def test_default_input_munger_with_input_parameters():
method = Method(
json_rpc_method='eth_method',
)
assert method.input_munger(object(), [1], {}) == [1]
fselmo marked this conversation as resolved.
Show resolved Hide resolved


@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",
(
(
{
Expand All @@ -116,17 +181,15 @@ def test_default_input_munger_with_input_parameters_exception():
[],
{},
ValueError,
2
),
(
{
'mungers': [],
'json_rpc_method': 'eth_getBalance',
},
['unexpected_argument'],
['only_the_first_argument_but_expects_two'],
{},
TypeError,
2
IndexError,
fselmo marked this conversation as resolved.
Show resolved Hide resolved
),
(
{
Expand All @@ -136,7 +199,6 @@ def test_default_input_munger_with_input_parameters_exception():
['0x0000000000000000000000000000000000000000', 3],
{},
('eth_getBalance', (('0x' + '00' * 20), "0x3")),
2
),
(
{
Expand All @@ -146,7 +208,6 @@ def test_default_input_munger_with_input_parameters_exception():
['0x0000000000000000000000000000000000000000', 3],
{},
('eth_getBalance', (('0x' + '00' * 20), "0x3")),
2
),
(
{
Expand All @@ -159,7 +220,6 @@ def test_default_input_munger_with_input_parameters_exception():
[1, 2, 3, ('0x' + '00' * 20)],
{},
('eth_getBalance', (('0x' + '00' * 20), "1")),
2,
),
(
{
Expand All @@ -172,7 +232,6 @@ def test_default_input_munger_with_input_parameters_exception():
[1, 2, 3, 4],
{},
TypeError,
2,
),
(
{
Expand All @@ -182,7 +241,14 @@ def test_default_input_munger_with_input_parameters_exception():
('0x0000000000000000000000000000000000000000', 3),
{},
('eth_getBalance', ('0x0000000000000000000000000000000000000000', '0x3')),
2,
),
(
{
'json_rpc_method': 'eth_getBalance',
},
('0x0000000000000000000000000000000000000000', 3),
{},
('eth_getBalance', ('0x0000000000000000000000000000000000000000', '0x3')),
),
(
{
Expand All @@ -195,7 +261,6 @@ def test_default_input_munger_with_input_parameters_exception():
[('0x' + '00' * 20), 1, 2, 3],
{},
('eth_getBalance', (('0x' + '00' * 20), '1')),
2,
),
(
{
Expand All @@ -205,39 +270,51 @@ def test_default_input_munger_with_input_parameters_exception():
[],
{},
('eth_chainId', ()),
2,
)
),
(
{
'is_property': True,
'json_rpc_method': 'eth_chainId',
},
[],
{},
('eth_chainId', ()),
),
),
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-request-formatters',
'test-munger-too-many-args',
'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(
method_config,
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):
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)
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):
Expand All @@ -248,8 +325,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)

Expand Down
78 changes: 78 additions & 0 deletions tests/core/module-class/test_module.py
@@ -0,0 +1,78 @@
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

# `method1` uses `eth_getBlockByNumber` but makes use of unique mungers
w3.module1.attach_methods({
'method1': Method('eth_getBlockByNumber', mungers=[
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, False)['baseFeePerGas'] == 1000000000
assert w3.eth.get_block(1, False)['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
1 change: 0 additions & 1 deletion web3/__init__.py
Expand Up @@ -45,7 +45,6 @@
"HTTPProvider",
"IPCProvider",
"WebsocketProvider",
"TestRPCProvider",
fselmo marked this conversation as resolved.
Show resolved Hide resolved
"EthereumTesterProvider",
"Account",
"AsyncHTTPProvider",
Expand Down