Signals
Table of contents
- Introduction
- Defining and sending a signal
- Connecting to a signal
- Letting a listener veto
- Reacting inside the same transaction
- Declaring signals in the contract
- Seeing who sends and who listens
- Signals or template hooks
- Reference
- 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.
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()orquery.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
- Template system, template hooks for the UI
- Feature loading, how
signals.pyis imported product:signalsandfeature:contract