-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Detect Vyper version from source (#23)
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
1 parent
bc1581f
commit c9a294b
Showing
6 changed files
with
178 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |