Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion sqlmodel/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,20 @@ def get_model_fields(model: InstanceOrType[BaseModel]) -> dict[str, "FieldInfo"]
def init_pydantic_private_attrs(new_object: InstanceOrType["SQLModel"]) -> None:
object.__setattr__(new_object, "__pydantic_fields_set__", set())
object.__setattr__(new_object, "__pydantic_extra__", None)
object.__setattr__(new_object, "__pydantic_private__", None)
# Initialize __pydantic_private__ with defaults from __private_attributes__,
# mirroring what Pydantic's own BaseModel.__init__ does. Previously this was
# set to None, which caused AttributeError when accessing PrivateAttr fields
# on instances reconstructed from the database (via __new__, bypassing __init__).
cls = new_object if isinstance(new_object, type) else new_object.__class__
private_attributes = getattr(cls, "__private_attributes__", {})
pydantic_private = {}
for k, v in private_attributes.items():
pydantic_private[k] = v.get_default()
object.__setattr__(
new_object,
"__pydantic_private__",
pydantic_private if pydantic_private else None,
)


def get_annotations(class_dict: dict[str, Any]) -> dict[str, Any]:
Expand Down
52 changes: 52 additions & 0 deletions tests/test_private_attr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from pydantic import PrivateAttr
from sqlmodel import Field, Session, SQLModel, create_engine, select


def test_private_attr_default_preserved_after_db_load(clear_sqlmodel):
"""PrivateAttr defaults should be available on instances loaded from DB (#149)."""

class Item(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
_secret: str = PrivateAttr(default="default_secret")

engine = create_engine("sqlite:///:memory:")
SQLModel.metadata.create_all(engine)

with Session(engine) as db:
item = Item(name="test")
assert item._secret == "default_secret"
db.add(item)
db.commit()

with Session(engine) as db:
loaded_item = db.exec(select(Item)).one()
# Previously raised AttributeError because __pydantic_private__ was None.
assert loaded_item._secret == "default_secret"

SQLModel.metadata.clear()


def test_private_attr_default_factory_preserved_after_db_load(clear_sqlmodel):
"""PrivateAttr with default_factory should work on DB-loaded instances (#149)."""

class Item(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
_tags: list[str] = PrivateAttr(default_factory=list)

engine = create_engine("sqlite:///:memory:")
SQLModel.metadata.create_all(engine)

with Session(engine) as db:
item = Item(name="test")
item._tags.append("hello")
db.add(item)
db.commit()

with Session(engine) as db:
loaded_item = db.exec(select(Item)).one()
# Should get a fresh empty list from default_factory, not AttributeError.
assert loaded_item._tags == []

SQLModel.metadata.clear()
Loading