SQLAlchemy 2.0.53__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.
- sqlalchemy/__init__.py +1 -1
- sqlalchemy/dialects/mssql/base.py +28 -0
- sqlalchemy/dialects/mssql/pyodbc.py +6 -9
- sqlalchemy/dialects/oracle/base.py +4 -1
- sqlalchemy/dialects/postgresql/base.py +30 -8
- sqlalchemy/dialects/postgresql/named_types.py +15 -0
- sqlalchemy/engine/base.py +13 -6
- sqlalchemy/engine/characteristics.py +1 -1
- sqlalchemy/engine/create.py +20 -10
- sqlalchemy/engine/default.py +21 -6
- sqlalchemy/engine/interfaces.py +19 -1
- sqlalchemy/ext/asyncio/base.py +31 -0
- sqlalchemy/ext/asyncio/engine.py +3 -5
- sqlalchemy/ext/asyncio/session.py +3 -5
- sqlalchemy/ext/automap.py +8 -8
- sqlalchemy/ext/mutable.py +11 -0
- sqlalchemy/orm/bulk_persistence.py +5 -3
- sqlalchemy/orm/decl_api.py +17 -5
- sqlalchemy/orm/util.py +25 -1
- sqlalchemy/pool/base.py +62 -28
- sqlalchemy/sql/_elements_constructors.py +6 -0
- sqlalchemy/sql/compiler.py +54 -17
- sqlalchemy/sql/dml.py +8 -0
- sqlalchemy/sql/elements.py +7 -1
- sqlalchemy/sql/schema.py +2 -0
- sqlalchemy/sql/selectable.py +4 -2
- sqlalchemy/sql/sqltypes.py +1 -0
- sqlalchemy/sql/type_api.py +53 -24
- sqlalchemy/testing/cancellation.py +272 -73
- sqlalchemy/testing/requirements.py +27 -2
- sqlalchemy/testing/suite/test_types.py +204 -3
- sqlalchemy/util/queue.py +11 -0
- {sqlalchemy-2.0.53.dist-info → sqlalchemy-2.0.55.dist-info}/METADATA +1 -1
- {sqlalchemy-2.0.53.dist-info → sqlalchemy-2.0.55.dist-info}/RECORD +38 -38
- {sqlalchemy-2.0.53.dist-info → sqlalchemy-2.0.55.dist-info}/WHEEL +0 -0
- {sqlalchemy-2.0.53.dist-info → sqlalchemy-2.0.55.dist-info}/licenses/AUTHORS +0 -0
- {sqlalchemy-2.0.53.dist-info → sqlalchemy-2.0.55.dist-info}/licenses/LICENSE +0 -0
- {sqlalchemy-2.0.53.dist-info → sqlalchemy-2.0.55.dist-info}/top_level.txt +0 -0
sqlalchemy/__init__.py
CHANGED
|
@@ -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
|
-
|
|
498
|
-
# offset
|
|
499
|
-
#
|
|
500
|
-
#
|
|
501
|
-
|
|
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
|
-
|
|
4383
|
-
|
|
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
|
-
|
|
4608
|
-
|
|
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
|
-
|
|
4631
|
-
rf"REFERENCES (?:({qtoken})\.)?({qtoken})
|
|
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
|
-
|
|
5249
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
145
|
+
conn._message_formatter = None
|
|
146
146
|
|
|
147
147
|
def get_characteristic(
|
|
148
148
|
self, dialect: Dialect, dbapi_conn: DBAPIConnection
|
sqlalchemy/engine/create.py
CHANGED
|
@@ -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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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,
|
sqlalchemy/engine/default.py
CHANGED
|
@@ -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
|
sqlalchemy/engine/interfaces.py
CHANGED
|
@@ -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
|
-
|
|
3281
|
+
if not TYPE_CHECKING:
|
|
3282
|
+
__slots__ = ()
|
|
3265
3283
|
|
|
3266
3284
|
dialect: Dialect
|
|
3267
3285
|
"""The :class:`_engine.Dialect` in use.
|
sqlalchemy/ext/asyncio/base.py
CHANGED
|
@@ -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]]]
|
sqlalchemy/ext/asyncio/engine.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
),
|
|
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
|
-
),
|
|
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
|
-
),
|
|
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
|
-
),
|
|
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
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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)
|
sqlalchemy/orm/decl_api.py
CHANGED
|
@@ -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`
|
|
400
|
-
|
|
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
|
|
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 "
|