Skip to content

Commit 8ddc820

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

2 files changed

Lines changed: 86 additions & 4 deletions

File tree

sqlmodel/_compat.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ def sqlmodel_table_construct(
242242
_extra = {}
243243
for k, v in values.items():
244244
_extra[k] = v
245+
setattr_ = object.__setattr__ if self_instance.model_config.get("frozen") else setattr
245246
# SQLModel override, do not include everything, only the model fields
246247
# else:
247248
# fields_values.update(values)
@@ -251,7 +252,7 @@ def sqlmodel_table_construct(
251252
# object.__setattr__(new_obj, "__dict__", fields_values)
252253
# instrumentation
253254
for key, value in {**old_dict, **fields_values}.items():
254-
setattr(self_instance, key, value)
255+
setattr_(self_instance, key, value)
255256
# End SQLModel override
256257
object.__setattr__(self_instance, "__pydantic_fields_set__", _fields_set)
257258
if not cls.__pydantic_root_model__:
@@ -268,7 +269,7 @@ def sqlmodel_table_construct(
268269
for key in self_instance.__sqlmodel_relationships__:
269270
value = values.get(key, Undefined)
270271
if value is not Undefined:
271-
setattr(self_instance, key, value)
272+
setattr_(self_instance, key, value)
272273
# End SQLModel override
273274
return self_instance
274275

@@ -305,6 +306,7 @@ def sqlmodel_validate(
305306
context=context,
306307
self_instance=new_obj,
307308
)
309+
setattr_ = object.__setattr__ if new_obj.model_config.get("frozen") else setattr
308310
# Capture fields set to restore it later
309311
fields_set = new_obj.__pydantic_fields_set__.copy()
310312
if not is_table_model_class(cls):
@@ -314,15 +316,15 @@ def sqlmodel_validate(
314316
# Do not set __dict__, instead use setattr to trigger SQLAlchemy
315317
# instrumentation
316318
for key, value in {**old_dict, **new_obj.__dict__}.items():
317-
setattr(new_obj, key, value)
319+
setattr_(new_obj, key, value)
318320
# Restore fields set
319321
object.__setattr__(new_obj, "__pydantic_fields_set__", fields_set)
320322
# Get and set any relationship objects
321323
if is_table_model_class(cls):
322324
for key in new_obj.__sqlmodel_relationships__:
323325
value = getattr(use_obj, key, Undefined)
324326
if value is not Undefined:
325-
setattr(new_obj, key, value)
327+
setattr_(new_obj, key, value)
326328
return new_obj
327329

328330

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)