From 8f98411145db39e5e3953b33609601a9a187a90a Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Thu, 11 Apr 2024 16:33:58 -0500 Subject: [PATCH] feat: support 0.4 --- ape_vyper/__init__.py | 3 +- ape_vyper/_utils.py | 6 ++ ape_vyper/compiler.py | 82 ++++++++++++++----- setup.py | 2 +- tests/ape-config.yaml | 2 +- .../interfaces/IFaceZeroFour.vyi | 6 ++ .../contracts/passing_contracts/zero_four.vy | 16 ++++ .../passing_contracts/zero_four_module.vy | 5 ++ 8 files changed, 98 insertions(+), 24 deletions(-) create mode 100644 ape_vyper/_utils.py create mode 100644 tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi create mode 100644 tests/contracts/passing_contracts/zero_four.vy create mode 100644 tests/contracts/passing_contracts/zero_four_module.vy diff --git a/ape_vyper/__init__.py b/ape_vyper/__init__.py index f3de0527..e73c3517 100644 --- a/ape_vyper/__init__.py +++ b/ape_vyper/__init__.py @@ -1,5 +1,6 @@ from ape import plugins +from ._utils import Extension from .compiler import VyperCompiler, VyperConfig @@ -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 diff --git a/ape_vyper/_utils.py b/ape_vyper/_utils.py new file mode 100644 index 00000000..78b4495d --- /dev/null +++ b/ape_vyper/_utils.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class Extension(Enum): + VY = ".vy" + VYI = ".vyi" diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index e7691fd0..3e12a78b 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -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 @@ -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, @@ -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]: @@ -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: @@ -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], ) ) @@ -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) @@ -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) @@ -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 {} @@ -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}" @@ -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 diff --git a/setup.py b/setup.py index 8f83010c..28891900 100644 --- a/setup.py +++ b/setup.py @@ -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, diff --git a/tests/ape-config.yaml b/tests/ape-config.yaml index 1677b3e9..23187fe4 100644 --- a/tests/ape-config.yaml +++ b/tests/ape-config.yaml @@ -7,7 +7,7 @@ dependencies: local: ./ExampleDependency vyper: - evm_version: istanbul + evm_version: london # Allows importing dependencies. import_remapping: diff --git a/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi b/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi new file mode 100644 index 00000000..ff82c46b --- /dev/null +++ b/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi @@ -0,0 +1,6 @@ +# pragma version ~=0.4.0rc1 + +@external +@view +def implementThisPlease(role: bytes32) -> bool: + ... diff --git a/tests/contracts/passing_contracts/zero_four.vy b/tests/contracts/passing_contracts/zero_four.vy new file mode 100644 index 00000000..1a84e787 --- /dev/null +++ b/tests/contracts/passing_contracts/zero_four.vy @@ -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() diff --git a/tests/contracts/passing_contracts/zero_four_module.vy b/tests/contracts/passing_contracts/zero_four_module.vy new file mode 100644 index 00000000..4cac566d --- /dev/null +++ b/tests/contracts/passing_contracts/zero_four_module.vy @@ -0,0 +1,5 @@ +# pragma version ~=0.4.0rc1 + +@internal +def moduleMethod() -> bool: + return True