SQLAlchemy 2.0.52__py3-none-any.whl → 2.0.53__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 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.52"
272
+ __version__ = "2.0.53"
273
273
 
274
274
 
275
275
  def __go(lcls: Any) -> None:
@@ -3780,7 +3780,6 @@ order by
3780
3780
  elif coltype in (
3781
3781
  MSString,
3782
3782
  MSChar,
3783
- MSText,
3784
3783
  ):
3785
3784
  kwargs["length"] = maxlen if maxlen != -1 else None
3786
3785
  if collation:
@@ -3788,11 +3787,20 @@ order by
3788
3787
  elif coltype in (
3789
3788
  MSNVarchar,
3790
3789
  MSNChar,
3791
- MSNText,
3792
3790
  ):
3793
3791
  kwargs["length"] = maxlen // 2 if maxlen != -1 else None
3794
3792
  if collation:
3795
3793
  kwargs["collation"] = collation
3794
+ elif coltype in (
3795
+ MSText,
3796
+ MSNText,
3797
+ ):
3798
+ # TEXT / NTEXT are unlengthed LOB types.
3799
+ # sys.columns.max_length reports 16 for these, which is the
3800
+ # size of the in-row LOB pointer and not a character length,
3801
+ # so no length is applied.
3802
+ if collation:
3803
+ kwargs["collation"] = collation
3796
3804
 
3797
3805
  if coltype is None:
3798
3806
  if base_type is not None and base_type != type_:
@@ -2105,8 +2105,8 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
2105
2105
  "SUBPARTITION_BY",
2106
2106
  ]
2107
2107
 
2108
- nonpart_options = set(opts).difference(partition_options)
2109
- part_options = set(opts).intersection(partition_options)
2108
+ nonpart_options = util.OrderedSet(opts).difference(partition_options)
2109
+ part_options = util.OrderedSet(opts).intersection(partition_options)
2110
2110
 
2111
2111
  for opt in topological.sort(
2112
2112
  [
@@ -602,6 +602,7 @@ from ... import pool
602
602
  from ...connectors.asyncio import AsyncAdapt_dbapi_connection
603
603
  from ...connectors.asyncio import AsyncAdapt_dbapi_cursor
604
604
  from ...connectors.asyncio import AsyncAdapt_dbapi_ss_cursor
605
+ from ...connectors.asyncio import AsyncAdapt_terminate
605
606
  from ...connectors.asyncio import AsyncAdaptFallback_dbapi_connection
606
607
  from ...engine import default
607
608
  from ...util import asbool
@@ -795,7 +796,9 @@ class AsyncAdapt_oracledb_ss_cursor(
795
796
  self._cursor = None # type: ignore
796
797
 
797
798
 
798
- class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
799
+ class AsyncAdapt_oracledb_connection(
800
+ AsyncAdapt_terminate, AsyncAdapt_dbapi_connection
801
+ ):
799
802
  _connection: AsyncConnection
800
803
  __slots__ = ()
801
804
 
@@ -860,6 +863,12 @@ class AsyncAdapt_oracledb_connection(AsyncAdapt_dbapi_connection):
860
863
  def tpc_rollback(self, *args: Any, **kwargs: Any) -> Any:
861
864
  return self.await_(self._connection.tpc_rollback(*args, **kwargs))
862
865
 
866
+ async def _terminate_graceful_close(self) -> None:
867
+ await self._connection.close()
868
+
869
+ def _terminate_force_close(self) -> None:
870
+ self._connection.terminate()
871
+
863
872
 
864
873
  class AsyncAdaptFallback_oracledb_connection(
865
874
  AsyncAdaptFallback_dbapi_connection, AsyncAdapt_oracledb_connection
@@ -918,6 +927,11 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
918
927
 
919
928
  _min_version = (2,)
920
929
 
930
+ def __init__(self, **kwargs):
931
+ super().__init__(**kwargs)
932
+
933
+ self.has_terminate = self.oracledb_ver >= (26, 0, 0)
934
+
921
935
  # thick_mode mode is not supported by asyncio, oracledb will raise
922
936
  @classmethod
923
937
  def import_dbapi(cls):
@@ -937,6 +951,12 @@ class OracleDialectAsync_oracledb(OracleDialect_oracledb):
937
951
  def get_driver_connection(self, connection):
938
952
  return connection._connection
939
953
 
954
+ def do_terminate(self, dbapi_connection) -> None:
955
+ if self.has_terminate:
956
+ dbapi_connection.terminate()
957
+ else:
958
+ super().do_terminate(dbapi_connection)
959
+
940
960
 
941
961
  dialect = OracleDialect_oracledb
942
962
  dialect_async = OracleDialectAsync_oracledb
@@ -3754,7 +3754,11 @@ class PGDialect(default.DefaultDialect):
3754
3754
  def get_schema_names(self, connection, **kw):
3755
3755
  query = (
3756
3756
  select(pg_catalog.pg_namespace.c.nspname)
3757
- .where(pg_catalog.pg_namespace.c.nspname.not_like("pg_%"))
3757
+ .where(
3758
+ ~pg_catalog.pg_namespace.c.nspname.startswith(
3759
+ "pg_", autoescape=True
3760
+ )
3761
+ )
3758
3762
  .order_by(pg_catalog.pg_namespace.c.nspname)
3759
3763
  )
3760
3764
  return connection.scalars(query).all()
@@ -949,6 +949,15 @@ dialect in conjunction with the :class:`_schema.Table` construct:
949
949
 
950
950
  .. versionadded:: 2.0.37
951
951
 
952
+ Both options are also reflected, so that a :class:`_schema.Table` which is
953
+ autoloaded from a database that was created using either keyword will render
954
+ that keyword again when the table is recreated. The reflected values are
955
+ also available directly from
956
+ :meth:`_engine.Inspector.get_table_options`.
957
+
958
+ .. versionadded:: 2.0.53 Added reflection support for the ``WITHOUT ROWID``
959
+ and ``STRICT`` table options.
960
+
952
961
  .. seealso::
953
962
 
954
963
  `SQLite CREATE TABLE options
@@ -2067,6 +2076,37 @@ class SQLiteExecutionContext(default.DefaultExecutionContext):
2067
2076
  return colname, None
2068
2077
 
2069
2078
 
2079
+ # regexp that locates a FOREIGN KEY clause within the verbatim CREATE TABLE
2080
+ # text that sqlite stores in sqlite_master. the referred-columns group
2081
+ # requires a non-empty separator between column tokens; an earlier form made
2082
+ # the separator optional, which turned the repeat into a nested quantifier and
2083
+ # let a long word run backtrack exponentially. kept at module level so it can
2084
+ # be exercised directly from the tests.
2085
+ FK_PATTERN = re.compile(
2086
+ r'(?:CONSTRAINT\s+(?:"(.+?)"|(\w+))\s+)?'
2087
+ r"FOREIGN\s+KEY\s*\(\s*(.+?)\s*\)\s+"
2088
+ r'REFERENCES\s+(?:(?:"(.+?)")|([a-z0-9_]+))\s*\(\s*((?:"[^"]+"|[a-z0-9_]+)(?:(?:\s*,\s*|\s+)(?:"[^"]+"|[a-z0-9_]+))*\s*)\)\s*' # noqa: E501
2089
+ r"((?:ON\s+(?:DELETE|UPDATE)\s+"
2090
+ r"(?:SET\s+NULL|SET\s+DEFAULT|CASCADE|RESTRICT|"
2091
+ r"NO\s+ACTION)\s*)*)"
2092
+ r"((?:NOT\s+)?DEFERRABLE)?"
2093
+ r"(?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))?",
2094
+ re.I,
2095
+ )
2096
+
2097
+ # regexp that locates the table option keywords which may trail the closing
2098
+ # paren of the column list in the verbatim CREATE TABLE text. the match is
2099
+ # anchored at the end of the statement, which makes the closing paren
2100
+ # unambiguous, as neither keyword can itself contain one. the keywords may
2101
+ # be given in either order; as each may appear only once, the repeated group
2102
+ # leaves one named group per keyword holding the text that was matched.
2103
+ TABLE_OPTIONS_PATTERN = re.compile(
2104
+ r"\)(?:\s*,?\s*(?:(?P<without_rowid>WITHOUT\s+ROWID)"
2105
+ r"|(?P<strict>STRICT)))*\s*$",
2106
+ re.I,
2107
+ )
2108
+
2109
+
2070
2110
  class SQLiteDialect(default.DefaultDialect):
2071
2111
  name = "sqlite"
2072
2112
  supports_alter = False
@@ -2381,6 +2421,29 @@ class SQLiteDialect(default.DefaultDialect):
2381
2421
  f"{schema}.{view_name}" if schema else view_name
2382
2422
  )
2383
2423
 
2424
+ @reflection.cache
2425
+ def get_table_options(self, connection, table_name, schema=None, **kw):
2426
+ tablesql = self._get_table_sql(
2427
+ connection, table_name, schema=schema, **kw
2428
+ )
2429
+
2430
+ options = {}
2431
+
2432
+ # tablesql is None for the internal sqlite_ tables, which have no
2433
+ # entry in sqlite_master
2434
+ if tablesql is not None:
2435
+ match = TABLE_OPTIONS_PATTERN.search(tablesql.strip())
2436
+ if match:
2437
+ if match.group("without_rowid"):
2438
+ options["sqlite_with_rowid"] = False
2439
+ if match.group("strict"):
2440
+ options["sqlite_strict"] = True
2441
+
2442
+ if options:
2443
+ return options
2444
+ else:
2445
+ return ReflectionDefaults.table_options()
2446
+
2384
2447
  @reflection.cache
2385
2448
  def get_columns(self, connection, table_name, schema=None, **kw):
2386
2449
  pragma = "table_info"
@@ -2549,7 +2612,7 @@ class SQLiteDialect(default.DefaultDialect):
2549
2612
  constraint_name = None
2550
2613
  table_data = self._get_table_sql(connection, table_name, schema=schema)
2551
2614
  if table_data:
2552
- PK_PATTERN = r'CONSTRAINT +(?:"(.+?)"|(\w+)) +PRIMARY KEY'
2615
+ PK_PATTERN = r'CONSTRAINT\s+(?:"(.+?)"|(\w+))\s+PRIMARY\s+KEY'
2553
2616
  result = re.search(PK_PATTERN, table_data, re.I)
2554
2617
  if result:
2555
2618
  constraint_name = result.group(1) or result.group(2)
@@ -2653,16 +2716,7 @@ class SQLiteDialect(default.DefaultDialect):
2653
2716
  # FKs, namely the name of the constraint and other options.
2654
2717
  # so parsing the columns is really about matching it up to what
2655
2718
  # we already have.
2656
- FK_PATTERN = (
2657
- r'(?:CONSTRAINT +(?:"(.+?)"|(\w+)) +)?'
2658
- r"FOREIGN KEY *\( *(.+?) *\) +"
2659
- r'REFERENCES +(?:(?:"(.+?)")|([a-z0-9_]+)) *\( *((?:(?:"[^"]+"|[a-z0-9_]+) *(?:, *)?)+)\) *' # noqa: E501
2660
- r"((?:ON (?:DELETE|UPDATE) "
2661
- r"(?:SET NULL|SET DEFAULT|CASCADE|RESTRICT|NO ACTION) *)*)"
2662
- r"((?:NOT +)?DEFERRABLE)?"
2663
- r"(?: +INITIALLY +(DEFERRED|IMMEDIATE))?"
2664
- )
2665
- for match in re.finditer(FK_PATTERN, table_data, re.I):
2719
+ for match in FK_PATTERN.finditer(table_data):
2666
2720
  (
2667
2721
  constraint_quoted_name,
2668
2722
  constraint_name,
@@ -2687,7 +2741,13 @@ class SQLiteDialect(default.DefaultDialect):
2687
2741
  referred_name = referred_quoted_name or referred_name
2688
2742
  options = {}
2689
2743
 
2690
- for token in re.split(r" *\bON\b *", onupdatedelete.upper()):
2744
+ # a newline may separate the words of an
2745
+ # ON DELETE / ON UPDATE clause; normalize to single
2746
+ # spaces so the tokens below compare correctly
2747
+ onupdatedelete = re.sub(
2748
+ r"\s+", " ", onupdatedelete.upper()
2749
+ ).strip()
2750
+ for token in re.split(r" *\bON\b *", onupdatedelete):
2691
2751
  if token.startswith("DELETE"):
2692
2752
  ondelete = token[6:].strip()
2693
2753
  if ondelete and ondelete != "NO ACTION":
@@ -2770,7 +2830,7 @@ class SQLiteDialect(default.DefaultDialect):
2770
2830
  if table_data is None:
2771
2831
  return
2772
2832
  UNIQUE_PATTERN = (
2773
- r'(?:CONSTRAINT +(?:"(.+?)"|(\w+)) +)?UNIQUE *\((.+?)\)'
2833
+ r'(?:CONSTRAINT\s+(?:"(.+?)"|(\w+))\s+)?UNIQUE\s*\((.+?)\)'
2774
2834
  )
2775
2835
  INLINE_UNIQUE_PATTERN = (
2776
2836
  r'(?:(".+?")|(?:[\[`])?([a-z0-9_]+)(?:[\]`])?)[\t ]'
@@ -1758,6 +1758,15 @@ class Inspector(inspection.Inspectable["Inspector"]):
1758
1758
  ):
1759
1759
  continue
1760
1760
 
1761
+ if len(set(constrained_columns)) != len(constrained_columns):
1762
+ util.warn(
1763
+ f"On reflected table {table.name}, skipping reflection of "
1764
+ f"foreign key constraint {conname}; duplicate source "
1765
+ f"columns within name(s) {', '.join(constrained_columns)} "
1766
+ "are not supported"
1767
+ )
1768
+ continue
1769
+
1761
1770
  referred_schema = fkey_d["referred_schema"]
1762
1771
  referred_table = fkey_d["referred_table"]
1763
1772
  referred_columns = fkey_d["referred_columns"]
@@ -902,7 +902,7 @@ class AsyncConnection( # type: ignore[misc]
902
902
  # statically generated** by tools/generate_proxy_methods.py
903
903
 
904
904
  @property
905
- def closed(self) -> Any:
905
+ def closed(self) -> bool:
906
906
  r"""Return True if this connection is closed.
907
907
 
908
908
  .. container:: class_bases
@@ -915,7 +915,7 @@ class AsyncConnection( # type: ignore[misc]
915
915
  return self._proxied.closed
916
916
 
917
917
  @property
918
- def invalidated(self) -> Any:
918
+ def invalidated(self) -> bool:
919
919
  r"""Return True if this connection was invalidated.
920
920
 
921
921
  .. container:: class_bases
@@ -945,7 +945,7 @@ class AsyncConnection( # type: ignore[misc]
945
945
  self._proxied.dialect = attr
946
946
 
947
947
  @property
948
- def default_isolation_level(self) -> Any:
948
+ def default_isolation_level(self) -> Optional[IsolationLevel]:
949
949
  r"""The initial-connection time isolation level associated with the
950
950
  :class:`_engine.Dialect` in use.
951
951
 
@@ -1258,7 +1258,7 @@ class AsyncEngine(ProxyComparable[Engine], AsyncConnectable): # type: ignore[mi
1258
1258
  self._proxied.dialect = attr
1259
1259
 
1260
1260
  @property
1261
- def engine(self) -> Any:
1261
+ def engine(self) -> Engine:
1262
1262
  r"""Returns this :class:`.Engine`.
1263
1263
 
1264
1264
  .. container:: class_bases
@@ -1275,7 +1275,7 @@ class AsyncEngine(ProxyComparable[Engine], AsyncConnectable): # type: ignore[mi
1275
1275
  return self._proxied.engine
1276
1276
 
1277
1277
  @property
1278
- def name(self) -> Any:
1278
+ def name(self) -> str:
1279
1279
  r"""String name of the :class:`~sqlalchemy.engine.interfaces.Dialect`
1280
1280
  in use by this :class:`Engine`.
1281
1281
 
@@ -1290,7 +1290,7 @@ class AsyncEngine(ProxyComparable[Engine], AsyncConnectable): # type: ignore[mi
1290
1290
  return self._proxied.name
1291
1291
 
1292
1292
  @property
1293
- def driver(self) -> Any:
1293
+ def driver(self) -> str:
1294
1294
  r"""Driver name of the :class:`~sqlalchemy.engine.interfaces.Dialect`
1295
1295
  in use by this :class:`Engine`.
1296
1296
 
@@ -48,15 +48,18 @@ if TYPE_CHECKING:
48
48
  from ...orm._typing import _IdentityKeyType
49
49
  from ...orm._typing import _O
50
50
  from ...orm._typing import OrmExecuteOptionsParameter
51
+ from ...orm.identity import IdentityMap
51
52
  from ...orm.interfaces import ORMOption
52
53
  from ...orm.session import _BindArguments
53
54
  from ...orm.session import _EntityBindKey
54
55
  from ...orm.session import _PKIdentityArgument
55
56
  from ...orm.session import _SessionBind
57
+ from ...sql._typing import _InfoType
56
58
  from ...sql.base import Executable
57
59
  from ...sql.elements import ClauseElement
58
60
  from ...sql.selectable import ForUpdateParameter
59
61
  from ...sql.selectable import TypedReturnsRows
62
+ from ...util import IdentitySet
60
63
 
61
64
  _T = TypeVar("_T", bound=Any)
62
65
 
@@ -1305,7 +1308,7 @@ class async_scoped_session(Generic[_AS]):
1305
1308
  self._proxied.bind = attr
1306
1309
 
1307
1310
  @property
1308
- def dirty(self) -> Any:
1311
+ def dirty(self) -> IdentitySet:
1309
1312
  r"""The set of all persistent instances considered dirty.
1310
1313
 
1311
1314
  .. container:: class_bases
@@ -1344,7 +1347,7 @@ class async_scoped_session(Generic[_AS]):
1344
1347
  return self._proxied.dirty
1345
1348
 
1346
1349
  @property
1347
- def deleted(self) -> Any:
1350
+ def deleted(self) -> IdentitySet:
1348
1351
  r"""The set of all instances marked as 'deleted' within this ``Session``
1349
1352
 
1350
1353
  .. container:: class_bases
@@ -1363,7 +1366,7 @@ class async_scoped_session(Generic[_AS]):
1363
1366
  return self._proxied.deleted
1364
1367
 
1365
1368
  @property
1366
- def new(self) -> Any:
1369
+ def new(self) -> IdentitySet:
1367
1370
  r"""The set of all instances marked as 'new' within this ``Session``.
1368
1371
 
1369
1372
  .. container:: class_bases
@@ -1382,7 +1385,7 @@ class async_scoped_session(Generic[_AS]):
1382
1385
  return self._proxied.new
1383
1386
 
1384
1387
  @property
1385
- def identity_map(self) -> Any:
1388
+ def identity_map(self) -> IdentityMap:
1386
1389
  r"""Proxy for the :attr:`_orm.Session.identity_map` attribute
1387
1390
  on behalf of the :class:`_asyncio.AsyncSession` class.
1388
1391
 
@@ -1397,11 +1400,11 @@ class async_scoped_session(Generic[_AS]):
1397
1400
  return self._proxied.identity_map
1398
1401
 
1399
1402
  @identity_map.setter
1400
- def identity_map(self, attr: Any) -> None:
1403
+ def identity_map(self, attr: IdentityMap) -> None:
1401
1404
  self._proxied.identity_map = attr
1402
1405
 
1403
1406
  @property
1404
- def is_active(self) -> Any:
1407
+ def is_active(self) -> bool:
1405
1408
  r"""True if this :class:`.Session` not in "partial rollback" state.
1406
1409
 
1407
1410
  .. container:: class_bases
@@ -1444,7 +1447,7 @@ class async_scoped_session(Generic[_AS]):
1444
1447
  return self._proxied.is_active
1445
1448
 
1446
1449
  @property
1447
- def autoflush(self) -> Any:
1450
+ def autoflush(self) -> bool:
1448
1451
  r"""Proxy for the :attr:`_orm.Session.autoflush` attribute
1449
1452
  on behalf of the :class:`_asyncio.AsyncSession` class.
1450
1453
 
@@ -1459,7 +1462,7 @@ class async_scoped_session(Generic[_AS]):
1459
1462
  return self._proxied.autoflush
1460
1463
 
1461
1464
  @autoflush.setter
1462
- def autoflush(self, attr: Any) -> None:
1465
+ def autoflush(self, attr: bool) -> None:
1463
1466
  self._proxied.autoflush = attr
1464
1467
 
1465
1468
  @property
@@ -1498,7 +1501,7 @@ class async_scoped_session(Generic[_AS]):
1498
1501
  return self._proxied.no_autoflush
1499
1502
 
1500
1503
  @property
1501
- def info(self) -> Any:
1504
+ def info(self) -> _InfoType:
1502
1505
  r"""A user-modifiable dictionary.
1503
1506
 
1504
1507
  .. container:: class_bases
@@ -68,6 +68,7 @@ if TYPE_CHECKING:
68
68
  from ...sql.elements import ClauseElement
69
69
  from ...sql.selectable import ForUpdateParameter
70
70
  from ...sql.selectable import TypedReturnsRows
71
+ from ...util import IdentitySet
71
72
 
72
73
  _AsyncSessionBind = Union["AsyncEngine", "AsyncConnection"]
73
74
 
@@ -1385,7 +1386,7 @@ class AsyncSession(ReversibleProxy[Session]):
1385
1386
  return self._proxied.in_nested_transaction()
1386
1387
 
1387
1388
  @property
1388
- def dirty(self) -> Any:
1389
+ def dirty(self) -> IdentitySet:
1389
1390
  r"""The set of all persistent instances considered dirty.
1390
1391
 
1391
1392
  .. container:: class_bases
@@ -1418,7 +1419,7 @@ class AsyncSession(ReversibleProxy[Session]):
1418
1419
  return self._proxied.dirty
1419
1420
 
1420
1421
  @property
1421
- def deleted(self) -> Any:
1422
+ def deleted(self) -> IdentitySet:
1422
1423
  r"""The set of all instances marked as 'deleted' within this ``Session``
1423
1424
 
1424
1425
  .. container:: class_bases
@@ -1431,7 +1432,7 @@ class AsyncSession(ReversibleProxy[Session]):
1431
1432
  return self._proxied.deleted
1432
1433
 
1433
1434
  @property
1434
- def new(self) -> Any:
1435
+ def new(self) -> IdentitySet:
1435
1436
  r"""The set of all instances marked as 'new' within this ``Session``.
1436
1437
 
1437
1438
  .. container:: class_bases
@@ -1457,7 +1458,7 @@ class AsyncSession(ReversibleProxy[Session]):
1457
1458
  self._proxied.identity_map = attr
1458
1459
 
1459
1460
  @property
1460
- def is_active(self) -> Any:
1461
+ def is_active(self) -> bool:
1461
1462
  r"""True if this :class:`.Session` not in "partial rollback" state.
1462
1463
 
1463
1464
  .. container:: class_bases
@@ -1536,7 +1537,7 @@ class AsyncSession(ReversibleProxy[Session]):
1536
1537
  return self._proxied.no_autoflush
1537
1538
 
1538
1539
  @property
1539
- def info(self) -> Any:
1540
+ def info(self) -> _InfoType:
1540
1541
  r"""A user-modifiable dictionary.
1541
1542
 
1542
1543
  .. container:: class_bases
@@ -3115,10 +3115,12 @@ class JoinCondition:
3115
3115
  secondary_sync_pairs
3116
3116
  )
3117
3117
 
3118
+ # ensure all references are weak so that no self-referencing cycles
3119
+ # may form
3118
3120
  _track_overlapping_sync_targets: weakref.WeakKeyDictionary[
3119
3121
  ColumnElement[Any],
3120
3122
  weakref.WeakKeyDictionary[
3121
- RelationshipProperty[Any], ColumnElement[Any]
3123
+ RelationshipProperty[Any], weakref.ref[ColumnElement[Any]]
3122
3124
  ],
3123
3125
  ] = weakref.WeakKeyDictionary()
3124
3126
 
@@ -3145,13 +3147,16 @@ class JoinCondition:
3145
3147
 
3146
3148
  if to_ not in self._track_overlapping_sync_targets:
3147
3149
  self._track_overlapping_sync_targets[to_] = (
3148
- weakref.WeakKeyDictionary({self.prop: from_})
3150
+ weakref.WeakKeyDictionary({self.prop: weakref.ref(from_)})
3149
3151
  )
3150
3152
  else:
3151
3153
  other_props = []
3152
3154
  prop_to_from = self._track_overlapping_sync_targets[to_]
3153
3155
 
3154
- for pr, fr_ in prop_to_from.items():
3156
+ for pr, fr_ref in prop_to_from.items():
3157
+ fr_ = fr_ref()
3158
+ if fr_ is None:
3159
+ continue
3155
3160
  if (
3156
3161
  not pr.mapper._dispose_called
3157
3162
  and pr not in self.prop._reverse_property
@@ -3206,7 +3211,9 @@ class JoinCondition:
3206
3211
  ),
3207
3212
  code="qzyx",
3208
3213
  )
3209
- self._track_overlapping_sync_targets[to_][self.prop] = from_
3214
+ self._track_overlapping_sync_targets[to_][self.prop] = (
3215
+ weakref.ref(from_)
3216
+ )
3210
3217
 
3211
3218
  @util.memoized_property
3212
3219
  def remote_columns(self) -> Set[ColumnElement[Any]]:
sqlalchemy/orm/scoping.py CHANGED
@@ -57,6 +57,7 @@ if TYPE_CHECKING:
57
57
  from ..engine.interfaces import CoreExecuteOptionsParameter
58
58
  from ..engine.result import ScalarResult
59
59
  from ..sql._typing import _ColumnsClauseArgument
60
+ from ..sql._typing import _InfoType
60
61
  from ..sql._typing import _T0
61
62
  from ..sql._typing import _T1
62
63
  from ..sql._typing import _T2
@@ -71,6 +72,7 @@ if TYPE_CHECKING:
71
72
  from ..sql.roles import TypedColumnsClauseRole
72
73
  from ..sql.selectable import ForUpdateParameter
73
74
  from ..sql.selectable import TypedReturnsRows
75
+ from ..util import IdentitySet
74
76
 
75
77
  _T = TypeVar("_T", bound=Any)
76
78
 
@@ -1909,7 +1911,7 @@ class scoped_session(Generic[_S]):
1909
1911
  self._proxied.bind = attr
1910
1912
 
1911
1913
  @property
1912
- def dirty(self) -> Any:
1914
+ def dirty(self) -> IdentitySet:
1913
1915
  r"""The set of all persistent instances considered dirty.
1914
1916
 
1915
1917
  .. container:: class_bases
@@ -1942,7 +1944,7 @@ class scoped_session(Generic[_S]):
1942
1944
  return self._proxied.dirty
1943
1945
 
1944
1946
  @property
1945
- def deleted(self) -> Any:
1947
+ def deleted(self) -> IdentitySet:
1946
1948
  r"""The set of all instances marked as 'deleted' within this ``Session``
1947
1949
 
1948
1950
  .. container:: class_bases
@@ -1955,7 +1957,7 @@ class scoped_session(Generic[_S]):
1955
1957
  return self._proxied.deleted
1956
1958
 
1957
1959
  @property
1958
- def new(self) -> Any:
1960
+ def new(self) -> IdentitySet:
1959
1961
  r"""The set of all instances marked as 'new' within this ``Session``.
1960
1962
 
1961
1963
  .. container:: class_bases
@@ -1981,7 +1983,7 @@ class scoped_session(Generic[_S]):
1981
1983
  self._proxied.identity_map = attr
1982
1984
 
1983
1985
  @property
1984
- def is_active(self) -> Any:
1986
+ def is_active(self) -> bool:
1985
1987
  r"""True if this :class:`.Session` not in "partial rollback" state.
1986
1988
 
1987
1989
  .. container:: class_bases
@@ -2060,7 +2062,7 @@ class scoped_session(Generic[_S]):
2060
2062
  return self._proxied.no_autoflush
2061
2063
 
2062
2064
  @property
2063
- def info(self) -> Any:
2065
+ def info(self) -> _InfoType:
2064
2066
  r"""A user-modifiable dictionary.
2065
2067
 
2066
2068
  .. container:: class_bases
@@ -67,6 +67,13 @@ if TYPE_CHECKING:
67
67
  from ..sql.elements import ColumnElement
68
68
 
69
69
 
70
+ # sentinel for _LoadLazyAttribute.extra_criteria indicating that the loader
71
+ # option had no additional criteria. the attribute is otherwise a single
72
+ # element returned by Load._generate_extra_criteria(), which has no
73
+ # meaningful boolean value of its own.
74
+ _NO_EXTRA_CRITERIA = None
75
+
76
+
70
77
  def _register_attribute(
71
78
  prop,
72
79
  mapper,
@@ -1209,7 +1216,7 @@ class LazyLoader(
1209
1216
  (
1210
1217
  loadopt._generate_extra_criteria(context)
1211
1218
  if loadopt._extra_criteria
1212
- else None
1219
+ else _NO_EXTRA_CRITERIA
1213
1220
  ),
1214
1221
  ),
1215
1222
  key,
@@ -1257,7 +1264,7 @@ class LoadLazyAttribute:
1257
1264
  self.extra_criteria = extra_criteria
1258
1265
 
1259
1266
  def __getstate__(self):
1260
- if self.extra_criteria is not None:
1267
+ if self.extra_criteria is not _NO_EXTRA_CRITERIA:
1261
1268
  util.warn(
1262
1269
  "Can't reliably serialize a lazyload() option that "
1263
1270
  "contains additional criteria; please use eager loading "
@@ -1267,7 +1274,7 @@ class LoadLazyAttribute:
1267
1274
  "key": self.key,
1268
1275
  "strategy_key": self.strategy_key,
1269
1276
  "loadopt": self.loadopt,
1270
- "extra_criteria": (),
1277
+ "extra_criteria": _NO_EXTRA_CRITERIA,
1271
1278
  }
1272
1279
 
1273
1280
  def __call__(self, state, passive=attributes.PASSIVE_OFF):