SQLAlchemy 2.1.0rc2__py3-none-any.whl → 2.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/dialects/mssql/base.py +21 -0
  3. sqlalchemy/dialects/mssql/mssqlpython.py +13 -4
  4. sqlalchemy/dialects/mssql/pyodbc.py +6 -9
  5. sqlalchemy/dialects/oracle/oracledb.py +24 -1
  6. sqlalchemy/dialects/postgresql/base.py +62 -8
  7. sqlalchemy/engine/_row_cy.py +1 -1
  8. sqlalchemy/engine/default.py +12 -0
  9. sqlalchemy/engine/interfaces.py +31 -2
  10. sqlalchemy/ext/asyncio/base.py +31 -0
  11. sqlalchemy/ext/asyncio/engine.py +3 -5
  12. sqlalchemy/ext/asyncio/session.py +3 -5
  13. sqlalchemy/ext/automap.py +8 -8
  14. sqlalchemy/orm/_orm_constructors.py +17 -0
  15. sqlalchemy/orm/descriptor_props.py +24 -2
  16. sqlalchemy/orm/relationships.py +29 -12
  17. sqlalchemy/orm/strategies.py +13 -4
  18. sqlalchemy/orm/util.py +93 -4
  19. sqlalchemy/pool/base.py +22 -6
  20. sqlalchemy/sql/_elements_constructors.py +26 -0
  21. sqlalchemy/sql/base.py +151 -1
  22. sqlalchemy/sql/compiler.py +58 -17
  23. sqlalchemy/sql/elements.py +32 -3
  24. sqlalchemy/sql/schema.py +5 -0
  25. sqlalchemy/sql/selectable.py +23 -11
  26. sqlalchemy/sql/sqltypes.py +10 -4
  27. sqlalchemy/sql/type_api.py +53 -24
  28. sqlalchemy/testing/cancellation.py +203 -73
  29. sqlalchemy/testing/requirements.py +35 -2
  30. sqlalchemy/testing/suite/test_dialect.py +1 -0
  31. sqlalchemy/testing/suite/test_types.py +129 -3
  32. sqlalchemy/testing/suite/test_update_delete.py +37 -0
  33. sqlalchemy/util/langhelpers.py +8 -2
  34. {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/METADATA +2 -24
  35. {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/RECORD +39 -39
  36. {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/WHEEL +0 -0
  37. {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/licenses/AUTHORS +0 -0
  38. {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/licenses/LICENSE +0 -0
  39. {sqlalchemy-2.1.0rc2.dist-info → sqlalchemy-2.1.2.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -285,7 +285,7 @@ from .types import Uuid as Uuid
285
285
  from .types import VARBINARY as VARBINARY
286
286
  from .types import VARCHAR as VARCHAR
287
287
 
288
- __version__ = "2.1.0rc2"
288
+ __version__ = "2.1.2"
289
289
 
290
290
 
291
291
  def __go(lcls: Any) -> None:
@@ -3315,6 +3315,27 @@ class MSDialect(default._BackendsMultiReflection, default.DefaultDialect):
3315
3315
  finally:
3316
3316
  cursor.close()
3317
3317
 
3318
+ def min_year_for_type(self, typeobj):
3319
+ typeobj = sqltypes.to_instance(typeobj)
3320
+
3321
+ if isinstance(typeobj, SMALLDATETIME):
3322
+ return 1900
3323
+ elif isinstance(typeobj, (DATETIME2, DATETIMEOFFSET)):
3324
+ return datetime.MINYEAR
3325
+ elif isinstance(typeobj, sqltypes.DateTime):
3326
+ # DateTime renders DATETIMEOFFSET with timezone=True,
3327
+ # else DATETIME
3328
+ return datetime.MINYEAR if typeobj.timezone else 1753
3329
+ elif (
3330
+ isinstance(typeobj, sqltypes.Date)
3331
+ and not isinstance(typeobj, sqltypes.DATE)
3332
+ and self.server_version_info < MS_2008_VERSION
3333
+ ):
3334
+ # Date renders DATETIME before SQL Server 2008
3335
+ return 1753
3336
+ else:
3337
+ return datetime.MINYEAR
3338
+
3318
3339
  def initialize(self, connection):
3319
3340
  super().initialize(connection)
3320
3341
  self._setup_version_attributes()
@@ -101,9 +101,6 @@ class MSDialect_mssqlpython(MSDialect):
101
101
  }
102
102
  )
103
103
 
104
- # used by pyodbc _ms_numeric_pyodbc class
105
- _need_decimal_fix = True
106
-
107
104
  colspecs = util.update_copy(
108
105
  MSDialect.colspecs,
109
106
  {
@@ -117,6 +114,18 @@ class MSDialect_mssqlpython(MSDialect):
117
114
  if not enable_pooling and self.dbapi is not None:
118
115
  self.loaded_dbapi.pooling(enabled=False)
119
116
 
117
+ # used by pyodbc _ms_numeric_pyodbc class. mssql-python 1.15
118
+ # binds every Decimal as SQL_NUMERIC; prior versions bound Decimal
119
+ # values within the MONEY range as VARCHAR, so that very large and
120
+ # very small values needed to be passed as strings as well. On
121
+ # 1.15 those strings are instead coerced to the NUMERIC type of
122
+ # the Decimal values alongside them, which overflows.
123
+ # a version which can't be determined is assumed to be an old one
124
+ version = self._dbapi_version_or_none
125
+ self._need_decimal_fix = bool(self.dbapi) and (
126
+ version is None or version < (1, 15)
127
+ )
128
+
120
129
  @classmethod
121
130
  def import_dbapi(cls) -> DBAPIModule:
122
131
  return __import__("mssql_python")
@@ -204,7 +213,7 @@ class MSDialect_mssqlpython(MSDialect):
204
213
  def retrieve_dbapi_version(
205
214
  self, dbapi: interfaces.DBAPIModule
206
215
  ) -> util.VersionInfo:
207
- return util.parse_version_string(dbapi.version)
216
+ return util.parse_version_string(getattr(dbapi, "__version__", None))
208
217
 
209
218
  def _get_server_version_info(self, connection):
210
219
  vers = connection.exec_driver_sql("select @@version").scalar()
@@ -489,15 +489,12 @@ class _ODBCDateTimeBindProcessor:
489
489
  else:
490
490
  # for DATETIMEOFFSET or DateTime(timezone=True)
491
491
  #
492
- # Convert to string format required by T-SQL
493
- dto_string = value.strftime("%Y-%m-%d %H:%M:%S.%f %z")
494
- # offset needs a colon, e.g., -0700 -> -07:00
495
- # "UTC offset in the form (+-)HHMM[SS[.ffffff]]"
496
- # backend currently rejects seconds / fractional seconds
497
- dto_string = re.sub(
498
- r"([\+\-]\d{2})([\d\.]+)$", r"\1:\2", dto_string
499
- )
500
- return dto_string
492
+ # Convert to string format required by T-SQL. isoformat()
493
+ # zero-pads the year, which strftime("%Y") does not do on
494
+ # all platforms, and renders the offset with a colon,
495
+ # e.g. -07:00. backend currently rejects seconds /
496
+ # fractional seconds in the offset
497
+ return value.isoformat(sep=" ", timespec="microseconds")
501
498
 
502
499
  return process
503
500
 
@@ -600,6 +600,7 @@ from ...connectors.asyncio import AsyncAdapt_dbapi_connection
600
600
  from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
601
601
  from ...connectors.asyncio import AsyncAdapt_dbapi_module
602
602
  from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
603
+ from ...connectors.asyncio import AsyncAdapt_terminate
603
604
  from ...engine import default
604
605
  from ...util import await_
605
606
 
@@ -773,7 +774,9 @@ class AsyncAdapt_oracledb_ss_cursor(
773
774
  self._cursor = None # type: ignore
774
775
 
775
776
 
776
- class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
777
+ class AsyncAdapt_oracledb_connection(
778
+ AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
779
+ ):
777
780
  _connection: AsyncConnection
778
781
  __slots__ = ()
779
782
 
@@ -838,6 +841,12 @@ class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
838
841
  def tpc_rollback(self, *args: Any, **kwargs: Any) -> Any:
839
842
  return await_(self._connection.tpc_rollback(*args, **kwargs))
840
843
 
844
+ async def _terminate_graceful_close(self) -> None:
845
+ await self._connection.close()
846
+
847
+ def _terminate_force_close(self) -> None:
848
+ self._connection.terminate()
849
+
841
850
 
842
851
  class OracledbAdaptDBAPI(AsyncAdapt_dbapi_module):
843
852
  def __init__(self, oracledb) -> None:
@@ -883,6 +892,14 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
883
892
 
884
893
  minimum_dbapi_version = util.VersionInfo((2, 0, 1))
885
894
 
895
+ def __init__(self, **kwargs):
896
+ super().__init__(**kwargs)
897
+
898
+ self.has_terminate = (
899
+ self._dbapi_version_or_none is not None
900
+ and self._dbapi_version_or_none >= (26, 0, 0)
901
+ )
902
+
886
903
  # thick_mode mode is not supported by asyncio, oracledb will raise
887
904
  @classmethod
888
905
  def import_dbapi(cls):
@@ -893,6 +910,12 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
893
910
  def get_driver_connection(self, connection):
894
911
  return connection._connection
895
912
 
913
+ def do_terminate(self, dbapi_connection) -> None:
914
+ if self.has_terminate:
915
+ dbapi_connection.terminate()
916
+ else:
917
+ super().do_terminate(dbapi_connection)
918
+
896
919
 
897
920
  dialect = OracleDialect_oracledb
898
921
  dialect_async = OracleDialectAsync_oracledb
@@ -1179,6 +1179,12 @@ construct, the DBAPI's "autocommit" mode must be used::
1179
1179
  PostgreSQL Index Reflection
1180
1180
  ---------------------------
1181
1181
 
1182
+ Notes on Index reflection
1183
+
1184
+
1185
+ Implicit UNIQUE index for UNIQUE CONSTRAINT
1186
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1187
+
1182
1188
  The PostgreSQL database creates a UNIQUE INDEX implicitly whenever the
1183
1189
  UNIQUE CONSTRAINT construct is used. When inspecting a table using
1184
1190
  :class:`_reflection.Inspector`, the :meth:`_reflection.Inspector.get_indexes`
@@ -1189,8 +1195,38 @@ two constructs distinctly; in the case of the index, the key
1189
1195
  detected as mirroring a constraint. When performing reflection using
1190
1196
  ``Table(..., autoload_with=engine)``, the UNIQUE INDEX is **not** returned
1191
1197
  in :attr:`_schema.Table.indexes` when it is detected as mirroring a
1192
- :class:`.UniqueConstraint` in the :attr:`_schema.Table.constraints` collection
1193
- .
1198
+ :class:`.UniqueConstraint` in the :attr:`_schema.Table.constraints` collection.
1199
+
1200
+ .. _postgresql_invalid_index_reflection:
1201
+
1202
+ Invalid indexes
1203
+ ^^^^^^^^^^^^^^^
1204
+
1205
+ Indexes marked "invalid" by PostgreSQL (i.e. an index that exists in the
1206
+ catalog, but is ignored by the query planner because it may be incomplete,
1207
+ while still being maintained on writes) remain present in reflection. The
1208
+ :meth:`_reflection.Inspector.get_indexes` and
1209
+ :meth:`_reflection.Inspector.get_multi_indexes` methods include
1210
+ ``postgresql_invalid=True`` in the index's ``dialect_options`` dictionary when
1211
+ ``pg_index.indisvalid`` is false. Valid indexes omit this flag.
1212
+
1213
+ When reflecting a :class:`_schema.Table`, the flag, if present, is stored
1214
+ separately from DDL options on each reflected :class:`.Index`::
1215
+
1216
+ table = Table("my_table", MetaData(), autoload_with=engine)
1217
+ for index in table.indexes:
1218
+ if index.reflect_only_elements["postgresql"].get("invalid"):
1219
+ print(index.name)
1220
+
1221
+ The :attr:`.Index.reflect_only_elements` mapping is read-only. These values are
1222
+ not included in :attr:`.Index.dialect_kwargs` and do not affect DDL.
1223
+
1224
+ An invalid index may be left by a failed ``CREATE INDEX CONCURRENTLY``, may
1225
+ still be building, or may be a partitioned index awaiting attachment of its
1226
+ partition indexes. This flag reports the state at reflection time; it does not
1227
+ request an index rebuild.
1228
+
1229
+ .. versionadded:: 2.1
1194
1230
 
1195
1231
  Special Reflection Options
1196
1232
  --------------------------
@@ -1946,6 +1982,7 @@ from ...sql import functions
1946
1982
  from ...sql import roles
1947
1983
  from ...sql import sqltypes
1948
1984
  from ...sql import util as sql_util
1985
+ from ...sql.base import DialectKWArgConst
1949
1986
  from ...sql.compiler import InsertmanyvaluesSentinelOpts
1950
1987
  from ...sql.visitors import InternalTraversal
1951
1988
  from ...types import BIGINT
@@ -3666,6 +3703,7 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
3666
3703
  "with": {},
3667
3704
  "tablespace": None,
3668
3705
  "nulls_not_distinct": None,
3706
+ "invalid": DialectKWArgConst.REFLECTED_ONLY,
3669
3707
  },
3670
3708
  ),
3671
3709
  (
@@ -4808,8 +4846,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
4808
4846
  )
4809
4847
  .outerjoin(
4810
4848
  pg_catalog.pg_description,
4811
- pg_catalog.pg_description.c.objoid
4812
- == pg_catalog.pg_constraint.c.oid,
4849
+ sql.and_(
4850
+ pg_catalog.pg_description.c.objoid
4851
+ == pg_catalog.pg_constraint.c.oid,
4852
+ pg_catalog.pg_description.c.classoid
4853
+ == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
4854
+ ),
4813
4855
  )
4814
4856
  .where(
4815
4857
  pg_catalog.pg_constraint.c.contype == bindparam("contype"),
@@ -5001,8 +5043,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5001
5043
  )
5002
5044
  .outerjoin(
5003
5045
  pg_catalog.pg_description,
5004
- pg_catalog.pg_description.c.objoid
5005
- == pg_catalog.pg_constraint.c.oid,
5046
+ sql.and_(
5047
+ pg_catalog.pg_description.c.objoid
5048
+ == pg_catalog.pg_constraint.c.oid,
5049
+ pg_catalog.pg_description.c.classoid
5050
+ == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
5051
+ ),
5006
5052
  )
5007
5053
  .order_by(
5008
5054
  pg_catalog.pg_class.c.relname,
@@ -5269,6 +5315,7 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5269
5315
  pg_catalog.pg_index.c.indrelid,
5270
5316
  pg_catalog.pg_class.c.relname,
5271
5317
  pg_catalog.pg_index.c.indisunique,
5318
+ pg_catalog.pg_index.c.indisvalid,
5272
5319
  pg_catalog.pg_constraint.c.conrelid.is_not(None).label(
5273
5320
  "has_constraint"
5274
5321
  ),
@@ -5475,6 +5522,9 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5475
5522
  "indnullsnotdistinct"
5476
5523
  ]
5477
5524
 
5525
+ if not row["indisvalid"]:
5526
+ dialect_options["postgresql_invalid"] = True
5527
+
5478
5528
  if dialect_options:
5479
5529
  index["dialect_options"] = dialect_options
5480
5530
 
@@ -5590,8 +5640,12 @@ class PGDialect(default._BackendsMultiReflection, default.DefaultDialect):
5590
5640
  )
5591
5641
  .outerjoin(
5592
5642
  pg_catalog.pg_description,
5593
- pg_catalog.pg_description.c.objoid
5594
- == pg_catalog.pg_constraint.c.oid,
5643
+ sql.and_(
5644
+ pg_catalog.pg_description.c.objoid
5645
+ == pg_catalog.pg_constraint.c.oid,
5646
+ pg_catalog.pg_description.c.classoid
5647
+ == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
5648
+ ),
5595
5649
  )
5596
5650
  .order_by(
5597
5651
  pg_catalog.pg_class.c.relname,
@@ -69,7 +69,7 @@ class BaseRow:
69
69
  (
70
70
  _apply_processors(processors, data)
71
71
  if processors is not None
72
- else data if isinstance(data, tuple) else tuple(data)
72
+ else data if type(data) is tuple else tuple(data)
73
73
  ),
74
74
  )
75
75
 
@@ -16,6 +16,7 @@ as the base class for their own corresponding classes.
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import datetime
19
20
  import functools
20
21
  import operator
21
22
  import random
@@ -829,6 +830,17 @@ class DefaultDialect(Dialect):
829
830
  """
830
831
  return type_api.adapt_type(typeobj, self.colspecs)
831
832
 
833
+ def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
834
+ """Return the earliest year the database accepts for the given
835
+ date or datetime type.
836
+
837
+ The default implementation returns ``datetime.MINYEAR``.
838
+
839
+ .. versionadded:: 2.0.55
840
+
841
+ """
842
+ return datetime.MINYEAR
843
+
832
844
  def has_index(self, connection, table_name, index_name, schema=None, **kw):
833
845
  if not self.has_table(connection, table_name, schema=schema, **kw):
834
846
  return False
@@ -560,7 +560,7 @@ class ReflectedIndex(TypedDict):
560
560
  """
561
561
 
562
562
  dialect_options: NotRequired[Dict[str, Any]]
563
- """Additional dialect-specific options detected for this index"""
563
+ """Additional dialect-specific options detected for this index."""
564
564
 
565
565
 
566
566
  class ReflectedTableComment(TypedDict):
@@ -1259,6 +1259,17 @@ class Dialect(EventTarget):
1259
1259
  here is so that third-party dialects that haven't yet implemented this
1260
1260
  feature continue to function in the old way.
1261
1261
 
1262
+ Arguments that represent database state without any corresponding DDL,
1263
+ but may nonetheless appear in a reflected set of arguments that should
1264
+ be ignored by the construct, may make use of the
1265
+ :attr:`.DialectKWArgConst.REFLECTED_ONLY` token as the default value.
1266
+ Arguments passed to the schema object on such a key will be passed along
1267
+ into the separate read-only collection
1268
+ :attr:`.DialectKWArgs.reflect_only_elements` and should be omitted from
1269
+ DDL rendering.
1270
+
1271
+ .. versionadded:: 2.1 Added :class:`.DialectKWArgConst`.
1272
+
1262
1273
  .. seealso::
1263
1274
 
1264
1275
  :class:`.DialectKWArgs` - implementing base class which consumes
@@ -1454,6 +1465,23 @@ class Dialect(EventTarget):
1454
1465
 
1455
1466
  raise NotImplementedError()
1456
1467
 
1468
+ def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
1469
+ """Return the earliest year the database accepts for the given
1470
+ date or datetime type.
1471
+
1472
+ The type may be a generic type such as :class:`.DateTime`, in
1473
+ which case the limit is that of the database type the dialect
1474
+ renders for it. Dialects whose date or datetime types don't
1475
+ accept all years from ``datetime.MINYEAR`` forward override this
1476
+ method; e.g. SQL Server's ``DATETIME`` type, which the dialect
1477
+ renders for :class:`.DateTime`, accepts years from 1753 forward.
1478
+
1479
+ .. versionadded:: 2.0.55
1480
+
1481
+ """
1482
+
1483
+ raise NotImplementedError()
1484
+
1457
1485
  def initialize(self, connection: Connection) -> None:
1458
1486
  """Called during strategized creation of the dialect with a
1459
1487
  connection.
@@ -3426,7 +3454,8 @@ class ExceptionContext:
3426
3454
 
3427
3455
  """
3428
3456
 
3429
- __slots__ = ()
3457
+ if not TYPE_CHECKING:
3458
+ __slots__ = ()
3430
3459
 
3431
3460
  dialect: Dialect
3432
3461
  """The :class:`_engine.Dialect` in use.
@@ -8,6 +8,7 @@
8
8
  from __future__ import annotations
9
9
 
10
10
  import abc
11
+ import asyncio
11
12
  import functools
12
13
  from typing import Any
13
14
  from typing import AsyncGenerator
@@ -15,6 +16,7 @@ from typing import AsyncIterator
15
16
  from typing import Awaitable
16
17
  from typing import Callable
17
18
  from typing import ClassVar
19
+ from typing import Coroutine
18
20
  from typing import Dict
19
21
  from typing import Generator
20
22
  from typing import Generic
@@ -37,6 +39,35 @@ _T_co = TypeVar("_T_co", bound=Any, covariant=True)
37
39
  _PT = TypeVar("_PT", bound=Any)
38
40
 
39
41
 
42
+ async def _run_to_completion(coro: Coroutine[Any, Any, _T]) -> _T:
43
+ """Run ``coro`` in a task of its own and wait for it to complete, even
44
+ if the calling task is cancelled meanwhile.
45
+
46
+ Used for cleanup, such as the ``close()`` performed by an async context
47
+ manager on exit, that must not be abandoned partway through. A
48
+ cancellation received while waiting is re-raised once ``coro`` has
49
+ completed, so that the caller does not return before its cleanup has
50
+ finished; were it to return early, the cleanup would carry on in the
51
+ background, where for example it could return a connection into a pool
52
+ that the application has meanwhile disposed of.
53
+
54
+ """
55
+ task = asyncio.create_task(coro)
56
+ cancelled = None
57
+ while True:
58
+ try:
59
+ result = await asyncio.shield(task)
60
+ except asyncio.CancelledError as err:
61
+ if task.done():
62
+ raise
63
+ cancelled = err
64
+ else:
65
+ break
66
+ if cancelled is not None:
67
+ raise cancelled
68
+ return result
69
+
70
+
40
71
  class ReversibleProxy(Generic[_PT]):
41
72
  _proxy_objects: ClassVar[
42
73
  Dict[weakref.ref[Any], weakref.ref[ReversibleProxy[Any]]]
@@ -6,7 +6,6 @@
6
6
  # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
7
  from __future__ import annotations
8
8
 
9
- import asyncio
10
9
  import contextlib
11
10
  from typing import Any
12
11
  from typing import AsyncIterator
@@ -24,6 +23,7 @@ from typing import TypeVar
24
23
  from typing import Union
25
24
 
26
25
  from . import exc as async_exc
26
+ from .base import _run_to_completion
27
27
  from .base import asyncstartablecontext
28
28
  from .base import GeneratorStartableContext
29
29
  from .base import ProxyComparable
@@ -606,8 +606,7 @@ class AsyncConnection( # type: ignore[misc]
606
606
  except GeneratorExit:
607
607
  pass
608
608
  else:
609
- task = asyncio.create_task(ar.close())
610
- await asyncio.shield(task)
609
+ await _run_to_completion(ar.close())
611
610
 
612
611
  @overload
613
612
  async def execute(
@@ -913,8 +912,7 @@ class AsyncConnection( # type: ignore[misc]
913
912
  return self.start().__await__()
914
913
 
915
914
  async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
916
- task = asyncio.create_task(self.close())
917
- await asyncio.shield(task)
915
+ await _run_to_completion(self.close())
918
916
 
919
917
  # START PROXY METHODS AsyncConnection
920
918
 
@@ -6,7 +6,6 @@
6
6
  # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
7
  from __future__ import annotations
8
8
 
9
- import asyncio
10
9
  from typing import Any
11
10
  from typing import Awaitable
12
11
  from typing import Callable
@@ -30,6 +29,7 @@ from typing import Union
30
29
 
31
30
  from . import engine
32
31
  from . import exc as async_exc
32
+ from .base import _run_to_completion
33
33
  from .base import ReversibleProxy
34
34
  from .base import StartableContext
35
35
  from .result import _ensure_sync_result
@@ -1375,8 +1375,7 @@ class AsyncSession(ReversibleProxy[Session]):
1375
1375
  return self
1376
1376
 
1377
1377
  async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
1378
- task = asyncio.create_task(self.close())
1379
- await asyncio.shield(task)
1378
+ await _run_to_completion(self.close())
1380
1379
 
1381
1380
  def _maker_context_manager(self: _AS) -> _AsyncSessionContextManager[_AS]:
1382
1381
  return _AsyncSessionContextManager(self)
@@ -2121,8 +2120,7 @@ class _AsyncSessionContextManager(Generic[_AS]):
2121
2120
  await self.trans.__aexit__(type_, value, traceback)
2122
2121
  await self.async_session.__aexit__(type_, value, traceback)
2123
2122
 
2124
- task = asyncio.create_task(go())
2125
- await asyncio.shield(task)
2123
+ await _run_to_completion(go())
2126
2124
 
2127
2125
 
2128
2126
  class AsyncSessionTransaction(
sqlalchemy/ext/automap.py CHANGED
@@ -1661,11 +1661,11 @@ def _m2m_relationship(
1661
1661
  overlaps=overlaps,
1662
1662
  secondary=table,
1663
1663
  primaryjoin=and_(
1664
- fk.column == fk.parent for fk in m2m_const[0].elements
1665
- ), # type: ignore [arg-type]
1664
+ *(fk.column == fk.parent for fk in m2m_const[0].elements)
1665
+ ),
1666
1666
  secondaryjoin=and_(
1667
- fk.column == fk.parent for fk in m2m_const[1].elements
1668
- ), # type: ignore [arg-type]
1667
+ *(fk.column == fk.parent for fk in m2m_const[1].elements)
1668
+ ),
1669
1669
  backref=backref_obj,
1670
1670
  collection_class=collection_class,
1671
1671
  )
@@ -1687,11 +1687,11 @@ def _m2m_relationship(
1687
1687
  overlaps=overlaps,
1688
1688
  secondary=table,
1689
1689
  primaryjoin=and_(
1690
- fk.column == fk.parent for fk in m2m_const[1].elements
1691
- ), # type: ignore [arg-type]
1690
+ *(fk.column == fk.parent for fk in m2m_const[1].elements)
1691
+ ),
1692
1692
  secondaryjoin=and_(
1693
- fk.column == fk.parent for fk in m2m_const[0].elements
1694
- ), # type: ignore [arg-type]
1693
+ *(fk.column == fk.parent for fk in m2m_const[0].elements)
1694
+ ),
1695
1695
  back_populates=relationship_name,
1696
1696
  collection_class=collection_class,
1697
1697
  )
@@ -642,6 +642,7 @@ def composite(
642
642
  return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
643
643
  comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
644
644
  active_history: bool = False,
645
+ column_template: Optional[str] = None,
645
646
  init: Union[_NoArg, bool] = _NoArg.NO_ARG,
646
647
  repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
647
648
  default: Optional[Any] = _NoArg.NO_ARG,
@@ -667,6 +668,7 @@ def composite(
667
668
  return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
668
669
  comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
669
670
  active_history: bool = False,
671
+ column_template: Optional[str] = None,
670
672
  init: Union[_NoArg, bool] = _NoArg.NO_ARG,
671
673
  repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
672
674
  default: Optional[Any] = _NoArg.NO_ARG,
@@ -691,6 +693,7 @@ def composite(
691
693
  return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
692
694
  comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
693
695
  active_history: bool = False,
696
+ column_template: Optional[str] = None,
694
697
  init: Union[_NoArg, bool] = _NoArg.NO_ARG,
695
698
  repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
696
699
  default: Optional[Any] = _NoArg.NO_ARG,
@@ -716,6 +719,7 @@ def composite(
716
719
  return_none_on: Union[_NoArg, None, Callable[..., bool]] = _NoArg.NO_ARG,
717
720
  comparator_factory: Optional[Type[Composite.Comparator[_T]]] = None,
718
721
  active_history: bool = False,
722
+ column_template: Optional[str] = None,
719
723
  init: Union[_NoArg, bool] = _NoArg.NO_ARG,
720
724
  repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
721
725
  default: Optional[Any] = _NoArg.NO_ARG,
@@ -785,6 +789,18 @@ def composite(
785
789
  :class:`.Composite.Comparator` which provides custom SQL
786
790
  clause generation for comparison operations.
787
791
 
792
+ :param column_template: A string template such as ``"person_%s"``,
793
+ containing exactly one ``%s`` placeholder, that's used to generate
794
+ column names for fields of a dataclass :paramref:`.composite.class_`
795
+ that don't otherwise have an explicit name. Only supported for
796
+ dataclass-based composite classes.
797
+
798
+ .. seealso::
799
+
800
+ :ref:`composite_column_template`
801
+
802
+ .. versionadded:: 2.1
803
+
788
804
  :param doc:
789
805
  optional string that will be applied as the doc on the
790
806
  class-bound descriptor.
@@ -852,6 +868,7 @@ def composite(
852
868
  raiseload=raiseload,
853
869
  comparator_factory=comparator_factory,
854
870
  active_history=active_history,
871
+ column_template=column_template,
855
872
  info=info,
856
873
  doc=doc,
857
874
  )
@@ -208,6 +208,7 @@ class CompositeProperty(
208
208
 
209
209
  composite_class: Union[Type[_CC], Callable[..., _CC]]
210
210
  attrs: Tuple[_CompositeAttrType[Any], ...]
211
+ column_template: Optional[str]
211
212
 
212
213
  _generated_composite_accessor: CallableReference[
213
214
  Optional[Callable[[_CC], Tuple[Any, ...]]]
@@ -229,6 +230,7 @@ class CompositeProperty(
229
230
  deferred: bool = False,
230
231
  group: Optional[str] = None,
231
232
  comparator_factory: Optional[Type[Comparator[_CC]]] = None,
233
+ column_template: Optional[str] = None,
232
234
  info: Optional[_InfoType] = None,
233
235
  **kwargs: Any,
234
236
  ):
@@ -242,6 +244,17 @@ class CompositeProperty(
242
244
  self.composite_class = _class_or_attr # type: ignore[assignment]
243
245
  self.attrs = attrs
244
246
 
247
+ if column_template is not None:
248
+ try:
249
+ column_template % "x"
250
+ except (TypeError, ValueError) as te:
251
+ raise sa_exc.ArgumentError(
252
+ f"column_template {column_template!r} is not a valid "
253
+ "template; expected a string containing exactly one "
254
+ "'%s' placeholder"
255
+ ) from te
256
+ self.column_template = column_template
257
+
245
258
  self.return_none_on = return_none_on
246
259
  self.active_history = active_history
247
260
  self.deferred = deferred
@@ -428,6 +441,11 @@ class CompositeProperty(
428
441
  decl_scan, registry, cls, originating_module, key
429
442
  )
430
443
  else:
444
+ if self.column_template is not None:
445
+ raise sa_exc.ArgumentError(
446
+ "column_template is only supported when composite_class "
447
+ "is a dataclass"
448
+ )
431
449
  for attr in self.attrs:
432
450
  if (
433
451
  isinstance(attr, (MappedColumn, schema.Column))
@@ -492,8 +510,12 @@ class CompositeProperty(
492
510
  f"{self.composite_class.__name__} {len(insp.parameters)}"
493
511
  )
494
512
  if attr is None:
495
- # fill in missing attr spots with empty MappedColumn
496
- attr = MappedColumn()
513
+ # fill in missing attr spots with empty MappedColumn,
514
+ # or one named from column_template if present
515
+ if self.column_template is not None:
516
+ attr = MappedColumn(self.column_template % param.name)
517
+ else:
518
+ attr = MappedColumn()
497
519
  self.attrs += (attr,)
498
520
 
499
521
  if isinstance(attr, MappedColumn):