Skip to content

Commit

Permalink
fix: issue where could not define custom networks in any non-local pr…
Browse files Browse the repository at this point in the history
…oject (ApeWorX#2153)
  • Loading branch information
antazoey authored Jun 19, 2024
1 parent 5ae5360 commit fad486d
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 37 deletions.
46 changes: 26 additions & 20 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
from collections.abc import Collection, Iterator, Sequence
from functools import partial
from pathlib import Path
Expand Down Expand Up @@ -237,48 +238,52 @@ def networks(self) -> dict[str, "NetworkAPI"]:
networks = {**self._networks_from_plugins}

# Include configured custom networks.
custom_networks: list = [
custom_networks: list[dict] = [
n
for n in self.config_manager.get_config("networks").custom
if (n.ecosystem or self.network_manager.default_ecosystem.name) == self.name
for n in self.network_manager.custom_networks
if n.get("ecosystem", self.network_manager.default_ecosystem.name) == self.name
]

# Ensure forks are added automatically for custom networks.
forked_custom_networks = []
for net in custom_networks:
if net.name.endswith("-fork"):
if net["name"].endswith("-fork"):
# Already a fork.
continue

fork_network_name = f"{net.name}-fork"
if any(x.name == fork_network_name for x in custom_networks):
fork_network_name = f"{net['name']}-fork"
if any(x["name"] == fork_network_name for x in custom_networks):
# The forked version of this network is already known.
continue

# Create a forked network mirroring the custom network.
forked_net = net.model_copy(deep=True)
forked_net.name = fork_network_name
forked_net = copy.deepcopy(net)
forked_net["name"] = fork_network_name
forked_custom_networks.append(forked_net)

# NOTE: Forked custom networks are still custom networks.
custom_networks.extend(forked_custom_networks)

for custom_net in custom_networks:
if custom_net.name in networks:
model_data = copy.deepcopy(custom_net)
net_name = custom_net["name"]
if net_name in networks:
raise NetworkError(
f"More than one network named '{custom_net.name}' in ecosystem '{self.name}'."
f"More than one network named '{net_name}' in ecosystem '{self.name}'."
)

is_fork = custom_net.is_fork
network_data = custom_net.model_dump(by_alias=True, exclude=("default_provider",))
network_data["ecosystem"] = self
is_fork = net_name.endswith("-fork")
model_data["ecosystem"] = self
network_type = create_network_type(
custom_net.chain_id, custom_net.chain_id, is_fork=is_fork
custom_net["chain_id"], custom_net["chain_id"], is_fork=is_fork
)
network_api = network_type.model_validate(network_data)
network_api._default_provider = custom_net.default_provider
if "request_header" not in model_data:
model_data["request_header"] = self.request_header

network_api = network_type.model_validate(model_data)
network_api._default_provider = custom_net.get("default_provider", "node")
network_api._is_custom = True
networks[custom_net.name] = network_api
networks[net_name] = network_api

return networks

Expand Down Expand Up @@ -503,15 +508,16 @@ def get_network(self, network_name: str) -> "NetworkAPI":
"""

names = {network_name, network_name.replace("-", "_"), network_name.replace("_", "-")}
networks = self.networks
for name in names:
if name in self.networks:
return self.networks[name]
if name in networks:
return networks[name]

elif name == "custom":
# Is an adhoc-custom network NOT from config.
return self.custom_network

raise NetworkNotFoundError(network_name, ecosystem=self.name, options=self.networks)
raise NetworkNotFoundError(network_name, ecosystem=self.name, options=networks)

def get_network_data(
self, network_name: str, provider_filter: Optional[Collection[str]] = None
Expand Down
8 changes: 6 additions & 2 deletions src/ape/managers/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,10 +334,14 @@ def outgoing(self) -> Iterator[ReceiptAPI]:
# TODO: Add ephemeral network sessional history to `ape-cache` instead,
# and remove this (replace with `yield from iter(self[:len(self)])`)
for receipt in self.sessional:
if receipt.nonce < start_nonce:
if receipt.nonce is None:
# Not an on-chain receipt? idk - has only seen as anomaly in tests.
continue

elif receipt.nonce < start_nonce:
raise QueryEngineError("Sessional history corrupted")

if receipt.nonce > start_nonce:
elif receipt.nonce > start_nonce:
# NOTE: There's a gap in our sessional history, so fetch from query engine
yield from iter(self[start_nonce : receipt.nonce + 1]) # noqa: E203

Expand Down
26 changes: 23 additions & 3 deletions src/ape/managers/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class NetworkManager(BaseManager, ExtraAttributesMixin):
_active_provider: Optional[ProviderAPI] = None
_default_ecosystem_name: Optional[str] = None

# For adhoc adding custom networks, or incorporating some defined
# in other projects' configs.
_custom_networks: list[dict] = []

@log_instead_of_fail(default="<NetworkManager>")
def __repr__(self) -> str:
provider = self.active_provider
Expand Down Expand Up @@ -165,17 +169,33 @@ def provider_names(self) -> set[str]:
for provider in network.providers
)

@property
def custom_networks(self) -> list[dict]:
"""
Custom network data defined in various ape-config files
or added adhoc to the network manager.
"""
return [
*[
n.model_dump(by_alias=True)
for n in self.config_manager.get_config("networks").get("custom", [])
],
*self._custom_networks,
]

@property
def ecosystems(self) -> dict[str, EcosystemAPI]:
"""
All the registered ecosystems in ``ape``, such as ``ethereum``.
"""
plugin_ecosystems = self._plugin_ecosystems

# Load config.
custom_networks: list = self.config_manager.get_config("networks").get("custom", [])
# Load config-based custom ecosystems.
# NOTE: Non-local projects will automatically add their custom networks
# to `self.custom_networks`.
custom_networks: list = self.custom_networks
for custom_network in custom_networks:
ecosystem_name = custom_network.ecosystem
ecosystem_name = custom_network["ecosystem"]
if ecosystem_name in plugin_ecosystems:
# Already included in previous network.
continue
Expand Down
5 changes: 5 additions & 0 deletions src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,11 @@ def __init__(
if data:
self.update_manifest(**data)

# Ensure any custom networks will work, otherwise Ape's network manager
# only knows about the "local" project's.
if custom_nets := (config_override or {}).get("networks", {}).get("custom", []):
self.network_manager._custom_networks.extend(custom_nets)

@log_instead_of_fail(default="<ProjectManager>")
def __repr__(self):
path = f" {clean_path(self.path)}"
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ def wrapper(fn):
compiler = ape.compilers.get_compiler(name)
if compiler:

def test_skip_from_compiler():
def test_skip_from_compiler(*args, **kwargs):
pytest.mark.skip(msg_f.format(name))

# NOTE: By returning a function, we avoid a collection warning.
Expand Down
1 change: 1 addition & 0 deletions tests/functional/geth/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def custom_network_connection(
):
data = copy.deepcopy(custom_networks_config_dict)
data["networks"]["custom"][0]["chain_id"] = geth_provider.chain_id

config = {
ethereum.name: {custom_network_name_0: {"default_transaction_type": 0}},
geth_provider.name: {ethereum.name: {custom_network_name_0: {"uri": geth_provider.uri}}},
Expand Down
8 changes: 4 additions & 4 deletions tests/functional/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Optional, Union

import pytest
from pydantic import ValidationError
from pydantic_settings import SettingsConfigDict

from ape.api.config import ApeConfig, ConfigEnum, PluginConfig
Expand Down Expand Up @@ -218,10 +219,9 @@ def test_network_gas_limit_invalid_numeric_string(project):
Test that using hex strings for a network's gas_limit config must be
prefixed with '0x'
"""
eth_config = _sepolia_with_gas_limit("4D2")
with project.temp_config(**eth_config):
with pytest.raises(AttributeError, match="Gas limit hex str must include '0x' prefix."):
_ = project.config.ethereum
sep_cfg = _sepolia_with_gas_limit("4D2")["ethereum"]["sepolia"]
with pytest.raises(ValidationError, match="Gas limit hex str must include '0x' prefix."):
NetworkConfig.model_validate(sep_cfg)


def test_dependencies(project_with_dependency_config):
Expand Down
9 changes: 6 additions & 3 deletions tests/functional/test_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,10 @@ def test_compile(self, project):
api = LocalDependency(local=path, name="ooga", version="1.0.0")
dependency = Dependency(api, project)
contract_path = dependency.project.contracts_folder / "CCC.json"
contract_path.parent.mkdir(exist_ok=True, parents=True)
contract_path.write_text(
'[{"name":"foo","type":"fallback", "stateMutability":"nonpayable"}]'
'[{"name":"foo","type":"fallback", "stateMutability":"nonpayable"}]',
encoding="utf8",
)
result = dependency.compile()
assert len(result) == 1
Expand All @@ -630,9 +632,10 @@ def test_compile_missing_compilers(self, project, ape_caplog):
api = LocalDependency(local=path, name="ooga2", version="1.1.0")
dependency = Dependency(api, project)
sol_path = dependency.project.contracts_folder / "Sol.sol"
sol_path.write_text("// Sol")
sol_path.parent.mkdir(exist_ok=True, parents=True)
sol_path.write_text("// Sol", encoding="utf8")
vy_path = dependency.project.contracts_folder / "Vy.vy"
vy_path.write_text("# Vy")
vy_path.write_text("# Vy", encoding="utf8")
expected = (
"Compiling dependency produced no contract types. "
"Try installing 'ape-solidity' or 'ape-vyper'."
Expand Down
4 changes: 0 additions & 4 deletions tests/functional/test_ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,10 +643,6 @@ class L2NetworkConfig(BaseEthereumConfig):
assert config.mainnet_fork.default_transaction_type.value == 0


def test_default_transaction_type_configured_from_custom_network():
pass


@pytest.mark.parametrize("network_name", (LOCAL_NETWORK_NAME, "mainnet-fork", "mainnet_fork"))
def test_gas_limit_local_networks(ethereum, network_name):
network = ethereum.get_network(network_name)
Expand Down
21 changes: 21 additions & 0 deletions tests/functional/test_network_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest

import ape
from ape.api import EcosystemAPI
from ape.api.networks import LOCAL_NETWORK_NAME
from ape.exceptions import NetworkError, ProviderNotFoundError
Expand Down Expand Up @@ -400,3 +401,23 @@ def test_network_names(networks, custom_networks_config_dict, project):
assert "mainnet-fork" in actual # Forked
assert "apenet" in actual # Custom
assert "apenet-fork" in actual # Custom forked


def test_custom_networks_defined_in_non_local_project(custom_networks_config_dict):
"""
Showing we can read and use custom networks that are not defined
in the local project.
"""
# Ensure we are using a name that is not used anywhere else, for accurte testing.
net_name = "customnetdefinedinnonlocalproj"
eco_name = "customecosystemnotdefinedyet"
custom_networks = copy.deepcopy(custom_networks_config_dict)
custom_networks["networks"]["custom"][0]["name"] = net_name
custom_networks["networks"]["custom"][0]["ecosystem"] = eco_name

with ape.Project.create_temporary_project(config_override=custom_networks) as temp_project:
nm = temp_project.network_manager
ecosystem = nm.get_ecosystem(eco_name)
assert ecosystem.name == eco_name
network = ecosystem.get_network(net_name)
assert network.name == net_name

0 comments on commit fad486d

Please sign in to comment.