Skip to content

Commit e0e7863

Browse files
xr843claude
andcommitted
Fixed mypy error and added tests for PrivateAttr defaults (#149)
- Used getattr() instead of direct attribute access to satisfy mypy's union-attr check on InstanceOrType. - Added regression tests verifying PrivateAttr with default and default_factory work correctly on database-loaded instances. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d589082 commit e0e7863

File tree

2 files changed

+55
-1
lines changed

2 files changed

+55
-1
lines changed

sqlmodel/_compat.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,10 @@ def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None:
9898
# mirroring what Pydantic's own BaseModel.__init__ does. Previously this was
9999
# set to None, which caused AttributeError when accessing PrivateAttr fields
100100
# on instances reconstructed from the database (via __new__, bypassing __init__).
101+
cls = new_object if isinstance(new_object, type) else new_object.__class__
102+
private_attributes = getattr(cls, "__private_attributes__", {})
101103
pydantic_private = {}
102-
for k, v in new_object.__class__.__private_attributes__.items():
104+
for k, v in private_attributes.items():
103105
pydantic_private[k] = v.get_default(call_default_factory=True)
104106
object.__setattr__(
105107
new_object,

tests/test_private_attr.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pydantic import PrivateAttr
2+
from sqlmodel import Field, Session, SQLModel, create_engine, select
3+
4+
5+
def test_private_attr_default_preserved_after_db_load(clear_sqlmodel):
6+
"""PrivateAttr defaults should be available on instances loaded from DB (#149)."""
7+
8+
class Item(SQLModel, table=True):
9+
id: int | None = Field(default=None, primary_key=True)
10+
name: str
11+
_secret: str = PrivateAttr(default="default_secret")
12+
13+
engine = create_engine("sqlite:///:memory:")
14+
SQLModel.metadata.create_all(engine)
15+
16+
with Session(engine) as db:
17+
item = Item(name="test")
18+
assert item._secret == "default_secret"
19+
db.add(item)
20+
db.commit()
21+
22+
with Session(engine) as db:
23+
loaded_item = db.exec(select(Item)).one()
24+
# Previously raised AttributeError because __pydantic_private__ was None.
25+
assert loaded_item._secret == "default_secret"
26+
27+
SQLModel.metadata.clear()
28+
29+
30+
def test_private_attr_default_factory_preserved_after_db_load(clear_sqlmodel):
31+
"""PrivateAttr with default_factory should work on DB-loaded instances (#149)."""
32+
33+
class Item(SQLModel, table=True):
34+
id: int | None = Field(default=None, primary_key=True)
35+
name: str
36+
_tags: list[str] = PrivateAttr(default_factory=list)
37+
38+
engine = create_engine("sqlite:///:memory:")
39+
SQLModel.metadata.create_all(engine)
40+
41+
with Session(engine) as db:
42+
item = Item(name="test")
43+
item._tags.append("hello")
44+
db.add(item)
45+
db.commit()
46+
47+
with Session(engine) as db:
48+
loaded_item = db.exec(select(Item)).one()
49+
# Should get a fresh empty list from default_factory, not AttributeError.
50+
assert loaded_item._tags == []
51+
52+
SQLModel.metadata.clear()

0 commit comments

Comments
 (0)