Brownie Suite Installation
Brownie is a Python framework for Ethereum development with full support for both Solidity and Vyper. You do not need to install Vyper separately: eth-brownie declares a pinned vyper (and vvm, the Vyper version manager) as a dependency, and it installs and selects the compiler version your contracts need automatically. Put your Vyper sources in contracts/*.vy and Brownie compiles them.
Install Brownie with pipx (not plain pip), so it stays isolated and you do not need a virtualenv:
python3 -m pip install --user pipx
python3 -m pipx ensurepath
pipx does not ship with Python. After ensurepath you may need to open a new terminal before the updated PATH is available. Then install Brownie:
pipx install eth-brownie
To upgrade it later:
pipx upgrade eth-brownie
Brownie uses Ganache as its local development network. Ganache must be installed, but you do not start it by hand — Brownie launches and connects to it automatically each time it loads:
npm install -g ganache
Note that ganache-cli was renamed to ganache, so install the current package.
In an empty directory, initialize a new project and open the console:
mkdir brownietest
cd brownietest
brownie init --force
brownie compile
brownie console
brownie console starts its own Ganache instance for you, so there is no separate ganache step. Use brownie console --network <id> if you want to connect to a different network instead.
Then you can run the script of brownie under scripts/testscript.py with the following command:
brownie run scripts/testscript.py dummycall
A Minimal Vyper Contract
With the project initialized, put a Vyper contract in contracts/GreeterVyper.vy. The # pragma version line is what tells Brownie which compiler to use — it installs the matching Vyper automatically if it is missing:
# pragma version >=0.4.0
owner: public(address)
greeting: public(String[100])
@deploy
def __init__(_greeting: String[100]):
self.owner = msg.sender
self.greeting = _greeting
@external
def set_greeting(_greeting: String[100]):
assert msg.sender == self.owner, "not owner"
self.greeting = _greeting
@external
@view
def greet() -> String[100]:
return self.greeting
The owner and greeting state variables are declared public, so Vyper generates getters for them. The @deploy decorator marks the constructor in Vyper 0.4.x (older 0.3.x uses @external def __init__(...) instead).
The Same Contract in Solidity
Brownie compiles both languages side by side, so the identical logic in contracts/Greeter.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Greeter {
string public greeting;
address public owner;
event GreetingChanged(address indexed by, string newGreeting);
constructor(string memory _greeting) {
greeting = _greeting;
owner = msg.sender;
}
function setGreeting(string calldata _greeting) external {
require(msg.sender == owner, "not owner");
greeting = _greeting;
emit GreetingChanged(msg.sender, _greeting);
}
}
Testing Both with pytest
Brownie exposes every contract in your project as a pytest fixture named after the contract, plus built-in fixtures like accounts. Tests live in tests/ and their filenames must start with test_. Reverted transactions are checked with brownie.reverts:
# tests/test_greeter.py
import brownie
def test_vyper_initial_greeting(GreeterVyper, accounts):
greeter = GreeterVyper.deploy("hello from Vyper", {"from": accounts[0]})
assert greeter.greeting() == "hello from Vyper"
assert greeter.owner() == accounts[0].address
def test_vyper_owner_can_update(GreeterVyper, accounts):
greeter = GreeterVyper.deploy("hello", {"from": accounts[0]})
greeter.set_greeting("hola", {"from": accounts[0]})
assert greeter.greet() == "hola"
def test_vyper_non_owner_reverts(GreeterVyper, accounts):
greeter = GreeterVyper.deploy("hello", {"from": accounts[0]})
with brownie.reverts("not owner"):
greeter.set_greeting("nope", {"from": accounts[1]})
def test_solidity_initial_greeting(Greeter, accounts):
greeter = Greeter.deploy("hello from Solidity", {"from": accounts[0]})
assert greeter.greeting() == "hello from Solidity"
assert greeter.owner() == accounts[0].address
def test_solidity_owner_can_update(Greeter, accounts):
greeter = Greeter.deploy("hello", {"from": accounts[0]})
greeter.setGreeting("hola", {"from": accounts[0]})
assert greeter.greeting() == "hola"
def test_solidity_non_owner_reverts(Greeter, accounts):
greeter = Greeter.deploy("hello", {"from": accounts[0]})
with brownie.reverts("not owner"):
greeter.setGreeting("nope", {"from": accounts[1]})
brownie test runs the suite; brownie test --coverage adds a coverage report and brownie test -n auto parallelizes it.
A Deploy Script
Deployment scripts are plain Python functions. Brownie hands them the accounts container, and each call that changes state needs a {"from": ...} sender:
# scripts/deploy.py
from brownie import Greeter, GreeterVyper, accounts
def main():
owner = accounts[0]
Greeter.deploy("hello from Solidity", {"from": owner})
GreeterVyper.deploy("hello from Vyper", {"from": owner})
The finished project looks like this:
brownietest/
├─ contracts/
│ ├─ Greeter.sol
│ └─ GreeterVyper.vy
├─ scripts/
│ └─ deploy.py
└─ tests/
└─ test_greeter.py
And the three commands you will use every day:
brownie compile # compiles both contracts
brownie test # runs the pytest suite
brownie run scripts/deploy.py # deploys to the local Ganache network
Comments
Leave a comment