Skip to content

Commit

Permalink
perf: package load performance improvements (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Dec 10, 2024
1 parent 7f40f53 commit a492a6b
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 104 deletions.
39 changes: 3 additions & 36 deletions eth_pydantic_types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,4 @@
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
def __getattr__(name: str):
import eth_pydantic_types._main as module

__all__ = [
"Address",
"AddressType",
"Bip122Uri",
"HashBytes4",
"HashBytes8",
"HashBytes16",
"HashBytes20",
"HashBytes32",
"HashBytes64",
"HashStr4",
"HashStr8",
"HashStr16",
"HashStr20",
"HashStr32",
"HashStr64",
"HexBytes",
"HexStr",
]
return getattr(module, name)
17 changes: 11 additions & 6 deletions eth_pydantic_types/_error.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
from collections.abc import Callable
from typing import Any
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from pydantic_core._pydantic_core import PydanticCustomError

from pydantic_core import PydanticCustomError

# NOTE: We use the factory approach because PydanticCustomError is a final class.
# That is also why this module is internal.


def CustomError(fn: Callable, invalid_tag: str, **kwargs) -> PydanticCustomError:
def CustomError(fn: Callable, invalid_tag: str, **kwargs) -> "PydanticCustomError":
# perf: keep module loading super fast by localizing this import.
from pydantic_core._pydantic_core import PydanticCustomError

return PydanticCustomError(fn.__name__, f"Invalid {invalid_tag}", kwargs)


def HexValueError(value: Any) -> PydanticCustomError:
def HexValueError(value: Any) -> "PydanticCustomError":
return CustomError(HexValueError, "hex value", value=value)


def SizeError(size: Any, value: Any) -> PydanticCustomError:
def SizeError(size: Any, value: Any) -> "PydanticCustomError":
return CustomError(SizeError, "size of value", size=size, value=value)


def Bip122UriFormatError(value: str) -> PydanticCustomError:
def Bip122UriFormatError(value: str) -> "PydanticCustomError":
return CustomError(
Bip122UriFormatError,
"BIP-122 URI format",
Expand Down
37 changes: 37 additions & 0 deletions eth_pydantic_types/_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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

__all__ = [
"Address",
"AddressType",
"Bip122Uri",
"HashBytes4",
"HashBytes8",
"HashBytes16",
"HashBytes20",
"HashBytes32",
"HashBytes64",
"HashStr4",
"HashStr8",
"HashStr16",
"HashStr20",
"HashStr32",
"HashStr64",
"HexBytes",
"HexStr",
]
67 changes: 48 additions & 19 deletions eth_pydantic_types/address.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import Annotated, Any, ClassVar, Optional, cast
from functools import cached_property
from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Optional

from eth_typing import ChecksumAddress
from eth_utils import is_checksum_address, to_checksum_address
from pydantic_core.core_schema import ValidationInfo, str_schema

from eth_pydantic_types.hash import HashStr20

if TYPE_CHECKING:
from eth_typing import ChecksumAddress

ADDRESS_PATTERN = "^0x[a-fA-F0-9]{40}$"


Expand Down Expand Up @@ -33,19 +35,46 @@ def __eth_pydantic_validate__(cls, value: Any, info: Optional[ValidationInfo] =
return cls.to_checksum_address(value)

@classmethod
def to_checksum_address(cls, value: str) -> ChecksumAddress:
return (
cast(ChecksumAddress, value)
if is_checksum_address(value)
else to_checksum_address(value)
)


"""
A type that can be used in place of ``eth_typing.ChecksumAddress``.
**NOTE**: We are unable to subclass ``eth_typing.ChecksumAddress``
in :class:`~eth_pydantic_types.address.Address` because it is
a NewType; that is why we offer this annotated approach.
"""
AddressType = Annotated[ChecksumAddress, Address]
def to_checksum_address(cls, value: str) -> "ChecksumAddress":
# perf: eth_utils imports are too slow. Hence they are localized.
from eth_utils import is_checksum_address, to_checksum_address

if is_checksum_address(value):
return value # type: ignore[return-value]

else:
return to_checksum_address(value)


class _AddressTypeFactory:
@cached_property
def address_type(self):
from eth_typing import ChecksumAddress

# Lazy define for performance reasons.
AddressType = Annotated[ChecksumAddress, Address]
AddressType.__doc__ = """
A type that can be used in place of ``eth_typing.ChecksumAddress``.
**NOTE**: We are unable to subclass ``eth_typing.ChecksumAddress``
in :class:`~eth_pydantic_types.address.Address` because it is
a NewType; that is why we offer this annotated approach.
"""
return AddressType


_factory = _AddressTypeFactory()


def __getattr__(name: str):
if name == "Address":
return Address

elif name == "AddressType":
return _factory.address_type


__all__ = [
"AddressType",
"Address",
]
64 changes: 40 additions & 24 deletions eth_pydantic_types/hash.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from typing import Any, ClassVar, Optional
from typing import TYPE_CHECKING, Any, ClassVar, Optional

from pydantic_core.core_schema import (
CoreSchema,
ValidationInfo,
bytes_schema,
str_schema,
with_info_before_validator_function,
)
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}}}$"
Expand All @@ -38,7 +35,7 @@ class HashBytes(HexBytes):
schema_examples: ClassVar[tuple[str, ...]] = _get_hash_examples(1)

@classmethod
def __get_pydantic_core_schema__(cls, value, handler=None) -> CoreSchema:
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),
Expand All @@ -48,7 +45,7 @@ def __get_pydantic_core_schema__(cls, value, handler=None) -> CoreSchema:

@classmethod
def __eth_pydantic_validate__(
cls, value: Any, info: Optional[ValidationInfo] = None
cls, value: Any, info: Optional["ValidationInfo"] = None
) -> HexBytes:
return cls(cls.validate_size(HexBytes(value)))

Expand All @@ -69,14 +66,14 @@ class HashStr(BaseHexStr):
schema_examples: ClassVar[tuple[str, ...]] = _get_hash_examples(1)

@classmethod
def __get_pydantic_core_schema__(cls, value, handler=None) -> CoreSchema:
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:
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)
Expand Down Expand Up @@ -107,15 +104,34 @@ def _make_hash_cls(size: int, base_type: type):
)


HashBytes4 = _make_hash_cls(4, bytes)
HashBytes8 = _make_hash_cls(8, bytes)
HashBytes16 = _make_hash_cls(16, bytes)
HashBytes20 = _make_hash_cls(20, bytes)
HashBytes32 = _make_hash_cls(32, bytes)
HashBytes64 = _make_hash_cls(64, bytes)
HashStr4 = _make_hash_cls(4, str)
HashStr8 = _make_hash_cls(8, str)
HashStr16 = _make_hash_cls(16, str)
HashStr20 = _make_hash_cls(20, str)
HashStr32 = _make_hash_cls(32, str)
HashStr64 = _make_hash_cls(64, str)
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",
]
7 changes: 3 additions & 4 deletions eth_pydantic_types/hex.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union, cast
from typing import TYPE_CHECKING, Any, ClassVar, Optional, Union

from eth_typing import HexStr as EthTypingHexStr
from eth_utils import add_0x_prefix
from hexbytes import HexBytes as BaseHexBytes
from pydantic_core.core_schema import (
ValidationInfo,
Expand All @@ -17,6 +15,7 @@
if TYPE_CHECKING:
from pydantic_core import CoreSchema


schema_pattern = "^0x([0-9a-f][0-9a-f])*$"
schema_examples = (
"0x", # empty bytes
Expand Down Expand Up @@ -112,7 +111,7 @@ def __eth_pydantic_validate__(cls, value):
@classmethod
def from_bytes(cls, data: bytes) -> "HexStr":
value_str = super().from_bytes(data)
value = add_0x_prefix(cast(EthTypingHexStr, value_str))
value = value_str if value_str.startswith("0x") else f"0x{value_str}"
return HexStr(value)


Expand Down
17 changes: 4 additions & 13 deletions eth_pydantic_types/validators.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
from collections.abc import Sized
from typing import Any, Callable, Optional, TypeVar, cast

from pydantic import WithJsonSchema
from pydantic_core.core_schema import bytes_schema
from typing import TYPE_CHECKING, Callable, Optional, TypeVar

from eth_pydantic_types._error import SizeError

__SIZED_T = TypeVar("__SIZED_T", bound=Sized)


class WithBytesSchema(WithJsonSchema):
def __init__(self, **kwargs):
mode = kwargs.pop("mode", None)
schema = cast(dict[str, Any], bytes_schema(**kwargs))
super().__init__(schema, mode=mode)
if TYPE_CHECKING:
__SIZED_T = TypeVar("__SIZED_T", bound=Sized)


def validate_size(value: __SIZED_T, size: int, coerce: Optional[Callable] = None) -> __SIZED_T:
def validate_size(value: "__SIZED_T", size: int, coerce: Optional[Callable] = None) -> "__SIZED_T":
if len(value) == size:
return value

Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
include = '\.pyi?$'

[tool.pytest.ini_options]
addopts = "-p no:ape_test" # NOTE: Prevents the ape plugin from activating on our tests
addopts = """
-p no:ape_test
-p no:pytest_ethereum
"""
python_files = "test_*.py"
testpaths = "tests"
markers = "fuzzing: Run Hypothesis fuzz test suite"
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]
max-line-length = 100
ignore = E704,W503,PYD002,TC003,TC006
ignore = E701,E704,F822,W503,PYD002,TC003,TC006
exclude =
venv*
docs
Expand Down

0 comments on commit a492a6b

Please sign in to comment.