Skip to content

Commit 0566470

Browse files
committed
feat: implement __sqlalchemy_hybrid_property_setters__ for hybrid properties and enhance setter handling
1 parent ddd0474 commit 0566470

File tree

7 files changed

+132
-569
lines changed

7 files changed

+132
-569
lines changed

sqlmodel/_compat.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,9 @@ def sqlmodel_validate(
360360
if value is not Undefined:
361361
setattr(new_obj, key, value)
362362
# Get and set any hybrid property values with setters
363-
for key, value in cls.__dict__.items():
364-
if hasattr(value, '__set__') and hasattr(value, 'fget'):
365-
# This is likely a hybrid property with a setter
363+
if hasattr(new_obj, "__sqlalchemy_hybrid_property_setters__"):
364+
for key in new_obj.__sqlalchemy_hybrid_property_setters__:
365+
# Handle both dict and object access
366366
if isinstance(use_obj, dict):
367367
hybrid_value = use_obj.get(key, Undefined)
368368
else:
@@ -590,7 +590,10 @@ def sqlmodel_validate(
590590
):
591591
setattr(m, key, obj[key])
592592
# Check for hybrid properties with setters
593-
elif key in cls.__dict__ and hasattr(cls.__dict__[key], '__set__') and hasattr(cls.__dict__[key], 'fget'):
593+
elif (
594+
hasattr(m, "__sqlalchemy_hybrid_property_setters__")
595+
and key in m.__sqlalchemy_hybrid_property_setters__
596+
):
594597
setattr(m, key, obj[key])
595598
m._init_private_attributes() # type: ignore[attr-defined] # noqa
596599
return m
@@ -617,5 +620,8 @@ def sqlmodel_init(*, self: "SQLModel", data: Dict[str, Any]) -> None:
617620
elif key in self.__sqlalchemy_association_proxies__:
618621
setattr(self, key, data[key])
619622
# Check for hybrid properties with setters
620-
elif key in self.__class__.__dict__ and hasattr(self.__class__.__dict__[key], '__set__') and hasattr(self.__class__.__dict__[key], 'fget'):
623+
elif (
624+
hasattr(self, "__sqlalchemy_hybrid_property_setters__")
625+
and key in self.__sqlalchemy_hybrid_property_setters__
626+
):
621627
setattr(self, key, data[key])

sqlmodel/main.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ class SQLModelMetaclass(ModelMetaclass, DeclarativeMeta):
499499
__sqlmodel_relationships__: Dict[str, RelationshipInfo]
500500
__sqlalchemy_constructs__: Dict[str, SQLAlchemyConstruct]
501501
__sqlalchemy_association_proxies__: Dict[str, AssociationProxy]
502+
__sqlalchemy_hybrid_property_setters__: Dict[str, hybrid_property]
502503
model_config: SQLModelConfig
503504
model_fields: Dict[str, FieldInfo]
504505
__config__: Type[SQLModelConfig]
@@ -529,14 +530,37 @@ def __new__(
529530
relationships: Dict[str, RelationshipInfo] = {}
530531
sqlalchemy_constructs: Dict[str, SQLAlchemyConstruct] = {}
531532
sqlalchemy_association_proxies: Dict[str, AssociationProxy] = {}
533+
sqlalchemy_hybrid_property_setters: Dict[str, hybrid_property] = {}
532534
dict_for_pydantic = {}
533535
original_annotations = get_annotations(class_dict)
534536
pydantic_annotations = {}
535537
relationship_annotations = {}
538+
# First pass - collect all hybrid properties
539+
hybrid_properties_with_setters = {}
540+
for k, v in class_dict.items():
541+
if (
542+
isinstance(v, hybrid_property)
543+
and hasattr(v, "fset")
544+
and v.fset is not None
545+
):
546+
# Store by the hybrid_property object itself to avoid duplicates
547+
hybrid_properties_with_setters[v] = k
548+
549+
# Find the main property name (shortest name for each hybrid_property object)
550+
for hybrid_prop_obj, _prop_name in hybrid_properties_with_setters.items():
551+
# Find all names that point to this same hybrid_property object
552+
all_names_for_this_prop = [
553+
name for name, obj in class_dict.items() if obj is hybrid_prop_obj
554+
]
555+
# Choose the shortest name (main property, not setter function)
556+
main_name = min(all_names_for_this_prop, key=len)
557+
sqlalchemy_hybrid_property_setters[main_name] = hybrid_prop_obj
558+
559+
# Second pass - process all items
536560
for k, v in class_dict.items():
537561
if isinstance(v, AssociationProxy):
538562
sqlalchemy_association_proxies[k] = v
539-
if isinstance(v, RelationshipInfo):
563+
elif isinstance(v, RelationshipInfo):
540564
relationships[k] = v
541565
elif isinstance(
542566
v,
@@ -563,6 +587,7 @@ def __new__(
563587
"__annotations__": pydantic_annotations,
564588
"__sqlalchemy_constructs__": sqlalchemy_constructs,
565589
"__sqlalchemy_association_proxies__": sqlalchemy_association_proxies,
590+
"__sqlalchemy_hybrid_property_setters__": sqlalchemy_hybrid_property_setters,
566591
}
567592
# Duplicate logic from Pydantic to filter config kwargs because if they are
568593
# passed directly including the registry Pydantic will pass them over to the
@@ -872,6 +897,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry
872897
__tablename__: ClassVar[Union[str, Callable[..., str]]]
873898
__sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty[Any]]]
874899
__sqlalchemy_association_proxies__: ClassVar[Dict[str, AssociationProxy]]
900+
__sqlalchemy_hybrid_property_setters__: ClassVar[Dict[str, hybrid_property]]
875901
__name__: ClassVar[str]
876902
metadata: ClassVar[MetaData]
877903
__allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six
@@ -941,23 +967,19 @@ def __setattr__(self, name: str, value: Any) -> None:
941967
# Set in SQLAlchemy hybrid properties with setters
942968
if (
943969
is_table_model_class(self.__class__)
944-
and name in self.__class__.__dict__
970+
and name in self.__sqlalchemy_hybrid_property_setters__
945971
):
946-
class_attr = self.__class__.__dict__[name]
947-
# Check if this is a hybrid property with a setter
948-
if hasattr(class_attr, '__set__'):
949-
try:
950-
# Try to use the hybrid property setter
951-
class_attr.__set__(self, value)
952-
return
953-
except AttributeError:
954-
# No setter available, continue with normal flow
955-
pass
972+
hybrid_property_setter = self.__sqlalchemy_hybrid_property_setters__[
973+
name
974+
]
975+
hybrid_property_setter.__set__(self, value)
976+
return
956977
# Set in Pydantic model to trigger possible validation changes, only for
957978
# non relationship values
958979
if (
959980
name not in self.__sqlmodel_relationships__
960981
and name not in self.__sqlalchemy_association_proxies__
982+
and name not in self.__sqlalchemy_hybrid_property_setters__
961983
):
962984
super().__setattr__(name, value)
963985

test_hybrid_property.py

Lines changed: 0 additions & 183 deletions
This file was deleted.

0 commit comments

Comments
 (0)