Managing Hook Dependencies

In most projects you are likely to bind multiple handlers to each hook. As you further separate different services to modules and decouple complex code, you may encounter cases in which a handler needs to execute only after another handler has finished. This sounds trivial, but it gets tricky as you take into account module load order.

By default, Gossip executes handlers by order of registration:

>>> import gossip

>>> @gossip.register("handler")
... def handler1():
...     print("Handler 1 called")

>>> @gossip.register("handler")
... def handler2():
...     print("Handler 2 called")

>>> gossip.trigger("handler")
Handler 1 called
Handler 2 called

Now let’s separate the two registrations to two separate modules:

module1.py:

@gossip.register("handler")
def handler1():
    print("Handler 1 called")

module2.py:

@gossip.register("handler")
def handler2():
    print("Handler 2 called")

Now the results of your program vary unexpectedly depending on your import order:

# in this case, handler1 registers first, and so will be triggered first
import module1
import module2
# in this case, handler2 registers first, and so will be triggered first
import module2
import module1

Gossip aims to help you with such issues, and still leave you with the ability to register handlers from anywhere in your code. It provides two main ways of controlling hook dependencies: Signaling helpers and needs/provides markers.

Using the Signaling Helpers

Gossip offers a few helper functions to assist with dependency resolution. These helpers all abort the current execution of the handler via an exception, and signal Gossip to try again after other hooks are called.

wait_for

gossip.wait_for() receives a boolean expression and defers execution if it is False-y:

>>> second_handler_called = False

>>> @gossip.register("hook1")
... def first_handler():
...     gossip.wait_for(second_handler_called)
...     print("First handler")

>>> @gossip.register("hook1")
... def second_handler():
...     global second_handler_called
...     second_handler_called = True
...     print("Second handler")

>>> gossip.trigger("hook1")
Second handler
First handler

not_now

gossip.not_now() defers execution, and is a different way of writing gossip.wait_for(False):

>>> @gossip.register("hook1")
... def handler():
...     if not some_condition():
...         gossip.not_now()

Using Numerical Priorities

Gossip supports specifying priorities for registrations. Priorities are numbers (0 by default) by which the call order is determined – the higher the number, the earlier the registration will be called:

>>> @gossip.register('prioritized', priority=1)
... def handler1():
...    print('priority 1')
>>> @gossip.register('prioritized', priority=100)
... def handler2():
...    print('priority 100')
>>> @gossip.register('prioritized', priority=-5)
... def handler3():
...    print('priority -5')
>>> gossip.trigger('prioritized')
priority 100
priority 1
priority -5

Note

Numerical priorities might not behave as expected when mixed with other ordering features, such as needs/provides or signaling

Using Needs/Provides Markers

You can also tackle dependencies at the point of registration, stating that a certain registration needs to happen before or after another registration. The way to do that is stating that the registration needs or provides something compared to another registration. For example:

>>> @gossip.register("some_hook", needs=["phase1_complete"])
... def handler1():
...     print("Handler1")

>>> @gossip.register("some_hook", provides=["phase1_complete"])
... def phase1_prepare_something():
...     print("Preparing phase 1")

>>> @gossip.register("some_hook", provides=["phase1_complete"])
... def phase1_prepare_another_thing():
...     print("Still preparing phase 1")

>>> gossip.trigger("some_hook")
Preparing phase 1
Still preparing phase 1
Handler1

needs and provides are mere strings representing a resource or constraint that can be referred to as needed or provided by hook handlers. It merely means that any handler needing a certain thing must be called after the handler providing the thing had fired.

In the above example, the registrations are fired in order to satisfy the needs/provides dependencies. You’ll note that multiple handlers can provide the same thing, which means that all of them must be fired before the handler that needs that thing.

Dealing with Regular Handlers in Needs/Provides Scenarios

Hook handlers that do not specify andy needs/provides constraints, by default, are considered free handlers that can be fired at any point.

In some cases though, you want to make sure those handlers fire only after or before all the constrained handlers are fired. To control this, you should use the gossip.groups.Group.set_unconstrained_handler_priority():

>>> @gossip.register('my_group.my_hook')
... def dontcare():
...     print("I don't care")

>>> @gossip.register('my_group.my_hook', provides=['something'])
... def i_care():
...     print("I care!")

>>> gossip.trigger('my_group.my_hook')
I don't care
I care!

>>> gossip.get_group('my_group').set_unconstrained_handler_priority(gossip.FIRST)
>>> gossip.trigger('my_group.my_hook')
I don't care
I care!

>>> gossip.get_group('my_group').set_unconstrained_handler_priority(gossip.LAST)
>>> gossip.trigger('my_group.my_hook')
I care!
I don't care

Circular and Unmet Dependencies

In both approaches to dependency management, Gossip detects dependencies that aren’t resolved in time, such as circular dependencies or cases like gossip.wait_for(False). In such cases, gossip.exceptions.CannotResolveDependencies is raised immediately.