Source

n0v4pl4n3t / werkzeug / contrib / kickstart.py

Full commit
# -*- coding: utf-8 -*-
"""
    werkzeug.contrib.kickstart
    ~~~~~~~~~~~~~~~~~~~~~~~~~~

    This module provides some simple shortcuts to make using Werkzeug simpler
    for small scripts.

    These improvements include predefined `Request` and `Response` objects as
    well as a predefined `Application` object which can be customized in child
    classes, of course.  The `Request` and `Reponse` objects handle URL
    generation as well as sessions via `werkzeug.contrib.sessions` and are
    purely optional.

    There is also some integration of template engines.  The template loaders
    are, of course, not neccessary to use the template engines in Werkzeug,
    but they provide a common interface.  Currently supported template engines
    include Werkzeug's minitmpl and Genshi_.  Support for other engines can be
    added in a trivial way.  These loaders provide a template interface
    similar to the one used by Django_.

    .. _Genshi: http://genshi.edgewall.org/
    .. _Django: http://www.djangoproject.com/

    :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
    :license: BSD, see LICENSE for more details.
"""
from os import path
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from werkzeug.templates import Template
from werkzeug.exceptions import HTTPException
from werkzeug.routing import RequestRedirect

__all__ = ['Request', 'Response', 'TemplateNotFound', 'TemplateLoader',
           'GenshiTemplateLoader', 'Application']

from warnings import warn
warn(DeprecationWarning('werkzeug.contrib.kickstart is deprecated and '
                        'will be removed in Werkzeug 1.0'))


class Request(RequestBase):
    """A handy subclass of the base request that adds a URL builder.
    It when supplied a session store, it is also able to handle sessions.
    """

    def __init__(self, environ, url_map,
            session_store=None, cookie_name=None):
        # call the parent for initialization
        RequestBase.__init__(self, environ)
        # create an adapter
        self.url_adapter = url_map.bind_to_environ(environ)
        # create all stuff for sessions
        self.session_store = session_store
        self.cookie_name = cookie_name

        if session_store is not None and cookie_name is not None:
            if cookie_name in self.cookies:
                # get the session out of the storage
                self.session = session_store.get(self.cookies[cookie_name])
            else:
                # create a new session
                self.session = session_store.new()

    def url_for(self, callback, **values):
        return self.url_adapter.build(callback, values)


class Response(ResponseBase):
    """
    A subclass of base response which sets the default mimetype to text/html.
    It the `Request` that came in is using Werkzeug sessions, this class
    takes care of saving that session.
    """
    default_mimetype = 'text/html'

    def __call__(self, environ, start_response):
        # get the request object
        request = environ['werkzeug.request']

        if request.session_store is not None:
            # save the session if neccessary
            request.session_store.save_if_modified(request.session)

            # set the cookie for the browser if it is not there:
            if request.cookie_name not in request.cookies:
                self.set_cookie(request.cookie_name, request.session.sid)

        # go on with normal response business
        return ResponseBase.__call__(self, environ, start_response)


class Processor(object):
    """A request and response processor - it is what Django calls a
    middleware, but Werkzeug also includes straight-foward support for real
    WSGI middlewares, so another name was chosen.

    The code of this processor is derived from the example in the Werkzeug
    trac, called `Request and Response Processor
    <http://dev.pocoo.org/projects/werkzeug/wiki/RequestResponseProcessor>`_
    """

    def process_request(self, request):
        return request

    def process_response(self, request, response):
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        """process_view() is called just before the Application calls the
        function specified by view_func.

        If this returns None, the Application processes the next Processor,
        and if it returns something else (like a Response instance), that
        will be returned without any further processing.
        """
        return None

    def process_exception(self, request, exception):
        return None


class Application(object):
    """A generic WSGI application which can be used to start with Werkzeug in
    an easy, straightforward way.
    """

    def __init__(self, name, url_map, session=False, processors=None):
        # save the name and the URL-map, as it'll be needed later on
        self.name = name
        self.url_map = url_map
        # save the list of processors if supplied
        self.processors = processors or []
        # create an instance of the storage
        if session:
            self.store = session
        else:
            self.store = None

    def __call__(self, environ, start_response):
        # create a request - with or without session support
        if self.store is not None:
            request = Request(environ, self.url_map,
                session_store=self.store, cookie_name='%s_sid' % self.name)
        else:
            request = Request(environ, self.url_map)

        # apply the request processors
        for processor in self.processors:
            request = processor.process_request(request)

        try:
            # find the callback to which the URL is mapped
            callback, args = request.url_adapter.match(request.path)
        except (HTTPException, RequestRedirect), e:
            response = e
        else:
            # check all view processors
            for processor in self.processors:
                action = processor.process_view(request, callback, (), args)
                if action is not None:
                    # it is overriding the default behaviour, this is
                    # short-circuiting the processing, so it returns here
                    return action(environ, start_response)

            try:
                response = callback(request, **args)
            except Exception, exception:
                # the callback raised some exception, need to process that
                for processor in reversed(self.processors):
                    # filter it through the exception processor
                    action = processor.process_exception(request, exception)
                    if action is not None:
                        # the exception processor returned some action
                        return action(environ, start_response)
                # still not handled by a exception processor, so re-raise
                raise

        # apply the response processors
        for processor in reversed(self.processors):
            response = processor.process_response(request, response)

        # return the completely processed response
        return response(environ, start_response)


    def config_session(self, store, expiration='session'):
        """
        Configures the setting for cookies. You can also disable cookies by
        setting store to None.
        """
        self.store = store
        # expiration=session is the default anyway
        # TODO: add settings to define the expiration date, the domain, the
        # path any maybe the secure parameter.


class TemplateNotFound(IOError, LookupError):
    """
    A template was not found by the template loader.
    """

    def __init__(self, name):
        IOError.__init__(self, name)
        self.name = name


class TemplateLoader(object):
    """
    A simple loader interface for the werkzeug minitmpl
    template language.
    """

    def __init__(self, search_path, encoding='utf-8'):
        self.search_path = path.abspath(search_path)
        self.encoding = encoding

    def get_template(self, name):
        """Get a template from a given name."""
        filename = path.join(self.search_path, *[p for p in name.split('/')
                                                 if p and p[0] != '.'])
        if not path.exists(filename):
            raise TemplateNotFound(name)
        return Template.from_file(filename, self.encoding)

    def render_to_response(self, *args, **kwargs):
        """Load and render a template into a response object."""
        return Response(self.render_to_string(*args, **kwargs))

    def render_to_string(self, *args, **kwargs):
        """Load and render a template into a unicode string."""
        try:
            template_name, args = args[0], args[1:]
        except IndexError:
            raise TypeError('name of template required')
        return self.get_template(template_name).render(*args, **kwargs)


class GenshiTemplateLoader(TemplateLoader):
    """A unified interface for loading Genshi templates. Actually a quite thin
    wrapper for Genshi's TemplateLoader.

    It sets some defaults that differ from the Genshi loader, most notably
    auto_reload is active. All imporant options can be passed through to
    Genshi.
    The default output type is 'html', but can be adjusted easily by changing
    the `output_type` attribute.
    """
    def __init__(self, search_path, encoding='utf-8', **kwargs):
        TemplateLoader.__init__(self, search_path, encoding)
        # import Genshi here, because we don't want a general Genshi
        # dependency, only a local one
        from genshi.template import TemplateLoader as GenshiLoader
        from genshi.template.loader import TemplateNotFound

        self.not_found_exception = TemplateNotFound
        # set auto_reload to True per default
        reload_template = kwargs.pop('auto_reload', True)
        # get rid of default_encoding as this template loaders overwrites it
        # with the value of encoding
        kwargs.pop('default_encoding', None)

        # now, all arguments are clean, pass them on
        self.loader = GenshiLoader(search_path, default_encoding=encoding,
                auto_reload=reload_template, **kwargs)

        # the default output is HTML but can be overridden easily
        self.output_type = 'html'
        self.encoding = encoding

    def get_template(self, template_name):
        """Get the template which is at the given name"""
        try:
            return self.loader.load(template_name, encoding=self.encoding)
        except self.not_found_exception, e:
            # catch the exception raised by Genshi, convert it into a werkzeug
            # exception (for the sake of consistency)
            raise TemplateNotFound(template_name)

    def render_to_string(self, template_name, context=None):
        """Load and render a template into an unicode string"""
        # create an empty context if no context was specified
        context = context or {}
        tmpl = self.get_template(template_name)
        # render the template into a unicode string (None means unicode)
        return tmpl. \
            generate(**context). \
            render(self.output_type, encoding=None)