Skip to content

Commit 8cff826

Browse files
committed
Move pytest-ethereum code to web3.tools
1 parent 309186c commit 8cff826

File tree

9 files changed

+300
-15
lines changed

9 files changed

+300
-15
lines changed

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@
7676
"requests>=2.16.0,<3.0.0",
7777
"websockets>=7.0.0,<8.0.0",
7878
"pypiwin32>=223;platform_system=='Windows'",
79+
"ipfshttpclient>=0.4.12,<1",
80+
"jsonschema>=2.6.0,<3",
81+
"protobuf>=3.0.0,<4",
7982
],
8083
setup_requires=['setuptools-markdown'],
8184
python_requires='>=3.6,<4',

tests/core/pm-module/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
from ethpm.contract import (
1818
LinkableContract,
1919
)
20-
from pytest_ethereum import (
20+
from web3.tools import (
2121
linker,
2222
)
23-
from pytest_ethereum.deployer import (
23+
from web3.tools.pytest_ethereum.deployer import (
2424
Deployer,
2525
)
2626

tests/ethpm/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from eth_utils.toolz import assoc_in
55
import pytest
6-
from pytest_ethereum import linker as l # noqa: E741
6+
from web3.tools import linker as l # noqa: E741
77
from web3 import Web3
88

99
from ethpm import ASSETS_DIR, V2_PACKAGES_DIR, Package

web3/pm.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,20 @@
3131
ASSETS_DIR,
3232
Package,
3333
)
34-
from ethpm.typing import (
34+
from eth_typing import (
3535
URI,
3636
Address,
3737
Manifest,
3838
)
39-
from ethpm.utils.backend import (
40-
resolve_uri_contents,
41-
)
42-
from ethpm.utils.ipfs import (
43-
is_ipfs_uri,
44-
)
45-
from ethpm.utils.manifest_validation import (
39+
from ethpm.validation.manifest import (
4640
validate_manifest_against_schema,
4741
validate_raw_manifest_format,
4842
)
49-
from ethpm.utils.uri import (
50-
is_valid_content_addressed_github_uri,
43+
from ethpm.uri import (
44+
is_supported_content_addressed_uri,
45+
resolve_uri_contents,
5146
)
52-
from ethpm.validation import (
47+
from ethpm.validation.package import (
5348
validate_package_name,
5449
validate_package_version,
5550
)
@@ -659,7 +654,7 @@ def get_solidity_registry_manifest() -> Dict[str, Any]:
659654

660655

661656
def validate_is_supported_manifest_uri(uri):
662-
if not is_ipfs_uri(uri) and not is_valid_content_addressed_github_uri(uri):
657+
if not is_supported_content_addressed_uri(uri):
663658
raise ManifestValidationError(
664659
f"URI: {uri} is not a valid content-addressed URI. "
665660
"Currently only IPFS and Github content-addressed URIs are supported."

web3/tools/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .pytest_ethereum import linker

web3/tools/pytest_ethereum/_utils.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from typing import Any, Dict, Iterable, List, Tuple
2+
3+
from eth_typing import URI, Address, Manifest
4+
from eth_utils import to_canonical_address, to_dict, to_hex, to_list
5+
from eth_utils.toolz import assoc, assoc_in, dissoc
6+
from ethpm import Package
7+
from ethpm.uri import check_if_chain_matches_chain_uri
8+
from web3 import Web3
9+
10+
from web3.tools.pytest_ethereum.exceptions import LinkerError
11+
12+
13+
def pluck_matching_uri(deployment_data: Dict[URI, Dict[str, str]], w3: Web3) -> URI:
14+
"""
15+
Return any blockchain uri that matches w3-connected chain, if one
16+
is present in the deployment data keys.
17+
"""
18+
for uri in deployment_data.keys():
19+
if check_if_chain_matches_chain_uri(w3, uri):
20+
return uri
21+
raise LinkerError(
22+
f"No matching blockchain URI found in deployment_data: {list(deployment_data.keys())}, "
23+
"for w3 instance: {w3.__repr__()}."
24+
)
25+
26+
27+
def contains_matching_uri(deployment_data: Dict[str, Dict[str, str]], w3: Web3) -> bool:
28+
"""
29+
Returns true if any blockchain uri in deployment data matches
30+
w3-connected chain.
31+
"""
32+
for uri in deployment_data.keys():
33+
if check_if_chain_matches_chain_uri(w3, uri):
34+
return True
35+
return False
36+
37+
38+
def insert_deployment(
39+
package: Package,
40+
deployment_name: str,
41+
deployment_data: Dict[str, str],
42+
latest_block_uri: URI,
43+
) -> Manifest:
44+
"""
45+
Returns a new manifest. If a matching chain uri is found in the old manifest, it will
46+
update the chain uri along with the new deployment data. If no match, it will simply add
47+
the new chain uri and deployment data.
48+
"""
49+
old_deployments_data = package.manifest.get("deployments")
50+
if old_deployments_data and contains_matching_uri(old_deployments_data, package.w3):
51+
old_chain_uri = pluck_matching_uri(old_deployments_data, package.w3)
52+
old_deployments_chain_data = old_deployments_data[old_chain_uri]
53+
# Replace specific on-chain deployment (i.e. deployment_name)
54+
new_deployments_chain_data_init = dissoc(
55+
old_deployments_chain_data, deployment_name
56+
)
57+
new_deployments_chain_data = {
58+
**new_deployments_chain_data_init,
59+
**{deployment_name: deployment_data},
60+
}
61+
# Replace all on-chain deployments
62+
new_deployments_data_init = dissoc(
63+
old_deployments_data, "deployments", old_chain_uri
64+
)
65+
new_deployments_data = {
66+
**new_deployments_data_init,
67+
**{latest_block_uri: new_deployments_chain_data},
68+
}
69+
return assoc(package.manifest, "deployments", new_deployments_data)
70+
71+
return assoc_in(
72+
package.manifest,
73+
("deployments", latest_block_uri, deployment_name),
74+
deployment_data,
75+
)
76+
77+
78+
@to_dict
79+
def create_deployment_data(
80+
contract_name: str,
81+
new_address: Address,
82+
tx_receipt: Dict[str, Any],
83+
link_refs: List[Dict[str, Any]] = None,
84+
) -> Iterable[Tuple[str, Any]]:
85+
yield "contract_type", contract_name
86+
yield "address", to_hex(new_address)
87+
yield "transaction", to_hex(tx_receipt.transactionHash)
88+
yield "block", to_hex(tx_receipt.blockHash)
89+
if link_refs:
90+
yield "runtime_bytecode", {"link_dependencies": create_link_dep(link_refs)}
91+
92+
93+
@to_list
94+
def create_link_dep(link_refs: List[Dict[str, Any]]) -> Iterable[Dict[str, Any]]:
95+
for link_ref in link_refs:
96+
yield {
97+
"offsets": link_ref["offsets"],
98+
"type": "reference",
99+
"value": link_ref["name"],
100+
}
101+
102+
103+
def get_deployment_address(linked_type: str, package: Package) -> Address:
104+
"""
105+
Return the address of a linked_type found in a package's manifest deployments.
106+
"""
107+
try:
108+
deployment_address = to_canonical_address(
109+
package.deployments.get(linked_type)["address"]
110+
)
111+
except KeyError:
112+
raise LinkerError(
113+
f"Package data does not contain a valid deployment of {linked_type} on the "
114+
"current w3-connected chain."
115+
)
116+
return deployment_address
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing import Any, Callable, Dict, Tuple # noqa: F401
2+
3+
from eth_typing import Address
4+
from ethpm import Package
5+
6+
from web3.tools.pytest_ethereum.exceptions import DeployerError
7+
from web3.tools.pytest_ethereum.linker import deploy, linker
8+
9+
10+
class Deployer:
11+
def __init__(self, package: Package) -> None:
12+
if not isinstance(package, Package):
13+
raise TypeError(
14+
f"Expected a Package object, instead received {type(package)}."
15+
)
16+
self.package = package
17+
self.strategies = {} # type: Dict[str, Callable[[Package], Package]]
18+
19+
def deploy(
20+
self, contract_type: str, *args: Any, **kwargs: Any
21+
) -> Tuple[Package, Address]:
22+
factory = self.package.get_contract_factory(contract_type)
23+
if contract_type in self.strategies:
24+
strategy = self.strategies[contract_type]
25+
return strategy(self.package)
26+
if factory.needs_bytecode_linking:
27+
raise DeployerError(
28+
"Unable to deploy an unlinked factory. "
29+
"Please register a strategy for this contract type."
30+
)
31+
strategy = linker(deploy(contract_type, *args, **kwargs))
32+
return strategy(self.package)
33+
34+
def register_strategy(
35+
self, contract_type: str, strategy: Callable[[Package], Package]
36+
) -> None:
37+
self.strategies[contract_type] = strategy
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class PytestEthereumError(Exception):
2+
"""
3+
Base class for all Pytest-Ethereum errors.
4+
"""
5+
6+
pass
7+
8+
9+
class DeployerError(PytestEthereumError):
10+
"""
11+
Raised when the Deployer is unable to deploy a contract type.
12+
"""
13+
14+
pass
15+
16+
17+
class LinkerError(PytestEthereumError):
18+
"""
19+
Raised when the Linker is unable to link two contract types.
20+
"""
21+
22+
pass
23+
24+
25+
class LogError(PytestEthereumError):
26+
"""
27+
Raised when the Log class is instantiated with invalid arguments.
28+
"""
29+
30+
pass

web3/tools/pytest_ethereum/linker.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import logging
2+
from typing import Any, Callable, Dict, Tuple
3+
4+
from eth_typing import Address
5+
from eth_utils import to_canonical_address, to_checksum_address, to_hex
6+
from eth_utils.toolz import assoc_in, curry, pipe
7+
from ethpm import Package
8+
from ethpm.uri import create_latest_block_uri
9+
10+
from web3.tools.pytest_ethereum._utils import (
11+
create_deployment_data,
12+
get_deployment_address,
13+
insert_deployment,
14+
)
15+
from web3.tools.pytest_ethereum.exceptions import LinkerError
16+
17+
logger = logging.getLogger("pytest_ethereum.linker")
18+
19+
20+
def linker(*args: Callable[..., Any]) -> Callable[..., Any]:
21+
return _linker(args)
22+
23+
24+
@curry
25+
def _linker(operations: Callable[..., Any], package: Package) -> Callable[..., Package]:
26+
return pipe(package, *operations)
27+
28+
29+
def deploy(
30+
contract_name: str, *args: Any, transaction: Dict[str, Any] = None
31+
) -> Callable[..., Tuple[Package, Address]]:
32+
"""
33+
Return a newly created package and contract address.
34+
Will deploy the given contract_name, if data exists in package. If
35+
a deployment is found on the current w3 instance, it will return that deployment
36+
rather than creating a new instance.
37+
"""
38+
return _deploy(contract_name, args, transaction)
39+
40+
41+
@curry
42+
def _deploy(
43+
contract_name: str, args: Any, transaction: Dict[str, Any], package: Package
44+
) -> Tuple[Package, Address]:
45+
# Deploy new instance
46+
factory = package.get_contract_factory(contract_name)
47+
if not factory.linked_references and factory.unlinked_references:
48+
raise LinkerError(
49+
f"Contract factory: {contract_name} is missing runtime link references, which are "
50+
"necessary to populate manifest deployments that have a link reference. If using the "
51+
"builder tool, use `contract_type(..., runtime_bytecode=True)`."
52+
)
53+
tx_hash = factory.constructor(*args).transact(transaction)
54+
tx_receipt = package.w3.eth.waitForTransactionReceipt(tx_hash)
55+
address = to_canonical_address(tx_receipt.contractAddress)
56+
# Create manifest copy with new deployment instance
57+
latest_block_uri = create_latest_block_uri(package.w3, 0)
58+
deployment_data = create_deployment_data(
59+
contract_name, address, tx_receipt, factory.linked_references
60+
)
61+
manifest = insert_deployment(
62+
package, contract_name, deployment_data, latest_block_uri
63+
)
64+
logger.info("%s deployed." % contract_name)
65+
return Package(manifest, package.w3)
66+
67+
68+
@curry
69+
def link(contract: str, linked_type: str, package: Package) -> Package:
70+
"""
71+
Return a new package, created with a new manifest after applying the linked type
72+
reference to the contract factory.
73+
"""
74+
deployment_address = get_deployment_address(linked_type, package)
75+
unlinked_factory = package.get_contract_factory(contract)
76+
if not unlinked_factory.needs_bytecode_linking:
77+
raise LinkerError(
78+
f"Contract factory: {unlinked_factory.__repr__()} does not need bytecode linking, "
79+
"so it is not a valid contract type for link()"
80+
)
81+
linked_factory = unlinked_factory.link_bytecode({linked_type: deployment_address})
82+
# todo replace runtime_bytecode in manifest
83+
manifest = assoc_in(
84+
package.manifest,
85+
("contract_types", contract, "deployment_bytecode", "bytecode"),
86+
to_hex(linked_factory.bytecode),
87+
)
88+
logger.info(
89+
"%s linked to %s at address %s."
90+
% (contract, linked_type, to_checksum_address(deployment_address))
91+
)
92+
return Package(manifest, package.w3)
93+
94+
95+
@curry
96+
def run_python(callback_fn: Callable[..., None], package: Package) -> Package:
97+
"""
98+
Return the unmodified package, after performing any user-defined callback function on
99+
the contracts in the package.
100+
"""
101+
callback_fn(package)
102+
logger.info("%s python function ran." % callback_fn.__name__)
103+
return package

0 commit comments

Comments
 (0)