Skip to content

Commit

Permalink
feat: use new settings and implement compile code [APE-1512] (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Nov 13, 2023
1 parent e8a53df commit c11e583
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 128 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
rev: v4.5.0
hooks:
- id: check-yaml

Expand All @@ -10,24 +10,24 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.11.0
hooks:
- id: black
name: black

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
rev: 6.1.0
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
rev: v1.7.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools, pydantic, types-pkg-resources]

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.14
rev: 0.7.17
hooks:
- id: mdformat
additional_dependencies: [mdformat-gfm, mdformat-frontmatter]
Expand Down
9 changes: 0 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,3 @@ If you are opening a work-in-progress pull request to verify that it passes CI t
[marking it as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests).

Join the ApeWorX [Discord](https://discord.gg/apeworx) if you have any questions.

## Testing

By default, the test suite will use a new, temporary path for the Solidity compiler installations.
This ensures that the tests always run from a clean slate without any relying on existing installations.

If you wish to use your existing `~/.solcx` installations instead, you must set the environment variable `APE_SOLIDITY_USE_SYSTEM_SOLC=1`.

This will ensure that py-solc-x's default path will be used, but any compilers installed as part of the tests will not be removed after tests have completed.
108 changes: 71 additions & 37 deletions ape_solidity/_utils.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import json
import os
import re
import subprocess
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional, Set, Union
from typing import Dict, List, Optional, Sequence, Set, Union

from ape._pydantic_compat import BaseModel, validator
from ape.exceptions import CompilerError
from ape.logging import logger
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion
from packaging.version import Version
from packaging.version import Version as _Version
from pydantic import BaseModel, validator
from semantic_version import NpmSpec, Version # type: ignore
from solcx.exceptions import SolcError # type: ignore
from solcx.install import get_executable # type: ignore
from solcx.wrapper import VERSION_REGEX # type: ignore
from solcx.install import get_executable
from solcx.wrapper import get_solc_version as get_solc_version_from_binary

from ape_solidity.exceptions import IncorrectMappingFormatError

Expand Down Expand Up @@ -138,35 +137,64 @@ def get_import_lines(source_paths: Set[Path]) -> Dict[Path, List[str]]:
return imports_dict


def get_pragma_spec(source_file_path: Path) -> Optional[NpmSpec]:
def get_pragma_spec_from_path(source_file_path: Union[Path, str]) -> Optional[SpecifierSet]:
"""
Extracts pragma information from Solidity source code.
Args:
source_file_path: Solidity source code
Returns: NpmSpec object or None, if no valid pragma is found
source_file_path (Union[Path, str]): Solidity source file path.
Returns:
``packaging.specifiers.SpecifierSet``
"""
if not source_file_path.is_file():
path = Path(source_file_path)
if not path.is_file():
return None

source = source_file_path.read_text()
pragma_match = next(re.finditer(r"(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)", source), None)
if pragma_match is None:
source_str = path.read_text()
return get_pragma_spec_from_str(source_str)


def get_pragma_spec_from_str(source_str: str) -> Optional[SpecifierSet]:
if not (
pragma_match := next(
re.finditer(r"(?:\n|^)\s*pragma\s*solidity\s*([^;\n]*)", source_str), None
)
):
return None # Try compiling with latest

# The following logic handles the case where the user puts a space
# between the operator and the version number in the pragam string,
# between the operator and the version number in the pragma string,
# such as `solidity >= 0.4.19 < 0.7.0`.
pragma_expression = ""
pragma_parts = pragma_match.groups()[0].split()
num_parts = len(pragma_parts)
for index in range(num_parts):
pragma_expression += pragma_parts[index]
if any([c.isdigit() for c in pragma_parts[index]]) and index < num_parts - 1:
pragma_expression += " "

try:
return NpmSpec(pragma_expression)
def _to_spec(item: str) -> str:
item = item.replace("^", "~=")
if item and item[0].isnumeric():
return f"=={item}"
elif item and len(item) >= 2 and item[0] == "=" and item[1] != "=":
return f"={item}"

return item

pragma_parts_fixed = []
builder = ""
for sub_part in pragma_parts:
if not any(c.isnumeric() for c in sub_part):
# Handle pragma with spaces between constraint and values
# like `>= 0.6.0`.
builder += sub_part
continue
elif builder:
spec = _to_spec(f"{builder}{sub_part}")
builder = ""
else:
spec = _to_spec(sub_part)

pragma_parts_fixed.append(spec)

try:
return SpecifierSet(",".join(pragma_parts_fixed))
except ValueError as err:
logger.error(str(err))
return None
Expand All @@ -176,21 +204,15 @@ def load_dict(data: Union[str, dict]) -> Dict:
return data if isinstance(data, dict) else json.loads(data)


def get_version_with_commit_hash(version: Union[str, Version]) -> Version:
# Borrowed from:
# https://github.com/iamdefinitelyahuman/py-solc-x/blob/master/solcx/wrapper.py#L15-L28
if "+commit" in str(version):
return Version(str(version))
def add_commit_hash(version: Union[str, Version]) -> Version:
vers = Version(f"{version}") if isinstance(version, str) else version
has_commit = len(f"{vers}") > len(vers.base_version)
if has_commit:
# Already added.
return vers

executable = get_executable(version)
stdout_data = subprocess.check_output([str(executable), "--version"], encoding="utf8")
try:
match = next(re.finditer(VERSION_REGEX, stdout_data))
version_str = "".join(match.groups())
except StopIteration:
raise SolcError("Could not determine the solc binary version")

return Version.coerce(version_str)
solc = get_executable(version=vers)
return get_solc_version_from_binary(solc, with_commit_hash=True)


def verify_contract_filepaths(contract_filepaths: List[Path]) -> Set[Path]:
Expand All @@ -200,3 +222,15 @@ def verify_contract_filepaths(contract_filepaths: List[Path]) -> Set[Path]:

sources_str = "', '".join(invalid_files)
raise CompilerError(f"Unable to compile '{sources_str}' using Solidity compiler.")


def select_version(pragma_spec: SpecifierSet, options: Sequence[Version]) -> Optional[Version]:
choices = sorted(list(pragma_spec.filter(options)), reverse=True)
return choices[0] if choices else None


def strip_commit_hash(version: Union[str, Version]) -> Version:
"""
Version('0.8.21+commit.d9974bed') => Version('0.8.21')> the simple way.
"""
return Version(f"{str(version).split('+')[0].strip()}")
Loading

0 comments on commit c11e583

Please sign in to comment.