Skip to content

Commit

Permalink
feat: adhoc code compile
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Oct 27, 2023
1 parent 84d25ed commit d6d9615
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 46 deletions.
14 changes: 14 additions & 0 deletions src/ape/api/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ def compile(
List[:class:`~ape.type.contract.ContractType`]
"""

@raises_not_implemented
def compile_code(self, code: str, **kwargs) -> ContractType: # type: ignore[empty-body]
"""
Compile a program.
Args:
code (str): The code to compile.
**kwargs: ContractType overrides (needed if unable to gather
required properties from source code alone, such as ``contractName``).
Returns:
``ContractType``: A compiled contract artifact.
"""

@raises_not_implemented
def get_imports( # type: ignore[empty-body]
self, contract_filepaths: List[Path], base_path: Optional[Path]
Expand Down
23 changes: 23 additions & 0 deletions src/ape/managers/compilers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import difflib
from pathlib import Path
from typing import Any, Dict, List, Optional, Set

Expand Down Expand Up @@ -179,6 +180,28 @@ def compile(self, contract_filepaths: List[Path]) -> Dict[str, ContractType]:

return contract_types_dict

def compile_source(self, compiler_name: str, code: str, **kwargs) -> ContractType:
"""
Compile the given program.
Args:
compiler_name (str): The name of the compiler to use.
code (str): The source code to compile.
Returns:
``ContractType``: A compile contract artifact.
"""
if compiler := self.get_compiler(compiler_name):
return compiler.compile_code(code, **kwargs)

names = [x.name for x in self.registered_compilers.values()]
similar = difflib.get_close_matches(compiler_name, names, cutoff=0.6)
message = f"No compiler with name '{compiler}'."
if similar:
message = f"{message} Did you mean '{', '.join(similar)}'?"

raise CompilerError(message)

def get_imports(
self, contract_filepaths: List[Path], base_path: Optional[Path] = None
) -> Dict[str, List[str]]:
Expand Down
99 changes: 53 additions & 46 deletions src/ape_pm/compiler.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import json
from pathlib import Path
from typing import Dict, List, Optional, Set
from typing import List, Optional, Set

from eth_utils import is_0x_prefixed
from ethpm_types import ContractType, HexBytes

from ape.api import CompilerAPI
from ape.exceptions import ContractLogicError
from ape.exceptions import CompilerError, ContractLogicError
from ape.logging import logger
from ape.utils import get_relative_path

Expand All @@ -26,63 +26,70 @@ def compile(
) -> List[ContractType]:
filepaths.sort() # Sort to assist in reproducing consistent results.
contract_types: List[ContractType] = []
contract_type_data: Dict
for path in filepaths:
data = json.loads(path.read_text())
source_path = (
get_relative_path(path, base_path) if base_path and path.is_absolute() else path
)
source_id = str(source_path)
if isinstance(data, list):
# ABI JSON list
contract_type_data = {"abi": data, "contractName": path.stem, "sourceId": source_id}

elif isinstance(data, dict) and (
"contractName" in data or "abi" in data or "sourceId" in data
):
# Raw contract type JSON or raw compiler output.
contract_type_data = data
if "contractName" not in contract_type_data:
contract_type_data["contractName"] = path.stem

# NOTE: Always set the source ID to the source of the JSON file
# to avoid manifest corruptions later on.
contract_type_data["sourceId"] = source_id

if (
"deploymentBytecode" not in contract_type_data
or "runtimeBytecode" not in contract_type_data
):
if "bin" in contract_type_data:
# Handle raw Solidity output.
deployment_bytecode = data["bin"]
runtime_bytecode = data["bin"]

elif (
"bytecode" in contract_type_data or "bytecode_runtime" in contract_type_data
):
# Handle raw Vyper output.
deployment_bytecode = contract_type_data.pop("bytecode", None)
runtime_bytecode = contract_type_data.pop("bytecode_runtime", None)

else:
deployment_bytecode = None
runtime_bytecode = None

if deployment_bytecode:
contract_type_data["deploymentBytecode"] = {"bytecode": deployment_bytecode}
if runtime_bytecode:
contract_type_data["runtimeBytecode"] = {"bytecode": runtime_bytecode}

else:
try:
contract_type = self.compile_code(
path.read_text(), contractName=source_path.stem, sourceId=source_id
)
except CompilerError:
logger.warning(f"Unable to parse {ContractType.__name__} from '{source_id}'.")
continue

contract_type = ContractType(**contract_type_data)
contract_types.append(contract_type)

return contract_types

def compile_code(self, code: str, **kwargs) -> ContractType:
data = json.loads(code)
if isinstance(data, list):
# ABI JSON list
contract_type_data = {"abi": data, **kwargs}

elif isinstance(data, dict) and (
"contractName" in data or "abi" in data or "sourceId" in data
):
# Raw contract type JSON or raw compiler output.
contract_type_data = data
if "contractName" not in contract_type_data:
contract_type_data["contractName"] = kwargs.get("contractName")

# NOTE: Always set the source ID to the source of the JSON file
# to avoid manifest corruptions later on.
contract_type_data["sourceId"] = kwargs.get("sourceId")

if (
"deploymentBytecode" not in contract_type_data
or "runtimeBytecode" not in contract_type_data
):
if "bin" in contract_type_data:
# Handle raw Solidity output.
deployment_bytecode = data["bin"]
runtime_bytecode = data["bin"]

elif "bytecode" in contract_type_data or "bytecode_runtime" in contract_type_data:
# Handle raw Vyper output.
deployment_bytecode = contract_type_data.pop("bytecode", None)
runtime_bytecode = contract_type_data.pop("bytecode_runtime", None)

else:
deployment_bytecode = None
runtime_bytecode = None

if deployment_bytecode:
contract_type_data["deploymentBytecode"] = {"bytecode": deployment_bytecode}
if runtime_bytecode:
contract_type_data["runtimeBytecode"] = {"bytecode": runtime_bytecode}

else:
raise CompilerError(f"Unable to parse {ContractType.__name__}.")

return ContractType(**contract_type_data)

def enrich_error(self, err: ContractLogicError) -> ContractLogicError:
if not (address := err.address) or not is_0x_prefixed(err.revert_message):
return err
Expand Down
7 changes: 7 additions & 0 deletions tests/functional/test_compilers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pathlib import Path

import pytest
from ethpm_types import ContractType

from ape.exceptions import APINotImplementedError, CompilerError
from tests.conftest import skip_if_plugin_installed
Expand Down Expand Up @@ -89,3 +90,9 @@ def test_contract_type_collision(compilers, project_with_contract, mock_compiler

finally:
compilers._registered_compilers_cache[project_with_contract.path] = existing_compilers


def test_compile_source(compilers):
code = '[{"name":"foo","type":"fallback", "stateMutability":"nonpayable"}]'
actual = compilers.compile_source("ethpm", code)
assert isinstance(actual, ContractType)

0 comments on commit d6d9615

Please sign in to comment.