series as well. For changes that are specific to 1.0 with an emphasis
on compatibility concerns, see :doc:`/changelog/migration_10`.
+ .. change::
+ :tags: feature, orm
+
+ Added new event handlers :meth:`.AttributeEvents.init_collection`
+ and :meth:`.AttributeEvents.dispose_collection`, which track when
+ a collection is first associated with an instance and when it is
+ replaced. These handlers supersede the :meth:`.collection.linker`
+ annotation. The old hook remains supported through an event adapter.
+
.. change::
:tags: bug, orm
:tickets: 3148, 3188
self.copy = copy_function
self.collection_factory = typecallable
+ if hasattr(self.collection_factory, "_sa_linker"):
+
+ @event.listens_for(self, "init_collection")
+ def link(target, collection, collection_adapter):
+ collection._sa_linker(collection_adapter)
+
+ @event.listens_for(self, "dispose_collection")
+ def unlink(target, collection, collection_adapter):
+ collection._sa_linker(None)
+
def __copy(self, item):
return [y for y in collections.collection_adapter(item)]
return user_data
def _initialize_collection(self, state):
- return state.manager.initialize_collection(
+
+ adapter, collection = state.manager.initialize_collection(
self.key, state, self.collection_factory)
+ self.dispatch.init_collection(state, collection, adapter)
+
+ return adapter, collection
+
def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
collection = self.get_collection(state, dict_, passive=passive)
if collection is PASSIVE_NO_RESULT:
# place a copy of "old" in state.committed_state
state._modified_event(dict_, self, old, True)
- old_collection = getattr(old, '_sa_adapter')
+ old_collection = old._sa_adapter
dict_[self.key] = user_data
collections.bulk_replace(new_values, old_collection, new_collection)
- old_collection.unlink(old)
+
+ del old._sa_adapter
+ self.dispatch.dispose_collection(state, old, old_collection)
def _invalidate_collection(self, collection):
adapter = getattr(collection, '_sa_adapter')
the instance. A single argument is passed: the collection adapter
that has been linked, or None if unlinking.
+ .. deprecated:: 1.0.0 - the :meth:`.collection.linker` handler
+ is superseded by the :meth:`.AttributeEvents.init_collection`
+ and :meth:`.AttributeEvents.dispose_collection` handlers.
+
"""
fn._sa_instrument_role = 'linker'
return fn
self._key = attr.key
self._data = weakref.ref(data)
self.owner_state = owner_state
- self.link_to_self(data)
+ data._sa_adapter = self
def _warn_invalidated(self):
util.warn("This collection has been invalidated.")
def attr(self):
return self.owner_state.manager[self._key].impl
- def link_to_self(self, data):
- """Link a collection to this adapter"""
-
- data._sa_adapter = self
- if data._sa_linker:
- data._sa_linker(self)
-
- def unlink(self, data):
- """Unlink a collection from any adapter"""
-
- del data._sa_adapter
- if data._sa_linker:
- data._sa_linker(None)
-
def adapt_like_to_iterable(self, obj):
"""Converts collection-compatible objects to an iterable of values.
setattr(cls, '_sa_%s' % role, getattr(cls, method_name))
cls._sa_adapter = None
- if not hasattr(cls, '_sa_linker'):
- cls._sa_linker = None
+
if not hasattr(cls, '_sa_converter'):
cls._sa_converter = None
cls._sa_instrumented = id(cls)
the given value, or a new effective value, should be returned.
"""
+
+ def init_collection(self, target, collection, collection_adapter):
+ """Receive a 'collection init' event.
+
+ This event is triggered for a collection-based attribute, when
+ the initial "empty collection" is first generated for a blank
+ attribute, as well as for when the collection is replaced with
+ a new one, such as via a set event.
+
+ E.g., given that ``User.addresses`` is a relationship-based
+ collection, the event is triggered here::
+
+ u1 = User()
+ u1.addresses.append(a1) # <- new collection
+
+ and also during replace operations::
+
+ u1.addresess = [a2, a3] # <- new collection
+
+ :param target: the object instance receiving the event.
+ If the listener is registered with ``raw=True``, this will
+ be the :class:`.InstanceState` object.
+ :param collection: the new collection. This will always be generated
+ from what was specified as
+ :paramref:`.RelationshipProperty.collection_class`, and will always
+ be empty.
+ :param collection_adpater: the :class:`.CollectionAdapter` that will
+ mediate internal access to the collection.
+
+ .. versionadded:: 1.0.0 the :meth:`.AttributeEvents.init_collection`
+ and :meth:`.AttributeEvents.dispose_collection` events supersede
+ the :class:`.collection.linker` hook.
+
+ """
+
+ def dispose_collection(self, target, collection, collection_adpater):
+ """Receive a 'collection dispose' event.
+
+ This event is triggered for a collection-based attribute when
+ a collection is replaced, that is::
+
+ u1.addresses.append(a1)
+
+ u1.addresses = [a2, a3] # <- old collection is disposed
+
+ The mechanics of the event will typically include that the given
+ collection is empty, even if it stored objects while being replaced.
+
+ .. versionadded:: 1.0.0 the :meth:`.AttributeEvents.init_collection`
+ and :meth:`.AttributeEvents.dispose_collection` events supersede
+ the :class:`.collection.linker` hook.
+
+ """
f1.barset.add(b1)
assert f1.barset.pop().data == 'some bar appended'
+ def test_collection_link_events(self):
+ class Foo(object):
+ pass
+ class Bar(object):
+ pass
+ instrumentation.register_class(Foo)
+ instrumentation.register_class(Bar)
+ attributes.register_attribute(Foo, 'barlist', uselist=True,
+ useobject=True)
+
+ canary = Mock()
+ event.listen(Foo.barlist, "init_collection", canary.init)
+ event.listen(Foo.barlist, "dispose_collection", canary.dispose)
+
+ f1 = Foo()
+ eq_(f1.barlist, [])
+ adapter_one = f1.barlist._sa_adapter
+ eq_(canary.init.mock_calls, [call(f1, [], adapter_one)])
+
+ b1 = Bar()
+ f1.barlist.append(b1)
+
+ b2 = Bar()
+ f1.barlist = [b2]
+ adapter_two = f1.barlist._sa_adapter
+ eq_(canary.init.mock_calls, [
+ call(f1, [], adapter_one),
+ call(f1, [b2], adapter_two),
+ ])
+ eq_(
+ canary.dispose.mock_calls,
+ [
+ call(f1, [], adapter_one)
+ ]
+ )
+
+
+
+
+
def test_none_on_collection_event(self):
"""test that append/remove of None in collections emits events.