From e300e64d834182916496183d46fb861992c6d79c Mon Sep 17 00:00:00 2001 From: Piper Merriam Date: Mon, 6 Feb 2017 15:14:15 -0700 Subject: [PATCH] Modify web3 contract API to include the extra compiler fields --- .../test_legacy_constructor_adapter.py | 90 ++++++++++ .../test_empty_object_is_falsy.py | 8 + web3/contract.py | 169 ++++++++++++++---- web3/eth.py | 37 +++- web3/utils/empty.py | 11 +- 5 files changed, 276 insertions(+), 39 deletions(-) create mode 100644 tests/contracts/test_legacy_constructor_adapter.py create mode 100644 tests/empty-object/test_empty_object_is_falsy.py diff --git a/tests/contracts/test_legacy_constructor_adapter.py b/tests/contracts/test_legacy_constructor_adapter.py new file mode 100644 index 0000000..7945949 --- /dev/null +++ b/tests/contracts/test_legacy_constructor_adapter.py @@ -0,0 +1,90 @@ +import pytest +import warnings + +from web3.contract import ( + Contract, +) + + +ABI = [ + { + "constant": False, + "inputs": [], + "name": "func_1", + "outputs": [], + "type": "function", + }, +] + +ADDRESS = '0x0000000000000000000000000000000000000000' + + +class ContactClassForTest(Contract): + web3 = True + + +@pytest.mark.parametrize( + 'args,kwargs,expected', + ( + ((ABI,), {}, {'abi': ABI}), + ((ABI,), {'abi': ABI}, TypeError), + ((ABI, ADDRESS), {}, {'abi': ABI, 'address': ADDRESS}), + ( + (ABI, ADDRESS, '0x1', '0x2', '0x3'), + {}, + {'abi': ABI, 'address': ADDRESS, 'binary': '0x1', 'binary_runtime': '0x2', 'source': '0x3'}, + ), + ( + tuple(), + {'abi': ABI, 'address': ADDRESS, 'code': '0x1', 'code_runtime': '0x2', 'source': '0x3'}, + {'abi': ABI, 'address': ADDRESS, 'binary': '0x1', 'binary_runtime': '0x2', 'source': '0x3'}, + ), + ((ABI, ADDRESS), {'abi': ABI}, TypeError), + ((ABI, ADDRESS), {'address': ADDRESS}, TypeError), + ((ADDRESS,), {}, {'address': ADDRESS}), + ) +) +def test_process_legacy_constructor_signature(args, kwargs, expected): + + if isinstance(expected, type) and issubclass(expected, Exception): + with pytest.raises(expected): + ContactClassForTest(*args, **kwargs) + return + + actual = ContactClassForTest(*args, **kwargs) + for key, value in expected.items(): + assert getattr(actual, key) == value + + +def test_deprecated_properties(): + instance = ContactClassForTest(ABI, ADDRESS, '0x1', '0x2', '0x3') + + with pytest.warns(DeprecationWarning): + instance.code + + with pytest.warns(DeprecationWarning): + instance.code_runtime + + with pytest.warns(DeprecationWarning): + instance.source + + +def test_deprecated_instantiation(): + with pytest.warns(Warning) as record: + ContactClassForTest(ADDRESS) + ContactClassForTest(address=ADDRESS) + warnings.warn(Warning('test')) + + assert len(record) == 1 + + with pytest.warns(DeprecationWarning): + ContactClassForTest() # no address + + with pytest.warns(DeprecationWarning): + ContactClassForTest(ABI, ADDRESS) # no address + + with pytest.warns(DeprecationWarning): + ContactClassForTest(ABI) # no address + + with pytest.warns(DeprecationWarning): + ContactClassForTest(code='0x1') # no address diff --git a/tests/empty-object/test_empty_object_is_falsy.py b/tests/empty-object/test_empty_object_is_falsy.py new file mode 100644 index 0000000..8db2692 --- /dev/null +++ b/tests/empty-object/test_empty_object_is_falsy.py @@ -0,0 +1,8 @@ +from web3.utils.empty import ( + empty, +) + + +def test_empty_object_is_falsy(): + assert bool(empty) is False + assert not empty diff --git a/web3/contract.py b/web3/contract.py index ff5251c..8bc5fbb 100644 --- a/web3/contract.py +++ b/web3/contract.py @@ -2,6 +2,8 @@ """ import functools +import warnings +import itertools from eth_abi import ( encode_abi, @@ -16,6 +18,9 @@ from web3.exceptions import ( BadFunctionCallOutput, ) +from web3.utils.address import ( + is_address, +) from web3.utils.abi import ( filter_by_type, filter_by_name, @@ -61,6 +66,17 @@ from web3.utils.string import ( coerce_return_to_text, force_obj_to_bytes, ) +from web3.utils.types import ( + is_array, +) + + +DEPRECATED_SIGNATURE_MESSAGE = ( + "The constructor signature for the `Contract` object has changed. " + "Please update your code to reflect the updated function signature: " + "'Contract(address)'. To construct contract classes use the " + "'Contract.factory(...)' class methog." +) class Contract(object): @@ -83,23 +99,33 @@ class Contract(object): # set during class construction web3 = None - # class properties (overridable at instance level) - _abi = None - _code = None - _code_runtime = None - _source = None - # instance level properties address = None + # class properties (overridable at instance level) + abi = None + asm = None + ast = None + + binary = None + binary_runtime = None + clone_bin = None + + dev_doc = None + interface = None + metadata = None + opcodes = None + src_map = None + src_map_runtime = None + user_doc = None + def __init__(self, - abi=empty, - address=empty, + *args, code=empty, - bytecode=empty, - runtime=empty, code_runtime=empty, - source=empty): + source=empty, + abi=empty, + address=empty): """Create a new smart contract proxy object. :param address: Contract address as 0x hex string @@ -108,51 +134,127 @@ class Contract(object): :param code_runtime: Override class level definition :param source: Override class level definition """ - if self.web3 is empty: + if self.web3 is None: raise AttributeError( 'The `Contract` class has not been initialized. Please use the ' '`web3.contract` interface to create your contract class.' ) + + arg_0, arg_1, arg_2, arg_3, arg_4 = tuple(itertools.chain( + args, + itertools.repeat(empty, 5), + ))[:5] + + if is_array(arg_0): + if abi: + raise TypeError("The 'abi' argument was found twice") + abi = arg_0 + elif is_address(arg_0): + if address: + raise TypeError("The 'address' argument was found twice") + address = arg_0 + + if is_address(arg_1): + if address: + raise TypeError("The 'address' argument was found twice") + address = arg_1 + + if arg_2: + if code: + raise TypeError("The 'code' argument was found twice") + code = arg_2 + + if arg_3: + if code_runtime: + raise TypeError("The 'code_runtime' argument was found twice") + code_runtime = arg_3 + + if arg_4: + if source: + raise TypeError("The 'source' argument was found twice") + source = arg_4 + + if any((abi, code, code_runtime, source)): + warnings.warn(DeprecationWarning( + "The arguments abi, code, code_runtime, and source have been " + "deprecated and will be removed from the Contract class " + "constructor in future releases. Update your code to use the " + "Contract.factory method." + )) + if abi is not empty: - self._abi = abi + self.abi = abi if code is not empty: - self._code = code + self.binary = code if code_runtime is not empty: - self._code_runtime = code_runtime + self.binary_runtime = code_runtime if source is not empty: self._source = source - self.address = address + if address is not empty: + self.address = address + else: + warnings.warn(DeprecationWarning( + "The address argument is now required for contract class " + "instantiation. Please update your code to reflect this change" + )) - @property - def abi(self): - if self._abi is not None: - return self._abi - # TODO: abi can be derived from the contract source. - raise AttributeError("No contract abi was specified for thes contract") + @classmethod + def factory(cls, web3, contract_name=None, **kwargs): + if contract_name is None: + contract_name = cls.__name__ + + kwargs['web3'] = web3 + + for key in kwargs: + if not hasattr(cls, key): + raise AttributeError( + "Property {0} not found on contract class. " + "`Contract.factory` only accepts keyword arguments which are " + "present on the contract class" + ) + return type(contract_name, (cls,), kwargs) + + # + # deprecated properties + # + _source = None @property def code(self): - if self._code is not None: - return self._code - # TODO: code can be derived from the contract source. + warnings.warn(DeprecationWarning( + "The `code` property has been deprecated. You should update your " + "code to access this value through `contract.binary`. The `code` " + "property will be removed in future releases" + )) + if self.binary is not None: + return self.binary raise AttributeError("No contract code was specified for thes contract") @property def code_runtime(self): - if self._code_runtime is not None: - return self._code_runtime - # TODO: runtime can be derived from the contract source. - raise AttributeError( - "No contract code_runtime was specified for thes contract" - ) + warnings.warn(DeprecationWarning( + "The `code_runtime` property has been deprecated. You should update your " + "code to access this value through `contract.binary_runtime`. The `code_runtime` " + "property will be removed in future releases" + )) + if self.binary_runtime is not None: + return self.binary_runtime + raise AttributeError("No contract code_runtime was specified for thes contract") @property def source(self): + warnings.warn(DeprecationWarning( + "The `source` property has been deprecated and will be removed in " + "future releases" + )) if self._source is not None: return self._source raise AttributeError("No contract source was specified for thes contract") + # + # Contract Methods + # @classmethod def deploy(cls, transaction=None, args=None, kwargs=None): """ @@ -794,6 +896,11 @@ def construct_contract_factory(web3, :param source: As given by solc compiler :return: Contract class (not instance) """ + warnings.warn(DeprecationWarning( + "This function has been deprecated. Please use the `Contract.factory` " + "method as this function will be removed in future releases" + )) + _dict = { 'web3': web3, 'abi': abi, diff --git a/web3/eth.py b/web3/eth.py index b36513f..a8f31fb 100644 --- a/web3/eth.py +++ b/web3/eth.py @@ -1,11 +1,16 @@ from web3 import formatters from web3.iban import Iban -from web3.contract import construct_contract_factory +from web3.contract import ( + Contract, +) from web3.utils.blocks import ( is_predefined_block_number, ) +from web3.utils.address import ( + is_address, +) from web3.utils.encoding import ( to_decimal, encode_hex, @@ -334,13 +339,33 @@ class Eth(object): "eth_uninstallFilter", [filter_id], ) - def contract(self, abi, address=None, **kwargs): - contract_class = construct_contract_factory(self.web3, abi, **kwargs) + def contract(self, + *args, + contract_name=None, + ContractFactoryClass=Contract, + **kwargs): + has_address = any(( + 'address' in kwargs, + len(args) >= 1 and is_address(args[0]), + len(args) >= 2 and is_address(args[1]), + )) - if address is None: - return contract_class + if has_address: + if 'address' in kwargs: + address = kwargs.pop('address') + elif is_address(args[0]): + address = args[0] + elif is_address(args[1]): + address = args[1] + kwargs['abi'] = args[0] + + return Contract.factory(self.web3, contract_name, **kwargs)(address) else: - return contract_class(address=address) + try: + kwargs['abi'] = args[0] + except IndexError: + pass + return Contract.factory(self.web3, contract_name, **kwargs) def getCompilers(self): return self.web3._requestManager.request_blocking("eth_getCompilers", []) diff --git a/web3/utils/empty.py b/web3/utils/empty.py index aa5ee9b..16af454 100644 --- a/web3/utils/empty.py +++ b/web3/utils/empty.py @@ -1,2 +1,9 @@ -class empty(object): - pass +class Empty(object): + def __bool__(self): + return False + + def __nonzero__(self): + return False + + +empty = Empty()