diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ff57a2..873032a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 @@ -10,25 +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 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.13.0 hooks: - id: mypy additional_dependencies: [types-requests, 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] diff --git a/ape_safe/__init__.py b/ape_safe/__init__.py index 4407700..90fd8b2 100644 --- a/ape_safe/__init__.py +++ b/ape_safe/__init__.py @@ -1,11 +1,9 @@ -from typing import Optional +from importlib import import_module +from typing import Any, Optional from ape import plugins from ape.api import PluginConfig -from .accounts import SafeAccount, SafeContainer -from .multisend import MultiSend - class SafeConfig(PluginConfig): default_safe: Optional[str] = None @@ -19,10 +17,26 @@ def config_class(): @plugins.register(plugins.AccountPlugin) def account_types(): + from .accounts import SafeAccount, SafeContainer + return SafeContainer, SafeAccount +def __getattr__(name: str) -> Any: + if name == "MultiSend": + from .multisend import MultiSend + + return MultiSend + + elif name in ("SafeAccount", "SafeContainer"): + return getattr(import_module("ape_safe.accounts"), name) + + else: + raise AttributeError(name) + + __all__ = [ "MultiSend", "SafeAccount", + "SafeContainer", ] diff --git a/ape_safe/_cli/click_ext.py b/ape_safe/_cli/click_ext.py index ecb8a09..9ff82e3 100644 --- a/ape_safe/_cli/click_ext.py +++ b/ape_safe/_cli/click_ext.py @@ -1,5 +1,5 @@ from collections.abc import Sequence -from typing import NoReturn, Optional, Union, cast +from typing import TYPE_CHECKING, NoReturn, Optional, Union, cast import click from ape.api import AccountAPI @@ -8,15 +8,18 @@ from ape.utils import ManagerAccessMixin from click import BadOptionUsage, MissingParameter -from ape_safe.accounts import SafeContainer +if TYPE_CHECKING: + from ape_safe.accounts import SafeContainer class SafeCliContext(ApeCliContextObject): @property - def safes(self) -> SafeContainer: + def safes(self) -> "SafeContainer": # NOTE: Would only happen in local development of this plugin. assert "safe" in self.account_manager.containers, "Are all API methods implemented?" + from ape_safe.accounts import SafeContainer + safe_container = self.account_manager.containers["safe"] return cast(SafeContainer, safe_container) diff --git a/ape_safe/_cli/pending.py b/ape_safe/_cli/pending.py index 75229da..b5c8056 100644 --- a/ape_safe/_cli/pending.py +++ b/ape_safe/_cli/pending.py @@ -1,9 +1,8 @@ from collections.abc import Sequence -from typing import Optional, Union, cast +from typing import TYPE_CHECKING, Optional, Union, cast import click import rich -from ape.api import AccountAPI from ape.cli import ConnectedProviderCommand from ape.exceptions import SignatureError from ape.types import AddressType, MessageSignature @@ -11,7 +10,6 @@ from eth_utils import humanize_hash from hexbytes import HexBytes -from ape_safe import SafeAccount from ape_safe._cli.click_ext import ( SafeCliContext, execute_option, @@ -21,9 +19,12 @@ submitter_option, txn_ids_argument, ) -from ape_safe.accounts import get_signatures -from ape_safe.client import UnexecutedTxData -from ape_safe.utils import get_safe_tx_hash + +if TYPE_CHECKING: + from ape.api import AccountAPI + + from ape_safe.accounts import SafeAccount + from ape_safe.client import UnexecutedTxData @click.group() @@ -119,6 +120,11 @@ def propose(cli_ctx, ecosystem, safe, data, gas_price, value, receiver, nonce, s """ Create a new transaction """ + from ape.api import AccountAPI + + from ape_safe.accounts import get_signatures + from ape_safe.utils import get_safe_tx_hash + nonce = safe.new_nonce if nonce is None else nonce txn = ecosystem.create_transaction( value=value, @@ -178,6 +184,10 @@ def propose(cli_ctx, ecosystem, safe, data, gas_price, value, receiver, nonce, s @txn_ids_argument @execute_option def approve(cli_ctx: SafeCliContext, safe, txn_ids, execute): + from ape.api import AccountAPI + + from ape_safe.utils import get_safe_tx_hash + submitter: Optional[AccountAPI] = execute if isinstance(execute, AccountAPI) else None pending_transactions = list( safe.client.get_transactions(confirmed=False, starting_nonce=safe.next_nonce) @@ -258,7 +268,7 @@ def execute(cli_ctx, safe, txn_ids, submitter, nonce): cli_ctx.abort_txns_not_found(txn_ids) -def _execute(safe: SafeAccount, txn: UnexecutedTxData, submitter: AccountAPI, **tx_kwargs): +def _execute(safe: "SafeAccount", txn: "UnexecutedTxData", submitter: "AccountAPI", **tx_kwargs): 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 @@ -276,6 +286,8 @@ def reject(cli_ctx: SafeCliContext, safe, txn_ids, execute): """ Reject one or more pending transactions """ + from ape.api import AccountAPI + submit = False if execute in (False, None) else True submitter = execute if isinstance(execute, AccountAPI) else None if submitter is None and submit: @@ -369,7 +381,7 @@ def _show_confs(confs, extra_line: bool = True, prefix: Optional[str] = None): # Helper method for handling transactions in a loop. def _filter_tx_from_ids( - txn_ids: Sequence[Union[int, str]], txn: UnexecutedTxData + txn_ids: Sequence[Union[int, str]], txn: "UnexecutedTxData" ) -> Sequence[Union[int, str]]: if txn.nonce in txn_ids: # Filter out all transactions with the same nonce diff --git a/ape_safe/_cli/safe_mgmt.py b/ape_safe/_cli/safe_mgmt.py index 54700ff..65cc67c 100644 --- a/ape_safe/_cli/safe_mgmt.py +++ b/ape_safe/_cli/safe_mgmt.py @@ -7,11 +7,9 @@ skip_confirmation_option, ) from ape.exceptions import ChainError, ProviderNotConnectedError -from ape.types import AddressType from eth_typing import ChecksumAddress from ape_safe._cli.click_ext import SafeCliContext, safe_argument, safe_cli_ctx -from ape_safe.client import ExecutedTxData @click.command(name="list") @@ -90,6 +88,7 @@ def add(cli_ctx: SafeCliContext, ecosystem, network, address, alias): """ Add a Safe to locally tracked Safes """ + from ape.types import AddressType address = cli_ctx.conversion_manager.convert(address, AddressType) safe_contract = cli_ctx.chain_manager.contracts.instance_at(address) @@ -138,6 +137,10 @@ def all_txns(cli_ctx: SafeCliContext, account, confirmed): """ View and filter all transactions for a given Safe using Safe API """ + from ape.types import AddressType + + from ape_safe.client import ExecutedTxData + if account in cli_ctx.account_manager.aliases: account = cli_ctx.account_manager.load(account) diff --git a/ape_safe/client/types.py b/ape_safe/client/types.py index f3603dd..66c1cdd 100644 --- a/ape_safe/client/types.py +++ b/ape_safe/client/types.py @@ -35,9 +35,9 @@ class SignatureType(str, Enum): class SafeTxConfirmation(BaseModel): owner: AddressType submission_date: datetime = Field(alias="submissionDate") - transaction_hash: Optional[HexBytes] = Field(None, alias="transactionHash") + transaction_hash: Optional[HexBytes] = Field(default=None, alias="transactionHash") signature: HexBytes - signature_type: Optional[SignatureType] = Field(None, alias="signatureType") + signature_type: Optional[SignatureType] = Field(default=None, alias="signatureType") class OperationType(int, Enum): diff --git a/pyproject.toml b/pyproject.toml index cfadd88..bde4d82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ include = '\.pyi?$' [tool.pytest.ini_options] # NOTE: can't use xdist addopts = """ + -p no:pytest_ethereum -n 0 --cov-branch --cov-report term diff --git a/setup.py b/setup.py index 06d3a05..fbf7aac 100644 --- a/setup.py +++ b/setup.py @@ -11,13 +11,13 @@ "ape-solidity>=0.8", # Needed for compiling the Safe contracts ], "lint": [ - "black>=24.8.0,<25", # Auto-formatter and linter - "mypy>=1.11.1,<2", # Static type analyzer + "black>=24.10.0,<25", # Auto-formatter and linter + "mypy>=1.13.0,<2", # Static type analyzer "types-requests", # Needed for mypy type shed "types-setuptools", # Needed for mypy type shed "flake8>=7.1.1,<8", # Style linter "isort>=5.13.2,<6", # Import sorting linter - "mdformat>=0.7.17,<0.8", # Docs formatter and linter + "mdformat>=0.7.18,<0.8", # Docs formatter and linter "mdformat-pyproject>=0.0.1", # Allows configuring in pyproject.toml ], "release": [ # `release` GitHub Action job uses this