Skip to content

Commit

Permalink
perf: more things that make ape --help way faster (#2351)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 29, 2024
1 parent 4199cac commit 46a1d2c
Show file tree
Hide file tree
Showing 29 changed files with 398 additions and 246 deletions.
33 changes: 17 additions & 16 deletions src/ape/_cli.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import difflib
import re
import sys
import warnings
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 Any, Optional
from warnings import catch_warnings, simplefilter

import click
import rich
Expand All @@ -17,7 +18,6 @@
from ape.cli.options import ape_cli_context
from ape.exceptions import Abort, ApeException, ConfigError, handle_ape_exception
from ape.logging import logger
from ape.utils.basemodel import ManagerAccessMixin as access

_DIFFLIB_CUT_OFF = 0.6

Expand All @@ -27,6 +27,8 @@ def display_config(ctx, param, value):
if not value or ctx.resilient_parsing:
return

from ape.utils.basemodel import ManagerAccessMixin as access

click.echo("# Current configuration")

# NOTE: Using json-mode as yaml.dump requires JSON-like structure.
Expand All @@ -37,6 +39,8 @@ def display_config(ctx, param, value):


def _validate_config():
from ape.utils.basemodel import ManagerAccessMixin as access

project = access.local_project
try:
_ = project.config
Expand All @@ -47,7 +51,6 @@ def _validate_config():


class ApeCLI(click.MultiCommand):
_commands: Optional[dict] = None
_CLI_GROUP_NAME = "ape_cli_subcommands"

def parse_args(self, ctx: Context, args: list[str]) -> list[str]:
Expand All @@ -60,6 +63,8 @@ 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.utils.basemodel import ManagerAccessMixin as access

commands = []
for subcommand in self.list_commands(ctx):
cmd = self.get_command(ctx, subcommand)
Expand Down Expand Up @@ -142,25 +147,21 @@ def _suggest_cmd(usage_error):

raise usage_error

@property
@cached_property
def commands(self) -> dict:
if self._commands:
return self._commands

_entry_points = entry_points()
eps: Iterable
if select_fn := getattr(_entry_points, "select", None):
# NOTE: Using getattr because mypy.
eps = select_fn(group=self._CLI_GROUP_NAME)
else:
# Python 3.9. Can remove once we drop support.
with warnings.catch_warnings():
warnings.simplefilter("ignore")

try:
eps = _entry_points.select(group=self._CLI_GROUP_NAME)
except AttributeError:
# Fallback for Python 3.9
with catch_warnings():
simplefilter("ignore")
eps = _entry_points.get(self._CLI_GROUP_NAME, []) # type: ignore

commands = {cmd.name.replace("_", "-").replace("ape-", ""): cmd.load for cmd in eps}
self._commands = {k: commands[k] for k in sorted(commands)}
return self._commands
return dict(sorted(commands.items()))

def list_commands(self, ctx) -> list[str]:
return [k for k in self.commands]
Expand Down
2 changes: 2 additions & 0 deletions src/ape/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ape.cli.choices import (
AccountAliasPromptChoice,
Alias,
LazyChoice,
NetworkChoice,
OutputFormat,
PromptChoice,
Expand Down Expand Up @@ -42,6 +43,7 @@
"existing_alias_argument",
"incompatible_with",
"JSON",
"LazyChoice",
"network_option",
"NetworkChoice",
"NetworkOption",
Expand Down
21 changes: 14 additions & 7 deletions src/ape/cli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

from ape.cli.choices import _ACCOUNT_TYPE_FILTER, Alias
from ape.logging import logger
from ape.utils.basemodel import ManagerAccessMixin
from ape.utils.os import get_full_extension
from ape.utils.validators import _validate_account_alias

if TYPE_CHECKING:
from ape.managers.project import ProjectManager


def _alias_callback(ctx, param, value):
from ape.utils.validators import _validate_account_alias

return _validate_account_alias(value)


Expand All @@ -28,7 +27,6 @@ def existing_alias_argument(account_type: _ACCOUNT_TYPE_FILTER = None, **kwargs)
If given, limits the type of account the user may choose from.
**kwargs: click.argument overrides.
"""

type_ = kwargs.pop("type", Alias(key=account_type))
return click.argument("alias", type=type_, **kwargs)

Expand All @@ -45,12 +43,14 @@ def non_existing_alias_argument(**kwargs):
return click.argument("alias", callback=callback, **kwargs)


class _ContractPaths(ManagerAccessMixin):
class _ContractPaths:
"""
Helper callback class for handling CLI-given contract paths.
"""

def __init__(self, value, project: Optional["ProjectManager"] = None):
from ape.utils.basemodel import ManagerAccessMixin

self.value = value
self.missing_compilers: set[str] = set() # set of .ext
self.project = project or ManagerAccessMixin.local_project
Expand Down Expand Up @@ -105,14 +105,21 @@ def filtered_paths(self) -> set[Path]:

@property
def exclude_patterns(self) -> set[str]:
return self.config_manager.get_config("compile").exclude or set()
from ape.utils.basemodel import ManagerAccessMixin as access

return access.config_manager.get_config("compile").exclude or set()

def do_exclude(self, path: Union[Path, str]) -> bool:
return self.project.sources.is_excluded(path)

def compiler_is_unknown(self, path: Union[Path, str]) -> bool:
from ape.utils.basemodel import ManagerAccessMixin
from ape.utils.os import get_full_extension

ext = get_full_extension(path)
unknown_compiler = ext and ext not in self.compiler_manager.registered_compilers
unknown_compiler = (
ext and ext not in ManagerAccessMixin.compiler_manager.registered_compilers
)
if unknown_compiler and ext not in self.missing_compilers:
self.missing_compilers.add(ext)

Expand Down
57 changes: 47 additions & 10 deletions src/ape/cli/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
NetworkNotFoundError,
ProviderNotFoundError,
)
from ape.utils.basemodel import ManagerAccessMixin as access

if TYPE_CHECKING:
from ape.api.accounts import AccountAPI
Expand All @@ -26,6 +25,8 @@


def _get_accounts(key: _ACCOUNT_TYPE_FILTER) -> list["AccountAPI"]:
from ape.utils.basemodel import ManagerAccessMixin as access

accounts = access.account_manager

add_test_accounts = False
Expand Down Expand Up @@ -68,8 +69,11 @@ def __init__(self, key: _ACCOUNT_TYPE_FILTER = None):
# NOTE: we purposely skip the constructor of `Choice`
self.case_sensitive = False
self._key_filter = key

@cached_property
def choices(self) -> Sequence: # type: ignore[override]
module = import_module("ape.types.basic")
self.choices = module._LazySequence(self._choices_iterator)
return module._LazySequence(self._choices_iterator)

@property
def _choices_iterator(self) -> Iterator[str]:
Expand Down Expand Up @@ -206,6 +210,8 @@ def convert(
else:
alias = value

from ape.utils.basemodel import ManagerAccessMixin as access

accounts = access.account_manager
if isinstance(alias, str) and alias.upper().startswith("TEST::"):
idx_str = alias.upper().replace("TEST::", "")
Expand Down Expand Up @@ -235,6 +241,8 @@ def print_choices(self):
click.echo(f"{idx}. {choice}")
did_print = True

from ape.utils.basemodel import ManagerAccessMixin as access

accounts = access.account_manager
len_test_accounts = len(accounts.test_accounts) - 1
if len_test_accounts > 0:
Expand All @@ -261,6 +269,7 @@ def select_account(self) -> "AccountAPI":
Returns:
:class:`~ape.api.accounts.AccountAPI`
"""
from ape.utils.basemodel import ManagerAccessMixin as access

accounts = access.account_manager
if not self.choices or len(self.choices) == 0:
Expand Down Expand Up @@ -348,19 +357,29 @@ def __init__(
base_type: Optional[type] = None,
callback: Optional[Callable] = None,
):
provider_module = import_module("ape.api.providers")
base_type = provider_module.ProviderAPI if base_type is None else base_type
if not issubclass(base_type, (provider_module.ProviderAPI, str)):
raise TypeError(f"Unhandled type '{base_type}' for NetworkChoice.")

self.base_type = base_type
self._base_type = base_type
self.callback = callback
self.case_sensitive = case_sensitive
self.ecosystem = ecosystem
self.network = network
self.provider = provider
# NOTE: Purposely avoid super().init for performance reasons.

@property
def base_type(self) -> type["ProviderAPI"]:
# perf: property exists to delay import ProviderAPI at init time.
from ape.api.providers import ProviderAPI

if self._base_type is not None:
return self._base_type

self._base_type = ProviderAPI
return ProviderAPI

@base_type.setter
def base_type(self, value):
self._base_type = value

@cached_property
def choices(self) -> Sequence[Any]: # type: ignore[override]
return get_networks(ecosystem=self.ecosystem, network=self.network, provider=self.provider)
Expand All @@ -369,6 +388,8 @@ 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:
Expand Down Expand Up @@ -406,8 +427,9 @@ def convert(self, value: Any, param: Optional[Parameter], ctx: Optional[Context]
) from err

if choice not in (None, _NONE_NETWORK) and isinstance(choice, str):
provider_module = import_module("ape.api.providers")
if issubclass(self.base_type, provider_module.ProviderAPI):
from ape.api.providers import ProviderAPI

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

Expand Down Expand Up @@ -454,3 +476,18 @@ def output_format_choice(options: Optional[list[OutputFormat]] = None) -> Choice

# Uses `str` form of enum for CLI choices.
return click.Choice([o.value for o in options], case_sensitive=False)


class LazyChoice(Choice):
"""
A simple lazy-choice where choices are evaluated lazily.
"""

def __init__(self, get_choices: Callable[[], Sequence[str]], case_sensitive: bool = False):
self._get_choices = get_choices
self.case_sensitive = case_sensitive
# Note: Purposely avoid super init.

@cached_property
def choices(self) -> Sequence[str]: # type: ignore[override]
return self._get_choices()
3 changes: 2 additions & 1 deletion src/ape/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

from ape.cli.choices import _NONE_NETWORK, NetworkChoice
from ape.exceptions import NetworkError
from ape.utils.basemodel import ManagerAccessMixin as access

if TYPE_CHECKING:
from ape.api.networks import ProviderContextManager
Expand All @@ -26,6 +25,8 @@ def get_param_from_ctx(ctx: Context, param: str) -> Optional[Any]:


def parse_network(ctx: Context) -> Optional["ProviderContextManager"]:
from ape.utils.basemodel import ManagerAccessMixin as access

interactive = get_param_from_ctx(ctx, "interactive")

# Handle if already parsed (as when using network-option)
Expand Down
Loading

0 comments on commit 46a1d2c

Please sign in to comment.