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 args: Function arguments.
|
||||||
:param kwargs: Function keyword 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:
|
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)
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -922,7 +922,7 @@ class TestMiscUtils(FrappeTestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestTypingValidations(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):
|
def test_validate_whitelisted_api(self):
|
||||||
from inspect import signature
|
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 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 = {
|
SLACK_DICT = {
|
||||||
bool: (int, bool, float),
|
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
|
Validate the types of the arguments passed to a function with the type annotations
|
||||||
defined on the function.
|
defined on the function.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if annotations := func.__annotations__:
|
if annotations := func.__annotations__:
|
||||||
|
new_args, new_kwargs = list(args), kwargs
|
||||||
|
|
||||||
# generate kwargs dict from args
|
# generate kwargs dict from args
|
||||||
arg_names = func.__code__.co_varnames[: func.__code__.co_argcount]
|
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:
|
elif param_def.default != current_arg_type:
|
||||||
current_arg_type = Union[current_arg_type, type(param_def.default)]
|
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",
|
"psycopg2-binary~=2.9.1",
|
||||||
"pyOpenSSL~=22.1.0",
|
"pyOpenSSL~=22.1.0",
|
||||||
"pycryptodome~=3.10.1",
|
"pycryptodome~=3.10.1",
|
||||||
|
"pydantic~=1.10.2",
|
||||||
"pyotp~=2.6.0",
|
"pyotp~=2.6.0",
|
||||||
"python-dateutil~=2.8.1",
|
"python-dateutil~=2.8.1",
|
||||||
"pytz==2022.1",
|
"pytz==2022.1",
|
||||||
|
|
@ -66,7 +67,6 @@ dependencies = [
|
||||||
"tenacity~=8.0.1",
|
"tenacity~=8.0.1",
|
||||||
"terminaltables~=3.1.0",
|
"terminaltables~=3.1.0",
|
||||||
"traceback-with-variables~=2.0.4",
|
"traceback-with-variables~=2.0.4",
|
||||||
"typeguard~=2.13.3",
|
|
||||||
"xlrd~=2.0.1",
|
"xlrd~=2.0.1",
|
||||||
"zxcvbn-python~=4.4.24",
|
"zxcvbn-python~=4.4.24",
|
||||||
"markdownify~=0.11.2",
|
"markdownify~=0.11.2",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue