|
2 | 2 | import re |
3 | 3 | import subprocess |
4 | 4 | import time |
5 | | -from collections.abc import Iterable |
| 5 | +from collections.abc import Iterable, Iterator |
6 | 6 | from enum import Enum |
7 | 7 | from pathlib import Path |
8 | 8 | from typing import TYPE_CHECKING, Any |
|
15 | 15 | from eth_utils import is_0x_prefixed |
16 | 16 | from ethpm_types import ASTNode, PCMap, SourceMapItem |
17 | 17 | from packaging.specifiers import InvalidSpecifier, SpecifierSet |
| 18 | +from packaging.version import Version |
| 19 | +from semantic_version import NpmSpec # type: ignore[import-untyped] |
| 20 | +from semantic_version import Version as NpmVersion # type: ignore[import-untyped] |
18 | 21 | from vvm.exceptions import UnknownOption, UnknownValue # type: ignore |
19 | 22 |
|
20 | | -from ape_vyper.exceptions import RuntimeErrorType, VyperError, VyperInstallError |
| 23 | +from ape_vyper.exceptions import RuntimeErrorType, VyperCompileError, VyperError, VyperInstallError |
21 | 24 |
|
22 | 25 | if TYPE_CHECKING: |
23 | 26 | from ape.types.trace import SourceTraceback |
24 | 27 | from ethpm_types.source import Function |
25 | | - from packaging.version import Version |
26 | 28 |
|
27 | 29 | Optimization = str | bool |
28 | 30 | EVM_VERSION_DEFAULT = { |
@@ -82,34 +84,106 @@ def install_vyper(version: "Version"): |
82 | 84 | ) from err |
83 | 85 |
|
84 | 86 |
|
85 | | -def get_version_pragma_spec(source: str | Path) -> SpecifierSet | None: |
| 87 | +VERSION_PRAGMA_PATTERN = re.compile( |
| 88 | + r"(?:\n|^)\s*#\s*(?P<style>@version|pragma\s+version)\s*(?P<version>[^\n]*)" |
| 89 | +) |
| 90 | +# Vyper 0.3.10 switched version pragma matching from NpmSpec to SpecifierSet. |
| 91 | +VYPER_PEP440_PRAGMA_START_VERSION = Version("0.3.10") |
| 92 | + |
| 93 | + |
| 94 | +def _as_pep440_spec(pragma_str: str) -> str: |
| 95 | + pragma_str = pragma_str.replace("^", "~=") |
| 96 | + if pragma_str and pragma_str[0].isnumeric(): |
| 97 | + return f"=={pragma_str}" |
| 98 | + |
| 99 | + return pragma_str |
| 100 | + |
| 101 | + |
| 102 | +def _as_npm_version(version: Version) -> NpmVersion: |
| 103 | + version_str = str(version) |
| 104 | + version_str = re.sub(r"(?<=\d)a(?=\d)", "-alpha.", version_str) |
| 105 | + version_str = re.sub(r"(?<=\d)b(?=\d)", "-beta.", version_str) |
| 106 | + version_str = re.sub(r"(?<=\d)rc(?=\d)", "-rc.", version_str) |
| 107 | + return NpmVersion(version_str) |
| 108 | + |
| 109 | + |
| 110 | +class VyperVersionSpecifier: |
| 111 | + def __init__(self, pragma_str: str, style: str, source_path: Path | None = None): |
| 112 | + self.pragma_str = pragma_str |
| 113 | + self.style = style |
| 114 | + self.source_path = source_path |
| 115 | + self._npm_spec: NpmSpec | None = None |
| 116 | + self._pep440_spec: SpecifierSet | None = None |
| 117 | + |
| 118 | + try: |
| 119 | + self._npm_spec = NpmSpec(pragma_str) |
| 120 | + except ValueError: |
| 121 | + pass |
| 122 | + |
| 123 | + try: |
| 124 | + self._pep440_spec = SpecifierSet(_as_pep440_spec(pragma_str)) |
| 125 | + except InvalidSpecifier: |
| 126 | + pass |
| 127 | + |
| 128 | + if not self._npm_spec and not self._pep440_spec: |
| 129 | + raise self._error() |
| 130 | + |
| 131 | + if self.is_modern_pragma and not self._pep440_spec: |
| 132 | + raise self._error() |
| 133 | + |
| 134 | + @property |
| 135 | + def is_modern_pragma(self) -> bool: |
| 136 | + return self.style.startswith("pragma") |
| 137 | + |
| 138 | + def match(self, version: Version) -> bool: |
| 139 | + if version >= VYPER_PEP440_PRAGMA_START_VERSION: |
| 140 | + return bool(self._pep440_spec and self._pep440_spec.contains(version, prereleases=True)) |
| 141 | + |
| 142 | + return not self.is_modern_pragma and bool( |
| 143 | + self._npm_spec and self._npm_spec.match(_as_npm_version(version)) |
| 144 | + ) |
| 145 | + |
| 146 | + def contains(self, version: str | Version) -> bool: |
| 147 | + return self.match(version if isinstance(version, Version) else Version(version)) |
| 148 | + |
| 149 | + def filter(self, versions: Iterable[Version]) -> Iterator[Version]: |
| 150 | + return (version for version in versions if self.match(version)) |
| 151 | + |
| 152 | + def _error(self) -> VyperCompileError: |
| 153 | + source_label = f" in '{self.source_path}'" if self.source_path else "" |
| 154 | + return VyperCompileError( |
| 155 | + f"Invalid Vyper version pragma{source_label}: '{self.pragma_str}'." |
| 156 | + ) |
| 157 | + |
| 158 | + def __str__(self) -> str: |
| 159 | + return self.pragma_str |
| 160 | + |
| 161 | + |
| 162 | +def get_version_pragma_spec(source: str | Path) -> VyperVersionSpecifier | None: |
86 | 163 | """ |
87 | 164 | Extracts version pragma information from Vyper source code. |
88 | 165 |
|
89 | 166 | Args: |
90 | 167 | source (str): Vyper source code |
91 | 168 |
|
92 | 169 | Returns: |
93 | | - ``packaging.specifiers.SpecifierSet``, or None if no valid pragma is found. |
| 170 | + ``VyperVersionSpecifier``, or None if no pragma is found. |
94 | 171 | """ |
95 | | - _version_pragma_patterns: tuple[str, str] = ( |
96 | | - r"(?:\n|^)\s*#\s*@version\s*([^\n]*)", |
97 | | - r"(?:\n|^)\s*#\s*pragma\s+version\s*([^\n]*)", |
98 | | - ) |
99 | | - |
100 | 172 | source_str = source if isinstance(source, str) else source.read_text(encoding="utf8") |
101 | | - for pattern in _version_pragma_patterns: |
102 | | - for match in re.finditer(pattern, source_str): |
103 | | - raw_pragma = match.groups()[0] |
104 | | - pragma_str = " ".join(raw_pragma.split()).replace("^", "~=") |
105 | | - if pragma_str and pragma_str[0].isnumeric(): |
106 | | - pragma_str = f"=={pragma_str}" |
107 | | - |
108 | | - try: |
109 | | - return SpecifierSet(pragma_str) |
110 | | - except InvalidSpecifier: |
111 | | - logger.warning(f"Invalid pragma spec: '{raw_pragma}'. Trying latest.") |
112 | | - return None |
| 173 | + source_path = source if isinstance(source, Path) else None |
| 174 | + if pragma_match := next(re.finditer(VERSION_PRAGMA_PATTERN, source_str), None): |
| 175 | + raw_pragma = pragma_match.group("version") |
| 176 | + pragma_str = " ".join(raw_pragma.split()) |
| 177 | + if not pragma_str: |
| 178 | + source_label = f" in '{source_path}'" if source_path else "" |
| 179 | + raise VyperCompileError(f"Invalid Vyper version pragma{source_label}: missing version.") |
| 180 | + |
| 181 | + return VyperVersionSpecifier( |
| 182 | + pragma_str, |
| 183 | + style=" ".join(pragma_match.group("style").split()), |
| 184 | + source_path=source_path, |
| 185 | + ) |
| 186 | + |
113 | 187 | return None |
114 | 188 |
|
115 | 189 |
|
@@ -260,7 +334,9 @@ def seek() -> Path | None: |
260 | 334 | return None |
261 | 335 |
|
262 | 336 |
|
263 | | -def safe_append(data: dict, version: "Version | SpecifierSet", paths: Path | set): |
| 337 | +def safe_append( |
| 338 | + data: dict, version: "Version | SpecifierSet | VyperVersionSpecifier", paths: Path | set |
| 339 | +): |
264 | 340 | if isinstance(paths, Path): |
265 | 341 | paths = {paths} |
266 | 342 | if version in data: |
|
0 commit comments