Skip to content

Commit

Permalink
add not overloading test (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
sentilesdal authored Nov 16, 2023
1 parent 00c2e03 commit ebb75f4
Show file tree
Hide file tree
Showing 29 changed files with 608 additions and 24 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
run: |
python -m pip install --upgrade .[all]
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: run pytest with coverage
run: |
IN_CI=true coverage run -m pytest
Expand Down
5 changes: 5 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,11 @@ good-names=i,
k,
ex,
Run,
s,
x,
y,
z,
w3, # web3
_

# Good variable names regexes, separated by a comma. If names match any regex,
Expand Down
93 changes: 93 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Test fixture for deploying local anvil chain."""
from __future__ import annotations

import subprocess
import time
from typing import Iterator

import pytest
from eth_typing import URI
from web3 import Web3
from web3.middleware import geth_poa
from web3.types import RPCEndpoint


@pytest.fixture(scope="function")
def local_chain() -> Iterator[str]:
"""Launch a local anvil chain for testing and kill the anvil chain after.
Returns
-------
Iterator[str]
Yields the local anvil chain URI
"""
anvil_port = 9999
host = "127.0.0.1" # localhost

# Assuming anvil command is accessible in path
# running into issue with contract size without --code-size-limit arg

# Using context manager here seems to make CI hang, so explicitly killing process at the end of yield
# pylint: disable=consider-using-with
anvil_process = subprocess.Popen(
["anvil", "--silent", "--host", "127.0.0.1", "--port", str(anvil_port), "--code-size-limit", "9999999999"]
)

local_chain_ = "http://" + host + ":" + str(anvil_port)

# TODO Hack, wait for anvil chain to initialize
time.sleep(3)

yield local_chain_

# Kill anvil process at end
anvil_process.kill()


@pytest.fixture(scope="function")
def w3(local_chain) -> Web3: # pylint: disable=redefined-outer-name
"""gets a Web3 instance connected to the local chain.
Parameters
----------
local_chain : str
A local anvil chain.
Returns
-------
Web3
A web3.py instance.
"""

return initialize_web3_with_http_provider(local_chain)


def initialize_web3_with_http_provider(
ethereum_node: URI | str, request_kwargs: dict | None = None, reset_provider: bool = False
) -> Web3:
"""Initialize a Web3 instance using an HTTP provider and inject a geth Proof of Authority (poa) middleware.
Arguments
---------
ethereum_node: URI | str
Address of the http provider
request_kwargs: dict
The HTTPProvider uses the python requests library for making requests.
If you would like to modify how requests are made,
you can use the request_kwargs to do so.
Notes
-----
The geth_poa_middleware is required to connect to geth --dev or the Goerli public network.
It may also be needed for other EVM compatible blockchains like Polygon or BNB Chain (Binance Smart Chain).
See more `here <https://web3py.readthedocs.io/en/stable/middleware.html#proof-of-authority>`_.
"""
if request_kwargs is None:
request_kwargs = {}
provider = Web3.HTTPProvider(ethereum_node, request_kwargs)
web3 = Web3(provider)
web3.middleware_onion.inject(geth_poa.geth_poa_middleware, layer=0)
if reset_provider:
# TODO: Check that the user is running on anvil, raise error if not
_ = web3.provider.make_request(method=RPCEndpoint("anvil_reset"), params=[])
return web3
46 changes: 46 additions & 0 deletions pypechain/foundry/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Types for foundry-rs."""
from typing import Any, Literal, TypedDict

from web3.types import ABI


class FoundryByteCode(TypedDict):
"""Foundry"""

object: str
sourceMap: str
linkReference: Any


class FoundryDeployedByteCode(TypedDict):
"""Foundry"""

object: str
sourceMap: str
linkReference: Any


class FoundryCompiler(TypedDict):
"""Foundry"""

version: str


class FoundryMetadata(TypedDict, total=False):
"""Foundry"""

compiler: FoundryCompiler
language: Literal["Solidity", "Vyper"]


class FoundryJson(TypedDict):
"""Foundry"""

abi: ABI
bytecode: FoundryByteCode
deployedBytecode: FoundryDeployedByteCode
methodIdentifiers: dict[str, str]
rawMetadata: str
metadata: FoundryMetadata
ast: Any
id: int
15 changes: 15 additions & 0 deletions pypechain/foundry/utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Utilities for working with foundry-rs."""
from typing import TypeGuard

from pypechain.foundry.types import FoundryJson


def is_foundry_json(val: object) -> TypeGuard[FoundryJson]:
"""Determines whether a json object is a FoundryJson."""
required_keys = {"abi", "bytecode", "deployedBytecode", "methodIdentifiers", "rawMetadata", "metadata", "ast", "id"}
return isinstance(val, dict) and required_keys.issubset(val.keys())


def get_bytecode_from_foundry_json(json_abi: FoundryJson) -> str:
"""Gets the bytecode from a foundry json file."""
return json_abi.get("bytecode").get("object")
2 changes: 1 addition & 1 deletion pypechain/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ def parse_arguments(argv: Sequence[str] | None = None) -> Args:
parser.print_help(sys.stderr)
sys.exit(1)

return namespace_to_args(parser.parse_args(argv))
return namespace_to_args(parser.parse_args())


if __name__ == "__main__":
Expand Down
41 changes: 31 additions & 10 deletions pypechain/render/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

from pathlib import Path
from typing import TypedDict
from typing import Any, NamedTuple, TypedDict

from web3.types import ABI

Expand Down Expand Up @@ -51,39 +51,41 @@ def render_contract_file(contract_name: str, abi_file_path: Path) -> str:
A serialized python file.
"""
env = get_jinja_env()
base_template = env.get_template("contract.py/base.py.jinja2")
functions_template = env.get_template("contract.py/functions.py.jinja2")
abi_template = env.get_template("contract.py/abi.py.jinja2")
contract_template = env.get_template("contract.py/contract.py.jinja2")
templates = get_templates_for_contract_file(env)

# TODO: add return types to function calls

abi = load_abi_from_file(abi_file_path)
abi, bytecode = load_abi_from_file(abi_file_path)
function_datas, constructor_data = get_function_datas(abi)
has_overloading = any(len(function_data["signature_datas"]) > 1 for function_data in function_datas.values())
has_bytecode = bool(bytecode)

functions_block = functions_template.render(
functions_block = templates.functions_template.render(
abi=abi,
has_overloading=has_overloading,
contract_name=contract_name,
functions=function_datas,
# TODO: use this data to add a typed constructor
constructor=constructor_data,
)

abi_block = abi_template.render(
abi_block = templates.abi_template.render(
abi=abi,
bytecode=bytecode,
contract_name=contract_name,
)

contract_block = contract_template.render(
contract_block = templates.contract_template.render(
has_bytecode=has_bytecode,
contract_name=contract_name,
functions=function_datas,
)

# Render the template
return base_template.render(
return templates.base_template.render(
contract_name=contract_name,
has_overloading=has_overloading,
has_bytecode=has_bytecode,
functions_block=functions_block,
abi_block=abi_block,
contract_block=contract_block,
Expand All @@ -92,6 +94,25 @@ def render_contract_file(contract_name: str, abi_file_path: Path) -> str:
)


class ContractTemplates(NamedTuple):
"""Templates for the generated contract file."""

base_template: Any
functions_template: Any
abi_template: Any
contract_template: Any


def get_templates_for_contract_file(env):
"""Templates for the generated contract file."""
return ContractTemplates(
base_template=env.get_template("contract.py/base.py.jinja2"),
functions_template=env.get_template("contract.py/functions.py.jinja2"),
abi_template=env.get_template("contract.py/abi.py.jinja2"),
contract_template=env.get_template("contract.py/contract.py.jinja2"),
)


def get_function_datas(abi: ABI) -> tuple[dict[str, FunctionData], SignatureData | None]:
"""_summary_
Expand Down
52 changes: 52 additions & 0 deletions pypechain/render/contract_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,55 @@ def test_overloading(self, snapshot):

snapshot.snapshot_dir = "snapshots" # This line is optional.
snapshot.assert_match(functions_block, "expected_overloading.py")

def test_notoverloading(self, snapshot):
"""Runs the entire pipeline and checks the database at the end.
All arguments are fixtures.
"""

env = get_jinja_env()
functions_template = env.get_template("contract.py/functions.py.jinja2")

# TODO: add return types to function calls

# different names, should NOT be overloaded
abi_str = """
[
{
"constant": true,
"inputs": [],
"name": "balanceOf",
"outputs": [{"name": "", "type": "uint256"}],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{"name": "who", "type": "address"}],
"name": "balanceOfWho",
"outputs": [{"name": "", "type": "bool"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
"""

abi: ABI = json.loads(abi_str)

function_datas, constructor_data = get_function_datas(abi)
has_overloading = any(len(function_data["signature_datas"]) > 1 for function_data in function_datas.values())
contract_name = "Overloaded"

functions_block = functions_template.render(
abi=abi,
contract_name=contract_name,
functions=function_datas,
# TODO: use this data to add a typed constructor
constructor=constructor_data,
)
assert has_overloading is False

snapshot.snapshot_dir = "snapshots"
snapshot.assert_match(functions_block, "expected_not_overloading.py")
2 changes: 1 addition & 1 deletion pypechain/render/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def render_types_file(contract_name: str, abi_file_path: Path) -> str:
env = get_jinja_env()
types_template = env.get_template("types.py.jinja2")

abi = load_abi_from_file(abi_file_path)
abi, _ = load_abi_from_file(abi_file_path)

structs_by_name = get_structs_for_abi(abi)
structs_list = list(structs_by_name.values())
Expand Down
20 changes: 20 additions & 0 deletions pypechain/solc/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Types for solc."""

from typing import TypedDict

from web3.types import ABI


class SolcContract(TypedDict):
"""Foundry"""

abi: ABI
bin: str
metadata: str


class SolcJson(TypedDict):
"""Foundry"""

contracts: dict[str, SolcContract]
version: str
26 changes: 26 additions & 0 deletions pypechain/solc/utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Utilities for working with solc."""
from typing import TypeGuard

from pypechain.solc.types import SolcJson


def is_solc_json(val: object) -> TypeGuard[SolcJson]:
"""Determines whether a json object is a SolcJson."""
return (
isinstance(val, dict)
and "contracts" in val
and isinstance(val["contracts"], dict)
and all(
isinstance(contract, dict) and "abi" in contract and "bin" in contract and "metadata" in contract
for contract in val["contracts"].values()
)
and "version" in val
)


def get_bytecode_from_solc_json(json_abi: SolcJson) -> str:
"""Gets the bytecode from a foundry json file."""
# assume one contract right now
contract = list(json_abi.get("contracts").values())[0]
binary = contract.get("bin")
return f"0x{binary}"
5 changes: 4 additions & 1 deletion pypechain/templates/contract.py/abi.py.jinja2
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
{{contract_name | lower}}_abi: ABI = cast(ABI, {{abi}})
{{contract_name | lower}}_abi: ABI = cast(ABI, {{abi}})
{% if bytecode %}# pylint: disable=line-too-long
{{contract_name | lower}}_bytecode = HexStr("{{bytecode}}")
{%- endif -%}
Loading

0 comments on commit ebb75f4

Please sign in to comment.