]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
refine in_() check to use proper duck-typing for __clause_element__
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 21 Oct 2024 14:03:01 +0000 (10:03 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 21 Oct 2024 14:19:27 +0000 (10:19 -0400)
Fixed regression caused by an internal code change in response to recent
Mypy releases that caused the very unusual case of a list of ORM-mapped
attribute expressions passed to :meth:`.ColumnOperators.in_` to no longer
be accepted.

in this commit we had to revisit d8dd28c42e where mypy typing
didn't accept ColumnOperartors.   the type here is the _HasClauseElement[_T]
protocol which means we need to use a duck type for a runtime check.

Fixes: #12019
Change-Id: Ib378e9cb8defb49d5ac4d726ec93d6bdc581b6a9

doc/build/changelog/unreleased_20/12019.rst [new file with mode: 0644]
lib/sqlalchemy/sql/coercions.py
test/orm/test_query.py
test/sql/test_operators.py

diff --git a/doc/build/changelog/unreleased_20/12019.rst b/doc/build/changelog/unreleased_20/12019.rst
new file mode 100644 (file)
index 0000000..3c7c1f4
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: orm, bug
+    :tickets: 12019
+
+    Fixed regression caused by an internal code change in response to recent
+    Mypy releases that caused the very unusual case of a list of ORM-mapped
+    attribute expressions passed to :meth:`.ColumnOperators.in_` to no longer
+    be accepted.
index 1d11cbbd3d2829c2f775ceabff6c032fcc779299..136fc486463f6679d78e9e1b6c9abc9fcdc53782 100644 (file)
@@ -846,15 +846,15 @@ class InElementImpl(RoleImpl):
     def _literal_coercion(self, element, *, expr, operator, **kw):
         if util.is_non_string_iterable(element):
             non_literal_expressions: Dict[
-                Optional[ColumnElement[Any]],
-                ColumnElement[Any],
+                Optional[_ColumnExpressionArgument[Any]],
+                _ColumnExpressionArgument[Any],
             ] = {}
             element = list(element)
             for o in element:
                 if not _is_literal(o):
                     if not isinstance(
                         o, util.preloaded.sql_elements.ColumnElement
-                    ):
+                    ) and not hasattr(o, "__clause_element__"):
                         self._raise_for_expected(element, **kw)
 
                     else:
index e86283de30c1a0cd192dc4e19551b6867fd0c5f8..7910ddb9246620539ae7129bcea337eae96089a3 100644 (file)
@@ -1979,6 +1979,15 @@ class OperatorTest(QueryTest, AssertsCompiledSQL):
 
         assert_raises(NotImplementedError, Address.user.in_, [User(id=5)])
 
+    def test_in_instrumented_attribute(self):
+        """test #12019"""
+        User = self.classes.User
+
+        self._test(
+            User.id.in_([User.id, User.name]),
+            "users.id IN (users.id, users.name)",
+        )
+
     def test_neg(self):
         User = self.classes.User
 
index 9c87b3557763cc415f3a0b5b1c8c37400018a6eb..8afe091925ae7f21a4b4ba708eea4287a29fdd82 100644 (file)
@@ -83,6 +83,14 @@ class LoopOperate(operators.ColumnOperators):
         return op
 
 
+class ColExpressionDuckTypeOnly:
+    def __init__(self, expr):
+        self.expr = expr
+
+    def __clause_element__(self):
+        return self.expr
+
+
 class DefaultColumnComparatorTest(
     testing.AssertsCompiledSQL, fixtures.TestBase
 ):
@@ -2198,6 +2206,15 @@ class InTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             "mytable.myid IN (mytable.myid)",
         )
 
+    def test_in_14_5(self):
+        """test #12019"""
+        self.assert_compile(
+            self.table1.c.myid.in_(
+                [ColExpressionDuckTypeOnly(self.table1.c.myid)]
+            ),
+            "mytable.myid IN (mytable.myid)",
+        )
+
     def test_in_15(self):
         self.assert_compile(
             self.table1.c.myid.in_(["a", self.table1.c.myid]),