django temporarily disable signals
Asked Answered
P

4

22

I have a signal callback in django:

@receiver(post_save, sender=MediumCategory)
def update_category_descendants(sender, **kwargs):
    
    def children_for(category):
        return MediumCategory.objects.filter(parent=category)
    
    def do_update_descendants(category):
        children = children_for(category)
        descendants = list() + list(children)
        
        for descendants_part in [do_update_descendants(child) for child in children]:
            descendants += descendants_part
        
        category.descendants.clear()
        for descendant in descendants:
            if category and not (descendant in category.descendants.all()):
                category.descendants.add(descendant)
                category.save()
        return list(descendants)
    
    # call it for update
    do_update_descendants(None)

...but in the signal handler's body I'm using .save() on the same model MediumCategory. This causes the signal to be dispatched again. How can I disable it?

The perfect solution would be a with statement with some 'magic' inside.

UPDATE: Here is my final solution, if anyone interested:

class MediumCategory(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(blank=True)
    parent = models.ForeignKey('self', blank=True, null=True)
    parameters = models.ManyToManyField(AdvertisementDescriptonParameter, blank=True)
    count_mediums = models.PositiveIntegerField(default=0)
    count_ads = models.PositiveIntegerField(default=0)
    
    descendants = models.ManyToManyField('self', blank=True, null=True)
    
    def save(self, *args, **kwargs):
        self.slug = slugify(self.name)
        super(MediumCategory, self).save(*args, **kwargs)
    
    def __unicode__(self):
        return unicode(self.name)
(...)
@receiver(post_save, sender=MediumCategory)
def update_category_descendants(sender=None, **kwargs):
    def children_for(category):
        return MediumCategory.objects.filter(parent=category)
    
    def do_update_descendants(category):
        children = children_for(category)
        descendants = list() + list(children)
        
        for descendants_part in [do_update_descendants(child) for child in children]:
            descendants += descendants_part
        
        if category:
            category.descendants.clear()
            for descendant in descendants:
                category.descendants.add(descendant)
        return list(descendants)
    
    # call it for update
    do_update_descendants(None)
Pyxidium answered 14/7, 2012 at 20:23 Comment(1)
I found an answer and I wrote it here. Hope can help someone :)Hydra
U
25

Perhaps I'm wrong, but I think that category.save() is not needed in your code, add() is enough because change is made in descendant but in category.

Also, to avoid signals you can:

  • Disconnect signal and reconnect.
  • Use update: Descendant.objects.filter( pk = descendant.pk ).update( category = category )
Understate answered 14/7, 2012 at 20:42 Comment(3)
ok, that's what I was looking for: disconnect is the solution, putting it into with statement is a matter of purity :) But after removing save(), disconnect is not needed. Perfect.Pyxidium
and make disconnect + reconnect atomically consistent, no matter what exception appears after disconnection. Also, do you care about other threads?Saraann
Here's the guide: blog.theletstream.com/…Allegedly
A
21

To disable a signal on your model, a simple way to go is to set an attribute on the current instance to prevent upcoming signals firing.

This can be done using a simple decorator that checks if the given instance has the 'skip_signal' attribute, and if so prevents the method from being called:

from functools import wraps

def skip_signal():
    def _skip_signal(signal_func):
        @wraps(signal_func)
        def _decorator(sender, instance, **kwargs):
            if hasattr(instance, 'skip_signal'):
                return None
            return signal_func(sender, instance, **kwargs)  
        return _decorator
    return _skip_signal

You can now use it this way:

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=MyModel)
@skip_signal()
def my_model_post_save(sender, instance, **kwargs):
    instance.some_field = my_value
    # Here we flag the instance with 'skip_signal'
    # and my_model_post_save won't be called again
    # thanks to our decorator, avoiding any signal recursion
    instance.skip_signal  = True
    instance.save()

Hope This helps.

Affectional answered 27/11, 2014 at 0:30 Comment(6)
Why is it not DRY and consistent?Chromolithography
@Chromolithography ig there was some exception after signal disconnection and before reconnection, then signal will be left disconnected which can be fatal for your application.Paraformaldehyde
Doesnt Dry mean Don't Repeat Yourself?Chromolithography
If disconnecting a signal was not DRY, I don't think Django would offer a way to disconnect them. In my use case, I am performing some updates inside a signal that end up re-triggering the signal. I want to prevent that from being recursive.Tonsil
@Tonsil you are right, my comment was lacking specificity, I edited my answer to remove the « DRY » comment.Affectional
If you are connecting signals using connect the skip_decorator overrides the firing of signal and hence signals are not fired.Paleethnology
E
2

Here is solution to temporary disable signal receiver per instance which allows to use it on production (bc it is thread-safe)

[usage.py]

from django.db.models.signals import post_save

payment = Payment()
with mute_signals_for(payment, signals=[post_save]):
   payment.save()  # handle_payment signal receiver will be skipped

[code.py]

from contextlib import contextmanager
from functools import wraps

MUTE_SIGNALS_ATTR = '_mute_signals'


def mutable_signal_receiver(func):
    """Decorator for signals to allow to skip them by setting attr MUTE_SIGNALS_ATTR on instance,
    which can be done via mute_signals_for"""
    @wraps(func)
    def wrapper(sender, instance, signal, **kwargs):
        mute_signals = getattr(instance, MUTE_SIGNALS_ATTR, False)
        if mute_signals is True:
            pass  # skip all signals
        elif isinstance(mute_signals, list) and signal in mute_signals:
            pass  # skip user requested signal
        else:  # allow signal receiver
            return func(sender=sender, instance=instance, signal=signal, **kwargs)
    return wrapper


@contextmanager
def mute_signals_for(instance, signals):
    """Context manager to skip signals for @instance (django model), @signals can be
    True to skip all signals or list of specified signals, like [post_delete, post_save] """
    try:
        yield setattr(instance, MUTE_SIGNALS_ATTR, signals)
    finally:
        setattr(instance, MUTE_SIGNALS_ATTR, False)

[signals.py]

@receiver(post_save, sender=Payment, dispatch_uid='post_payment_signal')
@mutable_signal_receiver
def handle_payment(sender, instance, created, **kwargs):
    """called after payment is registered in the system."""
Encrust answered 19/4, 2022 at 12:55 Comment(2)
Hi @pymen. Thank you for this idea of muting signals. I've been trying to make your code working and to me it seems like there's an issue in mutable_signal_receiver method, namely at condition elif isinstance(mute_signals, list) and signal in mute_signals: where is signal compared with items of passed list. It's comparison between python function reference and django.db.models.signals.ModelSignal and condition is never met. So for now I'm simply compairing __name__ variables of both func parameter and items of mute_signals. Don't you have this issue?Woodenhead
@MichalPůlpán you are right, i have fixed the usage example to pass post_save to signals which should be muted. Same approach could be applied to signal handlers, just need to use different attribute like MUTE_HANDLERS_ATTREncrust
Z
1

Where the previous answers demonstrate how to do this without external libraries, django-model-utils offers a clean way to do exactly this. The advantage of the library is that the code is explicit. Performing a bulk_create or filter followed by an update doesn't make it clear that you want to disable the signal. Another advantage is that the save() method may perform additional validation.

from model_utils.models import SaveSignalHandlingModel

class Film(SaveSignalHandlingModel):
    title = models.CharField(max_length=100)

film = Film(title="Cidade de Deus")
film.save(signals_to_disable=["post_save"])

See the post here: https://django.wtf/blog/disable-django-model-signals/

Zirconia answered 5/4, 2023 at 14:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.