Code

Opened 6 years ago

Closed 3 years ago

#9015 closed New feature (wontfix)

Signal Connection Decorators

Reported by: zvoase Owned by: brosner
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 6 years ago.
Patch-v1.diff
signal_connection_patch.2.diff (1.1 KB) - added by zvoase 6 years ago.
Patch-v2.diff
cc63e2f848524b3e5a994a85891f8212158c13b4.patch (9.1 KB) - added by jezdez 3 years ago.
Enabling the connect to act as a decorator

Download all attachments as: .zip

Change History (27)

Changed 6 years ago by zvoase

Patch-v1.diff

comment:1 Changed 6 years ago by kratorius

  • Component changed from Uncategorized to Core framework
  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

comment:2 Changed 6 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 6 years ago by zvoase

Patch-v2.diff

comment:3 Changed 6 years ago by zvoase

  • Owner changed from nobody to zvoase
  • Patch needs improvement unset
  • Status changed from new to assigned

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 6 years ago by jacob

  • Triage Stage changed from Unreviewed to Accepted

comment:5 Changed 5 years ago by anonymous

  • milestone post-1.0 deleted

Milestone post-1.0 deleted

comment:6 Changed 5 years ago by bendavis78

  • Needs tests set

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

comment:7 Changed 5 years ago by bendavis78

  • Needs documentation set

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

comment:8 Changed 5 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 5 years ago by anonymous

Untested example:

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

comment:10 Changed 4 years ago by brosner

  • Owner changed from zvoase to brosner
  • Status changed from assigned to new

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

comment:11 Changed 4 years ago by brosner

  • Status changed from new to assigned

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

  • Cc semente+djangoproject@… added

comment:13 Changed 4 years ago by anonymous

  • Cc john+djangoproject@… added

comment:14 Changed 4 years ago by gsakkis

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 4 years ago by jezdez

  • Needs documentation unset
  • Needs tests unset
  • Triage Stage changed from Accepted to Ready for checkin

comment:16 Changed 4 years ago by jezdez

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

comment:17 Changed 4 years ago by brosner

  • Triage Stage changed from Ready for checkin to Accepted

Moving to accepted until I can get my patch in.

comment:18 Changed 4 years ago by brosner

  • Resolution set to fixed
  • Status changed from assigned to closed

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

comment:19 follow-up: Changed 4 years ago by anonymous

  • Patch needs improvement set
  • Resolution fixed deleted
  • Status changed from closed to reopened
  • Triage Stage changed from Accepted to Design 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 4 years ago by jezdez

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 3 years ago by lukeplant

  • Severity set to Normal
  • Type set to New feature

Changed 3 years ago by jezdez

Enabling the connect to act as a decorator

comment:22 Changed 3 years ago by jezdez

  • 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 3 years ago by aaugustin

  • 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 3 years ago by aaugustin

  • Resolution set to wontfix
  • Status changed from reopened to closed

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.