Time to decorate ...

Time to decorate ...

Ok, if you're not a Python programmer this might be irrelevant to you, unless of course it proves to be yet another reason why you should be ... maybe in preference some lesser scripting languages. (No flames please, I didn't mention PHP ...)

In more recent times I've started to notice more and more lines in code beginning with an '@' sign, which provoked two reactions. Firstly, WTF? And secondly, "why do they keep having to add all this new s**t when we've managed without it for all these years!".

As it turns out, both sentiments were sadly misplaced.

So, essentially an '@' sign prefixes a 'decorator'.

For us 'oldies', a decorator is a kind of 'macro' (or a wrapper), for want of a better analogy ... so for example;

def function(a):
   print a

Will call a function called 'mydecorator', which in turn will call 'function', the key here is that 'mydecorator' effectively intercepts all calls to 'function', and has the ability to pre-process the input to 'function', and to post-process it's output. So for example;

def mydecorator(f):
  def fn(*args, **kwargs):
    print "Woz ere"
  return fn

When you run 'function' as described above you get;

>>> function("Gareth")
Woz ere

Looks interesting, but at this stage it's really still just a fancy widget, so let's take it for a test drive in the real world. When working with 'Crossbar' my RPC routines are sent an object reference as a first argument, a 'map' of parameters as it's second argument, and a session reference as a third argument. Good basic information, but not really what I want to work with inside each function. What I really want is to have my parameters as parameters (and not a map), to automatically fail if required parameters are not present, and to have actual session and user details present rather than an obscure reference .. otherwise I end up with lots of 'cruft' at the beginning of each function (of which there are hundreds) just to get usable information before I start doing any real work.

This is what a typical function looks like before we start;

def function(self, args, caller):
   """this is my microservice function call stub"""
   # my parameters are in "args"
   # I need to resolve user and session details from "caller"
   # then I can do some work ...

The register decorator is a WAMP provided decorator for setting up an RPC stub, takes a lot of the pain out of setting up RPC call definitions ... anyway, after we add our new decorator, it becomes something like this;

def function(self, user=None, session=None, req_1=None, req_2=None):
   """this is my microservice function call stub"""
   .. ready to roll here ..

So what do we have in addition / instead;

  • req_1 and req_2 must be passed, else the function will report an error
  • user and session are pre-processed and provided as native parameters
  • req_1 and req_2 are provided as native parameters rather than map lookups

In addition, there seems to be an issue with 'deferreds' inside of Crossbar, sometimes a generator will produce a deferred object rather than a direct result, despite the @inlineCallbacks declaration (I'll figure out why one day ...) which can on occasion cause a problem, so I'm making sure the returning object isn't a deferred, and if it is I'm evaluating it inline and actively waiting on the result. Here's the code;

def wrapmycode(required):    
    def wrap(f):       
        def function(*args, **kwargs):  
            caller = kwargs['details'].caller
            fn = args[0].server.call
            uid = session.get('uid', None)
            session = yield fn(u'xbr.session.get', caller)
            user = yield args[0].db.users.find_one({'_id': ObjectId(uid)})
            if not user:               
                returnValue({'error':'unable to resolve user'})                 
            kwlist = {'session': session, 'user': user}
            argv = args[1]
            for arg in required:
                if arg not in argv:
                    returnValue({'error':'missing parameter'}) 
                kwlist[arg] = argv.get(arg, None)                               
            ret = f(*args, **kwlist)
            if isinstance(ret,Deferred):
                result = yield DeferredList([ret])
                ret = result[0][1]                            
        return function                                            
    return wrap

So by prefixing each function definition with @wrapmycode, I'm removing a shed load of setup and checking code which would be replicated repeatedly and making the parameters available to the function is much more programmer friendly.

Anyway, if you've not used decorators or don't see the point, they're well worth a second look, especially if you're working with a lot of boilerplate code you'd like to offload onto something more re-usable.

Certainly one of the more appealing features of this (to me) is the ability to preprocess and actually change the list of parameters delivered to the function ... very useful for instance when you're passing data from an API to your own code and the parameters used need on-the-fly conversion.