Skip to content

Commit

Permalink
Add basic configurable Method class with attr lookup
Browse files Browse the repository at this point in the history
- Demonstrates limited feature set using the Version module
  • Loading branch information
dylanjw committed Dec 15, 2018
1 parent c028368 commit 1e4a021
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 0 deletions.
21 changes: 21 additions & 0 deletions tests/core/base-module/test_base_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import pytest

from web3.module import Module


class DummyWeb3:
def __init__(self):
Module.attach(self, 'module')


@pytest.fixture(scope='module')
def dw3():
w3 = DummyWeb3()
return w3


def test_module_lookup_method(dw3):
with pytest.raises(AttributeError):
assert dw3.module.blahblahblah
dw3.module.lookup_method_fn = lambda *_: True
assert dw3.module.blahblahblah is True
52 changes: 52 additions & 0 deletions tests/core/version-module/test_version_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest

from web3 import Web3, EthereumTesterProvider
from web3.providers.eth_tester.main import AsyncEthereumTesterProvider
from web3.version import BlockingVersion, AsyncVersion, Version


@pytest.fixture
def blocking_w3():
return Web3(
EthereumTesterProvider(),
modules={
'blocking_version': BlockingVersion,
'legacy_version': Version
})


@pytest.fixture
def async_w3():
return Web3(
AsyncEthereumTesterProvider(),
middlewares=[],
modules={
'async_version': AsyncVersion,
})


def test_blocking_version(blocking_w3):
assert blocking_w3.blocking_version.api == blocking_w3.legacy_version.api
assert blocking_w3.blocking_version.node == blocking_w3.legacy_version.node
assert blocking_w3.blocking_version.network == blocking_w3.legacy_version.network
assert blocking_w3.blocking_version.ethereum == blocking_w3.legacy_version.ethereum


@pytest.mark.asyncio
async def test_async_blocking_version(async_w3, blocking_w3):
# This seems a little awkward. How do we know if something is an awaitable
# or a static method?
assert async_w3.async_version.api == blocking_w3.legacy_version.api

assert await async_w3.async_version.node == blocking_w3.legacy_version.node
with pytest.raises(
ValueError,
message="RPC Endpoint has not been implemented: net_version"
):
# net_version is provided through a middleware
assert await async_w3.async_version.network == blocking_w3.legacy_version.network
with pytest.raises(
ValueError,
message="RPC Endpoint has not been implemented: eth_protocolVersion"
):
assert await async_w3.async_version.ethereum == blocking_w3.legacy_version.ethereum
4 changes: 4 additions & 0 deletions web3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ class TimeExhausted(Exception):
Raised when a method has not retrieved the desired result within a specified timeout.
"""
pass


class UndefinedMethodError(Exception):
pass
144 changes: 144 additions & 0 deletions web3/method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from eth_utils import (
to_tuple,
)
from eth_utils.toolz import (
identity,
pipe,
)

from web3.exceptions import (
UndefinedMethodError,
)

TEST_METHOD_CONFIG = {
'name': "signInBlood",
'mungers': [],
'json_rpc_method': "eth_signTransactionInBlood",
}


def lookup_method(module, module_config, method_class, attr_name):
try:
method = module_config[attr_name]
except KeyError:
raise UndefinedMethodError("No method named {0}".format(attr_name))
return method_class(module.web3, method)


class DummyRequestManager:
def request_blocking(method, params):
return (method, params)


class DummyWeb3:
manager = DummyRequestManager


def test_sync_method_config_loading():
signInBlood = BlockingMethod(DummyWeb3(), TEST_METHOD_CONFIG)
signInBlood.input_munger = lambda *_: {}
assert signInBlood.method_selector
assert ('eth_signTransactionInBlood', {}) == signInBlood()


class BaseMethod:
"""BaseMethod for web3 module methods
Calls to the Method go through these steps:
1. input munging ;) - includes normalization, parameter checking, formatters.
Any processing on the input parameters that need to happen before json_rpc
method string selection occurs.
2. method selection - function that selects the correct rpc_method. accepts a
function or an string.
3. constructing formatter middlewares - takes the rpc_method and looks up the
corresponding input/output formatters. these are the middlewares migrated here.
4. making the request through the middleware (pipeline)? wrapped request
function.
"""
def __init__(self, web3, method_config):
self.__name__ = method_config.get('name', 'anonymous')
self.__doc__ = method_config.get('doc', '')
self.is_property = method_config.get('is_property', False)
self.web3 = web3
self.input_munger = self._construct_input_pipe(
method_config.get('mungers')) or identity
self.method_selector = self._method_selector(
method_config.get('json_rpc_method'))
# TODO: Write formatter lookup.
self.lookup_formatter = None

def _construct_input_pipe(self, formatters):
formatters = formatters or [identity]

def _method_selector(self, selector):
"""Method selector can just be the method string.
"""
if isinstance(selector, (str,)):
return lambda _: selector
else:
return selector

def get_formatters(self, method_string):
"""Lookup the request formatters for the rpc_method"""
if not self.lookup_formatter:
return ([identity], [identity],)
else:
raise NotImplementedError()

def prep_for_call(self, *args, **kwargs):
# takes in input params, steps 1-3
params, method, (req_formatters, ret_formatters) = pipe_appends(
[self.input_munger, self.method_selector, self.get_formatters],
(args, kwargs,))
return pipe((method, params,), *req_formatters), ret_formatters

def __call__(self):
raise NotImplementedError()


@to_tuple
def pipe_appends(fns, val):
"""pipes val through a list of fns while appending the result to the
tuple output
e.g.:
>>> pipe_appends([lambda x: x**2, lambda x: x*10], 5)
(25, 250)
"""
for fn in fns:
val = fn(val)
yield val


class BlockingMethod(BaseMethod):
def __call__(self, *args, **kwargs):
(method, params), output_formatters = self.prep_for_call(*args, **kwargs)
return pipe(
self.web3.manager.request_blocking(method, params),
*output_formatters)

def __get__(self, obj, objType):
# allow methods to be configured for property access
if self.is_property is True:
return self.__call__()
else:
return self


class AsyncMethod(BaseMethod):
async def __call__(self, *args, **kwargs):
(method, params), output_formatters = self.prep_for_call(*args, **kwargs)
raw_result = await self.web3.manager.request_async(method, params)
return pipe(raw_result, *output_formatters)

async def __get__(self, obj, objType):
if self.is_property is True:
return await self.__call__()
else:
return self
26 changes: 26 additions & 0 deletions web3/module.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
from web3.method import lookup_method
from web3.exceptions import UndefinedMethodError


class Module:
web3 = None
method_class = None
module_config = None
lookup_method_fn = lookup_method

def __init__(self, web3):
self.web3 = web3
if self.module_config is None:
self.module_config = dict()

def __getattr__(self, attr):
# Method lookup magic
try:
method = self.lookup_method_fn(
self.module_config,
self.method_class,
attr)
except UndefinedMethodError:
raise AttributeError

# Emulate descriptor behavior to allow methods to control if
# they are properties or not.
if hasattr(method, '__get__'):
return method.__get__(None, self)

return method

@classmethod
def attach(cls, target, module_name=None):
Expand Down
40 changes: 40 additions & 0 deletions web3/version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
from web3.module import (
Module,
)
from web3.method import (
AsyncMethod,
BlockingMethod,
)


VERSION_MODULE_CONFIG = {
'node': {
'name': 'node',
'json_rpc_method': 'web3_clientVersion',
'is_property': True
},
'network': {
'name': 'network',
'json_rpc_method': 'net_version',
'is_property': True
},
'ethereum': {
'name': 'ethereum',
'json_rpc_method': 'eth_protocolVersion',
'is_property': True
},
}


class BaseVersion(Module):
module_config = VERSION_MODULE_CONFIG

@property
def api(self):
from web3 import __version__
return __version__


class AsyncVersion(BaseVersion):
method_class = AsyncMethod


class BlockingVersion(BaseVersion):
method_class = BlockingMethod


class Version(Module):
Expand Down

0 comments on commit 1e4a021

Please sign in to comment.