From 5ec17fa601f0d1f254db09a731be5ac8af73e51f Mon Sep 17 00:00:00 2001 From: Micah Denbraver Date: Wed, 20 Aug 2025 16:58:02 -0400 Subject: [PATCH] Fix typing for `hybrid_property.__set__` to properly validate setter values While iterating on some typing improvements, my colleague @seamuswn pointed out mypy wasn't catching when values with invalid types were set using a `hybrid_property` setter. I believe this is all that's needed to fix the typing. ### Description Adjust `hybrid_property.__set__` to expect a value of the type that matches the generic's type variable. ### Checklist This pull request is: - [x] A documentation / typographical / small typing error fix - Good to go, no issue or tests are needed - [ ] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. **Have a nice day!** Closes: #12814 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12814 Pull-request-sha: 8a17b26aea264acf70c52de324b8ccb92b469f2d Change-Id: Ic99ccc68a32354ef6fe013ec17242058ad2d6d63 (cherry picked from commit 7a68e2aeffd43fc5b78df6182969e031e31043b9) --- lib/sqlalchemy/ext/hybrid.py | 6 ++-- .../plain_files/ext/hybrid/hybrid_five.py | 33 +++++++++++++++++++ .../plain_files/ext/hybrid/hybrid_two.py | 1 - 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 test/typing/plain_files/ext/hybrid/hybrid_five.py diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index c1c46e7c5f..13e8fbad19 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -1140,10 +1140,12 @@ class hybrid_property(interfaces.InspectionAttrInfo, ORMDescriptor[_T]): else: return self.fget(instance) - def __set__(self, instance: object, value: Any) -> None: + def __set__( + self, instance: object, value: Union[SQLCoreOperations[_T], _T] + ) -> None: if self.fset is None: raise AttributeError("can't set attribute") - self.fset(instance, value) + self.fset(instance, value) # type: ignore[arg-type] def __delete__(self, instance: object) -> None: if self.fdel is None: diff --git a/test/typing/plain_files/ext/hybrid/hybrid_five.py b/test/typing/plain_files/ext/hybrid/hybrid_five.py new file mode 100644 index 0000000000..24f5737d62 --- /dev/null +++ b/test/typing/plain_files/ext/hybrid/hybrid_five.py @@ -0,0 +1,33 @@ +from sqlalchemy import func +from sqlalchemy import select +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.sql.elements import SQLCoreOperations + + +class Base(DeclarativeBase): + pass + + +class MyModel(Base): + __tablename__ = "my_model" + + id: Mapped[int] = mapped_column(primary_key=True) + + int_col: Mapped[int | None] = mapped_column() + + @hybrid_property + def some_col(self) -> int: + return (self.int_col or 0) + 1 + + @some_col.inplace.setter + def _str_col_setter(self, value: int | SQLCoreOperations[int]) -> None: + self.int_col = value - 1 + + +m = MyModel(id=42, int_col=1) +m.some_col = 42 +m.some_col = select(func.max(MyModel.id)).scalar_subquery() +m.some_col = func.max(MyModel.id) diff --git a/test/typing/plain_files/ext/hybrid/hybrid_two.py b/test/typing/plain_files/ext/hybrid/hybrid_two.py index db50d7678e..b4f2aca769 100644 --- a/test/typing/plain_files/ext/hybrid/hybrid_two.py +++ b/test/typing/plain_files/ext/hybrid/hybrid_two.py @@ -133,7 +133,6 @@ class Foo(Base): def needs_update_getter(self) -> bool: return self.val - ... def needs_update_setter(self, value: bool) -> None: self.val = value -- 2.47.3