Opened 3 years ago

Last modified 3 years ago

#6681 enhancement new

'after' decorator, for doing things after other things

Reported by: glyph Owned by:
Priority: normal Milestone:
Component: core Keywords:
Cc: Branch:
Author:

Description

Today, in Twisted, if you want to add a callback to a Deferred, there are multiple possible idioms. There's this:

foo().addCallback(lambda bar: baz())

but of course that only works if your Python code fits into an expression, which it rarely does. So then there's this:

def onFoo(result):
    ...
foo().addCallback(onFoo)

which is serviceable enough, but can be confusing, especially if onFoo is more than a couple of lines long. It's not clear that 'foo' is going to happen until 'onFoo' has already been designed. So then there's this:

d = foo()
def onFoo(result):
    ...
d.addCallback(onFoo)

and that's a bit more straightforward, but now you've had to write an additional line of code, come up with an additional temporary name (inevitably: d, d1, d2, d3), and pollute your namespace with two names that you're not going to use any more after the addCallback call.

It would be nice to use a decorator for this, but:

@foo().addCallback
def onFoo(result):
    ...

is a syntax error, because I guess Python doesn't have a real parser and can't tell what an expression is? So, I've started resorting to this, in some places:

from twisted.internet.defer import passthru
@passthru(foo().addCallback)
def onFoo(result):
    ...

and that works if you know what the point is, but it is somewhat obscure.

I propose a new function in twisted.internet.defer, after, so that we can get the benefits of a decorator (fewer redundant temporary variables, operation comes lexically before the consequences of the operation) with better readability than the passthru hack.

This would be used like so:

from twisted.internet.defer import after

@after(foo())
def fooDone(result):
    ...

The after decorator might be trivially implemented like so:

def after(deferred):
    return deferred.addCallback

Although perhaps a more robust implementation would return the function itself, to reduce confusion; I'm open to suggestions there.

This does beg for some symmetry with error handling, which could be used like so:

@after(foo())
def fooDone(result):
    ...
@catch(ZeroDivisionError, fooDone)
def fooOops(why):
    ...

which could then be implemented like so:

def catch(exceptionType, deferred):
    def addIt(handler):
        def errback(failure):
            failure.trap(exceptionType)
            return handler(failure)
        return handler.addErrback(errback)
    return addIt

The implementation here is trivially easy, but I think we should have some good consensus before heading in. I know some people feel this syntax is gross, and I'd like to get some actual explanation of why.

Change History (2)

comment:1 Changed 3 years ago by therve

I dislike the syntax for the same reason I don't like property setter using @x.setter (http://docs.python.org/2.7/library/functions.html#property). This is very much a gut feeling though, so I'm not sure it's good to communicate. At the very least, I wouldn't call it "after", but "callback", and not "catch", but "errback".

Spiv made an interesting comment on IRC too, stating: "adding yet another set of idioms does on the face of it seem like a poor idea without some large value". If your core argument is to make callbacks easier, this is just another way to do things, and it just seems marginally better.

comment:2 Changed 3 years ago by radix

I don't like the weirdness of the decorator returning something other than the [wrapped] function.

passing the *previously defined function* to catch() as the second argument is particularly weird to me.

I think I have the same reservations as therve and spiv, as well. I'm not really sure it's worth it.

Note: See TracTickets for help on using tickets.