Skip to content

Commit

Permalink
Merge pull request #294 from iamdefinitelyahuman/vyper
Browse files Browse the repository at this point in the history
Vyper support
  • Loading branch information
iamdefinitelyahuman committed Jan 7, 2020
2 parents d291a19 + 604c15a commit 7003314
Show file tree
Hide file tree
Showing 43 changed files with 1,248 additions and 417 deletions.
2 changes: 1 addition & 1 deletion .gitignore
@@ -1,7 +1,7 @@
__pycache__
.vscode
.history
.coverage
.coverage*
build/
brownie/data/*/
brownie/data/*.json
Expand Down
46 changes: 23 additions & 23 deletions .travis.yml
@@ -1,7 +1,7 @@
# Based on https://github.com/cclauss/Travis-CI-Python-on-three-OSes
matrix:
include:
- name: "Standard Tests, Parametrized EVM Tests - Python 3.7.1 on Xenial Linux"
- name: "EVM Tests - Python 3.7 on Xenial Linux"
language: python
python: 3.7
dist: xenial
Expand All @@ -13,21 +13,8 @@ matrix:
- sudo apt-get install -y python3.7-dev npm solc
- npm -g install ganache-cli@6.7.0
- python -m pip install coveralls==1.9.2 tox==3.14.0
script: tox -e py37,evmtests
- name: "Standard Tests, Linting, Docs - Python 3.6.8 on Xenial Linux"
language: python
python: 3.6
dist: xenial
sudo: true
install:
- sudo add-apt-repository -y ppa:ethereum/ethereum
- sudo add-apt-repository -y ppa:deadsnakes/ppa
- sudo apt-get update
- sudo apt-get install -y python3.6-dev npm solc
- npm -g install ganache-cli@6.7.0
- python -m pip install coveralls==1.9.2 tox==3.14.0
script: tox -e lint,doctest,py36
- name: "Standard Tests - Python 3.7.4 on Windows"
script: tox -e evmtests
- name: "Standard Tests - Python 3.7 on Windows"
os: windows
language: node_js
node_js: '10'
Expand All @@ -39,22 +26,22 @@ matrix:
- python setup.py install
env: PATH=/c/Python37:/c/Python37/Scripts:$PATH
script: python -m pytest tests/ --cov=brownie/ -p no:pytest-brownie -n auto --dist=loadscope
- name: "Standard Tests - Python 3.8.0 on Xenial Linux"
- name: "Standard Tests, Linting, Docs - Python 3.6 on Xenial Linux"
language: python
python: 3.8
python: 3.6
dist: xenial
sudo: true
install:
- sudo add-apt-repository -y ppa:ethereum/ethereum
- sudo add-apt-repository -y ppa:deadsnakes/ppa
- sudo apt-get update
- sudo apt-get install -y python3.8-dev npm solc
- sudo apt-get install -y python3.6-dev npm solc
- npm -g install ganache-cli@6.7.0
- python -m pip install coveralls==1.9.2 tox==3.14.0
script: tox -e py38
- name: "Brownie Mix Tests - Python 3.6.8 on Xenial Linux"
script: tox -e lint,doctest,py36
- name: "Standard Tests, Brownie Mix Tests - Python 3.7 on Xenial Linux"
language: python
python: 3.6
python: 3.7
dist: xenial
sudo: true
install:
Expand All @@ -64,7 +51,20 @@ matrix:
- sudo apt-get install -y python3.6-dev npm solc
- npm -g install ganache-cli@6.7.0
- python -m pip install coveralls==1.9.2 tox==3.14.0
script: tox -e mixtests
script: tox -e py37,mixtests
- name: "Standard Tests - Python 3.8 on Xenial Linux"
language: python
python: 3.8
dist: xenial
sudo: true
install:
- sudo add-apt-repository -y ppa:ethereum/ethereum
- sudo add-apt-repository -y ppa:deadsnakes/ppa
- sudo apt-get update
- sudo apt-get install -y python3.8-dev npm solc
- npm -g install ganache-cli@6.7.0
- python -m pip install coveralls==1.9.2 tox==3.14.0
script: tox -e py38

env:
global: COVERALLS_PARALLEL=true
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,7 +6,10 @@ This changelog format is based on [Keep a Changelog](https://keepachangelog.com/
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/iamdefinitelyahuman/brownie)

## [1.4.0](https://github.com/iamdefinitelyahuman/brownie/tree/v1.4.0) - 2020-01-07
### Added
- support for Vyper smart contracts ([v0.1.0-beta15](https://github.com/vyperlang/vyper/releases/tag/v0.1.0-beta.15))
- `brownie accounts` commandline interface

## [1.3.2](https://github.com/iamdefinitelyahuman/brownie/tree/v1.3.2) - 2020-01-01
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -6,6 +6,7 @@ Brownie is a Python-based development and testing framework for smart contracts

## Features

* Full support for [Solidity](https://github.com/ethereum/solidity) (`>=0.4.22`) and [Vyper](https://github.com/vyperlang/vyper) (`0.1.0b-15`)
* Contract testing via [pytest](https://github.com/pytest-dev/pytest), including trace-based coverage evaluation
* Powerful debugging tools, including python-style tracebacks and custom error strings
* Built-in console for quick project interaction
Expand Down
3 changes: 2 additions & 1 deletion brownie/__init__.py
@@ -1,7 +1,7 @@
#!/usr/bin/python3

from brownie._config import CONFIG as config
from brownie.convert import Wei
from brownie.convert import Fixed, Wei
from brownie.project import compile_source, run
from brownie.network import accounts, alert, history, rpc, web3
from brownie.network.contract import Contract # NOQA: F401
Expand All @@ -16,6 +16,7 @@
"project",
"compile_source",
"run",
"Fixed",
"Wei",
"config",
]
2 changes: 1 addition & 1 deletion brownie/_cli/__main__.py
Expand Up @@ -11,7 +11,7 @@
from brownie.exceptions import ProjectNotFound
from brownie.utils import color, notify

__version__ = "1.3.2"
__version__ = "1.4.0"

__doc__ = """Usage: brownie <command> [<args>...] [options <args>]
Expand Down
14 changes: 9 additions & 5 deletions brownie/_gui/source.py
Expand Up @@ -23,8 +23,9 @@ def __init__(self, parent):
self.root.bind("<Left>", self.key_left)
self.root.bind("<Right>", self.key_right)
base_path = self.root.active_project._path.joinpath("contracts")
for path in base_path.glob("**/*.sol"):
self.add(path)
for path in base_path.glob("**/*"):
if path.suffix in (".sol", ".vy"):
self.add(path)
self.set_visible([])

def add(self, path):
Expand All @@ -33,7 +34,7 @@ def add(self, path):
if label in [i._label for i in self._frames]:
return
with path.open() as fp:
frame = SourceFrame(self, fp.read())
frame = SourceFrame(self, fp.read(), path.suffix)
super().add(frame, text=f" {label} ")
frame._id = len(self._frames)
frame._label = label
Expand Down Expand Up @@ -168,7 +169,7 @@ def key(k):


class SourceFrame(tk.Frame):
def __init__(self, root, text):
def __init__(self, root, text, suffix):
super().__init__(root)
self._text = tk.Text(self, width=90, yscrollcommand=self._text_scroll)
self._scroll = ttk.Scrollbar(self)
Expand All @@ -183,7 +184,10 @@ def __init__(self, root, text):
for k, v in TEXT_COLORS.items():
self._text.tag_config(k, **v)

pattern = r"((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))"
if suffix == ".sol":
pattern = r"((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))"
else:
pattern = r"((#[^\n]*\n)|(\"\"\"[\s\S]*?\"\"\")|('''[\s\S]*?'''))"
for match in re.finditer(pattern, text):
self.tag_add("comment", match.start(), match.end())

Expand Down
82 changes: 81 additions & 1 deletion brownie/convert.py
@@ -1,6 +1,7 @@
#!/usr/bin/python3

from copy import deepcopy
from decimal import Decimal
from typing import Any, Dict, ItemsView, KeysView, List, Tuple, TypeVar, Union

import eth_utils
Expand Down Expand Up @@ -101,7 +102,7 @@ def _return_int(original: Any, value: Any) -> int:
try:
return int(value)
except ValueError:
raise TypeError(f"Could not convert {type(original)} '{original}' to wei.")
raise TypeError(f"Cannot convert {type(original)} '{original}' to wei.")


def to_uint(value: Any, type_: str = "uint256") -> "Wei":
Expand Down Expand Up @@ -129,6 +130,83 @@ def _check_int_size(type_: Any) -> int:
return size


class Fixed(Decimal):

"""
Decimal subclass that allows comparison against strings, integers and Wei.
Raises TypeError when operations are attempted against floats.
"""

# Known typing error: https://github.com/python/mypy/issues/4290
def __new__(cls, value: Any) -> Any: # type: ignore
return super().__new__(cls, _to_fixed(value)) # type: ignore

def __repr__(self):
return f"Fixed('{str(self)}')"

def __hash__(self) -> int:
return super().__hash__()

def __lt__(self, other: Any) -> bool: # type: ignore
return super().__lt__(_to_fixed(other))

def __le__(self, other: Any) -> bool: # type: ignore
return super().__le__(_to_fixed(other))

def __eq__(self, other: Any) -> bool: # type: ignore
if isinstance(other, float):
raise TypeError("Cannot compare to floating point - use a string instead")
try:
return super().__eq__(_to_fixed(other))
except TypeError:
return False

def __ne__(self, other: Any) -> bool:
if isinstance(other, float):
raise TypeError("Cannot compare to floating point - use a string instead")
try:
return super().__ne__(_to_fixed(other))
except TypeError:
return True

def __ge__(self, other: Any) -> bool: # type: ignore
return super().__ge__(_to_fixed(other))

def __gt__(self, other: Any) -> bool: # type: ignore
return super().__gt__(_to_fixed(other))

def __add__(self, other: Any) -> "Fixed": # type: ignore
return Fixed(super().__add__(_to_fixed(other)))

def __sub__(self, other: Any) -> "Fixed": # type: ignore
return Fixed(super().__sub__(_to_fixed(other)))


def _to_fixed(value: Any) -> Decimal:
if isinstance(value, float):
raise TypeError("Cannot convert float to decimal - use a string instead")
elif isinstance(value, (str, bytes)):
try:
value = Wei(value)
except TypeError:
pass
try:
return Decimal(value)
except Exception:
raise TypeError(f"Cannot convert {type(value)} '{value}' to decimal.")


def to_decimal(value: Any) -> Fixed:
"""Convert a value to a fixed point decimal"""
d: Fixed = Fixed(value)
if d < -2 ** 127 or d >= 2 ** 127:
raise OverflowError(f"{value} is outside allowable range for decimal")
if d.quantize(Decimal("1.0000000000")) != d:
raise ValueError("Maximum of 10 decimal points allowed")
return d


class EthAddress(str):

"""String subclass that raises TypeError when compared to a non-address."""
Expand Down Expand Up @@ -313,6 +391,8 @@ def _format_single(type_: str, value: Any) -> Any:
return to_uint(value, type_)
elif "int" in type_:
return to_int(value, type_)
elif type_ == "fixed168x10":
return to_decimal(value)
elif type_ == "bool":
return to_bool(value)
elif type_ == "address":
Expand Down
4 changes: 4 additions & 0 deletions brownie/exceptions.py
Expand Up @@ -117,3 +117,7 @@ class PragmaError(Exception):

class InvalidManifest(Exception):
pass


class UnsupportedLanguage(Exception):
pass
29 changes: 23 additions & 6 deletions brownie/network/contract.py
Expand Up @@ -374,7 +374,11 @@ def __init__(self, address: str, abi: Dict, name: str, owner: Optional[AccountsT
self.signature = _signature(abi)

def __repr__(self) -> str:
pay = "payable " if self.abi["stateMutability"] == "payable" else ""
if "payable" in self.abi:
pay_bool = self.abi["payable"]
else:
pay_bool = self.abi["stateMutability"] == "payable"
pay = "payable " if pay_bool else ""
return f"<{type(self).__name__} {pay}object '{self.abi['name']}({_inputs(self.abi)})'>"

def call(self, *args: Tuple) -> Any:
Expand Down Expand Up @@ -515,23 +519,36 @@ def _get_tx(owner: Optional[AccountsType], args: Tuple) -> Tuple:
def _get_method_object(
address: str, abi: Dict, name: str, owner: Optional[AccountsType]
) -> Union["ContractCall", "ContractTx"]:
if abi["stateMutability"] in ("view", "pure"):

if "constant" in abi:
constant = abi["constant"]
else:
constant = abi["stateMutability"] in ("view", "pure")

if constant:
return ContractCall(address, abi, name, owner)
return ContractTx(address, abi, name, owner)


def _params(abi_params: List) -> List:
def _params(abi_params: List, substitutions: Optional[Dict] = None) -> List:
types = []
if substitutions is None:
substitutions = {}
for i in abi_params:
if i["type"] != "tuple":
types.append((i["name"], i["type"]))
type_ = i["type"]
for orig, sub in substitutions.items():
if type_.startswith(orig):
type_ = type_.replace(orig, sub)
types.append((i["name"], type_))
continue
types.append((i["name"], f"({','.join(x[1] for x in _params(i['components']))})"))
params = [i[1] for i in _params(i["components"], substitutions)]
types.append((i["name"], f"({','.join(params)})"))
return types


def _inputs(abi: Dict) -> str:
params = _params(abi["inputs"])
params = _params(abi["inputs"], {"fixed168x10": "decimal"})
return ", ".join(f"{i[1]}{' '+i[0] if i[0] else ''}" for i in params)


Expand Down
13 changes: 8 additions & 5 deletions brownie/network/transaction.py
Expand Up @@ -440,19 +440,20 @@ def _expand_trace(self) -> None:
continue

# calculate coverage
if "active_branches" in last:
if last["coverage"]:
if pc["path"] not in coverage_eval[last["name"]]:
coverage_eval[last["name"]][pc["path"]] = [set(), set(), set()]
if "statement" in pc:
coverage_eval[last["name"]][pc["path"]][0].add(pc["statement"])
if "branch" in pc:
if pc["op"] != "JUMPI":
last["active_branches"].add(pc["branch"])
elif pc["branch"] in last["active_branches"]:
elif "active_branches" not in last or pc["branch"] in last["active_branches"]:
# false, true
key = 1 if trace[i + 1]["pc"] == trace[i]["pc"] + 1 else 2
coverage_eval[last["name"]][pc["path"]][key].add(pc["branch"])
last["active_branches"].remove(pc["branch"])
if "active_branches" in last:
last["active_branches"].remove(pc["branch"])

# ignore jumps with no function - they are compiler optimizations
if "jump" in pc:
Expand Down Expand Up @@ -733,15 +734,17 @@ def _raise(msg: str, source: str) -> None:

def _get_last_map(address: EthAddress, sig: str) -> Dict:
contract = _find_contract(address)
last_map = {"address": EthAddress(address), "jumpDepth": 0, "name": None}
last_map = {"address": EthAddress(address), "jumpDepth": 0, "name": None, "coverage": False}
if contract:
last_map.update(
contract=contract,
name=contract._name,
fn=[f"{contract._name}.{contract.get_method(sig)}"],
)
if contract._build:
last_map.update(pc_map=contract._build["pcMap"], active_branches=set())
last_map.update(pc_map=contract._build["pcMap"], coverage=True)
if contract._build["language"] == "Solidity":
last_map["active_branches"] = set()
else:
last_map.update(contract=None, fn=[f"<UnknownContract>.{sig}"])
return last_map

0 comments on commit 7003314

Please sign in to comment.