diff --git a/frappe/app.py b/frappe/app.py index 1cbdca1361a0..ee4ba7d1700d 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -33,6 +33,8 @@ # If gc.freeze is done then importing modules before forking allows us to share the memory if frappe._tune_gc: + import pydantic + import frappe.boot import frappe.client import frappe.core.doctype.user.user diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 99975226925d..59e942ef42db 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -1163,3 +1163,44 @@ def test_bankers_rounding_property(self, number, precision): def test_default_rounding(self): self.assertEqual(frappe.get_system_settings("rounding_method"), "Banker's Rounding") + + +class TestTypingValidations(FrappeTestCase): + def test_validate_argument_types(self): + from frappe.core.doctype.doctype.doctype import DocType + from frappe.utils.typing_validations import FrappeTypeError, validate_argument_types + + @validate_argument_types + def test_simple_types(a: int, b: float, c: bool): + return a, b, c + + @validate_argument_types + def test_sequence(a: str, b: list[dict] | None = None, c: dict[str, int] | None = None): + return a, b, c + + @validate_argument_types + def test_doctypes(a: DocType | dict): + return a + + self.assertEqual(test_simple_types(True, 2.0, True), (1, 2.0, True)) + self.assertEqual(test_simple_types(1, 2, 1), (1, 2.0, True)) + self.assertEqual(test_simple_types(1.0, 2, 1), (1, 2.0, True)) + self.assertEqual(test_simple_types(1, 2, "1"), (1, 2.0, True)) + with self.assertRaises(FrappeTypeError): + test_simple_types(1, 2, "a") + with self.assertRaises(FrappeTypeError): + test_simple_types(1, 2, None) + + self.assertEqual(test_sequence("a", [{"a": 1}], {"a": 1}), ("a", [{"a": 1}], {"a": 1})) + self.assertEqual(test_sequence("a", None, None), ("a", None, None)) + self.assertEqual(test_sequence("a", [{"a": 1}], None), ("a", [{"a": 1}], None)) + self.assertEqual(test_sequence("a", None, {"a": 1}), ("a", None, {"a": 1})) + self.assertEqual(test_sequence("a", [{"a": 1}], {"a": "1.0"}), ("a", [{"a": 1}], {"a": 1})) + with self.assertRaises(FrappeTypeError): + test_sequence("a", [{"a": 1}], True) + + doctype = frappe.get_last_doc("DocType") + self.assertEqual(test_doctypes(doctype), doctype) + self.assertEqual(test_doctypes(doctype.as_dict()), doctype.as_dict()) + with self.assertRaises(FrappeTypeError): + test_doctypes("a") diff --git a/frappe/utils/typing_validations.py b/frappe/utils/typing_validations.py index e7ebcfbdffcd..91a318eae4d1 100644 --- a/frappe/utils/typing_validations.py +++ b/frappe/utils/typing_validations.py @@ -1,11 +1,7 @@ from functools import lru_cache, wraps from inspect import _empty, isclass, signature from types import EllipsisType -from typing import Any, Callable, ForwardRef, TypeVar, Union - -from pydantic.config import BaseConfig -from pydantic.error_wrappers import ValidationError as PyValidationError -from pydantic.tools import NameFactory, _generate_parsing_type_name +from typing import Callable, ForwardRef, TypeVar, Union from frappe.exceptions import FrappeTypeError @@ -69,29 +65,10 @@ def raise_type_error( @lru_cache(maxsize=2048) -def _get_parsing_type( - type_: Any, *, type_name: NameFactory | None = None, config: type[BaseConfig] = None -) -> Any: - # Note: this is a copy of pydantic.tools._get_parsing_type with the addition of allowing a config argument - from pydantic.main import create_model - - if type_name is None: - type_name = _generate_parsing_type_name - if not isinstance(type_name, str): - type_name = type_name(type_) - return create_model(type_name, __root__=(type_, ...), __config__=config) - - -def parse_obj_as( - type_: type[T], - obj: Any, - *, - type_name: NameFactory | None = None, - config: type[BaseConfig] | None = None, -) -> T: - # Note: This is a copy of pydantic.tools.parse_obj_as with the addition of allowing a config argument - model_type = _get_parsing_type(type_, type_name=type_name, config=config) # type: ignore[arg-type] - return model_type(__root__=obj).__root__ +def TypeAdapter(type_): + from pydantic import TypeAdapter as PyTypeAdapter + + return PyTypeAdapter(type_, config=FrappePydanticConfig) def transform_parameter_types(func: Callable, args: tuple, kwargs: dict): @@ -103,6 +80,8 @@ def transform_parameter_types(func: Callable, args: tuple, kwargs: dict): if not (args or kwargs) or not func.__annotations__: return args, kwargs + from pydantic import ValidationError as PyValidationError + annotations = func.__annotations__ new_args, new_kwargs = list(args), kwargs @@ -157,9 +136,7 @@ def transform_parameter_types(func: Callable, args: tuple, kwargs: dict): # validate the type set using pydantic - raise a TypeError if Validation is raised or Ellipsis is returned try: - current_arg_value_after = parse_obj_as( - current_arg_type, current_arg_value, type_name=current_arg, config=FrappePydanticConfig - ) + current_arg_value_after = TypeAdapter(current_arg_type).validate_python(current_arg_value) except (TypeError, PyValidationError) as e: raise_type_error(current_arg, current_arg_type, current_arg_value, current_exception=e) diff --git a/pyproject.toml b/pyproject.toml index f2ce88f2949d..17a75f3c0b83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "psycopg2-binary~=2.9.1", "pyOpenSSL~=23.2.0", "pycryptodome~=3.18.0", - "pydantic~=1.10.8", + "pydantic==2.0", "pyotp~=2.8.0", "python-dateutil~=2.8.2", "pytz==2023.3",