Opened 14 years ago
Closed 11 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)
Change History (27)
Changed 14 years ago by
Attachment: | signal_deco_patch.diff added |
---|
comment:1 Changed 14 years ago by
Component: | Uncategorized → Core framework |
---|
comment:2 Changed 14 years ago by
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.
comment:3 Changed 14 years ago by
Owner: | changed from nobody to zvoase |
---|---|
Patch needs improvement: | unset |
Status: | new → 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 14 years ago by
Triage Stage: | Unreviewed → Accepted |
---|
comment:6 Changed 13 years ago by
Needs tests: | set |
---|
Marking as Needs tests. I'd love to see this get into 1.2.
comment:7 Changed 13 years ago by
Needs documentation: | set |
---|
Also marking as "Needs documentation", since this affects the public api
comment:8 Changed 13 years ago by
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 13 years ago by
Untested example:
def connect(signal, **kwargs): def receive(receiver): signal.connect(receiver, **kwargs) return receiver return receive
comment:10 Changed 12 years ago by
Owner: | changed from zvoase to Brian Rosner |
---|---|
Status: | assigned → new |
I've made a slightly new patch and started discussion at http://groups.google.com/group/django-developers/msg/eb8cd203df82ad5a
comment:11 Changed 12 years ago by
Status: | new → assigned |
---|
comment:12 Changed 12 years ago by
Cc: | semente+djangoproject@… added |
---|
comment:13 Changed 12 years ago by
Cc: | john+djangoproject@… added |
---|
comment:14 Changed 12 years ago by
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 12 years ago by
Needs documentation: | unset |
---|---|
Needs tests: | unset |
Triage Stage: | Accepted → Ready for checkin |
comment:16 Changed 12 years ago by
Removed the needs tests and docs flags referring to brosner's patch.
comment:17 Changed 12 years ago by
Triage Stage: | Ready for checkin → Accepted |
---|
Moving to accepted until I can get my patch in.
comment:18 Changed 12 years ago by
Resolution: | → fixed |
---|---|
Status: | assigned → closed |
comment:19 follow-up: 20 Changed 12 years ago by
Patch needs improvement: | set |
---|---|
Resolution: | fixed |
Status: | closed → reopened |
Triage Stage: | Accepted → 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 Changed 12 years ago by
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 11 years ago by
Severity: | → Normal |
---|---|
Type: | → New feature |
Changed 11 years ago by
Attachment: | cc63e2f848524b3e5a994a85891f8212158c13b4.patch added |
---|
Enabling the connect to act as a decorator
comment:22 Changed 11 years ago by
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 11 years ago by
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 11 years ago by
Resolution: | → wontfix |
---|---|
Status: | reopened → closed |
Patch-v1.diff