-
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
Contract Events APIs #3472
Contract Events APIs #3472
Conversation
4ae2950
to
e1eabd2
Compare
e1eabd2
to
711f58a
Compare
find_events_by_identifier
for Contract Events
find_events_by_identifier
for Contract Events479723f
to
40cc3f9
Compare
40cc3f9
to
716b756
Compare
290094b
to
d4727ed
Compare
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 will be a great addition to get in. Currently as it stands, if I have an ABI that has both a Deposit
event and a Deposited
event, and I try to use contract.get_event_by_name()
with either "Deposit" or "Deposited", I can get neither event. Instead, I get:
Web3ValueError: Could not find any event with matching name
I don't think this is desired behavior. Those are not ambiguous but distinct names. Can we add a test for something like this and make sure it passes? I saw a few nits in message wording, feel free to take or leave those as I see it in the function messages as well. I'll take another pass here once we get that test in.
@@ -365,6 +373,46 @@ def get_function_by_identifier( | |||
return fns[0] | |||
|
|||
|
|||
def find_events_by_identifier( |
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.
nit: Should we rename this to get_events_by_identifier
since the singular is get_event_by_identifier
?
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.
There's a convention that follows the find_*
used for lookups which return a list of items, and get_*
when expecting a single result. The get method takes the resulting list from the find and just returns a single element (if there is not a single result in the list, the get method raises an exception).
""" | ||
if len(events) > 1: | ||
raise Web3ValueError( | ||
f"Found multiple events with matching {identifier}. " f"Found: {events!r}" |
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.
nit: use "identifier" in the message for clarity
f"Found multiple events with matching {identifier}. " f"Found: {events!r}" | |
f"Found multiple events with matching identifier `{identifier}`: {events!r}" |
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 identifier
is a little confusing because we use abi_element_identifier
to represent the name of the ABIFunction
or ABIEvent
.
In this context, identifier
is a string that represents the type of "check" passed in. For example, get_function_by_signature
uses find_functions_by_identifier
by passing a "check" function that retrieves all functions from the ABI which passes the check. Then the resulting list of ABI components is passed to get_function_by_identifier
to ensure there is only one result in the list and returns the ABI component itself. If not, an exception is raised, and the identifier
is "signature"
, indicating the search type.
f"Found multiple events with matching {identifier}. " f"Found: {events!r}" | ||
) | ||
elif len(events) == 0: | ||
raise Web3ValueError(f"Could not find any event with matching {identifier}") |
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.
nit
raise Web3ValueError(f"Could not find any event with matching {identifier}") | |
raise Web3ValueError(f"Could not find any event with matching identifier `{identifier}`") |
""" | ||
Check that the provided list of TContractFunction instances contains one element and | ||
return it. | ||
""" | ||
if len(fns) > 1: | ||
raise Web3ValueError( | ||
f"Found multiple functions with matching {identifier}. " f"Found: {fns!r}" |
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 see now we already phrase it this way with functions... take or leave really. Tiniest of nits.
f"Found multiple functions with matching {identifier}. " f"Found: {fns!r}" | |
f"Found multiple functions with matching identifier `{identifier}`. Found: {fns!r}" |
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 is looking good to me once the Deposit/Deposited edge case that @fselmo found is cleaned up. Those doctests are super nice, thanks for getting those in. I just left a few comments!
@@ -232,12 +248,13 @@ Each Contract Factory exposes the following methods. | |||
|
|||
Returns the transaction hash for the deploy transaction. | |||
|
|||
.. code-block:: python | |||
.. doctest:: contractmethods |
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.
Nice work with this!
@@ -545,14 +545,14 @@ def test_contract_event_get_logs_sorted_by_log_index(w3, emitter, request_mocker | |||
] | |||
|
|||
with request_mocker(w3, mock_results={"eth_getLogs": get_logs_response}): | |||
logs = emitter.events.LogNoArguments().get_logs() |
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.
Do we have any tests to make sure that an event called with parens still passes? I'm fine taking them out here, but it might be good to have an explicit test to make sure it still works since it's documented. And the parens/no parens might be worth removing in v8. It feels antithetical to the zen of python, and I don't think there is a whole lot of value add in being able to call events with both parentheses and without. Although I can see the argument for maintaining consistency with ContractFunctions
/shrug
web3/contract/base_contract.py
Outdated
Raises a Web3ValueError if the signature is invalid or if there is no match or | ||
more than one is found. | ||
""" | ||
if " " in signature: |
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.
Should we just strip out the spaces instead of raising 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.
I pulled this logic from get_function_by_signature
but it might be better to strip out spaces in the long run. I can strip those in get_event_by_signature
for now.
>>> contract.get_event_by_name('Approval') | ||
<Event Approval(address,address,uint256)> | ||
|
||
.. py:classmethod:: Contract.find_events_by_selector(selector) |
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.
Does this work with the 4byte selector too or just the whole thing?
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 not certain I've come across events that are represented by a 4byte selector, so this is implemented to match the whole event signature encoding. If it makes sense to use the 4byte selector I can update this to match the function selector.
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.
Oh, right. This is events. I think we can just leave it as-is. If we get a request to change it later, we can.
Contract utils for `get_event_by_name`, `get_event_by_signature`, `get_event_by_selector`, `get_event_by_topic` Contract utils for `find_events_by_name`, `find_events_by_signature`, `find_events_by_selector`, `find_events_by_topic`
32c0e16
to
465def7
Compare
@fselmo I havent reproduced the error you saw but added tests to cover those cases for both functions and events. Please let me know what the ABI was that you used and maybe there's an edge case I'm missing. Thanks! |
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 good to me! Just left a nit or two, and some typing suggestions/clarifications 🚀
web3/contract/async_contract.py
Outdated
address: ChecksumAddress, | ||
callable_check: Callable[..., Any], | ||
) -> List["AsyncContractEvent"]: | ||
return cast( |
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 is a redundant cast
return PropertyCheckingFactory(class_name, (cls,), kwargs) | ||
def factory( | ||
cls, class_name: str, **kwargs: Any | ||
) -> Union["ContractEvent", "AsyncContractEvent"]: |
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 wonder if this should be TContractEvent
?
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 found that using TContractEvent
requires one argument for factory
to be of that same type.
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.
Ah, okay. Thanks for looking into it!
@@ -956,6 +1121,26 @@ def get_function_by_identifier( | |||
"This method should be implemented in the inherited class" | |||
) | |||
|
|||
@combomethod | |||
def find_events_by_identifier( |
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.
nit:find/get_functions/events_by_id
are all beneath the # Private helpers
comment. Consider moving above 🤷
web3/contract/contract.py
Outdated
address: ChecksumAddress, | ||
callable_check: Callable[..., Any], | ||
) -> List["ContractEvent"]: | ||
return cast( |
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.
redundant cast
What was wrong?
Contract function classes provide functions for extracting contract components. Contract event API methods do not yet exist. The first set of methods for
find_events_by_identifier
andget_event_by_identifier
should be added.Contract Event APIs should be provided, namely
all_events
,find_events_by_name
andget_event_by_name
.How was it fixed?
Implement functions for
Contract
classes to find many events or getting a single event by identifier,find_events_by_identifier
andget_event_by_identifier
.Additional Event APIs are now available (
all_events
,find_events_by_name
,get_event_by_name
,find_events_by_signature
,get_event_by_signature
,find_events_by_selector
,get_event_by_selector
,find_events_by_topic
, andget_event_by_topic
)Todo:
Cute Animal Picture