]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added new event handlers :meth:`.AttributeEvents.init_collection`
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 8 Sep 2014 00:28:19 +0000 (20:28 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 8 Sep 2014 00:28:19 +0000 (20:28 -0400)
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.

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/collections.py
lib/sqlalchemy/orm/events.py
test/orm/test_attributes.py

index cb7a9088dc61848ecd057ff525fbaf5c1e966abf..9c7f207cc71068d2560d1a99b0c2d4d636126d2c 100644 (file)
     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
index 66197ba0efa30aaf4b104d7dbdc5b55826f48089..459a52539212ace8fab3d3e04a0647b7ccb6f6ff 100644 (file)
@@ -863,6 +863,16 @@ class CollectionAttributeImpl(AttributeImpl):
         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)]
 
@@ -955,9 +965,14 @@ class CollectionAttributeImpl(AttributeImpl):
         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:
@@ -1026,12 +1041,14 @@ class CollectionAttributeImpl(AttributeImpl):
         # 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')
index 698677a0bb1749f3e7e825b813f6152975668c28..1fc0873bd41648c3f40352cc2c316bf1222a8f3c 100644 (file)
@@ -429,6 +429,10 @@ class collection(object):
         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
@@ -575,7 +579,7 @@ class CollectionAdapter(object):
         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.")
@@ -589,20 +593,6 @@ class CollectionAdapter(object):
     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.
 
@@ -945,8 +935,7 @@ def _instrument_class(cls):
         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)
index daf7050408e641d633b2873297f0f06d77449e52..c50a7b062c95afbff0db12a03d014f17a0eef36d 100644 (file)
@@ -1593,3 +1593,56 @@ class AttributeEvents(event.Events):
          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.
+
+        """
index 36ad6450617249a042a6c1353634dd967888bfef..46d5f86e504292b39fa7b698759b3344c644c16f 100644 (file)
@@ -2522,6 +2522,46 @@ class ListenerTest(fixtures.ORMTest):
         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.