Merge pull request #155 from pipermerriam/piper/fancier-contract-objects

Add additional compiler fields to Contract object.
This commit is contained in:
Piper Merriam 2017-02-06 16:12:58 -07:00 committed by GitHub
commit 95c824044e
14 changed files with 398 additions and 135 deletions

View File

@ -8,7 +8,7 @@ Contracts
Contract Factories
------------------
.. py:class:: Contract(abi, address=None, code=None, code_runtime=None, source=None)
.. py:class:: Contract(address)
The ``Contract`` class is not intended to be used or instantiated directly.
Instead you should use the ``web3.eth.contract(...)`` method to generate
@ -35,19 +35,19 @@ Each Contract Factory exposes the following properties.
The contract ABI array.
.. py:attribute:: Contract.code
.. py:attribute:: Contract.bytecode
The contract bytecode string. May be ``None`` if not provided during
factory creation.
.. py:attribute:: Contract.code_runtime
.. py:attribute:: Contract.bytecode_runtime
The runtime part of the contract bytecode string. May be ``None`` if not
provided during factory creation.
.. py:attribute:: Contract.code_runtime
.. py:attribute:: Contract.bytecode_runtime
The runtime part of the contract bytecode string. May be ``None`` if not
provided during factory creation.

View File

@ -579,22 +579,31 @@ with the filtering API.
Contracts
---------
.. py:method:: Eth.contract(abi, address=None, code=None, code_runtime=None, source=None)
.. py:method:: Eth.contract(address=None, contract_name=None, ContractFactoryClass=Contract, **contract_factory_kwargs)
If ``address`` is provided then this method will return an instance of the
contract defined by ``abi``.
contract defined by ``abi``. Otherwise the newly created contract class
will be returned.
If ``address`` is ``None`` then this method will return a Contract Factory,
which can be though of as the python class that represents your contract.
``contract_name`` will be used as the name of the contract class. If
``None`` then the name of the ``ContractFactoryClass`` will be used.
The ``abi`` parameter should be an array containing the ABI definition of
the contract functions and events.
``ContractFactoryClass`` will be used as the base Contract class.
The ``code`` parameter should be the full contract bytecode.
The following arguments are accepted for contract class creation.
The ``code_runtime`` parameter should be the runtime part of the contract bytecode.
The ``source`` parameter should be a string containing the full source code
of the contract.
- ``abi``
- ``asm``
- ``ast``
- ``bytecode``
- ``bytecode_runtime``
- ``clone_bin``
- ``dev_doc``
- ``interface``
- ``metadata``
- ``opcodes``
- ``src_map``
- ``src_map_runtime``
- ``user_doc``
See :doc:`./contracts` for more information about how to use contracts.

View File

@ -74,8 +74,8 @@ def MATH_ABI():
def MathContract(web3, MATH_ABI, MATH_CODE, MATH_RUNTIME, MATH_SOURCE):
return web3.eth.contract(
abi=MATH_ABI,
code=MATH_CODE,
code_runtime=MATH_RUNTIME,
bytecode=MATH_CODE,
bytecode_runtime=MATH_RUNTIME,
source=MATH_SOURCE,
)
@ -114,8 +114,8 @@ def SimpleConstructorContract(web3,
SIMPLE_CONSTRUCTOR_ABI):
return web3.eth.contract(
abi=SIMPLE_CONSTRUCTOR_ABI,
code=SIMPLE_CONSTRUCTOR_CODE,
code_runtime=SIMPLE_CONSTRUCTOR_RUNTIME,
bytecode=SIMPLE_CONSTRUCTOR_CODE,
bytecode_runtime=SIMPLE_CONSTRUCTOR_RUNTIME,
source=SIMPLE_CONSTRUCTOR_SOURCE,
)
@ -155,8 +155,8 @@ def WithConstructorArgumentsContract(web3,
WITH_CONSTRUCTOR_ARGUMENTS_ABI):
return web3.eth.contract(
abi=WITH_CONSTRUCTOR_ARGUMENTS_ABI,
code=WITH_CONSTRUCTOR_ARGUMENTS_CODE,
code_runtime=WITH_CONSTRUCTOR_ARGUMENTS_RUNTIME,
bytecode=WITH_CONSTRUCTOR_ARGUMENTS_CODE,
bytecode_runtime=WITH_CONSTRUCTOR_ARGUMENTS_RUNTIME,
source=WITH_CONSTRUCTOR_ARGUMENTS_SOURCE,
)
@ -195,8 +195,8 @@ def WithConstructorAddressArgumentsContract(web3,
WITH_CONSTRUCTOR_ADDRESS_ABI):
return web3.eth.contract(
abi=WITH_CONSTRUCTOR_ADDRESS_ABI,
code=WITH_CONSTRUCTOR_ADDRESS_CODE,
code_runtime=WITH_CONSTRUCTOR_ADDRESS_RUNTIME,
bytecode=WITH_CONSTRUCTOR_ADDRESS_CODE,
bytecode_runtime=WITH_CONSTRUCTOR_ADDRESS_RUNTIME,
source=WITH_CONSTRUCTOR_ADDRESS_SOURCE,
)
@ -255,8 +255,8 @@ def STRING_ABI():
@pytest.fixture()
def STRING_CONTRACT(STRING_SOURCE, STRING_CODE, STRING_RUNTIME, STRING_ABI):
return {
'code': STRING_CODE,
'code_runtime': STRING_RUNTIME,
'bytecode': STRING_CODE,
'bytecode_runtime': STRING_RUNTIME,
'abi': STRING_ABI,
'source': STRING_SOURCE,
}
@ -382,8 +382,8 @@ def EMITTER(EMITTER_CODE,
EMITTER_ABI,
EMITTER_SOURCE):
return {
'code': EMITTER_CODE,
'code_runtime': EMITTER_RUNTIME,
'bytecode': EMITTER_CODE,
'bytecode_runtime': EMITTER_RUNTIME,
'source': EMITTER_SOURCE,
'abi': EMITTER_ABI,
}
@ -404,8 +404,8 @@ def emitter(web3_empty, Emitter, wait_for_transaction, wait_for_block):
deploy_receipt = wait_for_transaction(web3, deploy_txn_hash)
contract_address = deploy_receipt['contractAddress']
code = web3.eth.getCode(contract_address)
assert code == Emitter.code_runtime
bytecode = web3.eth.getCode(contract_address)
assert bytecode == Emitter.bytecode_runtime
return Emitter(address=contract_address)

View File

@ -7,14 +7,14 @@ def test_class_construction_sets_class_vars(web3, MATH_ABI, MATH_CODE,
MATH_RUNTIME, MATH_SOURCE):
MathContract = web3.eth.contract(
abi=MATH_ABI,
code=MATH_CODE,
code_runtime=MATH_RUNTIME,
bytecode=MATH_CODE,
bytecode_runtime=MATH_RUNTIME,
source=MATH_SOURCE,
)
assert MathContract.web3 == web3
assert MathContract.code == MATH_CODE
assert MathContract.code_runtime == MATH_RUNTIME
assert MathContract.bytecode == MATH_CODE
assert MathContract.bytecode_runtime == MATH_RUNTIME
assert MathContract.source == MATH_SOURCE

View File

@ -15,8 +15,8 @@ def math_contract(web3, MATH_ABI, MATH_CODE, MATH_RUNTIME, MATH_SOURCE,
wait_for_transaction):
MathContract = web3.eth.contract(
abi=MATH_ABI,
code=MATH_CODE,
code_runtime=MATH_RUNTIME,
bytecode=MATH_CODE,
bytecode_runtime=MATH_RUNTIME,
source=MATH_SOURCE,
)
deploy_txn = MathContract.deploy({'from': web3.eth.coinbase})

View File

@ -23,8 +23,8 @@ def emitter(web3, Emitter, wait_for_transaction, wait_for_block):
deploy_receipt = wait_for_transaction(web3, deploy_txn_hash)
contract_address = deploy_receipt['contractAddress']
code = web3.eth.getCode(contract_address)
assert code == Emitter.code_runtime
bytecode = web3.eth.getCode(contract_address)
assert bytecode == Emitter.bytecode_runtime
return Emitter(address=contract_address)

View File

@ -0,0 +1,97 @@
import pytest
import warnings
import sys
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),
{'code': '0x1', 'code_runtime': '0x2'},
{'abi': ABI, 'address': ADDRESS, 'bytecode': '0x1', 'bytecode_runtime': '0x2'}),
(
(ABI, ADDRESS, '0x1', '0x2', '0x3'),
{},
{'abi': ABI, 'address': ADDRESS, 'bytecode': '0x1', 'bytecode_runtime': '0x2', 'source': '0x3'},
),
(
tuple(),
{'abi': ABI, 'address': ADDRESS, 'code': '0x1', 'code_runtime': '0x2', 'source': '0x3'},
{'abi': ABI, 'address': ADDRESS, 'bytecode': '0x1', 'bytecode_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
@pytest.mark.skipif(sys.version_info.major == 2, reason="Python2 fails weirdly on this test")
def test_deprecated_properties():
instance = ContactClassForTest(ABI, ADDRESS, '0x1', '0x2', source='0x3')
with pytest.warns(DeprecationWarning):
instance.source
with pytest.warns(DeprecationWarning):
instance.code
with pytest.warns(DeprecationWarning):
instance.code_runtime
@pytest.mark.skipif(sys.version_info.major == 2, reason="Python2 fails weirdly on this test")
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

View File

@ -0,0 +1,8 @@
from web3.utils.empty import (
empty,
)
def test_empty_object_is_falsy():
assert bool(empty) is False
assert not empty

View File

@ -15,8 +15,8 @@ def math_contract(web3, MATH_ABI, MATH_CODE, MATH_RUNTIME, MATH_SOURCE,
wait_for_transaction):
MathContract = web3.eth.contract(
abi=MATH_ABI,
code=MATH_CODE,
code_runtime=MATH_RUNTIME,
bytecode=MATH_CODE,
bytecode_runtime=MATH_RUNTIME,
source=MATH_SOURCE,
)
deploy_txn = MathContract.deploy({'from': web3.eth.coinbase})

View File

@ -123,8 +123,8 @@ def EMITTER(EMITTER_CODE,
EMITTER_ABI,
EMITTER_SOURCE):
return {
'code': EMITTER_CODE,
'code_runtime': EMITTER_RUNTIME,
'bytecode': EMITTER_CODE,
'bytecode_runtime': EMITTER_RUNTIME,
'source': EMITTER_SOURCE,
'abi': EMITTER_ABI,
}
@ -142,8 +142,8 @@ def emitter(web3, Emitter, wait_for_transaction, wait_for_block):
deploy_receipt = wait_for_transaction(web3, deploy_txn_hash)
contract_address = deploy_receipt['contractAddress']
code = web3.eth.getCode(contract_address)
assert code == Emitter.code_runtime
bytecode = web3.eth.getCode(contract_address)
assert bytecode == Emitter.bytecode_runtime
return Emitter(address=contract_address)

View File

@ -2,6 +2,8 @@
"""
import functools
import warnings
import itertools
from eth_abi import (
encode_abi,
@ -16,23 +18,8 @@ from web3.exceptions import (
BadFunctionCallOutput,
)
from web3.utils.encoding import (
encode_hex,
)
from web3.utils.exception import (
raise_from,
)
from web3.utils.formatting import (
add_0x_prefix,
remove_0x_prefix,
)
from web3.utils.string import (
force_bytes,
coerce_return_to_text,
force_obj_to_bytes,
)
from web3.utils.functional import (
compose,
from web3.utils.address import (
is_address,
)
from web3.utils.abi import (
filter_by_type,
@ -51,13 +38,45 @@ from web3.utils.abi import (
from web3.utils.decorators import (
combomethod,
)
from web3.utils.empty import (
empty,
)
from web3.utils.encoding import (
encode_hex,
)
from web3.utils.events import (
get_event_data,
)
from web3.utils.exception import (
raise_from,
)
from web3.utils.filters import (
construct_event_filter_params,
PastLogFilter,
)
from web3.utils.formatting import (
add_0x_prefix,
remove_0x_prefix,
)
from web3.utils.functional import (
compose,
)
from web3.utils.string import (
force_bytes,
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):
@ -80,74 +99,160 @@ 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
bytecode = None
bytecode_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=None,
address=None,
code=None,
code_runtime=None,
source=None):
*args,
**kwargs):
"""Create a new smart contract proxy object.
:param address: Contract address as 0x hex string
:param abi: Override class level definition
:param code: Override class level definition
:param code_runtime: Override class level definition
:param source: Override class level definition
"""
code = kwargs.pop('code', empty)
code_runtime = kwargs.pop('code_runtime', empty)
source = kwargs.pop('source', empty)
abi = kwargs.pop('abi', empty)
address = kwargs.pop('address', 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.'
)
if abi is not None:
self._abi = abi
if code is not None:
self._code = code
if code_runtime is not None:
self._code_runtime = code_runtime
if source is not None:
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 is not empty:
if code:
raise TypeError("The 'code' argument was found twice")
code = arg_2
if arg_3 is not empty:
if code_runtime:
raise TypeError("The 'code_runtime' argument was found twice")
code_runtime = arg_3
if arg_4 is not empty:
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
if code is not empty:
self.bytecode = code
if code_runtime is not empty:
self.bytecode_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.bytecode`. The `code` "
"property will be removed in future releases"
))
if self.bytecode is not None:
return self.bytecode
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.bytecode_runtime`. The `code_runtime` "
"property will be removed in future releases"
))
if self.bytecode_runtime is not None:
return self.bytecode_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):
"""
@ -180,9 +285,9 @@ class Contract(object):
else:
deploy_transaction = dict(**transaction)
if not cls.code:
if not cls.bytecode:
raise ValueError(
"Cannot deploy a contract that does not have 'code' associated "
"Cannot deploy a contract that does not have 'bytecode' associated "
"with it"
)
@ -559,6 +664,8 @@ class Contract(object):
transaction=None):
"""
Returns a dictionary of the transaction that could be used to call this
TODO: make this a public API
TODO: add new prepare_deploy_transaction API
"""
if transaction is None:
prepared_transaction = {}
@ -631,10 +738,10 @@ class Contract(object):
arguments = merge_args_and_kwargs(constructor_abi, args, kwargs)
deploy_data = add_0x_prefix(
cls._encode_abi(constructor_abi, arguments, data=cls.code)
cls._encode_abi(constructor_abi, arguments, data=cls.bytecode)
)
else:
deploy_data = add_0x_prefix(cls.code)
deploy_data = add_0x_prefix(cls.bytecode)
return deploy_data
@ -787,6 +894,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,

View File

@ -1,32 +1,38 @@
from web3 import formatters
from web3.iban import Iban
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,
)
from web3.utils.types import (
is_integer,
is_string,
)
from web3.utils.string import (
coerce_return_to_text,
)
from web3.utils.functional import (
apply_formatters_to_return,
)
from web3.utils.transactions import (
get_buffered_gas_estimate,
)
from web3.utils.filters import (
BlockFilter,
TransactionFilter,
LogFilter,
)
from web3.utils.blocks import (
is_predefined_block_number,
from web3.utils.functional import (
apply_formatters_to_return,
)
from web3.utils.string import (
coerce_return_to_text,
)
from web3.utils.transactions import (
get_buffered_gas_estimate,
)
from web3.utils.types import (
is_integer,
is_string,
)
from web3.contract import construct_contract_factory
class Eth(object):
@ -42,6 +48,7 @@ class Eth(object):
def defaultAccount(self):
if self._defaultAccount is not None:
return self._defaultAccount
# TODO: deprecate defaulting to the coinbase for the from address.
return self.coinbase
@defaultAccount.setter
@ -332,13 +339,34 @@ 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,
**kwargs):
ContractFactoryClass = kwargs.pop('ContractFactoryClass', Contract)
contract_name = kwargs.pop('contract_name', None)
if address is None:
return contract_class
has_address = any((
'address' in kwargs,
len(args) >= 1 and is_address(args[0]),
len(args) >= 2 and is_address(args[1]),
))
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 ContractFactoryClass.factory(self.web3, contract_name, **kwargs)(address)
else:
return contract_class(address=address)
try:
kwargs['abi'] = args[0]
except IndexError:
pass
return ContractFactoryClass.factory(self.web3, contract_name, **kwargs)
def getCompilers(self):
return self.web3._requestManager.request_blocking("eth_getCompilers", [])

View File

@ -31,12 +31,14 @@ from web3.providers.manager import (
PrivateKeySigningManager,
)
from web3.utils.functional import (
compose,
from web3.utils.address import (
is_address,
is_checksum_address,
to_checksum_address,
)
from web3.utils.string import (
force_text,
coerce_return_to_text,
from web3.utils.currency import (
to_wei,
from_wei,
)
from web3.utils.encoding import (
to_hex,
@ -45,14 +47,12 @@ from web3.utils.encoding import (
to_decimal,
from_decimal,
)
from web3.utils.currency import (
to_wei,
from_wei,
from web3.utils.functional import (
compose,
)
from web3.utils.address import (
is_address,
is_checksum_address,
to_checksum_address,
from web3.utils.string import (
force_text,
coerce_return_to_text,
)

9
web3/utils/empty.py Normal file
View File

@ -0,0 +1,9 @@
class Empty(object):
def __bool__(self):
return False
def __nonzero__(self):
return False
empty = Empty()