diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 395f61ed..ee91cd8d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -66,10 +66,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] # eventually add `windows-latest` - python-version: [3.9, "3.10", "3.11", "3.12"] - - env: - GETH_VERSION: 1.12.0 + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -84,23 +81,15 @@ jobs: with: go-version: '^1.20.1' - - name: Cache Geth - id: cache-geth - uses: actions/cache@v3 + - name: Setup Go + uses: actions/setup-go@v5 with: - path: $HOME/.local/bin - key: ${{ runner.os }}-geth-${{ env.GETH_VERSION }} + go-version: '^1.20.7' - name: Install Geth - if: steps.cache-geth.outputs.cache-hit != 'true' - run: | - mkdir -p $HOME/.local/bin - wget -O geth.tar.gz "https://github.com/ethereum/go-ethereum/archive/v$GETH_VERSION.tar.gz" - tar -zxvf geth.tar.gz - cd go-ethereum-$GETH_VERSION - make geth - cp ./build/bin/geth /usr/local/bin - geth version + uses: gacts/install-geth-tools@v1 + with: + version: 1.14.5 - name: Install Dependencies run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 47442f7b..6292b9c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,12 +16,12 @@ repos: name: black - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: [types-setuptools, pydantic==1.10.4] diff --git a/README.md b/README.md index 5b548838..9d3ae00d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Ape compiler plugin around [VVM](https://github.com/vyperlang/vvm) ## Dependencies -- [python3](https://www.python.org/downloads) version 3.9 up to 3.12. +- [python3](https://www.python.org/downloads) version 3.10 up to 3.12. ## Installation diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 5aaf566c..03855f31 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -9,12 +9,13 @@ from fnmatch import fnmatch from importlib import import_module from pathlib import Path +from site import getsitepackages from typing import Any, Optional, Union, cast import vvm # type: ignore from ape.api import PluginConfig, TraceAPI from ape.api.compiler import CompilerAPI -from ape.exceptions import ContractLogicError +from ape.exceptions import ContractLogicError, ProjectError from ape.logging import logger from ape.managers.project import LocalProject, ProjectManager from ape.types import ContractSourceCoverage, ContractType, SourceTraceback @@ -73,7 +74,7 @@ "0.3.8": "shanghai", "0.3.9": "shanghai", "0.3.10": "shanghai", - "0.4.0rc6": "shanghai", + "0.4.0": "shanghai", } @@ -179,7 +180,7 @@ def get_version_pragma_spec(source: Union[str, Path]) -> Optional[SpecifierSet]: r"(?:\n|^)\s*#\s*pragma\s+version\s*([^\n]*)", ) - source_str = source if isinstance(source, str) else source.read_text() + source_str = source if isinstance(source, str) else source.read_text(encoding="utf8") for pattern in _version_pragma_patterns: for match in re.finditer(pattern, source_str): raw_pragma = match.groups()[0] @@ -210,7 +211,7 @@ def get_optimization_pragma(source: Union[str, Path]) -> Optional[str]: elif not source.is_file(): return None else: - source_str = source.read_text() + source_str = source.read_text(encoding="utf8") if pragma_match := next( re.finditer(r"(?:\n|^)\s*#pragma\s+optimize\s+([^\n]*)", source_str), None @@ -235,7 +236,7 @@ def get_evmversion_pragma(source: Union[str, Path]) -> Optional[str]: elif not source.is_file(): return None else: - source_str = source.read_text() + source_str = source.read_text(encoding="utf8") if pragma_match := next( re.finditer(r"(?:\n|^)\s*#pragma\s+evm-version\s+([^\n]*)", source_str), None @@ -251,7 +252,7 @@ def get_optimization_pragma_map( pragma_map: dict[str, Optimization] = {} for path in contract_filepaths: - pragma = get_optimization_pragma(path) or True + pragma = get_optimization_pragma(path) or "codesize" source_id = str(get_relative_path(path.absolute(), base_path.absolute())) pragma_map[source_id] = pragma @@ -293,6 +294,11 @@ def _get_imports( handled: Optional[set[str]] = None, ): pm = project or self.local_project + + # When compiling projects outside the cwd, we must + # use absolute paths. + use_absolute_paths = pm.path != Path.cwd() + import_map: defaultdict = defaultdict(list) handled = handled or set() dependencies = self.get_dependencies(project=pm) @@ -300,8 +306,12 @@ def _get_imports( if not path.is_file(): continue - content = path.read_text().splitlines() - source_id = str(get_relative_path(path.absolute(), pm.path.absolute())) + content = path.read_text(encoding="utf8").splitlines() + source_id = ( + str(path.absolute()) + if use_absolute_paths + else str(get_relative_path(path.absolute(), pm.path.absolute())) + ) handled.add(source_id) for line in content: if line.startswith("import "): @@ -348,9 +358,9 @@ def _get_imports( else: ext = ".json" dep_key = prefix.split(os.path.sep)[0] + dependency_name = prefix.split(os.path.sep)[0] + filestem = prefix.replace(f"{dependency_name}{os.path.sep}", "") if dep_key in dependencies: - dependency_name = prefix.split(os.path.sep)[0] - filestem = prefix.replace(f"{dependency_name}{os.path.sep}", "") for version_str, dep_project in pm.dependencies[dependency_name].items(): dependency = pm.dependencies.get_dependency( dependency_name, version_str @@ -377,6 +387,21 @@ def _get_imports( is_local = False break + else: + # Attempt looking up dependency from site-packages. + if res := self._lookup_source_from_site_packages(dependency_name, filestem): + source_path, imported_project = res + import_source_id = str(source_path) + # Also include imports of imports. + sub_imports = self._get_imports( + (source_path,), + project=imported_project, + handled=handled, + ) + for sub_import_ls in sub_imports.values(): + import_map[source_id].extend(sub_import_ls) + + is_local = False if is_local: import_source_id = f"{local_prefix}{ext}" @@ -387,11 +412,85 @@ def _get_imports( for sub_import_ls in sub_imports.values(): import_map[source_id].extend(sub_import_ls) + if use_absolute_paths: + import_source_id = str(full_path) + if import_source_id and import_source_id not in import_map[source_id]: import_map[source_id].append(import_source_id) return dict(import_map) + def _lookup_source_from_site_packages( + self, + dependency_name: str, + filestem: str, + config_override: Optional[dict] = None, + ) -> Optional[tuple[Path, ProjectManager]]: + # Attempt looking up dependency from site-packages. + config_override = config_override or {} + if "contracts_folder" not in config_override: + # Default to looking through the whole release for + # contracts. Most often, Python-based dependencies publish + # only their contracts this way, and we are only looking + # for sources so accurate project configuration is not required. + config_override["contracts_folder"] = "." + + try: + imported_project = ProjectManager.from_python_library( + dependency_name, + config_override=config_override, + ) + except ProjectError as err: + # Still attempt to let Vyper handle this during compilation. + logger.error( + f"'{dependency_name}' may not be installed. " + "Could not find it in Ape dependencies or Python's site-packages. " + f"Error: {err}" + ) + else: + extensions = [*[f"{t}" for t in FileType], ".json"] + + def seek() -> Optional[Path]: + for ext in extensions: + try_source_id = f"{filestem}{ext}" + if source_path := imported_project.sources.lookup(try_source_id): + return source_path + + return None + + if res := seek(): + return res, imported_project + + # Still not found. Try again without contracts_folder set. + # This will attempt to use Ape's contracts_folder detection system. + # However, I am not sure this situation occurs, as Vyper-python + # based dependencies are new at the time of writing this. + new_override = config_override or {} + if "contracts_folder" in new_override: + del new_override["contracts_folder"] + + imported_project.reconfigure(**new_override) + if res := seek(): + return res, imported_project + + # Still not found. Log a very helpful message. + existing_filestems = [f.stem for f in imported_project.path.iterdir()] + fs_str = ", ".join(existing_filestems) + contracts_folder = imported_project.contracts_folder + path = imported_project.path + + # This will log the calculated / user-set contracts_folder. + contracts_path = f"{get_relative_path(contracts_folder, path)}" + + logger.error( + f"Source for stem '{filestem}' not found in " + f"'{imported_project.path}'." + f"Contracts folder: {contracts_path}, " + f"Existing file(s): {fs_str}" + ) + + return None + def get_versions(self, all_paths: Iterable[Path]) -> set[str]: versions = set() for path in all_paths: @@ -572,6 +671,11 @@ def compile( settings: Optional[dict] = None, ) -> Iterator[ContractType]: pm = project or self.local_project + + # (0.4.0): If compiling a project outside the cwd (such as a dependency), + # we are forced to use absolute paths. + use_absolute_paths = pm.path != Path.cwd() + self.compiler_settings = {**self.compiler_settings, **(settings or {})} contract_types: list[ContractType] = [] import_map = self.get_imports(contract_filepaths, project=pm) @@ -594,10 +698,23 @@ def compile( if not sources: continue - src_dict = {p: {"content": Path(p).read_text()} for p in sources} + if use_absolute_paths: + src_dict = { + str(pm.path / p): {"content": (pm.path / p).read_text(encoding="utf8")} + for p in sources + } + else: + src_dict = { + p: {"content": Path(p).read_text(encoding="utf8")} for p in sources + } + for src in sources: if Path(src).is_absolute(): - src_id = f"{get_relative_path(Path(src), pm.path)}" + src_id = ( + str(Path(src)) + if use_absolute_paths + else f"{get_relative_path(Path(src), pm.path)}" + ) else: src_id = src @@ -606,17 +723,38 @@ def compile( if imp in src_dict: continue - imp_path = pm.path / imp - if not imp_path.is_file(): - continue - - src_dict[str(imp_path)] = {"content": imp_path.read_text()} + imp_path = Path(imp) + if (pm.path / imp).is_file(): + imp_path = pm.path / imp + if not imp_path.is_file(): + continue + + src_dict[imp] = {"content": imp_path.read_text(encoding="utf8")} + + else: + for parent in imp_path.parents: + if parent.name == "site-packages": + src_id = f"{get_relative_path(imp_path, parent)}" + break + + elif parent.name == ".ape": + dm = self.local_project.dependencies + full_parent = dm.packages_cache.projects_folder + src_id = f"{get_relative_path(imp_path, full_parent)}" + break + + # Likely from a dependency. Exclude absolute prefixes so Vyper + # knows what to do. + if imp_path.is_file(): + src_dict[src_id] = { + "content": imp_path.read_text(encoding="utf8") + } else: # NOTE: Pre vyper 0.4.0, interfaces CANNOT be in the source dict, # but post 0.4.0, they MUST be. src_dict = { - s: {"content": p.read_text()} + s: {"content": p.read_text(encoding="utf8")} for s, p in { p: pm.path / p for p in settings_set["outputSelection"] }.items() @@ -651,7 +789,7 @@ def compile( comp_kwargs = {"vyper_version": vyper_version, "vyper_binary": vyper_binary} # `base_path` is required for pre-0.4 versions or else imports won't resolve. - if vyper_version < Version("0.4.0rc6"): + if vyper_version < Version("0.4.0"): comp_kwargs["base_path"] = pm.path try: @@ -662,7 +800,9 @@ def compile( for source_id, output_items in result["contracts"].items(): content = { i + 1: ln - for i, ln in enumerate((pm.path / source_id).read_text().splitlines()) + for i, ln in enumerate( + (pm.path / source_id).read_text(encoding="utf8").splitlines() + ) } for name, output in output_items.items(): # De-compress source map to get PC POS map. @@ -778,27 +918,21 @@ def compile( def compile_code( self, code: str, project: Optional[ProjectManager] = None, **kwargs ) -> ContractType: + # NOTE: We are unable to use `vvm.compile_code()` because it does not + # appear to honor altered VVM install paths, thus always re-installs + # Vyper in our tests because of the monkeypatch. Also, their approach + # isn't really different than our approach implemented below. pm = project or self.local_project + with pm.isolate_in_tempdir(): + name = kwargs.get("contractName", "code") + file = pm.path / f"{name}.vy" + file.write_text(code, encoding="utf8") + contract_type = next(self.compile((file,), project=pm), None) + if contract_type is None: + # Not sure when this would happen. + raise VyperCompileError("Failed to produce contract type.") - # Figure out what compiler version we need for this contract... - version = self._source_vyper_version(code) - # ...and install it if necessary - _install_vyper(version) - - try: - result = vvm.compile_source(code, base_path=pm.path, vyper_version=version) - except Exception as err: - raise VyperCompileError(str(err)) from err - - output = result.get("", {}) - return ContractType.model_validate( - { - "abi": output["abi"], - "deploymentBytecode": {"bytecode": output["bytecode"]}, - "runtimeBytecode": {"bytecode": output["bytecode_runtime"]}, - **kwargs, - } - ) + return contract_type def _source_vyper_version(self, code: str) -> Version: """Given source code, figure out which Vyper version to use""" @@ -844,7 +978,7 @@ def _flatten_source(self, path: Path, project: Optional[ProjectManager] = None) dependencies[import_match] = manifest interfaces_source = "" - og_source = (pm.path / path).read_text() + og_source = (pm.path / path).read_text(encoding="utf8") # Get info about imports and source meta aliases = extract_import_aliases(og_source) @@ -889,7 +1023,7 @@ def _match_source(imp_path: str) -> Optional[PackageManifest]: # Generate an ABI from the source code elif import_file.is_file(): - abis = source_to_abi(import_file.read_text()) + abis = source_to_abi(import_file.read_text(encoding="utf8")) interfaces_source += generate_interface(abis, iface_name) def no_nones(it: Iterable[Optional[str]]) -> Iterable[str]: @@ -1042,6 +1176,11 @@ def _get_compiler_settings_from_version_map( project: Optional[ProjectManager] = None, ): pm = project or self.local_project + + # When compiling projects outside the cwd, use absolute paths for ease. + # Also, struggled to get it work any other way. + use_absolute_path = pm.path != Path.cwd() + compiler_data = self._get_compiler_arguments(version_map, project=pm) settings = {} for version, data in compiler_data.items(): @@ -1057,7 +1196,7 @@ def _get_compiler_settings_from_version_map( ) or EVM_VERSION_DEFAULT.get(version.base_version) for source_path in source_paths: source_id = str(get_relative_path(source_path.absolute(), pm.path)) - optimization = optimizations_map.get(source_id, True) + optimization = optimizations_map.get(source_id, "codesize") evm_version = evm_version_map.get(source_id, default_evm_version) settings_key = f"{optimization}%{evm_version}".lower() if settings_key not in output_selection: @@ -1073,12 +1212,15 @@ def _get_compiler_settings_from_version_map( elif optimization == "false": optimization = False - if version >= Version("0.4.0rc6"): - # Vyper 0.4.0 seems to require absolute paths. + if version >= Version("0.4.0"): + + def _to_src_id(s): + return str(pm.path / s) if use_absolute_path else s + selection_dict = { - (pm.path / s).as_posix(): ["*"] + _to_src_id(s): ["*"] for s in selection - if (pm.path / s).is_file() + if ((pm.path / s).is_file() if use_absolute_path else Path(s).is_file()) and f"interfaces{os.path.sep}" not in s and get_full_extension(pm.path / s) != FileType.INTERFACE } @@ -1090,9 +1232,15 @@ def _get_compiler_settings_from_version_map( if "interfaces" not in s } + search_paths = [*getsitepackages()] + if pm.path == Path.cwd(): + search_paths.append(".") + # else: only seem to get absolute paths to work (for compiling deps alone). + version_settings[settings_key] = { "optimize": optimization, "outputSelection": selection_dict, + "search_paths": [".", *getsitepackages()], } if evm_version and evm_version not in ("none", "null"): version_settings[settings_key]["evmVersion"] = f"{evm_version}" diff --git a/pyproject.toml b/pyproject.toml index 9d0c5d65..196f5504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ write_to = "ape_vyper/version.py" [tool.black] line-length = 100 -target-version = ['py39', 'py310', 'py311', 'py312'] +target-version = ['py310', 'py311', 'py312'] include = '\.pyi?$' [tool.pytest.ini_options] diff --git a/setup.py b/setup.py index f4e367de..4943dfcc 100644 --- a/setup.py +++ b/setup.py @@ -8,12 +8,13 @@ "pytest-xdist", # multi-process runner "pytest-cov", # Coverage analyzer plugin "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer + "snekmate", # Python package-sources integration testing ], "lint": [ "black>=24.4.2,<25", # Auto-formatter and linter - "mypy>=1.10.0,<2", # Static type analyzer + "mypy>=1.10.1,<2", # Static type analyzer "types-setuptools", # Needed due to mypy typeshed - "flake8>=7.0.0,<8", # Style linter + "flake8>=7.1.0,<8", # Style linter "isort>=5.13.2", # Import sorting linter "mdformat>=0.7.17", # Auto-formatter for markdown "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown @@ -58,13 +59,13 @@ url="https://github.com/ApeWorX/ape-vyper", include_package_data=True, install_requires=[ - "eth-ape>=0.8.3,<0.9", + "eth-ape>=0.8.7,<0.9", "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", ], - python_requires=">=3.9,<4", + python_requires=">=3.10,<4", extras_require=extras_require, py_modules=["ape_vyper"], entry_points={ @@ -85,7 +86,6 @@ "Operating System :: MacOS", "Operating System :: POSIX", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/tests/conftest.py b/tests/conftest.py index 0c72115e..76589955 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,13 @@ import os import shutil -import tempfile from contextlib import contextmanager from pathlib import Path -from tempfile import mkdtemp import ape import pytest import vvm # type: ignore from ape.contracts import ContractContainer +from ape.utils import create_tempdir from click.testing import CliRunner BASE_CONTRACTS_PATH = Path(__file__).parent / "contracts" @@ -29,7 +28,7 @@ "0.3.7", "0.3.9", "0.3.10", - "0.4.0rc6", + "0.4.0", ) CONTRACT_VERSION_GEN_MAP = { @@ -58,13 +57,8 @@ def from_tests_dir(): @pytest.fixture(scope="session", autouse=True) def config(): - cfg = ape.config - - # Ensure we don't persist any .ape data. - with tempfile.TemporaryDirectory() as temp_dir: - path = Path(temp_dir).resolve() - cfg.DATA_FOLDER = path - yield cfg + with ape.config.isolate_data_folder(): + yield ape.config def contract_test_cases(passing: bool) -> list[str]: @@ -86,17 +80,12 @@ def contract_test_cases(passing: bool) -> list[str]: @contextmanager def _tmp_vvm_path(monkeypatch): - vvm_install_path = mkdtemp() - - monkeypatch.setenv( - vvm.install.VVM_BINARY_PATH_VARIABLE, - vvm_install_path, - ) - - yield vvm_install_path - - if Path(vvm_install_path).is_dir(): - shutil.rmtree(vvm_install_path, ignore_errors=True) + with create_tempdir() as path: + monkeypatch.setenv( + vvm.install.VVM_BINARY_PATH_VARIABLE, + f"{path}", + ) + yield path @pytest.fixture( diff --git a/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi b/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi index 969691dd..cb314c87 100644 --- a/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi +++ b/tests/contracts/passing_contracts/interfaces/IFaceZeroFour.vyi @@ -1,4 +1,4 @@ -# pragma version ~=0.4.0rc6 +# pragma version ~=0.4.0 @external @view diff --git a/tests/contracts/passing_contracts/zero_four.vy b/tests/contracts/passing_contracts/zero_four.vy index f3b72488..fbab951d 100644 --- a/tests/contracts/passing_contracts/zero_four.vy +++ b/tests/contracts/passing_contracts/zero_four.vy @@ -1,10 +1,12 @@ -# pragma version ~=0.4.0rc6 +# pragma version ~=0.4.0 import interfaces.IFaceZeroFour as IFaceZeroFour implements: IFaceZeroFour from . import zero_four_module as zero_four_module +from snekmate.auth import ownable + @external @view def implementThisPlease(role: bytes32) -> bool: diff --git a/tests/contracts/passing_contracts/zero_four_module.vy b/tests/contracts/passing_contracts/zero_four_module.vy index cc00b46c..e3e277a0 100644 --- a/tests/contracts/passing_contracts/zero_four_module.vy +++ b/tests/contracts/passing_contracts/zero_four_module.vy @@ -1,4 +1,4 @@ -# pragma version ~=0.4.0rc6 +# pragma version ~=0.4.0 @internal def moduleMethod() -> bool: diff --git a/tests/test_ape_reverts.py b/tests/test_ape_reverts.py index abebef26..45fe1b21 100644 --- a/tests/test_ape_reverts.py +++ b/tests/test_ape_reverts.py @@ -1,9 +1,20 @@ import re +import shutil import pytest +from ape import chain from ape.pytest.contextmanagers import RevertsContextManager as reverts +@pytest.fixture(autouse=True) +def clear_db(geth_provider): + yield + # Helps prevent replacement tx errors. + # TODO: Move this into Ape's isolation handling for geth. + blobpool = chain.provider._process._data_dir / "geth" / "blobpool" + shutil.rmtree(blobpool, ignore_errors=True) + + @pytest.fixture(params=("021", "022", "023", "0215", "0216", "034")) def older_reverts_contract(account, project, geth_provider, request): container = project.get_contract(f"sub_reverts_{request.param}") diff --git a/tests/test_compiler.py b/tests/test_compiler.py index a2ac7fde..fc6bbc41 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -26,7 +26,7 @@ OLDER_VERSION_FROM_PRAGMA = Version("0.2.16") VERSION_37 = Version("0.3.7") VERSION_FROM_PRAGMA = Version("0.3.10") -VERSION_04 = Version("0.4.0rc6") +VERSION_04 = Version("0.4.0") @pytest.fixture @@ -123,8 +123,6 @@ def test_get_version_map(project, compiler, all_versions): "optimize_codesize.vy", "evm_pragma.vy", "use_iface2.vy", - "contract_no_pragma.vy", # no pragma should compile with latest version - "empty.vy", # empty file still compiles with latest version "pragma_with_space.vy", "flatten_me.vy", ] @@ -155,7 +153,7 @@ def test_get_version_map(project, compiler, all_versions): # Vyper 0.4.0 assertions. actual4 = {x.name for x in actual[VERSION_04]} - expected4 = {"zero_four_module.vy", "zero_four.vy"} + expected4 = {"contract_no_pragma.vy", "empty.vy", "zero_four_module.vy", "zero_four.vy"} assert actual4 == expected4 @@ -163,39 +161,33 @@ def test_compiler_data_in_manifest(project): def run_test(manifest): assert len(manifest.compilers) >= 3, manifest.compilers - all_latest = [c for c in manifest.compilers if str(c.version) == str(VERSION_FROM_PRAGMA)] - codesize_latest = [c for c in all_latest if c.settings["optimize"] == "codesize"][0] - evm_latest = [c for c in all_latest if c.settings.get("evmVersion") == "paris"][0] - true_latest = [ + all_latest_03 = [ + c for c in manifest.compilers if str(c.version) == str(VERSION_FROM_PRAGMA) + ] + evm_latest = [c for c in all_latest_03 if c.settings.get("evmVersion") == "paris"][0] + codesize_latest = [ c - for c in all_latest - if c.settings["optimize"] is True and c.settings.get("evmVersion") != "paris" + for c in all_latest_03 + if c.settings["optimize"] == "codesize" and c.settings.get("evmVersion") != "paris" ][0] vyper_028 = [ c for c in manifest.compilers if str(c.version) == str(OLDER_VERSION_FROM_PRAGMA) ][0] - for compiler in (vyper_028, codesize_latest, true_latest): + for compiler in (vyper_028, codesize_latest): assert compiler.name == "vyper" assert vyper_028.settings["evmVersion"] == "berlin" assert codesize_latest.settings["evmVersion"] == "shanghai" - assert true_latest.settings["evmVersion"] == "shanghai" - - # There is only one contract with codesize pragma. - assert codesize_latest.contractTypes == ["optimize_codesize"] - assert codesize_latest.settings["optimize"] == "codesize" # There is only one contract with evm-version pragma. assert evm_latest.contractTypes == ["evm_pragma"] assert evm_latest.settings.get("evmVersion") == "paris" - assert len(true_latest.contractTypes) >= 9 + assert len(codesize_latest.contractTypes) >= 9 assert len(vyper_028.contractTypes) >= 1 - assert "contract_0310" in true_latest.contractTypes + assert "contract_0310" in codesize_latest.contractTypes assert "older_version" in vyper_028.contractTypes - for compiler in (true_latest, vyper_028): - assert compiler.settings["optimize"] is True project.update_manifest(compilers=[]) project.load_contracts(use_cache=False) @@ -233,19 +225,24 @@ def test_get_imports(compiler, project): actual = compiler.get_imports(vyper_files, project=project) prefix = "contracts/passing_contracts" builtin_import = "vyper/interfaces/ERC20.json" - local_import = f"{prefix}/interfaces/IFace.vy" - local_from_import = f"{prefix}/interfaces/IFace2.vy" - dep_key = project.dependencies.get_dependency("exampledependency", "local").package_id.replace( - "/", "_" - ) - dependency_import = f"{dep_key}/local/contracts/Dependency.vy" - assert set(actual[f"{prefix}/contract_037.vy"]) == {builtin_import} - assert set(actual[f"{prefix}/use_iface.vy"]) == { - local_import, - local_from_import, - dependency_import, - } - assert set(actual[f"{prefix}/use_iface2.vy"]) == {local_import} + local_import = "IFace.vy" + local_from_import = "IFace2.vy" + dependency_import = "Dependency.vy" + + # The source IDs end up as absolute paths because they are in tempdir + # (not direct local project) and because of Vyper 0.4 reasons, we need + # this to be the case. And we don't know the version map yet at this point. + contract_37_key = [k for k in actual if f"{prefix}/contract_037.vy" in k][0] + use_iface_key = [k for k in actual if f"{prefix}/use_iface.vy" in k][0] + use_iface2_key = [k for k in actual if f"{prefix}/use_iface2.vy" in k][0] + + assert set(actual[contract_37_key]) == {builtin_import} + + actual_iface_use = actual[use_iface_key] + for expected in (local_import, local_from_import, dependency_import): + assert any(k for k in actual_iface_use if expected in k) + + assert actual[use_iface2_key][0].endswith(local_import) @pytest.mark.parametrize("src,vers", [("contract_039", "0.3.9"), ("contract_037", "0.3.7")]) @@ -521,7 +518,7 @@ def test_compile_with_version_set_in_config(config, projects_path, compiler, moc def test_compile_code(project, compiler, dev_revert_source): - code = dev_revert_source.read_text() + code = dev_revert_source.read_text(encoding="utf8") actual = compiler.compile_code(code, project=project, contractName="MyContract") assert isinstance(actual, ContractType) assert actual.name == "MyContract"