From fe3b2e903672f9986cf51aceaf7ffee7c80977a9 Mon Sep 17 00:00:00 2001 From: trocher <43437004+trocher@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:20:35 +0100 Subject: [PATCH 1/7] fix vvm version detection --- boa/contracts/vvm/vvm_contract.py | 13 ------------- boa/interpret.py | 20 ++++++++++++++------ boa/util/version.py | 22 ++++++++++++++++++++++ tests/unitary/contracts/vvm/test_vvm.py | 14 ++++++++++++++ tests/unitary/utils/test_cache.py | 5 +++-- 5 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 boa/util/version.py diff --git a/boa/contracts/vvm/vvm_contract.py b/boa/contracts/vvm/vvm_contract.py index 87038a2d..966b76d2 100644 --- a/boa/contracts/vvm/vvm_contract.py +++ b/boa/contracts/vvm/vvm_contract.py @@ -1,22 +1,9 @@ -import re from functools import cached_property from boa.contracts.abi.abi_contract import ABIContractFactory, ABIFunction from boa.environment import Env from boa.util.eip5202 import generate_blueprint_bytecode -# TODO: maybe this doesn't detect release candidates -VERSION_RE = re.compile(r"\s*#\s*(pragma\s+version|@version)\s+(\d+\.\d+\.\d+)") - - -# TODO: maybe move this up to vvm? -def _detect_version(source_code: str): - res = VERSION_RE.findall(source_code) - if len(res) < 1: - return None - # TODO: handle len(res) > 1 - return res[0][1] - class VVMDeployer: """ diff --git a/boa/interpret.py b/boa/interpret.py index 8870b168..5ba8cac9 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -8,6 +8,7 @@ import vvm import vyper +from packaging.specifiers import SpecifierSet from vyper.ast.parse import parse_to_ast from vyper.cli.vyper_compile import get_search_paths from vyper.compiler.input_bundle import ( @@ -23,7 +24,7 @@ from vyper.utils import sha256sum from boa.contracts.abi.abi_contract import ABIContractFactory -from boa.contracts.vvm.vvm_contract import VVMDeployer, _detect_version +from boa.contracts.vvm.vvm_contract import VVMDeployer from boa.contracts.vyper.vyper_contract import ( VyperBlueprint, VyperContract, @@ -34,6 +35,7 @@ from boa.rpc import json from boa.util.abi import Address from boa.util.disk_cache import DiskCache +from boa.util.version import detect_version if TYPE_CHECKING: from vyper.semantics.analysis.base import ImportInfo @@ -249,12 +251,11 @@ def loads_partial( if dedent: source_code = textwrap.dedent(source_code) - version = _detect_version(source_code) - if version is not None and version != vyper.__version__: + version_set = detect_version(source_code) + if version_set is not None and not version_set.contains(vyper.__version__): filename = str(filename) # help mypy # TODO: pass name to loads_partial_vvm, not filename - return _loads_partial_vvm(source_code, version, filename) - + return _loads_partial_vvm(source_code, version_set, filename) compiler_args = compiler_args or {} deployer_class = _get_default_deployer_class() @@ -269,9 +270,16 @@ def load_partial(filename: str, compiler_args=None): ) -def _loads_partial_vvm(source_code: str, version: str, filename: str): +def _loads_partial_vvm(source_code: str, version_set: SpecifierSet, filename: str): global _disk_cache + available = vvm.get_installable_vyper_versions() + # get the most recent version compatible with the set + version = next(version_set.filter(available), None) + + if version is None: + raise ValueError(f"no matching version found for {version_set}") + # install the requested version if not already installed vvm.install_vyper(version=version) diff --git a/boa/util/version.py b/boa/util/version.py new file mode 100644 index 00000000..da5ac499 --- /dev/null +++ b/boa/util/version.py @@ -0,0 +1,22 @@ +import re +from typing import Optional + +from packaging.specifiers import SpecifierSet + +VERSION_RE = r"^(#\s*(@version|pragma\s+version)\s+(.*))" + + +def detect_version(source_code: str) -> Optional[SpecifierSet]: + res = re.findall(VERSION_RE, source_code, re.MULTILINE) + if len(res) < 1: + return None + + # If there are multiple version pragmas, use the first one found and let Vyper fail compilation + version_str = res[0][2] + + # X.Y.Z or vX.Y.Z => ==X.Y.Z, ==vX.Y.Z + if re.match("[v0-9]", version_str): + version_str = "==" + version_str + # convert npm to pep440 + version_str = re.sub("^\\^", "~=", version_str) + return SpecifierSet(version_str) diff --git a/tests/unitary/contracts/vvm/test_vvm.py b/tests/unitary/contracts/vvm/test_vvm.py index 3f10106d..76670ed8 100644 --- a/tests/unitary/contracts/vvm/test_vvm.py +++ b/tests/unitary/contracts/vvm/test_vvm.py @@ -29,6 +29,20 @@ def test_load_vvm(): assert contract.bar() == 43 +def test_load_complex_version_vvm(): + contracts = [ + "# @version ^0.3.1", + "# @version ^0.3.7", + "# @version ==0.3.10", + "# pragma version >=0.3.8, <0.4.0, !=0.3.10", + # "# pragma version ==0.4.0rc3", + # TODO: uncomment when vvm is fixed (https://github.com/vyperlang/vvm/pull/29) + ] + for contract in contracts: + contract = boa.loads(contract + "\nfoo: public(uint256)") + assert contract.foo() == 0 + + def test_loads_vvm(): with open(mock_3_10_path) as f: code = f.read() diff --git a/tests/unitary/utils/test_cache.py b/tests/unitary/utils/test_cache.py index ad58eec5..d9c46d96 100644 --- a/tests/unitary/utils/test_cache.py +++ b/tests/unitary/utils/test_cache.py @@ -1,6 +1,7 @@ from unittest.mock import patch import pytest +from packaging.specifiers import SpecifierSet from vyper.compiler import CompilerData from boa.contracts.vyper.vyper_contract import VyperDeployer @@ -34,8 +35,8 @@ def test_cache_vvm(): code = """ x: constant(int128) = 1000 """ - version = "0.2.8" - version2 = "0.3.1" + version = SpecifierSet("0.2.8") + version2 = SpecifierSet("0.3.1") assert _disk_cache is not None # Mock vvm.compile_source From fc10a5238cce2b9fcd9dd373db27b15107ab9d6e Mon Sep 17 00:00:00 2001 From: trocher <43437004+trocher@users.noreply.github.com> Date: Mon, 18 Nov 2024 19:31:25 +0100 Subject: [PATCH 2/7] fix test --- tests/unitary/utils/test_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unitary/utils/test_cache.py b/tests/unitary/utils/test_cache.py index d9c46d96..6251abc2 100644 --- a/tests/unitary/utils/test_cache.py +++ b/tests/unitary/utils/test_cache.py @@ -35,8 +35,8 @@ def test_cache_vvm(): code = """ x: constant(int128) = 1000 """ - version = SpecifierSet("0.2.8") - version2 = SpecifierSet("0.3.1") + version = SpecifierSet("==0.2.8") + version2 = SpecifierSet("==0.3.1") assert _disk_cache is not None # Mock vvm.compile_source From 2a9964fcad3196de79ff592fab8426ece3e66676 Mon Sep 17 00:00:00 2001 From: trocher <43437004+trocher@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:06:04 +0100 Subject: [PATCH 3/7] use vvm for version detection --- boa/interpret.py | 21 ++++++++------------- boa/util/version.py | 22 ---------------------- tests/unitary/contracts/vvm/test_vvm.py | 21 ++++++++++++++------- tests/unitary/utils/test_cache.py | 6 +++--- 4 files changed, 25 insertions(+), 45 deletions(-) delete mode 100644 boa/util/version.py diff --git a/boa/interpret.py b/boa/interpret.py index 5ba8cac9..c401e656 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -8,7 +8,8 @@ import vvm import vyper -from packaging.specifiers import SpecifierSet +from packaging.version import Version +from vvm.utils.versioning import detect_vyper_version_from_source from vyper.ast.parse import parse_to_ast from vyper.cli.vyper_compile import get_search_paths from vyper.compiler.input_bundle import ( @@ -35,7 +36,6 @@ from boa.rpc import json from boa.util.abi import Address from boa.util.disk_cache import DiskCache -from boa.util.version import detect_version if TYPE_CHECKING: from vyper.semantics.analysis.base import ImportInfo @@ -251,11 +251,13 @@ def loads_partial( if dedent: source_code = textwrap.dedent(source_code) - version_set = detect_version(source_code) - if version_set is not None and not version_set.contains(vyper.__version__): + # TODO: let vvm know our preferred version (vyper.__version__) + version = detect_vyper_version_from_source(source_code) + if version is not None and version != Version(vyper.__version__): filename = str(filename) # help mypy # TODO: pass name to loads_partial_vvm, not filename - return _loads_partial_vvm(source_code, version_set, filename) + return _loads_partial_vvm(source_code, version, filename) + compiler_args = compiler_args or {} deployer_class = _get_default_deployer_class() @@ -270,16 +272,9 @@ def load_partial(filename: str, compiler_args=None): ) -def _loads_partial_vvm(source_code: str, version_set: SpecifierSet, filename: str): +def _loads_partial_vvm(source_code: str, version: Version, filename: str): global _disk_cache - available = vvm.get_installable_vyper_versions() - # get the most recent version compatible with the set - version = next(version_set.filter(available), None) - - if version is None: - raise ValueError(f"no matching version found for {version_set}") - # install the requested version if not already installed vvm.install_vyper(version=version) diff --git a/boa/util/version.py b/boa/util/version.py deleted file mode 100644 index da5ac499..00000000 --- a/boa/util/version.py +++ /dev/null @@ -1,22 +0,0 @@ -import re -from typing import Optional - -from packaging.specifiers import SpecifierSet - -VERSION_RE = r"^(#\s*(@version|pragma\s+version)\s+(.*))" - - -def detect_version(source_code: str) -> Optional[SpecifierSet]: - res = re.findall(VERSION_RE, source_code, re.MULTILINE) - if len(res) < 1: - return None - - # If there are multiple version pragmas, use the first one found and let Vyper fail compilation - version_str = res[0][2] - - # X.Y.Z or vX.Y.Z => ==X.Y.Z, ==vX.Y.Z - if re.match("[v0-9]", version_str): - version_str = "==" + version_str - # convert npm to pep440 - version_str = re.sub("^\\^", "~=", version_str) - return SpecifierSet(version_str) diff --git a/tests/unitary/contracts/vvm/test_vvm.py b/tests/unitary/contracts/vvm/test_vvm.py index 76670ed8..e124bac9 100644 --- a/tests/unitary/contracts/vvm/test_vvm.py +++ b/tests/unitary/contracts/vvm/test_vvm.py @@ -1,3 +1,5 @@ +import pytest + import boa mock_3_10_path = "tests/unitary/contracts/vvm/mock_3_10.vy" @@ -29,18 +31,23 @@ def test_load_vvm(): assert contract.bar() == 43 -def test_load_complex_version_vvm(): - contracts = [ +@pytest.mark.parametrize( + "version_pragma", + [ "# @version ^0.3.1", "# @version ^0.3.7", "# @version ==0.3.10", - "# pragma version >=0.3.8, <0.4.0, !=0.3.10", + "# @version ~=0.3.10", + "# @version 0.3.10", + # "# pragma version >=0.3.8, <0.4.0, !=0.3.10", + # TODO: uncomment when vvm accept Specifier sets # "# pragma version ==0.4.0rc3", # TODO: uncomment when vvm is fixed (https://github.com/vyperlang/vvm/pull/29) - ] - for contract in contracts: - contract = boa.loads(contract + "\nfoo: public(uint256)") - assert contract.foo() == 0 + ], +) +def test_load_complex_version_vvm(version_pragma): + contract = boa.loads(version_pragma + "\nfoo: public(uint256)") + assert contract.foo() == 0 def test_loads_vvm(): diff --git a/tests/unitary/utils/test_cache.py b/tests/unitary/utils/test_cache.py index 6251abc2..3ef4395c 100644 --- a/tests/unitary/utils/test_cache.py +++ b/tests/unitary/utils/test_cache.py @@ -1,7 +1,7 @@ from unittest.mock import patch import pytest -from packaging.specifiers import SpecifierSet +from packaging.version import Version from vyper.compiler import CompilerData from boa.contracts.vyper.vyper_contract import VyperDeployer @@ -35,8 +35,8 @@ def test_cache_vvm(): code = """ x: constant(int128) = 1000 """ - version = SpecifierSet("==0.2.8") - version2 = SpecifierSet("==0.3.1") + version = Version("0.2.8") + version2 = Version("0.3.1") assert _disk_cache is not None # Mock vvm.compile_source From 57fded405f6c748724ea09437c38cc14b61efc6b Mon Sep 17 00:00:00 2001 From: trocher <43437004+trocher@users.noreply.github.com> Date: Mon, 18 Nov 2024 23:46:04 +0100 Subject: [PATCH 4/7] update example pragmas --- examples/ERC20.vy | 2 +- tests/unitary/fixtures/module_contract.vy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ERC20.vy b/examples/ERC20.vy index cc219373..d27f942b 100644 --- a/examples/ERC20.vy +++ b/examples/ERC20.vy @@ -1,4 +1,4 @@ -# pragma version ^0.4.0 +# pragma version ~=0.4.0 # @dev Implementation of ERC-20 token standard. # @author Takayuki Jimba (@yudetamago) # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md diff --git a/tests/unitary/fixtures/module_contract.vy b/tests/unitary/fixtures/module_contract.vy index 886424db..9a243065 100644 --- a/tests/unitary/fixtures/module_contract.vy +++ b/tests/unitary/fixtures/module_contract.vy @@ -1,4 +1,4 @@ -# pragma version ^0.4.0 +# pragma version ~=0.4.0 import module_lib From 0344d188c690219032db7d71f21aea4685c102e4 Mon Sep 17 00:00:00 2001 From: trocher <43437004+trocher@users.noreply.github.com> Date: Fri, 22 Nov 2024 00:44:26 +0100 Subject: [PATCH 5/7] bump vvm version, uses local vyper version when possible, only default to vvm otherwise --- boa/interpret.py | 12 ++++++++---- pyproject.toml | 2 +- tests/unitary/contracts/vvm/test_vvm.py | 6 ++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index c401e656..4c457ba4 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -9,7 +9,10 @@ import vvm import vyper from packaging.version import Version -from vvm.utils.versioning import detect_vyper_version_from_source +from vvm.utils.versioning import ( + detect_version_specifier_set, + detect_vyper_version_from_source, +) from vyper.ast.parse import parse_to_ast from vyper.cli.vyper_compile import get_search_paths from vyper.compiler.input_bundle import ( @@ -251,9 +254,10 @@ def loads_partial( if dedent: source_code = textwrap.dedent(source_code) - # TODO: let vvm know our preferred version (vyper.__version__) - version = detect_vyper_version_from_source(source_code) - if version is not None and version != Version(vyper.__version__): + specifier_set = detect_version_specifier_set(source_code) + # Use VVM only if the installed version is not in the specifier set + if specifier_set is not None and not specifier_set.contains(vyper.__version__): + version = detect_vyper_version_from_source(source_code) filename = str(filename) # help mypy # TODO: pass name to loads_partial_vvm, not filename return _loads_partial_vvm(source_code, version, filename) diff --git a/pyproject.toml b/pyproject.toml index 2ed482e0..6775cc46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "pytest-cov", # required to compile older versions of vyper - "vvm", + "vvm>=0.3.2", # eth-rlp requirement, not installed by default with 3.12 "typing-extensions", diff --git a/tests/unitary/contracts/vvm/test_vvm.py b/tests/unitary/contracts/vvm/test_vvm.py index e124bac9..dfcd1980 100644 --- a/tests/unitary/contracts/vvm/test_vvm.py +++ b/tests/unitary/contracts/vvm/test_vvm.py @@ -39,10 +39,8 @@ def test_load_vvm(): "# @version ==0.3.10", "# @version ~=0.3.10", "# @version 0.3.10", - # "# pragma version >=0.3.8, <0.4.0, !=0.3.10", - # TODO: uncomment when vvm accept Specifier sets - # "# pragma version ==0.4.0rc3", - # TODO: uncomment when vvm is fixed (https://github.com/vyperlang/vvm/pull/29) + "# pragma version >=0.3.8, <0.4.0, !=0.3.10", + "# pragma version ==0.4.0rc3", ], ) def test_load_complex_version_vvm(version_pragma): From f0ca93eccc62d17aa289a257d30f447973da6804 Mon Sep 17 00:00:00 2001 From: trocher <43437004+trocher@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:56:58 +0100 Subject: [PATCH 6/7] use _pick_vyper_version --- boa/interpret.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index 4c457ba4..e92fe317 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -11,7 +11,7 @@ from packaging.version import Version from vvm.utils.versioning import ( detect_version_specifier_set, - detect_vyper_version_from_source, + _pick_vyper_version, ) from vyper.ast.parse import parse_to_ast from vyper.cli.vyper_compile import get_search_paths @@ -257,7 +257,7 @@ def loads_partial( specifier_set = detect_version_specifier_set(source_code) # Use VVM only if the installed version is not in the specifier set if specifier_set is not None and not specifier_set.contains(vyper.__version__): - version = detect_vyper_version_from_source(source_code) + version = _pick_vyper_version(specifier_set) filename = str(filename) # help mypy # TODO: pass name to loads_partial_vvm, not filename return _loads_partial_vvm(source_code, version, filename) From 3390d38203d9bb30702eb725d376385731a82c35 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 22 Nov 2024 10:09:52 +0100 Subject: [PATCH 7/7] fix lint --- boa/interpret.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index e92fe317..3e66262b 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -9,10 +9,7 @@ import vvm import vyper from packaging.version import Version -from vvm.utils.versioning import ( - detect_version_specifier_set, - _pick_vyper_version, -) +from vvm.utils.versioning import _pick_vyper_version, detect_version_specifier_set from vyper.ast.parse import parse_to_ast from vyper.cli.vyper_compile import get_search_paths from vyper.compiler.input_bundle import (