You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1707 lines
53KB

  1. # orm/collections.py
  2. # Copyright (C) 2005-2021 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: http://www.opensource.org/licenses/mit-license.php
  7. """Support for collections of mapped entities.
  8. The collections package supplies the machinery used to inform the ORM of
  9. collection membership changes. An instrumentation via decoration approach is
  10. used, allowing arbitrary types (including built-ins) to be used as entity
  11. collections without requiring inheritance from a base class.
  12. Instrumentation decoration relays membership change events to the
  13. :class:`.CollectionAttributeImpl` that is currently managing the collection.
  14. The decorators observe function call arguments and return values, tracking
  15. entities entering or leaving the collection. Two decorator approaches are
  16. provided. One is a bundle of generic decorators that map function arguments
  17. and return values to events::
  18. from sqlalchemy.orm.collections import collection
  19. class MyClass(object):
  20. # ...
  21. @collection.adds(1)
  22. def store(self, item):
  23. self.data.append(item)
  24. @collection.removes_return()
  25. def pop(self):
  26. return self.data.pop()
  27. The second approach is a bundle of targeted decorators that wrap appropriate
  28. append and remove notifiers around the mutation methods present in the
  29. standard Python ``list``, ``set`` and ``dict`` interfaces. These could be
  30. specified in terms of generic decorator recipes, but are instead hand-tooled
  31. for increased efficiency. The targeted decorators occasionally implement
  32. adapter-like behavior, such as mapping bulk-set methods (``extend``,
  33. ``update``, ``__setslice__``, etc.) into the series of atomic mutation events
  34. that the ORM requires.
  35. The targeted decorators are used internally for automatic instrumentation of
  36. entity collection classes. Every collection class goes through a
  37. transformation process roughly like so:
  38. 1. If the class is a built-in, substitute a trivial sub-class
  39. 2. Is this class already instrumented?
  40. 3. Add in generic decorators
  41. 4. Sniff out the collection interface through duck-typing
  42. 5. Add targeted decoration to any undecorated interface method
  43. This process modifies the class at runtime, decorating methods and adding some
  44. bookkeeping properties. This isn't possible (or desirable) for built-in
  45. classes like ``list``, so trivial sub-classes are substituted to hold
  46. decoration::
  47. class InstrumentedList(list):
  48. pass
  49. Collection classes can be specified in ``relationship(collection_class=)`` as
  50. types or a function that returns an instance. Collection classes are
  51. inspected and instrumented during the mapper compilation phase. The
  52. collection_class callable will be executed once to produce a specimen
  53. instance, and the type of that specimen will be instrumented. Functions that
  54. return built-in types like ``lists`` will be adapted to produce instrumented
  55. instances.
  56. When extending a known type like ``list``, additional decorations are not
  57. generally not needed. Odds are, the extension method will delegate to a
  58. method that's already instrumented. For example::
  59. class QueueIsh(list):
  60. def push(self, item):
  61. self.append(item)
  62. def shift(self):
  63. return self.pop(0)
  64. There's no need to decorate these methods. ``append`` and ``pop`` are already
  65. instrumented as part of the ``list`` interface. Decorating them would fire
  66. duplicate events, which should be avoided.
  67. The targeted decoration tries not to rely on other methods in the underlying
  68. collection class, but some are unavoidable. Many depend on 'read' methods
  69. being present to properly instrument a 'write', for example, ``__setitem__``
  70. needs ``__getitem__``. "Bulk" methods like ``update`` and ``extend`` may also
  71. reimplemented in terms of atomic appends and removes, so the ``extend``
  72. decoration will actually perform many ``append`` operations and not call the
  73. underlying method at all.
  74. Tight control over bulk operation and the firing of events is also possible by
  75. implementing the instrumentation internally in your methods. The basic
  76. instrumentation package works under the general assumption that collection
  77. mutation will not raise unusual exceptions. If you want to closely
  78. orchestrate append and remove events with exception management, internal
  79. instrumentation may be the answer. Within your method,
  80. ``collection_adapter(self)`` will retrieve an object that you can use for
  81. explicit control over triggering append and remove events.
  82. The owning object and :class:`.CollectionAttributeImpl` are also reachable
  83. through the adapter, allowing for some very sophisticated behavior.
  84. """
  85. import operator
  86. import weakref
  87. from sqlalchemy.util.compat import inspect_getfullargspec
  88. from . import base
  89. from .. import exc as sa_exc
  90. from .. import util
  91. from ..sql import coercions
  92. from ..sql import expression
  93. from ..sql import roles
  94. __all__ = [
  95. "collection",
  96. "collection_adapter",
  97. "mapped_collection",
  98. "column_mapped_collection",
  99. "attribute_mapped_collection",
  100. ]
  101. __instrumentation_mutex = util.threading.Lock()
  102. class _PlainColumnGetter(object):
  103. """Plain column getter, stores collection of Column objects
  104. directly.
  105. Serializes to a :class:`._SerializableColumnGetterV2`
  106. which has more expensive __call__() performance
  107. and some rare caveats.
  108. """
  109. def __init__(self, cols):
  110. self.cols = cols
  111. self.composite = len(cols) > 1
  112. def __reduce__(self):
  113. return _SerializableColumnGetterV2._reduce_from_cols(self.cols)
  114. def _cols(self, mapper):
  115. return self.cols
  116. def __call__(self, value):
  117. state = base.instance_state(value)
  118. m = base._state_mapper(state)
  119. key = [
  120. m._get_state_attr_by_column(state, state.dict, col)
  121. for col in self._cols(m)
  122. ]
  123. if self.composite:
  124. return tuple(key)
  125. else:
  126. return key[0]
  127. class _SerializableColumnGetter(object):
  128. """Column-based getter used in version 0.7.6 only.
  129. Remains here for pickle compatibility with 0.7.6.
  130. """
  131. def __init__(self, colkeys):
  132. self.colkeys = colkeys
  133. self.composite = len(colkeys) > 1
  134. def __reduce__(self):
  135. return _SerializableColumnGetter, (self.colkeys,)
  136. def __call__(self, value):
  137. state = base.instance_state(value)
  138. m = base._state_mapper(state)
  139. key = [
  140. m._get_state_attr_by_column(
  141. state, state.dict, m.mapped_table.columns[k]
  142. )
  143. for k in self.colkeys
  144. ]
  145. if self.composite:
  146. return tuple(key)
  147. else:
  148. return key[0]
  149. class _SerializableColumnGetterV2(_PlainColumnGetter):
  150. """Updated serializable getter which deals with
  151. multi-table mapped classes.
  152. Two extremely unusual cases are not supported.
  153. Mappings which have tables across multiple metadata
  154. objects, or which are mapped to non-Table selectables
  155. linked across inheriting mappers may fail to function
  156. here.
  157. """
  158. def __init__(self, colkeys):
  159. self.colkeys = colkeys
  160. self.composite = len(colkeys) > 1
  161. def __reduce__(self):
  162. return self.__class__, (self.colkeys,)
  163. @classmethod
  164. def _reduce_from_cols(cls, cols):
  165. def _table_key(c):
  166. if not isinstance(c.table, expression.TableClause):
  167. return None
  168. else:
  169. return c.table.key
  170. colkeys = [(c.key, _table_key(c)) for c in cols]
  171. return _SerializableColumnGetterV2, (colkeys,)
  172. def _cols(self, mapper):
  173. cols = []
  174. metadata = getattr(mapper.local_table, "metadata", None)
  175. for (ckey, tkey) in self.colkeys:
  176. if tkey is None or metadata is None or tkey not in metadata:
  177. cols.append(mapper.local_table.c[ckey])
  178. else:
  179. cols.append(metadata.tables[tkey].c[ckey])
  180. return cols
  181. def column_mapped_collection(mapping_spec):
  182. """A dictionary-based collection type with column-based keying.
  183. Returns a :class:`.MappedCollection` factory with a keying function
  184. generated from mapping_spec, which may be a Column or a sequence
  185. of Columns.
  186. The key value must be immutable for the lifetime of the object. You
  187. can not, for example, map on foreign key values if those key values will
  188. change during the session, i.e. from None to a database-assigned integer
  189. after a session flush.
  190. """
  191. cols = [
  192. coercions.expect(roles.ColumnArgumentRole, q, argname="mapping_spec")
  193. for q in util.to_list(mapping_spec)
  194. ]
  195. keyfunc = _PlainColumnGetter(cols)
  196. return lambda: MappedCollection(keyfunc)
  197. class _SerializableAttrGetter(object):
  198. def __init__(self, name):
  199. self.name = name
  200. self.getter = operator.attrgetter(name)
  201. def __call__(self, target):
  202. return self.getter(target)
  203. def __reduce__(self):
  204. return _SerializableAttrGetter, (self.name,)
  205. def attribute_mapped_collection(attr_name):
  206. """A dictionary-based collection type with attribute-based keying.
  207. Returns a :class:`.MappedCollection` factory with a keying based on the
  208. 'attr_name' attribute of entities in the collection, where ``attr_name``
  209. is the string name of the attribute.
  210. .. warning:: the key value must be assigned to its final value
  211. **before** it is accessed by the attribute mapped collection.
  212. Additionally, changes to the key attribute are **not tracked**
  213. automatically, which means the key in the dictionary is not
  214. automatically synchronized with the key value on the target object
  215. itself. See the section :ref:`key_collections_mutations`
  216. for an example.
  217. """
  218. getter = _SerializableAttrGetter(attr_name)
  219. return lambda: MappedCollection(getter)
  220. def mapped_collection(keyfunc):
  221. """A dictionary-based collection type with arbitrary keying.
  222. Returns a :class:`.MappedCollection` factory with a keying function
  223. generated from keyfunc, a callable that takes an entity and returns a
  224. key value.
  225. The key value must be immutable for the lifetime of the object. You
  226. can not, for example, map on foreign key values if those key values will
  227. change during the session, i.e. from None to a database-assigned integer
  228. after a session flush.
  229. """
  230. return lambda: MappedCollection(keyfunc)
  231. class collection(object):
  232. """Decorators for entity collection classes.
  233. The decorators fall into two groups: annotations and interception recipes.
  234. The annotating decorators (appender, remover, iterator, converter,
  235. internally_instrumented) indicate the method's purpose and take no
  236. arguments. They are not written with parens::
  237. @collection.appender
  238. def append(self, append): ...
  239. The recipe decorators all require parens, even those that take no
  240. arguments::
  241. @collection.adds('entity')
  242. def insert(self, position, entity): ...
  243. @collection.removes_return()
  244. def popitem(self): ...
  245. """
  246. # Bundled as a class solely for ease of use: packaging, doc strings,
  247. # importability.
  248. @staticmethod
  249. def appender(fn):
  250. """Tag the method as the collection appender.
  251. The appender method is called with one positional argument: the value
  252. to append. The method will be automatically decorated with 'adds(1)'
  253. if not already decorated::
  254. @collection.appender
  255. def add(self, append): ...
  256. # or, equivalently
  257. @collection.appender
  258. @collection.adds(1)
  259. def add(self, append): ...
  260. # for mapping type, an 'append' may kick out a previous value
  261. # that occupies that slot. consider d['a'] = 'foo'- any previous
  262. # value in d['a'] is discarded.
  263. @collection.appender
  264. @collection.replaces(1)
  265. def add(self, entity):
  266. key = some_key_func(entity)
  267. previous = None
  268. if key in self:
  269. previous = self[key]
  270. self[key] = entity
  271. return previous
  272. If the value to append is not allowed in the collection, you may
  273. raise an exception. Something to remember is that the appender
  274. will be called for each object mapped by a database query. If the
  275. database contains rows that violate your collection semantics, you
  276. will need to get creative to fix the problem, as access via the
  277. collection will not work.
  278. If the appender method is internally instrumented, you must also
  279. receive the keyword argument '_sa_initiator' and ensure its
  280. promulgation to collection events.
  281. """
  282. fn._sa_instrument_role = "appender"
  283. return fn
  284. @staticmethod
  285. def remover(fn):
  286. """Tag the method as the collection remover.
  287. The remover method is called with one positional argument: the value
  288. to remove. The method will be automatically decorated with
  289. :meth:`removes_return` if not already decorated::
  290. @collection.remover
  291. def zap(self, entity): ...
  292. # or, equivalently
  293. @collection.remover
  294. @collection.removes_return()
  295. def zap(self, ): ...
  296. If the value to remove is not present in the collection, you may
  297. raise an exception or return None to ignore the error.
  298. If the remove method is internally instrumented, you must also
  299. receive the keyword argument '_sa_initiator' and ensure its
  300. promulgation to collection events.
  301. """
  302. fn._sa_instrument_role = "remover"
  303. return fn
  304. @staticmethod
  305. def iterator(fn):
  306. """Tag the method as the collection remover.
  307. The iterator method is called with no arguments. It is expected to
  308. return an iterator over all collection members::
  309. @collection.iterator
  310. def __iter__(self): ...
  311. """
  312. fn._sa_instrument_role = "iterator"
  313. return fn
  314. @staticmethod
  315. def internally_instrumented(fn):
  316. """Tag the method as instrumented.
  317. This tag will prevent any decoration from being applied to the
  318. method. Use this if you are orchestrating your own calls to
  319. :func:`.collection_adapter` in one of the basic SQLAlchemy
  320. interface methods, or to prevent an automatic ABC method
  321. decoration from wrapping your implementation::
  322. # normally an 'extend' method on a list-like class would be
  323. # automatically intercepted and re-implemented in terms of
  324. # SQLAlchemy events and append(). your implementation will
  325. # never be called, unless:
  326. @collection.internally_instrumented
  327. def extend(self, items): ...
  328. """
  329. fn._sa_instrumented = True
  330. return fn
  331. @staticmethod
  332. @util.deprecated(
  333. "1.3",
  334. "The :meth:`.collection.converter` handler is deprecated and will "
  335. "be removed in a future release. Please refer to the "
  336. ":class:`.AttributeEvents.bulk_replace` listener interface in "
  337. "conjunction with the :func:`.event.listen` function.",
  338. )
  339. def converter(fn):
  340. """Tag the method as the collection converter.
  341. This optional method will be called when a collection is being
  342. replaced entirely, as in::
  343. myobj.acollection = [newvalue1, newvalue2]
  344. The converter method will receive the object being assigned and should
  345. return an iterable of values suitable for use by the ``appender``
  346. method. A converter must not assign values or mutate the collection,
  347. its sole job is to adapt the value the user provides into an iterable
  348. of values for the ORM's use.
  349. The default converter implementation will use duck-typing to do the
  350. conversion. A dict-like collection will be convert into an iterable
  351. of dictionary values, and other types will simply be iterated::
  352. @collection.converter
  353. def convert(self, other): ...
  354. If the duck-typing of the object does not match the type of this
  355. collection, a TypeError is raised.
  356. Supply an implementation of this method if you want to expand the
  357. range of possible types that can be assigned in bulk or perform
  358. validation on the values about to be assigned.
  359. """
  360. fn._sa_instrument_role = "converter"
  361. return fn
  362. @staticmethod
  363. def adds(arg):
  364. """Mark the method as adding an entity to the collection.
  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. @collection.adds(1)
  370. def push(self, item): ...
  371. @collection.adds('entity')
  372. def do_stuff(self, thing, entity=None): ...
  373. """
  374. def decorator(fn):
  375. fn._sa_instrument_before = ("fire_append_event", arg)
  376. return fn
  377. return decorator
  378. @staticmethod
  379. def replaces(arg):
  380. """Mark the method as replacing an entity in the collection.
  381. Adds "add to collection" and "remove from collection" handling to
  382. the method. The decorator argument indicates which method argument
  383. holds the SQLAlchemy-relevant value to be added, and return value, if
  384. any will be considered the value to remove.
  385. Arguments can be specified positionally (i.e. integer) or by name::
  386. @collection.replaces(2)
  387. def __setitem__(self, index, item): ...
  388. """
  389. def decorator(fn):
  390. fn._sa_instrument_before = ("fire_append_event", arg)
  391. fn._sa_instrument_after = "fire_remove_event"
  392. return fn
  393. return decorator
  394. @staticmethod
  395. def removes(arg):
  396. """Mark the method as removing an entity in the collection.
  397. Adds "remove from collection" handling to the method. The decorator
  398. argument indicates which method argument holds the SQLAlchemy-relevant
  399. value to be removed. Arguments can be specified positionally (i.e.
  400. integer) or by name::
  401. @collection.removes(1)
  402. def zap(self, item): ...
  403. For methods where the value to remove is not known at call-time, use
  404. collection.removes_return.
  405. """
  406. def decorator(fn):
  407. fn._sa_instrument_before = ("fire_remove_event", arg)
  408. return fn
  409. return decorator
  410. @staticmethod
  411. def removes_return():
  412. """Mark the method as removing an entity in the collection.
  413. Adds "remove from collection" handling to the method. The return
  414. value of the method, if any, is considered the value to remove. The
  415. method arguments are not inspected::
  416. @collection.removes_return()
  417. def pop(self): ...
  418. For methods where the value to remove is known at call-time, use
  419. collection.remove.
  420. """
  421. def decorator(fn):
  422. fn._sa_instrument_after = "fire_remove_event"
  423. return fn
  424. return decorator
  425. collection_adapter = operator.attrgetter("_sa_adapter")
  426. """Fetch the :class:`.CollectionAdapter` for a collection."""
  427. class CollectionAdapter(object):
  428. """Bridges between the ORM and arbitrary Python collections.
  429. Proxies base-level collection operations (append, remove, iterate)
  430. to the underlying Python collection, and emits add/remove events for
  431. entities entering or leaving the collection.
  432. The ORM uses :class:`.CollectionAdapter` exclusively for interaction with
  433. entity collections.
  434. """
  435. __slots__ = (
  436. "attr",
  437. "_key",
  438. "_data",
  439. "owner_state",
  440. "_converter",
  441. "invalidated",
  442. "empty",
  443. )
  444. def __init__(self, attr, owner_state, data):
  445. self.attr = attr
  446. self._key = attr.key
  447. self._data = weakref.ref(data)
  448. self.owner_state = owner_state
  449. data._sa_adapter = self
  450. self._converter = data._sa_converter
  451. self.invalidated = False
  452. self.empty = False
  453. def _warn_invalidated(self):
  454. util.warn("This collection has been invalidated.")
  455. @property
  456. def data(self):
  457. "The entity collection being adapted."
  458. return self._data()
  459. @property
  460. def _referenced_by_owner(self):
  461. """return True if the owner state still refers to this collection.
  462. This will return False within a bulk replace operation,
  463. where this collection is the one being replaced.
  464. """
  465. return self.owner_state.dict[self._key] is self._data()
  466. def bulk_appender(self):
  467. return self._data()._sa_appender
  468. def append_with_event(self, item, initiator=None):
  469. """Add an entity to the collection, firing mutation events."""
  470. self._data()._sa_appender(item, _sa_initiator=initiator)
  471. def _set_empty(self, user_data):
  472. assert (
  473. not self.empty
  474. ), "This collection adapter is already in the 'empty' state"
  475. self.empty = True
  476. self.owner_state._empty_collections[self._key] = user_data
  477. def _reset_empty(self):
  478. assert (
  479. self.empty
  480. ), "This collection adapter is not in the 'empty' state"
  481. self.empty = False
  482. self.owner_state.dict[
  483. self._key
  484. ] = self.owner_state._empty_collections.pop(self._key)
  485. def _refuse_empty(self):
  486. raise sa_exc.InvalidRequestError(
  487. "This is a special 'empty' collection which cannot accommodate "
  488. "internal mutation operations"
  489. )
  490. def append_without_event(self, item):
  491. """Add or restore an entity to the collection, firing no events."""
  492. if self.empty:
  493. self._refuse_empty()
  494. self._data()._sa_appender(item, _sa_initiator=False)
  495. def append_multiple_without_event(self, items):
  496. """Add or restore an entity to the collection, firing no events."""
  497. if self.empty:
  498. self._refuse_empty()
  499. appender = self._data()._sa_appender
  500. for item in items:
  501. appender(item, _sa_initiator=False)
  502. def bulk_remover(self):
  503. return self._data()._sa_remover
  504. def remove_with_event(self, item, initiator=None):
  505. """Remove an entity from the collection, firing mutation events."""
  506. self._data()._sa_remover(item, _sa_initiator=initiator)
  507. def remove_without_event(self, item):
  508. """Remove an entity from the collection, firing no events."""
  509. if self.empty:
  510. self._refuse_empty()
  511. self._data()._sa_remover(item, _sa_initiator=False)
  512. def clear_with_event(self, initiator=None):
  513. """Empty the collection, firing a mutation event for each entity."""
  514. if self.empty:
  515. self._refuse_empty()
  516. remover = self._data()._sa_remover
  517. for item in list(self):
  518. remover(item, _sa_initiator=initiator)
  519. def clear_without_event(self):
  520. """Empty the collection, firing no events."""
  521. if self.empty:
  522. self._refuse_empty()
  523. remover = self._data()._sa_remover
  524. for item in list(self):
  525. remover(item, _sa_initiator=False)
  526. def __iter__(self):
  527. """Iterate over entities in the collection."""
  528. return iter(self._data()._sa_iterator())
  529. def __len__(self):
  530. """Count entities in the collection."""
  531. return len(list(self._data()._sa_iterator()))
  532. def __bool__(self):
  533. return True
  534. __nonzero__ = __bool__
  535. def fire_append_wo_mutation_event(self, item, initiator=None):
  536. """Notify that a entity is entering the collection but is already
  537. present.
  538. Initiator is a token owned by the InstrumentedAttribute that
  539. initiated the membership mutation, and should be left as None
  540. unless you are passing along an initiator value from a chained
  541. operation.
  542. .. versionadded:: 1.4.15
  543. """
  544. if initiator is not False:
  545. if self.invalidated:
  546. self._warn_invalidated()
  547. if self.empty:
  548. self._reset_empty()
  549. return self.attr.fire_append_wo_mutation_event(
  550. self.owner_state, self.owner_state.dict, item, initiator
  551. )
  552. else:
  553. return item
  554. def fire_append_event(self, item, initiator=None):
  555. """Notify that a entity has entered the collection.
  556. Initiator is a token owned by the InstrumentedAttribute that
  557. initiated the membership mutation, and should be left as None
  558. unless you are passing along an initiator value from a chained
  559. operation.
  560. """
  561. if initiator is not False:
  562. if self.invalidated:
  563. self._warn_invalidated()
  564. if self.empty:
  565. self._reset_empty()
  566. return self.attr.fire_append_event(
  567. self.owner_state, self.owner_state.dict, item, initiator
  568. )
  569. else:
  570. return item
  571. def fire_remove_event(self, item, initiator=None):
  572. """Notify that a entity has been removed from the collection.
  573. Initiator is the InstrumentedAttribute that initiated the membership
  574. mutation, and should be left as None unless you are passing along
  575. an initiator value from a chained operation.
  576. """
  577. if initiator is not False:
  578. if self.invalidated:
  579. self._warn_invalidated()
  580. if self.empty:
  581. self._reset_empty()
  582. self.attr.fire_remove_event(
  583. self.owner_state, self.owner_state.dict, item, initiator
  584. )
  585. def fire_pre_remove_event(self, initiator=None):
  586. """Notify that an entity is about to be removed from the collection.
  587. Only called if the entity cannot be removed after calling
  588. fire_remove_event().
  589. """
  590. if self.invalidated:
  591. self._warn_invalidated()
  592. self.attr.fire_pre_remove_event(
  593. self.owner_state, self.owner_state.dict, initiator=initiator
  594. )
  595. def __getstate__(self):
  596. return {
  597. "key": self._key,
  598. "owner_state": self.owner_state,
  599. "owner_cls": self.owner_state.class_,
  600. "data": self.data,
  601. "invalidated": self.invalidated,
  602. "empty": self.empty,
  603. }
  604. def __setstate__(self, d):
  605. self._key = d["key"]
  606. self.owner_state = d["owner_state"]
  607. self._data = weakref.ref(d["data"])
  608. self._converter = d["data"]._sa_converter
  609. d["data"]._sa_adapter = self
  610. self.invalidated = d["invalidated"]
  611. self.attr = getattr(d["owner_cls"], self._key).impl
  612. self.empty = d.get("empty", False)
  613. def bulk_replace(values, existing_adapter, new_adapter, initiator=None):
  614. """Load a new collection, firing events based on prior like membership.
  615. Appends instances in ``values`` onto the ``new_adapter``. Events will be
  616. fired for any instance not present in the ``existing_adapter``. Any
  617. instances in ``existing_adapter`` not present in ``values`` will have
  618. remove events fired upon them.
  619. :param values: An iterable of collection member instances
  620. :param existing_adapter: A :class:`.CollectionAdapter` of
  621. instances to be replaced
  622. :param new_adapter: An empty :class:`.CollectionAdapter`
  623. to load with ``values``
  624. """
  625. assert isinstance(values, list)
  626. idset = util.IdentitySet
  627. existing_idset = idset(existing_adapter or ())
  628. constants = existing_idset.intersection(values or ())
  629. additions = idset(values or ()).difference(constants)
  630. removals = existing_idset.difference(constants)
  631. appender = new_adapter.bulk_appender()
  632. for member in values or ():
  633. if member in additions:
  634. appender(member, _sa_initiator=initiator)
  635. elif member in constants:
  636. appender(member, _sa_initiator=False)
  637. if existing_adapter:
  638. for member in removals:
  639. existing_adapter.fire_remove_event(member, initiator=initiator)
  640. def prepare_instrumentation(factory):
  641. """Prepare a callable for future use as a collection class factory.
  642. Given a collection class factory (either a type or no-arg callable),
  643. return another factory that will produce compatible instances when
  644. called.
  645. This function is responsible for converting collection_class=list
  646. into the run-time behavior of collection_class=InstrumentedList.
  647. """
  648. # Convert a builtin to 'Instrumented*'
  649. if factory in __canned_instrumentation:
  650. factory = __canned_instrumentation[factory]
  651. # Create a specimen
  652. cls = type(factory())
  653. # Did factory callable return a builtin?
  654. if cls in __canned_instrumentation:
  655. # Wrap it so that it returns our 'Instrumented*'
  656. factory = __converting_factory(cls, factory)
  657. cls = factory()
  658. # Instrument the class if needed.
  659. if __instrumentation_mutex.acquire():
  660. try:
  661. if getattr(cls, "_sa_instrumented", None) != id(cls):
  662. _instrument_class(cls)
  663. finally:
  664. __instrumentation_mutex.release()
  665. return factory
  666. def __converting_factory(specimen_cls, original_factory):
  667. """Return a wrapper that converts a "canned" collection like
  668. set, dict, list into the Instrumented* version.
  669. """
  670. instrumented_cls = __canned_instrumentation[specimen_cls]
  671. def wrapper():
  672. collection = original_factory()
  673. return instrumented_cls(collection)
  674. # often flawed but better than nothing
  675. wrapper.__name__ = "%sWrapper" % original_factory.__name__
  676. wrapper.__doc__ = original_factory.__doc__
  677. return wrapper
  678. def _instrument_class(cls):
  679. """Modify methods in a class and install instrumentation."""
  680. # In the normal call flow, a request for any of the 3 basic collection
  681. # types is transformed into one of our trivial subclasses
  682. # (e.g. InstrumentedList). Catch anything else that sneaks in here...
  683. if cls.__module__ == "__builtin__":
  684. raise sa_exc.ArgumentError(
  685. "Can not instrument a built-in type. Use a "
  686. "subclass, even a trivial one."
  687. )
  688. roles, methods = _locate_roles_and_methods(cls)
  689. _setup_canned_roles(cls, roles, methods)
  690. _assert_required_roles(cls, roles, methods)
  691. _set_collection_attributes(cls, roles, methods)
  692. def _locate_roles_and_methods(cls):
  693. """search for _sa_instrument_role-decorated methods in
  694. method resolution order, assign to roles.
  695. """
  696. roles = {}
  697. methods = {}
  698. for supercls in cls.__mro__:
  699. for name, method in vars(supercls).items():
  700. if not callable(method):
  701. continue
  702. # note role declarations
  703. if hasattr(method, "_sa_instrument_role"):
  704. role = method._sa_instrument_role
  705. assert role in (
  706. "appender",
  707. "remover",
  708. "iterator",
  709. "converter",
  710. )
  711. roles.setdefault(role, name)
  712. # transfer instrumentation requests from decorated function
  713. # to the combined queue
  714. before, after = None, None
  715. if hasattr(method, "_sa_instrument_before"):
  716. op, argument = method._sa_instrument_before
  717. assert op in ("fire_append_event", "fire_remove_event")
  718. before = op, argument
  719. if hasattr(method, "_sa_instrument_after"):
  720. op = method._sa_instrument_after
  721. assert op in ("fire_append_event", "fire_remove_event")
  722. after = op
  723. if before:
  724. methods[name] = before + (after,)
  725. elif after:
  726. methods[name] = None, None, after
  727. return roles, methods
  728. def _setup_canned_roles(cls, roles, methods):
  729. """see if this class has "canned" roles based on a known
  730. collection type (dict, set, list). Apply those roles
  731. as needed to the "roles" dictionary, and also
  732. prepare "decorator" methods
  733. """
  734. collection_type = util.duck_type_collection(cls)
  735. if collection_type in __interfaces:
  736. canned_roles, decorators = __interfaces[collection_type]
  737. for role, name in canned_roles.items():
  738. roles.setdefault(role, name)
  739. # apply ABC auto-decoration to methods that need it
  740. for method, decorator in decorators.items():
  741. fn = getattr(cls, method, None)
  742. if (
  743. fn
  744. and method not in methods
  745. and not hasattr(fn, "_sa_instrumented")
  746. ):
  747. setattr(cls, method, decorator(fn))
  748. def _assert_required_roles(cls, roles, methods):
  749. """ensure all roles are present, and apply implicit instrumentation if
  750. needed
  751. """
  752. if "appender" not in roles or not hasattr(cls, roles["appender"]):
  753. raise sa_exc.ArgumentError(
  754. "Type %s must elect an appender method to be "
  755. "a collection class" % cls.__name__
  756. )
  757. elif roles["appender"] not in methods and not hasattr(
  758. getattr(cls, roles["appender"]), "_sa_instrumented"
  759. ):
  760. methods[roles["appender"]] = ("fire_append_event", 1, None)
  761. if "remover" not in roles or not hasattr(cls, roles["remover"]):
  762. raise sa_exc.ArgumentError(
  763. "Type %s must elect a remover method to be "
  764. "a collection class" % cls.__name__
  765. )
  766. elif roles["remover"] not in methods and not hasattr(
  767. getattr(cls, roles["remover"]), "_sa_instrumented"
  768. ):
  769. methods[roles["remover"]] = ("fire_remove_event", 1, None)
  770. if "iterator" not in roles or not hasattr(cls, roles["iterator"]):
  771. raise sa_exc.ArgumentError(
  772. "Type %s must elect an iterator method to be "
  773. "a collection class" % cls.__name__
  774. )
  775. def _set_collection_attributes(cls, roles, methods):
  776. """apply ad-hoc instrumentation from decorators, class-level defaults
  777. and implicit role declarations
  778. """
  779. for method_name, (before, argument, after) in methods.items():
  780. setattr(
  781. cls,
  782. method_name,
  783. _instrument_membership_mutator(
  784. getattr(cls, method_name), before, argument, after
  785. ),
  786. )
  787. # intern the role map
  788. for role, method_name in roles.items():
  789. setattr(cls, "_sa_%s" % role, getattr(cls, method_name))
  790. cls._sa_adapter = None
  791. if not hasattr(cls, "_sa_converter"):
  792. cls._sa_converter = None
  793. cls._sa_instrumented = id(cls)
  794. def _instrument_membership_mutator(method, before, argument, after):
  795. """Route method args and/or return value through the collection
  796. adapter."""
  797. # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))'
  798. if before:
  799. fn_args = list(
  800. util.flatten_iterator(inspect_getfullargspec(method)[0])
  801. )
  802. if isinstance(argument, int):
  803. pos_arg = argument
  804. named_arg = len(fn_args) > argument and fn_args[argument] or None
  805. else:
  806. if argument in fn_args:
  807. pos_arg = fn_args.index(argument)
  808. else:
  809. pos_arg = None
  810. named_arg = argument
  811. del fn_args
  812. def wrapper(*args, **kw):
  813. if before:
  814. if pos_arg is None:
  815. if named_arg not in kw:
  816. raise sa_exc.ArgumentError(
  817. "Missing argument %s" % argument
  818. )
  819. value = kw[named_arg]
  820. else:
  821. if len(args) > pos_arg:
  822. value = args[pos_arg]
  823. elif named_arg in kw:
  824. value = kw[named_arg]
  825. else:
  826. raise sa_exc.ArgumentError(
  827. "Missing argument %s" % argument
  828. )
  829. initiator = kw.pop("_sa_initiator", None)
  830. if initiator is False:
  831. executor = None
  832. else:
  833. executor = args[0]._sa_adapter
  834. if before and executor:
  835. getattr(executor, before)(value, initiator)
  836. if not after or not executor:
  837. return method(*args, **kw)
  838. else:
  839. res = method(*args, **kw)
  840. if res is not None:
  841. getattr(executor, after)(res, initiator)
  842. return res
  843. wrapper._sa_instrumented = True
  844. if hasattr(method, "_sa_instrument_role"):
  845. wrapper._sa_instrument_role = method._sa_instrument_role
  846. wrapper.__name__ = method.__name__
  847. wrapper.__doc__ = method.__doc__
  848. return wrapper
  849. def __set_wo_mutation(collection, item, _sa_initiator=None):
  850. """Run set wo mutation events.
  851. The collection is not mutated.
  852. """
  853. if _sa_initiator is not False:
  854. executor = collection._sa_adapter
  855. if executor:
  856. executor.fire_append_wo_mutation_event(item, _sa_initiator)
  857. def __set(collection, item, _sa_initiator=None):
  858. """Run set events.
  859. This event always occurs before the collection is actually mutated.
  860. """
  861. if _sa_initiator is not False:
  862. executor = collection._sa_adapter
  863. if executor:
  864. item = executor.fire_append_event(item, _sa_initiator)
  865. return item
  866. def __del(collection, item, _sa_initiator=None):
  867. """Run del events.
  868. This event occurs before the collection is actually mutated, *except*
  869. in the case of a pop operation, in which case it occurs afterwards.
  870. For pop operations, the __before_pop hook is called before the
  871. operation occurs.
  872. """
  873. if _sa_initiator is not False:
  874. executor = collection._sa_adapter
  875. if executor:
  876. executor.fire_remove_event(item, _sa_initiator)
  877. def __before_pop(collection, _sa_initiator=None):
  878. """An event which occurs on a before a pop() operation occurs."""
  879. executor = collection._sa_adapter
  880. if executor:
  881. executor.fire_pre_remove_event(_sa_initiator)
  882. def _list_decorators():
  883. """Tailored instrumentation wrappers for any list-like class."""
  884. def _tidy(fn):
  885. fn._sa_instrumented = True
  886. fn.__doc__ = getattr(list, fn.__name__).__doc__
  887. def append(fn):
  888. def append(self, item, _sa_initiator=None):
  889. item = __set(self, item, _sa_initiator)
  890. fn(self, item)
  891. _tidy(append)
  892. return append
  893. def remove(fn):
  894. def remove(self, value, _sa_initiator=None):
  895. __del(self, value, _sa_initiator)
  896. # testlib.pragma exempt:__eq__
  897. fn(self, value)
  898. _tidy(remove)
  899. return remove
  900. def insert(fn):
  901. def insert(self, index, value):
  902. value = __set(self, value)
  903. fn(self, index, value)
  904. _tidy(insert)
  905. return insert
  906. def __setitem__(fn):
  907. def __setitem__(self, index, value):
  908. if not isinstance(index, slice):
  909. existing = self[index]
  910. if existing is not None:
  911. __del(self, existing)
  912. value = __set(self, value)
  913. fn(self, index, value)
  914. else:
  915. # slice assignment requires __delitem__, insert, __len__
  916. step = index.step or 1
  917. start = index.start or 0
  918. if start < 0:
  919. start += len(self)
  920. if index.stop is not None:
  921. stop = index.stop
  922. else:
  923. stop = len(self)
  924. if stop < 0:
  925. stop += len(self)
  926. if step == 1:
  927. if value is self:
  928. return
  929. for i in range(start, stop, step):
  930. if len(self) > start:
  931. del self[start]
  932. for i, item in enumerate(value):
  933. self.insert(i + start, item)
  934. else:
  935. rng = list(range(start, stop, step))
  936. if len(value) != len(rng):
  937. raise ValueError(
  938. "attempt to assign sequence of size %s to "
  939. "extended slice of size %s"
  940. % (len(value), len(rng))
  941. )
  942. for i, item in zip(rng, value):
  943. self.__setitem__(i, item)
  944. _tidy(__setitem__)
  945. return __setitem__
  946. def __delitem__(fn):
  947. def __delitem__(self, index):
  948. if not isinstance(index, slice):
  949. item = self[index]
  950. __del(self, item)
  951. fn(self, index)
  952. else:
  953. # slice deletion requires __getslice__ and a slice-groking
  954. # __getitem__ for stepped deletion
  955. # note: not breaking this into atomic dels
  956. for item in self[index]:
  957. __del(self, item)
  958. fn(self, index)
  959. _tidy(__delitem__)
  960. return __delitem__
  961. if util.py2k:
  962. def __setslice__(fn):
  963. def __setslice__(self, start, end, values):
  964. for value in self[start:end]:
  965. __del(self, value)
  966. values = [__set(self, value) for value in values]
  967. fn(self, start, end, values)
  968. _tidy(__setslice__)
  969. return __setslice__
  970. def __delslice__(fn):
  971. def __delslice__(self, start, end):
  972. for value in self[start:end]:
  973. __del(self, value)
  974. fn(self, start, end)
  975. _tidy(__delslice__)
  976. return __delslice__
  977. def extend(fn):
  978. def extend(self, iterable):
  979. for value in iterable:
  980. self.append(value)
  981. _tidy(extend)
  982. return extend
  983. def __iadd__(fn):
  984. def __iadd__(self, iterable):
  985. # list.__iadd__ takes any iterable and seems to let TypeError
  986. # raise as-is instead of returning NotImplemented
  987. for value in iterable:
  988. self.append(value)
  989. return self
  990. _tidy(__iadd__)
  991. return __iadd__
  992. def pop(fn):
  993. def pop(self, index=-1):
  994. __before_pop(self)
  995. item = fn(self, index)
  996. __del(self, item)
  997. return item
  998. _tidy(pop)
  999. return pop
  1000. if not util.py2k:
  1001. def clear(fn):
  1002. def clear(self, index=-1):
  1003. for item in self:
  1004. __del(self, item)
  1005. fn(self)
  1006. _tidy(clear)
  1007. return clear
  1008. # __imul__ : not wrapping this. all members of the collection are already
  1009. # present, so no need to fire appends... wrapping it with an explicit
  1010. # decorator is still possible, so events on *= can be had if they're
  1011. # desired. hard to imagine a use case for __imul__, though.
  1012. l = locals().copy()
  1013. l.pop("_tidy")
  1014. return l
  1015. def _dict_decorators():
  1016. """Tailored instrumentation wrappers for any dict-like mapping class."""
  1017. def _tidy(fn):
  1018. fn._sa_instrumented = True
  1019. fn.__doc__ = getattr(dict, fn.__name__).__doc__
  1020. Unspecified = util.symbol("Unspecified")
  1021. def __setitem__(fn):
  1022. def __setitem__(self, key, value, _sa_initiator=None):
  1023. if key in self:
  1024. __del(self, self[key], _sa_initiator)
  1025. value = __set(self, value, _sa_initiator)
  1026. fn(self, key, value)
  1027. _tidy(__setitem__)
  1028. return __setitem__
  1029. def __delitem__(fn):
  1030. def __delitem__(self, key, _sa_initiator=None):
  1031. if key in self:
  1032. __del(self, self[key], _sa_initiator)
  1033. fn(self, key)
  1034. _tidy(__delitem__)
  1035. return __delitem__
  1036. def clear(fn):
  1037. def clear(self):
  1038. for key in self:
  1039. __del(self, self[key])
  1040. fn(self)
  1041. _tidy(clear)
  1042. return clear
  1043. def pop(fn):
  1044. def pop(self, key, default=Unspecified):
  1045. __before_pop(self)
  1046. _to_del = key in self
  1047. if default is Unspecified:
  1048. item = fn(self, key)
  1049. else:
  1050. item = fn(self, key, default)
  1051. if _to_del:
  1052. __del(self, item)
  1053. return item
  1054. _tidy(pop)
  1055. return pop
  1056. def popitem(fn):
  1057. def popitem(self):
  1058. __before_pop(self)
  1059. item = fn(self)
  1060. __del(self, item[1])
  1061. return item
  1062. _tidy(popitem)
  1063. return popitem
  1064. def setdefault(fn):
  1065. def setdefault(self, key, default=None):
  1066. if key not in self:
  1067. self.__setitem__(key, default)
  1068. return default
  1069. else:
  1070. value = self.__getitem__(key)
  1071. if value is default:
  1072. __set_wo_mutation(self, value, None)
  1073. return value
  1074. _tidy(setdefault)
  1075. return setdefault
  1076. def update(fn):
  1077. def update(self, __other=Unspecified, **kw):
  1078. if __other is not Unspecified:
  1079. if hasattr(__other, "keys"):
  1080. for key in list(__other):
  1081. if key not in self or self[key] is not __other[key]:
  1082. self[key] = __other[key]
  1083. else:
  1084. __set_wo_mutation(self, __other[key], None)
  1085. else:
  1086. for key, value in __other:
  1087. if key not in self or self[key] is not value:
  1088. self[key] = value
  1089. else:
  1090. __set_wo_mutation(self, value, None)
  1091. for key in kw:
  1092. if key not in self or self[key] is not kw[key]:
  1093. self[key] = kw[key]
  1094. else:
  1095. __set_wo_mutation(self, kw[key], None)
  1096. _tidy(update)
  1097. return update
  1098. l = locals().copy()
  1099. l.pop("_tidy")
  1100. l.pop("Unspecified")
  1101. return l
  1102. _set_binop_bases = (set, frozenset)
  1103. def _set_binops_check_strict(self, obj):
  1104. """Allow only set, frozenset and self.__class__-derived
  1105. objects in binops."""
  1106. return isinstance(obj, _set_binop_bases + (self.__class__,))
  1107. def _set_binops_check_loose(self, obj):
  1108. """Allow anything set-like to participate in set binops."""
  1109. return (
  1110. isinstance(obj, _set_binop_bases + (self.__class__,))
  1111. or util.duck_type_collection(obj) == set
  1112. )
  1113. def _set_decorators():
  1114. """Tailored instrumentation wrappers for any set-like class."""
  1115. def _tidy(fn):
  1116. fn._sa_instrumented = True
  1117. fn.__doc__ = getattr(set, fn.__name__).__doc__
  1118. Unspecified = util.symbol("Unspecified")
  1119. def add(fn):
  1120. def add(self, value, _sa_initiator=None):
  1121. if value not in self:
  1122. value = __set(self, value, _sa_initiator)
  1123. else:
  1124. __set_wo_mutation(self, value, _sa_initiator)
  1125. # testlib.pragma exempt:__hash__
  1126. fn(self, value)
  1127. _tidy(add)
  1128. return add
  1129. def discard(fn):
  1130. def discard(self, value, _sa_initiator=None):
  1131. # testlib.pragma exempt:__hash__
  1132. if value in self:
  1133. __del(self, value, _sa_initiator)
  1134. # testlib.pragma exempt:__hash__
  1135. fn(self, value)
  1136. _tidy(discard)
  1137. return discard
  1138. def remove(fn):
  1139. def remove(self, value, _sa_initiator=None):
  1140. # testlib.pragma exempt:__hash__
  1141. if value in self:
  1142. __del(self, value, _sa_initiator)
  1143. # testlib.pragma exempt:__hash__
  1144. fn(self, value)
  1145. _tidy(remove)
  1146. return remove
  1147. def pop(fn):
  1148. def pop(self):
  1149. __before_pop(self)
  1150. item = fn(self)
  1151. # for set in particular, we have no way to access the item
  1152. # that will be popped before pop is called.
  1153. __del(self, item)
  1154. return item
  1155. _tidy(pop)
  1156. return pop
  1157. def clear(fn):
  1158. def clear(self):
  1159. for item in list(self):
  1160. self.remove(item)
  1161. _tidy(clear)
  1162. return clear
  1163. def update(fn):
  1164. def update(self, value):
  1165. for item in value:
  1166. self.add(item)
  1167. _tidy(update)
  1168. return update
  1169. def __ior__(fn):
  1170. def __ior__(self, value):
  1171. if not _set_binops_check_strict(self, value):
  1172. return NotImplemented
  1173. for item in value:
  1174. self.add(item)
  1175. return self
  1176. _tidy(__ior__)
  1177. return __ior__
  1178. def difference_update(fn):
  1179. def difference_update(self, value):
  1180. for item in value:
  1181. self.discard(item)
  1182. _tidy(difference_update)
  1183. return difference_update
  1184. def __isub__(fn):
  1185. def __isub__(self, value):
  1186. if not _set_binops_check_strict(self, value):
  1187. return NotImplemented
  1188. for item in value:
  1189. self.discard(item)
  1190. return self
  1191. _tidy(__isub__)
  1192. return __isub__
  1193. def intersection_update(fn):
  1194. def intersection_update(self, other):
  1195. want, have = self.intersection(other), set(self)
  1196. remove, add = have - want, want - have
  1197. for item in remove:
  1198. self.remove(item)
  1199. for item in add:
  1200. self.add(item)
  1201. _tidy(intersection_update)
  1202. return intersection_update
  1203. def __iand__(fn):
  1204. def __iand__(self, other):
  1205. if not _set_binops_check_strict(self, other):
  1206. return NotImplemented
  1207. want, have = self.intersection(other), set(self)
  1208. remove, add = have - want, want - have
  1209. for item in remove:
  1210. self.remove(item)
  1211. for item in add:
  1212. self.add(item)
  1213. return self
  1214. _tidy(__iand__)
  1215. return __iand__
  1216. def symmetric_difference_update(fn):
  1217. def symmetric_difference_update(self, other):
  1218. want, have = self.symmetric_difference(other), set(self)
  1219. remove, add = have - want, want - have
  1220. for item in remove:
  1221. self.remove(item)
  1222. for item in add:
  1223. self.add(item)
  1224. _tidy(symmetric_difference_update)
  1225. return symmetric_difference_update
  1226. def __ixor__(fn):
  1227. def __ixor__(self, other):
  1228. if not _set_binops_check_strict(self, other):
  1229. return NotImplemented
  1230. want, have = self.symmetric_difference(other), set(self)
  1231. remove, add = have - want, want - have
  1232. for item in remove:
  1233. self.remove(item)
  1234. for item in add:
  1235. self.add(item)
  1236. return self
  1237. _tidy(__ixor__)
  1238. return __ixor__
  1239. l = locals().copy()
  1240. l.pop("_tidy")
  1241. l.pop("Unspecified")
  1242. return l
  1243. class InstrumentedList(list):
  1244. """An instrumented version of the built-in list."""
  1245. class InstrumentedSet(set):
  1246. """An instrumented version of the built-in set."""
  1247. class InstrumentedDict(dict):
  1248. """An instrumented version of the built-in dict."""
  1249. __canned_instrumentation = {
  1250. list: InstrumentedList,
  1251. set: InstrumentedSet,
  1252. dict: InstrumentedDict,
  1253. }
  1254. __interfaces = {
  1255. list: (
  1256. {"appender": "append", "remover": "remove", "iterator": "__iter__"},
  1257. _list_decorators(),
  1258. ),
  1259. set: (
  1260. {"appender": "add", "remover": "remove", "iterator": "__iter__"},
  1261. _set_decorators(),
  1262. ),
  1263. # decorators are required for dicts and object collections.
  1264. dict: ({"iterator": "values"}, _dict_decorators())
  1265. if util.py3k
  1266. else ({"iterator": "itervalues"}, _dict_decorators()),
  1267. }
  1268. class MappedCollection(dict):
  1269. """A basic dictionary-based collection class.
  1270. Extends dict with the minimal bag semantics that collection
  1271. classes require. ``set`` and ``remove`` are implemented in terms
  1272. of a keying function: any callable that takes an object and
  1273. returns an object for use as a dictionary key.
  1274. """
  1275. def __init__(self, keyfunc):
  1276. """Create a new collection with keying provided by keyfunc.
  1277. keyfunc may be any callable that takes an object and returns an object
  1278. for use as a dictionary key.
  1279. The keyfunc will be called every time the ORM needs to add a member by
  1280. value-only (such as when loading instances from the database) or
  1281. remove a member. The usual cautions about dictionary keying apply-
  1282. ``keyfunc(object)`` should return the same output for the life of the
  1283. collection. Keying based on mutable properties can result in
  1284. unreachable instances "lost" in the collection.
  1285. """
  1286. self.keyfunc = keyfunc
  1287. @collection.appender
  1288. @collection.internally_instrumented
  1289. def set(self, value, _sa_initiator=None):
  1290. """Add an item by value, consulting the keyfunc for the key."""
  1291. key = self.keyfunc(value)
  1292. self.__setitem__(key, value, _sa_initiator)
  1293. @collection.remover
  1294. @collection.internally_instrumented
  1295. def remove(self, value, _sa_initiator=None):
  1296. """Remove an item by value, consulting the keyfunc for the key."""
  1297. key = self.keyfunc(value)
  1298. # Let self[key] raise if key is not in this collection
  1299. # testlib.pragma exempt:__ne__
  1300. if self[key] != value:
  1301. raise sa_exc.InvalidRequestError(
  1302. "Can not remove '%s': collection holds '%s' for key '%s'. "
  1303. "Possible cause: is the MappedCollection key function "
  1304. "based on mutable properties or properties that only obtain "
  1305. "values after flush?" % (value, self[key], key)
  1306. )
  1307. self.__delitem__(key, _sa_initiator)
  1308. # ensure instrumentation is associated with
  1309. # these built-in classes; if a user-defined class
  1310. # subclasses these and uses @internally_instrumented,
  1311. # the superclass is otherwise not instrumented.
  1312. # see [ticket:2406].
  1313. _instrument_class(MappedCollection)
  1314. _instrument_class(InstrumentedList)
  1315. _instrument_class(InstrumentedSet)