I am exposing some library functions using a TurboGears2 controller (see Building a web-based API with Turbogears2). It turns out that some functions return a dict, some a list, some a string, and TurboGears 2 only allows JSON serialisation for dicts.
A simple work-around for this is to wrap the function result into a dict, something like this:
@expose("json")
@validate(validator_dispatcher, error_handler=api_validation_error)
def list_colours(self, filter=None, productID=None, maxResults=100, **kw):
# Call API
res = self.engine.list_colours(filter, productID, maxResults)
# Return result
return dict(r=res)
It would be nice, however, to have an @webapi()
decorator that
automatically wraps the function result with the dict:
def webapi(func):
def dict_wrap(*args, **kw):
return dict(r=func(*args, **kw))
return dict_wrap
# ...in the controller...
@expose("json")
@validate(validator_dispatcher, error_handler=api_validation_error)
@webapi
def list_colours(self, filter=None, productID=None, maxResults=100, **kw):
# Call API
res = self.engine.list_colours(filter, productID, maxResults)
# Return result
return res
This works, as long as @webapi appears last in the list of decorators.
This is because if it appears last it will be the first to wrap the function,
and so it will not interfere with the tg.decorators
machinery.
Would it be possible to create a decorator that can be put anywhere among the decorator list? Yes, it is possible but tricky, and it gives me the feeling that it may break in any future version of TurboGears:
class webapi(object):
def __call__(self, func):
def dict_wrap(*args, **kw):
return dict(r=func(*args, **kw))
# Migrate the decoration attribute to our new function
if hasattr(func, 'decoration'):
dict_wrap.decoration = func.decoration
dict_wrap.decoration.controller = dict_wrap
delattr(func, 'decoration')
return dict_wrap
# ...in the controller...
@expose("json")
@validate(validator_dispatcher, error_handler=api_validation_error)
@webapi
def list_colours(self, filter=None, productID=None, maxResults=100, **kw):
# Call API
res = self.engine.list_colours(filter, productID, maxResults)
# Return result
return res
As a convenience, TurboGears 2 offers, in the decorators
module, a way to
build decorator "hooks":
class before_validate(_hook_decorator):
'''A list of callables to be run before validation is performed'''
hook_name = 'before_validate'
class before_call(_hook_decorator):
'''A list of callables to be run before the controller method is called'''
hook_name = 'before_call'
class before_render(_hook_decorator):
'''A list of callables to be run before the template is rendered'''
hook_name = 'before_render'
class after_render(_hook_decorator):
'''A list of callables to be run after the template is rendered.
Will be run before it is returned returned up the WSGI stack'''
hook_name = 'after_render'
The way these are invoked can be found in the _perform_call
function in
tg/controllers.py
.
To show an example use of those hooks, let's add a some polygen wisdom to every data structure we return:
class wisdom(decorators.before_render):
def __init__(self, grammar):
super(wisdom, self).__init__(self.add_wisdom)
self.grammar = grammar
def add_wisdom(self, remainder, params, output):
from subprocess import Popen, PIPE
output["wisdom"] = Popen(["polyrun", self.grammar], stdout=PIPE).communicate()[0]
# ...in the controller...
@wisdom("genius")
@expose("json")
@validate(validator_dispatcher, error_handler=api_validation_error)
def list_colours(self, filter=None, productID=None, maxResults=100, **kw):
# Call API
res = self.engine.list_colours(filter, productID, maxResults)
# Return result
return res
These hooks cannot however be used for what I need, that is, to wrap the result inside a dict. The reason is because they are called in this way:
controller.decoration.run_hooks(
'before_render', remainder, params, output)
and not in this way:
output = controller.decoration.run_hooks(
'before_render', remainder, params, output)
So it is possible to modify the output (if it is a mutable structure) but not to exchange it with something else.
Can we do even better? Sure we can. We can assimilate @expose
and @validate
inside @webapi
to avoid repeating those same many decorator lines over and
over again:
class webapi(object):
def __init__(self, error_handler = None):
self.error_handler = error_handler
def __call__(self, func):
def dict_wrap(*args, **kw):
return dict(r=func(*args, **kw))
res = expose("json")(dict_wrap)
res = validate(validator_dispatcher, error_handler=self.error_handler)(res)
return res
# ...in the controller...
@expose("json")
def api_validation_error(self, **kw):
pylons.response.status = "400 Error"
return dict(e="validation error on input fields", form_errors=pylons.c.form_errors)
@webapi(error_handler=api_validation_error)
def list_colours(self, filter=None, productID=None, maxResults=100, **kw):
# Call API
res = self.engine.list_colours(filter, productID, maxResults)
# Return result
return res
This got rid of @expose
and @validate
, and provides almost all the
default values that I need. Unfortunately I could not find out how to access
api_validation_error
from the decorator so that I can pass it to the
validator, therefore I remain with the inconvenience of having to explicitly
pass it every time.