From: Mike Bayer Date: Mon, 21 Oct 2024 14:03:01 +0000 (-0400) Subject: refine in_() check to use proper duck-typing for __clause_element__ X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=aaddd7c8403e9ca2f77113467b5e2ae279a542c4;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git refine in_() check to use proper duck-typing for __clause_element__ 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 --- diff --git a/doc/build/changelog/unreleased_20/12019.rst b/doc/build/changelog/unreleased_20/12019.rst new file mode 100644 index 0000000000..3c7c1f4d01 --- /dev/null +++ b/doc/build/changelog/unreleased_20/12019.rst @@ -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. diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index 1d11cbbd3d..136fc48646 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -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: diff --git a/test/orm/test_query.py b/test/orm/test_query.py index e86283de30..7910ddb924 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -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 diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index 9c87b35577..8afe091925 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -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]),