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