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

feat: add conversion for fixed ABI types #1946

Merged
merged 8 commits into from
Mar 7, 2024
19 changes: 18 additions & 1 deletion src/ape/managers/converters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, List, Sequence, Tuple, Type, Union
Expand Down Expand Up @@ -185,6 +186,22 @@ def convert(self, value: Union[str, datetime, timedelta]) -> int:
raise ConversionError()


class StringDecimalConverter(ConverterAPI):
"""
Convert string-formatted floating point values to `Decimal` type.
"""

def is_convertible(self, value: Any) -> bool:
# Matches only string-formatted floats with an optional sign character (+/-).
# Leading and trailing zeros are required.
# NOTE: `re.fullmatch` will only match the full string, so "1.0 ether" and "10.0 USDC"
# will not be identified as convertible.
return isinstance(value, str) and re.fullmatch(r"[+-]?\d+\.\d+", value) is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re.fullmatch is the same as checking the full string? That's helpful because we do already have conversion sequences like "1.0 ether" and "10.0 USDC"

Can you add a comment to this effect?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, re.fullmatch will check the full string - it should be equivalent to sandwiching the above expression with ^...$ but I'm not experienced enough with regex to know for sure if that expression has edge cases.

Added a note to the comment with b31222a


def convert(self, value: str) -> Decimal:
return Decimal(value)


class ConversionManager(BaseManager):
"""
A singleton that manages all the converters.
Expand Down Expand Up @@ -219,7 +236,7 @@ def _converters(self) -> Dict[Type, List[ConverterAPI]]:
StringIntConverter(),
AccountIntConverter(),
],
Decimal: [],
Decimal: [StringDecimalConverter()],
bool: [],
str: [],
}
Expand Down
4 changes: 4 additions & 0 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
from copy import deepcopy
from decimal import Decimal
from functools import cached_property
from typing import Any, ClassVar, Dict, Iterator, List, Optional, Sequence, Tuple, Type, Union, cast

Expand Down Expand Up @@ -582,6 +583,9 @@ def _python_type_for_abi_type(self, abi_type: ABIType) -> Union[Type, Sequence]:
elif "int" in abi_type.type:
return int

elif "fixed" in abi_type.type:
return Decimal

raise ConversionError(f"Unable to convert '{abi_type}'.")

def encode_calldata(self, abi: Union[ConstructorABI, MethodABI], *args) -> HexBytes:
Expand Down
83 changes: 83 additions & 0 deletions tests/functional/conversion/test_decimal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from decimal import Decimal

import pytest

from ape.exceptions import ConversionError


def test_convert_formatted_float_strings_to_decimal(convert):
test_strings = [
"1.000",
"1.00",
"1.0",
"0.1",
"0.01",
"0.001",
]
for test_string in test_strings:
actual = convert(test_string, Decimal)
assert actual == Decimal(test_string)


def test_convert_badly_formatted_float_strings_to_decimal(convert):
test_strings = [
".1",
"1.",
".",
]
for test_string in test_strings:
with pytest.raises(
ConversionError, match=f"No conversion registered to handle '{test_string}'"
):
convert(test_string, Decimal)


def test_convert_int_strings(convert):
test_strings = [
"1",
"10",
"100",
]
for test_string in test_strings:
with pytest.raises(
ConversionError, match=f"No conversion registered to handle '{test_string}'"
):
convert(test_string, Decimal)


def test_convert_alphanumeric_strings(convert):
test_strings = [
"a",
"H",
"XYZ",
]
for test_string in test_strings:
with pytest.raises(
ConversionError, match=f"No conversion registered to handle '{test_string}'"
):
convert(test_string, Decimal)


def test_convert_strings_with_token_names(convert):
test_strings = [
"0.999 DAI",
"10.0 USDC",
]
for test_string in test_strings:
with pytest.raises(
ConversionError, match=f"No conversion registered to handle '{test_string}'"
):
convert(test_string, Decimal)


def test_convert_strings_with_ether_alias(convert):
test_strings = [
"0 wei",
"999 gwei",
"1.0 ether",
]
for test_string in test_strings:
with pytest.raises(
ConversionError, match=f"No conversion registered to handle '{test_string}'"
):
convert(test_string, Decimal)
Loading