Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: output_format #21

Merged
merged 9 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
44 changes: 22 additions & 22 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,95 +28,95 @@ jobs:
- name: Run Tox
run: tox -e lint

py36:
py37:
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Setup Python 3.6
- name: Setup Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.6
python-version: 3.7

- name: Install Tox
run: pip install tox wheel

- name: Run Tox
run: tox -e py36
run: tox -e py37

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
fail_ci_if_error: true

py37:
py38:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Setup Python 3.7
- name: Setup Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8

- name: Install Tox
run: pip install tox wheel

- name: Run Tox
run: tox -e py37
run: tox -e py38

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
fail_ci_if_error: true

py38:
runs-on: ubuntu-latest
py39:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v2

- name: Setup Python 3.8
- name: Setup Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9

- name: Install Tox
run: pip install tox wheel

- name: Run Tox
run: tox -e py38
run: tox -e py39

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
fail_ci_if_error: true

py39:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
py312:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Setup Python 3.9
- name: Setup Python 3.12
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: 3.12

- name: Install Tox
run: pip install tox wheel

- name: Run Tox
run: tox -e py39
run: tox -e py312

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/

# PyCharm
.idea/
8 changes: 6 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/python3

import pytest
from requests import ConnectionError
from packaging.version import Version
from requests import ConnectionError

import vvm

Expand Down Expand Up @@ -67,8 +67,12 @@ def all_versions(request):
@pytest.fixture
def foo_source(all_versions):
visibility = "external" if all_versions >= Version("0.2.0") else "public"
interface = "IERC20" if all_versions >= Version("0.4.0a") else "ERC20"
import_path = "ethereum.ercs" if all_versions >= Version("0.4.0a") else "vyper.interfaces"
pragma_version = "pragma version" if all_versions >= Version("0.3.10") else "@version"
yield f"""
from vyper.interfaces import ERC20
#{pragma_version} ^{all_versions}
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
from {import_path} import {interface}

@{visibility}
def foo() -> int128:
Expand Down
15 changes: 15 additions & 0 deletions tests/test_compile_source.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from packaging.version import Version

import vvm

Expand Down Expand Up @@ -30,3 +31,17 @@ def foo() -> int128:
return 42
"""
vvm.compile_source(source, vyper_version=version_str)


def test_compile_metadata(foo_source, all_versions):
if all_versions <= Version("0.3.1"):
raise pytest.skip("metadata output not supported in vyper < 0.3.2")
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
output = vvm.compile_source(foo_source, output_format="metadata")
assert "function_info" in output


def test_compile_metadata_from_file(foo_path, all_versions):
if all_versions <= Version("0.3.1"):
raise pytest.skip("metadata output not supported in vyper < 0.3.2")
output = vvm.compile_files([foo_path], output_format="metadata")
assert "function_info" in output
26 changes: 26 additions & 0 deletions tests/test_detect_vyper_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import pytest

from vvm import detect_vyper_version_from_source


def test_detect_vyper_version_from_source(foo_source, all_versions):
assert detect_vyper_version_from_source(foo_source) == str(all_versions)


@pytest.mark.parametrize(
"version_str,decorator",
[
("0.1.0b17", "public"),
("0.3.0-beta17", "external"),
("0.4.0rc6", "external"),
],
)
def test_detect_vyper_version_beta(version_str, decorator):
source = f"""
# @version {version_str}

@{decorator}
def foo() -> int128:
return 42
"""
assert detect_vyper_version_from_source(source) == version_str
8 changes: 7 additions & 1 deletion vvm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,10 @@
install_vyper,
set_vyper_version,
)
from vvm.main import compile_files, compile_source, compile_standard, get_vyper_version
from vvm.main import (
compile_files,
compile_source,
compile_standard,
detect_vyper_version_from_source,
get_vyper_version,
)
95 changes: 71 additions & 24 deletions vvm/main.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import json
import re
import tempfile
from pathlib import Path
from typing import Any, Dict, List, Optional, Union

from packaging.version import Version
from packaging.version import InvalidVersion, Version

from vvm import wrapper
from vvm.exceptions import VyperError
from vvm.install import get_executable

VERSION_RE = re.compile(r"\s*#\s*(?:pragma\s+|@)version\s+[=><^]?(\d+\.\d+\.\d+\S*)")


def get_vyper_version() -> Version:
"""
Expand All @@ -23,13 +26,38 @@ def get_vyper_version() -> Version:
return wrapper._get_vyper_version(vyper_binary)


def detect_vyper_version_from_source(source_code: str) -> Optional[str]:
DanielSchiavini marked this conversation as resolved.
Show resolved Hide resolved
"""
Detect the version given by the pragma version in the source code.
TODO: when the user has a range, we should compare to the installed versions

Arguments
---------
source_code : str
Source code to detect the version from.

Returns
-------
str
vyper version, or None if no version could be detected.
"""
try:
finditer = VERSION_RE.finditer(source_code)
version_str = next(finditer).group(1)
Version(version_str) # validate the version
return version_str
except (StopIteration, InvalidVersion):
return None


def compile_source(
source: str,
base_path: Union[Path, str] = None,
evm_version: str = None,
vyper_binary: Union[str, Path] = None,
vyper_version: Version = None,
) -> Dict:
vyper_version: Union[str, Version, None] = None,
output_format: str = None,
) -> Any:
"""
Compile a Vyper contract.

Expand All @@ -51,34 +79,44 @@ def compile_source(
vyper_version: Version, optional
`vyper` version to use. If not given, the currently active version is used.
Ignored if `vyper_binary` is also given.
output_format: str, optional
Output format of the compiler. See `vyper --help` for more information.

Returns
-------
Dict
Compiler output. The source file name is given as `<stdin>`.
Any
Compiler output (depends on `output_format`).
For JSON output the return type is a dictionary, otherwise it is a string.
"""
source_path = tempfile.mkstemp(suffix=".vy", prefix="vyper-", text=True)[1]
with open(source_path, "w") as fp:
fp.write(source)
if vyper_version is None:
vyper_version = detect_vyper_version_from_source(source)

compiler_data = _compile(
vyper_binary=vyper_binary,
vyper_version=vyper_version,
source_files=[source_path],
base_path=base_path,
evm_version=evm_version,
)
with tempfile.NamedTemporaryFile(suffix=".vy", prefix="vyper-") as source_file:
source_file.write(source.encode())
source_file.flush()

compiler_data = _compile(
vyper_binary=vyper_binary,
vyper_version=vyper_version,
source_files=[source_file.name],
base_path=base_path,
evm_version=evm_version,
output_format=output_format,
)

return {"<stdin>": list(compiler_data.values())[0]}
if output_format in ("combined_json", None):
return {"<stdin>": list(compiler_data.values())[0]}
return compiler_data


def compile_files(
source_files: Union[List, Path, str],
base_path: Union[Path, str] = None,
evm_version: str = None,
vyper_binary: Union[str, Path] = None,
vyper_version: Version = None,
) -> Dict:
vyper_version: Union[str, Version, None] = None,
output_format: str = None,
) -> Any:
"""
Compile one or more Vyper source files.

Expand All @@ -100,36 +138,45 @@ def compile_files(
vyper_version: Version, optional
`vyper` version to use. If not given, the currently active version is used.
Ignored if `vyper_binary` is also given.
output_format: str, optional
Output format of the compiler. See `vyper --help` for more information.

Returns
-------
Dict
Compiler output
Any
Compiler output (depends on `output_format`).
For JSON output the return type is a dictionary, otherwise it is a string.
"""
return _compile(
vyper_binary=vyper_binary,
vyper_version=vyper_version,
source_files=source_files,
base_path=base_path,
evm_version=evm_version,
output_format=output_format,
)


def _compile(
base_path: Union[str, Path, None],
vyper_binary: Union[str, Path, None],
vyper_version: Optional[Version],
vyper_version: Union[str, Version, None],
output_format: Optional[str],
**kwargs: Any,
) -> Dict:
) -> Any:

if vyper_binary is None:
vyper_binary = get_executable(vyper_version)
if output_format is None:
output_format = "combined_json"

stdoutdata, stderrdata, command, proc = wrapper.vyper_wrapper(
vyper_binary=vyper_binary, f="combined_json", p=base_path, **kwargs
vyper_binary=vyper_binary, f=output_format, p=base_path, **kwargs
)

return json.loads(stdoutdata)
if output_format in ("combined_json", "standard_json", "metadata"):
return json.loads(stdoutdata)
return stdoutdata


def compile_standard(
Expand Down