]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
typing updates
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 19 Jan 2023 20:34:46 +0000 (15:34 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 20 Jan 2023 14:13:58 +0000 (09:13 -0500)
The :meth:`_sql.ColumnOperators.in_` and
:meth:`_sql.ColumnOperators.not_in_` are typed to include
``Iterable[Any]`` rather than ``Sequence[Any]`` for more flexibility in
argument type.

The :func:`_sql.or_` and :func:`_sql.and_` from a typing perspective
require the first argument to be present, however these functions still
accept zero arguments which will emit a deprecation warning at runtime.
Typing is also added to support sending the fixed literal ``False`` for
:func:`_sql.or_` and ``True`` for :func:`_sql.and_` as the first argument
only, however the documentation now indicates sending the
:func:`_sql.false` and :func:`_sql.true` constructs in these cases as a
more explicit approach.

Fixed typing issue where iterating over a :class:`_orm.Query` object
was not correctly typed.

Fixes: #9122
Fixes: #9123
Fixes: #9125
Change-Id: I500e3e1b826717b3dd49afa1e682c3c8279c9226

doc/build/changelog/unreleased_20/more_typing.rst [new file with mode: 0644]
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/sql/_elements_constructors.py
lib/sqlalchemy/sql/coercions.py
lib/sqlalchemy/sql/elements.py
test/ext/mypy/plain_files/session.py
test/ext/mypy/plain_files/sql_operations.py
test/sql/test_deprecations.py
test/sql/test_operators.py

diff --git a/doc/build/changelog/unreleased_20/more_typing.rst b/doc/build/changelog/unreleased_20/more_typing.rst
new file mode 100644 (file)
index 0000000..b958d0d
--- /dev/null
@@ -0,0 +1,30 @@
+.. change::
+    :tags: typing, bug
+    :tickets: 9122
+
+    The :meth:`_sql.ColumnOperators.in_` and
+    :meth:`_sql.ColumnOperators.not_in_` are typed to include
+    ``Iterable[Any]`` rather than ``Sequence[Any]`` for more flexibility in
+    argument type.
+
+
+.. change::
+    :tags: typing, bug
+    :tickets: 9123
+
+    The :func:`_sql.or_` and :func:`_sql.and_` from a typing perspective
+    require the first argument to be present, however these functions still
+    accept zero arguments which will emit a deprecation warning at runtime.
+    Typing is also added to support sending the fixed literal ``False`` for
+    :func:`_sql.or_` and ``True`` for :func:`_sql.and_` as the first argument
+    only, however the documentation now indicates sending the
+    :func:`_sql.false` and :func:`_sql.true` constructs in these cases as a
+    more explicit approach.
+
+
+.. change::
+    :tags: typing, bug
+    :tickets: 9125
+
+    Fixed typing issue where iterating over a :class:`_orm.Query` object
+    was not correctly typed. 
index 8d29a45a841d5a31d2176730bdb979ff6bd1f5eb..ad4b3abcf56ea21ca7b7d71a1ed535e803e66526 100644 (file)
@@ -28,6 +28,7 @@ from typing import cast
 from typing import Dict
 from typing import Generic
 from typing import Iterable
+from typing import Iterator
 from typing import List
 from typing import Mapping
 from typing import Optional
@@ -2856,10 +2857,10 @@ class Query(
         except sa_exc.NoResultFound:
             return None
 
-    def __iter__(self) -> Iterable[_T]:
+    def __iter__(self) -> Iterator[_T]:
         result = self._iter()
         try:
-            yield from result
+            yield from result  # type: ignore
         except GeneratorExit:
             # issue #8710 - direct iteration is not re-usable after
             # an iterable block is broken, so close the result
index d97ede8685a54d86eaa7cb38d4e2997692cd4f1a..9b96322734b75fbd228add5eb0472fd898009bc9 100644 (file)
@@ -16,6 +16,7 @@ from typing import Optional
 from typing import overload
 from typing import Sequence
 from typing import Tuple as typing_Tuple
+from typing import TYPE_CHECKING
 from typing import TypeVar
 from typing import Union
 
@@ -112,7 +113,10 @@ def all_(expr: _ColumnExpressionArgument[_T]) -> CollectionAggregate[bool]:
     return CollectionAggregate._create_all(expr)
 
 
-def and_(*clauses: _ColumnExpressionArgument[bool]) -> ColumnElement[bool]:
+def and_(  # type: ignore[empty-body]
+    initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool]],
+    *clauses: _ColumnExpressionArgument[bool],
+) -> ColumnElement[bool]:
     r"""Produce a conjunction of expressions joined by ``AND``.
 
     E.g.::
@@ -150,13 +154,15 @@ def and_(*clauses: _ColumnExpressionArgument[bool]) -> ColumnElement[bool]:
     argument in order to be valid; a :func:`.and_` construct with no
     arguments is ambiguous.   To produce an "empty" or dynamically
     generated :func:`.and_`  expression, from a given list of expressions,
-    a "default" element of ``True`` should be specified::
+    a "default" element of :func:`_sql.true` (or just ``True``) should be
+    specified::
 
-        criteria = and_(True, *expressions)
+        from sqlalchemy import true
+        criteria = and_(true(), *expressions)
 
     The above expression will compile to SQL as the expression ``true``
     or ``1 = 1``, depending on backend, if no other expressions are
-    present.  If expressions are present, then the ``True`` value is
+    present.  If expressions are present, then the :func:`_sql.true` value is
     ignored as it does not affect the outcome of an AND expression that
     has other elements.
 
@@ -170,7 +176,13 @@ def and_(*clauses: _ColumnExpressionArgument[bool]) -> ColumnElement[bool]:
         :func:`.or_`
 
     """
-    return BooleanClauseList.and_(*clauses)
+    ...
+
+
+if not TYPE_CHECKING:
+    # handle deprecated case which allows zero-arguments
+    def and_(*clauses):  # noqa: F811
+        return BooleanClauseList.and_(*clauses)
 
 
 def any_(expr: _ColumnExpressionArgument[_T]) -> CollectionAggregate[bool]:
@@ -1251,7 +1263,10 @@ def nulls_last(column: _ColumnExpressionArgument[_T]) -> UnaryExpression[_T]:
     return UnaryExpression._create_nulls_last(column)
 
 
-def or_(*clauses: _ColumnExpressionArgument[bool]) -> ColumnElement[bool]:
+def or_(  # type: ignore[empty-body]
+    initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool]],
+    *clauses: _ColumnExpressionArgument[bool],
+) -> ColumnElement[bool]:
     """Produce a conjunction of expressions joined by ``OR``.
 
     E.g.::
@@ -1279,13 +1294,15 @@ def or_(*clauses: _ColumnExpressionArgument[bool]) -> ColumnElement[bool]:
     argument in order to be valid; a :func:`.or_` construct with no
     arguments is ambiguous.   To produce an "empty" or dynamically
     generated :func:`.or_`  expression, from a given list of expressions,
-    a "default" element of ``False`` should be specified::
+    a "default" element of :func:`_sql.false` (or just ``False``) should be
+    specified::
 
-        or_criteria = or_(False, *expressions)
+        from sqlalchemy import false
+        or_criteria = or_(false(), *expressions)
 
     The above expression will compile to SQL as the expression ``false``
     or ``0 = 1``, depending on backend, if no other expressions are
-    present.  If expressions are present, then the ``False`` value is
+    present.  If expressions are present, then the :func:`_sql.false` value is
     ignored as it does not affect the outcome of an OR expression which
     has other elements.
 
@@ -1299,7 +1316,13 @@ def or_(*clauses: _ColumnExpressionArgument[bool]) -> ColumnElement[bool]:
         :func:`.and_`
 
     """
-    return BooleanClauseList.or_(*clauses)
+    ...
+
+
+if not TYPE_CHECKING:
+    # handle deprecated case which allows zero-arguments
+    def or_(*clauses):  # noqa: F811
+        return BooleanClauseList.or_(*clauses)
 
 
 def over(
index 9fe65b0cd6401f2d25bafff1afabab075e1e416c..3b187e49d4d965fd6fffc583f14888c415d5e171 100644 (file)
@@ -311,7 +311,7 @@ def expect(
 
 @overload
 def expect(
-    role: Union[Type[roles.JoinTargetRole], Type[roles.OnClauseRole]],
+    role: Type[roles.JoinTargetRole],
     element: _JoinTargetProtocol,
     **kw: Any,
 ) -> _JoinTargetProtocol:
index 6d19494253cb4cf0f49fdc588c2e585f22ca0f5d..981f71964927e171b4676e4a546732ceb519296f 100644 (file)
@@ -863,7 +863,7 @@ class SQLCoreOperations(Generic[_T], ColumnOperators, TypingOnly):
         def in_(
             self,
             other: Union[
-                Sequence[Any], BindParameter[Any], roles.InElementRole
+                Iterable[Any], BindParameter[Any], roles.InElementRole
             ],
         ) -> BinaryExpression[bool]:
             ...
@@ -871,7 +871,7 @@ class SQLCoreOperations(Generic[_T], ColumnOperators, TypingOnly):
         def not_in(
             self,
             other: Union[
-                Sequence[Any], BindParameter[Any], roles.InElementRole
+                Iterable[Any], BindParameter[Any], roles.InElementRole
             ],
         ) -> BinaryExpression[bool]:
             ...
@@ -2944,16 +2944,39 @@ class BooleanClauseList(ExpressionClauseList[bool]):
         operator: OperatorType,
         continue_on: Any,
         skip_on: Any,
-        *clauses: _ColumnExpressionArgument[Any],
+        initial_clause: Any = _NoArg.NO_ARG,
+        *clauses: Any,
         **kw: Any,
     ) -> ColumnElement[Any]:
+
+        if initial_clause is _NoArg.NO_ARG:
+            # no elements period.  deprecated use case.  return an empty
+            # ClauseList construct that generates nothing unless it has
+            # elements added to it.
+            name = operator.__name__
+
+            util.warn_deprecated(
+                f"Invoking {name}() without arguments is deprecated, and "
+                f"will be disallowed in a future release.   For an empty "
+                f"""{name}() construct, use '{name}({
+                    'true()' if continue_on is True_._singleton else 'false()'
+                }, *args)' """
+                f"""or '{name}({
+                    'True' if continue_on is True_._singleton else 'False'
+                }, *args)'.""",
+                version="1.4",
+            )
+            return cls._construct_raw(operator)  # type: ignore[no-any-return]
+
         lcc, convert_clauses = cls._process_clauses_for_boolean(
             operator,
             continue_on,
             skip_on,
             [
                 coercions.expect(roles.WhereHavingRole, clause)
-                for clause in util.coerce_generator_arg(clauses)
+                for clause in util.coerce_generator_arg(
+                    (initial_clause,) + clauses
+                )
             ],
         )
 
@@ -2969,27 +2992,11 @@ class BooleanClauseList(ExpressionClauseList[bool]):
             )
 
             return cls._construct_raw(operator, flattened_clauses)  # type: ignore # noqa: E501
-        elif lcc == 1:
+        else:
+            assert lcc
             # just one element.  return it as a single boolean element,
             # not a list and discard the operator.
             return convert_clauses[0]  # type: ignore[no-any-return] # noqa: E501
-        else:
-            # no elements period.  deprecated use case.  return an empty
-            # ClauseList construct that generates nothing unless it has
-            # elements added to it.
-            util.warn_deprecated(
-                "Invoking %(name)s() without arguments is deprecated, and "
-                "will be disallowed in a future release.   For an empty "
-                "%(name)s() construct, use %(name)s(%(continue_on)s, *args)."
-                % {
-                    "name": operator.__name__,
-                    "continue_on": "True"
-                    if continue_on is True_._singleton
-                    else "False",
-                },
-                version="1.4",
-            )
-            return cls._construct_raw(operator)  # type: ignore[no-any-return] # noqa: E501
 
     @classmethod
     def _construct_for_whereclause(
@@ -3035,26 +3042,42 @@ class BooleanClauseList(ExpressionClauseList[bool]):
 
     @classmethod
     def and_(
-        cls, *clauses: _ColumnExpressionArgument[bool]
+        cls,
+        initial_clause: Union[
+            Literal[True], _ColumnExpressionArgument[bool], _NoArg
+        ] = _NoArg.NO_ARG,
+        *clauses: _ColumnExpressionArgument[bool],
     ) -> ColumnElement[bool]:
         r"""Produce a conjunction of expressions joined by ``AND``.
 
         See :func:`_sql.and_` for full documentation.
         """
         return cls._construct(
-            operators.and_, True_._singleton, False_._singleton, *clauses
+            operators.and_,
+            True_._singleton,
+            False_._singleton,
+            initial_clause,
+            *clauses,
         )
 
     @classmethod
     def or_(
-        cls, *clauses: _ColumnExpressionArgument[bool]
+        cls,
+        initial_clause: Union[
+            Literal[False], _ColumnExpressionArgument[bool], _NoArg
+        ] = _NoArg.NO_ARG,
+        *clauses: _ColumnExpressionArgument[bool],
     ) -> ColumnElement[bool]:
         """Produce a conjunction of expressions joined by ``OR``.
 
         See :func:`_sql.or_` for full documentation.
         """
         return cls._construct(
-            operators.or_, False_._singleton, True_._singleton, *clauses
+            operators.or_,
+            False_._singleton,
+            True_._singleton,
+            initial_clause,
+            *clauses,
         )
 
     @property
index 49f1b44cb4a31a23bd93c09f44f3101d52a2bd82..636e3854a51da3cdd0255b422df578d11c6833d7 100644 (file)
@@ -77,4 +77,16 @@ with Session(e) as sess:
     )
     sess.query(User).update({"name": User.name + " some name"})
 
+    # test #9125
+
+    for row in sess.query(User.id, User.name):
+
+        # EXPECTED_TYPE: Row[Tuple[int, str]]
+        reveal_type(row)
+
+    for uobj1 in sess.query(User):
+
+        # EXPECTED_TYPE: User
+        reveal_type(uobj1)
+
 # more result tests in typed_results.py
index 0ed0df661d111f825f8f7522047468b3ab813b8e..33db5f2ccf173036a08609bfc6c3b0d253939b6b 100644 (file)
@@ -1,11 +1,15 @@
 import typing
 
+from sqlalchemy import and_
 from sqlalchemy import Boolean
 from sqlalchemy import column
+from sqlalchemy import false
 from sqlalchemy import func
 from sqlalchemy import Integer
+from sqlalchemy import or_
 from sqlalchemy import select
 from sqlalchemy import String
+from sqlalchemy import true
 
 
 # builtin.pyi stubs define object.__eq__() as returning bool,  which
@@ -21,6 +25,28 @@ c2 = column("a", Integer)
 
 expr2 = c2.in_([1, 2, 3])
 
+expr2_set = c2.in_({1, 2, 3})
+
+expr2_gen = c2.in_((x for x in (1, 2, 3)))
+
+nexpr2 = c2.not_in([1, 2, 3])
+
+nexpr2_set = c2.not_in({1, 2, 3})
+
+nexpr2_gen = c2.not_in((x for x in (1, 2, 3)))
+
+short_cir1 = and_(True, c2 == 5)
+short_cir2 = or_(False, c2 == 5)
+
+short_cir3 = and_(true(), c2 == 5)
+short_cir4 = or_(false(), c2 == 5)
+
+# EXPECTED_MYPY: Missing positional argument "initial_clause" in call to "and_"
+no_empty_1 = and_()
+
+# EXPECTED_MYPY: Missing positional argument "initial_clause" in call to "or_"
+no_empty_2 = or_()
+
 expr3 = c2 / 5
 
 expr4 = -c2
index fdfb87f7246cc06222e4ba2d633fe4750ce122aa..65f6e7776891b09cd9f8326889367fd06d79fc14 100644 (file)
@@ -85,7 +85,8 @@ class DeprecationWarningsTest(fixtures.TestBase, AssertsCompiledSQL):
         with testing.expect_deprecated(
             r"Invoking and_\(\) without arguments is deprecated, and "
             r"will be disallowed in a future release.   For an empty "
-            r"and_\(\) construct, use and_\(True, \*args\)"
+            r"and_\(\) construct, use 'and_\(true\(\), \*args\)' or "
+            r"'and_\(True, \*args\)'"
         ):
             self.assert_compile(or_(and_()), "")
 
index 103520f1faa98a7007f9c43695e3c3ae778cfc40..d93ba61bac637f0add3f9e2d8d8a46278319aeb4 100644 (file)
@@ -2,6 +2,7 @@ import collections.abc as collections_abc
 import datetime
 import operator
 import pickle
+import re
 
 from sqlalchemy import and_
 from sqlalchemy import between
@@ -1401,20 +1402,34 @@ class ConjunctionTest(fixtures.TestBase, testing.AssertsCompiledSQL):
             dialect=default.DefaultDialect(supports_native_boolean=False),
         )
 
-    @combinations((and_, "and_", "True"), (or_, "or_", "False"))
-    def test_empty_clauses(self, op, str_op, str_continue):
+    @combinations(
+        (and_, "and_", r"true", "True"),
+        (or_, "or_", r"false", "False"),
+    )
+    def test_empty_clauses(self, op, str_op, str_continue, str_continue_2):
         # these warning classes will change to ArgumentError when the
         # deprecated behavior is disabled
 
         with expect_deprecated(
-            r"Invoking %(str_op)s\(\) without arguments is deprecated, and "
-            r"will be disallowed in a future release.   For an empty "
-            r"%(str_op)s\(\) construct, use "
-            r"%(str_op)s\(%(str_continue)s, \*args\)\."
-            % {"str_op": str_op, "str_continue": str_continue}
+            re.escape(
+                f"Invoking {str_op}() without arguments is deprecated, and "
+                "will be disallowed in a future release.   For an empty "
+                f"{str_op}() construct, use "
+                f"'{str_op}({str_continue}(), *args)' or "
+                f"'{str_op}({str_continue_2}, *args)'."
+            )
         ):
             op()
 
+    def test_empty_construct_for_whereclause(self):
+        eq_(BooleanClauseList._construct_for_whereclause(()), None)
+
+    def test_non_empty_construct_for_whereclause(self):
+        self.assert_compile(
+            BooleanClauseList._construct_for_whereclause([column("q") == 5]),
+            "q = :q_1",
+        )
+
     def test_empty_and_raw(self):
         self.assert_compile(
             BooleanClauseList._construct_raw(operators.and_), ""