Advanced Usage

Hook Arguments

Hooks can receive arguments, which are then passed to the handlers.

>>> import gossip

>>> @gossip.register('with_arguments')
... def handler(a, b, c):
...     print('Called: {0} {1} {2}'.format(a, b, c))

>>> gossip.trigger('with_arguments', a=1, b=2, c=3)
Called: 1 2 3

Note that argument mismatches means a runtime error:

>>> gossip.trigger('with_arguments', a=1) 
Traceback (most recent call last):
 ...
TypeError: handler() takes exactly 3 arguments (1 given)

Note

Since hook handlers are likely to be spread across many locations in your projects, argument ordering changes make your code more likely to break. This is why gossip forces all arguments to be passed by keywords, and not as positionals.

Hook Tags

Hooks can receive tags, enabling you to divide callbacks into several categories.

>>> @gossip.register('hook', tags=['a'])
... def callback_a():
...     print('A called')
>>> @gossip.register('hook', tags=['b'])
... def callback_a():
...     print('B called')
>>> gossip.trigger_with_tags('hook', tags=['a'])
A called
>>> gossip.trigger_with_tags('hook', tags=['b'])
B called

Note

registering with multiple tags will fire the callback if any of the tags match.

Note

hook tags have a special relationship to strictness, see below for more details.

Defining Hooks Explicitly

By default, registering hooks in with gossip.register() takes care of hook definition and registration at the same time. In several cases, however, you may want to simply define a hook, but not register anything to it yet. For this we have the gossip.define() API:

>>> import gossip
>>> hook = gossip.define('hook_name_here')
>>> @hook.register
... def handler():
...     pass

The gossip.register() returns the gossip.hooks.Hook object for the defined hook, so further operations can be executed against it.

Hooks cannot be define-d more than once:

>>> import gossip
>>> hook = gossip.define('some_hook')
>>> gossip.define('some_hook') 
Traceback (most recent call last):
   ...
NameAlreadyUsed: ...

Strict Registration

By default, handlers can be registered to hooks that haven’t been defined yet. While this is ok for most uses, in some cases you may want to limit this behavior, to avoid typos like this one:

>>> @gossip.register('my_group.on_initialize')
... def handler():
...     pass

>>> gossip.trigger('my_group.on_initailize') # spot the difference?

To do this, you can make any hook group into a strict group, meaning it requires registered hooks to be properly defined first:

>>> group = gossip.create_group('some_group')
>>> group.set_strict()

>>> @gossip.register('some_group.nonexisting') 
... def handler():
...     pass
Traceback (most recent call last):
   ...
UndefinedHook: hook 'some_group.nonexisting' wasn't defined yet

This also works if you set a group as a strict group after you registered hooks to it – any existing hook that wasn’t formally defined will trigger an exception:

>>> group = gossip.create_group('other_group')
>>> @gossip.register('other_group.nonexisting')
... def handler():
...     pass

>>> group.set_strict() 
Traceback (most recent call last):
   ...
UndefinedHook: hook 'other_group.nonexisting' was already registered, but not defined

Strictness and Tags

Strict hooks always perform checks on the tags that are passed to them:

>>> gossip.create_group('strict_group').set_strict()
>>> _ = gossip.define('strict_group.hook1', tags=['a', 'b'])
>>> @gossip.register('strict_group.hook1', tags=['c']) 
... def f():
...     pass
Traceback (most recent call last):
    ...
UnsupportedHookTags: ...

Strictness and Hook Arguments

Strict hooks validate their keyword arguments according to the arg_names parameter passed to define:

>>> gossip.create_group('strict_group_2').set_strict()
>>> _ = gossip.define('strict_group_2.hook1', arg_names=('a', 'b'))
>>> gossip.trigger('strict_group_2.hook1') 
Traceback (most recent call last):
   ...
TypeError: Missing argument ...

Hook arguments can also have their types specified for a stricter validation:

>>> _ = gossip.define('strict_group_2.hook2', arg_names={'a': int, 'b': (str, float)})
>>> gossip.trigger('strict_group_2.hook2', a=2, b=object()) 
Traceback (most recent call last):
...
TypeError: Incorrect type for argument b. Expected (<class 'str'>, <class 'float'>), got <class 'object'>

Token Registration

Handlers can be registered with tokens. A token is anything that supports equality and hashing, but it is most commonly used for Python strings. Token are useful to unregister a group of handlers in a single operation, with gossip.unregister_token():

>>> @gossip.register('some_hook', token='token1')
... def handler1():
...     pass

>>> @gossip.register('some_hook', token='token1')
... def handler2():
...     pass

>>> gossip.unregister_token('token1') # unregisters all handlers of all hooks that were registered with 'token1'

Getting Hooks by Name

Once a hook is defined you can get the underlying gossip.hooks.Hook object by using gossip.get_hook():

>>> gossip.get_hook('some_hook')
<Hook some_hook()>

However, in this way the hook is never defined for you:

>>> gossip.get_hook('nonexisting_hook') 
Traceback (most recent call last):
   ...
HookNotFound: ...

Muting Hooks

You can selectively mute hooks (prevent their callbacks from being called) through the mute_context() context:

>>> def function_that_triggers_hooks():
...     gossip.trigger('my.hook.name')

>>> with gossip.mute_context(['my.hook.name']):
...     function_that_triggers_hooks()  # <--- nothing happens

However, both hooks and groups can forbid the usage of mute_context() on them:

>>> hook = gossip.define('my_unmuted_hook')
>>> hook.forbid_muting()
>>> with gossip.mute_context(['my_unmuted_hook']): 
...     pass
Traceback (most recent call last):
   ...
CannotMuteHooks: Hooks cannot be muted: my_unmuted_hook

Registration Blueprints

In some cases you may want to register or unregister several hooks at once, for instance when implementing plugins that can load and unload on demand. Registration blueprints are just for that:

>>> from gossip import Blueprint
>>> my_blueprint = Blueprint()
>>> @my_blueprint.register('hook_name')
... def hook_handler():
...     print('called!')

The code above doesn’t really do anything and no hook is actually registered. Triggering your hook will not call the handler:

>>> gossip.trigger('hook_name')

You can install or uninstall your blueprint as a whole using gossip.Blueprint.install()/gossip.Blueprint.uninstall():

>>> my_blueprint.install()
>>> gossip.trigger('hook_name')
called!
>>> my_blueprint.uninstall()

Pre-Trigger Callbacks

In some advanced scenarios you might want to add a callback before each registration being triggered on a specific hook. This can be done with gossip.hooks.Hook.add_pre_trigger_callback():

>>> hook = gossip.define('my_hook')
>>> @hook.add_pre_trigger_callback
... def before_trigger(registration, kwargs):
...     print('{0} is about to be called with {1}'.format(registration.func, kwargs))

Deprecating Hooks

New in version 1.1.0.

It is possible to define a hook as deprecated, meaning that registering on it will cause a deprecation:

>>> hook = gossip.define('deprecated_hook', deprecated=True)

Non-reentrant Hooks

New in version 2.0.0.

Gossip allows you to make specific registrations non-reentrant, meaning any attempt to trigger them while they’re still being called will do nothing:

>>> @gossip.register('hook', reentrant=False)
... def handler():
...     gossip.trigger('hook') # this will not cause a recursion since this handler is non-reentrant

Toggle Hooks

New in version 2.0.0.

Consider this registration code:

state = ...

@gossip.register('start')
def start():
    state.lock.acquire()

@gossip.register('end')
def end():
    state.lock.release()

Let’s also assume this code is the one triggering the hooks:

try:
    gossip.trigger('start')
    ...
finally:
    gossip.trigger('end')

Since Gossip’s default exception policy is “raise immediately”, this code might encounter a second failure during cleanup, since it will be releasing an un-acquired lock (end might be called without start being called first, because another registration might raise).

Solving this on the triggering side is hard, and requiring all registrants to properly implement safeguards in these unexpected places is a hassle.

For this purpose toggle hooks were created. These hooks rely on a shared Toggle object that can be either on or off (they’re off when they’re created). Each registration specified if it turns the toggle on or off:

from gossip import Toggle

_toggle = Toggle()

@gossip.register('start', toggles_on=_toggle)
def start():
    ...

@gossip.register('end', toggles_off=_toggle)
def end():
    ...

Now the earlier example no longer has the flaw we mentioned above. Gossip will ensure a registration will not be called if it would not change the currnet state of a toggle.