Skip to content

Commit

Permalink
feat: Detect Vyper version from source (#23)
Browse files Browse the repository at this point in the history
Add two functions that allow to:
- Detect the version specifier in the pragma given some source code
- Detect the best version given the specifier

cache the result of get_installable_vyper_versions

---------

Co-authored-by: Charles Cooper <cooper.charles.m@gmail.com>
  • Loading branch information
DanielSchiavini and charles-cooper authored Sep 16, 2024
1 parent bc1581f commit c9a294b
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update contact information in `CONTRIBUTING.md`
- Update dependencies. Minimum python version is now 3.8 ([#22](https://github.com/vyperlang/vvm/pull/22))
- Add `output_format` argument to `compile_source` and `compile_files` ([#21](https://github.com/vyperlang/vvm/pull/21))
- New public function `detect_vyper_version_from_source` ([#23](https://github.com/vyperlang/vvm/pull/23))

## [0.1.0](https://github.com/vyperlang/vvm/tree/v0.1.0) - 2020-10-07
### Added
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ def vyper_version(request):
return version


@pytest.fixture
def latest_version():
global VERSIONS
return VERSIONS[0]


@pytest.fixture
def foo_source(vyper_version):
visibility = "external" if vyper_version >= Version("0.2.0") else "public"
Expand Down
59 changes: 59 additions & 0 deletions tests/test_versioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import pytest
from packaging.specifiers import Specifier
from packaging.version import Version

from vvm import detect_vyper_version_from_source
from vvm.exceptions import UnexpectedVersionError
from vvm.utils.versioning import _detect_version_specifier, _pick_vyper_version

LAST_PER_MINOR = {
1: Version("0.1.0b17"),
2: Version("0.2.16"),
3: Version("0.3.10"),
}


def test_foo_vyper_version(foo_source, vyper_version):
specifier = _detect_version_specifier(foo_source)
assert str(specifier) == f"=={vyper_version}"
assert vyper_version.major == 0
assert _pick_vyper_version(specifier) == vyper_version


@pytest.mark.parametrize(
"version_str,decorator,pragma,expected_specifier,expected_version",
[
("^0.1.1", "public", "@version", "~=0.1", "latest"),
("~0.3.0", "external", "pragma version", "~=0.3.0", "0.3.10"),
("0.1.0b17", "public", "@version", "==0.1.0b17", "0.1.0b17"),
(">=0.3.0-beta17", "external", "@version", ">=0.3.0-beta17", "latest"),
("0.4.0rc6", "external", "pragma version", "==0.4.0rc6", "0.4.0rc6"),
],
)
def test_vyper_version(
version_str, decorator, pragma, expected_specifier, expected_version, latest_version
):
source = f"""
# {pragma} {version_str}
@{decorator}
def foo() -> int128:
return 42
"""
detected = _detect_version_specifier(source)
assert detected == Specifier(expected_specifier)
if expected_version == "latest":
expected_version = str(latest_version)
assert detect_vyper_version_from_source(source) == Version(expected_version)


def test_no_version_in_source():
with pytest.raises(UnexpectedVersionError) as excinfo:
detect_vyper_version_from_source("def foo() -> int128: return 42")
assert str(excinfo.value) == "No version detected in source code"


def test_version_does_not_exist():
with pytest.raises(UnexpectedVersionError) as excinfo:
detect_vyper_version_from_source("# pragma version 2024.0.1")
assert str(excinfo.value) == "No installable Vyper satisfies the specifier ==2024.0.1"
1 change: 1 addition & 0 deletions vvm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
set_vyper_version,
)
from vvm.main import compile_files, compile_source, compile_standard, get_vyper_version
from vvm.utils.versioning import detect_vyper_version_from_source
13 changes: 12 additions & 1 deletion vvm/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
VVM_BINARY_PATH_VARIABLE = "VVM_BINARY_PATH"

_default_vyper_binary = None
_installable_vyper_versions: Optional[List[Version]] = None


def _get_os_name() -> str:
Expand Down Expand Up @@ -166,11 +167,19 @@ def get_installable_vyper_versions(headers: Dict = None) -> List[Version]:
"""
Return a list of all `vyper` versions that can be installed by vvm.
Note: this function is cached, so subsequent calls will not change the result.
When new versions of vyper are released, the cache will need to be cleared
manually or the application restarted.
Returns
-------
List
List of Versions objects of installable `vyper` versions.
"""
global _installable_vyper_versions
if _installable_vyper_versions is not None:
return _installable_vyper_versions

version_list = []

headers = _get_headers(headers)
Expand All @@ -180,7 +189,9 @@ def get_installable_vyper_versions(headers: Dict = None) -> List[Version]:
asset = next((i for i in release["assets"] if _get_os_name() in i["name"]), False)
if asset:
version_list.append(version)
return sorted(version_list, reverse=True)

_installable_vyper_versions = sorted(version_list, reverse=True)
return _installable_vyper_versions


def get_installed_vyper_versions(vvm_binary_path: Union[Path, str] = None) -> List[Version]:
Expand Down
99 changes: 99 additions & 0 deletions vvm/utils/versioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import itertools
import re
from typing import Any, Optional

from packaging.specifiers import Specifier
from packaging.version import Version

from vvm.exceptions import UnexpectedVersionError
from vvm.install import get_installable_vyper_versions, get_installed_vyper_versions

_VERSION_RE = re.compile(r"\s*#\s*(?:pragma\s+|@)version\s+([=><^~]*)(\d+\.\d+\.\d+\S*)")


def _detect_version_specifier(source_code: str) -> Specifier:
"""
Detect the version given by the pragma version in the source code.
Arguments
---------
source_code : str
Source code to detect the version from.
Returns
-------
str
vyper version specifier, or None if none could be detected.
"""
match = _VERSION_RE.search(source_code)
if match is None:
raise UnexpectedVersionError("No version detected in source code")

specifier, version_str = match.groups()
if specifier in ("~", "^"): # convert from npm-style to pypi-style
if specifier == "^": # minor match, remove the patch from the version
version_str = ".".join(version_str.split(".")[:-1])
specifier = "~=" # finds compatible versions

if specifier == "":
specifier = "=="
return Specifier(specifier + version_str)


def _pick_vyper_version(
specifier: Specifier,
prereleases: Optional[bool] = None,
check_installed: bool = True,
check_installable: bool = True,
) -> Version:
"""
Pick the latest vyper version that is installed and satisfies the given specifier.
If None of the installed versions satisfy the specifier, pick the latest installable
version.
Arguments
---------
specifier : Specifier
Specifier to pick a version for.
prereleases : bool, optional
Whether to allow prereleases in the returned iterator. If set to
``None`` (the default), it will be intelligently decide whether to allow
prereleases or not (based on the specifier.prereleases attribute, and
whether the only versions matching are prereleases).
check_installed : bool, optional
Whether to check the installed versions. Defaults to True.
check_installable : bool, optional
Whether to check the installable versions. Defaults to True.
Returns
-------
Version
Vyper version that satisfies the specifier, or None if no version satisfies the specifier.
"""
versions = itertools.chain(
get_installed_vyper_versions() if check_installed else [],
get_installable_vyper_versions() if check_installable else [],
)
if (ret := next(specifier.filter(versions, prereleases), None)) is None:
raise UnexpectedVersionError(f"No installable Vyper satisfies the specifier {specifier}")
return ret


def detect_vyper_version_from_source(source_code: str, **kwargs: Any) -> Version:
"""
Detect the version given by the pragma version in the source code.
Arguments
---------
source_code : str
Source code to detect the version from.
kwargs : Any
Keyword arguments to pass to `pick_vyper_version`.
Returns
-------
Version
vyper version, or None if no version could be detected.
"""
specifier = _detect_version_specifier(source_code)
return _pick_vyper_version(specifier, **kwargs)

0 comments on commit c9a294b

Please sign in to comment.