zope.interface icon indicating copy to clipboard operation
zope.interface copied to clipboard

How to change the order of subscribers?

Open wesleybl opened this issue 2 years ago • 8 comments

BUG/PROBLEM REPORT (OR OTHER COMMON ISSUE)

What I did:

In a Plone addon, I registered a subscriber:

  <subscriber for="my.products.contents.mycontent.IMyContent
                              OFS.interfaces.IObjectWillBeRemovedEvent"
                      handler=".handler.myhandler" />

What I expect to happen:

I would like this event of mine to be executed before the subscribers registered by Zope. When I delete an object of mine, the notification occurs:

    notify(ObjectWillBeRemovedEvent(ob, self, id))

In that notification, the event:

  <subscriber
      for=".interfaces.IContentish
           zope.interface.interfaces.IObjectEvent"
      handler=".CMFCatalogAware.handleContentishEvent"
      />

runs before mine. How do I get mine to run sooner?

What actually happened:

Event registered by Zope runs before mine.

What version of Python and Zope/Addons I am using:

Python: 3.8.12 Zope 4.6.3 zope.interface 5.4.0

wesleybl avatar Mar 28 '22 18:03 wesleybl

Wesley Barroso Lopes wrote at 2022-3-28 11:57 -0700:

... In a Plone addon, I registered a subscriber:

 <subscriber for="my.products.contents.mycontent.IMyContent
                             OFS.interfaces.IObjectWillBeRemovedEvent"
                     handler=".handler.myhandler" />

What I expect to happen:

I would like this event of mine to be executed before the subscribers registered by Zope.

Subscribers are a special kind of subscriptions.

The documentation specifies the order in which subscriptions are returned. It is influenced by the registration order. Thus, controling the registration order allows you to influence the subscription order. The site.zcml determines the top level registrations, each of its directives can recursively cause further registrations. Thus, it is possible (though quite difficult) to ensure that your registrations are the first or last ones (I do not know what is necessary to achieve your goal -- read the documentation or try it out).

The other possibility is to directly change zope.event's subscribers data structure. The event mechanism is implented by (--> zope.event:__init__):

subscribers = []

def notify(event):
    """ Notify all subscribers of ``event``.
    """
    for subscriber in subscribers:
        subscriber(event)

Zope puts a function dispatch into subscribers[0] which delegates the event processing to the subscription registries of zope.component/zope.interface.

You can put your own "subscriber" in zope.event.subscribers before the default event processor (i.e. the dispatch above). This will guarantee that its effects come before the standard ones.

Unfortunately, subscriptions (unlike adapters and 'utilities) cannot be named. It is therefore not easy to use the zope.component/zope.interface` registries for your own subscriber function. You might need to create your own registries for this purpose: they can work like the standard registries but have their own content.

You could also replace dispatch. Your replacement might for example determine the sequence of all applicable subscriptions and then decide about the order in which they should get applied.

Neither approach is easy: zope.event/zope.interface simply lacks support to easily control the order in which event handlers are executed.

d-maurer avatar Mar 28 '22 22:03 d-maurer

Neither approach is easy: zope.event/zope.interface simply lacks support to easily control the order in which event handlers are executed.

I just couldn't in any way put my subscriber ahead of Zope's. This would be a great feature for Zope.

What I'm trying to do is prevent the deletion of content, under certain conditions. So I thought I'd make a subscriber that checks this and raise an exception in case the object can't be deleted. It's just that there are subscribers that run before mine, that do things to the object, like uncatalog from catalog.

wesleybl avatar Mar 29 '22 19:03 wesleybl

Wesley Barroso Lopes wrote at 2022-3-29 12:26 -0700:

What I'm trying to do is prevent the deletion of content, under certain conditions.

For this use case, it is not necessary that your subscriber runs before the normal subscribers: your subscriber will be informed about the deletion -- and if it objects, it can raise an exception. Then the transaction will be aborted and the deletion will not take effect.

It will not work for a Zope Manager: someone decided that exceptions during a deletion by a Manager should not prevent the deletion. But, non Manager deletions can be prevented in this way.

You can use a specific exception and register a corresponding error view: to inform the user about the prevented deletion. Without such an error view, the user will see a standard error message.

d-maurer avatar Mar 29 '22 19:03 d-maurer

You can use a specific exception and register a corresponding error view: to inform the user about the prevented deletion.

If I catch the exception to display a friendly message, there is no rollback of things done by subscribers, like uncatalog.

wesleybl avatar Mar 29 '22 22:03 wesleybl

Wesley Barroso Lopes wrote at 2022-3-29 15:17 -0700:

You can use a specific exception and register a corresponding error view: to inform the user about the prevented deletion.

If I catch the exception to display a friendly message, there is no rollback of things done by subscribers, like uncatalog.

You do not catch the exception: an error/exception view does not change transaction handling -- even if it "fires" the transaction is aborted.

d-maurer avatar Mar 30 '22 05:03 d-maurer

@d-maurer remembering that I'm in the Plone context. In Plone, I can delete multiple objects at the same time. So it is necessary to catch the error, because some objects can be deleted and others not, in the same request. See:

https://github.com/plone/plone.app.content/blob/a4a7b277bd9b50b9755b23ea72d973a657e9b3f2/plone/app/content/browser/contents/init.py#L88-L104

self.action(obj) it could be a delete:

https://github.com/plone/plone.app.content/blob/a4a7b277bd9b50b9755b23ea72d973a657e9b3f2/plone/app/content/browser/contents/delete.py#L66

wesleybl avatar Mar 30 '22 12:03 wesleybl

Wesley Barroso Lopes wrote at 2022-3-30 05:11 -0700: @.*** remembering that I'm in the Plone context. In Plone, I can delete multiple objects at the same time. So it is necessary to catch the error, because some objects can be deleted and others not, in the same request. See:

Then, I am in doubt that your design it right. Apparently, you want to prevent (under the hood) the deletion of some objects while in the same transaction you want to delete other objects.

Deletion has a semantics: if it succeeds, the deleted object should no longer be there. If it fails, the transaction should get aborted -- to keep a consistent persistent state. Do not call Zope/Plone's deletion methods when you do not want this semantics.

You can use transaction.savepoint to emulate nested transactions. This would allow you to catch exceptions from parts of your request processing and manually roll back the persistent changes from those parts. Still, you could not perform deletion of some objects and prevent deletion for other objects in those parts but other parts could do whatever they want (including deletion of objects not prevented by your special logic).

d-maurer avatar Mar 30 '22 13:03 d-maurer

but other parts could do whatever they want (including deletion of objects not prevented by your special logic).

Yes, that's why it would be good for validation to be on the subscriber. But for this to work, my subscriber would have to be run before Zope's subscribers. That's why it would be interesting to be able to choose the order of execution of the subscribers. I think I'll open an issue on zope.configuration requesting this feature

wesleybl avatar Mar 30 '22 15:03 wesleybl