Skip to content

Commit

Permalink
perf: make ape --help faster when safe installed pt. 2 (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Oct 29, 2024
1 parent c800f58 commit 0846ac5
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 62 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ repos:
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.13.0
Expand Down
16 changes: 9 additions & 7 deletions ape_safe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
from importlib import import_module
from typing import Any, Optional
from typing import Any

from ape import plugins
from ape.api import PluginConfig


class SafeConfig(PluginConfig):
default_safe: Optional[str] = None
"""Alias of the default safe."""


@plugins.register(plugins.Config)
def config_class():
from ape_safe.config import SafeConfig

return SafeConfig


Expand All @@ -31,12 +27,18 @@ def __getattr__(name: str) -> Any:
elif name in ("SafeAccount", "SafeContainer"):
return getattr(import_module("ape_safe.accounts"), name)

elif name == "SafeConfig":
from ape_safe.config import SafeConfig

return SafeConfig

else:
raise AttributeError(name)


__all__ = [
"MultiSend",
"SafeAccount",
"SafeConfig",
"SafeContainer",
]
33 changes: 19 additions & 14 deletions ape_safe/_cli/click_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
from typing import TYPE_CHECKING, NoReturn, Optional, Union, cast

import click
from ape.api import AccountAPI
from ape.cli import ApeCliContextObject, ape_cli_context
from ape.exceptions import Abort
from ape.utils import ManagerAccessMixin
from click import BadOptionUsage, MissingParameter

if TYPE_CHECKING:
# perf: Keep the CLI module loading fast as possible.
from ape.api import AccountAPI

from ape_safe.accounts import SafeContainer


Expand Down Expand Up @@ -41,22 +42,24 @@ def _txn_ids_callback(ctx, param, value):
)


class CallbackFactory(ManagerAccessMixin):
class CallbackFactory:
"""
Helper class to prevent circular import and have access
to Ape objects.
"""

@classmethod
def safe_callback(cls, ctx, param, value):
from ape.utils import ManagerAccessMixin as access

# NOTE: For some reason, the Cli CTX object is not the SafeCliCtx yet at this point.
safes = cls.account_manager.containers["safe"]
safes = access.account_manager.containers["safe"]
if value is None:
# First, check config for a default. If one is there,
# we must use that.
safe_config = cls.config_manager.get_config("safe")
safe_config = access.config_manager.get_config("safe")
if alias := safe_config.default_safe:
return cls.account_manager.load(alias)
return access.account_manager.load(alias)

# If there is only 1 safe, just use that.
elif len(safes) == 1:
Expand All @@ -69,7 +72,7 @@ def safe_callback(cls, ctx, param, value):
raise MissingParameter(message=f"Must specify one of '{options}').")

elif value in safes.aliases:
return cls.account_manager.load(value)
return access.account_manager.load(value)

else:
raise BadOptionUsage("--safe", f"No safe with alias '{value}'")
Expand All @@ -79,16 +82,18 @@ def submitter_callback(cls, ctx, param, val):
if val is None:
return None

elif val in cls.account_manager.aliases:
return cls.account_manager.load(val)
from ape.utils import ManagerAccessMixin as access

if val in access.account_manager.aliases:
return access.account_manager.load(val)

# Account address - execute using this account.
elif val in cls.account_manager:
return cls.account_manager[val]
elif val in access.account_manager:
return access.account_manager[val]

# Saying "yes, execute". Use first "local signer".
elif val.lower() in ("true", "t", "1"):
safe = cls.account_manager.load(ctx.params["alias"])
safe = access.account_manager.load(ctx.params["alias"])
if not safe.local_signers:
ctx.obj.abort("Cannot use `--execute TRUE` without a local signer.")

Expand All @@ -97,7 +102,7 @@ def submitter_callback(cls, ctx, param, val):
return None

@classmethod
def sender_callback(cls, ctx, param, val) -> Optional[Union[AccountAPI, bool]]:
def sender_callback(cls, ctx, param, val) -> Optional[Union["AccountAPI", bool]]:
"""
Either returns the account or ``False`` meaning don't execute.
NOTE: The handling of the `--execute` flag in the `pending` CLI
Expand All @@ -107,7 +112,7 @@ def sender_callback(cls, ctx, param, val) -> Optional[Union[AccountAPI, bool]]:
return cls._get_execute_callback(ctx, param, val, name="sender")

@classmethod
def execute_callback(cls, ctx, param, val) -> Optional[Union[AccountAPI, bool]]:
def execute_callback(cls, ctx, param, val) -> Optional[Union["AccountAPI", bool]]:
"""
Either returns the account or ``False`` meaning don't execute.
"""
Expand Down
4 changes: 3 additions & 1 deletion ape_safe/_cli/pending.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import rich
from ape.cli import ConnectedProviderCommand
from ape.exceptions import SignatureError
from ape.types import AddressType, MessageSignature
from eth_typing import ChecksumAddress, Hash32
from eth_utils import humanize_hash
from hexbytes import HexBytes
Expand Down Expand Up @@ -269,6 +268,9 @@ def execute(cli_ctx, safe, txn_ids, submitter, nonce):


def _execute(safe: "SafeAccount", txn: "UnexecutedTxData", submitter: "AccountAPI", **tx_kwargs):
# perf: Avoid these imports during CLI load time for `ape --help` performance.
from ape.types import AddressType, MessageSignature

safe_tx = safe.create_safe_tx(**txn.model_dump(mode="json", by_alias=True))
signatures: dict[AddressType, MessageSignature] = {
c.owner: MessageSignature.from_rsv(c.signature) for c in txn.confirmations
Expand Down
16 changes: 9 additions & 7 deletions ape_safe/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
import os
from collections.abc import Iterable, Iterator, Mapping
from pathlib import Path
from typing import Any, Dict, Optional, Union, cast
from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast

from ape.api import AccountAPI, AccountContainerAPI, ReceiptAPI, TransactionAPI
from ape.api.address import BaseAddress
from ape.api.networks import ForkedNetworkAPI
from ape.cli import select_account
from ape.contracts import ContractInstance
from ape.exceptions import ContractNotFoundError, ProviderNotConnectedError
from ape.logging import logger
from ape.managers.accounts import AccountManager, TestAccountManager
Expand Down Expand Up @@ -37,6 +35,10 @@
)
from ape_safe.utils import get_safe_tx_hash, order_by_signer

if TYPE_CHECKING:
from ape.api.address import BaseAddress
from ape.contracts import ContractInstance


class SafeContainer(AccountContainerAPI):
_accounts: Dict[str, "SafeAccount"] = {}
Expand Down Expand Up @@ -215,7 +217,7 @@ def address(self) -> AddressType:
return ecosystem.decode_address(self.account_file["address"])

@cached_property
def contract(self) -> ContractInstance:
def contract(self) -> "ContractInstance":
safe_contract = self.chain_manager.contracts.instance_at(self.address)
if self.fallback_handler:
contract_signatures = {x.signature for x in safe_contract.contract_type.abi}
Expand All @@ -236,7 +238,7 @@ def contract(self) -> ContractInstance:
return safe_contract

@cached_property
def fallback_handler(self) -> Optional[ContractInstance]:
def fallback_handler(self) -> Optional["ContractInstance"]:
slot = keccak(text="fallback_manager.handler.address")
value = self.provider.get_storage(self.address, slot)
address = self.network_manager.ecosystem.decode_address(value[-20:])
Expand Down Expand Up @@ -408,7 +410,7 @@ def create_execute_transaction(
*exec_args, encoded_signatures, **txn_options
)

def compute_prev_signer(self, signer: Union[str, AddressType, BaseAddress]) -> AddressType:
def compute_prev_signer(self, signer: Union[str, AddressType, "BaseAddress"]) -> AddressType:
"""
Sometimes it's handy to have "previous owner" for ownership change operations,
this function makes it easy to calculate.
Expand Down Expand Up @@ -465,7 +467,7 @@ def estimate_gas_cost(self, **kwargs) -> int:
)

def _preapproved_signature(
self, signer: Union[AddressType, BaseAddress, str]
self, signer: Union[AddressType, "BaseAddress", str]
) -> MessageSignature:
# Get the Safe-style "preapproval" signature type, which is a sentinel value used to denote
# when a signer approved via some other method, such as `approveHash` or being `msg.sender`
Expand Down
22 changes: 12 additions & 10 deletions ape_safe/client/base.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
from abc import ABC, abstractmethod
from collections.abc import Iterator
from functools import cached_property
from typing import Optional, Union
from typing import TYPE_CHECKING, Optional, Union

import certifi
import requests
import urllib3
from ape.types import AddressType, MessageSignature
from requests import Response
from requests.adapters import HTTPAdapter

from ape_safe.client.types import (
Expand All @@ -21,6 +19,10 @@
)
from ape_safe.exceptions import ClientResponseError

if TYPE_CHECKING:
from ape.types import AddressType, MessageSignature
from requests import Response

DEFAULT_HEADERS = {
"Accept": "application/json",
"Content-Type": "application/json",
Expand Down Expand Up @@ -48,19 +50,19 @@ def get_confirmations(self, safe_tx_hash: SafeTxID) -> Iterator[SafeTxConfirmati

@abstractmethod
def post_transaction(
self, safe_tx: SafeTx, signatures: dict[AddressType, MessageSignature], **kwargs
self, safe_tx: SafeTx, signatures: dict["AddressType", "MessageSignature"], **kwargs
): ...

@abstractmethod
def post_signatures(
self,
safe_tx_or_hash: Union[SafeTx, SafeTxID],
signatures: dict[AddressType, MessageSignature],
signatures: dict["AddressType", "MessageSignature"],
): ...

@abstractmethod
def estimate_gas_cost(
self, receiver: AddressType, value: int, data: bytes, operation: int = 0
self, receiver: "AddressType", value: int, data: bytes, operation: int = 0
) -> Optional[int]: ...

"""Shared methods"""
Expand All @@ -71,7 +73,7 @@ def get_transactions(
starting_nonce: int = 0,
ending_nonce: Optional[int] = None,
filter_by_ids: Optional[set[SafeTxID]] = None,
filter_by_missing_signers: Optional[set[AddressType]] = None,
filter_by_missing_signers: Optional[set["AddressType"]] = None,
) -> Iterator[SafeApiTxData]:
"""
confirmed: Confirmed if True, not confirmed if False, both if None
Expand Down Expand Up @@ -126,17 +128,17 @@ def session(self) -> requests.Session:
session.mount("https://", adapter)
return session

def _get(self, url: str) -> Response:
def _get(self, url: str) -> "Response":
return self._request("GET", url)

def _post(self, url: str, json: Optional[dict] = None, **kwargs) -> Response:
def _post(self, url: str, json: Optional[dict] = None, **kwargs) -> "Response":
return self._request("POST", url, json=json, **kwargs)

@cached_property
def _http(self):
return urllib3.PoolManager(ca_certs=certifi.where())

def _request(self, method: str, url: str, json: Optional[dict] = None, **kwargs) -> Response:
def _request(self, method: str, url: str, json: Optional[dict] = None, **kwargs) -> "Response":
# NOTE: paged requests include full url already
if url.startswith(f"{self.transaction_service_url}/api/v1/"):
api_url = url
Expand Down
20 changes: 11 additions & 9 deletions ape_safe/client/mock.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from collections.abc import Iterator
from datetime import datetime, timezone
from typing import Optional, Union, cast
from typing import TYPE_CHECKING, Optional, Union, cast

from ape.contracts import ContractInstance
from ape.types import AddressType, MessageSignature
from ape.utils import ZERO_ADDRESS, ManagerAccessMixin
from eth_utils import keccak
from hexbytes import HexBytes
Expand All @@ -20,9 +18,13 @@
)
from ape_safe.utils import get_safe_tx_hash

if TYPE_CHECKING:
from ape.contracts import ContractInstance
from ape.types import AddressType, MessageSignature


class MockSafeClient(BaseSafeClient, ManagerAccessMixin):
def __init__(self, contract: ContractInstance):
def __init__(self, contract: "ContractInstance"):
self.contract = contract
self.transactions: dict[SafeTxID, SafeApiTxData] = {}
self.transactions_by_nonce: dict[int, list[SafeTxID]] = {}
Expand All @@ -47,13 +49,13 @@ def safe_details(self) -> SafeDetails:
)

@property
def guard(self) -> AddressType:
def guard(self) -> "AddressType":
return (
self.contract.getGuard() if "getGuard" in self.contract._view_methods_ else ZERO_ADDRESS
)

@property
def modules(self) -> list[AddressType]:
def modules(self) -> list["AddressType"]:
return self.contract.getModules() if "getModules" in self.contract._view_methods_ else []

def get_next_nonce(self) -> int:
Expand All @@ -73,7 +75,7 @@ def get_confirmations(self, safe_tx_hash: SafeTxID) -> Iterator[SafeTxConfirmati
yield from safe_tx_data.confirmations

def post_transaction(
self, safe_tx: SafeTx, signatures: dict[AddressType, MessageSignature], **kwargs
self, safe_tx: SafeTx, signatures: dict["AddressType", "MessageSignature"], **kwargs
):
safe_tx_data = UnexecutedTxData.from_safe_tx(safe_tx, self.safe_details.threshold)
safe_tx_data.confirmations.extend(
Expand All @@ -95,7 +97,7 @@ def post_transaction(
def post_signatures(
self,
safe_tx_or_hash: Union[SafeTx, SafeTxID],
signatures: dict[AddressType, MessageSignature],
signatures: dict["AddressType", "MessageSignature"],
):
for signer, signature in signatures.items():
safe_tx_id = (
Expand All @@ -114,6 +116,6 @@ def post_signatures(
)

def estimate_gas_cost(
self, receiver: AddressType, value: int, data: bytes, operation: int = 0
self, receiver: "AddressType", value: int, data: bytes, operation: int = 0
) -> Optional[int]:
return None # Estimate gas normally
8 changes: 8 additions & 0 deletions ape_safe/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from typing import Optional

from ape.api import PluginConfig


class SafeConfig(PluginConfig):
default_safe: Optional[str] = None
"""Alias of the default safe."""
Loading

0 comments on commit 0846ac5

Please sign in to comment.