Skip to content

Commit

Permalink
perf: make ape --help faster (#2333)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 25, 2024
1 parent c2203f5 commit 85eddb8
Show file tree
Hide file tree
Showing 57 changed files with 671 additions and 392 deletions.
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
84 changes: 82 additions & 2 deletions docs/methoddocs/ape.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,86 @@
# ape

```{eval-rst}
.. automodule:: ape
:members:
.. attribute:: ape.accounts
Manage accounts.
See the `AccountManager <../methoddocs/managers.html#ape.managers.accounts.AccountManager>`__ for more info.
```

```{eval-rst}
.. attribute:: ape.chain
Manage the blockchain.
See the `ChainManager <../methoddocs/managers.html#ape.managers.chain.ChainManager>`__ for more info.
```

```{eval-rst}
.. attribute:: ape.networks
Manage networks.
See the `NetworkManager <../methoddocs/managers.html#ape.managers.networks.NetworkManager>`__ for more info.
```

```{eval-rst}
.. attribute:: ape.project
Access the local project.
See the `ProjectManager <../methoddocs/managers.html#ape.managers.project.ProjectManager>`__ for more info.
```

```{eval-rst}
.. attribute:: ape.config
Access the local project config.
See the `ConfigManager <../methoddocs/managers.html#ape.managers.config.ConfigManager>`__ for more info.
```

```{eval-rst}
.. function:: ape.Project(path)
Instantiate other projects.
See the `ProjectManager <../methoddocs/managers.html#ape.managers.project.ProjectManager>`__ for more info.
:path: The path to the project.
```

```{eval-rst}
.. function:: ape.Contract(address, contract_type)
Instantiate contract-classes at a given address.
See the `ContractInstance <../methoddocs/contracts.html#ape.contracts.base.ContractInstance>`__ for more info.
:address: The address of the instance.
:contract_type: Optionally provide the ABI or contract type data.
```

```{eval-rst}
.. function:: ape.convert(value, to_type)
Conversion utility.
See the `ConversionManager <../methoddocs/managers.html#ape.managers.converters.ConversionManager>`__ for more info.
:value: The value to convert.
:to_type: The destination type.
Example usage::
result = ape.convert("1 ETH", int)
```

```{eval-rst}
.. attribute:: ape.compilers
Access compiler classes.
See the `CompilerManager <../methoddocs/managers.html#ape.managers.compilers.CompilerManager>`__ for more info.
```

```{eval-rst}
.. function:: ape.reverts(expected_message, dev_message)
Catch contract-revert exceptions.
Mimics ``pytest.raises``.
:expected_message: The expected revert message (optional).
:dev_message: The expected dev-message (optional).
```
41 changes: 38 additions & 3 deletions docs/methoddocs/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,44 @@
:show-inheritance:
```

## Miscellaneous
## Events

```{eval-rst}
.. automodule:: ape.types
:members: BlockID, BaseContractLog, ContractLog, MockContractLog
.. automodule:: ape.types.events
:members: ContractLog, ContractLogContainer, MockContractLog, LogFilter
```

## VM

```{eval-rst}
.. automodule:: ape.types.vm
:members: BlockID, SnapshotID
```

## Gas

```{eval-rst}
.. automodule:: ape.types.gas
:members: GasLimit, AutoGasLimit
```

## Trace

```{eval-rst}
.. automodule:: ape.types.trace
:members: GasReport, ControlFlow, SourceTraceback
```

## Units

```{eval-rst}
.. automodule:: ape.types.units
:members: CurrencyValueComparable, CurrencyValue
```

## Basic

```{eval-rst}
.. automodule:: ape.types.basic
:members: HexInt
```
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
Loading

0 comments on commit 85eddb8

Please sign in to comment.