| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | """ |
|---|
| 3 | werkzeug.contrib.kickstart |
|---|
| 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|---|
| 5 | |
|---|
| 6 | This module provides some simple shortcuts to make using Werkzeug simpler |
|---|
| 7 | for small scripts. |
|---|
| 8 | |
|---|
| 9 | These improvements include predefined `Request` and `Response` objects as |
|---|
| 10 | well as a predefined `Application` object which can be customized in child |
|---|
| 11 | classes, of course. The `Request` and `Reponse` objects handle URL |
|---|
| 12 | generation as well as sessions via `werkzeug.contrib.sessions` and are |
|---|
| 13 | purely optional. |
|---|
| 14 | |
|---|
| 15 | There is also some integration of template engines. The template loaders |
|---|
| 16 | are, of course, not neccessary to use the template engines in Werkzeug, |
|---|
| 17 | but they provide a common interface. Currently supported template engines |
|---|
| 18 | include Werkzeug's minitmpl and Genshi_. Support for other engines can be |
|---|
| 19 | added in a trivial way. These loaders provide a template interface |
|---|
| 20 | similar to the one used by Django_. |
|---|
| 21 | |
|---|
| 22 | .. _Genshi: http://genshi.edgewall.org/ |
|---|
| 23 | .. _Django: http://www.djangoproject.com/ |
|---|
| 24 | |
|---|
| 25 | :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details. |
|---|
| 26 | :license: BSD, see LICENSE for more details. |
|---|
| 27 | """ |
|---|
| 28 | from os import path |
|---|
| 29 | from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase |
|---|
| 30 | from werkzeug.templates import Template |
|---|
| 31 | from werkzeug.exceptions import HTTPException |
|---|
| 32 | from werkzeug.routing import RequestRedirect |
|---|
| 33 | |
|---|
| 34 | __all__ = ['Request', 'Response', 'TemplateNotFound', 'TemplateLoader', |
|---|
| 35 | 'GenshiTemplateLoader', 'Application'] |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | class Request(RequestBase): |
|---|
| 39 | """A handy subclass of the base request that adds a URL builder. |
|---|
| 40 | It when supplied a session store, it is also able to handle sessions. |
|---|
| 41 | """ |
|---|
| 42 | |
|---|
| 43 | def __init__(self, environ, url_map, |
|---|
| 44 | session_store=None, cookie_name=None): |
|---|
| 45 | # call the parent for initialization |
|---|
| 46 | RequestBase.__init__(self, environ) |
|---|
| 47 | # create an adapter |
|---|
| 48 | self.url_adapter = url_map.bind_to_environ(environ) |
|---|
| 49 | # create all stuff for sessions |
|---|
| 50 | self.session_store = session_store |
|---|
| 51 | self.cookie_name = cookie_name |
|---|
| 52 | |
|---|
| 53 | if session_store is not None and cookie_name is not None: |
|---|
| 54 | if cookie_name in self.cookies: |
|---|
| 55 | # get the session out of the storage |
|---|
| 56 | self.session = session_store.get(self.cookies[cookie_name]) |
|---|
| 57 | else: |
|---|
| 58 | # create a new session |
|---|
| 59 | self.session = session_store.new() |
|---|
| 60 | |
|---|
| 61 | def url_for(self, callback, **values): |
|---|
| 62 | return self.url_adapter.build(callback, values) |
|---|
| 63 | |
|---|
| 64 | |
|---|
| 65 | class Response(ResponseBase): |
|---|
| 66 | """ |
|---|
| 67 | A subclass of base response which sets the default mimetype to text/html. |
|---|
| 68 | It the `Request` that came in is using Werkzeug sessions, this class |
|---|
| 69 | takes care of saving that session. |
|---|
| 70 | """ |
|---|
| 71 | default_mimetype = 'text/html' |
|---|
| 72 | |
|---|
| 73 | def __call__(self, environ, start_response): |
|---|
| 74 | # get the request object |
|---|
| 75 | request = environ['werkzeug.request'] |
|---|
| 76 | |
|---|
| 77 | if request.session_store is not None: |
|---|
| 78 | # save the session if neccessary |
|---|
| 79 | request.session_store.save_if_modified(request.session) |
|---|
| 80 | |
|---|
| 81 | # set the cookie for the browser if it is not there: |
|---|
| 82 | if request.cookie_name not in request.cookies: |
|---|
| 83 | self.set_cookie(request.cookie_name, request.session.sid) |
|---|
| 84 | |
|---|
| 85 | # go on with normal response business |
|---|
| 86 | return ResponseBase.__call__(self, environ, start_response) |
|---|
| 87 | |
|---|
| 88 | |
|---|
| 89 | class Processor(object): |
|---|
| 90 | """A request and response processor - it is what Django calls a |
|---|
| 91 | middleware, but Werkzeug also includes straight-foward support for real |
|---|
| 92 | WSGI middlewares, so another name was chosen. |
|---|
| 93 | |
|---|
| 94 | The code of this processor is derived from the example in the Werkzeug |
|---|
| 95 | trac, called `Request and Response Processor |
|---|
| 96 | <http://dev.pocoo.org/projects/werkzeug/wiki/RequestResponseProcessor>`_ |
|---|
| 97 | """ |
|---|
| 98 | |
|---|
| 99 | def process_request(self, request): |
|---|
| 100 | return request |
|---|
| 101 | |
|---|
| 102 | def process_response(self, request, response): |
|---|
| 103 | return response |
|---|
| 104 | |
|---|
| 105 | def process_view(self, request, view_func, view_args, view_kwargs): |
|---|
| 106 | """process_view() is called just before the Application calls the |
|---|
| 107 | function specified by view_func. |
|---|
| 108 | |
|---|
| 109 | If this returns None, the Application processes the next Processor, |
|---|
| 110 | and if it returns something else (like a Response instance), that |
|---|
| 111 | will be returned without any further processing. |
|---|
| 112 | """ |
|---|
| 113 | return None |
|---|
| 114 | |
|---|
| 115 | def process_exception(self, request, exception): |
|---|
| 116 | return None |
|---|
| 117 | |
|---|
| 118 | |
|---|
| 119 | class Application(object): |
|---|
| 120 | """A generic WSGI application which can be used to start with Werkzeug in |
|---|
| 121 | an easy, straightforward way. |
|---|
| 122 | """ |
|---|
| 123 | |
|---|
| 124 | def __init__(self, name, url_map, session=False, processors=None): |
|---|
| 125 | # save the name and the URL-map, as it'll be needed later on |
|---|
| 126 | self.name = name |
|---|
| 127 | self.url_map = url_map |
|---|
| 128 | # save the list of processors if supplied |
|---|
| 129 | self.processors = processors or [] |
|---|
| 130 | # create an instance of the storage |
|---|
| 131 | if session: |
|---|
| 132 | self.store = session |
|---|
| 133 | else: |
|---|
| 134 | self.store = None |
|---|
| 135 | |
|---|
| 136 | def __call__(self, environ, start_response): |
|---|
| 137 | # create a request - with or without session support |
|---|
| 138 | if self.store is not None: |
|---|
| 139 | request = Request(environ, self.url_map, |
|---|
| 140 | session_store=self.store, cookie_name='%s_sid' % self.name) |
|---|
| 141 | else: |
|---|
| 142 | request = Request(environ, self.url_map) |
|---|
| 143 | |
|---|
| 144 | # apply the request processors |
|---|
| 145 | for processor in self.processors: |
|---|
| 146 | request = processor.process_request(request) |
|---|
| 147 | |
|---|
| 148 | try: |
|---|
| 149 | # find the callback to which the URL is mapped |
|---|
| 150 | callback, args = request.url_adapter.match(request.path) |
|---|
| 151 | except (HTTPException, RequestRedirect), e: |
|---|
| 152 | response = e |
|---|
| 153 | else: |
|---|
| 154 | # check all view processors |
|---|
| 155 | for processor in self.processors: |
|---|
| 156 | action = processor.process_view(request, callback, (), args) |
|---|
| 157 | if action is not None: |
|---|
| 158 | # it is overriding the default behaviour, this is |
|---|
| 159 | # short-circuiting the processing, so it returns here |
|---|
| 160 | return action(environ, start_response) |
|---|
| 161 | |
|---|
| 162 | try: |
|---|
| 163 | response = callback(request, **args) |
|---|
| 164 | except Exception, exception: |
|---|
| 165 | # the callback raised some exception, need to process that |
|---|
| 166 | for processor in reversed(self.processors): |
|---|
| 167 | # filter it through the exception processor |
|---|
| 168 | action = processor.process_exception(request, exception) |
|---|
| 169 | if action is not None: |
|---|
| 170 | # the exception processor returned some action |
|---|
| 171 | return action(environ, start_response) |
|---|
| 172 | # still not handled by a exception processor, so re-raise |
|---|
| 173 | raise |
|---|
| 174 | |
|---|
| 175 | # apply the response processors |
|---|
| 176 | for processor in reversed(self.processors): |
|---|
| 177 | response = processor.process_response(request, response) |
|---|
| 178 | |
|---|
| 179 | # return the completely processed response |
|---|
| 180 | return response(environ, start_response) |
|---|
| 181 | |
|---|
| 182 | |
|---|
| 183 | def config_session(self, store, expiration='session'): |
|---|
| 184 | """ |
|---|
| 185 | Configures the setting for cookies. You can also disable cookies by |
|---|
| 186 | setting store to None. |
|---|
| 187 | """ |
|---|
| 188 | self.store = store |
|---|
| 189 | # expiration=session is the default anyway |
|---|
| 190 | # TODO: add settings to define the expiration date, the domain, the |
|---|
| 191 | # path any maybe the secure parameter. |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | class TemplateNotFound(IOError, LookupError): |
|---|
| 195 | """ |
|---|
| 196 | A template was not found by the template loader. |
|---|
| 197 | """ |
|---|
| 198 | |
|---|
| 199 | def __init__(self, name): |
|---|
| 200 | IOError.__init__(self, name) |
|---|
| 201 | self.name = name |
|---|
| 202 | |
|---|
| 203 | |
|---|
| 204 | class TemplateLoader(object): |
|---|
| 205 | """ |
|---|
| 206 | A simple loader interface for the werkzeug minitmpl |
|---|
| 207 | template language. |
|---|
| 208 | """ |
|---|
| 209 | |
|---|
| 210 | def __init__(self, search_path, encoding='utf-8'): |
|---|
| 211 | self.search_path = path.abspath(search_path) |
|---|
| 212 | self.encoding = encoding |
|---|
| 213 | |
|---|
| 214 | def get_template(self, name): |
|---|
| 215 | """Get a template from a given name.""" |
|---|
| 216 | filename = path.join(self.search_path, *[p for p in name.split('/') |
|---|
| 217 | if p and p[0] != '.']) |
|---|
| 218 | if not path.exists(filename): |
|---|
| 219 | raise TemplateNotFound(name) |
|---|
| 220 | return Template.from_file(filename, self.encoding) |
|---|
| 221 | |
|---|
| 222 | def render_to_response(self, *args, **kwargs): |
|---|
| 223 | """Load and render a template into a response object.""" |
|---|
| 224 | return Response(self.render_to_string(*args, **kwargs)) |
|---|
| 225 | |
|---|
| 226 | def render_to_string(self, *args, **kwargs): |
|---|
| 227 | """Load and render a template into a unicode string.""" |
|---|
| 228 | try: |
|---|
| 229 | template_name, args = args[0], args[1:] |
|---|
| 230 | except IndexError: |
|---|
| 231 | raise TypeError('name of template required') |
|---|
| 232 | return self.get_template(template_name).render(*args, **kwargs) |
|---|
| 233 | |
|---|
| 234 | |
|---|
| 235 | class GenshiTemplateLoader(TemplateLoader): |
|---|
| 236 | """A unified interface for loading Genshi templates. Actually a quite thin |
|---|
| 237 | wrapper for Genshi's TemplateLoader. |
|---|
| 238 | |
|---|
| 239 | It sets some defaults that differ from the Genshi loader, most notably |
|---|
| 240 | auto_reload is active. All imporant options can be passed through to |
|---|
| 241 | Genshi. |
|---|
| 242 | The default output type is 'html', but can be adjusted easily by changing |
|---|
| 243 | the `output_type` attribute. |
|---|
| 244 | """ |
|---|
| 245 | def __init__(self, search_path, encoding='utf-8', **kwargs): |
|---|
| 246 | TemplateLoader.__init__(self, search_path, encoding) |
|---|
| 247 | # import Genshi here, because we don't want a general Genshi |
|---|
| 248 | # dependency, only a local one |
|---|
| 249 | from genshi.template import TemplateLoader as GenshiLoader |
|---|
| 250 | from genshi.template.loader import TemplateNotFound |
|---|
| 251 | |
|---|
| 252 | self.not_found_exception = TemplateNotFound |
|---|
| 253 | # set auto_reload to True per default |
|---|
| 254 | reload_template = kwargs.pop('auto_reload', True) |
|---|
| 255 | # get rid of default_encoding as this template loaders overwrites it |
|---|
| 256 | # with the value of encoding |
|---|
| 257 | kwargs.pop('default_encoding', None) |
|---|
| 258 | |
|---|
| 259 | # now, all arguments are clean, pass them on |
|---|
| 260 | self.loader = GenshiLoader(search_path, default_encoding=encoding, |
|---|
| 261 | auto_reload=reload_template, **kwargs) |
|---|
| 262 | |
|---|
| 263 | # the default output is HTML but can be overridden easily |
|---|
| 264 | self.output_type = 'html' |
|---|
| 265 | self.encoding = encoding |
|---|
| 266 | |
|---|
| 267 | def get_template(self, template_name): |
|---|
| 268 | """Get the template which is at the given name""" |
|---|
| 269 | try: |
|---|
| 270 | return self.loader.load(template_name, encoding=self.encoding) |
|---|
| 271 | except self.not_found_exception, e: |
|---|
| 272 | # catch the exception raised by Genshi, convert it into a werkzeug |
|---|
| 273 | # exception (for the sake of consistency) |
|---|
| 274 | raise TemplateNotFound(template_name) |
|---|
| 275 | |
|---|
| 276 | def render_to_string(self, template_name, context=None): |
|---|
| 277 | """Load and render a template into an unicode string""" |
|---|
| 278 | # create an empty context if no context was specified |
|---|
| 279 | context = context or {} |
|---|
| 280 | tmpl = self.get_template(template_name) |
|---|
| 281 | # render the template into a unicode string (None means unicode) |
|---|
| 282 | return tmpl. \ |
|---|
| 283 | generate(**context). \ |
|---|
| 284 | render(self.output_type, encoding=None) |
|---|