diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d725bb8b..fb1ca56a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.5.0 hooks: - id: check-yaml @@ -10,24 +10,24 @@ repos: - id: isort - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.10.1 hooks: - id: black name: black - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.6.1 hooks: - id: mypy additional_dependencies: [types-setuptools, pydantic==1.10.4] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.14 + rev: 0.7.17 hooks: - id: mdformat additional_dependencies: [mdformat-gfm, mdformat-frontmatter] diff --git a/README.md b/README.md index bd0c6820..a577e65b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,16 @@ ape compile The `.vy` files in your project will compile into `ContractTypes` that you can deploy and interact with in Ape. +### Compiler Version + +By default, the `ape-vyper` plugin uses version pragma for version specification. +However, you can also configure the version directly in your `ape-config.yaml` file: + +```yaml +vyper: + version: 0.3.7 +``` + ### Interfaces You can not compile interface source files directly. diff --git a/ape_vyper/compiler.py b/ape_vyper/compiler.py index 411fe195..4348d579 100644 --- a/ape_vyper/compiler.py +++ b/ape_vyper/compiler.py @@ -9,6 +9,7 @@ from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast import vvm # type: ignore +from ape._pydantic_compat import validator from ape.api import PluginConfig from ape.api.compiler import CompilerAPI from ape.exceptions import ContractLogicError @@ -23,6 +24,7 @@ from evm_trace.enums import CALL_OPCODES from packaging.specifiers import InvalidSpecifier, SpecifierSet from packaging.version import Version +from vvm import compile_standard as vvm_compile_standard from vvm.exceptions import VyperError # type: ignore from ape_vyper.exceptions import ( @@ -42,7 +44,16 @@ class VyperConfig(PluginConfig): + version: Optional[SpecifierSet] = None + """ + Configure a version to use for all files, + regardless of pragma. + """ + evm_version: Optional[str] = None + """ + The evm-version or hard-fork name. + """ import_remapping: List[str] = [] """ @@ -58,6 +69,10 @@ class VyperConfig(PluginConfig): """ + @validator("version", pre=True) + def validate_version(cls, value): + return SpecifierSet(_version_to_specifier(value)) if isinstance(value, str) else value + def _install_vyper(version: Version): try: @@ -113,6 +128,7 @@ def get_optimization_pragma(source: Union[str, Path]) -> Optional[str]: pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+optimize\s+([^\n]*)", source_str), None) if pragma_match is None: return None + return pragma_match.groups()[0] @@ -252,6 +268,13 @@ def vyper_json(self): except ImportError: return None + @property + def config_version_pragma(self) -> Optional[SpecifierSet]: + if version := self.config.version: + return version + + return None + @property def import_remapping(self) -> Dict[str, Dict]: """ @@ -330,7 +353,7 @@ def compile( vyper_binary = compiler_data[vyper_version]["vyper_binary"] try: - result = vvm.compile_standard( + result = vvm_compile_standard( input_json, base_path=base_path, vyper_version=vyper_version, @@ -412,18 +435,20 @@ def get_version_map( self, contract_filepaths: List[Path], base_path: Optional[Path] = None ) -> Dict[Version, Set[Path]]: version_map: Dict[Version, Set[Path]] = {} - source_path_by_pragma_spec: Dict[SpecifierSet, 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): - if pragma := get_version_pragma_spec(path): - _safe_append(source_path_by_pragma_spec, pragma, path) + if config_spec := self.config_version_pragma: + _safe_append(source_path_by_version_spec, config_spec, path) + elif pragma := get_version_pragma_spec(path): + _safe_append(source_path_by_version_spec, pragma, path) else: source_paths_without_pragma.add(path) # Install all requires versions *before* building map - for pragma_spec, path_set in source_path_by_pragma_spec.items(): + for pragma_spec, path_set in source_path_by_version_spec.items(): if list(pragma_spec.filter(self.installed_versions)): # Already met. continue @@ -449,7 +474,7 @@ def get_version_map( # By this point, all the of necessary versions will be installed. # Thus, we will select only the best versions to use per source set. - for pragma_spec, path_set in source_path_by_pragma_spec.items(): + for pragma_spec, path_set in source_path_by_version_spec.items(): versions = sorted(list(pragma_spec.filter(self.installed_versions)), reverse=True) if versions: _safe_append(version_map, versions[0], path_set) @@ -1201,3 +1226,11 @@ def _is_fallback_check(opcodes: List[str], op: str) -> bool: and opcodes[6] == "SHR" and opcodes[5] == "0xE0" ) + + +def _version_to_specifier(version: str) -> str: + pragma_str = " ".join(version.split()).replace("^", "~=") + if pragma_str and pragma_str[0].isnumeric(): + return f"=={pragma_str}" + + return pragma_str diff --git a/setup.py b/setup.py index 30a487ce..27eac582 100644 --- a/setup.py +++ b/setup.py @@ -11,13 +11,13 @@ "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer ], "lint": [ - "black>=23.3.0,<24", # Auto-formatter and linter - "mypy>=0.991,<1", # Static type analyzer + "black>=23.10.1,<24", # Auto-formatter and linter + "mypy>=1.6.1", # Static type analyzer "types-setuptools", # Needed due to mypy typeshed "pydantic<2.0", # Needed for successful type check. TODO: Remove after full v2 support. - "flake8>=6.0.0,<7", # Style linter + "flake8>=6.1.0,<7", # Style linter "isort>=5.10.1", # Import sorting linter - "mdformat>=0.7.16", # Auto-formatter for markdown + "mdformat>=0.7.17", # Auto-formatter for markdown "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates ], diff --git a/tests/conftest.py b/tests/conftest.py index dd803fba..e2898b84 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,8 +10,6 @@ import pytest import vvm # type: ignore -from ape_vyper.compiler import VyperCompiler - # NOTE: Ensure that we don't use local paths for these DATA_FOLDER = Path(mkdtemp()).resolve() PROJECT_FOLDER = Path(mkdtemp()).resolve() @@ -140,7 +138,7 @@ def project_folder(): @pytest.fixture def compiler(): - return VyperCompiler() + return ape.compilers.vyper @pytest.fixture diff --git a/tests/projects/version_in_config/ape-config.yaml b/tests/projects/version_in_config/ape-config.yaml new file mode 100644 index 00000000..5eb0b078 --- /dev/null +++ b/tests/projects/version_in_config/ape-config.yaml @@ -0,0 +1,2 @@ +vyper: + version: 0.3.7 diff --git a/tests/projects/version_in_config/contracts/v_contract.vy b/tests/projects/version_in_config/contracts/v_contract.vy new file mode 100644 index 00000000..38602d92 --- /dev/null +++ b/tests/projects/version_in_config/contracts/v_contract.vy @@ -0,0 +1,5 @@ +# @version 0.3.10 + +@external +def foo(a: uint256) -> bool: + return True diff --git a/tests/test_compiler.py b/tests/test_compiler.py index b4258b5f..fb2f52ac 100644 --- a/tests/test_compiler.py +++ b/tests/test_compiler.py @@ -445,3 +445,17 @@ def test_trace_source_default_method(geth_provider, account, project): actual = str(src_tb[-1][-1]).lstrip() # Last line in traceback (without indent). expected = "8 log NotPayment(msg.sender)" assert actual == expected + + +def test_compile_with_version_set(config, projects_path, compiler, mocker): + path = projects_path / "version_in_config" + version_from_config = "0.3.7" + spy = mocker.patch("ape_vyper.compiler.vvm_compile_standard") + with config.using_project(path) as project: + contract = project.contracts_folder / "v_contract.vy" + settings = compiler.get_compiler_settings((contract,)) + assert str(list(settings.keys())[0]) == version_from_config + + # Show it uses this version in the compiler. + project.load_contracts(use_cache=False) + assert str(spy.call_args[1]["vyper_version"]) == version_from_config