From: Yurii Karabas <1998uriyyo@gmail.com> Date: Wed, 17 Jan 2024 17:08:05 +0000 (-0500) Subject: Add PEP 646 integration X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=00072000c53d27fff1044722e3fbf265887c6ef3;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add PEP 646 integration The :class:`.Row` object now no longer makes use of an intermediary ``Tuple`` in order to represent its individual element types; instead, the individual element types are present directly, via new :pep:`646` integration, now available in more recent versions of Mypy. Mypy 1.7 or greater is now required for statements, results and rows to be correctly typed. Pull request courtesy Yurii Karabas. Fixes: #10635 Closes: #10634 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/10634 Pull-request-sha: 430785c8a04a48fe96ce35b4f4e08476700c1f79 Change-Id: Ibd0ae31a98b4ea69dcb89f970e640920b2be6c48 --- diff --git a/doc/build/changelog/migration_21.rst b/doc/build/changelog/migration_21.rst index 8edea83839..95112b09b7 100644 --- a/doc/build/changelog/migration_21.rst +++ b/doc/build/changelog/migration_21.rst @@ -10,6 +10,76 @@ What's New in SQLAlchemy 2.1? version 2.1. +.. _change_10635: + +``Row`` now represents individual column types directly without ``Tuple`` +-------------------------------------------------------------------------- + +SQLAlchemy 2.0 implemented a broad array of :pep:`484` typing throughout +all components, including a new ability for row-returning statements such +as :func:`_sql.select` to maintain track of individual column types, which +were then passed through the execution phase onto the :class:`_engine.Result` +object and then to the individual :class:`_engine.Row` objects. Described +at :ref:`change_result_typing_20`, this approach solved several issues +with statement / row typing, but some remained unsolvable. In 2.1, one +of those issues, that the individual column types needed to be packaged +into a ``typing.Tuple``, is now resolved using new :pep:`646` integration, +which allows for tuple-like types that are not actually typed as ``Tuple``. + +In SQLAlchemy 2.0, a statement such as:: + + stmt = select(column("x", Integer), column("y", String)) + +Would be typed as:: + + Select[Tuple[int, str]] + +In 2.1, it's now typed as:: + + Select[int, str] + +When executing ``stmt``, the :class:`_engine.Result` and :class:`_engine.Row` +objects will be typed as ``Result[int, str]`` and ``Row[int, str]``, respectively. +The prior workaround using :attr:`_engine.Row._t` to type as a real ``Tuple`` +is no longer needed and projects can migrate off this pattern. + +Mypy users will need to make use of **Mypy 1.7 or greater** for pep-646 +integration to be available. + +Limitations +^^^^^^^^^^^ + +Not yet solved by pep-646 or any other pep is the ability for an arbitrary +number of expressions within :class:`_sql.Select` and others to be mapped to +row objects, without stating each argument position explicitly within typing +annotations. To work around this issue, SQLAlchemy makes use of automated +"stub generation" tools to generate hardcoded mappings of different numbers of +positional arguments to constructs like :func:`_sql.select` to resolve to +individual ``Unpack[]`` expressions (in SQLAlchemy 2.0, this generation +prodcued ``Tuple[]`` annotations instead). This means that there are arbitrary +limits on how many specific column expressions will be typed within the +:class:`_engine.Row` object, without restoring to ``Any`` for remaining +expressions; for :func:`_sql.select`, it's currently ten expressions, and +for DML expresions like :func:`_dml.insert` that use :meth:`_dml.Insert.returning`, +it's eight. If and when a new pep that provides a ``Map`` operator +to pep-646 is proposed, this limitation can be lifted. [1]_ Originally, it was +mistakenly assumed that this limitation prevented pep-646 from being usable at all, +however, the ``Unpack`` construct does in fact replace everything that +was done using ``Tuple`` in 2.0. + +An additional limitation for which there is no proposed solution is that +there's no way for the name-based attributes on :class:`_engine.Row` to be +automatically typed, so these continue to be typed as ``Any`` (e.g. ``row.x`` +and ``row.y`` for the above example). With current language features, +this could only be fixed by having an explicit class-based construct that +allows one to compose an explicit :class:`_engine.Row` with explicit fields +up front, which would be verbose and not automatic. + +.. [1] https://github.com/python/typing/discussions/1001#discussioncomment-1897813 + +:ticket:`10635` + + .. _change_10197: Asyncio "greenlet" dependency no longer installs by default diff --git a/doc/build/changelog/unreleased_21/10296.rst b/doc/build/changelog/unreleased_21/10296.rst index c674ecbe1a..c58eb85660 100644 --- a/doc/build/changelog/unreleased_21/10296.rst +++ b/doc/build/changelog/unreleased_21/10296.rst @@ -7,4 +7,4 @@ be imported only when the asyncio extension is first imported. Alternatively, the ``greenlet`` library is still imported lazily on first use to support use case that don't make direct use of the - SQLAlchemy asyncio extension. \ No newline at end of file + SQLAlchemy asyncio extension. diff --git a/doc/build/changelog/unreleased_21/10635.rst b/doc/build/changelog/unreleased_21/10635.rst new file mode 100644 index 0000000000..81fbba97d8 --- /dev/null +++ b/doc/build/changelog/unreleased_21/10635.rst @@ -0,0 +1,14 @@ +.. change:: + :tags: typing, feature + :tickets: 10635 + + The :class:`.Row` object now no longer makes use of an intermediary + ``Tuple`` in order to represent its individual element types; instead, + the individual element types are present directly, via new :pep:`646` + integration, now available in more recent versions of Mypy. Mypy + 1.7 or greater is now required for statements, results and rows + to be correctly typed. Pull request courtesy Yurii Karabas. + + .. seealso:: + + :ref:`change_10635` diff --git a/doc/build/changelog/whatsnew_20.rst b/doc/build/changelog/whatsnew_20.rst index 179ed55f2d..66610e26c4 100644 --- a/doc/build/changelog/whatsnew_20.rst +++ b/doc/build/changelog/whatsnew_20.rst @@ -75,6 +75,7 @@ result set. for the 2.0 series. Typing details are subject to change however significant backwards-incompatible changes are not planned. +.. _change_result_typing_20: SQL Expression / Statement / Result Set Typing ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/sqlalchemy/engine/_py_row.py b/lib/sqlalchemy/engine/_py_row.py index 4e1dd7d430..94ba85f2c2 100644 --- a/lib/sqlalchemy/engine/_py_row.py +++ b/lib/sqlalchemy/engine/_py_row.py @@ -18,10 +18,11 @@ from typing import Optional from typing import Tuple from typing import Type +from ..util.typing import TupleAny + if typing.TYPE_CHECKING: from .result import _KeyType from .result import _ProcessorsType - from .result import _RawRowType from .result import _TupleGetterType from .result import ResultMetaData @@ -33,14 +34,14 @@ class BaseRow: _parent: ResultMetaData _key_to_index: Mapping[_KeyType, int] - _data: _RawRowType + _data: TupleAny def __init__( self, parent: ResultMetaData, processors: Optional[_ProcessorsType], key_to_index: Mapping[_KeyType, int], - data: _RawRowType, + data: TupleAny, ): """Row objects are constructed by CursorResult objects.""" object.__setattr__(self, "_parent", parent) diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index dcce3ed342..2706bbe0ee 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -43,6 +43,9 @@ from .. import log from .. import util from ..sql import compiler from ..sql import util as sql_util +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack if typing.TYPE_CHECKING: from . import CursorResult @@ -80,6 +83,7 @@ if typing.TYPE_CHECKING: _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") _EMPTY_EXECUTION_OPTS: _ExecuteOptions = util.EMPTY_DICT NO_OPTIONS: Mapping[str, Any] = util.EMPTY_DICT @@ -1258,7 +1262,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): @overload def scalar( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], parameters: Optional[_CoreSingleExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, @@ -1307,7 +1311,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): @overload def scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, @@ -1352,11 +1356,11 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): @overload def execute( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[_T]: + ) -> CursorResult[Unpack[_Ts]]: ... @overload @@ -1366,7 +1370,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: ... def execute( @@ -1375,7 +1379,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: r"""Executes a SQL statement construct and returns a :class:`_engine.CursorResult`. @@ -1424,7 +1428,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): func: FunctionElement[Any], distilled_parameters: _CoreMultiExecuteParams, execution_options: CoreExecuteOptionsParameter, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """Execute a sql.FunctionElement object.""" return self._execute_clauseelement( @@ -1495,7 +1499,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): ddl: ExecutableDDLElement, distilled_parameters: _CoreMultiExecuteParams, execution_options: CoreExecuteOptionsParameter, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """Execute a schema.DDL object.""" exec_opts = ddl._execution_options.merge_with( @@ -1590,7 +1594,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): elem: Executable, distilled_parameters: _CoreMultiExecuteParams, execution_options: CoreExecuteOptionsParameter, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """Execute a sql.ClauseElement object.""" execution_options = elem._execution_options.merge_with( @@ -1663,7 +1667,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): compiled: Compiled, distilled_parameters: _CoreMultiExecuteParams, execution_options: CoreExecuteOptionsParameter = _EMPTY_EXECUTION_OPTS, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """Execute a sql.Compiled object. TODO: why do we have this? likely deprecate or remove @@ -1713,7 +1717,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): statement: str, parameters: Optional[_DBAPIAnyExecuteParams] = None, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: r"""Executes a string SQL statement on the DBAPI cursor directly, without any SQL compilation steps. @@ -1795,7 +1799,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): execution_options: _ExecuteOptions, *args: Any, **kw: Any, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """Create an :class:`.ExecutionContext` and execute, returning a :class:`_engine.CursorResult`.""" @@ -1854,7 +1858,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): context: ExecutionContext, statement: Union[str, Compiled], parameters: Optional[_AnyMultiExecuteParams], - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """continue the _execute_context() method for a single DBAPI cursor.execute() or cursor.executemany() call. @@ -1994,7 +1998,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): self, dialect: Dialect, context: ExecutionContext, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: """continue the _execute_context() method for an "insertmanyvalues" operation, which will invoke DBAPI cursor.execute() one or more times with individual log and diff --git a/lib/sqlalchemy/engine/cursor.py b/lib/sqlalchemy/engine/cursor.py index a46a9af16f..c56065bfe6 100644 --- a/lib/sqlalchemy/engine/cursor.py +++ b/lib/sqlalchemy/engine/cursor.py @@ -28,7 +28,6 @@ from typing import Optional from typing import Sequence from typing import Tuple from typing import TYPE_CHECKING -from typing import TypeVar from typing import Union from .result import IteratorResult @@ -53,6 +52,9 @@ from ..sql.type_api import TypeEngine from ..util import compat from ..util.typing import Literal from ..util.typing import Self +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack if typing.TYPE_CHECKING: @@ -71,7 +73,7 @@ if typing.TYPE_CHECKING: from ..sql.type_api import _ResultProcessorType -_T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") # metadata entry tuple indexes. @@ -344,7 +346,7 @@ class CursorResultMetaData(ResultMetaData): def __init__( self, - parent: CursorResult[Any], + parent: CursorResult[Unpack[TupleAny]], cursor_description: _DBAPICursorDescription, ): context = parent.context @@ -928,18 +930,22 @@ class ResultFetchStrategy: alternate_cursor_description: Optional[_DBAPICursorDescription] = None def soft_close( - self, result: CursorResult[Any], dbapi_cursor: Optional[DBAPICursor] + self, + result: CursorResult[Unpack[TupleAny]], + dbapi_cursor: Optional[DBAPICursor], ) -> None: raise NotImplementedError() def hard_close( - self, result: CursorResult[Any], dbapi_cursor: Optional[DBAPICursor] + self, + result: CursorResult[Unpack[TupleAny]], + dbapi_cursor: Optional[DBAPICursor], ) -> None: raise NotImplementedError() def yield_per( self, - result: CursorResult[Any], + result: CursorResult[Unpack[TupleAny]], dbapi_cursor: Optional[DBAPICursor], num: int, ) -> None: @@ -947,7 +953,7 @@ class ResultFetchStrategy: def fetchone( self, - result: CursorResult[Any], + result: CursorResult[Unpack[TupleAny]], dbapi_cursor: DBAPICursor, hard_close: bool = False, ) -> Any: @@ -955,7 +961,7 @@ class ResultFetchStrategy: def fetchmany( self, - result: CursorResult[Any], + result: CursorResult[Unpack[TupleAny]], dbapi_cursor: DBAPICursor, size: Optional[int] = None, ) -> Any: @@ -963,14 +969,14 @@ class ResultFetchStrategy: def fetchall( self, - result: CursorResult[Any], + result: CursorResult[Unpack[TupleAny]], dbapi_cursor: DBAPICursor, ) -> Any: raise NotImplementedError() def handle_exception( self, - result: CursorResult[Any], + result: CursorResult[Unpack[TupleAny]], dbapi_cursor: Optional[DBAPICursor], err: BaseException, ) -> NoReturn: @@ -1375,7 +1381,7 @@ def null_dml_result() -> IteratorResult[Any]: return it -class CursorResult(Result[_T]): +class CursorResult(Result[Unpack[_Ts]]): """A Result that is representing state from a DBAPI cursor. .. versionchanged:: 1.4 The :class:`.CursorResult`` @@ -2108,7 +2114,9 @@ class CursorResult(Result[_T]): def _raw_row_iterator(self): return self._fetchiter_impl() - def merge(self, *others: Result[Any]) -> MergedResult[Any]: + def merge( + self, *others: Result[Unpack[TupleAny]] + ) -> MergedResult[Unpack[TupleAny]]: merged_result = super().merge(*others) setup_rowcounts = self.context._has_rowcount if setup_rowcounts: diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index afbda08b46..4e4561df38 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -66,6 +66,9 @@ from ..sql.compiler import InsertmanyvaluesSentinelOpts from ..sql.compiler import SQLCompiler from ..sql.elements import quoted_name from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import Unpack + if typing.TYPE_CHECKING: from types import ModuleType @@ -1187,7 +1190,7 @@ class DefaultExecutionContext(ExecutionContext): result_column_struct: Optional[ Tuple[List[ResultColumnsEntry], bool, bool, bool, bool] ] = None - returned_default_rows: Optional[Sequence[Row[Any]]] = None + returned_default_rows: Optional[Sequence[Row[Unpack[TupleAny]]]] = None execution_options: _ExecuteOptions = util.EMPTY_DICT diff --git a/lib/sqlalchemy/engine/events.py b/lib/sqlalchemy/engine/events.py index b8e8936b94..2416cd989f 100644 --- a/lib/sqlalchemy/engine/events.py +++ b/lib/sqlalchemy/engine/events.py @@ -25,6 +25,8 @@ from .interfaces import Dialect from .. import event from .. import exc from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import Unpack if typing.TYPE_CHECKING: from .interfaces import _CoreMultiExecuteParams @@ -270,7 +272,7 @@ class ConnectionEvents(event.Events[ConnectionEventsTarget]): multiparams: _CoreMultiExecuteParams, params: _CoreSingleExecuteParams, execution_options: _ExecuteOptions, - result: Result[Any], + result: Result[Unpack[TupleAny]], ) -> None: """Intercept high level execute() events after execute. diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index f1c18cf456..b74b9d343b 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -40,11 +40,15 @@ from .. import util from ..sql.base import _generative from ..sql.base import HasMemoized from ..sql.base import InPlaceGenerative +from ..util import deprecated from ..util import HasMemoized_ro_memoized_attribute from ..util import NONE_SET from ..util._has_cy import HAS_CYEXTENSION from ..util.typing import Literal from ..util.typing import Self +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack if typing.TYPE_CHECKING or not HAS_CYEXTENSION: from ._py_row import tuplegetter as tuplegetter @@ -64,25 +68,23 @@ _KeyMapRecType = Any _KeyMapType = Mapping[_KeyType, _KeyMapRecType] -_RowData = Union[Row[Any], RowMapping, Any] +_RowData = Union[Row[Unpack[TupleAny]], RowMapping, Any] """A generic form of "row" that accommodates for the different kinds of "rows" that different result objects return, including row, row mapping, and scalar values""" -_RawRowType = Tuple[Any, ...] -"""represents the kind of row we get from a DBAPI cursor""" _R = TypeVar("_R", bound=_RowData) _T = TypeVar("_T", bound=Any) -_TP = TypeVar("_TP", bound=Tuple[Any, ...]) +_Ts = TypeVarTuple("_Ts") -_InterimRowType = Union[_R, _RawRowType] +_InterimRowType = Union[_R, TupleAny] """a catchall "anything" kind of return type that can be applied across all the result types """ -_InterimSupportsScalarsRowType = Union[Row[Any], Any] +_InterimSupportsScalarsRowType = Union[Row[Unpack[TupleAny]], Any] _ProcessorsType = Sequence[Optional["_ResultProcessorType[Any]"]] _TupleGetterType = Callable[[Sequence[Any]], Sequence[Any]] @@ -168,7 +170,7 @@ class ResultMetaData: def _getter( self, key: Any, raiseerr: bool = True - ) -> Optional[Callable[[Row[Any]], Any]]: + ) -> Optional[Callable[[Row[Unpack[TupleAny]]], Any]]: index = self._index_for_key(key, raiseerr) if index is not None: @@ -391,7 +393,7 @@ class SimpleResultMetaData(ResultMetaData): def result_tuple( fields: Sequence[str], extra: Optional[Any] = None -) -> Callable[[Iterable[Any]], Row[Any]]: +) -> Callable[[Iterable[Any]], Row[Unpack[TupleAny]]]: parent = SimpleResultMetaData(fields, extra) return functools.partial( Row, parent, parent._effective_processors, parent._key_to_index @@ -411,7 +413,7 @@ _NO_ROW = _NoRow._NO_ROW class ResultInternal(InPlaceGenerative, Generic[_R]): __slots__ = () - _real_result: Optional[Result[Any]] = None + _real_result: Optional[Result[Unpack[TupleAny]]] = None _generate_rows: bool = True _row_logging_fn: Optional[Callable[[Any], Any]] @@ -423,20 +425,24 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): _source_supports_scalars: bool - def _fetchiter_impl(self) -> Iterator[_InterimRowType[Row[Any]]]: + def _fetchiter_impl( + self, + ) -> Iterator[_InterimRowType[Row[Unpack[TupleAny]]]]: raise NotImplementedError() def _fetchone_impl( self, hard_close: bool = False - ) -> Optional[_InterimRowType[Row[Any]]]: + ) -> Optional[_InterimRowType[Row[Unpack[TupleAny]]]]: raise NotImplementedError() def _fetchmany_impl( self, size: Optional[int] = None - ) -> List[_InterimRowType[Row[Any]]]: + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: raise NotImplementedError() - def _fetchall_impl(self) -> List[_InterimRowType[Row[Any]]]: + def _fetchall_impl( + self, + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: raise NotImplementedError() def _soft_close(self, hard: bool = False) -> None: @@ -444,10 +450,10 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): @HasMemoized_ro_memoized_attribute def _row_getter(self) -> Optional[Callable[..., _R]]: - real_result: Result[Any] = ( + real_result: Result[Unpack[TupleAny]] = ( self._real_result if self._real_result - else cast("Result[Any]", self) + else cast("Result[Unpack[TupleAny]]", self) ) if real_result._source_supports_scalars: @@ -461,7 +467,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): processors: Optional[_ProcessorsType], key_to_index: Mapping[_KeyType, int], scalar_obj: Any, - ) -> Row[Any]: + ) -> Row[Unpack[TupleAny]]: return _proc( metadata, processors, key_to_index, (scalar_obj,) ) @@ -485,7 +491,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): fixed_tf = tf - def make_row(row: _InterimRowType[Row[Any]]) -> _R: + def make_row(row: _InterimRowType[Row[Unpack[TupleAny]]]) -> _R: return _make_row_orig(fixed_tf(row)) else: @@ -497,7 +503,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): _log_row = real_result._row_logging_fn _make_row = make_row - def make_row(row: _InterimRowType[Row[Any]]) -> _R: + def make_row(row: _InterimRowType[Row[Unpack[TupleAny]]]) -> _R: return _log_row(_make_row(row)) # type: ignore return make_row @@ -511,7 +517,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): if self._unique_filter_state: uniques, strategy = self._unique_strategy - def iterrows(self: Result[Any]) -> Iterator[_R]: + def iterrows(self: Result[Unpack[TupleAny]]) -> Iterator[_R]: for raw_row in self._fetchiter_impl(): obj: _InterimRowType[Any] = ( make_row(raw_row) if make_row else raw_row @@ -526,7 +532,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): else: - def iterrows(self: Result[Any]) -> Iterator[_R]: + def iterrows(self: Result[Unpack[TupleAny]]) -> Iterator[_R]: for raw_row in self._fetchiter_impl(): row: _InterimRowType[Any] = ( make_row(raw_row) if make_row else raw_row @@ -591,7 +597,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): if self._unique_filter_state: uniques, strategy = self._unique_strategy - def onerow(self: Result[Any]) -> Union[_NoRow, _R]: + def onerow(self: Result[Unpack[TupleAny]]) -> Union[_NoRow, _R]: _onerow = self._fetchone_impl while True: row = _onerow() @@ -612,7 +618,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): else: - def onerow(self: Result[Any]) -> Union[_NoRow, _R]: + def onerow(self: Result[Unpack[TupleAny]]) -> Union[_NoRow, _R]: row = self._fetchone_impl() if row is None: return _NO_ROW @@ -672,7 +678,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): real_result = ( self._real_result if self._real_result - else cast("Result[Any]", self) + else cast("Result[Unpack[TupleAny]]", self) ) if real_result._yield_per: num_required = num = real_result._yield_per @@ -712,7 +718,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): real_result = ( self._real_result if self._real_result - else cast("Result[Any]", self) + else cast("Result[Unpack[TupleAny]]", self) ) num = real_result._yield_per @@ -862,7 +868,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]): real_result = ( self._real_result if self._real_result is not None - else cast("Result[Any]", self) + else cast("Result[Unpack[TupleAny]]", self) ) if not strategy and self._metadata._unique_filters: @@ -906,7 +912,7 @@ class _WithKeys: return self._metadata.keys -class Result(_WithKeys, ResultInternal[Row[_TP]]): +class Result(_WithKeys, ResultInternal[Row[Unpack[_Ts]]]): """Represent a set of database results. .. versionadded:: 1.4 The :class:`_engine.Result` object provides a @@ -934,7 +940,9 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): __slots__ = ("_metadata", "__dict__") - _row_logging_fn: Optional[Callable[[Row[Any]], Row[Any]]] = None + _row_logging_fn: Optional[ + Callable[[Row[Unpack[TupleAny]]], Row[Unpack[TupleAny]]] + ] = None _source_supports_scalars: bool = False @@ -1129,12 +1137,12 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): return self._column_slices(col_expressions) @overload - def scalars(self: Result[Tuple[_T]]) -> ScalarResult[_T]: + def scalars(self: Result[_T, Unpack[TupleAny]]) -> ScalarResult[_T]: ... @overload def scalars( - self: Result[Tuple[_T]], index: Literal[0] + self: Result[_T, Unpack[TupleAny]], index: Literal[0] ) -> ScalarResult[_T]: ... @@ -1169,7 +1177,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): def _getter( self, key: _KeyIndexType, raiseerr: bool = True - ) -> Optional[Callable[[Row[Any]], Any]]: + ) -> Optional[Callable[[Row[Unpack[TupleAny]]], Any]]: """return a callable that will retrieve the given key from a :class:`_engine.Row`. @@ -1209,7 +1217,12 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): return MappingResult(self) @property - def t(self) -> TupleResult[_TP]: + @deprecated( + "2.1.0", + "The :attr:`.Result.t` method is deprecated, :class:`.Row` " + "now behaves like a tuple and can unpack types directly.", + ) + def t(self) -> TupleResult[Tuple[Unpack[_Ts]]]: """Apply a "typed tuple" typing filter to returned rows. The :attr:`_engine.Result.t` attribute is a synonym for @@ -1217,10 +1230,20 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): .. versionadded:: 2.0 + .. seealso:: + + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + """ return self # type: ignore - def tuples(self) -> TupleResult[_TP]: + @deprecated( + "2.1.0", + "The :method:`.Result.tuples` method is deprecated, :class:`.Row` " + "now behaves like a tuple and can unpack types directly.", + ) + def tuples(self) -> TupleResult[Tuple[Unpack[_Ts]]]: """Apply a "typed tuple" typing filter to returned rows. This method returns the same :class:`_engine.Result` object @@ -1238,6 +1261,9 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): .. seealso:: + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + :attr:`_engine.Result.t` - shorter synonym :attr:`_engine.Row._t` - :class:`_engine.Row` version @@ -1255,15 +1281,15 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): """ raise NotImplementedError() - def __iter__(self) -> Iterator[Row[_TP]]: + def __iter__(self) -> Iterator[Row[Unpack[_Ts]]]: return self._iter_impl() - def __next__(self) -> Row[_TP]: + def __next__(self) -> Row[Unpack[_Ts]]: return self._next_impl() def partitions( self, size: Optional[int] = None - ) -> Iterator[Sequence[Row[_TP]]]: + ) -> Iterator[Sequence[Row[Unpack[_Ts]]]]: """Iterate through sub-lists of rows of the size given. Each list will be of the size given, excluding the last list to @@ -1319,12 +1345,12 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): else: break - def fetchall(self) -> Sequence[Row[_TP]]: + def fetchall(self) -> Sequence[Row[Unpack[_Ts]]]: """A synonym for the :meth:`_engine.Result.all` method.""" return self._allrows() - def fetchone(self) -> Optional[Row[_TP]]: + def fetchone(self) -> Optional[Row[Unpack[_Ts]]]: """Fetch one row. When all rows are exhausted, returns None. @@ -1346,7 +1372,9 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): else: return row - def fetchmany(self, size: Optional[int] = None) -> Sequence[Row[_TP]]: + def fetchmany( + self, size: Optional[int] = None + ) -> Sequence[Row[Unpack[_Ts]]]: """Fetch many rows. When all rows are exhausted, returns an empty sequence. @@ -1367,7 +1395,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): return self._manyrow_getter(self, size) - def all(self) -> Sequence[Row[_TP]]: + def all(self) -> Sequence[Row[Unpack[_Ts]]]: """Return all rows in a sequence. Closes the result set after invocation. Subsequent invocations @@ -1386,7 +1414,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): return self._allrows() - def first(self) -> Optional[Row[_TP]]: + def first(self) -> Optional[Row[Unpack[_Ts]]]: """Fetch the first row or ``None`` if no row is present. Closes the result set and discards remaining rows. @@ -1425,7 +1453,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): raise_for_second_row=False, raise_for_none=False, scalar=False ) - def one_or_none(self) -> Optional[Row[_TP]]: + def one_or_none(self) -> Optional[Row[Unpack[_Ts]]]: """Return at most one result or raise an exception. Returns ``None`` if the result has no rows. @@ -1451,7 +1479,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): ) @overload - def scalar_one(self: Result[Tuple[_T]]) -> _T: + def scalar_one(self: Result[_T]) -> _T: ... @overload @@ -1476,7 +1504,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): ) @overload - def scalar_one_or_none(self: Result[Tuple[_T]]) -> Optional[_T]: + def scalar_one_or_none(self: Result[_T]) -> Optional[_T]: ... @overload @@ -1500,7 +1528,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): raise_for_second_row=True, raise_for_none=False, scalar=True ) - def one(self) -> Row[_TP]: + def one(self) -> Row[Unpack[_Ts]]: """Return exactly one row or raise an exception. Raises :class:`.NoResultFound` if the result returns no @@ -1534,7 +1562,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): ) @overload - def scalar(self: Result[Tuple[_T]]) -> Optional[_T]: + def scalar(self: Result[_T]) -> Optional[_T]: ... @overload @@ -1559,7 +1587,7 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): raise_for_second_row=False, raise_for_none=False, scalar=True ) - def freeze(self) -> FrozenResult[_TP]: + def freeze(self) -> FrozenResult[Unpack[_Ts]]: """Return a callable object that will produce copies of this :class:`_engine.Result` when invoked. @@ -1582,7 +1610,9 @@ class Result(_WithKeys, ResultInternal[Row[_TP]]): return FrozenResult(self) - def merge(self, *others: Result[Any]) -> MergedResult[_TP]: + def merge( + self, *others: Result[Unpack[TupleAny]] + ) -> MergedResult[Unpack[TupleAny]]: """Merge this :class:`_engine.Result` with other compatible result objects. @@ -1619,7 +1649,7 @@ class FilterResult(ResultInternal[_R]): _post_creational_filter: Optional[Callable[[Any], Any]] - _real_result: Result[Any] + _real_result: Result[Unpack[TupleAny]] def __enter__(self) -> Self: return self @@ -1678,20 +1708,24 @@ class FilterResult(ResultInternal[_R]): def _attributes(self) -> Dict[Any, Any]: return self._real_result._attributes - def _fetchiter_impl(self) -> Iterator[_InterimRowType[Row[Any]]]: + def _fetchiter_impl( + self, + ) -> Iterator[_InterimRowType[Row[Unpack[TupleAny]]]]: return self._real_result._fetchiter_impl() def _fetchone_impl( self, hard_close: bool = False - ) -> Optional[_InterimRowType[Row[Any]]]: + ) -> Optional[_InterimRowType[Row[Unpack[TupleAny]]]]: return self._real_result._fetchone_impl(hard_close=hard_close) - def _fetchall_impl(self) -> List[_InterimRowType[Row[Any]]]: + def _fetchall_impl( + self, + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: return self._real_result._fetchall_impl() def _fetchmany_impl( self, size: Optional[int] = None - ) -> List[_InterimRowType[Row[Any]]]: + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: return self._real_result._fetchmany_impl(size=size) @@ -1717,7 +1751,9 @@ class ScalarResult(FilterResult[_R]): _post_creational_filter: Optional[Callable[[Any], Any]] - def __init__(self, real_result: Result[Any], index: _KeyIndexType): + def __init__( + self, real_result: Result[Unpack[TupleAny]], index: _KeyIndexType + ): self._real_result = real_result if real_result._source_supports_scalars: @@ -2010,7 +2046,7 @@ class MappingResult(_WithKeys, FilterResult[RowMapping]): _post_creational_filter = operator.attrgetter("_mapping") - def __init__(self, result: Result[Any]): + def __init__(self, result: Result[Unpack[TupleAny]]): self._real_result = result self._unique_filter_state = result._unique_filter_state self._metadata = result._metadata @@ -2137,7 +2173,7 @@ class MappingResult(_WithKeys, FilterResult[RowMapping]): ) -class FrozenResult(Generic[_TP]): +class FrozenResult(Generic[Unpack[_Ts]]): """Represents a :class:`_engine.Result` object in a "frozen" state suitable for caching. @@ -2178,7 +2214,7 @@ class FrozenResult(Generic[_TP]): data: Sequence[Any] - def __init__(self, result: Result[_TP]): + def __init__(self, result: Result[Unpack[_Ts]]): self.metadata = result._metadata._for_freeze() self._source_supports_scalars = result._source_supports_scalars self._attributes = result._attributes @@ -2195,21 +2231,21 @@ class FrozenResult(Generic[_TP]): return [list(row) for row in self.data] def with_new_rows( - self, tuple_data: Sequence[Row[_TP]] - ) -> FrozenResult[_TP]: + self, tuple_data: Sequence[Row[Unpack[_Ts]]] + ) -> FrozenResult[Unpack[_Ts]]: fr = FrozenResult.__new__(FrozenResult) fr.metadata = self.metadata fr._attributes = self._attributes fr._source_supports_scalars = self._source_supports_scalars if self._source_supports_scalars: - fr.data = [d[0] for d in tuple_data] + fr.data = [d[0] for d in tuple_data] # type: ignore[misc] else: fr.data = tuple_data return fr - def __call__(self) -> Result[_TP]: - result: IteratorResult[_TP] = IteratorResult( + def __call__(self) -> Result[Unpack[_Ts]]: + result: IteratorResult[Unpack[_Ts]] = IteratorResult( self.metadata, iter(self.data) ) result._attributes = self._attributes @@ -2217,7 +2253,7 @@ class FrozenResult(Generic[_TP]): return result -class IteratorResult(Result[_TP]): +class IteratorResult(Result[Unpack[_Ts]]): """A :class:`_engine.Result` that gets data from a Python iterator of :class:`_engine.Row` objects or similar row-like data. @@ -2272,7 +2308,7 @@ class IteratorResult(Result[_TP]): def _fetchone_impl( self, hard_close: bool = False - ) -> Optional[_InterimRowType[Row[Any]]]: + ) -> Optional[_InterimRowType[Row[Unpack[TupleAny]]]]: if self._hard_closed: self._raise_hard_closed() @@ -2283,7 +2319,9 @@ class IteratorResult(Result[_TP]): else: return row - def _fetchall_impl(self) -> List[_InterimRowType[Row[Any]]]: + def _fetchall_impl( + self, + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: if self._hard_closed: self._raise_hard_closed() try: @@ -2293,7 +2331,7 @@ class IteratorResult(Result[_TP]): def _fetchmany_impl( self, size: Optional[int] = None - ) -> List[_InterimRowType[Row[Any]]]: + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: if self._hard_closed: self._raise_hard_closed() @@ -2304,7 +2342,7 @@ def null_result() -> IteratorResult[Any]: return IteratorResult(SimpleResultMetaData([]), iter([])) -class ChunkedIteratorResult(IteratorResult[_TP]): +class ChunkedIteratorResult(IteratorResult[Unpack[_Ts]]): """An :class:`_engine.IteratorResult` that works from an iterator-producing callable. @@ -2355,13 +2393,13 @@ class ChunkedIteratorResult(IteratorResult[_TP]): def _fetchmany_impl( self, size: Optional[int] = None - ) -> List[_InterimRowType[Row[Any]]]: + ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]: if self.dynamic_yield_per: self.iterator = itertools.chain.from_iterable(self.chunks(size)) return super()._fetchmany_impl(size=size) -class MergedResult(IteratorResult[_TP]): +class MergedResult(IteratorResult[Unpack[_Ts]]): """A :class:`_engine.Result` that is merged from any number of :class:`_engine.Result` objects. @@ -2375,7 +2413,9 @@ class MergedResult(IteratorResult[_TP]): rowcount: Optional[int] def __init__( - self, cursor_metadata: ResultMetaData, results: Sequence[Result[_TP]] + self, + cursor_metadata: ResultMetaData, + results: Sequence[Result[Unpack[_Ts]]], ): self._results = results super().__init__( diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py index f620935228..5e6db0599e 100644 --- a/lib/sqlalchemy/engine/row.py +++ b/lib/sqlalchemy/engine/row.py @@ -22,16 +22,16 @@ from typing import List from typing import Mapping from typing import NoReturn from typing import Optional -from typing import overload from typing import Sequence from typing import Tuple from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union from ..sql import util as sql_util from ..util import deprecated from ..util._has_cy import HAS_CYEXTENSION +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack if TYPE_CHECKING or not HAS_CYEXTENSION: from ._py_row import BaseRow as BaseRow @@ -39,15 +39,20 @@ else: from sqlalchemy.cyextension.resultproxy import BaseRow as BaseRow if TYPE_CHECKING: + from typing import Tuple as _RowBase + from .result import _KeyType from .result import _ProcessorsType from .result import RMKeyView +else: + _RowBase = Sequence + _T = TypeVar("_T", bound=Any) -_TP = TypeVar("_TP", bound=Tuple[Any, ...]) +_Ts = TypeVarTuple("_Ts") -class Row(BaseRow, Sequence[Any], Generic[_TP]): +class Row(BaseRow, _RowBase[Unpack[_Ts]], Generic[Unpack[_Ts]]): """Represent a single result row. The :class:`.Row` object represents a row of a database result. It is @@ -83,7 +88,12 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): def __delattr__(self, name: str) -> NoReturn: raise AttributeError("can't delete attribute") - def _tuple(self) -> _TP: + @deprecated( + "2.1.0", + "The :meth:`.Row._tuple` method is deprecated, :class:`.Row` " + "now behaves like a tuple and can unpack types directly.", + ) + def _tuple(self) -> Tuple[Unpack[_Ts]]: """Return a 'tuple' form of this :class:`.Row`. At runtime, this method returns "self"; the :class:`.Row` object is @@ -99,13 +109,16 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): .. seealso:: + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + :attr:`.Row._t` - shorthand attribute notation :meth:`.Result.tuples` """ - return self # type: ignore + return self @deprecated( "2.0.19", @@ -114,16 +127,26 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): "methods and library-level attributes are intended to be underscored " "to avoid name conflicts. Please use :meth:`Row._tuple`.", ) - def tuple(self) -> _TP: + def tuple(self) -> Tuple[Unpack[_Ts]]: """Return a 'tuple' form of this :class:`.Row`. .. versionadded:: 2.0 + .. seealso:: + + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + """ return self._tuple() @property - def _t(self) -> _TP: + @deprecated( + "2.1.0", + "The :attr:`.Row._t` attribute is deprecated, :class:`.Row` " + "now behaves like a tuple and can unpack types directly.", + ) + def _t(self) -> Tuple[Unpack[_Ts]]: """A synonym for :meth:`.Row._tuple`. .. versionadded:: 2.0.19 - The :attr:`.Row._t` attribute supersedes @@ -133,9 +156,12 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): .. seealso:: + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + :attr:`.Result.t` """ - return self # type: ignore + return self @property @deprecated( @@ -145,11 +171,16 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): "methods and library-level attributes are intended to be underscored " "to avoid name conflicts. Please use :attr:`Row._t`.", ) - def t(self) -> _TP: + def t(self) -> Tuple[Unpack[_Ts]]: """A synonym for :meth:`.Row._tuple`. .. versionadded:: 2.0 + .. seealso:: + + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + """ return self._t @@ -172,7 +203,7 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): def _filter_on_values( self, processor: Optional[_ProcessorsType] - ) -> Row[Any]: + ) -> Row[Unpack[_Ts]]: return Row(self._parent, processor, self._key_to_index, self._data) if not TYPE_CHECKING: @@ -210,19 +241,6 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]): __hash__ = BaseRow.__hash__ - if TYPE_CHECKING: - - @overload - def __getitem__(self, index: int) -> Any: - ... - - @overload - def __getitem__(self, index: slice) -> Sequence[Any]: - ... - - def __getitem__(self, index: Union[int, slice]) -> Any: - ... - def __lt__(self, other: Any) -> bool: return self._op(other, operator.lt) diff --git a/lib/sqlalchemy/ext/asyncio/engine.py b/lib/sqlalchemy/ext/asyncio/engine.py index 02b70ecd58..817594e148 100644 --- a/lib/sqlalchemy/ext/asyncio/engine.py +++ b/lib/sqlalchemy/ext/asyncio/engine.py @@ -16,7 +16,6 @@ from typing import Generator from typing import NoReturn from typing import Optional from typing import overload -from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import TypeVar @@ -41,6 +40,9 @@ from ...engine.base import NestedTransaction from ...engine.base import Transaction from ...exc import ArgumentError from ...util.concurrency import greenlet_spawn +from ...util.typing import TupleAny +from ...util.typing import TypeVarTuple +from ...util.typing import Unpack if TYPE_CHECKING: from ...engine.cursor import CursorResult @@ -62,6 +64,7 @@ if TYPE_CHECKING: from ...sql.selectable import TypedReturnsRows _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") def create_async_engine(url: Union[str, URL], **kw: Any) -> AsyncEngine: @@ -514,11 +517,11 @@ class AsyncConnection( @overload def stream( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> GeneratorStartableContext[AsyncResult[_T]]: + ) -> GeneratorStartableContext[AsyncResult[Unpack[_Ts]]]: ... @overload @@ -528,7 +531,7 @@ class AsyncConnection( parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> GeneratorStartableContext[AsyncResult[Any]]: + ) -> GeneratorStartableContext[AsyncResult[Unpack[TupleAny]]]: ... @asyncstartablecontext @@ -538,7 +541,7 @@ class AsyncConnection( parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> AsyncIterator[AsyncResult[Any]]: + ) -> AsyncIterator[AsyncResult[Unpack[TupleAny]]]: """Execute a statement and return an awaitable yielding a :class:`_asyncio.AsyncResult` object. @@ -601,11 +604,11 @@ class AsyncConnection( @overload async def execute( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[_T]: + ) -> CursorResult[Unpack[_Ts]]: ... @overload @@ -615,7 +618,7 @@ class AsyncConnection( parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: ... async def execute( @@ -624,7 +627,7 @@ class AsyncConnection( parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: r"""Executes a SQL statement construct and return a buffered :class:`_engine.Result`. @@ -668,7 +671,7 @@ class AsyncConnection( @overload async def scalar( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], parameters: Optional[_CoreSingleExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, @@ -710,7 +713,7 @@ class AsyncConnection( @overload async def scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], parameters: Optional[_CoreAnyExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, @@ -753,7 +756,7 @@ class AsyncConnection( @overload def stream_scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], parameters: Optional[_CoreSingleExecuteParams] = None, *, execution_options: Optional[CoreExecuteOptionsParameter] = None, diff --git a/lib/sqlalchemy/ext/asyncio/result.py b/lib/sqlalchemy/ext/asyncio/result.py index 2f664bcd62..14c0840d95 100644 --- a/lib/sqlalchemy/ext/asyncio/result.py +++ b/lib/sqlalchemy/ext/asyncio/result.py @@ -28,9 +28,13 @@ from ...engine.result import ResultMetaData from ...engine.row import Row from ...engine.row import RowMapping from ...sql.base import _generative +from ...util import deprecated from ...util.concurrency import greenlet_spawn from ...util.typing import Literal from ...util.typing import Self +from ...util.typing import TupleAny +from ...util.typing import TypeVarTuple +from ...util.typing import Unpack if TYPE_CHECKING: from ...engine import CursorResult @@ -38,13 +42,13 @@ if TYPE_CHECKING: from ...engine.result import _UniqueFilterType _T = TypeVar("_T", bound=Any) -_TP = TypeVar("_TP", bound=Tuple[Any, ...]) +_Ts = TypeVarTuple("_Ts") class AsyncCommon(FilterResult[_R]): __slots__ = () - _real_result: Result[Any] + _real_result: Result[Unpack[TupleAny]] _metadata: ResultMetaData async def close(self) -> None: # type: ignore[override] @@ -63,7 +67,7 @@ class AsyncCommon(FilterResult[_R]): return self._real_result.closed -class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): +class AsyncResult(_WithKeys, AsyncCommon[Row[Unpack[_Ts]]]): """An asyncio wrapper around a :class:`_result.Result` object. The :class:`_asyncio.AsyncResult` only applies to statement executions that @@ -86,9 +90,9 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): __slots__ = () - _real_result: Result[_TP] + _real_result: Result[Unpack[_Ts]] - def __init__(self, real_result: Result[_TP]): + def __init__(self, real_result: Result[Unpack[_Ts]]): self._real_result = real_result self._metadata = real_result._metadata @@ -103,7 +107,12 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): ) @property - def t(self) -> AsyncTupleResult[_TP]: + @deprecated( + "2.1.0", + "The :attr:`.AsyncResult.t` attribute is deprecated, :class:`.Row` " + "now behaves like a tuple and can unpack types directly.", + ) + def t(self) -> AsyncTupleResult[Tuple[Unpack[_Ts]]]: """Apply a "typed tuple" typing filter to returned rows. The :attr:`_asyncio.AsyncResult.t` attribute is a synonym for @@ -111,10 +120,21 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): .. versionadded:: 2.0 + .. seealso:: + + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + """ return self # type: ignore - def tuples(self) -> AsyncTupleResult[_TP]: + @deprecated( + "2.1.0", + "The :method:`.AsyncResult.tuples` method is deprecated, " + ":class:`.Row` now behaves like a tuple and can unpack types " + "directly.", + ) + def tuples(self) -> AsyncTupleResult[Tuple[Unpack[_Ts]]]: """Apply a "typed tuple" typing filter to returned rows. This method returns the same :class:`_asyncio.AsyncResult` object @@ -132,6 +152,9 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): .. seealso:: + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + :attr:`_asyncio.AsyncResult.t` - shorter synonym :attr:`_engine.Row.t` - :class:`_engine.Row` version @@ -163,7 +186,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): async def partitions( self, size: Optional[int] = None - ) -> AsyncIterator[Sequence[Row[_TP]]]: + ) -> AsyncIterator[Sequence[Row[Unpack[_Ts]]]]: """Iterate through sub-lists of rows of the size given. An async iterator is returned:: @@ -188,7 +211,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): else: break - async def fetchall(self) -> Sequence[Row[_TP]]: + async def fetchall(self) -> Sequence[Row[Unpack[_Ts]]]: """A synonym for the :meth:`_asyncio.AsyncResult.all` method. .. versionadded:: 2.0 @@ -197,7 +220,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): return await greenlet_spawn(self._allrows) - async def fetchone(self) -> Optional[Row[_TP]]: + async def fetchone(self) -> Optional[Row[Unpack[_Ts]]]: """Fetch one row. When all rows are exhausted, returns None. @@ -221,7 +244,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): async def fetchmany( self, size: Optional[int] = None - ) -> Sequence[Row[_TP]]: + ) -> Sequence[Row[Unpack[_Ts]]]: """Fetch many rows. When all rows are exhausted, returns an empty list. @@ -242,7 +265,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): return await greenlet_spawn(self._manyrow_getter, self, size) - async def all(self) -> Sequence[Row[_TP]]: + async def all(self) -> Sequence[Row[Unpack[_Ts]]]: """Return all rows in a list. Closes the result set after invocation. Subsequent invocations @@ -254,17 +277,17 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): return await greenlet_spawn(self._allrows) - def __aiter__(self) -> AsyncResult[_TP]: + def __aiter__(self) -> AsyncResult[Unpack[_Ts]]: return self - async def __anext__(self) -> Row[_TP]: + async def __anext__(self) -> Row[Unpack[_Ts]]: row = await greenlet_spawn(self._onerow_getter, self) if row is _NO_ROW: raise StopAsyncIteration() else: return row - async def first(self) -> Optional[Row[_TP]]: + async def first(self) -> Optional[Row[Unpack[_Ts]]]: """Fetch the first row or ``None`` if no row is present. Closes the result set and discards remaining rows. @@ -300,7 +323,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): """ return await greenlet_spawn(self._only_one_row, False, False, False) - async def one_or_none(self) -> Optional[Row[_TP]]: + async def one_or_none(self) -> Optional[Row[Unpack[_Ts]]]: """Return at most one result or raise an exception. Returns ``None`` if the result has no rows. @@ -324,7 +347,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): return await greenlet_spawn(self._only_one_row, True, False, False) @overload - async def scalar_one(self: AsyncResult[Tuple[_T]]) -> _T: + async def scalar_one(self: AsyncResult[_T]) -> _T: ... @overload @@ -348,7 +371,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): @overload async def scalar_one_or_none( - self: AsyncResult[Tuple[_T]], + self: AsyncResult[_T], ) -> Optional[_T]: ... @@ -371,7 +394,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): """ return await greenlet_spawn(self._only_one_row, True, False, True) - async def one(self) -> Row[_TP]: + async def one(self) -> Row[Unpack[_Ts]]: """Return exactly one row or raise an exception. Raises :class:`.NoResultFound` if the result returns no @@ -403,7 +426,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): return await greenlet_spawn(self._only_one_row, True, True, False) @overload - async def scalar(self: AsyncResult[Tuple[_T]]) -> Optional[_T]: + async def scalar(self: AsyncResult[_T]) -> Optional[_T]: ... @overload @@ -426,7 +449,7 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): """ return await greenlet_spawn(self._only_one_row, False, False, True) - async def freeze(self) -> FrozenResult[_TP]: + async def freeze(self) -> FrozenResult[Unpack[_Ts]]: """Return a callable object that will produce copies of this :class:`_asyncio.AsyncResult` when invoked. @@ -451,12 +474,14 @@ class AsyncResult(_WithKeys, AsyncCommon[Row[_TP]]): @overload def scalars( - self: AsyncResult[Tuple[_T]], index: Literal[0] + self: AsyncResult[_T, Unpack[TupleAny]], index: Literal[0] ) -> AsyncScalarResult[_T]: ... @overload - def scalars(self: AsyncResult[Tuple[_T]]) -> AsyncScalarResult[_T]: + def scalars( + self: AsyncResult[_T, Unpack[TupleAny]], + ) -> AsyncScalarResult[_T]: ... @overload @@ -513,7 +538,11 @@ class AsyncScalarResult(AsyncCommon[_R]): _generate_rows = False - def __init__(self, real_result: Result[Any], index: _KeyIndexType): + def __init__( + self, + real_result: Result[Unpack[TupleAny]], + index: _KeyIndexType, + ): self._real_result = real_result if real_result._source_supports_scalars: @@ -644,7 +673,7 @@ class AsyncMappingResult(_WithKeys, AsyncCommon[RowMapping]): _post_creational_filter = operator.attrgetter("_mapping") - def __init__(self, result: Result[Any]): + def __init__(self, result: Result[Unpack[TupleAny]]): self._real_result = result self._unique_filter_state = result._unique_filter_state self._metadata = result._metadata @@ -944,7 +973,7 @@ class AsyncTupleResult(AsyncCommon[_R], util.TypingOnly): ... -_RT = TypeVar("_RT", bound="Result[Any]") +_RT = TypeVar("_RT", bound="Result[Unpack[TupleAny]]") async def _ensure_sync_result(result: _RT, calling_method: Any) -> _RT: diff --git a/lib/sqlalchemy/ext/asyncio/scoping.py b/lib/sqlalchemy/ext/asyncio/scoping.py index a5127b8661..850b4b750f 100644 --- a/lib/sqlalchemy/ext/asyncio/scoping.py +++ b/lib/sqlalchemy/ext/asyncio/scoping.py @@ -31,6 +31,9 @@ from ...util import create_proxy_methods from ...util import ScopedRegistry from ...util import warn from ...util import warn_deprecated +from ...util.typing import TupleAny +from ...util.typing import TypeVarTuple +from ...util.typing import Unpack if TYPE_CHECKING: from .engine import AsyncConnection @@ -61,6 +64,7 @@ if TYPE_CHECKING: from ...sql.selectable import TypedReturnsRows _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") @create_proxy_methods( @@ -529,14 +533,14 @@ class async_scoped_session(Generic[_AS]): @overload async def execute( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[_T]: + ) -> Result[Unpack[_Ts]]: ... @overload @@ -549,7 +553,7 @@ class async_scoped_session(Generic[_AS]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: ... @overload @@ -562,7 +566,7 @@ class async_scoped_session(Generic[_AS]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: ... async def execute( @@ -573,7 +577,7 @@ class async_scoped_session(Generic[_AS]): execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: r"""Execute a statement and return a buffered :class:`_engine.Result` object. @@ -1009,7 +1013,7 @@ class async_scoped_session(Generic[_AS]): @overload async def scalar( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -1064,7 +1068,7 @@ class async_scoped_session(Generic[_AS]): @overload async def scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -1207,13 +1211,13 @@ class async_scoped_session(Generic[_AS]): @overload async def stream( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> AsyncResult[_T]: + ) -> AsyncResult[Unpack[_Ts]]: ... @overload @@ -1225,7 +1229,7 @@ class async_scoped_session(Generic[_AS]): execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> AsyncResult[Any]: + ) -> AsyncResult[Unpack[TupleAny]]: ... async def stream( @@ -1236,7 +1240,7 @@ class async_scoped_session(Generic[_AS]): execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> AsyncResult[Any]: + ) -> AsyncResult[Unpack[TupleAny]]: r"""Execute a statement and return a streaming :class:`_asyncio.AsyncResult` object. @@ -1259,7 +1263,7 @@ class async_scoped_session(Generic[_AS]): @overload async def stream_scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -1593,7 +1597,7 @@ class async_scoped_session(Generic[_AS]): ident: Union[Any, Tuple[Any, ...]] = None, *, instance: Optional[Any] = None, - row: Optional[Union[Row[Any], RowMapping]] = None, + row: Optional[Union[Row[Unpack[TupleAny]], RowMapping]] = None, identity_token: Optional[Any] = None, ) -> _IdentityKeyType[Any]: r"""Return an identity key. diff --git a/lib/sqlalchemy/ext/asyncio/session.py b/lib/sqlalchemy/ext/asyncio/session.py index cdca94a9ab..f7a2469868 100644 --- a/lib/sqlalchemy/ext/asyncio/session.py +++ b/lib/sqlalchemy/ext/asyncio/session.py @@ -38,6 +38,10 @@ from ...orm import Session from ...orm import SessionTransaction from ...orm import state as _instance_state from ...util.concurrency import greenlet_spawn +from ...util.typing import TupleAny +from ...util.typing import TypeVarTuple +from ...util.typing import Unpack + if TYPE_CHECKING: from .engine import AsyncConnection @@ -72,7 +76,7 @@ if TYPE_CHECKING: _AsyncSessionBind = Union["AsyncEngine", "AsyncConnection"] _T = TypeVar("_T", bound=Any) - +_Ts = TypeVarTuple("_Ts") _EXECUTE_OPTIONS = util.immutabledict({"prebuffer_rows": True}) _STREAM_OPTIONS = util.immutabledict({"stream_results": True}) @@ -391,14 +395,14 @@ class AsyncSession(ReversibleProxy[Session]): @overload async def execute( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[_T]: + ) -> Result[Unpack[_Ts]]: ... @overload @@ -411,7 +415,7 @@ class AsyncSession(ReversibleProxy[Session]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: ... @overload @@ -424,7 +428,7 @@ class AsyncSession(ReversibleProxy[Session]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: ... async def execute( @@ -435,7 +439,7 @@ class AsyncSession(ReversibleProxy[Session]): execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: """Execute a statement and return a buffered :class:`_engine.Result` object. @@ -465,7 +469,7 @@ class AsyncSession(ReversibleProxy[Session]): @overload async def scalar( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -522,7 +526,7 @@ class AsyncSession(ReversibleProxy[Session]): @overload async def scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -649,13 +653,13 @@ class AsyncSession(ReversibleProxy[Session]): @overload async def stream( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> AsyncResult[_T]: + ) -> AsyncResult[Unpack[_Ts]]: ... @overload @@ -667,7 +671,7 @@ class AsyncSession(ReversibleProxy[Session]): execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> AsyncResult[Any]: + ) -> AsyncResult[Unpack[TupleAny]]: ... async def stream( @@ -678,7 +682,7 @@ class AsyncSession(ReversibleProxy[Session]): execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, **kw: Any, - ) -> AsyncResult[Any]: + ) -> AsyncResult[Unpack[TupleAny]]: """Execute a statement and return a streaming :class:`_asyncio.AsyncResult` object. @@ -704,7 +708,7 @@ class AsyncSession(ReversibleProxy[Session]): @overload async def stream_scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -1590,7 +1594,7 @@ class AsyncSession(ReversibleProxy[Session]): ident: Union[Any, Tuple[Any, ...]] = None, *, instance: Optional[Any] = None, - row: Optional[Union[Row[Any], RowMapping]] = None, + row: Optional[Union[Row[Unpack[TupleAny]], RowMapping]] = None, identity_token: Optional[Any] = None, ) -> _IdentityKeyType[Any]: r"""Return an identity key. diff --git a/lib/sqlalchemy/ext/horizontal_shard.py b/lib/sqlalchemy/ext/horizontal_shard.py index 24060a062e..ad8b3444ad 100644 --- a/lib/sqlalchemy/ext/horizontal_shard.py +++ b/lib/sqlalchemy/ext/horizontal_shard.py @@ -50,12 +50,15 @@ from ..orm.session import _BindArguments from ..orm.session import _PKIdentityArgument from ..orm.session import Session from ..util.typing import Self +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack + if TYPE_CHECKING: from ..engine.base import Connection from ..engine.base import Engine from ..engine.base import OptionEngine - from ..engine.result import IteratorResult from ..engine.result import Result from ..orm import LoaderCallableStatus from ..orm._typing import _O @@ -66,12 +69,12 @@ if TYPE_CHECKING: from ..orm.session import ORMExecuteState from ..orm.state import InstanceState from ..sql import Executable - from ..sql._typing import _TP from ..sql.elements import ClauseElement __all__ = ["ShardedSession", "ShardedQuery"] _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") ShardIdentifier = str @@ -427,7 +430,7 @@ class set_shard_id(ORMOption): def execute_and_instances( orm_context: ORMExecuteState, -) -> Union[Result[_T], IteratorResult[_TP]]: +) -> Result[Unpack[TupleAny]]: active_options: Union[ None, QueryContext.default_load_options, @@ -449,7 +452,7 @@ def execute_and_instances( def iter_for_shard( shard_id: ShardIdentifier, - ) -> Union[Result[_T], IteratorResult[_TP]]: + ) -> Result[Unpack[TupleAny]]: bind_arguments = dict(orm_context.bind_arguments) bind_arguments["shard_id"] = shard_id diff --git a/lib/sqlalchemy/orm/bulk_persistence.py b/lib/sqlalchemy/orm/bulk_persistence.py index 3f558d2d40..c2ef0980e6 100644 --- a/lib/sqlalchemy/orm/bulk_persistence.py +++ b/lib/sqlalchemy/orm/bulk_persistence.py @@ -53,6 +53,8 @@ from ..sql.dml import InsertDMLState from ..sql.dml import UpdateDMLState from ..util import EMPTY_DICT from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import Unpack if TYPE_CHECKING: from ._typing import DMLStrategyArgument @@ -249,7 +251,7 @@ def _bulk_update( update_changed_only: bool, use_orm_update_stmt: Optional[dml.Update] = ..., enable_check_rowcount: bool = True, -) -> _result.Result[Any]: +) -> _result.Result[Unpack[TupleAny]]: ... @@ -261,7 +263,7 @@ def _bulk_update( update_changed_only: bool, use_orm_update_stmt: Optional[dml.Update] = None, enable_check_rowcount: bool = True, -) -> Optional[_result.Result[Any]]: +) -> Optional[_result.Result[Unpack[TupleAny]]]: base_mapper = mapper.base_mapper search_keys = mapper._primary_key_propkeys @@ -1236,7 +1238,7 @@ class BulkORMInsert(ORMDMLState, InsertDMLState): "are 'raw', 'orm', 'bulk', 'auto" ) - result: _result.Result[Any] + result: _result.Result[Unpack[TupleAny]] if insert_options._dml_strategy == "raw": result = conn.execute( @@ -1572,7 +1574,7 @@ class BulkORMUpdate(BulkUDCompileState, UpdateDMLState): "are 'orm', 'auto', 'bulk', 'core_only'" ) - result: _result.Result[Any] + result: _result.Result[Unpack[TupleAny]] if update_options._dml_strategy == "bulk": enable_check_rowcount = not statement._where_criteria diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index b417825318..b51f2b9613 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -46,7 +46,6 @@ from ..sql import expression from ..sql import roles from ..sql import util as sql_util from ..sql import visitors -from ..sql._typing import _TP from ..sql._typing import is_dml from ..sql._typing import is_insert_update from ..sql._typing import is_select_base @@ -68,6 +67,10 @@ from ..sql.selectable import SelectLabelStyle from ..sql.selectable import SelectState from ..sql.selectable import TypedReturnsRows from ..sql.visitors import InternalTraversal +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack + if TYPE_CHECKING: from ._typing import _InternalEntityType @@ -91,6 +94,7 @@ if TYPE_CHECKING: from ..sql.type_api import TypeEngine _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") _path_registry = PathRegistry.root _EMPTY_DICT = util.immutabledict() @@ -147,7 +151,10 @@ class QueryContext: def __init__( self, compile_state: CompileState, - statement: Union[Select[Any], FromStatement[Any]], + statement: Union[ + Select[Unpack[TupleAny]], + FromStatement[Unpack[TupleAny]], + ], params: _CoreSingleExecuteParams, session: Session, load_options: Union[ @@ -401,8 +408,10 @@ class ORMCompileState(AbstractORMCompileState): attributes: Dict[Any, Any] global_attributes: Dict[Any, Any] - statement: Union[Select[Any], FromStatement[Any]] - select_statement: Union[Select[Any], FromStatement[Any]] + statement: Union[Select[Unpack[TupleAny]], FromStatement[Unpack[TupleAny]]] + select_statement: Union[ + Select[Unpack[TupleAny]], FromStatement[Unpack[TupleAny]] + ] _entities: List[_QueryEntity] _polymorphic_adapters: Dict[_InternalEntityType, ORMAdapter] compile_options: Union[ @@ -416,7 +425,7 @@ class ORMCompileState(AbstractORMCompileState): dedupe_columns: Set[ColumnElement[Any]] create_eager_joins: List[ # TODO: this structure is set up by JoinedLoader - Tuple[Any, ...] + TupleAny ] current_path: PathRegistry = _path_registry _has_mapper_entities = False @@ -856,7 +865,7 @@ class ORMFromStatementCompileState(ORMCompileState): entity.setup_dml_returning_compile_state(self, adapter) -class FromStatement(GroupedElement, Generative, TypedReturnsRows[_TP]): +class FromStatement(GroupedElement, Generative, TypedReturnsRows[Unpack[_Ts]]): """Core construct that represents a load of ORM objects from various :class:`.ReturnsRows` and other classes including: @@ -2433,7 +2442,7 @@ def _column_descriptions( def _legacy_filter_by_entity_zero( - query_or_augmented_select: Union[Query[Any], Select[Any]] + query_or_augmented_select: Union[Query[Any], Select[Unpack[TupleAny]]] ) -> Optional[_InternalEntityType[Any]]: self = query_or_augmented_select if self._setup_joins: @@ -2448,7 +2457,7 @@ def _legacy_filter_by_entity_zero( def _entity_from_pre_ent_zero( - query_or_augmented_select: Union[Query[Any], Select[Any]] + query_or_augmented_select: Union[Query[Any], Select[Unpack[TupleAny]]] ) -> Optional[_InternalEntityType[Any]]: self = query_or_augmented_select if not self._raw_columns: diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index a70f0b3ec3..4d5775ee2d 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -55,7 +55,10 @@ from ..sql import operators from ..sql.elements import BindParameter from ..util.typing import is_fwd_ref from ..util.typing import is_pep593 +from ..util.typing import TupleAny from ..util.typing import typing_get_args +from ..util.typing import Unpack + if typing.TYPE_CHECKING: from ._typing import _InstanceDict @@ -713,11 +716,11 @@ class CompositeProperty( def create_row_processor( self, - query: Select[Any], - procs: Sequence[Callable[[Row[Any]], Any]], + query: Select[Unpack[TupleAny]], + procs: Sequence[Callable[[Row[Unpack[TupleAny]]], Any]], labels: Sequence[str], - ) -> Callable[[Row[Any]], Any]: - def proc(row: Row[Any]) -> Any: + ) -> Callable[[Row[Unpack[TupleAny]]], Any]: + def proc(row: Row[Unpack[TupleAny]]) -> Any: return self.property.composite_class( *[proc(row) for proc in procs] ) diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 5cab1d348c..dd9e558cd3 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -72,6 +72,9 @@ from ..sql.schema import Column from ..sql.type_api import TypeEngine from ..util import warn_deprecated from ..util.typing import RODescriptorReference +from ..util.typing import TupleAny +from ..util.typing import Unpack + if typing.TYPE_CHECKING: from ._typing import _EntityType @@ -486,7 +489,7 @@ class MapperProperty( query_entity: _MapperEntity, path: AbstractEntityRegistry, mapper: Mapper[Any], - result: Result[Any], + result: Result[Unpack[TupleAny]], adapter: Optional[ORMAdapter], populators: _PopulatorDict, ) -> None: @@ -1056,7 +1059,7 @@ class StrategizedProperty(MapperProperty[_T]): query_entity: _MapperEntity, path: AbstractEntityRegistry, mapper: Mapper[Any], - result: Result[Any], + result: Result[Unpack[TupleAny]], adapter: Optional[ORMAdapter], populators: _PopulatorDict, ) -> None: @@ -1447,7 +1450,7 @@ class LoaderStrategy: path: AbstractEntityRegistry, loadopt: Optional[_LoadElement], mapper: Mapper[Any], - result: Result[Any], + result: Result[Unpack[TupleAny]], adapter: Optional[ORMAdapter], populators: _PopulatorDict, ) -> None: diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index 1de71f9c71..b430cbf424 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -53,6 +53,8 @@ from ..sql.selectable import ForUpdateArg from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..sql.selectable import SelectState from ..util import EMPTY_DICT +from ..util.typing import TupleAny +from ..util.typing import Unpack if TYPE_CHECKING: from ._typing import _IdentityKeyType @@ -75,7 +77,9 @@ _new_runid = util.counter() _PopulatorDict = Dict[str, List[Tuple[str, Any]]] -def instances(cursor: CursorResult[Any], context: QueryContext) -> Result[Any]: +def instances( + cursor: CursorResult[Unpack[TupleAny]], context: QueryContext +) -> Result[Unpack[TupleAny]]: """Return a :class:`.Result` given an ORM query context. :param cursor: a :class:`.CursorResult`, generated by a statement diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 7ad4fc6be1..e91b1a6bd0 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -89,6 +89,8 @@ from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..util import HasMemoized from ..util import HasMemoized_ro_memoized_attribute from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import Unpack if TYPE_CHECKING: from ._typing import _IdentityKeyType @@ -3428,7 +3430,7 @@ class Mapper( def identity_key_from_row( self, - row: Optional[Union[Row[Any], RowMapping]], + row: Optional[Union[Row[Unpack[TupleAny]], RowMapping]], identity_token: Optional[Any] = None, adapter: Optional[ORMAdapter] = None, ) -> _IdentityKeyType[_O]: diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 019a658f1e..4aaae3ee4f 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -74,7 +74,6 @@ from ..sql import Select from ..sql import util as sql_util from ..sql import visitors from ..sql._typing import _FromClauseArgument -from ..sql._typing import _TP from ..sql.annotation import SupportsCloneAnnotations from ..sql.base import _entity_namespace_key from ..sql.base import _generative @@ -91,8 +90,12 @@ from ..sql.selectable import HasPrefixes from ..sql.selectable import HasSuffixes from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..sql.selectable import SelectLabelStyle +from ..util import deprecated from ..util.typing import Literal from ..util.typing import Self +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack if TYPE_CHECKING: @@ -150,6 +153,7 @@ if TYPE_CHECKING: __all__ = ["Query", "QueryContext"] _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") @inspection._self_inspects @@ -295,6 +299,11 @@ class Query( for ent in util.to_list(entities) ] + @deprecated( + "2.1.0", + "The :method:`.Query.tuples` method is deprecated, :class:`.Row` " + "now behaves like a tuple and can unpack types directly.", + ) def tuples(self: Query[_O]) -> Query[Tuple[_O]]: """return a tuple-typed form of this :class:`.Query`. @@ -316,6 +325,9 @@ class Query( .. seealso:: + :ref:`change_10635` - describes a migration path from this + workaround for SQLAlchemy 2.1. + :meth:`.Result.tuples` - v2 equivalent method. """ @@ -533,7 +545,9 @@ class Query( return stmt - def _final_statement(self, legacy_query_style: bool = True) -> Select[Any]: + def _final_statement( + self, legacy_query_style: bool = True + ) -> Select[Unpack[TupleAny]]: """Return the 'final' SELECT statement for this :class:`.Query`. This is used by the testing suite only and is fairly inefficient. @@ -822,7 +836,7 @@ class Query( @overload def only_return_tuples( self: Query[_O], value: Literal[True] - ) -> RowReturningQuery[Tuple[_O]]: + ) -> RowReturningQuery[_O]: ... @overload @@ -1493,13 +1507,13 @@ class Query( @overload def with_entities( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], / - ) -> RowReturningQuery[Tuple[_T0, _T1]]: + ) -> RowReturningQuery[_T0, _T1]: ... @overload def with_entities( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], / - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2]]: + ) -> RowReturningQuery[_T0, _T1, _T2]: ... @overload @@ -1510,7 +1524,7 @@ class Query( __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3]: ... @overload @@ -1522,7 +1536,7 @@ class Query( __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -1535,7 +1549,7 @@ class Query( __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -1549,7 +1563,7 @@ class Query( __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -1564,7 +1578,10 @@ class Query( __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + *entities: _ColumnsClauseArgument[Any], + ) -> RowReturningQuery[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, Unpack[TupleAny] + ]: ... # END OVERLOADED FUNCTIONS self.with_entities @@ -3414,8 +3431,8 @@ class BulkDelete(BulkUD): """BulkUD which handles DELETEs.""" -class RowReturningQuery(Query[Row[_TP]]): +class RowReturningQuery(Query[Row[Unpack[_Ts]]]): if TYPE_CHECKING: - def tuples(self) -> Query[_TP]: # type: ignore + def tuples(self) -> Query[Tuple[Unpack[_Ts]]]: # type: ignore ... diff --git a/lib/sqlalchemy/orm/scoping.py b/lib/sqlalchemy/orm/scoping.py index ead18d1862..2e87f41879 100644 --- a/lib/sqlalchemy/orm/scoping.py +++ b/lib/sqlalchemy/orm/scoping.py @@ -32,6 +32,9 @@ from ..util import ScopedRegistry from ..util import ThreadLocalRegistry from ..util import warn from ..util import warn_deprecated +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack if TYPE_CHECKING: from ._typing import _EntityType @@ -75,7 +78,9 @@ if TYPE_CHECKING: from ..sql.selectable import ForUpdateParameter from ..sql.selectable import TypedReturnsRows + _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") class QueryPropertyDescriptor(Protocol): @@ -675,14 +680,14 @@ class scoped_session(Generic[_S]): @overload def execute( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[_T]: + ) -> Result[Unpack[_Ts]]: ... @overload @@ -695,7 +700,7 @@ class scoped_session(Generic[_S]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: ... @overload @@ -708,7 +713,7 @@ class scoped_session(Generic[_S]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: ... def execute( @@ -720,7 +725,7 @@ class scoped_session(Generic[_S]): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: r"""Execute a SQL expression construct. .. container:: class_bases @@ -1580,7 +1585,7 @@ class scoped_session(Generic[_S]): @overload def query( self, _colexpr: TypedColumnsClauseRole[_T] - ) -> RowReturningQuery[Tuple[_T]]: + ) -> RowReturningQuery[_T]: ... # START OVERLOADED FUNCTIONS self.query RowReturningQuery 2-8 @@ -1591,13 +1596,13 @@ class scoped_session(Generic[_S]): @overload def query( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], / - ) -> RowReturningQuery[Tuple[_T0, _T1]]: + ) -> RowReturningQuery[_T0, _T1]: ... @overload def query( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], / - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2]]: + ) -> RowReturningQuery[_T0, _T1, _T2]: ... @overload @@ -1608,7 +1613,7 @@ class scoped_session(Generic[_S]): __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3]: ... @overload @@ -1620,7 +1625,7 @@ class scoped_session(Generic[_S]): __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -1633,7 +1638,7 @@ class scoped_session(Generic[_S]): __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -1647,7 +1652,7 @@ class scoped_session(Generic[_S]): __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -1662,7 +1667,10 @@ class scoped_session(Generic[_S]): __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + *entities: _ColumnsClauseArgument[Any], + ) -> RowReturningQuery[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, Unpack[TupleAny] + ]: ... # END OVERLOADED FUNCTIONS self.query @@ -1817,7 +1825,7 @@ class scoped_session(Generic[_S]): @overload def scalar( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreSingleExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -1872,7 +1880,7 @@ class scoped_session(Generic[_S]): @overload def scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -2158,7 +2166,7 @@ class scoped_session(Generic[_S]): ident: Union[Any, Tuple[Any, ...]] = None, *, instance: Optional[Any] = None, - row: Optional[Union[Row[Any], RowMapping]] = None, + row: Optional[Union[Row[Unpack[TupleAny]], RowMapping]] = None, identity_token: Optional[Any] = None, ) -> _IdentityKeyType[Any]: r"""Return an identity key. diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 26c907446e..4315ac7f30 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -91,6 +91,10 @@ from ..sql.selectable import ForUpdateArg from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..util import IdentitySet from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack + if typing.TYPE_CHECKING: from ._typing import _EntityType @@ -134,6 +138,7 @@ if typing.TYPE_CHECKING: from ..sql.selectable import TypedReturnsRows _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") __all__ = [ "Session", @@ -222,7 +227,7 @@ class _SessionClassMethods: ident: Union[Any, Tuple[Any, ...]] = None, *, instance: Optional[Any] = None, - row: Optional[Union[Row[Any], RowMapping]] = None, + row: Optional[Union[Row[Unpack[TupleAny]], RowMapping]] = None, identity_token: Optional[Any] = None, ) -> _IdentityKeyType[Any]: """Return an identity key. @@ -385,7 +390,7 @@ class ORMExecuteState(util.MemoizedSlots): params: Optional[_CoreAnyExecuteParams] = None, execution_options: Optional[OrmExecuteOptionsParameter] = None, bind_arguments: Optional[_BindArguments] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: """Execute the statement represented by this :class:`.ORMExecuteState`, without re-invoking events that have already proceeded. @@ -2071,7 +2076,7 @@ class Session(_SessionClassMethods, EventTarget): _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, _scalar_result: bool = ..., - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: ... def _execute_internal( @@ -2147,7 +2152,9 @@ class Session(_SessionClassMethods, EventTarget): ) for idx, fn in enumerate(events_todo): orm_exec_state._starting_event_idx = idx - fn_result: Optional[Result[Any]] = fn(orm_exec_state) + fn_result: Optional[Result[Unpack[TupleAny]]] = fn( + orm_exec_state + ) if fn_result: if _scalar_result: return fn_result.scalar() @@ -2187,7 +2194,9 @@ class Session(_SessionClassMethods, EventTarget): ) if compile_state_cls: - result: Result[Any] = compile_state_cls.orm_execute_statement( + result: Result[ + Unpack[TupleAny] + ] = compile_state_cls.orm_execute_statement( self, statement, params or {}, @@ -2208,14 +2217,14 @@ class Session(_SessionClassMethods, EventTarget): @overload def execute( self, - statement: TypedReturnsRows[_T], + statement: TypedReturnsRows[Unpack[_Ts]], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[_T]: + ) -> Result[Unpack[_Ts]]: ... @overload @@ -2228,7 +2237,7 @@ class Session(_SessionClassMethods, EventTarget): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> CursorResult[Any]: + ) -> CursorResult[Unpack[TupleAny]]: ... @overload @@ -2241,7 +2250,7 @@ class Session(_SessionClassMethods, EventTarget): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: ... def execute( @@ -2253,7 +2262,7 @@ class Session(_SessionClassMethods, EventTarget): bind_arguments: Optional[_BindArguments] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: r"""Execute a SQL expression construct. Returns a :class:`_engine.Result` object representing @@ -2317,7 +2326,7 @@ class Session(_SessionClassMethods, EventTarget): @overload def scalar( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreSingleExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -2367,7 +2376,7 @@ class Session(_SessionClassMethods, EventTarget): @overload def scalars( self, - statement: TypedReturnsRows[Tuple[_T]], + statement: TypedReturnsRows[_T], params: Optional[_CoreAnyExecuteParams] = None, *, execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, @@ -2801,7 +2810,7 @@ class Session(_SessionClassMethods, EventTarget): @overload def query( self, _colexpr: TypedColumnsClauseRole[_T] - ) -> RowReturningQuery[Tuple[_T]]: + ) -> RowReturningQuery[_T]: ... # START OVERLOADED FUNCTIONS self.query RowReturningQuery 2-8 @@ -2812,13 +2821,13 @@ class Session(_SessionClassMethods, EventTarget): @overload def query( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], / - ) -> RowReturningQuery[Tuple[_T0, _T1]]: + ) -> RowReturningQuery[_T0, _T1]: ... @overload def query( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], / - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2]]: + ) -> RowReturningQuery[_T0, _T1, _T2]: ... @overload @@ -2829,7 +2838,7 @@ class Session(_SessionClassMethods, EventTarget): __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3]: ... @overload @@ -2841,7 +2850,7 @@ class Session(_SessionClassMethods, EventTarget): __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -2854,7 +2863,7 @@ class Session(_SessionClassMethods, EventTarget): __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -2868,7 +2877,7 @@ class Session(_SessionClassMethods, EventTarget): __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> RowReturningQuery[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -2883,7 +2892,10 @@ class Session(_SessionClassMethods, EventTarget): __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, - ) -> RowReturningQuery[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + *entities: _ColumnsClauseArgument[Any], + ) -> RowReturningQuery[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, Unpack[TupleAny] + ]: ... # END OVERLOADED FUNCTIONS self.query @@ -3124,7 +3136,7 @@ class Session(_SessionClassMethods, EventTarget): with_for_update = ForUpdateArg._from_argument(with_for_update) - stmt: Select[Any] = sql.select(object_mapper(instance)) + stmt: Select[Unpack[TupleAny]] = sql.select(object_mapper(instance)) if ( loading.load_on_ident( self, diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 786b55e149..234a028a15 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -46,6 +46,8 @@ from .. import exc as sa_exc from .. import inspection from .. import util from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import Unpack if TYPE_CHECKING: from ._typing import _IdentityKeyType @@ -93,7 +95,10 @@ class _InstallLoaderCallableProto(Protocol[_O]): """ def __call__( - self, state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any] + self, + state: InstanceState[_O], + dict_: _InstanceDict, + row: Row[Unpack[TupleAny]], ) -> None: ... @@ -673,7 +678,9 @@ class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]): fixed_impl = impl def _set_callable( - state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any] + state: InstanceState[_O], + dict_: _InstanceDict, + row: Row[Unpack[TupleAny]], ) -> None: if "callables" not in state.__dict__: state.callables = {} @@ -685,7 +692,9 @@ class InstanceState(interfaces.InspectionAttrInfo, Generic[_O]): else: def _set_callable( - state: InstanceState[_O], dict_: _InstanceDict, row: Row[Any] + state: InstanceState[_O], + dict_: _InstanceDict, + row: Row[Unpack[TupleAny]], ) -> None: if "callables" not in state.__dict__: state.callables = {} diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 89d5ad491c..4309cb119e 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -92,7 +92,9 @@ from ..util.typing import ( from ..util.typing import eval_name_only as _eval_name_only from ..util.typing import is_origin_of_cls from ..util.typing import Literal +from ..util.typing import TupleAny from ..util.typing import typing_get_origin +from ..util.typing import Unpack if typing.TYPE_CHECKING: from ._typing import _EntityType @@ -426,7 +428,7 @@ def identity_key( ident: Union[Any, Tuple[Any, ...]] = None, *, instance: Optional[_T] = None, - row: Optional[Union[Row[Any], RowMapping]] = None, + row: Optional[Union[Row[Unpack[TupleAny]], RowMapping]] = None, identity_token: Optional[Any] = None, ) -> _IdentityKeyType[_T]: r"""Generate "identity key" tuples, as are used as keys in the @@ -1721,10 +1723,10 @@ class Bundle( def create_row_processor( self, - query: Select[Any], - procs: Sequence[Callable[[Row[Any]], Any]], + query: Select[Unpack[TupleAny]], + procs: Sequence[Callable[[Row[Unpack[TupleAny]]], Any]], labels: Sequence[str], - ) -> Callable[[Row[Any]], Any]: + ) -> Callable[[Row[Unpack[TupleAny]]], Any]: """Produce the "row processing" function for this :class:`.Bundle`. May be overridden by subclasses to provide custom behaviors when @@ -1760,7 +1762,7 @@ class Bundle( """ keyed_tuple = result_tuple(labels, [() for l in labels]) - def proc(row: Row[Any]) -> Any: + def proc(row: Row[Unpack[TupleAny]]) -> Any: return keyed_tuple([proc(row) for proc in procs]) return proc diff --git a/lib/sqlalchemy/orm/writeonly.py b/lib/sqlalchemy/orm/writeonly.py index 2f54fc9a86..3764a6bb5c 100644 --- a/lib/sqlalchemy/orm/writeonly.py +++ b/lib/sqlalchemy/orm/writeonly.py @@ -587,7 +587,7 @@ class WriteOnlyCollection(AbstractCollectionWriter[_T]): "produce a SQL statement and execute it with session.scalars()." ) - def select(self) -> Select[Tuple[_T]]: + def select(self) -> Select[_T]: """Produce a :class:`_sql.Select` construct that represents the rows within this instance-local :class:`_orm.WriteOnlyCollection`. diff --git a/lib/sqlalchemy/sql/_selectable_constructors.py b/lib/sqlalchemy/sql/_selectable_constructors.py index 77e36d04bb..736b4961ec 100644 --- a/lib/sqlalchemy/sql/_selectable_constructors.py +++ b/lib/sqlalchemy/sql/_selectable_constructors.py @@ -10,7 +10,6 @@ from __future__ import annotations from typing import Any from typing import Optional from typing import overload -from typing import Tuple from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -32,6 +31,8 @@ from .selectable import Select from .selectable import TableClause from .selectable import TableSample from .selectable import Values +from ..util.typing import TupleAny +from ..util.typing import Unpack if TYPE_CHECKING: from ._typing import _FromClauseArgument @@ -331,21 +332,19 @@ def outerjoin( @overload -def select(__ent0: _TCCA[_T0], /) -> Select[Tuple[_T0]]: +def select(__ent0: _TCCA[_T0], /) -> Select[_T0]: ... @overload -def select( - __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], / -) -> Select[Tuple[_T0, _T1]]: +def select(__ent0: _TCCA[_T0], __ent1: _TCCA[_T1], /) -> Select[_T0, _T1]: ... @overload def select( __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], / -) -> Select[Tuple[_T0, _T1, _T2]]: +) -> Select[_T0, _T1, _T2]: ... @@ -356,7 +355,7 @@ def select( __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3]]: +) -> Select[_T0, _T1, _T2, _T3]: ... @@ -368,7 +367,7 @@ def select( __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4]]: +) -> Select[_T0, _T1, _T2, _T3, _T4]: ... @@ -381,7 +380,7 @@ def select( __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: +) -> Select[_T0, _T1, _T2, _T3, _T4, _T5]: ... @@ -395,7 +394,7 @@ def select( __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: +) -> Select[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @@ -410,7 +409,7 @@ def select( __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: +) -> Select[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]: ... @@ -426,7 +425,7 @@ def select( __ent7: _TCCA[_T7], __ent8: _TCCA[_T8], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8]]: +) -> Select[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8]: ... @@ -443,7 +442,10 @@ def select( __ent8: _TCCA[_T8], __ent9: _TCCA[_T9], /, -) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8, _T9]]: + *entities: _ColumnsClauseArgument[Any], +) -> Select[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, _T8, _T9, Unpack[TupleAny] +]: ... @@ -451,11 +453,15 @@ def select( @overload -def select(*entities: _ColumnsClauseArgument[Any], **__kw: Any) -> Select[Any]: +def select( + *entities: _ColumnsClauseArgument[Any], **__kw: Any +) -> Select[Unpack[TupleAny]]: ... -def select(*entities: _ColumnsClauseArgument[Any], **__kw: Any) -> Select[Any]: +def select( + *entities: _ColumnsClauseArgument[Any], **__kw: Any +) -> Select[Unpack[TupleAny]]: r"""Construct a new :class:`_expression.Select`. diff --git a/lib/sqlalchemy/sql/_typing.py b/lib/sqlalchemy/sql/_typing.py index 7c3e58b4bc..689ed19a9f 100644 --- a/lib/sqlalchemy/sql/_typing.py +++ b/lib/sqlalchemy/sql/_typing.py @@ -19,7 +19,6 @@ from typing import Optional from typing import overload from typing import Protocol from typing import Set -from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import TypeVar @@ -30,7 +29,9 @@ from .. import exc from .. import util from ..inspection import Inspectable from ..util.typing import Literal +from ..util.typing import TupleAny from ..util.typing import TypeAlias +from ..util.typing import Unpack if TYPE_CHECKING: from datetime import date @@ -157,8 +158,6 @@ _TypedColumnClauseArgument = Union[ Type[_T], ] -_TP = TypeVar("_TP", bound=Tuple[Any, ...]) - _T0 = TypeVar("_T0", bound=Any) _T1 = TypeVar("_T1", bound=Any) _T2 = TypeVar("_T2", bound=Any) @@ -329,7 +328,7 @@ if TYPE_CHECKING: def is_select_statement( t: Union[Executable, ReturnsRows] - ) -> TypeGuard[Select[Any]]: + ) -> TypeGuard[Select[Unpack[TupleAny]]]: ... def is_table(t: FromClause) -> TypeGuard[TableClause]: diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 2d6f330631..ea19e9a86d 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -88,6 +88,8 @@ from .. import exc from .. import util from ..util import FastIntFlag from ..util.typing import Literal +from ..util.typing import TupleAny +from ..util.typing import Unpack if typing.TYPE_CHECKING: from .annotation import _AnnotationDict @@ -405,7 +407,7 @@ class _CompilerStackEntry(_BaseCompilerStackEntry, total=False): need_result_map_for_nested: bool need_result_map_for_compound: bool select_0: ReturnsRows - insert_from_select: Select[Any] + insert_from_select: Select[Unpack[TupleAny]] class ExpandedState(NamedTuple): @@ -4786,7 +4788,7 @@ class SQLCompiler(Compiled): return text def _setup_select_hints( - self, select: Select[Any] + self, select: Select[Unpack[TupleAny]] ) -> Tuple[str, _FromHintsType]: byfrom = { from_: hinttext diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 0e3f872988..f35815ca4f 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -32,7 +32,6 @@ from typing import Union from . import coercions from . import roles from . import util as sql_util -from ._typing import _TP from ._typing import _unexpected_kw from ._typing import is_column_element from ._typing import is_named_from_clause @@ -66,7 +65,11 @@ from .visitors import InternalTraversal from .. import exc from .. import util from ..util.typing import Self +from ..util.typing import TupleAny from ..util.typing import TypeGuard +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack + if TYPE_CHECKING: from ._typing import _ColumnExpressionArgument @@ -107,6 +110,7 @@ else: _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") _DMLColumnElement = Union[str, ColumnClause[Any]] _DMLTableElement = Union[TableClause, Alias, Join] @@ -960,7 +964,7 @@ class ValuesBase(UpdateBase): _supports_multi_parameters = False - select: Optional[Select[Any]] = None + select: Optional[Select[Unpack[TupleAny]]] = None """SELECT statement for INSERT .. FROM SELECT""" _post_values_clause: Optional[ClauseElement] = None @@ -1299,7 +1303,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0]]: + ) -> ReturningInsert[_T0]: ... @overload @@ -1310,7 +1314,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1]]: + ) -> ReturningInsert[_T0, _T1]: ... @overload @@ -1322,7 +1326,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1, _T2]]: + ) -> ReturningInsert[_T0, _T1, _T2]: ... @overload @@ -1335,7 +1339,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1, _T2, _T3]]: + ) -> ReturningInsert[_T0, _T1, _T2, _T3]: ... @overload @@ -1349,7 +1353,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> ReturningInsert[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -1364,7 +1368,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> ReturningInsert[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -1380,7 +1384,7 @@ class Insert(ValuesBase): /, *, sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> ReturningInsert[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -1395,9 +1399,11 @@ class Insert(ValuesBase): __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, - *, + *entities: _ColumnsClauseArgument[Any], sort_by_parameter_order: bool = False, - ) -> ReturningInsert[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + ) -> ReturningInsert[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, Unpack[TupleAny] + ]: ... # END OVERLOADED FUNCTIONS self.returning @@ -1420,7 +1426,7 @@ class Insert(ValuesBase): ... -class ReturningInsert(Insert, TypedReturnsRows[_TP]): +class ReturningInsert(Insert, TypedReturnsRows[Unpack[_Ts]]): """Typing-only class that establishes a generic type form of :class:`.Insert` which tracks returned column types. @@ -1607,21 +1613,19 @@ class Update(DMLWhereBase, ValuesBase): # statically generated** by tools/generate_tuple_map_overloads.py @overload - def returning( - self, __ent0: _TCCA[_T0], / - ) -> ReturningUpdate[Tuple[_T0]]: + def returning(self, __ent0: _TCCA[_T0], /) -> ReturningUpdate[_T0]: ... @overload def returning( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], / - ) -> ReturningUpdate[Tuple[_T0, _T1]]: + ) -> ReturningUpdate[_T0, _T1]: ... @overload def returning( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], / - ) -> ReturningUpdate[Tuple[_T0, _T1, _T2]]: + ) -> ReturningUpdate[_T0, _T1, _T2]: ... @overload @@ -1632,7 +1636,7 @@ class Update(DMLWhereBase, ValuesBase): __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], /, - ) -> ReturningUpdate[Tuple[_T0, _T1, _T2, _T3]]: + ) -> ReturningUpdate[_T0, _T1, _T2, _T3]: ... @overload @@ -1644,7 +1648,7 @@ class Update(DMLWhereBase, ValuesBase): __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], /, - ) -> ReturningUpdate[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> ReturningUpdate[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -1657,7 +1661,7 @@ class Update(DMLWhereBase, ValuesBase): __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], /, - ) -> ReturningUpdate[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> ReturningUpdate[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -1671,7 +1675,7 @@ class Update(DMLWhereBase, ValuesBase): __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], /, - ) -> ReturningUpdate[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> ReturningUpdate[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -1686,7 +1690,10 @@ class Update(DMLWhereBase, ValuesBase): __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, - ) -> ReturningUpdate[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + *entities: _ColumnsClauseArgument[Any], + ) -> ReturningUpdate[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, Unpack[TupleAny] + ]: ... # END OVERLOADED FUNCTIONS self.returning @@ -1703,7 +1710,7 @@ class Update(DMLWhereBase, ValuesBase): ... -class ReturningUpdate(Update, TypedReturnsRows[_TP]): +class ReturningUpdate(Update, TypedReturnsRows[Unpack[_Ts]]): """Typing-only class that establishes a generic type form of :class:`.Update` which tracks returned column types. @@ -1752,21 +1759,19 @@ class Delete(DMLWhereBase, UpdateBase): # statically generated** by tools/generate_tuple_map_overloads.py @overload - def returning( - self, __ent0: _TCCA[_T0], / - ) -> ReturningDelete[Tuple[_T0]]: + def returning(self, __ent0: _TCCA[_T0], /) -> ReturningDelete[_T0]: ... @overload def returning( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], / - ) -> ReturningDelete[Tuple[_T0, _T1]]: + ) -> ReturningDelete[_T0, _T1]: ... @overload def returning( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], / - ) -> ReturningDelete[Tuple[_T0, _T1, _T2]]: + ) -> ReturningDelete[_T0, _T1, _T2]: ... @overload @@ -1777,7 +1782,7 @@ class Delete(DMLWhereBase, UpdateBase): __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], /, - ) -> ReturningDelete[Tuple[_T0, _T1, _T2, _T3]]: + ) -> ReturningDelete[_T0, _T1, _T2, _T3]: ... @overload @@ -1789,7 +1794,7 @@ class Delete(DMLWhereBase, UpdateBase): __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], /, - ) -> ReturningDelete[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> ReturningDelete[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -1802,7 +1807,7 @@ class Delete(DMLWhereBase, UpdateBase): __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], /, - ) -> ReturningDelete[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> ReturningDelete[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -1816,7 +1821,7 @@ class Delete(DMLWhereBase, UpdateBase): __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], /, - ) -> ReturningDelete[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> ReturningDelete[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -1831,7 +1836,10 @@ class Delete(DMLWhereBase, UpdateBase): __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], /, - ) -> ReturningDelete[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + *entities: _ColumnsClauseArgument[Any], + ) -> ReturningDelete[ + _T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7, Unpack[TupleAny] + ]: ... # END OVERLOADED FUNCTIONS self.returning @@ -1839,16 +1847,16 @@ class Delete(DMLWhereBase, UpdateBase): @overload def returning( self, *cols: _ColumnsClauseArgument[Any], **__kw: Any - ) -> ReturningDelete[Any]: + ) -> ReturningDelete[Unpack[TupleAny]]: ... def returning( self, *cols: _ColumnsClauseArgument[Any], **__kw: Any - ) -> ReturningDelete[Any]: + ) -> ReturningDelete[Unpack[TupleAny]]: ... -class ReturningDelete(Update, TypedReturnsRows[_TP]): +class ReturningDelete(Update, TypedReturnsRows[Unpack[_Ts]]): """Typing-only class that establishes a generic type form of :class:`.Delete` which tracks returned column types. diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 8f48e78ed0..973b332d47 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -78,6 +78,8 @@ from ..util import HasMemoized_ro_memoized_attribute from ..util import TypingOnly from ..util.typing import Literal from ..util.typing import Self +from ..util.typing import TupleAny +from ..util.typing import Unpack if typing.TYPE_CHECKING: from ._typing import _ByArgument @@ -510,7 +512,7 @@ class ClauseElement( connection: Connection, distilled_params: _CoreMultiExecuteParams, execution_options: CoreExecuteOptionsParameter, - ) -> Result[Any]: + ) -> Result[Unpack[TupleAny]]: if self.supports_execution: if TYPE_CHECKING: assert isinstance(self, Executable) @@ -2144,12 +2146,10 @@ class BindParameter(roles.InElementRole, KeyedColumnElement[_T]): else: check_value = value cast( - "BindParameter[typing_Tuple[Any, ...]]", self + "BindParameter[TupleAny]", self ).type = type_._resolve_values_to_types(check_value) else: - cast( - "BindParameter[typing_Tuple[Any, ...]]", self - ).type = type_ + cast("BindParameter[TupleAny]", self).type = type_ else: self.type = type_ @@ -3277,7 +3277,7 @@ and_ = BooleanClauseList.and_ or_ = BooleanClauseList.or_ -class Tuple(ClauseList, ColumnElement[typing_Tuple[Any, ...]]): +class Tuple(ClauseList, ColumnElement[TupleAny]): """Represent a SQL tuple.""" __visit_name__ = "tuple" diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 5cb5812d69..fd38c78d28 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -702,7 +702,7 @@ class FunctionElement(Executable, ColumnElement[_T], FromClause, Generative): joins_implicitly=joins_implicitly, ) - def select(self) -> Select[Tuple[_T]]: + def select(self) -> Select[_T]: """Produce a :func:`_expression.select` construct against this :class:`.FunctionElement`. @@ -711,7 +711,7 @@ class FunctionElement(Executable, ColumnElement[_T], FromClause, Generative): s = select(function_element) """ - s: Select[Any] = Select(self) + s: Select[_T] = Select(self) if self._execution_options: s = s.execution_options(**self._execution_options) return s diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 7e3c7150cf..ae52e5db45 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -48,7 +48,6 @@ from . import type_api from . import visitors from ._typing import _ColumnsClauseArgument from ._typing import _no_kw -from ._typing import _TP from ._typing import is_column_element from ._typing import is_select_statement from ._typing import is_subquery @@ -100,10 +99,16 @@ from .. import util from ..util import HasMemoized_ro_memoized_attribute from ..util.typing import Literal from ..util.typing import Self +from ..util.typing import TupleAny +from ..util.typing import TypeVarTuple +from ..util.typing import Unpack + and_ = BooleanClauseList.and_ _T = TypeVar("_T", bound=Any) +_Ts = TypeVarTuple("_Ts") + if TYPE_CHECKING: from ._typing import _ColumnExpressionArgument @@ -283,7 +288,7 @@ class ExecutableReturnsRows(Executable, ReturnsRows): """base for executable statements that return rows.""" -class TypedReturnsRows(ExecutableReturnsRows, Generic[_TP]): +class TypedReturnsRows(ExecutableReturnsRows, Generic[Unpack[_Ts]]): """base for executable statements that return rows.""" @@ -610,7 +615,7 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): _use_schema_map = False - def select(self) -> Select[Any]: + def select(self) -> Select[Unpack[TupleAny]]: r"""Return a SELECT of this :class:`_expression.FromClause`. @@ -1496,7 +1501,7 @@ class Join(roles.DMLTableRole, FromClause): "join explicitly." % (a.description, b.description) ) - def select(self) -> Select[Any]: + def select(self) -> Select[Unpack[TupleAny]]: r"""Create a :class:`_expression.Select` from this :class:`_expression.Join`. @@ -2052,7 +2057,7 @@ class CTE( def _init( self, - selectable: Select[Any], + selectable: Select[Unpack[TupleAny]], *, name: Optional[str] = None, recursive: bool = False, @@ -3477,7 +3482,7 @@ class SelectBase( "first in order to create " "a subquery, which then can be selected.", ) - def select(self, *arg: Any, **kw: Any) -> Select[Any]: + def select(self, *arg: Any, **kw: Any) -> Select[Unpack[TupleAny]]: return self._implicit_subquery.select(*arg, **kw) @HasMemoized.memoized_attribute @@ -4492,7 +4497,7 @@ class SelectState(util.MemoizedSlots, CompileState): def __init__( self, - statement: Select[Any], + statement: Select[Unpack[TupleAny]], compiler: Optional[SQLCompiler], **kw: Any, ): @@ -4520,7 +4525,7 @@ class SelectState(util.MemoizedSlots, CompileState): @classmethod def get_column_descriptions( - cls, statement: Select[Any] + cls, statement: Select[Unpack[TupleAny]] ) -> List[Dict[str, Any]]: return [ { @@ -4535,13 +4540,15 @@ class SelectState(util.MemoizedSlots, CompileState): @classmethod def from_statement( - cls, statement: Select[Any], from_statement: roles.ReturnsRowsRole + cls, + statement: Select[Unpack[TupleAny]], + from_statement: roles.ReturnsRowsRole, ) -> ExecutableReturnsRows: cls._plugin_not_implemented() @classmethod def get_columns_clause_froms( - cls, statement: Select[Any] + cls, statement: Select[Unpack[TupleAny]] ) -> List[FromClause]: return cls._normalize_froms( itertools.chain.from_iterable( @@ -4596,7 +4603,9 @@ class SelectState(util.MemoizedSlots, CompileState): return go - def _get_froms(self, statement: Select[Any]) -> List[FromClause]: + def _get_froms( + self, statement: Select[Unpack[TupleAny]] + ) -> List[FromClause]: ambiguous_table_name_map: _AmbiguousTableNameMap self._ambiguous_table_name_map = ambiguous_table_name_map = {} @@ -4624,7 +4633,7 @@ class SelectState(util.MemoizedSlots, CompileState): def _normalize_froms( cls, iterable_of_froms: Iterable[FromClause], - check_statement: Optional[Select[Any]] = None, + check_statement: Optional[Select[Unpack[TupleAny]]] = None, ambiguous_table_name_map: Optional[_AmbiguousTableNameMap] = None, ) -> List[FromClause]: """given an iterable of things to select FROM, reduce them to what @@ -4769,7 +4778,7 @@ class SelectState(util.MemoizedSlots, CompileState): @classmethod def determine_last_joined_entity( - cls, stmt: Select[Any] + cls, stmt: Select[Unpack[TupleAny]] ) -> Optional[_JoinTargetElement]: if stmt._setup_joins: return stmt._setup_joins[-1][0] @@ -4777,7 +4786,9 @@ class SelectState(util.MemoizedSlots, CompileState): return None @classmethod - def all_selected_columns(cls, statement: Select[Any]) -> _SelectIterable: + def all_selected_columns( + cls, statement: Select[Unpack[TupleAny]] + ) -> _SelectIterable: return [c for c in _select_iterables(statement._raw_columns)] def _setup_joins( @@ -5023,7 +5034,9 @@ class _MemoizedSelectEntities( return c @classmethod - def _generate_for_statement(cls, select_stmt: Select[Any]) -> None: + def _generate_for_statement( + cls, select_stmt: Select[Unpack[TupleAny]] + ) -> None: if select_stmt._setup_joins or select_stmt._with_options: self = _MemoizedSelectEntities() self._raw_columns = select_stmt._raw_columns @@ -5042,7 +5055,7 @@ class Select( HasCompileState, _SelectFromElements, GenerativeSelect, - TypedReturnsRows[_TP], + TypedReturnsRows[Unpack[_Ts]], ): """Represents a ``SELECT`` statement. @@ -5116,7 +5129,7 @@ class Select( _compile_state_factory: Type[SelectState] @classmethod - def _create_raw_select(cls, **kw: Any) -> Select[Any]: + def _create_raw_select(cls, **kw: Any) -> Select[Unpack[TupleAny]]: """Create a :class:`.Select` using raw ``__new__`` with no coercions. Used internally to build up :class:`.Select` constructs with @@ -5178,13 +5191,13 @@ class Select( @overload def scalar_subquery( - self: Select[Tuple[_MAYBE_ENTITY]], + self: Select[_MAYBE_ENTITY], ) -> ScalarSelect[Any]: ... @overload def scalar_subquery( - self: Select[Tuple[_NOT_ENTITY]], + self: Select[_NOT_ENTITY], ) -> ScalarSelect[_NOT_ENTITY]: ... @@ -5666,7 +5679,7 @@ class Select( @_generative def add_columns( self, *entities: _ColumnsClauseArgument[Any] - ) -> Select[Any]: + ) -> Select[Unpack[TupleAny]]: r"""Return a new :func:`_expression.select` construct with the given entities appended to its columns clause. @@ -5716,7 +5729,9 @@ class Select( "be removed in a future release. Please use " ":meth:`_expression.Select.add_columns`", ) - def column(self, column: _ColumnsClauseArgument[Any]) -> Select[Any]: + def column( + self, column: _ColumnsClauseArgument[Any] + ) -> Select[Unpack[TupleAny]]: """Return a new :func:`_expression.select` construct with the given column expression added to its columns clause. @@ -5733,7 +5748,9 @@ class Select( return self.add_columns(column) @util.preload_module("sqlalchemy.sql.util") - def reduce_columns(self, only_synonyms: bool = True) -> Select[Any]: + def reduce_columns( + self, only_synonyms: bool = True + ) -> Select[Unpack[TupleAny]]: """Return a new :func:`_expression.select` construct with redundantly named, equivalently-valued columns removed from the columns clause. @@ -5756,7 +5773,7 @@ class Select( all columns that are equivalent to another are removed. """ - woc: Select[Any] + woc: Select[Unpack[TupleAny]] woc = self.with_only_columns( *util.preloaded.sql_util.reduce_columns( self._all_selected_columns, @@ -5772,19 +5789,19 @@ class Select( # statically generated** by tools/generate_sel_v1_overloads.py @overload - def with_only_columns(self, __ent0: _TCCA[_T0]) -> Select[Tuple[_T0]]: + def with_only_columns(self, __ent0: _TCCA[_T0]) -> Select[_T0]: ... @overload def with_only_columns( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1] - ) -> Select[Tuple[_T0, _T1]]: + ) -> Select[_T0, _T1]: ... @overload def with_only_columns( self, __ent0: _TCCA[_T0], __ent1: _TCCA[_T1], __ent2: _TCCA[_T2] - ) -> Select[Tuple[_T0, _T1, _T2]]: + ) -> Select[_T0, _T1, _T2]: ... @overload @@ -5794,7 +5811,7 @@ class Select( __ent1: _TCCA[_T1], __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], - ) -> Select[Tuple[_T0, _T1, _T2, _T3]]: + ) -> Select[_T0, _T1, _T2, _T3]: ... @overload @@ -5805,7 +5822,7 @@ class Select( __ent2: _TCCA[_T2], __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], - ) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4]]: + ) -> Select[_T0, _T1, _T2, _T3, _T4]: ... @overload @@ -5817,7 +5834,7 @@ class Select( __ent3: _TCCA[_T3], __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], - ) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5]]: + ) -> Select[_T0, _T1, _T2, _T3, _T4, _T5]: ... @overload @@ -5830,7 +5847,7 @@ class Select( __ent4: _TCCA[_T4], __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], - ) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6]]: + ) -> Select[_T0, _T1, _T2, _T3, _T4, _T5, _T6]: ... @overload @@ -5844,7 +5861,7 @@ class Select( __ent5: _TCCA[_T5], __ent6: _TCCA[_T6], __ent7: _TCCA[_T7], - ) -> Select[Tuple[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]]: + ) -> Select[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _T7]: ... # END OVERLOADED FUNCTIONS self.with_only_columns @@ -5855,7 +5872,7 @@ class Select( *entities: _ColumnsClauseArgument[Any], maintain_column_froms: bool = False, **__kw: Any, - ) -> Select[Any]: + ) -> Select[Unpack[TupleAny]]: ... @_generative @@ -5864,7 +5881,7 @@ class Select( *entities: _ColumnsClauseArgument[Any], maintain_column_froms: bool = False, **__kw: Any, - ) -> Select[Any]: + ) -> Select[Unpack[TupleAny]]: r"""Return a new :func:`_expression.select` construct with its columns clause replaced with the given entities. @@ -6257,7 +6274,7 @@ class Select( meth = SelectState.get_plugin_class(self).all_selected_columns return list(meth(self)) - def _ensure_disambiguated_names(self) -> Select[Any]: + def _ensure_disambiguated_names(self) -> Select[Unpack[TupleAny]]: if self._label_style is LABEL_STYLE_NONE: self = self.set_label_style(LABEL_STYLE_DISAMBIGUATE_ONLY) return self @@ -6517,7 +6534,9 @@ class ScalarSelect( by this :class:`_expression.ScalarSelect`. """ - self.element = cast("Select[Any]", self.element).where(crit) + self.element = cast("Select[Unpack[TupleAny]]", self.element).where( + crit + ) return self @overload @@ -6539,7 +6558,7 @@ class ScalarSelect( if TYPE_CHECKING: - def _ungroup(self) -> Select[Any]: + def _ungroup(self) -> Select[Unpack[TupleAny]]: ... @_generative @@ -6573,9 +6592,9 @@ class ScalarSelect( """ - self.element = cast("Select[Any]", self.element).correlate( - *fromclauses - ) + self.element = cast( + "Select[Unpack[TupleAny]]", self.element + ).correlate(*fromclauses) return self @_generative @@ -6611,9 +6630,9 @@ class ScalarSelect( """ - self.element = cast("Select[Any]", self.element).correlate_except( - *fromclauses - ) + self.element = cast( + "Select[Unpack[TupleAny]]", self.element + ).correlate_except(*fromclauses) return self @@ -6628,7 +6647,10 @@ class Exists(UnaryExpression[bool]): """ inherit_cache = True - element: Union[SelectStatementGrouping[Select[Any]], ScalarSelect[Any]] + element: Union[ + SelectStatementGrouping[Select[Unpack[TupleAny]]], + ScalarSelect[Any], + ] def __init__( self, @@ -6663,8 +6685,9 @@ class Exists(UnaryExpression[bool]): return [] def _regroup( - self, fn: Callable[[Select[Any]], Select[Any]] - ) -> SelectStatementGrouping[Select[Any]]: + self, + fn: Callable[[Select[Unpack[TupleAny]]], Select[Unpack[TupleAny]]], + ) -> SelectStatementGrouping[Select[Unpack[TupleAny]]]: element = self.element._ungroup() new_element = fn(element) @@ -6672,7 +6695,7 @@ class Exists(UnaryExpression[bool]): assert isinstance(return_value, SelectStatementGrouping) return return_value - def select(self) -> Select[Any]: + def select(self) -> Select[Unpack[TupleAny]]: r"""Return a SELECT of this :class:`_expression.Exists`. e.g.:: diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 0963e8ed20..a9e0084995 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -61,6 +61,7 @@ from ..util import langhelpers from ..util import OrderedDict from ..util.typing import is_literal from ..util.typing import Literal +from ..util.typing import TupleAny from ..util.typing import typing_get_args if TYPE_CHECKING: @@ -3156,7 +3157,7 @@ class ARRAY( ) -class TupleType(TypeEngine[Tuple[Any, ...]]): +class TupleType(TypeEngine[TupleAny]): """represent the composite type of a Tuple.""" _is_tuple_type = True diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index ac8b30eb31..53e5726722 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -70,6 +70,7 @@ from .visitors import _ET from .. import exc from .. import util from ..util.typing import Literal +from ..util.typing import Unpack if typing.TYPE_CHECKING: from ._typing import _EquivalentColumnMap @@ -588,7 +589,9 @@ class _repr_row(_repr_base): __slots__ = ("row",) - def __init__(self, row: Row[Any], max_chars: int = 300): + def __init__( + self, row: Row[Unpack[Tuple[Any, ...]]], max_chars: int = 300 + ): self.row = row self.max_chars = max_chars diff --git a/lib/sqlalchemy/util/typing.py b/lib/sqlalchemy/util/typing.py index ce3aa9fe32..a3e9397640 100644 --- a/lib/sqlalchemy/util/typing.py +++ b/lib/sqlalchemy/util/typing.py @@ -49,8 +49,11 @@ if True: # zimports removes the tailing comments from typing_extensions import ParamSpec as ParamSpec # 3.10 from typing_extensions import TypeAlias as TypeAlias # 3.10 from typing_extensions import TypeGuard as TypeGuard # 3.10 + from typing_extensions import TypeVarTuple as TypeVarTuple # 3.11 from typing_extensions import Self as Self # 3.11 from typing_extensions import TypeAliasType as TypeAliasType # 3.12 + from typing_extensions import Unpack as Unpack # 3.11 + _T = TypeVar("_T", bound=Any) _KT = TypeVar("_KT") @@ -59,6 +62,8 @@ _KT_contra = TypeVar("_KT_contra", contravariant=True) _VT = TypeVar("_VT") _VT_co = TypeVar("_VT_co", covariant=True) +TupleAny = Tuple[Any, ...] + if compat.py310: # why they took until py310 to put this in stdlib is beyond me, diff --git a/test/base/test_result.py b/test/base/test_result.py index 78117e3228..3bbd1b8788 100644 --- a/test/base/test_result.py +++ b/test/base/test_result.py @@ -7,6 +7,7 @@ from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures from sqlalchemy.testing import is_false from sqlalchemy.testing import is_true +from sqlalchemy.testing.assertions import expect_deprecated from sqlalchemy.testing.assertions import expect_raises from sqlalchemy.testing.util import picklers from sqlalchemy.util import compat @@ -331,6 +332,7 @@ class ResultTest(fixtures.TestBase): eq_(m1.fetchone(), {"a": 1, "b": 1, "c": 1}) eq_(r1.fetchone(), (2, 1, 2)) + @expect_deprecated(".*is deprecated, Row now behaves like a tuple.*") def test_tuples_plus_base(self): r1 = self._fixture() diff --git a/test/orm/test_query.py b/test/orm/test_query.py index a06406c115..1e2b368107 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -75,6 +75,7 @@ from sqlalchemy.testing.assertions import assert_raises from sqlalchemy.testing.assertions import assert_raises_message from sqlalchemy.testing.assertions import assert_warns_message from sqlalchemy.testing.assertions import eq_ +from sqlalchemy.testing.assertions import expect_deprecated from sqlalchemy.testing.assertions import expect_raises from sqlalchemy.testing.assertions import expect_warnings from sqlalchemy.testing.assertions import is_not_none @@ -188,6 +189,7 @@ class OnlyReturnTuplesTest(QueryTest): assert isinstance(row, collections_abc.Sequence) assert isinstance(row._mapping, collections_abc.Mapping) + @expect_deprecated(".*is deprecated, Row now behaves like a tuple.*") def test_single_entity_tuples(self): User = self.classes.User query = fixture_session().query(User).tuples() @@ -214,6 +216,7 @@ class OnlyReturnTuplesTest(QueryTest): assert isinstance(row, collections_abc.Sequence) assert isinstance(row._mapping, collections_abc.Mapping) + @expect_deprecated(".*is deprecated, Row now behaves like a tuple.*") def test_multiple_entity_true_tuples(self): User = self.classes.User query = fixture_session().query(User.id, User).tuples() diff --git a/test/sql/test_resultset.py b/test/sql/test_resultset.py index 2caef7f8e5..1848f7bdd3 100644 --- a/test/sql/test_resultset.py +++ b/test/sql/test_resultset.py @@ -50,6 +50,7 @@ from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import assertions from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ +from sqlalchemy.testing import expect_deprecated from sqlalchemy.testing import expect_raises from sqlalchemy.testing import expect_raises_message from sqlalchemy.testing import fixtures @@ -269,6 +270,7 @@ class CursorResultTest(fixtures.TablesTest): r = connection.scalars(users.select().order_by(users.c.user_id)) eq_(r.all(), [7, 8, 9]) + @expect_deprecated(".*is deprecated, Row now behaves like a tuple.*") def test_result_tuples(self, connection): users = self.tables.users @@ -285,6 +287,7 @@ class CursorResultTest(fixtures.TablesTest): ).tuples() eq_(r.all(), [(7, "jack"), (8, "ed"), (9, "fred")]) + @expect_deprecated(".*is deprecated, Row now behaves like a tuple.*") def test_row_tuple(self, connection): users = self.tables.users diff --git a/test/typing/plain_files/engine/engines.py b/test/typing/plain_files/engine/engines.py index 5777b91484..7d56c51a5b 100644 --- a/test/typing/plain_files/engine/engines.py +++ b/test/typing/plain_files/engine/engines.py @@ -15,7 +15,7 @@ def regular() -> None: result = conn.execute(text("select * from table")) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(result) with e.begin() as conn: @@ -24,7 +24,7 @@ def regular() -> None: result = conn.execute(text("select * from table")) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(result) engine = create_engine("postgresql://scott:tiger@localhost/test") diff --git a/test/typing/plain_files/ext/asyncio/engines.py b/test/typing/plain_files/ext/asyncio/engines.py index 598d319a77..ae7880f584 100644 --- a/test/typing/plain_files/ext/asyncio/engines.py +++ b/test/typing/plain_files/ext/asyncio/engines.py @@ -14,20 +14,20 @@ async def asyncio() -> None: result = await conn.execute(text("select * from table")) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(result) # stream with direct await async_result = await conn.stream(text("select * from table")) - # EXPECTED_TYPE: AsyncResult[Any] + # EXPECTED_TYPE: AsyncResult[Unpack[.*tuple[Any, ...]]] reveal_type(async_result) # stream with context manager async with conn.stream( text("select * from table") ) as ctx_async_result: - # EXPECTED_TYPE: AsyncResult[Any] + # EXPECTED_TYPE: AsyncResult[Unpack[.*tuple[Any, ...]]] reveal_type(ctx_async_result) # stream_scalars with direct await @@ -51,5 +51,5 @@ async def asyncio() -> None: result = await conn.execute(text("select * from table")) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(result) diff --git a/test/typing/plain_files/ext/hybrid/hybrid_one.py b/test/typing/plain_files/ext/hybrid/hybrid_one.py index 52a2a19ed0..aef41395fe 100644 --- a/test/typing/plain_files/ext/hybrid/hybrid_one.py +++ b/test/typing/plain_files/ext/hybrid/hybrid_one.py @@ -87,5 +87,5 @@ if typing.TYPE_CHECKING: # EXPECTED_TYPE: SQLCoreOperations[bool] reveal_type(expr4) - # EXPECTED_TYPE: Select[Tuple[bool]] + # EXPECTED_TYPE: Select[bool] reveal_type(stmt2) diff --git a/test/typing/plain_files/orm/composite.py b/test/typing/plain_files/orm/composite.py index 8ac1f504c2..f82bbe7c2d 100644 --- a/test/typing/plain_files/orm/composite.py +++ b/test/typing/plain_files/orm/composite.py @@ -58,7 +58,7 @@ v1 = Vertex(start=Point(3, 4), end=Point(5, 6)) stmt = select(Vertex).where(Vertex.start.in_([Point(3, 4)])) -# EXPECTED_TYPE: Select[Tuple[Vertex]] +# EXPECTED_TYPE: Select[Vertex] reveal_type(stmt) # EXPECTED_TYPE: composite.Point diff --git a/test/typing/plain_files/orm/composite_dc.py b/test/typing/plain_files/orm/composite_dc.py index fa1b16a2a6..3d8117a999 100644 --- a/test/typing/plain_files/orm/composite_dc.py +++ b/test/typing/plain_files/orm/composite_dc.py @@ -38,7 +38,7 @@ v1 = Vertex(start=Point(3, 4), end=Point(5, 6)) stmt = select(Vertex).where(Vertex.start.in_([Point(3, 4)])) -# EXPECTED_TYPE: Select[Tuple[Vertex]] +# EXPECTED_TYPE: Select[Vertex] reveal_type(stmt) # EXPECTED_TYPE: composite.Point diff --git a/test/typing/plain_files/orm/declared_attr_one.py b/test/typing/plain_files/orm/declared_attr_one.py index fc304db87e..79f1548e36 100644 --- a/test/typing/plain_files/orm/declared_attr_one.py +++ b/test/typing/plain_files/orm/declared_attr_one.py @@ -74,7 +74,7 @@ class Manager(Employee): def do_something_with_mapped_class( cls_: MappedClassProtocol[Employee], ) -> None: - # EXPECTED_TYPE: Select[Any] + # EXPECTED_TYPE: Select[Unpack[.*tuple[Any, ...]]] reveal_type(cls_.__table__.select()) # EXPECTED_TYPE: Mapper[Employee] diff --git a/test/typing/plain_files/orm/issue_9340.py b/test/typing/plain_files/orm/issue_9340.py index 72dc72df1e..20bc424ce2 100644 --- a/test/typing/plain_files/orm/issue_9340.py +++ b/test/typing/plain_files/orm/issue_9340.py @@ -40,7 +40,7 @@ def get_messages() -> Sequence[Message]: message_query = select(Message) if TYPE_CHECKING: - # EXPECTED_TYPE: Select[Tuple[Message]] + # EXPECTED_TYPE: Select[Message] reveal_type(message_query) return session.scalars(message_query).all() @@ -57,7 +57,7 @@ def get_poly_messages() -> Sequence[Message]: poly_query = select(PolymorphicMessage) if TYPE_CHECKING: - # EXPECTED_TYPE: Select[Tuple[Message]] + # EXPECTED_TYPE: Select[Message] reveal_type(poly_query) return session.scalars(poly_query).all() diff --git a/test/typing/plain_files/orm/session.py b/test/typing/plain_files/orm/session.py index 0f1c35eafa..12a261a84f 100644 --- a/test/typing/plain_files/orm/session.py +++ b/test/typing/plain_files/orm/session.py @@ -61,7 +61,7 @@ with Session(e) as sess: q2 = sess.query(User.id).filter_by(id=7) rows2 = q2.all() - # EXPECTED_TYPE: List[Row[Tuple[int]]] + # EXPECTED_TYPE: List[.*Row[.*int].*] reveal_type(rows2) # test #8280 @@ -86,7 +86,7 @@ with Session(e) as sess: # test #9125 for row in sess.query(User.id, User.name): - # EXPECTED_TYPE: Row[Tuple[int, str]] + # EXPECTED_TYPE: .*Row[int, str].* reveal_type(row) for uobj1 in sess.query(User): diff --git a/test/typing/plain_files/orm/typed_queries.py b/test/typing/plain_files/orm/typed_queries.py index 7d8a2dd1a3..47168f474b 100644 --- a/test/typing/plain_files/orm/typed_queries.py +++ b/test/typing/plain_files/orm/typed_queries.py @@ -53,12 +53,12 @@ connection = e.connect() def t_select_1() -> None: stmt = select(User.id, User.name).filter(User.id == 5) - # EXPECTED_TYPE: Select[Tuple[int, str]] + # EXPECTED_TYPE: Select[int, str] reveal_type(stmt) result = session.execute(stmt) - # EXPECTED_TYPE: Result[Tuple[int, str]] + # EXPECTED_TYPE: .*Result[int, str].* reveal_type(result) @@ -77,12 +77,12 @@ def t_select_2() -> None: .fetch(User.id) ) - # EXPECTED_TYPE: Select[Tuple[User]] + # EXPECTED_TYPE: Select[User] reveal_type(stmt) result = session.execute(stmt) - # EXPECTED_TYPE: Result[Tuple[User]] + # EXPECTED_TYPE: .*Result[User].* reveal_type(result) @@ -102,12 +102,12 @@ def t_select_3() -> None: stmt = select(ua.id, ua.name).filter(User.id == 5) - # EXPECTED_TYPE: Select[Tuple[int, str]] + # EXPECTED_TYPE: Select[int, str] reveal_type(stmt) result = session.execute(stmt) - # EXPECTED_TYPE: Result[Tuple[int, str]] + # EXPECTED_TYPE: .*Result[int, str].* reveal_type(result) @@ -115,12 +115,12 @@ def t_select_4() -> None: ua = aliased(User) stmt = select(ua, User).filter(User.id == 5) - # EXPECTED_TYPE: Select[Tuple[User, User]] + # EXPECTED_TYPE: Select[User, User] reveal_type(stmt) result = session.execute(stmt) - # EXPECTED_TYPE: Result[Tuple[User, User]] + # EXPECTED_TYPE: Result[User, User] reveal_type(result) @@ -137,7 +137,7 @@ def t_legacy_query_single_entity() -> None: reveal_type(q1.all()) # mypy switches to builtins.list for some reason here - # EXPECTED_RE_TYPE: .*\.[Ll]ist\[.*Row\*?\[Tuple\[.*User\]\]\] + # EXPECTED_RE_TYPE: .*\.[Ll]ist\[.*Row\*?\[.*User\].*\] reveal_type(q1.only_return_tuples(True).all()) # EXPECTED_TYPE: List[Tuple[User]] @@ -147,15 +147,15 @@ def t_legacy_query_single_entity() -> None: def t_legacy_query_cols_1() -> None: q1 = session.query(User.id, User.name).filter(User.id == 5) - # EXPECTED_TYPE: RowReturningQuery[Tuple[int, str]] + # EXPECTED_TYPE: RowReturningQuery[int, str] reveal_type(q1) - # EXPECTED_TYPE: Row[Tuple[int, str]] + # EXPECTED_TYPE: .*Row[int, str].* reveal_type(q1.one()) r1 = q1.one() - x, y = r1.t + x, y = r1 # EXPECTED_TYPE: int reveal_type(x) @@ -167,7 +167,7 @@ def t_legacy_query_cols_1() -> None: def t_legacy_query_cols_tupleq_1() -> None: q1 = session.query(User.id, User.name).filter(User.id == 5) - # EXPECTED_TYPE: RowReturningQuery[Tuple[int, str]] + # EXPECTED_TYPE: RowReturningQuery[int, str] reveal_type(q1) q2 = q1.tuples() @@ -194,15 +194,15 @@ def t_legacy_query_cols_1_with_entities() -> None: q2 = q1.with_entities(User.id, User.name) - # EXPECTED_TYPE: RowReturningQuery[Tuple[int, str]] + # EXPECTED_TYPE: RowReturningQuery[int, str] reveal_type(q2) - # EXPECTED_TYPE: Row[Tuple[int, str]] + # EXPECTED_TYPE: .*Row[int, str].* reveal_type(q2.one()) r1 = q2.one() - x, y = r1.t + x, y = r1 # EXPECTED_TYPE: int reveal_type(x) @@ -214,20 +214,20 @@ def t_legacy_query_cols_1_with_entities() -> None: def t_select_with_only_cols() -> None: q1 = select(User).where(User.id == 5) - # EXPECTED_TYPE: Select[Tuple[User]] + # EXPECTED_TYPE: Select[User] reveal_type(q1) q2 = q1.with_only_columns(User.id, User.name) - # EXPECTED_TYPE: Select[Tuple[int, str]] + # EXPECTED_TYPE: Select[int, str] reveal_type(q2) row = connection.execute(q2).one() - # EXPECTED_TYPE: Row[Tuple[int, str]] + # EXPECTED_TYPE: .*Row[int, str].* reveal_type(row) - x, y = row.t + x, y = row # EXPECTED_TYPE: int reveal_type(x) @@ -240,15 +240,15 @@ def t_legacy_query_cols_2() -> None: a1 = aliased(User) q1 = session.query(User, a1, User.name).filter(User.id == 5) - # EXPECTED_TYPE: RowReturningQuery[Tuple[User, User, str]] + # EXPECTED_TYPE: RowReturningQuery[User, User, str] reveal_type(q1) - # EXPECTED_TYPE: Row[Tuple[User, User, str]] + # EXPECTED_TYPE: .*Row[User, User, str].* reveal_type(q1.one()) r1 = q1.one() - x, y, z = r1.t + x, y, z = r1 # EXPECTED_TYPE: User reveal_type(x) @@ -269,15 +269,15 @@ def t_legacy_query_cols_2_with_entities() -> None: a1 = aliased(User) q2 = q1.with_entities(User, a1, User.name).filter(User.id == 5) - # EXPECTED_TYPE: RowReturningQuery[Tuple[User, User, str]] + # EXPECTED_TYPE: RowReturningQuery[User, User, str] reveal_type(q2) - # EXPECTED_TYPE: Row[Tuple[User, User, str]] + # EXPECTED_TYPE: .*Row[User, User, str].* reveal_type(q2.one()) r1 = q2.one() - x, y, z = r1.t + x, y, z = r1 # EXPECTED_TYPE: User reveal_type(x) @@ -295,7 +295,7 @@ def t_select_add_col_loses_type() -> None: q2 = q1.add_columns(User.data) # note this should not match Select - # EXPECTED_TYPE: Select[Any] + # EXPECTED_TYPE: Select[Unpack[.*tuple[Any, ...]]] reveal_type(q2) @@ -388,7 +388,7 @@ def t_select_w_core_selectables() -> None: # mypy would downgrade to Any rather than picking the basemost type. # with typing integrated into Select etc. we can at least get a Select # object back. - # EXPECTED_TYPE: Select[Any] + # EXPECTED_TYPE: Select[Unpack[.*tuple[Any, ...]]] reveal_type(s2) # so a fully explicit type may be given @@ -400,7 +400,7 @@ def t_select_w_core_selectables() -> None: # plain FromClause etc we at least get Select s3 = select(s1) - # EXPECTED_TYPE: Select[Any] + # EXPECTED_TYPE: Select[Unpack[.*tuple[Any, ...]]] reveal_type(s3) t1 = User.__table__ @@ -411,7 +411,7 @@ def t_select_w_core_selectables() -> None: s4 = select(t1) - # EXPECTED_TYPE: Select[Any] + # EXPECTED_TYPE: Select[Unpack[.*tuple[Any, ...]]] reveal_type(s4) @@ -420,31 +420,31 @@ def t_dml_insert() -> None: r1 = session.execute(s1) - # EXPECTED_TYPE: Result[Tuple[int, str]] + # EXPECTED_TYPE: Result[int, str] reveal_type(r1) s2 = insert(User).returning(User) r2 = session.execute(s2) - # EXPECTED_TYPE: Result[Tuple[User]] + # EXPECTED_TYPE: Result[User] reveal_type(r2) s3 = insert(User).returning(func.foo(), column("q")) - # EXPECTED_TYPE: ReturningInsert[Any] + # EXPECTED_TYPE: ReturningInsert[Unpack[.*tuple[Any, ...]]] reveal_type(s3) r3 = session.execute(s3) - # EXPECTED_TYPE: Result[Any] + # EXPECTED_TYPE: Result[Unpack[.*tuple[Any, ...]]] reveal_type(r3) def t_dml_bare_insert() -> None: s1 = insert(User) r1 = session.execute(s1) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(r1) # EXPECTED_TYPE: int reveal_type(r1.rowcount) @@ -453,7 +453,7 @@ def t_dml_bare_insert() -> None: def t_dml_bare_update() -> None: s1 = update(User) r1 = session.execute(s1) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(r1) # EXPECTED_TYPE: int reveal_type(r1.rowcount) @@ -462,7 +462,7 @@ def t_dml_bare_update() -> None: def t_dml_update_with_values() -> None: s1 = update(User).values({User.id: 123, User.data: "value"}) r1 = session.execute(s1) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(r1) # EXPECTED_TYPE: int reveal_type(r1.rowcount) @@ -471,7 +471,7 @@ def t_dml_update_with_values() -> None: def t_dml_bare_delete() -> None: s1 = delete(User) r1 = session.execute(s1) - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(r1) # EXPECTED_TYPE: int reveal_type(r1.rowcount) @@ -482,7 +482,7 @@ def t_dml_update() -> None: r1 = session.execute(s1) - # EXPECTED_TYPE: Result[Tuple[int, str]] + # EXPECTED_TYPE: Result[int, str] reveal_type(r1) @@ -491,7 +491,7 @@ def t_dml_delete() -> None: r1 = session.execute(s1) - # EXPECTED_TYPE: Result[Tuple[int, str]] + # EXPECTED_TYPE: Result[int, str] reveal_type(r1) diff --git a/test/typing/plain_files/sql/common_sql_element.py b/test/typing/plain_files/sql/common_sql_element.py index 57aae8fac8..730d99bc15 100644 --- a/test/typing/plain_files/sql/common_sql_element.py +++ b/test/typing/plain_files/sql/common_sql_element.py @@ -66,7 +66,7 @@ reveal_type(e1) stmt = select(e1) -# EXPECTED_TYPE: Select[Tuple[bool]] +# EXPECTED_TYPE: Select[bool] reveal_type(stmt) stmt = stmt.where(e1) @@ -79,7 +79,7 @@ reveal_type(e2) stmt = select(e2) -# EXPECTED_TYPE: Select[Tuple[bool]] +# EXPECTED_TYPE: Select[bool] reveal_type(stmt) stmt = stmt.where(e2) @@ -89,14 +89,14 @@ stmt2 = select(User.id).order_by("id", "email").group_by("email", "id") stmt2 = ( select(User.id).order_by(asc("id"), desc("email")).group_by("email", "id") ) -# EXPECTED_TYPE: Select[Tuple[int]] +# EXPECTED_TYPE: Select[int] reveal_type(stmt2) stmt2 = select(User.id).order_by(User.id).group_by(User.email) stmt2 = ( select(User.id).order_by(User.id, User.email).group_by(User.email, User.id) ) -# EXPECTED_TYPE: Select[Tuple[int]] +# EXPECTED_TYPE: Select[int] reveal_type(stmt2) @@ -118,7 +118,7 @@ receives_bool_col_expr(user_table.c.email == "x") q1 = Session().query(User.id).order_by("email").group_by("email") q1 = Session().query(User.id).order_by("id", "email").group_by("email", "id") -# EXPECTED_TYPE: RowReturningQuery[Tuple[int]] +# EXPECTED_TYPE: RowReturningQuery[int] reveal_type(q1) q1 = Session().query(User.id).order_by(User.id).group_by(User.email) @@ -128,7 +128,7 @@ q1 = ( .order_by(User.id, User.email) .group_by(User.email, User.id) ) -# EXPECTED_TYPE: RowReturningQuery[Tuple[int]] +# EXPECTED_TYPE: RowReturningQuery[int] reveal_type(q1) # test 9174 diff --git a/test/typing/plain_files/sql/functions.py b/test/typing/plain_files/sql/functions.py index 6a345fcf6e..726c24b3f1 100644 --- a/test/typing/plain_files/sql/functions.py +++ b/test/typing/plain_files/sql/functions.py @@ -14,139 +14,139 @@ from sqlalchemy import String stmt1 = select(func.aggregate_strings(column("x", String), ",")) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*str\]\] +# EXPECTED_RE_TYPE: .*Select\[.*str\] reveal_type(stmt1) stmt2 = select(func.char_length(column("x"))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt2) stmt3 = select(func.coalesce(column("x", Integer))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt3) stmt4 = select(func.concat()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*str\]\] +# EXPECTED_RE_TYPE: .*Select\[.*str\] reveal_type(stmt4) stmt5 = select(func.count(column("x"))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt5) stmt6 = select(func.cume_dist()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*Decimal\]\] +# EXPECTED_RE_TYPE: .*Select\[.*Decimal\] reveal_type(stmt6) stmt7 = select(func.current_date()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*date\]\] +# EXPECTED_RE_TYPE: .*Select\[.*date\] reveal_type(stmt7) stmt8 = select(func.current_time()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*time\]\] +# EXPECTED_RE_TYPE: .*Select\[.*time\] reveal_type(stmt8) stmt9 = select(func.current_timestamp()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*datetime\]\] +# EXPECTED_RE_TYPE: .*Select\[.*datetime\] reveal_type(stmt9) stmt10 = select(func.current_user()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*str\]\] +# EXPECTED_RE_TYPE: .*Select\[.*str\] reveal_type(stmt10) stmt11 = select(func.dense_rank()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt11) stmt12 = select(func.localtime()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*datetime\]\] +# EXPECTED_RE_TYPE: .*Select\[.*datetime\] reveal_type(stmt12) stmt13 = select(func.localtimestamp()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*datetime\]\] +# EXPECTED_RE_TYPE: .*Select\[.*datetime\] reveal_type(stmt13) stmt14 = select(func.max(column("x", Integer))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt14) stmt15 = select(func.min(column("x", Integer))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt15) stmt16 = select(func.next_value(Sequence("x_seq"))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt16) stmt17 = select(func.now()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*datetime\]\] +# EXPECTED_RE_TYPE: .*Select\[.*datetime\] reveal_type(stmt17) stmt18 = select(func.percent_rank()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*Decimal\]\] +# EXPECTED_RE_TYPE: .*Select\[.*Decimal\] reveal_type(stmt18) stmt19 = select(func.rank()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt19) stmt20 = select(func.session_user()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*str\]\] +# EXPECTED_RE_TYPE: .*Select\[.*str\] reveal_type(stmt20) stmt21 = select(func.sum(column("x", Integer))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt21) stmt22 = select(func.sysdate()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*datetime\]\] +# EXPECTED_RE_TYPE: .*Select\[.*datetime\] reveal_type(stmt22) stmt23 = select(func.user()) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*str\]\] +# EXPECTED_RE_TYPE: .*Select\[.*str\] reveal_type(stmt23) # END GENERATED FUNCTION TYPING TESTS diff --git a/test/typing/plain_files/sql/functions_again.py b/test/typing/plain_files/sql/functions_again.py index da656f2d1d..b5a12f24d6 100644 --- a/test/typing/plain_files/sql/functions_again.py +++ b/test/typing/plain_files/sql/functions_again.py @@ -40,7 +40,7 @@ stmt1 = select( Foo.a, func.min(Foo.b), ).group_by(Foo.a) -# EXPECTED_TYPE: Select[Tuple[int, int]] +# EXPECTED_TYPE: Select[int, int] reveal_type(stmt1) # test #10818 @@ -52,5 +52,5 @@ stmt2 = select( Foo.a, func.coalesce(Foo.c, "a", "b"), ).group_by(Foo.a) -# EXPECTED_TYPE: Select[Tuple[int, str]] +# EXPECTED_TYPE: Select[int, str] reveal_type(stmt2) diff --git a/test/typing/plain_files/sql/lambda_stmt.py b/test/typing/plain_files/sql/lambda_stmt.py index bce5557db8..035fde800d 100644 --- a/test/typing/plain_files/sql/lambda_stmt.py +++ b/test/typing/plain_files/sql/lambda_stmt.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Tuple from typing import TYPE_CHECKING from sqlalchemy import Column @@ -62,15 +61,15 @@ with e.connect() as conn: result = conn.execute(s6) if TYPE_CHECKING: - # EXPECTED_TYPE: CursorResult[Any] + # EXPECTED_TYPE: CursorResult[Unpack[.*tuple[Any, ...]]] reveal_type(result) # we can type these like this - my_result: Result[Tuple[User]] = conn.execute(s6) + my_result: Result[User] = conn.execute(s6) if TYPE_CHECKING: # pyright and mypy disagree on the specific type here, # mypy sees Result as we said, pyright seems to upgrade it to # CursorResult - # EXPECTED_RE_TYPE: .*(?:Cursor)?Result\[Tuple\[.*User\]\] + # EXPECTED_RE_TYPE: .*(?:Cursor)?Result\[.*User\] reveal_type(my_result) diff --git a/test/typing/plain_files/sql/typed_results.py b/test/typing/plain_files/sql/typed_results.py index c7842a7e79..3c8b7f9134 100644 --- a/test/typing/plain_files/sql/typed_results.py +++ b/test/typing/plain_files/sql/typed_results.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from typing import cast from typing import Optional -from typing import Tuple from typing import Type from sqlalchemy import Column @@ -87,18 +86,18 @@ reveal_type(async_session) single_stmt = select(User.name).where(User.name == "foo") -# EXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[Tuple\[builtins.str\*?\]\] +# EXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[builtins.str\*?\] reveal_type(single_stmt) multi_stmt = select(User.id, User.name).where(User.name == "foo") -# EXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[Tuple\[builtins.int\*?, builtins.str\*?\]\] +# EXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[builtins.int\*?, builtins.str\*?\] reveal_type(multi_stmt) def t_result_ctxmanager() -> None: with connection.execute(select(column("q", Integer))) as r1: - # EXPECTED_TYPE: CursorResult[Tuple[int]] + # EXPECTED_TYPE: CursorResult[int] reveal_type(r1) with r1.mappings() as r1m: @@ -110,7 +109,7 @@ def t_result_ctxmanager() -> None: reveal_type(r2) with session.execute(select(User.id)) as r3: - # EXPECTED_TYPE: Result[Tuple[int]] + # EXPECTED_TYPE: Result[int] reveal_type(r3) with session.scalars(select(User.id)) as r4: @@ -130,14 +129,14 @@ def t_entity_varieties() -> None: r1 = session.execute(s1) - # EXPECTED_RE_TYPE: sqlalchemy..*.Result\[Tuple\[builtins.int\*?, typed_results.User\*?, builtins.str\*?\]\] + # EXPECTED_RE_TYPE: sqlalchemy..*.Result\[builtins.int\*?, typed_results.User\*?, builtins.str\*?\] reveal_type(r1) s2 = select(User, a1).where(User.name == "foo") r2 = session.execute(s2) - # EXPECTED_RE_TYPE: sqlalchemy.*Result\[Tuple\[typed_results.User\*?, typed_results.User\*?\]\] + # EXPECTED_RE_TYPE: sqlalchemy.*Result\[typed_results.User\*?, typed_results.User\*?\] reveal_type(r2) row = r2.t.one() @@ -153,18 +152,18 @@ def t_entity_varieties() -> None: # automatically typed since they are dynamically generated a1_id = cast(Mapped[int], a1.id) s3 = select(User.id, a1_id, a1, User).where(User.name == "foo") - # EXPECTED_RE_TYPE: sqlalchemy.*Select\*?\[Tuple\[builtins.int\*?, builtins.int\*?, typed_results.User\*?, typed_results.User\*?\]\] + # EXPECTED_RE_TYPE: sqlalchemy.*Select\*?\[builtins.int\*?, builtins.int\*?, typed_results.User\*?, typed_results.User\*?\] reveal_type(s3) # testing Mapped[entity] some_mp = cast(Mapped[User], object()) s4 = select(some_mp, a1, User).where(User.name == "foo") - # NOTEXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[Tuple\[typed_results.User\*?, typed_results.User\*?, typed_results.User\*?\]\] + # NOTEXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[typed_results.User\*?, typed_results.User\*?, typed_results.User\*?\] - # sqlalchemy.sql._gen_overloads.Select[Tuple[typed_results.User, typed_results.User, typed_results.User]] + # sqlalchemy.sql._gen_overloads.Select[typed_results.User, typed_results.User, typed_results.User] - # EXPECTED_TYPE: Select[Tuple[User, User, User]] + # EXPECTED_TYPE: Select[User, User, User] reveal_type(s4) # test plain core expressions @@ -173,30 +172,30 @@ def t_entity_varieties() -> None: s5 = select(x, y, User.name + "hi") - # EXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[Tuple\[builtins.int\*?, builtins.int\*?\, builtins.str\*?]\] + # EXPECTED_RE_TYPE: sqlalchemy..*Select\*?\[builtins.int\*?, builtins.int\*?\, builtins.str\*?] reveal_type(s5) def t_ambiguous_result_type_one() -> None: stmt = select(column("q", Integer), table("x", column("y"))) - # EXPECTED_TYPE: Select[Any] + # EXPECTED_TYPE: Select[Unpack[.*tuple[Any, ...]]] reveal_type(stmt) result = session.execute(stmt) - # EXPECTED_TYPE: Result[Any] + # EXPECTED_TYPE: Result[Unpack[.*tuple[Any, ...]]] reveal_type(result) def t_ambiguous_result_type_two() -> None: stmt = select(column("q")) - # EXPECTED_TYPE: Select[Tuple[Any]] + # EXPECTED_TYPE: Select[Any] reveal_type(stmt) result = session.execute(stmt) - # EXPECTED_TYPE: Result[Any] + # EXPECTED_TYPE: Result[Unpack[.*tuple[Any, ...]]] reveal_type(result) @@ -204,11 +203,11 @@ def t_aliased() -> None: a1 = aliased(User) s1 = select(a1) - # EXPECTED_TYPE: Select[Tuple[User]] + # EXPECTED_TYPE: Select[User] reveal_type(s1) s4 = select(a1.name, a1, a1, User).where(User.name == "foo") - # EXPECTED_TYPE: Select[Tuple[str, User, User, User]] + # EXPECTED_TYPE: Select[str, User, User, User] reveal_type(s4) @@ -341,11 +340,11 @@ async def t_async_result_insertmanyvalues_scalars() -> None: def t_connection_execute_multi_row_t() -> None: result = connection.execute(multi_stmt) - # EXPECTED_RE_TYPE: sqlalchemy.*CursorResult\[Tuple\[builtins.int\*?, builtins.str\*?\]\] + # EXPECTED_RE_TYPE: sqlalchemy.*CursorResult\[builtins.int\*?, builtins.str\*?\] reveal_type(result) row = result.one() - # EXPECTED_RE_TYPE: sqlalchemy.*Row\[Tuple\[builtins.int\*?, builtins.str\*?\]\] + # EXPECTED_RE_TYPE: .*sqlalchemy.*Row\[builtins.int\*?, builtins.str\*?\].* reveal_type(row) x, y = row.t @@ -681,18 +680,18 @@ def test_outerjoin_10173() -> None: id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] - stmt: Select[Tuple[User, Other]] = select(User, Other).outerjoin( + stmt: Select[User, Other] = select(User, Other).outerjoin( Other, User.id == Other.id ) - stmt2: Select[Tuple[User, Optional[Other]]] = select( + stmt2: Select[User, Optional[Other]] = select( User, Nullable(Other) ).outerjoin(Other, User.id == Other.id) - stmt3: Select[Tuple[int, Optional[str]]] = select( + stmt3: Select[int, Optional[str]] = select( User.id, Nullable(Other.name) ).outerjoin(Other, User.id == Other.id) def go(W: Optional[Type[Other]]) -> None: - stmt4: Select[Tuple[str, Other]] = select( + stmt4: Select[str, Other] = select( NotNullable(User.value), NotNullable(W) ).where(User.value.is_not(None)) print(stmt4) diff --git a/tools/generate_sql_functions.py b/tools/generate_sql_functions.py index 51422dc7e6..411cfed721 100644 --- a/tools/generate_sql_functions.py +++ b/tools/generate_sql_functions.py @@ -169,7 +169,7 @@ def {key}(self) -> Type[{_type}]:{_reserved_word} rf""" stmt{count} = select(func.{key}(column('x', Integer))) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*int\]\] +# EXPECTED_RE_TYPE: .*Select\[.*int\] reveal_type(stmt{count}) """, @@ -183,7 +183,7 @@ reveal_type(stmt{count}) rf""" stmt{count} = select(func.{key}(column('x', String), ',')) -# EXPECTED_RE_TYPE: .*Select\[Tuple\[.*str\]\] +# EXPECTED_RE_TYPE: .*Select\[.*str\] reveal_type(stmt{count}) """, @@ -195,7 +195,7 @@ reveal_type(stmt{count}) fn_class.type, TypeEngine ): python_type = fn_class.type.python_type - python_expr = rf"Tuple\[.*{python_type.__name__}\]" + python_expr = rf".*{python_type.__name__}" argspec = inspect.getfullargspec(fn_class) if fn_class.__name__ == "next_value": args = "Sequence('x_seq')" diff --git a/tools/generate_tuple_map_overloads.py b/tools/generate_tuple_map_overloads.py index e886b7fddc..9ca648333c 100644 --- a/tools/generate_tuple_map_overloads.py +++ b/tools/generate_tuple_map_overloads.py @@ -82,17 +82,26 @@ def process_module(modname: str, filename: str, cmd: code_writer_cmd) -> str: ) for num_args in range(start_index, end_index + 1): + ret_suffix = "" combinations = [ f"__ent{arg}: _TCCA[_T{arg}]" for arg in range(num_args) ] + + if num_args == end_index: + ret_suffix = ", Unpack[TupleAny]" + extra_args = ( + f", *entities: _ColumnsClauseArgument[Any]" + f"{extra_args.replace(', *', '')}" + ) + buf.write( textwrap.indent( f""" @overload def {current_fnname}( {'self, ' if use_self else ''}{", ".join(combinations)},/{extra_args} -) -> {return_type}[Tuple[{', '.join(f'_T{i}' for i in range(num_args))}]]: +) -> {return_type}[{', '.join(f'_T{i}' for i in range(num_args))}{ret_suffix}]: ... """, # noqa: E501 diff --git a/tox.ini b/tox.ini index cd07aa9620..dbffc9e206 100644 --- a/tox.ini +++ b/tox.ini @@ -180,7 +180,7 @@ commands= [testenv:pep484] deps= greenlet != 0.4.17 - mypy >= 1.6.0 + mypy >= 1.7.0 types-greenlet commands = mypy {env:MYPY_COLOR} ./lib/sqlalchemy @@ -193,7 +193,7 @@ deps= pytest>=7.0.0rc1,<8 pytest-xdist greenlet != 0.4.17 - mypy >= 1.2.0 + mypy >= 1.7.0 patch==1.* types-greenlet commands =