From 868e5a4d056b19cc9a747eb4d775b5dc59dc20f8 Mon Sep 17 00:00:00 2001 From: Pavion Date: Sun, 8 May 2016 17:31:46 +0200 Subject: [PATCH] Fixed login for Python 3.5 --- bottle.py | 1491 +++++++++++++++++++++++++++++++-------------- tvstreamrecord.py | 17 +- 2 files changed, 1027 insertions(+), 481 deletions(-) diff --git a/bottle.py b/bottle.py index 5e32209..c2e3435 100644 --- a/bottle.py +++ b/bottle.py @@ -2,75 +2,134 @@ # -*- coding: utf-8 -*- """ Bottle is a fast and simple micro-framework for small web applications. It -offers request dispatching (Routes) with url parameter support, templates, +offers request dispatching (Routes) with URL parameter support, templates, a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and template engines - all in a single file and with no dependencies other than the Python Standard Library. Homepage and documentation: http://bottlepy.org/ -Copyright (c) 2014, Marcel Hellkamp. +Copyright (c) 2015, Marcel Hellkamp. License: MIT (see LICENSE for details) """ from __future__ import with_statement +import sys __author__ = 'Marcel Hellkamp' __version__ = '0.13-dev' __license__ = 'MIT' -# The gevent and eventlet server adapters need to patch some modules before -# they are imported. This is why we parse the commandline parameters here but -# handle them later -if __name__ == '__main__': +############################################################################### +# Command-line interface ######################################################## +############################################################################### +# INFO: Some server adapters need to monkey-patch std-lib modules before they +# are imported. This is why some of the command-line handling is done here, but +# the actual call to main() is at the end of the file. + + +def _cli_parse(args): 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: - if _cmd_options.server.startswith('gevent'): - import gevent.monkey; gevent.monkey.patch_all() - elif _cmd_options.server.startswith('eventlet'): - import eventlet; eventlet.monkey_patch() + parser = OptionParser( + usage="usage: %prog [options] package.module:app") + opt = 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("-c", "--conf", action="append", metavar="FILE", + help="load config values from FILE.") + opt("-C", "--param", action="append", metavar="NAME=VALUE", + help="override config values.") + opt("--debug", action="store_true", help="start server in debug mode.") + opt("--reload", action="store_true", help="auto-reload on file changes.") + opts, args = parser.parse_args(args[1:]) + + return opts, args, parser + + +def _cli_patch(args): + opts, _, _ = _cli_parse(args) + if opts.server: + if opts.server.startswith('gevent'): + import gevent.monkey + gevent.monkey.patch_all() + elif opts.server.startswith('eventlet'): + import eventlet + eventlet.monkey_patch() + + +if __name__ == '__main__': + _cli_patch(sys.argv) + +############################################################################### +# Imports and Python 2/3 unification ########################################### +############################################################################### + import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ - os, re, subprocess, sys, tempfile, threading, time, warnings + os, re, tempfile, threading, time, warnings, hashlib +from types import FunctionType from datetime import date as datedate, datetime, timedelta from tempfile import TemporaryFile from traceback import format_exc, print_exc -from inspect import getargspec from unicodedata import normalize +# inspect.getargspec was removed in Python 3.6, use +# Signature-based version where we can (Python 3.3+) +try: + from inspect import signature + def getargspec(func): + params = signature(func).parameters + args, varargs, keywords, defaults = [], None, None, [] + for name, param in params.items(): + if param.kind == param.VAR_POSITIONAL: + varargs = name + elif param.kind == param.VAR_KEYWORD: + keywords = name + else: + args.append(name) + if param.default is not param.empty: + defaults.append(param.default) + return (args, varargs, keywords, tuple(defaults) or None) +except ImportError: + try: + from inspect import getfullargspec + def getargspec(func): + spec = getfullargspec(func) + kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) + return kwargs, spec[1], spec[2], spec[3] + except ImportError: + from inspect import getargspec -try: from simplejson import dumps as json_dumps, loads as json_lds -except ImportError: # pragma: no cover - try: from json import dumps as json_dumps, loads as json_lds +try: + from simplejson import dumps as json_dumps, loads as json_lds +except ImportError: # pragma: no cover + try: + from json import dumps as json_dumps, loads as json_lds except ImportError: - try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds + try: + from django.utils.simplejson import dumps as json_dumps, loads as json_lds except ImportError: - def json_dumps(data): - raise ImportError("JSON support requires Python 2.6 or simplejson.") - json_lds = json_dumps + def json_dumps(data): + raise ImportError( + "JSON support requires Python 2.6 or simplejson.") + json_lds = json_dumps # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities. # It ain't pretty but it works... Sorry for the mess. -py = sys.version_info +py = sys.version_info py3k = py >= (3, 0, 0) py25 = py < (2, 6, 0) py31 = (3, 1, 0) <= py < (3, 2, 0) # Workaround for the missing "as" keyword in py3k. -def _e(): return sys.exc_info()[1] +def _e(): + return sys.exc_info()[1] # Workaround for the "print is a keyword/function" Python 2/3 dilemma # and a fallback for mod_wsgi (resticts stdout/err attribute access) @@ -91,14 +150,16 @@ def _e(): return sys.exc_info()[1] from collections import MutableMapping as DictMixin import pickle from io import BytesIO - from configparser import ConfigParser + from configparser import ConfigParser, Error as ConfigParserError basestring = str unicode = str json_loads = lambda s: json_lds(touni(s)) callable = lambda x: hasattr(x, '__call__') imap = map - def _raise(*a): raise a[0](a[1]).with_traceback(a[2]) -else: # 2.x + + def _raise(*a): + raise a[0](a[1]).with_traceback(a[2]) +else: # 2.x import httplib import thread from urlparse import urljoin, SplitResult as UrlSplitResult @@ -107,19 +168,24 @@ def _raise(*a): raise a[0](a[1]).with_traceback(a[2]) from itertools import imap import cPickle as pickle from StringIO import StringIO as BytesIO - from ConfigParser import SafeConfigParser as ConfigParser + from ConfigParser import SafeConfigParser as ConfigParser, \ + Error as ConfigParserError if py25: - msg = "Python 2.5 support may be dropped in future versions of Bottle." - warnings.warn(msg, DeprecationWarning) from UserDict import DictMixin - def next(it): return it.next() + + def next(it): + return it.next() + bytes = str - else: # 2.6, 2.7 + else: # 2.6, 2.7 from collections import MutableMapping as DictMixin unicode = unicode json_loads = json_lds eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) +if py25 or py31: + msg = "Python 2.5 and 3.1 support will be dropped in future versions of Bottle." + warnings.warn(msg, DeprecationWarning) # Some helpers for string/byte handling def tob(s, enc='utf8'): @@ -132,6 +198,7 @@ def touni(s, enc='utf8', err='strict'): else: return unicode(s or ("" if s is None else s)) + tonat = touni if py3k else tob # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense). @@ -140,7 +207,8 @@ def touni(s, enc='utf8', err='strict'): from io import TextIOWrapper class NCTextIOWrapper(TextIOWrapper): - def close(self): pass # Keep wrapped buffer open. + def close(self): + pass # Keep wrapped buffer open. # A bug in functools causes it to break if the wrapper is an instance method @@ -150,14 +218,21 @@ def update_wrapper(wrapper, wrapped, *a, **ka): except AttributeError: pass - # These helpers are used at module level and need to be defined first. # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. -def depr(message, strict=False): - warnings.warn(message, DeprecationWarning, stacklevel=3) -def makelist(data): # This is just to handy +def depr(major, minor, cause, fix): + text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\ + "Cause: %s\n"\ + "Fix: %s\n" % (major, minor, cause, fix) + if DEBUG == 'strict': + raise DeprecationWarning(text) + warnings.warn(text, DeprecationWarning, stacklevel=3) + return DeprecationWarning(text) + + +def makelist(data): # This is just too handy if isinstance(data, (tuple, list, set, dict)): return list(data) elif data: @@ -168,6 +243,7 @@ def makelist(data): # This is just to handy class DictProperty(object): """ Property that maps to a key in a local dict-like attribute. """ + def __init__(self, attr, key=None, read_only=False): self.attr, self.key, self.read_only = attr, key, read_only @@ -208,6 +284,7 @@ def __get__(self, obj, cls): class lazy_attribute(object): """ A property that caches itself to the class object. """ + def __init__(self, func): functools.update_wrapper(self, func, updated=[]) self.getter = func @@ -217,11 +294,6 @@ def __get__(self, obj, cls): setattr(cls, self.__name__, value) return value - - - - - ############################################################################### # Exceptions and Events ######################################################## ############################################################################### @@ -231,11 +303,6 @@ class BottleException(Exception): """ A base class for exceptions used by bottle. """ pass - - - - - ############################################################################### # Routing ###################################################################### ############################################################################### @@ -249,7 +316,10 @@ 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 RouterUnknownModeError(RouteError): + + pass class RouteSyntaxError(RouteError): @@ -265,8 +335,8 @@ def _re_flatten(p): non-capturing groups. """ if '(' not in p: return p - return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', - lambda m: m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:', p) + return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if + len(m.group(1)) % 2 else m.group(1) + '(?:', p) class Router(object): @@ -282,27 +352,28 @@ class Router(object): """ default_pattern = '[^/]+' - default_filter = 're' + default_filter = 're' #: The current CPython regexp implementation does not allow more #: than 99 matching groups per regular expression. _MAX_GROUPS_PER_PATTERN = 99 def __init__(self, strict=False): - self.rules = [] # All rules in order - self._groups = {} # index of regexes to find them in dyna_routes - self.builder = {} # Data structure for the url builder - self.static = {} # Search structure for static routes - self.dyna_routes = {} - self.dyna_regexes = {} # Search structure for dynamic routes + self.rules = [] # All rules in order + self._groups = {} # index of regexes to find them in dyna_routes + self.builder = {} # Data structure for the url builder + self.static = {} # Search structure for static routes + self.dyna_routes = {} + self.dyna_regexes = {} # Search structure for dynamic routes #: If true, static routes are no longer checked first. self.strict_order = strict self.filters = { - 're': lambda conf: - (_re_flatten(conf or self.default_pattern), None, None), - 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), + 're': lambda conf: (_re_flatten(conf or self.default_pattern), + None, None), + 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), - 'path': lambda conf: (r'.+?', None, None)} + 'path': lambda conf: (r'.+?', None, None) + } def add_filter(self, name, func): """ Add a filter. The provided function is called with the configuration @@ -320,7 +391,10 @@ def _itertokens(self, rule): for match in self.rule_syntax.finditer(rule): prefix += rule[offset:match.start()] g = match.groups() - if len(g[0])%2: # Escaped wildcard + if g[2] is not None: + depr(0, 13, "Use of old route syntax.", + "Use instead of :name in routes.") + if len(g[0]) % 2: # Escaped wildcard prefix += match.group(0)[len(g[0]):] offset = match.end() continue @@ -330,15 +404,15 @@ def _itertokens(self, rule): yield name, filtr or 'default', conf or None offset, prefix = match.end(), '' if offset <= len(rule) or prefix: - yield prefix+rule[offset:], None, None + yield prefix + rule[offset:], None, None def add(self, rule, method, target, name=None): """ Add a new rule or replace the target for an existing rule. """ - anons = 0 # Number of anonymous wildcards found - keys = [] # Names of keys - pattern = '' # Regular expression pattern with named groups - filters = [] # Lists of wildcard input filters - builder = [] # Data structure for the URL builder + anons = 0 # Number of anonymous wildcards found + keys = [] # Names of keys + pattern = '' # Regular expression pattern with named groups + filters = [] # Lists of wildcard input filters + builder = [] # Data structure for the URL builder is_static = True for key, mode, conf in self._itertokens(rule): @@ -371,9 +445,11 @@ def add(self, rule, method, target, name=None): re_pattern = re.compile('^(%s)$' % pattern) re_match = re_pattern.match except re.error: - raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e())) + raise RouteSyntaxError("Could not add Route: %s (%s)" % + (rule, _e())) if filters: + def getargs(path): url_args = re_match(path).groupdict() for name, wildcard_filter in filters: @@ -383,6 +459,7 @@ def getargs(path): raise HTTPError(400, 'Path has wrong format.') return url_args elif re_pattern.groupindex: + def getargs(path): return re_match(path).groupdict() else: @@ -395,7 +472,8 @@ def getargs(path): if DEBUG: msg = 'Route <%s %s> overwrites a previously defined route' warnings.warn(msg % (method, rule), RuntimeWarning) - self.dyna_routes[method][self._groups[flatpat, method]] = whole_rule + self.dyna_routes[method][ + self._groups[flatpat, method]] = whole_rule else: self.dyna_routes.setdefault(method, []).append(whole_rule) self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 @@ -407,7 +485,7 @@ def _compile(self, method): comborules = self.dyna_regexes[method] = [] maxgroups = self._MAX_GROUPS_PER_PATTERN for x in range(0, len(all_rules), maxgroups): - some = all_rules[x:x+maxgroups] + some = all_rules[x:x + maxgroups] combined = (flatpat for (_, flatpat, _, _) in some) combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) combined = re.compile(combined).match @@ -417,16 +495,18 @@ def _compile(self, method): 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) + 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) + 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: raise RouteBuildError('Missing URL argument: %r' % _e().args[0]) def match(self, environ): - """ Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). """ + """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ verb = environ['REQUEST_METHOD'].upper() path = environ['PATH_INFO'] or '/' @@ -465,21 +545,19 @@ def match(self, environ): raise HTTPError(404, "Not found: " + repr(path)) - - - - 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): + 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``). + #: The path-rule string (e.g. ``/wiki/``). self.rule = rule #: The HTTP method as a string (e.g. ``GET``). self.method = method @@ -530,7 +608,7 @@ def _make_callback(self): callback = plugin.apply(callback, self) else: callback = plugin(callback) - except RouteReset: # Try again with changed configuration. + except RouteReset: # Try again with changed configuration. return self._make_callback() if not callback is self.callback: update_wrapper(callback, self.callback) @@ -543,7 +621,15 @@ def get_undecorated_callback(self): func = getattr(func, '__func__' if py3k else 'im_func', func) closure_attr = '__closure__' if py3k else 'func_closure' while hasattr(func, closure_attr) and getattr(func, closure_attr): - func = getattr(func, closure_attr)[0].cell_contents + attributes = getattr(func, closure_attr) + func = attributes[0].cell_contents + + # in case of decorators with multiple arguments + if not isinstance(func, FunctionType): + # pick first FunctionType instance from multiple arguments + func = filter(lambda x: isinstance(x, FunctionType), + map(lambda x: x.cell_contents, attributes)) + func = list(func)[0] # py3 support return func def get_callback_args(self): @@ -555,7 +641,7 @@ def get_callback_args(self): def get_config(self, key, default=None): """ Lookup a config field and return its value, first checking the route.config, then route.app.config.""" - for conf in (self.config, self.app.conifg): + for conf in (self.config, self.app.config): if key in conf: return conf[key] return default @@ -563,11 +649,6 @@ def __repr__(self): cb = self.get_undecorated_callback() return '<%s %r %r>' % (self.method, self.rule, cb) - - - - - ############################################################################### # Application Object ########################################################### ############################################################################### @@ -583,24 +664,25 @@ class Bottle(object): """ def __init__(self, catchall=True, autojson=True): - #: A :class:`ConfigDict` for app specific configuration. self.config = ConfigDict() - self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config._add_change_listener(functools.partial(self.trigger_hook, 'config')) self.config.meta_set('autojson', 'validate', bool) self.config.meta_set('catchall', 'validate', bool) self.config['catchall'] = catchall self.config['autojson'] = autojson + self._mounts = [] + #: A :class:`ResourceManager` for application files self.resources = ResourceManager() - self.routes = [] # List of installed :class:`Route` instances. - self.router = Router() # Maps requests to :class:`Route` instances. + self.routes = [] # List of installed :class:`Route` instances. + self.router = Router() # Maps requests to :class:`Route` instances. self.error_handler = {} # Core plugins - self.plugins = [] # List of installed plugins. + self.plugins = [] # List of installed plugins. if self.config['autojson']: self.install(JSONPlugin()) self.install(TemplatePlugin()) @@ -644,41 +726,34 @@ def trigger_hook(self, __name, *args, **kwargs): def hook(self, name): """ Return a decorator that attaches a callback to a hook. See :meth:`add_hook` for details.""" + def decorator(func): self.add_hook(name, func) return func - return decorator - - def mount(self, prefix, app, **options): - """ Mount an application (:class:`Bottle` or plain WSGI) to a specific - URL prefix. Example:: - - root_app.mount('/admin/', admin_app) - - :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. - """ + return decorator + def _mount_wsgi(self, prefix, app, **options): segments = [p for p in prefix.split('/') if p] - if not segments: raise ValueError('Empty path prefix.') + if not segments: + raise ValueError('WSGI applications cannot be mounted to "/".') path_depth = len(segments) def mountpoint_wrapper(): try: request.path_shift(path_depth) rs = HTTPResponse([]) + def start_response(status, headerlist, exc_info=None): if exc_info: _raise(*exc_info) rs.status = status - for name, value in headerlist: rs.add_header(name, value) + for name, value in headerlist: + rs.add_header(name, value) return rs.body.append + body = app(request.environ, start_response) - if body and rs.body: body = itertools.chain(rs.body, body) - rs.body = body or rs.body + rs.body = itertools.chain(rs.body, body) if rs.body else body return rs finally: request.path_shift(-path_depth) @@ -692,6 +767,59 @@ def start_response(status, headerlist, exc_info=None): if not prefix.endswith('/'): self.route('/' + '/'.join(segments), **options) + def _mount_app(self, prefix, app, **options): + if app in self._mounts or '_mount.app' in app.config: + depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.", + "Clone application before mounting to a different location.") + return self._mount_wsgi(prefix, app, **options) + + if options: + depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.", + "Do not specify any route options when mounting bottle application.") + return self._mount_wsgi(prefix, app, **options) + + if not prefix.endswith("/"): + depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.", + "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.") + return self._mount_wsgi(prefix, app, **options) + + self._mounts.append(app) + app.config['_mount.prefix'] = prefix + app.config['_mount.app'] = self + for route in app.routes: + route.rule = prefix + route.rule.lstrip('/') + self.add_route(route) + + def mount(self, prefix, app, **options): + """ Mount an application (:class:`Bottle` or plain WSGI) to a specific + URL prefix. Example:: + + parent_app.mount('/prefix/', child_app) + + :param prefix: path prefix or `mount-point`. + :param app: an instance of :class:`Bottle` or a WSGI application. + + Plugins from the parent application are not applied to the routes + of the mounted child application. If you need plugins in the child + application, install them separately. + + While it is possible to use path wildcards within the prefix path + (:class:`Bottle` childs only), it is highly discouraged. + + The prefix path must end with a slash. If you want to access the + root of the child application via `/prefix` in addition to + `/prefix/`, consider adding a route with a 307 redirect to the + parent application. + """ + + if not prefix.startswith('/'): + raise ValueError("Prefix must start with '/'") + + if isinstance(app, Bottle): + return self._mount_app(prefix, app, **options) + else: + return self._mount_wsgi(prefix, app, **options) + def merge(self, routes): """ Merge the routes of another :class:`Bottle` application or a list of :class:`Route` objects into this application. The routes keep their @@ -736,9 +864,11 @@ def reset(self, route=None): if route is None: routes = self.routes elif isinstance(route, Route): routes = [route] else: routes = [self.routes[route]] - for route in routes: route.reset() + for route in routes: + route.reset() if DEBUG: - for route in routes: route.prepare() + for route in routes: + route.prepare() self.trigger_hook('app_reset') def close(self): @@ -769,15 +899,20 @@ def add_route(self, route): self.router.add(route.rule, route.method, route, name=route.name) if DEBUG: route.prepare() - def route(self, path=None, method='GET', callback=None, name=None, - apply=None, skip=None, **config): + def route(self, + path=None, + method='GET', + callback=None, + name=None, + apply=None, + skip=None, **config): """ A decorator to bind a function to a request URL. Example:: - @app.route('/hello/:name') + @app.route('/hello/') def hello(name): return 'Hello %s' % name - The ``:name`` part is a wildcard. See :class:`Router` for syntax + The ```` part is a wildcard. See :class:`Router` for syntax details. :param path: Request path or a list of paths to listen to. If no @@ -799,15 +934,19 @@ def hello(name): if callable(path): path, callback = None, path plugins = makelist(apply) skiplist = makelist(skip) + def decorator(callback): if isinstance(callback, basestring): callback = load(callback) for rule in makelist(path) or yieldroutes(callback): for verb in makelist(method): verb = verb.upper() - route = Route(self, rule, verb, callback, name=name, - plugins=plugins, skiplist=skiplist, **config) + route = Route(self, rule, verb, callback, + name=name, + plugins=plugins, + skiplist=skiplist, **config) self.add_route(route) return callback + return decorator(callback) if callback else decorator def get(self, path=None, method='GET', **options): @@ -832,9 +971,11 @@ def patch(self, path=None, method='PATCH', **options): def error(self, code=500): """ Decorator: Register an output handler for a HTTP error code""" + def wrapper(handler): self.error_handler[int(code)] = handler return handler + return wrapper def default_error_handler(self, res): @@ -843,36 +984,44 @@ def default_error_handler(self, res): def _handle(self, environ): path = environ['bottle.raw_path'] = environ['PATH_INFO'] if py3k: + environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') + + def _inner_handle(): + # Maybe pass variables as locals for better performance? try: - environ['PATH_INFO'] = path.encode('latin1').decode('utf8') - except UnicodeError: - return HTTPError(400, 'Invalid path string. Expected UTF-8') + route, args = self.router.match(environ) + environ['route.handle'] = route + environ['bottle.route'] = route + environ['route.url_args'] = args + return route.call(**args) + except HTTPResponse: + return _e() + except RouteReset: + route.reset() + return _inner_handle() + except (KeyboardInterrupt, SystemExit, MemoryError): + raise + except Exception: + if not self.catchall: raise + stacktrace = format_exc() + environ['wsgi.errors'].write(stacktrace) + return HTTPError(500, "Internal Server Error", _e(), stacktrace) try: + out = None environ['bottle.app'] = self request.bind(environ) response.bind() try: self.trigger_hook('before_request') - route, args = self.router.match(environ) - environ['route.handle'] = route - environ['bottle.route'] = route - environ['route.url_args'] = args - return route.call(**args) - finally: - self.trigger_hook('after_request') - except HTTPResponse: - return _e() - except RouteReset: - route.reset() - return self._handle(environ) - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception: - if not self.catchall: raise - stacktrace = format_exc() - environ['wsgi.errors'].write(stacktrace) - return HTTPError(500, "Internal Server Error", _e(), stacktrace) + except HTTPResponse: + return _e() + out = _inner_handle() + return out + finally: + if isinstance(out, HTTPResponse): + out.apply(response) + self.trigger_hook('after_request') def _cast(self, out, peek=None): """ Try to convert the parameter into something WSGI compatible and set @@ -889,7 +1038,7 @@ def _cast(self, out, peek=None): # Join lists of byte or unicode strings. Mixed lists are NOT supported if isinstance(out, (tuple, list))\ and isinstance(out[0], (bytes, unicode)): - out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' + out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' # Encode unicode strings if isinstance(out, unicode): out = out.encode(response.charset) @@ -902,7 +1051,8 @@ def _cast(self, out, peek=None): # TODO: Handle these explicitly in handle() or make them iterable. if isinstance(out, HTTPError): out.apply(response) - out = self.error_handler.get(out.status_code, self.default_error_handler)(out) + out = self.error_handler.get(out.status_code, + self.default_error_handler)(out) return self._cast(out) if isinstance(out, HTTPResponse): out.apply(response) @@ -984,14 +1134,17 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): default_app.pop() - - + def __setattr__(self, name, value): + if name in self.__dict__: + raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) + self.__dict__[name] = value ############################################################################### # HTTP and WSGI Tools ########################################################## ############################################################################### + class BaseRequest(object): """ A wrapper for WSGI environment dictionaries that adds a lot of convenient access methods and properties. Most of them are read-only. @@ -1032,7 +1185,7 @@ def url_args(self): 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('/') + return '/' + self.environ.get('PATH_INFO', '').lstrip('/') @property def method(self): @@ -1053,7 +1206,7 @@ def get_header(self, name, default=None): 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','')).values() + cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() return FormsDict((c.key, c.value) for c in cookies) def get_cookie(self, key, default=None, secret=None): @@ -1063,7 +1216,7 @@ def get_cookie(self, key, default=None, secret=None): 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 + 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 @@ -1116,13 +1269,21 @@ def files(self): @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 the ``Content-Type`` header is ``application/json`` or + ``application/json-rpc``, this property holds the parsed content + of the request body. Only requests smaller than :attr:`MEMFILE_MAX` + are processed to avoid memory exhaustion. + Invalid JSON raises a 400 error response. + """ ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] - if ctype == 'application/json': - return json_loads(self._get_body_string()) + if ctype in ('application/json', 'application/json-rpc'): + b = self._get_body_string() + if not b: + return None + try: + return json_loads(b) + except (ValueError, TypeError): + raise HTTPError(400, 'Invalid JSON') return None def _iter_body(self, read, bufsize): @@ -1160,11 +1321,15 @@ def _iter_chunked(read, bufsize): maxread -= len(part) if read(2) != rn: raise err - + @DictProperty('environ', 'bottle.request.body', read_only=True) def _body(self): + try: + read_func = self.environ['wsgi.input'].read + except KeyError: + self.environ['wsgi.input'] = BytesIO() + return self.environ['wsgi.input'] body_iter = self._iter_chunked if self.chunked else self._iter_body - read_func = self.environ['wsgi.input'].read body, body_size, is_temp_file = BytesIO(), 0, False for part in body_iter(read_func, self.MEMFILE_MAX): body.write(part) @@ -1183,11 +1348,11 @@ def _get_body_string(self): HTTPError(413) on requests that are to large. """ clen = self.content_length if clen > self.MEMFILE_MAX: - raise HTTPError(413, 'Request to large') + raise HTTPError(413, 'Request entity too large') if clen < 0: clen = self.MEMFILE_MAX + 1 data = self.body.read(clen) - if len(data) > self.MEMFILE_MAX: # Fail fast - raise HTTPError(413, 'Request to large') + if len(data) > self.MEMFILE_MAX: # Fail fast + raise HTTPError(413, 'Request entity too large') return data @property @@ -1203,7 +1368,8 @@ def body(self): @property def chunked(self): """ True if Chunked transfer encoding was. """ - return 'chunked' in self.environ.get('HTTP_TRANSFER_ENCODING', '').lower() + return 'chunked' in self.environ.get( + 'HTTP_TRANSFER_ENCODING', '').lower() #: An alias for :attr:`query`. GET = query @@ -1223,17 +1389,18 @@ def POST(self): post[key] = value return post - safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi + 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] args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) if py31: - args['fp'] = NCTextIOWrapper(args['fp'], encoding='utf8', + args['fp'] = NCTextIOWrapper(args['fp'], + encoding='utf8', newline='\n') elif py3k: args['encoding'] = 'utf8' data = cgi.FieldStorage(**args) - self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394#msg207958 + self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 data = data.list or [] for item in data: if item.filename: @@ -1258,7 +1425,8 @@ def urlparts(self): but the fragment is always empty because it is not visible to the server. """ env = self.environ - http = env.get('HTTP_X_FORWARDED_PROTO') or env.get('wsgi.url_scheme', 'http') + http = env.get('HTTP_X_FORWARDED_PROTO') \ + or 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. @@ -1296,8 +1464,8 @@ def path_shift(self, shift=1): :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) + script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) + self['SCRIPT_NAME'], self['PATH_INFO'] = script, path @property def content_length(self): @@ -1316,7 +1484,7 @@ 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','') + requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') return requested_with.lower() == 'xmlhttprequest' @property @@ -1332,7 +1500,7 @@ def auth(self): 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','')) + basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) if basic: return basic ruser = self.environ.get('REMOTE_USER') if ruser: return (ruser, None) @@ -1360,12 +1528,25 @@ def copy(self): """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ return Request(self.environ.copy()) - def get(self, value, default=None): return self.environ.get(value, default) - 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 get(self, value, default=None): + return self.environ.get(value, default) + + 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. """ @@ -1383,7 +1564,7 @@ def __setitem__(self, key, value): todelete = ('headers', 'cookies') for key in todelete: - self.environ.pop('bottle.request.'+key, None) + self.environ.pop('bottle.request.' + key, None) def __repr__(self): return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) @@ -1391,20 +1572,26 @@ def __repr__(self): def __getattr__(self, name): """ Search in self.environ for additional user defined attributes. """ try: - var = self.environ['bottle.request.ext.%s'%name] + var = self.environ['bottle.request.ext.%s' % name] return var.__get__(self) if hasattr(var, '__get__') else var except KeyError: raise AttributeError('Attribute %r not defined.' % name) def __setattr__(self, name, value): if name == 'environ': return object.__setattr__(self, name, value) - self.environ['bottle.request.ext.%s'%name] = value - - + key = 'bottle.request.ext.%s' % name + if key in self.environ: + raise AttributeError("Attribute already defined: %s" % name) + self.environ[key] = value + def __delattr__(self, name, value): + try: + del self.environ['bottle.request.ext.%s' % name] + except KeyError: + raise AttributeError("Attribute not defined: %s" % name) def _hkey(s): - return s.title().replace('_','-') + return s.title().replace('_', '-') class HeaderProperty(object): @@ -1447,10 +1634,11 @@ class BaseResponse(object): # Header blacklist for specific response codes # (rfc2616 section 10.2.3 and 10.3.5) bad_headers = { - 204: set(('Content-Type',)), + 204: set(('Content-Type', 'Content-Length')), 304: set(('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-Range', 'Content-Type', - 'Content-Md5', 'Last-Modified'))} + 'Content-Md5', 'Last-Modified')) + } def __init__(self, body='', status=None, headers=None, **more_headers): self._cookies = None @@ -1475,7 +1663,7 @@ def copy(self, cls=None): copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) if self._cookies: copy._cookies = SimpleCookie() - copy._cookies.load(self._cookies.output()) + copy._cookies.load(self._cookies.output(header='')) return copy def __iter__(self): @@ -1500,17 +1688,19 @@ def _set_status(self, status): code, status = status, _HTTP_STATUS_LINES.get(status) elif ' ' in status: status = status.strip() - code = int(status.split()[0]) + 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.') + if not 100 <= code <= 999: + raise ValueError('Status code out of range.') self._status_code = code self._status_line = str(status or ('%d Unknown' % code)) def _get_status(self): return self._status_line - status = property(_get_status, _set_status, None, + 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 @@ -1526,10 +1716,18 @@ def headers(self): 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 __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)] = [value if isinstance(value, unicode) else + str(value)] def get_header(self, name, default=None): """ Return the value of a previously defined header. If there is no @@ -1539,11 +1737,13 @@ def get_header(self, name, default=None): def set_header(self, name, value): """ Create a new response header, replacing any previously defined headers with the same name. """ - self._headers[_hkey(name)] = [str(value)] + self._headers[_hkey(name)] = [value if isinstance(value, unicode) + else str(value)] def add_header(self, name, value): """ Add an additional response header, not removing duplicates. """ - self._headers.setdefault(_hkey(name), []).append(str(value)) + self._headers.setdefault(_hkey(name), []).append( + value if isinstance(value, unicode) else str(value)) def iter_headers(self): """ Yield (header, value) tuples, skipping headers that are not @@ -1560,15 +1760,20 @@ def headerlist(self): if self._status_code in self.bad_headers: bad_headers = self.bad_headers[self._status_code] headers = [h for h in headers if h[0] not in bad_headers] - out += [(name, val) for name, vals in headers for val in vals] + out += [(name, val) for (name, vals) in headers for val in vals] if self._cookies: for c in self._cookies.values(): out.append(('Set-Cookie', c.OutputString())) - return out + if py3k: + return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] + else: + return [(k, v.encode('utf8') if isinstance(v, unicode) else v) + for (k, v) in out] content_type = HeaderProperty('Content-Type') content_length = HeaderProperty('Content-Length', reader=int) - expires = HeaderProperty('Expires', + expires = HeaderProperty( + 'Expires', reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), writer=lambda x: http_date(x)) @@ -1620,7 +1825,10 @@ def set_cookie(self, name, value, secret=None, **options): elif not isinstance(value, basestring): raise TypeError('Secret key missing for non-string Cookie.') - if len(value) > 4096: raise ValueError('Cookie value to long.') + # Cookie size plus options must not exceed 4kb. + if len(name) + len(value) > 3800: + raise ValueError('Content does not fit into a cookie.') + self._cookies[name] = value for key, value in options.items(): @@ -1633,6 +1841,8 @@ def set_cookie(self, name, value, secret=None, **options): elif isinstance(value, (int, float)): value = time.gmtime(value) value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) + if key in ('secure', 'httponly') and not value: + continue self._cookies[name][key.replace('_', '-')] = value def delete_cookie(self, key, **kwargs): @@ -1651,12 +1861,19 @@ def __repr__(self): def _local_property(): ls = threading.local() + def fget(_): - try: return ls.var + try: + return ls.var except AttributeError: raise RuntimeError("Request context not initialized.") - def fset(_, value): ls.var = value - def fdel(_): del ls.var + + def fset(_, value): + ls.var = value + + def fdel(_): + del ls.var + return property(fget, fset, fdel, 'Thread-local property') @@ -1679,9 +1896,9 @@ class LocalResponse(BaseResponse): bind = BaseResponse.__init__ _status_line = _local_property() _status_code = _local_property() - _cookies = _local_property() - _headers = _local_property() - body = _local_property() + _cookies = _local_property() + _headers = _local_property() + body = _local_property() Request = BaseRequest @@ -1702,26 +1919,28 @@ def apply(self, other): class HTTPError(HTTPResponse): default_status = 500 - def __init__(self, status=None, body=None, exception=None, traceback=None, - **options): + + def __init__(self, + status=None, + body=None, + exception=None, + traceback=None, **more_headers): self.exception = exception self.traceback = traceback - super(HTTPError, self).__init__(body, status, **options) - - - - + super(HTTPError, self).__init__(body, status, **more_headers) ############################################################################### # Plugins ###################################################################### ############################################################################### -class PluginError(BottleException): pass + +class PluginError(BottleException): + pass class JSONPlugin(object): name = 'json' - api = 2 + api = 2 def __init__(self, json_dumps=json_dumps): self.json_dumps = json_dumps @@ -1729,6 +1948,7 @@ def __init__(self, json_dumps=json_dumps): def apply(self, callback, _): dumps = self.json_dumps if not dumps: return callback + def wrapper(*a, **ka): try: rv = callback(*a, **ka) @@ -1755,7 +1975,10 @@ 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 + api = 2 + + def setup(self, app): + app.tpl = self def apply(self, callback, route): conf = route.config.get('template') @@ -1767,6 +1990,8 @@ def apply(self, callback, route): return callback + + #: Not a plugin, but part of the plugin API. TODO: Find a better place. class _ImportRedirect(object): def __init__(self, name, impmask): @@ -1774,8 +1999,12 @@ def __init__(self, name, impmask): self.name = name self.impmask = impmask self.module = sys.modules.setdefault(name, imp.new_module(name)) - self.module.__dict__.update({'__file__': __file__, '__path__': [], - '__all__': [], '__loader__': self}) + self.module.__dict__.update({ + '__file__': __file__, + '__path__': [], + '__all__': [], + '__loader__': self + }) sys.meta_path.append(self) def find_module(self, fullname, path=None): @@ -1794,11 +2023,6 @@ def load_module(self, fullname): module.__loader__ = self return module - - - - - ############################################################################### # Common Utilities ############################################################# ############################################################################### @@ -1813,33 +2037,63 @@ class MultiDict(DictMixin): def __init__(self, *a, **k): self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) - 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 __getitem__(self, key): return self.dict[key][-1] - def __setitem__(self, key, value): self.append(key, value) - def keys(self): return self.dict.keys() + 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 __getitem__(self, key): + return self.dict[key][-1] + + def __setitem__(self, key, value): + self.append(key, value) + + def keys(self): + return self.dict.keys() if py3k: - def values(self): return (v[-1] for v in self.dict.values()) - def items(self): return ((k, v[-1]) for k, v in self.dict.items()) + + def values(self): + return (v[-1] for v in self.dict.values()) + + def items(self): + return ((k, v[-1]) for k, v in self.dict.items()) + def allitems(self): return ((k, v) for k, vl in self.dict.items() for v in vl) + iterkeys = keys itervalues = values iteritems = items iterallitems = allitems else: - def values(self): return [v[-1] for v in self.dict.values()] - def items(self): return [(k, v[-1]) for k, v in self.dict.items()] - def iterkeys(self): return self.dict.iterkeys() - def itervalues(self): return (v[-1] for v in self.dict.itervalues()) + + def values(self): + return [v[-1] for v in self.dict.values()] + + def items(self): + return [(k, v[-1]) for k, v in self.dict.items()] + + 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): return ((k, v) for k, vl in self.dict.iteritems() for v in vl) + def allitems(self): return [(k, v) for k, vl in self.dict.iteritems() for v in vl] @@ -1892,9 +2146,9 @@ class FormsDict(MultiDict): recode_unicode = True def _fix(self, s, encoding=None): - if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI + if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI return s.encode('latin1').decode(encoding or self.input_encoding) - elif isinstance(s, bytes): # Python 2 WSGI + elif isinstance(s, bytes): # Python 2 WSGI return s.decode(encoding or self.input_encoding) else: return s @@ -1932,16 +2186,33 @@ 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 __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)] = [value if isinstance(value, unicode) else + 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 [] + self.dict.setdefault(_hkey(key), []).append( + value if isinstance(value, unicode) else str(value)) + + def replace(self, key, value): + self.dict[_hkey(key)] = [value if isinstance(value, unicode) else + 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 [_hkey(n) for n in names]: if name in self.dict: @@ -1967,7 +2238,7 @@ def __init__(self, environ): def _ekey(self, key): """ Translate header field name to CGI/WSGI environ key. """ - key = key.replace('-','_').upper() + key = key.replace('-', '_').upper() if key in self.cgikeys: return key return 'HTTP_' + key @@ -1977,7 +2248,13 @@ def raw(self, key, default=None): return self.environ.get(self._ekey(key), default) def __getitem__(self, key): - return tonat(self.environ[self._ekey(key)], 'latin1') + val = self.environ[self._ekey(key)] + if py3k: + if isinstance(val, unicode): + val = val.encode('latin1').decode('utf8') + else: + val = val.decode('utf8') + return val def __setitem__(self, key, value): raise TypeError("%s is read-only." % self.__class__) @@ -1988,14 +2265,18 @@ def __delitem__(self, key): def __iter__(self): for key in self.environ: if key[:5] == 'HTTP_': - yield key[5:].replace('_', '-').title() + yield _hkey(key[5:]) elif key in self.cgikeys: - yield key.replace('_', '-').title() + yield _hkey(key) + + def keys(self): + return [x for x in 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 + def __len__(self): + return len(self.keys()) + def __contains__(self, key): + return self._ekey(key) in self.environ class ConfigDict(dict): @@ -2003,11 +2284,28 @@ class ConfigDict(dict): namespaces, validators, meta-data, on_change listeners and more. """ - __slots__ = ('_meta', '_on_change') + __slots__ = ('_meta', '_change_listener', '_fallbacks') def __init__(self): self._meta = {} - self._on_change = lambda name, value: None + self._change_listener = [] + self._fallbacks = [] + + def load_module(self, path, squash): + """ Load values from a Python module. + :param squash: Squash nested dicts into namespaces by using + load_dict(), otherwise use update() + Example: load_config('my.app.settings', True) + Example: load_config('my.app.settings', False) + """ + config_obj = __import__(path) + obj = dict([(key, getattr(config_obj, key)) + for key in dir(config_obj) if key.isupper()]) + if squash: + self.load_dict(obj) + else: + self.update(obj) + return self def load_config(self, filename): """ Load values from an ``*.ini`` style config file. @@ -2034,7 +2332,7 @@ def load_dict(self, source, namespace=''): {'some.namespace.key': 'value'} """ for key, value in source.items(): - if isinstance(key, str): + if isinstance(key, basestring): nskey = (namespace + '.' + key).strip('.') if isinstance(value, dict): self.load_dict(value, namespace=nskey) @@ -2049,19 +2347,21 @@ def update(self, *a, **ka): namespace. Apart from that it works just as the usual dict.update(). Example: ``update('some.namespace', key='value')`` """ prefix = '' - if a and isinstance(a[0], str): + if a and isinstance(a[0], basestring): prefix = a[0].strip('.') + '.' a = a[1:] for key, value in dict(*a, **ka).items(): - self[prefix+key] = value + self[prefix + key] = value def setdefault(self, key, value): if key not in self: self[key] = value + return self[key] def __setitem__(self, key, value): - if not isinstance(key, str): + if not isinstance(key, basestring): raise TypeError('Key has type %r (not a string)' % type(key)) + value = self.meta_get(key, 'filter', lambda x: x)(value) if key in self and self[key] is value: return @@ -2072,16 +2372,44 @@ def __delitem__(self, key): self._on_change(key, None) dict.__delitem__(self, key) + def __missing__(self, key): + for fallback in self._fallbacks: + if key in fallback: + value = self[key] = fallback[key] + self.meta_set(key, 'fallback', fallback) + return value + raise KeyError(key) + + def _on_change(self, key, value): + for cb in self._change_listener: + if cb(self, key, value): + return True + + def _add_change_listener(self, func): + self._change_listener.append(func) + return func + + def _set_fallback(self, fallback): + self._fallbacks.append(fallback) + + @fallback._add_change_listener + def fallback_update(conf, key, value): + if self.meta_get(key, 'fallback') is conf: + self.meta_set(key, 'fallback', None) + dict.__delitem__(self, key) + + @self._add_change_listener + def self_update(conf, key, value): + if conf.meta_get(key, 'fallback'): + conf.meta_set(key, 'fallback', None) + def meta_get(self, key, metafield, default=None): """ Return the value of a meta field for a key. """ return self._meta.get(key, {}).get(metafield, default) def meta_set(self, key, metafield, value): - """ Set the meta field for a key to a new value. This triggers the - on-change handler for existing keys. """ + """ Set the meta field for a key to a new value. """ self._meta.setdefault(key, {})[metafield] = value - if key in self: - self[key] = self[key] def meta_list(self, key): """ Return an iterable of meta field names defined for a key. """ @@ -2093,7 +2421,7 @@ class AppStack(list): def __call__(self): """ Return the current default application. """ - return self[-1] + return self.default def push(self, value=None): """ Add a new :class:`Bottle` instance to the stack """ @@ -2101,11 +2429,17 @@ def push(self, value=None): value = Bottle() self.append(value) return value + new_app = push + @property + def default(self): + try: + return self[-1] + except IndexError: + return self.push() class WSGIFileWrapper(object): - - def __init__(self, fp, buffer_size=1024*64): + def __init__(self, fp, buffer_size=1024 * 64): self.fp, self.buffer_size = fp, buffer_size for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) @@ -2221,7 +2555,6 @@ def open(self, name, mode='r', *args, **kwargs): class FileUpload(object): - def __init__(self, fileobj, name, filename, headers=None): """ Wrapper for file uploads. """ #: Open file(-like) object (BytesIO buffer or temporary file) @@ -2249,13 +2582,14 @@ def filename(self): fname = self.raw_filename if not isinstance(fname, unicode): fname = fname.decode('utf8', 'ignore') - fname = normalize('NFKD', fname).encode('ASCII', 'ignore').decode('ASCII') + fname = normalize('NFKD', fname) + fname = fname.encode('ASCII', 'ignore').decode('ASCII') fname = os.path.basename(fname.replace('\\', os.path.sep)) fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() fname = re.sub(r'[-\s]+', '-', fname).strip('.-') return fname[:255] or 'empty' - def _copy_file(self, fp, chunk_size=2**16): + def _copy_file(self, fp, chunk_size=2 ** 16): read, write, offset = self.file.read, fp.write, self.file.tell() while 1: buf = read(chunk_size) @@ -2263,7 +2597,7 @@ def _copy_file(self, fp, chunk_size=2**16): write(buf) self.file.seek(offset) - def save(self, destination, overwrite=False, chunk_size=2**16): + def save(self, destination, overwrite=False, chunk_size=2 ** 16): """ Save file to disk or copy its content to an open file(-like) object. If *destination* is a directory, :attr:`filename` is added to the path. Existing files are not overwritten by default (IOError). @@ -2272,7 +2606,7 @@ def save(self, destination, overwrite=False, chunk_size=2**16): :param overwrite: If True, replace existing files. (default: False) :param chunk_size: Bytes to read at a time. (default: 64kb) """ - if isinstance(destination, basestring): # Except file-likes here + if isinstance(destination, basestring): # Except file-likes here if os.path.isdir(destination): destination = os.path.join(destination, self.filename) if not overwrite and os.path.exists(destination): @@ -2282,11 +2616,6 @@ def save(self, destination, overwrite=False, chunk_size=2**16): else: self._copy_file(destination, chunk_size) - - - - - ############################################################################### # Application Helper ########################################################### ############################################################################### @@ -2309,7 +2638,7 @@ def redirect(url, code=None): raise res -def _file_iter_range(fp, offset, bytes, maxread=1024*1024): +def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024): """ Yield chunks from a range in a file. No chunk is bigger than maxread.""" fp.seek(offset) while bytes > 0: @@ -2319,7 +2648,10 @@ def _file_iter_range(fp, offset, bytes, maxread=1024*1024): yield part -def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8'): +def static_file(filename, root, + mimetype='auto', + download=False, + charset='UTF-8'): """ Open a file in a safe way and return :exc:`HTTPResponse` with status code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``, ``Content-Length`` and ``Last-Modified`` headers are set if possible. @@ -2339,7 +2671,7 @@ def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8' mime-type. (default: UTF-8) """ - root = os.path.abspath(root) + os.sep + root = os.path.join(os.path.abspath(root), '') filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) headers = dict() @@ -2351,11 +2683,14 @@ def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8' return HTTPError(403, "You do not have permission to access this file.") if mimetype == 'auto': - mimetype, encoding = mimetypes.guess_type(filename) + if download and download != True: + mimetype, encoding = mimetypes.guess_type(download) + else: + mimetype, encoding = mimetypes.guess_type(filename) if encoding: headers['Content-Encoding'] = encoding if mimetype: - if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype: + if (mimetype[:5] == 'text/' or mimetype == 'application/javascript') and charset and 'charset' not in mimetype: mimetype += '; charset=%s' % charset headers['Content-Type'] = mimetype @@ -2372,7 +2707,8 @@ def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8' if ims: ims = parse_date(ims.split(";")[0].strip()) if ims is not None and ims >= int(stats.st_mtime): - headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) + headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", + time.gmtime()) return HTTPResponse(status=304, **headers) body = '' if request.method == 'HEAD' else open(filename, 'rb') @@ -2384,17 +2720,12 @@ def static_file(filename, root, mimetype='auto', download=False, charset='UTF-8' if not ranges: return HTTPError(416, "Requested Range Not Satisfiable") offset, end = ranges[0] - headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end-1, clen) - headers["Content-Length"] = str(end-offset) - if body: body = _file_iter_range(body, offset, end-offset) + headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) + headers["Content-Length"] = str(end - offset) + if body: body = _file_iter_range(body, offset, end - offset) return HTTPResponse(body, status=206, **headers) return HTTPResponse(body, **headers) - - - - - ############################################################################### # HTTP Utilities and MISC (TODO) ############################################### ############################################################################### @@ -2407,6 +2738,7 @@ def debug(mode=True): if mode: warnings.simplefilter('default') DEBUG = bool(mode) + def http_date(value): if isinstance(value, (datedate, datetime)): value = value.utctimetuple() @@ -2416,24 +2748,27 @@ def http_date(value): value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) return value + def parse_date(ims): """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ try: ts = email.utils.parsedate_tz(ims) - return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone + return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone except (TypeError, ValueError, IndexError, OverflowError): return None + def parse_auth(header): """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" try: method, data = header.split(None, 1) if method.lower() == 'basic': - user, pwd = touni(base64.b64decode(tob(data))).split(':',1) + user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) return user, pwd except (KeyError, ValueError): return None + def parse_range_header(header, maxlen=0): """ Yield (start, end) ranges parsed from a HTTP Range header. Skip unsatisfiable ranges. The end index is non-inclusive.""" @@ -2442,19 +2777,56 @@ def parse_range_header(header, maxlen=0): for start, end in ranges: try: if not start: # bytes=-100 -> last 100 bytes - start, end = max(0, maxlen-int(end)), maxlen + start, end = max(0, maxlen - int(end)), maxlen elif not end: # bytes=100- -> all but the first 99 bytes start, end = int(start), maxlen - else: # bytes=100-200 -> bytes 100-200 (inclusive) - start, end = int(start), min(int(end)+1, maxlen) + else: # bytes=100-200 -> bytes 100-200 (inclusive) + start, end = int(start), min(int(end) + 1, maxlen) if 0 <= start < end <= maxlen: yield start, end except ValueError: pass + +#: Header tokenizer used by _parse_http_header() +_hsplit = re.compile('(?:(?:"((?:[^"\\\\]+|\\\\.)*)")|([^;,=]+))([;,=]?)').findall + +def _parse_http_header(h): + """ Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values + and parameters. For non-standard or broken input, this implementation may return partial results. + :param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``) + :return: List of (value, params) tuples. The second element is a (possibly empty) dict. + """ + values = [] + if '"' not in h: # INFO: Fast path without regexp (~2x faster) + for value in h.split(','): + parts = value.split(';') + values.append((parts[0].strip(), {})) + for attr in parts[1:]: + name, value = attr.split('=', 1) + values[-1][1][name.strip()] = value.strip() + else: + lop, key, attrs = ',', None, {} + for quoted, plain, tok in _hsplit(h): + value = plain.strip() if plain else quoted.replace('\\"', '"') + if lop == ',': + attrs = {} + values.append((value, attrs)) + elif lop == ';': + if tok == '=': + key = value + else: + attrs[value] = '' + elif lop == '=' and key: + attrs[key] = value + key = None + lop = tok + return values + + def _parse_qsl(qs): r = [] - for pair in qs.replace(';','&').split('&'): + for pair in qs.replace(';', '&').split('&'): if not pair: continue nv = pair.split('=', 1) if len(nv) != 2: nv.append('') @@ -2463,25 +2835,30 @@ def _parse_qsl(qs): r.append((key, value)) return r + def _lscmp(a, b): """ Compares two strings in a cryptographically safe way: Runtime is not affected by length of common prefix. """ - return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b) + return not sum(0 if x == y else 1 + for x, y in zip(a, b)) and len(a) == len(b) -def cookie_encode(data, key): +def cookie_encode(data, key, digestmod=None): """ Encode and sign a pickle-able object. Return a (byte) string """ + digestmod = digestmod or hashlib.sha256 msg = base64.b64encode(pickle.dumps(data, -1)) - sig = base64.b64encode(hmac.new(tob(key), msg).digest()) + sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest()) return tob('!') + sig + tob('?') + msg -def cookie_decode(data, key): +def cookie_decode(data, key, digestmod=None): """ Verify and decode an encoded string. Return an object or None.""" data = tob(data) if cookie_is_encoded(data): sig, msg = data.split(tob('?'), 1) - if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())): + digestmod = digestmod or hashlib.sha256 + hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest() + if _lscmp(sig[1:], base64.b64encode(hashed)): return pickle.loads(base64.b64decode(msg)) return None @@ -2493,14 +2870,14 @@ def cookie_is_encoded(data): def html_escape(string): """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ - return string.replace('&','&').replace('<','<').replace('>','>')\ - .replace('"','"').replace("'",''') + 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',' ')\ - .replace('\r',' ').replace('\t',' ') + return '"%s"' % html_escape(string).replace('\n', ' ')\ + .replace('\r', ' ').replace('\t', ' ') def yieldroutes(func): @@ -2513,7 +2890,7 @@ def yieldroutes(func): c(x, y=5) -> '/c/' and '/c//' d(x=5, y=6) -> '/d' and '/d/' and '/d//' """ - path = '/' + func.__name__.replace('__','/').lstrip('/') + path = '/' + func.__name__.replace('__', '/').lstrip('/') spec = getargspec(func) argc = len(spec[0]) - len(spec[3] or []) path += ('/<%s>' * argc) % tuple(spec[0][:argc]) @@ -2557,7 +2934,9 @@ def path_shift(script_name, path_info, shift=1): def auth_basic(check, realm="private", text="Access denied"): """ Callback decorator to require HTTP auth (basic). TODO: Add route(check_auth=...) parameter. """ + def decorator(func): + @functools.wraps(func) def wrapper(*a, **ka): user, password = request.auth or (None, None) @@ -2566,20 +2945,25 @@ def wrapper(*a, **ka): err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) return err return func(*a, **ka) + return wrapper - return decorator + return decorator # Shortcuts for common Bottle methods. # They all refer to the current default application. + def make_default_app_wrapper(name): """ Return a callable that relays calls to the current default app. """ + @functools.wraps(getattr(Bottle, name)) def wrapper(*a, **ka): return getattr(app(), name)(*a, **ka) + return wrapper + route = make_default_app_wrapper('route') get = make_default_app_wrapper('get') post = make_default_app_wrapper('post') @@ -2593,12 +2977,6 @@ def wrapper(*a, **ka): uninstall = make_default_app_wrapper('uninstall') url = make_default_app_wrapper('get_url') - - - - - - ############################################################################### # Server Adapter ############################################################### ############################################################################### @@ -2606,80 +2984,93 @@ def wrapper(*a, **ka): class ServerAdapter(object): quiet = False + def __init__(self, host='127.0.0.1', port=8080, **options): self.options = options self.host = host self.port = int(port) - def run(self, handler): # pragma: no cover + def run(self, handler): # pragma: no cover pass def __repr__(self): - args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()]) + args = ', '.join(['%s=%s' % (k, repr(v)) + for k, v in self.options.items()]) return "%s(%s)" % (self.__class__.__name__, args) class CGIServer(ServerAdapter): quiet = True - def run(self, handler): # pragma: no cover + + def run(self, handler): # pragma: no cover from wsgiref.handlers import CGIHandler + 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 + def run(self, handler): # pragma: no cover import flup.server.fcgi self.options.setdefault('bindAddress', (self.host, self.port)) flup.server.fcgi.WSGIServer(handler, **self.options).run() class WSGIRefServer(ServerAdapter): - def run(self, app): # pragma: no cover - from wsgiref.simple_server import WSGIRequestHandler, WSGIServer + def run(self, app): # pragma: no cover from wsgiref.simple_server import make_server + from wsgiref.simple_server import WSGIRequestHandler, WSGIServer import socket class FixedHandler(WSGIRequestHandler): - def address_string(self): # Prevent reverse DNS lookups please. + def address_string(self): # Prevent reverse DNS lookups please. return self.client_address[0] + def log_request(*args, **kw): if not self.quiet: return WSGIRequestHandler.log_request(*args, **kw) handler_cls = self.options.get('handler_class', FixedHandler) - server_cls = self.options.get('server_class', WSGIServer) + server_cls = self.options.get('server_class', WSGIServer) - if ':' in self.host: # Fix wsgiref for IPv6 addresses. + if ':' in self.host: # Fix wsgiref for IPv6 addresses. if getattr(server_cls, 'address_family') == socket.AF_INET: + class server_cls(server_cls): address_family = socket.AF_INET6 - srv = make_server(self.host, self.port, app, server_cls, handler_cls) - srv.serve_forever() + self.srv = make_server(self.host, self.port, app, server_cls, + handler_cls) + self.port = self.srv.server_port # update port actual port (0 means random) + try: + self.srv.serve_forever() + except KeyboardInterrupt: + self.srv.server_close() # Prevent ResourceWarning: unclosed socket + raise class CherryPyServer(ServerAdapter): - def run(self, handler): # pragma: no cover + def run(self, handler): # pragma: no cover from cherrypy import wsgiserver self.options['bind_addr'] = (self.host, self.port) self.options['wsgi_app'] = handler - + certfile = self.options.get('certfile') if certfile: del self.options['certfile'] keyfile = self.options.get('keyfile') if keyfile: del self.options['keyfile'] - + server = wsgiserver.CherryPyWSGIServer(**self.options) if certfile: server.ssl_certificate = certfile if keyfile: server.ssl_private_key = keyfile - + try: server.start() finally: @@ -2689,16 +3080,17 @@ def run(self, handler): # pragma: no cover class WaitressServer(ServerAdapter): def run(self, handler): from waitress import serve - serve(handler, host=self.host, port=self.port) + serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options) class PasteServer(ServerAdapter): - def run(self, handler): # pragma: no cover + def run(self, handler): # pragma: no cover from paste import httpserver from paste.translogger import TransLogger handler = TransLogger(handler, setup_console_handler=(not self.quiet)) - httpserver.serve(handler, host=self.host, port=str(self.port), - **self.options) + httpserver.serve(handler, + host=self.host, + port=str(self.port), **self.options) class MeinheldServer(ServerAdapter): @@ -2710,7 +3102,8 @@ def run(self, handler): class FapwsServer(ServerAdapter): """ Extremely fast webserver using libev. See http://www.fapws.org/ """ - def run(self, handler): # pragma: no cover + + def run(self, handler): # pragma: no cover import fapws._evwsgi as evwsgi from fapws import base, config port = self.port @@ -2723,26 +3116,30 @@ def run(self, handler): # pragma: no cover _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") _stderr(" (Fapws3 breaks python thread support)\n") evwsgi.set_base_module(base) + def app(environ, start_response): environ['wsgi.multiprocess'] = False return handler(environ, start_response) + evwsgi.wsgi_cb(('', app)) evwsgi.run() class TornadoServer(ServerAdapter): """ The super hyped asynchronous server by facebook. Untested. """ - def run(self, handler): # pragma: no cover + + def run(self, handler): # pragma: no cover import tornado.wsgi, tornado.httpserver, tornado.ioloop container = tornado.wsgi.WSGIContainer(handler) server = tornado.httpserver.HTTPServer(container) - server.listen(port=self.port,address=self.host) + server.listen(port=self.port, address=self.host) tornado.ioloop.IOLoop.instance().start() class AppEngineServer(ServerAdapter): """ Adapter for Google App Engine. """ quiet = True + def run(self, handler): from google.appengine.ext.webapp import util # A main() function in the handler script enables 'App Caching'. @@ -2755,6 +3152,7 @@ def run(self, handler): class TwistedServer(ServerAdapter): """ Untested. """ + def run(self, handler): from twisted.web import server, wsgi from twisted.python.threadpool import ThreadPool @@ -2770,6 +3168,7 @@ def run(self, handler): class DieselServer(ServerAdapter): """ Untested. """ + def run(self, handler): from diesel.protocols.wsgi import WSGIApplication app = WSGIApplication(handler, port=self.port) @@ -2783,6 +3182,7 @@ class GeventServer(ServerAdapter): issues: No streaming, no pipelining, no SSL. * See gevent.wsgi.WSGIServer() documentation for more options. """ + def run(self, handler): from gevent import wsgi, pywsgi, local if not isinstance(threading.local(), local.local): @@ -2799,7 +3199,7 @@ def run(self, handler): class GeventSocketIOServer(ServerAdapter): - def run(self,handler): + def run(self, handler): from socketio import server address = (self.host, self.port) server.SocketIOServer(address, handler, **self.options).serve_forever() @@ -2807,6 +3207,7 @@ def run(self,handler): class GunicornServer(ServerAdapter): """ Untested. See http://gunicorn.org/configure.html for options. """ + def run(self, handler): from gunicorn.app.base import Application @@ -2832,6 +3233,7 @@ class EventletServer(ServerAdapter): * `family`: (default is 2) socket family, optional. See socket documentation for available families. """ + def run(self, handler): from eventlet import wsgi, listen, patcher if not patcher.is_monkey_patched(os): @@ -2854,22 +3256,56 @@ def run(self, handler): class RocketServer(ServerAdapter): """ Untested. """ + def run(self, handler): from rocket import Rocket - server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler }) + server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) server.start() class BjoernServer(ServerAdapter): """ Fast server written in C: https://github.com/jonashaag/bjoern """ + def run(self, handler): from bjoern import run run(handler, self.host, self.port) +class AiohttpServer(ServerAdapter): + """ Untested. + aiohttp + https://pypi.python.org/pypi/aiohttp/ + """ + + def run(self, handler): + import asyncio + from aiohttp.wsgi import WSGIServerHttpProtocol + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + protocol_factory = lambda: WSGIServerHttpProtocol( + handler, + readpayload=True, + debug=(not self.quiet)) + self.loop.run_until_complete(self.loop.create_server(protocol_factory, + self.host, + self.port)) + + if 'BOTTLE_CHILD' in os.environ: + import signal + signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) + + try: + self.loop.run_forever() + except KeyboardInterrupt: + self.loop.stop() + + class AutoServer(ServerAdapter): """ Untested. """ - adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, WSGIRefServer] + adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, + WSGIRefServer] + def run(self, handler): for sa in self.adapters: try: @@ -2877,6 +3313,7 @@ def run(self, handler): except ImportError: pass + server_names = { 'cgi': CGIServer, 'flup': FlupFCGIServer, @@ -2893,17 +3330,13 @@ def run(self, handler): 'gunicorn': GunicornServer, 'eventlet': EventletServer, 'gevent': GeventServer, - 'geventSocketIO':GeventSocketIOServer, + 'geventSocketIO': GeventSocketIOServer, 'rocket': RocketServer, - 'bjoern' : BjoernServer, + 'bjoern': BjoernServer, + 'aiohttp': AiohttpServer, 'auto': AutoServer, } - - - - - ############################################################################### # Application Control ########################################################## ############################################################################### @@ -2933,19 +3366,30 @@ def load_app(target): """ 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 - tmp = default_app.push() # Create a new "default application" + global NORUN + NORUN, nr_old = True, NORUN + tmp = default_app.push() # Create a new "default application" try: - rv = load(target) # Import the target module + 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 + default_app.remove(tmp) # Remove the temporary added default application NORUN = nr_old + _debug = debug -def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, - interval=1, reloader=False, quiet=False, plugins=None, - debug=None, **kargs): + + +def run(app=None, + server='wsgiref', + host='127.0.0.1', + port=8080, + interval=1, + reloader=False, + quiet=False, + plugins=None, + debug=None, + config=None, **kargs): """ Start a server instance. This method blocks until the server terminates. :param app: WSGI application or target string supported by @@ -2964,18 +3408,19 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, """ if NORUN: return if reloader and not os.environ.get('BOTTLE_CHILD'): + import subprocess lockfile = None try: fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') - os.close(fd) # We only need this file to exist. We never write to it + 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! + 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) @@ -3000,6 +3445,9 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, plugin = load(plugin) app.install(plugin) + if config: + app.config.update(config) + if server in server_names: server = server_names.get(server) if isinstance(server, basestring): @@ -3011,8 +3459,10 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, server.quiet = server.quiet or quiet if not server.quiet: - _stderr("Bottle v%s server starting up (using %s)...\n" % (__version__, repr(server))) - _stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) + _stderr("Bottle v%s server starting up (using %s)...\n" % + (__version__, repr(server))) + _stderr("Listening on http://%s:%d/\n" % + (server.host, server.port)) _stderr("Hit Ctrl-C to quit.\n\n") if reloader: @@ -3036,13 +3486,13 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, sys.exit(3) - class FileCheckerThread(threading.Thread): """ Interrupt main-thread as soon as a changed module file is detected, - the lockfile gets deleted or gets to old. """ + the lockfile gets deleted or gets too old. """ def __init__(self, lockfile, interval): threading.Thread.__init__(self) + self.daemon = True self.lockfile, self.interval = lockfile, interval #: Is one of 'reload', 'error' or 'exit' self.status = None @@ -3073,14 +3523,10 @@ def __enter__(self): self.start() def __exit__(self, exc_type, *_): - if not self.status: self.status = 'exit' # silent exit + if not self.status: self.status = 'exit' # silent exit self.join() return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) - - - - ############################################################################### # Template Adapters ############################################################ ############################################################################### @@ -3093,11 +3539,15 @@ def __init__(self, message): class BaseTemplate(object): """ Base class and minimal API for template adapters """ - extensions = ['tpl','html','thtml','stpl'] - settings = {} #used in prepare() - defaults = {} #used in render() - - def __init__(self, source=None, name=None, lookup=None, encoding='utf8', **settings): + extensions = ['tpl', 'html', 'thtml', 'stpl'] + settings = {} #used in prepare() + defaults = {} #used in render() + + def __init__(self, + source=None, + name=None, + lookup=None, + encoding='utf8', **settings): """ Create a new template. If the source parameter (str or buffer) is missing, the name argument is used to guess a template filename. Subclasses can assume that @@ -3113,8 +3563,8 @@ def __init__(self, source=None, name=None, lookup=None, encoding='utf8', **setti self.filename = source.filename if hasattr(source, 'filename') else None self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] self.encoding = encoding - self.settings = self.settings.copy() # Copy from class variable - self.settings.update(settings) # Apply + self.settings = self.settings.copy() # Copy from class variable + self.settings.update(settings) # Apply if not self.source and self.name: self.filename = self.search(self.name, self.lookup) if not self.filename: @@ -3128,12 +3578,11 @@ def search(cls, name, lookup=None): """ Search name in all directories specified in lookup. First without, then with common extensions. Return first hit. """ if not lookup: - depr('The template lookup path list should not be empty.', True) #0.12 - lookup = ['.'] + raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.") - if os.path.isabs(name) and os.path.isfile(name): - depr('Absolute template path names are deprecated.', True) #0.12 - return os.path.abspath(name) + if os.path.isabs(name): + raise depr(0, 12, "Use of absolute path for template name.", + "Refer to templates with names or paths relative to the lookup path.") for spath in lookup: spath = os.path.abspath(spath) + os.sep @@ -3148,7 +3597,7 @@ def search(cls, name, lookup=None): 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 = cls.settings.copy() # Make settings local to class cls.settings[key] = args[0] else: return cls.settings[key] @@ -3174,16 +3623,19 @@ class MakoTemplate(BaseTemplate): def prepare(self, **options): from mako.template import Template from mako.lookup import TemplateLookup - options.update({'input_encoding':self.encoding}) + options.update({'input_encoding': self.encoding}) options.setdefault('format_exceptions', bool(DEBUG)) lookup = TemplateLookup(directories=self.lookup, **options) if self.source: self.tpl = Template(self.source, lookup=lookup, **options) else: - self.tpl = Template(uri=self.name, filename=self.filename, lookup=lookup, **options) + self.tpl = Template(uri=self.name, + filename=self.filename, + lookup=lookup, **options) def render(self, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) + for dictarg in args: + kwargs.update(dictarg) _defaults = self.defaults.copy() _defaults.update(kwargs) return self.tpl.render(**_defaults) @@ -3201,7 +3653,8 @@ def prepare(self, **options): self.tpl = Template(file=self.filename, **options) def render(self, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) + for dictarg in args: + kwargs.update(dictarg) self.context.vars.update(self.defaults) self.context.vars.update(kwargs) out = str(self.tpl) @@ -3222,21 +3675,27 @@ def prepare(self, filters=None, tests=None, globals={}, **kwargs): self.tpl = self.env.get_template(self.filename) def render(self, *args, **kwargs): - for dictarg in args: kwargs.update(dictarg) + for dictarg in args: + kwargs.update(dictarg) _defaults = self.defaults.copy() _defaults.update(kwargs) return self.tpl.render(**_defaults) def loader(self, name): - fname = self.search(name, self.lookup) + if name == self.filename: + fname = name + else: + fname = self.search(name, self.lookup) if not fname: return with open(fname, "rb") as f: return f.read().decode(self.encoding) class SimpleTemplate(BaseTemplate): - - def prepare(self, escape_func=html_escape, noescape=False, syntax=None, **ka): + def prepare(self, + escape_func=html_escape, + noescape=False, + syntax=None, **ka): self.cache = {} enc = self.encoding self._str = lambda x: touni(x, enc) @@ -3258,8 +3717,7 @@ def code(self): try: source, encoding = touni(source), 'utf8' except UnicodeError: - depr('Template encodings other than utf8 are no longer supported.') #0.11 - source, encoding = touni(source, 'latin1'), 'latin1' + raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.') parser = StplParser(source, encoding=encoding, syntax=self.syntax) code = parser.translate() self.encoding = parser.encoding @@ -3272,62 +3730,96 @@ def _include(self, _env, _name=None, **kwargs): env = _env.copy() env.update(kwargs) if _name not in self.cache: - self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax) return self.cache[_name].execute(env['_stdout'], env) def execute(self, _stdout, kwargs): env = self.defaults.copy() env.update(kwargs) - env.update({'_stdout': _stdout, '_printlist': _stdout.extend, + env.update({ + '_stdout': _stdout, + '_printlist': _stdout.extend, 'include': functools.partial(self._include, env), - 'rebase': functools.partial(self._rebase, env), '_rebase': None, - '_str': self._str, '_escape': self._escape, 'get': env.get, - 'setdefault': env.setdefault, 'defined': env.__contains__ }) + 'rebase': functools.partial(self._rebase, env), + '_rebase': None, + '_str': self._str, + '_escape': self._escape, + 'get': env.get, + 'setdefault': env.setdefault, + 'defined': env.__contains__ + }) eval(self.co, env) if env.get('_rebase'): subtpl, rargs = env.pop('_rebase') - rargs['base'] = ''.join(_stdout) #copy stdout - del _stdout[:] # clear stdout + rargs['base'] = ''.join(_stdout) #copy stdout + del _stdout[:] # clear stdout return self._include(env, subtpl, **rargs) return env def render(self, *args, **kwargs): """ Render the template using keyword arguments as local variables. """ - env = {}; stdout = [] - for dictarg in args: env.update(dictarg) + env = {} + stdout = [] + for dictarg in args: + env.update(dictarg) env.update(kwargs) self.execute(stdout, env) return ''.join(stdout) -class StplSyntaxError(TemplateError): pass +class StplSyntaxError(TemplateError): + + pass class StplParser(object): """ Parser for stpl templates. """ - _re_cache = {} #: Cache for compiled re patterns + _re_cache = {} #: Cache for compiled re patterns + # This huge pile of voodoo magic splits python code into 8 different tokens. - # 1: All kinds of python strings (trust me, it works) - _re_tok = '((?m)[urbURB]?(?:\'\'(?!\')|""(?!")|\'{6}|"{6}' \ - '|\'(?:[^\\\\\']|\\\\.)+?\'|"(?:[^\\\\"]|\\\\.)+?"' \ - '|\'{3}(?:[^\\\\]|\\\\.|\\n)+?\'{3}' \ - '|"{3}(?:[^\\\\]|\\\\.|\\n)+?"{3}))' - _re_inl = _re_tok.replace('|\\n','') # We re-use this string pattern later - # 2: Comments (until end of line, but not the newline itself) - _re_tok += '|(#.*)' - # 3,4: Keywords that start or continue a python block (only start of line) - _re_tok += '|^([ \\t]*(?:if|for|while|with|try|def|class)\\b)' \ - '|^([ \\t]*(?:elif|else|except|finally)\\b)' - # 5: Our special 'end' keyword (but only if it stands alone) - _re_tok += '|((?:^|;)[ \\t]*end[ \\t]*(?=(?:%(block_close)s[ \\t]*)?\\r?$|;|#))' - # 6: A customizable end-of-code-block template token (only end of line) - _re_tok += '|(%(block_close)s[ \\t]*(?=$))' - # 7: And finally, a single newline. The 8th token is 'everything else' - _re_tok += '|(\\r?\\n)' + # We use the verbose (?x) regex mode to make this more manageable + + _re_tok = _re_inl = r'''((?mx) # verbose and dot-matches-newline mode + [urbURB]* + (?: ''(?!') + |""(?!") + |'{6} + |"{6} + |'(?:[^\\']|\\.)+?' + |"(?:[^\\"]|\\.)+?" + |'{3}(?:[^\\]|\\.|\n)+?'{3} + |"{3}(?:[^\\]|\\.|\n)+?"{3} + ) + )''' + + _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later + + _re_tok += r''' + # 2: Comments (until end of line, but not the newline itself) + |(\#.*) + + # 3: Open and close (4) grouping tokens + |([\[\{\(]) + |([\]\}\)]) + + # 5,6: Keywords that start or continue a python block (only start of line) + |^([\ \t]*(?:if|for|while|with|try|def|class)\b) + |^([\ \t]*(?:elif|else|except|finally)\b) + + # 7: Our special 'end' keyword (but only if it stands alone) + |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) + + # 8: A customizable end-of-code-block template token (only end of line) + |(%(block_close)s[\ \t]*(?=\r?$)) + + # 9: And finally, a single newline. The 10th token is 'everything else' + |(\r?\n) + ''' + # Match the start tokens of code areas in a template - _re_split = '(?m)^[ \t]*(\\\\?)((%(line_start)s)|(%(block_start)s))' + _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' # Match inline statements (may contain python strings) - _re_inl = '%%(inline_start)s((?:%s|[^\'"\n]*?)+)%%(inline_end)s' % _re_inl + _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl default_syntax = '<% %> % {{ }}' @@ -3337,6 +3829,7 @@ def __init__(self, source, syntax=None, encoding='utf8'): self.code_buffer, self.text_buffer = [], [] self.lineno, self.offset = 1, 0 self.indent, self.indent_mod = 0, 0 + self.paren_depth = 0 def get_syntax(self): """ Tokens as a space separated string (default: <% %> % {{ }}) """ @@ -3350,7 +3843,7 @@ def set_syntax(self, syntax): etokens = map(re.escape, self._tokens) pattern_vars = dict(zip(names.split(), etokens)) patterns = (self._re_split, self._re_tok, self._re_inl) - patterns = [re.compile(p%pattern_vars) for p in patterns] + patterns = [re.compile(p % pattern_vars) for p in patterns] self._re_cache[syntax] = patterns self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] @@ -3359,66 +3852,81 @@ def set_syntax(self, syntax): def translate(self): if self.offset: raise RuntimeError('Parser is a one time instance.') while True: - m = self.re_split.search(self.source[self.offset:]) + m = self.re_split.search(self.source, pos=self.offset) if m: - text = self.source[self.offset:self.offset+m.start()] + text = self.source[self.offset:m.start()] self.text_buffer.append(text) - self.offset += m.end() - if m.group(1): # Escape syntax + self.offset = m.end() + if m.group(1): # Escape syntax line, sep, _ = self.source[self.offset:].partition('\n') - self.text_buffer.append(m.group(2)+line+sep) - self.offset += len(line+sep)+1 + self.text_buffer.append(self.source[m.start():m.start(1)] + + m.group(2) + line + sep) + self.offset += len(line + sep) continue self.flush_text() - self.read_code(multiline=bool(m.group(4))) - else: break + self.offset += self.read_code(self.source[self.offset:], + multiline=bool(m.group(4))) + else: + break self.text_buffer.append(self.source[self.offset:]) self.flush_text() return ''.join(self.code_buffer) - def read_code(self, multiline): + def read_code(self, pysource, multiline): code_line, comment = '', '' + offset = 0 while True: - m = self.re_tok.search(self.source[self.offset:]) + m = self.re_tok.search(pysource, pos=offset) if not m: - code_line += self.source[self.offset:] - self.offset = len(self.source) + code_line += pysource[offset:] + offset = len(pysource) self.write_code(code_line.strip(), comment) - return - code_line += self.source[self.offset:self.offset+m.start()] - self.offset += m.end() - _str, _com, _blk1, _blk2, _end, _cend, _nl = m.groups() - if code_line and (_blk1 or _blk2): # a if b else c + break + code_line += pysource[offset:m.start()] + offset = m.end() + _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() + if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c code_line += _blk1 or _blk2 continue - if _str: # Python string + if _str: # Python string code_line += _str elif _com: # Python comment (up to EOL) comment = _com if multiline and _com.strip().endswith(self._tokens[1]): - multiline = False # Allow end-of-block in comments - elif _blk1: # Start-block keyword (if/for/while/def/try/...) + multiline = False # Allow end-of-block in comments + elif _po: # open parenthesis + self.paren_depth += 1 + code_line += _po + elif _pc: # close parenthesis + if self.paren_depth > 0: + # we could check for matching parentheses here, but it's + # easier to leave that to python - just check counts + self.paren_depth -= 1 + code_line += _pc + elif _blk1: # Start-block keyword (if/for/while/def/try/...) code_line, self.indent_mod = _blk1, -1 self.indent += 1 - elif _blk2: # Continue-block keyword (else/elif/except/...) + elif _blk2: # Continue-block keyword (else/elif/except/...) code_line, self.indent_mod = _blk2, -1 elif _end: # The non-standard 'end'-keyword (ends a block) self.indent -= 1 - elif _cend: # The end-code-block template token (usually '%>') + elif _cend: # The end-code-block template token (usually '%>') if multiline: multiline = False else: code_line += _cend - else: # \n + else: # \n self.write_code(code_line.strip(), comment) self.lineno += 1 code_line, comment, self.indent_mod = '', '', 0 if not multiline: break + return offset + def flush_text(self): text = ''.join(self.text_buffer) del self.text_buffer[:] if not text: return - parts, pos, nl = [], 0, '\\\n'+' '*self.indent + parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent for m in self.re_inl.finditer(text): prefix, pos = text[pos:m.start()], m.end() if prefix: @@ -3432,7 +3940,7 @@ def flush_text(self): elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] parts.append(nl.join(map(repr, lines))) code = '_printlist((%s,))' % ', '.join(parts) - self.lineno += code.count('\n')+1 + self.lineno += code.count('\n') + 1 self.write_code(code) @staticmethod @@ -3441,7 +3949,7 @@ def process_inline(chunk): return '_escape(%s)' % chunk def write_code(self, line, comment=''): - code = ' ' * (self.indent+self.indent_mod) + code = ' ' * (self.indent + self.indent_mod) code += line.lstrip() + comment + '\n' self.code_buffer.append(code) @@ -3454,6 +3962,8 @@ def template(*args, **kwargs): or directly (as keyword arguments). """ tpl = args[0] if args else None + for dictarg in args[1:]: + kwargs.update(dictarg) adapter = kwargs.pop('template_adapter', SimpleTemplate) lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) tplid = (id(lookup), tpl) @@ -3468,11 +3978,12 @@ def template(*args, **kwargs): TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) if not TEMPLATES[tplid]: abort(500, 'Template (%s) not found' % tpl) - for dictarg in args[1:]: kwargs.update(dictarg) return TEMPLATES[tplid].render(kwargs) + mako_template = functools.partial(template, template_adapter=MakoTemplate) -cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) +cheetah_template = functools.partial(template, + template_adapter=CheetahTemplate) jinja2_template = functools.partial(template, template_adapter=Jinja2Template) @@ -3486,7 +3997,9 @@ def view(tpl_name, **defaults): This includes returning a HTTPResponse(dict) to get, for instance, JSON with autojson or other castfilters. """ + def decorator(func): + @functools.wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) @@ -3497,36 +4010,34 @@ def wrapper(*args, **kwargs): elif result is None: return template(tpl_name, defaults) return result + return wrapper + return decorator + mako_view = functools.partial(view, template_adapter=MakoTemplate) cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) jinja2_view = functools.partial(view, template_adapter=Jinja2Template) - - - - - ############################################################################### # Constants and Globals ######################################################## ############################################################################### - TEMPLATE_PATH = ['./', './views/'] TEMPLATES = {} DEBUG = False -NORUN = False # If set, run() does nothing. Used by load_app() +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 = httplib.responses.copy() +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.items()) +_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) + for (k, v) in HTTP_CODES.items()) #: The default template used for error pages. Override with @error() ERROR_PAGE_TEMPLATE = """ @@ -3566,7 +4077,7 @@ def wrapper(*args, **kwargs): #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a #: request callback, this instance always refers to the *current* request -#: (even on a multithreaded server). +#: (even on a multi-threaded server). request = LocalRequest() #: A thread-safe instance of :class:`LocalResponse`. It is used to change the @@ -3576,24 +4087,31 @@ def wrapper(*args, **kwargs): #: A thread-safe namespace. Not used by Bottle. local = threading.local() -# Initialize app stack (create first empty Bottle app) +# Initialize app stack (create first empty Bottle app now deferred until needed) # BC: 0.6.4 and needed for run() -app = default_app = AppStack() -app.push() +apps = app = default_app = AppStack() + #: A virtual package that redirects import statements. #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. -ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else __name__+".ext", 'bottle_%s').module +ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else + __name__ + ".ext", 'bottle_%s').module + + if __name__ == '__main__': - opt, args, parser = _cmd_options, _cmd_args, _cmd_parser + opt, args, parser = _cli_parse(sys.argv) + + def _cli_error(msg): + parser.print_help() + _stderr('\nError: %s\n' % msg) + sys.exit(1) + if opt.version: - _stdout('Bottle %s\n'%__version__) + _stdout('Bottle %s\n' % __version__) sys.exit(0) if not args: - parser.print_help() - _stderr('\nError: No application entry point specified.\n') - sys.exit(1) + _cli_error("No application entry point specified.") sys.path.insert(0, '.') sys.modules.setdefault('bottle', sys.modules['__main__']) @@ -3603,10 +4121,35 @@ def wrapper(*args, **kwargs): host, port = host.rsplit(':', 1) host = host.strip('[]') - run(args[0], host=host, port=int(port), server=opt.server, - reloader=opt.reload, plugins=opt.plugin, debug=opt.debug) - - + config = ConfigDict() + for cfile in opt.conf or []: + try: + if cfile.endswith('.json'): + with open(cfile, 'rb') as fp: + config.load_dict(json_loads(fp.read())) + else: + config.load_config(cfile) + except ConfigParserError: + _cli_error(str(_e())) + except IOError: + _cli_error("Unable to read config file %r" % cfile) + except (UnicodeError, TypeError, ValueError): + _cli_error("Unable to parse config file %r: %s" % (cfile, _e())) + + for cval in opt.param or []: + if '=' in cval: + config.update((cval.split('=', 1),)) + else: + config[cval] = True + + run(args[0], + host=host, + port=int(port), + server=opt.server, + reloader=opt.reload, + plugins=opt.plugin, + debug=opt.debug, + config=config) # THE END diff --git a/tvstreamrecord.py b/tvstreamrecord.py index f1863d6..1bb1357 100644 --- a/tvstreamrecord.py +++ b/tvstreamrecord.py @@ -36,9 +36,11 @@ if sys.version_info[0] == 2: # Python 2.x import urllib as urllib32 + tvcookie = b"tvstreamrecord_user" else: # Python 3.x import urllib.request as urllib32 + tvcookie = "tvstreamrecord_user" from threading import Thread, Timer import os from mylogging import logInit, logRenew, logStop @@ -116,21 +118,22 @@ def server_static5(filename): @post('/login') def postLogin(): global credentials - hash = hashlib.sha224(request.forms.pw).hexdigest() + pw = request.forms.pw.encode("utf-8") + hash = hashlib.sha224(pw).hexdigest() if hash == credentials: config.clearIP(request.remote_addr) else: config.banIP(request.remote_addr) - if not request.forms.store_pw: - response.set_cookie(name=b"tvstreamrecord_user", value=hash) + if not request.forms.store_pw: + response.set_cookie(name=tvcookie, value=hash) else: - response.set_cookie(name=b"tvstreamrecord_user", value=hash, max_age=315360000) + response.set_cookie(name=tvcookie, value=hash, max_age=315360000) redirect("/") @route('/logoff') def postLogout(): - response.delete_cookie(b"tvstreamrecord_user") + response.delete_cookie(tvcookie) if config.checkIP(request.remote_addr) == True: return template('login') @@ -141,7 +144,7 @@ def setPass(): pass_new_1 = hashlib.sha224(request.forms.pass_new_1).hexdigest() if request.forms.pass_new_1 else "" pass_new_2 = hashlib.sha224(request.forms.pass_new_2).hexdigest() if request.forms.pass_new_2 else "" if pass_old == credentials and pass_new_1 == pass_new_2: - response.delete_cookie(b"tvstreamrecord_user") + response.delete_cookie(tvcookie) credentials = config.setUser(pass_new_1) ret = 0 elif pass_old != credentials: @@ -158,7 +161,7 @@ def checkLogin(): localhost = True global credentials if credentials and not localhost: - if credentials != request.get_cookie(b"tvstreamrecord_user"): + if credentials != request.get_cookie(tvcookie): if config.checkIP(request.remote_addr) == True: return template('login') else: