diff --git a/src/ape/contracts/__init__.py b/src/ape/contracts/__init__.py index 8ebd5e04f2..535514e7af 100644 --- a/src/ape/contracts/__init__.py +++ b/src/ape/contracts/__init__.py @@ -1,4 +1,8 @@ -from .base import ContractContainer, ContractEvent, ContractInstance, ContractLog, ContractNamespace +def __getattr__(name: str): + import ape.contracts.base as module + + return getattr(module, name) + __all__ = [ "ContractContainer", diff --git a/src/ape/contracts/base.py b/src/ape/contracts/base.py index 9c73fb8158..3c6be64c0b 100644 --- a/src/ape/contracts/base.py +++ b/src/ape/contracts/base.py @@ -7,14 +7,10 @@ from typing import TYPE_CHECKING, Any, Optional, Union import click -import pandas as pd from eth_pydantic_types import HexBytes from eth_utils import to_hex -from ethpm_types.abi import EventABI, MethodABI -from ethpm_types.contract_type import ABI_W_SELECTOR_T, ContractType -from IPython.lib.pretty import for_type +from ethpm_types.abi import EventABI -from ape.api.accounts import AccountAPI from ape.api.address import Address, BaseAddress from ape.api.query import ( ContractCreation, @@ -34,7 +30,6 @@ MissingDeploymentBytecodeError, ) from ape.logging import get_rich_console, logger -from ape.types.address import AddressType from ape.types.events import ContractLog, LogFilter, MockContractLog from ape.utils.abi import StructParser, _enrich_natspec from ape.utils.basemodel import ( @@ -49,9 +44,12 @@ from ape.utils.misc import log_instead_of_fail if TYPE_CHECKING: - from ethpm_types.abi import ConstructorABI, ErrorABI + from ethpm_types.abi import ConstructorABI, ErrorABI, MethodABI + from ethpm_types.contract_type import ABI_W_SELECTOR_T, ContractType + from pandas import DataFrame from ape.api.transactions import ReceiptAPI, TransactionAPI + from ape.types.address import AddressType class ContractConstructor(ManagerAccessMixin): @@ -90,7 +88,7 @@ def serialize_transaction(self, *args, **kwargs) -> "TransactionAPI": def __call__(self, private: bool = False, *args, **kwargs) -> "ReceiptAPI": txn = self.serialize_transaction(*args, **kwargs) - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): sender = kwargs["sender"] return sender.call(txn, **kwargs) elif "sender" not in kwargs and self.account_manager.default_sender is not None: @@ -104,7 +102,7 @@ def __call__(self, private: bool = False, *args, **kwargs) -> "ReceiptAPI": class ContractCall(ManagerAccessMixin): - def __init__(self, abi: MethodABI, address: AddressType) -> None: + def __init__(self, abi: "MethodABI", address: "AddressType") -> None: super().__init__() self.abi = abi self.address = address @@ -140,9 +138,9 @@ def __call__(self, *args, **kwargs) -> Any: class ContractMethodHandler(ManagerAccessMixin): contract: "ContractInstance" - abis: list[MethodABI] + abis: list["MethodABI"] - def __init__(self, contract: "ContractInstance", abis: list[MethodABI]) -> None: + def __init__(self, contract: "ContractInstance", abis: list["MethodABI"]) -> None: super().__init__() self.contract = contract self.abis = abis @@ -320,7 +318,7 @@ def estimate_gas_cost(self, *args, **kwargs) -> int: return self.transact.estimate_gas_cost(*arguments, **kwargs) -def _select_method_abi(abis: list[MethodABI], args: Union[tuple, list]) -> MethodABI: +def _select_method_abi(abis: list["MethodABI"], args: Union[tuple, list]) -> "MethodABI": args = args or [] selected_abi = None for abi in abis: @@ -335,13 +333,10 @@ def _select_method_abi(abis: list[MethodABI], args: Union[tuple, list]) -> Metho class ContractTransaction(ManagerAccessMixin): - abi: MethodABI - address: AddressType - - def __init__(self, abi: MethodABI, address: AddressType) -> None: + def __init__(self, abi: "MethodABI", address: "AddressType") -> None: super().__init__() - self.abi = abi - self.address = address + self.abi: "MethodABI" = abi + self.address: "AddressType" = address @log_instead_of_fail(default="") def __repr__(self) -> str: @@ -362,7 +357,7 @@ def __call__(self, *args, **kwargs) -> "ReceiptAPI": txn = self.serialize_transaction(*args, **kwargs) private = kwargs.get("private", False) - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): return kwargs["sender"].call(txn, **kwargs) txn = self.provider.prepare_transaction(txn) @@ -441,6 +436,7 @@ def _as_transaction(self, *args) -> ContractTransaction: ) +# TODO: In Ape 0.9 - make not a BaseModel - no reason to. class ContractEvent(BaseInterfaceModel): """ The types of events on a :class:`~ape.contracts.base.ContractInstance`. @@ -616,7 +612,7 @@ def query( stop_block: Optional[int] = None, step: int = 1, engine_to_use: Optional[str] = None, - ) -> pd.DataFrame: + ) -> "DataFrame": """ Iterate through blocks for log events @@ -635,6 +631,8 @@ def query( Returns: pd.DataFrame """ + # perf: pandas import is really slow. Avoid importing at module level. + import pandas as pd if start_block < 0: start_block = self.chain_manager.blocks.height + start_block @@ -800,7 +798,7 @@ def poll_logs( class ContractTypeWrapper(ManagerAccessMixin): - contract_type: ContractType + contract_type: "ContractType" base_path: Optional[Path] = None @property @@ -812,7 +810,7 @@ def selector_identifiers(self) -> dict[str, str]: return self.contract_type.selector_identifiers @property - def identifier_lookup(self) -> dict[str, ABI_W_SELECTOR_T]: + def identifier_lookup(self) -> dict[str, "ABI_W_SELECTOR_T"]: """ Provides a mapping of method, error, and event selector identifiers to ABI Types. @@ -898,6 +896,9 @@ def repr_pretty_for_assignment(cls, *args, **kwargs): info = _get_info() error_type.info = error_type.__doc__ = info # type: ignore if info: + # perf: Avoid forcing everyone to import from IPython. + from IPython.lib.pretty import for_type + error_type._repr_pretty_ = repr_pretty_for_assignment # type: ignore # Register the dynamically-created type with IPython so it integrates. @@ -922,8 +923,8 @@ class ContractInstance(BaseAddress, ContractTypeWrapper): def __init__( self, - address: AddressType, - contract_type: ContractType, + address: "AddressType", + contract_type: "ContractType", txn_hash: Optional[Union[str, HexBytes]] = None, ) -> None: super().__init__() @@ -957,7 +958,9 @@ def __call__(self, *args, **kwargs) -> "ReceiptAPI": return super().__call__(*args, **kwargs) @classmethod - def from_receipt(cls, receipt: "ReceiptAPI", contract_type: ContractType) -> "ContractInstance": + def from_receipt( + cls, receipt: "ReceiptAPI", contract_type: "ContractType" + ) -> "ContractInstance": """ Create a contract instance from the contract deployment receipt. """ @@ -997,7 +1000,7 @@ def __repr__(self) -> str: return f"<{contract_name} {self.address}>" @property - def address(self) -> AddressType: + def address(self) -> "AddressType": """ The address of the contract. @@ -1009,7 +1012,7 @@ def address(self) -> AddressType: @cached_property def _view_methods_(self) -> dict[str, ContractCallHandler]: - view_methods: dict[str, list[MethodABI]] = dict() + view_methods: dict[str, list["MethodABI"]] = dict() for abi in self.contract_type.view_methods: if abi.name in view_methods: @@ -1028,7 +1031,7 @@ def _view_methods_(self) -> dict[str, ContractCallHandler]: @cached_property def _mutable_methods_(self) -> dict[str, ContractTransactionHandler]: - mutable_methods: dict[str, list[MethodABI]] = dict() + mutable_methods: dict[str, list["MethodABI"]] = dict() for abi in self.contract_type.mutable_methods: if abi.name in mutable_methods: @@ -1075,7 +1078,7 @@ def call_view_method(self, method_name: str, *args, **kwargs) -> Any: else: # Didn't find anything that matches - name = self.contract_type.name or ContractType.__name__ + name = self.contract_type.name or "ContractType" raise ApeAttributeError(f"'{name}' has no attribute '{method_name}'.") def invoke_transaction(self, method_name: str, *args, **kwargs) -> "ReceiptAPI": @@ -1110,7 +1113,7 @@ def invoke_transaction(self, method_name: str, *args, **kwargs) -> "ReceiptAPI": else: # Didn't find anything that matches - name = self.contract_type.name or ContractType.__name__ + name = self.contract_type.name or "ContractType" raise ApeAttributeError(f"'{name}' has no attribute '{method_name}'.") def get_event_by_signature(self, signature: str) -> ContractEvent: @@ -1168,7 +1171,7 @@ def get_error_by_signature(self, signature: str) -> type[CustomError]: @cached_property def _events_(self) -> dict[str, list[ContractEvent]]: - events: dict[str, list[EventABI]] = {} + events: dict[str, list["EventABI"]] = {} for abi in self.contract_type.events: if abi.name in events: @@ -1339,7 +1342,7 @@ class ContractContainer(ContractTypeWrapper, ExtraAttributesMixin): contract_container = project.MyContract # Assuming there is a contract named "MyContract" """ - def __init__(self, contract_type: ContractType) -> None: + def __init__(self, contract_type: "ContractType") -> None: self.contract_type = contract_type @log_instead_of_fail(default="") @@ -1404,7 +1407,7 @@ def deployments(self): return self.chain_manager.contracts.get_deployments(self) def at( - self, address: AddressType, txn_hash: Optional[Union[str, HexBytes]] = None + self, address: "AddressType", txn_hash: Optional[Union[str, HexBytes]] = None ) -> ContractInstance: """ Get a contract at the given address. @@ -1473,7 +1476,7 @@ def deploy(self, *args, publish: bool = False, **kwargs) -> ContractInstance: if kwargs.get("value") and not self.contract_type.constructor.is_payable: raise MethodNonPayableError("Sending funds to a non-payable constructor.") - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): # Handle account-related preparation if needed, such as signing receipt = self._cache_wrap(lambda: kwargs["sender"].call(txn, **kwargs)) @@ -1533,7 +1536,7 @@ def declare(self, *args, **kwargs) -> "ReceiptAPI": transaction = self.provider.network.ecosystem.encode_contract_blueprint( self.contract_type, *args, **kwargs ) - if "sender" in kwargs and isinstance(kwargs["sender"], AccountAPI): + if "sender" in kwargs and hasattr(kwargs["sender"], "call"): return kwargs["sender"].call(transaction) receipt = self.provider.send_transaction(transaction) diff --git a/src/ape_console/_cli.py b/src/ape_console/_cli.py index c3eef924fa..a3d3431cac 100644 --- a/src/ape_console/_cli.py +++ b/src/ape_console/_cli.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any, Optional, cast import click -from IPython import InteractiveShell from ape.cli.commands import ConnectedProviderCommand from ape.cli.options import ape_cli_context, project_option @@ -195,6 +194,8 @@ def _launch_console( def _execute_code(code: list[str], **ipython_kwargs): + from IPython import InteractiveShell + shell = InteractiveShell.instance(**ipython_kwargs) # NOTE: Using `store_history=True` just so the cell IDs are accurate. for line in code: diff --git a/src/ape_ethereum/provider.py b/src/ape_ethereum/provider.py index 8359729a09..58bba04dde 100644 --- a/src/ape_ethereum/provider.py +++ b/src/ape_ethereum/provider.py @@ -1340,7 +1340,7 @@ def uri(self) -> str: else: raise TypeError(f"Not an URI: {uri}") - config = self.config.model_dump().get(self.network.ecosystem.name, None) + config: dict = self.config.get(self.network.ecosystem.name, None) if config is None: if rpc := self._get_random_rpc(): return rpc @@ -1351,7 +1351,7 @@ def uri(self) -> str: raise ProviderError(f"Please configure a URL for '{self.network_choice}'.") # Use value from config file - network_config = config.get(self.network.name) or DEFAULT_SETTINGS + network_config: dict = (config or {}).get(self.network.name) or DEFAULT_SETTINGS if "url" in network_config: raise ConfigError("Unknown provider setting 'url'. Did you mean 'uri'?") @@ -1370,10 +1370,11 @@ def uri(self) -> str: settings_uri = network_config.get(key, DEFAULT_SETTINGS["uri"]) if _is_uri(settings_uri): + # Is true if HTTP, WS, or IPC. return settings_uri - # Likely was an IPC Path (or websockets) and will connect that way. - return super().http_uri or "" + # Is not HTTP, WS, or IPC. Raise an error. + raise ConfigError(f"Invalid URI (not HTTP, WS, or IPC): {settings_uri}") @property def http_uri(self) -> Optional[str]: diff --git a/src/ape_node/provider.py b/src/ape_node/provider.py index 95bd54d2b7..aa7fb0f2ee 100644 --- a/src/ape_node/provider.py +++ b/src/ape_node/provider.py @@ -208,7 +208,7 @@ def disconnect(self): def _clean(self): if self._data_dir.is_dir(): - shutil.rmtree(self._data_dir) + shutil.rmtree(self._data_dir, ignore_errors=True) # dir must exist when initializing chain. self._data_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/ape_run/_cli.py b/src/ape_run/_cli.py index 94e30d361d..6dc0153295 100644 --- a/src/ape_run/_cli.py +++ b/src/ape_run/_cli.py @@ -8,19 +8,18 @@ from typing import Any, Union import click -from click import Command, Context, Option from ape.cli.commands import ConnectedProviderCommand from ape.cli.options import _VERBOSITY_VALUES, _create_verbosity_kwargs, verbosity_option from ape.exceptions import ApeException, handle_ape_exception from ape.logging import logger -from ape.utils.basemodel import ManagerAccessMixin as access -from ape.utils.os import get_relative_path, use_temp_sys_path -from ape_console._cli import console @contextmanager def use_scripts_sys_path(path: Path): + # perf: avoid importing at top of module so `--help` is faster. + from ape.utils.os import use_temp_sys_path + # First, ensure there is not an existing scripts module. scripts = sys.modules.get("scripts") if scripts: @@ -70,7 +69,9 @@ def __init__(self, *args, **kwargs): self._command_called = None self._has_warned_missing_hook: set[Path] = set() - def invoke(self, ctx: Context) -> Any: + def invoke(self, ctx: click.Context) -> Any: + from ape.utils.basemodel import ManagerAccessMixin as access + try: return super().invoke(ctx) except Exception as err: @@ -95,7 +96,8 @@ def invoke(self, ctx: Context) -> Any: raise def _get_command(self, filepath: Path) -> Union[click.Command, click.Group, None]: - relative_filepath = get_relative_path(filepath, access.local_project.path) + scripts_folder = Path.cwd() / "scripts" + relative_filepath = filepath.relative_to(scripts_folder) # First load the code module by compiling it # NOTE: This does not execute the module @@ -122,14 +124,14 @@ def _get_command(self, filepath: Path) -> Union[click.Command, click.Group, None self._namespace[filepath.stem] = cli_ns cli_obj = cli_ns["cli"] - if not isinstance(cli_obj, Command): + if not isinstance(cli_obj, click.Command): logger.warning("Found `cli()` method but it is not a click command.") return None params = [getattr(x, "name", None) for x in cli_obj.params] if "verbosity" not in params: option_kwargs = _create_verbosity_kwargs() - option = Option(_VERBOSITY_VALUES, **option_kwargs) + option = click.Option(_VERBOSITY_VALUES, **option_kwargs) cli_obj.params.append(option) cli_obj.name = filepath.stem if cli_obj.name in ("cli", "", None) else cli_obj.name @@ -175,13 +177,16 @@ def call(): @property def commands(self) -> dict[str, Union[click.Command, click.Group]]: - if not access.local_project.scripts_folder.is_dir(): + # perf: Don't reference `.local_project.scripts_folder` here; + # it's too slow when doing just doing `--help`. + scripts_folder = Path.cwd() / "scripts" + if not scripts_folder.is_dir(): return {} - return self._get_cli_commands(access.local_project.scripts_folder) + return self._get_cli_commands(scripts_folder) def _get_cli_commands(self, base_path: Path) -> dict: - commands: dict[str, Command] = {} + commands: dict[str, click.Command] = {} for filepath in base_path.iterdir(): if filepath.stem.startswith("_"): @@ -194,6 +199,7 @@ def _get_cli_commands(self, base_path: Path) -> dict: subcommands = self._get_cli_commands(filepath) for subcommand in subcommands.values(): group.add_command(subcommand) + commands[filepath.stem] = group if filepath.suffix == ".py": @@ -223,6 +229,8 @@ def result_callback(self, result, interactive: bool): # type: ignore[override] return result def _launch_console(self): + from ape.utils.basemodel import ManagerAccessMixin as access + trace = inspect.trace() trace_frames = [ x for x in trace if x.filename.startswith(str(access.local_project.scripts_folder)) @@ -247,6 +255,8 @@ def _launch_console(self): if frame: del frame + from ape_console._cli import console + return console(project=access.local_project, extra_locals=extra_locals, embed=True) diff --git a/tests/functional/geth/test_provider.py b/tests/functional/geth/test_provider.py index 57cb676451..93e8c550ca 100644 --- a/tests/functional/geth/test_provider.py +++ b/tests/functional/geth/test_provider.py @@ -1,3 +1,4 @@ +import re from pathlib import Path from typing import cast @@ -16,6 +17,7 @@ from ape.exceptions import ( APINotImplementedError, BlockNotFoundError, + ConfigError, ContractLogicError, NetworkMismatchError, ProviderError, @@ -127,6 +129,23 @@ def test_uri_non_dev_and_not_configured(mocker, ethereum): assert actual == expected +def test_uri_invalid(geth_provider, project, ethereum): + settings = geth_provider.provider_settings + geth_provider.provider_settings = {} + value = "I AM NOT A URI OF ANY KIND!" + config = {"node": {"ethereum": {"local": {"uri": value}}}} + + try: + with project.temp_config(**config): + # Assert we use the config value. + expected = rf"Invalid URI \(not HTTP, WS, or IPC\): {re.escape(value)}" + with pytest.raises(ConfigError, match=expected): + _ = geth_provider.uri + + finally: + geth_provider.provider_settings = settings + + @geth_process_test def test_repr_connected(geth_provider): actual = repr(geth_provider) diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index bb0ee3de41..b432fc1b94 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -673,8 +673,12 @@ class TestProject: def test_init(self, with_dependencies_project_path): # Purpose not using `project_with_contracts` fixture. project = Project(with_dependencies_project_path) - project.manifest_path.unlink(missing_ok=True) assert project.path == with_dependencies_project_path + project.manifest_path.unlink(missing_ok=True) + + # Re-init to show it doesn't create the manifest file. + project = Project(with_dependencies_project_path) + # Manifest should have been created by default. assert not project.manifest_path.is_file()