Skip to content

Commit

Permalink
feat!: network customization and tooling improvements (#1764)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Dec 11, 2023
1 parent 0e9143b commit e29b3e3
Show file tree
Hide file tree
Showing 29 changed files with 1,054 additions and 587 deletions.
58 changes: 45 additions & 13 deletions docs/userguides/clis.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,28 +63,60 @@ def cli(cli_ctx):

## Network Tools

The [@network_option()](../methoddocs/cli.html#ape.cli.options.network_option) allows you to select an ecosystem / network / provider combination.
When using with the [NetworkBoundCommand](../methoddocs/cli.html#ape.cli.commands.NetworkBoundCommand) class, you can cause your CLI to connect before any of your code executes.
This is useful if your script or command requires a provider connection in order for it to run.
The [@network_option()](../methoddocs/cli.html#ape.cli.options.network_option) allows you to select an ecosystem, network, and provider.
To specify the network option, use values like:

```shell
--network ethereum
--network ethereum:sepolia
--network ethereum:mainnet:alchemy
--network ::foundry
```

To use default values automatically, omit sections of the choice, but leave the semi-colons for parsing.
For example, `::test` means use the default ecosystem and network and the `test` provider.

Use `ecosystem`, `network`, and `provider` argument names in your command implementation to access their corresponding class instances:

```python
import click
from ape import networks
from ape.cli import network_option, NetworkBoundCommand
from ape.cli import network_option

@click.command()
@network_option()
def cmd(provider):
# This command only needs the provider.
click.echo(provider.name)

@click.command()
@network_option()
def cmd(network):
# Choices like "ethereum" or "polygon:local:test".
click.echo(network)
def cmd_2(ecosystem, network, provider):
# This command uses all parts of the parsed network choice.
click.echo(ecosystem.name)
click.echo(network.name)
click.echo(provider.name)
```

The [ConnectedProviderCommand](../methoddocs/cli.html#ape.cli.commands.ConnectedProviderCommand) automatically uses the `--network` option and connects to the network before any of your code executes and then disconnects afterward.
This is useful if your script or command requires a provider connection in order for it to run.
Additionally, specify `ecosystem`, `network`, or `provider` in your command function if you need any of those instances in your `ConnectedProviderCommand`, just like when using `network_option`.

@click.command(cls=NetworkBoundCommand)
@network_option()
def cmd(network):
# Fails if we are not connected.
click.echo(networks.provider.network.name)
```python
import click
from ape.cli import ConnectedProviderCommand

@click.command(cls=ConnectedProviderCommand)
def cmd(network, provider):
click.echo(network.name)
click.echo(provider.is_connected) # True

@click.command(cls=ConnectedProviderCommand)
def cmd(provider):
click.echo(provider.is_connected) # True

@click.command(cls=ConnectedProviderCommand)
def cmd():
click.echo("Using params is from ConnectedProviderCommand is optional")
```

## Account Tools
Expand Down
6 changes: 3 additions & 3 deletions docs/userguides/networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ape test --network ethereum:local:foundry
ape console --network arbitrum:testnet:alchemy
```

You can also use the `--network` option on scripts that use the `main()` method approach or scripts that implement that `NetworkBoundCommand` command type.
You can also use the `--network` option on scripts that use the `main()` method approach or scripts that implement that `ConnectedProviderCommand` command type.
See [the scripting guide](./scripts.html) to learn more about scripts and how to add the network option.

**NOTE**: You can omit values to use defaults.
Expand Down Expand Up @@ -85,9 +85,9 @@ geth:
uri: https://foo.node.bar
```

## Ad-hoc Network Connection
## Custom Network Connection

If you would like to connect to a URI using the `geth` provider, you can specify a URI for the provider name in the `--network` option:
If you would like to connect to a URI using the default Ethereum node provider, you can specify a URI for the provider name in the `--network` option:

```bash
ape run script --network ethereum:mainnet:https://foo.bar
Expand Down
20 changes: 14 additions & 6 deletions docs/userguides/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,25 @@ ape run hello helloworld
```

Note that by default, `cli` scripts do not have [`ape.cli.network_option`](../methoddocs/cli.html?highlight=options#ape.cli.options.network_option) installed, giving you more flexibility in how you define your scripts.
However, you can add the `network_option` to your scripts by importing both the `NetworkBoundCommand` and the `network_option` from the `ape.cli` namespace:
However, you can add the `network_option` or `ConnectedProviderCommand` to your scripts by importing them from the `ape.cli` namespace:

```python
import click
from ape.cli import network_option, NetworkBoundCommand
from ape.cli import network_option, ConnectedProviderCommand


@click.command(cls=NetworkBoundCommand)
@network_option()
def cli(network):
click.echo(f"You are connected to network '{network}'.")
@click.command(cls=ConnectedProviderCommand)
def cli(ecosystem, network):
click(f"You selected a provider on ecosystem '{ecosystem.name}' and {network.name}.")

@click.command(cls=ConnectedProviderCommand)
def cli(network, provider):
click.echo(f"You are connected to network '{network.name}'.")
click.echo(provider.chain_id)

@click.command(cls=ConnectedProviderCommand)
def cli_2():
click.echo(f"Using any network-based argument is completely optional.")
```

Assume we saved this script as `shownet.py` and have the [ape-alchemy](https://github.com/ApeWorX/ape-alchemy) plugin installed.
Expand Down
121 changes: 77 additions & 44 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from eth_utils import keccak, to_int
from ethpm_types import BaseModel, ContractType
from ethpm_types.abi import ABIType, ConstructorABI, EventABI, MethodABI
from pydantic import computed_field

from ape.exceptions import (
NetworkError,
Expand Down Expand Up @@ -94,6 +93,29 @@ class EcosystemAPI(BaseInterfaceModel):
def __repr__(self) -> str:
return f"<{self.name}>"

@cached_property
def custom_network(self) -> "NetworkAPI":
ethereum_class = None
for plugin_name, ecosystem_class in self.plugin_manager.ecosystems:
if plugin_name == "ethereum":
ethereum_class = ecosystem_class
break

if ethereum_class is None:
raise NetworkError("Core Ethereum plugin missing.")

data_folder = mkdtemp()
request_header = self.config_manager.REQUEST_HEADER
init_kwargs = {"data_folder": data_folder, "request_header": request_header}
ethereum = ethereum_class(**init_kwargs) # type: ignore
return NetworkAPI(
name="custom",
ecosystem=ethereum,
data_folder=Path(data_folder),
request_header=request_header,
_default_provider="geth",
)

@classmethod
@abstractmethod
def decode_address(cls, raw_address: RawAddress) -> AddressType:
Expand Down Expand Up @@ -258,7 +280,7 @@ def add_network(self, network_name: str, network: "NetworkAPI"):
self.networks[network_name] = network

@property
def default_network(self) -> str:
def default_network_name(self) -> str:
"""
The name of the default network in this ecosystem.
Expand All @@ -284,6 +306,10 @@ def default_network(self) -> str:
# Very unlikely scenario.
raise NetworkError("No networks found.")

@property
def default_network(self) -> "NetworkAPI":
return self.get_network(self.default_network_name)

def set_default_network(self, network_name: str):
"""
Change the default network.
Expand Down Expand Up @@ -424,19 +450,22 @@ def get_network(self, network_name: str) -> "NetworkAPI":
Get the network for the given name.
Args:
network_name (str): The name of the network to get.
network_name (str): The name of the network to get.
Raises:
:class:`~ape.exceptions.NetworkNotFoundError`: When the network is not present.
:class:`~ape.exceptions.NetworkNotFoundError`: When the network is not present.
Returns:
:class:`~ape.api.networks.NetworkAPI`
:class:`~ape.api.networks.NetworkAPI`
"""

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

elif name == "custom":
return self.custom_network

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

def get_network_data(
Expand All @@ -459,7 +488,7 @@ def get_network_data(
data: Dict[str, Any] = {"name": str(network_name)}

# Only add isDefault key when True
if network_name == self.default_network:
if network_name == self.default_network_name:
data["isDefault"] = True

data["providers"] = []
Expand All @@ -475,7 +504,7 @@ def get_network_data(
provider_data: Dict = {"name": str(provider_name)}

# Only add isDefault key when True
if provider_name == network.default_provider:
if provider_name == network.default_provider_name:
provider_data["isDefault"] = True

data["providers"].append(provider_data)
Expand Down Expand Up @@ -605,6 +634,7 @@ def __enter__(self, *args, **kwargs):
# set inner var to the recycled provider for use in push_provider()
self._provider = self._recycled_provider
ProviderContextManager._recycled_provider = None

return self.push_provider()

def __exit__(self, exception, *args, **kwargs):
Expand Down Expand Up @@ -700,29 +730,6 @@ class NetworkAPI(BaseInterfaceModel):
# See ``.default_provider`` which is the proper field.
_default_provider: str = ""

@classmethod
def create_adhoc_network(cls) -> "NetworkAPI":
ethereum_class = None
for plugin_name, ecosystem_class in cls.plugin_manager.ecosystems:
if plugin_name == "ethereum":
ethereum_class = ecosystem_class
break

if ethereum_class is None:
raise NetworkError("Core Ethereum plugin missing.")

data_folder = mkdtemp()
request_header = cls.config_manager.REQUEST_HEADER
init_kwargs = {"data_folder": data_folder, "request_header": request_header}
ethereum = ethereum_class(**init_kwargs) # type: ignore
return cls(
name="adhoc",
ecosystem=ethereum,
data_folder=Path(data_folder),
request_header=request_header,
_default_provider="geth",
)

def __repr__(self) -> str:
try:
chain_id = self.chain_id
Expand Down Expand Up @@ -879,7 +886,10 @@ def providers(self): # -> Dict[str, Partial[ProviderAPI]]
ecosystem_name, network_name, provider_class = plugin_tuple
provider_name = clean_plugin_name(provider_class.__module__.split(".")[0])

if self.ecosystem.name == ecosystem_name and self.name == network_name:
# NOTE: Custom networks work with any provider.
if self.name == "custom" or (
self.ecosystem.name == ecosystem_name and self.name == network_name
):
# NOTE: Lazily load provider config
providers[provider_name] = partial(
provider_class,
Expand Down Expand Up @@ -910,7 +920,7 @@ def get_provider(
:class:`~ape.api.providers.ProviderAPI`
"""

provider_name = provider_name or self.default_provider
provider_name = provider_name or self.default_provider_name
if not provider_name:
from ape.managers.config import CONFIG_FILE_NAME

Expand Down Expand Up @@ -947,9 +957,10 @@ def get_provider(

def use_provider(
self,
provider_name: str,
provider: Union[str, "ProviderAPI"],
provider_settings: Optional[Dict] = None,
disconnect_after: bool = False,
disconnect_on_exit: bool = True,
) -> ProviderContextManager:
"""
Use and connect to a provider in a temporary context. When entering the context, it calls
Expand All @@ -965,24 +976,37 @@ def use_provider(
...
Args:
provider_name (str): The name of the provider to use.
provider (str): The provider instance or the name of the provider to use.
provider_settings (dict, optional): Settings to apply to the provider.
Defaults to ``None``.
disconnect_after (bool): Set to ``True`` to force a disconnect after ending
the context. This defaults to ``False`` so you can re-connect to the
same network, such as in a multi-chain testing scenario.
provider_settings (dict, optional): Settings to apply to the provider.
Defaults to ``None``.
disconnect_on_exit (bool): Whether to disconnect on the exit of the python
session. Defaults to ``True``.
Returns:
:class:`~ape.api.networks.ProviderContextManager`
"""

settings = provider_settings or {}
provider = self.get_provider(provider_name=provider_name, provider_settings=settings)
return ProviderContextManager(provider=provider, disconnect_after=disconnect_after)

@computed_field() # type: ignore[misc]
# NOTE: The main reason we allow a provider instance here is to avoid unnecessarily
# re-initializing the class.
provider_obj = (
self.get_provider(provider_name=provider, provider_settings=settings)
if isinstance(provider, str)
else provider
)

return ProviderContextManager(
provider=provider_obj,
disconnect_after=disconnect_after,
disconnect_on_exit=disconnect_on_exit,
)

@property
def default_provider(self) -> Optional[str]:
def default_provider_name(self) -> Optional[str]:
"""
The name of the default provider or ``None``.
Expand All @@ -1005,6 +1029,13 @@ def default_provider(self) -> Optional[str]:
# There are no providers at all for this network.
return None

@property
def default_provider(self) -> Optional["ProviderAPI"]:
if (name := self.default_provider_name) and name in self.providers:
return self.get_provider(name)

return None

@property
def choice(self) -> str:
return f"{self.ecosystem.name}:{self.name}"
Expand Down Expand Up @@ -1056,7 +1087,9 @@ def use_default_provider(
if self.default_provider:
settings = provider_settings or {}
return self.use_provider(
self.default_provider, provider_settings=settings, disconnect_after=disconnect_after
self.default_provider.name,
provider_settings=settings,
disconnect_after=disconnect_after,
)

raise NetworkError(f"No providers for network '{self.name}'.")
Expand Down Expand Up @@ -1089,7 +1122,7 @@ def verify_chain_id(self, chain_id: int):
not local or adhoc and has a different hardcoded chain ID than
the given one.
"""
if self.name not in ("adhoc", LOCAL_NETWORK_NAME) and self.chain_id != chain_id:
if self.name not in ("custom", LOCAL_NETWORK_NAME) and self.chain_id != chain_id:
raise NetworkMismatchError(chain_id, self)


Expand All @@ -1112,7 +1145,7 @@ def upstream_provider(self) -> "UpstreamProvider":
"""

config_choice = self._network_config.get("upstream_provider")
if provider_name := config_choice or self.upstream_network.default_provider:
if provider_name := config_choice or self.upstream_network.default_provider_name:
return self.upstream_network.get_provider(provider_name)

raise NetworkError(f"Upstream network '{self.upstream_network}' has no providers.")
Expand All @@ -1135,7 +1168,7 @@ def use_upstream_provider(self) -> ProviderContextManager:
Returns:
:class:`~ape.api.networks.ProviderContextManager`
"""
return self.upstream_network.use_provider(self.upstream_provider.name)
return self.upstream_network.use_provider(self.upstream_provider)


def create_network_type(chain_id: int, network_id: int) -> Type[NetworkAPI]:
Expand Down
Loading

0 comments on commit e29b3e3

Please sign in to comment.