|
| 1 | +# Brownie to Ape Migration |
| 2 | + |
| 3 | +[Brownie is no longer actively maintained. The Brownie README directs Python Ethereum developers to Ape Framework.](https://github.com/eth-brownie/brownie#readme) This guide documents a practical migration path for Brownie projects moving to Ape and references [ApeWorX/ape issue #640](https://github.com/ApeWorX/ape/issues/640), which originally tracked Brownie project migration support. |
| 4 | + |
| 5 | +## Automated Migration with ApeShift |
| 6 | + |
| 7 | +Run ApeShift to apply deterministic Brownie-to-Ape rewrites, validation, reports, and manual-review TODOs. |
| 8 | + |
| 9 | +```bash |
| 10 | +npx apeshift migrate . |
| 11 | +``` |
| 12 | + |
| 13 | +ApeShift is published in the Codemod registry: https://app.codemod.com/registry/apeshift |
| 14 | + |
| 15 | +> **Note on tooling:** ApeShift is distributed through the Codemod registry, which uses npm packaging for codemod distribution. |
| 16 | +> The migration itself operates on Python files using ast-grep rules that parse and transform Python AST directly. |
| 17 | +> No JavaScript runtime is involved in the user's Python project code. |
| 18 | +> A Python-native wrapper may be added in the future. |
| 19 | +
|
| 20 | +## Imports |
| 21 | + |
| 22 | +Brownie scripts often import accounts, contracts, config, and networks from `brownie`. Migrated Ape scripts import public Ape APIs and access contracts through `project`. |
| 23 | + |
| 24 | +```python |
| 25 | +# Before |
| 26 | +from brownie import accounts, config, SimpleStorage, network |
| 27 | + |
| 28 | +# After |
| 29 | +from ape import accounts, config, networks, project |
| 30 | +``` |
| 31 | + |
| 32 | +Official docs: [Contracts](./contracts.html) |
| 33 | + |
| 34 | +## Accounts |
| 35 | + |
| 36 | +Use `accounts.test_accounts` for local generated accounts and `accounts.load()` for named account aliases. The alias must be imported by the user before live-network use. |
| 37 | + |
| 38 | +```python |
| 39 | +# Before |
| 40 | +if network.show_active() == "development": |
| 41 | + return accounts[0] |
| 42 | +return accounts.add(config["wallets"]["from_key"]) |
| 43 | + |
| 44 | +# After |
| 45 | +if networks.provider.network.name == "development": |
| 46 | + return accounts.test_accounts[0] |
| 47 | +return accounts.add(config["wallets"]["from_key"]) # TODO(apeshift): accounts.add(key) not valid in Ape; use accounts.load("account-name") after: ape accounts import <name> |
| 48 | +``` |
| 49 | + |
| 50 | +Official docs: [Accounts](./accounts.html) |
| 51 | + |
| 52 | +## Contract Deployment and Transactions |
| 53 | + |
| 54 | +Brownie transaction dictionaries become explicit Ape keyword arguments such as `sender=` and `value=`. |
| 55 | + |
| 56 | +```python |
| 57 | +# Before |
| 58 | +simple_storage = SimpleStorage.deploy({"from": account}) |
| 59 | +transaction = simple_storage.store(15, {"from": account}) |
| 60 | + |
| 61 | +# After |
| 62 | +simple_storage = project.SimpleStorage.deploy(sender=account) |
| 63 | +transaction = simple_storage.store(15, sender=account) |
| 64 | +``` |
| 65 | + |
| 66 | +For payable calls: |
| 67 | + |
| 68 | +```python |
| 69 | +# Before |
| 70 | +tx = fund_me.fund({"from": account, "value": entrance_fee}) |
| 71 | + |
| 72 | +# After |
| 73 | +tx = fund_me.fund(sender=account, value=entrance_fee) |
| 74 | +``` |
| 75 | + |
| 76 | +Official docs: [Contracts](./contracts.html) |
| 77 | + |
| 78 | +## Networks |
| 79 | + |
| 80 | +Brownie's active-network helper maps to Ape's active provider network metadata. |
| 81 | + |
| 82 | +```python |
| 83 | +# Before |
| 84 | +print(f"The active network is {network.show_active()}") |
| 85 | + |
| 86 | +# After |
| 87 | +print(f"The active network is {networks.provider.network.name}") |
| 88 | +``` |
| 89 | + |
| 90 | +Official docs: [Networks](./networks.html) |
| 91 | + |
| 92 | +## Testing and Reverts |
| 93 | + |
| 94 | +Brownie revert helpers and exceptions map to Ape testing helpers and exceptions. |
| 95 | + |
| 96 | +```python |
| 97 | +# Before |
| 98 | +with brownie.reverts("Ownable: caller is not the owner"): |
| 99 | + fund_me.withdraw({"from": bad_actor}) |
| 100 | + |
| 101 | +# After |
| 102 | +with ape.reverts("Ownable: caller is not the owner"): |
| 103 | + fund_me.withdraw(sender=bad_actor) |
| 104 | +``` |
| 105 | + |
| 106 | +```python |
| 107 | +# Before |
| 108 | +with pytest.raises(exceptions.VirtualMachineError): |
| 109 | + fund_me.withdraw({"from": bad_actor}) |
| 110 | + |
| 111 | +# After |
| 112 | +from ape.exceptions import ContractLogicError |
| 113 | + |
| 114 | +with pytest.raises(ContractLogicError): |
| 115 | + fund_me.withdraw(sender=bad_actor) |
| 116 | +``` |
| 117 | + |
| 118 | +Official docs: [Testing](./testing.html) |
| 119 | + |
| 120 | +## Testing: pytest Fixtures |
| 121 | + |
| 122 | +Ape tests use pytest fixtures from the `ape-test` plugin. |
| 123 | +In Ape, contract types are not injected as pytest fixtures. |
| 124 | +Use the `project` fixture and access contracts as `project.ContractName`. |
| 125 | + |
| 126 | +Remove Brownie's `fn_isolation` fixture when migrating tests: |
| 127 | + |
| 128 | +```python |
| 129 | +# Before |
| 130 | +@pytest.fixture(autouse=True) |
| 131 | +def isolate(fn_isolation): |
| 132 | + pass |
| 133 | + |
| 134 | +# After |
| 135 | +# Remove entirely — Ape handles test isolation through its pytest plugin. |
| 136 | +``` |
| 137 | + |
| 138 | +Use Ape's `accounts` fixture with `project` for deployments: |
| 139 | + |
| 140 | +```python |
| 141 | +# Before |
| 142 | +def test_deploy(Token, accounts): |
| 143 | + account = accounts[0] |
| 144 | + token = Token.deploy({"from": account}) |
| 145 | + |
| 146 | +# After |
| 147 | +def test_deploy(project, accounts): |
| 148 | + account = accounts[0] |
| 149 | + token = project.Token.deploy(sender=account) |
| 150 | +``` |
| 151 | + |
| 152 | +Update contract fixture patterns the same way: |
| 153 | + |
| 154 | +```python |
| 155 | +# Before |
| 156 | +@pytest.fixture |
| 157 | +def token(Token, accounts): |
| 158 | + return Token.deploy({"from": accounts[0]}) |
| 159 | + |
| 160 | +# After |
| 161 | +@pytest.fixture |
| 162 | +def token(project, accounts): |
| 163 | + return project.Token.deploy(sender=accounts[0]) |
| 164 | +``` |
| 165 | + |
| 166 | +If tests manually use chain snapshots, `chain.snapshot()` remains similar, but Brownie's `chain.revert()` should become Ape's `chain.restore()`. |
| 167 | + |
| 168 | +```python |
| 169 | +# Before |
| 170 | +chain.snapshot() |
| 171 | +chain.revert() |
| 172 | + |
| 173 | +# After |
| 174 | +chain.snapshot() |
| 175 | +chain.restore() |
| 176 | +``` |
| 177 | + |
| 178 | +Brownie provides a `web3` pytest fixture for direct Web3.py access in tests. |
| 179 | +Ape does not provide a `web3` fixture. |
| 180 | + |
| 181 | +If direct Web3 access is needed, use the active provider: |
| 182 | + |
| 183 | +```python |
| 184 | +# Before |
| 185 | +def test_block_number(web3): |
| 186 | + assert web3.eth.block_number >= 0 |
| 187 | + |
| 188 | +# After |
| 189 | +from ape import networks |
| 190 | + |
| 191 | +def test_block_number(): |
| 192 | + assert networks.provider.web3.eth.block_number >= 0 |
| 193 | +``` |
| 194 | + |
| 195 | +Official docs: [Testing](./testing.html) |
| 196 | + |
| 197 | +## Config Files |
| 198 | + |
| 199 | +Brownie configuration values should move into Ape's `ape-config.yaml` structure. Wallet private keys are not copied into config; import an Ape account alias instead. |
| 200 | + |
| 201 | +```yaml |
| 202 | +# Before: brownie-config.yaml |
| 203 | +dependencies: |
| 204 | + - smartcontractkit/chainlink-brownie-contracts@1.1.1 |
| 205 | +compiler: |
| 206 | + solc: |
| 207 | + remappings: |
| 208 | + - "@chainlink=smartcontractkit/chainlink-brownie-contracts@1.1.1" |
| 209 | +wallets: |
| 210 | + from_key: ${PRIVATE_KEY} |
| 211 | +networks: |
| 212 | + development: |
| 213 | + verify: false |
| 214 | +``` |
| 215 | +
|
| 216 | +```yaml |
| 217 | +# After: ape-config.yaml |
| 218 | +name: migrated-ape-project |
| 219 | +plugins: |
| 220 | + - name: solidity |
| 221 | +solidity: |
| 222 | + version: 0.8.20 |
| 223 | + import_remapping: |
| 224 | + - "@chainlink=smartcontractkit/chainlink-brownie-contracts@1.1.1" |
| 225 | +ethereum: |
| 226 | + default_network: local |
| 227 | +# Import keys with `ape accounts import <alias>` and use `accounts.load("<alias>")`. |
| 228 | +networks: |
| 229 | + development: |
| 230 | + verify: false |
| 231 | +``` |
| 232 | +
|
| 233 | +Official docs: [Config](./config.html) |
| 234 | +
|
| 235 | +## Real-World Results |
| 236 | +
|
| 237 | +| Repo | Files | Patterns Before | Patterns After | Auto% | FP | FN | Syntax OK | Runtime Safe | Classification | |
| 238 | +|------|-------|----------------|----------------|-------|----|----|-----------|--------------|----------------| |
| 239 | +| brownie_simple_storage | 4 | 12 | 1 | 92% | 0 | 0 | ✅ | ✅ | PASS | |
| 240 | +| brownie_fund_me | 7 | 23 | 1 | 96% | 0 | 1 | ✅ | ✅ | DEPENDENCY_SOURCE_LAYOUT_BLOCKED | |
| 241 | +| chainlink-mix | 21 | 104 | 4 | 96% | 0 | 7 | ✅ | ✅ | DEPENDENCY_SOURCE_LAYOUT_BLOCKED | |
| 242 | +| brownie-nft-course | 18 | 76 | 11 | 86% | 0 | 3 | ✅ | ✅ | DEPENDENCY_SOURCE_LAYOUT_BLOCKED | |
| 243 | +| token-mix | 6 | 64 | 2 | 97% | 0 | 0 | ✅ | ✅ | PROJECT_TEST_SETUP_REVIEW | |
| 244 | +| **Combined** | 56 | 279 | 19 | 93% | **0** | 11 | ✅ | ✅ | | |
| 245 | +
|
| 246 | +Real Ape runtime validation was also run locally with Ape 0.8.48: |
| 247 | +
|
| 248 | +| Repo | Ape Compile | Ape Test | Notes | |
| 249 | +|------|-------------|----------|-------| |
| 250 | +| brownie_simple_storage | ✅ PASS | ✅ 2 passed | Fully validated | |
| 251 | +| brownie_fund_me | ❌ FAIL | ❌ 2 failed | Chainlink dependency source layout unresolved | |
| 252 | +| chainlink-mix | ❌ FAIL | ❌ collection error | Chainlink dependency source layout and import-time provider access unresolved | |
| 253 | +| brownie-nft-course | ❌ FAIL | ❌ collection error | Chainlink/OpenZeppelin dependency source layout unresolved | |
| 254 | +| token-mix | ✅ PASS | ❌ collection error | Brownie `fn_isolation` fixture requires Ape pytest isolation fixture migration | |
| 255 | + |
| 256 | +## What Remains Manual |
| 257 | + |
| 258 | +1. `web3.eth.contract(...)` is deterministically rewritten to `Contract(address, abi=...)` with a TODO if the ABI source is unclear. |
| 259 | +2. `accounts.load()` aliases require a human to choose and import the account name. |
| 260 | +3. Complex event filters receive TODO comments with exact guidance. |
| 261 | +4. `from brownie.network import priority_fee` receives a TODO because Ape has no safe deterministic equivalent. |
0 commit comments