brownie-to-ape logo

brownie-to-ape

Automated Brownie โ†’ ApeWorx Ape migration codemod. 17-pass jssg transform, 250 tests (90 fixture + 125 Vitest + 35 pytest), validated on 5 real OSS repos with zero false positives. Built for the Codemod Boring AI hackathon.

One command~3 seconds per repo, zero false positives

# Run on any Brownie project โ€” git diff to see what changed npx codemod@latest @pugarhuda/brownie-to-ape -t /path/to/your/brownie/project

Watch it runasciinema cast โ€” clones token-mix, applies codemod, shows diff

Cast file: demo/demo.cast (44 events, ~14s). Reproduce locally: bash demo/run-demo.sh.

By the numbers

17transform passes
250tests (90 jssg + 125 Vitest + 35 pytest)
5OSS repos validated
22files migrated cleanly
0false positives
~3sper repo
RepoShape.py modifiedPatternsFP
yearn/brownie-strategy-mixYearn DeFi strategy template4 / 7~330
aave_brownie_py_freecodeAave DeFi integration4 / 5~240
smartcontract-lotteryMulti-network + VRF5 / 7~300
brownie_fund_meOracle integration5 / 6~210
brownie-mix/token-mixToken tutorial4 / 5~620

Before / after โ€” real diffs from real repos

Tx-dict โ†’ kwargs (the most pervasive Brownie pattern)

Brownie's signature {"from": acct, "value": v} dict at the end of a method call becomes plain kwargs. Handles trailing kwargs, multi-line, single + double + mixed quotes.

Brownietests/test_transferFrom.py
import brownie

def test_insufficient_balance(accounts, token):
    balance = token.balanceOf(accounts[0])
    token.approve(accounts[1], balance + 1, {'from': accounts[0]})
    with brownie.reverts():
        token.transferFrom(
            accounts[0], accounts[2], balance + 1,
            {'from': accounts[1]}
        )
Apeafter codemod
import ape

def test_insufficient_balance(accounts, token):
    balance = token.balanceOf(accounts[0])
    token.approve(accounts[1], balance + 1, sender=accounts[0])
    with ape.reverts():
        token.transferFrom(
            accounts[0], accounts[2], balance + 1,
            sender=accounts[1]
        )

network.show_active() in subscripts and f-strings

Bare network.show_active() rewrites correctly inside subscripts (config["networks"][...]) and f-strings.

Browniescripts/helpful_scripts.py
from brownie import network, config, accounts

def get_account():
    if (
        network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS
        or network.show_active() in FORKED_LOCAL_ENVIRONMENTS
    ):
        return accounts[0]


def deploy_mocks():
    print(f"Active network is {network.show_active()}")
Apeafter codemod
from ape import networks, config, accounts

def get_account():
    if (
        networks.active_provider.network.name in LOCAL_BLOCKCHAIN_ENVIRONMENTS
        or networks.active_provider.network.name in FORKED_LOCAL_ENVIRONMENTS
    ):
        return accounts[0]


def deploy_mocks():
    print(f"Active network is {networks.active_provider.network.name}")

Exception class names (with import preservation)

Known Brownie exceptions are mapped to their Ape equivalents. The exceptions import is kept (Ape exposes ape.exceptions too).

Brownietests/test_fund_me.py
from brownie import network, accounts, exceptions
import pytest

def test_only_owner_can_withdraw():
    bad_actor = accounts.add()
    with pytest.raises(exceptions.VirtualMachineError):
        fund_me.withdraw({"from": bad_actor})
Apeafter codemod
from ape import networks, accounts, exceptions
import pytest

def test_only_owner_can_withdraw():
    bad_actor = accounts.add()  # TODO: Ape uses accounts.import_account_from_private_key
    with pytest.raises(exceptions.ContractLogicError):
        fund_me.withdraw(sender=bad_actor)

Wei("X") โ†’ convert("X", int) with auto-import

Wei calls become convert calls; from ape.utils import convert is auto-injected at the top of the file. Idempotent โ€” won't double-import.

Browniescripts/setup.py
from brownie import Wei, accounts

def amounts():
    base = Wei("1 ether")
    fee = Wei("100 gwei")
    return base, fee
Apeafter codemod
# TODO(brownie-to-ape): no direct Ape equivalent for: Wei
from ape import accounts

from ape.utils import convert
def amounts():
    base = convert("1 ether", int)
    fee = convert("100 gwei", int)
    return base, fee

accounts.at(addr, force=True) โ†’ accounts.impersonate_account(addr)

Brownie's whale-impersonation idiom maps cleanly to Ape's dedicated API. Only fires when force=True is present โ€” bare accounts.at(addr) stays alone (different operation).

Brownietests/conftest.py
from brownie import accounts

@pytest.fixture
def whale():
    return accounts.at(WHALE_ADDR, force=True)

@pytest.fixture
def existing_owner():
    # No force=True โ€” just address lookup, codemod leaves alone.
    return accounts.at(OWNER_ADDR)
Apeafter codemod
from ape import accounts

@pytest.fixture
def whale():
    return accounts.impersonate_account(WHALE_ADDR)

@pytest.fixture
def existing_owner():
    # No force=True โ€” just address lookup, codemod leaves alone.
    return accounts.at(OWNER_ADDR)

Multi-line interface call (Aave repo)

Multi-pass composition: inner network.show_active() rewrite (Pass 2b) and outer interface.X() TODO (Pass 10) compose cleanly because Pass 10 replaces only the closing ) token.

Browniescripts/aave_borrow.py
def get_lending_pool():
    addr_provider = interface.ILendingPoolAddressesProvider(
        config["networks"][network.show_active()]["lending_pool_addresses_provider"]
    )
    pool_addr = addr_provider.getLendingPool()
    pool = interface.ILendingPool(pool_addr)
    return pool
Apeafter codemod
def get_lending_pool():
    addr_provider = interface.ILendingPoolAddressesProvider(
        config["networks"][networks.active_provider.network.name]["lending_pool_addresses_provider"]
    )  # TODO(brownie-to-ape): interface.X(addr) -> Ape's Contract(addr) with explicit ABI
    pool_addr = addr_provider.getLendingPool()
    pool = interface.ILendingPool(pool_addr)  # TODO(brownie-to-ape): interface.X(addr) -> ...
    return pool

Why this is the strongest Brownie โ†’ Ape codemod on the registry

โš–๏ธ Zero-FP discipline

15+ negative tests prove the codemod does NOT fire in FP-risk contexts: lambda, list comprehension, walrus operator, async/await, OrderedDict, helper functions, malformed Python, dict spread, byte-string keys, brownie-only-in-strings.

๐Ÿงช Real-world breadth

Validated on 5 OSS repos covering different shapes: tutorial token contract, oracle (Chainlink), multi-network lottery + VRF, Aave DeFi integration, and the Yearn Finance strategy template (used by all Yearn strategy developers).

๐Ÿค– AI-friendly TODOs

Patterns the codemod can't safely auto-rewrite (contract artifacts, accounts.add private key, unknown exceptions) get specific, actionable inline TODO comments. AI cleanup steps know exactly what to fix.

๐Ÿ› ๏ธ Adoption tooling

Bundled scripts/preview.sh for dry-run with structured stats, scripts/benchmark.sh for repeatable timing across 5 repos, scripts/migrate_config.py for brownie-config.yaml โ†’ ape-config.yaml conversion, and an asciinema cast.