From 5a0b68bd913ca816466d09431285c02373970406 Mon Sep 17 00:00:00 2001 From: Arun Sharma Date: Wed, 29 Jan 2025 16:17:33 -0800 Subject: [PATCH] fquery.pydantic decorator --- fquery/pydantic.py | 46 ++++++++++++++++++++++++++++++++++++++++++ tests/test_pydantic.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 fquery/pydantic.py create mode 100644 tests/test_pydantic.py diff --git a/fquery/pydantic.py b/fquery/pydantic.py new file mode 100644 index 0000000..810dcda --- /dev/null +++ b/fquery/pydantic.py @@ -0,0 +1,46 @@ +import dataclasses +from dataclasses import dataclass, fields +from typing import Type + +from pydantic import BaseModel, ConfigDict, Field + + +def pydantic(cls): + return model(dataclass(kw_only=True)(cls)) + + +def validator(self) -> BaseModel: + attrs = {name: getattr(self, name) for name in self.__pydantic__.model_fields} + return self.__pydantic__(**attrs) + + +def get_field_def(cls, field): + # if the dataclass has a default_factory, or a default value, use it in pydantic Field + kwargs = {} + if not isinstance(field.default, dataclasses._MISSING_TYPE): + kwargs["default"] = field.default + if not isinstance(field.default_factory, dataclasses._MISSING_TYPE): + kwargs["default_factory"] = field.default_factory + return Field(**kwargs) + + +def model(cls: Type) -> Type: + """ + Decorator to convert a dataclass to a Pydantic model. + """ + # Generate the SQLModel class + pydantic_cls = type( + cls.__name__ + "Model", + (BaseModel,), + { + # Add type annotations to the generated fields + "__annotations__": {**{field.name: field.type for field in fields(cls)}}, + # Actual field defs + **{field.name: get_field_def(cls, field) for field in fields(cls)}, + }, + ) + cls.__pydantic__ = pydantic_cls + cls.model_config = ConfigDict(extra="ignore") + cls.validator = validator + + return cls diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py new file mode 100644 index 0000000..27705c5 --- /dev/null +++ b/tests/test_pydantic.py @@ -0,0 +1,34 @@ +from dataclasses import is_dataclass + +import pytest +from pydantic import BaseModel, ValidationError + +from fquery.pydantic import pydantic + + +@pydantic +class User: + name: str + age: int + is_active: bool = True + + +def test_pydantic(): + u1 = User(name="John Doe", age=42) + u2 = User(name="John Doe", age=42, is_active=False) + assert is_dataclass(u1) + assert is_dataclass(u2) + + v1 = u1.validator() + v2 = u2.validator() + assert isinstance(v1, BaseModel) + assert isinstance(v2, BaseModel) + + assert v1.model_dump() == u1.__dict__ + assert v2.model_dump() == u2.__dict__ + + +def test_pydantic_fail(): + u1 = User(name="John Doe", age=42.3) + with pytest.raises(ValidationError): + _ = u1.validator()