Skip to content

Commit

Permalink
feat: custom exceptions (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Sep 10, 2021
1 parent 714a1d4 commit 1311178
Show file tree
Hide file tree
Showing 19 changed files with 239 additions and 96 deletions.
18 changes: 12 additions & 6 deletions src/ape/_cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import difflib
import re
from typing import Dict
from typing import Any, Dict

import click
import yaml

from ape.exceptions import ApeException
from ape.plugins import clean_plugin_name
from ape.utils import notify
from ape.utils import Abort, notify

try:
from importlib import metadata # type: ignore
Expand All @@ -33,11 +34,13 @@ def display_config(ctx, param, value):
class ApeCLI(click.MultiCommand):
_commands = None

def invoke(self, ctx):
def invoke(self, ctx) -> Any:
try:
return super().invoke(ctx)
except click.UsageError as err:
self._suggest_cmd(err)
except ApeException as err:
raise Abort(str(err)) from err

@staticmethod
def _suggest_cmd(usage_error):
Expand Down Expand Up @@ -65,7 +68,7 @@ def commands(self) -> Dict:
entry_points = metadata.entry_points()

if "ape_cli_subcommands" not in entry_points:
raise Exception("Missing registered cli subcommands")
raise Abort("Missing registered cli subcommands")

self._commands = {
clean_plugin_name(entry_point.name): entry_point.load
Expand All @@ -81,8 +84,11 @@ def get_command(self, ctx, name):
if name in self.commands:
try:
return self.commands[name]()
except Exception:
notify("WARNING", f"Error loading cli endpoint for plugin 'ape_{name}'")
except Exception as err:
notify(
"WARNING",
f"Unable to load CLI endpoint for plugin 'ape_{name}'.\n\t{err}",
)

# NOTE: don't return anything so Click displays proper error

Expand Down
37 changes: 22 additions & 15 deletions src/ape/api/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from ape.utils import cached_property

from ..exceptions import AccountsError, AliasAlreadyInUseError
from .address import AddressAPI
from .base import abstractdataclass, abstractmethod
from .contracts import ContractContainer, ContractInstance
Expand Down Expand Up @@ -54,7 +55,7 @@ def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI
if txn.nonce is None:
txn.nonce = self.nonce
elif txn.nonce < self.nonce:
raise Exception("Invalid nonce, will not publish!")
raise AccountsError("Invalid nonce, will not publish")

# TODO: Add `GasEstimationAPI`
if txn.gas_price is None:
Expand All @@ -71,12 +72,12 @@ def call(self, txn: TransactionAPI, send_everything: bool = False) -> ReceiptAPI
txn.value = self.balance - txn.gas_limit * txn.gas_price

if txn.gas_limit * txn.gas_price + txn.value > self.balance:
raise Exception("Transfer value meets or exceeds account balance")
raise AccountsError("Transfer value meets or exceeds account balance")

txn.signature = self.sign_transaction(txn)

if not txn.signature:
raise Exception("User didn't sign!")
raise AccountsError("The transaction was not signed")

return self.provider.send_transaction(txn)

Expand Down Expand Up @@ -119,7 +120,7 @@ def deploy(self, contract_type: ContractType, *args, **kwargs) -> ContractInstan
receipt = self.call(txn)

if not receipt.contract_address:
raise Exception(f"{receipt.txn_hash} did not create a contract")
raise AccountsError(f"'{receipt.txn_hash}' did not create a contract")

return ContractInstance( # type: ignore
_provider=self.provider,
Expand Down Expand Up @@ -154,29 +155,23 @@ def __getitem__(self, address: AddressType) -> AccountAPI:
raise IndexError(f"No local account {address}.")

def append(self, account: AccountAPI):
if not isinstance(account, self.account_type):
raise Exception("Not the right type for this container")
self._verify_account_type(account)

if account.address in self:
raise Exception("Account already in container")
raise AccountsError(f"Account '{account.address}' already in container")

if account.alias and account.alias in self.aliases:
raise Exception("Alias already in use")
self._verify_unused_alias(account)

self.__setitem__(account.address, account)

def __setitem__(self, address: AddressType, account: AccountAPI):
raise NotImplementedError("Must define this method to use `container.append(acct)`")

def remove(self, account: AccountAPI):
if not isinstance(account, self.account_type):
raise Exception("Not the right type for this container")
self._verify_account_type(account)

if account.address not in self:
raise Exception("Account not in container")

if account.alias and account.alias in self.aliases:
raise Exception("Alias already in use")
raise AccountsError(f"Account '{account.address}' not known")

self.__delitem__(account.address)

Expand All @@ -190,3 +185,15 @@ def __contains__(self, address: AddressType) -> bool:

except IndexError:
return False

def _verify_account_type(self, account):
if not isinstance(account, self.account_type):
message = (
f"Container '{type(account).__name__}' is an incorrect "
f"type for container '{type(self.account_type).__name__}'"
)
raise AccountsError(message)

def _verify_unused_alias(self, account):
if account.alias and account.alias in self.aliases:
raise AliasAlreadyInUseError(account.alias)
5 changes: 4 additions & 1 deletion src/ape/api/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ape.types import AddressType

from ..exceptions import AddressError
from .base import abstractdataclass, abstractmethod
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI

Expand All @@ -13,7 +14,9 @@ class AddressAPI:
@property
def provider(self) -> ProviderAPI:
if not self._provider:
raise Exception("Wired incorrectly")
raise AddressError(
f"Incorrectly implemented provider API for class {type(self).__name__}"
)

return self._provider

Expand Down
30 changes: 16 additions & 14 deletions src/ape/api/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ape.types import ABI, AddressType, ContractType
from ape.utils import notify

from ..exceptions import ContractCallError, ContractDeployError
from .address import Address, AddressAPI
from .base import dataclass
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI
Expand All @@ -22,7 +23,7 @@ class ContractConstructor:

def __post_init__(self):
if len(self.deployment_bytecode) == 0:
raise Exception("No bytecode to deploy")
raise ContractDeployError("No bytecode to deploy")

def __repr__(self) -> str:
return self.abi.signature if self.abi else "constructor()"
Expand Down Expand Up @@ -91,13 +92,9 @@ def __repr__(self) -> str:
return abis[-1].signature

def __call__(self, *args, **kwargs) -> Any:
selected_abi = None
for abi in self.abis:
if len(args) == len(abi.inputs):
selected_abi = abi

selected_abi = _select_abi(self.abis, args)
if not selected_abi:
raise Exception("Number of args does not match")
raise ContractCallError()

return ContractCall( # type: ignore
abi=selected_abi,
Expand All @@ -106,6 +103,15 @@ def __call__(self, *args, **kwargs) -> Any:
)(*args, **kwargs)


def _select_abi(abis, args):
selected_abi = None
for abi in abis:
if len(args) == len(abi.inputs):
selected_abi = abi

return selected_abi


@dataclass
class ContractTransaction:
abi: ABI
Expand All @@ -130,7 +136,7 @@ def __call__(self, *args, **kwargs) -> ReceiptAPI:
return sender.call(txn)

else:
raise Exception("Must specify a `sender`")
raise ContractCallError("Must specify a `sender`")


@dataclass
Expand All @@ -144,13 +150,9 @@ def __repr__(self) -> str:
return abis[-1].signature

def __call__(self, *args, **kwargs) -> ReceiptAPI:
selected_abi = None
for abi in self.abis:
if len(args) == len(abi.inputs):
selected_abi = abi

selected_abi = _select_abi(self.abis, args)
if not selected_abi:
raise Exception("Number of args does not match")
raise ContractCallError()

return ContractTransaction( # type: ignore
abi=selected_abi,
Expand Down
46 changes: 25 additions & 21 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ape.types import ABI, AddressType
from ape.utils import cached_property

from ..exceptions import NetworkError, NetworkNotFoundError
from .base import abstractdataclass, abstractmethod, dataclass
from .config import ConfigItem

Expand Down Expand Up @@ -61,11 +62,11 @@ def networks(self) -> Dict[str, "NetworkAPI"]:
return networks

else:
raise Exception("No networks found!")
raise NetworkError("No networks found")

def __post_init__(self):
if len(self.networks) == 0:
raise Exception("Must define at least one network in ecosystem")
raise NetworkError("Must define at least one network in ecosystem")

def __iter__(self) -> Iterator[str]:
"""
Expand All @@ -74,25 +75,17 @@ def __iter__(self) -> Iterator[str]:
yield from self.networks

def __getitem__(self, network_name: str) -> "NetworkAPI":
if network_name in self.networks:
return self.networks[network_name]

else:
raise Exception("No network with name")
return self._try_get_network(network_name)

def __getattr__(self, network_name: str) -> "NetworkAPI":
if network_name in self.networks:
return self.networks[network_name]

else:
raise Exception("No network with name")
return self._try_get_network(network_name)

def add_network(self, network_name: str, network: "NetworkAPI"):
"""
Used to attach new networks to an ecosystem (e.g. L2 networks like Optimism)
"""
if network_name in self.networks:
raise Exception("Can't overwrite an existing network!")
raise NetworkError(f"Unable to overwrite existing network '{network_name}'")
else:
self.networks[network_name] = network

Expand All @@ -104,7 +97,8 @@ def set_default_network(self, network_name: str):
if network_name in self.networks:
self._default_network = network_name
else:
raise Exception("Not a valid network for ecosystem `self.name`")
message = f"'{network_name}' is not a valid network for ecosystem '{self.name}'"
raise NetworkError(message)

@abstractmethod
def encode_deployment(
Expand All @@ -122,6 +116,12 @@ def encode_transaction(
def decode_event(self, abi: ABI, receipt: "ReceiptAPI") -> "ContractLog":
...

def _try_get_network(self, network_name):
if network_name in self.networks:
return self.networks[network_name]
else:
raise NetworkNotFoundError(network_name)


class ProviderContextManager:
# NOTE: Class variable, so it will manage stack across instances of this object
Expand Down Expand Up @@ -161,7 +161,7 @@ class NetworkAPI:
A Network is a wrapper around a Provider for a specific Ecosystem.
"""

name: str # Name given when regsitered in ecosystem
name: str # Name given when registered in ecosystem
ecosystem: EcosystemAPI
config_manager: "ConfigManager"
plugin_manager: PluginManager
Expand All @@ -176,19 +176,20 @@ def config(self) -> ConfigItem:

@property
def chain_id(self) -> int:
# NOTE: Unless overriden, returns same as `provider.chain_id`
# NOTE: Unless overridden, returns same as `provider.chain_id`
provider = self.ecosystem.network_manager.active_provider

if not provider:
raise Exception(
message = (
"Cannot determine `chain_id`, please make sure you are connected to a provider"
)
raise NetworkError(message)

return provider.chain_id

@property
def network_id(self) -> int:
# NOTE: Unless overriden, returns same as chain_id
# NOTE: Unless overridden, returns same as chain_id
return self.chain_id

@cached_property
Expand Down Expand Up @@ -229,7 +230,7 @@ def providers(self): # -> Dict[str, Partial[ProviderAPI]]
return providers

else:
raise Exception("No providers found")
raise NetworkError("No network providers found")

def use_provider(
self,
Expand All @@ -251,7 +252,10 @@ def use_provider(
)

else:
raise Exception("Not a registered provider name")
message = (
f"'{provider_name}' is not a valid network for ecosystem '{self.ecosystem.name}'"
)
raise NetworkError(message)

@property
def default_provider(self) -> str:
Expand All @@ -261,7 +265,7 @@ def set_default_provider(self, provider_name: str):
if provider_name in self.providers:
self._default_provider = provider_name
else:
raise Exception("Not a valid provider for network `self.name`")
raise NetworkError(f"No providers found for network '{self.name}'")

def use_default_provider(self) -> ProviderContextManager:
# NOTE: If multiple providers, use whatever is "first" registered
Expand Down
3 changes: 2 additions & 1 deletion src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ape.types import TransactionSignature
from ape.utils import notify

from ..exceptions import ProviderError
from . import networks
from .base import abstractdataclass, abstractmethod
from .config import ConfigItem
Expand All @@ -27,7 +28,7 @@ class TransactionAPI:

def __post_init__(self):
if not self.is_valid:
raise Exception("Transaction is not valid!")
raise ProviderError("Transaction is not valid")

@property
@abstractmethod
Expand Down
Loading

0 comments on commit 1311178

Please sign in to comment.