Mercurial > bottledash
comparison bottle.py @ 0:e1264e58154c
initial commpit
author | sean |
---|---|
date | Sun, 12 Jul 2015 17:59:32 +0200 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:e1264e58154c |
---|---|
1 #!/usr/bin/env python | |
2 # -*- coding: utf-8 -*- | |
3 """ | |
4 Bottle is a fast and simple micro-framework for small web applications. It | |
5 offers request dispatching (Routes) with url parameter support, templates, | |
6 a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and | |
7 template engines - all in a single file and with no dependencies other than the | |
8 Python Standard Library. | |
9 | |
10 Homepage and documentation: http://bottlepy.org/ | |
11 | |
12 Copyright (c) 2014, Marcel Hellkamp. | |
13 License: MIT (see LICENSE for details) | |
14 """ | |
15 | |
16 from __future__ import with_statement | |
17 | |
18 __author__ = 'Marcel Hellkamp' | |
19 __version__ = '0.13-dev' | |
20 __license__ = 'MIT' | |
21 | |
22 # The gevent and eventlet server adapters need to patch some modules before | |
23 # they are imported. This is why we parse the commandline parameters here but | |
24 # handle them later | |
25 if __name__ == '__main__': | |
26 from optparse import OptionParser | |
27 _cmd_parser = OptionParser( | |
28 usage="usage: %prog [options] package.module:app") | |
29 _opt = _cmd_parser.add_option | |
30 _opt("--version", action="store_true", help="show version number.") | |
31 _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") | |
32 _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") | |
33 _opt("-p", "--plugin", | |
34 action="append", | |
35 help="install additional plugin/s.") | |
36 _opt("--debug", action="store_true", help="start server in debug mode.") | |
37 _opt("--reload", action="store_true", help="auto-reload on file changes.") | |
38 _cmd_options, _cmd_args = _cmd_parser.parse_args() | |
39 if _cmd_options.server: | |
40 if _cmd_options.server.startswith('gevent'): | |
41 import gevent.monkey | |
42 gevent.monkey.patch_all() | |
43 elif _cmd_options.server.startswith('eventlet'): | |
44 import eventlet | |
45 eventlet.monkey_patch() | |
46 | |
47 import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ | |
48 os, re, sys, tempfile, threading, time, warnings | |
49 | |
50 from types import FunctionType | |
51 from datetime import date as datedate, datetime, timedelta | |
52 from tempfile import TemporaryFile | |
53 from traceback import format_exc, print_exc | |
54 from inspect import getargspec | |
55 from unicodedata import normalize | |
56 | |
57 try: | |
58 from simplejson import dumps as json_dumps, loads as json_lds | |
59 except ImportError: # pragma: no cover | |
60 try: | |
61 from json import dumps as json_dumps, loads as json_lds | |
62 except ImportError: | |
63 try: | |
64 from django.utils.simplejson import dumps as json_dumps, loads as json_lds | |
65 except ImportError: | |
66 | |
67 def json_dumps(data): | |
68 raise ImportError( | |
69 "JSON support requires Python 2.6 or simplejson.") | |
70 | |
71 json_lds = json_dumps | |
72 | |
73 # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. | |
74 # It ain't pretty but it works... Sorry for the mess. | |
75 | |
76 py = sys.version_info | |
77 py3k = py >= (3, 0, 0) | |
78 py25 = py < (2, 6, 0) | |
79 py31 = (3, 1, 0) <= py < (3, 2, 0) | |
80 | |
81 # Workaround for the missing "as" keyword in py3k. | |
82 def _e(): | |
83 return sys.exc_info()[1] | |
84 | |
85 # Workaround for the "print is a keyword/function" Python 2/3 dilemma | |
86 # and a fallback for mod_wsgi (resticts stdout/err attribute access) | |
87 try: | |
88 _stdout, _stderr = sys.stdout.write, sys.stderr.write | |
89 except IOError: | |
90 _stdout = lambda x: sys.stdout.write(x) | |
91 _stderr = lambda x: sys.stderr.write(x) | |
92 | |
93 # Lots of stdlib and builtin differences. | |
94 if py3k: | |
95 import http.client as httplib | |
96 import _thread as thread | |
97 from urllib.parse import urljoin, SplitResult as UrlSplitResult | |
98 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote | |
99 urlunquote = functools.partial(urlunquote, encoding='latin1') | |
100 from http.cookies import SimpleCookie | |
101 from collections import MutableMapping as DictMixin | |
102 import pickle | |
103 from io import BytesIO | |
104 from configparser import ConfigParser | |
105 basestring = str | |
106 unicode = str | |
107 json_loads = lambda s: json_lds(touni(s)) | |
108 callable = lambda x: hasattr(x, '__call__') | |
109 imap = map | |
110 | |
111 def _raise(*a): | |
112 raise a[0](a[1]).with_traceback(a[2]) | |
113 else: # 2.x | |
114 import httplib | |
115 import thread | |
116 from urlparse import urljoin, SplitResult as UrlSplitResult | |
117 from urllib import urlencode, quote as urlquote, unquote as urlunquote | |
118 from Cookie import SimpleCookie | |
119 from itertools import imap | |
120 import cPickle as pickle | |
121 from StringIO import StringIO as BytesIO | |
122 from ConfigParser import SafeConfigParser as ConfigParser | |
123 if py25: | |
124 msg = "Python 2.5 support may be dropped in future versions of Bottle." | |
125 warnings.warn(msg, DeprecationWarning) | |
126 from UserDict import DictMixin | |
127 | |
128 def next(it): | |
129 return it.next() | |
130 | |
131 bytes = str | |
132 else: # 2.6, 2.7 | |
133 from collections import MutableMapping as DictMixin | |
134 unicode = unicode | |
135 json_loads = json_lds | |
136 eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec')) | |
137 | |
138 | |
139 # Some helpers for string/byte handling | |
140 def tob(s, enc='utf8'): | |
141 return s.encode(enc) if isinstance(s, unicode) else bytes(s) | |
142 | |
143 | |
144 def touni(s, enc='utf8', err='strict'): | |
145 if isinstance(s, bytes): | |
146 return s.decode(enc, err) | |
147 else: | |
148 return unicode(s or ("" if s is None else s)) | |
149 | |
150 | |
151 tonat = touni if py3k else tob | |
152 | |
153 # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). | |
154 # 3.1 needs a workaround. | |
155 if py31: | |
156 from io import TextIOWrapper | |
157 | |
158 class NCTextIOWrapper(TextIOWrapper): | |
159 def close(self): | |
160 pass # Keep wrapped buffer open. | |
161 | |
162 | |
163 # A bug in functools causes it to break if the wrapper is an instance method | |
164 def update_wrapper(wrapper, wrapped, *a, **ka): | |
165 try: | |
166 functools.update_wrapper(wrapper, wrapped, *a, **ka) | |
167 except AttributeError: | |
168 pass | |
169 | |
170 # These helpers are used at module level and need to be defined first. | |
171 # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. | |
172 | |
173 | |
174 def depr(message, strict=False): | |
175 warnings.warn(message, DeprecationWarning, stacklevel=3) | |
176 | |
177 | |
178 def makelist(data): # This is just too handy | |
179 if isinstance(data, (tuple, list, set, dict)): | |
180 return list(data) | |
181 elif data: | |
182 return [data] | |
183 else: | |
184 return [] | |
185 | |
186 | |
187 class DictProperty(object): | |
188 """ Property that maps to a key in a local dict-like attribute. """ | |
189 | |
190 def __init__(self, attr, key=None, read_only=False): | |
191 self.attr, self.key, self.read_only = attr, key, read_only | |
192 | |
193 def __call__(self, func): | |
194 functools.update_wrapper(self, func, updated=[]) | |
195 self.getter, self.key = func, self.key or func.__name__ | |
196 return self | |
197 | |
198 def __get__(self, obj, cls): | |
199 if obj is None: return self | |
200 key, storage = self.key, getattr(obj, self.attr) | |
201 if key not in storage: storage[key] = self.getter(obj) | |
202 return storage[key] | |
203 | |
204 def __set__(self, obj, value): | |
205 if self.read_only: raise AttributeError("Read-Only property.") | |
206 getattr(obj, self.attr)[self.key] = value | |
207 | |
208 def __delete__(self, obj): | |
209 if self.read_only: raise AttributeError("Read-Only property.") | |
210 del getattr(obj, self.attr)[self.key] | |
211 | |
212 | |
213 class cached_property(object): | |
214 """ A property that is only computed once per instance and then replaces | |
215 itself with an ordinary attribute. Deleting the attribute resets the | |
216 property. """ | |
217 | |
218 def __init__(self, func): | |
219 self.__doc__ = getattr(func, '__doc__') | |
220 self.func = func | |
221 | |
222 def __get__(self, obj, cls): | |
223 if obj is None: return self | |
224 value = obj.__dict__[self.func.__name__] = self.func(obj) | |
225 return value | |
226 | |
227 | |
228 class lazy_attribute(object): | |
229 """ A property that caches itself to the class object. """ | |
230 | |
231 def __init__(self, func): | |
232 functools.update_wrapper(self, func, updated=[]) | |
233 self.getter = func | |
234 | |
235 def __get__(self, obj, cls): | |
236 value = self.getter(cls) | |
237 setattr(cls, self.__name__, value) | |
238 return value | |
239 | |
240 ############################################################################### | |
241 # Exceptions and Events ######################################################## | |
242 ############################################################################### | |
243 | |
244 | |
245 class BottleException(Exception): | |
246 """ A base class for exceptions used by bottle. """ | |
247 pass | |
248 | |
249 ############################################################################### | |
250 # Routing ###################################################################### | |
251 ############################################################################### | |
252 | |
253 | |
254 class RouteError(BottleException): | |
255 """ This is a base class for all routing related exceptions """ | |
256 | |
257 | |
258 class RouteReset(BottleException): | |
259 """ If raised by a plugin or request handler, the route is reset and all | |
260 plugins are re-applied. """ | |
261 | |
262 | |
263 class RouterUnknownModeError(RouteError): | |
264 | |
265 pass | |
266 | |
267 | |
268 class RouteSyntaxError(RouteError): | |
269 """ The route parser found something not supported by this router. """ | |
270 | |
271 | |
272 class RouteBuildError(RouteError): | |
273 """ The route could not be built. """ | |
274 | |
275 | |
276 def _re_flatten(p): | |
277 """ Turn all capturing groups in a regular expression pattern into | |
278 non-capturing groups. """ | |
279 if '(' not in p: | |
280 return p | |
281 return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if | |
282 len(m.group(1)) % 2 else m.group(1) + '(?:', p) | |
283 | |
284 | |
285 class Router(object): | |
286 """ A Router is an ordered collection of route->target pairs. It is used to | |
287 efficiently match WSGI requests against a number of routes and return | |
288 the first target that satisfies the request. The target may be anything, | |
289 usually a string, ID or callable object. A route consists of a path-rule | |
290 and a HTTP method. | |
291 | |
292 The path-rule is either a static path (e.g. `/contact`) or a dynamic | |
293 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax | |
294 and details on the matching order are described in docs:`routing`. | |
295 """ | |
296 | |
297 default_pattern = '[^/]+' | |
298 default_filter = 're' | |
299 | |
300 #: The current CPython regexp implementation does not allow more | |
301 #: than 99 matching groups per regular expression. | |
302 _MAX_GROUPS_PER_PATTERN = 99 | |
303 | |
304 def __init__(self, strict=False): | |
305 self.rules = [] # All rules in order | |
306 self._groups = {} # index of regexes to find them in dyna_routes | |
307 self.builder = {} # Data structure for the url builder | |
308 self.static = {} # Search structure for static routes | |
309 self.dyna_routes = {} | |
310 self.dyna_regexes = {} # Search structure for dynamic routes | |
311 #: If true, static routes are no longer checked first. | |
312 self.strict_order = strict | |
313 self.filters = { | |
314 're': lambda conf: (_re_flatten(conf or self.default_pattern), | |
315 None, None), | |
316 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), | |
317 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), | |
318 'path': lambda conf: (r'.+?', None, None) | |
319 } | |
320 | |
321 def add_filter(self, name, func): | |
322 """ Add a filter. The provided function is called with the configuration | |
323 string as parameter and must return a (regexp, to_python, to_url) tuple. | |
324 The first element is a string, the last two are callables or None. """ | |
325 self.filters[name] = func | |
326 | |
327 rule_syntax = re.compile('(\\\\*)' | |
328 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' | |
329 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' | |
330 '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') | |
331 | |
332 def _itertokens(self, rule): | |
333 offset, prefix = 0, '' | |
334 for match in self.rule_syntax.finditer(rule): | |
335 prefix += rule[offset:match.start()] | |
336 g = match.groups() | |
337 if len(g[0]) % 2: # Escaped wildcard | |
338 prefix += match.group(0)[len(g[0]):] | |
339 offset = match.end() | |
340 continue | |
341 if prefix: | |
342 yield prefix, None, None | |
343 name, filtr, conf = g[4:7] if g[2] is None else g[1:4] | |
344 yield name, filtr or 'default', conf or None | |
345 offset, prefix = match.end(), '' | |
346 if offset <= len(rule) or prefix: | |
347 yield prefix + rule[offset:], None, None | |
348 | |
349 def add(self, rule, method, target, name=None): | |
350 """ Add a new rule or replace the target for an existing rule. """ | |
351 anons = 0 # Number of anonymous wildcards found | |
352 keys = [] # Names of keys | |
353 pattern = '' # Regular expression pattern with named groups | |
354 filters = [] # Lists of wildcard input filters | |
355 builder = [] # Data structure for the URL builder | |
356 is_static = True | |
357 | |
358 for key, mode, conf in self._itertokens(rule): | |
359 if mode: | |
360 is_static = False | |
361 if mode == 'default': mode = self.default_filter | |
362 mask, in_filter, out_filter = self.filters[mode](conf) | |
363 if not key: | |
364 pattern += '(?:%s)' % mask | |
365 key = 'anon%d' % anons | |
366 anons += 1 | |
367 else: | |
368 pattern += '(?P<%s>%s)' % (key, mask) | |
369 keys.append(key) | |
370 if in_filter: filters.append((key, in_filter)) | |
371 builder.append((key, out_filter or str)) | |
372 elif key: | |
373 pattern += re.escape(key) | |
374 builder.append((None, key)) | |
375 | |
376 self.builder[rule] = builder | |
377 if name: self.builder[name] = builder | |
378 | |
379 if is_static and not self.strict_order: | |
380 self.static.setdefault(method, {}) | |
381 self.static[method][self.build(rule)] = (target, None) | |
382 return | |
383 | |
384 try: | |
385 re_pattern = re.compile('^(%s)$' % pattern) | |
386 re_match = re_pattern.match | |
387 except re.error: | |
388 raise RouteSyntaxError("Could not add Route: %s (%s)" % | |
389 (rule, _e())) | |
390 | |
391 if filters: | |
392 | |
393 def getargs(path): | |
394 url_args = re_match(path).groupdict() | |
395 for name, wildcard_filter in filters: | |
396 try: | |
397 url_args[name] = wildcard_filter(url_args[name]) | |
398 except ValueError: | |
399 raise HTTPError(400, 'Path has wrong format.') | |
400 return url_args | |
401 elif re_pattern.groupindex: | |
402 | |
403 def getargs(path): | |
404 return re_match(path).groupdict() | |
405 else: | |
406 getargs = None | |
407 | |
408 flatpat = _re_flatten(pattern) | |
409 whole_rule = (rule, flatpat, target, getargs) | |
410 | |
411 if (flatpat, method) in self._groups: | |
412 if DEBUG: | |
413 msg = 'Route <%s %s> overwrites a previously defined route' | |
414 warnings.warn(msg % (method, rule), RuntimeWarning) | |
415 self.dyna_routes[method][ | |
416 self._groups[flatpat, method]] = whole_rule | |
417 else: | |
418 self.dyna_routes.setdefault(method, []).append(whole_rule) | |
419 self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 | |
420 | |
421 self._compile(method) | |
422 | |
423 def _compile(self, method): | |
424 all_rules = self.dyna_routes[method] | |
425 comborules = self.dyna_regexes[method] = [] | |
426 maxgroups = self._MAX_GROUPS_PER_PATTERN | |
427 for x in range(0, len(all_rules), maxgroups): | |
428 some = all_rules[x:x + maxgroups] | |
429 combined = (flatpat for (_, flatpat, _, _) in some) | |
430 combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) | |
431 combined = re.compile(combined).match | |
432 rules = [(target, getargs) for (_, _, target, getargs) in some] | |
433 comborules.append((combined, rules)) | |
434 | |
435 def build(self, _name, *anons, **query): | |
436 """ Build an URL by filling the wildcards in a rule. """ | |
437 builder = self.builder.get(_name) | |
438 if not builder: | |
439 raise RouteBuildError("No route with that name.", _name) | |
440 try: | |
441 for i, value in enumerate(anons): | |
442 query['anon%d' % i] = value | |
443 url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) | |
444 return url if not query else url + '?' + urlencode(query) | |
445 except KeyError: | |
446 raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) | |
447 | |
448 def match(self, environ): | |
449 """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ | |
450 verb = environ['REQUEST_METHOD'].upper() | |
451 path = environ['PATH_INFO'] or '/' | |
452 | |
453 if verb == 'HEAD': | |
454 methods = ['PROXY', verb, 'GET', 'ANY'] | |
455 else: | |
456 methods = ['PROXY', verb, 'ANY'] | |
457 | |
458 for method in methods: | |
459 if method in self.static and path in self.static[method]: | |
460 target, getargs = self.static[method][path] | |
461 return target, getargs(path) if getargs else {} | |
462 elif method in self.dyna_regexes: | |
463 for combined, rules in self.dyna_regexes[method]: | |
464 match = combined(path) | |
465 if match: | |
466 target, getargs = rules[match.lastindex - 1] | |
467 return target, getargs(path) if getargs else {} | |
468 | |
469 # No matching route found. Collect alternative methods for 405 response | |
470 allowed = set([]) | |
471 nocheck = set(methods) | |
472 for method in set(self.static) - nocheck: | |
473 if path in self.static[method]: | |
474 allowed.add(verb) | |
475 for method in set(self.dyna_regexes) - allowed - nocheck: | |
476 for combined, rules in self.dyna_regexes[method]: | |
477 match = combined(path) | |
478 if match: | |
479 allowed.add(method) | |
480 if allowed: | |
481 allow_header = ",".join(sorted(allowed)) | |
482 raise HTTPError(405, "Method not allowed.", Allow=allow_header) | |
483 | |
484 # No matching route and no alternative method found. We give up | |
485 raise HTTPError(404, "Not found: " + repr(path)) | |
486 | |
487 | |
488 class Route(object): | |
489 """ This class wraps a route callback along with route specific metadata and | |
490 configuration and applies Plugins on demand. It is also responsible for | |
491 turing an URL path rule into a regular expression usable by the Router. | |
492 """ | |
493 | |
494 def __init__(self, app, rule, method, callback, | |
495 name=None, | |
496 plugins=None, | |
497 skiplist=None, **config): | |
498 #: The application this route is installed to. | |
499 self.app = app | |
500 #: The path-rule string (e.g. ``/wiki/<page>``). | |
501 self.rule = rule | |
502 #: The HTTP method as a string (e.g. ``GET``). | |
503 self.method = method | |
504 #: The original callback with no plugins applied. Useful for introspection. | |
505 self.callback = callback | |
506 #: The name of the route (if specified) or ``None``. | |
507 self.name = name or None | |
508 #: A list of route-specific plugins (see :meth:`Bottle.route`). | |
509 self.plugins = plugins or [] | |
510 #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). | |
511 self.skiplist = skiplist or [] | |
512 #: Additional keyword arguments passed to the :meth:`Bottle.route` | |
513 #: decorator are stored in this dictionary. Used for route-specific | |
514 #: plugin configuration and meta-data. | |
515 self.config = ConfigDict().load_dict(config) | |
516 | |
517 @cached_property | |
518 def call(self): | |
519 """ The route callback with all plugins applied. This property is | |
520 created on demand and then cached to speed up subsequent requests.""" | |
521 return self._make_callback() | |
522 | |
523 def reset(self): | |
524 """ Forget any cached values. The next time :attr:`call` is accessed, | |
525 all plugins are re-applied. """ | |
526 self.__dict__.pop('call', None) | |
527 | |
528 def prepare(self): | |
529 """ Do all on-demand work immediately (useful for debugging).""" | |
530 self.call | |
531 | |
532 def all_plugins(self): | |
533 """ Yield all Plugins affecting this route. """ | |
534 unique = set() | |
535 for p in reversed(self.app.plugins + self.plugins): | |
536 if True in self.skiplist: break | |
537 name = getattr(p, 'name', False) | |
538 if name and (name in self.skiplist or name in unique): continue | |
539 if p in self.skiplist or type(p) in self.skiplist: continue | |
540 if name: unique.add(name) | |
541 yield p | |
542 | |
543 def _make_callback(self): | |
544 callback = self.callback | |
545 for plugin in self.all_plugins(): | |
546 try: | |
547 if hasattr(plugin, 'apply'): | |
548 callback = plugin.apply(callback, self) | |
549 else: | |
550 callback = plugin(callback) | |
551 except RouteReset: # Try again with changed configuration. | |
552 return self._make_callback() | |
553 if not callback is self.callback: | |
554 update_wrapper(callback, self.callback) | |
555 return callback | |
556 | |
557 def get_undecorated_callback(self): | |
558 """ Return the callback. If the callback is a decorated function, try to | |
559 recover the original function. """ | |
560 func = self.callback | |
561 func = getattr(func, '__func__' if py3k else 'im_func', func) | |
562 closure_attr = '__closure__' if py3k else 'func_closure' | |
563 while hasattr(func, closure_attr) and getattr(func, closure_attr): | |
564 attributes = getattr(func, closure_attr) | |
565 func = attributes[0].cell_contents | |
566 | |
567 # in case of decorators with multiple arguments | |
568 if not isinstance(func, FunctionType): | |
569 # pick first FunctionType instance from multiple arguments | |
570 func = filter(lambda x: isinstance(x, FunctionType), | |
571 map(lambda x: x.cell_contents, attributes)) | |
572 func = list(func)[0] # py3 support | |
573 return func | |
574 | |
575 def get_callback_args(self): | |
576 """ Return a list of argument names the callback (most likely) accepts | |
577 as keyword arguments. If the callback is a decorated function, try | |
578 to recover the original function before inspection. """ | |
579 return getargspec(self.get_undecorated_callback())[0] | |
580 | |
581 def get_config(self, key, default=None): | |
582 """ Lookup a config field and return its value, first checking the | |
583 route.config, then route.app.config.""" | |
584 for conf in (self.config, self.app.config): | |
585 if key in conf: return conf[key] | |
586 return default | |
587 | |
588 def __repr__(self): | |
589 cb = self.get_undecorated_callback() | |
590 return '<%s %r %r>' % (self.method, self.rule, cb) | |
591 | |
592 ############################################################################### | |
593 # Application Object ########################################################### | |
594 ############################################################################### | |
595 | |
596 | |
597 class Bottle(object): | |
598 """ Each Bottle object represents a single, distinct web application and | |
599 consists of routes, callbacks, plugins, resources and configuration. | |
600 Instances are callable WSGI applications. | |
601 | |
602 :param catchall: If true (default), handle all exceptions. Turn off to | |
603 let debugging middleware handle exceptions. | |
604 """ | |
605 | |
606 def __init__(self, catchall=True, autojson=True): | |
607 | |
608 #: A :class:`ConfigDict` for app specific configuration. | |
609 self.config = ConfigDict() | |
610 self.config._on_change = functools.partial(self.trigger_hook, 'config') | |
611 self.config.meta_set('autojson', 'validate', bool) | |
612 self.config.meta_set('catchall', 'validate', bool) | |
613 self.config['catchall'] = catchall | |
614 self.config['autojson'] = autojson | |
615 | |
616 #: A :class:`ResourceManager` for application files | |
617 self.resources = ResourceManager() | |
618 | |
619 self.routes = [] # List of installed :class:`Route` instances. | |
620 self.router = Router() # Maps requests to :class:`Route` instances. | |
621 self.error_handler = {} | |
622 | |
623 # Core plugins | |
624 self.plugins = [] # List of installed plugins. | |
625 if self.config['autojson']: | |
626 self.install(JSONPlugin()) | |
627 self.install(TemplatePlugin()) | |
628 | |
629 #: If true, most exceptions are caught and returned as :exc:`HTTPError` | |
630 catchall = DictProperty('config', 'catchall') | |
631 | |
632 __hook_names = 'before_request', 'after_request', 'app_reset', 'config' | |
633 __hook_reversed = 'after_request' | |
634 | |
635 @cached_property | |
636 def _hooks(self): | |
637 return dict((name, []) for name in self.__hook_names) | |
638 | |
639 def add_hook(self, name, func): | |
640 """ Attach a callback to a hook. Three hooks are currently implemented: | |
641 | |
642 before_request | |
643 Executed once before each request. The request context is | |
644 available, but no routing has happened yet. | |
645 after_request | |
646 Executed once after each request regardless of its outcome. | |
647 app_reset | |
648 Called whenever :meth:`Bottle.reset` is called. | |
649 """ | |
650 if name in self.__hook_reversed: | |
651 self._hooks[name].insert(0, func) | |
652 else: | |
653 self._hooks[name].append(func) | |
654 | |
655 def remove_hook(self, name, func): | |
656 """ Remove a callback from a hook. """ | |
657 if name in self._hooks and func in self._hooks[name]: | |
658 self._hooks[name].remove(func) | |
659 return True | |
660 | |
661 def trigger_hook(self, __name, *args, **kwargs): | |
662 """ Trigger a hook and return a list of results. """ | |
663 return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] | |
664 | |
665 def hook(self, name): | |
666 """ Return a decorator that attaches a callback to a hook. See | |
667 :meth:`add_hook` for details.""" | |
668 | |
669 def decorator(func): | |
670 self.add_hook(name, func) | |
671 return func | |
672 | |
673 return decorator | |
674 | |
675 def mount(self, prefix, app, **options): | |
676 """ Mount an application (:class:`Bottle` or plain WSGI) to a specific | |
677 URL prefix. Example:: | |
678 | |
679 root_app.mount('/admin/', admin_app) | |
680 | |
681 :param prefix: path prefix or `mount-point`. If it ends in a slash, | |
682 that slash is mandatory. | |
683 :param app: an instance of :class:`Bottle` or a WSGI application. | |
684 | |
685 All other parameters are passed to the underlying :meth:`route` call. | |
686 """ | |
687 | |
688 segments = [p for p in prefix.split('/') if p] | |
689 if not segments: raise ValueError('Empty path prefix.') | |
690 path_depth = len(segments) | |
691 | |
692 def mountpoint_wrapper(): | |
693 try: | |
694 request.path_shift(path_depth) | |
695 rs = HTTPResponse([]) | |
696 | |
697 def start_response(status, headerlist, exc_info=None): | |
698 if exc_info: | |
699 _raise(*exc_info) | |
700 rs.status = status | |
701 for name, value in headerlist: | |
702 rs.add_header(name, value) | |
703 return rs.body.append | |
704 | |
705 body = app(request.environ, start_response) | |
706 rs.body = itertools.chain(rs.body, body) if rs.body else body | |
707 return rs | |
708 finally: | |
709 request.path_shift(-path_depth) | |
710 | |
711 options.setdefault('skip', True) | |
712 options.setdefault('method', 'PROXY') | |
713 options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) | |
714 options['callback'] = mountpoint_wrapper | |
715 | |
716 self.route('/%s/<:re:.*>' % '/'.join(segments), **options) | |
717 if not prefix.endswith('/'): | |
718 self.route('/' + '/'.join(segments), **options) | |
719 | |
720 def merge(self, routes): | |
721 """ Merge the routes of another :class:`Bottle` application or a list of | |
722 :class:`Route` objects into this application. The routes keep their | |
723 'owner', meaning that the :data:`Route.app` attribute is not | |
724 changed. """ | |
725 if isinstance(routes, Bottle): | |
726 routes = routes.routes | |
727 for route in routes: | |
728 self.add_route(route) | |
729 | |
730 def install(self, plugin): | |
731 """ Add a plugin to the list of plugins and prepare it for being | |
732 applied to all routes of this application. A plugin may be a simple | |
733 decorator or an object that implements the :class:`Plugin` API. | |
734 """ | |
735 if hasattr(plugin, 'setup'): plugin.setup(self) | |
736 if not callable(plugin) and not hasattr(plugin, 'apply'): | |
737 raise TypeError("Plugins must be callable or implement .apply()") | |
738 self.plugins.append(plugin) | |
739 self.reset() | |
740 return plugin | |
741 | |
742 def uninstall(self, plugin): | |
743 """ Uninstall plugins. Pass an instance to remove a specific plugin, a type | |
744 object to remove all plugins that match that type, a string to remove | |
745 all plugins with a matching ``name`` attribute or ``True`` to remove all | |
746 plugins. Return the list of removed plugins. """ | |
747 removed, remove = [], plugin | |
748 for i, plugin in list(enumerate(self.plugins))[::-1]: | |
749 if remove is True or remove is plugin or remove is type(plugin) \ | |
750 or getattr(plugin, 'name', True) == remove: | |
751 removed.append(plugin) | |
752 del self.plugins[i] | |
753 if hasattr(plugin, 'close'): plugin.close() | |
754 if removed: self.reset() | |
755 return removed | |
756 | |
757 def reset(self, route=None): | |
758 """ Reset all routes (force plugins to be re-applied) and clear all | |
759 caches. If an ID or route object is given, only that specific route | |
760 is affected. """ | |
761 if route is None: routes = self.routes | |
762 elif isinstance(route, Route): routes = [route] | |
763 else: routes = [self.routes[route]] | |
764 for route in routes: | |
765 route.reset() | |
766 if DEBUG: | |
767 for route in routes: | |
768 route.prepare() | |
769 self.trigger_hook('app_reset') | |
770 | |
771 def close(self): | |
772 """ Close the application and all installed plugins. """ | |
773 for plugin in self.plugins: | |
774 if hasattr(plugin, 'close'): plugin.close() | |
775 | |
776 def run(self, **kwargs): | |
777 """ Calls :func:`run` with the same parameters. """ | |
778 run(self, **kwargs) | |
779 | |
780 def match(self, environ): | |
781 """ Search for a matching route and return a (:class:`Route` , urlargs) | |
782 tuple. The second value is a dictionary with parameters extracted | |
783 from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" | |
784 return self.router.match(environ) | |
785 | |
786 def get_url(self, routename, **kargs): | |
787 """ Return a string that matches a named route """ | |
788 scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' | |
789 location = self.router.build(routename, **kargs).lstrip('/') | |
790 return urljoin(urljoin('/', scriptname), location) | |
791 | |
792 def add_route(self, route): | |
793 """ Add a route object, but do not change the :data:`Route.app` | |
794 attribute.""" | |
795 self.routes.append(route) | |
796 self.router.add(route.rule, route.method, route, name=route.name) | |
797 if DEBUG: route.prepare() | |
798 | |
799 def route(self, | |
800 path=None, | |
801 method='GET', | |
802 callback=None, | |
803 name=None, | |
804 apply=None, | |
805 skip=None, **config): | |
806 """ A decorator to bind a function to a request URL. Example:: | |
807 | |
808 @app.route('/hello/<name>') | |
809 def hello(name): | |
810 return 'Hello %s' % name | |
811 | |
812 The ``<name>`` part is a wildcard. See :class:`Router` for syntax | |
813 details. | |
814 | |
815 :param path: Request path or a list of paths to listen to. If no | |
816 path is specified, it is automatically generated from the | |
817 signature of the function. | |
818 :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of | |
819 methods to listen to. (default: `GET`) | |
820 :param callback: An optional shortcut to avoid the decorator | |
821 syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` | |
822 :param name: The name for this route. (default: None) | |
823 :param apply: A decorator or plugin or a list of plugins. These are | |
824 applied to the route callback in addition to installed plugins. | |
825 :param skip: A list of plugins, plugin classes or names. Matching | |
826 plugins are not installed to this route. ``True`` skips all. | |
827 | |
828 Any additional keyword arguments are stored as route-specific | |
829 configuration and passed to plugins (see :meth:`Plugin.apply`). | |
830 """ | |
831 if callable(path): path, callback = None, path | |
832 plugins = makelist(apply) | |
833 skiplist = makelist(skip) | |
834 | |
835 def decorator(callback): | |
836 if isinstance(callback, basestring): callback = load(callback) | |
837 for rule in makelist(path) or yieldroutes(callback): | |
838 for verb in makelist(method): | |
839 verb = verb.upper() | |
840 route = Route(self, rule, verb, callback, | |
841 name=name, | |
842 plugins=plugins, | |
843 skiplist=skiplist, **config) | |
844 self.add_route(route) | |
845 return callback | |
846 | |
847 return decorator(callback) if callback else decorator | |
848 | |
849 def get(self, path=None, method='GET', **options): | |
850 """ Equals :meth:`route`. """ | |
851 return self.route(path, method, **options) | |
852 | |
853 def post(self, path=None, method='POST', **options): | |
854 """ Equals :meth:`route` with a ``POST`` method parameter. """ | |
855 return self.route(path, method, **options) | |
856 | |
857 def put(self, path=None, method='PUT', **options): | |
858 """ Equals :meth:`route` with a ``PUT`` method parameter. """ | |
859 return self.route(path, method, **options) | |
860 | |
861 def delete(self, path=None, method='DELETE', **options): | |
862 """ Equals :meth:`route` with a ``DELETE`` method parameter. """ | |
863 return self.route(path, method, **options) | |
864 | |
865 def patch(self, path=None, method='PATCH', **options): | |
866 """ Equals :meth:`route` with a ``PATCH`` method parameter. """ | |
867 return self.route(path, method, **options) | |
868 | |
869 def error(self, code=500): | |
870 """ Decorator: Register an output handler for a HTTP error code""" | |
871 | |
872 def wrapper(handler): | |
873 self.error_handler[int(code)] = handler | |
874 return handler | |
875 | |
876 return wrapper | |
877 | |
878 def default_error_handler(self, res): | |
879 return tob(template(ERROR_PAGE_TEMPLATE, e=res)) | |
880 | |
881 def _handle(self, environ): | |
882 path = environ['bottle.raw_path'] = environ['PATH_INFO'] | |
883 if py3k: | |
884 try: | |
885 environ['PATH_INFO'] = path.encode('latin1').decode('utf8') | |
886 except UnicodeError: | |
887 return HTTPError(400, 'Invalid path string. Expected UTF-8') | |
888 | |
889 try: | |
890 environ['bottle.app'] = self | |
891 request.bind(environ) | |
892 response.bind() | |
893 try: | |
894 self.trigger_hook('before_request') | |
895 route, args = self.router.match(environ) | |
896 environ['route.handle'] = route | |
897 environ['bottle.route'] = route | |
898 environ['route.url_args'] = args | |
899 return route.call(**args) | |
900 finally: | |
901 self.trigger_hook('after_request') | |
902 except HTTPResponse: | |
903 return _e() | |
904 except RouteReset: | |
905 route.reset() | |
906 return self._handle(environ) | |
907 except (KeyboardInterrupt, SystemExit, MemoryError): | |
908 raise | |
909 except Exception: | |
910 if not self.catchall: raise | |
911 stacktrace = format_exc() | |
912 environ['wsgi.errors'].write(stacktrace) | |
913 return HTTPError(500, "Internal Server Error", _e(), stacktrace) | |
914 | |
915 def _cast(self, out, peek=None): | |
916 """ Try to convert the parameter into something WSGI compatible and set | |
917 correct HTTP headers when possible. | |
918 Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, | |
919 iterable of strings and iterable of unicodes | |
920 """ | |
921 | |
922 # Empty output is done here | |
923 if not out: | |
924 if 'Content-Length' not in response: | |
925 response['Content-Length'] = 0 | |
926 return [] | |
927 # Join lists of byte or unicode strings. Mixed lists are NOT supported | |
928 if isinstance(out, (tuple, list))\ | |
929 and isinstance(out[0], (bytes, unicode)): | |
930 out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' | |
931 # Encode unicode strings | |
932 if isinstance(out, unicode): | |
933 out = out.encode(response.charset) | |
934 # Byte Strings are just returned | |
935 if isinstance(out, bytes): | |
936 if 'Content-Length' not in response: | |
937 response['Content-Length'] = len(out) | |
938 return [out] | |
939 # HTTPError or HTTPException (recursive, because they may wrap anything) | |
940 # TODO: Handle these explicitly in handle() or make them iterable. | |
941 if isinstance(out, HTTPError): | |
942 out.apply(response) | |
943 out = self.error_handler.get(out.status_code, | |
944 self.default_error_handler)(out) | |
945 return self._cast(out) | |
946 if isinstance(out, HTTPResponse): | |
947 out.apply(response) | |
948 return self._cast(out.body) | |
949 | |
950 # File-like objects. | |
951 if hasattr(out, 'read'): | |
952 if 'wsgi.file_wrapper' in request.environ: | |
953 return request.environ['wsgi.file_wrapper'](out) | |
954 elif hasattr(out, 'close') or not hasattr(out, '__iter__'): | |
955 return WSGIFileWrapper(out) | |
956 | |
957 # Handle Iterables. We peek into them to detect their inner type. | |
958 try: | |
959 iout = iter(out) | |
960 first = next(iout) | |
961 while not first: | |
962 first = next(iout) | |
963 except StopIteration: | |
964 return self._cast('') | |
965 except HTTPResponse: | |
966 first = _e() | |
967 except (KeyboardInterrupt, SystemExit, MemoryError): | |
968 raise | |
969 except: | |
970 if not self.catchall: raise | |
971 first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) | |
972 | |
973 # These are the inner types allowed in iterator or generator objects. | |
974 if isinstance(first, HTTPResponse): | |
975 return self._cast(first) | |
976 elif isinstance(first, bytes): | |
977 new_iter = itertools.chain([first], iout) | |
978 elif isinstance(first, unicode): | |
979 encoder = lambda x: x.encode(response.charset) | |
980 new_iter = imap(encoder, itertools.chain([first], iout)) | |
981 else: | |
982 msg = 'Unsupported response type: %s' % type(first) | |
983 return self._cast(HTTPError(500, msg)) | |
984 if hasattr(out, 'close'): | |
985 new_iter = _closeiter(new_iter, out.close) | |
986 return new_iter | |
987 | |
988 def wsgi(self, environ, start_response): | |
989 """ The bottle WSGI-interface. """ | |
990 try: | |
991 out = self._cast(self._handle(environ)) | |
992 # rfc2616 section 4.3 | |
993 if response._status_code in (100, 101, 204, 304)\ | |
994 or environ['REQUEST_METHOD'] == 'HEAD': | |
995 if hasattr(out, 'close'): out.close() | |
996 out = [] | |
997 start_response(response._status_line, response.headerlist) | |
998 return out | |
999 except (KeyboardInterrupt, SystemExit, MemoryError): | |
1000 raise | |
1001 except: | |
1002 if not self.catchall: raise | |
1003 err = '<h1>Critical error while processing request: %s</h1>' \ | |
1004 % html_escape(environ.get('PATH_INFO', '/')) | |
1005 if DEBUG: | |
1006 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \ | |
1007 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \ | |
1008 % (html_escape(repr(_e())), html_escape(format_exc())) | |
1009 environ['wsgi.errors'].write(err) | |
1010 headers = [('Content-Type', 'text/html; charset=UTF-8')] | |
1011 start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) | |
1012 return [tob(err)] | |
1013 | |
1014 def __call__(self, environ, start_response): | |
1015 """ Each instance of :class:'Bottle' is a WSGI application. """ | |
1016 return self.wsgi(environ, start_response) | |
1017 | |
1018 def __enter__(self): | |
1019 """ Use this application as default for all module-level shortcuts. """ | |
1020 default_app.push(self) | |
1021 return self | |
1022 | |
1023 def __exit__(self, exc_type, exc_value, traceback): | |
1024 default_app.pop() | |
1025 | |
1026 ############################################################################### | |
1027 # HTTP and WSGI Tools ########################################################## | |
1028 ############################################################################### | |
1029 | |
1030 | |
1031 class BaseRequest(object): | |
1032 """ A wrapper for WSGI environment dictionaries that adds a lot of | |
1033 convenient access methods and properties. Most of them are read-only. | |
1034 | |
1035 Adding new attributes to a request actually adds them to the environ | |
1036 dictionary (as 'bottle.request.ext.<name>'). This is the recommended | |
1037 way to store and access request-specific data. | |
1038 """ | |
1039 | |
1040 __slots__ = ('environ', ) | |
1041 | |
1042 #: Maximum size of memory buffer for :attr:`body` in bytes. | |
1043 MEMFILE_MAX = 102400 | |
1044 | |
1045 def __init__(self, environ=None): | |
1046 """ Wrap a WSGI environ dictionary. """ | |
1047 #: The wrapped WSGI environ dictionary. This is the only real attribute. | |
1048 #: All other attributes actually are read-only properties. | |
1049 self.environ = {} if environ is None else environ | |
1050 self.environ['bottle.request'] = self | |
1051 | |
1052 @DictProperty('environ', 'bottle.app', read_only=True) | |
1053 def app(self): | |
1054 """ Bottle application handling this request. """ | |
1055 raise RuntimeError('This request is not connected to an application.') | |
1056 | |
1057 @DictProperty('environ', 'bottle.route', read_only=True) | |
1058 def route(self): | |
1059 """ The bottle :class:`Route` object that matches this request. """ | |
1060 raise RuntimeError('This request is not connected to a route.') | |
1061 | |
1062 @DictProperty('environ', 'route.url_args', read_only=True) | |
1063 def url_args(self): | |
1064 """ The arguments extracted from the URL. """ | |
1065 raise RuntimeError('This request is not connected to a route.') | |
1066 | |
1067 @property | |
1068 def path(self): | |
1069 """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix | |
1070 broken clients and avoid the "empty path" edge case). """ | |
1071 return '/' + self.environ.get('PATH_INFO', '').lstrip('/') | |
1072 | |
1073 @property | |
1074 def method(self): | |
1075 """ The ``REQUEST_METHOD`` value as an uppercase string. """ | |
1076 return self.environ.get('REQUEST_METHOD', 'GET').upper() | |
1077 | |
1078 @DictProperty('environ', 'bottle.request.headers', read_only=True) | |
1079 def headers(self): | |
1080 """ A :class:`WSGIHeaderDict` that provides case-insensitive access to | |
1081 HTTP request headers. """ | |
1082 return WSGIHeaderDict(self.environ) | |
1083 | |
1084 def get_header(self, name, default=None): | |
1085 """ Return the value of a request header, or a given default value. """ | |
1086 return self.headers.get(name, default) | |
1087 | |
1088 @DictProperty('environ', 'bottle.request.cookies', read_only=True) | |
1089 def cookies(self): | |
1090 """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT | |
1091 decoded. Use :meth:`get_cookie` if you expect signed cookies. """ | |
1092 cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() | |
1093 return FormsDict((c.key, c.value) for c in cookies) | |
1094 | |
1095 def get_cookie(self, key, default=None, secret=None): | |
1096 """ Return the content of a cookie. To read a `Signed Cookie`, the | |
1097 `secret` must match the one used to create the cookie (see | |
1098 :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing | |
1099 cookie or wrong signature), return a default value. """ | |
1100 value = self.cookies.get(key) | |
1101 if secret and value: | |
1102 dec = cookie_decode(value, secret) # (key, value) tuple or None | |
1103 return dec[1] if dec and dec[0] == key else default | |
1104 return value or default | |
1105 | |
1106 @DictProperty('environ', 'bottle.request.query', read_only=True) | |
1107 def query(self): | |
1108 """ The :attr:`query_string` parsed into a :class:`FormsDict`. These | |
1109 values are sometimes called "URL arguments" or "GET parameters", but | |
1110 not to be confused with "URL wildcards" as they are provided by the | |
1111 :class:`Router`. """ | |
1112 get = self.environ['bottle.get'] = FormsDict() | |
1113 pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) | |
1114 for key, value in pairs: | |
1115 get[key] = value | |
1116 return get | |
1117 | |
1118 @DictProperty('environ', 'bottle.request.forms', read_only=True) | |
1119 def forms(self): | |
1120 """ Form values parsed from an `url-encoded` or `multipart/form-data` | |
1121 encoded POST or PUT request body. The result is returned as a | |
1122 :class:`FormsDict`. All keys and values are strings. File uploads | |
1123 are stored separately in :attr:`files`. """ | |
1124 forms = FormsDict() | |
1125 for name, item in self.POST.allitems(): | |
1126 if not isinstance(item, FileUpload): | |
1127 forms[name] = item | |
1128 return forms | |
1129 | |
1130 @DictProperty('environ', 'bottle.request.params', read_only=True) | |
1131 def params(self): | |
1132 """ A :class:`FormsDict` with the combined values of :attr:`query` and | |
1133 :attr:`forms`. File uploads are stored in :attr:`files`. """ | |
1134 params = FormsDict() | |
1135 for key, value in self.query.allitems(): | |
1136 params[key] = value | |
1137 for key, value in self.forms.allitems(): | |
1138 params[key] = value | |
1139 return params | |
1140 | |
1141 @DictProperty('environ', 'bottle.request.files', read_only=True) | |
1142 def files(self): | |
1143 """ File uploads parsed from `multipart/form-data` encoded POST or PUT | |
1144 request body. The values are instances of :class:`FileUpload`. | |
1145 | |
1146 """ | |
1147 files = FormsDict() | |
1148 for name, item in self.POST.allitems(): | |
1149 if isinstance(item, FileUpload): | |
1150 files[name] = item | |
1151 return files | |
1152 | |
1153 @DictProperty('environ', 'bottle.request.json', read_only=True) | |
1154 def json(self): | |
1155 """ If the ``Content-Type`` header is ``application/json``, this | |
1156 property holds the parsed content of the request body. Only requests | |
1157 smaller than :attr:`MEMFILE_MAX` are processed to avoid memory | |
1158 exhaustion. """ | |
1159 ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] | |
1160 if ctype == 'application/json': | |
1161 b = self._get_body_string() | |
1162 if not b: | |
1163 return None | |
1164 return json_loads(b) | |
1165 return None | |
1166 | |
1167 def _iter_body(self, read, bufsize): | |
1168 maxread = max(0, self.content_length) | |
1169 while maxread: | |
1170 part = read(min(maxread, bufsize)) | |
1171 if not part: break | |
1172 yield part | |
1173 maxread -= len(part) | |
1174 | |
1175 @staticmethod | |
1176 def _iter_chunked(read, bufsize): | |
1177 err = HTTPError(400, 'Error while parsing chunked transfer body.') | |
1178 rn, sem, bs = tob('\r\n'), tob(';'), tob('') | |
1179 while True: | |
1180 header = read(1) | |
1181 while header[-2:] != rn: | |
1182 c = read(1) | |
1183 header += c | |
1184 if not c: raise err | |
1185 if len(header) > bufsize: raise err | |
1186 size, _, _ = header.partition(sem) | |
1187 try: | |
1188 maxread = int(tonat(size.strip()), 16) | |
1189 except ValueError: | |
1190 raise err | |
1191 if maxread == 0: break | |
1192 buff = bs | |
1193 while maxread > 0: | |
1194 if not buff: | |
1195 buff = read(min(maxread, bufsize)) | |
1196 part, buff = buff[:maxread], buff[maxread:] | |
1197 if not part: raise err | |
1198 yield part | |
1199 maxread -= len(part) | |
1200 if read(2) != rn: | |
1201 raise err | |
1202 | |
1203 @DictProperty('environ', 'bottle.request.body', read_only=True) | |
1204 def _body(self): | |
1205 try: | |
1206 read_func = self.environ['wsgi.input'].read | |
1207 except KeyError: | |
1208 self.environ['wsgi.input'] = BytesIO() | |
1209 return self.environ['wsgi.input'] | |
1210 body_iter = self._iter_chunked if self.chunked else self._iter_body | |
1211 body, body_size, is_temp_file = BytesIO(), 0, False | |
1212 for part in body_iter(read_func, self.MEMFILE_MAX): | |
1213 body.write(part) | |
1214 body_size += len(part) | |
1215 if not is_temp_file and body_size > self.MEMFILE_MAX: | |
1216 body, tmp = TemporaryFile(mode='w+b'), body | |
1217 body.write(tmp.getvalue()) | |
1218 del tmp | |
1219 is_temp_file = True | |
1220 self.environ['wsgi.input'] = body | |
1221 body.seek(0) | |
1222 return body | |
1223 | |
1224 def _get_body_string(self): | |
1225 """ read body until content-length or MEMFILE_MAX into a string. Raise | |
1226 HTTPError(413) on requests that are to large. """ | |
1227 clen = self.content_length | |
1228 if clen > self.MEMFILE_MAX: | |
1229 raise HTTPError(413, 'Request entity too large') | |
1230 if clen < 0: clen = self.MEMFILE_MAX + 1 | |
1231 data = self.body.read(clen) | |
1232 if len(data) > self.MEMFILE_MAX: # Fail fast | |
1233 raise HTTPError(413, 'Request entity too large') | |
1234 return data | |
1235 | |
1236 @property | |
1237 def body(self): | |
1238 """ The HTTP request body as a seek-able file-like object. Depending on | |
1239 :attr:`MEMFILE_MAX`, this is either a temporary file or a | |
1240 :class:`io.BytesIO` instance. Accessing this property for the first | |
1241 time reads and replaces the ``wsgi.input`` environ variable. | |
1242 Subsequent accesses just do a `seek(0)` on the file object. """ | |
1243 self._body.seek(0) | |
1244 return self._body | |
1245 | |
1246 @property | |
1247 def chunked(self): | |
1248 """ True if Chunked transfer encoding was. """ | |
1249 return 'chunked' in self.environ.get( | |
1250 'HTTP_TRANSFER_ENCODING', '').lower() | |
1251 | |
1252 #: An alias for :attr:`query`. | |
1253 GET = query | |
1254 | |
1255 @DictProperty('environ', 'bottle.request.post', read_only=True) | |
1256 def POST(self): | |
1257 """ The values of :attr:`forms` and :attr:`files` combined into a single | |
1258 :class:`FormsDict`. Values are either strings (form values) or | |
1259 instances of :class:`cgi.FieldStorage` (file uploads). | |
1260 """ | |
1261 post = FormsDict() | |
1262 # We default to application/x-www-form-urlencoded for everything that | |
1263 # is not multipart and take the fast path (also: 3.1 workaround) | |
1264 if not self.content_type.startswith('multipart/'): | |
1265 pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) | |
1266 for key, value in pairs: | |
1267 post[key] = value | |
1268 return post | |
1269 | |
1270 safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi | |
1271 for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): | |
1272 if key in self.environ: safe_env[key] = self.environ[key] | |
1273 args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) | |
1274 if py31: | |
1275 args['fp'] = NCTextIOWrapper(args['fp'], | |
1276 encoding='utf8', | |
1277 newline='\n') | |
1278 elif py3k: | |
1279 args['encoding'] = 'utf8' | |
1280 data = cgi.FieldStorage(**args) | |
1281 self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 | |
1282 data = data.list or [] | |
1283 for item in data: | |
1284 if item.filename: | |
1285 post[item.name] = FileUpload(item.file, item.name, | |
1286 item.filename, item.headers) | |
1287 else: | |
1288 post[item.name] = item.value | |
1289 return post | |
1290 | |
1291 @property | |
1292 def url(self): | |
1293 """ The full request URI including hostname and scheme. If your app | |
1294 lives behind a reverse proxy or load balancer and you get confusing | |
1295 results, make sure that the ``X-Forwarded-Host`` header is set | |
1296 correctly. """ | |
1297 return self.urlparts.geturl() | |
1298 | |
1299 @DictProperty('environ', 'bottle.request.urlparts', read_only=True) | |
1300 def urlparts(self): | |
1301 """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. | |
1302 The tuple contains (scheme, host, path, query_string and fragment), | |
1303 but the fragment is always empty because it is not visible to the | |
1304 server. """ | |
1305 env = self.environ | |
1306 http = env.get('HTTP_X_FORWARDED_PROTO') \ | |
1307 or env.get('wsgi.url_scheme', 'http') | |
1308 host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') | |
1309 if not host: | |
1310 # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. | |
1311 host = env.get('SERVER_NAME', '127.0.0.1') | |
1312 port = env.get('SERVER_PORT') | |
1313 if port and port != ('80' if http == 'http' else '443'): | |
1314 host += ':' + port | |
1315 path = urlquote(self.fullpath) | |
1316 return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') | |
1317 | |
1318 @property | |
1319 def fullpath(self): | |
1320 """ Request path including :attr:`script_name` (if present). """ | |
1321 return urljoin(self.script_name, self.path.lstrip('/')) | |
1322 | |
1323 @property | |
1324 def query_string(self): | |
1325 """ The raw :attr:`query` part of the URL (everything in between ``?`` | |
1326 and ``#``) as a string. """ | |
1327 return self.environ.get('QUERY_STRING', '') | |
1328 | |
1329 @property | |
1330 def script_name(self): | |
1331 """ The initial portion of the URL's `path` that was removed by a higher | |
1332 level (server or routing middleware) before the application was | |
1333 called. This script path is returned with leading and tailing | |
1334 slashes. """ | |
1335 script_name = self.environ.get('SCRIPT_NAME', '').strip('/') | |
1336 return '/' + script_name + '/' if script_name else '/' | |
1337 | |
1338 def path_shift(self, shift=1): | |
1339 """ Shift path segments from :attr:`path` to :attr:`script_name` and | |
1340 vice versa. | |
1341 | |
1342 :param shift: The number of path segments to shift. May be negative | |
1343 to change the shift direction. (default: 1) | |
1344 """ | |
1345 script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) | |
1346 self['SCRIPT_NAME'], self['PATH_INFO'] = script, path | |
1347 | |
1348 @property | |
1349 def content_length(self): | |
1350 """ The request body length as an integer. The client is responsible to | |
1351 set this header. Otherwise, the real length of the body is unknown | |
1352 and -1 is returned. In this case, :attr:`body` will be empty. """ | |
1353 return int(self.environ.get('CONTENT_LENGTH') or -1) | |
1354 | |
1355 @property | |
1356 def content_type(self): | |
1357 """ The Content-Type header as a lowercase-string (default: empty). """ | |
1358 return self.environ.get('CONTENT_TYPE', '').lower() | |
1359 | |
1360 @property | |
1361 def is_xhr(self): | |
1362 """ True if the request was triggered by a XMLHttpRequest. This only | |
1363 works with JavaScript libraries that support the `X-Requested-With` | |
1364 header (most of the popular libraries do). """ | |
1365 requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') | |
1366 return requested_with.lower() == 'xmlhttprequest' | |
1367 | |
1368 @property | |
1369 def is_ajax(self): | |
1370 """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ | |
1371 return self.is_xhr | |
1372 | |
1373 @property | |
1374 def auth(self): | |
1375 """ HTTP authentication data as a (user, password) tuple. This | |
1376 implementation currently supports basic (not digest) authentication | |
1377 only. If the authentication happened at a higher level (e.g. in the | |
1378 front web-server or a middleware), the password field is None, but | |
1379 the user field is looked up from the ``REMOTE_USER`` environ | |
1380 variable. On any errors, None is returned. """ | |
1381 basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) | |
1382 if basic: return basic | |
1383 ruser = self.environ.get('REMOTE_USER') | |
1384 if ruser: return (ruser, None) | |
1385 return None | |
1386 | |
1387 @property | |
1388 def remote_route(self): | |
1389 """ A list of all IPs that were involved in this request, starting with | |
1390 the client IP and followed by zero or more proxies. This does only | |
1391 work if all proxies support the ```X-Forwarded-For`` header. Note | |
1392 that this information can be forged by malicious clients. """ | |
1393 proxy = self.environ.get('HTTP_X_FORWARDED_FOR') | |
1394 if proxy: return [ip.strip() for ip in proxy.split(',')] | |
1395 remote = self.environ.get('REMOTE_ADDR') | |
1396 return [remote] if remote else [] | |
1397 | |
1398 @property | |
1399 def remote_addr(self): | |
1400 """ The client IP as a string. Note that this information can be forged | |
1401 by malicious clients. """ | |
1402 route = self.remote_route | |
1403 return route[0] if route else None | |
1404 | |
1405 def copy(self): | |
1406 """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ | |
1407 return Request(self.environ.copy()) | |
1408 | |
1409 def get(self, value, default=None): | |
1410 return self.environ.get(value, default) | |
1411 | |
1412 def __getitem__(self, key): | |
1413 return self.environ[key] | |
1414 | |
1415 def __delitem__(self, key): | |
1416 self[key] = "" | |
1417 del (self.environ[key]) | |
1418 | |
1419 def __iter__(self): | |
1420 return iter(self.environ) | |
1421 | |
1422 def __len__(self): | |
1423 return len(self.environ) | |
1424 | |
1425 def keys(self): | |
1426 return self.environ.keys() | |
1427 | |
1428 def __setitem__(self, key, value): | |
1429 """ Change an environ value and clear all caches that depend on it. """ | |
1430 | |
1431 if self.environ.get('bottle.request.readonly'): | |
1432 raise KeyError('The environ dictionary is read-only.') | |
1433 | |
1434 self.environ[key] = value | |
1435 todelete = () | |
1436 | |
1437 if key == 'wsgi.input': | |
1438 todelete = ('body', 'forms', 'files', 'params', 'post', 'json') | |
1439 elif key == 'QUERY_STRING': | |
1440 todelete = ('query', 'params') | |
1441 elif key.startswith('HTTP_'): | |
1442 todelete = ('headers', 'cookies') | |
1443 | |
1444 for key in todelete: | |
1445 self.environ.pop('bottle.request.' + key, None) | |
1446 | |
1447 def __repr__(self): | |
1448 return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) | |
1449 | |
1450 def __getattr__(self, name): | |
1451 """ Search in self.environ for additional user defined attributes. """ | |
1452 try: | |
1453 var = self.environ['bottle.request.ext.%s' % name] | |
1454 return var.__get__(self) if hasattr(var, '__get__') else var | |
1455 except KeyError: | |
1456 raise AttributeError('Attribute %r not defined.' % name) | |
1457 | |
1458 def __setattr__(self, name, value): | |
1459 if name == 'environ': return object.__setattr__(self, name, value) | |
1460 self.environ['bottle.request.ext.%s' % name] = value | |
1461 | |
1462 | |
1463 def _hkey(s): | |
1464 return s.title().replace('_', '-') | |
1465 | |
1466 | |
1467 class HeaderProperty(object): | |
1468 def __init__(self, name, reader=None, writer=str, default=''): | |
1469 self.name, self.default = name, default | |
1470 self.reader, self.writer = reader, writer | |
1471 self.__doc__ = 'Current value of the %r header.' % name.title() | |
1472 | |
1473 def __get__(self, obj, _): | |
1474 if obj is None: return self | |
1475 value = obj.headers.get(self.name, self.default) | |
1476 return self.reader(value) if self.reader else value | |
1477 | |
1478 def __set__(self, obj, value): | |
1479 obj.headers[self.name] = self.writer(value) | |
1480 | |
1481 def __delete__(self, obj): | |
1482 del obj.headers[self.name] | |
1483 | |
1484 | |
1485 class BaseResponse(object): | |
1486 """ Storage class for a response body as well as headers and cookies. | |
1487 | |
1488 This class does support dict-like case-insensitive item-access to | |
1489 headers, but is NOT a dict. Most notably, iterating over a response | |
1490 yields parts of the body and not the headers. | |
1491 | |
1492 :param body: The response body as one of the supported types. | |
1493 :param status: Either an HTTP status code (e.g. 200) or a status line | |
1494 including the reason phrase (e.g. '200 OK'). | |
1495 :param headers: A dictionary or a list of name-value pairs. | |
1496 | |
1497 Additional keyword arguments are added to the list of headers. | |
1498 Underscores in the header name are replaced with dashes. | |
1499 """ | |
1500 | |
1501 default_status = 200 | |
1502 default_content_type = 'text/html; charset=UTF-8' | |
1503 | |
1504 # Header blacklist for specific response codes | |
1505 # (rfc2616 section 10.2.3 and 10.3.5) | |
1506 bad_headers = { | |
1507 204: set(('Content-Type', )), | |
1508 304: set(('Allow', 'Content-Encoding', 'Content-Language', | |
1509 'Content-Length', 'Content-Range', 'Content-Type', | |
1510 'Content-Md5', 'Last-Modified')) | |
1511 } | |
1512 | |
1513 def __init__(self, body='', status=None, headers=None, **more_headers): | |
1514 self._cookies = None | |
1515 self._headers = {} | |
1516 self.body = body | |
1517 self.status = status or self.default_status | |
1518 if headers: | |
1519 if isinstance(headers, dict): | |
1520 headers = headers.items() | |
1521 for name, value in headers: | |
1522 self.add_header(name, value) | |
1523 if more_headers: | |
1524 for name, value in more_headers.items(): | |
1525 self.add_header(name, value) | |
1526 | |
1527 def copy(self, cls=None): | |
1528 """ Returns a copy of self. """ | |
1529 cls = cls or BaseResponse | |
1530 assert issubclass(cls, BaseResponse) | |
1531 copy = cls() | |
1532 copy.status = self.status | |
1533 copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) | |
1534 if self._cookies: | |
1535 copy._cookies = SimpleCookie() | |
1536 copy._cookies.load(self._cookies.output(header='')) | |
1537 return copy | |
1538 | |
1539 def __iter__(self): | |
1540 return iter(self.body) | |
1541 | |
1542 def close(self): | |
1543 if hasattr(self.body, 'close'): | |
1544 self.body.close() | |
1545 | |
1546 @property | |
1547 def status_line(self): | |
1548 """ The HTTP status line as a string (e.g. ``404 Not Found``).""" | |
1549 return self._status_line | |
1550 | |
1551 @property | |
1552 def status_code(self): | |
1553 """ The HTTP status code as an integer (e.g. 404).""" | |
1554 return self._status_code | |
1555 | |
1556 def _set_status(self, status): | |
1557 if isinstance(status, int): | |
1558 code, status = status, _HTTP_STATUS_LINES.get(status) | |
1559 elif ' ' in status: | |
1560 status = status.strip() | |
1561 code = int(status.split()[0]) | |
1562 else: | |
1563 raise ValueError('String status line without a reason phrase.') | |
1564 if not 100 <= code <= 999: | |
1565 raise ValueError('Status code out of range.') | |
1566 self._status_code = code | |
1567 self._status_line = str(status or ('%d Unknown' % code)) | |
1568 | |
1569 def _get_status(self): | |
1570 return self._status_line | |
1571 | |
1572 status = property( | |
1573 _get_status, _set_status, None, | |
1574 ''' A writeable property to change the HTTP response status. It accepts | |
1575 either a numeric code (100-999) or a string with a custom reason | |
1576 phrase (e.g. "404 Brain not found"). Both :data:`status_line` and | |
1577 :data:`status_code` are updated accordingly. The return value is | |
1578 always a status string. ''') | |
1579 del _get_status, _set_status | |
1580 | |
1581 @property | |
1582 def headers(self): | |
1583 """ An instance of :class:`HeaderDict`, a case-insensitive dict-like | |
1584 view on the response headers. """ | |
1585 hdict = HeaderDict() | |
1586 hdict.dict = self._headers | |
1587 return hdict | |
1588 | |
1589 def __contains__(self, name): | |
1590 return _hkey(name) in self._headers | |
1591 | |
1592 def __delitem__(self, name): | |
1593 del self._headers[_hkey(name)] | |
1594 | |
1595 def __getitem__(self, name): | |
1596 return self._headers[_hkey(name)][-1] | |
1597 | |
1598 def __setitem__(self, name, value): | |
1599 self._headers[_hkey(name)] = [value if isinstance(value, unicode) else | |
1600 str(value)] | |
1601 | |
1602 def get_header(self, name, default=None): | |
1603 """ Return the value of a previously defined header. If there is no | |
1604 header with that name, return a default value. """ | |
1605 return self._headers.get(_hkey(name), [default])[-1] | |
1606 | |
1607 def set_header(self, name, value): | |
1608 """ Create a new response header, replacing any previously defined | |
1609 headers with the same name. """ | |
1610 self._headers[_hkey(name)] = [value if isinstance(value, unicode) | |
1611 else str(value)] | |
1612 | |
1613 def add_header(self, name, value): | |
1614 """ Add an additional response header, not removing duplicates. """ | |
1615 self._headers.setdefault(_hkey(name), []).append( | |
1616 value if isinstance(value, unicode) else str(value)) | |
1617 | |
1618 def iter_headers(self): | |
1619 """ Yield (header, value) tuples, skipping headers that are not | |
1620 allowed with the current response status code. """ | |
1621 return self.headerlist | |
1622 | |
1623 @property | |
1624 def headerlist(self): | |
1625 """ WSGI conform list of (header, value) tuples. """ | |
1626 out = [] | |
1627 headers = list(self._headers.items()) | |
1628 if 'Content-Type' not in self._headers: | |
1629 headers.append(('Content-Type', [self.default_content_type])) | |
1630 if self._status_code in self.bad_headers: | |
1631 bad_headers = self.bad_headers[self._status_code] | |
1632 headers = [h for h in headers if h[0] not in bad_headers] | |
1633 out += [(name, val) for (name, vals) in headers for val in vals] | |
1634 if self._cookies: | |
1635 for c in self._cookies.values(): | |
1636 out.append(('Set-Cookie', c.OutputString())) | |
1637 if py3k: | |
1638 return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] | |
1639 else: | |
1640 return [(k, v.encode('utf8') if isinstance(v, unicode) else v) | |
1641 for (k, v) in out] | |
1642 | |
1643 content_type = HeaderProperty('Content-Type') | |
1644 content_length = HeaderProperty('Content-Length', reader=int) | |
1645 expires = HeaderProperty( | |
1646 'Expires', | |
1647 reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), | |
1648 writer=lambda x: http_date(x)) | |
1649 | |
1650 @property | |
1651 def charset(self, default='UTF-8'): | |
1652 """ Return the charset specified in the content-type header (default: utf8). """ | |
1653 if 'charset=' in self.content_type: | |
1654 return self.content_type.split('charset=')[-1].split(';')[0].strip() | |
1655 return default | |
1656 | |
1657 def set_cookie(self, name, value, secret=None, **options): | |
1658 """ Create a new cookie or replace an old one. If the `secret` parameter is | |
1659 set, create a `Signed Cookie` (described below). | |
1660 | |
1661 :param name: the name of the cookie. | |
1662 :param value: the value of the cookie. | |
1663 :param secret: a signature key required for signed cookies. | |
1664 | |
1665 Additionally, this method accepts all RFC 2109 attributes that are | |
1666 supported by :class:`cookie.Morsel`, including: | |
1667 | |
1668 :param max_age: maximum age in seconds. (default: None) | |
1669 :param expires: a datetime object or UNIX timestamp. (default: None) | |
1670 :param domain: the domain that is allowed to read the cookie. | |
1671 (default: current domain) | |
1672 :param path: limits the cookie to a given path (default: current path) | |
1673 :param secure: limit the cookie to HTTPS connections (default: off). | |
1674 :param httponly: prevents client-side javascript to read this cookie | |
1675 (default: off, requires Python 2.6 or newer). | |
1676 | |
1677 If neither `expires` nor `max_age` is set (default), the cookie will | |
1678 expire at the end of the browser session (as soon as the browser | |
1679 window is closed). | |
1680 | |
1681 Signed cookies may store any pickle-able object and are | |
1682 cryptographically signed to prevent manipulation. Keep in mind that | |
1683 cookies are limited to 4kb in most browsers. | |
1684 | |
1685 Warning: Signed cookies are not encrypted (the client can still see | |
1686 the content) and not copy-protected (the client can restore an old | |
1687 cookie). The main intention is to make pickling and unpickling | |
1688 save, not to store secret information at client side. | |
1689 """ | |
1690 if not self._cookies: | |
1691 self._cookies = SimpleCookie() | |
1692 | |
1693 if secret: | |
1694 value = touni(cookie_encode((name, value), secret)) | |
1695 elif not isinstance(value, basestring): | |
1696 raise TypeError('Secret key missing for non-string Cookie.') | |
1697 | |
1698 if len(value) > 4096: raise ValueError('Cookie value to long.') | |
1699 self._cookies[name] = value | |
1700 | |
1701 for key, value in options.items(): | |
1702 if key == 'max_age': | |
1703 if isinstance(value, timedelta): | |
1704 value = value.seconds + value.days * 24 * 3600 | |
1705 if key == 'expires': | |
1706 if isinstance(value, (datedate, datetime)): | |
1707 value = value.timetuple() | |
1708 elif isinstance(value, (int, float)): | |
1709 value = time.gmtime(value) | |
1710 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) | |
1711 if key in ('secure', 'httponly') and not value: | |
1712 continue | |
1713 self._cookies[name][key.replace('_', '-')] = value | |
1714 | |
1715 def delete_cookie(self, key, **kwargs): | |
1716 """ Delete a cookie. Be sure to use the same `domain` and `path` | |
1717 settings as used to create the cookie. """ | |
1718 kwargs['max_age'] = -1 | |
1719 kwargs['expires'] = 0 | |
1720 self.set_cookie(key, '', **kwargs) | |
1721 | |
1722 def __repr__(self): | |
1723 out = '' | |
1724 for name, value in self.headerlist: | |
1725 out += '%s: %s\n' % (name.title(), value.strip()) | |
1726 return out | |
1727 | |
1728 | |
1729 def _local_property(): | |
1730 ls = threading.local() | |
1731 | |
1732 def fget(_): | |
1733 try: | |
1734 return ls.var | |
1735 except AttributeError: | |
1736 raise RuntimeError("Request context not initialized.") | |
1737 | |
1738 def fset(_, value): | |
1739 ls.var = value | |
1740 | |
1741 def fdel(_): | |
1742 del ls.var | |
1743 | |
1744 return property(fget, fset, fdel, 'Thread-local property') | |
1745 | |
1746 | |
1747 class LocalRequest(BaseRequest): | |
1748 """ A thread-local subclass of :class:`BaseRequest` with a different | |
1749 set of attributes for each thread. There is usually only one global | |
1750 instance of this class (:data:`request`). If accessed during a | |
1751 request/response cycle, this instance always refers to the *current* | |
1752 request (even on a multithreaded server). """ | |
1753 bind = BaseRequest.__init__ | |
1754 environ = _local_property() | |
1755 | |
1756 | |
1757 class LocalResponse(BaseResponse): | |
1758 """ A thread-local subclass of :class:`BaseResponse` with a different | |
1759 set of attributes for each thread. There is usually only one global | |
1760 instance of this class (:data:`response`). Its attributes are used | |
1761 to build the HTTP response at the end of the request/response cycle. | |
1762 """ | |
1763 bind = BaseResponse.__init__ | |
1764 _status_line = _local_property() | |
1765 _status_code = _local_property() | |
1766 _cookies = _local_property() | |
1767 _headers = _local_property() | |
1768 body = _local_property() | |
1769 | |
1770 | |
1771 Request = BaseRequest | |
1772 Response = BaseResponse | |
1773 | |
1774 | |
1775 class HTTPResponse(Response, BottleException): | |
1776 def __init__(self, body='', status=None, headers=None, **more_headers): | |
1777 super(HTTPResponse, self).__init__(body, status, headers, **more_headers) | |
1778 | |
1779 def apply(self, other): | |
1780 other._status_code = self._status_code | |
1781 other._status_line = self._status_line | |
1782 other._headers = self._headers | |
1783 other._cookies = self._cookies | |
1784 other.body = self.body | |
1785 | |
1786 | |
1787 class HTTPError(HTTPResponse): | |
1788 default_status = 500 | |
1789 | |
1790 def __init__(self, | |
1791 status=None, | |
1792 body=None, | |
1793 exception=None, | |
1794 traceback=None, **options): | |
1795 self.exception = exception | |
1796 self.traceback = traceback | |
1797 super(HTTPError, self).__init__(body, status, **options) | |
1798 | |
1799 ############################################################################### | |
1800 # Plugins ###################################################################### | |
1801 ############################################################################### | |
1802 | |
1803 | |
1804 class PluginError(BottleException): | |
1805 pass | |
1806 | |
1807 | |
1808 class JSONPlugin(object): | |
1809 name = 'json' | |
1810 api = 2 | |
1811 | |
1812 def __init__(self, json_dumps=json_dumps): | |
1813 self.json_dumps = json_dumps | |
1814 | |
1815 def apply(self, callback, _): | |
1816 dumps = self.json_dumps | |
1817 if not dumps: return callback | |
1818 | |
1819 def wrapper(*a, **ka): | |
1820 try: | |
1821 rv = callback(*a, **ka) | |
1822 except HTTPError: | |
1823 rv = _e() | |
1824 | |
1825 if isinstance(rv, dict): | |
1826 #Attempt to serialize, raises exception on failure | |
1827 json_response = dumps(rv) | |
1828 #Set content type only if serialization successful | |
1829 response.content_type = 'application/json' | |
1830 return json_response | |
1831 elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): | |
1832 rv.body = dumps(rv.body) | |
1833 rv.content_type = 'application/json' | |
1834 return rv | |
1835 | |
1836 return wrapper | |
1837 | |
1838 | |
1839 class TemplatePlugin(object): | |
1840 """ This plugin applies the :func:`view` decorator to all routes with a | |
1841 `template` config parameter. If the parameter is a tuple, the second | |
1842 element must be a dict with additional options (e.g. `template_engine`) | |
1843 or default variables for the template. """ | |
1844 name = 'template' | |
1845 api = 2 | |
1846 | |
1847 def apply(self, callback, route): | |
1848 conf = route.config.get('template') | |
1849 if isinstance(conf, (tuple, list)) and len(conf) == 2: | |
1850 return view(conf[0], **conf[1])(callback) | |
1851 elif isinstance(conf, str): | |
1852 return view(conf)(callback) | |
1853 else: | |
1854 return callback | |
1855 | |
1856 | |
1857 #: Not a plugin, but part of the plugin API. TODO: Find a better place. | |
1858 class _ImportRedirect(object): | |
1859 def __init__(self, name, impmask): | |
1860 """ Create a virtual package that redirects imports (see PEP 302). """ | |
1861 self.name = name | |
1862 self.impmask = impmask | |
1863 self.module = sys.modules.setdefault(name, imp.new_module(name)) | |
1864 self.module.__dict__.update({ | |
1865 '__file__': __file__, | |
1866 '__path__': [], | |
1867 '__all__': [], | |
1868 '__loader__': self | |
1869 }) | |
1870 sys.meta_path.append(self) | |
1871 | |
1872 def find_module(self, fullname, path=None): | |
1873 if '.' not in fullname: return | |
1874 packname = fullname.rsplit('.', 1)[0] | |
1875 if packname != self.name: return | |
1876 return self | |
1877 | |
1878 def load_module(self, fullname): | |
1879 if fullname in sys.modules: return sys.modules[fullname] | |
1880 modname = fullname.rsplit('.', 1)[1] | |
1881 realname = self.impmask % modname | |
1882 __import__(realname) | |
1883 module = sys.modules[fullname] = sys.modules[realname] | |
1884 setattr(self.module, modname, module) | |
1885 module.__loader__ = self | |
1886 return module | |
1887 | |
1888 ############################################################################### | |
1889 # Common Utilities ############################################################# | |
1890 ############################################################################### | |
1891 | |
1892 | |
1893 class MultiDict(DictMixin): | |
1894 """ This dict stores multiple values per key, but behaves exactly like a | |
1895 normal dict in that it returns only the newest value for any given key. | |
1896 There are special methods available to access the full list of values. | |
1897 """ | |
1898 | |
1899 def __init__(self, *a, **k): | |
1900 self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) | |
1901 | |
1902 def __len__(self): | |
1903 return len(self.dict) | |
1904 | |
1905 def __iter__(self): | |
1906 return iter(self.dict) | |
1907 | |
1908 def __contains__(self, key): | |
1909 return key in self.dict | |
1910 | |
1911 def __delitem__(self, key): | |
1912 del self.dict[key] | |
1913 | |
1914 def __getitem__(self, key): | |
1915 return self.dict[key][-1] | |
1916 | |
1917 def __setitem__(self, key, value): | |
1918 self.append(key, value) | |
1919 | |
1920 def keys(self): | |
1921 return self.dict.keys() | |
1922 | |
1923 if py3k: | |
1924 | |
1925 def values(self): | |
1926 return (v[-1] for v in self.dict.values()) | |
1927 | |
1928 def items(self): | |
1929 return ((k, v[-1]) for k, v in self.dict.items()) | |
1930 | |
1931 def allitems(self): | |
1932 return ((k, v) for k, vl in self.dict.items() for v in vl) | |
1933 | |
1934 iterkeys = keys | |
1935 itervalues = values | |
1936 iteritems = items | |
1937 iterallitems = allitems | |
1938 | |
1939 else: | |
1940 | |
1941 def values(self): | |
1942 return [v[-1] for v in self.dict.values()] | |
1943 | |
1944 def items(self): | |
1945 return [(k, v[-1]) for k, v in self.dict.items()] | |
1946 | |
1947 def iterkeys(self): | |
1948 return self.dict.iterkeys() | |
1949 | |
1950 def itervalues(self): | |
1951 return (v[-1] for v in self.dict.itervalues()) | |
1952 | |
1953 def iteritems(self): | |
1954 return ((k, v[-1]) for k, v in self.dict.iteritems()) | |
1955 | |
1956 def iterallitems(self): | |
1957 return ((k, v) for k, vl in self.dict.iteritems() for v in vl) | |
1958 | |
1959 def allitems(self): | |
1960 return [(k, v) for k, vl in self.dict.iteritems() for v in vl] | |
1961 | |
1962 def get(self, key, default=None, index=-1, type=None): | |
1963 """ Return the most recent value for a key. | |
1964 | |
1965 :param default: The default value to be returned if the key is not | |
1966 present or the type conversion fails. | |
1967 :param index: An index for the list of available values. | |
1968 :param type: If defined, this callable is used to cast the value | |
1969 into a specific type. Exception are suppressed and result in | |
1970 the default value to be returned. | |
1971 """ | |
1972 try: | |
1973 val = self.dict[key][index] | |
1974 return type(val) if type else val | |
1975 except Exception: | |
1976 pass | |
1977 return default | |
1978 | |
1979 def append(self, key, value): | |
1980 """ Add a new value to the list of values for this key. """ | |
1981 self.dict.setdefault(key, []).append(value) | |
1982 | |
1983 def replace(self, key, value): | |
1984 """ Replace the list of values with a single value. """ | |
1985 self.dict[key] = [value] | |
1986 | |
1987 def getall(self, key): | |
1988 """ Return a (possibly empty) list of values for a key. """ | |
1989 return self.dict.get(key) or [] | |
1990 | |
1991 #: Aliases for WTForms to mimic other multi-dict APIs (Django) | |
1992 getone = get | |
1993 getlist = getall | |
1994 | |
1995 | |
1996 class FormsDict(MultiDict): | |
1997 """ This :class:`MultiDict` subclass is used to store request form data. | |
1998 Additionally to the normal dict-like item access methods (which return | |
1999 unmodified data as native strings), this container also supports | |
2000 attribute-like access to its values. Attributes are automatically de- | |
2001 or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing | |
2002 attributes default to an empty string. """ | |
2003 | |
2004 #: Encoding used for attribute values. | |
2005 input_encoding = 'utf8' | |
2006 #: If true (default), unicode strings are first encoded with `latin1` | |
2007 #: and then decoded to match :attr:`input_encoding`. | |
2008 recode_unicode = True | |
2009 | |
2010 def _fix(self, s, encoding=None): | |
2011 if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI | |
2012 return s.encode('latin1').decode(encoding or self.input_encoding) | |
2013 elif isinstance(s, bytes): # Python 2 WSGI | |
2014 return s.decode(encoding or self.input_encoding) | |
2015 else: | |
2016 return s | |
2017 | |
2018 def decode(self, encoding=None): | |
2019 """ Returns a copy with all keys and values de- or recoded to match | |
2020 :attr:`input_encoding`. Some libraries (e.g. WTForms) want a | |
2021 unicode dictionary. """ | |
2022 copy = FormsDict() | |
2023 enc = copy.input_encoding = encoding or self.input_encoding | |
2024 copy.recode_unicode = False | |
2025 for key, value in self.allitems(): | |
2026 copy.append(self._fix(key, enc), self._fix(value, enc)) | |
2027 return copy | |
2028 | |
2029 def getunicode(self, name, default=None, encoding=None): | |
2030 """ Return the value as a unicode string, or the default. """ | |
2031 try: | |
2032 return self._fix(self[name], encoding) | |
2033 except (UnicodeError, KeyError): | |
2034 return default | |
2035 | |
2036 def __getattr__(self, name, default=unicode()): | |
2037 # Without this guard, pickle generates a cryptic TypeError: | |
2038 if name.startswith('__') and name.endswith('__'): | |
2039 return super(FormsDict, self).__getattr__(name) | |
2040 return self.getunicode(name, default=default) | |
2041 | |
2042 | |
2043 class HeaderDict(MultiDict): | |
2044 """ A case-insensitive version of :class:`MultiDict` that defaults to | |
2045 replace the old value instead of appending it. """ | |
2046 | |
2047 def __init__(self, *a, **ka): | |
2048 self.dict = {} | |
2049 if a or ka: self.update(*a, **ka) | |
2050 | |
2051 def __contains__(self, key): | |
2052 return _hkey(key) in self.dict | |
2053 | |
2054 def __delitem__(self, key): | |
2055 del self.dict[_hkey(key)] | |
2056 | |
2057 def __getitem__(self, key): | |
2058 return self.dict[_hkey(key)][-1] | |
2059 | |
2060 def __setitem__(self, key, value): | |
2061 self.dict[_hkey(key)] = [value if isinstance(value, unicode) else | |
2062 str(value)] | |
2063 | |
2064 def append(self, key, value): | |
2065 self.dict.setdefault(_hkey(key), []).append( | |
2066 value if isinstance(value, unicode) else str(value)) | |
2067 | |
2068 def replace(self, key, value): | |
2069 self.dict[_hkey(key)] = [value if isinstance(value, unicode) else | |
2070 str(value)] | |
2071 | |
2072 def getall(self, key): | |
2073 return self.dict.get(_hkey(key)) or [] | |
2074 | |
2075 def get(self, key, default=None, index=-1): | |
2076 return MultiDict.get(self, _hkey(key), default, index) | |
2077 | |
2078 def filter(self, names): | |
2079 for name in [_hkey(n) for n in names]: | |
2080 if name in self.dict: | |
2081 del self.dict[name] | |
2082 | |
2083 | |
2084 class WSGIHeaderDict(DictMixin): | |
2085 """ This dict-like class wraps a WSGI environ dict and provides convenient | |
2086 access to HTTP_* fields. Keys and values are native strings | |
2087 (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI | |
2088 environment contains non-native string values, these are de- or encoded | |
2089 using a lossless 'latin1' character set. | |
2090 | |
2091 The API will remain stable even on changes to the relevant PEPs. | |
2092 Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one | |
2093 that uses non-native strings.) | |
2094 """ | |
2095 #: List of keys that do not have a ``HTTP_`` prefix. | |
2096 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') | |
2097 | |
2098 def __init__(self, environ): | |
2099 self.environ = environ | |
2100 | |
2101 def _ekey(self, key): | |
2102 """ Translate header field name to CGI/WSGI environ key. """ | |
2103 key = key.replace('-', '_').upper() | |
2104 if key in self.cgikeys: | |
2105 return key | |
2106 return 'HTTP_' + key | |
2107 | |
2108 def raw(self, key, default=None): | |
2109 """ Return the header value as is (may be bytes or unicode). """ | |
2110 return self.environ.get(self._ekey(key), default) | |
2111 | |
2112 def __getitem__(self, key): | |
2113 val = self.environ[self._ekey(key)] | |
2114 if py3k: | |
2115 if isinstance(val, unicode): | |
2116 val = val.encode('latin1').decode('utf8') | |
2117 else: | |
2118 val = val.decode('utf8') | |
2119 return val | |
2120 | |
2121 def __setitem__(self, key, value): | |
2122 raise TypeError("%s is read-only." % self.__class__) | |
2123 | |
2124 def __delitem__(self, key): | |
2125 raise TypeError("%s is read-only." % self.__class__) | |
2126 | |
2127 def __iter__(self): | |
2128 for key in self.environ: | |
2129 if key[:5] == 'HTTP_': | |
2130 yield _hkey(key[5:]) | |
2131 elif key in self.cgikeys: | |
2132 yield _hkey(key) | |
2133 | |
2134 def keys(self): | |
2135 return [x for x in self] | |
2136 | |
2137 def __len__(self): | |
2138 return len(self.keys()) | |
2139 | |
2140 def __contains__(self, key): | |
2141 return self._ekey(key) in self.environ | |
2142 | |
2143 | |
2144 class ConfigDict(dict): | |
2145 """ A dict-like configuration storage with additional support for | |
2146 namespaces, validators, meta-data, on_change listeners and more. | |
2147 """ | |
2148 | |
2149 __slots__ = ('_meta', '_on_change') | |
2150 | |
2151 def __init__(self): | |
2152 self._meta = {} | |
2153 self._on_change = lambda name, value: None | |
2154 | |
2155 def load_config(self, filename): | |
2156 """ Load values from an ``*.ini`` style config file. | |
2157 | |
2158 If the config file contains sections, their names are used as | |
2159 namespaces for the values within. The two special sections | |
2160 ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). | |
2161 """ | |
2162 conf = ConfigParser() | |
2163 conf.read(filename) | |
2164 for section in conf.sections(): | |
2165 for key, value in conf.items(section): | |
2166 if section not in ('DEFAULT', 'bottle'): | |
2167 key = section + '.' + key | |
2168 self[key] = value | |
2169 return self | |
2170 | |
2171 def load_dict(self, source, namespace=''): | |
2172 """ Load values from a dictionary structure. Nesting can be used to | |
2173 represent namespaces. | |
2174 | |
2175 >>> c = ConfigDict() | |
2176 >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) | |
2177 {'some.namespace.key': 'value'} | |
2178 """ | |
2179 for key, value in source.items(): | |
2180 if isinstance(key, str): | |
2181 nskey = (namespace + '.' + key).strip('.') | |
2182 if isinstance(value, dict): | |
2183 self.load_dict(value, namespace=nskey) | |
2184 else: | |
2185 self[nskey] = value | |
2186 else: | |
2187 raise TypeError('Key has type %r (not a string)' % type(key)) | |
2188 return self | |
2189 | |
2190 def update(self, *a, **ka): | |
2191 """ If the first parameter is a string, all keys are prefixed with this | |
2192 namespace. Apart from that it works just as the usual dict.update(). | |
2193 Example: ``update('some.namespace', key='value')`` """ | |
2194 prefix = '' | |
2195 if a and isinstance(a[0], str): | |
2196 prefix = a[0].strip('.') + '.' | |
2197 a = a[1:] | |
2198 for key, value in dict(*a, **ka).items(): | |
2199 self[prefix + key] = value | |
2200 | |
2201 def setdefault(self, key, value): | |
2202 if key not in self: | |
2203 self[key] = value | |
2204 return self[key] | |
2205 | |
2206 def __setitem__(self, key, value): | |
2207 if not isinstance(key, str): | |
2208 raise TypeError('Key has type %r (not a string)' % type(key)) | |
2209 value = self.meta_get(key, 'filter', lambda x: x)(value) | |
2210 if key in self and self[key] is value: | |
2211 return | |
2212 self._on_change(key, value) | |
2213 dict.__setitem__(self, key, value) | |
2214 | |
2215 def __delitem__(self, key): | |
2216 self._on_change(key, None) | |
2217 dict.__delitem__(self, key) | |
2218 | |
2219 def meta_get(self, key, metafield, default=None): | |
2220 """ Return the value of a meta field for a key. """ | |
2221 return self._meta.get(key, {}).get(metafield, default) | |
2222 | |
2223 def meta_set(self, key, metafield, value): | |
2224 """ Set the meta field for a key to a new value. This triggers the | |
2225 on-change handler for existing keys. """ | |
2226 self._meta.setdefault(key, {})[metafield] = value | |
2227 if key in self: | |
2228 self[key] = self[key] | |
2229 | |
2230 def meta_list(self, key): | |
2231 """ Return an iterable of meta field names defined for a key. """ | |
2232 return self._meta.get(key, {}).keys() | |
2233 | |
2234 | |
2235 class AppStack(list): | |
2236 """ A stack-like list. Calling it returns the head of the stack. """ | |
2237 | |
2238 def __call__(self): | |
2239 """ Return the current default application. """ | |
2240 return self[-1] | |
2241 | |
2242 def push(self, value=None): | |
2243 """ Add a new :class:`Bottle` instance to the stack """ | |
2244 if not isinstance(value, Bottle): | |
2245 value = Bottle() | |
2246 self.append(value) | |
2247 return value | |
2248 | |
2249 | |
2250 class WSGIFileWrapper(object): | |
2251 def __init__(self, fp, buffer_size=1024 * 64): | |
2252 self.fp, self.buffer_size = fp, buffer_size | |
2253 for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): | |
2254 if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) | |
2255 | |
2256 def __iter__(self): | |
2257 buff, read = self.buffer_size, self.read | |
2258 while True: | |
2259 part = read(buff) | |
2260 if not part: return | |
2261 yield part | |
2262 | |
2263 | |
2264 class _closeiter(object): | |
2265 """ This only exists to be able to attach a .close method to iterators that | |
2266 do not support attribute assignment (most of itertools). """ | |
2267 | |
2268 def __init__(self, iterator, close=None): | |
2269 self.iterator = iterator | |
2270 self.close_callbacks = makelist(close) | |
2271 | |
2272 def __iter__(self): | |
2273 return iter(self.iterator) | |
2274 | |
2275 def close(self): | |
2276 for func in self.close_callbacks: | |
2277 func() | |
2278 | |
2279 | |
2280 class ResourceManager(object): | |
2281 """ This class manages a list of search paths and helps to find and open | |
2282 application-bound resources (files). | |
2283 | |
2284 :param base: default value for :meth:`add_path` calls. | |
2285 :param opener: callable used to open resources. | |
2286 :param cachemode: controls which lookups are cached. One of 'all', | |
2287 'found' or 'none'. | |
2288 """ | |
2289 | |
2290 def __init__(self, base='./', opener=open, cachemode='all'): | |
2291 self.opener = opener | |
2292 self.base = base | |
2293 self.cachemode = cachemode | |
2294 | |
2295 #: A list of search paths. See :meth:`add_path` for details. | |
2296 self.path = [] | |
2297 #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. | |
2298 self.cache = {} | |
2299 | |
2300 def add_path(self, path, base=None, index=None, create=False): | |
2301 """ Add a new path to the list of search paths. Return False if the | |
2302 path does not exist. | |
2303 | |
2304 :param path: The new search path. Relative paths are turned into | |
2305 an absolute and normalized form. If the path looks like a file | |
2306 (not ending in `/`), the filename is stripped off. | |
2307 :param base: Path used to absolutize relative search paths. | |
2308 Defaults to :attr:`base` which defaults to ``os.getcwd()``. | |
2309 :param index: Position within the list of search paths. Defaults | |
2310 to last index (appends to the list). | |
2311 | |
2312 The `base` parameter makes it easy to reference files installed | |
2313 along with a python module or package:: | |
2314 | |
2315 res.add_path('./resources/', __file__) | |
2316 """ | |
2317 base = os.path.abspath(os.path.dirname(base or self.base)) | |
2318 path = os.path.abspath(os.path.join(base, os.path.dirname(path))) | |
2319 path += os.sep | |
2320 if path in self.path: | |
2321 self.path.remove(path) | |
2322 if create and not os.path.isdir(path): | |
2323 os.makedirs(path) | |
2324 if index is None: | |
2325 self.path.append(path) | |
2326 else: | |
2327 self.path.insert(index, path) | |
2328 self.cache.clear() | |
2329 return os.path.exists(path) | |
2330 | |
2331 def __iter__(self): | |
2332 """ Iterate over all existing files in all registered paths. """ | |
2333 search = self.path[:] | |
2334 while search: | |
2335 path = search.pop() | |
2336 if not os.path.isdir(path): continue | |
2337 for name in os.listdir(path): | |
2338 full = os.path.join(path, name) | |
2339 if os.path.isdir(full): search.append(full) | |
2340 else: yield full | |
2341 | |
2342 def lookup(self, name): | |
2343 """ Search for a resource and return an absolute file path, or `None`. | |
2344 | |
2345 The :attr:`path` list is searched in order. The first match is | |
2346 returend. Symlinks are followed. The result is cached to speed up | |
2347 future lookups. """ | |
2348 if name not in self.cache or DEBUG: | |
2349 for path in self.path: | |
2350 fpath = os.path.join(path, name) | |
2351 if os.path.isfile(fpath): | |
2352 if self.cachemode in ('all', 'found'): | |
2353 self.cache[name] = fpath | |
2354 return fpath | |
2355 if self.cachemode == 'all': | |
2356 self.cache[name] = None | |
2357 return self.cache[name] | |
2358 | |
2359 def open(self, name, mode='r', *args, **kwargs): | |
2360 """ Find a resource and return a file object, or raise IOError. """ | |
2361 fname = self.lookup(name) | |
2362 if not fname: raise IOError("Resource %r not found." % name) | |
2363 return self.opener(fname, mode=mode, *args, **kwargs) | |
2364 | |
2365 | |
2366 class FileUpload(object): | |
2367 def __init__(self, fileobj, name, filename, headers=None): | |
2368 """ Wrapper for file uploads. """ | |
2369 #: Open file(-like) object (BytesIO buffer or temporary file) | |
2370 self.file = fileobj | |
2371 #: Name of the upload form field | |
2372 self.name = name | |
2373 #: Raw filename as sent by the client (may contain unsafe characters) | |
2374 self.raw_filename = filename | |
2375 #: A :class:`HeaderDict` with additional headers (e.g. content-type) | |
2376 self.headers = HeaderDict(headers) if headers else HeaderDict() | |
2377 | |
2378 content_type = HeaderProperty('Content-Type') | |
2379 content_length = HeaderProperty('Content-Length', reader=int, default=-1) | |
2380 | |
2381 @cached_property | |
2382 def filename(self): | |
2383 """ Name of the file on the client file system, but normalized to ensure | |
2384 file system compatibility. An empty filename is returned as 'empty'. | |
2385 | |
2386 Only ASCII letters, digits, dashes, underscores and dots are | |
2387 allowed in the final filename. Accents are removed, if possible. | |
2388 Whitespace is replaced by a single dash. Leading or tailing dots | |
2389 or dashes are removed. The filename is limited to 255 characters. | |
2390 """ | |
2391 fname = self.raw_filename | |
2392 if not isinstance(fname, unicode): | |
2393 fname = fname.decode('utf8', 'ignore') | |
2394 fname = normalize('NFKD', fname) | |
2395 fname = fname.encode('ASCII', 'ignore').decode('ASCII') | |
2396 fname = os.path.basename(fname.replace('\\', os.path.sep)) | |
2397 fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() | |
2398 fname = re.sub(r'[-\s]+', '-', fname).strip('.-') | |
2399 return fname[:255] or 'empty' | |
2400 | |
2401 def _copy_file(self, fp, chunk_size=2 ** 16): | |
2402 read, write, offset = self.file.read, fp.write, self.file.tell() | |
2403 while 1: | |
2404 buf = read(chunk_size) | |
2405 if not buf: break | |
2406 write(buf) | |
2407 self.file.seek(offset) | |
2408 | |
2409 def save(self, destination, overwrite=False, chunk_size=2 ** 16): | |
2410 """ Save file to disk or copy its content to an open file(-like) object. | |
2411 If *destination* is a directory, :attr:`filename` is added to the | |
2412 path. Existing files are not overwritten by default (IOError). | |
2413 | |
2414 :param destination: File path, directory or file(-like) object. | |
2415 :param overwrite: If True, replace existing files. (default: False) | |
2416 :param chunk_size: Bytes to read at a time. (default: 64kb) | |
2417 """ | |
2418 if isinstance(destination, basestring): # Except file-likes here | |
2419 if os.path.isdir(destination): | |
2420 destination = os.path.join(destination, self.filename) | |
2421 if not overwrite and os.path.exists(destination): | |
2422 raise IOError('File exists.') | |
2423 with open(destination, 'wb') as fp: | |
2424 self._copy_file(fp, chunk_size) | |
2425 else: | |
2426 self._copy_file(destination, chunk_size) | |
2427 | |
2428 ############################################################################### | |
2429 # Application Helper ########################################################### | |
2430 ############################################################################### | |
2431 | |
2432 | |
2433 def abort(code=500, text='Unknown Error.'): | |
2434 """ Aborts execution and causes a HTTP error. """ | |
2435 raise HTTPError(code, text) | |
2436 | |
2437 | |
2438 def redirect(url, code=None): | |
2439 """ Aborts execution and causes a 303 or 302 redirect, depending on | |
2440 the HTTP protocol version. """ | |
2441 if not code: | |
2442 code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 | |
2443 res = response.copy(cls=HTTPResponse) | |
2444 res.status = code | |
2445 res.body = "" | |
2446 res.set_header('Location', urljoin(request.url, url)) | |
2447 raise res | |
2448 | |
2449 | |
2450 def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): | |
2451 """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" | |
2452 fp.seek(offset) | |
2453 while bytes > 0: | |
2454 part = fp.read(min(bytes, maxread)) | |
2455 if not part: break | |
2456 bytes -= len(part) | |
2457 yield part | |
2458 | |
2459 | |
2460 def static_file(filename, root, | |
2461 mimetype='auto', | |
2462 download=False, | |
2463 charset='UTF-8'): | |
2464 """ Open a file in a safe way and return :exc:`HTTPResponse` with status | |
2465 code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, | |
2466 ``Content-Length`` and ``Last-Modified`` headers are set if possible. | |
2467 Special support for ``If-Modified-Since``, ``Range`` and ``HEAD`` | |
2468 requests. | |
2469 | |
2470 :param filename: Name or path of the file to send. | |
2471 :param root: Root path for file lookups. Should be an absolute directory | |
2472 path. | |
2473 :param mimetype: Defines the content-type header (default: guess from | |
2474 file extension) | |
2475 :param download: If True, ask the browser to open a `Save as...` dialog | |
2476 instead of opening the file with the associated program. You can | |
2477 specify a custom filename as a string. If not specified, the | |
2478 original filename is used (default: False). | |
2479 :param charset: The charset to use for files with a ``text/*`` | |
2480 mime-type. (default: UTF-8) | |
2481 """ | |
2482 | |
2483 root = os.path.abspath(root) + os.sep | |
2484 filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) | |
2485 headers = dict() | |
2486 | |
2487 if not filename.startswith(root): | |
2488 return HTTPError(403, "Access denied.") | |
2489 if not os.path.exists(filename) or not os.path.isfile(filename): | |
2490 return HTTPError(404, "File does not exist.") | |
2491 if not os.access(filename, os.R_OK): | |
2492 return HTTPError(403, "You do not have permission to access this file.") | |
2493 | |
2494 if mimetype == 'auto': | |
2495 if download and download != True: | |
2496 mimetype, encoding = mimetypes.guess_type(download) | |
2497 else: | |
2498 mimetype, encoding = mimetypes.guess_type(filename) | |
2499 if encoding: headers['Content-Encoding'] = encoding | |
2500 | |
2501 if mimetype: | |
2502 if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: | |
2503 mimetype += '; charset=%s' % charset | |
2504 headers['Content-Type'] = mimetype | |
2505 | |
2506 if download: | |
2507 download = os.path.basename(filename if download == True else download) | |
2508 headers['Content-Disposition'] = 'attachment; filename="%s"' % download | |
2509 | |
2510 stats = os.stat(filename) | |
2511 headers['Content-Length'] = clen = stats.st_size | |
2512 lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) | |
2513 headers['Last-Modified'] = lm | |
2514 | |
2515 ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') | |
2516 if ims: | |
2517 ims = parse_date(ims.split(";")[0].strip()) | |
2518 if ims is not None and ims >= int(stats.st_mtime): | |
2519 headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", | |
2520 time.gmtime()) | |
2521 return HTTPResponse(status=304, **headers) | |
2522 | |
2523 body = '' if request.method == 'HEAD' else open(filename, 'rb') | |
2524 | |
2525 headers["Accept-Ranges"] = "bytes" | |
2526 ranges = request.environ.get('HTTP_RANGE') | |
2527 if 'HTTP_RANGE' in request.environ: | |
2528 ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) | |
2529 if not ranges: | |
2530 return HTTPError(416, "Requested Range Not Satisfiable") | |
2531 offset, end = ranges[0] | |
2532 headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) | |
2533 headers["Content-Length"] = str(end - offset) | |
2534 if body: body = _file_iter_range(body, offset, end - offset) | |
2535 return HTTPResponse(body, status=206, **headers) | |
2536 return HTTPResponse(body, **headers) | |
2537 | |
2538 ############################################################################### | |
2539 # HTTP Utilities and MISC (TODO) ############################################### | |
2540 ############################################################################### | |
2541 | |
2542 | |
2543 def debug(mode=True): | |
2544 """ Change the debug level. | |
2545 There is only one debug level supported at the moment.""" | |
2546 global DEBUG | |
2547 if mode: warnings.simplefilter('default') | |
2548 DEBUG = bool(mode) | |
2549 | |
2550 | |
2551 def http_date(value): | |
2552 if isinstance(value, (datedate, datetime)): | |
2553 value = value.utctimetuple() | |
2554 elif isinstance(value, (int, float)): | |
2555 value = time.gmtime(value) | |
2556 if not isinstance(value, basestring): | |
2557 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) | |
2558 return value | |
2559 | |
2560 | |
2561 def parse_date(ims): | |
2562 """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ | |
2563 try: | |
2564 ts = email.utils.parsedate_tz(ims) | |
2565 return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone | |
2566 except (TypeError, ValueError, IndexError, OverflowError): | |
2567 return None | |
2568 | |
2569 | |
2570 def parse_auth(header): | |
2571 """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" | |
2572 try: | |
2573 method, data = header.split(None, 1) | |
2574 if method.lower() == 'basic': | |
2575 user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) | |
2576 return user, pwd | |
2577 except (KeyError, ValueError): | |
2578 return None | |
2579 | |
2580 | |
2581 def parse_range_header(header, maxlen=0): | |
2582 """ Yield (start, end) ranges parsed from a HTTP Range header. Skip | |
2583 unsatisfiable ranges. The end index is non-inclusive.""" | |
2584 if not header or header[:6] != 'bytes=': return | |
2585 ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] | |
2586 for start, end in ranges: | |
2587 try: | |
2588 if not start: # bytes=-100 -> last 100 bytes | |
2589 start, end = max(0, maxlen - int(end)), maxlen | |
2590 elif not end: # bytes=100- -> all but the first 99 bytes | |
2591 start, end = int(start), maxlen | |
2592 else: # bytes=100-200 -> bytes 100-200 (inclusive) | |
2593 start, end = int(start), min(int(end) + 1, maxlen) | |
2594 if 0 <= start < end <= maxlen: | |
2595 yield start, end | |
2596 except ValueError: | |
2597 pass | |
2598 | |
2599 | |
2600 def _parse_qsl(qs): | |
2601 r = [] | |
2602 for pair in qs.replace(';', '&').split('&'): | |
2603 if not pair: continue | |
2604 nv = pair.split('=', 1) | |
2605 if len(nv) != 2: nv.append('') | |
2606 key = urlunquote(nv[0].replace('+', ' ')) | |
2607 value = urlunquote(nv[1].replace('+', ' ')) | |
2608 r.append((key, value)) | |
2609 return r | |
2610 | |
2611 | |
2612 def _lscmp(a, b): | |
2613 """ Compares two strings in a cryptographically safe way: | |
2614 Runtime is not affected by length of common prefix. """ | |
2615 return not sum(0 if x == y else 1 | |
2616 for x, y in zip(a, b)) and len(a) == len(b) | |
2617 | |
2618 | |
2619 def cookie_encode(data, key): | |
2620 """ Encode and sign a pickle-able object. Return a (byte) string """ | |
2621 msg = base64.b64encode(pickle.dumps(data, -1)) | |
2622 sig = base64.b64encode(hmac.new(tob(key), msg).digest()) | |
2623 return tob('!') + sig + tob('?') + msg | |
2624 | |
2625 | |
2626 def cookie_decode(data, key): | |
2627 """ Verify and decode an encoded string. Return an object or None.""" | |
2628 data = tob(data) | |
2629 if cookie_is_encoded(data): | |
2630 sig, msg = data.split(tob('?'), 1) | |
2631 if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): | |
2632 return pickle.loads(base64.b64decode(msg)) | |
2633 return None | |
2634 | |
2635 | |
2636 def cookie_is_encoded(data): | |
2637 """ Return True if the argument looks like a encoded cookie.""" | |
2638 return bool(data.startswith(tob('!')) and tob('?') in data) | |
2639 | |
2640 | |
2641 def html_escape(string): | |
2642 """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ | |
2643 return string.replace('&', '&').replace('<', '<').replace('>', '>')\ | |
2644 .replace('"', '"').replace("'", ''') | |
2645 | |
2646 | |
2647 def html_quote(string): | |
2648 """ Escape and quote a string to be used as an HTTP attribute.""" | |
2649 return '"%s"' % html_escape(string).replace('\n', ' ')\ | |
2650 .replace('\r', ' ').replace('\t', '	') | |
2651 | |
2652 | |
2653 def yieldroutes(func): | |
2654 """ Return a generator for routes that match the signature (name, args) | |
2655 of the func parameter. This may yield more than one route if the function | |
2656 takes optional keyword arguments. The output is best described by example:: | |
2657 | |
2658 a() -> '/a' | |
2659 b(x, y) -> '/b/<x>/<y>' | |
2660 c(x, y=5) -> '/c/<x>' and '/c/<x>/<y>' | |
2661 d(x=5, y=6) -> '/d' and '/d/<x>' and '/d/<x>/<y>' | |
2662 """ | |
2663 path = '/' + func.__name__.replace('__', '/').lstrip('/') | |
2664 spec = getargspec(func) | |
2665 argc = len(spec[0]) - len(spec[3] or []) | |
2666 path += ('/<%s>' * argc) % tuple(spec[0][:argc]) | |
2667 yield path | |
2668 for arg in spec[0][argc:]: | |
2669 path += '/<%s>' % arg | |
2670 yield path | |
2671 | |
2672 | |
2673 def path_shift(script_name, path_info, shift=1): | |
2674 """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. | |
2675 | |
2676 :return: The modified paths. | |
2677 :param script_name: The SCRIPT_NAME path. | |
2678 :param script_name: The PATH_INFO path. | |
2679 :param shift: The number of path fragments to shift. May be negative to | |
2680 change the shift direction. (default: 1) | |
2681 """ | |
2682 if shift == 0: return script_name, path_info | |
2683 pathlist = path_info.strip('/').split('/') | |
2684 scriptlist = script_name.strip('/').split('/') | |
2685 if pathlist and pathlist[0] == '': pathlist = [] | |
2686 if scriptlist and scriptlist[0] == '': scriptlist = [] | |
2687 if 0 < shift <= len(pathlist): | |
2688 moved = pathlist[:shift] | |
2689 scriptlist = scriptlist + moved | |
2690 pathlist = pathlist[shift:] | |
2691 elif 0 > shift >= -len(scriptlist): | |
2692 moved = scriptlist[shift:] | |
2693 pathlist = moved + pathlist | |
2694 scriptlist = scriptlist[:shift] | |
2695 else: | |
2696 empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' | |
2697 raise AssertionError("Cannot shift. Nothing left from %s" % empty) | |
2698 new_script_name = '/' + '/'.join(scriptlist) | |
2699 new_path_info = '/' + '/'.join(pathlist) | |
2700 if path_info.endswith('/') and pathlist: new_path_info += '/' | |
2701 return new_script_name, new_path_info | |
2702 | |
2703 | |
2704 def auth_basic(check, realm="private", text="Access denied"): | |
2705 """ Callback decorator to require HTTP auth (basic). | |
2706 TODO: Add route(check_auth=...) parameter. """ | |
2707 | |
2708 def decorator(func): | |
2709 | |
2710 @functools.wraps(func) | |
2711 def wrapper(*a, **ka): | |
2712 user, password = request.auth or (None, None) | |
2713 if user is None or not check(user, password): | |
2714 err = HTTPError(401, text) | |
2715 err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) | |
2716 return err | |
2717 return func(*a, **ka) | |
2718 | |
2719 return wrapper | |
2720 | |
2721 return decorator | |
2722 | |
2723 # Shortcuts for common Bottle methods. | |
2724 # They all refer to the current default application. | |
2725 | |
2726 | |
2727 def make_default_app_wrapper(name): | |
2728 """ Return a callable that relays calls to the current default app. """ | |
2729 | |
2730 @functools.wraps(getattr(Bottle, name)) | |
2731 def wrapper(*a, **ka): | |
2732 return getattr(app(), name)(*a, **ka) | |
2733 | |
2734 return wrapper | |
2735 | |
2736 | |
2737 route = make_default_app_wrapper('route') | |
2738 get = make_default_app_wrapper('get') | |
2739 post = make_default_app_wrapper('post') | |
2740 put = make_default_app_wrapper('put') | |
2741 delete = make_default_app_wrapper('delete') | |
2742 patch = make_default_app_wrapper('patch') | |
2743 error = make_default_app_wrapper('error') | |
2744 mount = make_default_app_wrapper('mount') | |
2745 hook = make_default_app_wrapper('hook') | |
2746 install = make_default_app_wrapper('install') | |
2747 uninstall = make_default_app_wrapper('uninstall') | |
2748 url = make_default_app_wrapper('get_url') | |
2749 | |
2750 ############################################################################### | |
2751 # Server Adapter ############################################################### | |
2752 ############################################################################### | |
2753 | |
2754 | |
2755 class ServerAdapter(object): | |
2756 quiet = False | |
2757 | |
2758 def __init__(self, host='127.0.0.1', port=8080, **options): | |
2759 self.options = options | |
2760 self.host = host | |
2761 self.port = int(port) | |
2762 | |
2763 def run(self, handler): # pragma: no cover | |
2764 pass | |
2765 | |
2766 def __repr__(self): | |
2767 args = ', '.join(['%s=%s' % (k, repr(v)) | |
2768 for k, v in self.options.items()]) | |
2769 return "%s(%s)" % (self.__class__.__name__, args) | |
2770 | |
2771 | |
2772 class CGIServer(ServerAdapter): | |
2773 quiet = True | |
2774 | |
2775 def run(self, handler): # pragma: no cover | |
2776 from wsgiref.handlers import CGIHandler | |
2777 | |
2778 def fixed_environ(environ, start_response): | |
2779 environ.setdefault('PATH_INFO', '') | |
2780 return handler(environ, start_response) | |
2781 | |
2782 CGIHandler().run(fixed_environ) | |
2783 | |
2784 | |
2785 class FlupFCGIServer(ServerAdapter): | |
2786 def run(self, handler): # pragma: no cover | |
2787 import flup.server.fcgi | |
2788 self.options.setdefault('bindAddress', (self.host, self.port)) | |
2789 flup.server.fcgi.WSGIServer(handler, **self.options).run() | |
2790 | |
2791 | |
2792 class WSGIRefServer(ServerAdapter): | |
2793 def run(self, app): # pragma: no cover | |
2794 from wsgiref.simple_server import make_server | |
2795 from wsgiref.simple_server import WSGIRequestHandler, WSGIServer | |
2796 import socket | |
2797 | |
2798 class FixedHandler(WSGIRequestHandler): | |
2799 def address_string(self): # Prevent reverse DNS lookups please. | |
2800 return self.client_address[0] | |
2801 | |
2802 def log_request(*args, **kw): | |
2803 if not self.quiet: | |
2804 return WSGIRequestHandler.log_request(*args, **kw) | |
2805 | |
2806 handler_cls = self.options.get('handler_class', FixedHandler) | |
2807 server_cls = self.options.get('server_class', WSGIServer) | |
2808 | |
2809 if ':' in self.host: # Fix wsgiref for IPv6 addresses. | |
2810 if getattr(server_cls, 'address_family') == socket.AF_INET: | |
2811 | |
2812 class server_cls(server_cls): | |
2813 address_family = socket.AF_INET6 | |
2814 | |
2815 self.srv = make_server(self.host, self.port, app, server_cls, | |
2816 handler_cls) | |
2817 self.port = self.srv.server_port # update port actual port (0 means random) | |
2818 try: | |
2819 self.srv.serve_forever() | |
2820 except KeyboardInterrupt: | |
2821 self.srv.server_close() # Prevent ResourceWarning: unclosed socket | |
2822 raise | |
2823 | |
2824 | |
2825 class CherryPyServer(ServerAdapter): | |
2826 def run(self, handler): # pragma: no cover | |
2827 from cherrypy import wsgiserver | |
2828 self.options['bind_addr'] = (self.host, self.port) | |
2829 self.options['wsgi_app'] = handler | |
2830 | |
2831 certfile = self.options.get('certfile') | |
2832 if certfile: | |
2833 del self.options['certfile'] | |
2834 keyfile = self.options.get('keyfile') | |
2835 if keyfile: | |
2836 del self.options['keyfile'] | |
2837 | |
2838 server = wsgiserver.CherryPyWSGIServer(**self.options) | |
2839 if certfile: | |
2840 server.ssl_certificate = certfile | |
2841 if keyfile: | |
2842 server.ssl_private_key = keyfile | |
2843 | |
2844 try: | |
2845 server.start() | |
2846 finally: | |
2847 server.stop() | |
2848 | |
2849 | |
2850 class WaitressServer(ServerAdapter): | |
2851 def run(self, handler): | |
2852 from waitress import serve | |
2853 serve(handler, host=self.host, port=self.port, _quiet=self.quiet) | |
2854 | |
2855 | |
2856 class PasteServer(ServerAdapter): | |
2857 def run(self, handler): # pragma: no cover | |
2858 from paste import httpserver | |
2859 from paste.translogger import TransLogger | |
2860 handler = TransLogger(handler, setup_console_handler=(not self.quiet)) | |
2861 httpserver.serve(handler, | |
2862 host=self.host, | |
2863 port=str(self.port), **self.options) | |
2864 | |
2865 | |
2866 class MeinheldServer(ServerAdapter): | |
2867 def run(self, handler): | |
2868 from meinheld import server | |
2869 server.listen((self.host, self.port)) | |
2870 server.run(handler) | |
2871 | |
2872 | |
2873 class FapwsServer(ServerAdapter): | |
2874 """ Extremely fast webserver using libev. See http://www.fapws.org/ """ | |
2875 | |
2876 def run(self, handler): # pragma: no cover | |
2877 import fapws._evwsgi as evwsgi | |
2878 from fapws import base, config | |
2879 port = self.port | |
2880 if float(config.SERVER_IDENT[-2:]) > 0.4: | |
2881 # fapws3 silently changed its API in 0.5 | |
2882 port = str(port) | |
2883 evwsgi.start(self.host, port) | |
2884 # fapws3 never releases the GIL. Complain upstream. I tried. No luck. | |
2885 if 'BOTTLE_CHILD' in os.environ and not self.quiet: | |
2886 _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") | |
2887 _stderr(" (Fapws3 breaks python thread support)\n") | |
2888 evwsgi.set_base_module(base) | |
2889 | |
2890 def app(environ, start_response): | |
2891 environ['wsgi.multiprocess'] = False | |
2892 return handler(environ, start_response) | |
2893 | |
2894 evwsgi.wsgi_cb(('', app)) | |
2895 evwsgi.run() | |
2896 | |
2897 | |
2898 class TornadoServer(ServerAdapter): | |
2899 """ The super hyped asynchronous server by facebook. Untested. """ | |
2900 | |
2901 def run(self, handler): # pragma: no cover | |
2902 import tornado.wsgi, tornado.httpserver, tornado.ioloop | |
2903 container = tornado.wsgi.WSGIContainer(handler) | |
2904 server = tornado.httpserver.HTTPServer(container) | |
2905 server.listen(port=self.port, address=self.host) | |
2906 tornado.ioloop.IOLoop.instance().start() | |
2907 | |
2908 | |
2909 class AppEngineServer(ServerAdapter): | |
2910 """ Adapter for Google App Engine. """ | |
2911 quiet = True | |
2912 | |
2913 def run(self, handler): | |
2914 from google.appengine.ext.webapp import util | |
2915 # A main() function in the handler script enables 'App Caching'. | |
2916 # Lets makes sure it is there. This _really_ improves performance. | |
2917 module = sys.modules.get('__main__') | |
2918 if module and not hasattr(module, 'main'): | |
2919 module.main = lambda: util.run_wsgi_app(handler) | |
2920 util.run_wsgi_app(handler) | |
2921 | |
2922 | |
2923 class TwistedServer(ServerAdapter): | |
2924 """ Untested. """ | |
2925 | |
2926 def run(self, handler): | |
2927 from twisted.web import server, wsgi | |
2928 from twisted.python.threadpool import ThreadPool | |
2929 from twisted.internet import reactor | |
2930 thread_pool = ThreadPool() | |
2931 thread_pool.start() | |
2932 reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) | |
2933 factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) | |
2934 reactor.listenTCP(self.port, factory, interface=self.host) | |
2935 if not reactor.running: | |
2936 reactor.run() | |
2937 | |
2938 | |
2939 class DieselServer(ServerAdapter): | |
2940 """ Untested. """ | |
2941 | |
2942 def run(self, handler): | |
2943 from diesel.protocols.wsgi import WSGIApplication | |
2944 app = WSGIApplication(handler, port=self.port) | |
2945 app.run() | |
2946 | |
2947 | |
2948 class GeventServer(ServerAdapter): | |
2949 """ Untested. Options: | |
2950 | |
2951 * `fast` (default: False) uses libevent's http server, but has some | |
2952 issues: No streaming, no pipelining, no SSL. | |
2953 * See gevent.wsgi.WSGIServer() documentation for more options. | |
2954 """ | |
2955 | |
2956 def run(self, handler): | |
2957 from gevent import wsgi, pywsgi, local | |
2958 if not isinstance(threading.local(), local.local): | |
2959 msg = "Bottle requires gevent.monkey.patch_all() (before import)" | |
2960 raise RuntimeError(msg) | |
2961 if not self.options.pop('fast', None): wsgi = pywsgi | |
2962 self.options['log'] = None if self.quiet else 'default' | |
2963 address = (self.host, self.port) | |
2964 server = wsgi.WSGIServer(address, handler, **self.options) | |
2965 if 'BOTTLE_CHILD' in os.environ: | |
2966 import signal | |
2967 signal.signal(signal.SIGINT, lambda s, f: server.stop()) | |
2968 server.serve_forever() | |
2969 | |
2970 | |
2971 class GeventSocketIOServer(ServerAdapter): | |
2972 def run(self, handler): | |
2973 from socketio import server | |
2974 address = (self.host, self.port) | |
2975 server.SocketIOServer(address, handler, **self.options).serve_forever() | |
2976 | |
2977 | |
2978 class GunicornServer(ServerAdapter): | |
2979 """ Untested. See http://gunicorn.org/configure.html for options. """ | |
2980 | |
2981 def run(self, handler): | |
2982 from gunicorn.app.base import Application | |
2983 | |
2984 config = {'bind': "%s:%d" % (self.host, int(self.port))} | |
2985 config.update(self.options) | |
2986 | |
2987 class GunicornApplication(Application): | |
2988 def init(self, parser, opts, args): | |
2989 return config | |
2990 | |
2991 def load(self): | |
2992 return handler | |
2993 | |
2994 GunicornApplication().run() | |
2995 | |
2996 | |
2997 class EventletServer(ServerAdapter): | |
2998 """ Untested. Options: | |
2999 | |
3000 * `backlog` adjust the eventlet backlog parameter which is the maximum | |
3001 number of queued connections. Should be at least 1; the maximum | |
3002 value is system-dependent. | |
3003 * `family`: (default is 2) socket family, optional. See socket | |
3004 documentation for available families. | |
3005 """ | |
3006 | |
3007 def run(self, handler): | |
3008 from eventlet import wsgi, listen, patcher | |
3009 if not patcher.is_monkey_patched(os): | |
3010 msg = "Bottle requires eventlet.monkey_patch() (before import)" | |
3011 raise RuntimeError(msg) | |
3012 socket_args = {} | |
3013 for arg in ('backlog', 'family'): | |
3014 try: | |
3015 socket_args[arg] = self.options.pop(arg) | |
3016 except KeyError: | |
3017 pass | |
3018 address = (self.host, self.port) | |
3019 try: | |
3020 wsgi.server(listen(address, **socket_args), handler, | |
3021 log_output=(not self.quiet)) | |
3022 except TypeError: | |
3023 # Fallback, if we have old version of eventlet | |
3024 wsgi.server(listen(address), handler) | |
3025 | |
3026 | |
3027 class RocketServer(ServerAdapter): | |
3028 """ Untested. """ | |
3029 | |
3030 def run(self, handler): | |
3031 from rocket import Rocket | |
3032 server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) | |
3033 server.start() | |
3034 | |
3035 | |
3036 class BjoernServer(ServerAdapter): | |
3037 """ Fast server written in C: https://github.com/jonashaag/bjoern """ | |
3038 | |
3039 def run(self, handler): | |
3040 from bjoern import run | |
3041 run(handler, self.host, self.port) | |
3042 | |
3043 | |
3044 class AiohttpServer(ServerAdapter): | |
3045 """ Untested. | |
3046 aiohttp | |
3047 https://pypi.python.org/pypi/aiohttp/ | |
3048 """ | |
3049 | |
3050 def run(self, handler): | |
3051 import asyncio | |
3052 from aiohttp.wsgi import WSGIServerHttpProtocol | |
3053 self.loop = asyncio.new_event_loop() | |
3054 asyncio.set_event_loop(self.loop) | |
3055 | |
3056 protocol_factory = lambda: WSGIServerHttpProtocol( | |
3057 handler, | |
3058 readpayload=True, | |
3059 debug=(not self.quiet)) | |
3060 self.loop.run_until_complete(self.loop.create_server(protocol_factory, | |
3061 self.host, | |
3062 self.port)) | |
3063 | |
3064 if 'BOTTLE_CHILD' in os.environ: | |
3065 import signal | |
3066 signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) | |
3067 | |
3068 try: | |
3069 self.loop.run_forever() | |
3070 except KeyboardInterrupt: | |
3071 self.loop.stop() | |
3072 | |
3073 | |
3074 class AutoServer(ServerAdapter): | |
3075 """ Untested. """ | |
3076 adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, | |
3077 WSGIRefServer] | |
3078 | |
3079 def run(self, handler): | |
3080 for sa in self.adapters: | |
3081 try: | |
3082 return sa(self.host, self.port, **self.options).run(handler) | |
3083 except ImportError: | |
3084 pass | |
3085 | |
3086 | |
3087 server_names = { | |
3088 'cgi': CGIServer, | |
3089 'flup': FlupFCGIServer, | |
3090 'wsgiref': WSGIRefServer, | |
3091 'waitress': WaitressServer, | |
3092 'cherrypy': CherryPyServer, | |
3093 'paste': PasteServer, | |
3094 'fapws3': FapwsServer, | |
3095 'tornado': TornadoServer, | |
3096 'gae': AppEngineServer, | |
3097 'twisted': TwistedServer, | |
3098 'diesel': DieselServer, | |
3099 'meinheld': MeinheldServer, | |
3100 'gunicorn': GunicornServer, | |
3101 'eventlet': EventletServer, | |
3102 'gevent': GeventServer, | |
3103 'geventSocketIO': GeventSocketIOServer, | |
3104 'rocket': RocketServer, | |
3105 'bjoern': BjoernServer, | |
3106 'aiohttp': AiohttpServer, | |
3107 'auto': AutoServer, | |
3108 } | |
3109 | |
3110 ############################################################################### | |
3111 # Application Control ########################################################## | |
3112 ############################################################################### | |
3113 | |
3114 | |
3115 def load(target, **namespace): | |
3116 """ Import a module or fetch an object from a module. | |
3117 | |
3118 * ``package.module`` returns `module` as a module object. | |
3119 * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. | |
3120 * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. | |
3121 | |
3122 The last form accepts not only function calls, but any type of | |
3123 expression. Keyword arguments passed to this function are available as | |
3124 local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` | |
3125 """ | |
3126 module, target = target.split(":", 1) if ':' in target else (target, None) | |
3127 if module not in sys.modules: __import__(module) | |
3128 if not target: return sys.modules[module] | |
3129 if target.isalnum(): return getattr(sys.modules[module], target) | |
3130 package_name = module.split('.')[0] | |
3131 namespace[package_name] = sys.modules[package_name] | |
3132 return eval('%s.%s' % (module, target), namespace) | |
3133 | |
3134 | |
3135 def load_app(target): | |
3136 """ Load a bottle application from a module and make sure that the import | |
3137 does not affect the current default application, but returns a separate | |
3138 application object. See :func:`load` for the target parameter. """ | |
3139 global NORUN | |
3140 NORUN, nr_old = True, NORUN | |
3141 tmp = default_app.push() # Create a new "default application" | |
3142 try: | |
3143 rv = load(target) # Import the target module | |
3144 return rv if callable(rv) else tmp | |
3145 finally: | |
3146 default_app.remove(tmp) # Remove the temporary added default application | |
3147 NORUN = nr_old | |
3148 | |
3149 | |
3150 _debug = debug | |
3151 | |
3152 | |
3153 def run(app=None, | |
3154 server='wsgiref', | |
3155 host='127.0.0.1', | |
3156 port=8080, | |
3157 interval=1, | |
3158 reloader=False, | |
3159 quiet=False, | |
3160 plugins=None, | |
3161 debug=None, **kargs): | |
3162 """ Start a server instance. This method blocks until the server terminates. | |
3163 | |
3164 :param app: WSGI application or target string supported by | |
3165 :func:`load_app`. (default: :func:`default_app`) | |
3166 :param server: Server adapter to use. See :data:`server_names` keys | |
3167 for valid names or pass a :class:`ServerAdapter` subclass. | |
3168 (default: `wsgiref`) | |
3169 :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on | |
3170 all interfaces including the external one. (default: 127.0.0.1) | |
3171 :param port: Server port to bind to. Values below 1024 require root | |
3172 privileges. (default: 8080) | |
3173 :param reloader: Start auto-reloading server? (default: False) | |
3174 :param interval: Auto-reloader interval in seconds (default: 1) | |
3175 :param quiet: Suppress output to stdout and stderr? (default: False) | |
3176 :param options: Options passed to the server adapter. | |
3177 """ | |
3178 if NORUN: return | |
3179 if reloader and not os.environ.get('BOTTLE_CHILD'): | |
3180 import subprocess | |
3181 lockfile = None | |
3182 try: | |
3183 fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') | |
3184 os.close(fd) # We only need this file to exist. We never write to it | |
3185 while os.path.exists(lockfile): | |
3186 args = [sys.executable] + sys.argv | |
3187 environ = os.environ.copy() | |
3188 environ['BOTTLE_CHILD'] = 'true' | |
3189 environ['BOTTLE_LOCKFILE'] = lockfile | |
3190 p = subprocess.Popen(args, env=environ) | |
3191 while p.poll() is None: # Busy wait... | |
3192 os.utime(lockfile, None) # I am alive! | |
3193 time.sleep(interval) | |
3194 if p.poll() != 3: | |
3195 if os.path.exists(lockfile): os.unlink(lockfile) | |
3196 sys.exit(p.poll()) | |
3197 except KeyboardInterrupt: | |
3198 pass | |
3199 finally: | |
3200 if os.path.exists(lockfile): | |
3201 os.unlink(lockfile) | |
3202 return | |
3203 | |
3204 try: | |
3205 if debug is not None: _debug(debug) | |
3206 app = app or default_app() | |
3207 if isinstance(app, basestring): | |
3208 app = load_app(app) | |
3209 if not callable(app): | |
3210 raise ValueError("Application is not callable: %r" % app) | |
3211 | |
3212 for plugin in plugins or []: | |
3213 if isinstance(plugin, basestring): | |
3214 plugin = load(plugin) | |
3215 app.install(plugin) | |
3216 | |
3217 if server in server_names: | |
3218 server = server_names.get(server) | |
3219 if isinstance(server, basestring): | |
3220 server = load(server) | |
3221 if isinstance(server, type): | |
3222 server = server(host=host, port=port, **kargs) | |
3223 if not isinstance(server, ServerAdapter): | |
3224 raise ValueError("Unknown or unsupported server: %r" % server) | |
3225 | |
3226 server.quiet = server.quiet or quiet | |
3227 if not server.quiet: | |
3228 _stderr("Bottle v%s server starting up (using %s)...\n" % | |
3229 (__version__, repr(server))) | |
3230 _stderr("Listening on http://%s:%d/\n" % | |
3231 (server.host, server.port)) | |
3232 _stderr("Hit Ctrl-C to quit.\n\n") | |
3233 | |
3234 if reloader: | |
3235 lockfile = os.environ.get('BOTTLE_LOCKFILE') | |
3236 bgcheck = FileCheckerThread(lockfile, interval) | |
3237 with bgcheck: | |
3238 server.run(app) | |
3239 if bgcheck.status == 'reload': | |
3240 sys.exit(3) | |
3241 else: | |
3242 server.run(app) | |
3243 except KeyboardInterrupt: | |
3244 pass | |
3245 except (SystemExit, MemoryError): | |
3246 raise | |
3247 except: | |
3248 if not reloader: raise | |
3249 if not getattr(server, 'quiet', quiet): | |
3250 print_exc() | |
3251 time.sleep(interval) | |
3252 sys.exit(3) | |
3253 | |
3254 | |
3255 class FileCheckerThread(threading.Thread): | |
3256 """ Interrupt main-thread as soon as a changed module file is detected, | |
3257 the lockfile gets deleted or gets to old. """ | |
3258 | |
3259 def __init__(self, lockfile, interval): | |
3260 threading.Thread.__init__(self) | |
3261 self.daemon = True | |
3262 self.lockfile, self.interval = lockfile, interval | |
3263 #: Is one of 'reload', 'error' or 'exit' | |
3264 self.status = None | |
3265 | |
3266 def run(self): | |
3267 exists = os.path.exists | |
3268 mtime = lambda p: os.stat(p).st_mtime | |
3269 files = dict() | |
3270 | |
3271 for module in list(sys.modules.values()): | |
3272 path = getattr(module, '__file__', '') | |
3273 if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] | |
3274 if path and exists(path): files[path] = mtime(path) | |
3275 | |
3276 while not self.status: | |
3277 if not exists(self.lockfile)\ | |
3278 or mtime(self.lockfile) < time.time() - self.interval - 5: | |
3279 self.status = 'error' | |
3280 thread.interrupt_main() | |
3281 for path, lmtime in list(files.items()): | |
3282 if not exists(path) or mtime(path) > lmtime: | |
3283 self.status = 'reload' | |
3284 thread.interrupt_main() | |
3285 break | |
3286 time.sleep(self.interval) | |
3287 | |
3288 def __enter__(self): | |
3289 self.start() | |
3290 | |
3291 def __exit__(self, exc_type, *_): | |
3292 if not self.status: self.status = 'exit' # silent exit | |
3293 self.join() | |
3294 return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) | |
3295 | |
3296 ############################################################################### | |
3297 # Template Adapters ############################################################ | |
3298 ############################################################################### | |
3299 | |
3300 | |
3301 class TemplateError(HTTPError): | |
3302 def __init__(self, message): | |
3303 HTTPError.__init__(self, 500, message) | |
3304 | |
3305 | |
3306 class BaseTemplate(object): | |
3307 """ Base class and minimal API for template adapters """ | |
3308 extensions = ['tpl', 'html', 'thtml', 'stpl'] | |
3309 settings = {} #used in prepare() | |
3310 defaults = {} #used in render() | |
3311 | |
3312 def __init__(self, | |
3313 source=None, | |
3314 name=None, | |
3315 lookup=None, | |
3316 encoding='utf8', **settings): | |
3317 """ Create a new template. | |
3318 If the source parameter (str or buffer) is missing, the name argument | |
3319 is used to guess a template filename. Subclasses can assume that | |
3320 self.source and/or self.filename are set. Both are strings. | |
3321 The lookup, encoding and settings parameters are stored as instance | |
3322 variables. | |
3323 The lookup parameter stores a list containing directory paths. | |
3324 The encoding parameter should be used to decode byte strings or files. | |
3325 The settings parameter contains a dict for engine-specific settings. | |
3326 """ | |
3327 self.name = name | |
3328 self.source = source.read() if hasattr(source, 'read') else source | |
3329 self.filename = source.filename if hasattr(source, 'filename') else None | |
3330 self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] | |
3331 self.encoding = encoding | |
3332 self.settings = self.settings.copy() # Copy from class variable | |
3333 self.settings.update(settings) # Apply | |
3334 if not self.source and self.name: | |
3335 self.filename = self.search(self.name, self.lookup) | |
3336 if not self.filename: | |
3337 raise TemplateError('Template %s not found.' % repr(name)) | |
3338 if not self.source and not self.filename: | |
3339 raise TemplateError('No template specified.') | |
3340 self.prepare(**self.settings) | |
3341 | |
3342 @classmethod | |
3343 def search(cls, name, lookup=None): | |
3344 """ Search name in all directories specified in lookup. | |
3345 First without, then with common extensions. Return first hit. """ | |
3346 if not lookup: | |
3347 depr('The template lookup path list should not be empty.', | |
3348 True) #0.12 | |
3349 lookup = ['.'] | |
3350 | |
3351 if os.path.isabs(name) and os.path.isfile(name): | |
3352 depr('Absolute template path names are deprecated.', True) #0.12 | |
3353 return os.path.abspath(name) | |
3354 | |
3355 for spath in lookup: | |
3356 spath = os.path.abspath(spath) + os.sep | |
3357 fname = os.path.abspath(os.path.join(spath, name)) | |
3358 if not fname.startswith(spath): continue | |
3359 if os.path.isfile(fname): return fname | |
3360 for ext in cls.extensions: | |
3361 if os.path.isfile('%s.%s' % (fname, ext)): | |
3362 return '%s.%s' % (fname, ext) | |
3363 | |
3364 @classmethod | |
3365 def global_config(cls, key, *args): | |
3366 """ This reads or sets the global settings stored in class.settings. """ | |
3367 if args: | |
3368 cls.settings = cls.settings.copy() # Make settings local to class | |
3369 cls.settings[key] = args[0] | |
3370 else: | |
3371 return cls.settings[key] | |
3372 | |
3373 def prepare(self, **options): | |
3374 """ Run preparations (parsing, caching, ...). | |
3375 It should be possible to call this again to refresh a template or to | |
3376 update settings. | |
3377 """ | |
3378 raise NotImplementedError | |
3379 | |
3380 def render(self, *args, **kwargs): | |
3381 """ Render the template with the specified local variables and return | |
3382 a single byte or unicode string. If it is a byte string, the encoding | |
3383 must match self.encoding. This method must be thread-safe! | |
3384 Local variables may be provided in dictionaries (args) | |
3385 or directly, as keywords (kwargs). | |
3386 """ | |
3387 raise NotImplementedError | |
3388 | |
3389 | |
3390 class MakoTemplate(BaseTemplate): | |
3391 def prepare(self, **options): | |
3392 from mako.template import Template | |
3393 from mako.lookup import TemplateLookup | |
3394 options.update({'input_encoding': self.encoding}) | |
3395 options.setdefault('format_exceptions', bool(DEBUG)) | |
3396 lookup = TemplateLookup(directories=self.lookup, **options) | |
3397 if self.source: | |
3398 self.tpl = Template(self.source, lookup=lookup, **options) | |
3399 else: | |
3400 self.tpl = Template(uri=self.name, | |
3401 filename=self.filename, | |
3402 lookup=lookup, **options) | |
3403 | |
3404 def render(self, *args, **kwargs): | |
3405 for dictarg in args: | |
3406 kwargs.update(dictarg) | |
3407 _defaults = self.defaults.copy() | |
3408 _defaults.update(kwargs) | |
3409 return self.tpl.render(**_defaults) | |
3410 | |
3411 | |
3412 class CheetahTemplate(BaseTemplate): | |
3413 def prepare(self, **options): | |
3414 from Cheetah.Template import Template | |
3415 self.context = threading.local() | |
3416 self.context.vars = {} | |
3417 options['searchList'] = [self.context.vars] | |
3418 if self.source: | |
3419 self.tpl = Template(source=self.source, **options) | |
3420 else: | |
3421 self.tpl = Template(file=self.filename, **options) | |
3422 | |
3423 def render(self, *args, **kwargs): | |
3424 for dictarg in args: | |
3425 kwargs.update(dictarg) | |
3426 self.context.vars.update(self.defaults) | |
3427 self.context.vars.update(kwargs) | |
3428 out = str(self.tpl) | |
3429 self.context.vars.clear() | |
3430 return out | |
3431 | |
3432 | |
3433 class Jinja2Template(BaseTemplate): | |
3434 def prepare(self, filters=None, tests=None, globals={}, **kwargs): | |
3435 from jinja2 import Environment, FunctionLoader | |
3436 self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) | |
3437 if filters: self.env.filters.update(filters) | |
3438 if tests: self.env.tests.update(tests) | |
3439 if globals: self.env.globals.update(globals) | |
3440 if self.source: | |
3441 self.tpl = self.env.from_string(self.source) | |
3442 else: | |
3443 self.tpl = self.env.get_template(self.filename) | |
3444 | |
3445 def render(self, *args, **kwargs): | |
3446 for dictarg in args: | |
3447 kwargs.update(dictarg) | |
3448 _defaults = self.defaults.copy() | |
3449 _defaults.update(kwargs) | |
3450 return self.tpl.render(**_defaults) | |
3451 | |
3452 def loader(self, name): | |
3453 fname = self.search(name, self.lookup) | |
3454 if not fname: return | |
3455 with open(fname, "rb") as f: | |
3456 return f.read().decode(self.encoding) | |
3457 | |
3458 | |
3459 class SimpleTemplate(BaseTemplate): | |
3460 def prepare(self, | |
3461 escape_func=html_escape, | |
3462 noescape=False, | |
3463 syntax=None, **ka): | |
3464 self.cache = {} | |
3465 enc = self.encoding | |
3466 self._str = lambda x: touni(x, enc) | |
3467 self._escape = lambda x: escape_func(touni(x, enc)) | |
3468 self.syntax = syntax | |
3469 if noescape: | |
3470 self._str, self._escape = self._escape, self._str | |
3471 | |
3472 @cached_property | |
3473 def co(self): | |
3474 return compile(self.code, self.filename or '<string>', 'exec') | |
3475 | |
3476 @cached_property | |
3477 def code(self): | |
3478 source = self.source | |
3479 if not source: | |
3480 with open(self.filename, 'rb') as f: | |
3481 source = f.read() | |
3482 try: | |
3483 source, encoding = touni(source), 'utf8' | |
3484 except UnicodeError: | |
3485 depr('Template encodings other than utf8 are not supported.') #0.11 | |
3486 source, encoding = touni(source, 'latin1'), 'latin1' | |
3487 parser = StplParser(source, encoding=encoding, syntax=self.syntax) | |
3488 code = parser.translate() | |
3489 self.encoding = parser.encoding | |
3490 return code | |
3491 | |
3492 def _rebase(self, _env, _name=None, **kwargs): | |
3493 _env['_rebase'] = (_name, kwargs) | |
3494 | |
3495 def _include(self, _env, _name=None, **kwargs): | |
3496 env = _env.copy() | |
3497 env.update(kwargs) | |
3498 if _name not in self.cache: | |
3499 self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) | |
3500 return self.cache[_name].execute(env['_stdout'], env) | |
3501 | |
3502 def execute(self, _stdout, kwargs): | |
3503 env = self.defaults.copy() | |
3504 env.update(kwargs) | |
3505 env.update({ | |
3506 '_stdout': _stdout, | |
3507 '_printlist': _stdout.extend, | |
3508 'include': functools.partial(self._include, env), | |
3509 'rebase': functools.partial(self._rebase, env), | |
3510 '_rebase': None, | |
3511 '_str': self._str, | |
3512 '_escape': self._escape, | |
3513 'get': env.get, | |
3514 'setdefault': env.setdefault, | |
3515 'defined': env.__contains__ | |
3516 }) | |
3517 eval(self.co, env) | |
3518 if env.get('_rebase'): | |
3519 subtpl, rargs = env.pop('_rebase') | |
3520 rargs['base'] = ''.join(_stdout) #copy stdout | |
3521 del _stdout[:] # clear stdout | |
3522 return self._include(env, subtpl, **rargs) | |
3523 return env | |
3524 | |
3525 def render(self, *args, **kwargs): | |
3526 """ Render the template using keyword arguments as local variables. """ | |
3527 env = {} | |
3528 stdout = [] | |
3529 for dictarg in args: | |
3530 env.update(dictarg) | |
3531 env.update(kwargs) | |
3532 self.execute(stdout, env) | |
3533 return ''.join(stdout) | |
3534 | |
3535 | |
3536 class StplSyntaxError(TemplateError): | |
3537 | |
3538 pass | |
3539 | |
3540 | |
3541 class StplParser(object): | |
3542 """ Parser for stpl templates. """ | |
3543 _re_cache = {} #: Cache for compiled re patterns | |
3544 | |
3545 # This huge pile of voodoo magic splits python code into 8 different tokens. | |
3546 # We use the verbose (?x) regex mode to make this more manageable | |
3547 | |
3548 _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode | |
3549 [urbURB]* | |
3550 (?: ''(?!') | |
3551 |""(?!") | |
3552 |'{6} | |
3553 |"{6} | |
3554 |'(?:[^\\']|\\.)+?' | |
3555 |"(?:[^\\"]|\\.)+?" | |
3556 |'{3}(?:[^\\]|\\.|\n)+?'{3} | |
3557 |"{3}(?:[^\\]|\\.|\n)+?"{3} | |
3558 ) | |
3559 )''' | |
3560 | |
3561 _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later | |
3562 | |
3563 _re_tok += r''' | |
3564 # 2: Comments (until end of line, but not the newline itself) | |
3565 |(\#.*) | |
3566 | |
3567 # 3: Open and close (4) grouping tokens | |
3568 |([\[\{\(]) | |
3569 |([\]\}\)]) | |
3570 | |
3571 # 5,6: Keywords that start or continue a python block (only start of line) | |
3572 |^([\ \t]*(?:if|for|while|with|try|def|class)\b) | |
3573 |^([\ \t]*(?:elif|else|except|finally)\b) | |
3574 | |
3575 # 7: Our special 'end' keyword (but only if it stands alone) | |
3576 |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) | |
3577 | |
3578 # 8: A customizable end-of-code-block template token (only end of line) | |
3579 |(%(block_close)s[\ \t]*(?=\r?$)) | |
3580 | |
3581 # 9: And finally, a single newline. The 10th token is 'everything else' | |
3582 |(\r?\n) | |
3583 ''' | |
3584 | |
3585 # Match the start tokens of code areas in a template | |
3586 _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' | |
3587 # Match inline statements (may contain python strings) | |
3588 _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl | |
3589 | |
3590 default_syntax = '<% %> % {{ }}' | |
3591 | |
3592 def __init__(self, source, syntax=None, encoding='utf8'): | |
3593 self.source, self.encoding = touni(source, encoding), encoding | |
3594 self.set_syntax(syntax or self.default_syntax) | |
3595 self.code_buffer, self.text_buffer = [], [] | |
3596 self.lineno, self.offset = 1, 0 | |
3597 self.indent, self.indent_mod = 0, 0 | |
3598 self.paren_depth = 0 | |
3599 | |
3600 def get_syntax(self): | |
3601 """ Tokens as a space separated string (default: <% %> % {{ }}) """ | |
3602 return self._syntax | |
3603 | |
3604 def set_syntax(self, syntax): | |
3605 self._syntax = syntax | |
3606 self._tokens = syntax.split() | |
3607 if not syntax in self._re_cache: | |
3608 names = 'block_start block_close line_start inline_start inline_end' | |
3609 etokens = map(re.escape, self._tokens) | |
3610 pattern_vars = dict(zip(names.split(), etokens)) | |
3611 patterns = (self._re_split, self._re_tok, self._re_inl) | |
3612 patterns = [re.compile(p % pattern_vars) for p in patterns] | |
3613 self._re_cache[syntax] = patterns | |
3614 self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] | |
3615 | |
3616 syntax = property(get_syntax, set_syntax) | |
3617 | |
3618 def translate(self): | |
3619 if self.offset: raise RuntimeError('Parser is a one time instance.') | |
3620 while True: | |
3621 m = self.re_split.search(self.source, pos=self.offset) | |
3622 if m: | |
3623 text = self.source[self.offset:m.start()] | |
3624 self.text_buffer.append(text) | |
3625 self.offset = m.end() | |
3626 if m.group(1): # Escape syntax | |
3627 line, sep, _ = self.source[self.offset:].partition('\n') | |
3628 self.text_buffer.append(self.source[m.start():m.start(1)] + | |
3629 m.group(2) + line + sep) | |
3630 self.offset += len(line + sep) | |
3631 continue | |
3632 self.flush_text() | |
3633 self.offset += self.read_code(self.source[self.offset:], | |
3634 multiline=bool(m.group(4))) | |
3635 else: | |
3636 break | |
3637 self.text_buffer.append(self.source[self.offset:]) | |
3638 self.flush_text() | |
3639 return ''.join(self.code_buffer) | |
3640 | |
3641 def read_code(self, pysource, multiline): | |
3642 code_line, comment = '', '' | |
3643 offset = 0 | |
3644 while True: | |
3645 m = self.re_tok.search(pysource, pos=offset) | |
3646 if not m: | |
3647 code_line += pysource[offset:] | |
3648 offset = len(pysource) | |
3649 self.write_code(code_line.strip(), comment) | |
3650 break | |
3651 code_line += pysource[offset:m.start()] | |
3652 offset = m.end() | |
3653 _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() | |
3654 if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c | |
3655 code_line += _blk1 or _blk2 | |
3656 continue | |
3657 if _str: # Python string | |
3658 code_line += _str | |
3659 elif _com: # Python comment (up to EOL) | |
3660 comment = _com | |
3661 if multiline and _com.strip().endswith(self._tokens[1]): | |
3662 multiline = False # Allow end-of-block in comments | |
3663 elif _po: # open parenthesis | |
3664 self.paren_depth += 1 | |
3665 code_line += _po | |
3666 elif _pc: # close parenthesis | |
3667 if self.paren_depth > 0: | |
3668 # we could check for matching parentheses here, but it's | |
3669 # easier to leave that to python - just check counts | |
3670 self.paren_depth -= 1 | |
3671 code_line += _pc | |
3672 elif _blk1: # Start-block keyword (if/for/while/def/try/...) | |
3673 code_line, self.indent_mod = _blk1, -1 | |
3674 self.indent += 1 | |
3675 elif _blk2: # Continue-block keyword (else/elif/except/...) | |
3676 code_line, self.indent_mod = _blk2, -1 | |
3677 elif _end: # The non-standard 'end'-keyword (ends a block) | |
3678 self.indent -= 1 | |
3679 elif _cend: # The end-code-block template token (usually '%>') | |
3680 if multiline: multiline = False | |
3681 else: code_line += _cend | |
3682 else: # \n | |
3683 self.write_code(code_line.strip(), comment) | |
3684 self.lineno += 1 | |
3685 code_line, comment, self.indent_mod = '', '', 0 | |
3686 if not multiline: | |
3687 break | |
3688 | |
3689 return offset | |
3690 | |
3691 def flush_text(self): | |
3692 text = ''.join(self.text_buffer) | |
3693 del self.text_buffer[:] | |
3694 if not text: return | |
3695 parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent | |
3696 for m in self.re_inl.finditer(text): | |
3697 prefix, pos = text[pos:m.start()], m.end() | |
3698 if prefix: | |
3699 parts.append(nl.join(map(repr, prefix.splitlines(True)))) | |
3700 if prefix.endswith('\n'): parts[-1] += nl | |
3701 parts.append(self.process_inline(m.group(1).strip())) | |
3702 if pos < len(text): | |
3703 prefix = text[pos:] | |
3704 lines = prefix.splitlines(True) | |
3705 if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] | |
3706 elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] | |
3707 parts.append(nl.join(map(repr, lines))) | |
3708 code = '_printlist((%s,))' % ', '.join(parts) | |
3709 self.lineno += code.count('\n') + 1 | |
3710 self.write_code(code) | |
3711 | |
3712 @staticmethod | |
3713 def process_inline(chunk): | |
3714 if chunk[0] == '!': return '_str(%s)' % chunk[1:] | |
3715 return '_escape(%s)' % chunk | |
3716 | |
3717 def write_code(self, line, comment=''): | |
3718 code = ' ' * (self.indent + self.indent_mod) | |
3719 code += line.lstrip() + comment + '\n' | |
3720 self.code_buffer.append(code) | |
3721 | |
3722 | |
3723 def template(*args, **kwargs): | |
3724 """ | |
3725 Get a rendered template as a string iterator. | |
3726 You can use a name, a filename or a template string as first parameter. | |
3727 Template rendering arguments can be passed as dictionaries | |
3728 or directly (as keyword arguments). | |
3729 """ | |
3730 tpl = args[0] if args else None | |
3731 adapter = kwargs.pop('template_adapter', SimpleTemplate) | |
3732 lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) | |
3733 tplid = (id(lookup), tpl) | |
3734 if tplid not in TEMPLATES or DEBUG: | |
3735 settings = kwargs.pop('template_settings', {}) | |
3736 if isinstance(tpl, adapter): | |
3737 TEMPLATES[tplid] = tpl | |
3738 if settings: TEMPLATES[tplid].prepare(**settings) | |
3739 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: | |
3740 TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) | |
3741 else: | |
3742 TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) | |
3743 if not TEMPLATES[tplid]: | |
3744 abort(500, 'Template (%s) not found' % tpl) | |
3745 for dictarg in args[1:]: | |
3746 kwargs.update(dictarg) | |
3747 return TEMPLATES[tplid].render(kwargs) | |
3748 | |
3749 | |
3750 mako_template = functools.partial(template, template_adapter=MakoTemplate) | |
3751 cheetah_template = functools.partial(template, | |
3752 template_adapter=CheetahTemplate) | |
3753 jinja2_template = functools.partial(template, template_adapter=Jinja2Template) | |
3754 | |
3755 | |
3756 def view(tpl_name, **defaults): | |
3757 """ Decorator: renders a template for a handler. | |
3758 The handler can control its behavior like that: | |
3759 | |
3760 - return a dict of template vars to fill out the template | |
3761 - return something other than a dict and the view decorator will not | |
3762 process the template, but return the handler result as is. | |
3763 This includes returning a HTTPResponse(dict) to get, | |
3764 for instance, JSON with autojson or other castfilters. | |
3765 """ | |
3766 | |
3767 def decorator(func): | |
3768 | |
3769 @functools.wraps(func) | |
3770 def wrapper(*args, **kwargs): | |
3771 result = func(*args, **kwargs) | |
3772 if isinstance(result, (dict, DictMixin)): | |
3773 tplvars = defaults.copy() | |
3774 tplvars.update(result) | |
3775 return template(tpl_name, **tplvars) | |
3776 elif result is None: | |
3777 return template(tpl_name, defaults) | |
3778 return result | |
3779 | |
3780 return wrapper | |
3781 | |
3782 return decorator | |
3783 | |
3784 | |
3785 mako_view = functools.partial(view, template_adapter=MakoTemplate) | |
3786 cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) | |
3787 jinja2_view = functools.partial(view, template_adapter=Jinja2Template) | |
3788 | |
3789 ############################################################################### | |
3790 # Constants and Globals ######################################################## | |
3791 ############################################################################### | |
3792 | |
3793 TEMPLATE_PATH = ['./', './views/'] | |
3794 TEMPLATES = {} | |
3795 DEBUG = False | |
3796 NORUN = False # If set, run() does nothing. Used by load_app() | |
3797 | |
3798 #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') | |
3799 HTTP_CODES = httplib.responses.copy() | |
3800 HTTP_CODES[418] = "I'm a teapot" # RFC 2324 | |
3801 HTTP_CODES[428] = "Precondition Required" | |
3802 HTTP_CODES[429] = "Too Many Requests" | |
3803 HTTP_CODES[431] = "Request Header Fields Too Large" | |
3804 HTTP_CODES[511] = "Network Authentication Required" | |
3805 _HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) | |
3806 for (k, v) in HTTP_CODES.items()) | |
3807 | |
3808 #: The default template used for error pages. Override with @error() | |
3809 ERROR_PAGE_TEMPLATE = """ | |
3810 %%try: | |
3811 %%from %s import DEBUG, request | |
3812 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> | |
3813 <html> | |
3814 <head> | |
3815 <title>Error: {{e.status}}</title> | |
3816 <style type="text/css"> | |
3817 html {background-color: #eee; font-family: sans-serif;} | |
3818 body {background-color: #fff; border: 1px solid #ddd; | |
3819 padding: 15px; margin: 15px;} | |
3820 pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;} | |
3821 </style> | |
3822 </head> | |
3823 <body> | |
3824 <h1>Error: {{e.status}}</h1> | |
3825 <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> | |
3826 caused an error:</p> | |
3827 <pre>{{e.body}}</pre> | |
3828 %%if DEBUG and e.exception: | |
3829 <h2>Exception:</h2> | |
3830 <pre>{{repr(e.exception)}}</pre> | |
3831 %%end | |
3832 %%if DEBUG and e.traceback: | |
3833 <h2>Traceback:</h2> | |
3834 <pre>{{e.traceback}}</pre> | |
3835 %%end | |
3836 </body> | |
3837 </html> | |
3838 %%except ImportError: | |
3839 <b>ImportError:</b> Could not generate the error page. Please add bottle to | |
3840 the import path. | |
3841 %%end | |
3842 """ % __name__ | |
3843 | |
3844 #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a | |
3845 #: request callback, this instance always refers to the *current* request | |
3846 #: (even on a multithreaded server). | |
3847 request = LocalRequest() | |
3848 | |
3849 #: A thread-safe instance of :class:`LocalResponse`. It is used to change the | |
3850 #: HTTP response for the *current* request. | |
3851 response = LocalResponse() | |
3852 | |
3853 #: A thread-safe namespace. Not used by Bottle. | |
3854 local = threading.local() | |
3855 | |
3856 # Initialize app stack (create first empty Bottle app) | |
3857 # BC: 0.6.4 and needed for run() | |
3858 app = default_app = AppStack() | |
3859 app.push() | |
3860 | |
3861 #: A virtual package that redirects import statements. | |
3862 #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. | |
3863 ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else | |
3864 __name__ + ".ext", 'bottle_%s').module | |
3865 | |
3866 if __name__ == '__main__': | |
3867 opt, args, parser = _cmd_options, _cmd_args, _cmd_parser | |
3868 if opt.version: | |
3869 _stdout('Bottle %s\n' % __version__) | |
3870 sys.exit(0) | |
3871 if not args: | |
3872 parser.print_help() | |
3873 _stderr('\nError: No application entry point specified.\n') | |
3874 sys.exit(1) | |
3875 | |
3876 sys.path.insert(0, '.') | |
3877 sys.modules.setdefault('bottle', sys.modules['__main__']) | |
3878 | |
3879 host, port = (opt.bind or 'localhost'), 8080 | |
3880 if ':' in host and host.rfind(']') < host.rfind(':'): | |
3881 host, port = host.rsplit(':', 1) | |
3882 host = host.strip('[]') | |
3883 | |
3884 run(args[0], | |
3885 host=host, | |
3886 port=int(port), | |
3887 server=opt.server, | |
3888 reloader=opt.reload, | |
3889 plugins=opt.plugin, | |
3890 debug=opt.debug) | |
3891 | |
3892 # THE END |