PPugarHuda

pugarhuda/brownie-to-ape

Automated migration codemod from Brownie to ApeWorx Ape framework — imports, deploy syntax, transaction kwargs, network/chain API, reverts, conftest fixtures

brownieapeapeworxpythonethereumsmart-contractsmigration
Public
17 executions
Run locally
npx codemod @pugarhuda/brownie-to-ape
Documentation

brownie-to-ape

<p align="center"> <img src="./docs/banner.svg" alt="brownie-to-ape — 250 tests, 5 OSS repos, 0 false positives, ape test 38/38 passed" width="100%" /> </p>

CI
License: MIT
Tests
FP Rate
Validated repos
ape test
Version
Mutation
jssg

Automated migration codemod from Brownie to ApeWorx Ape. 17-pass deterministic transform built on Codemod's jssg engine. Validated on 5 real OSS Brownie projects (incl. Yearn Finance) with zero false positives.

🌐 Codemod registry: https://app.codemod.com/registry/@pugarhuda/brownie-to-ape · 🇮🇩 Bahasa Indonesia: README.id.md

Submitted to the Codemod Boring AI hackathon (Track 1: Production Migration Recipes + Track 2: Public Case Study).

Live demo: https://pugarhuda.github.io/brownie-to-ape/
Registry: @pugarhuda/brownie-to-ape (app.codemod.com)

TL;DR

bash

Or:

bash

📋 Hackathon evaluator? See EVALUATOR.md for the
3-step evaluation walkthrough (~15–20 min total): codemod run → AI
cleanup → ape compile && ape test verification. End-to-end AI-step
walkthrough on token-mix: demo/ai-step-demo.md.
Engineering tradeoffs / deferred features:
docs/DEFERRED_FEATURES.md.

Why use this

Brownie was deprecated in 2023; ApeWorX Ape is the recommended successor. A typical Brownie test suite contains 50–200 mechanical pattern rewrites: every Contract.deploy(…, {"from": acct}) becomes Contract.deploy(…, sender=acct), every network.show_active() becomes networks.active_provider.network.name, etc.

Manual migrationbrownie-to-ape
Time per repo (typical)half-day to 1 day~30 minutes (3s codemod + AI/human review)
Mechanical pattern rewrites100% manual~80–95% automated
Tx-dict → kwargs (deploy / method calls)Find-replace per fileOne pass, zero FP
network.show_active() in subscripts/f-stringsHand-edit eachAuto-rewritten
Exception class renamesLook up Ape docs per nameVirtualMachineErrorContractLogicError automatic
Risk of regressionsMedium (typos, missed sites)Zero (validated on 5 OSS repos incl. Yearn Finance)
brownie-config.yamlape-config.yamlManual rewritescripts/migrate_config.py handles known fields

What it migrates

#PatternBefore (Brownie)After (Ape)
1Module import renamefrom brownie import networkfrom ape import networks
2Contract artifact importfrom brownie import FundMe# TODO(brownie-to-ape): … FundMe
3Built-in unsupported namesfrom brownie import exceptions, WeiTODO comment, dropped from import
4Bare module importimport brownie (when brownie.reverts/.accounts used)import ape
5Reverts / accounts / project / config / chainbrownie.reverts(...)ape.reverts(...)
6Active network namebrownie.network.show_active() and bare network.show_active()networks.active_provider.network.name
7Tx-dict → kwargsContract.deploy(arg, {"from": x, "value": v})Contract.deploy(arg, sender=x, value=v)
8Tx-dict with trailing kwargdeploy(addr, {"from": x}, publish_source=...)deploy(addr, sender=x, publish_source=...)
9chain.mine(N) positional argchain.mine(10)chain.mine(num_blocks=10)
10chain.sleep(N) as a statementchain.sleep(60)chain.pending_timestamp += 60
11Brownie exception class namesexceptions.VirtualMachineError, brownie.exceptions.VirtualMachineErrorexceptions.ContractLogicError, ape.exceptions.ContractLogicError
12accounts.add(pk) inline TODOaccounts.add(pk)accounts.add(pk) # TODO: Ape uses accounts.import_account_from_private_key(...)
13Brownie's isolate(fn_isolation): pass fixturedef isolate(fn_isolation): passTODO comment above the decorator (Ape has chain.isolate() built-in)
14Wei("X") calls (auto-rewrite!)Wei("1 ether")convert("1 ether", int) + auto-injects from ape.utils import convert
15interface.X(addr) callsinterface.IERC20(addr)interface.IERC20(addr) # TODO: Ape's Contract(addr) with explicit ABI/type
16accounts.at(addr, force=True)accounts.at(WHALE, force=True)accounts.impersonate_account(WHALE) (strict force-keyword guard)
17tx.events[N][key] (event index access)tx.events[0]["amount"]tx.events[0].event_arguments["amount"]
18tx.events["Name"][key] (event name access)tx.events["Transfer"]["to"][log for log in tx.events if log.event_name == "Transfer"][0].event_arguments["to"]
19<Contract>[-1] / len(<C>) / <C>.at(addr)Token[-1], Token.at(addr)project.Token.deployments[-1], project.Token.at(addr)
20web3.eth.get_balance(X)web3.eth.get_balance(addr)chain.get_balance(addr) + auto-injects from ape import chain
21ZERO_ADDRESS importfrom brownie import ZERO_ADDRESSfrom ape.utils import ZERO_ADDRESS (moved to ape.utils)
22Unknown exceptions.X referencesexceptions.SomeUnknownExcTODO at top of file listing unmapped exception names
BonusYAML config helper (scripts/migrate_config.py)brownie-config.yamlape-config.yaml (networks, solidity, dependencies translated; legacy file preserved)

Install & Run

bash

Validated on real OSS repos

Tested on five Brownie OSS projects covering different shapes (token tutorial, oracle deploy, multi-network lottery with VRF mocks, Aave DeFi integration, Yearn Finance strategy template):

RepoFiles modifiedPatterns auto-migratedFalse positives
brownie-mix/token-mix4 / 5 .py~62 (3 imports, 30+ tx-dicts, 6 reverts, 2 bare imports, 1 isolate fixture)0
PatrickAlphaC/brownie_fund_me5 / 6 .py~21 (5 imports, 8 tx-dicts, 8 show_active, 1 exception rename, 1 accounts.add TODO)0
PatrickAlphaC/smartcontract-lottery5 / 7 .py~30 (5 imports, 13 tx-dicts, 9 show_active, 1 exception rename)0
PatrickAlphaC/aave_brownie_py_freecode4 / 5 .py~24 (3 imports, 8 tx-dicts, 7 show_active, 5 interface TODOs)0
yearn/brownie-strategy-mix4 / 7 .py~33 (3 imports incl. web3 preserve, 8 tx-dicts, 5 show_active, 4 contract drops + auto-add project)0

Combined: 22/30 files modified across 5 OSS repos. ~170 patterns auto-migrated. 0 false positives.

See CASE_STUDY.md for the full write-up, DEMO.md for curated before/after examples, API_REFERENCE.md for the comprehensive Brownie→Ape pattern map, and benchmark/results.md for timed runs across all five repos.

Zero-False-Positive Guards

The codemod is engineered to never make incorrect changes. Key guards:

  1. File-level marker — transforms only run on files that contain the substring brownie. Files with unrelated {"from": x} patterns (email APIs, regular dicts) are untouched.
  2. Tx-dict whitelist — a dict literal is treated as a Brownie tx-dict only if every key is in {"from", "value", "gas", "gas_limit", "gas_price", "max_fee", "priority_fee", "nonce", "required_confs", "allow_revert"} AND "from" is present.
  3. Contract-name heuristic — uppercase names that aren't built-in Brownie module names (accounts, network, chain, config, project) are assumed to be contract artifacts and dropped from imports with a TODO comment.
  4. brownie.network not auto-renamed — only the specific brownie.network.show_active() pattern is rewritten (to the specific Ape equivalent). Other brownie.network.* is left for manual review since Ape exposes them differently.
  5. Replace dict node, not arg list — preserves edits to surrounding positional args (e.g. brownie.accounts[0] inside the same call gets renamed independently).
  6. Wildcard from brownie import * skipped — too risky to rewrite without symbol tracking.

What's NOT auto-migrated (intentionally)

These patterns are flagged with # TODO(brownie-to-ape): … for manual review or AI-assisted follow-up. They are by design left manual to keep FP at zero.

  • Contract artifacts (Token, FundMe, etc.) — Ape uses project.<ContractName> access. The codemod can't infer the project structure.
  • MockV3Aggregator[-1] style — Brownie's "last deployed" subscript. Ape uses project.<Name>.deployments[-1].
  • accounts.add(private_key) — Ape requires accounts.import_account_from_private_key(alias, passphrase, key).
  • chain.sleep(N) in expressions — Only the statement form is auto-migrated. result = chain.sleep(N) (rare — Brownie returns None) is left alone since the rewrite would change semantics.
  • chain.mine(N, timedelta) — Brownie's two-arg form. Skipped (only single positional N is migrated to num_blocks=N).
  • brownie.exceptions.VirtualMachineError — class names differ in ape.exceptions.
  • brownie-config.yamlape-config.yaml — YAML config schema migration is out of jssg scope; needs a separate transform.

Project Layout

plaintext

Development

bash

Test suite (238 active tests, 0 failures)

SuiteTestsToolPurpose
jssg fixtures84Codemod CLIfull transform snapshot tests
Vitest unit50Vitestpure helpers in isolation
Vitest property11 active + 6 gatedVitestidempotency, determinism
Vitest QA53Vitestversion, docs, perf budget, golden-master
Python pytest29pytestYAML config translator (Describe* + Test*)
Plus: real-repo CI verificationGitHub Actionsape-verify.yml runs codemod inside Docker on freshly-cloned repos, see APE_VERIFY_REPORT.md

Rollback

If a codemod run produces something unexpected, every change is in your
target repo's working tree:

bash

The codemod never touches files outside --target and never overwrites
untracked files (the YAML helper renames legacy → .legacy).

Demo cast

A pre-recorded asciinema cast at demo/demo.cast
(asciicast v2, 44 events, ~14s). Play locally:

bash

FAQ

Q: Will running this break my codebase?
A: No. Every change is in your working tree until you git commit. If
the diff looks wrong, run git checkout -- '*.py' to discard. The
codemod is engineered for zero false positives — validated on 4 OSS
repos.

Q: I don't trust it. Can I preview first?
A: Yes. bash scripts/preview.sh /path/to/your/project runs in dry-run
mode and prints a per-file edit summary without modifying anything.
Or use --dry-run directly with codemod workflow run.

Q: My project uses Brownie + web3.py heavily. Will this migrate
web3.py too?

A: Partially. Web3.toWei(...) and Web3.fromWei(...) get inline
TODO comments pointing to Ape's convert(...). Other web3.py patterns
(web3.eth.X) are untouched — they're a different framework upgrade
(web3.py v6 → v7)
that warrants its own codemod.

Q: How long does manual cleanup take after the codemod?
A: Most projects: 5–30 minutes. The remaining work is ~5–20% of the
migration: replace contract artifact references with project.<Name>,
configure accounts via ape accounts import, run ape compile and
fix any compile errors. The codemod's TODO comments mark every spot
that needs attention.

Q: Do I have to use the bundled YAML config converter?
A: Optional but recommended. python scripts/migrate_config.py .
translates brownie-config.yaml to ape-config.yaml for known
fields. If you'd rather convert the YAML manually, just don't run it
— the codemod doesn't depend on it.

Q: Why is my first run so slow?
A: npx downloads the Codemod CLI on first invocation (~10–20s).
Subsequent runs are ~3 seconds. Install once with npm i -g codemod
to skip this.

Q: I have feature X (Curve / Yearn / etc.) in my Brownie repo. Will
it work?

A: The codemod only transforms Brownie SDK patterns — it doesn't
touch protocol-specific code. Validated repos already cover token
contracts, fund-me oracles, lottery + VRF, and Aave DeFi integration.
If you hit a Brownie pattern that isn't migrated, file a feature
request.

Troubleshooting

Symptom: codemod: command not found

  • Use npx codemod@latest … (no global install needed).
  • Or install once: npm i -g codemod.

Symptom: codemod runs but no files change.

  • The target may not be a Brownie project — only files containing the
    substring brownie are processed.
  • Run bash scripts/preview.sh <target> to see whether anything is
    detected.

Symptom: from ape import … line is missing some name.

  • The codemod intentionally drops names that have no direct Ape
    equivalent (contract artifacts, Wei, interface, etc.). Look for
    # TODO(brownie-to-ape): comments above the rewritten import.

Symptom: ape compile fails with NameError: contract not defined.

  • Brownie auto-injects contract artifacts into every namespace; Ape
    doesn't. Replace MyContract.deploy(...) with
    project.MyContract.deploy(...). The codemod's TODO comment marks
    these.

Symptom: pytest fails with

plaintext
.

  • The codemod maps VirtualMachineErrorContractLogicError and a
    few others, but unknown exception names need manual lookup. Check
    the Ape exceptions docs.

Symptom: Output has duplicate from ape.utils import convert.

  • Bug — file an issue. The dedup check (Pass 9) should catch this.

Symptom: Codemod CLI hangs in CI.

  • Pass --no-interactive --allow-dirty flags. The CLI prompts for
    confirmation by default if the target isn't a clean git tree.

License

MIT — see codemod.yaml.

Author

Pugar Huda Mantoro · pugarhudam@gmail.com

Before

This is one example from the codemod's test cases. The codemod may handle many more cases.

Ready to contribute?

Build your own codemod and share it with the community.