Opened 16 years ago

Closed 13 years ago

#9015 closed New feature (wontfix)

Signal Connection Decorators

Reported by: zvoase Owned by: Brian Rosner
Component: Core (Other) Version: dev
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 16 years ago.
Patch-v1.diff
signal_connection_patch.2.diff (1.1 KB ) - added by zvoase 16 years ago.
Patch-v2.diff
cc63e2f848524b3e5a994a85891f8212158c13b4.patch (9.1 KB ) - added by Jannis Leidel 14 years ago.
Enabling the connect to act as a decorator

Download all attachments as: .zip

Change History (27)

by zvoase, 16 years ago

Attachment: signal_deco_patch.diff added

Patch-v1.diff

comment:1 by Ivan Giuliani, 16 years ago

Component: UncategorizedCore framework

comment:2 by zvoase, 16 years ago

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.

by zvoase, 16 years ago

Patch-v2.diff

comment:3 by zvoase, 16 years ago

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 by Jacob, 16 years ago

Triage Stage: UnreviewedAccepted

comment:5 by (none), 16 years ago

milestone: post-1.0

Milestone post-1.0 deleted

comment:6 by Ben Davis, 15 years ago

Needs tests: set

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

comment:7 by Ben Davis, 15 years ago

Needs documentation: set

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

comment:8 by anon, 15 years ago

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 by anonymous, 15 years ago

Untested example:

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

comment:10 by Brian Rosner, 15 years ago

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 by Brian Rosner, 15 years ago

Status: newassigned

comment:12 by Guilherme Gondim (semente) <semente@…>, 15 years ago

Cc: semente+djangoproject@… added

comment:13 by anonymous, 14 years ago

Cc: john+djangoproject@… added

comment:14 by George Sakkis, 14 years ago

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 by Jannis Leidel, 14 years ago

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

comment:16 by Jannis Leidel, 14 years ago

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

comment:17 by Brian Rosner, 14 years ago

Triage Stage: Ready for checkinAccepted

Moving to accepted until I can get my patch in.

comment:18 by Brian Rosner, 14 years ago

Resolution: fixed
Status: assignedclosed

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

comment:19 by anonymous, 14 years ago

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?

in reply to:  19 comment:20 by Jannis Leidel, 14 years ago

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 by Luke Plant, 14 years ago

Severity: Normal
Type: New feature

by Jannis Leidel, 14 years ago

Enabling the connect to act as a decorator

comment:22 by Jannis Leidel, 14 years ago

Easy pickings: unset

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

comment:23 by Aymeric Augustin, 13 years ago

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 by Aymeric Augustin, 13 years ago

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