Skip to content

Commit bf5304d

Browse files
committed
fquery.pydantic decorator
1 parent 71ecfce commit bf5304d

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

fquery/pydantic.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import dataclasses
2+
from dataclasses import dataclass, fields
3+
from typing import Type
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
def pydantic(cls):
9+
return model(dataclass(kw_only=True)(cls))
10+
11+
12+
def validator(self) -> BaseModel:
13+
attrs = {name: getattr(self, name) for name in self.__pydantic__.model_fields}
14+
return self.__pydantic__(**attrs)
15+
16+
17+
def get_field_def(cls, field):
18+
# if the dataclass has a default_factory, or a default value, use it in pydantic Field
19+
kwargs = {}
20+
if not isinstance(field.default, dataclasses._MISSING_TYPE):
21+
kwargs["default"] = field.default
22+
if not isinstance(field.default_factory, dataclasses._MISSING_TYPE):
23+
kwargs["default_factory"] = field.default_factory
24+
return Field(**kwargs)
25+
26+
27+
def model(cls: Type) -> Type:
28+
"""
29+
Decorator to convert a dataclass to a Pydantic model.
30+
"""
31+
# Generate the SQLModel class
32+
pydantic_cls = type(
33+
cls.__name__ + "Model",
34+
(BaseModel,),
35+
{
36+
# Add type annotations to the generated fields
37+
"__annotations__": {**{field.name: field.type for field in fields(cls)}},
38+
# Actual field defs
39+
**{field.name: get_field_def(cls, field) for field in fields(cls)},
40+
},
41+
)
42+
cls.__pydantic__ = pydantic_cls
43+
cls.model_config = ConfigDict(extra="ignore")
44+
cls.validator = validator
45+
46+
return cls

tests/test_pydantic.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dataclasses import is_dataclass
2+
3+
import pytest
4+
from pydantic import BaseModel, ValidationError
5+
6+
from fquery.pydantic import pydantic
7+
8+
9+
@pydantic
10+
class User:
11+
name: str
12+
age: int
13+
is_active: bool = True
14+
15+
16+
def test_pydantic():
17+
u1 = User(name="John Doe", age=42)
18+
u2 = User(name="John Doe", age=42, is_active=False)
19+
assert is_dataclass(u1)
20+
assert is_dataclass(u2)
21+
22+
v1 = u1.validator()
23+
v2 = u2.validator()
24+
assert isinstance(v1, BaseModel)
25+
assert isinstance(v2, BaseModel)
26+
27+
assert v1.model_dump() == u1.__dict__
28+
assert v2.model_dump() == u2.__dict__
29+
30+
31+
def test_pydantic_fail():
32+
u1 = User(name="John Doe", age=42.3)
33+
with pytest.raises(ValidationError):
34+
_ = u1.validator()

0 commit comments

Comments
 (0)