Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: issue where could not define custom networks in any non-local project #2153

Merged
merged 10 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading