Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/core-ganache.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
include:
- python-version: "3.10"
infura-key: ddddf0c53f254d36aa76ce4e3a6a390e
Expand All @@ -62,8 +62,6 @@ jobs:
infura-key: ddddf0c53f254d36aa76ce4e3a6a390e
- python-version: "3.14"
infura-key: 1668fecbc9c242d58253476103a42ce9
- python-version: "3.14t"
infura-key: 21317ddb5ded42ce8d40c7d78f90474f
- os: ubuntu-latest
ccache-dir: ~/.cache/ccache
- os: macos-latest
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ concurrency:

jobs:
lint-all-the-things:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pip-compile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
fi

- name: Commit changes
if: env.changes_detected == 'true'
if: env.changes_detected == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
run: |
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
Expand Down
9 changes: 4 additions & 5 deletions brownie/_c_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
import importlib
import pathlib
import re
from typing import Final
from typing import Final, TypeAlias

import faster_eth_utils.toolz
import faster_hexbytes
import semantic_version
import ujson
from packaging.version import Version as PackagingVersion

# BUILTINS
# collections
Expand Down Expand Up @@ -56,9 +56,8 @@
# faster_hexbytes
HexBytes: Final = faster_hexbytes.HexBytes

# semantic_version
NpmSpec: Final = semantic_version.NpmSpec
Version: Final = semantic_version.Version
# packaging
Version: TypeAlias = PackagingVersion

# toolz
mapcat: Final = faster_eth_utils.toolz.mapcat
Expand Down
203 changes: 203 additions & 0 deletions brownie/_versioning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/python3

import re
from collections.abc import Iterable
from typing import Any

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

# Vyper switched version pragmas from semantic_version.NpmSpec to
# packaging.specifiers.SpecifierSet in v0.3.10.
VYPER_PEP440_PRAGMA_VERSION = Version("0.3.10")


def parse_compiler_version(version: Any) -> Version:
text = str(version).removeprefix("v")
return Version(text.split("+", 1)[0])


def parse_compiler_versions(versions: Iterable[Any]) -> list[Version]:
return [parse_compiler_version(version) for version in versions]


def next_minor(version: Version) -> Version:
return Version(f"{version.major}.{version.minor + 1}.0")


def _normalize_legacy_vyper_prerelease(expression: str) -> str:
expression = re.sub(r"(?<=\d)alpha(?=\d)", "-alpha.", expression, flags=re.I)
expression = re.sub(r"(?<=\d)beta(?=\d)", "-beta.", expression, flags=re.I)
expression = re.sub(r"(?<=\d)a(?=\d)", "-alpha.", expression, flags=re.I)
expression = re.sub(r"(?<=\d)b(?=\d)", "-beta.", expression, flags=re.I)
return re.sub(r"(?<=\d)rc(?=\d)", "-rc.", expression, flags=re.I)


def _reject_solidity_version_qualifiers(expression: str) -> None:
# solc supports qualifiers on its own compiler version, not inside pragma
# match expressions. Hyphen ranges remain valid because they use whitespace.
if re.search(r"\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z]|\+[0-9A-Za-z])", expression):
raise ValueError(f"Invalid Solidity version pragma: {expression}")


def _normalize_npm_expression(expression: str) -> str:
expression = expression.strip()
expression = re.sub(r"""(["'])([^"']+)\1""", r"\2", expression)
expression = re.sub(r"(<=|>=|<|>|=|\^|~)\s+(?=[v0-9xX*])", r"\1", expression)
return re.sub(r"(?<=[0-9xX*])(?=(?:<=|>=|<|>|=|\^|~)[v0-9xX*])", " ", expression)


def _normalize_solidity_npm_expression(expression: str) -> str:
expression = _normalize_npm_expression(expression)
# solc treats ^0.0.patch as >=0.0.patch,<0.1.0, while NpmSpec uses
# npm's narrower >=0.0.patch,<0.0.(patch+1) range.
return re.sub(r"(?<!\S)\^0\.0\.(\d+)(?!\S)", r">=0.0.\1 <0.1.0", expression)


def _to_semantic_version(version: Any) -> semantic_version.Version:
version = parse_compiler_version(version)
prerelease = ""
if version.pre:
phase, number = version.pre
prerelease_phase = {"a": "alpha", "b": "beta"}.get(phase, phase)
prerelease = f"-{prerelease_phase}.{number}"
return semantic_version.Version(f"{version.major}.{version.minor}.{version.micro}{prerelease}")


def _reject_solidity_npm_expression(expression: str) -> None:
for token in re.findall(r"(?<![0-9A-Za-z_.-])v\d+(?:\.\d+){0,2}", expression):
raise ValueError(f"Invalid Solidity version pragma: {token}")


class _NpmSpec:
def __init__(self, expression: str, *, normalize_legacy_vyper: bool = False) -> None:
self.expression = expression
if normalize_legacy_vyper:
expression = _normalize_legacy_vyper_prerelease(expression)
try:
self._spec = semantic_version.NpmSpec(_normalize_npm_expression(expression))
except Exception:
raise ValueError(f"Invalid npm version range: {expression}")

def __contains__(self, version: Any) -> bool:
return self._spec.match(_to_semantic_version(version))


class SolidityPragmaSpec:
"""Solidity pragma matcher with solc-style multiple-pragma intersection."""

def __init__(self, expressions: str | Iterable[str]) -> None:
if isinstance(expressions, str):
expressions = [expressions]

self.expressions = tuple(expressions)
if not self.expressions:
raise ValueError("No Solidity version pragma")

for expression in self.expressions:
_reject_solidity_version_qualifiers(expression)
_reject_solidity_npm_expression(expression)
try:
self._ranges = tuple(
_NpmSpec(_normalize_solidity_npm_expression(expression))
for expression in self.expressions
)
except Exception:
raise ValueError(f"Invalid Solidity version pragma: {self}")

def __contains__(self, version: Any) -> bool:
version = parse_compiler_version(version)
return all(version in range_spec for range_spec in self._ranges)

def select(self, versions: Iterable[Any]) -> Version | None:
return max(
(parse_compiler_version(version) for version in versions if version in self),
default=None,
)

def __eq__(self, other: object) -> bool:
return isinstance(other, SolidityPragmaSpec) and self.expressions == other.expressions

def __str__(self) -> str:
return " && ".join(self.expressions)

def __repr__(self) -> str:
return f"SolidityPragmaSpec({self.expressions!r})"


def _to_vyper_modern_expression(expression: str) -> str:
if re.match("[v0-9]", expression):
expression = f"=={expression}"
return re.sub(r"^\^", "~=", expression)


class _VyperLegacySpec:
def __init__(self, expression: str) -> None:
try:
self._spec = _NpmSpec(expression, normalize_legacy_vyper=True)
except Exception:
raise ValueError(f"Invalid Vyper version pragma: {expression}")

def __contains__(self, version: Any) -> bool:
return version in self._spec


def _to_vyper_legacy_spec(expression: str) -> _VyperLegacySpec:
try:
return _VyperLegacySpec(expression)
except Exception:
raise ValueError(f"Invalid Vyper version pragma: {expression}")


class VyperPragmaSpec:
"""Vyper pragma matcher with modern PEP440 and legacy @version support."""

def __init__(self, expression: str, allow_legacy: bool = False) -> None:
self.expression = expression
self.allow_legacy = allow_legacy

try:
self._modern_spec: SpecifierSet | None = SpecifierSet(
_to_vyper_modern_expression(expression)
)
except InvalidSpecifier:
self._modern_spec = None

try:
self._legacy_spec: _VyperLegacySpec | None = _to_vyper_legacy_spec(expression)
except Exception:
self._legacy_spec = None

if self._modern_spec is None and (not allow_legacy or self._legacy_spec is None):
raise ValueError(f"Invalid Vyper version pragma: {expression}")

def __contains__(self, version: Any) -> bool:
version = parse_compiler_version(version)

if version >= VYPER_PEP440_PRAGMA_VERSION:
if self._modern_spec is None:
return False
return self._modern_spec.contains(version, prereleases=True)

if self.allow_legacy and self._legacy_spec is not None:
return version in self._legacy_spec

return False

def select(self, versions: Iterable[Any]) -> Version | None:
return max(
(parse_compiler_version(version) for version in versions if version in self),
default=None,
)

def __eq__(self, other: object) -> bool:
if isinstance(other, VyperPragmaSpec):
return self.expression == other.expression and self.allow_legacy == other.allow_legacy
return False

def __str__(self) -> str:
return self.expression

def __repr__(self) -> str:
return f"VyperPragmaSpec({self.expression!r}, allow_legacy={self.allow_legacy!r})"
2 changes: 1 addition & 1 deletion brownie/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ class BadProjectName(Exception):

@final
class CompilerError(Exception):
def __init__(self, e: type[psutil.Popen], compiler: str = "Compiler") -> None:
def __init__(self, e: Any, compiler: str = "Compiler") -> None:
self.compiler: Final = compiler

err_json: dict[str, list[dict[str, str]]] = yaml.safe_load(e.stdout_data)
Expand Down
7 changes: 4 additions & 3 deletions brownie/network/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ujson_loads,
)
from brownie._config import BROWNIE_FOLDER, CONFIG, REQUEST_HEADERS, _load_project_compiler_config
from brownie._versioning import next_minor, parse_compiler_version, parse_compiler_versions
from brownie.convert.datatypes import Wei
from brownie.convert.normalize import format_input, format_output
from brownie.convert.utils import (
Expand Down Expand Up @@ -1185,7 +1186,7 @@ def get_solc_version(cls, compiler_str: str, address: str) -> Version:
address: str
The contract address to check for.
"""
version = Version(compiler_str.lstrip("v")).truncate()
version = parse_compiler_version(compiler_str)

compiler_config = _load_project_compiler_config(Path(os.getcwd()))
solc_config = compiler_config["solc"]
Expand All @@ -1198,8 +1199,8 @@ def get_solc_version(cls, compiler_str: str, address: str) -> Version:
needs_patch_version = address in use_latest_patch

if needs_patch_version:
versions = [Version(str(i)) for i in solcx.get_installable_solc_versions()]
for v in filter(lambda x: x < version.next_minor(), versions):
versions = parse_compiler_versions(solcx.get_installable_solc_versions())
for v in filter(lambda x: x < next_minor(version), versions):
if v > version:
version = v

Expand Down
7 changes: 4 additions & 3 deletions brownie/project/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import solcast
from eth_typing import ABIElement, HexStr

from brownie._c_constants import Path, Version, deepcopy, sha1, ujson_loads
from brownie._c_constants import Path, deepcopy, sha1, ujson_loads
from brownie._config import _get_data_folder
from brownie.exceptions import UnsupportedLanguage
from brownie.project import sources
Expand All @@ -16,7 +16,7 @@
install_solc,
set_solc_version,
)
from brownie.project.compiler.utils import _get_alias, merge_natspec
from brownie.project.compiler.utils import _get_alias, merge_natspec, parse_compiler_version
from brownie.project.compiler.vyper import find_vyper_versions, set_vyper_version
from brownie.typing import (
CompilerConfig,
Expand Down Expand Up @@ -154,7 +154,8 @@ def compile_and_format(
k: v
for k in interface_sources
if Path(k).suffix == ".sol"
and Version(version) in sources.get_pragma_spec(v := interface_sources[k], k)
and parse_compiler_version(version)
in sources.get_pragma_spec(v := interface_sources[k], k)
}

to_compile = {key: contract_sources[key] for key in contract_sources if key in path_list}
Expand Down
Loading
Loading