Avalanche Python Web Framework with a focus on testability and reusability

Project links



Source code for avalanche.snow

import copy
import json

from .params import _PARAM_VAR



class AvalancheException(Exception):
    pass


# FIXME - make this configurable - document
def use_namespace(func):
    """decorator that add use_namespace=True to function"""
    func.use_namespace = True
    return func


############ render

[docs]class JinjaRenderer(object): """render jinja2 templates"""
[docs] def __init__(self, jinja_env): """ :param jinja_env: instance of jinja2.Environment """ self.jinja_env = jinja_env
[docs] def render(self, handler, **context): """Renders a template and writes the result to the response.""" if handler.template: template = self.jinja_env.get_template(handler.template) uri_for = handler.app.router.build handler.response.write(template.render( uri_for=uri_for, handler=handler, **context))
[docs]class JsonRenderer(object): """render json data""" @staticmethod
[docs] def render(handler, **context): handler.response.write(json.dumps(context).replace("</", "<\\/"))
class ConfigurableMetaclass(type): """adds an attribute (dict) 'a_config' to the class * when the class is created it merges the confing from base classes * collect config info from the class methods (using _PARAM_VAR) * calls classmethod set_config (used by subclasses to alter config values from base classes) """ def __new__(mcs, name, bases, dict_): # create class _class = type.__new__(mcs, name, bases, dict_) config = {} # get config from base classes for base_class in bases: if hasattr(base_class, 'a_config'): config.update(copy.deepcopy(base_class.a_config)) # get config values from builders decorators for attr_name in dict_.iterkeys(): # get a_param from method attr = getattr(_class, attr_name) a_params = getattr(attr, _PARAM_VAR, None) if not a_params: continue # add to config dict handler_config = config.setdefault(attr_name, {}) for param in a_params: handler_config[param.obj_name] = param _class.a_config = config # modify config _class.set_config() return _class
[docs]class _AvalancheHandler(object): """avalanche functionality. Users should not subclass this directly, use :py:func:`avalanche.make_handler`. """ # @ivar context: (dict) with values computed on context_builders # TODO: support context_builders defined in a different class # this would allow make_handler combine many AvalancheHandler's without # subclassing them. __metaclass__ = ConfigurableMetaclass @classmethod def set_config(cls):pass #: list of context-builder method names used on GET requests context_get = ['a_get', ] #: list of context-builder method names used on POST requests context_post = ['a_post', ] #: tuple with (name, dict) to be passed to redirect_to redirect_info = () # must be explicitily configured renderer = None #: (string) path to jinja template to be rendered template = None def _convert_params(self, request, param_list): """convert params from HTTP string to python objects @param param_list: list of AvalancheParam @return dict """ param_objs = {} for param in param_list: str_value = param.get_str_value(request) if str_value is not None: obj_value = param.get_obj_value(str_value, self) param_objs[param.obj_name] = obj_value return param_objs def _builder(self, name): """retrieve builder method, give precise error messages if fails""" try: return getattr(self, name) except TypeError: msg_str = ("Error on handler '%s' context builder list contains " + "an item with wrong type. " + "List must contain strings with method names, " + "got (%s: %r).") msg = msg_str % (self.__class__.__name__, type(name), name) raise AvalancheException(msg) def _build_context(self, builders, request): """build context for given builders @param builders: list of string of builder method names """ # build context self.context = {} for builder_name in builders: builder = self._builder(builder_name) # get builder specific obj_params if builder_name in self.a_config: a_params = self.a_config[builder_name].values() param_objs = self._convert_params(request, a_params) else: param_objs = {} # run builder try: built_context = builder(**param_objs) except TypeError, exception: if 'arguments' not in str(exception): # should catch only: # TypeEror:"xxx takes exactly X arguments (Y given))" raise msg_str = ("Error on handler incomplete parameters for " + "builder '%s.%s'. Got params %r, (Original error:%s)") msg = msg_str % (self.__class__.__name__, builder.__name__, param_objs, str(exception)) raise AvalancheException(msg) # update handler context if getattr(builder, 'use_namespace', False): # use namespace self.context[builder_name] = built_context else: if built_context is not None: self.context.update(built_context) def render(self, **context): self.renderer.render(self, **context) def get(self, *args, **kwargs): """ * build context * render template """ # FIXME - do no attach stuff to request object! self.request.route_kwargs = kwargs self._build_context(self.context_get, self.request) if self.redirect_info: self.redirect_to(self.redirect_info[0], **self.redirect_info[1]) # XXX how to specify render should not occur self.render(**self.context) def post(self, *args, **kwargs): """build_context should redirect page""" self.request.route_kwargs = kwargs self._build_context(self.context_post, self.request) if self.redirect_info: self.redirect_to(self.redirect_info[0], **self.redirect_info[1])
[docs]class BaseHandler(object): """Base class that user should subclass when creating request handlers""" __metaclass__ = ConfigurableMetaclass @classmethod
[docs] def set_config(cls): """used to modify config values inherited from a base class config values are saved as a dict in the attribute 'a_config'. * on first level keys are the name of config-builders * on second level keys are the parameter names * values are instances of AvalancheParam :: class MyHandler(MyHandlerBase): @classmethod def set_config(cls): cls.a_config['builder_a']['x'] = avalanche.UrlQueryParam('x', 'x2') """ pass #: tuple with (name, dict) to be passed to redirect_to
redirect_info = ()
[docs] def a_redirect(self, _name, **kwargs): """do not redirect now just saves redirect info""" self.redirect_info = (_name, kwargs)
def _Mixer(class_name, bases, dict_=None): """create a new class/type with given name and base classes""" return type(class_name, bases, dict_ or {})
[docs]def make_handler(request_handler, app_handler, dict_=None): """creates a concrete request handler => ApplicationHandler(avalanche.Handler) + _AvalancheHandler + core.RequestHandler """ handler_name = app_handler.__name__ + 'Handler' bases = (app_handler, request_handler, _AvalancheHandler) return _Mixer(handler_name, bases, dict_)