Skip to content

Commit 0895c29

Browse files
committed
test: Test support of SQLalchemy 2.0 syntax
1 parent 1bf9d05 commit 0895c29

3 files changed

Lines changed: 232 additions & 2 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,41 @@ item = SomeModel.query.filter(...).one()
4040
result = item.to_dict()
4141
```
4242
You get values of all SQLAlchemy fields in the `result` var, even nested relationships
43+
44+
### Modern SQLAlchemy 2.0 Style
45+
46+
**SerializerMixin** works seamlessly with both traditional SQLAlchemy style and modern SQLAlchemy 2.0 style using type annotations:
47+
48+
**Traditional style:**
49+
```python
50+
from sqlalchemy_serializer import SerializerMixin
51+
import sqlalchemy as sa
52+
53+
class SomeModel(db.Model, SerializerMixin):
54+
id = sa.Column(sa.Integer, primary_key=True)
55+
name = sa.Column(sa.String(256))
56+
```
57+
58+
**Modern SQLAlchemy 2.0 style (also fully supported):**
59+
```python
60+
from __future__ import annotations
61+
62+
from sqlalchemy.orm import Mapped, mapped_column, relationship
63+
from sqlalchemy_serializer import SerializerMixin
64+
65+
class ModernModel(Base, SerializerMixin):
66+
__tablename__ = "modern_model"
67+
68+
id: Mapped[int] = mapped_column(primary_key=True)
69+
name: Mapped[str] = mapped_column(sa.String(256))
70+
created_at: Mapped[datetime] = mapped_column(sa.DateTime, default=datetime.utcnow)
71+
72+
# Relationships work too
73+
parent_id: Mapped[int | None] = mapped_column(sa.ForeignKey("parent.id"), nullable=True)
74+
parent: Mapped["ParentModel | None"] = relationship("ParentModel")
75+
```
76+
77+
Both styles work identically with **SerializerMixin** - use whichever style you prefer!
4378
In order to change the default output you shuld pass tuple of fieldnames as an argument
4479

4580
- If you want to exclude or add some extra fields (not from database)

tests/models.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from datetime import datetime
1+
from datetime import date, datetime, time
22
from decimal import Decimal
3+
from uuid import UUID as UUIDType
34
from uuid import uuid4
45

56
import pytz
67
import sqlalchemy as sa
78
from sqlalchemy.dialects.postgresql import UUID
8-
from sqlalchemy.orm import declarative_base, relationship
9+
from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship
910

1011
from sqlalchemy_serializer import SerializerMixin
1112

@@ -124,3 +125,71 @@ class CustomSerializerModel(Base, CustomSerializerMixin):
124125
time = sa.Column(sa.Time, default=TIME)
125126
bool = sa.Column(sa.Boolean, default=True)
126127
money = MONEY
128+
129+
130+
# Modern SQLAlchemy 2.0 style models
131+
class ModernFlatModel(Base, SerializerMixin):
132+
__tablename__ = "modern_flat_model"
133+
serialize_only = ()
134+
serialize_rules = ()
135+
136+
id: Mapped[int] = mapped_column(primary_key=True)
137+
string: Mapped[str] = mapped_column(sa.String(256), default="Modern string with")
138+
date_field: Mapped[date] = mapped_column(sa.Date, default=DATETIME)
139+
datetime_field: Mapped[datetime] = mapped_column(sa.DateTime, default=DATETIME)
140+
time_field: Mapped[time] = mapped_column(sa.Time, default=TIME)
141+
bool_field: Mapped[bool] = mapped_column(sa.Boolean, default=True)
142+
null: Mapped[str | None] = mapped_column(sa.String, nullable=True)
143+
uuid: Mapped[UUIDType] = mapped_column(UUID(as_uuid=True), default=uuid4)
144+
list = [1, "modern_test_string", 0.9, {"key": 123, "key2": 23423}, {"key": 234}]
145+
set = {1, 2, "modern_test_string"}
146+
dict = dict(key=123, key2={"key": 12})
147+
money = MONEY
148+
149+
@property
150+
def prop(self):
151+
return "Modern property"
152+
153+
@property
154+
def prop_with_bytes(self):
155+
return b"Modern bytes"
156+
157+
def method(self):
158+
return f"Modern method + {self.string}"
159+
160+
def _protected_method(self):
161+
return f"Modern protected method + {self.string}"
162+
163+
164+
class ModernNestedModel(Base, SerializerMixin):
165+
__tablename__ = "modern_nested_model"
166+
serialize_only = ()
167+
serialize_rules = ()
168+
169+
id: Mapped[int] = mapped_column(primary_key=True)
170+
string: Mapped[str] = mapped_column(
171+
sa.String(256), default="(MODERN_NESTED)Some string with"
172+
)
173+
date_field: Mapped[date] = mapped_column(sa.Date, default=DATETIME)
174+
datetime_field: Mapped[datetime] = mapped_column(sa.DateTime, default=DATETIME)
175+
time_field: Mapped[time] = mapped_column(sa.Time, default=TIME)
176+
bool_field: Mapped[bool] = mapped_column(sa.Boolean, default=False)
177+
null: Mapped[str | None] = mapped_column(sa.String, nullable=True)
178+
list = [1, "(MODERN_NESTED)test_string", 0.9, {"key": 123}]
179+
set = {1, 2, "(MODERN_NESTED)test_string"}
180+
dict = dict(key=123)
181+
182+
model_id: Mapped[int | None] = mapped_column(
183+
sa.ForeignKey("modern_flat_model.id"), nullable=True
184+
)
185+
model: Mapped["ModernFlatModel | None"] = relationship("ModernFlatModel")
186+
187+
@property
188+
def prop(self):
189+
return "(MODERN_NESTED)Some property"
190+
191+
def method(self):
192+
return f"(MODERN_NESTED)User defined method + {self.string}"
193+
194+
def _protected_method(self):
195+
return f"(MODERN_NESTED)User defined protected method + {self.string}"

tests/test_modern_models.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from .models import DATE, DATETIME, MONEY, TIME, ModernFlatModel, ModernNestedModel
2+
3+
4+
def test_modern_flat_model_basic(get_instance):
5+
"""Checks to_dict method of modern flat model with no predefined options"""
6+
i = get_instance(ModernFlatModel)
7+
data = i.to_dict()
8+
9+
# Check SQLAlchemy fields
10+
assert "id" in data
11+
assert "string" in data and data["string"] == i.string
12+
assert "date_field" in data
13+
assert "time_field" in data
14+
assert "datetime_field" in data
15+
assert "bool_field" in data and data["bool_field"] == i.bool_field
16+
assert "null" in data and data["null"] is None
17+
assert "uuid" in data and str(i.uuid) == data["uuid"]
18+
19+
# Check non-sql fields (not included in this case, need to be defined explicitly)
20+
assert "list" not in data
21+
assert "set" not in data
22+
assert "dict" not in data
23+
assert "prop" not in data
24+
assert "method" not in data
25+
assert "_protected_method" not in data
26+
assert "money" not in data
27+
28+
29+
def test_modern_flat_model_with_properties(get_instance):
30+
"Checks to_dict method of modern flat model with automatic serialization of @properties"
31+
32+
class AutoPropModernFlatModel(ModernFlatModel):
33+
auto_serialize_properties = True
34+
35+
i = get_instance(AutoPropModernFlatModel)
36+
data = i.to_dict()
37+
38+
# Check SQLAlchemy fields
39+
assert "id" in data
40+
assert "string" in data and data["string"] == i.string
41+
assert "date_field" in data
42+
assert "time_field" in data
43+
assert "datetime_field" in data
44+
assert "bool_field" in data and data["bool_field"] == i.bool_field
45+
assert "null" in data and data["null"] is None
46+
assert "uuid" in data and str(i.uuid) == data["uuid"]
47+
48+
# Properties
49+
assert "prop" in data
50+
assert "prop_with_bytes" in data
51+
52+
# Check non-sql fields
53+
assert "list" not in data
54+
assert "set" not in data
55+
assert "dict" not in data
56+
assert "method" not in data
57+
assert "_protected_method" not in data
58+
assert "money" not in data
59+
60+
61+
def test_modern_models_formats(get_instance):
62+
"""Check date/datetime/time/decimal default formats for modern models"""
63+
i = get_instance(ModernFlatModel)
64+
65+
# Default formats
66+
d_format = i.date_format
67+
dt_format = i.datetime_format
68+
t_format = i.time_format
69+
decimal_format = i.decimal_format
70+
71+
# Include non-SQL field to check decimal_format and bytes
72+
data = i.to_dict(rules=("money", "prop_with_bytes"))
73+
74+
assert "date_field" in data
75+
assert data["date_field"] == DATE.strftime(d_format)
76+
assert "datetime_field" in data
77+
assert data["datetime_field"] == DATETIME.strftime(dt_format)
78+
assert "time_field" in data
79+
assert data["time_field"] == TIME.strftime(t_format)
80+
81+
assert "money" in data
82+
assert data["money"] == decimal_format.format(MONEY)
83+
84+
assert "prop_with_bytes" in data
85+
assert data["prop_with_bytes"] == i.prop_with_bytes.decode()
86+
87+
88+
def test_modern_nested_model(get_instance):
89+
"""Test relationship serialization with modern nested model"""
90+
flat = get_instance(ModernFlatModel)
91+
nested = get_instance(ModernNestedModel, model_id=flat.id)
92+
93+
# Test basic serialization
94+
data = nested.to_dict()
95+
assert "id" in data
96+
assert "string" in data
97+
assert "model_id" in data
98+
assert data["model_id"] == flat.id
99+
100+
# Test relationship serialization
101+
data_with_relation = nested.to_dict(rules=("model",))
102+
assert "model" in data_with_relation
103+
assert isinstance(data_with_relation["model"], dict)
104+
assert data_with_relation["model"]["id"] == flat.id
105+
assert data_with_relation["model"]["string"] == flat.string
106+
107+
108+
def test_modern_models_rules(get_instance):
109+
"""Test rules and only parameters with modern models"""
110+
i = get_instance(ModernFlatModel)
111+
112+
# Test rules parameter
113+
data = i.to_dict(rules=("-id", "prop", "method"))
114+
assert "id" not in data
115+
assert "string" in data
116+
assert "prop" in data
117+
assert data["prop"] == i.prop
118+
assert "method" in data
119+
assert data["method"] == i.method()
120+
121+
# Test only parameter
122+
data_only = i.to_dict(only=("id", "string", "prop"))
123+
assert "id" in data_only
124+
assert "string" in data_only
125+
assert "prop" in data_only
126+
assert len(data_only.keys()) == 3

0 commit comments

Comments
 (0)