Skip to content

Commit

Permalink
perf: make CLI --help command and root module load faster (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 29, 2024
1 parent f8ae648 commit 74be954
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 57 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-PyYAML, 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
24 changes: 22 additions & 2 deletions ape_trezor/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
from ape import plugins
from importlib import import_module
from typing import Any

from .accounts import AccountContainer, TrezorAccount, TrezorConfig
from ape import plugins


@plugins.register(plugins.Config)
def config_class():
from .accounts import TrezorConfig

return TrezorConfig


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

return AccountContainer, TrezorAccount


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

else:
raise AttributeError(name)


__all__ = [
"AccountContainer",
"TrezorAccount",
"TrezorConfig",
]
79 changes: 54 additions & 25 deletions ape_trezor/_cli.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
from typing import cast
from typing import TYPE_CHECKING, cast

import click
from ape import accounts
from ape.cli import (
ape_cli_context,
existing_alias_argument,
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, skip_confirmation_option

from ape_trezor.accounts import TrezorAccount, TrezorConfig
from ape_trezor.choices import AddressPromptChoice
from ape_trezor.client import create_client
from ape_trezor.exceptions import TrezorSigningError
from ape_trezor.hdpath import HDBasePath
from ape_trezor.utils import DEFAULT_ETHEREUM_HD_PATH

if TYPE_CHECKING:
from ape.api.accounts import AccountAPI

from ape_trezor.client import TrezorClient
from ape_trezor.hdpath import HDBasePath


@click.group(short_help="Manage Trezor accounts")
def cli():
Expand All @@ -31,7 +26,9 @@ def cli():
def _list(cli_ctx):
"""List your Trezor accounts in ape"""

trezor_accounts = accounts.get_accounts_by_type(type_=TrezorAccount)
from ape_trezor.accounts import TrezorAccount

trezor_accounts = cli_ctx.account_manager.get_accounts_by_type(type_=TrezorAccount)
num_of_accts = len(trezor_accounts)

if num_of_accts == 0:
Expand All @@ -49,9 +46,14 @@ def _list(cli_ctx):


def handle_hd_path(ctx, param, value):
from ape.utils.basemodel import ManagerAccessMixin

from ape_trezor.accounts import TrezorConfig
from ape_trezor.hdpath import HDBasePath

if not value:
try:
config = cast(TrezorConfig, accounts.config_manager.get_config("trezor"))
config = cast(TrezorConfig, ManagerAccessMixin.config_manager.get_config("trezor"))
value = config.hd_path
except Exception:
value = DEFAULT_ETHEREUM_HD_PATH
Expand All @@ -66,6 +68,13 @@ def handle_hd_path(ctx, param, value):
)


def create_client(hd_path: "HDBasePath") -> "TrezorClient":
# NOTE: Abstracted for testing (and --help performance!) reasons.
from ape_trezor.client import create_client as _create_client

return _create_client(hd_path)


@cli.command()
@ape_cli_context()
@non_existing_alias_argument()
Expand All @@ -79,21 +88,29 @@ def add(cli_ctx, alias, hd_path):
"Please use an alternative HD-Path for a safer integration."
)

from ape_trezor.choices import AddressPromptChoice

client = create_client(hd_path)
choices = AddressPromptChoice(client, hd_path)
address, account_hd_path = choices.get_user_selected_account()
container = accounts.containers.get("trezor")
container = cli_ctx.account_manager.containers.get("trezor")
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_trezor.accounts import TrezorAccount

return isinstance(acct, TrezorAccount)


@cli.command()
@ape_cli_context()
@existing_alias_argument(account_type=TrezorAccount)
@existing_alias_argument(account_type=_filter_accounts)
def delete(cli_ctx, alias):
"""Remove a Trezor account from your ape configuration"""

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

Expand All @@ -103,9 +120,10 @@ def delete(cli_ctx, alias):
@skip_confirmation_option("Don't ask for confirmation when removing all accounts")
def delete_all(cli_ctx, skip_confirmation):
"""Remove all trezor accounts from your ape configuration"""
from ape_trezor.accounts import TrezorAccount

container = accounts.containers.get("trezor")
trezor_accounts = accounts.get_accounts_by_type(type_=TrezorAccount)
container = cli_ctx.account_manager.containers.get("trezor")
trezor_accounts = cli_ctx.account_manager.get_accounts_by_type(type_=TrezorAccount)
if len(trezor_accounts) == 0:
cli_ctx.logger.warning("No accounts found.")
return
Expand All @@ -125,11 +143,14 @@ def delete_all(cli_ctx, skip_confirmation):
@click.argument("message")
@ape_cli_context()
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.")

eip191_message = encode_defunct(text=message)
account = accounts.load(alias)
account = cli_ctx.account_manager.load(alias)
signature = account.sign_message(eip191_message)
signature_bytes = signature.encode_rsv()

Expand All @@ -143,9 +164,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 @@ -154,6 +179,10 @@ def verify_message(message, signature):
message = "Message cannot be verified. Check the signature and try again."
raise TrezorSigningError(message) from exc

alias = accounts[signer_address].alias if signer_address in accounts else "n/a"
alias = (
cli_ctx.account_manager[signer_address].alias
if signer_address in cli_ctx.account_manager
else "n/a"
)

click.echo(f"Signer: {signer_address} {alias}")
16 changes: 9 additions & 7 deletions ape_trezor/choices.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional

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

from ape_trezor.client import TrezorClient
from ape_trezor.hdpath import HDBasePath, HDPath
if TYPE_CHECKING:
from click import Context, Parameter

from ape_trezor.client import TrezorClient
from ape_trezor.hdpath import HDBasePath, HDPath


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

def __init__(
self,
client: TrezorClient,
hd_base_pth: HDBasePath,
client: "TrezorClient",
hd_base_pth: "HDBasePath",
index_offset: int = 0,
page_size: int = DEFAULT_PAGE_SIZE,
):
Expand Down Expand Up @@ -56,7 +58,7 @@ def convert(
self._choice_index = self.choices.index(address) # type: ignore
return address

def get_user_selected_account(self) -> tuple[str, HDPath]:
def get_user_selected_account(self) -> tuple[str, "HDPath"]:
"""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
17 changes: 10 additions & 7 deletions ape_trezor/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from collections.abc import Callable
from typing import Optional
from typing import TYPE_CHECKING, Optional

from ape.logging import logger
from eth_typing.evm import ChecksumAddress
from trezorlib.client import TrezorClient as LibTrezorClient
from trezorlib.client import get_default_client
from trezorlib.device import apply_settings
Expand All @@ -25,11 +24,15 @@
TrezorClientConnectionError,
TrezorClientError,
)
from ape_trezor.hdpath import HDBasePath, HDPath
from ape_trezor.utils import DEFAULT_ETHEREUM_HD_PATH

if TYPE_CHECKING:
from eth_typing.evm import ChecksumAddress

def create_client(hd_path: HDBasePath) -> "TrezorClient":
from ape_trezor.hdpath import HDBasePath, HDPath


def create_client(hd_path: "HDBasePath") -> "TrezorClient":
return TrezorClient(hd_path)


Expand All @@ -38,7 +41,7 @@ class TrezorClient:
This class is a client for the Trezor device.
"""

def __init__(self, hd_root_path: HDBasePath, client: Optional[LibTrezorClient] = None):
def __init__(self, hd_root_path: "HDBasePath", client: Optional[LibTrezorClient] = None):
if not client:
try:
self.client = get_default_client()
Expand Down Expand Up @@ -88,8 +91,8 @@ class TrezorAccountClient:

def __init__(
self,
address: ChecksumAddress,
account_hd_path: HDPath,
address: "ChecksumAddress",
account_hd_path: "HDPath",
client: Optional[LibTrezorClient] = None,
):
if not client:
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[flake8]
max-line-length = 100
ignore = E704,W503,PYD002,TC003,TC006
exclude =
venv*
docs
build
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,15 +10,17 @@
"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-PyYAML", # Needed for mypy typeshed
"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
2 changes: 2 additions & 0 deletions tests/test_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ def trezor_account(mocker, accounts, address, account_hd_path, mock_client):

try:
account = accounts.load(alias)
account.__dict__["client"] = mock_client # In case cached from another test
assert account.client == mock_client, "Setup failed: mock client not set"
yield account
finally:
container.delete_account(alias)
Expand Down
Loading

0 comments on commit 74be954

Please sign in to comment.