Skip to content

feat(mypy): proper decorator typing with ParamSpec #305

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
169 changes: 113 additions & 56 deletions eth_utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ def _wrapper(*args: Any, **kwargs: Any) -> Any:
return _wrapper


def return_arg_type(at_position: int) -> Callable[..., Callable[..., T]]:
"""
Wrap the return value with the result of `type(args[at_position])`.
"""

def decorator(to_wrap: Callable[..., Any]) -> Callable[..., T]:
@functools.wraps(to_wrap)
def wrapper(*args: Any, **kwargs: Any) -> T: # type: ignore
result = to_wrap(*args, **kwargs)
ReturnType = type(args[at_position])
return ReturnType(result) # type: ignore

return wrapper

return decorator


def _has_one_val(*args: T, **kwargs: T) -> bool:
vals = itertools.chain(args, kwargs.values())
not_nones = list(filter(lambda val: val is not None, vals))
Expand Down Expand Up @@ -68,65 +85,105 @@ def _validate_supported_kwarg(kwargs: Any) -> None:
)


def validate_conversion_arguments(to_wrap: Callable[..., T]) -> Callable[..., T]:
"""
Validates arguments for conversion functions.
- Only a single argument is present
- Kwarg must be 'primitive' 'hexstr' or 'text'
- If it is 'hexstr' or 'text' that it is a text type
"""

@functools.wraps(to_wrap)
def wrapper(*args: Any, **kwargs: Any) -> T:
_assert_one_val(*args, **kwargs)
if kwargs:
_validate_supported_kwarg(kwargs)

if len(args) == 0 and "primitive" not in kwargs:
_assert_hexstr_or_text_kwarg_is_text_type(**kwargs)
return to_wrap(*args, **kwargs)
try: # If you're using a recent enough version of python, we can enhance decorator typing with ParamSpec
from typing_extensions import ParamSpec

return wrapper

P = ParamSpec("P")


def return_arg_type(at_position: int) -> Callable[..., Callable[..., T]]:
"""
Wrap the return value with the result of `type(args[at_position])`.
"""

def decorator(to_wrap: Callable[..., Any]) -> Callable[..., T]:

def validate_conversion_arguments(to_wrap: Callable[P, T]) -> Callable[P, T]:
"""
Validates arguments for conversion functions.
- Only a single argument is present
- Kwarg must be 'primitive' 'hexstr' or 'text'
- If it is 'hexstr' or 'text' that it is a text type
"""

@functools.wraps(to_wrap)
def wrapper(*args: Any, **kwargs: Any) -> T: # type: ignore
result = to_wrap(*args, **kwargs)
ReturnType = type(args[at_position])
return ReturnType(result) # type: ignore

def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
_assert_one_val(*args, **kwargs)
if kwargs:
_validate_supported_kwarg(kwargs)

if len(args) == 0 and "primitive" not in kwargs:
_assert_hexstr_or_text_kwarg_is_text_type(**kwargs)
return to_wrap(*args, **kwargs)

return wrapper

return decorator


def replace_exceptions(
old_to_new_exceptions: Dict[Type[BaseException], Type[BaseException]]
) -> Callable[[Callable[..., T]], Callable[..., T]]:
"""
Replaces old exceptions with new exceptions to be raised in their place.
"""
old_exceptions = tuple(old_to_new_exceptions.keys())

def decorator(to_wrap: Callable[..., T]) -> Callable[..., T]:


def replace_exceptions(
old_to_new_exceptions: Dict[Type[BaseException], Type[BaseException]]
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""
Replaces old exceptions with new exceptions to be raised in their place.
"""
old_exceptions = tuple(old_to_new_exceptions.keys())

def decorator(to_wrap: Callable[P, T]) -> Callable[P, T]:
@functools.wraps(to_wrap)
def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
try:
return to_wrap(*args, **kwargs)
except old_exceptions as err:
try:
raise old_to_new_exceptions[type(err)](err) from err
except KeyError:
raise TypeError(
f"could not look up new exception to use for {repr(err)}"
) from err

return wrapped

return decorator

except ImportError: # Your python version is too low to use ParamSpec


def validate_conversion_arguments(to_wrap: Callable[..., T]) -> Callable[..., T]:
"""
Validates arguments for conversion functions.
- Only a single argument is present
- Kwarg must be 'primitive' 'hexstr' or 'text'
- If it is 'hexstr' or 'text' that it is a text type
"""

@functools.wraps(to_wrap)
def wrapped(*args: Any, **kwargs: Any) -> T:
try:
return to_wrap(*args, **kwargs)
except old_exceptions as err:
def wrapper(*args: Any, **kwargs: Any) -> T:
_assert_one_val(*args, **kwargs)
if kwargs:
_validate_supported_kwarg(kwargs)

if len(args) == 0 and "primitive" not in kwargs:
_assert_hexstr_or_text_kwarg_is_text_type(**kwargs)
return to_wrap(*args, **kwargs)

return wrapper


def replace_exceptions(
old_to_new_exceptions: Dict[Type[BaseException], Type[BaseException]]
) -> Callable[[Callable[..., T]], Callable[..., T]]:
"""
Replaces old exceptions with new exceptions to be raised in their place.
"""
old_exceptions = tuple(old_to_new_exceptions.keys())

def decorator(to_wrap: Callable[..., T]) -> Callable[..., T]:
@functools.wraps(to_wrap)
def wrapped(*args: Any, **kwargs: Any) -> T:
try:
raise old_to_new_exceptions[type(err)](err) from err
except KeyError:
raise TypeError(
f"could not look up new exception to use for {repr(err)}"
) from err

return wrapped

return decorator
return to_wrap(*args, **kwargs)
except old_exceptions as err:
try:
raise old_to_new_exceptions[type(err)](err) from err
except KeyError:
raise TypeError(
f"could not look up new exception to use for {repr(err)}"
) from err

return wrapped

return decorator