From f9d97e36f7f60379e065fa47dedfe120069a40eb Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Wed, 6 May 2020 20:00:50 +0200 Subject: [PATCH 01/15] feat: Add Wei.to() method for unit conversion Accepts a unit as string and will return a Fixed number converted to the desired unit type --- brownie/convert/datatypes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/brownie/convert/datatypes.py b/brownie/convert/datatypes.py index cb14612a9..36e2285bb 100644 --- a/brownie/convert/datatypes.py +++ b/brownie/convert/datatypes.py @@ -74,6 +74,12 @@ def __add__(self, other: Any) -> "Wei": def __sub__(self, other: Any) -> "Wei": return Wei(super().__sub__(_to_wei(other))) + def to(self, unit: str) -> Decimal: + try: + return _to_fixed(self) * Decimal(10) ** (-Decimal(UNITS[unit])) + except KeyError: + raise TypeError(f'Cannot convert wei to unknown unit: "{unit}". ') + def _to_wei(value: WeiInputTypes) -> int: original = value From 06817b46b1e9ded217712791bca2978a48716526 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Wed, 6 May 2020 20:13:48 +0200 Subject: [PATCH 02/15] feat: add cmd_settings to brownie-config Project specific cmd_settings for ganache-cli new params: --time, --blockTime, --defaultBalanceEther loading a project will update the network settings with project specific settings --- brownie/_config.py | 26 +++++++++++++++++++++++--- brownie/network/rpc.py | 20 +++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/brownie/_config.py b/brownie/_config.py index 18cc48833..81bdc19b5 100644 --- a/brownie/_config.py +++ b/brownie/_config.py @@ -60,7 +60,11 @@ def set_active_network(self, id_: str = None) -> Dict: key = "development" if "cmd" in network else "live" network["settings"] = self.settings["networks"][key].copy() - if key == "development" and "fork" in network["cmd_settings"]: + if ( + key == "development" + and isinstance(network["cmd_settings"], dict) + and "fork" in network["cmd_settings"] + ): fork = network["cmd_settings"]["fork"] if fork in self.networks: @@ -149,7 +153,7 @@ def _get_project_config_path(project_path: Path): def _load_config(project_path: Path) -> Dict: - # Loads configuration data from a file, returns as a dict + """Loads configuration data from a file, returns as a dict""" path = _get_project_config_path(project_path) if path is None: return {} @@ -163,7 +167,7 @@ def _load_config(project_path: Path) -> Dict: def _load_project_config(project_path: Path) -> None: - # Loads configuration settings from a project's brownie-config.yaml + """Loads configuration settings from a project's brownie-config.yaml""" config_path = project_path.joinpath("brownie-config") config_data = _load_config(config_path) if not config_data: @@ -178,6 +182,22 @@ def _load_project_config(project_path: Path) -> None: ) del config_data["network"] + # Update the network config cmd_settings with project specific cmd_settings + if "networks" in config_data and isinstance(config_data["networks"], dict): + for network, values in config_data["networks"].items(): + if ( + network != "default" + and network in CONFIG.networks.keys() + and "cmd_settings" in values + and isinstance(values["cmd_settings"], dict) + ): + if "cmd_settings" in CONFIG.networks[network]: + _recursive_update( + CONFIG.networks[network]["cmd_settings"], values["cmd_settings"] + ) + else: + CONFIG.networks[network]["cmd_settings"] = values["cmd_settings"] + CONFIG.settings._unlock() _recursive_update(CONFIG.settings, config_data) CONFIG.settings._lock() diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index 5a056b6bc..ecf9bc2cf 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -5,6 +5,7 @@ import sys import threading import time +import warnings import weakref from subprocess import DEVNULL, PIPE from typing import Any, Dict, List, Optional, Tuple, Union @@ -14,6 +15,7 @@ from brownie._config import EVM_EQUIVALENTS from brownie._singleton import _Singleton +from brownie.convert import to_int from brownie.exceptions import RPCConnectionError, RPCProcessError, RPCRequestError from .web3 import web3 @@ -26,6 +28,9 @@ "fork": "--fork", "mnemonic": "--mnemonic", "account_keys_path": "--acctKeys", + "block_time": "--blockTime", + "default_balance": "--defaultBalanceEther", + "time": "--time", } EVM_VERSIONS = ["byzantium", "constantinople", "petersburg", "istanbul"] @@ -82,7 +87,20 @@ def launch(self, cmd: str, **kwargs: Dict) -> None: if kwargs["evm_version"] in EVM_EQUIVALENTS: kwargs["evm_version"] = EVM_EQUIVALENTS[kwargs["evm_version"]] # type: ignore for key, value in [(k, v) for k, v in kwargs.items() if v]: - cmd += f" {CLI_FLAGS[key]} {value}" + if key == "default_balance": + try: + value = int(value) # type: ignore + except ValueError: + # convert any input to ether, then format it properly + value = to_int(value).to("ether") # type: ignore + value = value.quantize(1) if value > 1 else value.normalize() # type: ignore + try: + cmd += f" {CLI_FLAGS[key]} {value}" + except KeyError: + warnings.warn( + f"Ignoring invalid commandline setting for ganache-cli: " + f'"{key}" with value "{value}".' + ) print(f"Launching '{cmd}'...") self._time_offset = 0 self._snapshot_id = False From 545ccab5373888210d600765c1cb699945ba9842 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Thu, 7 May 2020 11:38:11 +0200 Subject: [PATCH 03/15] fix: Wei.to() properly returns "Fixed" --- brownie/convert/datatypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brownie/convert/datatypes.py b/brownie/convert/datatypes.py index 36e2285bb..665ecdc02 100644 --- a/brownie/convert/datatypes.py +++ b/brownie/convert/datatypes.py @@ -74,9 +74,9 @@ def __add__(self, other: Any) -> "Wei": def __sub__(self, other: Any) -> "Wei": return Wei(super().__sub__(_to_wei(other))) - def to(self, unit: str) -> Decimal: + def to(self, unit: str) -> "Fixed": try: - return _to_fixed(self) * Decimal(10) ** (-Decimal(UNITS[unit])) + return Fixed(self * Fixed(10) ** -UNITS[unit]) except KeyError: raise TypeError(f'Cannot convert wei to unknown unit: "{unit}". ') From 688cf68d0dea7aef02a7ae6e04dd04dc9c3f1bd6 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Thu, 7 May 2020 16:03:33 +0200 Subject: [PATCH 04/15] fix: validate cmd_settings and add Rpc.block_time() - better validation of cmd_settings with error handling - added block_time() to Rpc to get the time in seconds between blocks if specified - added support for --time parameter --- brownie/network/rpc.py | 58 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index ecf9bc2cf..2edd8663e 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import atexit +import datetime import gc import sys import threading @@ -15,7 +16,7 @@ from brownie._config import EVM_EQUIVALENTS from brownie._singleton import _Singleton -from brownie.convert import to_int +from brownie.convert import Wei from brownie.exceptions import RPCConnectionError, RPCProcessError, RPCRequestError from .web3 import web3 @@ -51,6 +52,7 @@ class Rpc(metaclass=_Singleton): def __init__(self) -> None: self._rpc: Any = None self._time_offset: int = 0 + self._block_time: int = 0 self._snapshot_id: Union[int, Optional[bool]] = False self._reset_id: Union[int, bool] = False self._current_id: Union[int, bool] = False @@ -86,14 +88,8 @@ def launch(self, cmd: str, **kwargs: Dict) -> None: kwargs.setdefault("evm_version", EVM_DEFAULT) # type: ignore if kwargs["evm_version"] in EVM_EQUIVALENTS: kwargs["evm_version"] = EVM_EQUIVALENTS[kwargs["evm_version"]] # type: ignore + kwargs = _validate_cmd_settings(kwargs) for key, value in [(k, v) for k, v in kwargs.items() if v]: - if key == "default_balance": - try: - value = int(value) # type: ignore - except ValueError: - # convert any input to ether, then format it properly - value = to_int(value).to("ether") # type: ignore - value = value.quantize(1) if value > 1 else value.normalize() # type: ignore try: cmd += f" {CLI_FLAGS[key]} {value}" except KeyError: @@ -102,6 +98,8 @@ def launch(self, cmd: str, **kwargs: Dict) -> None: f'"{key}" with value "{value}".' ) print(f"Launching '{cmd}'...") + if "block_time" in kwargs and isinstance(kwargs["block_time"], int): + self._block_time = kwargs["block_time"] self._time_offset = 0 self._snapshot_id = False self._reset_id = False @@ -116,6 +114,7 @@ def launch(self, cmd: str, **kwargs: Dict) -> None: if web3.isConnected(): self._reset_id = self._current_id = self._snap() _notify_registry(0) + self._time_offset = self._request("evm_increaseTime", [0]) return time.sleep(0.1) if type(self._rpc) is psutil.Popen: @@ -148,6 +147,7 @@ def attach(self, laddr: Union[str, Tuple]) -> None: ) from None print(f"Attached to local RPC client listening at '{laddr[0]}:{laddr[1]}'...") self._rpc = psutil.Process(proc.pid) + self._time_offset = self._request("evm_increaseTime", [0]) if web3.provider: self._reset_id = self._current_id = self._snap() _notify_registry(0) @@ -307,6 +307,13 @@ def time(self) -> int: raise SystemError("RPC is not active.") return int(time.time() + self._time_offset) + def block_time(self) -> int: + """Returns the time between mining of blocks in seconds. + A value of 0 stands for instant mining.""" + if not self.is_active(): + raise SystemError("RPC is not active.") + return self._block_time + def sleep(self, seconds: int) -> None: """Increases the time within the test RPC. @@ -390,3 +397,38 @@ def _check_connections(proc: psutil.Process, laddr: Tuple) -> bool: return laddr in [i.laddr for i in proc.connections()] except psutil.AccessDenied: return False + + +def _validate_cmd_settings(cmd_settings: dict) -> dict: + CMD_TYPES = { + "port": int, + "gas_limit": int, + "block_time": int, + "time": datetime.datetime, + "accounts": int, + "evm_version": str, + "mnemonic": str, + "account_keys_path": str, + "fork": str, + } + for cmd, value in cmd_settings.items(): + if ( + cmd in CLI_FLAGS.keys() + and cmd in CMD_TYPES.keys() + and not type(value) == CMD_TYPES[cmd] + ): + raise ValueError( + f'Wrong type for cmd_settings "{cmd}" ({value}). ' + f"Found {type(value)}, but expected {CMD_TYPES[cmd]}." + ) + + if "default_balance" in cmd_settings: + try: + cmd_settings["default_balance"] = int(cmd_settings["default_balance"]) + except ValueError: + # convert any input to ether, then format it properly + default_eth = Wei(cmd_settings["default_balance"]).to("ether") + cmd_settings["default_balance"] = ( + default_eth.quantize(1) if default_eth > 1 else default_eth.normalize() + ) + return cmd_settings From 90fca4bd43137a028777c9a898167954083481d4 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Thu, 7 May 2020 16:04:25 +0200 Subject: [PATCH 05/15] test: config tests added brownie-config.yaml for test project tests to verify that project specific configs update networks --- .../brownie-test-project/brownie-config.yaml | 44 +++++++++++++++ tests/project/test_brownie_config.py | 56 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/data/brownie-test-project/brownie-config.yaml create mode 100644 tests/project/test_brownie_config.py diff --git a/tests/data/brownie-test-project/brownie-config.yaml b/tests/data/brownie-test-project/brownie-config.yaml new file mode 100644 index 000000000..473865b56 --- /dev/null +++ b/tests/data/brownie-test-project/brownie-config.yaml @@ -0,0 +1,44 @@ +networks: + default: development + development: + gas_limit: 6543210 + gas_price: 1000 + reverting_tx_gas_limit: 8765432 + default_contract_owner: false + cmd_settings: + port: 1337 + gas_limit: 7654321 + block_time: 5 + default_balance: 15 milliether + time: 2019-04-05T14:30:11Z + accounts: 15 + evm_version: byzantium + mnemonic: brownie2 + live: + gas_limit: auto + gas_price: auto + reverting_tx_gas_limit: false + default_contract_owner: false + +compiler: + evm_version: null + solc: + version: null + optimizer: + enabled: true + runs: 200 + remappings: null + +console: + show_colors: true + color_style: monokai + auto_suggest: true + completions: true + +hypothesis: + deadline: null + max_examples: 50 + stateful_step_count: 10 + +autofetch_sources: false +dependencies: null \ No newline at end of file diff --git a/tests/project/test_brownie_config.py b/tests/project/test_brownie_config.py new file mode 100644 index 000000000..cb5155a73 --- /dev/null +++ b/tests/project/test_brownie_config.py @@ -0,0 +1,56 @@ +#!/usr/bin/python3 + +import pytest +import yaml + +from brownie._config import _get_data_folder, _load_config + + +@pytest.fixture +def settings_proj(testproject): + with testproject._path.joinpath("brownie-config.yaml").open() as fp: + settings_proj_raw = yaml.safe_load(fp)["networks"]["development"] + yield settings_proj_raw + + +def test_load_project_cmd_settings(config, testproject, settings_proj): + """Tests if project specific cmd_settings update the network config when a project is loaded""" + # get raw cmd_setting config data from files + config_path_network = _get_data_folder().joinpath("network-config.yaml") + cmd_settings_network_raw = _load_config(config_path_network)["development"][0]["cmd_settings"] + + # compare initial settings to network config + cmd_settings_network = config.networks["development"]["cmd_settings"] + for k, v in cmd_settings_network.items(): + assert cmd_settings_network_raw[k] == v + + # load project and check if settings correctly updated + testproject.load_config() + cmd_settings_proj = config.networks["development"]["cmd_settings"] + for k, v in settings_proj["cmd_settings"].items(): + assert cmd_settings_proj[k] == v + + +def test_rpc_project_cmd_settings(network, testproject, settings_proj): + """Test if project specific settings are properly passed on to the RPC.""" + cmd_settings_proj = settings_proj["cmd_settings"] + testproject.load_config() + network.connect("development") + + # Check if rpc time is roughly the start time in the config file + # Use diff < 25h to dodge potential timezone differences + assert cmd_settings_proj["time"].timestamp() - network.rpc.time() < 60 * 60 * 25 + + assert cmd_settings_proj["block_time"] == network.rpc.block_time() + accounts = network.accounts + assert cmd_settings_proj["accounts"] == len(accounts) + assert cmd_settings_proj["default_balance"] == accounts[0].balance() + + # Test if mnemonic was updated to "brownie2" + assert "0x816200940a049ff1DEAB864d67a71ae6Dd1ebc3e" == accounts[0].address + + tx = accounts[0].transfer(accounts[1], 0) + assert tx.gas_limit == settings_proj["gas_limit"] + assert tx.gas_price == settings_proj["gas_price"] + + assert network.rpc.evm_version() == cmd_settings_proj["evm_version"] From 8620cf8742ea5c428c9d694c8fa24ebc75978969 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Thu, 7 May 2020 21:04:48 +0200 Subject: [PATCH 06/15] fix: removed Rpc.block_time() No way to reliably query it from an attached instance --- brownie/network/rpc.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index 2edd8663e..a9f5d3987 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -52,7 +52,6 @@ class Rpc(metaclass=_Singleton): def __init__(self) -> None: self._rpc: Any = None self._time_offset: int = 0 - self._block_time: int = 0 self._snapshot_id: Union[int, Optional[bool]] = False self._reset_id: Union[int, bool] = False self._current_id: Union[int, bool] = False @@ -98,8 +97,6 @@ def launch(self, cmd: str, **kwargs: Dict) -> None: f'"{key}" with value "{value}".' ) print(f"Launching '{cmd}'...") - if "block_time" in kwargs and isinstance(kwargs["block_time"], int): - self._block_time = kwargs["block_time"] self._time_offset = 0 self._snapshot_id = False self._reset_id = False @@ -307,13 +304,6 @@ def time(self) -> int: raise SystemError("RPC is not active.") return int(time.time() + self._time_offset) - def block_time(self) -> int: - """Returns the time between mining of blocks in seconds. - A value of 0 stands for instant mining.""" - if not self.is_active(): - raise SystemError("RPC is not active.") - return self._block_time - def sleep(self, seconds: int) -> None: """Increases the time within the test RPC. From d9f8d5261558232c58a57a582116e23bf98a9c06 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Thu, 7 May 2020 21:05:57 +0200 Subject: [PATCH 07/15] test: Improved and fixed config tests will only copy the brownie-config.yaml file when needed will now properly open, close and reset the network --- tests/conftest.py | 11 +++++++ ...e-config.yaml => brownie-test-config.yaml} | 1 - tests/project/test_brownie_config.py | 30 +++++++++++-------- 3 files changed, 28 insertions(+), 14 deletions(-) rename tests/data/{brownie-test-project/brownie-config.yaml => brownie-test-config.yaml} (97%) diff --git a/tests/conftest.py b/tests/conftest.py index 27fb300c0..a30725b9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,6 +185,17 @@ def testproject(_project_factory, project, tmp_path): return project.load(path, "TestProject") +@pytest.fixture +def testprojectconfig(_project_factory, project, tmp_path): + path = tmp_path.joinpath("testprojectconfig") + os.chdir(tmp_path) + _copy_all(_project_factory, path) + os.chdir(Path(__file__).parent) + shutil.copyfile("data/brownie-test-config.yaml", path.joinpath("brownie-config.yaml")) + os.chdir(path) + return project.load(path, "TestProjectConfig") + + @pytest.fixture def tp_path(testproject): yield testproject._path diff --git a/tests/data/brownie-test-project/brownie-config.yaml b/tests/data/brownie-test-config.yaml similarity index 97% rename from tests/data/brownie-test-project/brownie-config.yaml rename to tests/data/brownie-test-config.yaml index 473865b56..1aab7d2f2 100644 --- a/tests/data/brownie-test-project/brownie-config.yaml +++ b/tests/data/brownie-test-config.yaml @@ -6,7 +6,6 @@ networks: reverting_tx_gas_limit: 8765432 default_contract_owner: false cmd_settings: - port: 1337 gas_limit: 7654321 block_time: 5 default_balance: 15 milliether diff --git a/tests/project/test_brownie_config.py b/tests/project/test_brownie_config.py index cb5155a73..de1e875bd 100644 --- a/tests/project/test_brownie_config.py +++ b/tests/project/test_brownie_config.py @@ -7,13 +7,13 @@ @pytest.fixture -def settings_proj(testproject): - with testproject._path.joinpath("brownie-config.yaml").open() as fp: +def settings_proj(testprojectconfig): + with testprojectconfig._path.joinpath("brownie-config.yaml").open() as fp: settings_proj_raw = yaml.safe_load(fp)["networks"]["development"] yield settings_proj_raw -def test_load_project_cmd_settings(config, testproject, settings_proj): +def test_load_project_cmd_settings(config, testprojectconfig, settings_proj): """Tests if project specific cmd_settings update the network config when a project is loaded""" # get raw cmd_setting config data from files config_path_network = _get_data_folder().joinpath("network-config.yaml") @@ -22,27 +22,30 @@ def test_load_project_cmd_settings(config, testproject, settings_proj): # compare initial settings to network config cmd_settings_network = config.networks["development"]["cmd_settings"] for k, v in cmd_settings_network.items(): - assert cmd_settings_network_raw[k] == v + if k != "port": + assert cmd_settings_network_raw[k] == v # load project and check if settings correctly updated - testproject.load_config() + testprojectconfig.load_config() cmd_settings_proj = config.networks["development"]["cmd_settings"] for k, v in settings_proj["cmd_settings"].items(): - assert cmd_settings_proj[k] == v + if k != "port": + assert cmd_settings_proj[k] == v -def test_rpc_project_cmd_settings(network, testproject, settings_proj): +def test_rpc_project_cmd_settings(devnetwork, testprojectconfig, config, settings_proj): """Test if project specific settings are properly passed on to the RPC.""" + if devnetwork.rpc.is_active(): + devnetwork.rpc.kill() cmd_settings_proj = settings_proj["cmd_settings"] - testproject.load_config() - network.connect("development") + testprojectconfig.load_config() + devnetwork.connect("development") # Check if rpc time is roughly the start time in the config file # Use diff < 25h to dodge potential timezone differences - assert cmd_settings_proj["time"].timestamp() - network.rpc.time() < 60 * 60 * 25 + assert cmd_settings_proj["time"].timestamp() - devnetwork.rpc.time() < 60 * 60 * 25 - assert cmd_settings_proj["block_time"] == network.rpc.block_time() - accounts = network.accounts + accounts = devnetwork.accounts assert cmd_settings_proj["accounts"] == len(accounts) assert cmd_settings_proj["default_balance"] == accounts[0].balance() @@ -53,4 +56,5 @@ def test_rpc_project_cmd_settings(network, testproject, settings_proj): assert tx.gas_limit == settings_proj["gas_limit"] assert tx.gas_price == settings_proj["gas_price"] - assert network.rpc.evm_version() == cmd_settings_proj["evm_version"] + assert devnetwork.rpc.evm_version() == cmd_settings_proj["evm_version"] + devnetwork.rpc.kill() From 46c6ed837fd1da87f7305f464043064ab805189f Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 13:34:25 +0200 Subject: [PATCH 08/15] fix: move test config from file to string replace shutil.copy with yaml.dump and dump the config string into the project root testprojectconfig fixture no longer necessary, additional logic moved to settings_proj fixture --- tests/conftest.py | 11 ------ tests/data/brownie-test-config.yaml | 43 --------------------- tests/project/test_brownie_config.py | 57 ++++++++++++++++++++-------- 3 files changed, 42 insertions(+), 69 deletions(-) delete mode 100644 tests/data/brownie-test-config.yaml diff --git a/tests/conftest.py b/tests/conftest.py index a30725b9a..27fb300c0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,17 +185,6 @@ def testproject(_project_factory, project, tmp_path): return project.load(path, "TestProject") -@pytest.fixture -def testprojectconfig(_project_factory, project, tmp_path): - path = tmp_path.joinpath("testprojectconfig") - os.chdir(tmp_path) - _copy_all(_project_factory, path) - os.chdir(Path(__file__).parent) - shutil.copyfile("data/brownie-test-config.yaml", path.joinpath("brownie-config.yaml")) - os.chdir(path) - return project.load(path, "TestProjectConfig") - - @pytest.fixture def tp_path(testproject): yield testproject._path diff --git a/tests/data/brownie-test-config.yaml b/tests/data/brownie-test-config.yaml deleted file mode 100644 index 1aab7d2f2..000000000 --- a/tests/data/brownie-test-config.yaml +++ /dev/null @@ -1,43 +0,0 @@ -networks: - default: development - development: - gas_limit: 6543210 - gas_price: 1000 - reverting_tx_gas_limit: 8765432 - default_contract_owner: false - cmd_settings: - gas_limit: 7654321 - block_time: 5 - default_balance: 15 milliether - time: 2019-04-05T14:30:11Z - accounts: 15 - evm_version: byzantium - mnemonic: brownie2 - live: - gas_limit: auto - gas_price: auto - reverting_tx_gas_limit: false - default_contract_owner: false - -compiler: - evm_version: null - solc: - version: null - optimizer: - enabled: true - runs: 200 - remappings: null - -console: - show_colors: true - color_style: monokai - auto_suggest: true - completions: true - -hypothesis: - deadline: null - max_examples: 50 - stateful_step_count: 10 - -autofetch_sources: false -dependencies: null \ No newline at end of file diff --git a/tests/project/test_brownie_config.py b/tests/project/test_brownie_config.py index de1e875bd..4b1795305 100644 --- a/tests/project/test_brownie_config.py +++ b/tests/project/test_brownie_config.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +import os import pytest import yaml @@ -7,38 +8,64 @@ @pytest.fixture -def settings_proj(testprojectconfig): - with testprojectconfig._path.joinpath("brownie-config.yaml").open() as fp: - settings_proj_raw = yaml.safe_load(fp)["networks"]["development"] - yield settings_proj_raw +def settings_proj(testproject): + """Creates a config file in the testproject root folder and loads it manually.""" + # Save the following config as "brownie-config.yaml" in the testproject root + test_brownie_config = """ + networks: + default: development + development: + gas_limit: 6543210 + gas_price: 1000 + reverting_tx_gas_limit: 8765432 + default_contract_owner: false + cmd_settings: + gas_limit: 7654321 + block_time: 5 + default_balance: 15 milliether + time: 2019-04-05T14:30:11Z + accounts: 15 + evm_version: byzantium + mnemonic: brownie2 + """ + with testproject._path.joinpath("brownie-config.yaml").open("w") as fp: + yaml.dump(yaml.load(test_brownie_config), fp) -def test_load_project_cmd_settings(config, testprojectconfig, settings_proj): + # Load the networks.development config from the created file and yield it + with testproject._path.joinpath("brownie-config.yaml").open() as fp: + conf = yaml.safe_load(fp)["networks"]["development"] + yield conf + + os.remove(testproject._path.joinpath("brownie-config.yaml")) + + +def test_load_project_cmd_settings(config, testproject, settings_proj): """Tests if project specific cmd_settings update the network config when a project is loaded""" - # get raw cmd_setting config data from files + # get raw cmd_setting config data from the network-config.yaml file config_path_network = _get_data_folder().joinpath("network-config.yaml") cmd_settings_network_raw = _load_config(config_path_network)["development"][0]["cmd_settings"] - # compare initial settings to network config - cmd_settings_network = config.networks["development"]["cmd_settings"] - for k, v in cmd_settings_network.items(): + # compare the manually loaded cmd_settings to the cmd_settings in the CONFIG singleton + cmd_settings_config = config.networks["development"]["cmd_settings"] + for k, v in cmd_settings_config.items(): if k != "port": assert cmd_settings_network_raw[k] == v - # load project and check if settings correctly updated - testprojectconfig.load_config() - cmd_settings_proj = config.networks["development"]["cmd_settings"] + # Load the project with its project specific settings and assert that the CONFIG was updated + testproject.load_config() + cmd_settings_config = config.networks["development"]["cmd_settings"] for k, v in settings_proj["cmd_settings"].items(): if k != "port": - assert cmd_settings_proj[k] == v + assert cmd_settings_config[k] == v -def test_rpc_project_cmd_settings(devnetwork, testprojectconfig, config, settings_proj): +def test_rpc_project_cmd_settings(devnetwork, testproject, config, settings_proj): """Test if project specific settings are properly passed on to the RPC.""" if devnetwork.rpc.is_active(): devnetwork.rpc.kill() cmd_settings_proj = settings_proj["cmd_settings"] - testprojectconfig.load_config() + testproject.load_config() devnetwork.connect("development") # Check if rpc time is roughly the start time in the config file From d695ed71bcfb78a47e2d06ae58746d85fa0ac906 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 13:59:06 +0200 Subject: [PATCH 09/15] test: add tests and docstring for Wei.to() --- brownie/convert/datatypes.py | 14 +++++++++++--- tests/convert/test_wei.py | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/brownie/convert/datatypes.py b/brownie/convert/datatypes.py index 665ecdc02..db42914b7 100644 --- a/brownie/convert/datatypes.py +++ b/brownie/convert/datatypes.py @@ -27,7 +27,7 @@ class Wei(int): - '''Integer subclass that converts a value to wei and allows comparison against + """Integer subclass that converts a value to wei and allows comparison against similarly formatted values. Useful for the following formats: @@ -35,7 +35,7 @@ class Wei(int): * a large float in scientific notation, where direct conversion to int would cause inaccuracy: 8.3e32 * bytes: b'\xff\xff' - * hex strings: "0x330124"''' + * hex strings: "0x330124\"""" # Known typing error: https://github.com/python/mypy/issues/4290 def __new__(cls, value: Any) -> Any: # type: ignore @@ -75,10 +75,18 @@ def __sub__(self, other: Any) -> "Wei": return Wei(super().__sub__(_to_wei(other))) def to(self, unit: str) -> "Fixed": + """ + Returns a converted denomination of the stored wei value. + Accepts any valid ether unit denomination as string, like: + "gwei", "milliether", "finney", "ether". + + :param unit: An ether denomination like "ether" or "gwei" + :return: A 'Fixed' type number in the specified denomination + """ try: return Fixed(self * Fixed(10) ** -UNITS[unit]) except KeyError: - raise TypeError(f'Cannot convert wei to unknown unit: "{unit}". ') + raise TypeError(f'Cannot convert wei to unknown unit: "{unit}". ') from None def _to_wei(value: WeiInputTypes) -> int: diff --git a/tests/convert/test_wei.py b/tests/convert/test_wei.py index 9b67a1519..3a456d4e4 100755 --- a/tests/convert/test_wei.py +++ b/tests/convert/test_wei.py @@ -1,5 +1,7 @@ #!/usr/bin/python3 +import pytest +from brownie import Fixed from brownie.convert import Wei @@ -64,3 +66,22 @@ def test_gt(): def test_ge(): assert Wei("2 ether") >= "1 ether" assert Wei("2 ether") >= "2 ether" + + +@pytest.mark.parametrize( + "conversion_tuples", + ( + ("999999", "gwei"), + ("50", "ether"), + ("0", "milliether"), + ("0.1", "kwei"), + ("0.00001", "ether"), + ), +) +def test_to(conversion_tuples): + assert Wei(" ".join(conversion_tuples)).to(conversion_tuples[1]) == Fixed(conversion_tuples[0]) + + +def test_raise_to(): + with pytest.raises(TypeError): + Wei("1 ether").to("foo") From 13dc7574f1a73b1670f2c4b75f4a56cd9648c5c9 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 14:22:20 +0200 Subject: [PATCH 10/15] fix: warnings & errors for invalid rpc arguments better messages and Brownie specific classes for warning changed type checking to isinstance --- brownie/exceptions.py | 4 ++++ brownie/network/rpc.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/brownie/exceptions.py b/brownie/exceptions.py index 84d99fe36..1a14a888e 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -131,3 +131,7 @@ class BrownieCompilerWarning(Warning): class BrownieEnvironmentWarning(Warning): pass + + +class InvalidArgumentWarning(BrownieEnvironmentWarning): + pass diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index a9f5d3987..0abd37956 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -17,7 +17,12 @@ from brownie._config import EVM_EQUIVALENTS from brownie._singleton import _Singleton from brownie.convert import Wei -from brownie.exceptions import RPCConnectionError, RPCProcessError, RPCRequestError +from brownie.exceptions import ( + InvalidArgumentWarning, + RPCConnectionError, + RPCProcessError, + RPCRequestError, +) from .web3 import web3 @@ -94,7 +99,8 @@ def launch(self, cmd: str, **kwargs: Dict) -> None: except KeyError: warnings.warn( f"Ignoring invalid commandline setting for ganache-cli: " - f'"{key}" with value "{value}".' + f'"{key}" with value "{value}".', + InvalidArgumentWarning, ) print(f"Launching '{cmd}'...") self._time_offset = 0 @@ -405,11 +411,11 @@ def _validate_cmd_settings(cmd_settings: dict) -> dict: if ( cmd in CLI_FLAGS.keys() and cmd in CMD_TYPES.keys() - and not type(value) == CMD_TYPES[cmd] + and not isinstance(value, CMD_TYPES[cmd]) ): raise ValueError( - f'Wrong type for cmd_settings "{cmd}" ({value}). ' - f"Found {type(value)}, but expected {CMD_TYPES[cmd]}." + f'Wrong type for cmd_settings "{cmd}": {value}. ' + f"Found {type(value).__name__}, but expected {CMD_TYPES[cmd].__name__}." ) if "default_balance" in cmd_settings: From 7623c335f012c164cc272c850329ab6ff4b88e46 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 14:42:39 +0200 Subject: [PATCH 11/15] test: add tests for _validate_cmd_settings() changed ValueError to TypeError --- brownie/network/rpc.py | 2 +- tests/project/test_brownie_config.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index 0abd37956..c1a71bfad 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -413,7 +413,7 @@ def _validate_cmd_settings(cmd_settings: dict) -> dict: and cmd in CMD_TYPES.keys() and not isinstance(value, CMD_TYPES[cmd]) ): - raise ValueError( + raise TypeError( f'Wrong type for cmd_settings "{cmd}": {value}. ' f"Found {type(value).__name__}, but expected {CMD_TYPES[cmd].__name__}." ) diff --git a/tests/project/test_brownie_config.py b/tests/project/test_brownie_config.py index 4b1795305..18f8c3293 100644 --- a/tests/project/test_brownie_config.py +++ b/tests/project/test_brownie_config.py @@ -5,6 +5,7 @@ import yaml from brownie._config import _get_data_folder, _load_config +from brownie.network.rpc import _validate_cmd_settings @pytest.fixture @@ -85,3 +86,30 @@ def test_rpc_project_cmd_settings(devnetwork, testproject, config, settings_proj assert devnetwork.rpc.evm_version() == cmd_settings_proj["evm_version"] devnetwork.rpc.kill() + + +def test_validate_cmd_settings(): + cmd_settings = """ + port: 1 + gas_limit: 2 + block_time: 3 + time: 2019-04-05T14:30:11 + accounts: 4 + evm_version: istanbul + mnemonic: brownie + account_keys_path: ../../ + fork: main + """ + cmd_settings_dict = yaml.load(cmd_settings) + valid_dict = _validate_cmd_settings(cmd_settings_dict) + for (k, v) in cmd_settings_dict.items(): + assert valid_dict[k] == v + + +@pytest.mark.parametrize( + "invalid_setting", + ({"port": "foo"}, {"gas_limit": 3.5}, {"block_time": [1]}, {"time": 1}, {"mnemonic": 0}), +) +def test_raise_validate_cmd_settings(invalid_setting): + with pytest.raises(TypeError): + _validate_cmd_settings(invalid_setting) From 2f7c8e243b483d32c03e8a147ad2732343506038 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 17:21:48 +0200 Subject: [PATCH 12/15] fix: add new args to networks.py add "cmd_settings: {}" to default-config.yaml to indicate that these can be specified --- brownie/_cli/networks.py | 3 +++ brownie/data/default-config.yaml | 1 + 2 files changed, 4 insertions(+) diff --git a/brownie/_cli/networks.py b/brownie/_cli/networks.py index 5355ebc74..3b444ab8b 100644 --- a/brownie/_cli/networks.py +++ b/brownie/_cli/networks.py @@ -49,6 +49,9 @@ "fork", "mnemonic", "account_keys_path", + "block_time", + "default_balance", + "time", ) diff --git a/brownie/data/default-config.yaml b/brownie/data/default-config.yaml index 8de09f5d7..bc31e1252 100644 --- a/brownie/data/default-config.yaml +++ b/brownie/data/default-config.yaml @@ -12,6 +12,7 @@ networks: gas_price: 0 reverting_tx_gas_limit: 6721975 default_contract_owner: true + cmd_settings: {} live: gas_limit: auto gas_price: auto From 57974dd7a383c8ca9f10a845f653ae540f09c432 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 17:22:34 +0200 Subject: [PATCH 13/15] doc: new cmd_settings and Wei.to() --- docs/api-brownie.rst | 4 ++++ docs/api-convert.rst | 12 ++++++++++++ docs/config.rst | 28 ++++++++++++++++++++++++++++ docs/network-management.rst | 9 +++++++++ 4 files changed, 53 insertions(+) diff --git a/docs/api-brownie.rst b/docs/api-brownie.rst index c5803c7ff..478ac1b50 100644 --- a/docs/api-brownie.rst +++ b/docs/api-brownie.rst @@ -114,6 +114,10 @@ Warnings Raised on unexpected environment conditions. +.. py:exception:: brownie.exceptions.InvalidArgumentWarning + + Raised on non-critical, invalid arguments passed to a method, function or config file. + ``brownie._config`` =================== diff --git a/docs/api-convert.rst b/docs/api-convert.rst index 2c4006838..caeb93de2 100644 --- a/docs/api-convert.rst +++ b/docs/api-convert.rst @@ -223,6 +223,18 @@ Wei >>> Wei("1 ether") - "0.75 ether" 250000000000000000 +.. py:classmethod:: Wei.to(unit) + + Returns a :class:`Fixed ` number converted to the specified unit. + + Attempting a conversion to an unknown unit raises a ``TypeError``. + + .. code-block:: python + + >>> from brownie import Wei + >>> Wei("20 gwei").to("ether") + Fixed('2.0000000000E-8') + ``brownie.convert.normalize`` ============================= diff --git a/docs/config.rst b/docs/config.rst index da47b6c11..ab9db16ad 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -36,6 +36,33 @@ Networks .. py:attribute:: networks.development + This setting is only available for development networks. + + .. py:attribute:: cmd_settings + + Additional commandline parameters, which are passed into Ganache as commandline arguments. These settings will update the network specific settings defined in :ref:`network management` whenever the project with this configuration file is active. + + The following example shows all commandline settings with their default value. ``fork`` has no default value and ``time`` will default to the current time. See :ref:`adding a development network` for more details on the arguments. + + .. code-block:: yaml + + networks: + development: + gas_limit: 6721975 + gas_price: 0 + reverting_tx_gas_limit: 6721975 + default_contract_owner: true + cmd_settings: + port: 8545 + gas_limit: 6721975 + accounts: 10 + evm_version: istanbul + fork: None + mnemonic: brownie + block_time: 0 + default_balance: 100 + time: 2020-05-08T14:54:08+0000 + .. py:attribute:: networks.live Default settings for development and live environments. @@ -68,6 +95,7 @@ Networks live default: ``false`` + .. _config-solc: diff --git a/docs/network-management.rst b/docs/network-management.rst index 0033ccfd0..a204f061a 100644 --- a/docs/network-management.rst +++ b/docs/network-management.rst @@ -81,6 +81,9 @@ The following fields are optional for live networks: * ``explorer``: API url used by :func:`Contract.from_explorer ` to fetch source code. If this field is not given, you will not be able to fetch source code when using this network. + +.. _adding-network: + Development Networks ******************** @@ -96,6 +99,12 @@ The following optional fields may be given for development networks, which are p * ``mnemonic``: A mnemonic to use when generating local accounts. * ``evm_version``: The EVM ruleset to use. Default is the most recent available. * ``fork``: If given, the local client will fork from another currently running Ethereum client. The value may be an HTTP location and port of the other client, e.g. ``http://localhost:8545``, or the ID of a production network, e.g. ``mainnet``. See :ref:`Using a Forked Development Network `. + * ``block_time``: The time waited between mining blocks. Defaults to instant mining. + * ``default_balance``: The starting balance for all unlocked accounts. Can be given as unit string like "1000 ether" or "50 gwei" or as an number **in Ether**. Will default to 100 ether. + * ``time``: Date (ISO 8601) that the first block should start. Use this feature, along with :func:`Rpc.sleep ` to test time-dependent code. Defaults to the current time. + +.. note:: + These optional commandline fields can also be specified on a project level in the project's ``brownie-config.yaml`` file. See the :ref:`configuration files`. .. note:: From 4b31de42d6212d9c5d209339d20a5b714245dac2 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Fri, 8 May 2020 17:32:25 +0200 Subject: [PATCH 14/15] doc: changelog for #501 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a72d2ad5..5c7138d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ 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) +### Added +- `Wei.to` for unit conversion ([#501](https://github.com/iamdefinitelyahuman/brownie/pull/501)) +- Exposed `block_time`, `default_balance` and `time` ganache-cli parameters ([#501](https://github.com/iamdefinitelyahuman/brownie/pull/501)) + +### Changed +- `brownie-config.yaml` can now specify ganache-cli parameters ([#501](https://github.com/iamdefinitelyahuman/brownie/pull/501)) ## [1.8.3](https://github.com/iamdefinitelyahuman/brownie/tree/v1.8.3) - 2020-05-06 ### Changed From 74790f1a68a62e0919282beba2b62ec3a116c693 Mon Sep 17 00:00:00 2001 From: Matthias Nadler Date: Sat, 9 May 2020 12:05:52 +0200 Subject: [PATCH 15/15] fix: requested changes - removed test file cleanup - changed default cmd_settings from {} to null - made _recursive_update null-safe - replaced yaml.load with yaml.safe_load - moved yield outside load context - improved import statements in test_wei.py --- brownie/_config.py | 4 +++- brownie/data/default-config.yaml | 2 +- tests/convert/test_wei.py | 3 +-- tests/project/test_brownie_config.py | 10 ++++------ 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/brownie/_config.py b/brownie/_config.py index 81bdc19b5..fc51098b1 100644 --- a/brownie/_config.py +++ b/brownie/_config.py @@ -235,7 +235,9 @@ def _modify_hypothesis_settings(settings, name, parent): def _recursive_update(original: Dict, new: Dict) -> None: - # merges project config with brownie default config + """Recursively merges a new dict into the original dict""" + if not original: + original = {} for k in new: if k in original and isinstance(new[k], dict): _recursive_update(original[k], new[k]) diff --git a/brownie/data/default-config.yaml b/brownie/data/default-config.yaml index bc31e1252..fbf9b91b3 100644 --- a/brownie/data/default-config.yaml +++ b/brownie/data/default-config.yaml @@ -12,7 +12,7 @@ networks: gas_price: 0 reverting_tx_gas_limit: 6721975 default_contract_owner: true - cmd_settings: {} + cmd_settings: null live: gas_limit: auto gas_price: auto diff --git a/tests/convert/test_wei.py b/tests/convert/test_wei.py index 3a456d4e4..e3ae2e4bc 100755 --- a/tests/convert/test_wei.py +++ b/tests/convert/test_wei.py @@ -1,8 +1,7 @@ #!/usr/bin/python3 import pytest -from brownie import Fixed -from brownie.convert import Wei +from brownie.convert.datatypes import Fixed, Wei def test_nonetype(): diff --git a/tests/project/test_brownie_config.py b/tests/project/test_brownie_config.py index 18f8c3293..e121c39b0 100644 --- a/tests/project/test_brownie_config.py +++ b/tests/project/test_brownie_config.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 -import os import pytest import yaml @@ -31,14 +30,13 @@ def settings_proj(testproject): mnemonic: brownie2 """ with testproject._path.joinpath("brownie-config.yaml").open("w") as fp: - yaml.dump(yaml.load(test_brownie_config), fp) + yaml.dump(yaml.safe_load(test_brownie_config), fp) # Load the networks.development config from the created file and yield it with testproject._path.joinpath("brownie-config.yaml").open() as fp: conf = yaml.safe_load(fp)["networks"]["development"] - yield conf - os.remove(testproject._path.joinpath("brownie-config.yaml")) + yield conf def test_load_project_cmd_settings(config, testproject, settings_proj): @@ -47,7 +45,7 @@ def test_load_project_cmd_settings(config, testproject, settings_proj): config_path_network = _get_data_folder().joinpath("network-config.yaml") cmd_settings_network_raw = _load_config(config_path_network)["development"][0]["cmd_settings"] - # compare the manually loaded cmd_settings to the cmd_settings in the CONFIG singleton + # compare the manually loaded network cmd_settings to the cmd_settings in the CONFIG singleton cmd_settings_config = config.networks["development"]["cmd_settings"] for k, v in cmd_settings_config.items(): if k != "port": @@ -100,7 +98,7 @@ def test_validate_cmd_settings(): account_keys_path: ../../ fork: main """ - cmd_settings_dict = yaml.load(cmd_settings) + cmd_settings_dict = yaml.safe_load(cmd_settings) valid_dict = _validate_cmd_settings(cmd_settings_dict) for (k, v) in cmd_settings_dict.items(): assert valid_dict[k] == v