From eda5355d8ad6cf763d52ab2ece36ea8585cc8c11 Mon Sep 17 00:00:00 2001 From: antazoey Date: Wed, 30 Oct 2024 09:49:49 -0500 Subject: [PATCH 01/12] refactor: move basemodel types to `ape.types` (#2353) --- src/ape/types/__init__.py | 20 ++++++++++++++++++++ src/ape/utils/basemodel.py | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index 3893f7bcb5..0b4cc07792 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -26,12 +26,26 @@ from ape.types.trace import ContractFunctionPath, ControlFlow, GasReport, SourceTraceback from ape.types.units import CurrencyValue, CurrencyValueComparable from ape.types.vm import BlockID, ContractCode, SnapshotID +from ape.utils.basemodel import ( + BaseInterface, + BaseInterfaceModel, + BaseModel, + ExtraAttributesMixin, + ExtraModelAttributes, + ManagerAccessMixin, + get_attribute_with_extras, + get_item_with_extras, + only_raise_attribute_error, +) __all__ = [ "_LazySequence", "ABI", "AddressType", "AutoGasLimit", + "BaseInterface", + "BaseInterfaceModel", + "BaseModel", "BlockID", "Bytecode", "Checksum", @@ -50,13 +64,19 @@ "CoverageStatement", "CurrencyValue", "CurrencyValueComparable", + "ExtraAttributesMixin", + "ExtraModelAttributes", "GasLimit", "GasReport", + "get_attribute_with_extras", + "get_item_with_extras", "HexInt", "HexBytes", "LogFilter", + "ManagerAccessMixin", "MessageSignature", "MockContractLog", + "only_raise_attribute_error", "PackageManifest", "PackageMeta", "RawAddress", diff --git a/src/ape/utils/basemodel.py b/src/ape/utils/basemodel.py index 03f1bf4c58..cabac370df 100644 --- a/src/ape/utils/basemodel.py +++ b/src/ape/utils/basemodel.py @@ -1,3 +1,7 @@ +""" +TODO: In 0.9, move this module to `ape.types`. +""" + import inspect from abc import ABC from collections.abc import Callable, Iterator, Sequence From 84928c527e22fbcd4a679eb0b5550a0b69d7f493 Mon Sep 17 00:00:00 2001 From: antazoey Date: Wed, 30 Oct 2024 10:21:16 -0500 Subject: [PATCH 02/12] refactor: use raw import statements over `import_module` for lazy imports (#2354) --- src/ape/__init__.py | 12 ++++----- src/ape/_cli.py | 6 ++--- src/ape/cli/choices.py | 18 ++++++++----- src/ape/cli/commands.py | 10 +++---- src/ape/cli/options.py | 4 +-- src/ape/exceptions.py | 4 +-- src/ape/managers/__init__.py | 51 ++++++++++++++++++++---------------- src/ape/types/basic.py | 3 +-- src/ape_accounts/__init__.py | 3 +-- src/ape_cache/_cli.py | 5 ++-- src/ape_networks/__init__.py | 3 +-- src/ape_networks/_cli.py | 6 ++--- 12 files changed, 65 insertions(+), 60 deletions(-) diff --git a/src/ape/__init__.py b/src/ape/__init__.py index 7fb72453ba..efb1345297 100644 --- a/src/ape/__init__.py +++ b/src/ape/__init__.py @@ -1,13 +1,11 @@ import signal import threading -from typing import Any if threading.current_thread() is threading.main_thread(): # If we are in the main thread, we can safely set the signal handler signal.signal(signal.SIGINT, lambda s, f: _sys.exit(130)) import sys as _sys -from importlib import import_module __all__ = [ "accounts", @@ -23,16 +21,18 @@ ] -def __getattr__(name: str) -> Any: +def __getattr__(name: str): if name not in __all__: raise AttributeError(name) elif name == "reverts": - contextmanagers = import_module("ape.pytest.contextmanagers") - return contextmanagers.RevertsContextManager + from ape.pytest.contextmanagers import RevertsContextManager + + return RevertsContextManager else: - access = import_module("ape.managers.project").ManagerAccessMixin + from ape.utils.basemodel import ManagerAccessMixin as access + if name == "Contract": return access.chain_manager.contracts.instance_at diff --git a/src/ape/_cli.py b/src/ape/_cli.py index 8afe38318b..545fed61c2 100644 --- a/src/ape/_cli.py +++ b/src/ape/_cli.py @@ -4,7 +4,6 @@ from collections.abc import Iterable from functools import cached_property from gettext import gettext -from importlib import import_module from importlib.metadata import entry_points from pathlib import Path from typing import TYPE_CHECKING, Any, Optional @@ -65,6 +64,7 @@ def parse_args(self, ctx: "Context", args: list[str]) -> list[str]: return super().parse_args(ctx, args) def format_commands(self, ctx, formatter) -> None: + from ape.plugins._utils import PluginMetadataList from ape.utils.basemodel import ManagerAccessMixin as access commands = [] @@ -86,10 +86,8 @@ def format_commands(self, ctx, formatter) -> None: "Plugin": [], "3rd-Party Plugin": [], } - plugin_utils = import_module("ape.plugins._utils") - metadata_cls = plugin_utils.PluginMetadataList plugin_manager = access.plugin_manager - pl_metadata = metadata_cls.load(plugin_manager, include_available=False) + pl_metadata = PluginMetadataList.load(plugin_manager, include_available=False) for cli_name, cmd in commands: help = cmd.get_short_help_str(limit) plugin = pl_metadata.get_plugin(cli_name, check_available=False) diff --git a/src/ape/cli/choices.py b/src/ape/cli/choices.py index e7dc752107..d880e262bc 100644 --- a/src/ape/cli/choices.py +++ b/src/ape/cli/choices.py @@ -72,8 +72,9 @@ def __init__(self, key: _ACCOUNT_TYPE_FILTER = None): @cached_property def choices(self) -> Sequence: # type: ignore[override] - module = import_module("ape.types.basic") - return module._LazySequence(self._choices_iterator) + from ape.types.basic import _LazySequence + + return _LazySequence(self._choices_iterator) @property def _choices_iterator(self) -> Iterator[str]: @@ -172,8 +173,9 @@ def select_account( Returns: :class:`~ape.api.accounts.AccountAPI` """ - account_module = import_module("ape.api.accounts") - if key and isinstance(key, type) and not issubclass(key, account_module.AccountAPI): + from ape.api.accounts import AccountAPI + + if key and isinstance(key, type) and not issubclass(key, AccountAPI): raise AccountsError(f"Cannot return accounts with type '{key}'.") prompt = AccountAliasPromptChoice(prompt_message=prompt_message, key=key) @@ -196,8 +198,12 @@ def __init__( self._key_filter = key self._prompt_message = prompt_message or "Select an account" self.name = name - module = import_module("ape.types.basic") - self.choices = module._LazySequence(self._choices_iterator) + + @cached_property + def choices(self) -> Sequence[str]: # type: ignore[override] + from ape.types.basic import _LazySequence + + return _LazySequence(self._choices_iterator) def convert( self, value: Any, param: Optional[Parameter], ctx: Optional[Context] diff --git a/src/ape/cli/commands.py b/src/ape/cli/commands.py index 63a8a2f246..ea6110d42c 100644 --- a/src/ape/cli/commands.py +++ b/src/ape/cli/commands.py @@ -1,5 +1,4 @@ import inspect -from importlib import import_module from typing import TYPE_CHECKING, Any, Optional import click @@ -26,6 +25,7 @@ def get_param_from_ctx(ctx: "Context", param: str) -> Optional[Any]: def parse_network(ctx: "Context") -> Optional["ProviderContextManager"]: + from ape.api.providers import ProviderAPI from ape.utils.basemodel import ManagerAccessMixin as access interactive = get_param_from_ctx(ctx, "interactive") @@ -36,8 +36,7 @@ def parse_network(ctx: "Context") -> Optional["ProviderContextManager"]: return provider.network.use_provider(provider, disconnect_on_exit=not interactive) provider = get_param_from_ctx(ctx, "network") - provider_module = import_module("ape.api.providers") - if provider is not None and isinstance(provider, provider_module.ProviderAPI): + if provider is not None and isinstance(provider, ProviderAPI): return provider.network.use_provider(provider, disconnect_on_exit=not interactive) elif provider not in (None, _NONE_NETWORK) and isinstance(provider, str): @@ -72,9 +71,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def parse_args(self, ctx: "Context", args: list[str]) -> list[str]: + from ape.api.providers import ProviderAPI + arguments = args # Renamed for better pdb support. - provider_module = import_module("ape.api.providers") - base_type = provider_module.ProviderAPI if self._use_cls_types else str + base_type = ProviderAPI if self._use_cls_types else str if existing_option := next( iter( x diff --git a/src/ape/cli/options.py b/src/ape/cli/options.py index 508051e13c..b2d7829440 100644 --- a/src/ape/cli/options.py +++ b/src/ape/cli/options.py @@ -1,7 +1,6 @@ import inspect from collections.abc import Callable from functools import partial -from importlib import import_module from pathlib import Path from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union @@ -347,11 +346,10 @@ def _update_context_with_network(ctx, provider, requested_network_objects): def _get_provider(value, default, keep_as_choice_str): + from ape.api.providers import ProviderAPI from ape.utils.basemodel import ManagerAccessMixin use_default = value is None and default == "auto" - provider_module = import_module("ape.api.providers") - ProviderAPI = provider_module.ProviderAPI if not keep_as_choice_str and use_default: default_ecosystem = ManagerAccessMixin.network_manager.default_ecosystem diff --git a/src/ape/exceptions.py b/src/ape/exceptions.py index db50583303..14e4aed00a 100644 --- a/src/ape/exceptions.py +++ b/src/ape/exceptions.py @@ -5,7 +5,6 @@ import traceback from collections.abc import Collection, Iterable from functools import cached_property -from importlib import import_module from inspect import getframeinfo, stack from pathlib import Path from types import CodeType, TracebackType @@ -922,7 +921,8 @@ def _get_custom_python_traceback( # https://github.com/pallets/jinja/blob/main/src/jinja2/debug.py#L142 if project is None: - access = import_module("ape.utils.basemodel").ManagerAccessMixin + from ape.utils.basemodel import ManagerAccessMixin as access + project = access.local_project if not (base_path := getattr(project, "path", None)): diff --git a/src/ape/managers/__init__.py b/src/ape/managers/__init__.py index 653548550f..8378c05871 100644 --- a/src/ape/managers/__init__.py +++ b/src/ape/managers/__init__.py @@ -1,43 +1,48 @@ -from importlib import import_module -from typing import Any - - -def __getattr__(name: str) -> Any: +def __getattr__(name: str): if name == "AccountManager": - module = import_module("ape.managers.accounts") - return module.AccountManager + from ape.managers.accounts import AccountManager + + return AccountManager elif name == "ChainManager": - module = import_module("ape.managers.chain") - return module.ChainManager + from ape.managers.chain import ChainManager + + return ChainManager elif name == "CompilerManager": - module = import_module("ape.managers.compilers") - return module.CompilerManager + from ape.managers.compilers import CompilerManager + + return CompilerManager elif name == "ConfigManager": - module = import_module("ape.managers.config") - return module.ConfigManager + from ape.managers.config import ConfigManager + + return ConfigManager elif name == "ConversionManager": - module = import_module("ape.managers.converters") - return module.ConversionManager + from ape.managers.converters import ConversionManager + + return ConversionManager elif name == "NetworkManager": - module = import_module("ape.managers.networks") - return module.NetworkManager + from ape.managers.networks import NetworkManager + + return NetworkManager elif name == "PluginManager": - module = import_module("ape.managers.plugins") - return module.PluginManager + from ape.managers.plugins import PluginManager + + return PluginManager elif name == "ProjectManager": - module = import_module("ape.managers.project") - return module.ProjectManager + from ape.managers.project import ProjectManager + + return ProjectManager elif name == "QueryManager": - module = import_module("ape.managers.query") - return module.QueryManager + from ape.managers.query import QueryManager + + return QueryManager else: raise AttributeError(name) diff --git a/src/ape/types/basic.py b/src/ape/types/basic.py index 56899b1209..b5e54c815a 100644 --- a/src/ape/types/basic.py +++ b/src/ape/types/basic.py @@ -1,5 +1,4 @@ from collections.abc import Callable, Iterator, Sequence -from importlib import import_module from typing import Annotated, TypeVar, Union, overload from pydantic import BeforeValidator @@ -11,7 +10,7 @@ def _hex_int_validator(value, info): return value # NOTE: Allows this module to load lazier. - access = import_module("ape.utils.basemodel").ManagerAccessMixin + from ape.utils.basemodel import ManagerAccessMixin as access convert = access.conversion_manager.convert return convert(value, int) diff --git a/src/ape_accounts/__init__.py b/src/ape_accounts/__init__.py index b3af3607f3..2c9148cf2a 100644 --- a/src/ape_accounts/__init__.py +++ b/src/ape_accounts/__init__.py @@ -1,5 +1,4 @@ from importlib import import_module -from typing import Any from ape.plugins import AccountPlugin, register @@ -11,7 +10,7 @@ def account_types(): return AccountContainer, KeyfileAccount -def __getattr__(name: str) -> Any: +def __getattr__(name: str): return getattr(import_module("ape_accounts.accounts"), name) diff --git a/src/ape_cache/_cli.py b/src/ape_cache/_cli.py index 89c949375b..07f0fe918f 100644 --- a/src/ape_cache/_cli.py +++ b/src/ape_cache/_cli.py @@ -12,8 +12,9 @@ def get_engine() -> "CacheQueryProvider": - basemodel = import_module("ape.utils.basemodel") - return basemodel.ManagerAccessMixin.query_manager.engines["cache"] + from ape.utils.basemodel import ManagerAccessMixin + + return ManagerAccessMixin.query_manager.engines["cache"] @click.group(short_help="Query from caching database") diff --git a/src/ape_networks/__init__.py b/src/ape_networks/__init__.py index 51e382e1f8..d5958b94e5 100644 --- a/src/ape_networks/__init__.py +++ b/src/ape_networks/__init__.py @@ -1,5 +1,4 @@ from importlib import import_module -from typing import Any from ape.plugins import Config, register @@ -11,7 +10,7 @@ def config_class(): return NetworksConfig -def __getattr__(name: str) -> Any: +def __getattr__(name: str): if name in ("NetworksConfig", "CustomNetwork"): return getattr(import_module("ape_networks.config"), name) diff --git a/src/ape_networks/_cli.py b/src/ape_networks/_cli.py index 41ff326e42..a8b2095f46 100644 --- a/src/ape_networks/_cli.py +++ b/src/ape_networks/_cli.py @@ -1,6 +1,5 @@ import json from collections.abc import Callable, Sequence -from importlib import import_module from typing import TYPE_CHECKING import click @@ -118,10 +117,11 @@ def run(cli_ctx, provider): Start a subprocess node as if running independently and stream stdout and stderr. """ + from ape.api.providers import SubprocessProvider + # Ignore extra loggers, such as web3 loggers. cli_ctx.logger._extra_loggers = {} - providers_module = import_module("ape.api.providers") - if not isinstance(provider, providers_module.SubprocessProvider): + if not isinstance(provider, SubprocessProvider): cli_ctx.abort( f"`ape networks run` requires a provider that manages a process, not '{provider.name}'." ) From faed339b9ce0de22bced4f54b2a581679a350875 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 08:28:03 -0500 Subject: [PATCH 03/12] perf: lazy load treatment for `ape-ethereum` core plugin (#2356) --- src/ape_ethereum/__init__.py | 112 ++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 40 deletions(-) diff --git a/src/ape_ethereum/__init__.py b/src/ape_ethereum/__init__.py index 552427d10e..849efbcac1 100644 --- a/src/ape_ethereum/__init__.py +++ b/src/ape_ethereum/__init__.py @@ -1,49 +1,32 @@ from ape import plugins -from ape.api.networks import ForkedNetworkAPI, NetworkAPI, create_network_type - -from ._converters import WeiConversions -from .ecosystem import ( - NETWORKS, - BaseEthereumConfig, - Block, - Ethereum, - EthereumConfig, - ForkedNetworkConfig, - NetworkConfig, -) -from .provider import EthereumNodeProvider, Web3Provider, assert_web3_provider_uri_env_var_not_set -from .query import EthereumQueryProvider -from .trace import CallTrace, Trace, TransactionTrace -from .transactions import ( - AccessListTransaction, - BaseTransaction, - DynamicFeeTransaction, - Receipt, - SharedBlobReceipt, - SharedBlobTransaction, - StaticFeeTransaction, - TransactionStatusEnum, - TransactionType, -) @plugins.register(plugins.Config) def config_class(): + from ape_ethereum.ecosystem import EthereumConfig + return EthereumConfig @plugins.register(plugins.ConversionPlugin) def converters(): + from ape_ethereum._converters import WeiConversions + yield int, WeiConversions @plugins.register(plugins.EcosystemPlugin) def ecosystems(): + from ape_ethereum.ecosystem import Ethereum + yield Ethereum @plugins.register(plugins.NetworkPlugin) def networks(): + from ape.api.networks import ForkedNetworkAPI, NetworkAPI, create_network_type + from ape_ethereum.ecosystem import NETWORKS + for network_name, network_params in NETWORKS.items(): yield "ethereum", network_name, create_network_type(*network_params) yield "ethereum", f"{network_name}-fork", ForkedNetworkAPI @@ -54,29 +37,78 @@ def networks(): @plugins.register(plugins.QueryPlugin) def query_engines(): + from .query import EthereumQueryProvider + yield EthereumQueryProvider +def __getattr__(name): + if name in ( + "BaseEthereumConfig", + "Block", + "Ethereum", + "EthereumConfig", + "ForkedNetworkConfig", + "NetworkConfig", + ): + import ape_ethereum.ecosystem as ecosystem_module + + return getattr(ecosystem_module, name) + + elif name in ( + "EthereumNodeProvider", + "Web3Provider", + "assert_web3_provider_uri_env_var_not_set", + ): + import ape_ethereum.provider as provider_module + + return getattr(provider_module, name) + + elif name in ( + "AccessListTransaction", + "BaseTransaction", + "DynamicFeeTransaction", + "Receipt", + "SharedBlobReceipt", + "SharedBlobTransaction", + "StaticFeeTransaction", + "TransactionStatusEnum", + "TransactionType", + ): + import ape_ethereum.transactions as tx_module + + return getattr(tx_module, name) + + elif name in ("CallTrace", "Trace", "TransactionTrace"): + import ape_ethereum.trace as trace_module + + return getattr(trace_module, name) + + else: + raise AttributeError(name) + + __all__ = [ - "Ethereum", - "EthereumConfig", - "NetworkConfig", - "ForkedNetworkConfig", + "AccessListTransaction", + "assert_web3_provider_uri_env_var_not_set", "BaseEthereumConfig", + "BaseTransaction", "Block", - "assert_web3_provider_uri_env_var_not_set", - "Web3Provider", + "CallTrace", + "DynamicFeeTransaction", + "Ethereum", + "EthereumConfig", "EthereumNodeProvider", + "ForkedNetworkConfig", + "NetworkConfig", + "Receipt", + "SharedBlobReceipt", + "SharedBlobTransaction", + "StaticFeeTransaction", "Trace", - "TransactionTrace", - "CallTrace", "TransactionStatusEnum", + "TransactionTrace", "TransactionType", - "BaseTransaction", - "StaticFeeTransaction", - "AccessListTransaction", - "DynamicFeeTransaction", "SharedBlobTransaction", - "Receipt", - "SharedBlobReceipt", + "Web3Provider", ] From a5429c54fe99abfc21e41194b34556cc8ba46171 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 09:15:37 -0500 Subject: [PATCH 04/12] perf: only load necessary plugins for CLI (#2358) --- src/ape/_cli.py | 4 +--- src/ape/plugins/_utils.py | 7 +++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ape/_cli.py b/src/ape/_cli.py index 545fed61c2..d8a193ee41 100644 --- a/src/ape/_cli.py +++ b/src/ape/_cli.py @@ -65,7 +65,6 @@ def parse_args(self, ctx: "Context", args: list[str]) -> list[str]: def format_commands(self, ctx, formatter) -> None: from ape.plugins._utils import PluginMetadataList - from ape.utils.basemodel import ManagerAccessMixin as access commands = [] for subcommand in self.list_commands(ctx): @@ -86,8 +85,7 @@ def format_commands(self, ctx, formatter) -> None: "Plugin": [], "3rd-Party Plugin": [], } - plugin_manager = access.plugin_manager - pl_metadata = PluginMetadataList.load(plugin_manager, include_available=False) + pl_metadata = PluginMetadataList.from_package_names(f"ape_{c[0]}" for c in commands) for cli_name, cmd in commands: help = cmd.get_short_help_str(limit) plugin = pl_metadata.get_plugin(cli_name, check_available=False) diff --git a/src/ape/plugins/_utils.py b/src/ape/plugins/_utils.py index 123580e288..d161284386 100644 --- a/src/ape/plugins/_utils.py +++ b/src/ape/plugins/_utils.py @@ -4,7 +4,7 @@ from enum import Enum from functools import cached_property from shutil import which -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional from urllib.parse import urlparse import click @@ -19,6 +19,9 @@ from ape.utils.misc import _get_distributions, get_package_version, log_instead_of_fail from ape.version import version as ape_version_str +if TYPE_CHECKING: + from ape.managers.plugins import PluginManager + # Plugins maintained OSS by ApeWorX (and trusted) # Use `uv pip` if installed, otherwise `python -m pip` PIP_COMMAND = ["uv", "pip"] if which("uv") else [sys.executable, "-m", "pip"] @@ -202,7 +205,7 @@ class PluginMetadataList(BaseModel): third_party: "PluginGroup" @classmethod - def load(cls, plugin_manager, include_available: bool = True): + def load(cls, plugin_manager: "PluginManager", include_available: bool = True): plugins = plugin_manager.registered_plugins if include_available: plugins = plugins.union(github_client.available_plugins) From a6ddfff80e65e0146d901863ef5104f04830f143 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 10:32:50 -0500 Subject: [PATCH 05/12] perf: make all commands faster, especially `ape init`, adds `--name` flag to `init` cmd (#2355) --- src/ape/_cli.py | 29 ++--------------------------- src/ape_init/_cli.py | 7 ++++--- 2 files changed, 6 insertions(+), 30 deletions(-) diff --git a/src/ape/_cli.py b/src/ape/_cli.py index d8a193ee41..3a61812a33 100644 --- a/src/ape/_cli.py +++ b/src/ape/_cli.py @@ -6,20 +6,16 @@ from gettext import gettext from importlib.metadata import entry_points from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional +from typing import Any, Optional from warnings import catch_warnings, simplefilter import click -import rich import yaml from ape.cli.options import ape_cli_context -from ape.exceptions import Abort, ApeException, ConfigError, handle_ape_exception +from ape.exceptions import Abort, ApeException, handle_ape_exception from ape.logging import logger -if TYPE_CHECKING: - from click import Context - _DIFFLIB_CUT_OFF = 0.6 @@ -39,30 +35,9 @@ def display_config(ctx, param, value): ctx.exit() # NOTE: Must exit to bypass running ApeCLI -def _validate_config(): - from ape.utils.basemodel import ManagerAccessMixin as access - - project = access.local_project - try: - _ = project.config - except ConfigError as err: - rich.print(err) - # Exit now to avoid weird problems. - sys.exit(1) - - class ApeCLI(click.MultiCommand): _CLI_GROUP_NAME = "ape_cli_subcommands" - def parse_args(self, ctx: "Context", args: list[str]) -> list[str]: - # Validate the config before any argument parsing, - # as arguments may utilize config. - if "--help" not in args and args != []: - # perf: don't bother w/ config if only doing --help. - _validate_config() - - return super().parse_args(ctx, args) - def format_commands(self, ctx, formatter) -> None: from ape.plugins._utils import PluginMetadataList diff --git a/src/ape_init/_cli.py b/src/ape_init/_cli.py index 1102dbb9e7..5b57012a23 100644 --- a/src/ape_init/_cli.py +++ b/src/ape_init/_cli.py @@ -5,7 +5,6 @@ from click import BadParameter from ape.cli.options import ape_cli_context -from ape.utils._github import github_client GITIGNORE_CONTENT = """ # Ape stuff @@ -45,12 +44,15 @@ def validate_github_repo(ctx, param, value): help="Clone a template from Github", callback=validate_github_repo, ) -def cli(cli_ctx, github): +@click.option("--name", "project_name", prompt=True, help="A project name") +def cli(cli_ctx, github, project_name): """ ``ape init`` allows the user to create an ape project with default folders and ape-config.yaml. """ if github: + from ape.utils._github import github_client + org, repo = github github_client.clone_repo(org, repo, Path.cwd()) shutil.rmtree(Path.cwd() / ".git", ignore_errors=True) @@ -76,6 +78,5 @@ def cli(cli_ctx, github): if ape_config.exists(): cli_ctx.logger.warning(f"'{ape_config}' exists") else: - project_name = click.prompt("Please enter project name") ape_config.write_text(f"name: {project_name}\n", encoding="utf8") cli_ctx.logger.success(f"{project_name} is written in ape-config.yaml") From 115648eb37ae2606837ca8b53fd277ed847b3219 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 10:46:00 -0500 Subject: [PATCH 06/12] fix: more helpful traceback for `__getattr__` on `project` (#2357) --- src/ape/managers/project.py | 6 ++++-- tests/functional/test_project.py | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index f84e19e67a..c900e8fdb6 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -2216,8 +2216,10 @@ def __getattr__(self, item: str) -> Any: "missing compilers for extensions: " + f'{", ".join(sorted(missing_exts))}?' ) - err.args = (message,) - raise # The same exception (keep the stack the same height). + # NOTE: Purposely discard the stack-trace and raise a new exception. + # This shows a better stack-trace to the user (rather than weird + # BaseModel internals). + raise AttributeError(message) @property def _contract_sources(self) -> list[ContractSource]: diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index 256fdb841e..e36855cc3f 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -209,9 +209,16 @@ def test_getattr(tmp_project): def test_getattr_not_exists(tmp_project): - with pytest.raises(AttributeError): + expected = ( + r"'LocalProject' object has no attribute 'nope'\. Also checked extra\(s\) 'contracts'\." + ) + with pytest.raises(AttributeError, match=expected) as err: _ = tmp_project.nope + # Was the case where the last entry was from Ape's basemodel stuff. + # Now, it points at the project manager last. + assert "ape/managers/project.py:" in repr(err.traceback[-1]) + def test_getattr_detects_changes(tmp_project): source_id = tmp_project.Other.contract_type.source_id From a84a5ea339e5b19114f9ed353b54c6df24fdd229 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 12:17:04 -0500 Subject: [PATCH 07/12] perf: able to load local project faster (#2359) --- src/ape/managers/project.py | 9 +++++++-- src/ape/pytest/runners.py | 25 ++++++++++++++++--------- tests/functional/test_project.py | 3 ++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/ape/managers/project.py b/src/ape/managers/project.py index c900e8fdb6..2f6f658945 100644 --- a/src/ape/managers/project.py +++ b/src/ape/managers/project.py @@ -2132,8 +2132,6 @@ def __init__( super().__init__(manifest, config_override=self._config_override) - self.path = self._base_path / (self.config.base_path or "") - # NOTE: Avoid pointlessly adding info to the __local__ manifest. # This is mainly for dependencies. if self.manifest_path.stem != "__local__" and not manifest.sources: @@ -2221,6 +2219,13 @@ def __getattr__(self, item: str) -> Any: # BaseModel internals). raise AttributeError(message) + @cached_property + def path(self) -> Path: + """ + The path to the project's "base" (where contract source IDs are relative to). + """ + return self._base_path / (self.config.base_path or "") + @property def _contract_sources(self) -> list[ContractSource]: sources = [] diff --git a/src/ape/pytest/runners.py b/src/ape/pytest/runners.py index e41f724027..6ddac468d8 100644 --- a/src/ape/pytest/runners.py +++ b/src/ape/pytest/runners.py @@ -72,15 +72,22 @@ def pytest_exception_interact(self, report, call): # Else, it gets way too noisy. show_locals = not self.config_wrapper.show_internal - report.longrepr = call.excinfo.getrepr( - funcargs=True, - abspath=Path.cwd(), - showlocals=show_locals, - style="short", - tbfilter=False, - truncate_locals=True, - chain=False, - ) + try: + here = Path.cwd() + + except FileNotFoundError: + pass # In a temp-folder, most likely. + + else: + report.longrepr = call.excinfo.getrepr( + funcargs=True, + abspath=here, + showlocals=show_locals, + style="short", + tbfilter=False, + truncate_locals=True, + chain=False, + ) if self.config_wrapper.interactive and report.failed: traceback = call.excinfo.traceback[-1] diff --git a/tests/functional/test_project.py b/tests/functional/test_project.py index e36855cc3f..bb0ee3de41 100644 --- a/tests/functional/test_project.py +++ b/tests/functional/test_project.py @@ -687,9 +687,10 @@ def test_init_invalid_config(self): os.chdir(temp_dir) expected = r"[.\n]*Input should be a valid string\n-->1: name:\n 2: {asdf}[.\n]*" + weird_project = Project(temp_dir) try: with pytest.raises(ConfigError, match=expected): - _ = Project(temp_dir) + _ = weird_project.path finally: os.chdir(here) From ebb6473a89c1c376b4a4b2901ed334962dfe7119 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 15:37:21 -0500 Subject: [PATCH 08/12] perf: lazy load style on api, utils, and types (#2360) --- docs/methoddocs/utils.md | 61 +++++++++- src/ape/api/__init__.py | 100 ++++++++++++---- src/ape/types/__init__.py | 133 ++++++++++++++------- src/ape/utils/__init__.py | 185 ++++++++++++++++++------------ tests/performance/test_project.py | 1 + 5 files changed, 342 insertions(+), 138 deletions(-) diff --git a/docs/methoddocs/utils.md b/docs/methoddocs/utils.md index 9eb96e3cee..09bafdfbf3 100644 --- a/docs/methoddocs/utils.md +++ b/docs/methoddocs/utils.md @@ -1,8 +1,65 @@ # ape.utils +## ABI + +```{eval-rst} +.. automodule:: ape.utils.abi + :members: + :show-inheritance: +``` + +## Basemodel + +```{eval-rst} +.. automodule:: ape.utils.basemodel + :members: + :show-inheritance: +``` + +## Miscellaneous + +```{eval-rst} +.. automodule:: ape.utils.misc + :members: + :show-inheritance: +``` + +## OS + +```{eval-rst} +.. automodule:: ape.utils.os + :members: + :show-inheritance: +``` + +## Process + +```{eval-rst} +.. automodule:: ape.utils.process + :members: + :show-inheritance: +``` + +## RPC + +```{eval-rst} +.. automodule:: ape.utils.rpc + :members: + :show-inheritance: +``` + +## Testing + +```{eval-rst} +.. automodule:: ape.utils.testing + :members: + :show-inheritance: +``` + +## Trace + ```{eval-rst} -.. automodule:: ape.utils +.. automodule:: ape.utils.trace :members: :show-inheritance: - :exclude-members: abstractmethod, dataclass, __init__ ``` diff --git a/src/ape/api/__init__.py b/src/ape/api/__init__.py index 8ce7a497fd..b3c2bbfb75 100644 --- a/src/ape/api/__init__.py +++ b/src/ape/api/__init__.py @@ -1,27 +1,79 @@ -from .accounts import ( - AccountAPI, - AccountContainerAPI, - ImpersonatedAccount, - TestAccountAPI, - TestAccountContainerAPI, -) -from .address import Address -from .compiler import CompilerAPI -from .config import ConfigDict, ConfigEnum, PluginConfig -from .convert import ConverterAPI -from .explorers import ExplorerAPI -from .networks import ( - EcosystemAPI, - ForkedNetworkAPI, - NetworkAPI, - ProviderContextManager, - create_network_type, -) -from .projects import DependencyAPI, ProjectAPI -from .providers import BlockAPI, ProviderAPI, SubprocessProvider, TestProviderAPI, UpstreamProvider -from .query import QueryAPI, QueryType -from .trace import TraceAPI -from .transactions import ReceiptAPI, TransactionAPI +def __getattr__(name: str): + if name in ( + "AccountAPI", + "AccountContainerAPI", + "ImpersonatedAccount", + "TestAccountAPI", + "TestAccountContainerAPI", + ): + import ape.api.accounts as accounts_module + + return getattr(accounts_module, name) + + elif name in ("Address",): + import ape.api.address as address_module + + return getattr(address_module, name) + + elif name in ("CompilerAPI",): + import ape.api.compiler as compiler_module + + return getattr(compiler_module, name) + + elif name in ("ConfigDict", "ConfigEnum", "PluginConfig"): + import ape.api.config as config_module + + return getattr(config_module, name) + + elif name in ("ConverterAPI",): + import ape.api.convert as convert_module + + return getattr(convert_module, name) + + elif name in ("ExplorerAPI",): + import ape.api.explorers as explorer_module + + return getattr(explorer_module, name) + + elif name in ("BlockAPI, ProviderAPI, SubprocessProvider, TestProviderAPI, UpstreamProvider"): + import ape.api.providers as provider_module + + return getattr(provider_module, name) + + elif name in ( + "EcosystemAPI", + "ForkedNetworkAPI", + "NetworkAPI", + "ProviderContextManager", + "create_network_type", + ): + import ape.api.networks as network_module + + return getattr(network_module, name) + + elif name in ("DependencyAPI", "ProjectAPI"): + import ape.api.projects as project_module + + return getattr(project_module, name) + + elif name in ("QueryAPI", "QueryType"): + import ape.api.query as query_module + + return getattr(query_module, name) + + elif name in ("TraceAPI",): + import ape.api.trace as trace_module + + return getattr(trace_module, name) + + elif name in ("ReceiptAPI", "TransactionAPI"): + import ape.api.transactions as tx_module + + return getattr(tx_module, name) + + else: + raise AttributeError(name) + __all__ = [ "AccountAPI", diff --git a/src/ape/types/__init__.py b/src/ape/types/__init__.py index 0b4cc07792..4b97d97bfa 100644 --- a/src/ape/types/__init__.py +++ b/src/ape/types/__init__.py @@ -1,42 +1,97 @@ -from eth_pydantic_types import HexBytes -from ethpm_types import ( - ABI, - Bytecode, - Checksum, - Compiler, - ContractType, - PackageManifest, - PackageMeta, - Source, -) -from ethpm_types.source import Closure - -from ape.types.address import AddressType, RawAddress -from ape.types.basic import HexInt, _LazySequence -from ape.types.coverage import ( - ContractCoverage, - ContractSourceCoverage, - CoverageProject, - CoverageReport, - CoverageStatement, -) -from ape.types.events import ContractLog, ContractLogContainer, LogFilter, MockContractLog -from ape.types.gas import AutoGasLimit, GasLimit -from ape.types.signatures import MessageSignature, SignableMessage, TransactionSignature -from ape.types.trace import ContractFunctionPath, ControlFlow, GasReport, SourceTraceback -from ape.types.units import CurrencyValue, CurrencyValueComparable -from ape.types.vm import BlockID, ContractCode, SnapshotID -from ape.utils.basemodel import ( - BaseInterface, - BaseInterfaceModel, - BaseModel, - ExtraAttributesMixin, - ExtraModelAttributes, - ManagerAccessMixin, - get_attribute_with_extras, - get_item_with_extras, - only_raise_attribute_error, -) +def __getattr__(name: str): + if name in ("HexBytes",): + from eth_pydantic_types import HexBytes + + return HexBytes + + elif name in ( + "ABI", + "Bytecode", + "Checksum", + "Compiler", + "ContractType", + "PackageManifest", + "PackageMeta", + "Source", + ): + import ethpm_types + + return getattr(ethpm_types, name) + + elif name in ("Closure",): + from ethpm_types.source import Closure + + return Closure + + elif name in ("AddressType", "RawAddress"): + import ape.types.address as address_module + + return getattr(address_module, name) + + elif name in ("HexInt", "_LazySequence"): + import ape.types.basic as basic_module + + return getattr(basic_module, name) + + elif name in ( + "ContractCoverage", + "ContractSourceCoverage", + "CoverageProject", + "CoverageReport", + "CoverageStatement", + ): + import ape.types.coverage as coverage_module + + return getattr(coverage_module, name) + + elif name in ("ContractLog", "ContractLogContainer", "LogFilter", "MockContractLog"): + import ape.types.events as events_module + + return getattr(events_module, name) + + elif name in ("AutoGasLimit", "GasLimit"): + import ape.types.gas as gas_module + + return getattr(gas_module, name) + + elif name in ("MessageSignature", "SignableMessage", "TransactionSignature"): + import ape.types.signatures as sig_module + + return getattr(sig_module, name) + + elif name in ("ContractFunctionPath", "ControlFlow", "GasReport", "SourceTraceback"): + import ape.types.trace as trace_module + + return getattr(trace_module, name) + + elif name in ("CurrencyValue", "CurrencyValueComparable"): + import ape.types.units as units_module + + return getattr(units_module, name) + + elif name in ("BlockID", "ContractCode", "SnapshotID"): + import ape.types.vm as vm_module + + return getattr(vm_module, name) + + elif name in ( + "BaseInterface", + "BaseInterfaceModel", + "BaseModel", + "ExtraAttributesMixin", + "ExtraModelAttributes", + "ManagerAccessMixin", + "get_attribute_with_extras", + "get_item_with_extras", + "only_raise_attribute_error", + ): + import ape.utils.basemodel as basemodel_module + + return getattr(basemodel_module, name) + + else: + raise AttributeError(name) + __all__ = [ "_LazySequence", diff --git a/src/ape/utils/__init__.py b/src/ape/utils/__init__.py index db5f197e05..ed8cc1bd87 100644 --- a/src/ape/utils/__init__.py +++ b/src/ape/utils/__init__.py @@ -1,76 +1,115 @@ -from abc import abstractmethod - -from ape.utils.abi import ( - LogInputABICollection, - Struct, - StructParser, - is_array, - is_dynamic_sized_type, - is_named_tuple, - is_struct, - returns_array, -) -from ape.utils.basemodel import ( - BaseInterface, - BaseInterfaceModel, - ExtraAttributesMixin, - ExtraModelAttributes, - ManagerAccessMixin, - injected_before_use, - only_raise_attribute_error, -) -from ape.utils.misc import ( - DEFAULT_LIVE_NETWORK_BASE_FEE_MULTIPLIER, - DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT, - DEFAULT_TRANSACTION_ACCEPTANCE_TIMEOUT, - EMPTY_BYTES32, - LOCAL_NETWORK_NAME, - SOURCE_EXCLUDE_PATTERNS, - ZERO_ADDRESS, - add_padding_to_strings, - as_our_module, - cached_property, - extract_nested_value, - gas_estimation_error_message, - get_current_timestamp_ms, - get_package_version, - is_evm_precompile, - is_zero_hex, - load_config, - log_instead_of_fail, - nonreentrant, - pragma_str_to_specifier_set, - raises_not_implemented, - run_until_complete, - singledispatchmethod, - to_int, -) -from ape.utils.os import ( - clean_path, - create_tempdir, - expand_environment_variables, - extract_archive, - get_all_files_in_directory, - get_full_extension, - get_package_path, - get_relative_path, - in_tempdir, - path_match, - run_in_tempdir, - use_temp_sys_path, -) -from ape.utils.process import JoinableQueue, spawn -from ape.utils.rpc import USER_AGENT, RPCHeaders, allow_disconnected, stream_response -from ape.utils.testing import ( - DEFAULT_NUMBER_OF_TEST_ACCOUNTS, - DEFAULT_TEST_ACCOUNT_BALANCE, - DEFAULT_TEST_CHAIN_ID, - DEFAULT_TEST_HD_PATH, - DEFAULT_TEST_MNEMONIC, - GeneratedDevAccount, - generate_dev_accounts, -) -from ape.utils.trace import USER_ASSERT_TAG, TraceStyles, parse_coverage_tables, parse_gas_table +def __getattr__(name: str): + if name == "abstractmethod": + from abc import abstractmethod + + return abstractmethod + + elif name in ( + "LogInputABICollection", + "Struct", + "StructParser", + "is_array", + "is_dynamic_sized_type", + "is_named_tuple", + "is_struct", + "returns_array", + ): + import ape.utils.abi as abi_module + + return getattr(abi_module, name) + + elif name in ( + "BaseInterface", + "BaseInterfaceModel", + "ExtraAttributesMixin", + "ExtraModelAttributes", + "ManagerAccessMixin", + "injected_before_use", + "only_raise_attribute_error", + ): + import ape.utils.basemodel as basemodel_module + + return getattr(basemodel_module, name) + + elif name in ( + "DEFAULT_LIVE_NETWORK_BASE_FEE_MULTIPLIER", + "DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT", + "DEFAULT_TRANSACTION_ACCEPTANCE_TIMEOUT", + "EMPTY_BYTES32", + "LOCAL_NETWORK_NAME", + "SOURCE_EXCLUDE_PATTERNS", + "ZERO_ADDRESS", + "add_padding_to_strings", + "as_our_module", + "cached_property", + "extract_nested_value", + "gas_estimation_error_message", + "get_current_timestamp_ms", + "get_package_version", + "is_evm_precompile", + "is_zero_hex", + "load_config", + "log_instead_of_fail", + "nonreentrant", + "pragma_str_to_specifier_set", + "raises_not_implemented", + "run_until_complete", + "singledispatchmethod", + "to_int", + ): + import ape.utils.misc as misc_module + + return getattr(misc_module, name) + + elif name in ( + "clean_path", + "create_tempdir", + "expand_environment_variables", + "extract_archive", + "get_all_files_in_directory", + "get_full_extension", + "get_package_path", + "get_relative_path", + "in_tempdir", + "path_match", + "run_in_tempdir", + "use_temp_sys_path", + ): + import ape.utils.os as os_module + + return getattr(os_module, name) + + elif name in ("JoinableQueue", "spawn"): + import ape.utils.process as process_module + + return getattr(process_module, name) + + elif name in ("USER_AGENT", "RPCHeaders", "allow_disconnected", "stream_response"): + import ape.utils.rpc as rpc_module + + return getattr(rpc_module, name) + + elif name in ( + "DEFAULT_NUMBER_OF_TEST_ACCOUNTS", + "DEFAULT_TEST_ACCOUNT_BALANCE", + "DEFAULT_TEST_CHAIN_ID", + "DEFAULT_TEST_HD_PATH", + "DEFAULT_TEST_MNEMONIC", + "GeneratedDevAccount", + "generate_dev_accounts", + ): + import ape.utils.testing as testing_module + + return getattr(testing_module, name) + + elif name in ("USER_ASSERT_TAG", "TraceStyles", "parse_coverage_tables", "parse_gas_table"): + import ape.utils.trace as trace_module + + return getattr(trace_module, name) + + else: + raise AttributeError(name) + __all__ = [ "abstractmethod", diff --git a/tests/performance/test_project.py b/tests/performance/test_project.py index 870b9b0071..1060114702 100644 --- a/tests/performance/test_project.py +++ b/tests/performance/test_project.py @@ -1,4 +1,5 @@ def test_get_contract(benchmark, project_with_contracts): + _ = project_with_contracts.Other # Ensure compiled first. benchmark.pedantic( lambda *args, **kwargs: project_with_contracts.get_contract(*args, **kwargs), args=(("Other",),), From 437d32670cf610299dcdb934f912634aea867920 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 16:06:40 -0500 Subject: [PATCH 09/12] perf: Ape's pytest plugin performance improvements (#2361) --- src/ape/pytest/config.py | 13 ++++++---- src/ape/pytest/contextmanagers.py | 4 +++- src/ape/pytest/coverage.py | 12 ++++++---- src/ape/pytest/gas.py | 5 ++-- src/ape/pytest/plugin.py | 40 +++++++++++++------------------ src/ape/pytest/runners.py | 4 +++- 6 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/ape/pytest/config.py b/src/ape/pytest/config.py index 77825f338f..a60cf15fd9 100644 --- a/src/ape/pytest/config.py +++ b/src/ape/pytest/config.py @@ -1,14 +1,17 @@ from functools import cached_property from typing import TYPE_CHECKING, Any, Optional, Union -from ape.types.trace import ContractFunctionPath from ape.utils.basemodel import ManagerAccessMixin if TYPE_CHECKING: from _pytest.config import Config as PytestConfig + from ape.types.trace import ContractFunctionPath + + +def _get_config_exclusions(config) -> list["ContractFunctionPath"]: + from ape.types.trace import ContractFunctionPath -def _get_config_exclusions(config) -> list[ContractFunctionPath]: return [ ContractFunctionPath(contract_name=x.contract_name, method_name=x.method_name) for x in config.exclude @@ -74,10 +77,12 @@ def show_internal(self) -> bool: return self.pytest_config.getoption("--show-internal") @cached_property - def gas_exclusions(self) -> list[ContractFunctionPath]: + def gas_exclusions(self) -> list["ContractFunctionPath"]: """ The combination of both CLI values and config values. """ + from ape.types.trace import ContractFunctionPath + cli_value = self.pytest_config.getoption("--gas-exclude") exclusions = ( [ContractFunctionPath.from_str(item) for item in cli_value.split(",")] @@ -89,7 +94,7 @@ def gas_exclusions(self) -> list[ContractFunctionPath]: return exclusions @cached_property - def coverage_exclusions(self) -> list[ContractFunctionPath]: + def coverage_exclusions(self) -> list["ContractFunctionPath"]: return _get_config_exclusions(self.ape_test_config.coverage) def get_pytest_plugin(self, name: str) -> Optional[Any]: diff --git a/src/ape/pytest/contextmanagers.py b/src/ape/pytest/contextmanagers.py index 652ce4ba92..905d1baebb 100644 --- a/src/ape/pytest/contextmanagers.py +++ b/src/ape/pytest/contextmanagers.py @@ -4,7 +4,6 @@ from ethpm_types.abi import ErrorABI -from ape.contracts import ContractInstance from ape.exceptions import ContractLogicError, CustomError, TransactionError from ape.utils.basemodel import ManagerAccessMixin @@ -105,6 +104,9 @@ def _check_expected_message(self, exception: ContractLogicError): raise AssertionError(f"{assertion_error_prefix} but got '{actual}'.") def _check_custom_error(self, exception: Union[CustomError]): + # perf: avoid loading from contracts namespace until needed. + from ape.contracts import ContractInstance + expected_error_cls = self.expected_message if not isinstance(expected_error_cls, ErrorABI) and not isinstance( diff --git a/src/ape/pytest/coverage.py b/src/ape/pytest/coverage.py index 784a025f06..768c385492 100644 --- a/src/ape/pytest/coverage.py +++ b/src/ape/pytest/coverage.py @@ -5,7 +5,6 @@ import click from ape.logging import logger -from ape.types.coverage import CoverageProject, CoverageReport from ape.utils.basemodel import ManagerAccessMixin from ape.utils.misc import get_current_timestamp_ms from ape.utils.os import get_full_extension, get_relative_path @@ -17,6 +16,7 @@ from ape.managers.project import ProjectManager from ape.pytest.config import ConfigWrapper + from ape.types.coverage import CoverageReport from ape.types.trace import ContractFunctionPath, ControlFlow, SourceTraceback @@ -30,7 +30,7 @@ def __init__( self._sources: Union[ Iterable["ContractSource"], Callable[[], Iterable["ContractSource"]] ] = sources - self._report: Optional[CoverageReport] = None + self._report: Optional["CoverageReport"] = None @property def sources(self) -> list["ContractSource"]: @@ -45,7 +45,7 @@ def sources(self) -> list["ContractSource"]: return self._sources @property - def report(self) -> CoverageReport: + def report(self) -> "CoverageReport": if self._report is None: self._report = self._init_coverage_profile() @@ -57,7 +57,9 @@ def reset(self): def _init_coverage_profile( self, - ) -> CoverageReport: + ) -> "CoverageReport": + from ape.types.coverage import CoverageProject, CoverageReport + # source_id -> pc(s) -> times hit project_coverage = CoverageProject(name=self.project.name or "__local__") @@ -161,7 +163,7 @@ def __init__( @property def data(self) -> Optional[CoverageData]: - if not self.config_wrapper.track_coverage: + if not self.enabled: return None elif self._data is None: diff --git a/src/ape/pytest/gas.py b/src/ape/pytest/gas.py index 1f37af2b68..c9739717b3 100644 --- a/src/ape/pytest/gas.py +++ b/src/ape/pytest/gas.py @@ -2,7 +2,6 @@ from evm_trace.gas import merge_reports -from ape.types.trace import GasReport from ape.utils.basemodel import ManagerAccessMixin from ape.utils.trace import _exclude_gas, parse_gas_table @@ -13,7 +12,7 @@ from ape.api.trace import TraceAPI from ape.pytest.config import ConfigWrapper from ape.types.address import AddressType - from ape.types.trace import ContractFunctionPath + from ape.types.trace import ContractFunctionPath, GasReport class GasTracker(ManagerAccessMixin): @@ -24,7 +23,7 @@ class GasTracker(ManagerAccessMixin): def __init__(self, config_wrapper: "ConfigWrapper"): self.config_wrapper = config_wrapper - self.session_gas_report: Optional[GasReport] = None + self.session_gas_report: Optional["GasReport"] = None @property def enabled(self) -> bool: diff --git a/src/ape/pytest/plugin.py b/src/ape/pytest/plugin.py index 72d09c1809..e23dd5bf32 100644 --- a/src/ape/pytest/plugin.py +++ b/src/ape/pytest/plugin.py @@ -1,24 +1,7 @@ import sys from pathlib import Path -from typing import TYPE_CHECKING, Optional from ape.exceptions import ConfigError -from ape.pytest.config import ConfigWrapper -from ape.pytest.coverage import CoverageTracker -from ape.pytest.fixtures import PytestApeFixtures, ReceiptCapture -from ape.pytest.gas import GasTracker -from ape.pytest.runners import PytestApeRunner -from ape.utils.basemodel import ManagerAccessMixin - -if TYPE_CHECKING: - from ape.api.networks import EcosystemAPI - - -def _get_default_network(ecosystem: Optional["EcosystemAPI"] = None) -> str: - if ecosystem is None: - ecosystem = ManagerAccessMixin.network_manager.default_ecosystem - - return ecosystem.name def pytest_addoption(parser): @@ -40,7 +23,6 @@ def add_option(*names, **kwargs): add_option( "--network", action="store", - default=_get_default_network(), help="Override the default network and provider (see ``ape networks list`` for options).", ) add_option( @@ -64,7 +46,7 @@ def add_option(*names, **kwargs): action="store", help="A comma-separated list of contract:method-name glob-patterns to ignore.", ) - parser.addoption("--coverage", action="store_true", help="Collect contract coverage.") + add_option("--coverage", action="store_true", help="Collect contract coverage.") # NOTE: Other pytest plugins, such as hypothesis, should integrate with pytest separately @@ -86,17 +68,27 @@ def is_module(v): except AttributeError: pass - config_wrapper = ConfigWrapper(config) - receipt_capture = ReceiptCapture(config_wrapper) - gas_tracker = GasTracker(config_wrapper) - coverage_tracker = CoverageTracker(config_wrapper) - if not config.option.verbose: # Enable verbose output if stdout capture is disabled config.option.verbose = config.getoption("capture") == "no" # else: user has already changes verbosity to an equal or higher level; avoid downgrading. + if "--help" in config.invocation_params.args: + # perf: Don't bother setting up runner if only showing help. + return + + from ape.pytest.config import ConfigWrapper + from ape.pytest.coverage import CoverageTracker + from ape.pytest.fixtures import PytestApeFixtures, ReceiptCapture + from ape.pytest.gas import GasTracker + from ape.pytest.runners import PytestApeRunner + from ape.utils.basemodel import ManagerAccessMixin + # Register the custom Ape test runner + config_wrapper = ConfigWrapper(config) + receipt_capture = ReceiptCapture(config_wrapper) + gas_tracker = GasTracker(config_wrapper) + coverage_tracker = CoverageTracker(config_wrapper) runner = PytestApeRunner(config_wrapper, receipt_capture, gas_tracker, coverage_tracker) config.pluginmanager.register(runner, "ape-test") diff --git a/src/ape/pytest/runners.py b/src/ape/pytest/runners.py index 6ddac468d8..a6528c53cb 100644 --- a/src/ape/pytest/runners.py +++ b/src/ape/pytest/runners.py @@ -9,7 +9,6 @@ from ape.exceptions import ConfigError from ape.logging import LogLevel from ape.utils.basemodel import ManagerAccessMixin -from ape_console._cli import console if TYPE_CHECKING: from ape.api.networks import ProviderContextManager @@ -90,6 +89,8 @@ def pytest_exception_interact(self, report, call): ) if self.config_wrapper.interactive and report.failed: + from ape_console._cli import console + traceback = call.excinfo.traceback[-1] # Suspend capsys to ignore our own output. @@ -124,6 +125,7 @@ def pytest_exception_interact(self, report, call): click.echo("Starting interactive mode. Type `exit` to halt current test.") namespace = {"_callinfo": call, **globals_dict, **locals_dict} + console(extra_locals=namespace, project=self.local_project, embed=True) if capman: From 0334b4a0ad37d088a50f5ed82b72fd89c9540cd8 Mon Sep 17 00:00:00 2001 From: antazoey Date: Thu, 31 Oct 2024 17:22:12 -0500 Subject: [PATCH 10/12] perf: ape compile --help wasnt fast (#2362) --- src/ape/cli/options.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ape/cli/options.py b/src/ape/cli/options.py index b2d7829440..88c715c040 100644 --- a/src/ape/cli/options.py +++ b/src/ape/cli/options.py @@ -1,4 +1,5 @@ import inspect +import sys from collections.abc import Callable from functools import partial from pathlib import Path @@ -528,6 +529,11 @@ def handle_parse_result(self, ctx, opts, args): def _project_callback(ctx, param, val): + if "--help" in sys.argv or "-h" in sys.argv: + # Perf: project option is eager; have to check sys.argv to + # know to exit early when only doing --help. + return + from ape.utils.basemodel import ManagerAccessMixin pm = None From aa1c02c96702639fc6405dc0cf64e649c9ade5b9 Mon Sep 17 00:00:00 2001 From: antazoey Date: Fri, 1 Nov 2024 11:40:51 -0500 Subject: [PATCH 11/12] fix: `ape plugins list` bad auth and unnecessary requests (#2365) --- src/ape/utils/_github.py | 32 +++++++++++++-- src/ape_plugins/_cli.py | 3 +- tests/functional/utils/test_github.py | 59 ++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/ape/utils/_github.py b/src/ape/utils/_github.py index b5da772cc4..0d23091551 100644 --- a/src/ape/utils/_github.py +++ b/src/ape/utils/_github.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any, Optional, Union -from requests import Session +from requests import HTTPError, Session from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry @@ -224,8 +224,34 @@ def _get(self, url: str, params: Optional[dict] = None) -> Any: def _request(self, method: str, url: str, **kwargs) -> Any: url = f"{self.API_URL_PREFIX}/{url}" response = self.__session.request(method, url, **kwargs) - response.raise_for_status() - return response.json() + + try: + response.raise_for_status() + except HTTPError as err: + if err.response.status_code == 401 and self.__session.headers.get("Authorization"): + token = self.__session.headers["Authorization"] + del self.__session.headers["Authorization"] + response = self.__session.request(method, url, **kwargs) + try: + response.raise_for_status() # Raise exception if the retry also fails + except HTTPError: + # Even without the Authorization token, the request still failed. + # Raise the original error in this case. Also, put back token just in case. + self.__session.headers["Authorization"] = token + raise err + else: + # The request failed with Authorization but succeeded without. + # Let the user know their token is likely expired. + logger.warning( + "Requests are not authorized! GITHUB_ACCESS_TOKEN is likely expired; " + "received 401 when attempted to use it. If you need GitHub authorization, " + "try resetting your token." + ) + return response.json() + + else: + # Successful response status code! + return response.json() github_client = _GithubClient() diff --git a/src/ape_plugins/_cli.py b/src/ape_plugins/_cli.py index b4c27b1a81..ecf5a80937 100644 --- a/src/ape_plugins/_cli.py +++ b/src/ape_plugins/_cli.py @@ -112,7 +112,8 @@ def _display_all_callback(ctx, param, value): help="Display all plugins installed and available (including Core)", ) def _list(cli_ctx, to_display): - metadata = PluginMetadataList.load(cli_ctx.plugin_manager) + include_available = PluginType.AVAILABLE in to_display + metadata = PluginMetadataList.load(cli_ctx.plugin_manager, include_available=include_available) if output := metadata.to_str(include=to_display): click.echo(output) if not metadata.installed and not metadata.third_party: diff --git a/tests/functional/utils/test_github.py b/tests/functional/utils/test_github.py index 4418a5f72a..206a2cf5f2 100644 --- a/tests/functional/utils/test_github.py +++ b/tests/functional/utils/test_github.py @@ -1,7 +1,7 @@ from pathlib import Path import pytest -from requests.exceptions import ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError from ape.utils._github import _GithubClient from ape.utils.os import create_tempdir @@ -98,3 +98,60 @@ def test_get_org_repos(self, github_client, mock_session): params = call.kwargs["params"] # Show we are fetching more than the default 30 per page. assert params == {"per_page": 100, "page": 1} + + def test_available_plugins(self, mocker, github_client, mock_session): + response1 = mocker.MagicMock() + response1.json.return_value = [{"name": "ape-myplugin"}] + response2 = mocker.MagicMock() + response2.json.return_value = [] + + def get_org_repos(method, url, **kwargs): + if kwargs["params"]["page"] == 1: + return response1 + else: + # End. + return response2 + + mock_session.request.side_effect = get_org_repos + actual = github_client.available_plugins + assert actual == {"ape_myplugin"} + + def test_available_plugins_handles_401(self, mocker, github_client, mock_session, ape_caplog): + """ + When you get a 401 from using a token, Ape's GitHub client should not + only warn the user but retry the request w/o authorization, as it likely + will still work. + """ + mock_session.headers = {"Authorization": "token mytoken"} + + response1 = mocker.MagicMock() + response1.json.return_value = [{"name": "ape-myplugin"}] + response2 = mocker.MagicMock() + response2.json.return_value = [] + + bad_auth_response = mocker.MagicMock() + bad_auth_response.status_code = 401 + bad_auth_response.raise_for_status.side_effect = HTTPError(response=bad_auth_response) + + def get_org_repos(method, url, **kwargs): + if mock_session.headers.get("Authorization") == "token mytoken": + return bad_auth_response + elif kwargs["params"]["page"] == 1: + return response1 + else: + # End. + return response2 + + mock_session.request.side_effect = get_org_repos + actual = github_client.available_plugins + + # Still works, even with bad auth. + assert actual == {"ape_myplugin"} + + # Show we got our log message. + expected = ( + "Requests are not authorized! GITHUB_ACCESS_TOKEN is likely " + "expired; received 401 when attempted to use it. If you need " + "GitHub authorization, try resetting your token." + ) + assert ape_caplog.head == expected From ffec552bd87e8219d4aecbd3576631053d65a9e8 Mon Sep 17 00:00:00 2001 From: Juliya Smith Date: Fri, 1 Nov 2024 16:31:04 -0500 Subject: [PATCH 12/12] feat: upgrade web3 --- src/ape_accounts/accounts.py | 2 +- src/ape_ethereum/provider.py | 10 +++------- src/ape_node/provider.py | 2 +- src/ape_test/accounts.py | 2 +- tests/functional/geth/test_provider.py | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/ape_accounts/accounts.py b/src/ape_accounts/accounts.py index 7dd0bae941..47d4949e9b 100644 --- a/src/ape_accounts/accounts.py +++ b/src/ape_accounts/accounts.py @@ -255,7 +255,7 @@ def sign_raw_msghash(self, msghash: HexBytes) -> Optional[MessageSignature]: # Also, we have already warned the user about the safety. with warnings.catch_warnings(): warnings.simplefilter("ignore") - signed_msg = EthAccount.signHash(msghash, self.__key) + signed_msg = EthAccount.unsafe_sign_hash(msghash, self.__key) return MessageSignature( v=signed_msg.v, diff --git a/src/ape_ethereum/provider.py b/src/ape_ethereum/provider.py index c1ff49e704..ab8f13cb33 100644 --- a/src/ape_ethereum/provider.py +++ b/src/ape_ethereum/provider.py @@ -19,9 +19,7 @@ from evmchains import get_random_rpc from pydantic.dataclasses import dataclass from requests import HTTPError -from web3 import HTTPProvider, IPCProvider, Web3 -from web3 import WebsocketProvider as WebSocketProvider -from web3._utils.http import construct_user_agent +from web3 import HTTPProvider, IPCProvider, Web3, WebSocketProvider from web3.exceptions import ContractLogicError as Web3ContractLogicError from web3.exceptions import ( ExtraDataLengthError, @@ -30,7 +28,7 @@ TransactionNotFound, ) from web3.gas_strategies.rpc import rpc_gas_price_strategy -from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware +from web3.middleware import ExtraDataToPOAMiddleware from web3.middleware.validation import MAX_EXTRADATA_LENGTH from web3.providers import AutoProvider from web3.providers.auto import load_provider_from_environment @@ -1326,9 +1324,7 @@ class EthereumNodeProvider(Web3Provider, ABC): name: str = "node" # NOTE: Appends user-agent to base User-Agent string. - request_header: dict = { - "User-Agent": construct_user_agent(str(HTTPProvider)), - } + request_header: dict = {} @property def uri(self) -> str: diff --git a/src/ape_node/provider.py b/src/ape_node/provider.py index 95bd54d2b7..278d8ca1f5 100644 --- a/src/ape_node/provider.py +++ b/src/ape_node/provider.py @@ -12,7 +12,7 @@ from pydantic import field_validator from pydantic_settings import SettingsConfigDict from requests.exceptions import ConnectionError -from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware +from web3.middleware import ExtraDataToPOAMiddleware from yarl import URL from ape.api.config import PluginConfig diff --git a/src/ape_test/accounts.py b/src/ape_test/accounts.py index 43c46a29fd..c2490d34bc 100644 --- a/src/ape_test/accounts.py +++ b/src/ape_test/accounts.py @@ -166,7 +166,7 @@ def sign_transaction( def sign_raw_msghash(self, msghash: HexBytes) -> MessageSignature: with warnings.catch_warnings(): warnings.simplefilter("ignore") - signed_msg = EthAccount.signHash(msghash, self.private_key) + signed_msg = EthAccount.unsafe_sign_hash(msghash, self.private_key) return MessageSignature( v=signed_msg.v, diff --git a/tests/functional/geth/test_provider.py b/tests/functional/geth/test_provider.py index 57cb676451..da0dc054ff 100644 --- a/tests/functional/geth/test_provider.py +++ b/tests/functional/geth/test_provider.py @@ -10,7 +10,7 @@ from web3 import AutoProvider, Web3 from web3.exceptions import ContractLogicError as Web3ContractLogicError from web3.exceptions import ExtraDataLengthError -from web3.middleware import geth_poa_middleware as ExtraDataToPOAMiddleware +from web3.middleware import ExtraDataToPOAMiddleware from web3.providers import HTTPProvider from ape.exceptions import (