From: Mike Bayer Date: Mon, 8 Sep 2014 00:28:19 +0000 (-0400) Subject: - Added new event handlers :meth:`.AttributeEvents.init_collection` X-Git-Tag: rel_1_0_0b1~194^2~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=390207e533854ab0c3abe6b7ebc45fae1b14eaba;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - 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. --- diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index cb7a9088dc..9c7f207cc7 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -21,6 +21,15 @@ 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 diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 66197ba0ef..459a525392 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -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') diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index 698677a0bb..1fc0873bd4 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -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) diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index daf7050408..c50a7b062c 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -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. + + """ diff --git a/test/orm/test_attributes.py b/test/orm/test_attributes.py index 36ad645061..46d5f86e50 100644 --- a/test/orm/test_attributes.py +++ b/test/orm/test_attributes.py @@ -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.