refactor: transform_parameter_types
- Switch to Pydantic which is under continuous development and can support more types - Equivalent Pydantic API will try to transform data if possible - The previous point makes it such that we don't need to explicitly try to parse each stringified int in app code since Pydantic can do this - Drop typeguard since it did not handle 3.10+ native typing definitions
This commit is contained in:
parent
73b0971a26
commit
4fe260e09e
4 changed files with 63 additions and 8 deletions
|
|
@ -723,10 +723,10 @@ def apply_validate_argument_types_wrapper(func):
|
|||
|
||||
:param args: Function arguments.
|
||||
:param kwargs: Function keyword arguments."""
|
||||
from frappe.utils.typing_validations import validate_argument_types
|
||||
from frappe.utils.typing_validations import transform_parameter_types
|
||||
|
||||
if getattr(local, "request", None) or local.flags.in_test:
|
||||
validate_argument_types(func, args, kwargs)
|
||||
args, kwargs = transform_parameter_types(func, args, kwargs)
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -922,7 +922,7 @@ class TestMiscUtils(FrappeTestCase):
|
|||
|
||||
|
||||
class TestTypingValidations(FrappeTestCase):
|
||||
ERR_REGEX = "^type of .* must be .*; got (object|list) instead$"
|
||||
ERR_REGEX = f"^Argument '.*' should be of type '.*' but got '.*' instead.$"
|
||||
|
||||
def test_validate_whitelisted_api(self):
|
||||
from inspect import signature
|
||||
|
|
|
|||
|
|
@ -1,20 +1,57 @@
|
|||
from inspect import _empty, signature
|
||||
from inspect import _empty, isclass, signature
|
||||
from types import EllipsisType
|
||||
from typing import Callable, ForwardRef, Union
|
||||
|
||||
from typeguard import check_type
|
||||
from pydantic import parse_obj_as
|
||||
from pydantic.error_wrappers import ValidationError as PyValidationError
|
||||
|
||||
SLACK_DICT = {
|
||||
bool: (int, bool, float),
|
||||
}
|
||||
|
||||
|
||||
def validate_argument_types(func: Callable, args: tuple, kwargs: dict):
|
||||
def qualified_name(obj) -> str:
|
||||
"""
|
||||
Return the qualified name (e.g. package.module.Type) for the given object.
|
||||
|
||||
Builtins and types from the :mod:typing package get special treatment by having the module
|
||||
name stripped from the generated name.
|
||||
|
||||
"""
|
||||
discovered_type = obj if isclass(obj) else type(obj)
|
||||
module, qualname = discovered_type.__module__, discovered_type.__qualname__
|
||||
|
||||
if module in {"typing", "types"}:
|
||||
return obj
|
||||
elif module in {"builtins"}:
|
||||
return qualname
|
||||
else:
|
||||
return f"{module}.{qualname}"
|
||||
|
||||
|
||||
def raise_type_error(
|
||||
arg_name: str, arg_type: type, arg_value: object, current_exception: Exception = None
|
||||
):
|
||||
"""
|
||||
Raise a TypeError with a message that includes the name of the argument, the expected type
|
||||
and the actual type of the value passed.
|
||||
|
||||
"""
|
||||
raise TypeError(
|
||||
f"Argument '{arg_name}' should be of type '{qualified_name(arg_type)}' but got "
|
||||
f"'{qualified_name(arg_value)}' instead."
|
||||
) from current_exception
|
||||
|
||||
|
||||
def transform_parameter_types(func: Callable, args: tuple, kwargs: dict):
|
||||
"""
|
||||
Validate the types of the arguments passed to a function with the type annotations
|
||||
defined on the function.
|
||||
|
||||
"""
|
||||
if annotations := func.__annotations__:
|
||||
new_args, new_kwargs = list(args), kwargs
|
||||
|
||||
# generate kwargs dict from args
|
||||
arg_names = func.__code__.co_varnames[: func.__code__.co_argcount]
|
||||
|
||||
|
|
@ -60,4 +97,22 @@ def validate_argument_types(func: Callable, args: tuple, kwargs: dict):
|
|||
elif param_def.default != current_arg_type:
|
||||
current_arg_type = Union[current_arg_type, type(param_def.default)]
|
||||
|
||||
check_type(current_arg, current_arg_value, current_arg_type)
|
||||
try:
|
||||
current_arg_value_after = parse_obj_as(
|
||||
current_arg_type, current_arg_value, type_name=current_arg
|
||||
)
|
||||
except PyValidationError as e:
|
||||
raise_type_error(current_arg, current_arg_type, current_arg_value, current_exception=e)
|
||||
|
||||
if isinstance(current_arg_value_after, EllipsisType):
|
||||
raise_type_error(current_arg, current_arg_type, current_arg_value)
|
||||
|
||||
else:
|
||||
if current_arg in kwargs:
|
||||
new_kwargs[current_arg] = current_arg_value_after
|
||||
else:
|
||||
new_args[arg_names.index(current_arg)] = current_arg_value_after
|
||||
|
||||
return new_args, new_kwargs
|
||||
|
||||
return args, kwargs
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ dependencies = [
|
|||
"psycopg2-binary~=2.9.1",
|
||||
"pyOpenSSL~=22.1.0",
|
||||
"pycryptodome~=3.10.1",
|
||||
"pydantic~=1.10.2",
|
||||
"pyotp~=2.6.0",
|
||||
"python-dateutil~=2.8.1",
|
||||
"pytz==2022.1",
|
||||
|
|
@ -66,7 +67,6 @@ dependencies = [
|
|||
"tenacity~=8.0.1",
|
||||
"terminaltables~=3.1.0",
|
||||
"traceback-with-variables~=2.0.4",
|
||||
"typeguard~=2.13.3",
|
||||
"xlrd~=2.0.1",
|
||||
"zxcvbn-python~=4.4.24",
|
||||
"markdownify~=0.11.2",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue