diff --git a/newsfragments/1594.bugfix.rst b/newsfragments/1594.bugfix.rst new file mode 100644 index 0000000000..2b35b9ce87 --- /dev/null +++ b/newsfragments/1594.bugfix.rst @@ -0,0 +1,9 @@ +Fixed hasattr overloader method in the web3.ContractEvent, web3.ContractFunction, +and web3.ContractCaller classes by implementing a try/except handler +that returns False if an exception is raised in the __getattr__ overloader method +(since __getattr__ HAS to be called in every __hasattr__ call). + +Created two new Exception classes, 'ABIEventFunctionNotFound' and 'ABIFunctionNotFound', +which inherit from both AttributeError and MismatchedABI, and replaced the MismatchedABI +raises in ContractEvent, ContractFunction, and ContractCaller with a raise to the created class +in the __getattr__ overloader method of the object. diff --git a/tests/core/contracts/test_contract_attributes.py b/tests/core/contracts/test_contract_attributes.py new file mode 100644 index 0000000000..db360357fe --- /dev/null +++ b/tests/core/contracts/test_contract_attributes.py @@ -0,0 +1,48 @@ +import pytest + +from web3.exceptions import ( + ABIEventFunctionNotFound, + ABIFunctionNotFound, +) + + +@pytest.fixture() +def abi(): + return '''[{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"function"}, {"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"event"}]''' # noqa: E501 + + +@pytest.mark.parametrize( + 'attribute', + ('functions', 'events', 'caller') +) +def test_getattr(web3, abi, attribute): + contract = web3.eth.contract(abi=abi) + contract_attribute = getattr(contract, attribute) + assert getattr(contract_attribute, "Increased") + + +@pytest.mark.parametrize( + 'attribute,error', ( + ('functions', ABIFunctionNotFound), + ('events', ABIEventFunctionNotFound), + ('caller', ABIFunctionNotFound), + ) +) +def test_getattr_raises_error(web3, abi, attribute, error): + contract = web3.eth.contract(abi=abi) + contract_attribute = getattr(contract, attribute) + + with pytest.raises(error): + getattr(contract_attribute, "Decreased") + + +@pytest.mark.parametrize( + 'attribute', + ('functions', 'events', 'caller') +) +def test_hasattr(web3, abi, attribute): + contract = web3.eth.contract(abi=abi) + contract_attribute = getattr(contract, attribute) + + assert hasattr(contract_attribute, "Increased") is True + assert hasattr(contract_attribute, "Decreased") is False diff --git a/web3/contract.py b/web3/contract.py index d6c90fa58b..5d9bdd9182 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -110,6 +110,8 @@ MutableAttributeDict, ) from web3.exceptions import ( + ABIEventFunctionNotFound, + ABIFunctionNotFound, BadFunctionCallOutput, BlockNumberOutofRange, FallbackNotFound, @@ -185,7 +187,7 @@ def __getattr__(self, function_name: str) -> "ContractFunction": "Are you sure you provided the correct contract abi?" ) elif function_name not in self.__dict__['_functions']: - raise MismatchedABI( + raise ABIFunctionNotFound( "The function '{}' was not found in this contract's abi. ".format(function_name), "Are you sure you provided the correct contract abi?" ) @@ -195,6 +197,12 @@ def __getattr__(self, function_name: str) -> "ContractFunction": def __getitem__(self, function_name: str) -> ABIFunction: return getattr(self, function_name) + def __hasattr__(self, event_name: str) -> bool: + try: + return event_name in self.__dict__['_events'] + except ABIFunctionNotFound: + return False + class ContractEvents: """Class containing contract event objects @@ -239,7 +247,7 @@ def __getattr__(self, event_name: str) -> "ContractEvent": "Are you sure you provided the correct contract abi?" ) elif event_name not in self.__dict__['_events']: - raise MismatchedABI( + raise ABIEventFunctionNotFound( "The event '{}' was not found in this contract's abi. ".format(event_name), "Are you sure you provided the correct contract abi?" ) @@ -257,6 +265,12 @@ def __iter__(self) -> Iterable["ContractEvent"]: for event in self._events: yield self[event['name']] + def __hasattr__(self, event_name: str) -> bool: + try: + return event_name in self.__dict__['_events'] + except ABIEventFunctionNotFound: + return False + class Contract: """Base class for Contract proxy classes. @@ -1353,7 +1367,7 @@ def __getattr__(self, function_name: str) -> Any: ) elif function_name not in set(fn['name'] for fn in self._functions): functions_available = ', '.join([fn['name'] for fn in self._functions]) - raise MismatchedABI( + raise ABIFunctionNotFound( "The function '{}' was not found in this contract's ABI. ".format(function_name), "Here is a list of all of the function names found: ", "{}. ".format(functions_available), @@ -1362,6 +1376,12 @@ def __getattr__(self, function_name: str) -> Any: else: return super().__getattribute__(function_name) + def __hasattr__(self, event_name: str) -> bool: + try: + return event_name in self.__dict__['_events'] + except ABIFunctionNotFound: + return False + def __call__( self, transaction: TxParams=None, block_identifier: BlockIdentifier='latest' ) -> 'ContractCaller': diff --git a/web3/exceptions.py b/web3/exceptions.py index 2241d887e3..be6c409321 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -70,6 +70,22 @@ class MismatchedABI(Exception): pass +class ABIEventFunctionNotFound(AttributeError, MismatchedABI): + """ + Raised when an attempt is made to access an event + that does not exist in the ABI. + """ + pass + + +class ABIFunctionNotFound(AttributeError, MismatchedABI): + """ + Raised when an attempt is made to access a function + that does not exist in the ABI. + """ + pass + + class FallbackNotFound(Exception): """ Raised when fallback function doesn't exist in contract.