Skip to content

Commit

Permalink
perf: make plugin load faster for ape --help (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 29, 2024
1 parent 7725b01 commit 5098377
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 66 deletions.
11 changes: 6 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-yaml

Expand All @@ -10,24 +10,25 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 24.4.2
rev: 24.10.0
hooks:
- id: black
name: black

- repo: https://github.com/pycqa/flake8
rev: 7.0.0
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [flake8-breakpoint, flake8-print, flake8-pydantic, flake8-type-checking]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: [types-setuptools, pydantic]

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.17
rev: 0.7.18
hooks:
- id: mdformat
additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject]
Expand Down
21 changes: 19 additions & 2 deletions ape_ledger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
from ape import plugins
from importlib import import_module
from typing import Any

from ape_ledger.accounts import AccountContainer, LedgerAccount
from ape import plugins


@plugins.register(plugins.AccountPlugin)
def account_types():
from ape_ledger.accounts import AccountContainer, LedgerAccount

return AccountContainer, LedgerAccount


def __getattr__(name: str) -> Any:
if name in ("AccountContainer", "LedgerAccount"):
return getattr(import_module("ape_ledger.accounts"), name)

else:
raise AttributeError(name)


__all__ = [
"AccountContainer",
"LedgerAccount",
]
77 changes: 51 additions & 26 deletions ape_ledger/_cli.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
from typing import Union
from typing import TYPE_CHECKING, Union

import click
from ape import accounts
from ape.cli import (
ape_cli_context,
existing_alias_argument,
network_option,
non_existing_alias_argument,
skip_confirmation_option,
)
from eth_account import Account
from eth_account.messages import encode_defunct
from ape.cli.arguments import existing_alias_argument, non_existing_alias_argument
from ape.cli.options import ape_cli_context, network_option, skip_confirmation_option

if TYPE_CHECKING:
# NOTE: Type-checking only imports so CLI help loads faster.
from ape.api import AccountAPI
from ape_ledger.accounts import LedgerAccount
from ape_ledger.hdpath import HDAccountPath, HDBasePath

from ape_ledger.accounts import LedgerAccount
from ape_ledger.choices import AddressPromptChoice
from ape_ledger.exceptions import LedgerSigningError
from ape_ledger.hdpath import HDAccountPath, HDBasePath


def _select_account(hd_path: Union[HDBasePath, str]) -> tuple[str, HDAccountPath]:
def _select_account(hd_path: Union["HDBasePath", str]) -> tuple[str, "HDAccountPath"]:
# NOTE: Lazy import so CLI help loads faster.
from ape_ledger.choices import AddressPromptChoice

choices = AddressPromptChoice(hd_path)
return choices.get_user_selected_account()

Expand Down Expand Up @@ -52,8 +50,18 @@ def _list(cli_ctx):
click.echo(f" {account.address}{alias_display}{hd_path_display}")


def _get_ledger_accounts() -> list[LedgerAccount]:
return [a for a in accounts if isinstance(a, LedgerAccount)]
def _get_ledger_accounts() -> list["LedgerAccount"]:
from ape.utils.basemodel import ManagerAccessMixin

from ape_ledger.accounts import LedgerAccount

return [a for a in ManagerAccessMixin.account_manager if isinstance(a, LedgerAccount)]


def _hdpath_callback(ctx, param, val) -> "HDBasePath":
from ape_ledger.hdpath import HDBasePath

return HDBasePath(val)


@cli.command()
Expand All @@ -66,24 +74,30 @@ def _get_ledger_accounts() -> list[LedgerAccount]:
"Defaults to m/44'/60'/{x}'/0/0 where {{x}} is the account ID. "
"Exclude {x} to append the account ID to the end of the base path."
),
callback=lambda ctx, param, arg: HDBasePath(arg),
callback=_hdpath_callback,
)
def add(cli_ctx, alias, hd_path):
"""Add an account from your Ledger hardware wallet"""

address, account_hd_path = _select_account(hd_path)
container = accounts.containers["ledger"]
container = cli_ctx.account_manager.containers["ledger"]
container.save_account(alias, address, str(account_hd_path))
cli_ctx.logger.success(f"Account '{address}' successfully added with alias '{alias}'.")


def _filter_accounts(acct: "AccountAPI") -> bool:
from ape_ledger.accounts import LedgerAccount

return isinstance(acct, LedgerAccount)


@cli.command()
@ape_cli_context()
@existing_alias_argument(account_type=LedgerAccount)
@existing_alias_argument(account_type=_filter_accounts)
def delete(cli_ctx, alias):
"""Remove a Ledger account from ape"""

container = accounts.containers["ledger"]
container = cli_ctx.account_manager.containers["ledger"]
container.delete_account(alias)
cli_ctx.logger.success(f"Account '{alias}' has been removed.")

Expand All @@ -94,7 +108,7 @@ def delete(cli_ctx, alias):
def delete_all(cli_ctx, skip_confirmation):
"""Remove all Ledger accounts from ape"""

container = accounts.containers["ledger"]
container = cli_ctx.account_manager.containers["ledger"]
ledger_accounts = _get_ledger_accounts()
if len(ledger_accounts) == 0:
cli_ctx.logger.warning("No accounts found.")
Expand Down Expand Up @@ -131,11 +145,14 @@ def sign_message(cli_ctx, alias, message, network):


def _sign_message(cli_ctx, alias, message):
if alias not in accounts.aliases:
from eth_account.account import Account
from eth_account.messages import encode_defunct

if alias not in cli_ctx.account_manager.aliases:
cli_ctx.abort(f"Account with alias '{alias}' does not exist.")

eip191message = encode_defunct(text=message)
account = accounts.load(alias)
account = cli_ctx.account_manager.load(alias)
signature = account.sign_message(eip191message)

if not signature:
Expand All @@ -153,9 +170,13 @@ def _sign_message(cli_ctx, alias, message):


@cli.command(short_help="Verify a message with your Trezor device")
@ape_cli_context()
@click.argument("message")
@click.argument("signature")
def verify_message(message, signature):
def verify_message(cli_ctx, message, signature):
from eth_account.account import Account
from eth_account.messages import encode_defunct

eip191message = encode_defunct(text=message)

try:
Expand All @@ -164,5 +185,9 @@ def verify_message(message, signature):
message = "Message cannot be verified. Check the signature and try again."
raise LedgerSigningError(message) from exc

alias = accounts[signer_address].alias if signer_address in accounts else ""
alias = (
cli_ctx.account_manager[signer_address].alias
if signer_address in cli_ctx.account_manager
else ""
)
click.echo(f"Signer: {signer_address} {alias}")
23 changes: 17 additions & 6 deletions ape_ledger/choices.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import Any, Optional, Union
from typing import TYPE_CHECKING, Any, Optional, Union

import click
from ape.cli import PromptChoice
from click import Context, Parameter

from ape_ledger.client import get_device
from ape_ledger.hdpath import HDAccountPath, HDBasePath
if TYPE_CHECKING:
from click import Context, Parameter

from ape_ledger.client import LedgerDeviceClient
from ape_ledger.hdpath import HDAccountPath, HDBasePath


class AddressPromptChoice(PromptChoice):
Expand All @@ -17,10 +19,12 @@ class AddressPromptChoice(PromptChoice):

def __init__(
self,
hd_path: Union[HDBasePath, str],
hd_path: Union["HDBasePath", str],
index_offset: int = 0,
page_size: int = DEFAULT_PAGE_SIZE,
):
from ape_ledger.hdpath import HDBasePath

if isinstance(hd_path, str):
hd_path = HDBasePath(base_path=hd_path)

Expand Down Expand Up @@ -58,7 +62,7 @@ def convert(
self._choice_index = self._choice_index if address_index is None else address_index
return address

def get_user_selected_account(self) -> tuple[str, HDAccountPath]:
def get_user_selected_account(self) -> tuple[str, "HDAccountPath"]:
"""Returns the selected address from the user along with the HD path.
The user is able to page using special characters ``n`` and ``p``.
"""
Expand Down Expand Up @@ -103,4 +107,11 @@ def _get_address(self, account_id: int) -> str:
return device.get_address()


def get_device(path: "HDAccountPath") -> "LedgerDeviceClient":
# Perf: lazy load so CLI-usage is faster and abstracted for testing purposes.
from ape_ledger.client import get_device as _get_device

return _get_device(path)


__all__ = ["AddressPromptChoice"]
10 changes: 6 additions & 4 deletions ape_ledger/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import atexit
from functools import cached_property
from typing import TYPE_CHECKING

import hid # type: ignore
from ape.logging import LogLevel, logger
Expand All @@ -8,13 +9,14 @@
from ledgereth.messages import sign_message, sign_typed_data_draft
from ledgereth.transactions import SignedType2Transaction, create_transaction

from ape_ledger.hdpath import HDAccountPath
if TYPE_CHECKING:
from ape_ledger.hdpath import HDAccountPath


class DeviceFactory:
device_map: dict[str, "LedgerDeviceClient"] = {}

def create_device(self, account: HDAccountPath):
def create_device(self, account: "HDAccountPath"):
if account.path in self.device_map:
return self.device_map[account.path]

Expand All @@ -37,7 +39,7 @@ def get_dongle(debug: bool = False, reopen_on_fail: bool = True) -> HIDDongleHID


class LedgerDeviceClient:
def __init__(self, account: HDAccountPath):
def __init__(self, account: "HDAccountPath"):
self._account = account.path.lstrip("m/")

@cached_property
Expand Down Expand Up @@ -78,5 +80,5 @@ def sign_transaction(self, txn: dict) -> tuple[int, int, int]:
_device_factory = DeviceFactory()


def get_device(account: HDAccountPath):
def get_device(account: "HDAccountPath") -> LedgerDeviceClient:
return _device_factory.create_device(account)
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ exclude =
venv*
docs
build
ignore = E704,W503,PYD002,TC003,TC006
per-file-ignores =
# Scared to change byte-indexing logic
ape_ledger/client.py: E203
ape_ledger/client.py: E203
type-checking-pydantic-enabled = True
10 changes: 6 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
"hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer
],
"lint": [
"black>=24.4.2,<25", # Auto-formatter and linter
"mypy>=1.10.0,<2", # Static type analyzer
"black>=24.10.0,<25", # Auto-formatter and linter
"mypy>=1.13.0,<2", # Static type analyzer
"types-setuptools", # Needed for mypy type shed
"flake8>=7.0.0,<8", # Style linter
"flake8>=7.1.1,<8", # Style linter
"flake8-breakpoint>=1.1.0,<2", # Detect breakpoints left in code
"flake8-print>=5.0.0,<6", # Detect print statements left in code
"flake8-pydantic", # For detecting issues with Pydantic models
"flake8-type-checking", # Detect imports to move in/out of type-checking blocks
"isort>=5.13.2,<6", # Import sorting linter
"mdformat>=0.7.17", # Auto-formatter for markdown
"mdformat>=0.7.18", # Auto-formatter for markdown
"mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown
"mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates
"mdformat-pyproject>=0.0.1", # Allows configuring in pyproject.toml
Expand Down
16 changes: 9 additions & 7 deletions tests/test_accounts.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import json
from typing import Optional, cast
from typing import TYPE_CHECKING, Optional, cast

import pytest
from ape import networks
from ape.api import TransactionAPI
from ape.types import AddressType
from ape.utils import create_tempdir
from ape_ethereum.ecosystem import DynamicFeeTransaction, StaticFeeTransaction
from eip712.messages import EIP712Message, EIP712Type
Expand All @@ -14,6 +12,10 @@
from ape_ledger.accounts import AccountContainer, LedgerAccount
from ape_ledger.exceptions import LedgerSigningError

if TYPE_CHECKING:
from ape.api import TransactionAPI
from ape.types import AddressType


@pytest.fixture(autouse=True)
def patch_device(device_factory):
Expand Down Expand Up @@ -44,8 +46,8 @@ class Mail(EIP712Message):


def build_transaction(
txn: TransactionAPI, receiver: Optional[AddressType] = None
) -> TransactionAPI:
txn: "TransactionAPI", receiver: Optional["AddressType"] = None
) -> "TransactionAPI":
txn.chain_id = 579875
txn.nonce = 0
txn.gas_limit = 2
Expand All @@ -62,7 +64,7 @@ def build_transaction(


def create_static_fee_txn(
receiver: Optional[AddressType] = None,
receiver: Optional["AddressType"] = None,
) -> StaticFeeTransaction:
txn = StaticFeeTransaction()
txn = cast(StaticFeeTransaction, build_transaction(txn, receiver=receiver))
Expand All @@ -71,7 +73,7 @@ def create_static_fee_txn(


def create_dynamic_fee_txn(
receiver: Optional[AddressType] = None,
receiver: Optional["AddressType"] = None,
) -> DynamicFeeTransaction:
txn = DynamicFeeTransaction()
txn = cast(DynamicFeeTransaction, build_transaction(txn, receiver=receiver))
Expand Down
Loading

0 comments on commit 5098377

Please sign in to comment.