-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
outline for async web3 api: implement async Version module #1166
Conversation
web3/method.py
Outdated
'name': "signInBlood", | ||
'mungers': [], | ||
'json_rpc_method': "eth_signTransactionInBlood", | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
meant to move this and the test below.
For those interested in what 'mungers' are (thanks for the fun word @carver). It is the list of the functions that the python method arguments get piped through, that should output a json_rpc ready param dict. Havent found a name I like better. Because these functions can be validators, normalizers, formatters, occur in any order, with probably a huge variation arity, etc., "munger" seems appropriate. These are being kept separate from the request parameter and result formatters, taken from the middleware formatters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm good with "mungers" unless something better surfaces. Maybe good to try and find a good place to write up a nice comment documenting the validator/normalizer/formatter
concepts it encapsulates
""" | ||
for fn in fns: | ||
val = fn(val) | ||
yield val |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to utils
web3/module.py
Outdated
self.method_class, | ||
attr) | ||
except UndefinedMethodError: | ||
raise AttributeError |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Im wondering if it is worth caching the method object after the first lookup.
web3/manager.py
Outdated
async def _make_async_request(self, method, params): | ||
"""TODO: Can this be made dry? Going for dumb solution for now. | ||
""" | ||
for provider in self.providers: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be able to drop this loop in v5
by removing the multi-provider API. cc @kclowes
web3/manager.py
Outdated
@@ -102,6 +98,26 @@ def _make_request(self, method, params): | |||
) | |||
) | |||
|
|||
async def _make_async_request(self, method, params): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming convention for this type of thing has been to use a coro_
prefix so maybe _coro_make_request
.
web3/method.py
Outdated
'name': "signInBlood", | ||
'mungers': [], | ||
'json_rpc_method': "eth_signTransactionInBlood", | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm good with "mungers" unless something better surfaces. Maybe good to try and find a good place to write up a nice comment documenting the validator/normalizer/formatter
concepts it encapsulates
web3/method.py
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about removing this and only implementing the concept of direct property
lookups at the Module
level?
class VersionModule:
get_version = VersionMethod(...)
@property
def version(self):
return self.get_version()
Would work similarly for async
web3/method.py
Outdated
|
||
|
||
@to_tuple | ||
def pipe_appends(fns, val): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, it's kind of like https://toolz.readthedocs.io/en/latest/api.html#toolz.itertoolz.accumulate combined with pipe
. Maybe re-use the name and call this accumulate_pipe
or pipe_and_accumulate
?
web3/module.py
Outdated
if self.module_config is None: | ||
self.module_config = dict() | ||
|
||
def __getattr__(self, attr): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm 👎 on using __getattr__
. It makes introspection and type hinting not work well.
I'd advocate for something like:
class Module:
get_version: GetVersionMethod # this is how we get strong-ish type hints.
def __init__(self, ...):
self.get_version = GetVersion(...)
In theory we can do this by assigning them at the class level and using the descriptor API to get access to the w3
instance.
I want to restate the goals for this pull request now that it has been worked through further. It is basically restating the opening description, but with more clarity on my part. This PRs primary intention is to implement the Method objects and the strategy for sync and async execution. More specifically it aims to determine the association between the modules, method definitions and async and sync calling functions. The following is a description of the structure ive chosen: The In this first version the method configuration is a dict, The Method class has a secondary concern with directing the calls to the method to a delegated call function. The Method class defines The method does not retain any state from the module object. The Using this structure the Method objects can be defined once in a shared base class. To make this safe, I need to find a way to protect the Method class from mutations after its instantiation with the method config. Items left todo:
Tests for the following: Method.method_selector_fn()
Method.get_formatters()
Method.input_munger():
Method.process_params():
|
@pipermerriam @kclowes @carver This is ready for review. For the next PR I plan on working on another module that requires more input processing and formatters taken from middlewares. |
Sorry, with the holidays, it will take a while before it is reviewed. Maybe early January (for me at least). |
NP. In the meantime I will start on another PR to work out how migrating the middleware formatters will look, and backmerge any changes from this PR. |
web3/method.py
Outdated
formatting. Any processing on the input parameters that need to happen before | ||
json_rpc method string selection occurs. | ||
|
||
A note about mungers: The first munger should reflect the desired |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This note would be a lot clearer were it to contain a code example.
web3/method.py
Outdated
3. request and response formatters are retrieved - formatters are retrieved | ||
using the json rpc method string. The lookup function provided by the | ||
formatter_lookup_fn configuration is passed the method string and is | ||
expected to return a 2 length tuple of lists containing the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick
My vocabulary for this would be 2 length tuple -> 2-tuple
but I'm not sure if that nomenclature is well known.
web3/method.py
Outdated
return obj.retrieve_caller_fn(self) | ||
|
||
def __setattr__(self, key, val): | ||
raise TypeError('Method cannot be modified after instantiation') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm leaning towards 👎 on this. trying to enforce immutability on objects seems like a deep rabbit hole.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as it's not trying to make a security guarantee, it's simply nudging users toward ideal behavior, then I'm more open to this kind of thing.
web3/method.py
Outdated
""" | ||
_config = None | ||
|
||
def __init__(self, method_config, web3=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious at the use of a dictionary instead of just having individual parameters for each of the various parts that a Method
needs. I'm preferential towards the later.
web3/method.py
Outdated
if i == 0: | ||
vals = munger(*args, **kwargs) | ||
else: | ||
vals = munger(*vals) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can split this up to be a bit cleaner.
mungers_iter = iter(mungers)
root_munger = first(mungers_iter)
munged_inputs = pipe(root_munger(*args, **kwargs), *mungers)
Code above doesn't take into account that each munger needs to be called as munger(*args)
. One way to deal with this would be to just adjust the API so that each munger is called as munger(args)
or maybe a small helper which did the *args
application that you wrap each munger
function in.
def star_apply(fn):
@functools.wraps(fn)
def inner(args):
return fn(*args)
return inner
which conversts the above code to:
mungers_iter = iter(mungers)
root_munger = first(mungers_iter)
munged_inputs = pipe(root_munger(*args, **kwargs), *map(star_apply, mungers))
web3/method.py
Outdated
|
||
|
||
@to_tuple | ||
def pipe_and_accumulate(val, fns): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potentially add an _
prefix to denote private API. (or move it to live under web3._utils
super().__setattr__('_config', dict(method_config)) | ||
|
||
def __get__(self, obj=None, objType=None): | ||
return obj.retrieve_caller_fn(self) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
web3/method.py
Outdated
def __init__(self, method_config, web3=None): | ||
super().__setattr__('_config', dict(method_config)) | ||
|
||
def __get__(self, obj=None, objType=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you convert to klass
or obj_type
or typ
to stick with python naming conventions.
2815866
to
8ca6573
Compare
Ive added changes to give the mungers access to the module instance, to support certain features, like the default account api. I didnt want to expose the entire web3 instance to the mungers, so I I created a new Module class to eventually replace it (ModuleV2) that isolates the web3 instance to the method calling functions, rather than exposing a web3 instance to the module. The method callers pass the module instance to the method input processing. Organizing it this way makes it hard to write "re-entrant" calls at the module level. If I come across any web3 calls in the modules as Im working through the conversion, they will be moved to a middleware, where they can be written for both blocking and async execution pathways. |
web3/method.py
Outdated
def inner(args): | ||
return fn(*args) | ||
def inner(module_args): | ||
module, args = module_args |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current API of having all the mungers return the module
and then pass it onto the next seems problematic/error prone. Instead of requiring the munger to both be accepted as an argument as well I think we should do the following.
- only pass as argument.
- use
curry
orfunctools.partial
to set themodule
parameter on all the munger functions before passing the values through.
This would make the map(star_apply, mungers)
into something like:
map(lambda munger: star_apply(functools.partial(munger, module)), mungers)
web3/method.py
Outdated
if not args and not kwargs: | ||
return list() | ||
return module, list() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the list()
intentional here or should this be converted to tuple
.localvimrc
Outdated
@@ -0,0 +1,3 @@ | |||
let test#python#pytest#options = { | |||
\ 'suite': ' tests/core', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like it leaked in and should be removed.
@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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this shouldn't be too big a deal, though I might advocate for us ensuring that every one of our convenience properties has a method version as well.
class Version(Module):
@property
def api(self):
return self.get_version()
def get_version(self):
...
Reason being that while awaitable properties work just fine, they are not a very intuitive API where-as their method equivalents don't suffer from this awkwardness. It will be up to the user to view the documentation for the various methods and to know that they are coroutines.
_get_protocol_version = Method('eth_protocolVersion') | ||
|
||
@property | ||
def api(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I'm 👍 on having a mixed-bag of coroutine and normal properties. Forcing simple APIs like this that don't require I/O to be coroutines makes them more complex than necessary and adds un-necessary performance overhead. 👍
8f264c1
to
486e28f
Compare
self.web3, | ||
tuple(self.middleware_stack)) | ||
self.logger.debug("Making request. Method: %s", method) | ||
return request_func(method, params) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kclowes @njgheorghita
Before I merge this, it would be good to get a couple sets of eyes on this. Its the only place in this PR that touches the critical api.
With no longer iterating over the provider list, I think the UnhandledRequest exception is unnecessary and we can let the CannotHandleRequest pass through. Or maybe this should be catching the "CannotHandleRequest" error like before?
For reference here is the request function when it had the provider list so you dont have to dig through the history:
#
# Provider requests and response
#
def _make_request(self, method, params):
for provider in self.providers:
request_func = provider.request_func(self.web3, tuple(self.middleware_stack))
self.logger.debug("Making request. Method: %s", method)
try:
return request_func(method, params)
except CannotHandleRequest:
continue
else:
raise UnhandledRequest(
"No providers responded to the RPC request:\n"
"method:{0}\n"
"params:{1}\n".format(
method,
params,
)
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I understand, it looks good to me. I'm thinking it's fine to let the CannotHandleRequest
exception pass through from the provider rather than catching it here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though, maybe beefing up the msg here to have more info like in the UnhandledRequest
msg from before could be useful
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If CannotHandleRequest
is no longer used anywhere it can be removed (didn't look to see if you already did this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree with @njgheorghita. CannotHandleRequest
makes sense to me, especially with a better error message!
- Allows method configuration to be made in one place. - Blocking and Async execution pathways can be controlled by the module, using the is_async attribute.
Hey, does web3 currently support async? How do I initiate HTTPProvider Web3 client in an async-matter? |
@adichen3798 It does not yet support async, but the goal is to get something out by the end of the quarter! |
Hello , still no support for async? |
looks like async web3 is in development here: https://github.com/guanqun/async-web3.py |
Nice! Async is a work in progress for us but is actively being worked on. This issue is the most up to date if you're interested in following along. |
Related to Issue #1161.
Minimal web3 async api. Includes:
AsyncEthereumTesterProvider
was added, which has eth-tester related middlewares removed, with a request function that doesnt await.request_async
was updated in RequestManager to be a coroutine. code copy of the blocking request. Im preferring simple over dry for this push.AsyncMethod
andBlockingMethod
callable classes (can be configured for property access)EDIT: methods are attached via a descriptor now.web3.Module
manages the "attaching" of method callables to the module, via amethod_config
andmethod_class
.Cute Animal Picture