diff options
Diffstat (limited to 'module/lib/bottle.py')
| -rw-r--r-- | module/lib/bottle.py | 2026 | 
1 files changed, 1215 insertions, 811 deletions
| diff --git a/module/lib/bottle.py b/module/lib/bottle.py index f449e182c..f8624bf13 100644 --- a/module/lib/bottle.py +++ b/module/lib/bottle.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python  # -*- coding: utf-8 -*-  """  Bottle is a fast and simple micro-framework for small web applications. It @@ -15,9 +16,26 @@ License: MIT (see LICENSE.txt for details)  from __future__ import with_statement  __author__ = 'Marcel Hellkamp' -__version__ = '0.9.1' +__version__ = '0.10.2'  __license__ = 'MIT' +# The gevent server adapter needs to patch some modules before they are imported +# This is why we parse the commandline parameters here but handle them later +if __name__ == '__main__': +    from optparse import OptionParser +    _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app") +    _opt = _cmd_parser.add_option +    _opt("--version", action="store_true", help="show version number.") +    _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") +    _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") +    _opt("-p", "--plugin", action="append", help="install additional plugin/s.") +    _opt("--debug", action="store_true", help="start server in debug mode.") +    _opt("--reload", action="store_true", help="auto-reload on file changes.") +    _cmd_options, _cmd_args = _cmd_parser.parse_args() +    if _cmd_options.server and _cmd_options.server.startswith('gevent'): +        import gevent.monkey; gevent.monkey.patch_all() + +import sys  import base64  import cgi  import email.utils @@ -30,7 +48,6 @@ import mimetypes  import os  import re  import subprocess -import sys  import tempfile  import thread  import threading @@ -38,10 +55,16 @@ import time  import warnings  from Cookie import SimpleCookie +from datetime import date as datedate, datetime, timedelta  from tempfile import TemporaryFile -from traceback import format_exc -from urllib import urlencode, quote as urlquote, unquote as urlunquote -from urlparse import urlunsplit, urljoin, SplitResult as UrlSplitResult +from traceback import format_exc, print_exc +from urlparse import urljoin, SplitResult as UrlSplitResult + +# Workaround for a bug in some versions of lib2to3 (fixed on CPython 2.7 and 3.2) +import urllib +urlencode = urllib.urlencode +urlquote = urllib.quote +urlunquote = urllib.unquote  try: from collections import MutableMapping as DictMixin  except ImportError: # pragma: no cover @@ -55,16 +78,25 @@ try: import cPickle as pickle  except ImportError: # pragma: no cover      import pickle -try: from json import dumps as json_dumps +try: from json import dumps as json_dumps, loads as json_lds  except ImportError: # pragma: no cover -    try: from simplejson import dumps as json_dumps +    try: from simplejson import dumps as json_dumps, loads as json_lds      except ImportError: # pragma: no cover -        try: from django.utils.simplejson import dumps as json_dumps +        try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds          except ImportError: # pragma: no cover -            json_dumps = None +            def json_dumps(data): +                raise ImportError("JSON support requires Python 2.6 or simplejson.") +            json_lds = json_dumps +py3k = sys.version_info >= (3,0,0)  NCTextIOWrapper = None -if sys.version_info >= (3,0,0): # pragma: no cover + +if sys.version_info < (2,6,0): +    msg = "Python 2.5 support may be dropped in future versions of Bottle." +    warnings.warn(msg, DeprecationWarning) + +if py3k: # pragma: no cover +    json_loads = lambda s: json_lds(touni(s))      # See Request.POST      from io import BytesIO      def touni(x, enc='utf8', err='strict'): @@ -77,6 +109,7 @@ if sys.version_info >= (3,0,0): # pragma: no cover                  the wrapped buffer. This subclass keeps it open. '''              def close(self): pass  else: +    json_loads = json_lds      from StringIO import StringIO as BytesIO      bytes = str      def touni(x, enc='utf8', err='strict'): @@ -87,17 +120,16 @@ def tob(data, enc='utf8'):      """ Convert anything to bytes """      return data.encode(enc) if isinstance(data, unicode) else bytes(data) -# Convert strings and unicode to native strings -if sys.version_info >= (3,0,0): -    tonat = touni -else: -    tonat = tob +tonat = touni if py3k else tob  tonat.__doc__ = """ Convert anything to native strings """ +def try_update_wrapper(wrapper, wrapped, *a, **ka): +    try: # Bug: functools breaks if wrapper is an instane method +        functools.update_wrapper(wrapper, wrapped, *a, **ka) +    except AttributeError: pass  # Backward compatibility -def depr(message, critical=False): -    if critical: raise DeprecationWarning(message) +def depr(message):      warnings.warn(message, DeprecationWarning, stacklevel=3) @@ -119,7 +151,7 @@ class DictProperty(object):          return self      def __get__(self, obj, cls): -        if not obj: return self +        if obj is None: return self          key, storage = self.key, getattr(obj, self.attr)          if key not in storage: storage[key] = self.getter(obj)          return storage[key] @@ -132,10 +164,22 @@ class DictProperty(object):          if self.read_only: raise AttributeError("Read-Only property.")          del getattr(obj, self.attr)[self.key] -def cached_property(func): -    ''' A property that, if accessed, replaces itself with the computed -        value. Subsequent accesses won't call the getter again. ''' -    return DictProperty('__dict__')(func) + +class CachedProperty(object): +    ''' A property that is only computed once per instance and then replaces +        itself with an ordinary attribute. Deleting the attribute resets the +        property. ''' + +    def __init__(self, func): +        self.func = func + +    def __get__(self, obj, cls): +        if obj is None: return self +        value = obj.__dict__[self.func.__name__] = self.func(obj) +        return value + +cached_property = CachedProperty +  class lazy_attribute(object): # Does not need configuration -> lower-case name      ''' A property that caches itself to the class object. ''' @@ -163,6 +207,8 @@ class BottleException(Exception):      pass +#TODO: These should subclass BaseRequest +  class HTTPResponse(BottleException):      """ Used to break execution and immediately finish the response """      def __init__(self, output='', status=200, header=None): @@ -207,15 +253,14 @@ class RouteReset(BottleException):      """ If raised by a plugin or request handler, the route is reset and all          plugins are re-applied. """ +class RouterUnknownModeError(RouteError): pass  class RouteSyntaxError(RouteError):      """ The route parser found something not supported by this router """ -  class RouteBuildError(RouteError):      """ The route could not been built """ -  class Router(object):      ''' A Router is an ordered collection of route->target pairs. It is used to          efficiently match WSGI requests against a number of routes and return @@ -224,83 +269,153 @@ class Router(object):          and a HTTP method.          The path-rule is either a static path (e.g. `/contact`) or a dynamic -        path that contains wildcards (e.g. `/wiki/:page`). By default, wildcards -        consume characters up to the next slash (`/`). To change that, you may -        add a regular expression pattern (e.g. `/wiki/:page#[a-z]+#`). - -        For performance reasons, static routes (rules without wildcards) are -        checked first. Dynamic routes are searched in order. Try to avoid -        ambiguous or overlapping rules. - -        The HTTP method string matches only on equality, with two exceptions: -          * ´GET´ routes also match ´HEAD´ requests if there is no appropriate -            ´HEAD´ route installed. -          * ´ANY´ routes do match if there is no other suitable route installed. - -        An optional ``name`` parameter is used by :meth:`build` to identify -        routes. +        path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax +        and details on the matching order are described in docs:`routing`.      ''' -    default = '[^/]+' - -    @lazy_attribute -    def syntax(cls): -        return re.compile(r'(?<!\\):([a-zA-Z_][a-zA-Z_0-9]*)?(?:#(.*?)#)?') +    default_pattern = '[^/]+' +    default_filter   = 're' +    #: Sorry for the mess. It works. Trust me. +    rule_syntax = re.compile('(\\\\*)'\ +        '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ +          '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ +            '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + +    def __init__(self, strict=False): +        self.rules    = {} # A {rule: Rule} mapping +        self.builder  = {} # A rule/name->build_info mapping +        self.static   = {} # Cache for static routes: {path: {method: target}} +        self.dynamic  = [] # Cache for dynamic routes. See _compile() +        #: If true, static routes are no longer checked first. +        self.strict_order = strict +        self.filters = {'re': self.re_filter, 'int': self.int_filter, +                        'float': self.re_filter, 'path': self.path_filter} + +    def re_filter(self, conf): +        return conf or self.default_pattern, None, None + +    def int_filter(self, conf): +        return r'-?\d+', int, lambda x: str(int(x)) + +    def float_filter(self, conf): +        return r'-?\d*\.\d+', float, lambda x: str(float(x)) + +    def path_filter(self, conf): +        return r'.*?', None, None +     +    def add_filter(self, name, func): +        ''' Add a filter. The provided function is called with the configuration +        string as parameter and must return a (regexp, to_python, to_url) tuple. +        The first element is a string, the last two are callables or None. ''' +        self.filters[name] = func +     +    def parse_rule(self, rule): +        ''' Parses a rule into a (name, filter, conf) token stream. If mode is +            None, name contains a static rule part. ''' +        offset, prefix = 0, '' +        for match in self.rule_syntax.finditer(rule): +            prefix += rule[offset:match.start()] +            g = match.groups() +            if len(g[0])%2: # Escaped wildcard +                prefix += match.group(0)[len(g[0]):] +                offset = match.end() +                continue +            if prefix: yield prefix, None, None +            name, filtr, conf = g[1:4] if not g[2] is None else g[4:7] +            if not filtr: filtr = self.default_filter +            yield name, filtr, conf or None +            offset, prefix = match.end(), '' +        if offset <= len(rule) or prefix: +            yield prefix+rule[offset:], None, None + +    def add(self, rule, method, target, name=None): +        ''' Add a new route or replace the target for an existing route. ''' +        if rule in self.rules: +            self.rules[rule][method] = target +            if name: self.builder[name] = self.builder[rule] +            return + +        target = self.rules[rule] = {method: target} + +        # Build pattern and other structures for dynamic routes +        anons = 0      # Number of anonymous wildcards +        pattern = ''   # Regular expression  pattern +        filters = []   # Lists of wildcard input filters +        builder = []   # Data structure for the URL builder +        is_static = True +        for key, mode, conf in self.parse_rule(rule): +            if mode: +                is_static = False +                mask, in_filter, out_filter = self.filters[mode](conf) +                if key: +                    pattern += '(?P<%s>%s)' % (key, mask) +                else: +                    pattern += '(?:%s)' % mask +                    key = 'anon%d' % anons; anons += 1 +                if in_filter: filters.append((key, in_filter)) +                builder.append((key, out_filter or str)) +            elif key: +                pattern += re.escape(key) +                builder.append((None, key)) +        self.builder[rule] = builder +        if name: self.builder[name] = builder + +        if is_static and not self.strict_order: +            self.static[self.build(rule)] = target +            return -    def __init__(self): -        self.routes = {}  # A {rule: {method: target}} mapping -        self.rules  = []  # An ordered list of rules -        self.named  = {}  # A name->(rule, build_info) mapping -        self.static = {}  # Cache for static routes: {path: {method: target}} -        self.dynamic = [] # Cache for dynamic routes. See _compile() +        def fpat_sub(m): +            return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' +        flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern) -    def add(self, rule, method, target, name=None, static=False): -        ''' Add a new route or replace the target for an existing route. ''' -        if static: -            depr("Use a backslash to escape ':' in routes.") # 0.9 -            rule = rule.replace(':','\\:') +        try: +            re_match = re.compile('^(%s)$' % pattern).match +        except re.error, e: +            raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) + +        def match(path): +            """ Return an url-argument dictionary. """ +            url_args = re_match(path).groupdict() +            for name, wildcard_filter in filters: +                try: +                    url_args[name] = wildcard_filter(url_args[name]) +                except ValueError: +                    raise HTTPError(400, 'Path has wrong format.') +            return url_args -        if rule in self.routes: -            self.routes[rule][method.upper()] = target -        else: -            self.routes[rule] = {method.upper(): target} -            self.rules.append(rule) -            if self.static or self.dynamic: # Clear precompiler cache. -                self.static, self.dynamic = {}, {} -        if name: -            self.named[name] = (rule, None) - -    def build(self, _name, *anon, **args): -        ''' Return a string that matches a named route. Use keyword arguments -            to fill out named wildcards. Remaining arguments are appended as a -            query string. Raises RouteBuildError or KeyError.''' -        if _name not in self.named: -            raise RouteBuildError("No route with that name.", _name) -        rule, pairs = self.named[_name] -        if not pairs: -            token = self.syntax.split(rule) -            parts = [p.replace('\\:',':') for p in token[::3]] -            names = token[1::3] -            if len(parts) > len(names): names.append(None) -            pairs = zip(parts, names) -            self.named[_name] = (rule, pairs)          try: -            anon = list(anon) -            url = [s if k is None -                   else s+str(args.pop(k)) if k else s+str(anon.pop()) -                   for s, k in pairs] -        except IndexError: -            msg = "Not enough arguments to fill out anonymous wildcards." -            raise RouteBuildError(msg) +            combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern) +            self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) +            self.dynamic[-1][1].append((match, target)) +        except (AssertionError, IndexError), e: # AssertionError: Too many groups +            self.dynamic.append((re.compile('(^%s$)' % flat_pattern), +                                [(match, target)])) +        return match + +    def build(self, _name, *anons, **query): +        ''' Build an URL by filling the wildcards in a rule. ''' +        builder = self.builder.get(_name) +        if not builder: raise RouteBuildError("No route with that name.", _name) +        try: +            for i, value in enumerate(anons): query['anon%d'%i] = value +            url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder]) +            return url if not query else url+'?'+urlencode(query)          except KeyError, e: -            raise RouteBuildError(*e.args) - -        if args: url += ['?', urlencode(args)] -        return ''.join(url) +            raise RouteBuildError('Missing URL argument: %r' % e.args[0])      def match(self, environ): -        ''' Return a (target, url_agrs) tuple or raise HTTPError(404/405). ''' -        targets, urlargs = self._match_path(environ) +        ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' +        path, targets, urlargs = environ['PATH_INFO'] or '/', None, {} +        if path in self.static: +            targets = self.static[path] +        else: +            for combined, rules in self.dynamic: +                match = combined.match(path) +                if not match: continue +                getargs, targets = rules[match.lastindex - 1] +                urlargs = getargs(path) if getargs else {} +                break +          if not targets:              raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO']))          method = environ['REQUEST_METHOD'].upper() @@ -316,66 +431,90 @@ class Router(object):          raise HTTPError(405, "Method not allowed.",                          header=[('Allow',",".join(allowed))]) -    def _match_path(self, environ): -        ''' Optimized PATH_INFO matcher. ''' -        path = environ['PATH_INFO'] or '/' -        # Assume we are in a warm state. Search compiled rules first. -        match = self.static.get(path) -        if match: return match, {} -        for combined, rules in self.dynamic: -            match = combined.match(path) -            if not match: continue -            gpat, match = rules[match.lastindex - 1] -            return match, gpat.match(path).groupdict() if gpat else {} -        # Lazy-check if we are really in a warm state. If yes, stop here. -        if self.static or self.dynamic or not self.routes: return None, {} -        # Cold state: We have not compiled any rules yet. Do so and try again. -        if not environ.get('wsgi.run_once'): -            self._compile() -            return self._match_path(environ) -        # For run_once (CGI) environments, don't compile. Just check one by one. -        epath = path.replace(':','\\:') # Turn path into its own static rule. -        match = self.routes.get(epath) # This returns static rule only. -        if match: return match, {} -        for rule in self.rules: -            #: Skip static routes to reduce re.compile() calls. -            if rule.count(':') < rule.count('\\:'): continue -            match = self._compile_pattern(rule).match(path) -            if match: return self.routes[rule], match.groupdict() -        return None, {} - -    def _compile(self): -        ''' Prepare static and dynamic search structures. ''' -        self.static = {} -        self.dynamic = [] -        def fpat_sub(m): -            return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' -        for rule in self.rules: -            target = self.routes[rule] -            if not self.syntax.search(rule): -                self.static[rule.replace('\\:',':')] = target -                continue -            gpat = self._compile_pattern(rule) -            fpat = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, gpat.pattern) -            gpat = gpat if gpat.groupindex else None + + +class Route(object): +    ''' This class wraps a route callback along with route specific metadata and +        configuration and applies Plugins on demand. It is also responsible for +        turing an URL path rule into a regular expression usable by the Router. +    ''' + + +    def __init__(self, app, rule, method, callback, name=None, +                 plugins=None, skiplist=None, **config): +        #: The application this route is installed to. +        self.app = app +        #: The path-rule string (e.g. ``/wiki/:page``). +        self.rule = rule +        #: The HTTP method as a string (e.g. ``GET``). +        self.method = method +        #: The original callback with no plugins applied. Useful for introspection. +        self.callback = callback +        #: The name of the route (if specified) or ``None``. +        self.name = name or None +        #: A list of route-specific plugins (see :meth:`Bottle.route`). +        self.plugins = plugins or [] +        #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). +        self.skiplist = skiplist or [] +        #: Additional keyword arguments passed to the :meth:`Bottle.route` +        #: decorator are stored in this dictionary. Used for route-specific +        #: plugin configuration and meta-data. +        self.config = ConfigDict(config) + +    def __call__(self, *a, **ka): +        depr("Some APIs changed to return Route() instances instead of"\ +             " callables. Make sure to use the Route.call method and not to"\ +             " call Route instances directly.") +        return self.call(*a, **ka) + +    @cached_property +    def call(self): +        ''' The route callback with all plugins applied. This property is +            created on demand and then cached to speed up subsequent requests.''' +        return self._make_callback() + +    def reset(self): +        ''' Forget any cached values. The next time :attr:`call` is accessed, +            all plugins are re-applied. ''' +        self.__dict__.pop('call', None) + +    def prepare(self): +        ''' Do all on-demand work immediately (useful for debugging).''' +        self.call + +    @property +    def _context(self): +        depr('Switch to Plugin API v2 and access the Route object directly.') +        return dict(rule=self.rule, method=self.method, callback=self.callback, +                    name=self.name, app=self.app, config=self.config, +                    apply=self.plugins, skip=self.skiplist) + +    def all_plugins(self): +        ''' Yield all Plugins affecting this route. ''' +        unique = set() +        for p in reversed(self.app.plugins + self.plugins): +            if True in self.skiplist: break +            name = getattr(p, 'name', False) +            if name and (name in self.skiplist or name in unique): continue +            if p in self.skiplist or type(p) in self.skiplist: continue +            if name: unique.add(name) +            yield p + +    def _make_callback(self): +        callback = self.callback +        for plugin in self.all_plugins():              try: -                combined = '%s|(%s)' % (self.dynamic[-1][0].pattern, fpat) -                self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) -                self.dynamic[-1][1].append((gpat, target)) -            except (AssertionError, IndexError), e: # AssertionError: Too many groups -                self.dynamic.append((re.compile('(^%s$)'%fpat), -                                    [(gpat, target)])) -            except re.error, e: -                raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) - -    def _compile_pattern(self, rule): -        ''' Return a regular expression with named groups for each wildcard. ''' -        out = '' -        for i, part in enumerate(self.syntax.split(rule)): -            if i%3 == 0:   out += re.escape(part.replace('\\:',':')) -            elif i%3 == 1: out += '(?P<%s>' % part if part else '(?:' -            else:          out += '%s)' % (part or '[^/]+') -        return re.compile('^%s$'%out) +                if hasattr(plugin, 'apply'): +                    api = getattr(plugin, 'api', 1) +                    context = self if api > 1 else self._context +                    callback = plugin.apply(callback, context) +                else: +                    callback = plugin(callback) +            except RouteReset: # Try again with changed configuration. +                return self._make_callback() +            if not callback is self.callback: +                try_update_wrapper(callback, self.callback) +        return callback @@ -394,61 +533,62 @@ class Bottle(object):          """ Create a new bottle instance.              You usually don't do that. Use `bottle.app.push()` instead.          """ -        self.routes = [] # List of installed routes including metadata. -        self.router = Router() # Maps requests to self.route indices. -        self.ccache = {} # Cache for callbacks with plugins applied. - +        self.routes = [] # List of installed :class:`Route` instances. +        self.router = Router() # Maps requests to :class:`Route` instances.          self.plugins = [] # List of installed plugins. -        self.mounts = {}          self.error_handler = {}          #: If true, most exceptions are catched and returned as :exc:`HTTPError` +        self.config = ConfigDict(config or {})          self.catchall = catchall -        self.config = config or {} -        self.serve = True -        # Default plugins -        self.hooks = self.install(HooksPlugin()) -        self.typefilter = self.install(TypeFilterPlugin()) +        #: An instance of :class:`HooksPlugin`. Empty by default. +        self.hooks = HooksPlugin() +        self.install(self.hooks)          if autojson:              self.install(JSONPlugin())          self.install(TemplatePlugin()) -    def optimize(self, *a, **ka): -        depr("Bottle.optimize() is obsolete.") +    def mount(self, prefix, app, **options): +        ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific +            URL prefix. Example:: -    def mount(self, app, prefix, **options): -        ''' Mount an application to a specific URL prefix. The prefix is added -            to SCIPT_PATH and removed from PATH_INFO before the sub-application -            is called. +                root_app.mount('/admin/', admin_app) -            :param app: an instance of :class:`Bottle`. -            :param prefix: path prefix used as a mount-point. +            :param prefix: path prefix or `mount-point`. If it ends in a slash, +                that slash is mandatory. +            :param app: an instance of :class:`Bottle` or a WSGI application.              All other parameters are passed to the underlying :meth:`route` call.          ''' -        if not isinstance(app, Bottle): -            raise TypeError('Only Bottle instances are supported for now.') -        prefix = '/'.join(filter(None, prefix.split('/'))) -        if not prefix: -            raise TypeError('Empty prefix. Perhaps you want a merge()?') -        for other in self.mounts: -            if other.startswith(prefix): -                raise TypeError('Conflict with existing mount: %s' % other) -        path_depth = prefix.count('/') + 1 -        options.setdefault('method', 'ANY') +        if isinstance(app, basestring): +            prefix, app = app, prefix +            depr('Parameter order of Bottle.mount() changed.') # 0.10 + +        parts = filter(None, prefix.split('/')) +        if not parts: raise ValueError('Empty path prefix.') +        path_depth = len(parts)          options.setdefault('skip', True) -        self.mounts[prefix] = app -        @self.route('/%s/:#.*#' % prefix, **options) -        def mountpoint(): -            request.path_shift(path_depth) -            return app.handle(request.environ) +        options.setdefault('method', 'ANY') -    def add_filter(self, ftype, func): -        depr("Filters are deprecated and can be replaced with plugins.") #0.9 -        self.typefilter.add(ftype, func) +        @self.route('/%s/:#.*#' % '/'.join(parts), **options) +        def mountpoint(): +            try: +                request.path_shift(path_depth) +                rs = BaseResponse([], 200) +                def start_response(status, header): +                    rs.status = status +                    for name, value in header: rs.add_header(name, value) +                    return rs.body.append +                rs.body = itertools.chain(rs.body, app(request.environ, start_response)) +                return HTTPResponse(rs.body, rs.status, rs.headers) +            finally: +                request.path_shift(-path_depth) + +        if not prefix.endswith('/'): +            self.route('/' + '/'.join(parts), callback=mountpoint, **options)      def install(self, plugin): -        ''' Add a plugin to the list of plugins and prepare it for beeing +        ''' Add a plugin to the list of plugins and prepare it for being              applied to all routes of this application. A plugin may be a simple              decorator or an object that implements the :class:`Plugin` API.          ''' @@ -460,11 +600,10 @@ class Bottle(object):          return plugin      def uninstall(self, plugin): -        ''' Uninstall plugins. Pass an instance to remove a specific plugin. -            Pass a type object to remove all plugins that match that type. -            Subclasses are not removed. Pass a string to remove all plugins with -            a matching ``name`` attribute. Pass ``True`` to remove all plugins. -            The list of affected plugins is returned. ''' +        ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type +            object to remove all plugins that match that type, a string to remove +            all plugins with a matching ``name`` attribute or ``True`` to remove all +            plugins. Return the list of removed plugins. '''          removed, remove = [], plugin          for i, plugin in list(enumerate(self.plugins))[::-1]:              if remove is True or remove is plugin or remove is type(plugin) \ @@ -475,15 +614,17 @@ class Bottle(object):          if removed: self.reset()          return removed -    def reset(self, id=None): +    def reset(self, route=None):          ''' Reset all routes (force plugins to be re-applied) and clear all -            caches. If an ID is given, only that specific route is affected. ''' -        if id is None: self.ccache.clear() -        else: self.ccache.pop(id, None) +            caches. If an ID or route object is given, only that specific route +            is affected. ''' +        if route is None: routes = self.routes +        elif isinstance(route, Route): routes = [route] +        else: routes = [self.routes[route]] +        for route in routes: route.reset()          if DEBUG: -            for route in self.routes: -                if route['id'] not in self.ccache: -                    self.ccache[route['id']] = self._build_callback(route) +            for route in routes: route.prepare() +        self.hooks.trigger('app_reset')      def close(self):          ''' Close the application and all installed plugins. ''' @@ -492,45 +633,10 @@ class Bottle(object):          self.stopped = True      def match(self, environ): -        """ (deprecated) Search for a matching route and return a -            (callback, urlargs) tuple. -            The first element is the associated route callback with plugins -            applied. The second value is a dictionary with parameters extracted -            from the URL. The :class:`Router` raises :exc:`HTTPError` (404/405) -            on a non-match.""" -        depr("This method will change semantics in 0.10.") -        return self._match(environ) -         -    def _match(self, environ): -        handle, args = self.router.match(environ) -        environ['route.handle'] = handle # TODO move to router? -        environ['route.url_args'] = args -        try: -            return self.ccache[handle], args -        except KeyError: -            config = self.routes[handle] -            callback = self.ccache[handle] = self._build_callback(config) -            return callback, args - -    def _build_callback(self, config): -        ''' Apply plugins to a route and return a new callable. ''' -        wrapped = config['callback'] -        plugins = self.plugins + config['apply'] -        skip    = config['skip'] -        try: -            for plugin in reversed(plugins): -                if True in skip: break -                if plugin in skip or type(plugin) in skip: continue -                if getattr(plugin, 'name', True) in skip: continue -                if hasattr(plugin, 'apply'): -                    wrapped = plugin.apply(wrapped, config) -                else: -                    wrapped = plugin(wrapped) -                if not wrapped: break -                functools.update_wrapper(wrapped, config['callback']) -            return wrapped -        except RouteReset: # A plugin may have changed the config dict inplace. -            return self._build_callback(config) # Apply all plugins again. +        """ Search for a matching route and return a (:class:`Route` , urlargs) +            tuple. The second value is a dictionary with parameters extracted +            from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" +        return self.router.match(environ)      def get_url(self, routename, **kargs):          """ Return a string that matches a named route """ @@ -566,31 +672,20 @@ class Bottle(object):              configuration and passed to plugins (see :meth:`Plugin.apply`).          """          if callable(path): path, callback = None, path -          plugins = makelist(apply)          skiplist = makelist(skip) -        if 'decorate' in config: -            depr("The 'decorate' parameter was renamed to 'apply'") # 0.9 -            plugins += makelist(config.pop('decorate')) -        if config.pop('no_hooks', False): -            depr("The no_hooks parameter is no longer used. Add 'hooks' to the"\ -                 " list of skipped plugins instead.") # 0.9 -            skiplist.append('hooks') -        static = config.get('static', False) # depr 0.9 -          def decorator(callback): +            # TODO: Documentation and tests +            if isinstance(callback, basestring): callback = load(callback)              for rule in makelist(path) or yieldroutes(callback):                  for verb in makelist(method):                      verb = verb.upper() -                    cfg = dict(rule=rule, method=verb, callback=callback, -                               name=name, app=self, config=config, -                               apply=plugins, skip=skiplist) -                    self.routes.append(cfg) -                    cfg['id'] = self.routes.index(cfg) -                    self.router.add(rule, verb, cfg['id'], name=name, static=static) -                    if DEBUG: self.ccache[cfg['id']] = self._build_callback(cfg) +                    route = Route(self, rule, verb, callback, name=name, +                                  plugins=plugins, skiplist=skiplist, **config) +                    self.routes.append(route) +                    self.router.add(rule, verb, route, name=name) +                    if DEBUG: route.prepare()              return callback -          return decorator(callback) if callback else decorator      def get(self, path=None, method='GET', **options): @@ -623,14 +718,6 @@ class Bottle(object):              return func          return wrapper -    def add_hook(self, name, func): -        depr("Call Bottle.hooks.add() instead.") #0.9 -        self.hooks.add(name, func) - -    def remove_hook(self, name, func): -        depr("Call Bottle.hooks.remove() instead.") #0.9 -        self.hooks.remove(name, func) -      def handle(self, path, method='GET'):          """ (deprecated) Execute the first matching route callback and return              the result. :exc:`HTTPResponse` exceptions are catched and returned. @@ -641,24 +728,25 @@ class Bottle(object):          if isinstance(path, dict):              return self._handle(path)          return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) -         +      def _handle(self, environ): -        if not self.serve: -            depr("Bottle.serve will be removed in 0.10.") -            return HTTPError(503, "Server stopped")          try: -            callback, args = self._match(environ) -            return callback(**args) +            route, args = self.router.match(environ) +            environ['route.handle'] = environ['bottle.route'] = route +            environ['route.url_args'] = args +            return route.call(**args)          except HTTPResponse, r:              return r -        except RouteReset: # Route reset requested by the callback or a plugin. -            del self.ccache[handle] -            return self.handle(environ) # Try again. +        except RouteReset: +            route.reset() +            return self._handle(environ)          except (KeyboardInterrupt, SystemExit, MemoryError):              raise          except Exception, e:              if not self.catchall: raise -            return HTTPError(500, "Internal Server Error", e, format_exc(10)) +            stacktrace = format_exc(10) +            environ['wsgi.errors'].write(stacktrace) +            return HTTPError(500, "Internal Server Error", e, stacktrace)      def _cast(self, out, request, response, peek=None):          """ Try to convert the parameter into something WSGI compatible and set @@ -669,7 +757,7 @@ class Bottle(object):          # Empty output is done here          if not out: -            response.headers['Content-Length'] = 0 +            response['Content-Length'] = 0              return []          # Join lists of byte or unicode strings. Mixed lists are NOT supported          if isinstance(out, (tuple, list))\ @@ -680,9 +768,10 @@ class Bottle(object):              out = out.encode(response.charset)          # Byte Strings are just returned          if isinstance(out, bytes): -            response.headers['Content-Length'] = str(len(out)) +            response['Content-Length'] = len(out)              return [out]          # HTTPError or HTTPException (recursive, because they may wrap anything) +        # TODO: Handle these explicitly in handle() or make them iterable.          if isinstance(out, HTTPError):              out.apply(response)              out = self.error_handler.get(out.status, repr)(out) @@ -732,14 +821,13 @@ class Bottle(object):              environ['bottle.app'] = self              request.bind(environ)              response.bind() -            out = self._handle(environ) -            out = self._cast(out, request, response) +            out = self._cast(self._handle(environ), request, response)              # rfc2616 section 4.3 -            if response.status in (100, 101, 204, 304) or request.method == 'HEAD': +            if response._status_code in (100, 101, 204, 304)\ +            or request.method == 'HEAD':                  if hasattr(out, 'close'): out.close()                  out = [] -            status = '%d %s' % (response.status, HTTP_CODES[response.status]) -            start_response(status, response.headerlist) +            start_response(response._status_line, list(response.iter_headers()))              return out          except (KeyboardInterrupt, SystemExit, MemoryError):              raise @@ -755,6 +843,7 @@ class Bottle(object):              return [tob(err)]      def __call__(self, environ, start_response): +        ''' Each instance of :class:'Bottle' is a WSGI application. '''          return self.wsgi(environ, start_response) @@ -767,196 +856,136 @@ class Bottle(object):  ############################################################################### -class Request(threading.local, DictMixin): -    """ Represents a single HTTP request using thread-local attributes. -        The Request object wraps a WSGI environment and can be used as such. -    """ -    def __init__(self, environ=None): -        """ Create a new Request instance. - -            You usually don't do this but use the global `bottle.request` -            instance instead. -        """ -        self.bind(environ or {},) +class BaseRequest(DictMixin): +    """ A wrapper for WSGI environment dictionaries that adds a lot of +        convenient access methods and properties. Most of them are read-only.""" -    def bind(self, environ): -        """ Bind a new WSGI environment. +    #: Maximum size of memory buffer for :attr:`body` in bytes. +    MEMFILE_MAX = 102400 -            This is done automatically for the global `bottle.request` -            instance on every request. -        """ +    def __init__(self, environ): +        """ Wrap a WSGI environ dictionary. """ +        #: The wrapped WSGI environ dictionary. This is the only real attribute. +        #: All other attributes actually are read-only properties.          self.environ = environ -        # These attributes are used anyway, so it is ok to compute them here -        self.path = '/' + environ.get('PATH_INFO', '/').lstrip('/') -        self.method = environ.get('REQUEST_METHOD', 'GET').upper() - -    @property -    def _environ(self): -        depr("Request._environ renamed to Request.environ") -        return self.environ - -    def copy(self): -        ''' Returns a copy of self ''' -        return Request(self.environ.copy()) - -    def path_shift(self, shift=1): -        ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. - -           :param shift: The number of path fragments to shift. May be negative -                         to change the shift direction. (default: 1) -        ''' -        script_name = self.environ.get('SCRIPT_NAME','/') -        self['SCRIPT_NAME'], self.path = path_shift(script_name, self.path, shift) -        self['PATH_INFO'] = self.path - -    def __getitem__(self, key): return self.environ[key] -    def __delitem__(self, key): self[key] = ""; del(self.environ[key]) -    def __iter__(self): return iter(self.environ) -    def __len__(self): return len(self.environ) -    def keys(self): return self.environ.keys() -    def __setitem__(self, key, value): -        """ Shortcut for Request.environ.__setitem__ """ -        self.environ[key] = value -        todelete = [] -        if key in ('PATH_INFO','REQUEST_METHOD'): -            self.bind(self.environ) -        elif key == 'wsgi.input': todelete = ('body','forms','files','params') -        elif key == 'QUERY_STRING': todelete = ('get','params') -        elif key.startswith('HTTP_'): todelete = ('headers', 'cookies') -        for key in todelete: -            if 'bottle.' + key in self.environ: -                del self.environ['bottle.' + key] - -    @DictProperty('environ', 'bottle.urlparts', read_only=True) -    def urlparts(self): -        ''' Return a :class:`urlparse.SplitResult` tuple that can be used -            to reconstruct the full URL as requested by the client. -            The tuple contains: (scheme, host, path, query_string, fragment). -            The fragment is always empty because it is not visible to the server. -        ''' -        env = self.environ -        host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST', '') -        http = env.get('wsgi.url_scheme', 'http') -        port = env.get('SERVER_PORT') -        if ':' in host: # Overrule SERVER_POST (proxy support) -            host, port = host.rsplit(':', 1) -        if not host or host == '127.0.0.1': -            host = env.get('SERVER_NAME', host) -        if port and http+port not in ('http80', 'https443'): -            host += ':' + port -        spath = self.environ.get('SCRIPT_NAME','').rstrip('/') + '/' -        rpath = self.path.lstrip('/') -        path = urlquote(urljoin(spath, rpath)) -        return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') - -    @property -    def url(self): -        """ Full URL as requested by the client. """ -        return self.urlparts.geturl() - -    @property -    def fullpath(self): -        """ Request path including SCRIPT_NAME (if present). """ -        return urlunquote(self.urlparts[2]) - -    @property -    def query_string(self): -        """ The part of the URL following the '?'. """ -        return self.environ.get('QUERY_STRING', '') +        environ['bottle.request'] = self      @property -    def content_length(self): -        """ Content-Length header as an integer, -1 if not specified """ -        return int(self.environ.get('CONTENT_LENGTH', '') or -1) +    def path(self): +        ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix +            broken clients and avoid the "empty path" edge case). ''' +        return '/' + self.environ.get('PATH_INFO','').lstrip('/')      @property -    def header(self): -        depr("The Request.header property was renamed to Request.headers") -        return self.headers +    def method(self): +        ''' The ``REQUEST_METHOD`` value as an uppercase string. ''' +        return self.environ.get('REQUEST_METHOD', 'GET').upper() -    @DictProperty('environ', 'bottle.headers', read_only=True) +    @DictProperty('environ', 'bottle.request.headers', read_only=True)      def headers(self): -        ''' Request HTTP Headers stored in a :class:`HeaderDict`. ''' +        ''' A :class:`WSGIHeaderDict` that provides case-insensitive access to +            HTTP request headers. '''          return WSGIHeaderDict(self.environ) -    @DictProperty('environ', 'bottle.get', read_only=True) -    def GET(self): -        """ The QUERY_STRING parsed into an instance of :class:`MultiDict`. """ +    def get_header(self, name, default=None): +        ''' Return the value of a request header, or a given default value. ''' +        return self.headers.get(name, default) + +    @DictProperty('environ', 'bottle.request.cookies', read_only=True) +    def cookies(self): +        """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT +            decoded. Use :meth:`get_cookie` if you expect signed cookies. """ +        cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')) +        return FormsDict((c.key, c.value) for c in cookies.itervalues()) + +    def get_cookie(self, key, default=None, secret=None): +        """ Return the content of a cookie. To read a `Signed Cookie`, the +            `secret` must match the one used to create the cookie (see +            :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing +            cookie or wrong signature), return a default value. """ +        value = self.cookies.get(key) +        if secret and value: +            dec = cookie_decode(value, secret) # (key, value) tuple or None +            return dec[1] if dec and dec[0] == key else default +        return value or default + +    @DictProperty('environ', 'bottle.request.query', read_only=True) +    def query(self): +        ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These +            values are sometimes called "URL arguments" or "GET parameters", but +            not to be confused with "URL wildcards" as they are provided by the +            :class:`Router`. '''          data = parse_qs(self.query_string, keep_blank_values=True) -        get = self.environ['bottle.get'] = MultiDict() +        get = self.environ['bottle.get'] = FormsDict()          for key, values in data.iteritems():              for value in values:                  get[key] = value          return get -    @DictProperty('environ', 'bottle.post', read_only=True) -    def POST(self): -        """ The combined values from :attr:`forms` and :attr:`files`. Values are -            either strings (form values) or instances of -            :class:`cgi.FieldStorage` (file uploads). -        """ -        post = MultiDict() -        safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi -        for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): -            if key in self.environ: safe_env[key] = self.environ[key] -        if NCTextIOWrapper: -            fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') -        else: -            fb = self.body -        data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) -        for item in data.list or []: -            post[item.name] = item if item.filename else item.value -        return post - -    @DictProperty('environ', 'bottle.forms', read_only=True) +    @DictProperty('environ', 'bottle.request.forms', read_only=True)      def forms(self): -        """ POST form values parsed into an instance of :class:`MultiDict`. - -            This property contains form values parsed from an `url-encoded` -            or `multipart/form-data` encoded POST request bidy. The values are -            native strings. -        """ -        forms = MultiDict() +        """ Form values parsed from an `url-encoded` or `multipart/form-data` +            encoded POST or PUT request body. The result is retuned as a +            :class:`FormsDict`. All keys and values are strings. File uploads +            are stored separately in :attr:`files`. """ +        forms = FormsDict()          for name, item in self.POST.iterallitems():              if not hasattr(item, 'filename'):                  forms[name] = item          return forms -    @DictProperty('environ', 'bottle.files', read_only=True) -    def files(self): -        """ File uploads parsed into an instance of :class:`MultiDict`. +    @DictProperty('environ', 'bottle.request.params', read_only=True) +    def params(self): +        """ A :class:`FormsDict` with the combined values of :attr:`query` and +            :attr:`forms`. File uploads are stored in :attr:`files`. """ +        params = FormsDict() +        for key, value in self.query.iterallitems(): +            params[key] = value +        for key, value in self.forms.iterallitems(): +            params[key] = value +        return params -            This property contains file uploads parsed from an -            `multipart/form-data` encoded POST request body. The values are -            instances of :class:`cgi.FieldStorage`. +    @DictProperty('environ', 'bottle.request.files', read_only=True) +    def files(self): +        """ File uploads parsed from an `url-encoded` or `multipart/form-data` +            encoded POST or PUT request body. The values are instances of +            :class:`cgi.FieldStorage`. The most important attributes are: + +            filename +                The filename, if specified; otherwise None; this is the client +                side filename, *not* the file name on which it is stored (that's +                a temporary file you don't deal with) +            file +                The file(-like) object from which you can read the data. +            value +                The value as a *string*; for file uploads, this transparently +                reads the file every time you request the value. Do not do this +                on big files.          """ -        files = MultiDict() +        files = FormsDict()          for name, item in self.POST.iterallitems():              if hasattr(item, 'filename'):                  files[name] = item          return files -    @DictProperty('environ', 'bottle.params', read_only=True) -    def params(self): -        """ A combined :class:`MultiDict` with values from :attr:`forms` and -            :attr:`GET`. File-uploads are not included. """ -        params = MultiDict(self.GET) -        for key, value in self.forms.iterallitems(): -            params[key] = value -        return params +    @DictProperty('environ', 'bottle.request.json', read_only=True) +    def json(self): +        ''' If the ``Content-Type`` header is ``application/json``, this +            property holds the parsed content of the request body. Only requests +            smaller than :attr:`MEMFILE_MAX` are processed to avoid memory +            exhaustion. ''' +        if 'application/json' in self.environ.get('CONTENT_TYPE', '') \ +        and 0 < self.content_length < self.MEMFILE_MAX: +            return json_loads(self.body.read(self.MEMFILE_MAX)) +        return None -    @DictProperty('environ', 'bottle.body', read_only=True) +    @DictProperty('environ', 'bottle.request.body', read_only=True)      def _body(self): -        """ The HTTP request body as a seekable file-like object. - -            This property returns a copy of the `wsgi.input` stream and should -            be used instead of `environ['wsgi.input']`. -         """          maxread = max(0, self.content_length)          stream = self.environ['wsgi.input'] -        body = BytesIO() if maxread < MEMFILE_MAX else TemporaryFile(mode='w+b') +        body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b')          while maxread > 0: -            part = stream.read(min(maxread, MEMFILE_MAX)) +            part = stream.read(min(maxread, self.MEMFILE_MAX))              if not part: break              body.write(part)              maxread -= len(part) @@ -966,124 +995,378 @@ class Request(threading.local, DictMixin):      @property      def body(self): +        """ The HTTP request body as a seek-able file-like object. Depending on +            :attr:`MEMFILE_MAX`, this is either a temporary file or a +            :class:`io.BytesIO` instance. Accessing this property for the first +            time reads and replaces the ``wsgi.input`` environ variable. +            Subsequent accesses just do a `seek(0)` on the file object. """          self._body.seek(0)          return self._body -    @property -    def auth(self): #TODO: Tests and docs. Add support for digest. namedtuple? -        """ HTTP authorization data as a (user, passwd) tuple. (experimental) +    #: An alias for :attr:`query`. +    GET = query -            This implementation currently only supports basic auth and returns -            None on errors. +    @DictProperty('environ', 'bottle.request.post', read_only=True) +    def POST(self): +        """ The values of :attr:`forms` and :attr:`files` combined into a single +            :class:`FormsDict`. Values are either strings (form values) or +            instances of :class:`cgi.FieldStorage` (file uploads).          """ -        return parse_auth(self.headers.get('Authorization','')) +        post = FormsDict() +        safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi +        for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): +            if key in self.environ: safe_env[key] = self.environ[key] +        if NCTextIOWrapper: +            fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') +        else: +            fb = self.body +        data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) +        for item in data.list or []: +            post[item.name] = item if item.filename else item.value +        return post -    @DictProperty('environ', 'bottle.cookies', read_only=True) +    @property      def COOKIES(self): -        """ Cookies parsed into a dictionary. Signed cookies are NOT decoded -            automatically. See :meth:`get_cookie` for details. -        """ -        raw_dict = SimpleCookie(self.headers.get('Cookie','')) -        cookies = {} -        for cookie in raw_dict.itervalues(): -            cookies[cookie.key] = cookie.value -        return cookies - -    def get_cookie(self, key, secret=None): -        """ Return the content of a cookie. To read a `Signed Cookies`, use the -            same `secret` as used to create the cookie (see -            :meth:`Response.set_cookie`). If anything goes wrong, None is -            returned. -        """ -        value = self.COOKIES.get(key) -        if secret and value: -            dec = cookie_decode(value, secret) # (key, value) tuple or None -            return dec[1] if dec and dec[0] == key else None -        return value or None +        ''' Alias for :attr:`cookies` (deprecated). ''' +        depr('BaseRequest.COOKIES was renamed to BaseRequest.cookies (lowercase).') +        return self.cookies      @property -    def is_ajax(self): -        ''' True if the request was generated using XMLHttpRequest ''' -        #TODO: write tests -        return self.header.get('X-Requested-With') == 'XMLHttpRequest' +    def url(self): +        """ The full request URI including hostname and scheme. If your app +            lives behind a reverse proxy or load balancer and you get confusing +            results, make sure that the ``X-Forwarded-Host`` header is set +            correctly. """ +        return self.urlparts.geturl() +    @DictProperty('environ', 'bottle.request.urlparts', read_only=True) +    def urlparts(self): +        ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. +            The tuple contains (scheme, host, path, query_string and fragment), +            but the fragment is always empty because it is not visible to the +            server. ''' +        env = self.environ +        http = env.get('wsgi.url_scheme', 'http') +        host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') +        if not host: +            # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. +            host = env.get('SERVER_NAME', '127.0.0.1') +            port = env.get('SERVER_PORT') +            if port and port != ('80' if http == 'http' else '443'): +                host += ':' + port +        path = urlquote(self.fullpath) +        return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') -class Response(threading.local): -    """ Represents a single HTTP response using thread-local attributes. -    """ +    @property +    def fullpath(self): +        """ Request path including :attr:`script_name` (if present). """ +        return urljoin(self.script_name, self.path.lstrip('/')) -    def __init__(self): -        self.bind() +    @property +    def query_string(self): +        """ The raw :attr:`query` part of the URL (everything in between ``?`` +            and ``#``) as a string. """ +        return self.environ.get('QUERY_STRING', '') -    def bind(self): -        """ Resets the Response object to its factory defaults. """ -        self._COOKIES = None -        self.status = 200 -        self.headers = HeaderDict() -        self.content_type = 'text/html; charset=UTF-8' +    @property +    def script_name(self): +        ''' The initial portion of the URL's `path` that was removed by a higher +            level (server or routing middleware) before the application was +            called. This script path is returned with leading and tailing +            slashes. ''' +        script_name = self.environ.get('SCRIPT_NAME', '').strip('/') +        return '/' + script_name + '/' if script_name else '/' + +    def path_shift(self, shift=1): +        ''' Shift path segments from :attr:`path` to :attr:`script_name` and +            vice versa. + +           :param shift: The number of path segments to shift. May be negative +                         to change the shift direction. (default: 1) +        ''' +        script = self.environ.get('SCRIPT_NAME','/') +        self['SCRIPT_NAME'], self['PATH_INFO'] = path_shift(script, self.path, shift)      @property -    def header(self): -        depr("Response.header renamed to Response.headers") -        return self.headers +    def content_length(self): +        ''' The request body length as an integer. The client is responsible to +            set this header. Otherwise, the real length of the body is unknown +            and -1 is returned. In this case, :attr:`body` will be empty. ''' +        return int(self.environ.get('CONTENT_LENGTH') or -1) + +    @property +    def is_xhr(self): +        ''' True if the request was triggered by a XMLHttpRequest. This only +            works with JavaScript libraries that support the `X-Requested-With` +            header (most of the popular libraries do). ''' +        requested_with = self.environ.get('HTTP_X_REQUESTED_WITH','') +        return requested_with.lower() == 'xmlhttprequest' + +    @property +    def is_ajax(self): +        ''' Alias for :attr:`is_xhr`. "Ajax" is not the right term. ''' +        return self.is_xhr + +    @property +    def auth(self): +        """ HTTP authentication data as a (user, password) tuple. This +            implementation currently supports basic (not digest) authentication +            only. If the authentication happened at a higher level (e.g. in the +            front web-server or a middleware), the password field is None, but +            the user field is looked up from the ``REMOTE_USER`` environ +            variable. On any errors, None is returned. """ +        basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION','')) +        if basic: return basic +        ruser = self.environ.get('REMOTE_USER') +        if ruser: return (ruser, None) +        return None + +    @property +    def remote_route(self): +        """ A list of all IPs that were involved in this request, starting with +            the client IP and followed by zero or more proxies. This does only +            work if all proxies support the ```X-Forwarded-For`` header. Note +            that this information can be forged by malicious clients. """ +        proxy = self.environ.get('HTTP_X_FORWARDED_FOR') +        if proxy: return [ip.strip() for ip in proxy.split(',')] +        remote = self.environ.get('REMOTE_ADDR') +        return [remote] if remote else [] + +    @property +    def remote_addr(self): +        """ The client IP as a string. Note that this information can be forged +            by malicious clients. """ +        route = self.remote_route +        return route[0] if route else None + +    def copy(self): +        """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ +        return Request(self.environ.copy()) + +    def __getitem__(self, key): return self.environ[key] +    def __delitem__(self, key): self[key] = ""; del(self.environ[key]) +    def __iter__(self): return iter(self.environ) +    def __len__(self): return len(self.environ) +    def keys(self): return self.environ.keys() +    def __setitem__(self, key, value): +        """ Change an environ value and clear all caches that depend on it. """ + +        if self.environ.get('bottle.request.readonly'): +            raise KeyError('The environ dictionary is read-only.') + +        self.environ[key] = value +        todelete = () + +        if key == 'wsgi.input': +            todelete = ('body', 'forms', 'files', 'params', 'post', 'json') +        elif key == 'QUERY_STRING': +            todelete = ('query', 'params') +        elif key.startswith('HTTP_'): +            todelete = ('headers', 'cookies') + +        for key in todelete: +            self.environ.pop('bottle.request.'+key, None) + +    def __repr__(self): +        return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + +def _hkey(s): +    return s.title().replace('_','-') + + +class HeaderProperty(object): +    def __init__(self, name, reader=None, writer=str, default=''): +        self.name, self.reader, self.writer, self.default = name, reader, writer, default +        self.__doc__ = 'Current value of the %r header.' % name.title() + +    def __get__(self, obj, cls): +        if obj is None: return self +        value = obj.headers.get(self.name) +        return self.reader(value) if (value and self.reader) else (value or self.default) + +    def __set__(self, obj, value): +        if self.writer: value = self.writer(value) +        obj.headers[self.name] = value + +    def __delete__(self, obj): +        if self.name in obj.headers: +            del obj.headers[self.name] + + +class BaseResponse(object): +    """ Storage class for a response body as well as headers and cookies. + +        This class does support dict-like case-insensitive item-access to +        headers, but is NOT a dict. Most notably, iterating over a response +        yields parts of the body and not the headers. +    """ + +    default_status = 200 +    default_content_type = 'text/html; charset=UTF-8' + +    # Header blacklist for specific response codes +    # (rfc2616 section 10.2.3 and 10.3.5) +    bad_headers = { +        204: set(('Content-Type',)), +        304: set(('Allow', 'Content-Encoding', 'Content-Language', +                  'Content-Length', 'Content-Range', 'Content-Type', +                  'Content-Md5', 'Last-Modified'))} + +    def __init__(self, body='', status=None, **headers): +        self._status_line = None +        self._status_code = None +        self.body = body +        self._cookies = None +        self._headers = {'Content-Type': [self.default_content_type]} +        self.status = status or self.default_status +        if headers: +            for name, value in headers.items(): +                self[name] = value      def copy(self):          ''' Returns a copy of self. '''          copy = Response()          copy.status = self.status -        copy.headers = self.headers.copy() -        copy.content_type = self.content_type +        copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())          return copy +    def __iter__(self): +        return iter(self.body) + +    def close(self): +        if hasattr(self.body, 'close'): +            self.body.close() + +    @property +    def status_line(self): +        ''' The HTTP status line as a string (e.g. ``404 Not Found``).''' +        return self._status_line + +    @property +    def status_code(self): +        ''' The HTTP status code as an integer (e.g. 404).''' +        return self._status_code + +    def _set_status(self, status): +        if isinstance(status, int): +            code, status = status, _HTTP_STATUS_LINES.get(status) +        elif ' ' in status: +            status = status.strip() +            code   = int(status.split()[0]) +        else: +            raise ValueError('String status line without a reason phrase.') +        if not 100 <= code <= 999: raise ValueError('Status code out of range.') +        self._status_code = code +        self._status_line = status or ('%d Unknown' % code) + +    def _get_status(self): +        depr('BaseReuqest.status will change to return a string in 0.11. Use'\ +             ' status_line and status_code to make sure.') #0.10 +        return self._status_code + +    status = property(_get_status, _set_status, None, +        ''' A writeable property to change the HTTP response status. It accepts +            either a numeric code (100-999) or a string with a custom reason +            phrase (e.g. "404 Brain not found"). Both :data:`status_line` and +            :data:`status_code` are updates accordingly. The return value is +            always a numeric code. ''') +    del _get_status, _set_status + +    @property +    def headers(self): +        ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like +            view on the response headers. ''' +        self.__dict__['headers'] = hdict = HeaderDict() +        hdict.dict = self._headers +        return hdict + +    def __contains__(self, name): return _hkey(name) in self._headers +    def __delitem__(self, name):  del self._headers[_hkey(name)] +    def __getitem__(self, name):  return self._headers[_hkey(name)][-1] +    def __setitem__(self, name, value): self._headers[_hkey(name)] = [str(value)] + +    def get_header(self, name, default=None): +        ''' Return the value of a previously defined header. If there is no +            header with that name, return a default value. ''' +        return self._headers.get(_hkey(name), [default])[-1] + +    def set_header(self, name, value, append=False): +        ''' Create a new response header, replacing any previously defined +            headers with the same name. ''' +        if append: +            self.add_header(name, value) +        else: +            self._headers[_hkey(name)] = [str(value)] + +    def add_header(self, name, value): +        ''' Add an additional response header, not removing duplicates. ''' +        self._headers.setdefault(_hkey(name), []).append(str(value)) + +    def iter_headers(self): +        ''' Yield (header, value) tuples, skipping headers that are not +            allowed with the current response status code. ''' +        headers = self._headers.iteritems() +        bad_headers = self.bad_headers.get(self.status_code) +        if bad_headers: +            headers = [h for h in headers if h[0] not in bad_headers] +        for name, values in headers: +            for value in values: +                yield name, value +        if self._cookies: +            for c in self._cookies.values(): +                yield 'Set-Cookie', c.OutputString() +      def wsgiheader(self): -        ''' Returns a wsgi conform list of header/value pairs. ''' -        for c in self.COOKIES.values(): -            if c.OutputString() not in self.headers.getall('Set-Cookie'): -                self.headers.append('Set-Cookie', c.OutputString()) -        # rfc2616 section 10.2.3, 10.3.5 -        if self.status in (204, 304) and 'content-type' in self.headers: -            del self.headers['content-type'] -        if self.status == 304: -            for h in ('allow', 'content-encoding', 'content-language', -                      'content-length', 'content-md5', 'content-range', -                      'content-type', 'last-modified'): # + c-location, expires? -                if h in self.headers: -                     del self.headers[h] -        return list(self.headers.iterallitems()) -    headerlist = property(wsgiheader) +        depr('The wsgiheader method is deprecated. See headerlist.') #0.10 +        return self.headerlist      @property -    def charset(self): -        """ Return the charset specified in the content-type header. +    def headerlist(self): +        ''' WSGI conform list of (header, value) tuples. ''' +        return list(self.iter_headers()) -            This defaults to `UTF-8`. -        """ +    content_type = HeaderProperty('Content-Type') +    content_length = HeaderProperty('Content-Length', reader=int) + +    @property +    def charset(self): +        """ Return the charset specified in the content-type header (default: utf8). """          if 'charset=' in self.content_type:              return self.content_type.split('charset=')[-1].split(';')[0].strip()          return 'UTF-8'      @property      def COOKIES(self): -        """ A dict-like SimpleCookie instance. Use :meth:`set_cookie` instead. """ -        if not self._COOKIES: -            self._COOKIES = SimpleCookie() -        return self._COOKIES - -    def set_cookie(self, key, value, secret=None, **kargs): -        ''' Add a cookie or overwrite an old one. If the `secret` parameter is +        """ A dict-like SimpleCookie instance. This should not be used directly. +            See :meth:`set_cookie`. """ +        depr('The COOKIES dict is deprecated. Use `set_cookie()` instead.') # 0.10 +        if not self._cookies: +            self._cookies = SimpleCookie() +        return self._cookies + +    def set_cookie(self, name, value, secret=None, **options): +        ''' Create a new cookie or replace an old one. If the `secret` parameter is              set, create a `Signed Cookie` (described below). -            :param key: the name of the cookie. +            :param name: the name of the cookie.              :param value: the value of the cookie. -            :param secret: required for signed cookies. (default: None) +            :param secret: a signature key required for signed cookies. + +            Additionally, this method accepts all RFC 2109 attributes that are +            supported by :class:`cookie.Morsel`, including: +              :param max_age: maximum age in seconds. (default: None) -            :param expires: a datetime object or UNIX timestamp. (defaut: None) +            :param expires: a datetime object or UNIX timestamp. (default: None)              :param domain: the domain that is allowed to read the cookie.                (default: current domain) -            :param path: limits the cookie to a given path (default: /) +            :param path: limits the cookie to a given path (default: current path) +            :param secure: limit the cookie to HTTPS connections (default: off). +            :param httponly: prevents client-side javascript to read this cookie +              (default: off, requires Python 2.6 or newer). -            If neither `expires` nor `max_age` are set (default), the cookie -            lasts only as long as the browser is not closed. +            If neither `expires` nor `max_age` is set (default), the cookie will +            expire at the end of the browser session (as soon as the browser +            window is closed).              Signed cookies may store any pickle-able object and are              cryptographically signed to prevent manipulation. Keep in mind that @@ -1094,31 +1377,55 @@ class Response(threading.local):              cookie). The main intention is to make pickling and unpickling              save, not to store secret information at client side.          ''' +        if not self._cookies: +            self._cookies = SimpleCookie() +          if secret: -            value = touni(cookie_encode((key, value), secret)) +            value = touni(cookie_encode((name, value), secret))          elif not isinstance(value, basestring): -            raise TypeError('Secret missing for non-string Cookie.') - -        self.COOKIES[key] = value -        for k, v in kargs.iteritems(): -            self.COOKIES[key][k.replace('_', '-')] = v +            raise TypeError('Secret key missing for non-string Cookie.') + +        if len(value) > 4096: raise ValueError('Cookie value to long.') +        self._cookies[name] = value + +        for key, value in options.iteritems(): +            if key == 'max_age': +                if isinstance(value, timedelta): +                    value = value.seconds + value.days * 24 * 3600 +            if key == 'expires': +                if isinstance(value, (datedate, datetime)): +                    value = value.timetuple() +                elif isinstance(value, (int, float)): +                    value = time.gmtime(value) +                value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) +            self._cookies[name][key.replace('_', '-')] = value      def delete_cookie(self, key, **kwargs):          ''' Delete a cookie. Be sure to use the same `domain` and `path` -            parameters as used to create the cookie. ''' +            settings as used to create the cookie. '''          kwargs['max_age'] = -1          kwargs['expires'] = 0          self.set_cookie(key, '', **kwargs) -    def get_content_type(self): -        """ Current 'Content-Type' header. """ -        return self.headers['Content-Type'] +    def __repr__(self): +        out = '' +        for name, value in self.headerlist: +            out += '%s: %s\n' % (name.title(), value.strip()) +        return out + -    def set_content_type(self, value): -        self.headers['Content-Type'] = value +class LocalRequest(BaseRequest, threading.local): +    ''' A thread-local subclass of :class:`BaseRequest`. ''' +    def __init__(self): pass +    bind = BaseRequest.__init__ -    content_type = property(get_content_type, set_content_type, None, -                            get_content_type.__doc__) + +class LocalResponse(BaseResponse, threading.local): +    ''' A thread-local subclass of :class:`BaseResponse`. ''' +    bind = BaseResponse.__init__ + +Response = LocalResponse # BC 0.9 +Request  = LocalRequest  # BC 0.9 @@ -1129,10 +1436,11 @@ class Response(threading.local):  # Plugins ######################################################################  ############################################################################### - +class PluginError(BottleException): pass  class JSONPlugin(object):      name = 'json' +    api  = 2      def __init__(self, json_dumps=json_dumps):          self.json_dumps = json_dumps @@ -1143,18 +1451,23 @@ class JSONPlugin(object):          def wrapper(*a, **ka):              rv = callback(*a, **ka)              if isinstance(rv, dict): +                #Attempt to serialize, raises exception on failure +                json_response = dumps(rv) +                #Set content type only if serialization succesful                  response.content_type = 'application/json' -                return dumps(rv) +                return json_response              return rv          return wrapper -  class HooksPlugin(object):      name = 'hooks' +    api  = 2 + +    _names = 'before_request', 'after_request', 'app_reset'      def __init__(self): -        self.hooks = {'before_request': [], 'after_request': []} +        self.hooks = dict((name, []) for name in self._names)          self.app = None      def _empty(self): @@ -1165,56 +1478,29 @@ class HooksPlugin(object):      def add(self, name, func):          ''' Attach a callback to a hook. ''' -        if name not in self.hooks: -            raise ValueError("Unknown hook name %s" % name)          was_empty = self._empty() -        self.hooks[name].append(func) +        self.hooks.setdefault(name, []).append(func)          if self.app and was_empty and not self._empty(): self.app.reset()      def remove(self, name, func):          ''' Remove a callback from a hook. ''' -        if name not in self.hooks: -            raise ValueError("Unknown hook name %s" % name)          was_empty = self._empty() -        self.hooks[name].remove(func) +        if name in self.hooks and func in self.hooks[name]: +            self.hooks[name].remove(func)          if self.app and not was_empty and self._empty(): self.app.reset() -    def apply(self, callback, context): -        if self._empty(): return callback -        before_request = self.hooks['before_request'] -        after_request  = self.hooks['after_request'] -        def wrapper(*a, **ka): -            for hook in before_request: hook() -            rv = callback(*a, **ka) -            for hook in after_request[::-1]: hook() -            return rv -        return wrapper - - - -class TypeFilterPlugin(object): -    def __init__(self): -        self.filter = [] -        self.app = None - -    def setup(self, app): -        self.app = app - -    def add(self, ftype, func): -        if not isinstance(ftype, type): -            raise TypeError("Expected type object, got %s" % type(ftype)) -        self.filter = [(t, f) for (t, f) in self.filter if t != ftype] -        self.filter.append((ftype, func)) -        if len(self.filter) == 1 and self.app: self.app.reset() +    def trigger(self, name, *a, **ka): +        ''' Trigger a hook and return a list of results. ''' +        hooks = self.hooks[name] +        if ka.pop('reversed', False): hooks = hooks[::-1] +        return [hook(*a, **ka) for hook in hooks]      def apply(self, callback, context): -        filter = self.filter -        if not filter: return callback +        if self._empty(): return callback          def wrapper(*a, **ka): +            self.trigger('before_request')              rv = callback(*a, **ka) -            for testtype, filterfunc in filter: -                if isinstance(rv, testtype): -                    rv = filterfunc(rv) +            self.trigger('after_request', reversed=True)              return rv          return wrapper @@ -1225,14 +1511,15 @@ class TemplatePlugin(object):          element must be a dict with additional options (e.g. `template_engine`)          or default variables for the template. '''      name = 'template' +    api  = 2 -    def apply(self, callback, context): -        conf = context['config'].get('template') +    def apply(self, callback, route): +        conf = route.config.get('template')          if isinstance(conf, (tuple, list)) and len(conf) == 2:              return view(conf[0], **conf[1])(callback) -        elif isinstance(conf, str) and 'template_opts' in context['config']: +        elif isinstance(conf, str) and 'template_opts' in route.config:              depr('The `template_opts` parameter is deprecated.') #0.9 -            return view(conf, **context['config']['template_opts'])(callback) +            return view(conf, **route.config['template_opts'])(callback)          elif isinstance(conf, str):              return view(conf)(callback)          else: @@ -1246,7 +1533,7 @@ class _ImportRedirect(object):          self.name = name          self.impmask = impmask          self.module = sys.modules.setdefault(name, imp.new_module(name)) -        self.module.__dict__.update({'__file__': '<virtual>', '__path__': [], +        self.module.__dict__.update({'__file__': __file__, '__path__': [],                                      '__all__': [], '__loader__': self})          sys.meta_path.append(self) @@ -1277,53 +1564,115 @@ class _ImportRedirect(object):  class MultiDict(DictMixin): -    """ A dict that remembers old values for each key """ -    # collections.MutableMapping would be better for Python >= 2.6 -    def __init__(self, *a, **k): -        self.dict = dict() -        for k, v in dict(*a, **k).iteritems(): -            self[k] = v +    """ This dict stores multiple values per key, but behaves exactly like a +        normal dict in that it returns only the newest value for any given key. +        There are special methods available to access the full list of values. +    """ +    def __init__(self, *a, **k): +        self.dict = dict((k, [v]) for k, v in dict(*a, **k).iteritems())      def __len__(self): return len(self.dict)      def __iter__(self): return iter(self.dict)      def __contains__(self, key): return key in self.dict      def __delitem__(self, key): del self.dict[key] -    def keys(self): return self.dict.keys() -    def __getitem__(self, key): return self.get(key, KeyError, -1) +    def __getitem__(self, key): return self.dict[key][-1]      def __setitem__(self, key, value): self.append(key, value) - -    def append(self, key, value): self.dict.setdefault(key, []).append(value) -    def replace(self, key, value): self.dict[key] = [value] -    def getall(self, key): return self.dict.get(key) or [] - -    def get(self, key, default=None, index=-1): -        if key not in self.dict and default != KeyError: -            return [default][index] -        return self.dict[key][index] - +    def iterkeys(self): return self.dict.iterkeys() +    def itervalues(self): return (v[-1] for v in self.dict.itervalues()) +    def iteritems(self): return ((k, v[-1]) for (k, v) in self.dict.iteritems())      def iterallitems(self):          for key, values in self.dict.iteritems():              for value in values:                  yield key, value +    # 2to3 is not able to fix these automatically. +    keys     = iterkeys     if py3k else lambda self: list(self.iterkeys()) +    values   = itervalues   if py3k else lambda self: list(self.itervalues()) +    items    = iteritems    if py3k else lambda self: list(self.iteritems()) +    allitems = iterallitems if py3k else lambda self: list(self.iterallitems()) + +    def get(self, key, default=None, index=-1, type=None): +        ''' Return the most recent value for a key. + +            :param default: The default value to be returned if the key is not +                   present or the type conversion fails. +            :param index: An index for the list of available values. +            :param type: If defined, this callable is used to cast the value +                    into a specific type. Exception are suppressed and result in +                    the default value to be returned. +        ''' +        try: +            val = self.dict[key][index] +            return type(val) if type else val +        except Exception, e: +            pass +        return default -class HeaderDict(MultiDict): -    """ Same as :class:`MultiDict`, but title()s the keys and overwrites. """ -    def __contains__(self, key): -        return MultiDict.__contains__(self, self.httpkey(key)) -    def __getitem__(self, key): -        return MultiDict.__getitem__(self, self.httpkey(key)) -    def __delitem__(self, key): -        return MultiDict.__delitem__(self, self.httpkey(key)) -    def __setitem__(self, key, value): self.replace(key, value) -    def get(self, key, default=None, index=-1): -        return MultiDict.get(self, self.httpkey(key), default, index)      def append(self, key, value): -        return MultiDict.append(self, self.httpkey(key), str(value)) +        ''' Add a new value to the list of values for this key. ''' +        self.dict.setdefault(key, []).append(value) +      def replace(self, key, value): -        return MultiDict.replace(self, self.httpkey(key), str(value)) -    def getall(self, key): return MultiDict.getall(self, self.httpkey(key)) -    def httpkey(self, key): return str(key).replace('_','-').title() +        ''' Replace the list of values with a single value. ''' +        self.dict[key] = [value] + +    def getall(self, key): +        ''' Return a (possibly empty) list of values for a key. ''' +        return self.dict.get(key) or [] + +    #: Aliases for WTForms to mimic other multi-dict APIs (Django) +    getone = get +    getlist = getall + + + +class FormsDict(MultiDict): +    ''' This :class:`MultiDict` subclass is used to store request form data. +        Additionally to the normal dict-like item access methods (which return +        unmodified data as native strings), this container also supports +        attribute-like access to its values. Attribues are automatiically de- or +        recoded to match :attr:`input_encoding` (default: 'utf8'). Missing +        attributes default to an empty string. ''' + +    #: Encoding used for attribute values. +    input_encoding = 'utf8' + +    def getunicode(self, name, default=None, encoding=None): +        value, enc = self.get(name, default), encoding or self.input_encoding +        try: +            if isinstance(value, bytes): # Python 2 WSGI +                return value.decode(enc) +            elif isinstance(value, unicode): # Python 3 WSGI +                return value.encode('latin1').decode(enc) +            return value +        except UnicodeError, e: +            return default + +    def __getattr__(self, name): return self.getunicode(name, default=u'') + + +class HeaderDict(MultiDict): +    """ A case-insensitive version of :class:`MultiDict` that defaults to +        replace the old value instead of appending it. """ + +    def __init__(self, *a, **ka): +        self.dict = {} +        if a or ka: self.update(*a, **ka) + +    def __contains__(self, key): return _hkey(key) in self.dict +    def __delitem__(self, key): del self.dict[_hkey(key)] +    def __getitem__(self, key): return self.dict[_hkey(key)][-1] +    def __setitem__(self, key, value): self.dict[_hkey(key)] = [str(value)] +    def append(self, key, value): +        self.dict.setdefault(_hkey(key), []).append(str(value)) +    def replace(self, key, value): self.dict[_hkey(key)] = [str(value)] +    def getall(self, key): return self.dict.get(_hkey(key)) or [] +    def get(self, key, default=None, index=-1): +        return MultiDict.get(self, _hkey(key), default, index) +    def filter(self, names): +        for name in map(_hkey, names): +            if name in self.dict: +                del self.dict[name]  class WSGIHeaderDict(DictMixin): @@ -1370,11 +1719,44 @@ class WSGIHeaderDict(DictMixin):              elif key in self.cgikeys:                  yield key.replace('_', '-').title() -    def keys(self): return list(self) -    def __len__(self): return len(list(self)) +    def keys(self): return [x for x in self] +    def __len__(self): return len(self.keys())      def __contains__(self, key): return self._ekey(key) in self.environ +class ConfigDict(dict): +    ''' A dict-subclass with some extras: You can access keys like attributes. +        Uppercase attributes create new ConfigDicts and act as name-spaces. +        Other missing attributes return None. Calling a ConfigDict updates its +        values and returns itself. + +        >>> cfg = ConfigDict() +        >>> cfg.Namespace.value = 5 +        >>> cfg.OtherNamespace(a=1, b=2) +        >>> cfg +        {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}} +    ''' + +    def __getattr__(self, key): +        if key not in self and key[0].isupper(): +            self[key] = ConfigDict() +        return self.get(key) + +    def __setattr__(self, key, value): +        if hasattr(dict, key): +            raise AttributeError('Read-only attribute.') +        if key in self and self[key] and isinstance(self[key], ConfigDict): +            raise AttributeError('Non-empty namespace attribute.') +        self[key] = value + +    def __delattr__(self, key): +        if key in self: del self[key] + +    def __call__(self, *a, **ka): +        for key, value in dict(*a, **ka).iteritems(): setattr(self, key, value) +        return self + +  class AppStack(list):      """ A stack-like list. Calling it returns the head of the stack. """ @@ -1414,30 +1796,21 @@ class WSGIFileWrapper(object):  ############################################################################### -def dict2json(d): -    depr('JSONPlugin is the preferred way to return JSON.') #0.9 -    response.content_type = 'application/json' -    return json_dumps(d) - -  def abort(code=500, text='Unknown Error: Application stopped.'):      """ Aborts execution and causes a HTTP error. """      raise HTTPError(code, text) -def redirect(url, code=303): -    """ Aborts execution and causes a 303 redirect. """ +def redirect(url, code=None): +    """ Aborts execution and causes a 303 or 302 redirect, depending on +        the HTTP protocol version. """ +    if code is None: +        code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302      location = urljoin(request.url, url)      raise HTTPResponse("", status=code, header=dict(Location=location)) -def send_file(*a, **k): #BC 0.6.4 -    """ Raises the output of static_file(). (deprecated) """ -    depr("Use 'raise static_file()' instead of 'send_file()'.") -    raise static_file(*a, **k) - - -def static_file(filename, root, mimetype='auto', guessmime=True, download=False): +def static_file(filename, root, mimetype='auto', download=False):      """ Open a file in a safe way and return :exc:`HTTPResponse` with status          code 200, 305, 401 or 404. Set Content-Type, Content-Encoding,          Content-Length and Last-Modified header. Obey If-Modified-Since header @@ -1454,9 +1827,6 @@ def static_file(filename, root, mimetype='auto', guessmime=True, download=False)      if not os.access(filename, os.R_OK):          return HTTPError(403, "You do not have permission to access this file.") -    if not guessmime: #0.9 -       if mimetype == 'auto': mimetype = 'text/plain' -       depr("To disable mime-type guessing, specify a type explicitly.")      if mimetype == 'auto':          mimetype, encoding = mimetypes.guess_type(filename)          if mimetype: header['Content-Type'] = mimetype @@ -1514,9 +1884,10 @@ def parse_auth(header):      try:          method, data = header.split(None, 1)          if method.lower() == 'basic': -            name, pwd = base64.b64decode(data).split(':', 1) -            return name, pwd -    except (KeyError, ValueError, TypeError): +            #TODO: Add 2to3 save base64[encode/decode] functions. +            user, pwd = touni(base64.b64decode(tob(data))).split(':',1) +            return user, pwd +    except (KeyError, ValueError):          return None @@ -1529,7 +1900,7 @@ def _lscmp(a, b):  def cookie_encode(data, key):      ''' Encode and sign a pickle-able object. Return a (byte) string '''      msg = base64.b64encode(pickle.dumps(data, -1)) -    sig = base64.b64encode(hmac.new(key, msg).digest()) +    sig = base64.b64encode(hmac.new(tob(key), msg).digest())      return tob('!') + sig + tob('?') + msg @@ -1538,7 +1909,7 @@ def cookie_decode(data, key):      data = tob(data)      if cookie_is_encoded(data):          sig, msg = data.split(tob('?'), 1) -        if _lscmp(sig[1:], base64.b64encode(hmac.new(key, msg).digest())): +        if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())):              return pickle.loads(base64.b64decode(msg))      return None @@ -1548,6 +1919,18 @@ def cookie_is_encoded(data):      return bool(data.startswith(tob('!')) and tob('?') in data) +def html_escape(string): +    ''' Escape HTML special characters ``&<>`` and quotes ``'"``. ''' +    return string.replace('&','&').replace('<','<').replace('>','>')\ +                 .replace('"','"').replace("'",''') + + +def html_quote(string): +    ''' Escape and quote a string to be used as an HTTP attribute.''' +    return '"%s"' % html_escape(string).replace('\n','%#10;')\ +                    .replace('\r','
').replace('\t','	') + +  def yieldroutes(func):      """ Return a generator for routes that match the signature (name, args)      of the func parameter. This may yield more than one route if the function @@ -1600,17 +1983,15 @@ def path_shift(script_name, path_info, shift=1):      return new_script_name, new_path_info - -# Decorators -#TODO: Replace default_app() with app() -  def validate(**vkargs):      """      Validates and manipulates keyword arguments by user defined callables.      Handles ValueError and missing arguments by raising HTTPError(403).      """ +    depr('Use route wildcard filters instead.')      def decorator(func): -        def wrapper(**kargs): +        @functools.wraps(func) +        def wrapper(*args, **kargs):              for key, value in vkargs.iteritems():                  if key not in kargs:                      abort(403, 'Missing parameter: %s' % key) @@ -1618,7 +1999,7 @@ def validate(**vkargs):                      kargs[key] = value(kargs[key])                  except ValueError:                      abort(403, 'Wrong parameter format for: %s' % key) -            return func(**kargs) +            return func(*args, **kargs)          return wrapper      return decorator @@ -1652,11 +2033,6 @@ url = make_default_app_wrapper('get_url')  del name -def default(): -    depr("The default() decorator is deprecated. Use @error(404) instead.") -    return error(404) - - @@ -1685,15 +2061,17 @@ class CGIServer(ServerAdapter):      quiet = True      def run(self, handler): # pragma: no cover          from wsgiref.handlers import CGIHandler -        CGIHandler().run(handler) # Just ignore host and port here +        def fixed_environ(environ, start_response): +            environ.setdefault('PATH_INFO', '') +            return handler(environ, start_response) +        CGIHandler().run(fixed_environ)  class FlupFCGIServer(ServerAdapter):      def run(self, handler): # pragma: no cover          import flup.server.fcgi -        kwargs = {'bindAddress':(self.host, self.port)} -        kwargs.update(self.options) # allow to override bindAddress and others -        flup.server.fcgi.WSGIServer(handler, **kwargs).run() +        self.options.setdefault('bindAddress', (self.host, self.port)) +        flup.server.fcgi.WSGIServer(handler, **self.options).run()  class WSGIRefServer(ServerAdapter): @@ -1711,7 +2089,10 @@ class CherryPyServer(ServerAdapter):      def run(self, handler): # pragma: no cover          from cherrypy import wsgiserver          server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) -        server.start() +        try: +            server.start() +        finally: +            server.stop()  class PasteServer(ServerAdapter): @@ -1723,6 +2104,7 @@ class PasteServer(ServerAdapter):          httpserver.serve(handler, host=self.host, port=str(self.port),                           **self.options) +  class MeinheldServer(ServerAdapter):      def run(self, handler):          from meinheld import server @@ -1755,9 +2137,7 @@ class FapwsServer(ServerAdapter):  class TornadoServer(ServerAdapter):      """ The super hyped asynchronous server by facebook. Untested. """      def run(self, handler): # pragma: no cover -        import tornado.wsgi -        import tornado.httpserver -        import tornado.ioloop +        import tornado.wsgi, tornado.httpserver, tornado.ioloop          container = tornado.wsgi.WSGIContainer(handler)          server = tornado.httpserver.HTTPServer(container)          server.listen(port=self.port) @@ -1807,22 +2187,29 @@ class GeventServer(ServerAdapter):            issues: No streaming, no pipelining, no SSL.      """      def run(self, handler): -        from gevent import wsgi as wsgi_fast, pywsgi as wsgi, monkey +        from gevent import wsgi as wsgi_fast, pywsgi, monkey, local          if self.options.get('monkey', True): -            monkey.patch_all() -        if self.options.get('fast', False): -            wsgi = wsgi_fast +            if not threading.local is local.local: monkey.patch_all() +        wsgi = wsgi_fast if self.options.get('fast') else pywsgi          wsgi.WSGIServer((self.host, self.port), handler).serve_forever()  class GunicornServer(ServerAdapter): -    """ Untested. """ +    """ Untested. See http://gunicorn.org/configure.html for options. """      def run(self, handler): -        from gunicorn.arbiter import Arbiter -        from gunicorn.config import Config -        handler.cfg = Config({'bind': "%s:%d" % (self.host, self.port), 'workers': 4}) -        arbiter = Arbiter(handler) -        arbiter.run() +        from gunicorn.app.base import Application + +        config = {'bind': "%s:%d" % (self.host, int(self.port))} +        config.update(self.options) + +        class GunicornApplication(Application): +            def init(self, parser, opts, args): +                return config + +            def load(self): +                return handler + +        GunicornApplication().run()  class EventletServer(ServerAdapter): @@ -1833,8 +2220,7 @@ class EventletServer(ServerAdapter):  class RocketServer(ServerAdapter): -    """ Untested. As requested in issue 63 -        https://github.com/defnull/bottle/issues/#issue/63 """ +    """ Untested. """      def run(self, handler):          from rocket import Rocket          server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler }) @@ -1842,7 +2228,7 @@ class RocketServer(ServerAdapter):  class BjoernServer(ServerAdapter): -    """ Screamingly fast server written in C: https://github.com/jonashaag/bjoern """ +    """ Fast server written in C: https://github.com/jonashaag/bjoern """      def run(self, handler):          from bjoern import run          run(handler, self.host, self.port) @@ -1858,7 +2244,6 @@ class AutoServer(ServerAdapter):              except ImportError:                  pass -  server_names = {      'cgi': CGIServer,      'flup': FlupFCGIServer, @@ -1889,57 +2274,41 @@ server_names = {  ############################################################################### -def _load(target, **vars): -    """ Fetch something from a module. The exact behaviour depends on the the -        target string: - -        If the target is a valid python import path (e.g. `package.module`), -        the rightmost part is returned as a module object. -        If the target contains a colon (e.g. `package.module:var`) the module -        variable specified after the colon is returned. -        If the part after the colon contains any non-alphanumeric characters -        (e.g. `package.module:func(var)`) the result of the expression -        is returned. The expression has access to keyword arguments supplied -        to this function. +def load(target, **namespace): +    """ Import a module or fetch an object from a module. -        Example:: -        >>> _load('bottle') -        <module 'bottle' from 'bottle.py'> -        >>> _load('bottle:Bottle') -        <class 'bottle.Bottle'> -        >>> _load('bottle:cookie_encode(v, secret)', v='foo', secret='bar') -        '!F+hN4dQxaDJ4QxxaZ+Z3jw==?gAJVA2Zvb3EBLg==' +        * ``package.module`` returns `module` as a module object. +        * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. +        * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. +        The last form accepts not only function calls, but any type of +        expression. Keyword arguments passed to this function are available as +        local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``      """      module, target = target.split(":", 1) if ':' in target else (target, None) -    if module not in sys.modules: -        __import__(module) -    if not target: -        return sys.modules[module] -    if target.isalnum(): -        return getattr(sys.modules[module], target) +    if module not in sys.modules: __import__(module) +    if not target: return sys.modules[module] +    if target.isalnum(): return getattr(sys.modules[module], target)      package_name = module.split('.')[0] -    vars[package_name] = sys.modules[package_name] -    return eval('%s.%s' % (module, target), vars) +    namespace[package_name] = sys.modules[package_name] +    return eval('%s.%s' % (module, target), namespace)  def load_app(target): -    """ Load a bottle application based on a target string and return the -        application object. - -        If the target is an import path (e.g. package.module), the application -        stack is used to isolate the routes defined in that module. -        If the target contains a colon (e.g. package.module:myapp) the -        module variable specified after the colon is returned instead. -    """ -    tmp = app.push() # Create a new "default application" -    rv = _load(target) # Import the target module -    app.remove(tmp) # Remove the temporary added default application -    return rv if isinstance(rv, Bottle) else tmp - +    """ Load a bottle application from a module and make sure that the import +        does not affect the current default application, but returns a separate +        application object. See :func:`load` for the target parameter. """ +    global NORUN; NORUN, nr_old = True, NORUN +    try: +        tmp = default_app.push() # Create a new "default application" +        rv = load(target) # Import the target module +        return rv if callable(rv) else tmp +    finally: +        default_app.remove(tmp) # Remove the temporary added default application +        NORUN = nr_old  def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, -        interval=1, reloader=False, quiet=False, **kargs): +        interval=1, reloader=False, quiet=False, plugins=None, **kargs):      """ Start a server instance. This method blocks until the server terminates.          :param app: WSGI application or target string supported by @@ -1956,114 +2325,115 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,          :param quiet: Suppress output to stdout and stderr? (default: False)          :param options: Options passed to the server adapter.       """ -    app = app or default_app() -    if isinstance(app, basestring): -        app = load_app(app) -    if isinstance(server, basestring): -        server = server_names.get(server) -    if isinstance(server, type): -        server = server(host=host, port=port, **kargs) -    if not isinstance(server, ServerAdapter): -        raise RuntimeError("Server must be a subclass of ServerAdapter") -    server.quiet = server.quiet or quiet -    if not server.quiet and not os.environ.get('BOTTLE_CHILD'): -        print "Bottle server starting up (using %s)..." % repr(server) -        print "Listening on http://%s:%d/" % (server.host, server.port) -        print "Use Ctrl-C to quit." -        print +    if NORUN: return +    if reloader and not os.environ.get('BOTTLE_CHILD'): +        try: +            fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') +            os.close(fd) # We only need this file to exist. We never write to it +            while os.path.exists(lockfile): +                args = [sys.executable] + sys.argv +                environ = os.environ.copy() +                environ['BOTTLE_CHILD'] = 'true' +                environ['BOTTLE_LOCKFILE'] = lockfile +                p = subprocess.Popen(args, env=environ) +                while p.poll() is None: # Busy wait... +                    os.utime(lockfile, None) # I am alive! +                    time.sleep(interval) +                if p.poll() != 3: +                    if os.path.exists(lockfile): os.unlink(lockfile) +                    sys.exit(p.poll()) +        except KeyboardInterrupt: +            pass +        finally: +            if os.path.exists(lockfile): +                os.unlink(lockfile) +        return + +    stderr = sys.stderr.write +      try: +        app = app or default_app() +        if isinstance(app, basestring): +            app = load_app(app) +        if not callable(app): +            raise ValueError("Application is not callable: %r" % app) + +        for plugin in plugins or []: +            app.install(plugin) + +        if server in server_names: +            server = server_names.get(server) +        if isinstance(server, basestring): +            server = load(server) +        if isinstance(server, type): +            server = server(host=host, port=port, **kargs) +        if not isinstance(server, ServerAdapter): +            raise ValueError("Unknown or unsupported server: %r" % server) + +        server.quiet = server.quiet or quiet +        if not server.quiet: +            stderr("Bottle server starting up (using %s)...\n" % repr(server)) +            stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) +            stderr("Hit Ctrl-C to quit.\n\n") +          if reloader: -            interval = min(interval, 1) -            if os.environ.get('BOTTLE_CHILD'): -                _reloader_child(server, app, interval) -            else: -                _reloader_observer(server, app, interval) +            lockfile = os.environ.get('BOTTLE_LOCKFILE') +            bgcheck = FileCheckerThread(lockfile, interval) +            with bgcheck: +                server.run(app) +            if bgcheck.status == 'reload': +                sys.exit(3)          else:              server.run(app)      except KeyboardInterrupt:          pass -    if not server.quiet and not os.environ.get('BOTTLE_CHILD'): -        print "Shutting down..." +    except (SyntaxError, ImportError): +        if not reloader: raise +        if not getattr(server, 'quiet', False): print_exc() +        sys.exit(3) +    finally: +        if not getattr(server, 'quiet', False): stderr('Shutdown...\n')  class FileCheckerThread(threading.Thread): -    ''' Thread that periodically checks for changed module files. ''' +    ''' Interrupt main-thread as soon as a changed module file is detected, +        the lockfile gets deleted or gets to old. '''      def __init__(self, lockfile, interval):          threading.Thread.__init__(self)          self.lockfile, self.interval = lockfile, interval -        #1: lockfile to old; 2: lockfile missing -        #3: module file changed; 5: external exit -        self.status = 0 +        #: Is one of 'reload', 'error' or 'exit' +        self.status = None      def run(self):          exists = os.path.exists          mtime = lambda path: os.stat(path).st_mtime          files = dict() +          for module in sys.modules.values():              path = getattr(module, '__file__', '')              if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]              if path and exists(path): files[path] = mtime(path) +          while not self.status: +            if not exists(self.lockfile)\ +            or mtime(self.lockfile) < time.time() - self.interval - 5: +                self.status = 'error' +                thread.interrupt_main()              for path, lmtime in files.iteritems():                  if not exists(path) or mtime(path) > lmtime: -                    self.status = 3 -            if not exists(self.lockfile): -                self.status = 2 -            elif mtime(self.lockfile) < time.time() - self.interval - 5: -                self.status = 1 -            if not self.status: -                time.sleep(self.interval) -        if self.status != 5: -            thread.interrupt_main() - - -def _reloader_child(server, app, interval): -    ''' Start the server and check for modified files in a background thread. -        As soon as an update is detected, KeyboardInterrupt is thrown in -        the main thread to exit the server loop. The process exists with status -        code 3 to request a reload by the observer process. If the lockfile -        is not modified in 2*interval second or missing, we assume that the -        observer process died and exit with status code 1 or 2. -    ''' -    lockfile = os.environ.get('BOTTLE_LOCKFILE') -    bgcheck = FileCheckerThread(lockfile, interval) -    try: -        bgcheck.start() -        server.run(app) -    except KeyboardInterrupt: -        pass -    bgcheck.status, status = 5, bgcheck.status -    bgcheck.join() # bgcheck.status == 5 --> silent exit -    if status: sys.exit(status) - - -def _reloader_observer(server, app, interval): -    ''' Start a child process with identical commandline arguments and restart -        it as long as it exists with status code 3. Also create a lockfile and -        touch it (update mtime) every interval seconds. -    ''' -    fd, lockfile = tempfile.mkstemp(prefix='bottle-reloader.', suffix='.lock') -    os.close(fd) # We only need this file to exist. We never write to it -    try: -        while os.path.exists(lockfile): -            args = [sys.executable] + sys.argv -            environ = os.environ.copy() -            environ['BOTTLE_CHILD'] = 'true' -            environ['BOTTLE_LOCKFILE'] = lockfile -            p = subprocess.Popen(args, env=environ) -            while p.poll() is None: # Busy wait... -                os.utime(lockfile, None) # I am alive! -                time.sleep(interval) -            if p.poll() != 3: -                if os.path.exists(lockfile): os.unlink(lockfile) -                sys.exit(p.poll()) -            elif not server.quiet: -                print "Reloading server..." -    except KeyboardInterrupt: -        pass -    if os.path.exists(lockfile): os.unlink(lockfile) - +                    self.status = 'reload' +                    thread.interrupt_main() +                    break +            time.sleep(self.interval) +     +    def __enter__(self): +        self.start() +     +    def __exit__(self, exc_type, exc_val, exc_tb): +        if not self.status: self.status = 'exit' # silent exit +        self.join() +        return issubclass(exc_type, KeyboardInterrupt) @@ -2081,7 +2451,7 @@ class TemplateError(HTTPError):  class BaseTemplate(object):      """ Base class and minimal API for template adapters """ -    extentions = ['tpl','html','thtml','stpl'] +    extensions = ['tpl','html','thtml','stpl']      settings = {} #used in prepare()      defaults = {} #used in render() @@ -2120,7 +2490,7 @@ class BaseTemplate(object):              fname = os.path.join(spath, name)              if os.path.isfile(fname):                  return fname -            for ext in cls.extentions: +            for ext in cls.extensions:                  if os.path.isfile('%s.%s' % (fname, ext)):                      return '%s.%s' % (fname, ext) @@ -2128,6 +2498,7 @@ class BaseTemplate(object):      def global_config(cls, key, *args):          ''' This reads or sets the global settings stored in class.settings. '''          if args: +            cls.settings = cls.settings.copy() # Make settings local to class              cls.settings[key] = args[0]          else:              return cls.settings[key] @@ -2185,7 +2556,7 @@ class CheetahTemplate(BaseTemplate):          self.context.vars.update(kwargs)          out = str(self.tpl)          self.context.vars.clear() -        return [out] +        return out  class Jinja2Template(BaseTemplate): @@ -2206,7 +2577,7 @@ class Jinja2Template(BaseTemplate):          for dictarg in args: kwargs.update(dictarg)          _defaults = self.defaults.copy()          _defaults.update(kwargs) -        return self.tpl.render(**_defaults).encode("utf-8") +        return self.tpl.render(**_defaults)      def loader(self, name):          fname = self.search(name, self.lookup) @@ -2228,7 +2599,6 @@ class SimpleTALTemplate(BaseTemplate):      def render(self, *args, **kwargs):          from simpletal import simpleTALES -        from StringIO import StringIO          for dictarg in args: kwargs.update(dictarg)          # TODO: maybe reuse a context instead of always creating one          context = simpleTALES.Context() @@ -2242,7 +2612,8 @@ class SimpleTALTemplate(BaseTemplate):  class SimpleTemplate(BaseTemplate): -    blocks = ('if','elif','else','try','except','finally','for','while','with','def','class') +    blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', +              'with', 'def', 'class')      dedent_blocks = ('elif', 'else', 'except', 'finally')      @lazy_attribute @@ -2258,7 +2629,7 @@ class SimpleTemplate(BaseTemplate):               |\#.*                        # Comments              )''', re.VERBOSE) -    def prepare(self, escape_func=cgi.escape, noescape=False): +    def prepare(self, escape_func=html_escape, noescape=False, **kwargs):          self.cache = {}          enc = self.encoding          self._str = lambda x: touni(x, enc) @@ -2285,7 +2656,7 @@ class SimpleTemplate(BaseTemplate):          ptrbuffer = [] # Buffer for printable strings and token tuple instances          codebuffer = [] # Buffer for generated python code          multiline = dedent = oneline = False -        template = self.source if self.source else open(self.filename).read() +        template = self.source or open(self.filename, 'rb').read()          def yield_tokens(line):              for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): @@ -2327,7 +2698,7 @@ class SimpleTemplate(BaseTemplate):                  line = line.split('%',1)[1].lstrip() # Full line following the %                  cline = self.split_comment(line).strip()                  cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] -                flush() ##encodig (TODO: why?) +                flush() # You are actually reading this? Good luck, it's a mess :)                  if cmd in self.blocks or multiline:                      cmd = multiline or cmd                      dedent = cmd in self.dedent_blocks # "else:" @@ -2374,15 +2745,15 @@ class SimpleTemplate(BaseTemplate):          env = self.defaults.copy()          env.update({'_stdout': _stdout, '_printlist': _stdout.extend,                 '_include': self.subtemplate, '_str': self._str, -               '_escape': self._escape}) +               '_escape': self._escape, 'get': env.get, +               'setdefault': env.setdefault, 'defined': env.__contains__})          env.update(kwargs)          eval(self.co, env)          if '_rebase' in env:              subtpl, rargs = env['_rebase'] -            subtpl = self.__class__(name=subtpl, lookup=self.lookup)              rargs['_base'] = _stdout[:] #copy stdout              del _stdout[:] # clear stdout -            return subtpl.execute(_stdout, rargs) +            return self.subtemplate(subtpl,_stdout,rargs)          return env      def render(self, *args, **kwargs): @@ -2463,11 +2834,16 @@ simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate)  TEMPLATE_PATH = ['./', './views/']  TEMPLATES = {}  DEBUG = False -MEMFILE_MAX = 1024*100 +NORUN = False # If set, run() does nothing. Used by load_app()  #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')  HTTP_CODES = httplib.responses  HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.iteritems())  #: The default template used for error pages. Override with @error()  ERROR_PAGE_TEMPLATE = """ @@ -2480,13 +2856,15 @@ ERROR_PAGE_TEMPLATE = """              <title>Error {{e.status}}: {{status_name}}</title>              <style type="text/css">                html {background-color: #eee; font-family: sans;} -              body {background-color: #fff; border: 1px solid #ddd; padding: 15px; margin: 15px;} +              body {background-color: #fff; border: 1px solid #ddd; +                    padding: 15px; margin: 15px;}                pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}              </style>          </head>          <body>              <h1>Error {{e.status}}: {{status_name}}</h1> -            <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> caused an error:</p> +            <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> +               caused an error:</p>              <pre>{{e.output}}</pre>              %if DEBUG and e.exception:                <h2>Exception:</h2> @@ -2499,17 +2877,18 @@ ERROR_PAGE_TEMPLATE = """          </body>      </html>  %except ImportError: -    <b>ImportError:</b> Could not generate the error page. Please add bottle to sys.path +    <b>ImportError:</b> Could not generate the error page. Please add bottle to +    the import path.  %end  """ -#: A thread-save instance of :class:`Request` representing the `current` request. +#: A thread-safe instance of :class:`Request` representing the `current` request.  request = Request() -#: A thread-save instance of :class:`Response` used to build the HTTP response. +#: A thread-safe instance of :class:`Response` used to build the HTTP response.  response = Response() -#: A thread-save namepsace. Not used by Bottle. +#: A thread-safe namespace. Not used by Bottle.  local = threading.local()  # Initialize app stack (create first empty Bottle app) @@ -2520,3 +2899,28 @@ app.push()  #: A virtual package that redirects import statements.  #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.  ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module + +if __name__ == '__main__': +    opt, args, parser = _cmd_options, _cmd_args, _cmd_parser +    if opt.version: +        print 'Bottle', __version__; sys.exit(0) +    if not args: +        parser.print_help() +        print '\nError: No application specified.\n' +        sys.exit(1) + +    try: +        sys.path.insert(0, '.') +        sys.modules.setdefault('bottle', sys.modules['__main__']) +    except (AttributeError, ImportError), e: +        parser.error(e.args[0]) + +    if opt.bind and ':' in opt.bind: +        host, port = opt.bind.rsplit(':', 1) +    else: +        host, port = (opt.bind or 'localhost'), 8080 + +    debug(opt.debug) +    run(args[0], host=host, port=port, server=opt.server, reloader=opt.reload, plugins=opt.plugin) + +# THE END | 
