Skip to content

Commit

Permalink
feat: support 0.4
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Apr 11, 2024
1 parent 06325c7 commit 8f98411
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 24 deletions.
3 changes: 2 additions & 1 deletion ape_vyper/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ape import plugins

from ._utils import Extension
from .compiler import VyperCompiler, VyperConfig


Expand All @@ -10,4 +11,4 @@ def config_class():

@plugins.register(plugins.CompilerPlugin)
def register_compiler():
return (".vy",), VyperCompiler
return tuple([e.value for e in Extension]), VyperCompiler
6 changes: 6 additions & 0 deletions ape_vyper/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from enum import Enum


class Extension(Enum):
VY = ".vy"
VYI = ".vyi"
82 changes: 61 additions & 21 deletions ape_vyper/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ape.logging import logger
from ape.types import ContractSourceCoverage, ContractType, SourceTraceback, TraceFrame
from ape.utils import GithubClient, cached_property, get_relative_path, pragma_str_to_specifier_set
from ape.utils.os import get_full_extension
from eth_pydantic_types import HexBytes
from eth_utils import is_0x_prefixed
from ethpm_types import ASTNode, PackageManifest, PCMap, SourceMapItem
Expand All @@ -28,6 +29,7 @@
from vvm import compile_standard as vvm_compile_standard
from vvm.exceptions import VyperError # type: ignore

from ape_vyper._utils import Extension
from ape_vyper.ast import source_to_abi
from ape_vyper.exceptions import (
RUNTIME_ERROR_MAP,
Expand Down Expand Up @@ -221,22 +223,34 @@ def get_imports(
for line in content:
if line.startswith("import "):
import_line_parts = line.replace("import ", "").split(" ")
suffix = import_line_parts[0].strip().replace(".", os.path.sep)
prefix = import_line_parts[0].replace(".", os.path.sep)

elif line.startswith("from ") and " import " in line:
import_line_parts = line.replace("from ", "").split(" ")
import_line_parts = line.replace("from ", "").strip().split(" ")
module_name = import_line_parts[0].strip().replace(".", os.path.sep)
suffix = os.path.sep.join([module_name, import_line_parts[2].strip()])
prefix = os.path.sep.join([module_name, import_line_parts[2].strip()])

else:
# Not an import line
continue

while f"{os.path.sep}{os.path.sep}" in prefix:
prefix = prefix.replace(f"{os.path.sep}{os.path.sep}", os.path.sep)

prefix = prefix.lstrip(os.path.sep)

# NOTE: Defaults to JSON (assuming from input JSON or a local JSON),
# unless a Vyper file exists.
ext = "vy" if (base_path / f"{suffix}.vy").is_file() else "json"
if (base_path / f"{prefix}{Extension.VY.value}").is_file():
ext = Extension.VY.value
elif (base_path / f"{prefix}{Extension.VY.value}").is_file():
ext = Extension.VY.value
elif (base_path / f"{prefix}{Extension.VYI.value}").is_file():
ext = Extension.VYI.value
else:
ext = ".json"

import_source_id = f"{suffix}.{ext}"
import_source_id = f"{prefix}{ext}"
if source_id not in import_map:
import_map[source_id] = [import_source_id]
elif import_source_id not in import_map[source_id]:
Expand Down Expand Up @@ -404,22 +418,28 @@ def compile(
) -> List[ContractType]:
contract_types = []
base_path = base_path or self.config_manager.contracts_folder
sources = [p for p in contract_filepaths if p.parent.name != "interfaces"]
version_map = self.get_version_map(sources)
version_map = self.get_version_map(contract_filepaths)
compiler_data = self._get_compiler_arguments(version_map, base_path)
all_settings = self.get_compiler_settings(sources, base_path=base_path)
all_settings = self.get_compiler_settings(contract_filepaths, base_path=base_path)
contract_versions: Dict[str, Tuple[Version, str]] = {}

for vyper_version, version_settings in all_settings.items():
for settings_key, settings in version_settings.items():
source_ids = settings["outputSelection"]
optimization_paths = {p: base_path / p for p in source_ids}
if vyper_version >= Version("0.4.0rc1"):
# Vyper 0.4.0 seems to require absolute paths.
src_dict = {
p: {"content": Path(p).read_text()} for p in settings["outputSelection"]
}
else:
src_dict = {
s: {"content": p.read_text()}
for s, p in {p: base_path / p for p in settings["outputSelection"]}.items()
}

input_json = {
"language": "Vyper",
"settings": settings,
"sources": {
s: {"content": p.read_text()} for s, p in optimization_paths.items()
},
"sources": src_dict,
}

if interfaces := self.import_remapping:
Expand Down Expand Up @@ -582,7 +602,7 @@ def _flatten_source(
imports = list(
filter(
lambda x: not x.startswith("vyper/"),
[y for x in self.get_imports([path], base_path).values() for y in x],
[y for x in self.get_imports((path,), base_path=base_path).values() for y in x],
)
)

Expand Down Expand Up @@ -670,16 +690,20 @@ def flatten_contract(self, path: Path, base_path: Optional[Path] = None) -> Cont
def get_version_map(
self, contract_filepaths: Sequence[Path], base_path: Optional[Path] = None
) -> Dict[Version, Set[Path]]:
contracts_path = base_path or self.project_manager.contracts_folder
version_map: Dict[Version, Set[Path]] = {}
source_path_by_version_spec: Dict[SpecifierSet, Set[Path]] = {}
source_paths_without_pragma = set()

# Sort contract_filepaths to promote consistent, reproduce-able behavior
for path in sorted(contract_filepaths):
import_map = self.get_imports((path,), base_path=contracts_path)
imports = [contracts_path / p for imps in import_map.values() for p in imps]

if config_spec := self.config_version_pragma:
_safe_append(source_path_by_version_spec, config_spec, path)
_safe_append(source_path_by_version_spec, config_spec, {path, *imports})
elif pragma := get_version_pragma_spec(path):
_safe_append(source_path_by_version_spec, pragma, path)
_safe_append(source_path_by_version_spec, pragma, {path, *imports})
else:
source_paths_without_pragma.add(path)

Expand Down Expand Up @@ -722,8 +746,11 @@ def get_version_map(

# Handle no-pragma sources
if source_paths_without_pragma:
# NOTE: Don't use max-rc version by default - those should be explicit.
max_installed_vyper_version = (
max(version_map) if version_map else max(self.installed_versions)
max(version_map)
if version_map
else max([v for v in self.installed_versions if not v.pre])
)
_safe_append(version_map, max_installed_vyper_version, source_paths_without_pragma)

Expand All @@ -732,8 +759,15 @@ def get_version_map(
def get_compiler_settings(
self, contract_filepaths: Sequence[Path], base_path: Optional[Path] = None
) -> Dict[Version, Dict]:
valid_paths = [p for p in contract_filepaths if p.suffix == ".vy"]
contracts_path = base_path or self.config_manager.contracts_folder
# NOTE: Interfaces cannot be in the outputSelection
# (but are required in `sources`).
valid_paths = [
p
for p in contract_filepaths
if get_full_extension(p) == Extension.VY
and not str(p).startswith(str(contracts_path / "interfaces"))
]
files_by_vyper_version = self.get_version_map(valid_paths, base_path=contracts_path)
if not files_by_vyper_version:
return {}
Expand Down Expand Up @@ -767,9 +801,15 @@ def get_compiler_settings(
elif optimization == "false":
optimization = False

if version >= Version("0.4.0rc1"):
# Vyper 0.4.0 seems to require absolute paths.
selection_dict = {(contracts_path / s).as_posix(): ["*"] for s in selection}
else:
selection_dict = {s: ["*"] for s in selection}

version_settings[settings_key] = {
"optimize": optimization,
"outputSelection": {s: ["*"] for s in selection},
"outputSelection": selection_dict,
}
if evm_version and evm_version not in ("none", "null"):
version_settings[settings_key]["evmVersion"] = f"{evm_version}"
Expand Down Expand Up @@ -1041,8 +1081,8 @@ def _get_traceback(
start_depth = frame.depth
called_contract, sub_calldata = self._create_contract_from_call(frame)
if called_contract:
ext = Path(called_contract.source_id).suffix
if ext.endswith(".vy"):
ext = get_full_extension(Path(called_contract.source_id))
if ext in [e.value for e in Extension]:
# Called another Vyper contract.
sub_trace = self._get_traceback(
called_contract, trace, sub_calldata, previous_depth=frame.depth
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"ethpm-types", # Use same version as eth-ape
"tqdm", # Use same version as eth-ape
"vvm>=0.2.0,<0.3",
"vyper~=0.3.7",
"vyper>=0.3.7,<0.5",
],
python_requires=">=3.8,<4",
extras_require=extras_require,
Expand Down
2 changes: 1 addition & 1 deletion tests/ape-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dependencies:
local: ./ExampleDependency

vyper:
evm_version: istanbul
evm_version: london

# Allows importing dependencies.
import_remapping:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# pragma version ~=0.4.0rc1

@external
@view
def implementThisPlease(role: bytes32) -> bool:
...
16 changes: 16 additions & 0 deletions tests/contracts/passing_contracts/zero_four.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# pragma version ~=0.4.0rc1

import interfaces.IFaceZeroFour as IFaceZeroFour
implements: IFaceZeroFour

from . import zero_four_module as zero_four_module

@external
@view
def implementThisPlease(role: bytes32) -> bool:
return True


@external
def callModuleFunction(role: bytes32) -> bool:
return zero_four_module.moduleMethod()
5 changes: 5 additions & 0 deletions tests/contracts/passing_contracts/zero_four_module.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# pragma version ~=0.4.0rc1

@internal
def moduleMethod() -> bool:
return True

0 comments on commit 8f98411

Please sign in to comment.