Skip to content

Commit

Permalink
fix[ux]: raise VersionException with source info (#3920)
Browse files Browse the repository at this point in the history
this commit adds source code information to the exception thrown in
`validate_version_pragma()` to improve user experience when dealing with
imports with invalid (or incompatible) version pragmas.
  • Loading branch information
tserg authored Apr 9, 2024
1 parent 8fcbde2 commit 3ea2ae1
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 11 deletions.
38 changes: 34 additions & 4 deletions tests/unit/ast/test_pre_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest

from vyper import compile_code
from vyper.ast.pre_parser import pre_parse, validate_version_pragma
from vyper.compiler.phases import CompilerData
from vyper.compiler.settings import OptimizationLevel, Settings
Expand Down Expand Up @@ -45,14 +46,14 @@ def set_version(version):
@pytest.mark.parametrize("file_version", valid_versions)
def test_valid_version_pragma(file_version, mock_version):
mock_version(COMPILER_VERSION)
validate_version_pragma(f"{file_version}", (SRC_LINE))
validate_version_pragma(f"{file_version}", file_version, (SRC_LINE))


@pytest.mark.parametrize("file_version", invalid_versions)
def test_invalid_version_pragma(file_version, mock_version):
mock_version(COMPILER_VERSION)
with pytest.raises(VersionException):
validate_version_pragma(f"{file_version}", (SRC_LINE))
validate_version_pragma(f"{file_version}", file_version, (SRC_LINE))


prerelease_valid_versions = [
Expand Down Expand Up @@ -82,14 +83,14 @@ def test_invalid_version_pragma(file_version, mock_version):
@pytest.mark.parametrize("file_version", prerelease_valid_versions)
def test_prerelease_valid_version_pragma(file_version, mock_version):
mock_version(PRERELEASE_COMPILER_VERSION)
validate_version_pragma(file_version, (SRC_LINE))
validate_version_pragma(file_version, file_version, (SRC_LINE))


@pytest.mark.parametrize("file_version", prerelease_invalid_versions)
def test_prerelease_invalid_version_pragma(file_version, mock_version):
mock_version(PRERELEASE_COMPILER_VERSION)
with pytest.raises(VersionException):
validate_version_pragma(file_version, (SRC_LINE))
validate_version_pragma(file_version, file_version, (SRC_LINE))


pragma_examples = [
Expand Down Expand Up @@ -224,3 +225,32 @@ def test_parse_pragmas(code, pre_parse_settings, compiler_data_settings, mock_ve
def test_invalid_pragma(code):
with pytest.raises(StructureException):
pre_parse(code)


def test_version_exception_in_import(make_input_bundle):
lib_version = "~=0.3.10"
lib = f"""
#pragma version {lib_version}
@external
def foo():
pass
"""

code = """
import lib
uses: lib
@external
def bar():
pass
"""
input_bundle = make_input_bundle({"lib.vy": lib})

with pytest.raises(VersionException) as excinfo:
compile_code(code, input_bundle=input_bundle)
annotation = excinfo.value.annotations[0]
assert annotation.lineno == 2
assert annotation.col_offset == 0
assert annotation.full_source_code == lib
15 changes: 9 additions & 6 deletions vyper/ast/pre_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
from vyper.typing import ModificationOffsets, ParserPosition


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

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

# X.Y.Z or vX.Y.Z => ==X.Y.Z, ==vX.Y.Z
if re.match("[v0-9]", version_str):
Expand All @@ -34,14 +34,17 @@ def validate_version_pragma(version_str: str, start: ParserPosition) -> None:
spec = SpecifierSet(version_str)
except InvalidSpecifier:
raise VersionException(
f'Version specification "{version_str}" is not a valid PEP440 specifier', start
f'Version specification "{version_str}" is not a valid PEP440 specifier',
full_source_code,
*start,
)

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


Expand Down Expand Up @@ -176,7 +179,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
if settings.compiler_version is not None:
raise StructureException("compiler version specified twice!", start)
compiler_version = contents.removeprefix("@version ").strip()
validate_version_pragma(compiler_version, start)
validate_version_pragma(compiler_version, code, start)
settings.compiler_version = compiler_version

if contents.startswith("pragma "):
Expand All @@ -185,7 +188,7 @@ def pre_parse(code: str) -> tuple[Settings, ModificationOffsets, dict, str]:
if settings.compiler_version is not None:
raise StructureException("pragma version specified twice!", start)
compiler_version = pragma.removeprefix("version ").strip()
validate_version_pragma(compiler_version, start)
validate_version_pragma(compiler_version, code, start)
settings.compiler_version = compiler_version

elif pragma.startswith("optimize "):
Expand Down
2 changes: 1 addition & 1 deletion vyper/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ class InstantiationException(StructureException):
"""Variable or expression cannot be instantiated"""


class VersionException(VyperException):
class VersionException(SyntaxException):
"""Version string is malformed or incompatible with this compiler version."""


Expand Down

0 comments on commit 3ea2ae1

Please sign in to comment.