diff --git a/src/ape/api/projects.py b/src/ape/api/projects.py index 8e91399b77..ba7c45c484 100644 --- a/src/ape/api/projects.py +++ b/src/ape/api/projects.py @@ -270,6 +270,7 @@ def __ape_extra_attributes__(self) -> Iterator[ExtraModelAttributes]: attributes=self.contracts, include_getattr=True, include_getitem=True, + additional_error_message="Do you have the necessary compiler plugins installed?", ) @property @@ -382,6 +383,15 @@ def compile(self, use_cache: bool = True) -> PackageManifest: ) as project: manifest.unpack_sources(contracts_folder) compiled_manifest = project.local_project.create_manifest() + + if not compiled_manifest.contract_types: + # Manifest is empty. No need to write to disk. + logger.warning( + "Compiled manifest produced no contract types! " + "Are you missing compiler plugins?" + ) + return compiled_manifest + self._write_manifest_to_cache(compiled_manifest) return compiled_manifest diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index 273064be75..cd79030f68 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -32,6 +32,13 @@ class ApeException(Exception): """ +class ApeIndexError(ApeException, IndexError): + """ + An exception that is also an IndexError. + Useful for nicely displaying IndexErrors. + """ + + class APINotImplementedError(ApeException, NotImplementedError): """ An error raised when an API class does not implement an abstract method. diff --git a/src/ape/utils/basemodel.py b/src/ape/utils/basemodel.py index dfd32ecdbb..593125b427 100644 --- a/src/ape/utils/basemodel.py +++ b/src/ape/utils/basemodel.py @@ -3,7 +3,7 @@ from ethpm_types import BaseModel as _BaseModel -from ape.exceptions import ApeAttributeError, ProviderNotConnectedError +from ape.exceptions import ApeAttributeError, ApeIndexError, ProviderNotConnectedError from ape.logging import logger from ape.utils.misc import cached_property, singledispatchmethod @@ -133,6 +133,12 @@ class ExtraModelAttributes(_BaseModel): include_getitem: bool = False """Whether to use these in ``__getitem__``.""" + additional_error_message: Optional[str] = None + """ + An additional error message to include at the end of + the normal IndexError message. + """ + def __contains__(self, name: str) -> bool: attr_dict = self.attributes if isinstance(self.attributes, dict) else self.attributes.dict() if name in attr_dict: @@ -223,6 +229,7 @@ def __getattr__(self, name: str) -> Any: def __getitem__(self, name: Any) -> Any: # For __getitem__, we first try the extra (unlike `__getattr__`). extras_checked = set() + additional_error_messages = {} for extra in self.__ape_extra_attributes__(): if not extra.include_getitem: continue @@ -232,11 +239,27 @@ def __getitem__(self, name: Any) -> Any: extras_checked.add(extra.name) + if extra.additional_error_message: + additional_error_messages[extra.name] = extra.additional_error_message + # NOTE: If extras were supplied, the user was expecting it to be # there (unlike __getattr__). if extras_checked: - extras_str = ", ".join(extras_checked) - raise IndexError(f"Unable to find '{name}' in any of '{extras_str}'.") + prefix = f"Unable to find '{name}' in" + if not additional_error_messages: + extras_str = ", ".join(extras_checked) + message = f"{prefix} any of '{extras_str}'." + + else: + # The class is including additional error messages for the IndexError. + message = "" + for extra_checked in extras_checked: + additional_message = additional_error_messages.get(extra_checked) + suffix = f" {additional_message}" if additional_message else "" + sub_message = f"{prefix} '{extra_checked}'.{suffix}" + message = f"{message}\n{sub_message}" if message else sub_message + + raise ApeIndexError(message) # The user did not supply any extra __getitem__ attributes. # Do what you would have normally done.