Skip to content

Commit

Permalink
feat: allow creating MockContractLog when multiple events have the …
Browse files Browse the repository at this point in the history
…same name (#2414)
  • Loading branch information
antazoey authored Dec 12, 2024
1 parent e92446f commit 98db2da
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 11 deletions.
50 changes: 46 additions & 4 deletions src/ape/contracts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,16 @@ def __len__(self):
return sum(1 for _ in logs)

def __call__(self, *args: Any, **kwargs: Any) -> MockContractLog:
"""
Create a mock-instance of a log using this event ABI and the contract address.
Args:
*args: Positional arguments for the event.
**kwargs: Key-word arguments for the event.
Returns:
:class:`~ape.types.events.MockContractLog`
"""
# Create a dictionary from the positional arguments
event_args: dict[Any, Any] = dict(zip((ipt.name for ipt in self.abi.inputs), args))
if overlapping_keys := set(event_args).intersection(kwargs):
Expand Down Expand Up @@ -827,6 +837,41 @@ def poll_logs(
)


# TODO: In 0.9, just make `_events_` or ContractEvent possibly handle multiple ABIs
# much like the transactions handlers do. OR at least take the opportunty to refactor.
class ContractEventWrapper:
"""
A wrapper used when multiple events have the same so that
you can still create mock-logs.
"""

def __init__(self, events: list[ContractEvent]):
self.events = events

def __call__(self, *args, **kwargs) -> MockContractLog:
"""
Create a mock contract log using the first working ABI.
Args:
*args: Positional arguments for the event.
**kwargs: Key-word arguments for the event.
Returns:
:class:`~ape.types.events.MockContractLog`
"""
# TODO: Use composite error.
errors = []
for evt in self.events:
try:
return evt(*args, **kwargs)
except ValueError as err:
errors.append(err)
continue # not a match

error_str = ", ".join([f"{e}" for e in errors])
raise ValueError(f"Could not make a mock contract log. Errors: {error_str}")


class ContractTypeWrapper(ManagerAccessMixin):
contract_type: "ContractType"
base_path: Optional[Path] = None
Expand Down Expand Up @@ -1336,10 +1381,7 @@ def __getattr__(self, attr_name: str) -> Any:
elif attr_name in self._events_:
evt_options = self._events_[attr_name]
if len(evt_options) > 1:
raise ApeAttributeError(
f"Multiple events named '{attr_name}' in '{self.contract_type.name}'.\n"
f"Use '{self.get_event_by_signature.__name__}' look-up."
)
return ContractEventWrapper(evt_options)

return evt_options[0]

Expand Down
14 changes: 7 additions & 7 deletions tests/functional/test_contract_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
from eth_utils import to_hex
from ethpm_types import ContractType

from ape.contracts.base import ContractEventWrapper
from ape.exceptions import ProviderError
from ape.types.events import ContractLog
from ape.types.events import ContractLog, MockContractLog
from ape.types.units import CurrencyValueComparable

if TYPE_CHECKING:
Expand Down Expand Up @@ -310,12 +311,11 @@ def test_contract_two_events_with_same_name(
impl_container = chain.contracts.get_container(impl_contract_type)
impl_instance = owner.deploy(impl_container)

expected_err = (
f"Multiple events named '{event_name}' in '{impl_contract_type.name}'.\n"
f"Use 'get_event_by_signature' look-up."
)
with pytest.raises(AttributeError, match=expected_err):
_ = impl_instance.FooEvent
# Show some features still work when referencing by __getattr__.
wrapper = impl_instance.FooEvent
assert isinstance(wrapper, ContractEventWrapper)
mock_log = wrapper(bar=16)
assert isinstance(mock_log, MockContractLog)

expected_sig_from_impl = "FooEvent(uint256 bar, uint256 baz)"
expected_sig_from_interface = "FooEvent(uint256 bar)"
Expand Down

0 comments on commit 98db2da

Please sign in to comment.