Signals
All signal utilities are in pragmatic.signals.
SignalsHelper
A utility class for deferring side-effect functions until after a model signal
fires. This avoids race conditions caused by executing related logic (e.g.
sending notifications) inside a pre_save handler before the transaction
commits.
add_task_and_connect
Register a function to be called after a signal fires on a specific instance.
from pragmatic.signals import SignalsHelper
from django.db.models.signals import post_save
def pre_save_handler(sender, instance, **kwargs):
# Schedule notification to run after the record is saved
SignalsHelper.add_task_and_connect(
sender=sender,
instance=instance,
func=send_notification,
arguments=(instance.pk,),
signal_type='post_save', # 'post_save' | 'post_delete' | 'm2m_changed'
)
post_save.connect(pre_save_handler, sender=MyModel)
execute_instance_tasks is called by the connected receiver and processes
all queued tasks for the instance, then clears the queue.
attribute_changed
Compares the current in-memory instance against the database to detect field
changes. Useful in pre_save handlers.
from pragmatic.signals import SignalsHelper
def my_pre_save(sender, instance, **kwargs):
if SignalsHelper.attribute_changed(instance, diff_fields=['status']):
# status was changed
...
# Check specific value transitions
if SignalsHelper.attribute_changed(
instance,
diff_fields=['status'],
diff_contains={'status': {'from': ['draft'], 'to': ['published']}}
):
# status changed from draft → published
...
Parameters of attribute_changed:
instance— unsaved model instancediff_fields— list of field names to checkdiff_contains— optional dict constraining which values trigger aTrueresult; accepts either a list (any match) or a{'from': [...], 'to': [...]}dictobj_exists— whenTrue, returnsFalsefor new (unsaved) objects instead ofTrue
Returns True if any of the listed fields changed (within the optional
constraints), or if the object did not previously exist (and obj_exists
is False).
get_db_instance
Fetches the currently persisted version of an instance from the database.
db_instance = SignalsHelper.get_db_instance(instance)
if db_instance and db_instance.status != instance.status:
...
Returns None if the object does not yet exist in the database.
APM Integration
apm_custom_context
A decorator that attaches signal/task context to an active Elastic APM
transaction. Silently no-ops if elastic-apm is not installed.
from pragmatic.signals import apm_custom_context
@apm_custom_context('signals')
def my_signal_handler(sender, instance, **kwargs):
...
@apm_custom_context('tasks')
def my_task(arg1, arg2):
...
Types: 'signals' (reads instance from kwargs) and 'tasks'
(logs the call arguments).
Context Managers
temporary_disconnect_signal
Temporarily disconnects a signal receiver and reconnects it on exit. Safe for use in tests or bulk-import scripts.
from pragmatic.signals import temporary_disconnect_signal
from django.db.models.signals import post_save
with temporary_disconnect_signal(
signal=post_save,
receiver=my_handler,
sender=MyModel,
dispatch_uid='my_uid', # optional
):
MyModel.objects.bulk_create(objects)
The receiver is only disconnected if it was actually connected when the context is entered; it is always reconnected on exit.
disable_signals
Disables a set of Django model signals for the duration of a block. By default
disables all standard signals (pre_init, post_init, pre_save,
post_save, pre_delete, post_delete, pre_migrate,
post_migrate, m2m_changed).
from pragmatic.signals import disable_signals
# Disable all signals
with disable_signals():
do_bulk_operation()
# Disable only specific signals
with disable_signals(disabled_signals=[post_save, pre_save]):
do_bulk_operation()
# Keep only specific signals enabled
with disable_signals(enabled_signals=[post_save]):
do_bulk_operation()
# Disable specific receivers by name
with disable_signals(disabled_receviers=['my_expensive_receiver']):
do_bulk_operation()