diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8aca554..25f459b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,8 +4,8 @@ repos: hooks: - id: check-yaml -- repo: https://github.com/pre-commit/mirrors-isort - rev: v5.10.1 +- repo: https://github.com/PyCQA/isort + rev: 5.13.2 hooks: - id: isort diff --git a/README.md b/README.md index bdd1d59..896cb0a 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,19 @@ The types in this package are pydantic types for Ethereum inspired from [eth-typ ## Hash -`HashBytes{n}` and `HashStr{n}` are good types to use when your hex values are sized. +`Bytes{n}` and `String{n}` are good types to use when your hex values are sized. Both types serialize to `string` in the JSON schema. -Use `HashBytes` types when you want types to serialize to bytes in the Pydantic core schema and `HashStr` types when you want to serialize to `str` in the core Pydantic schema. +Use `Bytes` types when you want types to serialize to bytes in the Pydantic core schema and `String` types when you want to serialize to `str` in the core Pydantic schema. ```python from pydantic import BaseModel -from eth_pydantic_types import HashBytes32, HashStr20 +from eth_pydantic_types import Bytes32, String20 # When serializing to JSON, both types are hex strings. class Transaction(BaseModel): - tx_hash: HashBytes32 # Will be bytes - address: HashStr20 # Will be str + tx_hash: Bytes32 # Will be bytes + address: String20 # Will be str # NOTE: I am able to pass an int-hash as the value and it will diff --git a/eth_pydantic_types/_main.py b/eth_pydantic_types/_main.py index 4c74729..ec9c25b 100644 --- a/eth_pydantic_types/_main.py +++ b/eth_pydantic_types/_main.py @@ -1,37 +1,15 @@ from .address import Address, AddressType from .bip122 import Bip122Uri -from .hash import ( - HashBytes4, - HashBytes8, - HashBytes16, - HashBytes20, - HashBytes32, - HashBytes64, - HashStr4, - HashStr8, - HashStr16, - HashStr20, - HashStr32, - HashStr64, -) -from .hex import HexBytes, HexStr +from .hex import HexBytes, HexBytes20, HexBytes32, HexStr, HexStr20, HexStr32 __all__ = [ "Address", "AddressType", "Bip122Uri", - "HashBytes4", - "HashBytes8", - "HashBytes16", - "HashBytes20", - "HashBytes32", - "HashBytes64", - "HashStr4", - "HashStr8", - "HashStr16", - "HashStr20", - "HashStr32", - "HashStr64", "HexBytes", + "HexBytes20", + "HexBytes32", "HexStr", + "HexStr20", + "HexStr32", ] diff --git a/eth_pydantic_types/abi.py b/eth_pydantic_types/abi.py new file mode 100644 index 0000000..fccfc58 --- /dev/null +++ b/eth_pydantic_types/abi.py @@ -0,0 +1,209 @@ +""" +These models are used to match the lowercase type names used by the abi. +""" + +from typing import ClassVar + +from pydantic import Field +from typing_extensions import Annotated, TypeAliasType + +from .address import Address +from .hex import BoundHexBytes, HexBytes + +bytes = TypeAliasType("bytes", HexBytes) +string = TypeAliasType("string", str) +address = TypeAliasType("address", Address) + + +class bytes1(BoundHexBytes): + size: ClassVar[int] = 1 + + +class bytes2(BoundHexBytes): + size: ClassVar[int] = 2 + + +class bytes3(BoundHexBytes): + size: ClassVar[int] = 3 + + +class bytes4(BoundHexBytes): + size: ClassVar[int] = 4 + + +class bytes5(BoundHexBytes): + size: ClassVar[int] = 5 + + +class bytes6(BoundHexBytes): + size: ClassVar[int] = 6 + + +class bytes7(BoundHexBytes): + size: ClassVar[int] = 7 + + +class bytes8(BoundHexBytes): + size: ClassVar[int] = 8 + + +class bytes9(BoundHexBytes): + size: ClassVar[int] = 9 + + +class bytes10(BoundHexBytes): + size: ClassVar[int] = 10 + + +class bytes11(BoundHexBytes): + size: ClassVar[int] = 11 + + +class bytes12(BoundHexBytes): + size: ClassVar[int] = 12 + + +class bytes13(BoundHexBytes): + size: ClassVar[int] = 13 + + +class bytes14(BoundHexBytes): + size: ClassVar[int] = 14 + + +class bytes15(BoundHexBytes): + size: ClassVar[int] = 15 + + +class bytes16(BoundHexBytes): + size: ClassVar[int] = 16 + + +class bytes17(BoundHexBytes): + size: ClassVar[int] = 17 + + +class bytes18(BoundHexBytes): + size: ClassVar[int] = 18 + + +class bytes19(BoundHexBytes): + size: ClassVar[int] = 19 + + +class bytes20(BoundHexBytes): + size: ClassVar[int] = 20 + + +class bytes21(BoundHexBytes): + size: ClassVar[int] = 21 + + +class bytes22(BoundHexBytes): + size: ClassVar[int] = 22 + + +class bytes23(BoundHexBytes): + size: ClassVar[int] = 23 + + +class bytes24(BoundHexBytes): + size: ClassVar[int] = 24 + + +class bytes25(BoundHexBytes): + size: ClassVar[int] = 25 + + +class bytes26(BoundHexBytes): + size: ClassVar[int] = 26 + + +class bytes27(BoundHexBytes): + size: ClassVar[int] = 27 + + +class bytes28(BoundHexBytes): + size: ClassVar[int] = 28 + + +class bytes29(BoundHexBytes): + size: ClassVar[int] = 29 + + +class bytes30(BoundHexBytes): + size: ClassVar[int] = 30 + + +class bytes31(BoundHexBytes): + size: ClassVar[int] = 31 + + +class bytes32(BoundHexBytes): + size: ClassVar[int] = 32 + + +int8 = TypeAliasType("int8", Annotated[int, Field(lt=2**7, ge=-(2**7))]) +int16 = TypeAliasType("int16", Annotated[int, Field(lt=2**15, ge=-(2**15))]) +int24 = TypeAliasType("int24", Annotated[int, Field(lt=2**23, ge=-(2**23))]) +int32 = TypeAliasType("int32", Annotated[int, Field(lt=2**31, ge=-(2**31))]) +int40 = TypeAliasType("int40", Annotated[int, Field(lt=2**39, ge=-(2**39))]) +int48 = TypeAliasType("int48", Annotated[int, Field(lt=2**47, ge=-(2**47))]) +int56 = TypeAliasType("int56", Annotated[int, Field(lt=2**55, ge=-(2**55))]) +int64 = TypeAliasType("int64", Annotated[int, Field(lt=2**63, ge=-(2**63))]) +int72 = TypeAliasType("int72", Annotated[int, Field(lt=2**71, ge=-(2**71))]) +int80 = TypeAliasType("int80", Annotated[int, Field(lt=2**79, ge=-(2**79))]) +int88 = TypeAliasType("int88", Annotated[int, Field(lt=2**87, ge=-(2**87))]) +int96 = TypeAliasType("int96", Annotated[int, Field(lt=2**95, ge=-(2**95))]) +int104 = TypeAliasType("int104", Annotated[int, Field(lt=2**103, ge=-(2**103))]) +int112 = TypeAliasType("int112", Annotated[int, Field(lt=2**111, ge=-(2**111))]) +int120 = TypeAliasType("int120", Annotated[int, Field(lt=2**119, ge=-(2**119))]) +int128 = TypeAliasType("int128", Annotated[int, Field(lt=2**127, ge=-(2**127))]) +int136 = TypeAliasType("int136", Annotated[int, Field(lt=2**135, ge=-(2**135))]) +int144 = TypeAliasType("int144", Annotated[int, Field(lt=2**143, ge=-(2**143))]) +int152 = TypeAliasType("int152", Annotated[int, Field(lt=2**151, ge=-(2**151))]) +int160 = TypeAliasType("int160", Annotated[int, Field(lt=2**159, ge=-(2**159))]) +int168 = TypeAliasType("int168", Annotated[int, Field(lt=2**167, ge=-(2**167))]) +int176 = TypeAliasType("int176", Annotated[int, Field(lt=2**175, ge=-(2**175))]) +int184 = TypeAliasType("int184", Annotated[int, Field(lt=2**183, ge=-(2**183))]) +int192 = TypeAliasType("int192", Annotated[int, Field(lt=2**191, ge=-(2**191))]) +int200 = TypeAliasType("int200", Annotated[int, Field(lt=2**199, ge=-(2**199))]) +int208 = TypeAliasType("int208", Annotated[int, Field(lt=2**207, ge=-(2**207))]) +int216 = TypeAliasType("int216", Annotated[int, Field(lt=2**215, ge=-(2**215))]) +int224 = TypeAliasType("int224", Annotated[int, Field(lt=2**223, ge=-(2**223))]) +int232 = TypeAliasType("int232", Annotated[int, Field(lt=2**231, ge=-(2**231))]) +int240 = TypeAliasType("int240", Annotated[int, Field(lt=2**239, ge=-(2**239))]) +int248 = TypeAliasType("int248", Annotated[int, Field(lt=2**247, ge=-(2**247))]) +int256 = TypeAliasType("int256", Annotated[int, Field(lt=2**255, ge=-(2**255))]) +uint8 = TypeAliasType("uint8", Annotated[int, Field(lt=2**8, ge=0)]) +uint16 = TypeAliasType("uint16", Annotated[int, Field(lt=2**16, ge=0)]) +uint24 = TypeAliasType("uint24", Annotated[int, Field(lt=2**24, ge=0)]) +uint32 = TypeAliasType("uint32", Annotated[int, Field(lt=2**32, ge=0)]) +uint40 = TypeAliasType("uint40", Annotated[int, Field(lt=2**40, ge=0)]) +uint48 = TypeAliasType("uint48", Annotated[int, Field(lt=2**48, ge=0)]) +uint56 = TypeAliasType("uint56", Annotated[int, Field(lt=2**56, ge=0)]) +uint64 = TypeAliasType("uint64", Annotated[int, Field(lt=2**64, ge=0)]) +uint72 = TypeAliasType("uint72", Annotated[int, Field(lt=2**72, ge=0)]) +uint80 = TypeAliasType("uint80", Annotated[int, Field(lt=2**80, ge=0)]) +uint88 = TypeAliasType("uint88", Annotated[int, Field(lt=2**88, ge=0)]) +uint96 = TypeAliasType("uint96", Annotated[int, Field(lt=2**96, ge=0)]) +uint104 = TypeAliasType("uint104", Annotated[int, Field(lt=2**104, ge=0)]) +uint112 = TypeAliasType("uint112", Annotated[int, Field(lt=2**112, ge=0)]) +uint120 = TypeAliasType("uint120", Annotated[int, Field(lt=2**120, ge=0)]) +uint128 = TypeAliasType("uint128", Annotated[int, Field(lt=2**128, ge=0)]) +uint136 = TypeAliasType("uint136", Annotated[int, Field(lt=2**136, ge=0)]) +uint144 = TypeAliasType("uint144", Annotated[int, Field(lt=2**144, ge=0)]) +uint152 = TypeAliasType("uint152", Annotated[int, Field(lt=2**152, ge=0)]) +uint160 = TypeAliasType("uint160", Annotated[int, Field(lt=2**160, ge=0)]) +uint168 = TypeAliasType("uint168", Annotated[int, Field(lt=2**168, ge=0)]) +uint176 = TypeAliasType("uint176", Annotated[int, Field(lt=2**176, ge=0)]) +uint184 = TypeAliasType("uint184", Annotated[int, Field(lt=2**184, ge=0)]) +uint192 = TypeAliasType("uint192", Annotated[int, Field(lt=2**192, ge=0)]) +uint200 = TypeAliasType("uint200", Annotated[int, Field(lt=2**200, ge=0)]) +uint208 = TypeAliasType("uint208", Annotated[int, Field(lt=2**208, ge=0)]) +uint216 = TypeAliasType("uint216", Annotated[int, Field(lt=2**216, ge=0)]) +uint224 = TypeAliasType("uint224", Annotated[int, Field(lt=2**224, ge=0)]) +uint232 = TypeAliasType("uint232", Annotated[int, Field(lt=2**232, ge=0)]) +uint240 = TypeAliasType("uint240", Annotated[int, Field(lt=2**240, ge=0)]) +uint248 = TypeAliasType("uint248", Annotated[int, Field(lt=2**248, ge=0)]) +uint256 = TypeAliasType("uint256", Annotated[int, Field(lt=2**256, ge=0)]) diff --git a/eth_pydantic_types/address.py b/eth_pydantic_types/address.py index a8af9c6..09fc835 100644 --- a/eth_pydantic_types/address.py +++ b/eth_pydantic_types/address.py @@ -1,11 +1,18 @@ from functools import cached_property -from typing import Annotated, Any, ClassVar, Optional +from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Optional from cchecksum import to_checksum_address from eth_typing import ChecksumAddress -from pydantic_core.core_schema import ValidationInfo, str_schema +from pydantic_core.core_schema import ( + ValidationInfo, + str_schema, + with_info_before_validator_function, +) -from eth_pydantic_types.hash import HashStr20 +from eth_pydantic_types.hex import HexStr20 + +if TYPE_CHECKING: + from pydantic_core import CoreSchema ADDRESS_PATTERN = "^0x[a-fA-F0-9]{40}$" @@ -14,7 +21,7 @@ def address_schema(): return str_schema(min_length=42, max_length=42, pattern=ADDRESS_PATTERN) -class Address(HashStr20): +class Address(HexStr20): """ Use for address-types. Validates as a checksummed address. Left-pads zeroes if necessary. @@ -28,11 +35,23 @@ class Address(HashStr20): "0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C", ) + @classmethod + def __get_pydantic_core_schema__(cls, value, handler=None) -> "CoreSchema": + return with_info_before_validator_function( + cls.__eth_pydantic_validate__, + address_schema(), + ) + @classmethod def __eth_pydantic_validate__(cls, value: Any, info: Optional[ValidationInfo] = None) -> str: value = super().__eth_pydantic_validate__(value) return cls.to_checksum_address(value) + @classmethod + def update_schema(cls): + # Already set statically in the class + return + @classmethod def to_checksum_address(cls, value: str) -> ChecksumAddress: return to_checksum_address(value) diff --git a/eth_pydantic_types/hash.py b/eth_pydantic_types/hash.py deleted file mode 100644 index 3787d76..0000000 --- a/eth_pydantic_types/hash.py +++ /dev/null @@ -1,137 +0,0 @@ -from typing import TYPE_CHECKING, Any, ClassVar, Optional - -from pydantic_core.core_schema import bytes_schema, str_schema, with_info_before_validator_function - -from eth_pydantic_types.hex import BaseHexStr, HexBytes -from eth_pydantic_types.serializers import hex_serializer -from eth_pydantic_types.validators import validate_bytes_size, validate_str_size - -if TYPE_CHECKING: - from pydantic_core.core_schema import CoreSchema, ValidationInfo - - -def _get_hash_pattern(str_size: int) -> str: - return f"^0x[a-fA-F0-9]{{{str_size}}}$" - - -def _get_hash_examples(str_size: int) -> tuple[str, str, str, str]: - zero_hash = f"0x{'0' * str_size}" - leading_zero = f"0x01{'1e' * ((str_size - 1) // 2)}" - trailing_zero = f"0x{'1e' * ((str_size - 1) // 2)}10" - full_hash = f"0x{'1e' * (str_size // 2)}" - return zero_hash, leading_zero, trailing_zero, full_hash - - -class HashBytes(HexBytes): - """ - Represents a single-slot static hash as bytes. - This type is meant to be overridden by the larger hash types with a new size. - The class variable "size" is overridden in subclasses for each byte-size, - e.g. HashBytes20, HashBytes32. - """ - - size: ClassVar[int] = 1 - schema_pattern: ClassVar[str] = _get_hash_pattern(1) - schema_examples: ClassVar[tuple[str, ...]] = _get_hash_examples(1) - - @classmethod - def __get_pydantic_core_schema__(cls, value, handler=None) -> "CoreSchema": - schema = with_info_before_validator_function( - cls.__eth_pydantic_validate__, - bytes_schema(max_length=cls.size, min_length=cls.size), - ) - schema["serialization"] = hex_serializer - return schema - - @classmethod - def __eth_pydantic_validate__( - cls, value: Any, info: Optional["ValidationInfo"] = None - ) -> HexBytes: - return cls(cls.validate_size(HexBytes(value))) - - @classmethod - def validate_size(cls, value: bytes) -> bytes: - return validate_bytes_size(value, cls.size) - - -class HashStr(BaseHexStr): - """ - Represents a single-slot static hash as a str. - This type is meant to be overridden by the larger hash types with a new size. - e.g. HashStr20, HashStr32. - """ - - size: ClassVar[int] = 1 - schema_pattern: ClassVar[str] = _get_hash_pattern(1) - schema_examples: ClassVar[tuple[str, ...]] = _get_hash_examples(1) - - @classmethod - def __get_pydantic_core_schema__(cls, value, handler=None) -> "CoreSchema": - str_size = cls.size * 2 + 2 - return with_info_before_validator_function( - cls.__eth_pydantic_validate__, str_schema(max_length=str_size, min_length=str_size) - ) - - @classmethod - def __eth_pydantic_validate__(cls, value: Any, info: Optional["ValidationInfo"] = None) -> str: - hex_str = cls.validate_hex(value) - hex_value = hex_str[2:] if hex_str.startswith("0x") else hex_str - sized_value = cls.validate_size(hex_value) - return cls(f"0x{sized_value}") - - @classmethod - def validate_size(cls, value: str) -> str: - return validate_str_size(value, cls.size * 2) - - -def _make_hash_cls(size: int, base_type: type): - if issubclass(base_type, bytes): - suffix = "Bytes" - base_type = HashBytes - else: - suffix = "Str" - base_type = HashStr - - str_size = size * 2 - return type( - f"Hash{suffix}{size}", - (base_type,), - dict( - size=size, - schema_pattern=_get_hash_pattern(str_size), - schema_examples=_get_hash_examples(str_size), - ), - ) - - -def __getattr__(name: str): - _type: type - if name.startswith("HashBytes"): - number = name.replace("HashBytes", "") - _type = bytes - elif name.startswith("HashStr"): - number = name.replace("HashStr", "") - _type = str - else: - raise AttributeError(name) - - if not number.isnumeric(): - raise AttributeError(name) - - return _make_hash_cls(int(number), _type) - - -__all__ = [ - "HashBytes4", - "HashBytes8", - "HashBytes16", - "HashBytes20", - "HashBytes32", - "HashBytes64", - "HashStr4", - "HashStr8", - "HashStr16", - "HashStr20", - "HashStr32", - "HashStr64", -] diff --git a/eth_pydantic_types/hex.py b/eth_pydantic_types/hex.py index cd3b88d..2011849 100644 --- a/eth_pydantic_types/hex.py +++ b/eth_pydantic_types/hex.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union -from hexbytes import HexBytes as BaseHexBytes +from hexbytes.main import HexBytes as BaseHexBytes from pydantic_core.core_schema import ( ValidationInfo, bytes_schema, @@ -11,9 +11,17 @@ from eth_pydantic_types._error import HexValueError from eth_pydantic_types.serializers import hex_serializer +from eth_pydantic_types.utils import ( + get_hash_examples, + get_hash_pattern, + validate_bytes_size, + validate_hex_str, + validate_str_size, +) if TYPE_CHECKING: from pydantic_core import CoreSchema + from typing_extensions import TypeAlias schema_pattern = "^0x([0-9a-f][0-9a-f])*$" @@ -29,6 +37,7 @@ class BaseHex: + size: ClassVar[int] = 0 schema_pattern: ClassVar[str] = schema_pattern schema_examples: ClassVar[tuple[str, ...]] = schema_examples @@ -62,7 +71,43 @@ def fromhex(cls, hex_str: str) -> "HexBytes": def __eth_pydantic_validate__( cls, value: Any, info: Optional[ValidationInfo] = None ) -> BaseHexBytes: - return BaseHexBytes(value) + return cls(cls.validate_size(HexBytes(value))) + + @classmethod + def validate_size(cls, value: bytes) -> bytes: + return value + + +class BoundHexBytes(HexBytes): + """ + Use when receiving ``hexbytes.HexBytes`` values and a specific size is required. + Includes a pydantic validator and serializer. + """ + + size: ClassVar[int] = 32 + + @classmethod + def __get_pydantic_core_schema__(cls, value, handle=None) -> "CoreSchema": + schema = with_info_before_validator_function( + cls.__eth_pydantic_validate__, + bytes_schema(max_length=cls.size, min_length=cls.size), + ) + schema["serialization"] = hex_serializer + return schema + + @classmethod + def validate_size(cls, value: bytes) -> bytes: + str_size = cls.size * 2 + cls.schema_pattern = get_hash_pattern(str_size) + cls.schema_examples = get_hash_examples(str_size) + return validate_bytes_size(value, cls.size) + + +class HexBytes20(BoundHexBytes): + size: ClassVar[int] = 20 + + +HexBytes32: "TypeAlias" = BoundHexBytes class BaseHexStr(str, BaseHex): @@ -105,8 +150,18 @@ class HexStr(BaseHexStr): """A hex string value, typically from a hash.""" @classmethod - def __eth_pydantic_validate__(cls, value): - return cls.validate_hex(value) + def __get_pydantic_core_schema__(cls, value, handler=None) -> "CoreSchema": + return with_info_before_validator_function( + cls.__eth_pydantic_validate__, + str_schema(), + ) + + @classmethod + def __eth_pydantic_validate__(cls, value: Any, info: Optional[ValidationInfo] = None) -> str: + hex_str = cls.validate_hex(value) + hex_value = hex_str[2:] if hex_str.startswith("0x") else hex_str + sized_value = hex_value + return cls(f"0x{sized_value}") @classmethod def from_bytes(cls, data: bytes) -> "HexStr": @@ -115,13 +170,40 @@ def from_bytes(cls, data: bytes) -> "HexStr": return HexStr(value) -def validate_hex_str(value: str) -> str: - hex_value = (value[2:] if value.startswith("0x") else value).lower() - if set(hex_value) - set("1234567890abcdef"): - raise HexValueError(value) +class BoundHexStr(BaseHexStr): + """A hex string value, typically from a hash, that is required to be a specific size.""" + + size: ClassVar[int] = 32 + + @classmethod + def __get_pydantic_core_schema__(cls, value, handler=None) -> "CoreSchema": + str_size = cls.size * 2 + 2 + return with_info_before_validator_function( + cls.__eth_pydantic_validate__, + str_schema(max_length=str_size, min_length=str_size), + ) + + @classmethod + def __eth_pydantic_validate__(cls, value: Any, info: Optional[ValidationInfo] = None) -> str: + hex_str = cls.validate_hex(value) + hex_value = hex_str[2:] if hex_str.startswith("0x") else hex_str + sized_value = cls.validate_size(hex_value) + return cls(f"0x{sized_value}") + + @classmethod + def validate_size(cls, value: str) -> str: + cls.update_schema() + return validate_str_size(value, cls.size * 2) + + @classmethod + def update_schema(cls): + str_size = cls.size * 2 + cls.schema_pattern = get_hash_pattern(str_size) + cls.schema_examples = get_hash_examples(str_size) + + +class HexStr20(BoundHexStr): + size: ClassVar[int] = 20 - # Missing zero padding. - if len(hex_value) % 2 != 0: - hex_value = f"0{hex_value}" - return f"0x{hex_value}" +HexStr32: "TypeAlias" = BoundHexStr diff --git a/eth_pydantic_types/validators.py b/eth_pydantic_types/utils.py similarity index 53% rename from eth_pydantic_types/validators.py rename to eth_pydantic_types/utils.py index ba5f80f..944bb98 100644 --- a/eth_pydantic_types/validators.py +++ b/eth_pydantic_types/utils.py @@ -1,7 +1,7 @@ from collections.abc import Sized from typing import TYPE_CHECKING, Callable, Optional, TypeVar -from eth_pydantic_types._error import SizeError +from eth_pydantic_types._error import HexValueError, SizeError if TYPE_CHECKING: __SIZED_T = TypeVar("__SIZED_T", bound=Sized) @@ -17,6 +17,18 @@ def validate_size(value: "__SIZED_T", size: int, coerce: Optional[Callable] = No raise SizeError(size, value) +def validate_in_range(value: int, size: int, signed: bool = True) -> int: + if signed: + if value >= -(2**size) / 2 and value < (2**size) / 2: + return value + + else: + if value >= 0 and value < 2**size: + return value + + raise SizeError(size, value) + + def validate_bytes_size(value: bytes, size: int) -> bytes: return validate_size(value, size, coerce=lambda v: _coerce_hexbytes_size(v, size)) @@ -29,6 +41,10 @@ def validate_str_size(value: str, size: int) -> str: return validate_size(value, size, coerce=lambda v: _coerce_hexstr_size(v, size)) +def validate_int_size(value: int, size: int, signed: bool) -> int: + return validate_in_range(value, size, signed) + + def _coerce_hexstr_size(val: str, length: int) -> str: val = val.replace("0x", "") if val.startswith("0x") else val if len(val) == length: @@ -48,3 +64,27 @@ def _coerce_hexbytes_size(val: bytes, num_bytes: int) -> bytes: num_zeroes = max(0, num_bytes - len(val_stripped)) zeroes = b"\x00" * num_zeroes return zeroes + val_stripped + + +def validate_hex_str(value: str) -> str: + hex_value = (value[2:] if value.startswith("0x") else value).lower() + if set(hex_value) - set("1234567890abcdef"): + raise HexValueError(value) + + # Missing zero padding. + if len(hex_value) % 2 != 0: + hex_value = f"0{hex_value}" + + return f"0x{hex_value}" + + +def get_hash_pattern(str_size: int) -> str: + return f"^0x[a-fA-F0-9]{{{str_size}}}$" + + +def get_hash_examples(str_size: int) -> tuple[str, str, str, str]: + zero_hash = f"0x{'0' * str_size}" + leading_zero = f"0x01{'1e' * ((str_size - 1) // 2)}" + trailing_zero = f"0x{'1e' * ((str_size - 1) // 2)}10" + full_hash = f"0x{'1e' * (str_size // 2)}" + return zero_hash, leading_zero, trailing_zero, full_hash diff --git a/setup.py b/setup.py index 533b2ff..df02119 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ extras_require = { "test": [ # `test` GitHub Action jobs uses this + "pytest>=6.0", # Core testing package "pytest-xdist", # Multi-process runner "pytest-cov>=4.0.0,<5", # Coverage analyzer plugin "pytest-mock", # For creating mocks @@ -23,7 +24,7 @@ "flake8-print>=5.0.0,<6", # Detect print statements left in code "flake8-pydantic", # For detecting issues with Pydantic models "flake8-type-checking", # Detect imports to move in/out of type-checking blocks - "isort>=5.10.1,<6", # Import sorting linter + "isort>=5.13.2,<6", # Import sorting linter "mdformat>=0.7.19", # Auto-formatter for markdown "mdformat-gfm>=0.3.5", # Needed for formatting GitHub-flavored markdown "mdformat-frontmatter>=0.4.1", # Needed for frontmatters-style headers in issue templates diff --git a/tests/test_hash.py b/tests/test_hash.py index 0abe877..075113a 100644 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -1,77 +1,43 @@ import pytest from pydantic import BaseModel, ValidationError -from eth_pydantic_types.hash import ( - HashBytes8, - HashBytes16, - HashBytes20, - HashBytes32, - HashBytes64, - HashStr8, - HashStr16, - HashStr32, - HashStr64, -) -from eth_pydantic_types.hex import HexBytes +from eth_pydantic_types.hex import HexBytes, HexBytes20, HexBytes32, HexStr32 class Model(BaseModel): - valuebytes8: HashBytes8 - valuebytes16: HashBytes16 - valuebytes20: HashBytes20 - valuebytes32: HashBytes32 - valuebytes64: HashBytes64 - valuestr8: HashStr8 - valuestr16: HashStr16 - valuestr32: HashStr32 - valuestr64: HashStr64 + valuebytes20: HexBytes20 + valuebytes32: HexBytes32 + valuestr32: HexStr32 @classmethod def from_single(cls, value): return cls( - valuebytes8=value, - valuebytes16=value, valuebytes20=value, valuebytes32=value, - valuebytes64=value, - valuestr8=value, - valuestr16=value, valuestr32=value, - valuestr64=value, ) def test_hashbytes_fromhex(bytes32str): - actual_with_0x = HashBytes32.fromhex(bytes32str) - actual_without_0x = HashBytes32.fromhex(bytes32str[2:]) + actual_with_0x = HexBytes32.fromhex(bytes32str) + actual_without_0x = HexBytes32.fromhex(bytes32str[2:]) expected = HexBytes(bytes32str) assert actual_with_0x == actual_without_0x == expected def test_hashbytes_is_bytes(bytes32str): - assert isinstance(HashBytes32.fromhex(bytes32str), bytes) + assert isinstance(HexBytes32.fromhex(bytes32str), bytes) @pytest.mark.parametrize("value", ("0x32", HexBytes("0x32"), b"2", 50)) def test_hash(value): model = Model.from_single(value) - assert len(model.valuebytes8) == 8 - assert len(model.valuebytes16) == 16 assert len(model.valuebytes20) == 20 assert len(model.valuebytes32) == 32 - assert len(model.valuebytes64) == 64 - assert len(model.valuestr8) == 18 - assert len(model.valuestr16) == 34 assert len(model.valuestr32) == 66 - assert len(model.valuestr64) == 130 - assert model.valuebytes8.hex().endswith("32") - assert model.valuebytes16.hex().endswith("32") + assert model.valuebytes20.hex().endswith("32") assert model.valuebytes32.hex().endswith("32") - assert model.valuestr64.endswith("32") - assert model.valuestr8.endswith("32") - assert model.valuestr16.endswith("32") assert model.valuestr32.endswith("32") - assert model.valuestr64.endswith("32") @pytest.mark.parametrize("value", ("foo", -35, "0x" + ("F" * 100))) @@ -84,7 +50,7 @@ def test_hash_removes_leading_zeroes_if_needed(): address = "0x000000000000000000000000cafac3dd18ac6c6e92c921884f9e4176737c052c" class MyModel(BaseModel): - my_address: HashBytes20 + my_address: HexBytes20 # Test both str and bytes for input. for addr in (address, HexBytes(address)): @@ -122,14 +88,8 @@ def test_model_dump(bytes32str): model = Model.from_single(5) actual = model.model_dump() expected = { - "valuebytes8": "0x0000000000000005", - "valuestr8": "0x0000000000000005", - "valuebytes16": "0x00000000000000000000000000000005", "valuebytes20": "0x0000000000000000000000000000000000000005", - "valuestr16": "0x00000000000000000000000000000005", "valuebytes32": "0x0000000000000000000000000000000000000000000000000000000000000005", "valuestr32": "0x0000000000000000000000000000000000000000000000000000000000000005", - "valuebytes64": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005", # noqa: E501 - "valuestr64": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005", # noqa: E501 } assert actual == expected diff --git a/tests/test_hex.py b/tests/test_hex.py index a2bf3a0..3a32f6c 100644 --- a/tests/test_hex.py +++ b/tests/test_hex.py @@ -3,8 +3,7 @@ from hexbytes import HexBytes as BaseHexBytes from pydantic import BaseModel, ValidationError -from eth_pydantic_types import HashStr20 -from eth_pydantic_types.hex import HexBytes, HexStr +from eth_pydantic_types.hex import HexBytes, HexStr, HexStr20 class BytesModel(BaseModel): @@ -123,7 +122,7 @@ def test_hex_removes_leading_zeroes_if_needed(): address = "0x000000000000000000000000cafac3dd18ac6c6e92c921884f9e4176737c052c" class MyModel(BaseModel): - my_address: HashStr20 + my_address: HexStr20 # Test both str and bytes for input. for addr in (address, HexBytes(address)): diff --git a/tests/test_int.py b/tests/test_int.py new file mode 100644 index 0000000..ca9ee32 --- /dev/null +++ b/tests/test_int.py @@ -0,0 +1,62 @@ +import pytest +from pydantic import BaseModel, ValidationError + +from eth_pydantic_types.abi import ( + int8, + int16, + int32, + int64, + int128, + int256, + uint8, + uint16, + uint32, + uint64, + uint128, + uint256, +) + + +class SignedModel(BaseModel): + valueint8: int8 + valueint16: int16 + valueint32: int32 + valueint64: int64 + valueint128: int128 + valueint256: int256 + + @classmethod + def from_single(cls, value): + return cls( + valueint8=value, + valueint16=value, + valueint32=value, + valueint64=value, + valueint128=value, + valueint256=value, + ) + + +class UnsignedModel(BaseModel): + valueuint8: uint8 + valueuint16: uint16 + valueuint32: uint32 + valueuint64: uint64 + valueuint128: uint128 + valueuint256: uint256 + + @classmethod + def from_single(cls, value): + return cls( + valueuint8=value, + valueuint16=value, + valueuint32=value, + valueuint64=value, + valueuint128=value, + valueuint256=value, + ) + + +def test_negative_unsigned_int(): + with pytest.raises(ValidationError): + UnsignedModel.from_single(-1)