Skip to content
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
22 changes: 21 additions & 1 deletion src/ape/api/convert.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from abc import abstractmethod
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar

from ape.utils.basemodel import BaseInterfaceModel
Expand Down Expand Up @@ -56,3 +56,23 @@ def name(self) -> str:
class_name = self.__class__.__name__
name = class_name.replace("Converter", "").replace("Conversions", "")
return name.lower()


class ConvertibleAPI(ABC):
"""
Use this base-class mixin if you want your custom class to be convertible to a more basic type
without having to register a converter plugin for it.
"""

@abstractmethod
def is_convertible(self, to_type: type) -> bool:
"""
Returns ``True`` if ``self`` can be converted to ``to_type``.
"""

@abstractmethod
def convert_to(self, to_type: type) -> Any:
"""
Convert ``self`` to the given type. Implementing classes _should_ raise ``ConversionError`` if not convertible.
Ape's conversion system will **only** attempt to convert classes where ``.is_convertible()`` returns ``True``.
"""
8 changes: 7 additions & 1 deletion src/ape/managers/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from eth_utils import is_0x_prefixed, is_checksum_address, is_hex, is_hex_address, to_int

from ape.api.address import BaseAddress
from ape.api.convert import ConverterAPI
from ape.api.convert import ConverterAPI, ConvertibleAPI
from ape.api.transactions import TransactionAPI
from ape.exceptions import ConversionError
from ape.logging import logger
Expand Down Expand Up @@ -365,6 +365,12 @@ def convert(self, value: Any, to_type: Union[type, tuple, list]) -> Any:
# NOTE: Always process lists and tuples
return value

if isinstance(value, ConvertibleAPI) and value.is_convertible(to_type):
return value.convert_to(to_type)

return self._convert_using_converter_apis(value, to_type)

def _convert_using_converter_apis(self, value: Any, to_type: type) -> Any:
for converter in self._converters[to_type]:
try:
is_convertible = converter.is_convertible(value)
Expand Down
6 changes: 5 additions & 1 deletion src/ape/utils/basemodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from pydantic import BaseModel as RootBaseModel
from pydantic import ConfigDict

from ape.exceptions import ApeAttributeError, ApeIndexError, ProviderNotConnectedError
from ape.exceptions import (
ApeAttributeError,
ApeIndexError,
ProviderNotConnectedError,
)
from ape.logging import logger
from ape.utils.misc import log_instead_of_fail, raises_not_implemented

Expand Down
32 changes: 32 additions & 0 deletions tests/functional/conversion/test_custom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Any

import pytest

from ape.api.convert import ConvertibleAPI
from ape.types.address import AddressType


@pytest.fixture(scope="module")
def custom_type(accounts):
class MyAccountWrapper(ConvertibleAPI):
def __init__(self, acct):
self.acct = acct

def is_convertible(self, to_type: type) -> bool:
return to_type is AddressType

def convert_to(self, to_type: type) -> Any:
if to_type is AddressType:
return self.acct.address

raise NotImplementedError()

return MyAccountWrapper(accounts[0])


def test_convert(custom_type, conversion_manager, accounts):
"""
You can use the regular conversion manager to convert the custom type.
"""
actual = conversion_manager.convert(custom_type, AddressType)
assert actual == accounts[0].address
Loading