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: make ape --help faster #2333

Merged
merged 6 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repos:
additional_dependencies: [flake8-breakpoint, flake8-print, flake8-pydantic]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: [
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
],
"lint": [
"black>=24.10.0,<25", # Auto-formatter and linter
"mypy>=1.11.2,<2", # Static type analyzer
"mypy>=1.13.0,<2", # Static type analyzer
"types-PyYAML", # Needed due to mypy typeshed
"types-requests", # Needed due to mypy typeshed
"types-setuptools", # Needed due to mypy typeshed
Expand Down
78 changes: 33 additions & 45 deletions src/ape/__init__.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,13 @@
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 ape.managers.project import ProjectManager as Project
from ape.pytest.contextmanagers import RevertsContextManager
from ape.utils import ManagerAccessMixin as _ManagerAccessMixin

# Wiring together the application

config = _ManagerAccessMixin.config_manager
"""
The active configs for the current project. See :class:`ape.managers.config.ConfigManager`.
"""

# Main types we export for the user
compilers = _ManagerAccessMixin.compiler_manager
"""Manages compilers for the current project. See
:class:`ape.managers.compilers.CompilerManager`."""

networks = _ManagerAccessMixin.network_manager
"""Manages the networks for the current project. See
:class:`ape.managers.networks.NetworkManager`."""

chain = _ManagerAccessMixin.chain_manager
"""
The current connected blockchain; requires an active provider.
Useful for development purposes, such as controlling the state of the blockchain.
Also handy for querying data about the chain and managing local caches.
"""

accounts = _ManagerAccessMixin.account_manager
"""Manages accounts for the current project. See :class:`ape.managers.accounts.AccountManager`."""

project = _ManagerAccessMixin.local_project
"""The currently active project. See :class:`ape.managers.project.ProjectManager`."""

Contract = chain.contracts.instance_at
"""User-facing class for instantiating contracts."""

convert = _ManagerAccessMixin.conversion_manager.convert
"""Conversion utility function. See :class:`ape.managers.converters.ConversionManager`."""

reverts = RevertsContextManager
"""
Catch and expect contract logic reverts. Resembles ``pytest.raises()``.
"""

from importlib import import_module

__all__ = [
"accounts",
Expand All @@ -64,3 +21,34 @@
"Project", # So you can load other projects
"reverts",
]


def __getattr__(name: str) -> Any:
if name not in __all__:
raise AttributeError(name)

elif name == "reverts":
contextmanagers = import_module("ape.pytest.contextmanagers")
return contextmanagers.RevertsContextManager

else:
access = import_module("ape.managers.project").ManagerAccessMixin
if name == "Contract":
return access.chain_manager.contracts.instance_at

elif name == "Project":
return access.Project

elif name == "convert":
return access.conversion_manager.convert

# The rest are managers; we can derive the name.
key = name
if name == "project":
key = "local_project"
elif name.endswith("s"):
key = f"{name[:-1]}_manager"
else:
key = f"{key}_manager"

return getattr(access, key)
96 changes: 47 additions & 49 deletions src/ape/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import warnings
from collections.abc import Iterable
from gettext import gettext
from importlib import import_module
from importlib.metadata import entry_points
from pathlib import Path
from typing import Any, Optional
Expand All @@ -13,11 +14,10 @@
import yaml
from click import Context

from ape.cli import ape_cli_context
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.plugins._utils import PluginMetadataList, clean_plugin_name
from ape.utils.basemodel import ManagerAccessMixin
from ape.utils.basemodel import ManagerAccessMixin as access

_DIFFLIB_CUT_OFF = 0.6

Expand All @@ -30,16 +30,16 @@ def display_config(ctx, param, value):
click.echo("# Current configuration")

# NOTE: Using json-mode as yaml.dump requires JSON-like structure.
model = ManagerAccessMixin.local_project.config_manager.model_dump(mode="json")
model = access.local_project.config.model_dump(mode="json")

click.echo(yaml.dump(model))

ctx.exit() # NOTE: Must exit to bypass running ApeCLI


def _validate_config():
project = access.local_project
try:
_ = ManagerAccessMixin.local_project.config
_ = project.config
except ConfigError as err:
rich.print(err)
# Exit now to avoid weird problems.
Expand Down Expand Up @@ -68,40 +68,40 @@ def format_commands(self, ctx, formatter) -> None:

commands.append((subcommand, cmd))

# Allow for 3 times the default spacing.
if len(commands):
limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)

# Split the commands into 3 sections.
sections: dict[str, list[tuple[str, str]]] = {
"Core": [],
"Plugin": [],
"3rd-Party Plugin": [],
}

pl_metadata = PluginMetadataList.load(
ManagerAccessMixin.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)
if not plugin:
continue

if plugin.in_core:
sections["Core"].append((cli_name, help))
elif plugin.is_installed and not plugin.is_third_party:
sections["Plugin"].append((cli_name, help))
else:
sections["3rd-Party Plugin"].append((cli_name, help))

for title, rows in sections.items():
if not rows:
continue

with formatter.section(gettext(f"{title} Commands")):
formatter.write_dl(rows)
if not commands:
return None

limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)

# Split the commands into 3 sections.
sections: dict[str, list[tuple[str, str]]] = {
"Core": [],
"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)
for cli_name, cmd in commands:
help = cmd.get_short_help_str(limit)
plugin = pl_metadata.get_plugin(cli_name, check_available=False)
if plugin is None:
continue

if plugin.in_core:
sections["Core"].append((cli_name, help))
elif plugin.is_installed and not plugin.is_third_party:
sections["Plugin"].append((cli_name, help))
else:
sections["3rd-Party Plugin"].append((cli_name, help))

for title, rows in sections.items():
if not rows:
continue

with formatter.section(gettext(f"{title} Commands")):
formatter.write_dl(rows)

def invoke(self, ctx) -> Any:
try:
Expand Down Expand Up @@ -158,20 +158,18 @@ def commands(self) -> dict:
warnings.simplefilter("ignore")
eps = _entry_points.get(self._CLI_GROUP_NAME, []) # type: ignore

self._commands = {clean_plugin_name(cmd.name): cmd.load for cmd in eps}
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

def list_commands(self, ctx) -> list[str]:
return list(sorted(self.commands))
return [k for k in self.commands]

def get_command(self, ctx, name) -> Optional[click.Command]:
if name in self.commands:
try:
return self.commands[name]()
except Exception as err:
logger.warn_from_exception(
err, f"Unable to load CLI endpoint for plugin 'ape_{name}'"
)
try:
return self.commands[name]()
except Exception as err:
logger.warn_from_exception(err, f"Unable to load CLI endpoint for plugin 'ape_{name}'")

# NOTE: don't return anything so Click displays proper error
return None
Expand Down
9 changes: 3 additions & 6 deletions src/ape/api/compiler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from abc import abstractmethod
from collections.abc import Iterable, Iterator
from functools import cached_property
from pathlib import Path
Expand All @@ -13,12 +14,8 @@
from ape.exceptions import APINotImplementedError, ContractLogicError
from ape.types.coverage import ContractSourceCoverage
from ape.types.trace import SourceTraceback
from ape.utils import (
BaseInterfaceModel,
abstractmethod,
log_instead_of_fail,
raises_not_implemented,
)
from ape.utils.basemodel import BaseInterfaceModel
from ape.utils.misc import log_instead_of_fail, raises_not_implemented

if TYPE_CHECKING:
from ape.managers.project import ProjectManager
Expand Down
3 changes: 2 additions & 1 deletion src/ape/api/convert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from abc import abstractmethod
from typing import Any, Generic, TypeVar

from ape.utils import BaseInterfaceModel, abstractmethod
from ape.utils.basemodel import BaseInterfaceModel

ConvertedType = TypeVar("ConvertedType")

Expand Down
3 changes: 2 additions & 1 deletion src/ape/api/projects.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from abc import abstractmethod
from functools import cached_property
from pathlib import Path
from typing import Optional

from pydantic import Field, field_validator

from ape.api.config import ApeConfig
from ape.utils import BaseInterfaceModel, abstractmethod
from ape.utils.basemodel import BaseInterfaceModel


class DependencyAPI(BaseInterfaceModel):
Expand Down
Loading
Loading