SQLAlchemy 2.0.47__cp313-cp313t-win32.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 (274) hide show
  1. sqlalchemy/__init__.py +283 -0
  2. sqlalchemy/connectors/__init__.py +18 -0
  3. sqlalchemy/connectors/aioodbc.py +184 -0
  4. sqlalchemy/connectors/asyncio.py +429 -0
  5. sqlalchemy/connectors/pyodbc.py +250 -0
  6. sqlalchemy/cyextension/__init__.py +6 -0
  7. sqlalchemy/cyextension/collections.cp313t-win32.pyd +0 -0
  8. sqlalchemy/cyextension/collections.pyx +409 -0
  9. sqlalchemy/cyextension/immutabledict.cp313t-win32.pyd +0 -0
  10. sqlalchemy/cyextension/immutabledict.pxd +8 -0
  11. sqlalchemy/cyextension/immutabledict.pyx +133 -0
  12. sqlalchemy/cyextension/processors.cp313t-win32.pyd +0 -0
  13. sqlalchemy/cyextension/processors.pyx +68 -0
  14. sqlalchemy/cyextension/resultproxy.cp313t-win32.pyd +0 -0
  15. sqlalchemy/cyextension/resultproxy.pyx +102 -0
  16. sqlalchemy/cyextension/util.cp313t-win32.pyd +0 -0
  17. sqlalchemy/cyextension/util.pyx +90 -0
  18. sqlalchemy/dialects/__init__.py +62 -0
  19. sqlalchemy/dialects/_typing.py +30 -0
  20. sqlalchemy/dialects/mssql/__init__.py +88 -0
  21. sqlalchemy/dialects/mssql/aioodbc.py +63 -0
  22. sqlalchemy/dialects/mssql/base.py +4093 -0
  23. sqlalchemy/dialects/mssql/information_schema.py +285 -0
  24. sqlalchemy/dialects/mssql/json.py +129 -0
  25. sqlalchemy/dialects/mssql/provision.py +185 -0
  26. sqlalchemy/dialects/mssql/pymssql.py +126 -0
  27. sqlalchemy/dialects/mssql/pyodbc.py +760 -0
  28. sqlalchemy/dialects/mysql/__init__.py +104 -0
  29. sqlalchemy/dialects/mysql/aiomysql.py +250 -0
  30. sqlalchemy/dialects/mysql/asyncmy.py +231 -0
  31. sqlalchemy/dialects/mysql/base.py +3949 -0
  32. sqlalchemy/dialects/mysql/cymysql.py +106 -0
  33. sqlalchemy/dialects/mysql/dml.py +225 -0
  34. sqlalchemy/dialects/mysql/enumerated.py +282 -0
  35. sqlalchemy/dialects/mysql/expression.py +146 -0
  36. sqlalchemy/dialects/mysql/json.py +91 -0
  37. sqlalchemy/dialects/mysql/mariadb.py +72 -0
  38. sqlalchemy/dialects/mysql/mariadbconnector.py +322 -0
  39. sqlalchemy/dialects/mysql/mysqlconnector.py +302 -0
  40. sqlalchemy/dialects/mysql/mysqldb.py +314 -0
  41. sqlalchemy/dialects/mysql/provision.py +153 -0
  42. sqlalchemy/dialects/mysql/pymysql.py +158 -0
  43. sqlalchemy/dialects/mysql/pyodbc.py +157 -0
  44. sqlalchemy/dialects/mysql/reflection.py +727 -0
  45. sqlalchemy/dialects/mysql/reserved_words.py +570 -0
  46. sqlalchemy/dialects/mysql/types.py +835 -0
  47. sqlalchemy/dialects/oracle/__init__.py +81 -0
  48. sqlalchemy/dialects/oracle/base.py +3802 -0
  49. sqlalchemy/dialects/oracle/cx_oracle.py +1555 -0
  50. sqlalchemy/dialects/oracle/dictionary.py +507 -0
  51. sqlalchemy/dialects/oracle/oracledb.py +941 -0
  52. sqlalchemy/dialects/oracle/provision.py +297 -0
  53. sqlalchemy/dialects/oracle/types.py +316 -0
  54. sqlalchemy/dialects/oracle/vector.py +365 -0
  55. sqlalchemy/dialects/postgresql/__init__.py +167 -0
  56. sqlalchemy/dialects/postgresql/_psycopg_common.py +189 -0
  57. sqlalchemy/dialects/postgresql/array.py +519 -0
  58. sqlalchemy/dialects/postgresql/asyncpg.py +1284 -0
  59. sqlalchemy/dialects/postgresql/base.py +5378 -0
  60. sqlalchemy/dialects/postgresql/dml.py +339 -0
  61. sqlalchemy/dialects/postgresql/ext.py +540 -0
  62. sqlalchemy/dialects/postgresql/hstore.py +406 -0
  63. sqlalchemy/dialects/postgresql/json.py +404 -0
  64. sqlalchemy/dialects/postgresql/named_types.py +524 -0
  65. sqlalchemy/dialects/postgresql/operators.py +129 -0
  66. sqlalchemy/dialects/postgresql/pg8000.py +669 -0
  67. sqlalchemy/dialects/postgresql/pg_catalog.py +326 -0
  68. sqlalchemy/dialects/postgresql/provision.py +183 -0
  69. sqlalchemy/dialects/postgresql/psycopg.py +862 -0
  70. sqlalchemy/dialects/postgresql/psycopg2.py +892 -0
  71. sqlalchemy/dialects/postgresql/psycopg2cffi.py +61 -0
  72. sqlalchemy/dialects/postgresql/ranges.py +1031 -0
  73. sqlalchemy/dialects/postgresql/types.py +313 -0
  74. sqlalchemy/dialects/sqlite/__init__.py +57 -0
  75. sqlalchemy/dialects/sqlite/aiosqlite.py +482 -0
  76. sqlalchemy/dialects/sqlite/base.py +3056 -0
  77. sqlalchemy/dialects/sqlite/dml.py +263 -0
  78. sqlalchemy/dialects/sqlite/json.py +92 -0
  79. sqlalchemy/dialects/sqlite/provision.py +229 -0
  80. sqlalchemy/dialects/sqlite/pysqlcipher.py +157 -0
  81. sqlalchemy/dialects/sqlite/pysqlite.py +756 -0
  82. sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
  83. sqlalchemy/engine/__init__.py +62 -0
  84. sqlalchemy/engine/_py_processors.py +136 -0
  85. sqlalchemy/engine/_py_row.py +128 -0
  86. sqlalchemy/engine/_py_util.py +74 -0
  87. sqlalchemy/engine/base.py +3390 -0
  88. sqlalchemy/engine/characteristics.py +155 -0
  89. sqlalchemy/engine/create.py +893 -0
  90. sqlalchemy/engine/cursor.py +2298 -0
  91. sqlalchemy/engine/default.py +2394 -0
  92. sqlalchemy/engine/events.py +965 -0
  93. sqlalchemy/engine/interfaces.py +3471 -0
  94. sqlalchemy/engine/mock.py +134 -0
  95. sqlalchemy/engine/processors.py +61 -0
  96. sqlalchemy/engine/reflection.py +2102 -0
  97. sqlalchemy/engine/result.py +2399 -0
  98. sqlalchemy/engine/row.py +400 -0
  99. sqlalchemy/engine/strategies.py +16 -0
  100. sqlalchemy/engine/url.py +924 -0
  101. sqlalchemy/engine/util.py +167 -0
  102. sqlalchemy/event/__init__.py +26 -0
  103. sqlalchemy/event/api.py +220 -0
  104. sqlalchemy/event/attr.py +676 -0
  105. sqlalchemy/event/base.py +472 -0
  106. sqlalchemy/event/legacy.py +258 -0
  107. sqlalchemy/event/registry.py +390 -0
  108. sqlalchemy/events.py +17 -0
  109. sqlalchemy/exc.py +832 -0
  110. sqlalchemy/ext/__init__.py +11 -0
  111. sqlalchemy/ext/associationproxy.py +2027 -0
  112. sqlalchemy/ext/asyncio/__init__.py +25 -0
  113. sqlalchemy/ext/asyncio/base.py +281 -0
  114. sqlalchemy/ext/asyncio/engine.py +1471 -0
  115. sqlalchemy/ext/asyncio/exc.py +21 -0
  116. sqlalchemy/ext/asyncio/result.py +965 -0
  117. sqlalchemy/ext/asyncio/scoping.py +1599 -0
  118. sqlalchemy/ext/asyncio/session.py +1947 -0
  119. sqlalchemy/ext/automap.py +1701 -0
  120. sqlalchemy/ext/baked.py +570 -0
  121. sqlalchemy/ext/compiler.py +600 -0
  122. sqlalchemy/ext/declarative/__init__.py +65 -0
  123. sqlalchemy/ext/declarative/extensions.py +564 -0
  124. sqlalchemy/ext/horizontal_shard.py +478 -0
  125. sqlalchemy/ext/hybrid.py +1535 -0
  126. sqlalchemy/ext/indexable.py +364 -0
  127. sqlalchemy/ext/instrumentation.py +450 -0
  128. sqlalchemy/ext/mutable.py +1085 -0
  129. sqlalchemy/ext/mypy/__init__.py +6 -0
  130. sqlalchemy/ext/mypy/apply.py +324 -0
  131. sqlalchemy/ext/mypy/decl_class.py +515 -0
  132. sqlalchemy/ext/mypy/infer.py +590 -0
  133. sqlalchemy/ext/mypy/names.py +335 -0
  134. sqlalchemy/ext/mypy/plugin.py +303 -0
  135. sqlalchemy/ext/mypy/util.py +357 -0
  136. sqlalchemy/ext/orderinglist.py +439 -0
  137. sqlalchemy/ext/serializer.py +185 -0
  138. sqlalchemy/future/__init__.py +16 -0
  139. sqlalchemy/future/engine.py +15 -0
  140. sqlalchemy/inspection.py +174 -0
  141. sqlalchemy/log.py +288 -0
  142. sqlalchemy/orm/__init__.py +171 -0
  143. sqlalchemy/orm/_orm_constructors.py +2661 -0
  144. sqlalchemy/orm/_typing.py +179 -0
  145. sqlalchemy/orm/attributes.py +2845 -0
  146. sqlalchemy/orm/base.py +971 -0
  147. sqlalchemy/orm/bulk_persistence.py +2135 -0
  148. sqlalchemy/orm/clsregistry.py +571 -0
  149. sqlalchemy/orm/collections.py +1627 -0
  150. sqlalchemy/orm/context.py +3334 -0
  151. sqlalchemy/orm/decl_api.py +2004 -0
  152. sqlalchemy/orm/decl_base.py +2192 -0
  153. sqlalchemy/orm/dependency.py +1302 -0
  154. sqlalchemy/orm/descriptor_props.py +1092 -0
  155. sqlalchemy/orm/dynamic.py +300 -0
  156. sqlalchemy/orm/evaluator.py +379 -0
  157. sqlalchemy/orm/events.py +3252 -0
  158. sqlalchemy/orm/exc.py +237 -0
  159. sqlalchemy/orm/identity.py +302 -0
  160. sqlalchemy/orm/instrumentation.py +754 -0
  161. sqlalchemy/orm/interfaces.py +1496 -0
  162. sqlalchemy/orm/loading.py +1686 -0
  163. sqlalchemy/orm/mapped_collection.py +557 -0
  164. sqlalchemy/orm/mapper.py +4444 -0
  165. sqlalchemy/orm/path_registry.py +809 -0
  166. sqlalchemy/orm/persistence.py +1788 -0
  167. sqlalchemy/orm/properties.py +935 -0
  168. sqlalchemy/orm/query.py +3459 -0
  169. sqlalchemy/orm/relationships.py +3508 -0
  170. sqlalchemy/orm/scoping.py +2148 -0
  171. sqlalchemy/orm/session.py +5280 -0
  172. sqlalchemy/orm/state.py +1168 -0
  173. sqlalchemy/orm/state_changes.py +196 -0
  174. sqlalchemy/orm/strategies.py +3470 -0
  175. sqlalchemy/orm/strategy_options.py +2568 -0
  176. sqlalchemy/orm/sync.py +164 -0
  177. sqlalchemy/orm/unitofwork.py +796 -0
  178. sqlalchemy/orm/util.py +2403 -0
  179. sqlalchemy/orm/writeonly.py +674 -0
  180. sqlalchemy/pool/__init__.py +44 -0
  181. sqlalchemy/pool/base.py +1524 -0
  182. sqlalchemy/pool/events.py +375 -0
  183. sqlalchemy/pool/impl.py +588 -0
  184. sqlalchemy/py.typed +0 -0
  185. sqlalchemy/schema.py +69 -0
  186. sqlalchemy/sql/__init__.py +145 -0
  187. sqlalchemy/sql/_dml_constructors.py +132 -0
  188. sqlalchemy/sql/_elements_constructors.py +1872 -0
  189. sqlalchemy/sql/_orm_types.py +20 -0
  190. sqlalchemy/sql/_py_util.py +75 -0
  191. sqlalchemy/sql/_selectable_constructors.py +763 -0
  192. sqlalchemy/sql/_typing.py +482 -0
  193. sqlalchemy/sql/annotation.py +587 -0
  194. sqlalchemy/sql/base.py +2293 -0
  195. sqlalchemy/sql/cache_key.py +1057 -0
  196. sqlalchemy/sql/coercions.py +1404 -0
  197. sqlalchemy/sql/compiler.py +8081 -0
  198. sqlalchemy/sql/crud.py +1752 -0
  199. sqlalchemy/sql/ddl.py +1444 -0
  200. sqlalchemy/sql/default_comparator.py +551 -0
  201. sqlalchemy/sql/dml.py +1850 -0
  202. sqlalchemy/sql/elements.py +5589 -0
  203. sqlalchemy/sql/events.py +458 -0
  204. sqlalchemy/sql/expression.py +159 -0
  205. sqlalchemy/sql/functions.py +2158 -0
  206. sqlalchemy/sql/lambdas.py +1442 -0
  207. sqlalchemy/sql/naming.py +209 -0
  208. sqlalchemy/sql/operators.py +2623 -0
  209. sqlalchemy/sql/roles.py +323 -0
  210. sqlalchemy/sql/schema.py +6222 -0
  211. sqlalchemy/sql/selectable.py +7265 -0
  212. sqlalchemy/sql/sqltypes.py +3930 -0
  213. sqlalchemy/sql/traversals.py +1024 -0
  214. sqlalchemy/sql/type_api.py +2368 -0
  215. sqlalchemy/sql/util.py +1485 -0
  216. sqlalchemy/sql/visitors.py +1164 -0
  217. sqlalchemy/testing/__init__.py +96 -0
  218. sqlalchemy/testing/assertions.py +994 -0
  219. sqlalchemy/testing/assertsql.py +520 -0
  220. sqlalchemy/testing/asyncio.py +135 -0
  221. sqlalchemy/testing/config.py +434 -0
  222. sqlalchemy/testing/engines.py +483 -0
  223. sqlalchemy/testing/entities.py +117 -0
  224. sqlalchemy/testing/exclusions.py +476 -0
  225. sqlalchemy/testing/fixtures/__init__.py +28 -0
  226. sqlalchemy/testing/fixtures/base.py +384 -0
  227. sqlalchemy/testing/fixtures/mypy.py +332 -0
  228. sqlalchemy/testing/fixtures/orm.py +227 -0
  229. sqlalchemy/testing/fixtures/sql.py +482 -0
  230. sqlalchemy/testing/pickleable.py +155 -0
  231. sqlalchemy/testing/plugin/__init__.py +6 -0
  232. sqlalchemy/testing/plugin/bootstrap.py +51 -0
  233. sqlalchemy/testing/plugin/plugin_base.py +828 -0
  234. sqlalchemy/testing/plugin/pytestplugin.py +892 -0
  235. sqlalchemy/testing/profiling.py +329 -0
  236. sqlalchemy/testing/provision.py +603 -0
  237. sqlalchemy/testing/requirements.py +1945 -0
  238. sqlalchemy/testing/schema.py +198 -0
  239. sqlalchemy/testing/suite/__init__.py +19 -0
  240. sqlalchemy/testing/suite/test_cte.py +237 -0
  241. sqlalchemy/testing/suite/test_ddl.py +389 -0
  242. sqlalchemy/testing/suite/test_deprecations.py +153 -0
  243. sqlalchemy/testing/suite/test_dialect.py +776 -0
  244. sqlalchemy/testing/suite/test_insert.py +630 -0
  245. sqlalchemy/testing/suite/test_reflection.py +3557 -0
  246. sqlalchemy/testing/suite/test_results.py +504 -0
  247. sqlalchemy/testing/suite/test_rowcount.py +258 -0
  248. sqlalchemy/testing/suite/test_select.py +2010 -0
  249. sqlalchemy/testing/suite/test_sequence.py +317 -0
  250. sqlalchemy/testing/suite/test_types.py +2147 -0
  251. sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
  252. sqlalchemy/testing/suite/test_update_delete.py +139 -0
  253. sqlalchemy/testing/util.py +535 -0
  254. sqlalchemy/testing/warnings.py +52 -0
  255. sqlalchemy/types.py +74 -0
  256. sqlalchemy/util/__init__.py +162 -0
  257. sqlalchemy/util/_collections.py +712 -0
  258. sqlalchemy/util/_concurrency_py3k.py +288 -0
  259. sqlalchemy/util/_has_cy.py +40 -0
  260. sqlalchemy/util/_py_collections.py +541 -0
  261. sqlalchemy/util/compat.py +421 -0
  262. sqlalchemy/util/concurrency.py +110 -0
  263. sqlalchemy/util/deprecations.py +401 -0
  264. sqlalchemy/util/langhelpers.py +2203 -0
  265. sqlalchemy/util/preloaded.py +150 -0
  266. sqlalchemy/util/queue.py +322 -0
  267. sqlalchemy/util/tool_support.py +201 -0
  268. sqlalchemy/util/topological.py +120 -0
  269. sqlalchemy/util/typing.py +734 -0
  270. sqlalchemy-2.0.47.dist-info/METADATA +243 -0
  271. sqlalchemy-2.0.47.dist-info/RECORD +274 -0
  272. sqlalchemy-2.0.47.dist-info/WHEEL +5 -0
  273. sqlalchemy-2.0.47.dist-info/licenses/LICENSE +19 -0
  274. sqlalchemy-2.0.47.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1627 @@
1
+ # orm/collections.py
2
+ # Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of SQLAlchemy and is released under
6
+ # the MIT License: https://www.opensource.org/licenses/mit-license.php
7
+ # mypy: allow-untyped-defs, allow-untyped-calls
8
+
9
+ """Support for collections of mapped entities.
10
+
11
+ The collections package supplies the machinery used to inform the ORM of
12
+ collection membership changes. An instrumentation via decoration approach is
13
+ used, allowing arbitrary types (including built-ins) to be used as entity
14
+ collections without requiring inheritance from a base class.
15
+
16
+ Instrumentation decoration relays membership change events to the
17
+ :class:`.CollectionAttributeImpl` that is currently managing the collection.
18
+ The decorators observe function call arguments and return values, tracking
19
+ entities entering or leaving the collection. Two decorator approaches are
20
+ provided. One is a bundle of generic decorators that map function arguments
21
+ and return values to events::
22
+
23
+ from sqlalchemy.orm.collections import collection
24
+
25
+
26
+ class MyClass:
27
+ # ...
28
+
29
+ @collection.adds(1)
30
+ def store(self, item):
31
+ self.data.append(item)
32
+
33
+ @collection.removes_return()
34
+ def pop(self):
35
+ return self.data.pop()
36
+
37
+ The second approach is a bundle of targeted decorators that wrap appropriate
38
+ append and remove notifiers around the mutation methods present in the
39
+ standard Python ``list``, ``set`` and ``dict`` interfaces. These could be
40
+ specified in terms of generic decorator recipes, but are instead hand-tooled
41
+ for increased efficiency. The targeted decorators occasionally implement
42
+ adapter-like behavior, such as mapping bulk-set methods (``extend``,
43
+ ``update``, ``__setslice__``, etc.) into the series of atomic mutation events
44
+ that the ORM requires.
45
+
46
+ The targeted decorators are used internally for automatic instrumentation of
47
+ entity collection classes. Every collection class goes through a
48
+ transformation process roughly like so:
49
+
50
+ 1. If the class is a built-in, substitute a trivial sub-class
51
+ 2. Is this class already instrumented?
52
+ 3. Add in generic decorators
53
+ 4. Sniff out the collection interface through duck-typing
54
+ 5. Add targeted decoration to any undecorated interface method
55
+
56
+ This process modifies the class at runtime, decorating methods and adding some
57
+ bookkeeping properties. This isn't possible (or desirable) for built-in
58
+ classes like ``list``, so trivial sub-classes are substituted to hold
59
+ decoration::
60
+
61
+ class InstrumentedList(list):
62
+ pass
63
+
64
+ Collection classes can be specified in ``relationship(collection_class=)`` as
65
+ types or a function that returns an instance. Collection classes are
66
+ inspected and instrumented during the mapper compilation phase. The
67
+ collection_class callable will be executed once to produce a specimen
68
+ instance, and the type of that specimen will be instrumented. Functions that
69
+ return built-in types like ``lists`` will be adapted to produce instrumented
70
+ instances.
71
+
72
+ When extending a known type like ``list``, additional decorations are not
73
+ generally not needed. Odds are, the extension method will delegate to a
74
+ method that's already instrumented. For example::
75
+
76
+ class QueueIsh(list):
77
+ def push(self, item):
78
+ self.append(item)
79
+
80
+ def shift(self):
81
+ return self.pop(0)
82
+
83
+ There's no need to decorate these methods. ``append`` and ``pop`` are already
84
+ instrumented as part of the ``list`` interface. Decorating them would fire
85
+ duplicate events, which should be avoided.
86
+
87
+ The targeted decoration tries not to rely on other methods in the underlying
88
+ collection class, but some are unavoidable. Many depend on 'read' methods
89
+ being present to properly instrument a 'write', for example, ``__setitem__``
90
+ needs ``__getitem__``. "Bulk" methods like ``update`` and ``extend`` may also
91
+ reimplemented in terms of atomic appends and removes, so the ``extend``
92
+ decoration will actually perform many ``append`` operations and not call the
93
+ underlying method at all.
94
+
95
+ Tight control over bulk operation and the firing of events is also possible by
96
+ implementing the instrumentation internally in your methods. The basic
97
+ instrumentation package works under the general assumption that collection
98
+ mutation will not raise unusual exceptions. If you want to closely
99
+ orchestrate append and remove events with exception management, internal
100
+ instrumentation may be the answer. Within your method,
101
+ ``collection_adapter(self)`` will retrieve an object that you can use for
102
+ explicit control over triggering append and remove events.
103
+
104
+ The owning object and :class:`.CollectionAttributeImpl` are also reachable
105
+ through the adapter, allowing for some very sophisticated behavior.
106
+
107
+ """
108
+ from __future__ import annotations
109
+
110
+ import operator
111
+ import threading
112
+ import typing
113
+ from typing import Any
114
+ from typing import Callable
115
+ from typing import cast
116
+ from typing import Collection
117
+ from typing import Dict
118
+ from typing import Iterable
119
+ from typing import List
120
+ from typing import NoReturn
121
+ from typing import Optional
122
+ from typing import Set
123
+ from typing import Tuple
124
+ from typing import Type
125
+ from typing import TYPE_CHECKING
126
+ from typing import TypeVar
127
+ from typing import Union
128
+ import weakref
129
+
130
+ from .base import NO_KEY
131
+ from .. import exc as sa_exc
132
+ from .. import util
133
+ from ..sql.base import NO_ARG
134
+ from ..util.compat import inspect_getfullargspec
135
+ from ..util.typing import Protocol
136
+
137
+ if typing.TYPE_CHECKING:
138
+ from .attributes import AttributeEventToken
139
+ from .attributes import CollectionAttributeImpl
140
+ from .mapped_collection import attribute_keyed_dict
141
+ from .mapped_collection import column_keyed_dict
142
+ from .mapped_collection import keyfunc_mapping
143
+ from .mapped_collection import KeyFuncDict # noqa: F401
144
+ from .state import InstanceState
145
+
146
+
147
+ __all__ = [
148
+ "collection",
149
+ "collection_adapter",
150
+ "keyfunc_mapping",
151
+ "column_keyed_dict",
152
+ "attribute_keyed_dict",
153
+ "KeyFuncDict",
154
+ # old names in < 2.0
155
+ "mapped_collection",
156
+ "column_mapped_collection",
157
+ "attribute_mapped_collection",
158
+ "MappedCollection",
159
+ ]
160
+
161
+ __instrumentation_mutex = threading.Lock()
162
+
163
+
164
+ _CollectionFactoryType = Callable[[], "_AdaptedCollectionProtocol"]
165
+
166
+ _T = TypeVar("_T", bound=Any)
167
+ _KT = TypeVar("_KT", bound=Any)
168
+ _VT = TypeVar("_VT", bound=Any)
169
+ _COL = TypeVar("_COL", bound="Collection[Any]")
170
+ _FN = TypeVar("_FN", bound="Callable[..., Any]")
171
+
172
+
173
+ class _CollectionConverterProtocol(Protocol):
174
+ def __call__(self, collection: _COL) -> _COL: ...
175
+
176
+
177
+ class _AdaptedCollectionProtocol(Protocol):
178
+ _sa_adapter: CollectionAdapter
179
+ _sa_appender: Callable[..., Any]
180
+ _sa_remover: Callable[..., Any]
181
+ _sa_iterator: Callable[..., Iterable[Any]]
182
+ _sa_converter: _CollectionConverterProtocol
183
+
184
+
185
+ class collection:
186
+ """Decorators for entity collection classes.
187
+
188
+ The decorators fall into two groups: annotations and interception recipes.
189
+
190
+ The annotating decorators (appender, remover, iterator, converter,
191
+ internally_instrumented) indicate the method's purpose and take no
192
+ arguments. They are not written with parens::
193
+
194
+ @collection.appender
195
+ def append(self, append): ...
196
+
197
+ The recipe decorators all require parens, even those that take no
198
+ arguments::
199
+
200
+ @collection.adds("entity")
201
+ def insert(self, position, entity): ...
202
+
203
+
204
+ @collection.removes_return()
205
+ def popitem(self): ...
206
+
207
+ """
208
+
209
+ # Bundled as a class solely for ease of use: packaging, doc strings,
210
+ # importability.
211
+
212
+ @staticmethod
213
+ def appender(fn):
214
+ """Tag the method as the collection appender.
215
+
216
+ The appender method is called with one positional argument: the value
217
+ to append. The method will be automatically decorated with 'adds(1)'
218
+ if not already decorated::
219
+
220
+ @collection.appender
221
+ def add(self, append): ...
222
+
223
+
224
+ # or, equivalently
225
+ @collection.appender
226
+ @collection.adds(1)
227
+ def add(self, append): ...
228
+
229
+
230
+ # for mapping type, an 'append' may kick out a previous value
231
+ # that occupies that slot. consider d['a'] = 'foo'- any previous
232
+ # value in d['a'] is discarded.
233
+ @collection.appender
234
+ @collection.replaces(1)
235
+ def add(self, entity):
236
+ key = some_key_func(entity)
237
+ previous = None
238
+ if key in self:
239
+ previous = self[key]
240
+ self[key] = entity
241
+ return previous
242
+
243
+ If the value to append is not allowed in the collection, you may
244
+ raise an exception. Something to remember is that the appender
245
+ will be called for each object mapped by a database query. If the
246
+ database contains rows that violate your collection semantics, you
247
+ will need to get creative to fix the problem, as access via the
248
+ collection will not work.
249
+
250
+ If the appender method is internally instrumented, you must also
251
+ receive the keyword argument '_sa_initiator' and ensure its
252
+ promulgation to collection events.
253
+
254
+ """
255
+ fn._sa_instrument_role = "appender"
256
+ return fn
257
+
258
+ @staticmethod
259
+ def remover(fn):
260
+ """Tag the method as the collection remover.
261
+
262
+ The remover method is called with one positional argument: the value
263
+ to remove. The method will be automatically decorated with
264
+ :meth:`removes_return` if not already decorated::
265
+
266
+ @collection.remover
267
+ def zap(self, entity): ...
268
+
269
+
270
+ # or, equivalently
271
+ @collection.remover
272
+ @collection.removes_return()
273
+ def zap(self): ...
274
+
275
+ If the value to remove is not present in the collection, you may
276
+ raise an exception or return None to ignore the error.
277
+
278
+ If the remove method is internally instrumented, you must also
279
+ receive the keyword argument '_sa_initiator' and ensure its
280
+ promulgation to collection events.
281
+
282
+ """
283
+ fn._sa_instrument_role = "remover"
284
+ return fn
285
+
286
+ @staticmethod
287
+ def iterator(fn):
288
+ """Tag the method as the collection remover.
289
+
290
+ The iterator method is called with no arguments. It is expected to
291
+ return an iterator over all collection members::
292
+
293
+ @collection.iterator
294
+ def __iter__(self): ...
295
+
296
+ """
297
+ fn._sa_instrument_role = "iterator"
298
+ return fn
299
+
300
+ @staticmethod
301
+ def internally_instrumented(fn):
302
+ """Tag the method as instrumented.
303
+
304
+ This tag will prevent any decoration from being applied to the
305
+ method. Use this if you are orchestrating your own calls to
306
+ :func:`.collection_adapter` in one of the basic SQLAlchemy
307
+ interface methods, or to prevent an automatic ABC method
308
+ decoration from wrapping your implementation::
309
+
310
+ # normally an 'extend' method on a list-like class would be
311
+ # automatically intercepted and re-implemented in terms of
312
+ # SQLAlchemy events and append(). your implementation will
313
+ # never be called, unless:
314
+ @collection.internally_instrumented
315
+ def extend(self, items): ...
316
+
317
+ """
318
+ fn._sa_instrumented = True
319
+ return fn
320
+
321
+ @staticmethod
322
+ @util.deprecated(
323
+ "1.3",
324
+ "The :meth:`.collection.converter` handler is deprecated and will "
325
+ "be removed in a future release. Please refer to the "
326
+ ":class:`.AttributeEvents.bulk_replace` listener interface in "
327
+ "conjunction with the :func:`.event.listen` function.",
328
+ )
329
+ def converter(fn):
330
+ """Tag the method as the collection converter.
331
+
332
+ This optional method will be called when a collection is being
333
+ replaced entirely, as in::
334
+
335
+ myobj.acollection = [newvalue1, newvalue2]
336
+
337
+ The converter method will receive the object being assigned and should
338
+ return an iterable of values suitable for use by the ``appender``
339
+ method. A converter must not assign values or mutate the collection,
340
+ its sole job is to adapt the value the user provides into an iterable
341
+ of values for the ORM's use.
342
+
343
+ The default converter implementation will use duck-typing to do the
344
+ conversion. A dict-like collection will be convert into an iterable
345
+ of dictionary values, and other types will simply be iterated::
346
+
347
+ @collection.converter
348
+ def convert(self, other): ...
349
+
350
+ If the duck-typing of the object does not match the type of this
351
+ collection, a TypeError is raised.
352
+
353
+ Supply an implementation of this method if you want to expand the
354
+ range of possible types that can be assigned in bulk or perform
355
+ validation on the values about to be assigned.
356
+
357
+ """
358
+ fn._sa_instrument_role = "converter"
359
+ return fn
360
+
361
+ @staticmethod
362
+ def adds(arg: int) -> Callable[[_FN], _FN]:
363
+ """Mark the method as adding an entity to the collection.
364
+
365
+ Adds "add to collection" handling to the method. The decorator
366
+ argument indicates which method argument holds the SQLAlchemy-relevant
367
+ value. Arguments can be specified positionally (i.e. integer) or by
368
+ name::
369
+
370
+ @collection.adds(1)
371
+ def push(self, item): ...
372
+
373
+
374
+ @collection.adds("entity")
375
+ def do_stuff(self, thing, entity=None): ...
376
+
377
+ """
378
+
379
+ def decorator(fn):
380
+ fn._sa_instrument_before = ("fire_append_event", arg)
381
+ return fn
382
+
383
+ return decorator
384
+
385
+ @staticmethod
386
+ def replaces(arg):
387
+ """Mark the method as replacing an entity in the collection.
388
+
389
+ Adds "add to collection" and "remove from collection" handling to
390
+ the method. The decorator argument indicates which method argument
391
+ holds the SQLAlchemy-relevant value to be added, and return value, if
392
+ any will be considered the value to remove.
393
+
394
+ Arguments can be specified positionally (i.e. integer) or by name::
395
+
396
+ @collection.replaces(2)
397
+ def __setitem__(self, index, item): ...
398
+
399
+ """
400
+
401
+ def decorator(fn):
402
+ fn._sa_instrument_before = ("fire_append_event", arg)
403
+ fn._sa_instrument_after = "fire_remove_event"
404
+ return fn
405
+
406
+ return decorator
407
+
408
+ @staticmethod
409
+ def removes(arg):
410
+ """Mark the method as removing an entity in the collection.
411
+
412
+ Adds "remove from collection" handling to the method. The decorator
413
+ argument indicates which method argument holds the SQLAlchemy-relevant
414
+ value to be removed. Arguments can be specified positionally (i.e.
415
+ integer) or by name::
416
+
417
+ @collection.removes(1)
418
+ def zap(self, item): ...
419
+
420
+ For methods where the value to remove is not known at call-time, use
421
+ collection.removes_return.
422
+
423
+ """
424
+
425
+ def decorator(fn):
426
+ fn._sa_instrument_before = ("fire_remove_event", arg)
427
+ return fn
428
+
429
+ return decorator
430
+
431
+ @staticmethod
432
+ def removes_return():
433
+ """Mark the method as removing an entity in the collection.
434
+
435
+ Adds "remove from collection" handling to the method. The return
436
+ value of the method, if any, is considered the value to remove. The
437
+ method arguments are not inspected::
438
+
439
+ @collection.removes_return()
440
+ def pop(self): ...
441
+
442
+ For methods where the value to remove is known at call-time, use
443
+ collection.remove.
444
+
445
+ """
446
+
447
+ def decorator(fn):
448
+ fn._sa_instrument_after = "fire_remove_event"
449
+ return fn
450
+
451
+ return decorator
452
+
453
+
454
+ if TYPE_CHECKING:
455
+
456
+ def collection_adapter(collection: Collection[Any]) -> CollectionAdapter:
457
+ """Fetch the :class:`.CollectionAdapter` for a collection."""
458
+
459
+ else:
460
+ collection_adapter = operator.attrgetter("_sa_adapter")
461
+
462
+
463
+ class CollectionAdapter:
464
+ """Bridges between the ORM and arbitrary Python collections.
465
+
466
+ Proxies base-level collection operations (append, remove, iterate)
467
+ to the underlying Python collection, and emits add/remove events for
468
+ entities entering or leaving the collection.
469
+
470
+ The ORM uses :class:`.CollectionAdapter` exclusively for interaction with
471
+ entity collections.
472
+
473
+
474
+ """
475
+
476
+ __slots__ = (
477
+ "attr",
478
+ "_key",
479
+ "_data",
480
+ "owner_state",
481
+ "_converter",
482
+ "invalidated",
483
+ "empty",
484
+ )
485
+
486
+ attr: CollectionAttributeImpl
487
+ _key: str
488
+
489
+ # this is actually a weakref; see note in constructor
490
+ _data: Callable[..., _AdaptedCollectionProtocol]
491
+
492
+ owner_state: InstanceState[Any]
493
+ _converter: _CollectionConverterProtocol
494
+ invalidated: bool
495
+ empty: bool
496
+
497
+ def __init__(
498
+ self,
499
+ attr: CollectionAttributeImpl,
500
+ owner_state: InstanceState[Any],
501
+ data: _AdaptedCollectionProtocol,
502
+ ):
503
+ self.attr = attr
504
+ self._key = attr.key
505
+
506
+ # this weakref stays referenced throughout the lifespan of
507
+ # CollectionAdapter. so while the weakref can return None, this
508
+ # is realistically only during garbage collection of this object, so
509
+ # we type this as a callable that returns _AdaptedCollectionProtocol
510
+ # in all cases.
511
+ self._data = weakref.ref(data) # type: ignore
512
+
513
+ self.owner_state = owner_state
514
+ data._sa_adapter = self
515
+ self._converter = data._sa_converter
516
+ self.invalidated = False
517
+ self.empty = False
518
+
519
+ def _warn_invalidated(self) -> None:
520
+ util.warn("This collection has been invalidated.")
521
+
522
+ @property
523
+ def data(self) -> _AdaptedCollectionProtocol:
524
+ "The entity collection being adapted."
525
+ return self._data()
526
+
527
+ @property
528
+ def _referenced_by_owner(self) -> bool:
529
+ """return True if the owner state still refers to this collection.
530
+
531
+ This will return False within a bulk replace operation,
532
+ where this collection is the one being replaced.
533
+
534
+ """
535
+ return self.owner_state.dict[self._key] is self._data()
536
+
537
+ def bulk_appender(self):
538
+ return self._data()._sa_appender
539
+
540
+ def append_with_event(
541
+ self, item: Any, initiator: Optional[AttributeEventToken] = None
542
+ ) -> None:
543
+ """Add an entity to the collection, firing mutation events."""
544
+
545
+ self._data()._sa_appender(item, _sa_initiator=initiator)
546
+
547
+ def _set_empty(self, user_data):
548
+ assert (
549
+ not self.empty
550
+ ), "This collection adapter is already in the 'empty' state"
551
+ self.empty = True
552
+ self.owner_state._empty_collections[self._key] = user_data
553
+
554
+ def _reset_empty(self) -> None:
555
+ assert (
556
+ self.empty
557
+ ), "This collection adapter is not in the 'empty' state"
558
+ self.empty = False
559
+ self.owner_state.dict[self._key] = (
560
+ self.owner_state._empty_collections.pop(self._key)
561
+ )
562
+
563
+ def _refuse_empty(self) -> NoReturn:
564
+ raise sa_exc.InvalidRequestError(
565
+ "This is a special 'empty' collection which cannot accommodate "
566
+ "internal mutation operations"
567
+ )
568
+
569
+ def append_without_event(self, item: Any) -> None:
570
+ """Add or restore an entity to the collection, firing no events."""
571
+
572
+ if self.empty:
573
+ self._refuse_empty()
574
+ self._data()._sa_appender(item, _sa_initiator=False)
575
+
576
+ def append_multiple_without_event(self, items: Iterable[Any]) -> None:
577
+ """Add or restore an entity to the collection, firing no events."""
578
+ if self.empty:
579
+ self._refuse_empty()
580
+ appender = self._data()._sa_appender
581
+ for item in items:
582
+ appender(item, _sa_initiator=False)
583
+
584
+ def bulk_remover(self):
585
+ return self._data()._sa_remover
586
+
587
+ def remove_with_event(
588
+ self, item: Any, initiator: Optional[AttributeEventToken] = None
589
+ ) -> None:
590
+ """Remove an entity from the collection, firing mutation events."""
591
+ self._data()._sa_remover(item, _sa_initiator=initiator)
592
+
593
+ def remove_without_event(self, item: Any) -> None:
594
+ """Remove an entity from the collection, firing no events."""
595
+ if self.empty:
596
+ self._refuse_empty()
597
+ self._data()._sa_remover(item, _sa_initiator=False)
598
+
599
+ def clear_with_event(
600
+ self, initiator: Optional[AttributeEventToken] = None
601
+ ) -> None:
602
+ """Empty the collection, firing a mutation event for each entity."""
603
+
604
+ if self.empty:
605
+ self._refuse_empty()
606
+ remover = self._data()._sa_remover
607
+ for item in list(self):
608
+ remover(item, _sa_initiator=initiator)
609
+
610
+ def clear_without_event(self) -> None:
611
+ """Empty the collection, firing no events."""
612
+
613
+ if self.empty:
614
+ self._refuse_empty()
615
+ remover = self._data()._sa_remover
616
+ for item in list(self):
617
+ remover(item, _sa_initiator=False)
618
+
619
+ def __iter__(self):
620
+ """Iterate over entities in the collection."""
621
+
622
+ return iter(self._data()._sa_iterator())
623
+
624
+ def __len__(self):
625
+ """Count entities in the collection."""
626
+ return len(list(self._data()._sa_iterator()))
627
+
628
+ def __bool__(self):
629
+ return True
630
+
631
+ def _fire_append_wo_mutation_event_bulk(
632
+ self, items, initiator=None, key=NO_KEY
633
+ ):
634
+ if not items:
635
+ return
636
+
637
+ if initiator is not False:
638
+ if self.invalidated:
639
+ self._warn_invalidated()
640
+
641
+ if self.empty:
642
+ self._reset_empty()
643
+
644
+ for item in items:
645
+ self.attr.fire_append_wo_mutation_event(
646
+ self.owner_state,
647
+ self.owner_state.dict,
648
+ item,
649
+ initiator,
650
+ key,
651
+ )
652
+
653
+ def fire_append_wo_mutation_event(self, item, initiator=None, key=NO_KEY):
654
+ """Notify that a entity is entering the collection but is already
655
+ present.
656
+
657
+
658
+ Initiator is a token owned by the InstrumentedAttribute that
659
+ initiated the membership mutation, and should be left as None
660
+ unless you are passing along an initiator value from a chained
661
+ operation.
662
+
663
+ .. versionadded:: 1.4.15
664
+
665
+ """
666
+ if initiator is not False:
667
+ if self.invalidated:
668
+ self._warn_invalidated()
669
+
670
+ if self.empty:
671
+ self._reset_empty()
672
+
673
+ return self.attr.fire_append_wo_mutation_event(
674
+ self.owner_state, self.owner_state.dict, item, initiator, key
675
+ )
676
+ else:
677
+ return item
678
+
679
+ def fire_append_event(self, item, initiator=None, key=NO_KEY):
680
+ """Notify that a entity has entered the collection.
681
+
682
+ Initiator is a token owned by the InstrumentedAttribute that
683
+ initiated the membership mutation, and should be left as None
684
+ unless you are passing along an initiator value from a chained
685
+ operation.
686
+
687
+ """
688
+ if initiator is not False:
689
+ if self.invalidated:
690
+ self._warn_invalidated()
691
+
692
+ if self.empty:
693
+ self._reset_empty()
694
+
695
+ return self.attr.fire_append_event(
696
+ self.owner_state, self.owner_state.dict, item, initiator, key
697
+ )
698
+ else:
699
+ return item
700
+
701
+ def _fire_remove_event_bulk(self, items, initiator=None, key=NO_KEY):
702
+ if not items:
703
+ return
704
+
705
+ if initiator is not False:
706
+ if self.invalidated:
707
+ self._warn_invalidated()
708
+
709
+ if self.empty:
710
+ self._reset_empty()
711
+
712
+ for item in items:
713
+ self.attr.fire_remove_event(
714
+ self.owner_state,
715
+ self.owner_state.dict,
716
+ item,
717
+ initiator,
718
+ key,
719
+ )
720
+
721
+ def fire_remove_event(self, item, initiator=None, key=NO_KEY):
722
+ """Notify that a entity has been removed from the collection.
723
+
724
+ Initiator is the InstrumentedAttribute that initiated the membership
725
+ mutation, and should be left as None unless you are passing along
726
+ an initiator value from a chained operation.
727
+
728
+ """
729
+ if initiator is not False:
730
+ if self.invalidated:
731
+ self._warn_invalidated()
732
+
733
+ if self.empty:
734
+ self._reset_empty()
735
+
736
+ self.attr.fire_remove_event(
737
+ self.owner_state, self.owner_state.dict, item, initiator, key
738
+ )
739
+
740
+ def fire_pre_remove_event(self, initiator=None, key=NO_KEY):
741
+ """Notify that an entity is about to be removed from the collection.
742
+
743
+ Only called if the entity cannot be removed after calling
744
+ fire_remove_event().
745
+
746
+ """
747
+ if self.invalidated:
748
+ self._warn_invalidated()
749
+ self.attr.fire_pre_remove_event(
750
+ self.owner_state,
751
+ self.owner_state.dict,
752
+ initiator=initiator,
753
+ key=key,
754
+ )
755
+
756
+ def __getstate__(self):
757
+ return {
758
+ "key": self._key,
759
+ "owner_state": self.owner_state,
760
+ "owner_cls": self.owner_state.class_,
761
+ "data": self.data,
762
+ "invalidated": self.invalidated,
763
+ "empty": self.empty,
764
+ }
765
+
766
+ def __setstate__(self, d):
767
+ self._key = d["key"]
768
+ self.owner_state = d["owner_state"]
769
+
770
+ # see note in constructor regarding this type: ignore
771
+ self._data = weakref.ref(d["data"]) # type: ignore
772
+
773
+ self._converter = d["data"]._sa_converter
774
+ d["data"]._sa_adapter = self
775
+ self.invalidated = d["invalidated"]
776
+ self.attr = getattr(d["owner_cls"], self._key).impl
777
+ self.empty = d.get("empty", False)
778
+
779
+
780
+ def bulk_replace(values, existing_adapter, new_adapter, initiator=None):
781
+ """Load a new collection, firing events based on prior like membership.
782
+
783
+ Appends instances in ``values`` onto the ``new_adapter``. Events will be
784
+ fired for any instance not present in the ``existing_adapter``. Any
785
+ instances in ``existing_adapter`` not present in ``values`` will have
786
+ remove events fired upon them.
787
+
788
+ :param values: An iterable of collection member instances
789
+
790
+ :param existing_adapter: A :class:`.CollectionAdapter` of
791
+ instances to be replaced
792
+
793
+ :param new_adapter: An empty :class:`.CollectionAdapter`
794
+ to load with ``values``
795
+
796
+
797
+ """
798
+
799
+ assert isinstance(values, list)
800
+
801
+ idset = util.IdentitySet
802
+ existing_idset = idset(existing_adapter or ())
803
+ constants = existing_idset.intersection(values or ())
804
+ additions = idset(values or ()).difference(constants)
805
+ removals = existing_idset.difference(constants)
806
+
807
+ appender = new_adapter.bulk_appender()
808
+
809
+ for member in values or ():
810
+ if member in additions:
811
+ appender(member, _sa_initiator=initiator)
812
+ elif member in constants:
813
+ appender(member, _sa_initiator=False)
814
+
815
+ if existing_adapter:
816
+ existing_adapter._fire_append_wo_mutation_event_bulk(
817
+ constants, initiator=initiator
818
+ )
819
+ existing_adapter._fire_remove_event_bulk(removals, initiator=initiator)
820
+
821
+
822
+ def prepare_instrumentation(
823
+ factory: Union[Type[Collection[Any]], _CollectionFactoryType],
824
+ ) -> _CollectionFactoryType:
825
+ """Prepare a callable for future use as a collection class factory.
826
+
827
+ Given a collection class factory (either a type or no-arg callable),
828
+ return another factory that will produce compatible instances when
829
+ called.
830
+
831
+ This function is responsible for converting collection_class=list
832
+ into the run-time behavior of collection_class=InstrumentedList.
833
+
834
+ """
835
+
836
+ impl_factory: _CollectionFactoryType
837
+
838
+ # Convert a builtin to 'Instrumented*'
839
+ if factory in __canned_instrumentation:
840
+ impl_factory = __canned_instrumentation[factory]
841
+ else:
842
+ impl_factory = cast(_CollectionFactoryType, factory)
843
+
844
+ cls: Union[_CollectionFactoryType, Type[Collection[Any]]]
845
+
846
+ # Create a specimen
847
+ cls = type(impl_factory())
848
+
849
+ # Did factory callable return a builtin?
850
+ if cls in __canned_instrumentation:
851
+ # if so, just convert.
852
+ # in previous major releases, this codepath wasn't working and was
853
+ # not covered by tests. prior to that it supplied a "wrapper"
854
+ # function that would return the class, though the rationale for this
855
+ # case is not known
856
+ impl_factory = __canned_instrumentation[cls]
857
+ cls = type(impl_factory())
858
+
859
+ # Instrument the class if needed.
860
+ if __instrumentation_mutex.acquire():
861
+ try:
862
+ if getattr(cls, "_sa_instrumented", None) != id(cls):
863
+ _instrument_class(cls)
864
+ finally:
865
+ __instrumentation_mutex.release()
866
+
867
+ return impl_factory
868
+
869
+
870
+ def _instrument_class(cls):
871
+ """Modify methods in a class and install instrumentation."""
872
+
873
+ # In the normal call flow, a request for any of the 3 basic collection
874
+ # types is transformed into one of our trivial subclasses
875
+ # (e.g. InstrumentedList). Catch anything else that sneaks in here...
876
+ if cls.__module__ == "__builtin__":
877
+ raise sa_exc.ArgumentError(
878
+ "Can not instrument a built-in type. Use a "
879
+ "subclass, even a trivial one."
880
+ )
881
+
882
+ roles, methods = _locate_roles_and_methods(cls)
883
+
884
+ _setup_canned_roles(cls, roles, methods)
885
+
886
+ _assert_required_roles(cls, roles, methods)
887
+
888
+ _set_collection_attributes(cls, roles, methods)
889
+
890
+
891
+ def _locate_roles_and_methods(cls):
892
+ """search for _sa_instrument_role-decorated methods in
893
+ method resolution order, assign to roles.
894
+
895
+ """
896
+
897
+ roles: Dict[str, str] = {}
898
+ methods: Dict[str, Tuple[Optional[str], Optional[int], Optional[str]]] = {}
899
+
900
+ for supercls in cls.__mro__:
901
+ for name, method in vars(supercls).items():
902
+ if not callable(method):
903
+ continue
904
+
905
+ # note role declarations
906
+ if hasattr(method, "_sa_instrument_role"):
907
+ role = method._sa_instrument_role
908
+ assert role in (
909
+ "appender",
910
+ "remover",
911
+ "iterator",
912
+ "converter",
913
+ )
914
+ roles.setdefault(role, name)
915
+
916
+ # transfer instrumentation requests from decorated function
917
+ # to the combined queue
918
+ before: Optional[Tuple[str, int]] = None
919
+ after: Optional[str] = None
920
+
921
+ if hasattr(method, "_sa_instrument_before"):
922
+ op, argument = method._sa_instrument_before
923
+ assert op in ("fire_append_event", "fire_remove_event")
924
+ before = op, argument
925
+ if hasattr(method, "_sa_instrument_after"):
926
+ op = method._sa_instrument_after
927
+ assert op in ("fire_append_event", "fire_remove_event")
928
+ after = op
929
+ if before:
930
+ methods[name] = before + (after,)
931
+ elif after:
932
+ methods[name] = None, None, after
933
+ return roles, methods
934
+
935
+
936
+ def _setup_canned_roles(cls, roles, methods):
937
+ """see if this class has "canned" roles based on a known
938
+ collection type (dict, set, list). Apply those roles
939
+ as needed to the "roles" dictionary, and also
940
+ prepare "decorator" methods
941
+
942
+ """
943
+ collection_type = util.duck_type_collection(cls)
944
+ if collection_type in __interfaces:
945
+ assert collection_type is not None
946
+ canned_roles, decorators = __interfaces[collection_type]
947
+ for role, name in canned_roles.items():
948
+ roles.setdefault(role, name)
949
+
950
+ # apply ABC auto-decoration to methods that need it
951
+ for method, decorator in decorators.items():
952
+ fn = getattr(cls, method, None)
953
+ if (
954
+ fn
955
+ and method not in methods
956
+ and not hasattr(fn, "_sa_instrumented")
957
+ ):
958
+ setattr(cls, method, decorator(fn))
959
+
960
+
961
+ def _assert_required_roles(cls, roles, methods):
962
+ """ensure all roles are present, and apply implicit instrumentation if
963
+ needed
964
+
965
+ """
966
+ if "appender" not in roles or not hasattr(cls, roles["appender"]):
967
+ raise sa_exc.ArgumentError(
968
+ "Type %s must elect an appender method to be "
969
+ "a collection class" % cls.__name__
970
+ )
971
+ elif roles["appender"] not in methods and not hasattr(
972
+ getattr(cls, roles["appender"]), "_sa_instrumented"
973
+ ):
974
+ methods[roles["appender"]] = ("fire_append_event", 1, None)
975
+
976
+ if "remover" not in roles or not hasattr(cls, roles["remover"]):
977
+ raise sa_exc.ArgumentError(
978
+ "Type %s must elect a remover method to be "
979
+ "a collection class" % cls.__name__
980
+ )
981
+ elif roles["remover"] not in methods and not hasattr(
982
+ getattr(cls, roles["remover"]), "_sa_instrumented"
983
+ ):
984
+ methods[roles["remover"]] = ("fire_remove_event", 1, None)
985
+
986
+ if "iterator" not in roles or not hasattr(cls, roles["iterator"]):
987
+ raise sa_exc.ArgumentError(
988
+ "Type %s must elect an iterator method to be "
989
+ "a collection class" % cls.__name__
990
+ )
991
+
992
+
993
+ def _set_collection_attributes(cls, roles, methods):
994
+ """apply ad-hoc instrumentation from decorators, class-level defaults
995
+ and implicit role declarations
996
+
997
+ """
998
+ for method_name, (before, argument, after) in methods.items():
999
+ setattr(
1000
+ cls,
1001
+ method_name,
1002
+ _instrument_membership_mutator(
1003
+ getattr(cls, method_name), before, argument, after
1004
+ ),
1005
+ )
1006
+ # intern the role map
1007
+ for role, method_name in roles.items():
1008
+ setattr(cls, "_sa_%s" % role, getattr(cls, method_name))
1009
+
1010
+ cls._sa_adapter = None
1011
+
1012
+ if not hasattr(cls, "_sa_converter"):
1013
+ cls._sa_converter = None
1014
+ cls._sa_instrumented = id(cls)
1015
+
1016
+
1017
+ def _instrument_membership_mutator(method, before, argument, after):
1018
+ """Route method args and/or return value through the collection
1019
+ adapter."""
1020
+ # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))'
1021
+ if before:
1022
+ fn_args = list(
1023
+ util.flatten_iterator(inspect_getfullargspec(method)[0])
1024
+ )
1025
+ if isinstance(argument, int):
1026
+ pos_arg = argument
1027
+ named_arg = len(fn_args) > argument and fn_args[argument] or None
1028
+ else:
1029
+ if argument in fn_args:
1030
+ pos_arg = fn_args.index(argument)
1031
+ else:
1032
+ pos_arg = None
1033
+ named_arg = argument
1034
+ del fn_args
1035
+
1036
+ def wrapper(*args, **kw):
1037
+ if before:
1038
+ if pos_arg is None:
1039
+ if named_arg not in kw:
1040
+ raise sa_exc.ArgumentError(
1041
+ "Missing argument %s" % argument
1042
+ )
1043
+ value = kw[named_arg]
1044
+ else:
1045
+ if len(args) > pos_arg:
1046
+ value = args[pos_arg]
1047
+ elif named_arg in kw:
1048
+ value = kw[named_arg]
1049
+ else:
1050
+ raise sa_exc.ArgumentError(
1051
+ "Missing argument %s" % argument
1052
+ )
1053
+
1054
+ initiator = kw.pop("_sa_initiator", None)
1055
+ if initiator is False:
1056
+ executor = None
1057
+ else:
1058
+ executor = args[0]._sa_adapter
1059
+
1060
+ if before and executor:
1061
+ getattr(executor, before)(value, initiator)
1062
+
1063
+ if not after or not executor:
1064
+ return method(*args, **kw)
1065
+ else:
1066
+ res = method(*args, **kw)
1067
+ if res is not None:
1068
+ getattr(executor, after)(res, initiator)
1069
+ return res
1070
+
1071
+ wrapper._sa_instrumented = True # type: ignore[attr-defined]
1072
+ if hasattr(method, "_sa_instrument_role"):
1073
+ wrapper._sa_instrument_role = method._sa_instrument_role # type: ignore[attr-defined] # noqa: E501
1074
+ wrapper.__name__ = method.__name__
1075
+ wrapper.__doc__ = method.__doc__
1076
+ return wrapper
1077
+
1078
+
1079
+ def __set_wo_mutation(collection, item, _sa_initiator=None):
1080
+ """Run set wo mutation events.
1081
+
1082
+ The collection is not mutated.
1083
+
1084
+ """
1085
+ if _sa_initiator is not False:
1086
+ executor = collection._sa_adapter
1087
+ if executor:
1088
+ executor.fire_append_wo_mutation_event(
1089
+ item, _sa_initiator, key=None
1090
+ )
1091
+
1092
+
1093
+ def __set(collection, item, _sa_initiator, key):
1094
+ """Run set events.
1095
+
1096
+ This event always occurs before the collection is actually mutated.
1097
+
1098
+ """
1099
+
1100
+ if _sa_initiator is not False:
1101
+ executor = collection._sa_adapter
1102
+ if executor:
1103
+ item = executor.fire_append_event(item, _sa_initiator, key=key)
1104
+ return item
1105
+
1106
+
1107
+ def __del(collection, item, _sa_initiator, key):
1108
+ """Run del events.
1109
+
1110
+ This event occurs before the collection is actually mutated, *except*
1111
+ in the case of a pop operation, in which case it occurs afterwards.
1112
+ For pop operations, the __before_pop hook is called before the
1113
+ operation occurs.
1114
+
1115
+ """
1116
+ if _sa_initiator is not False:
1117
+ executor = collection._sa_adapter
1118
+ if executor:
1119
+ executor.fire_remove_event(item, _sa_initiator, key=key)
1120
+
1121
+
1122
+ def __before_pop(collection, _sa_initiator=None):
1123
+ """An event which occurs on a before a pop() operation occurs."""
1124
+ executor = collection._sa_adapter
1125
+ if executor:
1126
+ executor.fire_pre_remove_event(_sa_initiator)
1127
+
1128
+
1129
+ def _list_decorators() -> Dict[str, Callable[[_FN], _FN]]:
1130
+ """Tailored instrumentation wrappers for any list-like class."""
1131
+
1132
+ def _tidy(fn):
1133
+ fn._sa_instrumented = True
1134
+ fn.__doc__ = getattr(list, fn.__name__).__doc__
1135
+
1136
+ def append(fn):
1137
+ def append(self, item, _sa_initiator=None):
1138
+ item = __set(self, item, _sa_initiator, NO_KEY)
1139
+ fn(self, item)
1140
+
1141
+ _tidy(append)
1142
+ return append
1143
+
1144
+ def remove(fn):
1145
+ def remove(self, value, _sa_initiator=None):
1146
+ __del(self, value, _sa_initiator, NO_KEY)
1147
+ # testlib.pragma exempt:__eq__
1148
+ fn(self, value)
1149
+
1150
+ _tidy(remove)
1151
+ return remove
1152
+
1153
+ def insert(fn):
1154
+ def insert(self, index, value):
1155
+ value = __set(self, value, None, index)
1156
+ fn(self, index, value)
1157
+
1158
+ _tidy(insert)
1159
+ return insert
1160
+
1161
+ def __setitem__(fn):
1162
+ def __setitem__(self, index, value):
1163
+ if not isinstance(index, slice):
1164
+ existing = self[index]
1165
+ if existing is not None:
1166
+ __del(self, existing, None, index)
1167
+ value = __set(self, value, None, index)
1168
+ fn(self, index, value)
1169
+ else:
1170
+ # slice assignment requires __delitem__, insert, __len__
1171
+ step = index.step or 1
1172
+ start = index.start or 0
1173
+ if start < 0:
1174
+ start += len(self)
1175
+ if index.stop is not None:
1176
+ stop = index.stop
1177
+ else:
1178
+ stop = len(self)
1179
+ if stop < 0:
1180
+ stop += len(self)
1181
+
1182
+ if step == 1:
1183
+ if value is self:
1184
+ return
1185
+ for i in range(start, stop, step):
1186
+ if len(self) > start:
1187
+ del self[start]
1188
+
1189
+ for i, item in enumerate(value):
1190
+ self.insert(i + start, item)
1191
+ else:
1192
+ rng = list(range(start, stop, step))
1193
+ if len(value) != len(rng):
1194
+ raise ValueError(
1195
+ "attempt to assign sequence of size %s to "
1196
+ "extended slice of size %s"
1197
+ % (len(value), len(rng))
1198
+ )
1199
+ for i, item in zip(rng, value):
1200
+ self.__setitem__(i, item)
1201
+
1202
+ _tidy(__setitem__)
1203
+ return __setitem__
1204
+
1205
+ def __delitem__(fn):
1206
+ def __delitem__(self, index):
1207
+ if not isinstance(index, slice):
1208
+ item = self[index]
1209
+ __del(self, item, None, index)
1210
+ fn(self, index)
1211
+ else:
1212
+ # slice deletion requires __getslice__ and a slice-groking
1213
+ # __getitem__ for stepped deletion
1214
+ # note: not breaking this into atomic dels
1215
+ for item in self[index]:
1216
+ __del(self, item, None, index)
1217
+ fn(self, index)
1218
+
1219
+ _tidy(__delitem__)
1220
+ return __delitem__
1221
+
1222
+ def extend(fn):
1223
+ def extend(self, iterable):
1224
+ for value in list(iterable):
1225
+ self.append(value)
1226
+
1227
+ _tidy(extend)
1228
+ return extend
1229
+
1230
+ def __iadd__(fn):
1231
+ def __iadd__(self, iterable):
1232
+ # list.__iadd__ takes any iterable and seems to let TypeError
1233
+ # raise as-is instead of returning NotImplemented
1234
+ for value in list(iterable):
1235
+ self.append(value)
1236
+ return self
1237
+
1238
+ _tidy(__iadd__)
1239
+ return __iadd__
1240
+
1241
+ def pop(fn):
1242
+ def pop(self, index=-1):
1243
+ __before_pop(self)
1244
+ item = fn(self, index)
1245
+ __del(self, item, None, index)
1246
+ return item
1247
+
1248
+ _tidy(pop)
1249
+ return pop
1250
+
1251
+ def clear(fn):
1252
+ def clear(self, index=-1):
1253
+ for item in self:
1254
+ __del(self, item, None, index)
1255
+ fn(self)
1256
+
1257
+ _tidy(clear)
1258
+ return clear
1259
+
1260
+ # __imul__ : not wrapping this. all members of the collection are already
1261
+ # present, so no need to fire appends... wrapping it with an explicit
1262
+ # decorator is still possible, so events on *= can be had if they're
1263
+ # desired. hard to imagine a use case for __imul__, though.
1264
+
1265
+ l = locals().copy()
1266
+ l.pop("_tidy")
1267
+ return l
1268
+
1269
+
1270
+ def _dict_decorators() -> Dict[str, Callable[[_FN], _FN]]:
1271
+ """Tailored instrumentation wrappers for any dict-like mapping class."""
1272
+
1273
+ def _tidy(fn):
1274
+ fn._sa_instrumented = True
1275
+ fn.__doc__ = getattr(dict, fn.__name__).__doc__
1276
+
1277
+ def __setitem__(fn):
1278
+ def __setitem__(self, key, value, _sa_initiator=None):
1279
+ if key in self:
1280
+ __del(self, self[key], _sa_initiator, key)
1281
+ value = __set(self, value, _sa_initiator, key)
1282
+ fn(self, key, value)
1283
+
1284
+ _tidy(__setitem__)
1285
+ return __setitem__
1286
+
1287
+ def __delitem__(fn):
1288
+ def __delitem__(self, key, _sa_initiator=None):
1289
+ if key in self:
1290
+ __del(self, self[key], _sa_initiator, key)
1291
+ fn(self, key)
1292
+
1293
+ _tidy(__delitem__)
1294
+ return __delitem__
1295
+
1296
+ def clear(fn):
1297
+ def clear(self):
1298
+ for key in self:
1299
+ __del(self, self[key], None, key)
1300
+ fn(self)
1301
+
1302
+ _tidy(clear)
1303
+ return clear
1304
+
1305
+ def pop(fn):
1306
+ def pop(self, key, default=NO_ARG):
1307
+ __before_pop(self)
1308
+ _to_del = key in self
1309
+ if default is NO_ARG:
1310
+ item = fn(self, key)
1311
+ else:
1312
+ item = fn(self, key, default)
1313
+ if _to_del:
1314
+ __del(self, item, None, key)
1315
+ return item
1316
+
1317
+ _tidy(pop)
1318
+ return pop
1319
+
1320
+ def popitem(fn):
1321
+ def popitem(self):
1322
+ __before_pop(self)
1323
+ item = fn(self)
1324
+ __del(self, item[1], None, 1)
1325
+ return item
1326
+
1327
+ _tidy(popitem)
1328
+ return popitem
1329
+
1330
+ def setdefault(fn):
1331
+ def setdefault(self, key, default=None):
1332
+ if key not in self:
1333
+ self.__setitem__(key, default)
1334
+ return default
1335
+ else:
1336
+ value = self.__getitem__(key)
1337
+ if value is default:
1338
+ __set_wo_mutation(self, value, None)
1339
+
1340
+ return value
1341
+
1342
+ _tidy(setdefault)
1343
+ return setdefault
1344
+
1345
+ def update(fn):
1346
+ def update(self, __other=NO_ARG, **kw):
1347
+ if __other is not NO_ARG:
1348
+ if hasattr(__other, "keys"):
1349
+ for key in list(__other):
1350
+ if key not in self or self[key] is not __other[key]:
1351
+ self[key] = __other[key]
1352
+ else:
1353
+ __set_wo_mutation(self, __other[key], None)
1354
+ else:
1355
+ for key, value in __other:
1356
+ if key not in self or self[key] is not value:
1357
+ self[key] = value
1358
+ else:
1359
+ __set_wo_mutation(self, value, None)
1360
+ for key in kw:
1361
+ if key not in self or self[key] is not kw[key]:
1362
+ self[key] = kw[key]
1363
+ else:
1364
+ __set_wo_mutation(self, kw[key], None)
1365
+
1366
+ _tidy(update)
1367
+ return update
1368
+
1369
+ l = locals().copy()
1370
+ l.pop("_tidy")
1371
+ return l
1372
+
1373
+
1374
+ _set_binop_bases = (set, frozenset)
1375
+
1376
+
1377
+ def _set_binops_check_strict(self: Any, obj: Any) -> bool:
1378
+ """Allow only set, frozenset and self.__class__-derived
1379
+ objects in binops."""
1380
+ return isinstance(obj, _set_binop_bases + (self.__class__,))
1381
+
1382
+
1383
+ def _set_binops_check_loose(self: Any, obj: Any) -> bool:
1384
+ """Allow anything set-like to participate in set binops."""
1385
+ return (
1386
+ isinstance(obj, _set_binop_bases + (self.__class__,))
1387
+ or util.duck_type_collection(obj) == set
1388
+ )
1389
+
1390
+
1391
+ def _set_decorators() -> Dict[str, Callable[[_FN], _FN]]:
1392
+ """Tailored instrumentation wrappers for any set-like class."""
1393
+
1394
+ def _tidy(fn):
1395
+ fn._sa_instrumented = True
1396
+ fn.__doc__ = getattr(set, fn.__name__).__doc__
1397
+
1398
+ def add(fn):
1399
+ def add(self, value, _sa_initiator=None):
1400
+ if value not in self:
1401
+ value = __set(self, value, _sa_initiator, NO_KEY)
1402
+ else:
1403
+ __set_wo_mutation(self, value, _sa_initiator)
1404
+ # testlib.pragma exempt:__hash__
1405
+ fn(self, value)
1406
+
1407
+ _tidy(add)
1408
+ return add
1409
+
1410
+ def discard(fn):
1411
+ def discard(self, value, _sa_initiator=None):
1412
+ # testlib.pragma exempt:__hash__
1413
+ if value in self:
1414
+ __del(self, value, _sa_initiator, NO_KEY)
1415
+ # testlib.pragma exempt:__hash__
1416
+ fn(self, value)
1417
+
1418
+ _tidy(discard)
1419
+ return discard
1420
+
1421
+ def remove(fn):
1422
+ def remove(self, value, _sa_initiator=None):
1423
+ # testlib.pragma exempt:__hash__
1424
+ if value in self:
1425
+ __del(self, value, _sa_initiator, NO_KEY)
1426
+ # testlib.pragma exempt:__hash__
1427
+ fn(self, value)
1428
+
1429
+ _tidy(remove)
1430
+ return remove
1431
+
1432
+ def pop(fn):
1433
+ def pop(self):
1434
+ __before_pop(self)
1435
+ item = fn(self)
1436
+ # for set in particular, we have no way to access the item
1437
+ # that will be popped before pop is called.
1438
+ __del(self, item, None, NO_KEY)
1439
+ return item
1440
+
1441
+ _tidy(pop)
1442
+ return pop
1443
+
1444
+ def clear(fn):
1445
+ def clear(self):
1446
+ for item in list(self):
1447
+ self.remove(item)
1448
+
1449
+ _tidy(clear)
1450
+ return clear
1451
+
1452
+ def update(fn):
1453
+ def update(self, value):
1454
+ for item in value:
1455
+ self.add(item)
1456
+
1457
+ _tidy(update)
1458
+ return update
1459
+
1460
+ def __ior__(fn):
1461
+ def __ior__(self, value):
1462
+ if not _set_binops_check_strict(self, value):
1463
+ return NotImplemented
1464
+ for item in value:
1465
+ self.add(item)
1466
+ return self
1467
+
1468
+ _tidy(__ior__)
1469
+ return __ior__
1470
+
1471
+ def difference_update(fn):
1472
+ def difference_update(self, value):
1473
+ for item in value:
1474
+ self.discard(item)
1475
+
1476
+ _tidy(difference_update)
1477
+ return difference_update
1478
+
1479
+ def __isub__(fn):
1480
+ def __isub__(self, value):
1481
+ if not _set_binops_check_strict(self, value):
1482
+ return NotImplemented
1483
+ for item in value:
1484
+ self.discard(item)
1485
+ return self
1486
+
1487
+ _tidy(__isub__)
1488
+ return __isub__
1489
+
1490
+ def intersection_update(fn):
1491
+ def intersection_update(self, other):
1492
+ want, have = self.intersection(other), set(self)
1493
+ remove, add = have - want, want - have
1494
+
1495
+ for item in remove:
1496
+ self.remove(item)
1497
+ for item in add:
1498
+ self.add(item)
1499
+
1500
+ _tidy(intersection_update)
1501
+ return intersection_update
1502
+
1503
+ def __iand__(fn):
1504
+ def __iand__(self, other):
1505
+ if not _set_binops_check_strict(self, other):
1506
+ return NotImplemented
1507
+ want, have = self.intersection(other), set(self)
1508
+ remove, add = have - want, want - have
1509
+
1510
+ for item in remove:
1511
+ self.remove(item)
1512
+ for item in add:
1513
+ self.add(item)
1514
+ return self
1515
+
1516
+ _tidy(__iand__)
1517
+ return __iand__
1518
+
1519
+ def symmetric_difference_update(fn):
1520
+ def symmetric_difference_update(self, other):
1521
+ want, have = self.symmetric_difference(other), set(self)
1522
+ remove, add = have - want, want - have
1523
+
1524
+ for item in remove:
1525
+ self.remove(item)
1526
+ for item in add:
1527
+ self.add(item)
1528
+
1529
+ _tidy(symmetric_difference_update)
1530
+ return symmetric_difference_update
1531
+
1532
+ def __ixor__(fn):
1533
+ def __ixor__(self, other):
1534
+ if not _set_binops_check_strict(self, other):
1535
+ return NotImplemented
1536
+ want, have = self.symmetric_difference(other), set(self)
1537
+ remove, add = have - want, want - have
1538
+
1539
+ for item in remove:
1540
+ self.remove(item)
1541
+ for item in add:
1542
+ self.add(item)
1543
+ return self
1544
+
1545
+ _tidy(__ixor__)
1546
+ return __ixor__
1547
+
1548
+ l = locals().copy()
1549
+ l.pop("_tidy")
1550
+ return l
1551
+
1552
+
1553
+ class InstrumentedList(List[_T]):
1554
+ """An instrumented version of the built-in list."""
1555
+
1556
+
1557
+ class InstrumentedSet(Set[_T]):
1558
+ """An instrumented version of the built-in set."""
1559
+
1560
+
1561
+ class InstrumentedDict(Dict[_KT, _VT]):
1562
+ """An instrumented version of the built-in dict."""
1563
+
1564
+
1565
+ __canned_instrumentation: util.immutabledict[Any, _CollectionFactoryType] = (
1566
+ util.immutabledict(
1567
+ {
1568
+ list: InstrumentedList,
1569
+ set: InstrumentedSet,
1570
+ dict: InstrumentedDict,
1571
+ }
1572
+ )
1573
+ )
1574
+
1575
+ __interfaces: util.immutabledict[
1576
+ Any,
1577
+ Tuple[
1578
+ Dict[str, str],
1579
+ Dict[str, Callable[..., Any]],
1580
+ ],
1581
+ ] = util.immutabledict(
1582
+ {
1583
+ list: (
1584
+ {
1585
+ "appender": "append",
1586
+ "remover": "remove",
1587
+ "iterator": "__iter__",
1588
+ },
1589
+ _list_decorators(),
1590
+ ),
1591
+ set: (
1592
+ {"appender": "add", "remover": "remove", "iterator": "__iter__"},
1593
+ _set_decorators(),
1594
+ ),
1595
+ # decorators are required for dicts and object collections.
1596
+ dict: ({"iterator": "values"}, _dict_decorators()),
1597
+ }
1598
+ )
1599
+
1600
+
1601
+ def __go(lcls):
1602
+ global keyfunc_mapping, mapped_collection
1603
+ global column_keyed_dict, column_mapped_collection
1604
+ global MappedCollection, KeyFuncDict
1605
+ global attribute_keyed_dict, attribute_mapped_collection
1606
+
1607
+ from .mapped_collection import keyfunc_mapping
1608
+ from .mapped_collection import column_keyed_dict
1609
+ from .mapped_collection import attribute_keyed_dict
1610
+ from .mapped_collection import KeyFuncDict
1611
+
1612
+ from .mapped_collection import mapped_collection
1613
+ from .mapped_collection import column_mapped_collection
1614
+ from .mapped_collection import attribute_mapped_collection
1615
+ from .mapped_collection import MappedCollection
1616
+
1617
+ # ensure instrumentation is associated with
1618
+ # these built-in classes; if a user-defined class
1619
+ # subclasses these and uses @internally_instrumented,
1620
+ # the superclass is otherwise not instrumented.
1621
+ # see [ticket:2406].
1622
+ _instrument_class(InstrumentedList)
1623
+ _instrument_class(InstrumentedSet)
1624
+ _instrument_class(KeyFuncDict)
1625
+
1626
+
1627
+ __go(locals())