Skip to content

Commit 7a40102

Browse files
committed
Fix frozen creation issue
Replaced setattr with object.__setattr__ for frozen models
1 parent 200286f commit 7a40102

File tree

2 files changed

+88
-4
lines changed

2 files changed

+88
-4
lines changed

sqlmodel/_compat.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ def sqlmodel_table_construct(
242242
_extra = {}
243243
for k, v in values.items():
244244
_extra[k] = v
245+
setattr_ = (
246+
object.__setattr__ if self_instance.model_config.get("frozen") else setattr
247+
)
245248
# SQLModel override, do not include everything, only the model fields
246249
# else:
247250
# fields_values.update(values)
@@ -251,7 +254,7 @@ def sqlmodel_table_construct(
251254
# object.__setattr__(new_obj, "__dict__", fields_values)
252255
# instrumentation
253256
for key, value in {**old_dict, **fields_values}.items():
254-
setattr(self_instance, key, value)
257+
setattr_(self_instance, key, value)
255258
# End SQLModel override
256259
object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set)
257260
if not cls.__pydantic_root_model__:
@@ -268,7 +271,7 @@ def sqlmodel_table_construct(
268271
for key in self_instance.__sqlmodel_relationships__:
269272
value = values.get(key, Undefined)
270273
if value is not Undefined:
271-
setattr(self_instance, key, value)
274+
setattr_(self_instance, key, value)
272275
# End SQLModel override
273276
return self_instance
274277

@@ -305,6 +308,7 @@ def sqlmodel_validate(
305308
context=context,
306309
self_instance=new_obj,
307310
)
311+
setattr_ = object.__setattr__ if new_obj.model_config.get("frozen") else setattr
308312
# Capture fields set to restore it later
309313
fields_set = new_obj.__pydantic_fields_set__.copy()
310314
if not is_table_model_class(cls):
@@ -314,15 +318,15 @@ def sqlmodel_validate(
314318
# Do not set __dict__, instead use setattr to trigger SQLAlchemy
315319
# instrumentation
316320
for key, value in {**old_dict, **new_obj.__dict__}.items():
317-
setattr(new_obj, key, value)
321+
setattr_(new_obj, key, value)
318322
# Restore fields set
319323
object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set)
320324
# Get and set any relationship objects
321325
if is_table_model_class(cls):
322326
for key in new_obj.__sqlmodel_relationships__:
323327
value = getattr(use_obj, key, Undefined)
324328
if value is not Undefined:
325-
setattr(new_obj, key, value)
329+
setattr_(new_obj, key, value)
326330
return new_obj
327331

328332

tests/test_frozen.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import pytest
2+
from pydantic import ConfigDict, ValidationError
3+
from sqlmodel import Field, Session, SQLModel, create_engine, select
4+
5+
6+
def test_frozen_non_table_model_creation(clear_sqlmodel):
7+
class HeroBase(SQLModel):
8+
model_config = ConfigDict(frozen=True)
9+
10+
name: str
11+
age: int | None = None
12+
13+
hero = HeroBase(name="Deadpond", age=30)
14+
15+
assert hero.name == "Deadpond"
16+
assert hero.age == 30
17+
18+
19+
def test_frozen_non_table_model_is_immutable(clear_sqlmodel):
20+
class HeroBase(SQLModel):
21+
model_config = ConfigDict(frozen=True)
22+
23+
name: str
24+
age: int | None = None
25+
26+
hero = HeroBase(name="Deadpond", age=30)
27+
28+
with pytest.raises((ValidationError, TypeError)):
29+
hero.name = "Spider-Boy" # type: ignore[misc]
30+
31+
32+
def test_frozen_table_model_creation(clear_sqlmodel):
33+
class Hero(SQLModel, table=True):
34+
model_config = ConfigDict(frozen=True)
35+
36+
id: int | None = Field(default=None, primary_key=True)
37+
name: str
38+
age: int | None = None
39+
40+
hero = Hero(name="Deadpond", age=30)
41+
42+
assert hero.name == "Deadpond"
43+
assert hero.age == 30
44+
45+
46+
def test_frozen_table_model_persists_and_retrieves(clear_sqlmodel):
47+
class Hero(SQLModel, table=True):
48+
model_config = ConfigDict(frozen=True)
49+
50+
id: int | None = Field(default=None, primary_key=True)
51+
name: str
52+
age: int | None = None
53+
54+
engine = create_engine("sqlite://")
55+
SQLModel.metadata.create_all(engine)
56+
57+
with Session(engine) as session:
58+
hero = Hero(name="Deadpond", age=30)
59+
session.add(hero)
60+
session.commit()
61+
session.refresh(hero)
62+
63+
with Session(engine) as session:
64+
retrieved = session.exec(select(Hero)).one()
65+
assert retrieved.name == "Deadpond"
66+
assert retrieved.age == 30
67+
68+
69+
def test_frozen_table_model_validate(clear_sqlmodel):
70+
class Hero(SQLModel, table=True):
71+
model_config = ConfigDict(frozen=True)
72+
73+
id: int | None = Field(default=None, primary_key=True)
74+
name: str
75+
age: int | None = None
76+
77+
hero = Hero.model_validate({"name": "Deadpond", "age": 30})
78+
79+
assert hero.name == "Deadpond"
80+
assert hero.age == 30

0 commit comments

Comments
 (0)