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 when safe installed pt. 2 #64

Merged
merged 2 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading