Skip to content

Commit

Permalink
fix: better network errors when choice not found [APE-1441] (#1691)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 6, 2023
1 parent af4d3e4 commit f7d84f3
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 23 deletions.
14 changes: 9 additions & 5 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
NetworkMismatchError,
NetworkNotFoundError,
ProviderNotConnectedError,
ProviderNotFoundError,
SignatureError,
)
from ape.logging import logger
Expand Down Expand Up @@ -287,8 +288,7 @@ def set_default_network(self, network_name: str):
if network_name in self.networks:
self._default_network = network_name
else:
message = f"'{network_name}' is not a valid network for ecosystem '{self.name}'."
raise NetworkError(message)
raise NetworkNotFoundError(network_name, ecosystem=self.name, options=self.networks)

@abstractmethod
def encode_deployment(
Expand Down Expand Up @@ -428,7 +428,7 @@ def get_network(self, network_name: str) -> "NetworkAPI":
if name in self.networks:
return self.networks[name]

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

def get_network_data(self, network_name: str) -> Dict:
"""
Expand Down Expand Up @@ -877,8 +877,12 @@ def get_provider(
return provider

else:
message = f"'{provider_name}' is not a valid provider for network '{self.name}'"
raise NetworkError(message)
raise ProviderNotFoundError(
provider_name,
network=self.name,
ecosystem=self.ecosystem.name,
options=self.providers,
)

def use_provider(
self,
Expand Down
15 changes: 13 additions & 2 deletions src/ape/cli/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Any, Iterator, List, Optional, Sequence, Type, Union

import click
from click import Choice, Context, Parameter
from click import BadParameter, Choice, Context, Parameter

from ape import accounts, networks
from ape.api.accounts import AccountAPI
Expand Down Expand Up @@ -313,7 +313,18 @@ def convert(self, value: Any, param: Optional[Parameter], ctx: Optional[Context]
# By-pass choice constraints when using adhoc network
return value

return super().convert(value, param, ctx)
try:
return super().convert(value, param, ctx)
except BadParameter as err:
# Find out actual bad parts of the value to show better error.
# The following line should raise a nicer error.
networks.get_provider_from_choice(network_choice=value)

# If an error was not raised for some reason, raise a simpler error.
# NOTE: Still avoid showing the massive network options list.
raise click.BadParameter(
"Invalid network choice. Use `ape networks list` to see options."
) from err


class OutputFormat(Enum):
Expand Down
80 changes: 77 additions & 3 deletions src/ape/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import difflib
import sys
import tempfile
import time
import traceback
from inspect import getframeinfo, stack
from pathlib import Path
from types import CodeType, TracebackType
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Union, cast
from typing import TYPE_CHECKING, Any, Collection, Dict, Iterator, List, Optional, Union, cast

import click
from eth_typing import Hash32
Expand Down Expand Up @@ -274,14 +275,87 @@ class NetworkError(ApeException):
"""


class EcosystemNotFoundError(NetworkError):
"""
Raised when the ecosystem with the given name was not found.
"""

def __init__(self, ecosystem: str, options: Optional[Collection[str]] = None):
self.ecosystem = ecosystem
self.options = options
message = f"No ecosystem named '{ecosystem}'."
if options:
close_matches = difflib.get_close_matches(ecosystem, options, cutoff=0.6)
if close_matches:
message = f"{message} Did you mean '{', '.join(close_matches)}'?"
else:
# No close matches. Show all the options.
options_str = "\n".join(sorted(options))
message = f"{message} Options:\n{options_str}"

super().__init__(message)


class NetworkNotFoundError(NetworkError):
"""
Raised when the network with the given name was not found.
"""

def __init__(self, network: str):
def __init__(
self,
network: str,
ecosystem: Optional[str] = None,
options: Optional[Collection[str]] = None,
):
self.network = network
message = f"No network named '{network}'."
message = (
f"No network in '{ecosystem}' named '{network}'."
if ecosystem
else f"No network named '{network}'."
)
if options:
close_matches = difflib.get_close_matches(network, options, cutoff=0.6)
if close_matches:
message = f"{message} Did you mean '{', '.join(close_matches)}'?"
else:
# No close matches - show all options.
options_str = "\n".join(sorted(options))
message = f"{message} Options:\n{options_str}"

super().__init__(message)


class ProviderNotFoundError(NetworkError):
"""
Raised when the provider with the given name was not found.
"""

def __init__(
self,
provider: str,
network: Optional[str] = None,
ecosystem: Optional[str] = None,
options: Optional[Collection[str]] = None,
):
self.provider = provider
self.network = network
self.ecosystem = ecosystem
message = f"No provider named '{provider}'"
if network:
message = f"{message} in network '{network}'"
if ecosystem:
message = f"{message} in ecosystem '{ecosystem}'"
if options:
close_matches = difflib.get_close_matches(provider, options, cutoff=0.6)
if close_matches:
message = f"{message} Did you mean '{', '.join(close_matches)}'?"
else:
# No close matches. Show all provider options.
options_str = "\n".join(sorted(options))
message = f"{message}. Options:\n{options_str}"
else:
message = f"{message}."

super().__init__(message)


Expand Down
13 changes: 8 additions & 5 deletions src/ape/managers/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from ape.api import EcosystemAPI, ProviderAPI, ProviderContextManager
from ape.api.networks import NetworkAPI
from ape.exceptions import ApeAttributeError, NetworkError
from ape.exceptions import ApeAttributeError, EcosystemNotFoundError, NetworkError
from ape.managers.base import BaseManager


Expand Down Expand Up @@ -207,7 +207,7 @@ def __getitem__(self, ecosystem_name: str) -> EcosystemAPI:
:class:`~ape.api.networks.EcosystemAPI`
"""
if ecosystem_name not in self.ecosystems:
raise NetworkError(f"Unknown ecosystem '{ecosystem_name}'.")
raise IndexError(f"Unknown ecosystem '{ecosystem_name}'.")

return self.ecosystems[ecosystem_name]

Expand Down Expand Up @@ -337,7 +337,7 @@ def get_ecosystem(self, ecosystem_name: str) -> EcosystemAPI:
"""

if ecosystem_name not in self.ecosystem_names:
raise NetworkError(f"Ecosystem '{ecosystem_name}' not found.")
raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names)

return self.ecosystems[ecosystem_name]

Expand Down Expand Up @@ -433,6 +433,9 @@ def parse_network_choice(
(see :meth:`~ape.managers.networks.NetworkManager.get_network_choices`).
Defaults to the default ecosystem, network, and provider combination.
provider_settings (dict, optional): Settings for the provider. Defaults to None.
disconnect_after (bool): Set to True to terminate the connection completely
at the end of context. NOTE: May only work if the network was also started
from this session.
Returns:
:class:`~api.api.networks.ProviderContextManager`
Expand Down Expand Up @@ -484,7 +487,7 @@ def set_default_ecosystem(self, ecosystem_name: str):
self._default = ecosystem_name

else:
raise NetworkError(f"Ecosystem '{ecosystem_name}' is not a registered ecosystem.")
raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names)

@property
def network_data(self) -> Dict:
Expand Down Expand Up @@ -549,7 +552,7 @@ def networks_yaml(self) -> str:
data_str = str(data)

raise NetworkError(
f"Network data did not dump to YAML: {data_str}\nAcual err: {err}"
f"Network data did not dump to YAML: {data_str}\nActual err: {err}"
) from err


Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def test_network_option_specified(runner, network_cmd):
def test_network_option_unknown(runner, network_cmd):
result = runner.invoke(network_cmd, ["--network", "UNKNOWN"])
assert result.exit_code != 0, result.output
assert "Invalid value for '--network'" in result.output
assert "No ecosystem named 'UNKNOWN'" in str(result.exception)


@pytest.mark.parametrize(
Expand Down
6 changes: 3 additions & 3 deletions tests/functional/test_ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ethpm_types.abi import ABIType, MethodABI

from ape.api.networks import LOCAL_NETWORK_NAME
from ape.exceptions import DecodingError, NetworkError
from ape.exceptions import DecodingError, NetworkNotFoundError
from ape.types import AddressType
from ape.utils import DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT
from ape_ethereum.ecosystem import BLUEPRINT_HEADER, Block
Expand Down Expand Up @@ -327,6 +327,6 @@ def test_encode_transaction(tx_type, ethereum, vyper_contract_instance, owner, e

def test_set_default_network_not_exists(temp_config, ethereum):
bad_network = "NOT_EXISTS"
expected = f"'{bad_network}' is not a valid network for ecosystem 'ethereum'."
with pytest.raises(NetworkError, match=expected):
expected = f"No network in 'ethereum' named '{bad_network}'. Options:.*"
with pytest.raises(NetworkNotFoundError, match=expected):
ethereum.set_default_network(bad_network)
9 changes: 5 additions & 4 deletions tests/functional/test_network_api.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import pytest

from ape.exceptions import NetworkError
from ape.exceptions import NetworkError, ProviderNotFoundError


def test_get_provider_when_not_found(ethereum):
network = ethereum.get_network("goerli-fork")
expected = "'test' is not a valid provider for network 'goerli-fork'"
with pytest.raises(NetworkError, match=expected):
name = "goerli-fork"
network = ethereum.get_network(name)
expected = f"No provider named 'test' in network '{name}' in ecosystem 'ethereum'.*"
with pytest.raises(ProviderNotFoundError, match=expected):
network.get_provider("test")


Expand Down

0 comments on commit f7d84f3

Please sign in to comment.