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