From 9982f1326c10b20c22d03649836ebbbe27d89557 Mon Sep 17 00:00:00 2001 From: John Ward Date: Mon, 2 Mar 2020 14:12:24 -0600 Subject: [PATCH 1/2] Implement hasattr for contract events --- newsfragments/1560.bugfix.rst | 9 +++++++ .../contracts/test_extracting_event_data.py | 24 +++++++++++++++++++ web3/contract.py | 9 ++++++- web3/exceptions.py | 8 +++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 newsfragments/1560.bugfix.rst diff --git a/newsfragments/1560.bugfix.rst b/newsfragments/1560.bugfix.rst new file mode 100644 index 0000000000..f059e9bf0d --- /dev/null +++ b/newsfragments/1560.bugfix.rst @@ -0,0 +1,9 @@ +Fixed hasattr overloader method in the web3.ContractEvent class 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 an Exception class called 'ABIEventFunctionNotFound', which inherits from both AttributeError and +MismatchedABI, and replaced the MismatchedABI raise with a raise to the created class in the __getattr__ +overloader method of the web3.ContractEvent object. + +Created the test scripts 'test_contract_events_hasattr.py' and 'test_contract_events_getattr.py' \ No newline at end of file diff --git a/tests/core/contracts/test_extracting_event_data.py b/tests/core/contracts/test_extracting_event_data.py index fd1a8de286..9f49186525 100644 --- a/tests/core/contracts/test_extracting_event_data.py +++ b/tests/core/contracts/test_extracting_event_data.py @@ -12,6 +12,7 @@ get_event_data, ) from web3.exceptions import ( + ABIEventFunctionNotFound, LogTopicError, ValidationError, ) @@ -113,6 +114,29 @@ def dup_txn_receipt( return wait_for_transaction(web3, dup_txn_hash) +@pytest.fixture() +def single_event_abi(): + return '''[{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"event"}]''' # noqa: E501 + + +def test_contract_event_getattr(web3, single_event_abi): + contract = web3.eth.contract(abi=single_event_abi) + assert getattr(contract.events, "Increased") + + +def test_contract_event_getattr_raises_error(web3, single_event_abi): + contract = web3.eth.contract(abi=single_event_abi) + + with pytest.raises(ABIEventFunctionNotFound): + getattr(contract.events, "Decreased") + + +def test_contract_event_hasattr(web3, single_event_abi): + contract = web3.eth.contract(abi=single_event_abi) + assert hasattr(contract.events, "Increased") is True + assert hasattr(contract.events, "Decreased") is False + + @pytest.mark.parametrize( 'contract_fn,event_name,call_args,expected_args', ( diff --git a/web3/contract.py b/web3/contract.py index d6c90fa58b..f4890c22bc 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -110,6 +110,7 @@ MutableAttributeDict, ) from web3.exceptions import ( + ABIEventFunctionNotFound, BadFunctionCallOutput, BlockNumberOutofRange, FallbackNotFound, @@ -239,7 +240,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 +258,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. diff --git a/web3/exceptions.py b/web3/exceptions.py index 2241d887e3..b591e78be2 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -70,6 +70,14 @@ 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 FallbackNotFound(Exception): """ Raised when fallback function doesn't exist in contract. From 518d5fc082aadb6f3733bdc885d2ae1e5626f3eb Mon Sep 17 00:00:00 2001 From: Keri Date: Tue, 31 Mar 2020 11:49:25 -0600 Subject: [PATCH 2/2] Add hasattr to ContractFunction and ContractCaller --- newsfragments/1560.bugfix.rst | 9 ---- newsfragments/1594.bugfix.rst | 9 ++++ .../contracts/test_contract_attributes.py | 48 +++++++++++++++++++ .../contracts/test_extracting_event_data.py | 24 ---------- web3/contract.py | 17 ++++++- web3/exceptions.py | 8 ++++ 6 files changed, 80 insertions(+), 35 deletions(-) delete mode 100644 newsfragments/1560.bugfix.rst create mode 100644 newsfragments/1594.bugfix.rst create mode 100644 tests/core/contracts/test_contract_attributes.py diff --git a/newsfragments/1560.bugfix.rst b/newsfragments/1560.bugfix.rst deleted file mode 100644 index f059e9bf0d..0000000000 --- a/newsfragments/1560.bugfix.rst +++ /dev/null @@ -1,9 +0,0 @@ -Fixed hasattr overloader method in the web3.ContractEvent class 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 an Exception class called 'ABIEventFunctionNotFound', which inherits from both AttributeError and -MismatchedABI, and replaced the MismatchedABI raise with a raise to the created class in the __getattr__ -overloader method of the web3.ContractEvent object. - -Created the test scripts 'test_contract_events_hasattr.py' and 'test_contract_events_getattr.py' \ No newline at end of file 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/tests/core/contracts/test_extracting_event_data.py b/tests/core/contracts/test_extracting_event_data.py index 9f49186525..fd1a8de286 100644 --- a/tests/core/contracts/test_extracting_event_data.py +++ b/tests/core/contracts/test_extracting_event_data.py @@ -12,7 +12,6 @@ get_event_data, ) from web3.exceptions import ( - ABIEventFunctionNotFound, LogTopicError, ValidationError, ) @@ -114,29 +113,6 @@ def dup_txn_receipt( return wait_for_transaction(web3, dup_txn_hash) -@pytest.fixture() -def single_event_abi(): - return '''[{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"Increased","type":"event"}]''' # noqa: E501 - - -def test_contract_event_getattr(web3, single_event_abi): - contract = web3.eth.contract(abi=single_event_abi) - assert getattr(contract.events, "Increased") - - -def test_contract_event_getattr_raises_error(web3, single_event_abi): - contract = web3.eth.contract(abi=single_event_abi) - - with pytest.raises(ABIEventFunctionNotFound): - getattr(contract.events, "Decreased") - - -def test_contract_event_hasattr(web3, single_event_abi): - contract = web3.eth.contract(abi=single_event_abi) - assert hasattr(contract.events, "Increased") is True - assert hasattr(contract.events, "Decreased") is False - - @pytest.mark.parametrize( 'contract_fn,event_name,call_args,expected_args', ( diff --git a/web3/contract.py b/web3/contract.py index f4890c22bc..5d9bdd9182 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -111,6 +111,7 @@ ) from web3.exceptions import ( ABIEventFunctionNotFound, + ABIFunctionNotFound, BadFunctionCallOutput, BlockNumberOutofRange, FallbackNotFound, @@ -186,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?" ) @@ -196,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 @@ -1360,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), @@ -1369,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 b591e78be2..be6c409321 100644 --- a/web3/exceptions.py +++ b/web3/exceptions.py @@ -78,6 +78,14 @@ class ABIEventFunctionNotFound(AttributeError, MismatchedABI): 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.