Skip to content

Commit

Permalink
feat: allow version in config
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Oct 26, 2023
1 parent 888ab4c commit 3d03212
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 18 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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]
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
54 changes: 48 additions & 6 deletions ape_vyper/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand All @@ -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] = []
"""
Expand All @@ -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:
Expand Down Expand Up @@ -112,7 +127,17 @@ def get_optimization_pragma(source: Union[str, Path]) -> Optional[str]:
source_str = source if isinstance(source, str) else source.read_text()
pragma_match = next(re.finditer(r"(?:\n|^)\s*#pragma\s+optimize\s+([^\n]*)", source_str), None)
if pragma_match is None:
return None # Try compiling with latest

raw_pragma = pragma_match.groups()[0]
pragma_str = _version_to_specifier(raw_pragma)

try:
return SpecifierSet(pragma_str)
except InvalidSpecifier:
logger.warning(f"Invalid pragma spec: '{raw_pragma}'. Trying latest.")
return None

return pragma_match.groups()[0]


Expand Down Expand Up @@ -252,6 +277,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]:
"""
Expand Down Expand Up @@ -330,7 +362,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,
Expand Down Expand Up @@ -412,18 +444,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
Expand All @@ -449,7 +483,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)
Expand Down Expand Up @@ -1201,3 +1235,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
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
],
Expand Down
4 changes: 1 addition & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -140,7 +138,7 @@ def project_folder():

@pytest.fixture
def compiler():
return VyperCompiler()
return ape.compilers.vyper


@pytest.fixture
Expand Down
2 changes: 2 additions & 0 deletions tests/projects/version_in_config/ape-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vyper:
version: 0.3.7
5 changes: 5 additions & 0 deletions tests/projects/version_in_config/contracts/v_contract.vy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @version 0.3.10

@external
def foo(a: uint256) -> bool:
return True
14 changes: 14 additions & 0 deletions tests/test_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 3d03212

Please sign in to comment.