Mercurial > templog
comparison web/bottle.py @ 248:317a1738f15c
Update to bottle 0.12.8
author | Matt Johnston <matt@ucc.asn.au> |
---|---|
date | Wed, 27 May 2015 22:17:39 +0800 |
parents | dbbd503119ba |
children |
comparison
equal
deleted
inserted
replaced
247:a19496c95be5 | 248:317a1738f15c |
---|---|
7 template engines - all in a single file and with no dependencies other than the | 7 template engines - all in a single file and with no dependencies other than the |
8 Python Standard Library. | 8 Python Standard Library. |
9 | 9 |
10 Homepage and documentation: http://bottlepy.org/ | 10 Homepage and documentation: http://bottlepy.org/ |
11 | 11 |
12 Copyright (c) 2011, Marcel Hellkamp. | 12 Copyright (c) 2013, Marcel Hellkamp. |
13 License: MIT (see LICENSE for details) | 13 License: MIT (see LICENSE for details) |
14 """ | 14 """ |
15 | 15 |
16 from __future__ import with_statement | 16 from __future__ import with_statement |
17 | 17 |
18 __author__ = 'Marcel Hellkamp' | 18 __author__ = 'Marcel Hellkamp' |
19 __version__ = '0.11.dev' | 19 __version__ = '0.12.8' |
20 __license__ = 'MIT' | 20 __license__ = 'MIT' |
21 | 21 |
22 # The gevent server adapter needs to patch some modules before they are imported | 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 | 23 # This is why we parse the commandline parameters here but handle them later |
24 if __name__ == '__main__': | 24 if __name__ == '__main__': |
34 _cmd_options, _cmd_args = _cmd_parser.parse_args() | 34 _cmd_options, _cmd_args = _cmd_parser.parse_args() |
35 if _cmd_options.server and _cmd_options.server.startswith('gevent'): | 35 if _cmd_options.server and _cmd_options.server.startswith('gevent'): |
36 import gevent.monkey; gevent.monkey.patch_all() | 36 import gevent.monkey; gevent.monkey.patch_all() |
37 | 37 |
38 import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ | 38 import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ |
39 os, re, subprocess, sys, tempfile, threading, time, urllib, warnings | 39 os, re, subprocess, sys, tempfile, threading, time, warnings |
40 | 40 |
41 from datetime import date as datedate, datetime, timedelta | 41 from datetime import date as datedate, datetime, timedelta |
42 from tempfile import TemporaryFile | 42 from tempfile import TemporaryFile |
43 from traceback import format_exc, print_exc | 43 from traceback import format_exc, print_exc |
44 | 44 from inspect import getargspec |
45 try: from json import dumps as json_dumps, loads as json_lds | 45 from unicodedata import normalize |
46 | |
47 | |
48 try: from simplejson import dumps as json_dumps, loads as json_lds | |
46 except ImportError: # pragma: no cover | 49 except ImportError: # pragma: no cover |
47 try: from simplejson import dumps as json_dumps, loads as json_lds | 50 try: from json import dumps as json_dumps, loads as json_lds |
48 except ImportError: | 51 except ImportError: |
49 try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds | 52 try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds |
50 except ImportError: | 53 except ImportError: |
51 def json_dumps(data): | 54 def json_dumps(data): |
52 raise ImportError("JSON support requires Python 2.6 or simplejson.") | 55 raise ImportError("JSON support requires Python 2.6 or simplejson.") |
56 | 59 |
57 # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. | 60 # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. |
58 # It ain't pretty but it works... Sorry for the mess. | 61 # It ain't pretty but it works... Sorry for the mess. |
59 | 62 |
60 py = sys.version_info | 63 py = sys.version_info |
61 py3k = py >= (3,0,0) | 64 py3k = py >= (3, 0, 0) |
62 py25 = py < (2,6,0) | 65 py25 = py < (2, 6, 0) |
66 py31 = (3, 1, 0) <= py < (3, 2, 0) | |
63 | 67 |
64 # Workaround for the missing "as" keyword in py3k. | 68 # Workaround for the missing "as" keyword in py3k. |
65 def _e(): return sys.exc_info()[1] | 69 def _e(): return sys.exc_info()[1] |
66 | 70 |
67 # Workaround for the "print is a keyword/function" Python 2/3 dilemma | 71 # Workaround for the "print is a keyword/function" Python 2/3 dilemma |
74 | 78 |
75 # Lots of stdlib and builtin differences. | 79 # Lots of stdlib and builtin differences. |
76 if py3k: | 80 if py3k: |
77 import http.client as httplib | 81 import http.client as httplib |
78 import _thread as thread | 82 import _thread as thread |
79 from urllib.parse import urljoin, parse_qsl, SplitResult as UrlSplitResult | 83 from urllib.parse import urljoin, SplitResult as UrlSplitResult |
80 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote | 84 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote |
85 urlunquote = functools.partial(urlunquote, encoding='latin1') | |
81 from http.cookies import SimpleCookie | 86 from http.cookies import SimpleCookie |
82 from collections import MutableMapping as DictMixin | 87 from collections import MutableMapping as DictMixin |
83 import pickle | 88 import pickle |
84 from io import BytesIO | 89 from io import BytesIO |
90 from configparser import ConfigParser | |
85 basestring = str | 91 basestring = str |
86 unicode = str | 92 unicode = str |
87 json_loads = lambda s: json_lds(touni(s)) | 93 json_loads = lambda s: json_lds(touni(s)) |
88 callable = lambda x: hasattr(x, '__call__') | 94 callable = lambda x: hasattr(x, '__call__') |
89 imap = map | 95 imap = map |
96 def _raise(*a): raise a[0](a[1]).with_traceback(a[2]) | |
90 else: # 2.x | 97 else: # 2.x |
91 import httplib | 98 import httplib |
92 import thread | 99 import thread |
93 from urlparse import urljoin, SplitResult as UrlSplitResult | 100 from urlparse import urljoin, SplitResult as UrlSplitResult |
94 from urllib import urlencode, quote as urlquote, unquote as urlunquote | 101 from urllib import urlencode, quote as urlquote, unquote as urlunquote |
95 from Cookie import SimpleCookie | 102 from Cookie import SimpleCookie |
96 from itertools import imap | 103 from itertools import imap |
97 import cPickle as pickle | 104 import cPickle as pickle |
98 from StringIO import StringIO as BytesIO | 105 from StringIO import StringIO as BytesIO |
106 from ConfigParser import SafeConfigParser as ConfigParser | |
99 if py25: | 107 if py25: |
100 msg = "Python 2.5 support may be dropped in future versions of Bottle." | 108 msg = "Python 2.5 support may be dropped in future versions of Bottle." |
101 warnings.warn(msg, DeprecationWarning) | 109 warnings.warn(msg, DeprecationWarning) |
102 from cgi import parse_qsl | |
103 from UserDict import DictMixin | 110 from UserDict import DictMixin |
104 def next(it): return it.next() | 111 def next(it): return it.next() |
105 bytes = str | 112 bytes = str |
106 else: # 2.6, 2.7 | 113 else: # 2.6, 2.7 |
107 from urlparse import parse_qsl | |
108 from collections import MutableMapping as DictMixin | 114 from collections import MutableMapping as DictMixin |
115 unicode = unicode | |
109 json_loads = json_lds | 116 json_loads = json_lds |
117 eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec')) | |
110 | 118 |
111 # Some helpers for string/byte handling | 119 # Some helpers for string/byte handling |
112 def tob(s, enc='utf8'): | 120 def tob(s, enc='utf8'): |
113 return s.encode(enc) if isinstance(s, unicode) else bytes(s) | 121 return s.encode(enc) if isinstance(s, unicode) else bytes(s) |
114 def touni(s, enc='utf8', err='strict'): | 122 def touni(s, enc='utf8', err='strict'): |
115 return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) | 123 return s.decode(enc, err) if isinstance(s, bytes) else unicode(s) |
116 tonat = touni if py3k else tob | 124 tonat = touni if py3k else tob |
117 | 125 |
118 # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). | 126 # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). |
119 # 3.1 needs a workaround. | 127 # 3.1 needs a workaround. |
120 NCTextIOWrapper = None | 128 if py31: |
121 if (3,0,0) < py < (3,2,0): | |
122 from io import TextIOWrapper | 129 from io import TextIOWrapper |
123 class NCTextIOWrapper(TextIOWrapper): | 130 class NCTextIOWrapper(TextIOWrapper): |
124 def close(self): pass # Keep wrapped buffer open. | 131 def close(self): pass # Keep wrapped buffer open. |
132 | |
125 | 133 |
126 # A bug in functools causes it to break if the wrapper is an instance method | 134 # A bug in functools causes it to break if the wrapper is an instance method |
127 def update_wrapper(wrapper, wrapped, *a, **ka): | 135 def update_wrapper(wrapper, wrapped, *a, **ka): |
128 try: functools.update_wrapper(wrapper, wrapped, *a, **ka) | 136 try: functools.update_wrapper(wrapper, wrapped, *a, **ka) |
129 except AttributeError: pass | 137 except AttributeError: pass |
131 | 139 |
132 | 140 |
133 # These helpers are used at module level and need to be defined first. | 141 # These helpers are used at module level and need to be defined first. |
134 # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. | 142 # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. |
135 | 143 |
136 def depr(message): | 144 def depr(message, hard=False): |
137 warnings.warn(message, DeprecationWarning, stacklevel=3) | 145 warnings.warn(message, DeprecationWarning, stacklevel=3) |
138 | 146 |
139 def makelist(data): # This is just to handy | 147 def makelist(data): # This is just to handy |
140 if isinstance(data, (tuple, list, set, dict)): return list(data) | 148 if isinstance(data, (tuple, list, set, dict)): return list(data) |
141 elif data: return [data] | 149 elif data: return [data] |
171 ''' A property that is only computed once per instance and then replaces | 179 ''' A property that is only computed once per instance and then replaces |
172 itself with an ordinary attribute. Deleting the attribute resets the | 180 itself with an ordinary attribute. Deleting the attribute resets the |
173 property. ''' | 181 property. ''' |
174 | 182 |
175 def __init__(self, func): | 183 def __init__(self, func): |
184 self.__doc__ = getattr(func, '__doc__') | |
176 self.func = func | 185 self.func = func |
177 | 186 |
178 def __get__(self, obj, cls): | 187 def __get__(self, obj, cls): |
179 if obj is None: return self | 188 if obj is None: return self |
180 value = obj.__dict__[self.func.__name__] = self.func(obj) | 189 value = obj.__dict__[self.func.__name__] = self.func(obj) |
205 class BottleException(Exception): | 214 class BottleException(Exception): |
206 """ A base class for exceptions used by bottle. """ | 215 """ A base class for exceptions used by bottle. """ |
207 pass | 216 pass |
208 | 217 |
209 | 218 |
210 #TODO: This should subclass BaseRequest | |
211 class HTTPResponse(BottleException): | |
212 """ Used to break execution and immediately finish the response """ | |
213 def __init__(self, output='', status=200, header=None): | |
214 super(BottleException, self).__init__("HTTP Response %d" % status) | |
215 self.status = int(status) | |
216 self.output = output | |
217 self.headers = HeaderDict(header) if header else None | |
218 | |
219 def apply(self, response): | |
220 if self.headers: | |
221 for key, value in self.headers.allitems(): | |
222 response.headers[key] = value | |
223 response.status = self.status | |
224 | |
225 | |
226 class HTTPError(HTTPResponse): | |
227 """ Used to generate an error page """ | |
228 def __init__(self, code=500, output='Unknown Error', exception=None, | |
229 traceback=None, header=None): | |
230 super(HTTPError, self).__init__(output, code, header) | |
231 self.exception = exception | |
232 self.traceback = traceback | |
233 | |
234 def __repr__(self): | |
235 return tonat(template(ERROR_PAGE_TEMPLATE, e=self)) | |
236 | |
237 | |
238 | 219 |
239 | 220 |
240 | 221 |
241 | 222 |
242 ############################################################################### | 223 ############################################################################### |
254 | 235 |
255 class RouterUnknownModeError(RouteError): pass | 236 class RouterUnknownModeError(RouteError): pass |
256 | 237 |
257 | 238 |
258 class RouteSyntaxError(RouteError): | 239 class RouteSyntaxError(RouteError): |
259 """ The route parser found something not supported by this router """ | 240 """ The route parser found something not supported by this router. """ |
260 | 241 |
261 | 242 |
262 class RouteBuildError(RouteError): | 243 class RouteBuildError(RouteError): |
263 """ The route could not been built """ | 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) | |
264 | 253 |
265 | 254 |
266 class Router(object): | 255 class Router(object): |
267 ''' A Router is an ordered collection of route->target pairs. It is used to | 256 ''' A Router is an ordered collection of route->target pairs. It is used to |
268 efficiently match WSGI requests against a number of routes and return | 257 efficiently match WSGI requests against a number of routes and return |
274 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax | 263 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax |
275 and details on the matching order are described in docs:`routing`. | 264 and details on the matching order are described in docs:`routing`. |
276 ''' | 265 ''' |
277 | 266 |
278 default_pattern = '[^/]+' | 267 default_pattern = '[^/]+' |
279 default_filter = 're' | 268 default_filter = 're' |
280 #: Sorry for the mess. It works. Trust me. | 269 |
281 rule_syntax = re.compile('(\\\\*)'\ | 270 #: The current CPython regexp implementation does not allow more |
282 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ | 271 #: than 99 matching groups per regular expression. |
283 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ | 272 _MAX_GROUPS_PER_PATTERN = 99 |
284 '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') | |
285 | 273 |
286 def __init__(self, strict=False): | 274 def __init__(self, strict=False): |
287 self.rules = {} # A {rule: Rule} mapping | 275 self.rules = [] # All rules in order |
288 self.builder = {} # A rule/name->build_info mapping | 276 self._groups = {} # index of regexes to find them in dyna_routes |
289 self.static = {} # Cache for static routes: {path: {method: target}} | 277 self.builder = {} # Data structure for the url builder |
290 self.dynamic = [] # Cache for dynamic routes. See _compile() | 278 self.static = {} # Search structure for static routes |
279 self.dyna_routes = {} | |
280 self.dyna_regexes = {} # Search structure for dynamic routes | |
291 #: If true, static routes are no longer checked first. | 281 #: If true, static routes are no longer checked first. |
292 self.strict_order = strict | 282 self.strict_order = strict |
293 self.filters = {'re': self.re_filter, 'int': self.int_filter, | 283 self.filters = { |
294 'float': self.float_filter, 'path': self.path_filter} | 284 're': lambda conf: |
295 | 285 (_re_flatten(conf or self.default_pattern), None, None), |
296 def re_filter(self, conf): | 286 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), |
297 return conf or self.default_pattern, None, None | 287 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), |
298 | 288 'path': lambda conf: (r'.+?', None, None)} |
299 def int_filter(self, conf): | |
300 return r'-?\d+', int, lambda x: str(int(x)) | |
301 | |
302 def float_filter(self, conf): | |
303 return r'-?[\d.]+', float, lambda x: str(float(x)) | |
304 | |
305 def path_filter(self, conf): | |
306 return r'.+?', None, None | |
307 | 289 |
308 def add_filter(self, name, func): | 290 def add_filter(self, name, func): |
309 ''' Add a filter. The provided function is called with the configuration | 291 ''' Add a filter. The provided function is called with the configuration |
310 string as parameter and must return a (regexp, to_python, to_url) tuple. | 292 string as parameter and must return a (regexp, to_python, to_url) tuple. |
311 The first element is a string, the last two are callables or None. ''' | 293 The first element is a string, the last two are callables or None. ''' |
312 self.filters[name] = func | 294 self.filters[name] = func |
313 | 295 |
314 def parse_rule(self, rule): | 296 rule_syntax = re.compile('(\\\\*)'\ |
315 ''' Parses a rule into a (name, filter, conf) token stream. If mode is | 297 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ |
316 None, name contains a static rule part. ''' | 298 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ |
299 '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') | |
300 | |
301 def _itertokens(self, rule): | |
317 offset, prefix = 0, '' | 302 offset, prefix = 0, '' |
318 for match in self.rule_syntax.finditer(rule): | 303 for match in self.rule_syntax.finditer(rule): |
319 prefix += rule[offset:match.start()] | 304 prefix += rule[offset:match.start()] |
320 g = match.groups() | 305 g = match.groups() |
321 if len(g[0])%2: # Escaped wildcard | 306 if len(g[0])%2: # Escaped wildcard |
322 prefix += match.group(0)[len(g[0]):] | 307 prefix += match.group(0)[len(g[0]):] |
323 offset = match.end() | 308 offset = match.end() |
324 continue | 309 continue |
325 if prefix: yield prefix, None, None | 310 if prefix: |
326 name, filtr, conf = g[1:4] if not g[2] is None else g[4:7] | 311 yield prefix, None, None |
327 if not filtr: filtr = self.default_filter | 312 name, filtr, conf = g[4:7] if g[2] is None else g[1:4] |
328 yield name, filtr, conf or None | 313 yield name, filtr or 'default', conf or None |
329 offset, prefix = match.end(), '' | 314 offset, prefix = match.end(), '' |
330 if offset <= len(rule) or prefix: | 315 if offset <= len(rule) or prefix: |
331 yield prefix+rule[offset:], None, None | 316 yield prefix+rule[offset:], None, None |
332 | 317 |
333 def add(self, rule, method, target, name=None): | 318 def add(self, rule, method, target, name=None): |
334 ''' Add a new route or replace the target for an existing route. ''' | 319 ''' Add a new rule or replace the target for an existing rule. ''' |
335 if rule in self.rules: | 320 anons = 0 # Number of anonymous wildcards found |
336 self.rules[rule][method] = target | 321 keys = [] # Names of keys |
337 if name: self.builder[name] = self.builder[rule] | 322 pattern = '' # Regular expression pattern with named groups |
338 return | 323 filters = [] # Lists of wildcard input filters |
339 | 324 builder = [] # Data structure for the URL builder |
340 target = self.rules[rule] = {method: target} | |
341 | |
342 # Build pattern and other structures for dynamic routes | |
343 anons = 0 # Number of anonymous wildcards | |
344 pattern = '' # Regular expression pattern | |
345 filters = [] # Lists of wildcard input filters | |
346 builder = [] # Data structure for the URL builder | |
347 is_static = True | 325 is_static = True |
348 for key, mode, conf in self.parse_rule(rule): | 326 |
327 for key, mode, conf in self._itertokens(rule): | |
349 if mode: | 328 if mode: |
350 is_static = False | 329 is_static = False |
330 if mode == 'default': mode = self.default_filter | |
351 mask, in_filter, out_filter = self.filters[mode](conf) | 331 mask, in_filter, out_filter = self.filters[mode](conf) |
352 if key: | 332 if not key: |
333 pattern += '(?:%s)' % mask | |
334 key = 'anon%d' % anons | |
335 anons += 1 | |
336 else: | |
353 pattern += '(?P<%s>%s)' % (key, mask) | 337 pattern += '(?P<%s>%s)' % (key, mask) |
354 else: | 338 keys.append(key) |
355 pattern += '(?:%s)' % mask | |
356 key = 'anon%d' % anons; anons += 1 | |
357 if in_filter: filters.append((key, in_filter)) | 339 if in_filter: filters.append((key, in_filter)) |
358 builder.append((key, out_filter or str)) | 340 builder.append((key, out_filter or str)) |
359 elif key: | 341 elif key: |
360 pattern += re.escape(key) | 342 pattern += re.escape(key) |
361 builder.append((None, key)) | 343 builder.append((None, key)) |
344 | |
362 self.builder[rule] = builder | 345 self.builder[rule] = builder |
363 if name: self.builder[name] = builder | 346 if name: self.builder[name] = builder |
364 | 347 |
365 if is_static and not self.strict_order: | 348 if is_static and not self.strict_order: |
366 self.static[self.build(rule)] = target | 349 self.static.setdefault(method, {}) |
350 self.static[method][self.build(rule)] = (target, None) | |
367 return | 351 return |
368 | 352 |
369 def fpat_sub(m): | |
370 return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' | |
371 flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern) | |
372 | |
373 try: | 353 try: |
374 re_match = re.compile('^(%s)$' % pattern).match | 354 re_pattern = re.compile('^(%s)$' % pattern) |
355 re_match = re_pattern.match | |
375 except re.error: | 356 except re.error: |
376 raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) | 357 raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) |
377 | 358 |
378 def match(path): | 359 if filters: |
379 """ Return an url-argument dictionary. """ | 360 def getargs(path): |
380 url_args = re_match(path).groupdict() | 361 url_args = re_match(path).groupdict() |
381 for name, wildcard_filter in filters: | 362 for name, wildcard_filter in filters: |
382 try: | 363 try: |
383 url_args[name] = wildcard_filter(url_args[name]) | 364 url_args[name] = wildcard_filter(url_args[name]) |
384 except ValueError: | 365 except ValueError: |
385 raise HTTPError(400, 'Path has wrong format.') | 366 raise HTTPError(400, 'Path has wrong format.') |
386 return url_args | 367 return url_args |
387 | 368 elif re_pattern.groupindex: |
388 try: | 369 def getargs(path): |
389 combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern) | 370 return re_match(path).groupdict() |
390 self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) | 371 else: |
391 self.dynamic[-1][1].append((match, target)) | 372 getargs = None |
392 except (AssertionError, IndexError): # AssertionError: Too many groups | 373 |
393 self.dynamic.append((re.compile('(^%s$)' % flat_pattern), | 374 flatpat = _re_flatten(pattern) |
394 [(match, target)])) | 375 whole_rule = (rule, flatpat, target, getargs) |
395 return match | 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)) | |
396 | 399 |
397 def build(self, _name, *anons, **query): | 400 def build(self, _name, *anons, **query): |
398 ''' Build an URL by filling the wildcards in a rule. ''' | 401 ''' Build an URL by filling the wildcards in a rule. ''' |
399 builder = self.builder.get(_name) | 402 builder = self.builder.get(_name) |
400 if not builder: raise RouteBuildError("No route with that name.", _name) | 403 if not builder: raise RouteBuildError("No route with that name.", _name) |
405 except KeyError: | 408 except KeyError: |
406 raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) | 409 raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) |
407 | 410 |
408 def match(self, environ): | 411 def match(self, environ): |
409 ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' | 412 ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' |
410 path, targets, urlargs = environ['PATH_INFO'] or '/', None, {} | 413 verb = environ['REQUEST_METHOD'].upper() |
411 if path in self.static: | 414 path = environ['PATH_INFO'] or '/' |
412 targets = self.static[path] | 415 target = None |
416 if verb == 'HEAD': | |
417 methods = ['PROXY', verb, 'GET', 'ANY'] | |
413 else: | 418 else: |
414 for combined, rules in self.dynamic: | 419 methods = ['PROXY', verb, 'ANY'] |
415 match = combined.match(path) | 420 |
416 if not match: continue | 421 for method in methods: |
417 getargs, targets = rules[match.lastindex - 1] | 422 if method in self.static and path in self.static[method]: |
418 urlargs = getargs(path) if getargs else {} | 423 target, getargs = self.static[method][path] |
419 break | 424 return target, getargs(path) if getargs else {} |
420 | 425 elif method in self.dyna_regexes: |
421 if not targets: | 426 for combined, rules in self.dyna_regexes[method]: |
422 raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO'])) | 427 match = combined(path) |
423 method = environ['REQUEST_METHOD'].upper() | 428 if match: |
424 if method in targets: | 429 target, getargs = rules[match.lastindex - 1] |
425 return targets[method], urlargs | 430 return target, getargs(path) if getargs else {} |
426 if method == 'HEAD' and 'GET' in targets: | 431 |
427 return targets['GET'], urlargs | 432 # No matching route found. Collect alternative methods for 405 response |
428 if 'ANY' in targets: | 433 allowed = set([]) |
429 return targets['ANY'], urlargs | 434 nocheck = set(methods) |
430 allowed = [verb for verb in targets if verb != 'ANY'] | 435 for method in set(self.static) - nocheck: |
431 if 'GET' in allowed and 'HEAD' not in allowed: | 436 if path in self.static[method]: |
432 allowed.append('HEAD') | 437 allowed.add(verb) |
433 raise HTTPError(405, "Method not allowed.", | 438 for method in set(self.dyna_regexes) - allowed - nocheck: |
434 header=[('Allow',",".join(allowed))]) | 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 | |
435 | 453 |
436 | 454 |
437 class Route(object): | 455 class Route(object): |
438 ''' This class wraps a route callback along with route specific metadata and | 456 ''' This class wraps a route callback along with route specific metadata and |
439 configuration and applies Plugins on demand. It is also responsible for | 457 configuration and applies Plugins on demand. It is also responsible for |
457 #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). | 475 #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). |
458 self.skiplist = skiplist or [] | 476 self.skiplist = skiplist or [] |
459 #: Additional keyword arguments passed to the :meth:`Bottle.route` | 477 #: Additional keyword arguments passed to the :meth:`Bottle.route` |
460 #: decorator are stored in this dictionary. Used for route-specific | 478 #: decorator are stored in this dictionary. Used for route-specific |
461 #: plugin configuration and meta-data. | 479 #: plugin configuration and meta-data. |
462 self.config = ConfigDict(config) | 480 self.config = ConfigDict().load_dict(config, make_namespaces=True) |
463 | 481 |
464 def __call__(self, *a, **ka): | 482 def __call__(self, *a, **ka): |
465 depr("Some APIs changed to return Route() instances instead of"\ | 483 depr("Some APIs changed to return Route() instances instead of"\ |
466 " callables. Make sure to use the Route.call method and not to"\ | 484 " callables. Make sure to use the Route.call method and not to"\ |
467 " call Route instances directly.") | 485 " call Route instances directly.") #0.12 |
468 return self.call(*a, **ka) | 486 return self.call(*a, **ka) |
469 | 487 |
470 @cached_property | 488 @cached_property |
471 def call(self): | 489 def call(self): |
472 ''' The route callback with all plugins applied. This property is | 490 ''' The route callback with all plugins applied. This property is |
482 ''' Do all on-demand work immediately (useful for debugging).''' | 500 ''' Do all on-demand work immediately (useful for debugging).''' |
483 self.call | 501 self.call |
484 | 502 |
485 @property | 503 @property |
486 def _context(self): | 504 def _context(self): |
487 depr('Switch to Plugin API v2 and access the Route object directly.') | 505 depr('Switch to Plugin API v2 and access the Route object directly.') #0.12 |
488 return dict(rule=self.rule, method=self.method, callback=self.callback, | 506 return dict(rule=self.rule, method=self.method, callback=self.callback, |
489 name=self.name, app=self.app, config=self.config, | 507 name=self.name, app=self.app, config=self.config, |
490 apply=self.plugins, skip=self.skiplist) | 508 apply=self.plugins, skip=self.skiplist) |
491 | 509 |
492 def all_plugins(self): | 510 def all_plugins(self): |
514 return self._make_callback() | 532 return self._make_callback() |
515 if not callback is self.callback: | 533 if not callback is self.callback: |
516 update_wrapper(callback, self.callback) | 534 update_wrapper(callback, self.callback) |
517 return callback | 535 return callback |
518 | 536 |
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 | |
519 def __repr__(self): | 560 def __repr__(self): |
520 return '<%s %r %r>' % (self.method, self.rule, self.callback) | 561 cb = self.get_undecorated_callback() |
562 return '<%s %r %r>' % (self.method, self.rule, cb) | |
521 | 563 |
522 | 564 |
523 | 565 |
524 | 566 |
525 | 567 |
537 :param catchall: If true (default), handle all exceptions. Turn off to | 579 :param catchall: If true (default), handle all exceptions. Turn off to |
538 let debugging middleware handle exceptions. | 580 let debugging middleware handle exceptions. |
539 """ | 581 """ |
540 | 582 |
541 def __init__(self, catchall=True, autojson=True): | 583 def __init__(self, catchall=True, autojson=True): |
542 #: If true, most exceptions are caught and returned as :exc:`HTTPError` | 584 |
543 self.catchall = catchall | 585 #: A :class:`ConfigDict` for app specific configuration. |
544 | 586 self.config = ConfigDict() |
545 #: A :cls:`ResourceManager` for application files | 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 | |
546 self.resources = ResourceManager() | 594 self.resources = ResourceManager() |
547 | |
548 #: A :cls:`ConfigDict` for app specific configuration. | |
549 self.config = ConfigDict() | |
550 self.config.autojson = autojson | |
551 | 595 |
552 self.routes = [] # List of installed :class:`Route` instances. | 596 self.routes = [] # List of installed :class:`Route` instances. |
553 self.router = Router() # Maps requests to :class:`Route` instances. | 597 self.router = Router() # Maps requests to :class:`Route` instances. |
554 self.error_handler = {} | 598 self.error_handler = {} |
555 | 599 |
556 # Core plugins | 600 # Core plugins |
557 self.plugins = [] # List of installed plugins. | 601 self.plugins = [] # List of installed plugins. |
558 self.hooks = HooksPlugin() | 602 if self.config['autojson']: |
559 self.install(self.hooks) | |
560 if self.config.autojson: | |
561 self.install(JSONPlugin()) | 603 self.install(JSONPlugin()) |
562 self.install(TemplatePlugin()) | 604 self.install(TemplatePlugin()) |
563 | 605 |
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 | |
564 | 649 |
565 def mount(self, prefix, app, **options): | 650 def mount(self, prefix, app, **options): |
566 ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific | 651 ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific |
567 URL prefix. Example:: | 652 URL prefix. Example:: |
568 | 653 |
573 :param app: an instance of :class:`Bottle` or a WSGI application. | 658 :param app: an instance of :class:`Bottle` or a WSGI application. |
574 | 659 |
575 All other parameters are passed to the underlying :meth:`route` call. | 660 All other parameters are passed to the underlying :meth:`route` call. |
576 ''' | 661 ''' |
577 if isinstance(app, basestring): | 662 if isinstance(app, basestring): |
578 prefix, app = app, prefix | 663 depr('Parameter order of Bottle.mount() changed.', True) # 0.10 |
579 depr('Parameter order of Bottle.mount() changed.') # 0.10 | |
580 | 664 |
581 segments = [p for p in prefix.split('/') if p] | 665 segments = [p for p in prefix.split('/') if p] |
582 if not segments: raise ValueError('Empty path prefix.') | 666 if not segments: raise ValueError('Empty path prefix.') |
583 path_depth = len(segments) | 667 path_depth = len(segments) |
584 | 668 |
585 def mountpoint_wrapper(): | 669 def mountpoint_wrapper(): |
586 try: | 670 try: |
587 request.path_shift(path_depth) | 671 request.path_shift(path_depth) |
588 rs = BaseResponse([], 200) | 672 rs = HTTPResponse([]) |
589 def start_response(status, header): | 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 | |
590 rs.status = status | 679 rs.status = status |
591 for name, value in header: rs.add_header(name, value) | 680 for name, value in headerlist: rs.add_header(name, value) |
592 return rs.body.append | 681 return rs.body.append |
593 body = app(request.environ, start_response) | 682 body = app(request.environ, start_response) |
594 body = itertools.chain(rs.body, body) | 683 if body and rs.body: body = itertools.chain(rs.body, body) |
595 return HTTPResponse(body, rs.status_code, rs.headers) | 684 rs.body = body or rs.body |
685 return rs | |
596 finally: | 686 finally: |
597 request.path_shift(-path_depth) | 687 request.path_shift(-path_depth) |
598 | 688 |
599 options.setdefault('skip', True) | 689 options.setdefault('skip', True) |
600 options.setdefault('method', 'ANY') | 690 options.setdefault('method', 'PROXY') |
601 options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) | 691 options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) |
602 options['callback'] = mountpoint_wrapper | 692 options['callback'] = mountpoint_wrapper |
603 | 693 |
604 self.route('/%s/<:re:.*>' % '/'.join(segments), **options) | 694 self.route('/%s/<:re:.*>' % '/'.join(segments), **options) |
605 if not prefix.endswith('/'): | 695 if not prefix.endswith('/'): |
640 del self.plugins[i] | 730 del self.plugins[i] |
641 if hasattr(plugin, 'close'): plugin.close() | 731 if hasattr(plugin, 'close'): plugin.close() |
642 if removed: self.reset() | 732 if removed: self.reset() |
643 return removed | 733 return removed |
644 | 734 |
645 def run(self, **kwargs): | |
646 ''' Calls :func:`run` with the same parameters. ''' | |
647 run(self, **kwargs) | |
648 | |
649 def reset(self, route=None): | 735 def reset(self, route=None): |
650 ''' Reset all routes (force plugins to be re-applied) and clear all | 736 ''' Reset all routes (force plugins to be re-applied) and clear all |
651 caches. If an ID or route object is given, only that specific route | 737 caches. If an ID or route object is given, only that specific route |
652 is affected. ''' | 738 is affected. ''' |
653 if route is None: routes = self.routes | 739 if route is None: routes = self.routes |
654 elif isinstance(route, Route): routes = [route] | 740 elif isinstance(route, Route): routes = [route] |
655 else: routes = [self.routes[route]] | 741 else: routes = [self.routes[route]] |
656 for route in routes: route.reset() | 742 for route in routes: route.reset() |
657 if DEBUG: | 743 if DEBUG: |
658 for route in routes: route.prepare() | 744 for route in routes: route.prepare() |
659 self.hooks.trigger('app_reset') | 745 self.trigger_hook('app_reset') |
660 | 746 |
661 def close(self): | 747 def close(self): |
662 ''' Close the application and all installed plugins. ''' | 748 ''' Close the application and all installed plugins. ''' |
663 for plugin in self.plugins: | 749 for plugin in self.plugins: |
664 if hasattr(plugin, 'close'): plugin.close() | 750 if hasattr(plugin, 'close'): plugin.close() |
665 self.stopped = True | 751 self.stopped = True |
752 | |
753 def run(self, **kwargs): | |
754 ''' Calls :func:`run` with the same parameters. ''' | |
755 run(self, **kwargs) | |
666 | 756 |
667 def match(self, environ): | 757 def match(self, environ): |
668 """ Search for a matching route and return a (:class:`Route` , urlargs) | 758 """ Search for a matching route and return a (:class:`Route` , urlargs) |
669 tuple. The second value is a dictionary with parameters extracted | 759 tuple. The second value is a dictionary with parameters extracted |
670 from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" | 760 from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" |
746 def wrapper(handler): | 836 def wrapper(handler): |
747 self.error_handler[int(code)] = handler | 837 self.error_handler[int(code)] = handler |
748 return handler | 838 return handler |
749 return wrapper | 839 return wrapper |
750 | 840 |
751 def hook(self, name): | 841 def default_error_handler(self, res): |
752 """ Return a decorator that attaches a callback to a hook. Three hooks | 842 return tob(template(ERROR_PAGE_TEMPLATE, e=res)) |
753 are currently implemented: | |
754 | |
755 - before_request: Executed once before each request | |
756 - after_request: Executed once after each request | |
757 - app_reset: Called whenever :meth:`reset` is called. | |
758 """ | |
759 def wrapper(func): | |
760 self.hooks.add(name, func) | |
761 return func | |
762 return wrapper | |
763 | |
764 def handle(self, path, method='GET'): | |
765 """ (deprecated) Execute the first matching route callback and return | |
766 the result. :exc:`HTTPResponse` exceptions are caught and returned. | |
767 If :attr:`Bottle.catchall` is true, other exceptions are caught as | |
768 well and returned as :exc:`HTTPError` instances (500). | |
769 """ | |
770 depr("This method will change semantics in 0.10. Try to avoid it.") | |
771 if isinstance(path, dict): | |
772 return self._handle(path) | |
773 return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) | |
774 | 843 |
775 def _handle(self, environ): | 844 def _handle(self, environ): |
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 | |
776 try: | 852 try: |
777 environ['bottle.app'] = self | 853 environ['bottle.app'] = self |
778 request.bind(environ) | 854 request.bind(environ) |
779 response.bind() | 855 response.bind() |
780 route, args = self.router.match(environ) | 856 try: |
781 environ['route.handle'] = environ['bottle.route'] = route | 857 self.trigger_hook('before_request') |
782 environ['route.url_args'] = args | 858 route, args = self.router.match(environ) |
783 return route.call(**args) | 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 | |
784 except HTTPResponse: | 866 except HTTPResponse: |
785 return _e() | 867 return _e() |
786 except RouteReset: | 868 except RouteReset: |
787 route.reset() | 869 route.reset() |
788 return self._handle(environ) | 870 return self._handle(environ) |
801 iterable of strings and iterable of unicodes | 883 iterable of strings and iterable of unicodes |
802 """ | 884 """ |
803 | 885 |
804 # Empty output is done here | 886 # Empty output is done here |
805 if not out: | 887 if not out: |
806 response['Content-Length'] = 0 | 888 if 'Content-Length' not in response: |
889 response['Content-Length'] = 0 | |
807 return [] | 890 return [] |
808 # Join lists of byte or unicode strings. Mixed lists are NOT supported | 891 # Join lists of byte or unicode strings. Mixed lists are NOT supported |
809 if isinstance(out, (tuple, list))\ | 892 if isinstance(out, (tuple, list))\ |
810 and isinstance(out[0], (bytes, unicode)): | 893 and isinstance(out[0], (bytes, unicode)): |
811 out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' | 894 out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' |
812 # Encode unicode strings | 895 # Encode unicode strings |
813 if isinstance(out, unicode): | 896 if isinstance(out, unicode): |
814 out = out.encode(response.charset) | 897 out = out.encode(response.charset) |
815 # Byte Strings are just returned | 898 # Byte Strings are just returned |
816 if isinstance(out, bytes): | 899 if isinstance(out, bytes): |
817 response['Content-Length'] = len(out) | 900 if 'Content-Length' not in response: |
901 response['Content-Length'] = len(out) | |
818 return [out] | 902 return [out] |
819 # HTTPError or HTTPException (recursive, because they may wrap anything) | 903 # HTTPError or HTTPException (recursive, because they may wrap anything) |
820 # TODO: Handle these explicitly in handle() or make them iterable. | 904 # TODO: Handle these explicitly in handle() or make them iterable. |
821 if isinstance(out, HTTPError): | 905 if isinstance(out, HTTPError): |
822 out.apply(response) | 906 out.apply(response) |
823 out = self.error_handler.get(out.status, repr)(out) | 907 out = self.error_handler.get(out.status_code, self.default_error_handler)(out) |
824 if isinstance(out, HTTPResponse): | |
825 depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9 | |
826 return self._cast(out) | 908 return self._cast(out) |
827 if isinstance(out, HTTPResponse): | 909 if isinstance(out, HTTPResponse): |
828 out.apply(response) | 910 out.apply(response) |
829 return self._cast(out.output) | 911 return self._cast(out.body) |
830 | 912 |
831 # File-like objects. | 913 # File-like objects. |
832 if hasattr(out, 'read'): | 914 if hasattr(out, 'read'): |
833 if 'wsgi.file_wrapper' in request.environ: | 915 if 'wsgi.file_wrapper' in request.environ: |
834 return request.environ['wsgi.file_wrapper'](out) | 916 return request.environ['wsgi.file_wrapper'](out) |
835 elif hasattr(out, 'close') or not hasattr(out, '__iter__'): | 917 elif hasattr(out, 'close') or not hasattr(out, '__iter__'): |
836 return WSGIFileWrapper(out) | 918 return WSGIFileWrapper(out) |
837 | 919 |
838 # Handle Iterables. We peek into them to detect their inner type. | 920 # Handle Iterables. We peek into them to detect their inner type. |
839 try: | 921 try: |
840 out = iter(out) | 922 iout = iter(out) |
841 first = next(out) | 923 first = next(iout) |
842 while not first: | 924 while not first: |
843 first = next(out) | 925 first = next(iout) |
844 except StopIteration: | 926 except StopIteration: |
845 return self._cast('') | 927 return self._cast('') |
846 except HTTPResponse: | 928 except HTTPResponse: |
847 first = _e() | 929 first = _e() |
848 except (KeyboardInterrupt, SystemExit, MemoryError): | 930 except (KeyboardInterrupt, SystemExit, MemoryError): |
852 first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) | 934 first = HTTPError(500, 'Unhandled exception', _e(), format_exc()) |
853 | 935 |
854 # These are the inner types allowed in iterator or generator objects. | 936 # These are the inner types allowed in iterator or generator objects. |
855 if isinstance(first, HTTPResponse): | 937 if isinstance(first, HTTPResponse): |
856 return self._cast(first) | 938 return self._cast(first) |
857 if isinstance(first, bytes): | 939 elif isinstance(first, bytes): |
858 return itertools.chain([first], out) | 940 new_iter = itertools.chain([first], iout) |
859 if isinstance(first, unicode): | 941 elif isinstance(first, unicode): |
860 return imap(lambda x: x.encode(response.charset), | 942 encoder = lambda x: x.encode(response.charset) |
861 itertools.chain([first], out)) | 943 new_iter = imap(encoder, itertools.chain([first], iout)) |
862 return self._cast(HTTPError(500, 'Unsupported response type: %s'\ | 944 else: |
863 % type(first))) | 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 | |
864 | 950 |
865 def wsgi(self, environ, start_response): | 951 def wsgi(self, environ, start_response): |
866 """ The bottle WSGI-interface. """ | 952 """ The bottle WSGI-interface. """ |
867 try: | 953 try: |
868 out = self._cast(self._handle(environ)) | 954 out = self._cast(self._handle(environ)) |
869 # rfc2616 section 4.3 | 955 # rfc2616 section 4.3 |
870 if response._status_code in (100, 101, 204, 304)\ | 956 if response._status_code in (100, 101, 204, 304)\ |
871 or request.method == 'HEAD': | 957 or environ['REQUEST_METHOD'] == 'HEAD': |
872 if hasattr(out, 'close'): out.close() | 958 if hasattr(out, 'close'): out.close() |
873 out = [] | 959 out = [] |
874 if isinstance(response._status_line, unicode): | 960 start_response(response._status_line, response.headerlist) |
875 response._status_line = str(response._status_line) | |
876 start_response(response._status_line, list(response.iter_headers())) | |
877 return out | 961 return out |
878 except (KeyboardInterrupt, SystemExit, MemoryError): | 962 except (KeyboardInterrupt, SystemExit, MemoryError): |
879 raise | 963 raise |
880 except Exception: | 964 except Exception: |
881 if not self.catchall: raise | 965 if not self.catchall: raise |
885 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \ | 969 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \ |
886 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \ | 970 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \ |
887 % (html_escape(repr(_e())), html_escape(format_exc())) | 971 % (html_escape(repr(_e())), html_escape(format_exc())) |
888 environ['wsgi.errors'].write(err) | 972 environ['wsgi.errors'].write(err) |
889 headers = [('Content-Type', 'text/html; charset=UTF-8')] | 973 headers = [('Content-Type', 'text/html; charset=UTF-8')] |
890 start_response('500 INTERNAL SERVER ERROR', headers) | 974 start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) |
891 return [tob(err)] | 975 return [tob(err)] |
892 | 976 |
893 def __call__(self, environ, start_response): | 977 def __call__(self, environ, start_response): |
894 ''' Each instance of :class:'Bottle' is a WSGI application. ''' | 978 ''' Each instance of :class:'Bottle' is a WSGI application. ''' |
895 return self.wsgi(environ, start_response) | 979 return self.wsgi(environ, start_response) |
900 | 984 |
901 | 985 |
902 ############################################################################### | 986 ############################################################################### |
903 # HTTP and WSGI Tools ########################################################## | 987 # HTTP and WSGI Tools ########################################################## |
904 ############################################################################### | 988 ############################################################################### |
905 | |
906 | 989 |
907 class BaseRequest(object): | 990 class BaseRequest(object): |
908 """ A wrapper for WSGI environment dictionaries that adds a lot of | 991 """ A wrapper for WSGI environment dictionaries that adds a lot of |
909 convenient access methods and properties. Most of them are read-only. | 992 convenient access methods and properties. Most of them are read-only. |
910 | 993 |
915 | 998 |
916 __slots__ = ('environ') | 999 __slots__ = ('environ') |
917 | 1000 |
918 #: Maximum size of memory buffer for :attr:`body` in bytes. | 1001 #: Maximum size of memory buffer for :attr:`body` in bytes. |
919 MEMFILE_MAX = 102400 | 1002 MEMFILE_MAX = 102400 |
920 #: Maximum number pr GET or POST parameters per request | |
921 MAX_PARAMS = 100 | |
922 | 1003 |
923 def __init__(self, environ=None): | 1004 def __init__(self, environ=None): |
924 """ Wrap a WSGI environ dictionary. """ | 1005 """ Wrap a WSGI environ dictionary. """ |
925 #: The wrapped WSGI environ dictionary. This is the only real attribute. | 1006 #: The wrapped WSGI environ dictionary. This is the only real attribute. |
926 #: All other attributes actually are read-only properties. | 1007 #: All other attributes actually are read-only properties. |
930 @DictProperty('environ', 'bottle.app', read_only=True) | 1011 @DictProperty('environ', 'bottle.app', read_only=True) |
931 def app(self): | 1012 def app(self): |
932 ''' Bottle application handling this request. ''' | 1013 ''' Bottle application handling this request. ''' |
933 raise RuntimeError('This request is not connected to an application.') | 1014 raise RuntimeError('This request is not connected to an application.') |
934 | 1015 |
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 | |
935 @property | 1026 @property |
936 def path(self): | 1027 def path(self): |
937 ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix | 1028 ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix |
938 broken clients and avoid the "empty path" edge case). ''' | 1029 broken clients and avoid the "empty path" edge case). ''' |
939 return '/' + self.environ.get('PATH_INFO','').lstrip('/') | 1030 return '/' + self.environ.get('PATH_INFO','').lstrip('/') |
955 | 1046 |
956 @DictProperty('environ', 'bottle.request.cookies', read_only=True) | 1047 @DictProperty('environ', 'bottle.request.cookies', read_only=True) |
957 def cookies(self): | 1048 def cookies(self): |
958 """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT | 1049 """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT |
959 decoded. Use :meth:`get_cookie` if you expect signed cookies. """ | 1050 decoded. Use :meth:`get_cookie` if you expect signed cookies. """ |
960 cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')) | 1051 cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')).values() |
961 cookies = list(cookies.values())[:self.MAX_PARAMS] | |
962 return FormsDict((c.key, c.value) for c in cookies) | 1052 return FormsDict((c.key, c.value) for c in cookies) |
963 | 1053 |
964 def get_cookie(self, key, default=None, secret=None): | 1054 def get_cookie(self, key, default=None, secret=None): |
965 """ Return the content of a cookie. To read a `Signed Cookie`, the | 1055 """ Return the content of a cookie. To read a `Signed Cookie`, the |
966 `secret` must match the one used to create the cookie (see | 1056 `secret` must match the one used to create the cookie (see |
976 def query(self): | 1066 def query(self): |
977 ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These | 1067 ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These |
978 values are sometimes called "URL arguments" or "GET parameters", but | 1068 values are sometimes called "URL arguments" or "GET parameters", but |
979 not to be confused with "URL wildcards" as they are provided by the | 1069 not to be confused with "URL wildcards" as they are provided by the |
980 :class:`Router`. ''' | 1070 :class:`Router`. ''' |
981 pairs = parse_qsl(self.query_string, keep_blank_values=True) | |
982 get = self.environ['bottle.get'] = FormsDict() | 1071 get = self.environ['bottle.get'] = FormsDict() |
983 for key, value in pairs[:self.MAX_PARAMS]: | 1072 pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) |
1073 for key, value in pairs: | |
984 get[key] = value | 1074 get[key] = value |
985 return get | 1075 return get |
986 | 1076 |
987 @DictProperty('environ', 'bottle.request.forms', read_only=True) | 1077 @DictProperty('environ', 'bottle.request.forms', read_only=True) |
988 def forms(self): | 1078 def forms(self): |
989 """ Form values parsed from an `url-encoded` or `multipart/form-data` | 1079 """ Form values parsed from an `url-encoded` or `multipart/form-data` |
990 encoded POST or PUT request body. The result is retuned as a | 1080 encoded POST or PUT request body. The result is returned as a |
991 :class:`FormsDict`. All keys and values are strings. File uploads | 1081 :class:`FormsDict`. All keys and values are strings. File uploads |
992 are stored separately in :attr:`files`. """ | 1082 are stored separately in :attr:`files`. """ |
993 forms = FormsDict() | 1083 forms = FormsDict() |
994 for name, item in self.POST.allitems(): | 1084 for name, item in self.POST.allitems(): |
995 if not hasattr(item, 'filename'): | 1085 if not isinstance(item, FileUpload): |
996 forms[name] = item | 1086 forms[name] = item |
997 return forms | 1087 return forms |
998 | 1088 |
999 @DictProperty('environ', 'bottle.request.params', read_only=True) | 1089 @DictProperty('environ', 'bottle.request.params', read_only=True) |
1000 def params(self): | 1090 def params(self): |
1007 params[key] = value | 1097 params[key] = value |
1008 return params | 1098 return params |
1009 | 1099 |
1010 @DictProperty('environ', 'bottle.request.files', read_only=True) | 1100 @DictProperty('environ', 'bottle.request.files', read_only=True) |
1011 def files(self): | 1101 def files(self): |
1012 """ File uploads parsed from an `url-encoded` or `multipart/form-data` | 1102 """ File uploads parsed from `multipart/form-data` encoded POST or PUT |
1013 encoded POST or PUT request body. The values are instances of | 1103 request body. The values are instances of :class:`FileUpload`. |
1014 :class:`cgi.FieldStorage`. The most important attributes are: | 1104 |
1015 | |
1016 filename | |
1017 The filename, if specified; otherwise None; this is the client | |
1018 side filename, *not* the file name on which it is stored (that's | |
1019 a temporary file you don't deal with) | |
1020 file | |
1021 The file(-like) object from which you can read the data. | |
1022 value | |
1023 The value as a *string*; for file uploads, this transparently | |
1024 reads the file every time you request the value. Do not do this | |
1025 on big files. | |
1026 """ | 1105 """ |
1027 files = FormsDict() | 1106 files = FormsDict() |
1028 for name, item in self.POST.allitems(): | 1107 for name, item in self.POST.allitems(): |
1029 if hasattr(item, 'filename'): | 1108 if isinstance(item, FileUpload): |
1030 files[name] = item | 1109 files[name] = item |
1031 return files | 1110 return files |
1032 | 1111 |
1033 @DictProperty('environ', 'bottle.request.json', read_only=True) | 1112 @DictProperty('environ', 'bottle.request.json', read_only=True) |
1034 def json(self): | 1113 def json(self): |
1035 ''' If the ``Content-Type`` header is ``application/json``, this | 1114 ''' If the ``Content-Type`` header is ``application/json``, this |
1036 property holds the parsed content of the request body. Only requests | 1115 property holds the parsed content of the request body. Only requests |
1037 smaller than :attr:`MEMFILE_MAX` are processed to avoid memory | 1116 smaller than :attr:`MEMFILE_MAX` are processed to avoid memory |
1038 exhaustion. ''' | 1117 exhaustion. ''' |
1039 if 'application/json' in self.environ.get('CONTENT_TYPE', '') \ | 1118 ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] |
1040 and 0 < self.content_length < self.MEMFILE_MAX: | 1119 if ctype == 'application/json': |
1041 return json_loads(self.body.read(self.MEMFILE_MAX)) | 1120 b = self._get_body_string() |
1121 if not b: | |
1122 return None | |
1123 return json_loads(b) | |
1042 return None | 1124 return None |
1125 | |
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 | |
1043 | 1160 |
1044 @DictProperty('environ', 'bottle.request.body', read_only=True) | 1161 @DictProperty('environ', 'bottle.request.body', read_only=True) |
1045 def _body(self): | 1162 def _body(self): |
1046 maxread = max(0, self.content_length) | 1163 body_iter = self._iter_chunked if self.chunked else self._iter_body |
1047 stream = self.environ['wsgi.input'] | 1164 read_func = self.environ['wsgi.input'].read |
1048 body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b') | 1165 body, body_size, is_temp_file = BytesIO(), 0, False |
1049 while maxread > 0: | 1166 for part in body_iter(read_func, self.MEMFILE_MAX): |
1050 part = stream.read(min(maxread, self.MEMFILE_MAX)) | |
1051 if not part: break | |
1052 body.write(part) | 1167 body.write(part) |
1053 maxread -= len(part) | 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 | |
1054 self.environ['wsgi.input'] = body | 1174 self.environ['wsgi.input'] = body |
1055 body.seek(0) | 1175 body.seek(0) |
1056 return body | 1176 return body |
1177 | |
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 | |
1057 | 1189 |
1058 @property | 1190 @property |
1059 def body(self): | 1191 def body(self): |
1060 """ The HTTP request body as a seek-able file-like object. Depending on | 1192 """ The HTTP request body as a seek-able file-like object. Depending on |
1061 :attr:`MEMFILE_MAX`, this is either a temporary file or a | 1193 :attr:`MEMFILE_MAX`, this is either a temporary file or a |
1063 time reads and replaces the ``wsgi.input`` environ variable. | 1195 time reads and replaces the ``wsgi.input`` environ variable. |
1064 Subsequent accesses just do a `seek(0)` on the file object. """ | 1196 Subsequent accesses just do a `seek(0)` on the file object. """ |
1065 self._body.seek(0) | 1197 self._body.seek(0) |
1066 return self._body | 1198 return self._body |
1067 | 1199 |
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 | |
1068 #: An alias for :attr:`query`. | 1205 #: An alias for :attr:`query`. |
1069 GET = query | 1206 GET = query |
1070 | 1207 |
1071 @DictProperty('environ', 'bottle.request.post', read_only=True) | 1208 @DictProperty('environ', 'bottle.request.post', read_only=True) |
1072 def POST(self): | 1209 def POST(self): |
1073 """ The values of :attr:`forms` and :attr:`files` combined into a single | 1210 """ The values of :attr:`forms` and :attr:`files` combined into a single |
1074 :class:`FormsDict`. Values are either strings (form values) or | 1211 :class:`FormsDict`. Values are either strings (form values) or |
1075 instances of :class:`cgi.FieldStorage` (file uploads). | 1212 instances of :class:`cgi.FieldStorage` (file uploads). |
1076 """ | 1213 """ |
1077 post = FormsDict() | 1214 post = FormsDict() |
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 | |
1078 safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi | 1223 safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi |
1079 for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): | 1224 for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): |
1080 if key in self.environ: safe_env[key] = self.environ[key] | 1225 if key in self.environ: safe_env[key] = self.environ[key] |
1081 if NCTextIOWrapper: | 1226 args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) |
1082 fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') | 1227 if py31: |
1083 else: | 1228 args['fp'] = NCTextIOWrapper(args['fp'], encoding='utf8', |
1084 fb = self.body | 1229 newline='\n') |
1085 data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) | 1230 elif py3k: |
1086 for item in (data.list or [])[:self.MAX_PARAMS]: | 1231 args['encoding'] = 'utf8' |
1087 post[item.name] = item if item.filename else item.value | 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 | |
1088 return post | 1241 return post |
1089 | |
1090 @property | |
1091 def COOKIES(self): | |
1092 ''' Alias for :attr:`cookies` (deprecated). ''' | |
1093 depr('BaseRequest.COOKIES was renamed to BaseRequest.cookies (lowercase).') | |
1094 return self.cookies | |
1095 | 1242 |
1096 @property | 1243 @property |
1097 def url(self): | 1244 def url(self): |
1098 """ The full request URI including hostname and scheme. If your app | 1245 """ The full request URI including hostname and scheme. If your app |
1099 lives behind a reverse proxy or load balancer and you get confusing | 1246 lives behind a reverse proxy or load balancer and you get confusing |
1106 ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. | 1253 ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. |
1107 The tuple contains (scheme, host, path, query_string and fragment), | 1254 The tuple contains (scheme, host, path, query_string and fragment), |
1108 but the fragment is always empty because it is not visible to the | 1255 but the fragment is always empty because it is not visible to the |
1109 server. ''' | 1256 server. ''' |
1110 env = self.environ | 1257 env = self.environ |
1111 http = env.get('wsgi.url_scheme', 'http') | 1258 http = env.get('HTTP_X_FORWARDED_PROTO') or env.get('wsgi.url_scheme', 'http') |
1112 host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') | 1259 host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') |
1113 if not host: | 1260 if not host: |
1114 # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. | 1261 # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. |
1115 host = env.get('SERVER_NAME', '127.0.0.1') | 1262 host = env.get('SERVER_NAME', '127.0.0.1') |
1116 port = env.get('SERVER_PORT') | 1263 port = env.get('SERVER_PORT') |
1153 def content_length(self): | 1300 def content_length(self): |
1154 ''' The request body length as an integer. The client is responsible to | 1301 ''' The request body length as an integer. The client is responsible to |
1155 set this header. Otherwise, the real length of the body is unknown | 1302 set this header. Otherwise, the real length of the body is unknown |
1156 and -1 is returned. In this case, :attr:`body` will be empty. ''' | 1303 and -1 is returned. In this case, :attr:`body` will be empty. ''' |
1157 return int(self.environ.get('CONTENT_LENGTH') or -1) | 1304 return int(self.environ.get('CONTENT_LENGTH') or -1) |
1305 | |
1306 @property | |
1307 def content_type(self): | |
1308 ''' The Content-Type header as a lowercase-string (default: empty). ''' | |
1309 return self.environ.get('CONTENT_TYPE', '').lower() | |
1158 | 1310 |
1159 @property | 1311 @property |
1160 def is_xhr(self): | 1312 def is_xhr(self): |
1161 ''' True if the request was triggered by a XMLHttpRequest. This only | 1313 ''' True if the request was triggered by a XMLHttpRequest. This only |
1162 works with JavaScript libraries that support the `X-Requested-With` | 1314 works with JavaScript libraries that support the `X-Requested-With` |
1237 ''' Search in self.environ for additional user defined attributes. ''' | 1389 ''' Search in self.environ for additional user defined attributes. ''' |
1238 try: | 1390 try: |
1239 var = self.environ['bottle.request.ext.%s'%name] | 1391 var = self.environ['bottle.request.ext.%s'%name] |
1240 return var.__get__(self) if hasattr(var, '__get__') else var | 1392 return var.__get__(self) if hasattr(var, '__get__') else var |
1241 except KeyError: | 1393 except KeyError: |
1242 raise AttributeError('Attribute %r not defined.' % name) | 1394 raise AttributeError('Attribute %r not defined.' % name) |
1243 | 1395 |
1244 def __setattr__(self, name, value): | 1396 def __setattr__(self, name, value): |
1245 if name == 'environ': return object.__setattr__(self, name, value) | 1397 if name == 'environ': return object.__setattr__(self, name, value) |
1246 self.environ['bottle.request.ext.%s'%name] = value | 1398 self.environ['bottle.request.ext.%s'%name] = value |
1247 | 1399 |
1274 """ Storage class for a response body as well as headers and cookies. | 1426 """ Storage class for a response body as well as headers and cookies. |
1275 | 1427 |
1276 This class does support dict-like case-insensitive item-access to | 1428 This class does support dict-like case-insensitive item-access to |
1277 headers, but is NOT a dict. Most notably, iterating over a response | 1429 headers, but is NOT a dict. Most notably, iterating over a response |
1278 yields parts of the body and not the headers. | 1430 yields parts of the body and not the headers. |
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. | |
1279 """ | 1439 """ |
1280 | 1440 |
1281 default_status = 200 | 1441 default_status = 200 |
1282 default_content_type = 'text/html; charset=UTF-8' | 1442 default_content_type = 'text/html; charset=UTF-8' |
1283 | 1443 |
1287 204: set(('Content-Type',)), | 1447 204: set(('Content-Type',)), |
1288 304: set(('Allow', 'Content-Encoding', 'Content-Language', | 1448 304: set(('Allow', 'Content-Encoding', 'Content-Language', |
1289 'Content-Length', 'Content-Range', 'Content-Type', | 1449 'Content-Length', 'Content-Range', 'Content-Type', |
1290 'Content-Md5', 'Last-Modified'))} | 1450 'Content-Md5', 'Last-Modified'))} |
1291 | 1451 |
1292 def __init__(self, body='', status=None, **headers): | 1452 def __init__(self, body='', status=None, headers=None, **more_headers): |
1293 self._status_line = None | |
1294 self._status_code = None | |
1295 self._cookies = None | 1453 self._cookies = None |
1296 self._headers = {'Content-Type': [self.default_content_type]} | 1454 self._headers = {} |
1297 self.body = body | 1455 self.body = body |
1298 self.status = status or self.default_status | 1456 self.status = status or self.default_status |
1299 if headers: | 1457 if headers: |
1300 for name, value in headers.items(): | 1458 if isinstance(headers, dict): |
1301 self[name] = value | 1459 headers = headers.items() |
1302 | 1460 for name, value in headers: |
1303 def copy(self): | 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): | |
1304 ''' Returns a copy of self. ''' | 1467 ''' Returns a copy of self. ''' |
1305 copy = Response() | 1468 cls = cls or BaseResponse |
1469 assert issubclass(cls, BaseResponse) | |
1470 copy = cls() | |
1306 copy.status = self.status | 1471 copy.status = self.status |
1307 copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) | 1472 copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) |
1473 if self._cookies: | |
1474 copy._cookies = SimpleCookie() | |
1475 copy._cookies.load(self._cookies.output(header='')) | |
1308 return copy | 1476 return copy |
1309 | 1477 |
1310 def __iter__(self): | 1478 def __iter__(self): |
1311 return iter(self.body) | 1479 return iter(self.body) |
1312 | 1480 |
1332 code = int(status.split()[0]) | 1500 code = int(status.split()[0]) |
1333 else: | 1501 else: |
1334 raise ValueError('String status line without a reason phrase.') | 1502 raise ValueError('String status line without a reason phrase.') |
1335 if not 100 <= code <= 999: raise ValueError('Status code out of range.') | 1503 if not 100 <= code <= 999: raise ValueError('Status code out of range.') |
1336 self._status_code = code | 1504 self._status_code = code |
1337 self._status_line = status or ('%d Unknown' % code) | 1505 self._status_line = str(status or ('%d Unknown' % code)) |
1338 | 1506 |
1339 def _get_status(self): | 1507 def _get_status(self): |
1340 return self._status_line | 1508 return self._status_line |
1341 | 1509 |
1342 status = property(_get_status, _set_status, None, | 1510 status = property(_get_status, _set_status, None, |
1349 | 1517 |
1350 @property | 1518 @property |
1351 def headers(self): | 1519 def headers(self): |
1352 ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like | 1520 ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like |
1353 view on the response headers. ''' | 1521 view on the response headers. ''' |
1354 self.__dict__['headers'] = hdict = HeaderDict() | 1522 hdict = HeaderDict() |
1355 hdict.dict = self._headers | 1523 hdict.dict = self._headers |
1356 return hdict | 1524 return hdict |
1357 | 1525 |
1358 def __contains__(self, name): return _hkey(name) in self._headers | 1526 def __contains__(self, name): return _hkey(name) in self._headers |
1359 def __delitem__(self, name): del self._headers[_hkey(name)] | 1527 def __delitem__(self, name): del self._headers[_hkey(name)] |
1363 def get_header(self, name, default=None): | 1531 def get_header(self, name, default=None): |
1364 ''' Return the value of a previously defined header. If there is no | 1532 ''' Return the value of a previously defined header. If there is no |
1365 header with that name, return a default value. ''' | 1533 header with that name, return a default value. ''' |
1366 return self._headers.get(_hkey(name), [default])[-1] | 1534 return self._headers.get(_hkey(name), [default])[-1] |
1367 | 1535 |
1368 def set_header(self, name, value, append=False): | 1536 def set_header(self, name, value): |
1369 ''' Create a new response header, replacing any previously defined | 1537 ''' Create a new response header, replacing any previously defined |
1370 headers with the same name. ''' | 1538 headers with the same name. ''' |
1371 if append: | 1539 self._headers[_hkey(name)] = [str(value)] |
1372 self.add_header(name, value) | |
1373 else: | |
1374 self._headers[_hkey(name)] = [str(value)] | |
1375 | 1540 |
1376 def add_header(self, name, value): | 1541 def add_header(self, name, value): |
1377 ''' Add an additional response header, not removing duplicates. ''' | 1542 ''' Add an additional response header, not removing duplicates. ''' |
1378 self._headers.setdefault(_hkey(name), []).append(str(value)) | 1543 self._headers.setdefault(_hkey(name), []).append(str(value)) |
1379 | 1544 |
1380 def iter_headers(self): | 1545 def iter_headers(self): |
1381 ''' Yield (header, value) tuples, skipping headers that are not | 1546 ''' Yield (header, value) tuples, skipping headers that are not |
1382 allowed with the current response status code. ''' | 1547 allowed with the current response status code. ''' |
1383 headers = self._headers.items() | |
1384 bad_headers = self.bad_headers.get(self._status_code) | |
1385 if bad_headers: | |
1386 headers = [h for h in headers if h[0] not in bad_headers] | |
1387 for name, values in headers: | |
1388 for value in values: | |
1389 yield name, value | |
1390 if self._cookies: | |
1391 for c in self._cookies.values(): | |
1392 yield 'Set-Cookie', c.OutputString() | |
1393 | |
1394 def wsgiheader(self): | |
1395 depr('The wsgiheader method is deprecated. See headerlist.') #0.10 | |
1396 return self.headerlist | 1548 return self.headerlist |
1397 | 1549 |
1398 @property | 1550 @property |
1399 def headerlist(self): | 1551 def headerlist(self): |
1400 ''' WSGI conform list of (header, value) tuples. ''' | 1552 ''' WSGI conform list of (header, value) tuples. ''' |
1401 return list(self.iter_headers()) | 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 | |
1402 | 1565 |
1403 content_type = HeaderProperty('Content-Type') | 1566 content_type = HeaderProperty('Content-Type') |
1404 content_length = HeaderProperty('Content-Length', reader=int) | 1567 content_length = HeaderProperty('Content-Length', reader=int) |
1568 expires = HeaderProperty('Expires', | |
1569 reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), | |
1570 writer=lambda x: http_date(x)) | |
1405 | 1571 |
1406 @property | 1572 @property |
1407 def charset(self): | 1573 def charset(self, default='UTF-8'): |
1408 """ Return the charset specified in the content-type header (default: utf8). """ | 1574 """ Return the charset specified in the content-type header (default: utf8). """ |
1409 if 'charset=' in self.content_type: | 1575 if 'charset=' in self.content_type: |
1410 return self.content_type.split('charset=')[-1].split(';')[0].strip() | 1576 return self.content_type.split('charset=')[-1].split(';')[0].strip() |
1411 return 'UTF-8' | 1577 return default |
1412 | |
1413 @property | |
1414 def COOKIES(self): | |
1415 """ A dict-like SimpleCookie instance. This should not be used directly. | |
1416 See :meth:`set_cookie`. """ | |
1417 depr('The COOKIES dict is deprecated. Use `set_cookie()` instead.') # 0.10 | |
1418 if not self._cookies: | |
1419 self._cookies = SimpleCookie() | |
1420 return self._cookies | |
1421 | 1578 |
1422 def set_cookie(self, name, value, secret=None, **options): | 1579 def set_cookie(self, name, value, secret=None, **options): |
1423 ''' Create a new cookie or replace an old one. If the `secret` parameter is | 1580 ''' Create a new cookie or replace an old one. If the `secret` parameter is |
1424 set, create a `Signed Cookie` (described below). | 1581 set, create a `Signed Cookie` (described below). |
1425 | 1582 |
1486 out = '' | 1643 out = '' |
1487 for name, value in self.headerlist: | 1644 for name, value in self.headerlist: |
1488 out += '%s: %s\n' % (name.title(), value.strip()) | 1645 out += '%s: %s\n' % (name.title(), value.strip()) |
1489 return out | 1646 return out |
1490 | 1647 |
1491 #: Thread-local storage for :class:`LocalRequest` and :class:`LocalResponse` | 1648 |
1492 #: attributes. | 1649 def local_property(name=None): |
1493 _lctx = threading.local() | 1650 if name: depr('local_property() is deprecated and will be removed.') #0.12 |
1494 | 1651 ls = threading.local() |
1495 def local_property(name): | |
1496 def fget(self): | 1652 def fget(self): |
1497 try: | 1653 try: return ls.var |
1498 return getattr(_lctx, name) | |
1499 except AttributeError: | 1654 except AttributeError: |
1500 raise RuntimeError("Request context not initialized.") | 1655 raise RuntimeError("Request context not initialized.") |
1501 def fset(self, value): setattr(_lctx, name, value) | 1656 def fset(self, value): ls.var = value |
1502 def fdel(self): delattr(_lctx, name) | 1657 def fdel(self): del ls.var |
1503 return property(fget, fset, fdel, | 1658 return property(fget, fset, fdel, 'Thread-local property') |
1504 'Thread-local property stored in :data:`_lctx.%s`' % name) | |
1505 | 1659 |
1506 | 1660 |
1507 class LocalRequest(BaseRequest): | 1661 class LocalRequest(BaseRequest): |
1508 ''' A thread-local subclass of :class:`BaseRequest` with a different | 1662 ''' A thread-local subclass of :class:`BaseRequest` with a different |
1509 set of attribues for each thread. There is usually only one global | 1663 set of attributes for each thread. There is usually only one global |
1510 instance of this class (:data:`request`). If accessed during a | 1664 instance of this class (:data:`request`). If accessed during a |
1511 request/response cycle, this instance always refers to the *current* | 1665 request/response cycle, this instance always refers to the *current* |
1512 request (even on a multithreaded server). ''' | 1666 request (even on a multithreaded server). ''' |
1513 bind = BaseRequest.__init__ | 1667 bind = BaseRequest.__init__ |
1514 environ = local_property('request_environ') | 1668 environ = local_property() |
1515 | 1669 |
1516 | 1670 |
1517 class LocalResponse(BaseResponse): | 1671 class LocalResponse(BaseResponse): |
1518 ''' A thread-local subclass of :class:`BaseResponse` with a different | 1672 ''' A thread-local subclass of :class:`BaseResponse` with a different |
1519 set of attribues for each thread. There is usually only one global | 1673 set of attributes for each thread. There is usually only one global |
1520 instance of this class (:data:`response`). Its attributes are used | 1674 instance of this class (:data:`response`). Its attributes are used |
1521 to build the HTTP response at the end of the request/response cycle. | 1675 to build the HTTP response at the end of the request/response cycle. |
1522 ''' | 1676 ''' |
1523 bind = BaseResponse.__init__ | 1677 bind = BaseResponse.__init__ |
1524 _status_line = local_property('response_status_line') | 1678 _status_line = local_property() |
1525 _status_code = local_property('response_status_code') | 1679 _status_code = local_property() |
1526 _cookies = local_property('response_cookies') | 1680 _cookies = local_property() |
1527 _headers = local_property('response_headers') | 1681 _headers = local_property() |
1528 body = local_property('response_body') | 1682 body = local_property() |
1529 | 1683 |
1530 Response = LocalResponse # BC 0.9 | 1684 |
1531 Request = LocalRequest # BC 0.9 | 1685 Request = BaseRequest |
1532 | 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) | |
1533 | 1708 |
1534 | 1709 |
1535 | 1710 |
1536 | 1711 |
1537 | 1712 |
1539 # Plugins ###################################################################### | 1714 # Plugins ###################################################################### |
1540 ############################################################################### | 1715 ############################################################################### |
1541 | 1716 |
1542 class PluginError(BottleException): pass | 1717 class PluginError(BottleException): pass |
1543 | 1718 |
1719 | |
1544 class JSONPlugin(object): | 1720 class JSONPlugin(object): |
1545 name = 'json' | 1721 name = 'json' |
1546 api = 2 | 1722 api = 2 |
1547 | 1723 |
1548 def __init__(self, json_dumps=json_dumps): | 1724 def __init__(self, json_dumps=json_dumps): |
1549 self.json_dumps = json_dumps | 1725 self.json_dumps = json_dumps |
1550 | 1726 |
1551 def apply(self, callback, context): | 1727 def apply(self, callback, route): |
1552 dumps = self.json_dumps | 1728 dumps = self.json_dumps |
1553 if not dumps: return callback | 1729 if not dumps: return callback |
1554 def wrapper(*a, **ka): | 1730 def wrapper(*a, **ka): |
1555 rv = callback(*a, **ka) | 1731 try: |
1732 rv = callback(*a, **ka) | |
1733 except HTTPError: | |
1734 rv = _e() | |
1735 | |
1556 if isinstance(rv, dict): | 1736 if isinstance(rv, dict): |
1557 #Attempt to serialize, raises exception on failure | 1737 #Attempt to serialize, raises exception on failure |
1558 json_response = dumps(rv) | 1738 json_response = dumps(rv) |
1559 #Set content type only if serialization succesful | 1739 #Set content type only if serialization succesful |
1560 response.content_type = 'application/json' | 1740 response.content_type = 'application/json' |
1561 return json_response | 1741 return json_response |
1742 elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): | |
1743 rv.body = dumps(rv.body) | |
1744 rv.content_type = 'application/json' | |
1562 return rv | 1745 return rv |
1563 return wrapper | 1746 |
1564 | |
1565 | |
1566 class HooksPlugin(object): | |
1567 name = 'hooks' | |
1568 api = 2 | |
1569 | |
1570 _names = 'before_request', 'after_request', 'app_reset' | |
1571 | |
1572 def __init__(self): | |
1573 self.hooks = dict((name, []) for name in self._names) | |
1574 self.app = None | |
1575 | |
1576 def _empty(self): | |
1577 return not (self.hooks['before_request'] or self.hooks['after_request']) | |
1578 | |
1579 def setup(self, app): | |
1580 self.app = app | |
1581 | |
1582 def add(self, name, func): | |
1583 ''' Attach a callback to a hook. ''' | |
1584 was_empty = self._empty() | |
1585 self.hooks.setdefault(name, []).append(func) | |
1586 if self.app and was_empty and not self._empty(): self.app.reset() | |
1587 | |
1588 def remove(self, name, func): | |
1589 ''' Remove a callback from a hook. ''' | |
1590 was_empty = self._empty() | |
1591 if name in self.hooks and func in self.hooks[name]: | |
1592 self.hooks[name].remove(func) | |
1593 if self.app and not was_empty and self._empty(): self.app.reset() | |
1594 | |
1595 def trigger(self, name, *a, **ka): | |
1596 ''' Trigger a hook and return a list of results. ''' | |
1597 hooks = self.hooks[name] | |
1598 if ka.pop('reversed', False): hooks = hooks[::-1] | |
1599 return [hook(*a, **ka) for hook in hooks] | |
1600 | |
1601 def apply(self, callback, context): | |
1602 if self._empty(): return callback | |
1603 def wrapper(*a, **ka): | |
1604 self.trigger('before_request') | |
1605 rv = callback(*a, **ka) | |
1606 self.trigger('after_request', reversed=True) | |
1607 return rv | |
1608 return wrapper | 1747 return wrapper |
1609 | 1748 |
1610 | 1749 |
1611 class TemplatePlugin(object): | 1750 class TemplatePlugin(object): |
1612 ''' This plugin applies the :func:`view` decorator to all routes with a | 1751 ''' This plugin applies the :func:`view` decorator to all routes with a |
1618 | 1757 |
1619 def apply(self, callback, route): | 1758 def apply(self, callback, route): |
1620 conf = route.config.get('template') | 1759 conf = route.config.get('template') |
1621 if isinstance(conf, (tuple, list)) and len(conf) == 2: | 1760 if isinstance(conf, (tuple, list)) and len(conf) == 2: |
1622 return view(conf[0], **conf[1])(callback) | 1761 return view(conf[0], **conf[1])(callback) |
1623 elif isinstance(conf, str) and 'template_opts' in route.config: | |
1624 depr('The `template_opts` parameter is deprecated.') #0.9 | |
1625 return view(conf, **route.config['template_opts'])(callback) | |
1626 elif isinstance(conf, str): | 1762 elif isinstance(conf, str): |
1627 return view(conf)(callback) | 1763 return view(conf)(callback) |
1628 else: | 1764 else: |
1629 return callback | 1765 return callback |
1630 | 1766 |
1640 '__all__': [], '__loader__': self}) | 1776 '__all__': [], '__loader__': self}) |
1641 sys.meta_path.append(self) | 1777 sys.meta_path.append(self) |
1642 | 1778 |
1643 def find_module(self, fullname, path=None): | 1779 def find_module(self, fullname, path=None): |
1644 if '.' not in fullname: return | 1780 if '.' not in fullname: return |
1645 packname, modname = fullname.rsplit('.', 1) | 1781 packname = fullname.rsplit('.', 1)[0] |
1646 if packname != self.name: return | 1782 if packname != self.name: return |
1647 return self | 1783 return self |
1648 | 1784 |
1649 def load_module(self, fullname): | 1785 def load_module(self, fullname): |
1650 if fullname in sys.modules: return sys.modules[fullname] | 1786 if fullname in sys.modules: return sys.modules[fullname] |
1651 packname, modname = fullname.rsplit('.', 1) | 1787 modname = fullname.rsplit('.', 1)[1] |
1652 realname = self.impmask % modname | 1788 realname = self.impmask % modname |
1653 __import__(realname) | 1789 __import__(realname) |
1654 module = sys.modules[fullname] = sys.modules[realname] | 1790 module = sys.modules[fullname] = sys.modules[realname] |
1655 setattr(self.module, modname, module) | 1791 setattr(self.module, modname, module) |
1656 module.__loader__ = self | 1792 module.__loader__ = self |
1737 #: Aliases for WTForms to mimic other multi-dict APIs (Django) | 1873 #: Aliases for WTForms to mimic other multi-dict APIs (Django) |
1738 getone = get | 1874 getone = get |
1739 getlist = getall | 1875 getlist = getall |
1740 | 1876 |
1741 | 1877 |
1742 | |
1743 class FormsDict(MultiDict): | 1878 class FormsDict(MultiDict): |
1744 ''' This :class:`MultiDict` subclass is used to store request form data. | 1879 ''' This :class:`MultiDict` subclass is used to store request form data. |
1745 Additionally to the normal dict-like item access methods (which return | 1880 Additionally to the normal dict-like item access methods (which return |
1746 unmodified data as native strings), this container also supports | 1881 unmodified data as native strings), this container also supports |
1747 attribute-like access to its values. Attributes are automatically de- | 1882 attribute-like access to its values. Attributes are automatically de- |
1754 #: and then decoded to match :attr:`input_encoding`. | 1889 #: and then decoded to match :attr:`input_encoding`. |
1755 recode_unicode = True | 1890 recode_unicode = True |
1756 | 1891 |
1757 def _fix(self, s, encoding=None): | 1892 def _fix(self, s, encoding=None): |
1758 if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI | 1893 if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI |
1759 s = s.encode('latin1') | 1894 return s.encode('latin1').decode(encoding or self.input_encoding) |
1760 if isinstance(s, bytes): # Python 2 WSGI | 1895 elif isinstance(s, bytes): # Python 2 WSGI |
1761 return s.decode(encoding or self.input_encoding) | 1896 return s.decode(encoding or self.input_encoding) |
1762 return s | 1897 else: |
1898 return s | |
1763 | 1899 |
1764 def decode(self, encoding=None): | 1900 def decode(self, encoding=None): |
1765 ''' Returns a copy with all keys and values de- or recoded to match | 1901 ''' Returns a copy with all keys and values de- or recoded to match |
1766 :attr:`input_encoding`. Some libraries (e.g. WTForms) want a | 1902 :attr:`input_encoding`. Some libraries (e.g. WTForms) want a |
1767 unicode dictionary. ''' | 1903 unicode dictionary. ''' |
1771 for key, value in self.allitems(): | 1907 for key, value in self.allitems(): |
1772 copy.append(self._fix(key, enc), self._fix(value, enc)) | 1908 copy.append(self._fix(key, enc), self._fix(value, enc)) |
1773 return copy | 1909 return copy |
1774 | 1910 |
1775 def getunicode(self, name, default=None, encoding=None): | 1911 def getunicode(self, name, default=None, encoding=None): |
1912 ''' Return the value as a unicode string, or the default. ''' | |
1776 try: | 1913 try: |
1777 return self._fix(self[name], encoding) | 1914 return self._fix(self[name], encoding) |
1778 except (UnicodeError, KeyError): | 1915 except (UnicodeError, KeyError): |
1779 return default | 1916 return default |
1780 | 1917 |
1818 | 1955 |
1819 The API will remain stable even on changes to the relevant PEPs. | 1956 The API will remain stable even on changes to the relevant PEPs. |
1820 Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one | 1957 Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one |
1821 that uses non-native strings.) | 1958 that uses non-native strings.) |
1822 ''' | 1959 ''' |
1823 #: List of keys that do not have a 'HTTP_' prefix. | 1960 #: List of keys that do not have a ``HTTP_`` prefix. |
1824 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') | 1961 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') |
1825 | 1962 |
1826 def __init__(self, environ): | 1963 def __init__(self, environ): |
1827 self.environ = environ | 1964 self.environ = environ |
1828 | 1965 |
1856 def keys(self): return [x for x in self] | 1993 def keys(self): return [x for x in self] |
1857 def __len__(self): return len(self.keys()) | 1994 def __len__(self): return len(self.keys()) |
1858 def __contains__(self, key): return self._ekey(key) in self.environ | 1995 def __contains__(self, key): return self._ekey(key) in self.environ |
1859 | 1996 |
1860 | 1997 |
1998 | |
1861 class ConfigDict(dict): | 1999 class ConfigDict(dict): |
1862 ''' A dict-subclass with some extras: You can access keys like attributes. | 2000 ''' A dict-like configuration storage with additional support for |
1863 Uppercase attributes create new ConfigDicts and act as name-spaces. | 2001 namespaces, validators, meta-data, on_change listeners and more. |
1864 Other missing attributes return None. Calling a ConfigDict updates its | 2002 |
1865 values and returns itself. | 2003 This storage is optimized for fast read access. Retrieving a key |
1866 | 2004 or using non-altering dict methods (e.g. `dict.get()`) has no overhead |
1867 >>> cfg = ConfigDict() | 2005 compared to a native dict. |
1868 >>> cfg.Namespace.value = 5 | |
1869 >>> cfg.OtherNamespace(a=1, b=2) | |
1870 >>> cfg | |
1871 {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}} | |
1872 ''' | 2006 ''' |
1873 | 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 | |
1874 def __getattr__(self, key): | 2170 def __getattr__(self, key): |
2171 depr('Attribute access is deprecated.') #0.12 | |
1875 if key not in self and key[0].isupper(): | 2172 if key not in self and key[0].isupper(): |
1876 self[key] = ConfigDict() | 2173 self[key] = self.Namespace(self, key) |
2174 if key not in self and key.startswith('__'): | |
2175 raise AttributeError(key) | |
1877 return self.get(key) | 2176 return self.get(key) |
1878 | 2177 |
1879 def __setattr__(self, key, value): | 2178 def __setattr__(self, key, value): |
2179 if key in self.__slots__: | |
2180 return dict.__setattr__(self, key, value) | |
2181 depr('Attribute assignment is deprecated.') #0.12 | |
1880 if hasattr(dict, key): | 2182 if hasattr(dict, key): |
1881 raise AttributeError('Read-only attribute.') | 2183 raise AttributeError('Read-only attribute.') |
1882 if key in self and self[key] and isinstance(self[key], ConfigDict): | 2184 if key in self and self[key] and isinstance(self[key], self.Namespace): |
1883 raise AttributeError('Non-empty namespace attribute.') | 2185 raise AttributeError('Non-empty namespace attribute.') |
1884 self[key] = value | 2186 self[key] = value |
1885 | 2187 |
1886 def __delattr__(self, key): | 2188 def __delattr__(self, key): |
1887 if key in self: del self[key] | 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] | |
1888 | 2196 |
1889 def __call__(self, *a, **ka): | 2197 def __call__(self, *a, **ka): |
1890 for key, value in dict(*a, **ka).items(): setattr(self, key, value) | 2198 depr('Calling ConfDict is deprecated. Use the update() method.') #0.12 |
2199 self.update(*a, **ka) | |
1891 return self | 2200 return self |
2201 | |
1892 | 2202 |
1893 | 2203 |
1894 class AppStack(list): | 2204 class AppStack(list): |
1895 """ A stack-like list. Calling it returns the head of the stack. """ | 2205 """ A stack-like list. Calling it returns the head of the stack. """ |
1896 | 2206 |
1919 part = read(buff) | 2229 part = read(buff) |
1920 if not part: return | 2230 if not part: return |
1921 yield part | 2231 yield part |
1922 | 2232 |
1923 | 2233 |
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 | |
1924 class ResourceManager(object): | 2250 class ResourceManager(object): |
1925 ''' This class manages a list of search paths and helps to find and open | 2251 ''' This class manages a list of search paths and helps to find and open |
1926 aplication-bound resources (files). | 2252 application-bound resources (files). |
1927 | 2253 |
1928 :param base: default value for same-named :meth:`add_path` parameter. | 2254 :param base: default value for :meth:`add_path` calls. |
1929 :param opener: callable used to open resources. | 2255 :param opener: callable used to open resources. |
1930 :param cachemode: controls which lookups are cached. One of 'all', | 2256 :param cachemode: controls which lookups are cached. One of 'all', |
1931 'found' or 'none'. | 2257 'found' or 'none'. |
1932 ''' | 2258 ''' |
1933 | 2259 |
1936 self.base = base | 2262 self.base = base |
1937 self.cachemode = cachemode | 2263 self.cachemode = cachemode |
1938 | 2264 |
1939 #: A list of search paths. See :meth:`add_path` for details. | 2265 #: A list of search paths. See :meth:`add_path` for details. |
1940 self.path = [] | 2266 self.path = [] |
1941 #: A cache for resolved paths. `res.cache.clear()`` clears the cache. | 2267 #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. |
1942 self.cache = {} | 2268 self.cache = {} |
1943 | 2269 |
1944 def add_path(self, path, base=None, index=None, create=False): | 2270 def add_path(self, path, base=None, index=None, create=False): |
1945 ''' Add a new path to the list of search paths. Return False if it does | 2271 ''' Add a new path to the list of search paths. Return False if the |
1946 not exist. | 2272 path does not exist. |
1947 | 2273 |
1948 :param path: The new search path. Relative paths are turned into an | 2274 :param path: The new search path. Relative paths are turned into |
1949 absolute and normalized form. If the path looks like a file (not | 2275 an absolute and normalized form. If the path looks like a file |
1950 ending in `/`), the filename is stripped off. | 2276 (not ending in `/`), the filename is stripped off. |
1951 :param base: Path used to absolutize relative search paths. | 2277 :param base: Path used to absolutize relative search paths. |
1952 Defaults to `:attr:base` which defaults to ``./``. | 2278 Defaults to :attr:`base` which defaults to ``os.getcwd()``. |
1953 :param index: Position within the list of search paths. Defaults to | 2279 :param index: Position within the list of search paths. Defaults |
1954 last index (appends to the list). | 2280 to last index (appends to the list). |
1955 :param create: Create non-existent search paths. Off by default. | |
1956 | 2281 |
1957 The `base` parameter makes it easy to reference files installed | 2282 The `base` parameter makes it easy to reference files installed |
1958 along with a python module or package:: | 2283 along with a python module or package:: |
1959 | 2284 |
1960 res.add_path('./resources/', __file__) | 2285 res.add_path('./resources/', __file__) |
1961 ''' | 2286 ''' |
1962 base = os.path.abspath(os.path.dirname(base or self.base)) | 2287 base = os.path.abspath(os.path.dirname(base or self.base)) |
1963 path = os.path.abspath(os.path.join(base, os.path.dirname(path))) | 2288 path = os.path.abspath(os.path.join(base, os.path.dirname(path))) |
1964 path += os.sep | 2289 path += os.sep |
1965 if path in self.path: | 2290 if path in self.path: |
1966 self.path.remove(path) | 2291 self.path.remove(path) |
1967 if create and not os.path.isdir(path): | 2292 if create and not os.path.isdir(path): |
1968 os.mkdirs(path) | 2293 os.makedirs(path) |
1969 if index is None: | 2294 if index is None: |
1970 self.path.append(path) | 2295 self.path.append(path) |
1971 else: | 2296 else: |
1972 self.path.insert(index, path) | 2297 self.path.insert(index, path) |
1973 self.cache.clear() | 2298 self.cache.clear() |
2299 return os.path.exists(path) | |
1974 | 2300 |
1975 def __iter__(self): | 2301 def __iter__(self): |
1976 ''' Iterate over all existing files in all registered paths. ''' | 2302 ''' Iterate over all existing files in all registered paths. ''' |
1977 search = self.path[:] | 2303 search = self.path[:] |
1978 while search: | 2304 while search: |
2002 | 2328 |
2003 def open(self, name, mode='r', *args, **kwargs): | 2329 def open(self, name, mode='r', *args, **kwargs): |
2004 ''' Find a resource and return a file object, or raise IOError. ''' | 2330 ''' Find a resource and return a file object, or raise IOError. ''' |
2005 fname = self.lookup(name) | 2331 fname = self.lookup(name) |
2006 if not fname: raise IOError("Resource %r not found." % name) | 2332 if not fname: raise IOError("Resource %r not found." % name) |
2007 return self.opener(name, mode=mode, *args, **kwargs) | 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) | |
2008 | 2397 |
2009 | 2398 |
2010 | 2399 |
2011 | 2400 |
2012 | 2401 |
2014 ############################################################################### | 2403 ############################################################################### |
2015 # Application Helper ########################################################### | 2404 # Application Helper ########################################################### |
2016 ############################################################################### | 2405 ############################################################################### |
2017 | 2406 |
2018 | 2407 |
2019 def abort(code=500, text='Unknown Error: Application stopped.'): | 2408 def abort(code=500, text='Unknown Error.'): |
2020 """ Aborts execution and causes a HTTP error. """ | 2409 """ Aborts execution and causes a HTTP error. """ |
2021 raise HTTPError(code, text) | 2410 raise HTTPError(code, text) |
2022 | 2411 |
2023 | 2412 |
2024 def redirect(url, code=None): | 2413 def redirect(url, code=None): |
2025 """ Aborts execution and causes a 303 or 302 redirect, depending on | 2414 """ Aborts execution and causes a 303 or 302 redirect, depending on |
2026 the HTTP protocol version. """ | 2415 the HTTP protocol version. """ |
2027 if code is None: | 2416 if not code: |
2028 code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 | 2417 code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 |
2029 location = urljoin(request.url, url) | 2418 res = response.copy(cls=HTTPResponse) |
2030 raise HTTPResponse("", status=code, header=dict(Location=location)) | 2419 res.status = code |
2420 res.body = "" | |
2421 res.set_header('Location', urljoin(request.url, url)) | |
2422 raise res | |
2031 | 2423 |
2032 | 2424 |
2033 def _file_iter_range(fp, offset, bytes, maxread=1024*1024): | 2425 def _file_iter_range(fp, offset, bytes, maxread=1024*1024): |
2034 ''' Yield chunks from a range in a file. No chunk is bigger than maxread.''' | 2426 ''' Yield chunks from a range in a file. No chunk is bigger than maxread.''' |
2035 fp.seek(offset) | 2427 fp.seek(offset) |
2038 if not part: break | 2430 if not part: break |
2039 bytes -= len(part) | 2431 bytes -= len(part) |
2040 yield part | 2432 yield part |
2041 | 2433 |
2042 | 2434 |
2043 def static_file(filename, root, mimetype='auto', download=False): | 2435 def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8'): |
2044 """ Open a file in a safe way and return :exc:`HTTPResponse` with status | 2436 """ Open a file in a safe way and return :exc:`HTTPResponse` with status |
2045 code 200, 305, 401 or 404. Set Content-Type, Content-Encoding, | 2437 code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, |
2046 Content-Length and Last-Modified header. Obey If-Modified-Since header | 2438 ``Content-Length`` and ``Last-Modified`` headers are set if possible. |
2047 and HEAD requests. | 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) | |
2048 """ | 2453 """ |
2454 | |
2049 root = os.path.abspath(root) + os.sep | 2455 root = os.path.abspath(root) + os.sep |
2050 filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) | 2456 filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) |
2051 header = dict() | 2457 headers = dict() |
2052 | 2458 |
2053 if not filename.startswith(root): | 2459 if not filename.startswith(root): |
2054 return HTTPError(403, "Access denied.") | 2460 return HTTPError(403, "Access denied.") |
2055 if not os.path.exists(filename) or not os.path.isfile(filename): | 2461 if not os.path.exists(filename) or not os.path.isfile(filename): |
2056 return HTTPError(404, "File does not exist.") | 2462 return HTTPError(404, "File does not exist.") |
2057 if not os.access(filename, os.R_OK): | 2463 if not os.access(filename, os.R_OK): |
2058 return HTTPError(403, "You do not have permission to access this file.") | 2464 return HTTPError(403, "You do not have permission to access this file.") |
2059 | 2465 |
2060 if mimetype == 'auto': | 2466 if mimetype == 'auto': |
2061 mimetype, encoding = mimetypes.guess_type(filename) | 2467 mimetype, encoding = mimetypes.guess_type(filename) |
2062 if mimetype: header['Content-Type'] = mimetype | 2468 if encoding: headers['Content-Encoding'] = encoding |
2063 if encoding: header['Content-Encoding'] = encoding | 2469 |
2064 elif mimetype: | 2470 if mimetype: |
2065 header['Content-Type'] = mimetype | 2471 if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: |
2472 mimetype += '; charset=%s' % charset | |
2473 headers['Content-Type'] = mimetype | |
2066 | 2474 |
2067 if download: | 2475 if download: |
2068 download = os.path.basename(filename if download == True else download) | 2476 download = os.path.basename(filename if download == True else download) |
2069 header['Content-Disposition'] = 'attachment; filename="%s"' % download | 2477 headers['Content-Disposition'] = 'attachment; filename="%s"' % download |
2070 | 2478 |
2071 stats = os.stat(filename) | 2479 stats = os.stat(filename) |
2072 header['Content-Length'] = clen = stats.st_size | 2480 headers['Content-Length'] = clen = stats.st_size |
2073 lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) | 2481 lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) |
2074 header['Last-Modified'] = lm | 2482 headers['Last-Modified'] = lm |
2075 | 2483 |
2076 ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') | 2484 ims = request.environ.get('HTTP_IF_MODIFIED_SINCE') |
2077 if ims: | 2485 if ims: |
2078 ims = parse_date(ims.split(";")[0].strip()) | 2486 ims = parse_date(ims.split(";")[0].strip()) |
2079 if ims is not None and ims >= int(stats.st_mtime): | 2487 if ims is not None and ims >= int(stats.st_mtime): |
2080 header['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) | 2488 headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) |
2081 return HTTPResponse(status=304, header=header) | 2489 return HTTPResponse(status=304, **headers) |
2082 | 2490 |
2083 body = '' if request.method == 'HEAD' else open(filename, 'rb') | 2491 body = '' if request.method == 'HEAD' else open(filename, 'rb') |
2084 | 2492 |
2085 header["Accept-Ranges"] = "bytes" | 2493 headers["Accept-Ranges"] = "bytes" |
2086 ranges = request.environ.get('HTTP_RANGE') | 2494 ranges = request.environ.get('HTTP_RANGE') |
2087 if 'HTTP_RANGE' in request.environ: | 2495 if 'HTTP_RANGE' in request.environ: |
2088 ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) | 2496 ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen)) |
2089 if not ranges: | 2497 if not ranges: |
2090 return HTTPError(416, "Requested Range Not Satisfiable") | 2498 return HTTPError(416, "Requested Range Not Satisfiable") |
2091 offset, end = ranges[0] | 2499 offset, end = ranges[0] |
2092 header["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) | 2500 headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) |
2093 header["Content-Length"] = str(end-offset) | 2501 headers["Content-Length"] = str(end-offset) |
2094 if body: body = _file_iter_range(body, offset, end-offset) | 2502 if body: body = _file_iter_range(body, offset, end-offset) |
2095 return HTTPResponse(body, header=header, status=206) | 2503 return HTTPResponse(body, status=206, **headers) |
2096 return HTTPResponse(body, header=header) | 2504 return HTTPResponse(body, **headers) |
2097 | 2505 |
2098 | 2506 |
2099 | 2507 |
2100 | 2508 |
2101 | 2509 |
2107 | 2515 |
2108 def debug(mode=True): | 2516 def debug(mode=True): |
2109 """ Change the debug level. | 2517 """ Change the debug level. |
2110 There is only one debug level supported at the moment.""" | 2518 There is only one debug level supported at the moment.""" |
2111 global DEBUG | 2519 global DEBUG |
2520 if mode: warnings.simplefilter('default') | |
2112 DEBUG = bool(mode) | 2521 DEBUG = bool(mode) |
2113 | 2522 |
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 | |
2114 | 2531 |
2115 def parse_date(ims): | 2532 def parse_date(ims): |
2116 """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ | 2533 """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ |
2117 try: | 2534 try: |
2118 ts = email.utils.parsedate_tz(ims) | 2535 ts = email.utils.parsedate_tz(ims) |
2119 return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone | 2536 return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone |
2120 except (TypeError, ValueError, IndexError, OverflowError): | 2537 except (TypeError, ValueError, IndexError, OverflowError): |
2121 return None | 2538 return None |
2122 | |
2123 | 2539 |
2124 def parse_auth(header): | 2540 def parse_auth(header): |
2125 """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" | 2541 """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" |
2126 try: | 2542 try: |
2127 method, data = header.split(None, 1) | 2543 method, data = header.split(None, 1) |
2147 if 0 <= start < end <= maxlen: | 2563 if 0 <= start < end <= maxlen: |
2148 yield start, end | 2564 yield start, end |
2149 except ValueError: | 2565 except ValueError: |
2150 pass | 2566 pass |
2151 | 2567 |
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 | |
2152 def _lscmp(a, b): | 2579 def _lscmp(a, b): |
2153 ''' Compares two strings in a cryptographically safe way: | 2580 ''' Compares two strings in a cryptographically safe way: |
2154 Runtime is not affected by length of common prefix. ''' | 2581 Runtime is not affected by length of common prefix. ''' |
2155 return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) | 2582 return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) |
2156 | 2583 |
2183 .replace('"','"').replace("'",''') | 2610 .replace('"','"').replace("'",''') |
2184 | 2611 |
2185 | 2612 |
2186 def html_quote(string): | 2613 def html_quote(string): |
2187 ''' Escape and quote a string to be used as an HTTP attribute.''' | 2614 ''' Escape and quote a string to be used as an HTTP attribute.''' |
2188 return '"%s"' % html_escape(string).replace('\n','%#10;')\ | 2615 return '"%s"' % html_escape(string).replace('\n',' ')\ |
2189 .replace('\r',' ').replace('\t','	') | 2616 .replace('\r',' ').replace('\t','	') |
2190 | 2617 |
2191 | 2618 |
2192 def yieldroutes(func): | 2619 def yieldroutes(func): |
2193 """ Return a generator for routes that match the signature (name, args) | 2620 """ Return a generator for routes that match the signature (name, args) |
2194 of the func parameter. This may yield more than one route if the function | 2621 of the func parameter. This may yield more than one route if the function |
2195 takes optional keyword arguments. The output is best described by example:: | 2622 takes optional keyword arguments. The output is best described by example:: |
2196 | 2623 |
2197 a() -> '/a' | 2624 a() -> '/a' |
2198 b(x, y) -> '/b/:x/:y' | 2625 b(x, y) -> '/b/<x>/<y>' |
2199 c(x, y=5) -> '/c/:x' and '/c/:x/:y' | 2626 c(x, y=5) -> '/c/<x>' and '/c/<x>/<y>' |
2200 d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y' | 2627 d(x=5, y=6) -> '/d' and '/d/<x>' and '/d/<x>/<y>' |
2201 """ | 2628 """ |
2202 import inspect # Expensive module. Only import if necessary. | |
2203 path = '/' + func.__name__.replace('__','/').lstrip('/') | 2629 path = '/' + func.__name__.replace('__','/').lstrip('/') |
2204 spec = inspect.getargspec(func) | 2630 spec = getargspec(func) |
2205 argc = len(spec[0]) - len(spec[3] or []) | 2631 argc = len(spec[0]) - len(spec[3] or []) |
2206 path += ('/:%s' * argc) % tuple(spec[0][:argc]) | 2632 path += ('/<%s>' * argc) % tuple(spec[0][:argc]) |
2207 yield path | 2633 yield path |
2208 for arg in spec[0][argc:]: | 2634 for arg in spec[0][argc:]: |
2209 path += '/:%s' % arg | 2635 path += '/<%s>' % arg |
2210 yield path | 2636 yield path |
2211 | 2637 |
2212 | 2638 |
2213 def path_shift(script_name, path_info, shift=1): | 2639 def path_shift(script_name, path_info, shift=1): |
2214 ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. | 2640 ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. |
2239 new_path_info = '/' + '/'.join(pathlist) | 2665 new_path_info = '/' + '/'.join(pathlist) |
2240 if path_info.endswith('/') and pathlist: new_path_info += '/' | 2666 if path_info.endswith('/') and pathlist: new_path_info += '/' |
2241 return new_script_name, new_path_info | 2667 return new_script_name, new_path_info |
2242 | 2668 |
2243 | 2669 |
2244 def validate(**vkargs): | |
2245 """ | |
2246 Validates and manipulates keyword arguments by user defined callables. | |
2247 Handles ValueError and missing arguments by raising HTTPError(403). | |
2248 """ | |
2249 depr('Use route wildcard filters instead.') | |
2250 def decorator(func): | |
2251 @functools.wraps(func) | |
2252 def wrapper(*args, **kargs): | |
2253 for key, value in vkargs.items(): | |
2254 if key not in kargs: | |
2255 abort(403, 'Missing parameter: %s' % key) | |
2256 try: | |
2257 kargs[key] = value(kargs[key]) | |
2258 except ValueError: | |
2259 abort(403, 'Wrong parameter format for: %s' % key) | |
2260 return func(*args, **kargs) | |
2261 return wrapper | |
2262 return decorator | |
2263 | |
2264 | |
2265 def auth_basic(check, realm="private", text="Access denied"): | 2670 def auth_basic(check, realm="private", text="Access denied"): |
2266 ''' Callback decorator to require HTTP auth (basic). | 2671 ''' Callback decorator to require HTTP auth (basic). |
2267 TODO: Add route(check_auth=...) parameter. ''' | 2672 TODO: Add route(check_auth=...) parameter. ''' |
2268 def decorator(func): | 2673 def decorator(func): |
2269 def wrapper(*a, **ka): | 2674 def wrapper(*a, **ka): |
2270 user, password = request.auth or (None, None) | 2675 user, password = request.auth or (None, None) |
2271 if user is None or not check(user, password): | 2676 if user is None or not check(user, password): |
2272 response.headers['WWW-Authenticate'] = 'Basic realm="%s"' % realm | 2677 err = HTTPError(401, text) |
2273 return HTTPError(401, text) | 2678 err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) |
2274 return func(*a, **ka) | 2679 return err |
2275 return wrapper | 2680 return func(*a, **ka) |
2681 return wrapper | |
2276 return decorator | 2682 return decorator |
2277 | 2683 |
2278 | 2684 |
2279 # Shortcuts for common Bottle methods. | 2685 # Shortcuts for common Bottle methods. |
2280 # They all refer to the current default application. | 2686 # They all refer to the current default application. |
2309 ############################################################################### | 2715 ############################################################################### |
2310 | 2716 |
2311 | 2717 |
2312 class ServerAdapter(object): | 2718 class ServerAdapter(object): |
2313 quiet = False | 2719 quiet = False |
2314 def __init__(self, host='127.0.0.1', port=8080, **config): | 2720 def __init__(self, host='127.0.0.1', port=8080, **options): |
2315 self.options = config | 2721 self.options = options |
2316 self.host = host | 2722 self.host = host |
2317 self.port = int(port) | 2723 self.port = int(port) |
2318 | 2724 |
2319 def run(self, handler): # pragma: no cover | 2725 def run(self, handler): # pragma: no cover |
2320 pass | 2726 pass |
2340 self.options.setdefault('bindAddress', (self.host, self.port)) | 2746 self.options.setdefault('bindAddress', (self.host, self.port)) |
2341 flup.server.fcgi.WSGIServer(handler, **self.options).run() | 2747 flup.server.fcgi.WSGIServer(handler, **self.options).run() |
2342 | 2748 |
2343 | 2749 |
2344 class WSGIRefServer(ServerAdapter): | 2750 class WSGIRefServer(ServerAdapter): |
2345 def run(self, handler): # pragma: no cover | 2751 def run(self, app): # pragma: no cover |
2346 from wsgiref.simple_server import make_server, WSGIRequestHandler | 2752 from wsgiref.simple_server import WSGIRequestHandler, WSGIServer |
2347 if self.quiet: | 2753 from wsgiref.simple_server import make_server |
2348 class QuietHandler(WSGIRequestHandler): | 2754 import socket |
2349 def log_request(*args, **kw): pass | 2755 |
2350 self.options['handler_class'] = QuietHandler | 2756 class FixedHandler(WSGIRequestHandler): |
2351 srv = make_server(self.host, self.port, handler, **self.options) | 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) | |
2352 srv.serve_forever() | 2772 srv.serve_forever() |
2353 | 2773 |
2354 | 2774 |
2355 class CherryPyServer(ServerAdapter): | 2775 class CherryPyServer(ServerAdapter): |
2356 def run(self, handler): # pragma: no cover | 2776 def run(self, handler): # pragma: no cover |
2357 from cherrypy import wsgiserver | 2777 from cherrypy import wsgiserver |
2358 server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) | 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 | |
2359 try: | 2794 try: |
2360 server.start() | 2795 server.start() |
2361 finally: | 2796 finally: |
2362 server.stop() | 2797 server.stop() |
2363 | 2798 |
2369 | 2804 |
2370 | 2805 |
2371 class PasteServer(ServerAdapter): | 2806 class PasteServer(ServerAdapter): |
2372 def run(self, handler): # pragma: no cover | 2807 def run(self, handler): # pragma: no cover |
2373 from paste import httpserver | 2808 from paste import httpserver |
2374 if not self.quiet: | 2809 from paste.translogger import TransLogger |
2375 from paste.translogger import TransLogger | 2810 handler = TransLogger(handler, setup_console_handler=(not self.quiet)) |
2376 handler = TransLogger(handler) | |
2377 httpserver.serve(handler, host=self.host, port=str(self.port), | 2811 httpserver.serve(handler, host=self.host, port=str(self.port), |
2378 **self.options) | 2812 **self.options) |
2379 | 2813 |
2380 | 2814 |
2381 class MeinheldServer(ServerAdapter): | 2815 class MeinheldServer(ServerAdapter): |
2411 """ The super hyped asynchronous server by facebook. Untested. """ | 2845 """ The super hyped asynchronous server by facebook. Untested. """ |
2412 def run(self, handler): # pragma: no cover | 2846 def run(self, handler): # pragma: no cover |
2413 import tornado.wsgi, tornado.httpserver, tornado.ioloop | 2847 import tornado.wsgi, tornado.httpserver, tornado.ioloop |
2414 container = tornado.wsgi.WSGIContainer(handler) | 2848 container = tornado.wsgi.WSGIContainer(handler) |
2415 server = tornado.httpserver.HTTPServer(container) | 2849 server = tornado.httpserver.HTTPServer(container) |
2416 server.listen(port=self.port) | 2850 server.listen(port=self.port,address=self.host) |
2417 tornado.ioloop.IOLoop.instance().start() | 2851 tornado.ioloop.IOLoop.instance().start() |
2418 | 2852 |
2419 | 2853 |
2420 class AppEngineServer(ServerAdapter): | 2854 class AppEngineServer(ServerAdapter): |
2421 """ Adapter for Google App Engine. """ | 2855 """ Adapter for Google App Engine. """ |
2453 | 2887 |
2454 | 2888 |
2455 class GeventServer(ServerAdapter): | 2889 class GeventServer(ServerAdapter): |
2456 """ Untested. Options: | 2890 """ Untested. Options: |
2457 | 2891 |
2458 * `monkey` (default: True) fixes the stdlib to use greenthreads. | |
2459 * `fast` (default: False) uses libevent's http server, but has some | 2892 * `fast` (default: False) uses libevent's http server, but has some |
2460 issues: No streaming, no pipelining, no SSL. | 2893 issues: No streaming, no pipelining, no SSL. |
2894 * See gevent.wsgi.WSGIServer() documentation for more options. | |
2461 """ | 2895 """ |
2462 def run(self, handler): | 2896 def run(self, handler): |
2463 from gevent import wsgi as wsgi_fast, pywsgi, monkey, local | 2897 from gevent import wsgi, pywsgi, local |
2464 if self.options.get('monkey', True): | 2898 if not isinstance(threading.local(), local.local): |
2465 if not threading.local is local.local: monkey.patch_all() | 2899 msg = "Bottle requires gevent.monkey.patch_all() (before import)" |
2466 wsgi = wsgi_fast if self.options.get('fast') else pywsgi | 2900 raise RuntimeError(msg) |
2467 log = None if self.quiet else 'default' | 2901 if not self.options.pop('fast', None): wsgi = pywsgi |
2468 wsgi.WSGIServer((self.host, self.port), handler, log=log).serve_forever() | 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() | |
2469 | 2916 |
2470 | 2917 |
2471 class GunicornServer(ServerAdapter): | 2918 class GunicornServer(ServerAdapter): |
2472 """ Untested. See http://gunicorn.org/configure.html for options. """ | 2919 """ Untested. See http://gunicorn.org/configure.html for options. """ |
2473 def run(self, handler): | 2920 def run(self, handler): |
2537 'diesel': DieselServer, | 2984 'diesel': DieselServer, |
2538 'meinheld': MeinheldServer, | 2985 'meinheld': MeinheldServer, |
2539 'gunicorn': GunicornServer, | 2986 'gunicorn': GunicornServer, |
2540 'eventlet': EventletServer, | 2987 'eventlet': EventletServer, |
2541 'gevent': GeventServer, | 2988 'gevent': GeventServer, |
2989 'geventSocketIO':GeventSocketIOServer, | |
2542 'rocket': RocketServer, | 2990 'rocket': RocketServer, |
2543 'bjoern' : BjoernServer, | 2991 'bjoern' : BjoernServer, |
2544 'auto': AutoServer, | 2992 'auto': AutoServer, |
2545 } | 2993 } |
2546 | 2994 |
2588 NORUN = nr_old | 3036 NORUN = nr_old |
2589 | 3037 |
2590 _debug = debug | 3038 _debug = debug |
2591 def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, | 3039 def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, |
2592 interval=1, reloader=False, quiet=False, plugins=None, | 3040 interval=1, reloader=False, quiet=False, plugins=None, |
2593 debug=False, **kargs): | 3041 debug=None, **kargs): |
2594 """ Start a server instance. This method blocks until the server terminates. | 3042 """ Start a server instance. This method blocks until the server terminates. |
2595 | 3043 |
2596 :param app: WSGI application or target string supported by | 3044 :param app: WSGI application or target string supported by |
2597 :func:`load_app`. (default: :func:`default_app`) | 3045 :func:`load_app`. (default: :func:`default_app`) |
2598 :param server: Server adapter to use. See :data:`server_names` keys | 3046 :param server: Server adapter to use. See :data:`server_names` keys |
2631 if os.path.exists(lockfile): | 3079 if os.path.exists(lockfile): |
2632 os.unlink(lockfile) | 3080 os.unlink(lockfile) |
2633 return | 3081 return |
2634 | 3082 |
2635 try: | 3083 try: |
2636 _debug(debug) | 3084 if debug is not None: _debug(debug) |
2637 app = app or default_app() | 3085 app = app or default_app() |
2638 if isinstance(app, basestring): | 3086 if isinstance(app, basestring): |
2639 app = load_app(app) | 3087 app = load_app(app) |
2640 if not callable(app): | 3088 if not callable(app): |
2641 raise ValueError("Application is not callable: %r" % app) | 3089 raise ValueError("Application is not callable: %r" % app) |
2768 | 3216 |
2769 @classmethod | 3217 @classmethod |
2770 def search(cls, name, lookup=[]): | 3218 def search(cls, name, lookup=[]): |
2771 """ Search name in all directories specified in lookup. | 3219 """ Search name in all directories specified in lookup. |
2772 First without, then with common extensions. Return first hit. """ | 3220 First without, then with common extensions. Return first hit. """ |
2773 if os.path.isfile(name): return name | 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 | |
2774 for spath in lookup: | 3229 for spath in lookup: |
2775 fname = os.path.join(spath, name) | 3230 spath = os.path.abspath(spath) + os.sep |
2776 if os.path.isfile(fname): | 3231 fname = os.path.abspath(os.path.join(spath, name)) |
2777 return fname | 3232 if not fname.startswith(spath): continue |
3233 if os.path.isfile(fname): return fname | |
2778 for ext in cls.extensions: | 3234 for ext in cls.extensions: |
2779 if os.path.isfile('%s.%s' % (fname, ext)): | 3235 if os.path.isfile('%s.%s' % (fname, ext)): |
2780 return '%s.%s' % (fname, ext) | 3236 return '%s.%s' % (fname, ext) |
2781 | 3237 |
2782 @classmethod | 3238 @classmethod |
2797 | 3253 |
2798 def render(self, *args, **kwargs): | 3254 def render(self, *args, **kwargs): |
2799 """ Render the template with the specified local variables and return | 3255 """ Render the template with the specified local variables and return |
2800 a single byte or unicode string. If it is a byte string, the encoding | 3256 a single byte or unicode string. If it is a byte string, the encoding |
2801 must match self.encoding. This method must be thread-safe! | 3257 must match self.encoding. This method must be thread-safe! |
2802 Local variables may be provided in dictionaries (*args) | 3258 Local variables may be provided in dictionaries (args) |
2803 or directly, as keywords (**kwargs). | 3259 or directly, as keywords (kwargs). |
2804 """ | 3260 """ |
2805 raise NotImplementedError | 3261 raise NotImplementedError |
2806 | 3262 |
2807 | 3263 |
2808 class MakoTemplate(BaseTemplate): | 3264 class MakoTemplate(BaseTemplate): |
2843 self.context.vars.clear() | 3299 self.context.vars.clear() |
2844 return out | 3300 return out |
2845 | 3301 |
2846 | 3302 |
2847 class Jinja2Template(BaseTemplate): | 3303 class Jinja2Template(BaseTemplate): |
2848 def prepare(self, filters=None, tests=None, **kwargs): | 3304 def prepare(self, filters=None, tests=None, globals={}, **kwargs): |
2849 from jinja2 import Environment, FunctionLoader | 3305 from jinja2 import Environment, FunctionLoader |
2850 if 'prefix' in kwargs: # TODO: to be removed after a while | 3306 if 'prefix' in kwargs: # TODO: to be removed after a while |
2851 raise RuntimeError('The keyword argument `prefix` has been removed. ' | 3307 raise RuntimeError('The keyword argument `prefix` has been removed. ' |
2852 'Use the full jinja2 environment name line_statement_prefix instead.') | 3308 'Use the full jinja2 environment name line_statement_prefix instead.') |
2853 self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) | 3309 self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) |
2854 if filters: self.env.filters.update(filters) | 3310 if filters: self.env.filters.update(filters) |
2855 if tests: self.env.tests.update(tests) | 3311 if tests: self.env.tests.update(tests) |
3312 if globals: self.env.globals.update(globals) | |
2856 if self.source: | 3313 if self.source: |
2857 self.tpl = self.env.from_string(self.source) | 3314 self.tpl = self.env.from_string(self.source) |
2858 else: | 3315 else: |
2859 self.tpl = self.env.get_template(self.filename) | 3316 self.tpl = self.env.get_template(self.filename) |
2860 | 3317 |
2869 if not fname: return | 3326 if not fname: return |
2870 with open(fname, "rb") as f: | 3327 with open(fname, "rb") as f: |
2871 return f.read().decode(self.encoding) | 3328 return f.read().decode(self.encoding) |
2872 | 3329 |
2873 | 3330 |
2874 class SimpleTALTemplate(BaseTemplate): | |
2875 ''' Deprecated, do not use. ''' | |
2876 def prepare(self, **options): | |
2877 depr('The SimpleTAL template handler is deprecated'\ | |
2878 ' and will be removed in 0.12') | |
2879 from simpletal import simpleTAL | |
2880 if self.source: | |
2881 self.tpl = simpleTAL.compileHTMLTemplate(self.source) | |
2882 else: | |
2883 with open(self.filename, 'rb') as fp: | |
2884 self.tpl = simpleTAL.compileHTMLTemplate(tonat(fp.read())) | |
2885 | |
2886 def render(self, *args, **kwargs): | |
2887 from simpletal import simpleTALES | |
2888 for dictarg in args: kwargs.update(dictarg) | |
2889 context = simpleTALES.Context() | |
2890 for k,v in self.defaults.items(): | |
2891 context.addGlobal(k, v) | |
2892 for k,v in kwargs.items(): | |
2893 context.addGlobal(k, v) | |
2894 output = StringIO() | |
2895 self.tpl.expand(context, output) | |
2896 return output.getvalue() | |
2897 | |
2898 | |
2899 class SimpleTemplate(BaseTemplate): | 3331 class SimpleTemplate(BaseTemplate): |
2900 blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', | 3332 |
2901 'with', 'def', 'class') | 3333 def prepare(self, escape_func=html_escape, noescape=False, syntax=None, **ka): |
2902 dedent_blocks = ('elif', 'else', 'except', 'finally') | |
2903 | |
2904 @lazy_attribute | |
2905 def re_pytokens(cls): | |
2906 ''' This matches comments and all kinds of quoted strings but does | |
2907 NOT match comments (#...) within quoted strings. (trust me) ''' | |
2908 return re.compile(r''' | |
2909 (''(?!')|""(?!")|'{6}|"{6} # Empty strings (all 4 types) | |
2910 |'(?:[^\\']|\\.)+?' # Single quotes (') | |
2911 |"(?:[^\\"]|\\.)+?" # Double quotes (") | |
2912 |'{3}(?:[^\\]|\\.|\n)+?'{3} # Triple-quoted strings (') | |
2913 |"{3}(?:[^\\]|\\.|\n)+?"{3} # Triple-quoted strings (") | |
2914 |\#.* # Comments | |
2915 )''', re.VERBOSE) | |
2916 | |
2917 def prepare(self, escape_func=html_escape, noescape=False, **kwargs): | |
2918 self.cache = {} | 3334 self.cache = {} |
2919 enc = self.encoding | 3335 enc = self.encoding |
2920 self._str = lambda x: touni(x, enc) | 3336 self._str = lambda x: touni(x, enc) |
2921 self._escape = lambda x: escape_func(touni(x, enc)) | 3337 self._escape = lambda x: escape_func(touni(x, enc)) |
3338 self.syntax = syntax | |
2922 if noescape: | 3339 if noescape: |
2923 self._str, self._escape = self._escape, self._str | 3340 self._str, self._escape = self._escape, self._str |
2924 | |
2925 @classmethod | |
2926 def split_comment(cls, code): | |
2927 """ Removes comments (#...) from python code. """ | |
2928 if '#' not in code: return code | |
2929 #: Remove comments only (leave quoted strings as they are) | |
2930 subf = lambda m: '' if m.group(0)[0]=='#' else m.group(0) | |
2931 return re.sub(cls.re_pytokens, subf, code) | |
2932 | 3341 |
2933 @cached_property | 3342 @cached_property |
2934 def co(self): | 3343 def co(self): |
2935 return compile(self.code, self.filename or '<string>', 'exec') | 3344 return compile(self.code, self.filename or '<string>', 'exec') |
2936 | 3345 |
2937 @cached_property | 3346 @cached_property |
2938 def code(self): | 3347 def code(self): |
2939 stack = [] # Current Code indentation | 3348 source = self.source |
2940 lineno = 0 # Current line of code | 3349 if not source: |
2941 ptrbuffer = [] # Buffer for printable strings and token tuple instances | 3350 with open(self.filename, 'rb') as f: |
2942 codebuffer = [] # Buffer for generated python code | 3351 source = f.read() |
2943 multiline = dedent = oneline = False | 3352 try: |
2944 template = self.source or open(self.filename, 'rb').read() | 3353 source, encoding = touni(source), 'utf8' |
2945 | 3354 except UnicodeError: |
2946 def yield_tokens(line): | 3355 depr('Template encodings other than utf8 are no longer supported.') #0.11 |
2947 for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): | 3356 source, encoding = touni(source, 'latin1'), 'latin1' |
2948 if i % 2: | 3357 parser = StplParser(source, encoding=encoding, syntax=self.syntax) |
2949 if part.startswith('!'): yield 'RAW', part[1:] | 3358 code = parser.translate() |
2950 else: yield 'CMD', part | 3359 self.encoding = parser.encoding |
2951 else: yield 'TXT', part | 3360 return code |
2952 | 3361 |
2953 def flush(): # Flush the ptrbuffer | 3362 def _rebase(self, _env, _name=None, **kwargs): |
2954 if not ptrbuffer: return | 3363 if _name is None: |
2955 cline = '' | 3364 depr('Rebase function called without arguments.' |
2956 for line in ptrbuffer: | 3365 ' You were probably looking for {{base}}?', True) #0.12 |
2957 for token, value in line: | 3366 _env['_rebase'] = (_name, kwargs) |
2958 if token == 'TXT': cline += repr(value) | 3367 |
2959 elif token == 'RAW': cline += '_str(%s)' % value | 3368 def _include(self, _env, _name=None, **kwargs): |
2960 elif token == 'CMD': cline += '_escape(%s)' % value | 3369 if _name is None: |
2961 cline += ', ' | 3370 depr('Rebase function called without arguments.' |
2962 cline = cline[:-2] + '\\\n' | 3371 ' You were probably looking for {{base}}?', True) #0.12 |
2963 cline = cline[:-2] | 3372 env = _env.copy() |
2964 if cline[:-1].endswith('\\\\\\\\\\n'): | 3373 env.update(kwargs) |
2965 cline = cline[:-7] + cline[-1] # 'nobr\\\\\n' --> 'nobr' | |
2966 cline = '_printlist([' + cline + '])' | |
2967 del ptrbuffer[:] # Do this before calling code() again | |
2968 code(cline) | |
2969 | |
2970 def code(stmt): | |
2971 for line in stmt.splitlines(): | |
2972 codebuffer.append(' ' * len(stack) + line.strip()) | |
2973 | |
2974 for line in template.splitlines(True): | |
2975 lineno += 1 | |
2976 line = touni(line, self.encoding) | |
2977 sline = line.lstrip() | |
2978 if lineno <= 2: | |
2979 m = re.match(r"%\s*#.*coding[:=]\s*([-\w.]+)", sline) | |
2980 if m: self.encoding = m.group(1) | |
2981 if m: line = line.replace('coding','coding (removed)') | |
2982 if sline and sline[0] == '%' and sline[:2] != '%%': | |
2983 line = line.split('%',1)[1].lstrip() # Full line following the % | |
2984 cline = self.split_comment(line).strip() | |
2985 cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] | |
2986 flush() # You are actually reading this? Good luck, it's a mess :) | |
2987 if cmd in self.blocks or multiline: | |
2988 cmd = multiline or cmd | |
2989 dedent = cmd in self.dedent_blocks # "else:" | |
2990 if dedent and not oneline and not multiline: | |
2991 cmd = stack.pop() | |
2992 code(line) | |
2993 oneline = not cline.endswith(':') # "if 1: pass" | |
2994 multiline = cmd if cline.endswith('\\') else False | |
2995 if not oneline and not multiline: | |
2996 stack.append(cmd) | |
2997 elif cmd == 'end' and stack: | |
2998 code('#end(%s) %s' % (stack.pop(), line.strip()[3:])) | |
2999 elif cmd == 'include': | |
3000 p = cline.split(None, 2)[1:] | |
3001 if len(p) == 2: | |
3002 code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1])) | |
3003 elif p: | |
3004 code("_=_include(%s, _stdout)" % repr(p[0])) | |
3005 else: # Empty %include -> reverse of %rebase | |
3006 code("_printlist(_base)") | |
3007 elif cmd == 'rebase': | |
3008 p = cline.split(None, 2)[1:] | |
3009 if len(p) == 2: | |
3010 code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1])) | |
3011 elif p: | |
3012 code("globals()['_rebase']=(%s, {})" % repr(p[0])) | |
3013 else: | |
3014 code(line) | |
3015 else: # Line starting with text (not '%') or '%%' (escaped) | |
3016 if line.strip().startswith('%%'): | |
3017 line = line.replace('%%', '%', 1) | |
3018 ptrbuffer.append(yield_tokens(line)) | |
3019 flush() | |
3020 return '\n'.join(codebuffer) + '\n' | |
3021 | |
3022 def subtemplate(self, _name, _stdout, *args, **kwargs): | |
3023 for dictarg in args: kwargs.update(dictarg) | |
3024 if _name not in self.cache: | 3374 if _name not in self.cache: |
3025 self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) | 3375 self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) |
3026 return self.cache[_name].execute(_stdout, kwargs) | 3376 return self.cache[_name].execute(env['_stdout'], env) |
3027 | 3377 |
3028 def execute(self, _stdout, *args, **kwargs): | 3378 def execute(self, _stdout, kwargs): |
3029 for dictarg in args: kwargs.update(dictarg) | |
3030 env = self.defaults.copy() | 3379 env = self.defaults.copy() |
3380 env.update(kwargs) | |
3031 env.update({'_stdout': _stdout, '_printlist': _stdout.extend, | 3381 env.update({'_stdout': _stdout, '_printlist': _stdout.extend, |
3032 '_include': self.subtemplate, '_str': self._str, | 3382 'include': functools.partial(self._include, env), |
3033 '_escape': self._escape, 'get': env.get, | 3383 'rebase': functools.partial(self._rebase, env), '_rebase': None, |
3034 'setdefault': env.setdefault, 'defined': env.__contains__}) | 3384 '_str': self._str, '_escape': self._escape, 'get': env.get, |
3035 env.update(kwargs) | 3385 'setdefault': env.setdefault, 'defined': env.__contains__ }) |
3036 eval(self.co, env) | 3386 eval(self.co, env) |
3037 if '_rebase' in env: | 3387 if env.get('_rebase'): |
3038 subtpl, rargs = env['_rebase'] | 3388 subtpl, rargs = env.pop('_rebase') |
3039 rargs['_base'] = _stdout[:] #copy stdout | 3389 rargs['base'] = ''.join(_stdout) #copy stdout |
3040 del _stdout[:] # clear stdout | 3390 del _stdout[:] # clear stdout |
3041 return self.subtemplate(subtpl,_stdout,rargs) | 3391 return self._include(env, subtpl, **rargs) |
3042 return env | 3392 return env |
3043 | 3393 |
3044 def render(self, *args, **kwargs): | 3394 def render(self, *args, **kwargs): |
3045 """ Render the template using keyword arguments as local variables. """ | 3395 """ Render the template using keyword arguments as local variables. """ |
3046 for dictarg in args: kwargs.update(dictarg) | 3396 env = {}; stdout = [] |
3047 stdout = [] | 3397 for dictarg in args: env.update(dictarg) |
3048 self.execute(stdout, kwargs) | 3398 env.update(kwargs) |
3399 self.execute(stdout, env) | |
3049 return ''.join(stdout) | 3400 return ''.join(stdout) |
3401 | |
3402 | |
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 | |
3050 | 3570 |
3051 | 3571 |
3052 def template(*args, **kwargs): | 3572 def template(*args, **kwargs): |
3053 ''' | 3573 ''' |
3054 Get a rendered template as a string iterator. | 3574 Get a rendered template as a string iterator. |
3055 You can use a name, a filename or a template string as first parameter. | 3575 You can use a name, a filename or a template string as first parameter. |
3056 Template rendering arguments can be passed as dictionaries | 3576 Template rendering arguments can be passed as dictionaries |
3057 or directly (as keyword arguments). | 3577 or directly (as keyword arguments). |
3058 ''' | 3578 ''' |
3059 tpl = args[0] if args else None | 3579 tpl = args[0] if args else None |
3060 template_adapter = kwargs.pop('template_adapter', SimpleTemplate) | 3580 adapter = kwargs.pop('template_adapter', SimpleTemplate) |
3061 if tpl not in TEMPLATES or DEBUG: | 3581 lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) |
3582 tplid = (id(lookup), tpl) | |
3583 if tplid not in TEMPLATES or DEBUG: | |
3062 settings = kwargs.pop('template_settings', {}) | 3584 settings = kwargs.pop('template_settings', {}) |
3063 lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) | 3585 if isinstance(tpl, adapter): |
3064 if isinstance(tpl, template_adapter): | 3586 TEMPLATES[tplid] = tpl |
3065 TEMPLATES[tpl] = tpl | 3587 if settings: TEMPLATES[tplid].prepare(**settings) |
3066 if settings: TEMPLATES[tpl].prepare(**settings) | |
3067 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: | 3588 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: |
3068 TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings) | 3589 TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) |
3069 else: | 3590 else: |
3070 TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings) | 3591 TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) |
3071 if not TEMPLATES[tpl]: | 3592 if not TEMPLATES[tplid]: |
3072 abort(500, 'Template (%s) not found' % tpl) | 3593 abort(500, 'Template (%s) not found' % tpl) |
3073 for dictarg in args[1:]: kwargs.update(dictarg) | 3594 for dictarg in args[1:]: kwargs.update(dictarg) |
3074 return TEMPLATES[tpl].render(kwargs) | 3595 return TEMPLATES[tplid].render(kwargs) |
3075 | 3596 |
3076 mako_template = functools.partial(template, template_adapter=MakoTemplate) | 3597 mako_template = functools.partial(template, template_adapter=MakoTemplate) |
3077 cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) | 3598 cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) |
3078 jinja2_template = functools.partial(template, template_adapter=Jinja2Template) | 3599 jinja2_template = functools.partial(template, template_adapter=Jinja2Template) |
3079 simpletal_template = functools.partial(template, template_adapter=SimpleTALTemplate) | |
3080 | 3600 |
3081 | 3601 |
3082 def view(tpl_name, **defaults): | 3602 def view(tpl_name, **defaults): |
3083 ''' Decorator: renders a template for a handler. | 3603 ''' Decorator: renders a template for a handler. |
3084 The handler can control its behavior like that: | 3604 The handler can control its behavior like that: |
3095 result = func(*args, **kwargs) | 3615 result = func(*args, **kwargs) |
3096 if isinstance(result, (dict, DictMixin)): | 3616 if isinstance(result, (dict, DictMixin)): |
3097 tplvars = defaults.copy() | 3617 tplvars = defaults.copy() |
3098 tplvars.update(result) | 3618 tplvars.update(result) |
3099 return template(tpl_name, **tplvars) | 3619 return template(tpl_name, **tplvars) |
3620 elif result is None: | |
3621 return template(tpl_name, defaults) | |
3100 return result | 3622 return result |
3101 return wrapper | 3623 return wrapper |
3102 return decorator | 3624 return decorator |
3103 | 3625 |
3104 mako_view = functools.partial(view, template_adapter=MakoTemplate) | 3626 mako_view = functools.partial(view, template_adapter=MakoTemplate) |
3105 cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) | 3627 cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) |
3106 jinja2_view = functools.partial(view, template_adapter=Jinja2Template) | 3628 jinja2_view = functools.partial(view, template_adapter=Jinja2Template) |
3107 simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate) | |
3108 | 3629 |
3109 | 3630 |
3110 | 3631 |
3111 | 3632 |
3112 | 3633 |
3122 NORUN = False # If set, run() does nothing. Used by load_app() | 3643 NORUN = False # If set, run() does nothing. Used by load_app() |
3123 | 3644 |
3124 #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') | 3645 #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') |
3125 HTTP_CODES = httplib.responses | 3646 HTTP_CODES = httplib.responses |
3126 HTTP_CODES[418] = "I'm a teapot" # RFC 2324 | 3647 HTTP_CODES[418] = "I'm a teapot" # RFC 2324 |
3648 HTTP_CODES[422] = "Unprocessable Entity" # RFC 4918 | |
3127 HTTP_CODES[428] = "Precondition Required" | 3649 HTTP_CODES[428] = "Precondition Required" |
3128 HTTP_CODES[429] = "Too Many Requests" | 3650 HTTP_CODES[429] = "Too Many Requests" |
3129 HTTP_CODES[431] = "Request Header Fields Too Large" | 3651 HTTP_CODES[431] = "Request Header Fields Too Large" |
3130 HTTP_CODES[511] = "Network Authentication Required" | 3652 HTTP_CODES[511] = "Network Authentication Required" |
3131 _HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items()) | 3653 _HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items()) |
3132 | 3654 |
3133 #: The default template used for error pages. Override with @error() | 3655 #: The default template used for error pages. Override with @error() |
3134 ERROR_PAGE_TEMPLATE = """ | 3656 ERROR_PAGE_TEMPLATE = """ |
3135 %%try: | 3657 %%try: |
3136 %%from %s import DEBUG, HTTP_CODES, request, touni | 3658 %%from %s import DEBUG, HTTP_CODES, request, touni |
3137 %%status_name = HTTP_CODES.get(e.status, 'Unknown').title() | |
3138 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> | 3659 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> |
3139 <html> | 3660 <html> |
3140 <head> | 3661 <head> |
3141 <title>Error {{e.status}}: {{status_name}}</title> | 3662 <title>Error: {{e.status}}</title> |
3142 <style type="text/css"> | 3663 <style type="text/css"> |
3143 html {background-color: #eee; font-family: sans;} | 3664 html {background-color: #eee; font-family: sans;} |
3144 body {background-color: #fff; border: 1px solid #ddd; | 3665 body {background-color: #fff; border: 1px solid #ddd; |
3145 padding: 15px; margin: 15px;} | 3666 padding: 15px; margin: 15px;} |
3146 pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;} | 3667 pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;} |
3147 </style> | 3668 </style> |
3148 </head> | 3669 </head> |
3149 <body> | 3670 <body> |
3150 <h1>Error {{e.status}}: {{status_name}}</h1> | 3671 <h1>Error: {{e.status}}</h1> |
3151 <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> | 3672 <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> |
3152 caused an error:</p> | 3673 caused an error:</p> |
3153 <pre>{{e.output}}</pre> | 3674 <pre>{{e.body}}</pre> |
3154 %%if DEBUG and e.exception: | 3675 %%if DEBUG and e.exception: |
3155 <h2>Exception:</h2> | 3676 <h2>Exception:</h2> |
3156 <pre>{{repr(e.exception)}}</pre> | 3677 <pre>{{repr(e.exception)}}</pre> |
3157 %%end | 3678 %%end |
3158 %%if DEBUG and e.traceback: | 3679 %%if DEBUG and e.traceback: |
3165 <b>ImportError:</b> Could not generate the error page. Please add bottle to | 3686 <b>ImportError:</b> Could not generate the error page. Please add bottle to |
3166 the import path. | 3687 the import path. |
3167 %%end | 3688 %%end |
3168 """ % __name__ | 3689 """ % __name__ |
3169 | 3690 |
3170 #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a | 3691 #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a |
3171 #: request callback, this instance always refers to the *current* request | 3692 #: request callback, this instance always refers to the *current* request |
3172 #: (even on a multithreaded server). | 3693 #: (even on a multithreaded server). |
3173 request = LocalRequest() | 3694 request = LocalRequest() |
3174 | 3695 |
3175 #: A thread-safe instance of :class:`LocalResponse`. It is used to change the | 3696 #: A thread-safe instance of :class:`LocalResponse`. It is used to change the |
3184 app = default_app = AppStack() | 3705 app = default_app = AppStack() |
3185 app.push() | 3706 app.push() |
3186 | 3707 |
3187 #: A virtual package that redirects import statements. | 3708 #: A virtual package that redirects import statements. |
3188 #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. | 3709 #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. |
3189 ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module | 3710 ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__+".ext", 'bottle_%s').module |
3190 | 3711 |
3191 if __name__ == '__main__': | 3712 if __name__ == '__main__': |
3192 opt, args, parser = _cmd_options, _cmd_args, _cmd_parser | 3713 opt, args, parser = _cmd_options, _cmd_args, _cmd_parser |
3193 if opt.version: | 3714 if opt.version: |
3194 _stdout('Bottle %s\n'%__version__) | 3715 _stdout('Bottle %s\n'%__version__) |
3200 | 3721 |
3201 sys.path.insert(0, '.') | 3722 sys.path.insert(0, '.') |
3202 sys.modules.setdefault('bottle', sys.modules['__main__']) | 3723 sys.modules.setdefault('bottle', sys.modules['__main__']) |
3203 | 3724 |
3204 host, port = (opt.bind or 'localhost'), 8080 | 3725 host, port = (opt.bind or 'localhost'), 8080 |
3205 if ':' in host: | 3726 if ':' in host and host.rfind(']') < host.rfind(':'): |
3206 host, port = host.rsplit(':', 1) | 3727 host, port = host.rsplit(':', 1) |
3207 | 3728 host = host.strip('[]') |
3208 run(args[0], host=host, port=port, server=opt.server, | 3729 |
3730 run(args[0], host=host, port=int(port), server=opt.server, | |
3209 reloader=opt.reload, plugins=opt.plugin, debug=opt.debug) | 3731 reloader=opt.reload, plugins=opt.plugin, debug=opt.debug) |
3210 | 3732 |
3211 | 3733 |
3212 | 3734 |
3213 | 3735 |