Signals

Table of contents

  1. Introduction
  2. Defining and sending a signal
  3. Connecting to a signal
  4. Letting a listener veto
  5. Reacting inside the same transaction
  6. Declaring signals in the contract
  7. Seeing who sends and who listens
  8. Signals or template hooks
  9. Reference
    1. The registry
  10. See also

Introduction

A signal lets one feature say that something happened, and lets other features react, without either importing the other. Use one when a change in your feature should trigger work that belongs to someone else, such as counting a new note in a report.

# notes/signals.py
from splent_framework.signals.signal_utils import define_signal

note_created = define_signal("note-created", "splent_feature_notes")
# reports/signals.py
from splent_framework.signals.signal_utils import connect_signal


@connect_signal("note-created", "splent_feature_reports")
def on_note_created(sender, note, **kwargs):
    db.session.add(ReportEntry(note_id=note.id))
    db.session.commit()

Without reports in the product, the signal fires and nobody listens. Notes never breaks.

A producer sends a signal and every listener answers PRODUCER REGISTRY LISTENERS notes signals.py define_signal(...) routes.py note_submitting.send() note-submitting provided by notes listeners recorded a blinker NamedSignal spam_filter @connect_signal(...) a feature of yours @connect_signal(...) each listener's answer comes back, and a False vetoes the note A listener that loads before its producer waits in a pending list, and define_signal attaches it. Without the producer installed, the listener never fires and nothing fails.
Neither side imports the other. The registry knows the provider and the listeners of each signal, which is what product:signals prints.

Defining and sending a signal

from flask import current_app

from splent_io.splent_feature_notes.signals import note_created

note = self.create(title=title, content=content)
note_created.send(current_app._get_current_object(), note=note)

define_signal(name, provider) creates a blinker signal and records your feature as its provider. Send it with .send(), a sender and any keyword arguments your listeners need.

Put your definitions in the feature’s signals.py. The framework imports it while it loads the feature, so everything is wired at start. feature:create scaffolds one with commented examples.

Connecting to a signal

from splent_framework.signals.signal_utils import connect_signal


@connect_signal("note-created", "splent_feature_reports")
def on_note_created(sender, note, **kwargs):
    ...

connect_signal(signal_name, listener_feature) takes the signal name and your feature’s package name. Accept **kwargs, so a producer can add arguments without breaking you.

Load order does not matter. A listener that loads before its producer waits, and define_signal attaches it later. If the producer is not in the product, the listener never fires and nothing fails.

Letting a listener veto

results = note_submitting.send(None, form=request.form, remoteip=request.remote_addr)
if any(value is False for _, value in results):
    flash("Your note was not accepted.")
    return redirect(request.referrer)

.send() returns a list of (receiver, return_value) pairs, so listeners can answer. Send a signal before you store something, and refuse when any listener returns False.

@connect_signal("note-submitting", "splent_feature_spam_filter")
def check_note(sender, form=None, remoteip=None, **kwargs):
    return check("note", form, remoteip)

A spam filter answers False for spam and None to let it through. The form never knows the filter is there.

Reacting inside the same transaction

@connect_signal("note-deleting", "splent_feature_reports")
def delete_entries_of_deleted_note(sender, note, session, **kwargs):
    for entry in session.query(ReportEntry).filter_by(note_id=note.id).all():
        session.delete(entry)

A producer may send a signal from a SQLAlchemy before_flush listener, with the row and the session. The signal then covers every way the row changes, from the feature’s own pages, the admin panel or the terminal.

Write through that session and never commit. Your rows go in the same transaction, and a rollback undoes both. For work outside the database, such as a mail, enqueue a task, which waits for the commit and is dropped on a rollback.

A bulk query.delete() or query.update() skips the session, so it sends no such signal.

Declaring signals in the contract

[tool.splent.contract.provides]
signals = ["note-created", "note-deleting", "note-submitting"]
[tool.splent.contract.requires]
signals = ["note-submitting", "order-submitting"]

The contract in your feature’s pyproject.toml lists the signals it sends and the ones it listens to. You rarely write it by hand.

splent feature:contract splent_feature_notes --write

feature:contract reads the define_signal and connect_signal calls in your signals.py, shows what differs from the declaration, and --write updates it. feature:xray --validate reports two features that provide the same signal.

Nothing checks that a product provides every signal its features require. A listener whose signal nobody defines is harmless, it never fires.

Seeing who sends and who listens

splent product:signals
  SIGNAL             EMITTED BY            LISTENERS
  ────────────────────────────────────────────────────────────────────
  note-created       splent_feature_notes  splent_feature_reports
  note-deleting      splent_feature_notes  splent_feature_reports
  note-submitting    splent_feature_notes  splent_feature_spam_filter

Signals or template hooks

Use a signal to trigger work in another feature, and a template hook to add HTML to a shared page.

  Signals Template hooks
For Business events UI extension points
Runs during Request handling and service calls Template rendering
API define_signal, connect_signal register_template_hook, get_template_hooks

Reference

The registry

splent_framework.signals.registry keeps every signal with its provider and listeners. product:signals reads it.

Function Description
register_signal(name, signal, provider) Records a signal definition
register_listener(signal_name, listener) Records that a feature listens to a signal
get_signal(name) A registered signal by name, or None
get_registry() The whole registry
clear_registry() Empties it, for test teardown

See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es