SQLAlchemy 2.0.54__py3-none-any.whl → 2.0.55__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 (38) hide show
  1. sqlalchemy/__init__.py +1 -1
  2. sqlalchemy/dialects/mssql/base.py +28 -0
  3. sqlalchemy/dialects/mssql/pyodbc.py +6 -9
  4. sqlalchemy/dialects/oracle/base.py +4 -1
  5. sqlalchemy/dialects/postgresql/base.py +30 -8
  6. sqlalchemy/dialects/postgresql/named_types.py +15 -0
  7. sqlalchemy/engine/base.py +13 -6
  8. sqlalchemy/engine/characteristics.py +1 -1
  9. sqlalchemy/engine/create.py +20 -10
  10. sqlalchemy/engine/default.py +21 -6
  11. sqlalchemy/engine/interfaces.py +19 -1
  12. sqlalchemy/ext/asyncio/base.py +31 -0
  13. sqlalchemy/ext/asyncio/engine.py +3 -5
  14. sqlalchemy/ext/asyncio/session.py +3 -5
  15. sqlalchemy/ext/automap.py +8 -8
  16. sqlalchemy/ext/mutable.py +11 -0
  17. sqlalchemy/orm/bulk_persistence.py +5 -3
  18. sqlalchemy/orm/decl_api.py +17 -5
  19. sqlalchemy/orm/util.py +25 -1
  20. sqlalchemy/pool/base.py +62 -28
  21. sqlalchemy/sql/_elements_constructors.py +6 -0
  22. sqlalchemy/sql/compiler.py +54 -17
  23. sqlalchemy/sql/dml.py +8 -0
  24. sqlalchemy/sql/elements.py +7 -1
  25. sqlalchemy/sql/schema.py +2 -0
  26. sqlalchemy/sql/selectable.py +4 -2
  27. sqlalchemy/sql/sqltypes.py +1 -0
  28. sqlalchemy/sql/type_api.py +53 -24
  29. sqlalchemy/testing/cancellation.py +272 -73
  30. sqlalchemy/testing/requirements.py +21 -2
  31. sqlalchemy/testing/suite/test_types.py +204 -3
  32. sqlalchemy/util/queue.py +11 -0
  33. {sqlalchemy-2.0.54.dist-info → sqlalchemy-2.0.55.dist-info}/METADATA +1 -1
  34. {sqlalchemy-2.0.54.dist-info → sqlalchemy-2.0.55.dist-info}/RECORD +38 -38
  35. {sqlalchemy-2.0.54.dist-info → sqlalchemy-2.0.55.dist-info}/WHEEL +0 -0
  36. {sqlalchemy-2.0.54.dist-info → sqlalchemy-2.0.55.dist-info}/licenses/AUTHORS +0 -0
  37. {sqlalchemy-2.0.54.dist-info → sqlalchemy-2.0.55.dist-info}/licenses/LICENSE +0 -0
  38. {sqlalchemy-2.0.54.dist-info → sqlalchemy-2.0.55.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py CHANGED
@@ -269,7 +269,7 @@ from .types import Uuid as Uuid
269
269
  from .types import VARBINARY as VARBINARY
270
270
  from .types import VARCHAR as VARCHAR
271
271
 
272
- __version__ = "2.0.54"
272
+ __version__ = "2.0.55"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -2080,6 +2080,13 @@ class MSSQLCompiler(compiler.SQLCompiler):
2080
2080
  self.process(binary.right, **kw),
2081
2081
  )
2082
2082
 
2083
+ def visit_truediv_binary(self, binary, operator, **kw):
2084
+ return (
2085
+ self.process(binary.left, **kw)
2086
+ + " / "
2087
+ + f"({self.process(binary.right, **kw)} + 0.0)"
2088
+ )
2089
+
2083
2090
  def visit_true(self, expr, **kw):
2084
2091
  return "1"
2085
2092
 
@@ -3243,6 +3250,27 @@ class MSDialect(default.DefaultDialect):
3243
3250
  finally:
3244
3251
  cursor.close()
3245
3252
 
3253
+ def min_year_for_type(self, typeobj):
3254
+ typeobj = sqltypes.to_instance(typeobj)
3255
+
3256
+ if isinstance(typeobj, SMALLDATETIME):
3257
+ return 1900
3258
+ elif isinstance(typeobj, (DATETIME2, DATETIMEOFFSET)):
3259
+ return datetime.MINYEAR
3260
+ elif isinstance(typeobj, sqltypes.DateTime):
3261
+ # DateTime renders DATETIMEOFFSET with timezone=True,
3262
+ # else DATETIME
3263
+ return datetime.MINYEAR if typeobj.timezone else 1753
3264
+ elif (
3265
+ isinstance(typeobj, sqltypes.Date)
3266
+ and not isinstance(typeobj, sqltypes.DATE)
3267
+ and self.server_version_info < MS_2008_VERSION
3268
+ ):
3269
+ # Date renders DATETIME before SQL Server 2008
3270
+ return 1753
3271
+ else:
3272
+ return datetime.MINYEAR
3273
+
3246
3274
  def initialize(self, connection):
3247
3275
  super().initialize(connection)
3248
3276
  self._setup_version_attributes()
@@ -493,15 +493,12 @@ class _ODBCDateTimeBindProcessor:
493
493
  else:
494
494
  # for DATETIMEOFFSET or DateTime(timezone=True)
495
495
  #
496
- # Convert to string format required by T-SQL
497
- dto_string = value.strftime("%Y-%m-%d %H:%M:%S.%f %z")
498
- # offset needs a colon, e.g., -0700 -> -07:00
499
- # "UTC offset in the form (+-)HHMM[SS[.ffffff]]"
500
- # backend currently rejects seconds / fractional seconds
501
- dto_string = re.sub(
502
- r"([\+\-]\d{2})([\d\.]+)$", r"\1:\2", dto_string
503
- )
504
- return dto_string
496
+ # Convert to string format required by T-SQL. isoformat()
497
+ # zero-pads the year, which strftime("%Y") does not do on
498
+ # all platforms, and renders the offset with a colon,
499
+ # e.g. -07:00. backend currently rejects seconds /
500
+ # fractional seconds in the offset
501
+ return value.isoformat(sep=" ", timespec="microseconds")
505
502
 
506
503
  return process
507
504
 
@@ -2108,7 +2108,10 @@ class OracleDialect(default.DefaultDialect):
2108
2108
  return self.get_isolation_level(dbapi_conn)
2109
2109
  except NotImplementedError:
2110
2110
  raise
2111
- except:
2111
+ except Exception:
2112
+ # Exception only; a bare except here also caught
2113
+ # asyncio.CancelledError and KeyboardInterrupt, returning the
2114
+ # default in place of propagating them
2112
2115
  return "READ COMMITTED"
2113
2116
 
2114
2117
  def _execute_reflection(
@@ -4379,8 +4379,12 @@ class PGDialect(default.DefaultDialect):
4379
4379
  )
4380
4380
  .outerjoin(
4381
4381
  pg_catalog.pg_description,
4382
- pg_catalog.pg_description.c.objoid
4383
- == pg_catalog.pg_constraint.c.oid,
4382
+ sql.and_(
4383
+ pg_catalog.pg_description.c.objoid
4384
+ == pg_catalog.pg_constraint.c.oid,
4385
+ pg_catalog.pg_description.c.classoid
4386
+ == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
4387
+ ),
4384
4388
  )
4385
4389
  .where(
4386
4390
  pg_catalog.pg_constraint.c.contype == bindparam("contype"),
@@ -4604,8 +4608,12 @@ class PGDialect(default.DefaultDialect):
4604
4608
  )
4605
4609
  .outerjoin(
4606
4610
  pg_catalog.pg_description,
4607
- pg_catalog.pg_description.c.objoid
4608
- == pg_catalog.pg_constraint.c.oid,
4611
+ sql.and_(
4612
+ pg_catalog.pg_description.c.objoid
4613
+ == pg_catalog.pg_constraint.c.oid,
4614
+ pg_catalog.pg_description.c.classoid
4615
+ == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
4616
+ ),
4609
4617
  )
4610
4618
  .order_by(
4611
4619
  pg_catalog.pg_class.c.relname,
@@ -4627,8 +4635,9 @@ class PGDialect(default.DefaultDialect):
4627
4635
 
4628
4636
  # https://www.postgresql.org/docs/current/static/sql-createtable.html
4629
4637
  return re.compile(
4630
- r"FOREIGN KEY \((.*?)\) "
4631
- rf"REFERENCES (?:({qtoken})\.)?({qtoken})\(((?:{qtoken}(?: *, *)?)+)\)" # noqa: E501
4638
+ rf"FOREIGN KEY \((.*?)(?:, PERIOD ({qtoken}))?\) "
4639
+ rf"REFERENCES (?:({qtoken})\.)?({qtoken})"
4640
+ rf"\(((?:{qtoken}(?: *, *)?)+)(?:, PERIOD ({qtoken}))?\)"
4632
4641
  r"[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?"
4633
4642
  r"[\s]?(?:ON (UPDATE|DELETE) "
4634
4643
  r"(CASCADE|RESTRICT|NO ACTION|"
@@ -4646,9 +4655,11 @@ class PGDialect(default.DefaultDialect):
4646
4655
 
4647
4656
  (
4648
4657
  constrained_columns,
4658
+ constrained_period_column,
4649
4659
  referred_schema,
4650
4660
  referred_table,
4651
4661
  referred_columns,
4662
+ referred_period_column,
4652
4663
  _,
4653
4664
  match,
4654
4665
  upddelkey1,
@@ -4660,6 +4671,13 @@ class PGDialect(default.DefaultDialect):
4660
4671
  initially,
4661
4672
  ) = m
4662
4673
 
4674
+ # TODO: the PERIOD designation of a temporal foreign key
4675
+ # (PostgreSQL 18+) is not yet reflected
4676
+ if constrained_period_column:
4677
+ constrained_columns += f", {constrained_period_column}"
4678
+ if referred_period_column:
4679
+ referred_columns += f", {referred_period_column}"
4680
+
4663
4681
  onupdate = (
4664
4682
  upddelval1
4665
4683
  if upddelkey1 == "UPDATE"
@@ -5245,8 +5263,12 @@ class PGDialect(default.DefaultDialect):
5245
5263
  )
5246
5264
  .outerjoin(
5247
5265
  pg_catalog.pg_description,
5248
- pg_catalog.pg_description.c.objoid
5249
- == pg_catalog.pg_constraint.c.oid,
5266
+ sql.and_(
5267
+ pg_catalog.pg_description.c.objoid
5268
+ == pg_catalog.pg_constraint.c.oid,
5269
+ pg_catalog.pg_description.c.classoid
5270
+ == sql.func.cast("pg_catalog.pg_constraint", REGCLASS),
5271
+ ),
5250
5272
  )
5251
5273
  .order_by(
5252
5274
  pg_catalog.pg_class.c.relname,
@@ -341,6 +341,9 @@ class ENUM(NamedType, type_api.NativeForEmulated, sqltypes.Enum):
341
341
  kw.setdefault("_create_events", False)
342
342
  kw.setdefault("values_callable", impl.values_callable)
343
343
  kw.setdefault("omit_aliases", impl._omit_aliases)
344
+ kw.setdefault("sort_key_function", impl._sort_key_function)
345
+ kw.setdefault("create_constraint", impl.create_constraint)
346
+ kw.setdefault("length", impl.length)
344
347
  kw.setdefault("_adapted_from", impl)
345
348
  if type_api._is_native_for_emulated(impl.__class__):
346
349
  kw.setdefault("create_type", impl.create_type)
@@ -503,6 +506,18 @@ class DOMAIN(NamedType, sqltypes.SchemaType):
503
506
  def __test_init__(cls):
504
507
  return cls("name", sqltypes.Integer)
505
508
 
509
+ def adapt(self, cls, **kw):
510
+ # the DOMAIN-specific arguments are keyword-only, which are not
511
+ # picked up by util.constructor_copy(); pass them explicitly so
512
+ # that copy(), to_metadata() and dialect adaptation retain them
513
+ kw.setdefault("collation", self.collation)
514
+ kw.setdefault("default", self.default)
515
+ kw.setdefault("constraint_name", self.constraint_name)
516
+ kw.setdefault("not_null", self.not_null)
517
+ kw.setdefault("check", self.check)
518
+ kw.setdefault("create_type", self.create_type)
519
+ return super().adapt(cls, **kw)
520
+
506
521
 
507
522
  class CreateEnumType(schema._CreateDropBase):
508
523
  __visit_name__ = "create_enum_type"
sqlalchemy/engine/base.py CHANGED
@@ -742,12 +742,19 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]):
742
742
  if self.closed:
743
743
  raise exc.ResourceClosedError("This Connection is closed")
744
744
 
745
- if self._still_open_and_dbapi_connection_is_valid:
746
- pool_proxied_connection = self._dbapi_connection
747
- assert pool_proxied_connection is not None
748
- pool_proxied_connection.invalidate(exception)
749
-
750
- self._dbapi_connection = None
745
+ try:
746
+ if self._still_open_and_dbapi_connection_is_valid:
747
+ pool_proxied_connection = self._dbapi_connection
748
+ assert pool_proxied_connection is not None
749
+ pool_proxied_connection.invalidate(exception)
750
+ finally:
751
+ # this is always a hard invalidation, which closes the DBAPI
752
+ # connection; for an asyncio driver that close is an awaited
753
+ # terminate() which a cancellation can interrupt. this
754
+ # Connection is invalidated regardless, so that nothing
755
+ # further, such as a rollback, is emitted on the DBAPI
756
+ # connection
757
+ self._dbapi_connection = None
751
758
 
752
759
  def detach(self) -> None:
753
760
  """Detach the underlying DB-API connection from its connection pool.
@@ -142,7 +142,7 @@ class LoggingTokenCharacteristic(ConnectionCharacteristic):
142
142
  if value:
143
143
  conn._message_formatter = lambda msg: "[%s] %s" % (value, msg)
144
144
  else:
145
- del conn._message_formatter
145
+ conn._message_formatter = None
146
146
 
147
147
  def get_characteristic(
148
148
  self, dialect: Dialect, dbapi_conn: DBAPIConnection
@@ -769,16 +769,26 @@ def create_engine(url: Union[str, _url.URL], **kwargs: Any) -> Engine:
769
769
  )
770
770
  c._execution_options = util.EMPTY_DICT
771
771
 
772
- try:
773
- dialect.initialize(c)
774
- finally:
775
- # note that "invalidated" and "closed" are mutually
776
- # exclusive in 1.4 Connection.
777
- if not c.invalidated and not c.closed:
778
- # transaction is rolled back otherwise, tested by
779
- # test/dialect/postgresql/test_dialect.py
780
- # ::MiscBackendTest::test_initial_transaction_state
781
- dialect.do_rollback(c.connection)
772
+ dialect.initialize(c)
773
+
774
+ # roll back only once initialize() has succeeded, rather than
775
+ # in a finally: block. when initialize() raises, the DBAPI
776
+ # connection may be partway through an operation that c
777
+ # doesn't know about, such as psycopg's TypeInfo.fetch()
778
+ # interrupted by a cancellation; the rollback then fails
779
+ # and its error replaces the original one. the pool closes
780
+ # the connection in that case, so there's nothing to roll
781
+ # back.
782
+ #
783
+ # note that "invalidated" and "closed" are mutually
784
+ # exclusive in 1.4 Connection. c may be invalidated here if
785
+ # initialize() caught an error on a statement that was a
786
+ # disconnect.
787
+ if not c.invalidated and not c.closed:
788
+ # transaction is rolled back otherwise, tested by
789
+ # test/dialect/postgresql/test_dialect.py
790
+ # ::MiscBackendTest::test_initial_transaction_state
791
+ dialect.do_rollback(c.connection)
782
792
 
783
793
  # previously, the "first_connect" event was used here, which was then
784
794
  # scaled back if the "on_connect" handler were present. now,
@@ -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
@@ -602,6 +603,17 @@ class DefaultDialect(Dialect):
602
603
  """
603
604
  return type_api.adapt_type(typeobj, self.colspecs)
604
605
 
606
+ def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
607
+ """Return the earliest year the database accepts for the given
608
+ date or datetime type.
609
+
610
+ The default implementation returns ``datetime.MINYEAR``.
611
+
612
+ .. versionadded:: 2.0.55
613
+
614
+ """
615
+ return datetime.MINYEAR
616
+
605
617
  def has_index(self, connection, table_name, index_name, schema=None, **kw):
606
618
  if not self.has_table(connection, table_name, schema=schema, **kw):
607
619
  return False
@@ -1883,6 +1895,15 @@ class DefaultExecutionContext(ExecutionContext):
1883
1895
  ):
1884
1896
  self._setup_out_parameters(result)
1885
1897
 
1898
+ if not result._soft_closed and not result._metadata.returns_rows:
1899
+ # no results, get rowcount (which requires open cursor on
1900
+ # some drivers) and release the cursor. this applies to all
1901
+ # statements that don't return rows, including DML, text(),
1902
+ # DDL and SAVEPOINT
1903
+ if self._rowcount is None:
1904
+ self._rowcount = self.cursor.rowcount
1905
+ result._soft_close()
1906
+
1886
1907
  self._soft_closed = result._soft_closed
1887
1908
 
1888
1909
  if yp:
@@ -2021,12 +2042,6 @@ class DefaultExecutionContext(ExecutionContext):
2021
2042
  # the rows have all been fetched however.
2022
2043
  assert result._metadata.returns_rows
2023
2044
 
2024
- elif not result._metadata.returns_rows:
2025
- # no results, get rowcount
2026
- # (which requires open cursor on some drivers)
2027
- if self._rowcount is None:
2028
- self._rowcount = self.cursor.rowcount
2029
- result._soft_close()
2030
2045
  elif self.isupdate or self.isdelete:
2031
2046
  if self._rowcount is None:
2032
2047
  self._rowcount = self.cursor.rowcount
@@ -1304,6 +1304,23 @@ class Dialect(EventTarget):
1304
1304
 
1305
1305
  raise NotImplementedError()
1306
1306
 
1307
+ def min_year_for_type(self, typeobj: TypeEngine[Any]) -> int:
1308
+ """Return the earliest year the database accepts for the given
1309
+ date or datetime type.
1310
+
1311
+ The type may be a generic type such as :class:`.DateTime`, in
1312
+ which case the limit is that of the database type the dialect
1313
+ renders for it. Dialects whose date or datetime types don't
1314
+ accept all years from ``datetime.MINYEAR`` forward override this
1315
+ method; e.g. SQL Server's ``DATETIME`` type, which the dialect
1316
+ renders for :class:`.DateTime`, accepts years from 1753 forward.
1317
+
1318
+ .. versionadded:: 2.0.55
1319
+
1320
+ """
1321
+
1322
+ raise NotImplementedError()
1323
+
1307
1324
  def initialize(self, connection: Connection) -> None:
1308
1325
  """Called during strategized creation of the dialect with a
1309
1326
  connection.
@@ -3261,7 +3278,8 @@ class ExceptionContext:
3261
3278
 
3262
3279
  """
3263
3280
 
3264
- __slots__ = ()
3281
+ if not TYPE_CHECKING:
3282
+ __slots__ = ()
3265
3283
 
3266
3284
  dialect: Dialect
3267
3285
  """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
@@ -23,6 +22,7 @@ from typing import TypeVar
23
22
  from typing import Union
24
23
 
25
24
  from . import exc as async_exc
25
+ from .base import _run_to_completion
26
26
  from .base import asyncstartablecontext
27
27
  from .base import GeneratorStartableContext
28
28
  from .base import ProxyComparable
@@ -597,8 +597,7 @@ class AsyncConnection( # type: ignore[misc]
597
597
  except GeneratorExit:
598
598
  pass
599
599
  else:
600
- task = asyncio.create_task(ar.close())
601
- await asyncio.shield(task)
600
+ await _run_to_completion(ar.close())
602
601
 
603
602
  @overload
604
603
  async def execute(
@@ -893,8 +892,7 @@ class AsyncConnection( # type: ignore[misc]
893
892
  return self.start().__await__()
894
893
 
895
894
  async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
896
- task = asyncio.create_task(self.close())
897
- await asyncio.shield(task)
895
+ await _run_to_completion(self.close())
898
896
 
899
897
  # START PROXY METHODS AsyncConnection
900
898
 
@@ -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
@@ -26,6 +25,7 @@ from typing import TypeVar
26
25
  from typing import Union
27
26
 
28
27
  from . import engine
28
+ from .base import _run_to_completion
29
29
  from .base import ReversibleProxy
30
30
  from .base import StartableContext
31
31
  from .result import _ensure_sync_result
@@ -1067,8 +1067,7 @@ class AsyncSession(ReversibleProxy[Session]):
1067
1067
  return self
1068
1068
 
1069
1069
  async def __aexit__(self, type_: Any, value: Any, traceback: Any) -> None:
1070
- task = asyncio.create_task(self.close())
1071
- await asyncio.shield(task)
1070
+ await _run_to_completion(self.close())
1072
1071
 
1073
1072
  def _maker_context_manager(self: _AS) -> _AsyncSessionContextManager[_AS]:
1074
1073
  return _AsyncSessionContextManager(self)
@@ -1800,8 +1799,7 @@ class _AsyncSessionContextManager(Generic[_AS]):
1800
1799
  await self.trans.__aexit__(type_, value, traceback)
1801
1800
  await self.async_session.__aexit__(type_, value, traceback)
1802
1801
 
1803
- task = asyncio.create_task(go())
1804
- await asyncio.shield(task)
1802
+ await _run_to_completion(go())
1805
1803
 
1806
1804
 
1807
1805
  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
  )
sqlalchemy/ext/mutable.py CHANGED
@@ -844,6 +844,12 @@ class MutableDict(Mutable, Dict[_KT, _VT]):
844
844
  dict.update(self, *a, **kw)
845
845
  self.changed()
846
846
 
847
+ def __ior__( # type: ignore[override,misc]
848
+ self, other: Any
849
+ ) -> MutableDict[_KT, _VT]:
850
+ self.update(other)
851
+ return self
852
+
847
853
  if TYPE_CHECKING:
848
854
 
849
855
  @overload
@@ -953,6 +959,11 @@ class MutableList(Mutable, List[_T]):
953
959
  self.extend(x)
954
960
  return self
955
961
 
962
+ def __imul__(self, n: SupportsIndex) -> MutableList[_T]:
963
+ list.__imul__(self, n)
964
+ self.changed()
965
+ return self
966
+
956
967
  def insert(self, i: SupportsIndex, x: _T) -> None:
957
968
  list.insert(self, i, x)
958
969
  self.changed()
@@ -1916,9 +1916,11 @@ class BulkORMUpdate(BulkUDCompileState, UpdateDMLState):
1916
1916
  # attributes that were formerly modified instead get expired.
1917
1917
  # this only gets hit if the session had pending changes
1918
1918
  # and autoflush were set to False.
1919
- to_expire = attrib.intersection(dict_).difference(to_evaluate)
1920
- if to_expire:
1921
- state._expire_attributes(dict_, to_expire)
1919
+ formerly_modified = attrib.intersection(dict_).difference(
1920
+ to_evaluate
1921
+ )
1922
+ if formerly_modified:
1923
+ state._expire_attributes(dict_, formerly_modified)
1922
1924
 
1923
1925
  states.add(state)
1924
1926
  session._register_altered(states)
@@ -356,6 +356,7 @@ class declared_attr(interfaces._MappedAttribute[_T_co], _declared_attr_common):
356
356
  user_id: Mapped[int] = mapped_column(ForeignKey("user_table.id"))
357
357
 
358
358
  @declared_attr
359
+ @classmethod
359
360
  def user(cls) -> Mapped["User"]:
360
361
  return relationship("User")
361
362
 
@@ -366,6 +367,7 @@ class declared_attr(interfaces._MappedAttribute[_T_co], _declared_attr_common):
366
367
 
367
368
  class CreateTableName:
368
369
  @declared_attr.directive
370
+ @classmethod
369
371
  def __tablename__(cls) -> str:
370
372
  return cls.__name__.lower()
371
373
 
@@ -383,6 +385,7 @@ class declared_attr(interfaces._MappedAttribute[_T_co], _declared_attr_common):
383
385
  type: Mapped[str] = mapped_column(String(50))
384
386
 
385
387
  @declared_attr.directive
388
+ @classmethod
386
389
  def __mapper_args__(cls) -> Dict[str, Any]:
387
390
  if cls.__name__ == "Employee":
388
391
  return {
@@ -396,11 +399,8 @@ class declared_attr(interfaces._MappedAttribute[_T_co], _declared_attr_common):
396
399
  class Engineer(Employee):
397
400
  pass
398
401
 
399
- :class:`_orm.declared_attr` supports decorating functions that are
400
- explicitly decorated with ``@classmethod``. This is never necessary from a
401
- runtime perspective, however may be needed in order to support :pep:`484`
402
- typing tools that don't otherwise recognize the decorated function as
403
- having class-level behaviors for the ``cls`` parameter::
402
+ :class:`_orm.declared_attr` can also be applied to non-directive
403
+ attributes such as :func:`_orm.column_property`::
404
404
 
405
405
  class SomethingMixin:
406
406
  x: Mapped[int]
@@ -411,6 +411,18 @@ class declared_attr(interfaces._MappedAttribute[_T_co], _declared_attr_common):
411
411
  def x_plus_y(cls) -> Mapped[int]:
412
412
  return column_property(cls.x + cls.y)
413
413
 
414
+ .. tip::
415
+
416
+ The examples above also apply the ``@classmethod`` decorator. This is
417
+ not required at runtime, however :pep:`484` typing tools have no way to
418
+ recognize ``def foo(cls)`` as a classmethod unless ``@classmethod`` is
419
+ present. Adding ``@classmethod`` allows typing tools to correctly
420
+ infer the ``cls`` parameter as the class, which is important when
421
+ accessing mapped attributes such as ``cls.ph1`` inside the body of
422
+ the method. :class:`_orm.declared_attr` supports both
423
+ ``@declared_attr`` and ``@declared_attr.directive`` being combined
424
+ with ``@classmethod``.
425
+
414
426
  .. versionadded:: 2.0 - :class:`_orm.declared_attr` can accommodate a
415
427
  function decorated with ``@classmethod`` to help with :pep:`484`
416
428
  integration where needed.
sqlalchemy/orm/util.py CHANGED
@@ -2360,6 +2360,30 @@ def _cleanup_mapped_str_annotation(
2360
2360
  return annotation
2361
2361
 
2362
2362
 
2363
+ def _unresolved_annotation_is_mapped(
2364
+ raw_annotation: _AnnotationScanType,
2365
+ ) -> bool:
2366
+ """given an un-resolvable annotation, guess if its outermost element
2367
+ is ``Mapped[]``.
2368
+
2369
+ Used only to decide whether a ``NameError`` raised while de-stringifying
2370
+ is worth reporting as a mapping error. The annotation may be a plain
2371
+ string (``__future__`` annotations / explicitly quoted), a ``ForwardRef``
2372
+ (:pep:`649` deferred annotations on Python 3.14 and above), or an
2373
+ already-resolved object.
2374
+
2375
+ """
2376
+ if isinstance(raw_annotation, str):
2377
+ return "Mapped[" in raw_annotation
2378
+ elif isinstance(raw_annotation, typing.ForwardRef):
2379
+ return "Mapped[" in raw_annotation.__forward_arg__
2380
+ else:
2381
+ origin = typing.get_origin(raw_annotation)
2382
+ return isinstance(origin, type) and issubclass(
2383
+ origin, _MappedAnnotationBase
2384
+ )
2385
+
2386
+
2363
2387
  def _extract_mapped_subtype(
2364
2388
  raw_annotation: Optional[_AnnotationScanType],
2365
2389
  cls: type,
@@ -2406,7 +2430,7 @@ def _extract_mapped_subtype(
2406
2430
  "module level. See chained stack trace for more hints."
2407
2431
  ) from ce
2408
2432
  except NameError as ne:
2409
- if raiseerr and "Mapped[" in raw_annotation: # type: ignore
2433
+ if raiseerr and _unresolved_annotation_is_mapped(raw_annotation):
2410
2434
  raise orm_exc.MappedAnnotationError(
2411
2435
  f"Could not interpret annotation {raw_annotation}. "
2412
2436
  "Check that it uses names that are correctly imported at the "