Skip to content

Commit

Permalink
fix: version parsing for release candidates (#3593)
Browse files Browse the repository at this point in the history
the npm spec library is buggy and does not handle release candidates
correctly. switch to the pypa packaging library which does pep440. note
that we do a hack in order to support commonly used npm prefixes: no
prefix, and `^` as prefix. going forward in v0.4.x, we will switch to
pep440 entirely.
  • Loading branch information
charles-cooper committed Sep 6, 2023
1 parent 1ed4457 commit 294d97c
Show file tree
Hide file tree
Showing 3 changed files with 21 additions and 56 deletions.
2 changes: 1 addition & 1 deletion docs/structure-of-a-contract.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Vyper supports several source code directives to control compiler modes and help
Version Pragma
--------------

The version pragma ensures that a contract is only compiled by the intended compiler version, or range of versions. Version strings use `NPM <https://docs.npmjs.com/about-semantic-versioning>`_ style syntax.
The version pragma ensures that a contract is only compiled by the intended compiler version, or range of versions. Version strings use `NPM <https://docs.npmjs.com/about-semantic-versioning>`_ style syntax. Starting from v0.4.0 and up, version strings will use `PEP440 version specifiers <https://peps.python.org/pep-0440/#version-specifiers>_`.

As of 0.3.10, the recommended way to specify the version pragma is as follows:

Expand Down
32 changes: 7 additions & 25 deletions tests/ast/test_pre_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,9 @@ def set_version(version):
"0.1.1",
">0.0.1",
"^0.1.0",
"<=1.0.0 >=0.1.0",
"0.1.0 - 1.0.0",
"~0.1.0",
"0.1",
"0",
"*",
"x",
"0.x",
"0.1.x",
"0.2.0 || 0.1.1",
"<=1.0.0,>=0.1.0",
# "0.1.0 - 1.0.0",
"~=0.1.0",
]
invalid_versions = [
"0.1.0",
Expand All @@ -44,7 +37,6 @@ def set_version(version):
"1.x",
"0.2.x",
"0.2.0 || 0.1.3",
"==0.1.1",
"abc",
]

Expand All @@ -70,29 +62,19 @@ def test_invalid_version_pragma(file_version, mock_version):
"<0.1.1-rc.1",
">0.1.1a1",
">0.1.1-alpha.1",
"0.1.1a9 - 0.1.1-rc.10",
">=0.1.1a9,<=0.1.1-rc.10",
"<0.1.1b8",
"<0.1.1rc1",
"<0.2.0",
]
prerelease_invalid_versions = [
">0.1.1-beta.9",
">0.1.1b9",
"0.1.1b8",
"0.1.1rc2",
"0.1.1-rc.9 - 0.1.1-rc.10",
"<0.2.0",
pytest.param(
"<0.1.1b1",
marks=pytest.mark.xfail(
reason="https://github.com/rbarrois/python-semanticversion/issues/100"
),
),
pytest.param(
"<0.1.1a9",
marks=pytest.mark.xfail(
reason="https://github.com/rbarrois/python-semanticversion/issues/100"
),
),
"<0.1.1b1",
"<0.1.1a9",
]


Expand Down
43 changes: 13 additions & 30 deletions vyper/ast/pre_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from tokenize import COMMENT, NAME, OP, TokenError, TokenInfo, tokenize, untokenize

from semantic_version import NpmSpec, Version
from packaging.specifiers import InvalidSpecifier, SpecifierSet

from vyper.compiler.settings import OptimizationLevel, Settings

Expand All @@ -12,50 +12,33 @@
from vyper.exceptions import StructureException, SyntaxException, VersionException
from vyper.typing import ModificationOffsets, ParserPosition

VERSION_ALPHA_RE = re.compile(r"(?<=\d)a(?=\d)") # 0.1.0a17
VERSION_BETA_RE = re.compile(r"(?<=\d)b(?=\d)") # 0.1.0b17
VERSION_RC_RE = re.compile(r"(?<=\d)rc(?=\d)") # 0.1.0rc17


def _convert_version_str(version_str: str) -> str:
"""
Convert loose version (0.1.0b17) to strict version (0.1.0-beta.17)
"""
version_str = re.sub(VERSION_ALPHA_RE, "-alpha.", version_str) # 0.1.0-alpha.17
version_str = re.sub(VERSION_BETA_RE, "-beta.", version_str) # 0.1.0-beta.17
version_str = re.sub(VERSION_RC_RE, "-rc.", version_str) # 0.1.0-rc.17

return version_str


def validate_version_pragma(version_str: str, start: ParserPosition) -> None:
"""
Validates a version pragma directive against the current compiler version.
"""
from vyper import __version__

# NOTE: should be `x.y.z.*`
installed_version = ".".join(__version__.split(".")[:3])

strict_file_version = _convert_version_str(version_str)
strict_compiler_version = Version(_convert_version_str(installed_version))

if len(strict_file_version) == 0:
if len(version_str) == 0:
raise VersionException("Version specification cannot be empty", start)

# 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)

try:
npm_spec = NpmSpec(strict_file_version)
except ValueError:
spec = SpecifierSet(version_str)
except InvalidSpecifier:
raise VersionException(
f'Version specification "{version_str}" is not a valid NPM semantic '
f"version specification",
start,
f'Version specification "{version_str}" is not a valid PEP440 specifier', start
)

if not npm_spec.match(strict_compiler_version):
if not spec.contains(__version__, prereleases=True):
raise VersionException(
f'Version specification "{version_str}" is not compatible '
f'with compiler version "{installed_version}"',
f'with compiler version "{__version__}"',
start,
)

Expand Down

0 comments on commit 294d97c

Please sign in to comment.