A Python metaclass for Twisted allowing __init__ to return a Deferred
OK, I admit, this is geeky.
But we’ve all run into the situation in which you’re using Python and Twisted, and you’re writing a new class and you want to call something from the __init
method that returns a Deferred. This is a problem. The __init
method is not allowed to return a value, let alone a Deferred. While you could just call the Deferred-returning function from inside your __init
, there’s no guarantee of when that Deferred will fire. Seeing as you’re in your __init
method, it’s a good bet that you need that function to have done its thing before you let anyone get their hands on an instance of your class.
For example, consider a class that provides access to a database table. You want the __init__
method to create the table in the db if it doesn’t already exist. But if you’re using Twisted’s twisted.enterprise.adbapi
class, the runInteraction
method returns a Deferred. You can call it to create the tables, but you don’t want the instance of your class back in the hands of whoever’s creating it until the table is created. Otherwise they might call a method on the instance that expects the table to be there.
A cumbersome solution would be to add a callback to the Deferred you get back from runInteraction
and have that callback add an attribute to self
to indicate that it is safe to proceed. Then all your class methods that access the db table would have to check to see if the attribute was on self
, and take some alternate action if not. That’s going to get ugly very fast plus, your caller has to deal with you potentially not being ready.
I ran into this problem a couple of days ago and after scratching my head for a while I came up with an idea for how to solve this pretty cleanly via a Python metaclass. Here’s the metaclass code:
from twisted.internet import defer
class TxDeferredInitMeta(type):
def __new__(mcl, classname, bases, classdict):
hidden = '__hidden__'
instantiate = '__instantiate__'
for name in hidden, instantiate:
if name in classdict:
raise Exception(
'Class %s contains an illegally-named %s method' %
(classname, name))
try:
origInit = classdict['__init__']
except KeyError:
origInit = lambda self: None
def newInit(self, *args, **kw):
hiddenDict = dict(args=args, kw=kw, __init__=origInit)
setattr(self, hidden, hiddenDict)
def _instantiate(self):
def addSelf(result):
return (self, result)
hiddenDict = getattr(self, hidden)
d = defer.maybeDeferred(hiddenDict['__init__'], self,
*hiddenDict['args'], **hiddenDict['kw'])
return d.addCallback(addSelf)
classdict['__init__'] = newInit
classdict[instantiate] = _instantiate
return super(TxDeferredInitMeta, mcl).__new__(
mcl, classname, bases, classdict)
I’m not going to explain what it does here. If it’s not clear and you want to know, send me mail or post a comment. But I’ll show you how you use it in practice. It’s kind of weird, but it makes sense once you get used to it.
First, we make a class whose metaclass is TxDeferredInitMeta and whose __init__
method returns a deferred:
class MyClass(object):
__metaclass__ = TxDeferredInitMeta
def __init__(self):
d = aFuncReturningADeferred()
return d
Having __init__
return anything other than None
is illegal in normal Python classes. But this is not a normal Python class, as you will now see.
Given our class, we use it like this:
def cb((instance, result)):
# instance is an instance of MyClass
# result is from the callback chain of aFuncReturningADeferred
pass
d = MyClass()
d.__instantiate__()
d.addCallback(cb)
That may look pretty funky, but if you’re used to Twisted it wont seem too bizarre. What’s happening is that when you ask to make an instance of MyClass, you get back an instance of a regular Python class. It has a method called __instantiate__
that returns a Deferred. You add a callback to that Deferred and that callback is eventually passed two things. The first is an instance of MyClass, as you requested. The second is the result that came down the callback chain from the Deferred that was returned by the __init__
method you wrote in MyClass.
The net result is that you have the value of the Deferred and you have your instance of MyClass. It’s safe to go ahead and use the instance because you know the Deferred has been called. It will probably seem a bit odd to get your instance later as a result of a Deferred firing, but that’s perfectly in keeping with the Twisted way.
That’s it for now. You can grab the code and a trial test suite to put it through its paces at http://foss.fluidinfo.com/txDeferredInitMeta.zip. The code could be cleaned up somewhat, and made more general. There is a caveat to using it – your class can’t have __hidden__
or __instantiate__
methods. That could be improved. But I’m not going to bother for now, unless someone cares.
You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.
November 3rd, 2008 at 4:50 pm
Following a suggestion from JP Calderone at http://twistedmatrix.com/pipermail/twisted-python/2008-November/018601.html I’ve made a slightly simpler version, available at http://foss.fluidinfo.com/txDeferredInitMeta2.zip
The simpler version simply returns the class instance after the Deferred has fired. The argument is that the caller likely has no business seeing/knowing what the Deferred returned (or even that there was a Deferred at all). If the class itself wants the value of the Deferred, it can add a callback in its own __init__ (which is where the Deferred is created) to put its value onto self.
November 3rd, 2008 at 6:50 pm
Following a suggestion from JP Calderone at http://twistedmatrix.com/pipermail/twisted-python/2008-November/018601.html I’ve made a slightly simpler version, available at http://foss.fluidinfo.com/txDeferredInitMeta2.zip
The simpler version simply returns the class instance after the Deferred has fired. The argument is that the caller likely has no business seeing/knowing what the Deferred returned (or even that there was a Deferred at all). If the class itself wants the value of the Deferred, it can add a callback in its own __init__ (which is where the Deferred is created) to put its value onto self.
April 23rd, 2014 at 4:12 pm
Found this post trying to solve exactly this issue. Unfortunately (now years after the original post), neither of the links work (more importantly the updated one referenced in your comment). Can you update? paste code in? link to a GIST?
April 23rd, 2014 at 4:14 pm
Nevermind… future readers should see Terry’s update document at http://blogs.fluidinfo.com/terry/tag/__init__/