Question

I am working on a notification app in Django 1.6 and I want to pass additional arguments to Django signals such as post_save. I tried to use partial from functools but no luck.

from functools import partial
post_save.connect(
    receiver=partial(notify,
        fragment_name="categories_index"),
            sender=nt.get_model(),
            dispatch_uid=nt.sender
    )

notify function has a keyword argument fragment_name which I want to pass as default in my signals.

Any suggestions?

Was it helpful?

Solution 2

Your attempt with partial isn't working because by default these receivers are connected using a weak reference.

According to the Django docs:

Django stores signal handlers as weak references by default, so if your handler is a local function, it may be garbage collected. To prevent this, pass weak=False when you call the signal’s connect().

from functools import partial
post_save.connect(
    receiver=partial(notify,
        fragment_name="categories_index"),
            sender=nt.get_model(),
            dispatch_uid=nt.sender,
            weak=False
    )

Include weak=False and this partial won't be garbage collected.

My original answer is below and took an approach that wasn't using partial.

You could decorate your post save function prior to connecting it with the post_save receiver.

from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, post_delete

def extra_args(fragment_name, *args, **kwargs):
    def inner1(f, *args, **kwargs):
        def inner2(sender, instance, **kwargs):
            f(sender, instance, fragment_name=fragment_name, **kwargs)
        return inner2
    return inner1

@receiver(post_save, sender=ExampleModel)
@extra_args(fragment_name="categories_index")
def my_post_save(sender, instance, fragment_name, **kwargs):
    print "fragment_name : ", fragment_name
    #rest of post save...

The extra inner in extra_args is for decorators that take parameters.

If you want to do this programmatically this works the same way but note that you need to include weak=False to have the wrapped function not be garbage collected.

receiver(post_save, sender=aSenderClass, weak=False)(extra_args(fragment_name="meep")(my_post_save))

Or without wrapping, but calling post_save.connect like your original attempt with partial

post_save.connect(extra_args(fragment_name="meepConnect")(my_post_save), sender=Author, weak=False)

OTHER TIPS

You can define additional arguments in custom save method of model like this:

class MyModel(models.Model):
    ....

    def save(self, *args, **kwargs):
        super(MyModel, self).save(*args, **kwargs)
        self.my_extra_param = 'hello world'

And access this additional argument through instance in post_save signal receiver:

@receiver(post_save, sender=MyModel)
def process_my_param(sender, instance, *args, **kwargs):
    my_extra_param = instance.my_extra_param

I tried Eugene Soldatov's answer, but it made me realize it could be much simpler:

You could have something like:

obj = MyModel.objects.first()
obj.my_extra_param = "hello world"
obj.save() # this will trigger the signal call

and then have the receiver like in Eugene's answer, and it would work all the same.

@receiver(post_save, sender=MyModel)
def process_my_param(sender, instance, *args, **kwargs):
    my_extra_param = instance.my_extra_param

No need to create a custom save method prone to bugs.

This is how it currently works in Django 3.0. I haven't tried prior versions.

Why this happens? Good ol' documentation has the answer for you: https://docs.djangoproject.com/en/3.2/ref/models/instances/#what-happens-when-you-save

If the predefined signals are not suitable, you can always define your own.

import django.dispatch

custom_post_save = django.dispatch.Signal(providing_args=[
    "sender", "instance", "created", "raw", "using", "update_fields", "fragment_name"
])

Then in your Model you just have to override save() method:

from django.db import router

class YourModel(Model):

    # Your fields and methods

    def save(self, force_insert=False, force_update=False, using=None,
         update_fields=None):
         custom_signal_kwargs = {
             "sender": self.__class__,
             "instance": self,
             "created": self.pk is None,
             "raw": False, # As docs say, it's True only for fixture loading
             "using": using or router.db_for_write(self.__class__, instance=self),
             "update_fields": update_fields,
             "fragment_name": "categories_index" # The thing you want
         }
         super(YourModel, self).save(force_insert=False, force_update=False, using=None,
             update_fields=None)
         custom_post_save.send(**custom_signal_kwargs) # Send custom signal

Now you just have to connect this custom signal to your notify(...) receiver and it will get fragment_name in kwargs.

The code in Django responsible for Signals is defined here https://github.com/django/django/blob/master/django/dispatch/dispatcher.py. See how it inspects the receiver? I suspect your problems lie there. Maybe what you want is a wrapper function that honors the arguments a signal needs to have but also sets the value of fragment_name.

def fragment_receiver(sender, **kwargs)
    return notify(sender, fragment_name="categories_index", **kwargs)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top