Opened 8 years ago

Closed 5 years ago

#9015 closed New feature (wontfix)

Signal Connection Decorators

Reported by: zvoase Owned by: Brian Rosner
Component: Core (Other) Version: master
Severity: Normal Keywords: signals
Cc: semente+djangoproject@…, john+djangoproject@… Triage Stage: Design decision needed
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

Usually, signal receivers are defined as functions and then connected to a specific signal via a function call to the signal instance's connect method. The problem is, this registration of the receiver is located outside the receiver's definition. This can cause clutter, it violates DRY, and it is not very Pythonic in style. Several examples of the current usage pattern are included in the signal docs, so I don't need to go into too much detail.

I propose a change to the connect method on the django.dispatch.dispatcher.Signal class, which would allow you to decorate a signal receiver function and therefore skip the explicit call to the method. The usage of the new method would look something like this:

from django.db.models.signals import pre_save # Just an example of a signal.

@pre_save.connect(sender=MyModel) # Other keyword arguments may be given.
def receiver(sender, instance, *args, **kwargs):
    pass # Do something here.

I've written a patch which does exactly this, preserving backwards compatibility also (although that's not the biggest issue right now; signals are a very recent addition). It works by moving the current Signal.connect method to Signal._connect, replacing it with a small wrapper around the original which, when called, does one of two things:

  • If called with positional arguments (and, optionally, keyword arguments), it behaves like the old Signal.connect, connecting the given receiver function to the signal. This allows it to be used as both a decorator and a registration function.
  • If called with keyword arguments but no positional arguments, it returns a wrapper function, allowing it to decorate the receiver function and include the given keyword arguments (thanks to Python's lexical scoping).

This means it will work like so (with the effects easy to determine from the code):

from django.db.models.signals import pre_save

# Decorator with no args.
@pre_save.connect
def recvr1(sender, instance, *args, **kwargs):
    pass # ...

# Registration with only the receiver.
pre_save.connect(recvr1)

# Decorator with keyword args.
@pre_save.connect(sender=MyModel)
def recvr2(sender, instance, *args, **kwargs):
    pass # ...

# Registration with both receiver and keyword args.
pre_save.connect(recvr2, sender=MyModel)

Attachments (3)

signal_deco_patch.diff (6.5 KB) - added by zvoase 8 years ago.
Patch-v1.diff
signal_connection_patch.2.diff (1.1 KB) - added by zvoase 8 years ago.
Patch-v2.diff
cc63e2f848524b3e5a994a85891f8212158c13b4.patch (9.1 KB) - added by Jannis Leidel 6 years ago.
Enabling the connect to act as a decorator

Download all attachments as: .zip

Change History (27)

Changed 8 years ago by zvoase

Attachment: signal_deco_patch.diff added

Patch-v1.diff

comment:1 Changed 8 years ago by Ivan Giuliani

Component: UncategorizedCore framework

comment:2 Changed 8 years ago by zvoase

Patch needs improvement: set

I've just realised that the patch is not from the top of the source tree. I'm on the verge of kicking myself.

Will submit a better patch promptly.

Changed 8 years ago by zvoase

Patch-v2.diff

comment:3 Changed 8 years ago by zvoase

Owner: changed from nobody to zvoase
Patch needs improvement: unset
Status: newassigned

Submitted new patch, claimed the ticket, and made the executive decision not to move the 'connect' method around, in the interest of not (potentially) screwing things up.

comment:4 Changed 8 years ago by Jacob

Triage Stage: UnreviewedAccepted

comment:5 Changed 8 years ago by (none)

milestone: post-1.0

Milestone post-1.0 deleted

comment:6 Changed 7 years ago by Ben Davis

Needs tests: set

Marking as Needs tests. I'd love to see this get into 1.2.

comment:7 Changed 7 years ago by Ben Davis

Needs documentation: set

Also marking as "Needs documentation", since this affects the public api

comment:8 Changed 7 years ago by anon

wouldn't it be nicer to have the decorator like this?

from django.db.models.signals import connect, pre_save

# Decorator with no args.
@connect(pre_save)
def recvr1(sender, instance, *args, **kwargs):
    pass # ...

# Decorator with keyword args.
@connect(pre_save, sender=MyModel)
def recvr2(sender, instance, *args, **kwargs):
    pass # ...

comment:9 Changed 7 years ago by anonymous

Untested example:

def connect(signal, **kwargs):
	def receive(receiver):
		signal.connect(receiver, **kwargs)
		return receiver
	return receive

comment:10 Changed 7 years ago by Brian Rosner

Owner: changed from zvoase to Brian Rosner
Status: assignednew

I've made a slightly new patch and started discussion at http://groups.google.com/group/django-developers/msg/eb8cd203df82ad5a

comment:11 Changed 7 years ago by Brian Rosner

Status: newassigned

comment:12 Changed 7 years ago by Guilherme Gondim (semente) <semente@…>

Cc: semente+djangoproject@… added

comment:13 Changed 7 years ago by anonymous

Cc: john+djangoproject@… added

comment:14 Changed 6 years ago by George Sakkis

I use a slightly more general version of the one posted on 10/04/09 that allows multiple signals at once:

def connect(*signals, **kwds):
    ...

@connect(post_save, post_delete, sender=MyModel)
def delete_stale_cache(sender, instance, **kwds):
    cache.delete('mymodel_%s' % instance.pk)

comment:15 Changed 6 years ago by Jannis Leidel

Needs documentation: unset
Needs tests: unset
Triage Stage: AcceptedReady for checkin

comment:16 Changed 6 years ago by Jannis Leidel

Removed the needs tests and docs flags referring to brosner's patch.

comment:17 Changed 6 years ago by Brian Rosner

Triage Stage: Ready for checkinAccepted

Moving to accepted until I can get my patch in.

comment:18 Changed 6 years ago by Brian Rosner

Resolution: fixed
Status: assignedclosed

(In [13773]) Fixed #9015 -- added a signal decorator for simplifying signal connections

comment:19 Changed 6 years ago by anonymous

Patch needs improvement: set
Resolution: fixed
Status: closedreopened
Triage Stage: AcceptedDesign decision needed

Function decorators usually named in the form of a verb - cache_page, require_POST, etc. - so why is this decorator named receiver instead of connect or receive_signal?

comment:20 in reply to:  19 Changed 6 years ago by Jannis Leidel

Replying to anonymous:

Function decorators usually named in the form of a verb - cache_page, require_POST, etc. - so why is this decorator named receiver instead of connect or receive_signal?

I have to say, that's a valid point, e.g. this looks nicer to me, too:

@receive_signal(post_login)
def update_last_seen(sender, user, **kwargs):
    user.last_seen = datetime.now()
    user.save()

comment:21 Changed 6 years ago by Luke Plant

Severity: Normal
Type: New feature

Changed 6 years ago by Jannis Leidel

Enabling the connect to act as a decorator

comment:22 Changed 6 years ago by Jannis Leidel

Easy pickings: unset

I've pushed a small update to my branch at https://github.com/jezdez/django/tree/feature%2Fsignal-decorator

comment:23 Changed 5 years ago by Aymeric Augustin

UI/UX: unset

1.3 was released with @receiver. It doesn't seem worth changing the API now that it has gone public.

comment:24 Changed 5 years ago by Aymeric Augustin

Resolution: wontfix
Status: reopenedclosed
Note: See TracTickets for help on using tickets.
Back to Top