Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: getting ape console to launch faster #2379

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 16 additions & 67 deletions src/ape/cli/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
from typing import TYPE_CHECKING, Any, Optional, Union

import click
from click import BadParameter, Choice, Context, Parameter
from click import Choice, Context, Parameter

from ape.exceptions import (
AccountsError,
EcosystemNotFoundError,
NetworkNotFoundError,
ProviderNotFoundError,
)
from ape.exceptions import AccountsError

if TYPE_CHECKING:
from ape.api.accounts import AccountAPI
Expand Down Expand Up @@ -360,7 +355,7 @@ def __init__(
ecosystem: _NETWORK_FILTER = None,
network: _NETWORK_FILTER = None,
provider: _NETWORK_FILTER = None,
base_type: Optional[type] = None,
base_type: Optional[Union[type, str]] = None,
callback: Optional[Callable] = None,
):
self._base_type = base_type
Expand All @@ -372,15 +367,14 @@ def __init__(
# NOTE: Purposely avoid super().init for performance reasons.

@property
def base_type(self) -> type["ProviderAPI"]:
def base_type(self) -> Union[type["ProviderAPI"], str]:
if self._base_type is not None:
return self._base_type

# perf: property exists to delay import ProviderAPI at init time.
from ape.api.providers import ProviderAPI

self._base_type = ProviderAPI
return ProviderAPI
# perf: Keep base-type as a forward-ref when only using the default.
# so things load faster.
self._base_type = "ProviderAPI"
return self._base_type

@base_type.setter
def base_type(self, value):
Expand All @@ -394,62 +388,17 @@ def get_metavar(self, param):
return "[ecosystem-name][:[network-name][:[provider-name]]]"

def convert(self, value: Any, param: Optional[Parameter], ctx: Optional[Context]) -> Any:
from ape.utils.basemodel import ManagerAccessMixin as access

choice: Optional[Union[str, "ProviderAPI"]]
networks = access.network_manager
if not value:
choice = None
if not value or value.lower() in ("none", "null"):
return self.callback(ctx, param, _NONE_NETWORK) if self.callback else _NONE_NETWORK

elif value.lower() in ("none", "null"):
choice = _NONE_NETWORK
if self.base_type == "ProviderAPI" or isinstance(self.base_type, type):
# Return the provider.
from ape.utils.basemodel import ManagerAccessMixin as access

elif self.is_custom_value(value):
# By-pass choice constraints when using custom network.
choice = value
networks = access.network_manager
value = networks.get_provider_from_choice(network_choice=value)

else:
# Regular conditions.
try:
# Validate result.
choice = super().convert(value, param, ctx)
except BadParameter:
# Attempt to get the provider anyway.
# Sometimes, depending on the provider, it'll still work.
# (as-is the case for custom-forked networks).
try:
choice = networks.get_provider_from_choice(network_choice=value)

except (EcosystemNotFoundError, NetworkNotFoundError, ProviderNotFoundError) as err:
# This error makes more sense, as it has attempted parsing.
# Show this message as the BadParameter message.
raise click.BadParameter(str(err)) from err

except Exception as err:
# If an error was not raised for some reason, raise a simpler error.
# NOTE: Still avoid showing the massive network options list.
raise click.BadParameter(
"Invalid network choice. Use `ape networks list` to see options."
) from err

if choice not in (None, _NONE_NETWORK) and isinstance(choice, str):
from ape.api.providers import ProviderAPI

if issubclass(self.base_type, ProviderAPI):
# Return the provider.
choice = networks.get_provider_from_choice(network_choice=value)

return self.callback(ctx, param, choice) if self.callback else choice

@classmethod
def is_custom_value(cls, value) -> bool:
return (
value is not None
and isinstance(value, str)
and cls.CUSTOM_NETWORK_PATTERN.match(value) is not None
or str(value).startswith("http://")
or str(value).startswith("https://")
)
return self.callback(ctx, param, value) if self.callback else value


class OutputFormat(Enum):
Expand Down
8 changes: 7 additions & 1 deletion src/ape/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,10 @@ def handle_parse_result(self, ctx, opts, args):
return IncompatibleOption


def _project_path_callback(ctx, param, val):
return Path(val) if val else Path.cwd()


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
Expand Down Expand Up @@ -560,10 +564,12 @@ def _project_callback(ctx, param, val):


def project_option(**kwargs):
_type = kwargs.pop("type", None)
callback = _project_path_callback if issubclass(_type, Path) else _project_callback
return click.option(
"--project",
help="The path to a local project or manifest",
callback=_project_callback,
callback=callback,
metavar="PATH",
is_eager=True,
**kwargs,
Expand Down
108 changes: 64 additions & 44 deletions src/ape/managers/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from evmchains import PUBLIC_CHAIN_META

from ape.api.networks import EcosystemAPI, NetworkAPI, ProviderContextManager
from ape.api.networks import ProviderContextManager
from ape.exceptions import EcosystemNotFoundError, NetworkError, NetworkNotFoundError
from ape.managers.base import BaseManager
from ape.utils.basemodel import (
Expand All @@ -17,6 +17,7 @@
from ape_ethereum.provider import EthereumNodeProvider

if TYPE_CHECKING:
from ape.api.networks import EcosystemAPI, NetworkAPI
from ape.api.providers import ProviderAPI
from ape.utils.rpc import RPCHeaders

Expand Down Expand Up @@ -62,7 +63,7 @@ def active_provider(self, new_value: "ProviderAPI"):
self._active_provider = new_value

@property
def network(self) -> NetworkAPI:
def network(self) -> "NetworkAPI":
"""
The current network if connected to one.

Expand All @@ -76,7 +77,7 @@ def network(self) -> NetworkAPI:
return self.provider.network

@property
def ecosystem(self) -> EcosystemAPI:
def ecosystem(self) -> "EcosystemAPI":
"""
The current ecosystem if connected to one.

Expand Down Expand Up @@ -194,28 +195,38 @@ def custom_networks(self) -> list[dict]:
Custom network data defined in various ape-config files
or added adhoc to the network manager.
"""
return [*self._custom_networks_from_config, *self._custom_networks]

@cached_property
def _custom_networks_from_config(self) -> list[dict]:
return [
*[
n.model_dump(by_alias=True)
for n in self.config_manager.get_config("networks").get("custom", [])
],
*self._custom_networks,
n.model_dump(by_alias=True)
for n in self.config_manager.get_config("networks").get("custom", [])
]

@property
def ecosystems(self) -> dict[str, EcosystemAPI]:
def ecosystems(self) -> dict[str, "EcosystemAPI"]:
"""
All the registered ecosystems in ``ape``, such as ``ethereum``.
"""
plugin_ecosystems = self._plugin_ecosystems
custom_ecosystems = self._custom_ecosystems
return {**custom_ecosystems, **plugin_ecosystems}

@cached_property
def _plugin_ecosystems(self) -> dict[str, "EcosystemAPI"]:
# Load plugins.
plugins = self.plugin_manager.ecosystems
return {n: cls(name=n) for n, cls in plugins} # type: ignore[operator]

# Load config-based custom ecosystems.
# NOTE: Non-local projects will automatically add their custom networks
# to `self.custom_networks`.
@cached_property
def _custom_ecosystems(self) -> dict[str, "EcosystemAPI"]:
custom_networks: list = self.custom_networks
plugin_ecosystems = self._plugin_ecosystems
custom_ecosystems: dict[str, "EcosystemAPI"] = {}
for custom_network in custom_networks:
ecosystem_name = custom_network["ecosystem"]
if ecosystem_name in plugin_ecosystems:
if ecosystem_name in plugin_ecosystems or ecosystem_name in custom_ecosystems:
# Already included in a prior network.
continue

Expand All @@ -239,15 +250,9 @@ def ecosystems(self) -> dict[str, EcosystemAPI]:
update={"name": ecosystem_name},
cache_clear=("_networks_from_plugins", "_networks_from_evmchains"),
)
plugin_ecosystems[ecosystem_name] = ecosystem_cls
custom_ecosystems[ecosystem_name] = ecosystem_cls

return plugin_ecosystems

@cached_property
def _plugin_ecosystems(self) -> dict[str, EcosystemAPI]:
# Load plugins.
plugins = self.plugin_manager.ecosystems
return {n: cls(name=n) for n, cls in plugins} # type: ignore[operator]
return custom_ecosystems

def create_custom_provider(
self,
Expand Down Expand Up @@ -323,7 +328,7 @@ def __ape_extra_attributes__(self) -> Iterator[ExtraModelAttributes]:
)

@only_raise_attribute_error
def __getattr__(self, attr_name: str) -> EcosystemAPI:
def __getattr__(self, attr_name: str) -> "EcosystemAPI":
"""
Get an ecosystem via ``.`` access.

Expand Down Expand Up @@ -424,7 +429,7 @@ def get_network_choices(
if ecosystem_has_providers:
yield ecosystem_name

def get_ecosystem(self, ecosystem_name: str) -> EcosystemAPI:
def get_ecosystem(self, ecosystem_name: str) -> "EcosystemAPI":
"""
Get the ecosystem for the given name.

Expand All @@ -437,30 +442,45 @@ def get_ecosystem(self, ecosystem_name: str) -> EcosystemAPI:
Returns:
:class:`~ape.api.networks.EcosystemAPI`
"""
# NOTE: This method purposely avoids "just checking self.ecosystems"
# for performance reasons and exiting the search as early as possible.
ecosystem_name = ecosystem_name.lower().replace(" ", "-")
try:
return self._plugin_ecosystems[ecosystem_name]
except KeyError:
pass

if ecosystem_name in self.ecosystem_names:
return self.ecosystems[ecosystem_name]
# Check if custom.
try:
return self._custom_ecosystems[ecosystem_name]
except KeyError:
pass

elif ecosystem_name.lower().replace(" ", "-") in PUBLIC_CHAIN_META:
ecosystem_name = ecosystem_name.lower().replace(" ", "-")
symbol = None
for net in PUBLIC_CHAIN_META[ecosystem_name].values():
if not (native_currency := net.get("nativeCurrency")):
continue
if ecosystem := self._get_ecosystem_from_evmchains(ecosystem_name):
return ecosystem

if "symbol" not in native_currency:
continue
raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names)

symbol = native_currency["symbol"]
break
def _get_ecosystem_from_evmchains(self, ecosystem_name: str) -> Optional["EcosystemAPI"]:
if ecosystem_name not in PUBLIC_CHAIN_META:
return None

symbol = symbol or "ETH"
symbol = None
for net in PUBLIC_CHAIN_META[ecosystem_name].values():
if not (native_currency := net.get("nativeCurrency")):
continue

# Is an EVM chain, can automatically make a class using evm-chains.
evm_class = self._plugin_ecosystems["ethereum"].__class__
return evm_class(name=ecosystem_name, fee_token_symbol=symbol)
if "symbol" not in native_currency:
continue

raise EcosystemNotFoundError(ecosystem_name, options=self.ecosystem_names)
symbol = native_currency["symbol"]
break

symbol = symbol or "ETH"

# Is an EVM chain, can automatically make a class using evm-chains.
evm_class = self._plugin_ecosystems["ethereum"].__class__
return evm_class(name=ecosystem_name, fee_token_symbol=symbol)

def get_provider_from_choice(
self,
Expand Down Expand Up @@ -582,18 +602,18 @@ def default_ecosystem_name(self) -> str:
if name := self._default_ecosystem_name:
return name

return self.config_manager.default_ecosystem or "ethereum"
return self.local_project.config.default_ecosystem or "ethereum"

@property
def default_ecosystem(self) -> EcosystemAPI:
@cached_property
def default_ecosystem(self) -> "EcosystemAPI":
"""
The default ecosystem. Call
:meth:`~ape.managers.networks.NetworkManager.set_default_ecosystem` to
change the default ecosystem. If a default is not set and there is
only a single ecosystem installed, such as Ethereum, then get
that ecosystem.
"""
return self.ecosystems[self.default_ecosystem_name]
return self.get_ecosystem(self.default_ecosystem_name)

def set_default_ecosystem(self, ecosystem_name: str):
"""
Expand Down
7 changes: 3 additions & 4 deletions src/ape/managers/plugins.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Generator, Iterable, Iterator
from functools import cached_property
from importlib import import_module
from itertools import chain
from typing import Any, Optional

from ape.exceptions import ApeAttributeError
Expand Down Expand Up @@ -123,10 +124,8 @@ def _register_plugins(self):
if self.__registered:
return

plugins = list({n.replace("-", "_") for n in get_plugin_dists()})
plugin_modules = tuple([*plugins, *CORE_PLUGINS])

for module_name in plugin_modules:
plugins = ({n.replace("-", "_") for n in get_plugin_dists()})
for module_name in chain(plugins, iter(CORE_PLUGINS)):
try:
module = import_module(module_name)
pluggy_manager.register(module)
Expand Down
8 changes: 4 additions & 4 deletions src/ape/managers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2281,10 +2281,10 @@ def project_api(self) -> ProjectAPI:
return default_project

# ape-config.yaml does no exist. Check for another ProjectAPI type.
project_classes: list[type[ProjectAPI]] = [
t[1] for t in list(self.plugin_manager.projects) # type: ignore
]
plugins = [t for t in project_classes if not issubclass(t, ApeProject)]
project_classes: Iterator[type[ProjectAPI]] = (
t[1] for t in self.plugin_manager.projects # type: ignore
)
plugins = (t for t in project_classes if not issubclass(t, ApeProject))
for api in plugins:
if instance := api.attempt_validate(path=self._base_path):
return instance
Expand Down
Loading
Loading