diff --git a/src/ape/api/networks.py b/src/ape/api/networks.py index 5d7e9ab40f..a0840adc26 100644 --- a/src/ape/api/networks.py +++ b/src/ape/api/networks.py @@ -18,6 +18,7 @@ NetworkMismatchError, NetworkNotFoundError, ProviderNotConnectedError, + ProviderNotFoundError, SignatureError, ) from ape.logging import logger @@ -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( @@ -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: """ @@ -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, diff --git a/src/ape/cli/choices.py b/src/ape/cli/choices.py index 387ef5a112..e8c5f9d46d 100644 --- a/src/ape/cli/choices.py +++ b/src/ape/cli/choices.py @@ -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 @@ -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): diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index f5cefa8c21..1a0b2fccc7 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -1,3 +1,4 @@ +import difflib import sys import tempfile import time @@ -5,7 +6,7 @@ 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 @@ -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) diff --git a/src/ape/managers/networks.py b/src/ape/managers/networks.py index b6f87bde16..64cf8ae044 100644 --- a/src/ape/managers/networks.py +++ b/src/ape/managers/networks.py @@ -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 @@ -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] @@ -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] @@ -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` @@ -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: @@ -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 diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index be4e951db0..6795c43656 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -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( diff --git a/tests/functional/test_ecosystem.py b/tests/functional/test_ecosystem.py index 6222ed21cd..c347f366a6 100644 --- a/tests/functional/test_ecosystem.py +++ b/tests/functional/test_ecosystem.py @@ -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 @@ -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) diff --git a/tests/functional/test_network_api.py b/tests/functional/test_network_api.py index 291a6bdf65..6c79314df8 100644 --- a/tests/functional/test_network_api.py +++ b/tests/functional/test_network_api.py @@ -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")