comparison web/bottle.py @ 27:dbbd503119ba

Add some web server handling
author Matt Johnston <matt@ucc.asn.au>
date Tue, 12 Jun 2012 00:09:09 +0800
parents
children 317a1738f15c
comparison
equal deleted inserted replaced
26:d3e5934fe55c 27:dbbd503119ba
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 Bottle is a fast and simple micro-framework for small web applications. It
5 offers request dispatching (Routes) with url parameter support, templates,
6 a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
7 template engines - all in a single file and with no dependencies other than the
8 Python Standard Library.
9
10 Homepage and documentation: http://bottlepy.org/
11
12 Copyright (c) 2011, Marcel Hellkamp.
13 License: MIT (see LICENSE for details)
14 """
15
16 from __future__ import with_statement
17
18 __author__ = 'Marcel Hellkamp'
19 __version__ = '0.11.dev'
20 __license__ = 'MIT'
21
22 # The gevent server adapter needs to patch some modules before they are imported
23 # This is why we parse the commandline parameters here but handle them later
24 if __name__ == '__main__':
25 from optparse import OptionParser
26 _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app")
27 _opt = _cmd_parser.add_option
28 _opt("--version", action="store_true", help="show version number.")
29 _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
30 _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
31 _opt("-p", "--plugin", action="append", help="install additional plugin/s.")
32 _opt("--debug", action="store_true", help="start server in debug mode.")
33 _opt("--reload", action="store_true", help="auto-reload on file changes.")
34 _cmd_options, _cmd_args = _cmd_parser.parse_args()
35 if _cmd_options.server and _cmd_options.server.startswith('gevent'):
36 import gevent.monkey; gevent.monkey.patch_all()
37
38 import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\
39 os, re, subprocess, sys, tempfile, threading, time, urllib, warnings
40
41 from datetime import date as datedate, datetime, timedelta
42 from tempfile import TemporaryFile
43 from traceback import format_exc, print_exc
44
45 try: from json import dumps as json_dumps, loads as json_lds
46 except ImportError: # pragma: no cover
47 try: from simplejson import dumps as json_dumps, loads as json_lds
48 except ImportError:
49 try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds
50 except ImportError:
51 def json_dumps(data):
52 raise ImportError("JSON support requires Python 2.6 or simplejson.")
53 json_lds = json_dumps
54
55
56
57 # 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.
59
60 py = sys.version_info
61 py3k = py >= (3,0,0)
62 py25 = py < (2,6,0)
63
64 # Workaround for the missing "as" keyword in py3k.
65 def _e(): return sys.exc_info()[1]
66
67 # Workaround for the "print is a keyword/function" Python 2/3 dilemma
68 # and a fallback for mod_wsgi (resticts stdout/err attribute access)
69 try:
70 _stdout, _stderr = sys.stdout.write, sys.stderr.write
71 except IOError:
72 _stdout = lambda x: sys.stdout.write(x)
73 _stderr = lambda x: sys.stderr.write(x)
74
75 # Lots of stdlib and builtin differences.
76 if py3k:
77 import http.client as httplib
78 import _thread as thread
79 from urllib.parse import urljoin, parse_qsl, SplitResult as UrlSplitResult
80 from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
81 from http.cookies import SimpleCookie
82 from collections import MutableMapping as DictMixin
83 import pickle
84 from io import BytesIO
85 basestring = str
86 unicode = str
87 json_loads = lambda s: json_lds(touni(s))
88 callable = lambda x: hasattr(x, '__call__')
89 imap = map
90 else: # 2.x
91 import httplib
92 import thread
93 from urlparse import urljoin, SplitResult as UrlSplitResult
94 from urllib import urlencode, quote as urlquote, unquote as urlunquote
95 from Cookie import SimpleCookie
96 from itertools import imap
97 import cPickle as pickle
98 from StringIO import StringIO as BytesIO
99 if py25:
100 msg = "Python 2.5 support may be dropped in future versions of Bottle."
101 warnings.warn(msg, DeprecationWarning)
102 from cgi import parse_qsl
103 from UserDict import DictMixin
104 def next(it): return it.next()
105 bytes = str
106 else: # 2.6, 2.7
107 from urlparse import parse_qsl
108 from collections import MutableMapping as DictMixin
109 json_loads = json_lds
110
111 # Some helpers for string/byte handling
112 def tob(s, enc='utf8'):
113 return s.encode(enc) if isinstance(s, unicode) else bytes(s)
114 def touni(s, enc='utf8', err='strict'):
115 return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)
116 tonat = touni if py3k else tob
117
118 # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense).
119 # 3.1 needs a workaround.
120 NCTextIOWrapper = None
121 if (3,0,0) < py < (3,2,0):
122 from io import TextIOWrapper
123 class NCTextIOWrapper(TextIOWrapper):
124 def close(self): pass # Keep wrapped buffer open.
125
126 # A bug in functools causes it to break if the wrapper is an instance method
127 def update_wrapper(wrapper, wrapped, *a, **ka):
128 try: functools.update_wrapper(wrapper, wrapped, *a, **ka)
129 except AttributeError: pass
130
131
132
133 # 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.
135
136 def depr(message):
137 warnings.warn(message, DeprecationWarning, stacklevel=3)
138
139 def makelist(data): # This is just to handy
140 if isinstance(data, (tuple, list, set, dict)): return list(data)
141 elif data: return [data]
142 else: return []
143
144
145 class DictProperty(object):
146 ''' Property that maps to a key in a local dict-like attribute. '''
147 def __init__(self, attr, key=None, read_only=False):
148 self.attr, self.key, self.read_only = attr, key, read_only
149
150 def __call__(self, func):
151 functools.update_wrapper(self, func, updated=[])
152 self.getter, self.key = func, self.key or func.__name__
153 return self
154
155 def __get__(self, obj, cls):
156 if obj is None: return self
157 key, storage = self.key, getattr(obj, self.attr)
158 if key not in storage: storage[key] = self.getter(obj)
159 return storage[key]
160
161 def __set__(self, obj, value):
162 if self.read_only: raise AttributeError("Read-Only property.")
163 getattr(obj, self.attr)[self.key] = value
164
165 def __delete__(self, obj):
166 if self.read_only: raise AttributeError("Read-Only property.")
167 del getattr(obj, self.attr)[self.key]
168
169
170 class cached_property(object):
171 ''' A property that is only computed once per instance and then replaces
172 itself with an ordinary attribute. Deleting the attribute resets the
173 property. '''
174
175 def __init__(self, func):
176 self.func = func
177
178 def __get__(self, obj, cls):
179 if obj is None: return self
180 value = obj.__dict__[self.func.__name__] = self.func(obj)
181 return value
182
183
184 class lazy_attribute(object):
185 ''' A property that caches itself to the class object. '''
186 def __init__(self, func):
187 functools.update_wrapper(self, func, updated=[])
188 self.getter = func
189
190 def __get__(self, obj, cls):
191 value = self.getter(cls)
192 setattr(cls, self.__name__, value)
193 return value
194
195
196
197
198
199
200 ###############################################################################
201 # Exceptions and Events ########################################################
202 ###############################################################################
203
204
205 class BottleException(Exception):
206 """ A base class for exceptions used by bottle. """
207 pass
208
209
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
239
240
241
242 ###############################################################################
243 # Routing ######################################################################
244 ###############################################################################
245
246
247 class RouteError(BottleException):
248 """ This is a base class for all routing related exceptions """
249
250
251 class RouteReset(BottleException):
252 """ If raised by a plugin or request handler, the route is reset and all
253 plugins are re-applied. """
254
255 class RouterUnknownModeError(RouteError): pass
256
257
258 class RouteSyntaxError(RouteError):
259 """ The route parser found something not supported by this router """
260
261
262 class RouteBuildError(RouteError):
263 """ The route could not been built """
264
265
266 class Router(object):
267 ''' 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
269 the first target that satisfies the request. The target may be anything,
270 usually a string, ID or callable object. A route consists of a path-rule
271 and a HTTP method.
272
273 The path-rule is either a static path (e.g. `/contact`) or a dynamic
274 path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
275 and details on the matching order are described in docs:`routing`.
276 '''
277
278 default_pattern = '[^/]+'
279 default_filter = 're'
280 #: Sorry for the mess. It works. Trust me.
281 rule_syntax = re.compile('(\\\\*)'\
282 '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\
283 '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\
284 '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))')
285
286 def __init__(self, strict=False):
287 self.rules = {} # A {rule: Rule} mapping
288 self.builder = {} # A rule/name->build_info mapping
289 self.static = {} # Cache for static routes: {path: {method: target}}
290 self.dynamic = [] # Cache for dynamic routes. See _compile()
291 #: If true, static routes are no longer checked first.
292 self.strict_order = strict
293 self.filters = {'re': self.re_filter, 'int': self.int_filter,
294 'float': self.float_filter, 'path': self.path_filter}
295
296 def re_filter(self, conf):
297 return conf or self.default_pattern, None, None
298
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
308 def add_filter(self, name, func):
309 ''' 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.
311 The first element is a string, the last two are callables or None. '''
312 self.filters[name] = func
313
314 def parse_rule(self, rule):
315 ''' Parses a rule into a (name, filter, conf) token stream. If mode is
316 None, name contains a static rule part. '''
317 offset, prefix = 0, ''
318 for match in self.rule_syntax.finditer(rule):
319 prefix += rule[offset:match.start()]
320 g = match.groups()
321 if len(g[0])%2: # Escaped wildcard
322 prefix += match.group(0)[len(g[0]):]
323 offset = match.end()
324 continue
325 if prefix: yield prefix, None, None
326 name, filtr, conf = g[1:4] if not g[2] is None else g[4:7]
327 if not filtr: filtr = self.default_filter
328 yield name, filtr, conf or None
329 offset, prefix = match.end(), ''
330 if offset <= len(rule) or prefix:
331 yield prefix+rule[offset:], None, None
332
333 def add(self, rule, method, target, name=None):
334 ''' Add a new route or replace the target for an existing route. '''
335 if rule in self.rules:
336 self.rules[rule][method] = target
337 if name: self.builder[name] = self.builder[rule]
338 return
339
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
348 for key, mode, conf in self.parse_rule(rule):
349 if mode:
350 is_static = False
351 mask, in_filter, out_filter = self.filters[mode](conf)
352 if key:
353 pattern += '(?P<%s>%s)' % (key, mask)
354 else:
355 pattern += '(?:%s)' % mask
356 key = 'anon%d' % anons; anons += 1
357 if in_filter: filters.append((key, in_filter))
358 builder.append((key, out_filter or str))
359 elif key:
360 pattern += re.escape(key)
361 builder.append((None, key))
362 self.builder[rule] = builder
363 if name: self.builder[name] = builder
364
365 if is_static and not self.strict_order:
366 self.static[self.build(rule)] = target
367 return
368
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:
374 re_match = re.compile('^(%s)$' % pattern).match
375 except re.error:
376 raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e()))
377
378 def match(path):
379 """ Return an url-argument dictionary. """
380 url_args = re_match(path).groupdict()
381 for name, wildcard_filter in filters:
382 try:
383 url_args[name] = wildcard_filter(url_args[name])
384 except ValueError:
385 raise HTTPError(400, 'Path has wrong format.')
386 return url_args
387
388 try:
389 combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern)
390 self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
391 self.dynamic[-1][1].append((match, target))
392 except (AssertionError, IndexError): # AssertionError: Too many groups
393 self.dynamic.append((re.compile('(^%s$)' % flat_pattern),
394 [(match, target)]))
395 return match
396
397 def build(self, _name, *anons, **query):
398 ''' Build an URL by filling the wildcards in a rule. '''
399 builder = self.builder.get(_name)
400 if not builder: raise RouteBuildError("No route with that name.", _name)
401 try:
402 for i, value in enumerate(anons): query['anon%d'%i] = value
403 url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder])
404 return url if not query else url+'?'+urlencode(query)
405 except KeyError:
406 raise RouteBuildError('Missing URL argument: %r' % _e().args[0])
407
408 def match(self, environ):
409 ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). '''
410 path, targets, urlargs = environ['PATH_INFO'] or '/', None, {}
411 if path in self.static:
412 targets = self.static[path]
413 else:
414 for combined, rules in self.dynamic:
415 match = combined.match(path)
416 if not match: continue
417 getargs, targets = rules[match.lastindex - 1]
418 urlargs = getargs(path) if getargs else {}
419 break
420
421 if not targets:
422 raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO']))
423 method = environ['REQUEST_METHOD'].upper()
424 if method in targets:
425 return targets[method], urlargs
426 if method == 'HEAD' and 'GET' in targets:
427 return targets['GET'], urlargs
428 if 'ANY' in targets:
429 return targets['ANY'], urlargs
430 allowed = [verb for verb in targets if verb != 'ANY']
431 if 'GET' in allowed and 'HEAD' not in allowed:
432 allowed.append('HEAD')
433 raise HTTPError(405, "Method not allowed.",
434 header=[('Allow',",".join(allowed))])
435
436
437 class Route(object):
438 ''' This class wraps a route callback along with route specific metadata and
439 configuration and applies Plugins on demand. It is also responsible for
440 turing an URL path rule into a regular expression usable by the Router.
441 '''
442
443 def __init__(self, app, rule, method, callback, name=None,
444 plugins=None, skiplist=None, **config):
445 #: The application this route is installed to.
446 self.app = app
447 #: The path-rule string (e.g. ``/wiki/:page``).
448 self.rule = rule
449 #: The HTTP method as a string (e.g. ``GET``).
450 self.method = method
451 #: The original callback with no plugins applied. Useful for introspection.
452 self.callback = callback
453 #: The name of the route (if specified) or ``None``.
454 self.name = name or None
455 #: A list of route-specific plugins (see :meth:`Bottle.route`).
456 self.plugins = plugins or []
457 #: A list of plugins to not apply to this route (see :meth:`Bottle.route`).
458 self.skiplist = skiplist or []
459 #: Additional keyword arguments passed to the :meth:`Bottle.route`
460 #: decorator are stored in this dictionary. Used for route-specific
461 #: plugin configuration and meta-data.
462 self.config = ConfigDict(config)
463
464 def __call__(self, *a, **ka):
465 depr("Some APIs changed to return Route() instances instead of"\
466 " callables. Make sure to use the Route.call method and not to"\
467 " call Route instances directly.")
468 return self.call(*a, **ka)
469
470 @cached_property
471 def call(self):
472 ''' The route callback with all plugins applied. This property is
473 created on demand and then cached to speed up subsequent requests.'''
474 return self._make_callback()
475
476 def reset(self):
477 ''' Forget any cached values. The next time :attr:`call` is accessed,
478 all plugins are re-applied. '''
479 self.__dict__.pop('call', None)
480
481 def prepare(self):
482 ''' Do all on-demand work immediately (useful for debugging).'''
483 self.call
484
485 @property
486 def _context(self):
487 depr('Switch to Plugin API v2 and access the Route object directly.')
488 return dict(rule=self.rule, method=self.method, callback=self.callback,
489 name=self.name, app=self.app, config=self.config,
490 apply=self.plugins, skip=self.skiplist)
491
492 def all_plugins(self):
493 ''' Yield all Plugins affecting this route. '''
494 unique = set()
495 for p in reversed(self.app.plugins + self.plugins):
496 if True in self.skiplist: break
497 name = getattr(p, 'name', False)
498 if name and (name in self.skiplist or name in unique): continue
499 if p in self.skiplist or type(p) in self.skiplist: continue
500 if name: unique.add(name)
501 yield p
502
503 def _make_callback(self):
504 callback = self.callback
505 for plugin in self.all_plugins():
506 try:
507 if hasattr(plugin, 'apply'):
508 api = getattr(plugin, 'api', 1)
509 context = self if api > 1 else self._context
510 callback = plugin.apply(callback, context)
511 else:
512 callback = plugin(callback)
513 except RouteReset: # Try again with changed configuration.
514 return self._make_callback()
515 if not callback is self.callback:
516 update_wrapper(callback, self.callback)
517 return callback
518
519 def __repr__(self):
520 return '<%s %r %r>' % (self.method, self.rule, self.callback)
521
522
523
524
525
526
527 ###############################################################################
528 # Application Object ###########################################################
529 ###############################################################################
530
531
532 class Bottle(object):
533 """ Each Bottle object represents a single, distinct web application and
534 consists of routes, callbacks, plugins, resources and configuration.
535 Instances are callable WSGI applications.
536
537 :param catchall: If true (default), handle all exceptions. Turn off to
538 let debugging middleware handle exceptions.
539 """
540
541 def __init__(self, catchall=True, autojson=True):
542 #: If true, most exceptions are caught and returned as :exc:`HTTPError`
543 self.catchall = catchall
544
545 #: A :cls:`ResourceManager` for application files
546 self.resources = ResourceManager()
547
548 #: A :cls:`ConfigDict` for app specific configuration.
549 self.config = ConfigDict()
550 self.config.autojson = autojson
551
552 self.routes = [] # List of installed :class:`Route` instances.
553 self.router = Router() # Maps requests to :class:`Route` instances.
554 self.error_handler = {}
555
556 # Core plugins
557 self.plugins = [] # List of installed plugins.
558 self.hooks = HooksPlugin()
559 self.install(self.hooks)
560 if self.config.autojson:
561 self.install(JSONPlugin())
562 self.install(TemplatePlugin())
563
564
565 def mount(self, prefix, app, **options):
566 ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific
567 URL prefix. Example::
568
569 root_app.mount('/admin/', admin_app)
570
571 :param prefix: path prefix or `mount-point`. If it ends in a slash,
572 that slash is mandatory.
573 :param app: an instance of :class:`Bottle` or a WSGI application.
574
575 All other parameters are passed to the underlying :meth:`route` call.
576 '''
577 if isinstance(app, basestring):
578 prefix, app = app, prefix
579 depr('Parameter order of Bottle.mount() changed.') # 0.10
580
581 segments = [p for p in prefix.split('/') if p]
582 if not segments: raise ValueError('Empty path prefix.')
583 path_depth = len(segments)
584
585 def mountpoint_wrapper():
586 try:
587 request.path_shift(path_depth)
588 rs = BaseResponse([], 200)
589 def start_response(status, header):
590 rs.status = status
591 for name, value in header: rs.add_header(name, value)
592 return rs.body.append
593 body = app(request.environ, start_response)
594 body = itertools.chain(rs.body, body)
595 return HTTPResponse(body, rs.status_code, rs.headers)
596 finally:
597 request.path_shift(-path_depth)
598
599 options.setdefault('skip', True)
600 options.setdefault('method', 'ANY')
601 options.setdefault('mountpoint', {'prefix': prefix, 'target': app})
602 options['callback'] = mountpoint_wrapper
603
604 self.route('/%s/<:re:.*>' % '/'.join(segments), **options)
605 if not prefix.endswith('/'):
606 self.route('/' + '/'.join(segments), **options)
607
608 def merge(self, routes):
609 ''' Merge the routes of another :class:`Bottle` application or a list of
610 :class:`Route` objects into this application. The routes keep their
611 'owner', meaning that the :data:`Route.app` attribute is not
612 changed. '''
613 if isinstance(routes, Bottle):
614 routes = routes.routes
615 for route in routes:
616 self.add_route(route)
617
618 def install(self, plugin):
619 ''' Add a plugin to the list of plugins and prepare it for being
620 applied to all routes of this application. A plugin may be a simple
621 decorator or an object that implements the :class:`Plugin` API.
622 '''
623 if hasattr(plugin, 'setup'): plugin.setup(self)
624 if not callable(plugin) and not hasattr(plugin, 'apply'):
625 raise TypeError("Plugins must be callable or implement .apply()")
626 self.plugins.append(plugin)
627 self.reset()
628 return plugin
629
630 def uninstall(self, plugin):
631 ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type
632 object to remove all plugins that match that type, a string to remove
633 all plugins with a matching ``name`` attribute or ``True`` to remove all
634 plugins. Return the list of removed plugins. '''
635 removed, remove = [], plugin
636 for i, plugin in list(enumerate(self.plugins))[::-1]:
637 if remove is True or remove is plugin or remove is type(plugin) \
638 or getattr(plugin, 'name', True) == remove:
639 removed.append(plugin)
640 del self.plugins[i]
641 if hasattr(plugin, 'close'): plugin.close()
642 if removed: self.reset()
643 return removed
644
645 def run(self, **kwargs):
646 ''' Calls :func:`run` with the same parameters. '''
647 run(self, **kwargs)
648
649 def reset(self, route=None):
650 ''' 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
652 is affected. '''
653 if route is None: routes = self.routes
654 elif isinstance(route, Route): routes = [route]
655 else: routes = [self.routes[route]]
656 for route in routes: route.reset()
657 if DEBUG:
658 for route in routes: route.prepare()
659 self.hooks.trigger('app_reset')
660
661 def close(self):
662 ''' Close the application and all installed plugins. '''
663 for plugin in self.plugins:
664 if hasattr(plugin, 'close'): plugin.close()
665 self.stopped = True
666
667 def match(self, environ):
668 """ Search for a matching route and return a (:class:`Route` , urlargs)
669 tuple. The second value is a dictionary with parameters extracted
670 from the URL. Raise :exc:`HTTPError` (404/405) on a non-match."""
671 return self.router.match(environ)
672
673 def get_url(self, routename, **kargs):
674 """ Return a string that matches a named route """
675 scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
676 location = self.router.build(routename, **kargs).lstrip('/')
677 return urljoin(urljoin('/', scriptname), location)
678
679 def add_route(self, route):
680 ''' Add a route object, but do not change the :data:`Route.app`
681 attribute.'''
682 self.routes.append(route)
683 self.router.add(route.rule, route.method, route, name=route.name)
684 if DEBUG: route.prepare()
685
686 def route(self, path=None, method='GET', callback=None, name=None,
687 apply=None, skip=None, **config):
688 """ A decorator to bind a function to a request URL. Example::
689
690 @app.route('/hello/:name')
691 def hello(name):
692 return 'Hello %s' % name
693
694 The ``:name`` part is a wildcard. See :class:`Router` for syntax
695 details.
696
697 :param path: Request path or a list of paths to listen to. If no
698 path is specified, it is automatically generated from the
699 signature of the function.
700 :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of
701 methods to listen to. (default: `GET`)
702 :param callback: An optional shortcut to avoid the decorator
703 syntax. ``route(..., callback=func)`` equals ``route(...)(func)``
704 :param name: The name for this route. (default: None)
705 :param apply: A decorator or plugin or a list of plugins. These are
706 applied to the route callback in addition to installed plugins.
707 :param skip: A list of plugins, plugin classes or names. Matching
708 plugins are not installed to this route. ``True`` skips all.
709
710 Any additional keyword arguments are stored as route-specific
711 configuration and passed to plugins (see :meth:`Plugin.apply`).
712 """
713 if callable(path): path, callback = None, path
714 plugins = makelist(apply)
715 skiplist = makelist(skip)
716 def decorator(callback):
717 # TODO: Documentation and tests
718 if isinstance(callback, basestring): callback = load(callback)
719 for rule in makelist(path) or yieldroutes(callback):
720 for verb in makelist(method):
721 verb = verb.upper()
722 route = Route(self, rule, verb, callback, name=name,
723 plugins=plugins, skiplist=skiplist, **config)
724 self.add_route(route)
725 return callback
726 return decorator(callback) if callback else decorator
727
728 def get(self, path=None, method='GET', **options):
729 """ Equals :meth:`route`. """
730 return self.route(path, method, **options)
731
732 def post(self, path=None, method='POST', **options):
733 """ Equals :meth:`route` with a ``POST`` method parameter. """
734 return self.route(path, method, **options)
735
736 def put(self, path=None, method='PUT', **options):
737 """ Equals :meth:`route` with a ``PUT`` method parameter. """
738 return self.route(path, method, **options)
739
740 def delete(self, path=None, method='DELETE', **options):
741 """ Equals :meth:`route` with a ``DELETE`` method parameter. """
742 return self.route(path, method, **options)
743
744 def error(self, code=500):
745 """ Decorator: Register an output handler for a HTTP error code"""
746 def wrapper(handler):
747 self.error_handler[int(code)] = handler
748 return handler
749 return wrapper
750
751 def hook(self, name):
752 """ Return a decorator that attaches a callback to a hook. Three hooks
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
775 def _handle(self, environ):
776 try:
777 environ['bottle.app'] = self
778 request.bind(environ)
779 response.bind()
780 route, args = self.router.match(environ)
781 environ['route.handle'] = environ['bottle.route'] = route
782 environ['route.url_args'] = args
783 return route.call(**args)
784 except HTTPResponse:
785 return _e()
786 except RouteReset:
787 route.reset()
788 return self._handle(environ)
789 except (KeyboardInterrupt, SystemExit, MemoryError):
790 raise
791 except Exception:
792 if not self.catchall: raise
793 stacktrace = format_exc()
794 environ['wsgi.errors'].write(stacktrace)
795 return HTTPError(500, "Internal Server Error", _e(), stacktrace)
796
797 def _cast(self, out, peek=None):
798 """ Try to convert the parameter into something WSGI compatible and set
799 correct HTTP headers when possible.
800 Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
801 iterable of strings and iterable of unicodes
802 """
803
804 # Empty output is done here
805 if not out:
806 response['Content-Length'] = 0
807 return []
808 # Join lists of byte or unicode strings. Mixed lists are NOT supported
809 if isinstance(out, (tuple, list))\
810 and isinstance(out[0], (bytes, unicode)):
811 out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
812 # Encode unicode strings
813 if isinstance(out, unicode):
814 out = out.encode(response.charset)
815 # Byte Strings are just returned
816 if isinstance(out, bytes):
817 response['Content-Length'] = len(out)
818 return [out]
819 # HTTPError or HTTPException (recursive, because they may wrap anything)
820 # TODO: Handle these explicitly in handle() or make them iterable.
821 if isinstance(out, HTTPError):
822 out.apply(response)
823 out = self.error_handler.get(out.status, repr)(out)
824 if isinstance(out, HTTPResponse):
825 depr('Error handlers must not return :exc:`HTTPResponse`.') #0.9
826 return self._cast(out)
827 if isinstance(out, HTTPResponse):
828 out.apply(response)
829 return self._cast(out.output)
830
831 # File-like objects.
832 if hasattr(out, 'read'):
833 if 'wsgi.file_wrapper' in request.environ:
834 return request.environ['wsgi.file_wrapper'](out)
835 elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
836 return WSGIFileWrapper(out)
837
838 # Handle Iterables. We peek into them to detect their inner type.
839 try:
840 out = iter(out)
841 first = next(out)
842 while not first:
843 first = next(out)
844 except StopIteration:
845 return self._cast('')
846 except HTTPResponse:
847 first = _e()
848 except (KeyboardInterrupt, SystemExit, MemoryError):
849 raise
850 except Exception:
851 if not self.catchall: raise
852 first = HTTPError(500, 'Unhandled exception', _e(), format_exc())
853
854 # These are the inner types allowed in iterator or generator objects.
855 if isinstance(first, HTTPResponse):
856 return self._cast(first)
857 if isinstance(first, bytes):
858 return itertools.chain([first], out)
859 if isinstance(first, unicode):
860 return imap(lambda x: x.encode(response.charset),
861 itertools.chain([first], out))
862 return self._cast(HTTPError(500, 'Unsupported response type: %s'\
863 % type(first)))
864
865 def wsgi(self, environ, start_response):
866 """ The bottle WSGI-interface. """
867 try:
868 out = self._cast(self._handle(environ))
869 # rfc2616 section 4.3
870 if response._status_code in (100, 101, 204, 304)\
871 or request.method == 'HEAD':
872 if hasattr(out, 'close'): out.close()
873 out = []
874 if isinstance(response._status_line, unicode):
875 response._status_line = str(response._status_line)
876 start_response(response._status_line, list(response.iter_headers()))
877 return out
878 except (KeyboardInterrupt, SystemExit, MemoryError):
879 raise
880 except Exception:
881 if not self.catchall: raise
882 err = '<h1>Critical error while processing request: %s</h1>' \
883 % html_escape(environ.get('PATH_INFO', '/'))
884 if DEBUG:
885 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
886 '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
887 % (html_escape(repr(_e())), html_escape(format_exc()))
888 environ['wsgi.errors'].write(err)
889 headers = [('Content-Type', 'text/html; charset=UTF-8')]
890 start_response('500 INTERNAL SERVER ERROR', headers)
891 return [tob(err)]
892
893 def __call__(self, environ, start_response):
894 ''' Each instance of :class:'Bottle' is a WSGI application. '''
895 return self.wsgi(environ, start_response)
896
897
898
899
900
901
902 ###############################################################################
903 # HTTP and WSGI Tools ##########################################################
904 ###############################################################################
905
906
907 class BaseRequest(object):
908 """ A wrapper for WSGI environment dictionaries that adds a lot of
909 convenient access methods and properties. Most of them are read-only.
910
911 Adding new attributes to a request actually adds them to the environ
912 dictionary (as 'bottle.request.ext.<name>'). This is the recommended
913 way to store and access request-specific data.
914 """
915
916 __slots__ = ('environ')
917
918 #: Maximum size of memory buffer for :attr:`body` in bytes.
919 MEMFILE_MAX = 102400
920 #: Maximum number pr GET or POST parameters per request
921 MAX_PARAMS = 100
922
923 def __init__(self, environ=None):
924 """ Wrap a WSGI environ dictionary. """
925 #: The wrapped WSGI environ dictionary. This is the only real attribute.
926 #: All other attributes actually are read-only properties.
927 self.environ = {} if environ is None else environ
928 self.environ['bottle.request'] = self
929
930 @DictProperty('environ', 'bottle.app', read_only=True)
931 def app(self):
932 ''' Bottle application handling this request. '''
933 raise RuntimeError('This request is not connected to an application.')
934
935 @property
936 def path(self):
937 ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix
938 broken clients and avoid the "empty path" edge case). '''
939 return '/' + self.environ.get('PATH_INFO','').lstrip('/')
940
941 @property
942 def method(self):
943 ''' The ``REQUEST_METHOD`` value as an uppercase string. '''
944 return self.environ.get('REQUEST_METHOD', 'GET').upper()
945
946 @DictProperty('environ', 'bottle.request.headers', read_only=True)
947 def headers(self):
948 ''' A :class:`WSGIHeaderDict` that provides case-insensitive access to
949 HTTP request headers. '''
950 return WSGIHeaderDict(self.environ)
951
952 def get_header(self, name, default=None):
953 ''' Return the value of a request header, or a given default value. '''
954 return self.headers.get(name, default)
955
956 @DictProperty('environ', 'bottle.request.cookies', read_only=True)
957 def cookies(self):
958 """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT
959 decoded. Use :meth:`get_cookie` if you expect signed cookies. """
960 cookies = SimpleCookie(self.environ.get('HTTP_COOKIE',''))
961 cookies = list(cookies.values())[:self.MAX_PARAMS]
962 return FormsDict((c.key, c.value) for c in cookies)
963
964 def get_cookie(self, key, default=None, secret=None):
965 """ Return the content of a cookie. To read a `Signed Cookie`, the
966 `secret` must match the one used to create the cookie (see
967 :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
968 cookie or wrong signature), return a default value. """
969 value = self.cookies.get(key)
970 if secret and value:
971 dec = cookie_decode(value, secret) # (key, value) tuple or None
972 return dec[1] if dec and dec[0] == key else default
973 return value or default
974
975 @DictProperty('environ', 'bottle.request.query', read_only=True)
976 def query(self):
977 ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These
978 values are sometimes called "URL arguments" or "GET parameters", but
979 not to be confused with "URL wildcards" as they are provided by the
980 :class:`Router`. '''
981 pairs = parse_qsl(self.query_string, keep_blank_values=True)
982 get = self.environ['bottle.get'] = FormsDict()
983 for key, value in pairs[:self.MAX_PARAMS]:
984 get[key] = value
985 return get
986
987 @DictProperty('environ', 'bottle.request.forms', read_only=True)
988 def forms(self):
989 """ Form values parsed from an `url-encoded` or `multipart/form-data`
990 encoded POST or PUT request body. The result is retuned as a
991 :class:`FormsDict`. All keys and values are strings. File uploads
992 are stored separately in :attr:`files`. """
993 forms = FormsDict()
994 for name, item in self.POST.allitems():
995 if not hasattr(item, 'filename'):
996 forms[name] = item
997 return forms
998
999 @DictProperty('environ', 'bottle.request.params', read_only=True)
1000 def params(self):
1001 """ A :class:`FormsDict` with the combined values of :attr:`query` and
1002 :attr:`forms`. File uploads are stored in :attr:`files`. """
1003 params = FormsDict()
1004 for key, value in self.query.allitems():
1005 params[key] = value
1006 for key, value in self.forms.allitems():
1007 params[key] = value
1008 return params
1009
1010 @DictProperty('environ', 'bottle.request.files', read_only=True)
1011 def files(self):
1012 """ File uploads parsed from an `url-encoded` or `multipart/form-data`
1013 encoded POST or PUT request body. The values are instances of
1014 :class:`cgi.FieldStorage`. The most important attributes are:
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 """
1027 files = FormsDict()
1028 for name, item in self.POST.allitems():
1029 if hasattr(item, 'filename'):
1030 files[name] = item
1031 return files
1032
1033 @DictProperty('environ', 'bottle.request.json', read_only=True)
1034 def json(self):
1035 ''' If the ``Content-Type`` header is ``application/json``, this
1036 property holds the parsed content of the request body. Only requests
1037 smaller than :attr:`MEMFILE_MAX` are processed to avoid memory
1038 exhaustion. '''
1039 if 'application/json' in self.environ.get('CONTENT_TYPE', '') \
1040 and 0 < self.content_length < self.MEMFILE_MAX:
1041 return json_loads(self.body.read(self.MEMFILE_MAX))
1042 return None
1043
1044 @DictProperty('environ', 'bottle.request.body', read_only=True)
1045 def _body(self):
1046 maxread = max(0, self.content_length)
1047 stream = self.environ['wsgi.input']
1048 body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b')
1049 while maxread > 0:
1050 part = stream.read(min(maxread, self.MEMFILE_MAX))
1051 if not part: break
1052 body.write(part)
1053 maxread -= len(part)
1054 self.environ['wsgi.input'] = body
1055 body.seek(0)
1056 return body
1057
1058 @property
1059 def body(self):
1060 """ 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
1062 :class:`io.BytesIO` instance. Accessing this property for the first
1063 time reads and replaces the ``wsgi.input`` environ variable.
1064 Subsequent accesses just do a `seek(0)` on the file object. """
1065 self._body.seek(0)
1066 return self._body
1067
1068 #: An alias for :attr:`query`.
1069 GET = query
1070
1071 @DictProperty('environ', 'bottle.request.post', read_only=True)
1072 def POST(self):
1073 """ The values of :attr:`forms` and :attr:`files` combined into a single
1074 :class:`FormsDict`. Values are either strings (form values) or
1075 instances of :class:`cgi.FieldStorage` (file uploads).
1076 """
1077 post = FormsDict()
1078 safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi
1079 for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'):
1080 if key in self.environ: safe_env[key] = self.environ[key]
1081 if NCTextIOWrapper:
1082 fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n')
1083 else:
1084 fb = self.body
1085 data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True)
1086 for item in (data.list or [])[:self.MAX_PARAMS]:
1087 post[item.name] = item if item.filename else item.value
1088 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
1096 @property
1097 def url(self):
1098 """ The full request URI including hostname and scheme. If your app
1099 lives behind a reverse proxy or load balancer and you get confusing
1100 results, make sure that the ``X-Forwarded-Host`` header is set
1101 correctly. """
1102 return self.urlparts.geturl()
1103
1104 @DictProperty('environ', 'bottle.request.urlparts', read_only=True)
1105 def urlparts(self):
1106 ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple.
1107 The tuple contains (scheme, host, path, query_string and fragment),
1108 but the fragment is always empty because it is not visible to the
1109 server. '''
1110 env = self.environ
1111 http = env.get('wsgi.url_scheme', 'http')
1112 host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST')
1113 if not host:
1114 # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients.
1115 host = env.get('SERVER_NAME', '127.0.0.1')
1116 port = env.get('SERVER_PORT')
1117 if port and port != ('80' if http == 'http' else '443'):
1118 host += ':' + port
1119 path = urlquote(self.fullpath)
1120 return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
1121
1122 @property
1123 def fullpath(self):
1124 """ Request path including :attr:`script_name` (if present). """
1125 return urljoin(self.script_name, self.path.lstrip('/'))
1126
1127 @property
1128 def query_string(self):
1129 """ The raw :attr:`query` part of the URL (everything in between ``?``
1130 and ``#``) as a string. """
1131 return self.environ.get('QUERY_STRING', '')
1132
1133 @property
1134 def script_name(self):
1135 ''' The initial portion of the URL's `path` that was removed by a higher
1136 level (server or routing middleware) before the application was
1137 called. This script path is returned with leading and tailing
1138 slashes. '''
1139 script_name = self.environ.get('SCRIPT_NAME', '').strip('/')
1140 return '/' + script_name + '/' if script_name else '/'
1141
1142 def path_shift(self, shift=1):
1143 ''' Shift path segments from :attr:`path` to :attr:`script_name` and
1144 vice versa.
1145
1146 :param shift: The number of path segments to shift. May be negative
1147 to change the shift direction. (default: 1)
1148 '''
1149 script = self.environ.get('SCRIPT_NAME','/')
1150 self['SCRIPT_NAME'], self['PATH_INFO'] = path_shift(script, self.path, shift)
1151
1152 @property
1153 def content_length(self):
1154 ''' 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
1156 and -1 is returned. In this case, :attr:`body` will be empty. '''
1157 return int(self.environ.get('CONTENT_LENGTH') or -1)
1158
1159 @property
1160 def is_xhr(self):
1161 ''' True if the request was triggered by a XMLHttpRequest. This only
1162 works with JavaScript libraries that support the `X-Requested-With`
1163 header (most of the popular libraries do). '''
1164 requested_with = self.environ.get('HTTP_X_REQUESTED_WITH','')
1165 return requested_with.lower() == 'xmlhttprequest'
1166
1167 @property
1168 def is_ajax(self):
1169 ''' Alias for :attr:`is_xhr`. "Ajax" is not the right term. '''
1170 return self.is_xhr
1171
1172 @property
1173 def auth(self):
1174 """ HTTP authentication data as a (user, password) tuple. This
1175 implementation currently supports basic (not digest) authentication
1176 only. If the authentication happened at a higher level (e.g. in the
1177 front web-server or a middleware), the password field is None, but
1178 the user field is looked up from the ``REMOTE_USER`` environ
1179 variable. On any errors, None is returned. """
1180 basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION',''))
1181 if basic: return basic
1182 ruser = self.environ.get('REMOTE_USER')
1183 if ruser: return (ruser, None)
1184 return None
1185
1186 @property
1187 def remote_route(self):
1188 """ A list of all IPs that were involved in this request, starting with
1189 the client IP and followed by zero or more proxies. This does only
1190 work if all proxies support the ```X-Forwarded-For`` header. Note
1191 that this information can be forged by malicious clients. """
1192 proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
1193 if proxy: return [ip.strip() for ip in proxy.split(',')]
1194 remote = self.environ.get('REMOTE_ADDR')
1195 return [remote] if remote else []
1196
1197 @property
1198 def remote_addr(self):
1199 """ The client IP as a string. Note that this information can be forged
1200 by malicious clients. """
1201 route = self.remote_route
1202 return route[0] if route else None
1203
1204 def copy(self):
1205 """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """
1206 return Request(self.environ.copy())
1207
1208 def get(self, value, default=None): return self.environ.get(value, default)
1209 def __getitem__(self, key): return self.environ[key]
1210 def __delitem__(self, key): self[key] = ""; del(self.environ[key])
1211 def __iter__(self): return iter(self.environ)
1212 def __len__(self): return len(self.environ)
1213 def keys(self): return self.environ.keys()
1214 def __setitem__(self, key, value):
1215 """ Change an environ value and clear all caches that depend on it. """
1216
1217 if self.environ.get('bottle.request.readonly'):
1218 raise KeyError('The environ dictionary is read-only.')
1219
1220 self.environ[key] = value
1221 todelete = ()
1222
1223 if key == 'wsgi.input':
1224 todelete = ('body', 'forms', 'files', 'params', 'post', 'json')
1225 elif key == 'QUERY_STRING':
1226 todelete = ('query', 'params')
1227 elif key.startswith('HTTP_'):
1228 todelete = ('headers', 'cookies')
1229
1230 for key in todelete:
1231 self.environ.pop('bottle.request.'+key, None)
1232
1233 def __repr__(self):
1234 return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)
1235
1236 def __getattr__(self, name):
1237 ''' Search in self.environ for additional user defined attributes. '''
1238 try:
1239 var = self.environ['bottle.request.ext.%s'%name]
1240 return var.__get__(self) if hasattr(var, '__get__') else var
1241 except KeyError:
1242 raise AttributeError('Attribute %r not defined.' % name)
1243
1244 def __setattr__(self, name, value):
1245 if name == 'environ': return object.__setattr__(self, name, value)
1246 self.environ['bottle.request.ext.%s'%name] = value
1247
1248
1249
1250
1251 def _hkey(s):
1252 return s.title().replace('_','-')
1253
1254
1255 class HeaderProperty(object):
1256 def __init__(self, name, reader=None, writer=str, default=''):
1257 self.name, self.default = name, default
1258 self.reader, self.writer = reader, writer
1259 self.__doc__ = 'Current value of the %r header.' % name.title()
1260
1261 def __get__(self, obj, cls):
1262 if obj is None: return self
1263 value = obj.headers.get(self.name, self.default)
1264 return self.reader(value) if self.reader else value
1265
1266 def __set__(self, obj, value):
1267 obj.headers[self.name] = self.writer(value)
1268
1269 def __delete__(self, obj):
1270 del obj.headers[self.name]
1271
1272
1273 class BaseResponse(object):
1274 """ Storage class for a response body as well as headers and cookies.
1275
1276 This class does support dict-like case-insensitive item-access to
1277 headers, but is NOT a dict. Most notably, iterating over a response
1278 yields parts of the body and not the headers.
1279 """
1280
1281 default_status = 200
1282 default_content_type = 'text/html; charset=UTF-8'
1283
1284 # Header blacklist for specific response codes
1285 # (rfc2616 section 10.2.3 and 10.3.5)
1286 bad_headers = {
1287 204: set(('Content-Type',)),
1288 304: set(('Allow', 'Content-Encoding', 'Content-Language',
1289 'Content-Length', 'Content-Range', 'Content-Type',
1290 'Content-Md5', 'Last-Modified'))}
1291
1292 def __init__(self, body='', status=None, **headers):
1293 self._status_line = None
1294 self._status_code = None
1295 self._cookies = None
1296 self._headers = {'Content-Type': [self.default_content_type]}
1297 self.body = body
1298 self.status = status or self.default_status
1299 if headers:
1300 for name, value in headers.items():
1301 self[name] = value
1302
1303 def copy(self):
1304 ''' Returns a copy of self. '''
1305 copy = Response()
1306 copy.status = self.status
1307 copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
1308 return copy
1309
1310 def __iter__(self):
1311 return iter(self.body)
1312
1313 def close(self):
1314 if hasattr(self.body, 'close'):
1315 self.body.close()
1316
1317 @property
1318 def status_line(self):
1319 ''' The HTTP status line as a string (e.g. ``404 Not Found``).'''
1320 return self._status_line
1321
1322 @property
1323 def status_code(self):
1324 ''' The HTTP status code as an integer (e.g. 404).'''
1325 return self._status_code
1326
1327 def _set_status(self, status):
1328 if isinstance(status, int):
1329 code, status = status, _HTTP_STATUS_LINES.get(status)
1330 elif ' ' in status:
1331 status = status.strip()
1332 code = int(status.split()[0])
1333 else:
1334 raise ValueError('String status line without a reason phrase.')
1335 if not 100 <= code <= 999: raise ValueError('Status code out of range.')
1336 self._status_code = code
1337 self._status_line = status or ('%d Unknown' % code)
1338
1339 def _get_status(self):
1340 return self._status_line
1341
1342 status = property(_get_status, _set_status, None,
1343 ''' A writeable property to change the HTTP response status. It accepts
1344 either a numeric code (100-999) or a string with a custom reason
1345 phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
1346 :data:`status_code` are updated accordingly. The return value is
1347 always a status string. ''')
1348 del _get_status, _set_status
1349
1350 @property
1351 def headers(self):
1352 ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like
1353 view on the response headers. '''
1354 self.__dict__['headers'] = hdict = HeaderDict()
1355 hdict.dict = self._headers
1356 return hdict
1357
1358 def __contains__(self, name): return _hkey(name) in self._headers
1359 def __delitem__(self, name): del self._headers[_hkey(name)]
1360 def __getitem__(self, name): return self._headers[_hkey(name)][-1]
1361 def __setitem__(self, name, value): self._headers[_hkey(name)] = [str(value)]
1362
1363 def get_header(self, name, default=None):
1364 ''' Return the value of a previously defined header. If there is no
1365 header with that name, return a default value. '''
1366 return self._headers.get(_hkey(name), [default])[-1]
1367
1368 def set_header(self, name, value, append=False):
1369 ''' Create a new response header, replacing any previously defined
1370 headers with the same name. '''
1371 if append:
1372 self.add_header(name, value)
1373 else:
1374 self._headers[_hkey(name)] = [str(value)]
1375
1376 def add_header(self, name, value):
1377 ''' Add an additional response header, not removing duplicates. '''
1378 self._headers.setdefault(_hkey(name), []).append(str(value))
1379
1380 def iter_headers(self):
1381 ''' Yield (header, value) tuples, skipping headers that are not
1382 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
1397
1398 @property
1399 def headerlist(self):
1400 ''' WSGI conform list of (header, value) tuples. '''
1401 return list(self.iter_headers())
1402
1403 content_type = HeaderProperty('Content-Type')
1404 content_length = HeaderProperty('Content-Length', reader=int)
1405
1406 @property
1407 def charset(self):
1408 """ Return the charset specified in the content-type header (default: utf8). """
1409 if 'charset=' in self.content_type:
1410 return self.content_type.split('charset=')[-1].split(';')[0].strip()
1411 return 'UTF-8'
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
1422 def set_cookie(self, name, value, secret=None, **options):
1423 ''' Create a new cookie or replace an old one. If the `secret` parameter is
1424 set, create a `Signed Cookie` (described below).
1425
1426 :param name: the name of the cookie.
1427 :param value: the value of the cookie.
1428 :param secret: a signature key required for signed cookies.
1429
1430 Additionally, this method accepts all RFC 2109 attributes that are
1431 supported by :class:`cookie.Morsel`, including:
1432
1433 :param max_age: maximum age in seconds. (default: None)
1434 :param expires: a datetime object or UNIX timestamp. (default: None)
1435 :param domain: the domain that is allowed to read the cookie.
1436 (default: current domain)
1437 :param path: limits the cookie to a given path (default: current path)
1438 :param secure: limit the cookie to HTTPS connections (default: off).
1439 :param httponly: prevents client-side javascript to read this cookie
1440 (default: off, requires Python 2.6 or newer).
1441
1442 If neither `expires` nor `max_age` is set (default), the cookie will
1443 expire at the end of the browser session (as soon as the browser
1444 window is closed).
1445
1446 Signed cookies may store any pickle-able object and are
1447 cryptographically signed to prevent manipulation. Keep in mind that
1448 cookies are limited to 4kb in most browsers.
1449
1450 Warning: Signed cookies are not encrypted (the client can still see
1451 the content) and not copy-protected (the client can restore an old
1452 cookie). The main intention is to make pickling and unpickling
1453 save, not to store secret information at client side.
1454 '''
1455 if not self._cookies:
1456 self._cookies = SimpleCookie()
1457
1458 if secret:
1459 value = touni(cookie_encode((name, value), secret))
1460 elif not isinstance(value, basestring):
1461 raise TypeError('Secret key missing for non-string Cookie.')
1462
1463 if len(value) > 4096: raise ValueError('Cookie value to long.')
1464 self._cookies[name] = value
1465
1466 for key, value in options.items():
1467 if key == 'max_age':
1468 if isinstance(value, timedelta):
1469 value = value.seconds + value.days * 24 * 3600
1470 if key == 'expires':
1471 if isinstance(value, (datedate, datetime)):
1472 value = value.timetuple()
1473 elif isinstance(value, (int, float)):
1474 value = time.gmtime(value)
1475 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value)
1476 self._cookies[name][key.replace('_', '-')] = value
1477
1478 def delete_cookie(self, key, **kwargs):
1479 ''' Delete a cookie. Be sure to use the same `domain` and `path`
1480 settings as used to create the cookie. '''
1481 kwargs['max_age'] = -1
1482 kwargs['expires'] = 0
1483 self.set_cookie(key, '', **kwargs)
1484
1485 def __repr__(self):
1486 out = ''
1487 for name, value in self.headerlist:
1488 out += '%s: %s\n' % (name.title(), value.strip())
1489 return out
1490
1491 #: Thread-local storage for :class:`LocalRequest` and :class:`LocalResponse`
1492 #: attributes.
1493 _lctx = threading.local()
1494
1495 def local_property(name):
1496 def fget(self):
1497 try:
1498 return getattr(_lctx, name)
1499 except AttributeError:
1500 raise RuntimeError("Request context not initialized.")
1501 def fset(self, value): setattr(_lctx, name, value)
1502 def fdel(self): delattr(_lctx, name)
1503 return property(fget, fset, fdel,
1504 'Thread-local property stored in :data:`_lctx.%s`' % name)
1505
1506
1507 class LocalRequest(BaseRequest):
1508 ''' A thread-local subclass of :class:`BaseRequest` with a different
1509 set of attribues for each thread. There is usually only one global
1510 instance of this class (:data:`request`). If accessed during a
1511 request/response cycle, this instance always refers to the *current*
1512 request (even on a multithreaded server). '''
1513 bind = BaseRequest.__init__
1514 environ = local_property('request_environ')
1515
1516
1517 class LocalResponse(BaseResponse):
1518 ''' A thread-local subclass of :class:`BaseResponse` with a different
1519 set of attribues for each thread. There is usually only one global
1520 instance of this class (:data:`response`). Its attributes are used
1521 to build the HTTP response at the end of the request/response cycle.
1522 '''
1523 bind = BaseResponse.__init__
1524 _status_line = local_property('response_status_line')
1525 _status_code = local_property('response_status_code')
1526 _cookies = local_property('response_cookies')
1527 _headers = local_property('response_headers')
1528 body = local_property('response_body')
1529
1530 Response = LocalResponse # BC 0.9
1531 Request = LocalRequest # BC 0.9
1532
1533
1534
1535
1536
1537
1538 ###############################################################################
1539 # Plugins ######################################################################
1540 ###############################################################################
1541
1542 class PluginError(BottleException): pass
1543
1544 class JSONPlugin(object):
1545 name = 'json'
1546 api = 2
1547
1548 def __init__(self, json_dumps=json_dumps):
1549 self.json_dumps = json_dumps
1550
1551 def apply(self, callback, context):
1552 dumps = self.json_dumps
1553 if not dumps: return callback
1554 def wrapper(*a, **ka):
1555 rv = callback(*a, **ka)
1556 if isinstance(rv, dict):
1557 #Attempt to serialize, raises exception on failure
1558 json_response = dumps(rv)
1559 #Set content type only if serialization succesful
1560 response.content_type = 'application/json'
1561 return json_response
1562 return rv
1563 return wrapper
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
1609
1610
1611 class TemplatePlugin(object):
1612 ''' This plugin applies the :func:`view` decorator to all routes with a
1613 `template` config parameter. If the parameter is a tuple, the second
1614 element must be a dict with additional options (e.g. `template_engine`)
1615 or default variables for the template. '''
1616 name = 'template'
1617 api = 2
1618
1619 def apply(self, callback, route):
1620 conf = route.config.get('template')
1621 if isinstance(conf, (tuple, list)) and len(conf) == 2:
1622 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):
1627 return view(conf)(callback)
1628 else:
1629 return callback
1630
1631
1632 #: Not a plugin, but part of the plugin API. TODO: Find a better place.
1633 class _ImportRedirect(object):
1634 def __init__(self, name, impmask):
1635 ''' Create a virtual package that redirects imports (see PEP 302). '''
1636 self.name = name
1637 self.impmask = impmask
1638 self.module = sys.modules.setdefault(name, imp.new_module(name))
1639 self.module.__dict__.update({'__file__': __file__, '__path__': [],
1640 '__all__': [], '__loader__': self})
1641 sys.meta_path.append(self)
1642
1643 def find_module(self, fullname, path=None):
1644 if '.' not in fullname: return
1645 packname, modname = fullname.rsplit('.', 1)
1646 if packname != self.name: return
1647 return self
1648
1649 def load_module(self, fullname):
1650 if fullname in sys.modules: return sys.modules[fullname]
1651 packname, modname = fullname.rsplit('.', 1)
1652 realname = self.impmask % modname
1653 __import__(realname)
1654 module = sys.modules[fullname] = sys.modules[realname]
1655 setattr(self.module, modname, module)
1656 module.__loader__ = self
1657 return module
1658
1659
1660
1661
1662
1663
1664 ###############################################################################
1665 # Common Utilities #############################################################
1666 ###############################################################################
1667
1668
1669 class MultiDict(DictMixin):
1670 """ This dict stores multiple values per key, but behaves exactly like a
1671 normal dict in that it returns only the newest value for any given key.
1672 There are special methods available to access the full list of values.
1673 """
1674
1675 def __init__(self, *a, **k):
1676 self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
1677
1678 def __len__(self): return len(self.dict)
1679 def __iter__(self): return iter(self.dict)
1680 def __contains__(self, key): return key in self.dict
1681 def __delitem__(self, key): del self.dict[key]
1682 def __getitem__(self, key): return self.dict[key][-1]
1683 def __setitem__(self, key, value): self.append(key, value)
1684 def keys(self): return self.dict.keys()
1685
1686 if py3k:
1687 def values(self): return (v[-1] for v in self.dict.values())
1688 def items(self): return ((k, v[-1]) for k, v in self.dict.items())
1689 def allitems(self):
1690 return ((k, v) for k, vl in self.dict.items() for v in vl)
1691 iterkeys = keys
1692 itervalues = values
1693 iteritems = items
1694 iterallitems = allitems
1695
1696 else:
1697 def values(self): return [v[-1] for v in self.dict.values()]
1698 def items(self): return [(k, v[-1]) for k, v in self.dict.items()]
1699 def iterkeys(self): return self.dict.iterkeys()
1700 def itervalues(self): return (v[-1] for v in self.dict.itervalues())
1701 def iteritems(self):
1702 return ((k, v[-1]) for k, v in self.dict.iteritems())
1703 def iterallitems(self):
1704 return ((k, v) for k, vl in self.dict.iteritems() for v in vl)
1705 def allitems(self):
1706 return [(k, v) for k, vl in self.dict.iteritems() for v in vl]
1707
1708 def get(self, key, default=None, index=-1, type=None):
1709 ''' Return the most recent value for a key.
1710
1711 :param default: The default value to be returned if the key is not
1712 present or the type conversion fails.
1713 :param index: An index for the list of available values.
1714 :param type: If defined, this callable is used to cast the value
1715 into a specific type. Exception are suppressed and result in
1716 the default value to be returned.
1717 '''
1718 try:
1719 val = self.dict[key][index]
1720 return type(val) if type else val
1721 except Exception:
1722 pass
1723 return default
1724
1725 def append(self, key, value):
1726 ''' Add a new value to the list of values for this key. '''
1727 self.dict.setdefault(key, []).append(value)
1728
1729 def replace(self, key, value):
1730 ''' Replace the list of values with a single value. '''
1731 self.dict[key] = [value]
1732
1733 def getall(self, key):
1734 ''' Return a (possibly empty) list of values for a key. '''
1735 return self.dict.get(key) or []
1736
1737 #: Aliases for WTForms to mimic other multi-dict APIs (Django)
1738 getone = get
1739 getlist = getall
1740
1741
1742
1743 class FormsDict(MultiDict):
1744 ''' This :class:`MultiDict` subclass is used to store request form data.
1745 Additionally to the normal dict-like item access methods (which return
1746 unmodified data as native strings), this container also supports
1747 attribute-like access to its values. Attributes are automatically de-
1748 or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing
1749 attributes default to an empty string. '''
1750
1751 #: Encoding used for attribute values.
1752 input_encoding = 'utf8'
1753 #: If true (default), unicode strings are first encoded with `latin1`
1754 #: and then decoded to match :attr:`input_encoding`.
1755 recode_unicode = True
1756
1757 def _fix(self, s, encoding=None):
1758 if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI
1759 s = s.encode('latin1')
1760 if isinstance(s, bytes): # Python 2 WSGI
1761 return s.decode(encoding or self.input_encoding)
1762 return s
1763
1764 def decode(self, encoding=None):
1765 ''' Returns a copy with all keys and values de- or recoded to match
1766 :attr:`input_encoding`. Some libraries (e.g. WTForms) want a
1767 unicode dictionary. '''
1768 copy = FormsDict()
1769 enc = copy.input_encoding = encoding or self.input_encoding
1770 copy.recode_unicode = False
1771 for key, value in self.allitems():
1772 copy.append(self._fix(key, enc), self._fix(value, enc))
1773 return copy
1774
1775 def getunicode(self, name, default=None, encoding=None):
1776 try:
1777 return self._fix(self[name], encoding)
1778 except (UnicodeError, KeyError):
1779 return default
1780
1781 def __getattr__(self, name, default=unicode()):
1782 # Without this guard, pickle generates a cryptic TypeError:
1783 if name.startswith('__') and name.endswith('__'):
1784 return super(FormsDict, self).__getattr__(name)
1785 return self.getunicode(name, default=default)
1786
1787
1788 class HeaderDict(MultiDict):
1789 """ A case-insensitive version of :class:`MultiDict` that defaults to
1790 replace the old value instead of appending it. """
1791
1792 def __init__(self, *a, **ka):
1793 self.dict = {}
1794 if a or ka: self.update(*a, **ka)
1795
1796 def __contains__(self, key): return _hkey(key) in self.dict
1797 def __delitem__(self, key): del self.dict[_hkey(key)]
1798 def __getitem__(self, key): return self.dict[_hkey(key)][-1]
1799 def __setitem__(self, key, value): self.dict[_hkey(key)] = [str(value)]
1800 def append(self, key, value):
1801 self.dict.setdefault(_hkey(key), []).append(str(value))
1802 def replace(self, key, value): self.dict[_hkey(key)] = [str(value)]
1803 def getall(self, key): return self.dict.get(_hkey(key)) or []
1804 def get(self, key, default=None, index=-1):
1805 return MultiDict.get(self, _hkey(key), default, index)
1806 def filter(self, names):
1807 for name in [_hkey(n) for n in names]:
1808 if name in self.dict:
1809 del self.dict[name]
1810
1811
1812 class WSGIHeaderDict(DictMixin):
1813 ''' This dict-like class wraps a WSGI environ dict and provides convenient
1814 access to HTTP_* fields. Keys and values are native strings
1815 (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI
1816 environment contains non-native string values, these are de- or encoded
1817 using a lossless 'latin1' character set.
1818
1819 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
1821 that uses non-native strings.)
1822 '''
1823 #: List of keys that do not have a 'HTTP_' prefix.
1824 cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')
1825
1826 def __init__(self, environ):
1827 self.environ = environ
1828
1829 def _ekey(self, key):
1830 ''' Translate header field name to CGI/WSGI environ key. '''
1831 key = key.replace('-','_').upper()
1832 if key in self.cgikeys:
1833 return key
1834 return 'HTTP_' + key
1835
1836 def raw(self, key, default=None):
1837 ''' Return the header value as is (may be bytes or unicode). '''
1838 return self.environ.get(self._ekey(key), default)
1839
1840 def __getitem__(self, key):
1841 return tonat(self.environ[self._ekey(key)], 'latin1')
1842
1843 def __setitem__(self, key, value):
1844 raise TypeError("%s is read-only." % self.__class__)
1845
1846 def __delitem__(self, key):
1847 raise TypeError("%s is read-only." % self.__class__)
1848
1849 def __iter__(self):
1850 for key in self.environ:
1851 if key[:5] == 'HTTP_':
1852 yield key[5:].replace('_', '-').title()
1853 elif key in self.cgikeys:
1854 yield key.replace('_', '-').title()
1855
1856 def keys(self): return [x for x in self]
1857 def __len__(self): return len(self.keys())
1858 def __contains__(self, key): return self._ekey(key) in self.environ
1859
1860
1861 class ConfigDict(dict):
1862 ''' A dict-subclass with some extras: You can access keys like attributes.
1863 Uppercase attributes create new ConfigDicts and act as name-spaces.
1864 Other missing attributes return None. Calling a ConfigDict updates its
1865 values and returns itself.
1866
1867 >>> cfg = ConfigDict()
1868 >>> cfg.Namespace.value = 5
1869 >>> cfg.OtherNamespace(a=1, b=2)
1870 >>> cfg
1871 {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}}
1872 '''
1873
1874 def __getattr__(self, key):
1875 if key not in self and key[0].isupper():
1876 self[key] = ConfigDict()
1877 return self.get(key)
1878
1879 def __setattr__(self, key, value):
1880 if hasattr(dict, key):
1881 raise AttributeError('Read-only attribute.')
1882 if key in self and self[key] and isinstance(self[key], ConfigDict):
1883 raise AttributeError('Non-empty namespace attribute.')
1884 self[key] = value
1885
1886 def __delattr__(self, key):
1887 if key in self: del self[key]
1888
1889 def __call__(self, *a, **ka):
1890 for key, value in dict(*a, **ka).items(): setattr(self, key, value)
1891 return self
1892
1893
1894 class AppStack(list):
1895 """ A stack-like list. Calling it returns the head of the stack. """
1896
1897 def __call__(self):
1898 """ Return the current default application. """
1899 return self[-1]
1900
1901 def push(self, value=None):
1902 """ Add a new :class:`Bottle` instance to the stack """
1903 if not isinstance(value, Bottle):
1904 value = Bottle()
1905 self.append(value)
1906 return value
1907
1908
1909 class WSGIFileWrapper(object):
1910
1911 def __init__(self, fp, buffer_size=1024*64):
1912 self.fp, self.buffer_size = fp, buffer_size
1913 for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'):
1914 if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr))
1915
1916 def __iter__(self):
1917 buff, read = self.buffer_size, self.read
1918 while True:
1919 part = read(buff)
1920 if not part: return
1921 yield part
1922
1923
1924 class ResourceManager(object):
1925 ''' This class manages a list of search paths and helps to find and open
1926 aplication-bound resources (files).
1927
1928 :param base: default value for same-named :meth:`add_path` parameter.
1929 :param opener: callable used to open resources.
1930 :param cachemode: controls which lookups are cached. One of 'all',
1931 'found' or 'none'.
1932 '''
1933
1934 def __init__(self, base='./', opener=open, cachemode='all'):
1935 self.opener = open
1936 self.base = base
1937 self.cachemode = cachemode
1938
1939 #: A list of search paths. See :meth:`add_path` for details.
1940 self.path = []
1941 #: A cache for resolved paths. `res.cache.clear()`` clears the cache.
1942 self.cache = {}
1943
1944 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
1946 not exist.
1947
1948 :param path: The new search path. Relative paths are turned into an
1949 absolute and normalized form. If the path looks like a file (not
1950 ending in `/`), the filename is stripped off.
1951 :param base: Path used to absolutize relative search paths.
1952 Defaults to `:attr:base` which defaults to ``./``.
1953 :param index: Position within the list of search paths. Defaults to
1954 last index (appends to the list).
1955 :param create: Create non-existent search paths. Off by default.
1956
1957 The `base` parameter makes it easy to reference files installed
1958 along with a python module or package::
1959
1960 res.add_path('./resources/', __file__)
1961 '''
1962 base = os.path.abspath(os.path.dirname(base or self.base))
1963 path = os.path.abspath(os.path.join(base, os.path.dirname(path)))
1964 path += os.sep
1965 if path in self.path:
1966 self.path.remove(path)
1967 if create and not os.path.isdir(path):
1968 os.mkdirs(path)
1969 if index is None:
1970 self.path.append(path)
1971 else:
1972 self.path.insert(index, path)
1973 self.cache.clear()
1974
1975 def __iter__(self):
1976 ''' Iterate over all existing files in all registered paths. '''
1977 search = self.path[:]
1978 while search:
1979 path = search.pop()
1980 if not os.path.isdir(path): continue
1981 for name in os.listdir(path):
1982 full = os.path.join(path, name)
1983 if os.path.isdir(full): search.append(full)
1984 else: yield full
1985
1986 def lookup(self, name):
1987 ''' Search for a resource and return an absolute file path, or `None`.
1988
1989 The :attr:`path` list is searched in order. The first match is
1990 returend. Symlinks are followed. The result is cached to speed up
1991 future lookups. '''
1992 if name not in self.cache or DEBUG:
1993 for path in self.path:
1994 fpath = os.path.join(path, name)
1995 if os.path.isfile(fpath):
1996 if self.cachemode in ('all', 'found'):
1997 self.cache[name] = fpath
1998 return fpath
1999 if self.cachemode == 'all':
2000 self.cache[name] = None
2001 return self.cache[name]
2002
2003 def open(self, name, mode='r', *args, **kwargs):
2004 ''' Find a resource and return a file object, or raise IOError. '''
2005 fname = self.lookup(name)
2006 if not fname: raise IOError("Resource %r not found." % name)
2007 return self.opener(name, mode=mode, *args, **kwargs)
2008
2009
2010
2011
2012
2013
2014 ###############################################################################
2015 # Application Helper ###########################################################
2016 ###############################################################################
2017
2018
2019 def abort(code=500, text='Unknown Error: Application stopped.'):
2020 """ Aborts execution and causes a HTTP error. """
2021 raise HTTPError(code, text)
2022
2023
2024 def redirect(url, code=None):
2025 """ Aborts execution and causes a 303 or 302 redirect, depending on
2026 the HTTP protocol version. """
2027 if code is None:
2028 code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
2029 location = urljoin(request.url, url)
2030 raise HTTPResponse("", status=code, header=dict(Location=location))
2031
2032
2033 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.'''
2035 fp.seek(offset)
2036 while bytes > 0:
2037 part = fp.read(min(bytes, maxread))
2038 if not part: break
2039 bytes -= len(part)
2040 yield part
2041
2042
2043 def static_file(filename, root, mimetype='auto', download=False):
2044 """ 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,
2046 Content-Length and Last-Modified header. Obey If-Modified-Since header
2047 and HEAD requests.
2048 """
2049 root = os.path.abspath(root) + os.sep
2050 filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
2051 header = dict()
2052
2053 if not filename.startswith(root):
2054 return HTTPError(403, "Access denied.")
2055 if not os.path.exists(filename) or not os.path.isfile(filename):
2056 return HTTPError(404, "File does not exist.")
2057 if not os.access(filename, os.R_OK):
2058 return HTTPError(403, "You do not have permission to access this file.")
2059
2060 if mimetype == 'auto':
2061 mimetype, encoding = mimetypes.guess_type(filename)
2062 if mimetype: header['Content-Type'] = mimetype
2063 if encoding: header['Content-Encoding'] = encoding
2064 elif mimetype:
2065 header['Content-Type'] = mimetype
2066
2067 if download:
2068 download = os.path.basename(filename if download == True else download)
2069 header['Content-Disposition'] = 'attachment; filename="%s"' % download
2070
2071 stats = os.stat(filename)
2072 header['Content-Length'] = clen = stats.st_size
2073 lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
2074 header['Last-Modified'] = lm
2075
2076 ims = request.environ.get('HTTP_IF_MODIFIED_SINCE')
2077 if ims:
2078 ims = parse_date(ims.split(";")[0].strip())
2079 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())
2081 return HTTPResponse(status=304, header=header)
2082
2083 body = '' if request.method == 'HEAD' else open(filename, 'rb')
2084
2085 header["Accept-Ranges"] = "bytes"
2086 ranges = request.environ.get('HTTP_RANGE')
2087 if 'HTTP_RANGE' in request.environ:
2088 ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen))
2089 if not ranges:
2090 return HTTPError(416, "Requested Range Not Satisfiable")
2091 offset, end = ranges[0]
2092 header["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen)
2093 header["Content-Length"] = str(end-offset)
2094 if body: body = _file_iter_range(body, offset, end-offset)
2095 return HTTPResponse(body, header=header, status=206)
2096 return HTTPResponse(body, header=header)
2097
2098
2099
2100
2101
2102
2103 ###############################################################################
2104 # HTTP Utilities and MISC (TODO) ###############################################
2105 ###############################################################################
2106
2107
2108 def debug(mode=True):
2109 """ Change the debug level.
2110 There is only one debug level supported at the moment."""
2111 global DEBUG
2112 DEBUG = bool(mode)
2113
2114
2115 def parse_date(ims):
2116 """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
2117 try:
2118 ts = email.utils.parsedate_tz(ims)
2119 return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone
2120 except (TypeError, ValueError, IndexError, OverflowError):
2121 return None
2122
2123
2124 def parse_auth(header):
2125 """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None"""
2126 try:
2127 method, data = header.split(None, 1)
2128 if method.lower() == 'basic':
2129 user, pwd = touni(base64.b64decode(tob(data))).split(':',1)
2130 return user, pwd
2131 except (KeyError, ValueError):
2132 return None
2133
2134 def parse_range_header(header, maxlen=0):
2135 ''' Yield (start, end) ranges parsed from a HTTP Range header. Skip
2136 unsatisfiable ranges. The end index is non-inclusive.'''
2137 if not header or header[:6] != 'bytes=': return
2138 ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r]
2139 for start, end in ranges:
2140 try:
2141 if not start: # bytes=-100 -> last 100 bytes
2142 start, end = max(0, maxlen-int(end)), maxlen
2143 elif not end: # bytes=100- -> all but the first 99 bytes
2144 start, end = int(start), maxlen
2145 else: # bytes=100-200 -> bytes 100-200 (inclusive)
2146 start, end = int(start), min(int(end)+1, maxlen)
2147 if 0 <= start < end <= maxlen:
2148 yield start, end
2149 except ValueError:
2150 pass
2151
2152 def _lscmp(a, b):
2153 ''' Compares two strings in a cryptographically safe way:
2154 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)
2156
2157
2158 def cookie_encode(data, key):
2159 ''' Encode and sign a pickle-able object. Return a (byte) string '''
2160 msg = base64.b64encode(pickle.dumps(data, -1))
2161 sig = base64.b64encode(hmac.new(tob(key), msg).digest())
2162 return tob('!') + sig + tob('?') + msg
2163
2164
2165 def cookie_decode(data, key):
2166 ''' Verify and decode an encoded string. Return an object or None.'''
2167 data = tob(data)
2168 if cookie_is_encoded(data):
2169 sig, msg = data.split(tob('?'), 1)
2170 if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())):
2171 return pickle.loads(base64.b64decode(msg))
2172 return None
2173
2174
2175 def cookie_is_encoded(data):
2176 ''' Return True if the argument looks like a encoded cookie.'''
2177 return bool(data.startswith(tob('!')) and tob('?') in data)
2178
2179
2180 def html_escape(string):
2181 ''' Escape HTML special characters ``&<>`` and quotes ``'"``. '''
2182 return string.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')\
2183 .replace('"','&quot;').replace("'",'&#039;')
2184
2185
2186 def html_quote(string):
2187 ''' Escape and quote a string to be used as an HTTP attribute.'''
2188 return '"%s"' % html_escape(string).replace('\n','%#10;')\
2189 .replace('\r','&#13;').replace('\t','&#9;')
2190
2191
2192 def yieldroutes(func):
2193 """ 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
2195 takes optional keyword arguments. The output is best described by example::
2196
2197 a() -> '/a'
2198 b(x, y) -> '/b/:x/:y'
2199 c(x, y=5) -> '/c/:x' and '/c/:x/:y'
2200 d(x=5, y=6) -> '/d' and '/d/:x' and '/d/:x/:y'
2201 """
2202 import inspect # Expensive module. Only import if necessary.
2203 path = '/' + func.__name__.replace('__','/').lstrip('/')
2204 spec = inspect.getargspec(func)
2205 argc = len(spec[0]) - len(spec[3] or [])
2206 path += ('/:%s' * argc) % tuple(spec[0][:argc])
2207 yield path
2208 for arg in spec[0][argc:]:
2209 path += '/:%s' % arg
2210 yield path
2211
2212
2213 def path_shift(script_name, path_info, shift=1):
2214 ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
2215
2216 :return: The modified paths.
2217 :param script_name: The SCRIPT_NAME path.
2218 :param script_name: The PATH_INFO path.
2219 :param shift: The number of path fragments to shift. May be negative to
2220 change the shift direction. (default: 1)
2221 '''
2222 if shift == 0: return script_name, path_info
2223 pathlist = path_info.strip('/').split('/')
2224 scriptlist = script_name.strip('/').split('/')
2225 if pathlist and pathlist[0] == '': pathlist = []
2226 if scriptlist and scriptlist[0] == '': scriptlist = []
2227 if shift > 0 and shift <= len(pathlist):
2228 moved = pathlist[:shift]
2229 scriptlist = scriptlist + moved
2230 pathlist = pathlist[shift:]
2231 elif shift < 0 and shift >= -len(scriptlist):
2232 moved = scriptlist[shift:]
2233 pathlist = moved + pathlist
2234 scriptlist = scriptlist[:shift]
2235 else:
2236 empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO'
2237 raise AssertionError("Cannot shift. Nothing left from %s" % empty)
2238 new_script_name = '/' + '/'.join(scriptlist)
2239 new_path_info = '/' + '/'.join(pathlist)
2240 if path_info.endswith('/') and pathlist: new_path_info += '/'
2241 return new_script_name, new_path_info
2242
2243
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"):
2266 ''' Callback decorator to require HTTP auth (basic).
2267 TODO: Add route(check_auth=...) parameter. '''
2268 def decorator(func):
2269 def wrapper(*a, **ka):
2270 user, password = request.auth or (None, None)
2271 if user is None or not check(user, password):
2272 response.headers['WWW-Authenticate'] = 'Basic realm="%s"' % realm
2273 return HTTPError(401, text)
2274 return func(*a, **ka)
2275 return wrapper
2276 return decorator
2277
2278
2279 # Shortcuts for common Bottle methods.
2280 # They all refer to the current default application.
2281
2282 def make_default_app_wrapper(name):
2283 ''' Return a callable that relays calls to the current default app. '''
2284 @functools.wraps(getattr(Bottle, name))
2285 def wrapper(*a, **ka):
2286 return getattr(app(), name)(*a, **ka)
2287 return wrapper
2288
2289 route = make_default_app_wrapper('route')
2290 get = make_default_app_wrapper('get')
2291 post = make_default_app_wrapper('post')
2292 put = make_default_app_wrapper('put')
2293 delete = make_default_app_wrapper('delete')
2294 error = make_default_app_wrapper('error')
2295 mount = make_default_app_wrapper('mount')
2296 hook = make_default_app_wrapper('hook')
2297 install = make_default_app_wrapper('install')
2298 uninstall = make_default_app_wrapper('uninstall')
2299 url = make_default_app_wrapper('get_url')
2300
2301
2302
2303
2304
2305
2306
2307 ###############################################################################
2308 # Server Adapter ###############################################################
2309 ###############################################################################
2310
2311
2312 class ServerAdapter(object):
2313 quiet = False
2314 def __init__(self, host='127.0.0.1', port=8080, **config):
2315 self.options = config
2316 self.host = host
2317 self.port = int(port)
2318
2319 def run(self, handler): # pragma: no cover
2320 pass
2321
2322 def __repr__(self):
2323 args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()])
2324 return "%s(%s)" % (self.__class__.__name__, args)
2325
2326
2327 class CGIServer(ServerAdapter):
2328 quiet = True
2329 def run(self, handler): # pragma: no cover
2330 from wsgiref.handlers import CGIHandler
2331 def fixed_environ(environ, start_response):
2332 environ.setdefault('PATH_INFO', '')
2333 return handler(environ, start_response)
2334 CGIHandler().run(fixed_environ)
2335
2336
2337 class FlupFCGIServer(ServerAdapter):
2338 def run(self, handler): # pragma: no cover
2339 import flup.server.fcgi
2340 self.options.setdefault('bindAddress', (self.host, self.port))
2341 flup.server.fcgi.WSGIServer(handler, **self.options).run()
2342
2343
2344 class WSGIRefServer(ServerAdapter):
2345 def run(self, handler): # pragma: no cover
2346 from wsgiref.simple_server import make_server, WSGIRequestHandler
2347 if self.quiet:
2348 class QuietHandler(WSGIRequestHandler):
2349 def log_request(*args, **kw): pass
2350 self.options['handler_class'] = QuietHandler
2351 srv = make_server(self.host, self.port, handler, **self.options)
2352 srv.serve_forever()
2353
2354
2355 class CherryPyServer(ServerAdapter):
2356 def run(self, handler): # pragma: no cover
2357 from cherrypy import wsgiserver
2358 server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler)
2359 try:
2360 server.start()
2361 finally:
2362 server.stop()
2363
2364
2365 class WaitressServer(ServerAdapter):
2366 def run(self, handler):
2367 from waitress import serve
2368 serve(handler, host=self.host, port=self.port)
2369
2370
2371 class PasteServer(ServerAdapter):
2372 def run(self, handler): # pragma: no cover
2373 from paste import httpserver
2374 if not self.quiet:
2375 from paste.translogger import TransLogger
2376 handler = TransLogger(handler)
2377 httpserver.serve(handler, host=self.host, port=str(self.port),
2378 **self.options)
2379
2380
2381 class MeinheldServer(ServerAdapter):
2382 def run(self, handler):
2383 from meinheld import server
2384 server.listen((self.host, self.port))
2385 server.run(handler)
2386
2387
2388 class FapwsServer(ServerAdapter):
2389 """ Extremely fast webserver using libev. See http://www.fapws.org/ """
2390 def run(self, handler): # pragma: no cover
2391 import fapws._evwsgi as evwsgi
2392 from fapws import base, config
2393 port = self.port
2394 if float(config.SERVER_IDENT[-2:]) > 0.4:
2395 # fapws3 silently changed its API in 0.5
2396 port = str(port)
2397 evwsgi.start(self.host, port)
2398 # fapws3 never releases the GIL. Complain upstream. I tried. No luck.
2399 if 'BOTTLE_CHILD' in os.environ and not self.quiet:
2400 _stderr("WARNING: Auto-reloading does not work with Fapws3.\n")
2401 _stderr(" (Fapws3 breaks python thread support)\n")
2402 evwsgi.set_base_module(base)
2403 def app(environ, start_response):
2404 environ['wsgi.multiprocess'] = False
2405 return handler(environ, start_response)
2406 evwsgi.wsgi_cb(('', app))
2407 evwsgi.run()
2408
2409
2410 class TornadoServer(ServerAdapter):
2411 """ The super hyped asynchronous server by facebook. Untested. """
2412 def run(self, handler): # pragma: no cover
2413 import tornado.wsgi, tornado.httpserver, tornado.ioloop
2414 container = tornado.wsgi.WSGIContainer(handler)
2415 server = tornado.httpserver.HTTPServer(container)
2416 server.listen(port=self.port)
2417 tornado.ioloop.IOLoop.instance().start()
2418
2419
2420 class AppEngineServer(ServerAdapter):
2421 """ Adapter for Google App Engine. """
2422 quiet = True
2423 def run(self, handler):
2424 from google.appengine.ext.webapp import util
2425 # A main() function in the handler script enables 'App Caching'.
2426 # Lets makes sure it is there. This _really_ improves performance.
2427 module = sys.modules.get('__main__')
2428 if module and not hasattr(module, 'main'):
2429 module.main = lambda: util.run_wsgi_app(handler)
2430 util.run_wsgi_app(handler)
2431
2432
2433 class TwistedServer(ServerAdapter):
2434 """ Untested. """
2435 def run(self, handler):
2436 from twisted.web import server, wsgi
2437 from twisted.python.threadpool import ThreadPool
2438 from twisted.internet import reactor
2439 thread_pool = ThreadPool()
2440 thread_pool.start()
2441 reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
2442 factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler))
2443 reactor.listenTCP(self.port, factory, interface=self.host)
2444 reactor.run()
2445
2446
2447 class DieselServer(ServerAdapter):
2448 """ Untested. """
2449 def run(self, handler):
2450 from diesel.protocols.wsgi import WSGIApplication
2451 app = WSGIApplication(handler, port=self.port)
2452 app.run()
2453
2454
2455 class GeventServer(ServerAdapter):
2456 """ Untested. Options:
2457
2458 * `monkey` (default: True) fixes the stdlib to use greenthreads.
2459 * `fast` (default: False) uses libevent's http server, but has some
2460 issues: No streaming, no pipelining, no SSL.
2461 """
2462 def run(self, handler):
2463 from gevent import wsgi as wsgi_fast, pywsgi, monkey, local
2464 if self.options.get('monkey', True):
2465 if not threading.local is local.local: monkey.patch_all()
2466 wsgi = wsgi_fast if self.options.get('fast') else pywsgi
2467 log = None if self.quiet else 'default'
2468 wsgi.WSGIServer((self.host, self.port), handler, log=log).serve_forever()
2469
2470
2471 class GunicornServer(ServerAdapter):
2472 """ Untested. See http://gunicorn.org/configure.html for options. """
2473 def run(self, handler):
2474 from gunicorn.app.base import Application
2475
2476 config = {'bind': "%s:%d" % (self.host, int(self.port))}
2477 config.update(self.options)
2478
2479 class GunicornApplication(Application):
2480 def init(self, parser, opts, args):
2481 return config
2482
2483 def load(self):
2484 return handler
2485
2486 GunicornApplication().run()
2487
2488
2489 class EventletServer(ServerAdapter):
2490 """ Untested """
2491 def run(self, handler):
2492 from eventlet import wsgi, listen
2493 try:
2494 wsgi.server(listen((self.host, self.port)), handler,
2495 log_output=(not self.quiet))
2496 except TypeError:
2497 # Fallback, if we have old version of eventlet
2498 wsgi.server(listen((self.host, self.port)), handler)
2499
2500
2501 class RocketServer(ServerAdapter):
2502 """ Untested. """
2503 def run(self, handler):
2504 from rocket import Rocket
2505 server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler })
2506 server.start()
2507
2508
2509 class BjoernServer(ServerAdapter):
2510 """ Fast server written in C: https://github.com/jonashaag/bjoern """
2511 def run(self, handler):
2512 from bjoern import run
2513 run(handler, self.host, self.port)
2514
2515
2516 class AutoServer(ServerAdapter):
2517 """ Untested. """
2518 adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, WSGIRefServer]
2519 def run(self, handler):
2520 for sa in self.adapters:
2521 try:
2522 return sa(self.host, self.port, **self.options).run(handler)
2523 except ImportError:
2524 pass
2525
2526 server_names = {
2527 'cgi': CGIServer,
2528 'flup': FlupFCGIServer,
2529 'wsgiref': WSGIRefServer,
2530 'waitress': WaitressServer,
2531 'cherrypy': CherryPyServer,
2532 'paste': PasteServer,
2533 'fapws3': FapwsServer,
2534 'tornado': TornadoServer,
2535 'gae': AppEngineServer,
2536 'twisted': TwistedServer,
2537 'diesel': DieselServer,
2538 'meinheld': MeinheldServer,
2539 'gunicorn': GunicornServer,
2540 'eventlet': EventletServer,
2541 'gevent': GeventServer,
2542 'rocket': RocketServer,
2543 'bjoern' : BjoernServer,
2544 'auto': AutoServer,
2545 }
2546
2547
2548
2549
2550
2551
2552 ###############################################################################
2553 # Application Control ##########################################################
2554 ###############################################################################
2555
2556
2557 def load(target, **namespace):
2558 """ Import a module or fetch an object from a module.
2559
2560 * ``package.module`` returns `module` as a module object.
2561 * ``pack.mod:name`` returns the module variable `name` from `pack.mod`.
2562 * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result.
2563
2564 The last form accepts not only function calls, but any type of
2565 expression. Keyword arguments passed to this function are available as
2566 local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``
2567 """
2568 module, target = target.split(":", 1) if ':' in target else (target, None)
2569 if module not in sys.modules: __import__(module)
2570 if not target: return sys.modules[module]
2571 if target.isalnum(): return getattr(sys.modules[module], target)
2572 package_name = module.split('.')[0]
2573 namespace[package_name] = sys.modules[package_name]
2574 return eval('%s.%s' % (module, target), namespace)
2575
2576
2577 def load_app(target):
2578 """ Load a bottle application from a module and make sure that the import
2579 does not affect the current default application, but returns a separate
2580 application object. See :func:`load` for the target parameter. """
2581 global NORUN; NORUN, nr_old = True, NORUN
2582 try:
2583 tmp = default_app.push() # Create a new "default application"
2584 rv = load(target) # Import the target module
2585 return rv if callable(rv) else tmp
2586 finally:
2587 default_app.remove(tmp) # Remove the temporary added default application
2588 NORUN = nr_old
2589
2590 _debug = debug
2591 def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,
2592 interval=1, reloader=False, quiet=False, plugins=None,
2593 debug=False, **kargs):
2594 """ Start a server instance. This method blocks until the server terminates.
2595
2596 :param app: WSGI application or target string supported by
2597 :func:`load_app`. (default: :func:`default_app`)
2598 :param server: Server adapter to use. See :data:`server_names` keys
2599 for valid names or pass a :class:`ServerAdapter` subclass.
2600 (default: `wsgiref`)
2601 :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on
2602 all interfaces including the external one. (default: 127.0.0.1)
2603 :param port: Server port to bind to. Values below 1024 require root
2604 privileges. (default: 8080)
2605 :param reloader: Start auto-reloading server? (default: False)
2606 :param interval: Auto-reloader interval in seconds (default: 1)
2607 :param quiet: Suppress output to stdout and stderr? (default: False)
2608 :param options: Options passed to the server adapter.
2609 """
2610 if NORUN: return
2611 if reloader and not os.environ.get('BOTTLE_CHILD'):
2612 try:
2613 lockfile = None
2614 fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
2615 os.close(fd) # We only need this file to exist. We never write to it
2616 while os.path.exists(lockfile):
2617 args = [sys.executable] + sys.argv
2618 environ = os.environ.copy()
2619 environ['BOTTLE_CHILD'] = 'true'
2620 environ['BOTTLE_LOCKFILE'] = lockfile
2621 p = subprocess.Popen(args, env=environ)
2622 while p.poll() is None: # Busy wait...
2623 os.utime(lockfile, None) # I am alive!
2624 time.sleep(interval)
2625 if p.poll() != 3:
2626 if os.path.exists(lockfile): os.unlink(lockfile)
2627 sys.exit(p.poll())
2628 except KeyboardInterrupt:
2629 pass
2630 finally:
2631 if os.path.exists(lockfile):
2632 os.unlink(lockfile)
2633 return
2634
2635 try:
2636 _debug(debug)
2637 app = app or default_app()
2638 if isinstance(app, basestring):
2639 app = load_app(app)
2640 if not callable(app):
2641 raise ValueError("Application is not callable: %r" % app)
2642
2643 for plugin in plugins or []:
2644 app.install(plugin)
2645
2646 if server in server_names:
2647 server = server_names.get(server)
2648 if isinstance(server, basestring):
2649 server = load(server)
2650 if isinstance(server, type):
2651 server = server(host=host, port=port, **kargs)
2652 if not isinstance(server, ServerAdapter):
2653 raise ValueError("Unknown or unsupported server: %r" % server)
2654
2655 server.quiet = server.quiet or quiet
2656 if not server.quiet:
2657 _stderr("Bottle v%s server starting up (using %s)...\n" % (__version__, repr(server)))
2658 _stderr("Listening on http://%s:%d/\n" % (server.host, server.port))
2659 _stderr("Hit Ctrl-C to quit.\n\n")
2660
2661 if reloader:
2662 lockfile = os.environ.get('BOTTLE_LOCKFILE')
2663 bgcheck = FileCheckerThread(lockfile, interval)
2664 with bgcheck:
2665 server.run(app)
2666 if bgcheck.status == 'reload':
2667 sys.exit(3)
2668 else:
2669 server.run(app)
2670 except KeyboardInterrupt:
2671 pass
2672 except (SystemExit, MemoryError):
2673 raise
2674 except:
2675 if not reloader: raise
2676 if not getattr(server, 'quiet', quiet):
2677 print_exc()
2678 time.sleep(interval)
2679 sys.exit(3)
2680
2681
2682
2683 class FileCheckerThread(threading.Thread):
2684 ''' Interrupt main-thread as soon as a changed module file is detected,
2685 the lockfile gets deleted or gets to old. '''
2686
2687 def __init__(self, lockfile, interval):
2688 threading.Thread.__init__(self)
2689 self.lockfile, self.interval = lockfile, interval
2690 #: Is one of 'reload', 'error' or 'exit'
2691 self.status = None
2692
2693 def run(self):
2694 exists = os.path.exists
2695 mtime = lambda path: os.stat(path).st_mtime
2696 files = dict()
2697
2698 for module in list(sys.modules.values()):
2699 path = getattr(module, '__file__', '')
2700 if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
2701 if path and exists(path): files[path] = mtime(path)
2702
2703 while not self.status:
2704 if not exists(self.lockfile)\
2705 or mtime(self.lockfile) < time.time() - self.interval - 5:
2706 self.status = 'error'
2707 thread.interrupt_main()
2708 for path, lmtime in list(files.items()):
2709 if not exists(path) or mtime(path) > lmtime:
2710 self.status = 'reload'
2711 thread.interrupt_main()
2712 break
2713 time.sleep(self.interval)
2714
2715 def __enter__(self):
2716 self.start()
2717
2718 def __exit__(self, exc_type, exc_val, exc_tb):
2719 if not self.status: self.status = 'exit' # silent exit
2720 self.join()
2721 return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)
2722
2723
2724
2725
2726
2727 ###############################################################################
2728 # Template Adapters ############################################################
2729 ###############################################################################
2730
2731
2732 class TemplateError(HTTPError):
2733 def __init__(self, message):
2734 HTTPError.__init__(self, 500, message)
2735
2736
2737 class BaseTemplate(object):
2738 """ Base class and minimal API for template adapters """
2739 extensions = ['tpl','html','thtml','stpl']
2740 settings = {} #used in prepare()
2741 defaults = {} #used in render()
2742
2743 def __init__(self, source=None, name=None, lookup=[], encoding='utf8', **settings):
2744 """ Create a new template.
2745 If the source parameter (str or buffer) is missing, the name argument
2746 is used to guess a template filename. Subclasses can assume that
2747 self.source and/or self.filename are set. Both are strings.
2748 The lookup, encoding and settings parameters are stored as instance
2749 variables.
2750 The lookup parameter stores a list containing directory paths.
2751 The encoding parameter should be used to decode byte strings or files.
2752 The settings parameter contains a dict for engine-specific settings.
2753 """
2754 self.name = name
2755 self.source = source.read() if hasattr(source, 'read') else source
2756 self.filename = source.filename if hasattr(source, 'filename') else None
2757 self.lookup = [os.path.abspath(x) for x in lookup]
2758 self.encoding = encoding
2759 self.settings = self.settings.copy() # Copy from class variable
2760 self.settings.update(settings) # Apply
2761 if not self.source and self.name:
2762 self.filename = self.search(self.name, self.lookup)
2763 if not self.filename:
2764 raise TemplateError('Template %s not found.' % repr(name))
2765 if not self.source and not self.filename:
2766 raise TemplateError('No template specified.')
2767 self.prepare(**self.settings)
2768
2769 @classmethod
2770 def search(cls, name, lookup=[]):
2771 """ Search name in all directories specified in lookup.
2772 First without, then with common extensions. Return first hit. """
2773 if os.path.isfile(name): return name
2774 for spath in lookup:
2775 fname = os.path.join(spath, name)
2776 if os.path.isfile(fname):
2777 return fname
2778 for ext in cls.extensions:
2779 if os.path.isfile('%s.%s' % (fname, ext)):
2780 return '%s.%s' % (fname, ext)
2781
2782 @classmethod
2783 def global_config(cls, key, *args):
2784 ''' This reads or sets the global settings stored in class.settings. '''
2785 if args:
2786 cls.settings = cls.settings.copy() # Make settings local to class
2787 cls.settings[key] = args[0]
2788 else:
2789 return cls.settings[key]
2790
2791 def prepare(self, **options):
2792 """ Run preparations (parsing, caching, ...).
2793 It should be possible to call this again to refresh a template or to
2794 update settings.
2795 """
2796 raise NotImplementedError
2797
2798 def render(self, *args, **kwargs):
2799 """ 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
2801 must match self.encoding. This method must be thread-safe!
2802 Local variables may be provided in dictionaries (*args)
2803 or directly, as keywords (**kwargs).
2804 """
2805 raise NotImplementedError
2806
2807
2808 class MakoTemplate(BaseTemplate):
2809 def prepare(self, **options):
2810 from mako.template import Template
2811 from mako.lookup import TemplateLookup
2812 options.update({'input_encoding':self.encoding})
2813 options.setdefault('format_exceptions', bool(DEBUG))
2814 lookup = TemplateLookup(directories=self.lookup, **options)
2815 if self.source:
2816 self.tpl = Template(self.source, lookup=lookup, **options)
2817 else:
2818 self.tpl = Template(uri=self.name, filename=self.filename, lookup=lookup, **options)
2819
2820 def render(self, *args, **kwargs):
2821 for dictarg in args: kwargs.update(dictarg)
2822 _defaults = self.defaults.copy()
2823 _defaults.update(kwargs)
2824 return self.tpl.render(**_defaults)
2825
2826
2827 class CheetahTemplate(BaseTemplate):
2828 def prepare(self, **options):
2829 from Cheetah.Template import Template
2830 self.context = threading.local()
2831 self.context.vars = {}
2832 options['searchList'] = [self.context.vars]
2833 if self.source:
2834 self.tpl = Template(source=self.source, **options)
2835 else:
2836 self.tpl = Template(file=self.filename, **options)
2837
2838 def render(self, *args, **kwargs):
2839 for dictarg in args: kwargs.update(dictarg)
2840 self.context.vars.update(self.defaults)
2841 self.context.vars.update(kwargs)
2842 out = str(self.tpl)
2843 self.context.vars.clear()
2844 return out
2845
2846
2847 class Jinja2Template(BaseTemplate):
2848 def prepare(self, filters=None, tests=None, **kwargs):
2849 from jinja2 import Environment, FunctionLoader
2850 if 'prefix' in kwargs: # TODO: to be removed after a while
2851 raise RuntimeError('The keyword argument `prefix` has been removed. '
2852 'Use the full jinja2 environment name line_statement_prefix instead.')
2853 self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
2854 if filters: self.env.filters.update(filters)
2855 if tests: self.env.tests.update(tests)
2856 if self.source:
2857 self.tpl = self.env.from_string(self.source)
2858 else:
2859 self.tpl = self.env.get_template(self.filename)
2860
2861 def render(self, *args, **kwargs):
2862 for dictarg in args: kwargs.update(dictarg)
2863 _defaults = self.defaults.copy()
2864 _defaults.update(kwargs)
2865 return self.tpl.render(**_defaults)
2866
2867 def loader(self, name):
2868 fname = self.search(name, self.lookup)
2869 if not fname: return
2870 with open(fname, "rb") as f:
2871 return f.read().decode(self.encoding)
2872
2873
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):
2900 blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while',
2901 'with', 'def', 'class')
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 = {}
2919 enc = self.encoding
2920 self._str = lambda x: touni(x, enc)
2921 self._escape = lambda x: escape_func(touni(x, enc))
2922 if noescape:
2923 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
2933 @cached_property
2934 def co(self):
2935 return compile(self.code, self.filename or '<string>', 'exec')
2936
2937 @cached_property
2938 def code(self):
2939 stack = [] # Current Code indentation
2940 lineno = 0 # Current line of code
2941 ptrbuffer = [] # Buffer for printable strings and token tuple instances
2942 codebuffer = [] # Buffer for generated python code
2943 multiline = dedent = oneline = False
2944 template = self.source or open(self.filename, 'rb').read()
2945
2946 def yield_tokens(line):
2947 for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)):
2948 if i % 2:
2949 if part.startswith('!'): yield 'RAW', part[1:]
2950 else: yield 'CMD', part
2951 else: yield 'TXT', part
2952
2953 def flush(): # Flush the ptrbuffer
2954 if not ptrbuffer: return
2955 cline = ''
2956 for line in ptrbuffer:
2957 for token, value in line:
2958 if token == 'TXT': cline += repr(value)
2959 elif token == 'RAW': cline += '_str(%s)' % value
2960 elif token == 'CMD': cline += '_escape(%s)' % value
2961 cline += ', '
2962 cline = cline[:-2] + '\\\n'
2963 cline = cline[:-2]
2964 if cline[:-1].endswith('\\\\\\\\\\n'):
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:
3025 self.cache[_name] = self.__class__(name=_name, lookup=self.lookup)
3026 return self.cache[_name].execute(_stdout, kwargs)
3027
3028 def execute(self, _stdout, *args, **kwargs):
3029 for dictarg in args: kwargs.update(dictarg)
3030 env = self.defaults.copy()
3031 env.update({'_stdout': _stdout, '_printlist': _stdout.extend,
3032 '_include': self.subtemplate, '_str': self._str,
3033 '_escape': self._escape, 'get': env.get,
3034 'setdefault': env.setdefault, 'defined': env.__contains__})
3035 env.update(kwargs)
3036 eval(self.co, env)
3037 if '_rebase' in env:
3038 subtpl, rargs = env['_rebase']
3039 rargs['_base'] = _stdout[:] #copy stdout
3040 del _stdout[:] # clear stdout
3041 return self.subtemplate(subtpl,_stdout,rargs)
3042 return env
3043
3044 def render(self, *args, **kwargs):
3045 """ Render the template using keyword arguments as local variables. """
3046 for dictarg in args: kwargs.update(dictarg)
3047 stdout = []
3048 self.execute(stdout, kwargs)
3049 return ''.join(stdout)
3050
3051
3052 def template(*args, **kwargs):
3053 '''
3054 Get a rendered template as a string iterator.
3055 You can use a name, a filename or a template string as first parameter.
3056 Template rendering arguments can be passed as dictionaries
3057 or directly (as keyword arguments).
3058 '''
3059 tpl = args[0] if args else None
3060 template_adapter = kwargs.pop('template_adapter', SimpleTemplate)
3061 if tpl not in TEMPLATES or DEBUG:
3062 settings = kwargs.pop('template_settings', {})
3063 lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
3064 if isinstance(tpl, template_adapter):
3065 TEMPLATES[tpl] = tpl
3066 if settings: TEMPLATES[tpl].prepare(**settings)
3067 elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
3068 TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings)
3069 else:
3070 TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings)
3071 if not TEMPLATES[tpl]:
3072 abort(500, 'Template (%s) not found' % tpl)
3073 for dictarg in args[1:]: kwargs.update(dictarg)
3074 return TEMPLATES[tpl].render(kwargs)
3075
3076 mako_template = functools.partial(template, template_adapter=MakoTemplate)
3077 cheetah_template = functools.partial(template, template_adapter=CheetahTemplate)
3078 jinja2_template = functools.partial(template, template_adapter=Jinja2Template)
3079 simpletal_template = functools.partial(template, template_adapter=SimpleTALTemplate)
3080
3081
3082 def view(tpl_name, **defaults):
3083 ''' Decorator: renders a template for a handler.
3084 The handler can control its behavior like that:
3085
3086 - return a dict of template vars to fill out the template
3087 - return something other than a dict and the view decorator will not
3088 process the template, but return the handler result as is.
3089 This includes returning a HTTPResponse(dict) to get,
3090 for instance, JSON with autojson or other castfilters.
3091 '''
3092 def decorator(func):
3093 @functools.wraps(func)
3094 def wrapper(*args, **kwargs):
3095 result = func(*args, **kwargs)
3096 if isinstance(result, (dict, DictMixin)):
3097 tplvars = defaults.copy()
3098 tplvars.update(result)
3099 return template(tpl_name, **tplvars)
3100 return result
3101 return wrapper
3102 return decorator
3103
3104 mako_view = functools.partial(view, template_adapter=MakoTemplate)
3105 cheetah_view = functools.partial(view, template_adapter=CheetahTemplate)
3106 jinja2_view = functools.partial(view, template_adapter=Jinja2Template)
3107 simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate)
3108
3109
3110
3111
3112
3113
3114 ###############################################################################
3115 # Constants and Globals ########################################################
3116 ###############################################################################
3117
3118
3119 TEMPLATE_PATH = ['./', './views/']
3120 TEMPLATES = {}
3121 DEBUG = False
3122 NORUN = False # If set, run() does nothing. Used by load_app()
3123
3124 #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
3125 HTTP_CODES = httplib.responses
3126 HTTP_CODES[418] = "I'm a teapot" # RFC 2324
3127 HTTP_CODES[428] = "Precondition Required"
3128 HTTP_CODES[429] = "Too Many Requests"
3129 HTTP_CODES[431] = "Request Header Fields Too Large"
3130 HTTP_CODES[511] = "Network Authentication Required"
3131 _HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.items())
3132
3133 #: The default template used for error pages. Override with @error()
3134 ERROR_PAGE_TEMPLATE = """
3135 %%try:
3136 %%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">
3139 <html>
3140 <head>
3141 <title>Error {{e.status}}: {{status_name}}</title>
3142 <style type="text/css">
3143 html {background-color: #eee; font-family: sans;}
3144 body {background-color: #fff; border: 1px solid #ddd;
3145 padding: 15px; margin: 15px;}
3146 pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}
3147 </style>
3148 </head>
3149 <body>
3150 <h1>Error {{e.status}}: {{status_name}}</h1>
3151 <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt>
3152 caused an error:</p>
3153 <pre>{{e.output}}</pre>
3154 %%if DEBUG and e.exception:
3155 <h2>Exception:</h2>
3156 <pre>{{repr(e.exception)}}</pre>
3157 %%end
3158 %%if DEBUG and e.traceback:
3159 <h2>Traceback:</h2>
3160 <pre>{{e.traceback}}</pre>
3161 %%end
3162 </body>
3163 </html>
3164 %%except ImportError:
3165 <b>ImportError:</b> Could not generate the error page. Please add bottle to
3166 the import path.
3167 %%end
3168 """ % __name__
3169
3170 #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
3171 #: request callback, this instance always refers to the *current* request
3172 #: (even on a multithreaded server).
3173 request = LocalRequest()
3174
3175 #: A thread-safe instance of :class:`LocalResponse`. It is used to change the
3176 #: HTTP response for the *current* request.
3177 response = LocalResponse()
3178
3179 #: A thread-safe namespace. Not used by Bottle.
3180 local = threading.local()
3181
3182 # Initialize app stack (create first empty Bottle app)
3183 # BC: 0.6.4 and needed for run()
3184 app = default_app = AppStack()
3185 app.push()
3186
3187 #: A virtual package that redirects import statements.
3188 #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
3189 ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module
3190
3191 if __name__ == '__main__':
3192 opt, args, parser = _cmd_options, _cmd_args, _cmd_parser
3193 if opt.version:
3194 _stdout('Bottle %s\n'%__version__)
3195 sys.exit(0)
3196 if not args:
3197 parser.print_help()
3198 _stderr('\nError: No application specified.\n')
3199 sys.exit(1)
3200
3201 sys.path.insert(0, '.')
3202 sys.modules.setdefault('bottle', sys.modules['__main__'])
3203
3204 host, port = (opt.bind or 'localhost'), 8080
3205 if ':' in host:
3206 host, port = host.rsplit(':', 1)
3207
3208 run(args[0], host=host, port=port, server=opt.server,
3209 reloader=opt.reload, plugins=opt.plugin, debug=opt.debug)
3210
3211
3212
3213
3214 # THE END