Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sqlmapproject
GitHub Repository: sqlmapproject/sqlmap
Path: blob/master/thirdparty/bottle/bottle.py
3553 views
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
from __future__ import print_function
4
"""
5
Bottle is a fast and simple micro-framework for small web applications. It
6
offers request dispatching (Routes) with URL parameter support, templates,
7
a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
8
template engines - all in a single file and with no dependencies other than the
9
Python Standard Library.
10
11
Homepage and documentation: http://bottlepy.org/
12
13
Copyright (c) 2009-2024, Marcel Hellkamp.
14
License: MIT (see LICENSE for details)
15
"""
16
17
import sys
18
19
__author__ = 'Marcel Hellkamp'
20
__version__ = '0.13.4'
21
__license__ = 'MIT'
22
23
###############################################################################
24
# Command-line interface ######################################################
25
###############################################################################
26
# INFO: Some server adapters need to monkey-patch std-lib modules before they
27
# are imported. This is why some of the command-line handling is done here, but
28
# the actual call to _main() is at the end of the file.
29
30
31
def _cli_parse(args): # pragma: no coverage
32
from argparse import ArgumentParser
33
34
parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app")
35
opt = parser.add_argument
36
opt("--version", action="store_true", help="show version number.")
37
opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
38
opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
39
opt("-p", "--plugin", action="append", help="install additional plugin/s.")
40
opt("-c", "--conf", action="append", metavar="FILE",
41
help="load config values from FILE.")
42
opt("-C", "--param", action="append", metavar="NAME=VALUE",
43
help="override config values.")
44
opt("--debug", action="store_true", help="start server in debug mode.")
45
opt("--reload", action="store_true", help="auto-reload on file changes.")
46
opt('app', help='WSGI app entry point.', nargs='?')
47
48
cli_args = parser.parse_args(args[1:])
49
50
return cli_args, parser
51
52
53
def _cli_patch(cli_args): # pragma: no coverage
54
parsed_args, _ = _cli_parse(cli_args)
55
opts = parsed_args
56
if opts.server:
57
if opts.server.startswith('gevent'):
58
import gevent.monkey
59
gevent.monkey.patch_all()
60
elif opts.server.startswith('eventlet'):
61
import eventlet
62
eventlet.monkey_patch()
63
64
65
if __name__ == '__main__':
66
_cli_patch(sys.argv)
67
68
###############################################################################
69
# Imports and Python 2/3 unification ##########################################
70
###############################################################################
71
72
import base64, calendar, email.utils, functools, hmac, itertools,\
73
mimetypes, os, re, tempfile, threading, time, warnings, weakref, hashlib
74
75
from types import FunctionType
76
from datetime import date as datedate, datetime, timedelta
77
from tempfile import NamedTemporaryFile
78
from traceback import format_exc, print_exc
79
from unicodedata import normalize
80
81
try:
82
from ujson import dumps as json_dumps, loads as json_lds
83
except ImportError:
84
from json import dumps as json_dumps, loads as json_lds
85
86
py = sys.version_info
87
py3k = py.major > 2
88
89
# Lots of stdlib and builtin differences.
90
if py3k:
91
import http.client as httplib
92
import _thread as thread
93
from urllib.parse import urljoin, SplitResult as UrlSplitResult
94
from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
95
urlunquote = functools.partial(urlunquote, encoding='latin1')
96
from http.cookies import SimpleCookie, Morsel, CookieError
97
from collections.abc import MutableMapping as DictMixin
98
from types import ModuleType as new_module
99
import pickle
100
from io import BytesIO
101
import configparser
102
from datetime import timezone
103
UTC = timezone.utc
104
# getfullargspec was deprecated in 3.5 and un-deprecated in 3.6
105
# getargspec was deprecated in 3.0 and removed in 3.11
106
from inspect import getfullargspec
107
def getargspec(func):
108
spec = getfullargspec(func)
109
kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs)
110
return kwargs, spec[1], spec[2], spec[3]
111
112
basestring = str
113
unicode = str
114
json_loads = lambda s: json_lds(touni(s))
115
callable = lambda x: hasattr(x, '__call__')
116
imap = map
117
118
def _raise(*a):
119
raise a[0](a[1]).with_traceback(a[2])
120
else: # 2.x
121
warnings.warn("Python 2 support will be dropped in Bottle 0.14", DeprecationWarning)
122
import httplib
123
import thread
124
from urlparse import urljoin, SplitResult as UrlSplitResult
125
from urllib import urlencode, quote as urlquote, unquote as urlunquote
126
from Cookie import SimpleCookie, Morsel, CookieError
127
from itertools import imap
128
import cPickle as pickle
129
from imp import new_module
130
from StringIO import StringIO as BytesIO
131
import ConfigParser as configparser
132
from collections import MutableMapping as DictMixin
133
from inspect import getargspec
134
from datetime import tzinfo
135
136
class _UTC(tzinfo):
137
def utcoffset(self, dt): return timedelta(0)
138
def tzname(self, dt): return "UTC"
139
def dst(self, dt): return timedelta(0)
140
UTC = _UTC()
141
142
unicode = unicode
143
json_loads = json_lds
144
145
exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec'))
146
147
# Some helpers for string/byte handling
148
def tob(s, enc='utf8'):
149
if isinstance(s, unicode):
150
return s.encode(enc)
151
return b'' if s is None else bytes(s)
152
153
154
def touni(s, enc='utf8', err='strict'):
155
if isinstance(s, bytes):
156
return s.decode(enc, err)
157
return unicode("" if s is None else s)
158
159
160
tonat = touni if py3k else tob
161
162
163
def _stderr(*args):
164
try:
165
print(*args, file=sys.stderr)
166
except (IOError, AttributeError):
167
pass # Some environments do not allow printing (mod_wsgi)
168
169
170
# A bug in functools causes it to break if the wrapper is an instance method
171
def update_wrapper(wrapper, wrapped, *a, **ka):
172
try:
173
functools.update_wrapper(wrapper, wrapped, *a, **ka)
174
except AttributeError:
175
pass
176
177
# These helpers are used at module level and need to be defined first.
178
# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense.
179
180
181
def depr(major, minor, cause, fix, stacklevel=3):
182
text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\
183
"Cause: %s\n"\
184
"Fix: %s\n" % (major, minor, cause, fix)
185
if DEBUG == 'strict':
186
raise DeprecationWarning(text)
187
warnings.warn(text, DeprecationWarning, stacklevel=stacklevel)
188
return DeprecationWarning(text)
189
190
191
def makelist(data): # This is just too handy
192
if isinstance(data, (tuple, list, set, dict)):
193
return list(data)
194
elif data:
195
return [data]
196
else:
197
return []
198
199
200
class DictProperty(object):
201
""" Property that maps to a key in a local dict-like attribute. """
202
203
def __init__(self, attr, key=None, read_only=False):
204
self.attr, self.key, self.read_only = attr, key, read_only
205
206
def __call__(self, func):
207
functools.update_wrapper(self, func, updated=[])
208
self.getter, self.key = func, self.key or func.__name__
209
return self
210
211
def __get__(self, obj, cls):
212
if obj is None: return self
213
key, storage = self.key, getattr(obj, self.attr)
214
if key not in storage: storage[key] = self.getter(obj)
215
return storage[key]
216
217
def __set__(self, obj, value):
218
if self.read_only: raise AttributeError("Read-Only property.")
219
getattr(obj, self.attr)[self.key] = value
220
221
def __delete__(self, obj):
222
if self.read_only: raise AttributeError("Read-Only property.")
223
del getattr(obj, self.attr)[self.key]
224
225
226
class cached_property(object):
227
""" A property that is only computed once per instance and then replaces
228
itself with an ordinary attribute. Deleting the attribute resets the
229
property. """
230
231
def __init__(self, func):
232
update_wrapper(self, func)
233
self.func = func
234
235
def __get__(self, obj, cls):
236
if obj is None: return self
237
value = obj.__dict__[self.func.__name__] = self.func(obj)
238
return value
239
240
241
class lazy_attribute(object):
242
""" A property that caches itself to the class object. """
243
244
def __init__(self, func):
245
functools.update_wrapper(self, func, updated=[])
246
self.getter = func
247
248
def __get__(self, obj, cls):
249
value = self.getter(cls)
250
setattr(cls, self.__name__, value)
251
return value
252
253
254
###############################################################################
255
# Exceptions and Events #######################################################
256
###############################################################################
257
258
259
class BottleException(Exception):
260
""" A base class for exceptions used by bottle. """
261
pass
262
263
###############################################################################
264
# Routing ######################################################################
265
###############################################################################
266
267
268
class RouteError(BottleException):
269
""" This is a base class for all routing related exceptions """
270
271
272
class RouteReset(BottleException):
273
""" If raised by a plugin or request handler, the route is reset and all
274
plugins are re-applied. """
275
276
277
class RouterUnknownModeError(RouteError):
278
279
pass
280
281
282
class RouteSyntaxError(RouteError):
283
""" The route parser found something not supported by this router. """
284
285
286
class RouteBuildError(RouteError):
287
""" The route could not be built. """
288
289
290
def _re_flatten(p):
291
""" Turn all capturing groups in a regular expression pattern into
292
non-capturing groups. """
293
if '(' not in p:
294
return p
295
return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if
296
len(m.group(1)) % 2 else m.group(1) + '(?:', p)
297
298
299
class Router(object):
300
""" A Router is an ordered collection of route->target pairs. It is used to
301
efficiently match WSGI requests against a number of routes and return
302
the first target that satisfies the request. The target may be anything,
303
usually a string, ID or callable object. A route consists of a path-rule
304
and a HTTP method.
305
306
The path-rule is either a static path (e.g. `/contact`) or a dynamic
307
path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax
308
and details on the matching order are described in docs:`routing`.
309
"""
310
311
default_pattern = '[^/]+'
312
default_filter = 're'
313
314
#: The current CPython regexp implementation does not allow more
315
#: than 99 matching groups per regular expression.
316
_MAX_GROUPS_PER_PATTERN = 99
317
318
def __init__(self, strict=False):
319
self.rules = [] # All rules in order
320
self._groups = {} # index of regexes to find them in dyna_routes
321
self.builder = {} # Data structure for the url builder
322
self.static = {} # Search structure for static routes
323
self.dyna_routes = {}
324
self.dyna_regexes = {} # Search structure for dynamic routes
325
#: If true, static routes are no longer checked first.
326
self.strict_order = strict
327
self.filters = {
328
're': lambda conf: (_re_flatten(conf or self.default_pattern),
329
None, None),
330
'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))),
331
'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))),
332
'path': lambda conf: (r'.+?', None, None)
333
}
334
335
def add_filter(self, name, func):
336
""" Add a filter. The provided function is called with the configuration
337
string as parameter and must return a (regexp, to_python, to_url) tuple.
338
The first element is a string, the last two are callables or None. """
339
self.filters[name] = func
340
341
rule_syntax = re.compile('(\\\\*)'
342
'(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'
343
'|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'
344
'(?::((?:\\\\.|[^\\\\>])+)?)?)?>))')
345
346
def _itertokens(self, rule):
347
offset, prefix = 0, ''
348
for match in self.rule_syntax.finditer(rule):
349
prefix += rule[offset:match.start()]
350
g = match.groups()
351
if g[2] is not None:
352
depr(0, 13, "Use of old route syntax.",
353
"Use <name> instead of :name in routes.",
354
stacklevel=4)
355
if len(g[0]) % 2: # Escaped wildcard
356
prefix += match.group(0)[len(g[0]):]
357
offset = match.end()
358
continue
359
if prefix:
360
yield prefix, None, None
361
name, filtr, conf = g[4:7] if g[2] is None else g[1:4]
362
yield name, filtr or 'default', conf or None
363
offset, prefix = match.end(), ''
364
if offset <= len(rule) or prefix:
365
yield prefix + rule[offset:], None, None
366
367
def add(self, rule, method, target, name=None):
368
""" Add a new rule or replace the target for an existing rule. """
369
anons = 0 # Number of anonymous wildcards found
370
keys = [] # Names of keys
371
pattern = '' # Regular expression pattern with named groups
372
filters = [] # Lists of wildcard input filters
373
builder = [] # Data structure for the URL builder
374
is_static = True
375
376
for key, mode, conf in self._itertokens(rule):
377
if mode:
378
is_static = False
379
if mode == 'default': mode = self.default_filter
380
mask, in_filter, out_filter = self.filters[mode](conf)
381
if not key:
382
pattern += '(?:%s)' % mask
383
key = 'anon%d' % anons
384
anons += 1
385
else:
386
pattern += '(?P<%s>%s)' % (key, mask)
387
keys.append(key)
388
if in_filter: filters.append((key, in_filter))
389
builder.append((key, out_filter or str))
390
elif key:
391
pattern += re.escape(key)
392
builder.append((None, key))
393
394
self.builder[rule] = builder
395
if name: self.builder[name] = builder
396
397
if is_static and not self.strict_order:
398
self.static.setdefault(method, {})
399
self.static[method][self.build(rule)] = (target, None)
400
return
401
402
try:
403
re_pattern = re.compile('^(%s)$' % pattern)
404
re_match = re_pattern.match
405
except re.error as e:
406
raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
407
408
if filters:
409
410
def getargs(path):
411
url_args = re_match(path).groupdict()
412
for name, wildcard_filter in filters:
413
try:
414
url_args[name] = wildcard_filter(url_args[name])
415
except ValueError:
416
raise HTTPError(400, 'Path has wrong format.')
417
return url_args
418
elif re_pattern.groupindex:
419
420
def getargs(path):
421
return re_match(path).groupdict()
422
else:
423
getargs = None
424
425
flatpat = _re_flatten(pattern)
426
whole_rule = (rule, flatpat, target, getargs)
427
428
if (flatpat, method) in self._groups:
429
if DEBUG:
430
msg = 'Route <%s %s> overwrites a previously defined route'
431
warnings.warn(msg % (method, rule), RuntimeWarning, stacklevel=3)
432
self.dyna_routes[method][
433
self._groups[flatpat, method]] = whole_rule
434
else:
435
self.dyna_routes.setdefault(method, []).append(whole_rule)
436
self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1
437
438
self._compile(method)
439
440
def _compile(self, method):
441
all_rules = self.dyna_routes[method]
442
comborules = self.dyna_regexes[method] = []
443
maxgroups = self._MAX_GROUPS_PER_PATTERN
444
for x in range(0, len(all_rules), maxgroups):
445
some = all_rules[x:x + maxgroups]
446
combined = (flatpat for (_, flatpat, _, _) in some)
447
combined = '|'.join('(^%s$)' % flatpat for flatpat in combined)
448
combined = re.compile(combined).match
449
rules = [(target, getargs) for (_, _, target, getargs) in some]
450
comborules.append((combined, rules))
451
452
def build(self, _name, *anons, **query):
453
""" Build an URL by filling the wildcards in a rule. """
454
builder = self.builder.get(_name)
455
if not builder:
456
raise RouteBuildError("No route with that name.", _name)
457
try:
458
for i, value in enumerate(anons):
459
query['anon%d' % i] = value
460
url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder])
461
return url if not query else url + '?' + urlencode(query)
462
except KeyError as E:
463
raise RouteBuildError('Missing URL argument: %r' % E.args[0])
464
465
def match(self, environ):
466
""" Return a (target, url_args) tuple or raise HTTPError(400/404/405). """
467
verb = environ['REQUEST_METHOD'].upper()
468
path = environ['PATH_INFO'] or '/'
469
470
methods = ('PROXY', 'HEAD', 'GET', 'ANY') if verb == 'HEAD' else ('PROXY', verb, 'ANY')
471
472
for method in methods:
473
if method in self.static and path in self.static[method]:
474
target, getargs = self.static[method][path]
475
return target, getargs(path) if getargs else {}
476
elif method in self.dyna_regexes:
477
for combined, rules in self.dyna_regexes[method]:
478
match = combined(path)
479
if match:
480
target, getargs = rules[match.lastindex - 1]
481
return target, getargs(path) if getargs else {}
482
483
# No matching route found. Collect alternative methods for 405 response
484
allowed = set([])
485
nocheck = set(methods)
486
for method in set(self.static) - nocheck:
487
if path in self.static[method]:
488
allowed.add(method)
489
for method in set(self.dyna_regexes) - allowed - nocheck:
490
for combined, rules in self.dyna_regexes[method]:
491
match = combined(path)
492
if match:
493
allowed.add(method)
494
if allowed:
495
allow_header = ",".join(sorted(allowed))
496
raise HTTPError(405, "Method not allowed.", Allow=allow_header)
497
498
# No matching route and no alternative method found. We give up
499
raise HTTPError(404, "Not found: " + repr(path))
500
501
502
class Route(object):
503
""" This class wraps a route callback along with route specific metadata and
504
configuration and applies Plugins on demand. It is also responsible for
505
turning an URL path rule into a regular expression usable by the Router.
506
"""
507
508
def __init__(self, app, rule, method, callback,
509
name=None,
510
plugins=None,
511
skiplist=None, **config):
512
#: The application this route is installed to.
513
self.app = app
514
#: The path-rule string (e.g. ``/wiki/<page>``).
515
self.rule = rule
516
#: The HTTP method as a string (e.g. ``GET``).
517
self.method = method
518
#: The original callback with no plugins applied. Useful for introspection.
519
self.callback = callback
520
#: The name of the route (if specified) or ``None``.
521
self.name = name or None
522
#: A list of route-specific plugins (see :meth:`Bottle.route`).
523
self.plugins = plugins or []
524
#: A list of plugins to not apply to this route (see :meth:`Bottle.route`).
525
self.skiplist = skiplist or []
526
#: Additional keyword arguments passed to the :meth:`Bottle.route`
527
#: decorator are stored in this dictionary. Used for route-specific
528
#: plugin configuration and meta-data.
529
self.config = app.config._make_overlay()
530
self.config.load_dict(config)
531
532
@cached_property
533
def call(self):
534
""" The route callback with all plugins applied. This property is
535
created on demand and then cached to speed up subsequent requests."""
536
return self._make_callback()
537
538
def reset(self):
539
""" Forget any cached values. The next time :attr:`call` is accessed,
540
all plugins are re-applied. """
541
self.__dict__.pop('call', None)
542
543
def prepare(self):
544
""" Do all on-demand work immediately (useful for debugging)."""
545
self.call
546
547
def all_plugins(self):
548
""" Yield all Plugins affecting this route. """
549
unique = set()
550
for p in reversed(self.app.plugins + self.plugins):
551
if True in self.skiplist: break
552
name = getattr(p, 'name', False)
553
if name and (name in self.skiplist or name in unique): continue
554
if p in self.skiplist or type(p) in self.skiplist: continue
555
if name: unique.add(name)
556
yield p
557
558
def _make_callback(self):
559
callback = self.callback
560
for plugin in self.all_plugins():
561
try:
562
if hasattr(plugin, 'apply'):
563
callback = plugin.apply(callback, self)
564
else:
565
callback = plugin(callback)
566
except RouteReset: # Try again with changed configuration.
567
return self._make_callback()
568
if callback is not self.callback:
569
update_wrapper(callback, self.callback)
570
return callback
571
572
def get_undecorated_callback(self):
573
""" Return the callback. If the callback is a decorated function, try to
574
recover the original function. """
575
func = self.callback
576
while True:
577
if getattr(func, '__wrapped__', False):
578
func = func.__wrapped__
579
elif getattr(func, '__func__', False):
580
func = func.__func__
581
elif getattr(func, '__closure__', False):
582
cells_values = (cell.cell_contents for cell in func.__closure__)
583
isfunc = lambda x: isinstance(x, FunctionType) or hasattr(x, '__call__')
584
func = next(iter(filter(isfunc, cells_values)), func)
585
else:
586
break
587
return func
588
589
def get_callback_args(self):
590
""" Return a list of argument names the callback (most likely) accepts
591
as keyword arguments. If the callback is a decorated function, try
592
to recover the original function before inspection. """
593
return getargspec(self.get_undecorated_callback())[0]
594
595
def get_config(self, key, default=None):
596
""" Lookup a config field and return its value, first checking the
597
route.config, then route.app.config."""
598
depr(0, 13, "Route.get_config() is deprecated.",
599
"The Route.config property already includes values from the"
600
" application config for missing keys. Access it directly.")
601
return self.config.get(key, default)
602
603
def __repr__(self):
604
cb = self.get_undecorated_callback()
605
return '<%s %s -> %s:%s>' % (
606
self.method, self.rule, cb.__module__, getattr(cb, '__name__', '__call__')
607
)
608
609
###############################################################################
610
# Application Object ###########################################################
611
###############################################################################
612
613
614
class Bottle(object):
615
""" Each Bottle object represents a single, distinct web application and
616
consists of routes, callbacks, plugins, resources and configuration.
617
Instances are callable WSGI applications.
618
619
:param catchall: If true (default), handle all exceptions. Turn off to
620
let debugging middleware handle exceptions.
621
"""
622
623
@lazy_attribute
624
def _global_config(cls):
625
cfg = ConfigDict()
626
cfg.meta_set('catchall', 'validate', bool)
627
return cfg
628
629
def __init__(self, **kwargs):
630
#: A :class:`ConfigDict` for app specific configuration.
631
self.config = self._global_config._make_overlay()
632
self.config._add_change_listener(
633
functools.partial(self.trigger_hook, 'config'))
634
635
self.config.update({
636
"catchall": True
637
})
638
639
if kwargs.get('catchall') is False:
640
depr(0, 13, "Bottle(catchall) keyword argument.",
641
"The 'catchall' setting is now part of the app "
642
"configuration. Fix: `app.config['catchall'] = False`")
643
self.config['catchall'] = False
644
if kwargs.get('autojson') is False:
645
depr(0, 13, "Bottle(autojson) keyword argument.",
646
"The 'autojson' setting is now part of the app "
647
"configuration. Fix: `app.config['json.enable'] = False`")
648
self.config['json.disable'] = True
649
650
self._mounts = []
651
652
#: A :class:`ResourceManager` for application files
653
self.resources = ResourceManager()
654
655
self.routes = [] # List of installed :class:`Route` instances.
656
self.router = Router() # Maps requests to :class:`Route` instances.
657
self.error_handler = {}
658
659
# Core plugins
660
self.plugins = [] # List of installed plugins.
661
self.install(JSONPlugin())
662
self.install(TemplatePlugin())
663
664
#: If true, most exceptions are caught and returned as :exc:`HTTPError`
665
catchall = DictProperty('config', 'catchall')
666
667
__hook_names = 'before_request', 'after_request', 'app_reset', 'config'
668
__hook_reversed = {'after_request'}
669
670
@cached_property
671
def _hooks(self):
672
return dict((name, []) for name in self.__hook_names)
673
674
def add_hook(self, name, func):
675
""" Attach a callback to a hook. Three hooks are currently implemented:
676
677
before_request
678
Executed once before each request. The request context is
679
available, but no routing has happened yet.
680
after_request
681
Executed once after each request regardless of its outcome.
682
app_reset
683
Called whenever :meth:`Bottle.reset` is called.
684
"""
685
if name in self.__hook_reversed:
686
self._hooks[name].insert(0, func)
687
else:
688
self._hooks[name].append(func)
689
690
def remove_hook(self, name, func):
691
""" Remove a callback from a hook. """
692
if name in self._hooks and func in self._hooks[name]:
693
self._hooks[name].remove(func)
694
return True
695
696
def trigger_hook(self, __name, *args, **kwargs):
697
""" Trigger a hook and return a list of results. """
698
return [hook(*args, **kwargs) for hook in self._hooks[__name][:]]
699
700
def hook(self, name):
701
""" Return a decorator that attaches a callback to a hook. See
702
:meth:`add_hook` for details."""
703
704
def decorator(func):
705
self.add_hook(name, func)
706
return func
707
708
return decorator
709
710
def _mount_wsgi(self, prefix, app, **options):
711
segments = [p for p in prefix.split('/') if p]
712
if not segments:
713
raise ValueError('WSGI applications cannot be mounted to "/".')
714
path_depth = len(segments)
715
716
def mountpoint_wrapper():
717
try:
718
request.path_shift(path_depth)
719
rs = HTTPResponse([])
720
721
def start_response(status, headerlist, exc_info=None):
722
if exc_info:
723
_raise(*exc_info)
724
if py3k:
725
# Errors here mean that the mounted WSGI app did not
726
# follow PEP-3333 (which requires latin1) or used a
727
# pre-encoding other than utf8 :/
728
status = status.encode('latin1').decode('utf8')
729
headerlist = [(k, v.encode('latin1').decode('utf8'))
730
for (k, v) in headerlist]
731
rs.status = status
732
for name, value in headerlist:
733
rs.add_header(name, value)
734
return rs.body.append
735
736
body = app(request.environ, start_response)
737
rs.body = itertools.chain(rs.body, body) if rs.body else body
738
return rs
739
finally:
740
request.path_shift(-path_depth)
741
742
options.setdefault('skip', True)
743
options.setdefault('method', 'PROXY')
744
options.setdefault('mountpoint', {'prefix': prefix, 'target': app})
745
options['callback'] = mountpoint_wrapper
746
747
self.route('/%s/<:re:.*>' % '/'.join(segments), **options)
748
if not prefix.endswith('/'):
749
self.route('/' + '/'.join(segments), **options)
750
751
def _mount_app(self, prefix, app, **options):
752
if app in self._mounts or '_mount.app' in app.config:
753
depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.",
754
"Clone application before mounting to a different location.")
755
return self._mount_wsgi(prefix, app, **options)
756
757
if options:
758
depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.",
759
"Do not specify any route options when mounting bottle application.")
760
return self._mount_wsgi(prefix, app, **options)
761
762
if not prefix.endswith("/"):
763
depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.",
764
"Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.")
765
return self._mount_wsgi(prefix, app, **options)
766
767
self._mounts.append(app)
768
app.config['_mount.prefix'] = prefix
769
app.config['_mount.app'] = self
770
for route in app.routes:
771
route.rule = prefix + route.rule.lstrip('/')
772
self.add_route(route)
773
774
def mount(self, prefix, app, **options):
775
""" Mount an application (:class:`Bottle` or plain WSGI) to a specific
776
URL prefix. Example::
777
778
parent_app.mount('/prefix/', child_app)
779
780
:param prefix: path prefix or `mount-point`.
781
:param app: an instance of :class:`Bottle` or a WSGI application.
782
783
Plugins from the parent application are not applied to the routes
784
of the mounted child application. If you need plugins in the child
785
application, install them separately.
786
787
While it is possible to use path wildcards within the prefix path
788
(:class:`Bottle` childs only), it is highly discouraged.
789
790
The prefix path must end with a slash. If you want to access the
791
root of the child application via `/prefix` in addition to
792
`/prefix/`, consider adding a route with a 307 redirect to the
793
parent application.
794
"""
795
796
if not prefix.startswith('/'):
797
raise ValueError("Prefix must start with '/'")
798
799
if isinstance(app, Bottle):
800
return self._mount_app(prefix, app, **options)
801
else:
802
return self._mount_wsgi(prefix, app, **options)
803
804
def merge(self, routes):
805
""" Merge the routes of another :class:`Bottle` application or a list of
806
:class:`Route` objects into this application. The routes keep their
807
'owner', meaning that the :data:`Route.app` attribute is not
808
changed. """
809
if isinstance(routes, Bottle):
810
routes = routes.routes
811
for route in routes:
812
self.add_route(route)
813
814
def install(self, plugin):
815
""" Add a plugin to the list of plugins and prepare it for being
816
applied to all routes of this application. A plugin may be a simple
817
decorator or an object that implements the :class:`Plugin` API.
818
"""
819
if hasattr(plugin, 'setup'): plugin.setup(self)
820
if not callable(plugin) and not hasattr(plugin, 'apply'):
821
raise TypeError("Plugins must be callable or implement .apply()")
822
self.plugins.append(plugin)
823
self.reset()
824
return plugin
825
826
def uninstall(self, plugin):
827
""" Uninstall plugins. Pass an instance to remove a specific plugin, a type
828
object to remove all plugins that match that type, a string to remove
829
all plugins with a matching ``name`` attribute or ``True`` to remove all
830
plugins. Return the list of removed plugins. """
831
removed, remove = [], plugin
832
for i, plugin in list(enumerate(self.plugins))[::-1]:
833
if remove is True or remove is plugin or remove is type(plugin) \
834
or getattr(plugin, 'name', True) == remove:
835
removed.append(plugin)
836
del self.plugins[i]
837
if hasattr(plugin, 'close'): plugin.close()
838
if removed: self.reset()
839
return removed
840
841
def reset(self, route=None):
842
""" Reset all routes (force plugins to be re-applied) and clear all
843
caches. If an ID or route object is given, only that specific route
844
is affected. """
845
if route is None: routes = self.routes
846
elif isinstance(route, Route): routes = [route]
847
else: routes = [self.routes[route]]
848
for route in routes:
849
route.reset()
850
if DEBUG:
851
for route in routes:
852
route.prepare()
853
self.trigger_hook('app_reset')
854
855
def close(self):
856
""" Close the application and all installed plugins. """
857
for plugin in self.plugins:
858
if hasattr(plugin, 'close'): plugin.close()
859
860
def run(self, **kwargs):
861
""" Calls :func:`run` with the same parameters. """
862
run(self, **kwargs)
863
864
def match(self, environ):
865
""" Search for a matching route and return a (:class:`Route`, urlargs)
866
tuple. The second value is a dictionary with parameters extracted
867
from the URL. Raise :exc:`HTTPError` (404/405) on a non-match."""
868
return self.router.match(environ)
869
870
def get_url(self, routename, **kargs):
871
""" Return a string that matches a named route """
872
scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
873
location = self.router.build(routename, **kargs).lstrip('/')
874
return urljoin(urljoin('/', scriptname), location)
875
876
def add_route(self, route):
877
""" Add a route object, but do not change the :data:`Route.app`
878
attribute."""
879
self.routes.append(route)
880
self.router.add(route.rule, route.method, route, name=route.name)
881
if DEBUG: route.prepare()
882
883
def route(self,
884
path=None,
885
method='GET',
886
callback=None,
887
name=None,
888
apply=None,
889
skip=None, **config):
890
""" A decorator to bind a function to a request URL. Example::
891
892
@app.route('/hello/<name>')
893
def hello(name):
894
return 'Hello %s' % name
895
896
The ``<name>`` part is a wildcard. See :class:`Router` for syntax
897
details.
898
899
:param path: Request path or a list of paths to listen to. If no
900
path is specified, it is automatically generated from the
901
signature of the function.
902
:param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of
903
methods to listen to. (default: `GET`)
904
:param callback: An optional shortcut to avoid the decorator
905
syntax. ``route(..., callback=func)`` equals ``route(...)(func)``
906
:param name: The name for this route. (default: None)
907
:param apply: A decorator or plugin or a list of plugins. These are
908
applied to the route callback in addition to installed plugins.
909
:param skip: A list of plugins, plugin classes or names. Matching
910
plugins are not installed to this route. ``True`` skips all.
911
912
Any additional keyword arguments are stored as route-specific
913
configuration and passed to plugins (see :meth:`Plugin.apply`).
914
"""
915
if callable(path): path, callback = None, path
916
plugins = makelist(apply)
917
skiplist = makelist(skip)
918
919
def decorator(callback):
920
if isinstance(callback, basestring): callback = load(callback)
921
for rule in makelist(path) or yieldroutes(callback):
922
for verb in makelist(method):
923
verb = verb.upper()
924
route = Route(self, rule, verb, callback,
925
name=name,
926
plugins=plugins,
927
skiplist=skiplist, **config)
928
self.add_route(route)
929
return callback
930
931
return decorator(callback) if callback else decorator
932
933
def get(self, path=None, method='GET', **options):
934
""" Equals :meth:`route`. """
935
return self.route(path, method, **options)
936
937
def post(self, path=None, method='POST', **options):
938
""" Equals :meth:`route` with a ``POST`` method parameter. """
939
return self.route(path, method, **options)
940
941
def put(self, path=None, method='PUT', **options):
942
""" Equals :meth:`route` with a ``PUT`` method parameter. """
943
return self.route(path, method, **options)
944
945
def delete(self, path=None, method='DELETE', **options):
946
""" Equals :meth:`route` with a ``DELETE`` method parameter. """
947
return self.route(path, method, **options)
948
949
def patch(self, path=None, method='PATCH', **options):
950
""" Equals :meth:`route` with a ``PATCH`` method parameter. """
951
return self.route(path, method, **options)
952
953
def error(self, code=500, callback=None):
954
""" Register an output handler for a HTTP error code. Can
955
be used as a decorator or called directly ::
956
957
def error_handler_500(error):
958
return 'error_handler_500'
959
960
app.error(code=500, callback=error_handler_500)
961
962
@app.error(404)
963
def error_handler_404(error):
964
return 'error_handler_404'
965
966
"""
967
968
def decorator(callback):
969
if isinstance(callback, basestring): callback = load(callback)
970
self.error_handler[int(code)] = callback
971
return callback
972
973
return decorator(callback) if callback else decorator
974
975
def default_error_handler(self, res):
976
return tob(template(ERROR_PAGE_TEMPLATE, e=res, template_settings=dict(name='__ERROR_PAGE_TEMPLATE')))
977
978
def _handle(self, environ):
979
path = environ['bottle.raw_path'] = environ['PATH_INFO']
980
if py3k:
981
environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore')
982
983
environ['bottle.app'] = self
984
request.bind(environ)
985
response.bind()
986
987
try:
988
while True: # Remove in 0.14 together with RouteReset
989
out = None
990
try:
991
self.trigger_hook('before_request')
992
route, args = self.router.match(environ)
993
environ['route.handle'] = route
994
environ['bottle.route'] = route
995
environ['route.url_args'] = args
996
out = route.call(**args)
997
break
998
except HTTPResponse as E:
999
out = E
1000
break
1001
except RouteReset:
1002
depr(0, 13, "RouteReset exception deprecated",
1003
"Call route.call() after route.reset() and "
1004
"return the result.")
1005
route.reset()
1006
continue
1007
finally:
1008
if isinstance(out, HTTPResponse):
1009
out.apply(response)
1010
try:
1011
self.trigger_hook('after_request')
1012
except HTTPResponse as E:
1013
out = E
1014
out.apply(response)
1015
except (KeyboardInterrupt, SystemExit, MemoryError):
1016
raise
1017
except Exception as E:
1018
if not self.catchall: raise
1019
stacktrace = format_exc()
1020
environ['wsgi.errors'].write(stacktrace)
1021
environ['wsgi.errors'].flush()
1022
environ['bottle.exc_info'] = sys.exc_info()
1023
out = HTTPError(500, "Internal Server Error", E, stacktrace)
1024
out.apply(response)
1025
1026
return out
1027
1028
def _cast(self, out, peek=None):
1029
""" Try to convert the parameter into something WSGI compatible and set
1030
correct HTTP headers when possible.
1031
Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
1032
iterable of strings and iterable of unicodes
1033
"""
1034
1035
# Empty output is done here
1036
if not out:
1037
if 'Content-Length' not in response:
1038
response['Content-Length'] = 0
1039
return []
1040
# Join lists of byte or unicode strings. Mixed lists are NOT supported
1041
if isinstance(out, (tuple, list))\
1042
and isinstance(out[0], (bytes, unicode)):
1043
out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
1044
# Encode unicode strings
1045
if isinstance(out, unicode):
1046
out = out.encode(response.charset)
1047
# Byte Strings are just returned
1048
if isinstance(out, bytes):
1049
if 'Content-Length' not in response:
1050
response['Content-Length'] = len(out)
1051
return [out]
1052
# HTTPError or HTTPException (recursive, because they may wrap anything)
1053
# TODO: Handle these explicitly in handle() or make them iterable.
1054
if isinstance(out, HTTPError):
1055
out.apply(response)
1056
out = self.error_handler.get(out.status_code,
1057
self.default_error_handler)(out)
1058
return self._cast(out)
1059
if isinstance(out, HTTPResponse):
1060
out.apply(response)
1061
return self._cast(out.body)
1062
1063
# File-like objects.
1064
if hasattr(out, 'read'):
1065
if 'wsgi.file_wrapper' in request.environ:
1066
return request.environ['wsgi.file_wrapper'](out)
1067
elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
1068
return WSGIFileWrapper(out)
1069
1070
# Handle Iterables. We peek into them to detect their inner type.
1071
try:
1072
iout = iter(out)
1073
first = next(iout)
1074
while not first:
1075
first = next(iout)
1076
except StopIteration:
1077
return self._cast('')
1078
except HTTPResponse as E:
1079
first = E
1080
except (KeyboardInterrupt, SystemExit, MemoryError):
1081
raise
1082
except Exception as error:
1083
if not self.catchall: raise
1084
first = HTTPError(500, 'Unhandled exception', error, format_exc())
1085
1086
# These are the inner types allowed in iterator or generator objects.
1087
if isinstance(first, HTTPResponse):
1088
return self._cast(first)
1089
elif isinstance(first, bytes):
1090
new_iter = itertools.chain([first], iout)
1091
elif isinstance(first, unicode):
1092
encoder = lambda x: x.encode(response.charset)
1093
new_iter = imap(encoder, itertools.chain([first], iout))
1094
else:
1095
msg = 'Unsupported response type: %s' % type(first)
1096
return self._cast(HTTPError(500, msg))
1097
if hasattr(out, 'close'):
1098
new_iter = _closeiter(new_iter, out.close)
1099
return new_iter
1100
1101
def wsgi(self, environ, start_response):
1102
""" The bottle WSGI-interface. """
1103
try:
1104
out = self._cast(self._handle(environ))
1105
# rfc2616 section 4.3
1106
if response._status_code in (100, 101, 204, 304)\
1107
or environ['REQUEST_METHOD'] == 'HEAD':
1108
if hasattr(out, 'close'): out.close()
1109
out = []
1110
exc_info = environ.get('bottle.exc_info')
1111
if exc_info is not None:
1112
del environ['bottle.exc_info']
1113
start_response(response._wsgi_status_line(), response.headerlist, exc_info)
1114
return out
1115
except (KeyboardInterrupt, SystemExit, MemoryError):
1116
raise
1117
except Exception as E:
1118
if not self.catchall: raise
1119
err = '<h1>Critical error while processing request: %s</h1>' \
1120
% html_escape(environ.get('PATH_INFO', '/'))
1121
if DEBUG:
1122
err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
1123
'<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
1124
% (html_escape(repr(E)), html_escape(format_exc()))
1125
environ['wsgi.errors'].write(err)
1126
environ['wsgi.errors'].flush()
1127
headers = [('Content-Type', 'text/html; charset=UTF-8')]
1128
start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
1129
return [tob(err)]
1130
1131
def __call__(self, environ, start_response):
1132
""" Each instance of :class:'Bottle' is a WSGI application. """
1133
return self.wsgi(environ, start_response)
1134
1135
def __enter__(self):
1136
""" Use this application as default for all module-level shortcuts. """
1137
default_app.push(self)
1138
return self
1139
1140
def __exit__(self, exc_type, exc_value, traceback):
1141
default_app.pop()
1142
1143
def __setattr__(self, name, value):
1144
if name in self.__dict__:
1145
raise AttributeError("Attribute %s already defined. Plugin conflict?" % name)
1146
object.__setattr__(self, name, value)
1147
1148
###############################################################################
1149
# HTTP and WSGI Tools ##########################################################
1150
###############################################################################
1151
1152
1153
class BaseRequest(object):
1154
""" A wrapper for WSGI environment dictionaries that adds a lot of
1155
convenient access methods and properties. Most of them are read-only.
1156
1157
Adding new attributes to a request actually adds them to the environ
1158
dictionary (as 'bottle.request.ext.<name>'). This is the recommended
1159
way to store and access request-specific data.
1160
"""
1161
1162
__slots__ = ('environ', )
1163
1164
#: Maximum size of memory buffer for :attr:`body` in bytes.
1165
MEMFILE_MAX = 102400
1166
1167
def __init__(self, environ=None):
1168
""" Wrap a WSGI environ dictionary. """
1169
#: The wrapped WSGI environ dictionary. This is the only real attribute.
1170
#: All other attributes actually are read-only properties.
1171
self.environ = {} if environ is None else environ
1172
self.environ['bottle.request'] = self
1173
1174
@DictProperty('environ', 'bottle.app', read_only=True)
1175
def app(self):
1176
""" Bottle application handling this request. """
1177
raise RuntimeError('This request is not connected to an application.')
1178
1179
@DictProperty('environ', 'bottle.route', read_only=True)
1180
def route(self):
1181
""" The bottle :class:`Route` object that matches this request. """
1182
raise RuntimeError('This request is not connected to a route.')
1183
1184
@DictProperty('environ', 'route.url_args', read_only=True)
1185
def url_args(self):
1186
""" The arguments extracted from the URL. """
1187
raise RuntimeError('This request is not connected to a route.')
1188
1189
@property
1190
def path(self):
1191
""" The value of ``PATH_INFO`` with exactly one prefixed slash (to fix
1192
broken clients and avoid the "empty path" edge case). """
1193
return '/' + self.environ.get('PATH_INFO', '').lstrip('/')
1194
1195
@property
1196
def method(self):
1197
""" The ``REQUEST_METHOD`` value as an uppercase string. """
1198
return self.environ.get('REQUEST_METHOD', 'GET').upper()
1199
1200
@DictProperty('environ', 'bottle.request.headers', read_only=True)
1201
def headers(self):
1202
""" A :class:`WSGIHeaderDict` that provides case-insensitive access to
1203
HTTP request headers. """
1204
return WSGIHeaderDict(self.environ)
1205
1206
def get_header(self, name, default=None):
1207
""" Return the value of a request header, or a given default value. """
1208
return self.headers.get(name, default)
1209
1210
@DictProperty('environ', 'bottle.request.cookies', read_only=True)
1211
def cookies(self):
1212
""" Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT
1213
decoded. Use :meth:`get_cookie` if you expect signed cookies. """
1214
cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values()
1215
return FormsDict((c.key, c.value) for c in cookies)
1216
1217
def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
1218
""" Return the content of a cookie. To read a `Signed Cookie`, the
1219
`secret` must match the one used to create the cookie (see
1220
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
1221
cookie or wrong signature), return a default value. """
1222
value = self.cookies.get(key)
1223
if secret:
1224
# See BaseResponse.set_cookie for details on signed cookies.
1225
if value and value.startswith('!') and '?' in value:
1226
sig, msg = map(tob, value[1:].split('?', 1))
1227
hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
1228
if _lscmp(sig, base64.b64encode(hash)):
1229
dst = pickle.loads(base64.b64decode(msg))
1230
if dst and dst[0] == key:
1231
return dst[1]
1232
return default
1233
return value or default
1234
1235
@DictProperty('environ', 'bottle.request.query', read_only=True)
1236
def query(self):
1237
""" The :attr:`query_string` parsed into a :class:`FormsDict`. These
1238
values are sometimes called "URL arguments" or "GET parameters", but
1239
not to be confused with "URL wildcards" as they are provided by the
1240
:class:`Router`. """
1241
get = self.environ['bottle.get'] = FormsDict()
1242
pairs = _parse_qsl(self.environ.get('QUERY_STRING', ''))
1243
for key, value in pairs:
1244
get[key] = value
1245
return get
1246
1247
@DictProperty('environ', 'bottle.request.forms', read_only=True)
1248
def forms(self):
1249
""" Form values parsed from an `url-encoded` or `multipart/form-data`
1250
encoded POST or PUT request body. The result is returned as a
1251
:class:`FormsDict`. All keys and values are strings. File uploads
1252
are stored separately in :attr:`files`. """
1253
forms = FormsDict()
1254
forms.recode_unicode = self.POST.recode_unicode
1255
for name, item in self.POST.allitems():
1256
if not isinstance(item, FileUpload):
1257
forms[name] = item
1258
return forms
1259
1260
@DictProperty('environ', 'bottle.request.params', read_only=True)
1261
def params(self):
1262
""" A :class:`FormsDict` with the combined values of :attr:`query` and
1263
:attr:`forms`. File uploads are stored in :attr:`files`. """
1264
params = FormsDict()
1265
for key, value in self.query.allitems():
1266
params[key] = value
1267
for key, value in self.forms.allitems():
1268
params[key] = value
1269
return params
1270
1271
@DictProperty('environ', 'bottle.request.files', read_only=True)
1272
def files(self):
1273
""" File uploads parsed from `multipart/form-data` encoded POST or PUT
1274
request body. The values are instances of :class:`FileUpload`.
1275
1276
"""
1277
files = FormsDict()
1278
files.recode_unicode = self.POST.recode_unicode
1279
for name, item in self.POST.allitems():
1280
if isinstance(item, FileUpload):
1281
files[name] = item
1282
return files
1283
1284
@DictProperty('environ', 'bottle.request.json', read_only=True)
1285
def json(self):
1286
""" If the ``Content-Type`` header is ``application/json`` or
1287
``application/json-rpc``, this property holds the parsed content
1288
of the request body. Only requests smaller than :attr:`MEMFILE_MAX`
1289
are processed to avoid memory exhaustion.
1290
Invalid JSON raises a 400 error response.
1291
"""
1292
ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0]
1293
if ctype in ('application/json', 'application/json-rpc'):
1294
b = self._get_body_string(self.MEMFILE_MAX)
1295
if not b:
1296
return None
1297
try:
1298
return json_loads(b)
1299
except (ValueError, TypeError):
1300
raise HTTPError(400, 'Invalid JSON')
1301
return None
1302
1303
def _iter_body(self, read, bufsize):
1304
maxread = max(0, self.content_length)
1305
while maxread:
1306
part = read(min(maxread, bufsize))
1307
if not part: break
1308
yield part
1309
maxread -= len(part)
1310
1311
@staticmethod
1312
def _iter_chunked(read, bufsize):
1313
err = HTTPError(400, 'Error while parsing chunked transfer body.')
1314
rn, sem, bs = tob('\r\n'), tob(';'), tob('')
1315
while True:
1316
header = read(1)
1317
while header[-2:] != rn:
1318
c = read(1)
1319
header += c
1320
if not c: raise err
1321
if len(header) > bufsize: raise err
1322
size, _, _ = header.partition(sem)
1323
try:
1324
maxread = int(tonat(size.strip()), 16)
1325
except ValueError:
1326
raise err
1327
if maxread == 0: break
1328
buff = bs
1329
while maxread > 0:
1330
if not buff:
1331
buff = read(min(maxread, bufsize))
1332
part, buff = buff[:maxread], buff[maxread:]
1333
if not part: raise err
1334
yield part
1335
maxread -= len(part)
1336
if read(2) != rn:
1337
raise err
1338
1339
@DictProperty('environ', 'bottle.request.body', read_only=True)
1340
def _body(self):
1341
try:
1342
read_func = self.environ['wsgi.input'].read
1343
except KeyError:
1344
self.environ['wsgi.input'] = BytesIO()
1345
return self.environ['wsgi.input']
1346
body_iter = self._iter_chunked if self.chunked else self._iter_body
1347
body, body_size, is_temp_file = BytesIO(), 0, False
1348
for part in body_iter(read_func, self.MEMFILE_MAX):
1349
body.write(part)
1350
body_size += len(part)
1351
if not is_temp_file and body_size > self.MEMFILE_MAX:
1352
body, tmp = NamedTemporaryFile(mode='w+b'), body
1353
body.write(tmp.getvalue())
1354
del tmp
1355
is_temp_file = True
1356
self.environ['wsgi.input'] = body
1357
body.seek(0)
1358
return body
1359
1360
def _get_body_string(self, maxread):
1361
""" Read body into a string. Raise HTTPError(413) on requests that are
1362
too large. """
1363
if self.content_length > maxread:
1364
raise HTTPError(413, 'Request entity too large')
1365
data = self.body.read(maxread + 1)
1366
if len(data) > maxread:
1367
raise HTTPError(413, 'Request entity too large')
1368
return data
1369
1370
@property
1371
def body(self):
1372
""" The HTTP request body as a seek-able file-like object. Depending on
1373
:attr:`MEMFILE_MAX`, this is either a temporary file or a
1374
:class:`io.BytesIO` instance. Accessing this property for the first
1375
time reads and replaces the ``wsgi.input`` environ variable.
1376
Subsequent accesses just do a `seek(0)` on the file object. """
1377
self._body.seek(0)
1378
return self._body
1379
1380
@property
1381
def chunked(self):
1382
""" True if Chunked transfer encoding was. """
1383
return 'chunked' in self.environ.get(
1384
'HTTP_TRANSFER_ENCODING', '').lower()
1385
1386
#: An alias for :attr:`query`.
1387
GET = query
1388
1389
@DictProperty('environ', 'bottle.request.post', read_only=True)
1390
def POST(self):
1391
""" The values of :attr:`forms` and :attr:`files` combined into a single
1392
:class:`FormsDict`. Values are either strings (form values) or
1393
instances of :class:`FileUpload`.
1394
"""
1395
post = FormsDict()
1396
content_type = self.environ.get('CONTENT_TYPE', '')
1397
content_type, options = _parse_http_header(content_type)[0]
1398
# We default to application/x-www-form-urlencoded for everything that
1399
# is not multipart and take the fast path (also: 3.1 workaround)
1400
if not content_type.startswith('multipart/'):
1401
body = tonat(self._get_body_string(self.MEMFILE_MAX), 'latin1')
1402
for key, value in _parse_qsl(body):
1403
post[key] = value
1404
return post
1405
1406
post.recode_unicode = False
1407
charset = options.get("charset", "utf8")
1408
boundary = options.get("boundary")
1409
if not boundary:
1410
raise MultipartError("Invalid content type header, missing boundary")
1411
parser = _MultipartParser(self.body, boundary, self.content_length,
1412
mem_limit=self.MEMFILE_MAX, memfile_limit=self.MEMFILE_MAX,
1413
charset=charset)
1414
1415
for part in parser.parse():
1416
if not part.filename and part.is_buffered():
1417
post[part.name] = tonat(part.value, 'utf8')
1418
else:
1419
post[part.name] = FileUpload(part.file, part.name,
1420
part.filename, part.headerlist)
1421
1422
return post
1423
1424
@property
1425
def url(self):
1426
""" The full request URI including hostname and scheme. If your app
1427
lives behind a reverse proxy or load balancer and you get confusing
1428
results, make sure that the ``X-Forwarded-Host`` header is set
1429
correctly. """
1430
return self.urlparts.geturl()
1431
1432
@DictProperty('environ', 'bottle.request.urlparts', read_only=True)
1433
def urlparts(self):
1434
""" The :attr:`url` string as an :class:`urlparse.SplitResult` tuple.
1435
The tuple contains (scheme, host, path, query_string and fragment),
1436
but the fragment is always empty because it is not visible to the
1437
server. """
1438
env = self.environ
1439
http = env.get('HTTP_X_FORWARDED_PROTO') \
1440
or env.get('wsgi.url_scheme', 'http')
1441
host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST')
1442
if not host:
1443
# HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients.
1444
host = env.get('SERVER_NAME', '127.0.0.1')
1445
port = env.get('SERVER_PORT')
1446
if port and port != ('80' if http == 'http' else '443'):
1447
host += ':' + port
1448
path = urlquote(self.fullpath)
1449
return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '')
1450
1451
@property
1452
def fullpath(self):
1453
""" Request path including :attr:`script_name` (if present). """
1454
return urljoin(self.script_name, self.path.lstrip('/'))
1455
1456
@property
1457
def query_string(self):
1458
""" The raw :attr:`query` part of the URL (everything in between ``?``
1459
and ``#``) as a string. """
1460
return self.environ.get('QUERY_STRING', '')
1461
1462
@property
1463
def script_name(self):
1464
""" The initial portion of the URL's `path` that was removed by a higher
1465
level (server or routing middleware) before the application was
1466
called. This script path is returned with leading and tailing
1467
slashes. """
1468
script_name = self.environ.get('SCRIPT_NAME', '').strip('/')
1469
return '/' + script_name + '/' if script_name else '/'
1470
1471
def path_shift(self, shift=1):
1472
""" Shift path segments from :attr:`path` to :attr:`script_name` and
1473
vice versa.
1474
1475
:param shift: The number of path segments to shift. May be negative
1476
to change the shift direction. (default: 1)
1477
"""
1478
script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift)
1479
self['SCRIPT_NAME'], self['PATH_INFO'] = script, path
1480
1481
@property
1482
def content_length(self):
1483
""" The request body length as an integer. The client is responsible to
1484
set this header. Otherwise, the real length of the body is unknown
1485
and -1 is returned. In this case, :attr:`body` will be empty. """
1486
return int(self.environ.get('CONTENT_LENGTH') or -1)
1487
1488
@property
1489
def content_type(self):
1490
""" The Content-Type header as a lowercase-string (default: empty). """
1491
return self.environ.get('CONTENT_TYPE', '').lower()
1492
1493
@property
1494
def is_xhr(self):
1495
""" True if the request was triggered by a XMLHttpRequest. This only
1496
works with JavaScript libraries that support the `X-Requested-With`
1497
header (most of the popular libraries do). """
1498
requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '')
1499
return requested_with.lower() == 'xmlhttprequest'
1500
1501
@property
1502
def is_ajax(self):
1503
""" Alias for :attr:`is_xhr`. "Ajax" is not the right term. """
1504
return self.is_xhr
1505
1506
@property
1507
def auth(self):
1508
""" HTTP authentication data as a (user, password) tuple. This
1509
implementation currently supports basic (not digest) authentication
1510
only. If the authentication happened at a higher level (e.g. in the
1511
front web-server or a middleware), the password field is None, but
1512
the user field is looked up from the ``REMOTE_USER`` environ
1513
variable. On any errors, None is returned. """
1514
basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', ''))
1515
if basic: return basic
1516
ruser = self.environ.get('REMOTE_USER')
1517
if ruser: return (ruser, None)
1518
return None
1519
1520
@property
1521
def remote_route(self):
1522
""" A list of all IPs that were involved in this request, starting with
1523
the client IP and followed by zero or more proxies. This does only
1524
work if all proxies support the ```X-Forwarded-For`` header. Note
1525
that this information can be forged by malicious clients. """
1526
proxy = self.environ.get('HTTP_X_FORWARDED_FOR')
1527
if proxy: return [ip.strip() for ip in proxy.split(',')]
1528
remote = self.environ.get('REMOTE_ADDR')
1529
return [remote] if remote else []
1530
1531
@property
1532
def remote_addr(self):
1533
""" The client IP as a string. Note that this information can be forged
1534
by malicious clients. """
1535
route = self.remote_route
1536
return route[0] if route else None
1537
1538
def copy(self):
1539
""" Return a new :class:`Request` with a shallow :attr:`environ` copy. """
1540
return Request(self.environ.copy())
1541
1542
def get(self, value, default=None):
1543
return self.environ.get(value, default)
1544
1545
def __getitem__(self, key):
1546
return self.environ[key]
1547
1548
def __delitem__(self, key):
1549
self[key] = ""
1550
del (self.environ[key])
1551
1552
def __iter__(self):
1553
return iter(self.environ)
1554
1555
def __len__(self):
1556
return len(self.environ)
1557
1558
def keys(self):
1559
return self.environ.keys()
1560
1561
def __setitem__(self, key, value):
1562
""" Change an environ value and clear all caches that depend on it. """
1563
1564
if self.environ.get('bottle.request.readonly'):
1565
raise KeyError('The environ dictionary is read-only.')
1566
1567
self.environ[key] = value
1568
todelete = ()
1569
1570
if key == 'wsgi.input':
1571
todelete = ('body', 'forms', 'files', 'params', 'post', 'json')
1572
elif key == 'QUERY_STRING':
1573
todelete = ('query', 'params')
1574
elif key.startswith('HTTP_'):
1575
todelete = ('headers', 'cookies')
1576
1577
for key in todelete:
1578
self.environ.pop('bottle.request.' + key, None)
1579
1580
def __repr__(self):
1581
return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)
1582
1583
def __getattr__(self, name):
1584
""" Search in self.environ for additional user defined attributes. """
1585
try:
1586
var = self.environ['bottle.request.ext.%s' % name]
1587
return var.__get__(self) if hasattr(var, '__get__') else var
1588
except KeyError:
1589
raise AttributeError('Attribute %r not defined.' % name)
1590
1591
def __setattr__(self, name, value):
1592
""" Define new attributes that are local to the bound request environment. """
1593
if name == 'environ': return object.__setattr__(self, name, value)
1594
key = 'bottle.request.ext.%s' % name
1595
if hasattr(self, name):
1596
raise AttributeError("Attribute already defined: %s" % name)
1597
self.environ[key] = value
1598
1599
def __delattr__(self, name):
1600
try:
1601
del self.environ['bottle.request.ext.%s' % name]
1602
except KeyError:
1603
raise AttributeError("Attribute not defined: %s" % name)
1604
1605
1606
def _hkey(key):
1607
if '\n' in key or '\r' in key or '\0' in key:
1608
raise ValueError("Header names must not contain control characters: %r" % key)
1609
return key.title().replace('_', '-')
1610
1611
1612
def _hval(value):
1613
value = tonat(value)
1614
if '\n' in value or '\r' in value or '\0' in value:
1615
raise ValueError("Header value must not contain control characters: %r" % value)
1616
return value
1617
1618
1619
class HeaderProperty(object):
1620
def __init__(self, name, reader=None, writer=None, default=''):
1621
self.name, self.default = name, default
1622
self.reader, self.writer = reader, writer
1623
self.__doc__ = 'Current value of the %r header.' % name.title()
1624
1625
def __get__(self, obj, _):
1626
if obj is None: return self
1627
value = obj.get_header(self.name, self.default)
1628
return self.reader(value) if self.reader else value
1629
1630
def __set__(self, obj, value):
1631
obj[self.name] = self.writer(value) if self.writer else value
1632
1633
def __delete__(self, obj):
1634
del obj[self.name]
1635
1636
1637
class BaseResponse(object):
1638
""" Storage class for a response body as well as headers and cookies.
1639
1640
This class does support dict-like case-insensitive item-access to
1641
headers, but is NOT a dict. Most notably, iterating over a response
1642
yields parts of the body and not the headers.
1643
"""
1644
1645
default_status = 200
1646
default_content_type = 'text/html; charset=UTF-8'
1647
1648
# Header denylist for specific response codes
1649
# (rfc2616 section 10.2.3 and 10.3.5)
1650
bad_headers = {
1651
204: frozenset(('Content-Type', 'Content-Length')),
1652
304: frozenset(('Allow', 'Content-Encoding', 'Content-Language',
1653
'Content-Length', 'Content-Range', 'Content-Type',
1654
'Content-Md5', 'Last-Modified'))
1655
}
1656
1657
def __init__(self, body='', status=None, headers=None, **more_headers):
1658
""" Create a new response object.
1659
1660
:param body: The response body as one of the supported types.
1661
:param status: Either an HTTP status code (e.g. 200) or a status line
1662
including the reason phrase (e.g. '200 OK').
1663
:param headers: A dictionary or a list of name-value pairs.
1664
1665
Additional keyword arguments are added to the list of headers.
1666
Underscores in the header name are replaced with dashes.
1667
"""
1668
self._cookies = None
1669
self._headers = {}
1670
self.body = body
1671
self.status = status or self.default_status
1672
if headers:
1673
if isinstance(headers, dict):
1674
headers = headers.items()
1675
for name, value in headers:
1676
self.add_header(name, value)
1677
if more_headers:
1678
for name, value in more_headers.items():
1679
self.add_header(name, value)
1680
1681
def copy(self, cls=None):
1682
""" Returns a copy of self. """
1683
cls = cls or BaseResponse
1684
assert issubclass(cls, BaseResponse)
1685
copy = cls()
1686
copy.status = self.status
1687
copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
1688
if self._cookies:
1689
cookies = copy._cookies = SimpleCookie()
1690
for k,v in self._cookies.items():
1691
cookies[k] = v.value
1692
cookies[k].update(v) # also copy cookie attributes
1693
return copy
1694
1695
def __iter__(self):
1696
return iter(self.body)
1697
1698
def close(self):
1699
if hasattr(self.body, 'close'):
1700
self.body.close()
1701
1702
@property
1703
def status_line(self):
1704
""" The HTTP status line as a string (e.g. ``404 Not Found``)."""
1705
return self._status_line
1706
1707
@property
1708
def status_code(self):
1709
""" The HTTP status code as an integer (e.g. 404)."""
1710
return self._status_code
1711
1712
def _set_status(self, status):
1713
if isinstance(status, int):
1714
code, status = status, _HTTP_STATUS_LINES.get(status)
1715
elif ' ' in status:
1716
if '\n' in status or '\r' in status or '\0' in status:
1717
raise ValueError('Status line must not include control chars.')
1718
status = status.strip()
1719
code = int(status.split()[0])
1720
else:
1721
raise ValueError('String status line without a reason phrase.')
1722
if not 100 <= code <= 999:
1723
raise ValueError('Status code out of range.')
1724
self._status_code = code
1725
self._status_line = str(status or ('%d Unknown' % code))
1726
1727
def _get_status(self):
1728
return self._status_line
1729
1730
status = property(
1731
_get_status, _set_status, None,
1732
''' A writeable property to change the HTTP response status. It accepts
1733
either a numeric code (100-999) or a string with a custom reason
1734
phrase (e.g. "404 Brain not found"). Both :data:`status_line` and
1735
:data:`status_code` are updated accordingly. The return value is
1736
always a status string. ''')
1737
del _get_status, _set_status
1738
1739
@property
1740
def headers(self):
1741
""" An instance of :class:`HeaderDict`, a case-insensitive dict-like
1742
view on the response headers. """
1743
hdict = HeaderDict()
1744
hdict.dict = self._headers
1745
return hdict
1746
1747
def __contains__(self, name):
1748
return _hkey(name) in self._headers
1749
1750
def __delitem__(self, name):
1751
del self._headers[_hkey(name)]
1752
1753
def __getitem__(self, name):
1754
return self._headers[_hkey(name)][-1]
1755
1756
def __setitem__(self, name, value):
1757
self._headers[_hkey(name)] = [_hval(value)]
1758
1759
def get_header(self, name, default=None):
1760
""" Return the value of a previously defined header. If there is no
1761
header with that name, return a default value. """
1762
return self._headers.get(_hkey(name), [default])[-1]
1763
1764
def set_header(self, name, value):
1765
""" Create a new response header, replacing any previously defined
1766
headers with the same name. """
1767
self._headers[_hkey(name)] = [_hval(value)]
1768
1769
def add_header(self, name, value):
1770
""" Add an additional response header, not removing duplicates. """
1771
self._headers.setdefault(_hkey(name), []).append(_hval(value))
1772
1773
def iter_headers(self):
1774
""" Yield (header, value) tuples, skipping headers that are not
1775
allowed with the current response status code. """
1776
return self.headerlist
1777
1778
def _wsgi_status_line(self):
1779
""" WSGI conform status line (latin1-encodeable) """
1780
if py3k:
1781
return self._status_line.encode('utf8').decode('latin1')
1782
return self._status_line
1783
1784
@property
1785
def headerlist(self):
1786
""" WSGI conform list of (header, value) tuples. """
1787
out = []
1788
headers = list(self._headers.items())
1789
if 'Content-Type' not in self._headers:
1790
headers.append(('Content-Type', [self.default_content_type]))
1791
if self._status_code in self.bad_headers:
1792
bad_headers = self.bad_headers[self._status_code]
1793
headers = [h for h in headers if h[0] not in bad_headers]
1794
out += [(name, val) for (name, vals) in headers for val in vals]
1795
if self._cookies:
1796
for c in self._cookies.values():
1797
out.append(('Set-Cookie', _hval(c.OutputString())))
1798
if py3k:
1799
out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out]
1800
return out
1801
1802
content_type = HeaderProperty('Content-Type')
1803
content_length = HeaderProperty('Content-Length', reader=int, default=-1)
1804
expires = HeaderProperty(
1805
'Expires',
1806
reader=lambda x: datetime.fromtimestamp(parse_date(x), UTC),
1807
writer=lambda x: http_date(x))
1808
1809
@property
1810
def charset(self, default='UTF-8'):
1811
""" Return the charset specified in the content-type header (default: utf8). """
1812
if 'charset=' in self.content_type:
1813
return self.content_type.split('charset=')[-1].split(';')[0].strip()
1814
return default
1815
1816
def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
1817
""" Create a new cookie or replace an old one. If the `secret` parameter is
1818
set, create a `Signed Cookie` (described below).
1819
1820
:param name: the name of the cookie.
1821
:param value: the value of the cookie.
1822
:param secret: a signature key required for signed cookies.
1823
1824
Additionally, this method accepts all RFC 2109 attributes that are
1825
supported by :class:`cookie.Morsel`, including:
1826
1827
:param maxage: maximum age in seconds. (default: None)
1828
:param expires: a datetime object or UNIX timestamp. (default: None)
1829
:param domain: the domain that is allowed to read the cookie.
1830
(default: current domain)
1831
:param path: limits the cookie to a given path (default: current path)
1832
:param secure: limit the cookie to HTTPS connections (default: off).
1833
:param httponly: prevents client-side javascript to read this cookie
1834
(default: off, requires Python 2.6 or newer).
1835
:param samesite: Control or disable third-party use for this cookie.
1836
Possible values: `lax`, `strict` or `none` (default).
1837
1838
If neither `expires` nor `maxage` is set (default), the cookie will
1839
expire at the end of the browser session (as soon as the browser
1840
window is closed).
1841
1842
Signed cookies may store any pickle-able object and are
1843
cryptographically signed to prevent manipulation. Keep in mind that
1844
cookies are limited to 4kb in most browsers.
1845
1846
Warning: Pickle is a potentially dangerous format. If an attacker
1847
gains access to the secret key, he could forge cookies that execute
1848
code on server side if unpickled. Using pickle is discouraged and
1849
support for it will be removed in later versions of bottle.
1850
1851
Warning: Signed cookies are not encrypted (the client can still see
1852
the content) and not copy-protected (the client can restore an old
1853
cookie). The main intention is to make pickling and unpickling
1854
save, not to store secret information at client side.
1855
"""
1856
if not self._cookies:
1857
self._cookies = SimpleCookie()
1858
1859
# Monkey-patch Cookie lib to support 'SameSite' parameter
1860
# https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
1861
if py < (3, 8, 0):
1862
Morsel._reserved.setdefault('samesite', 'SameSite')
1863
1864
if secret:
1865
if not isinstance(value, basestring):
1866
depr(0, 13, "Pickling of arbitrary objects into cookies is "
1867
"deprecated.", "Only store strings in cookies. "
1868
"JSON strings are fine, too.")
1869
encoded = base64.b64encode(pickle.dumps([name, value], -1))
1870
sig = base64.b64encode(hmac.new(tob(secret), encoded,
1871
digestmod=digestmod).digest())
1872
value = touni(tob('!') + sig + tob('?') + encoded)
1873
elif not isinstance(value, basestring):
1874
raise TypeError('Secret key required for non-string cookies.')
1875
1876
# Cookie size plus options must not exceed 4kb.
1877
if len(name) + len(value) > 3800:
1878
raise ValueError('Content does not fit into a cookie.')
1879
1880
self._cookies[name] = value
1881
1882
for key, value in options.items():
1883
if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13
1884
key = 'max-age'
1885
if isinstance(value, timedelta):
1886
value = value.seconds + value.days * 24 * 3600
1887
if key == 'expires':
1888
value = http_date(value)
1889
if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13
1890
key, value = 'samesite', (value or "none").lower()
1891
if value not in ('lax', 'strict', 'none'):
1892
raise CookieError("Invalid value for SameSite")
1893
if key in ('secure', 'httponly') and not value:
1894
continue
1895
self._cookies[name][key] = value
1896
1897
def delete_cookie(self, key, **kwargs):
1898
""" Delete a cookie. Be sure to use the same `domain` and `path`
1899
settings as used to create the cookie. """
1900
kwargs['max_age'] = -1
1901
kwargs['expires'] = 0
1902
self.set_cookie(key, '', **kwargs)
1903
1904
def __repr__(self):
1905
out = ''
1906
for name, value in self.headerlist:
1907
out += '%s: %s\n' % (name.title(), value.strip())
1908
return out
1909
1910
1911
def _local_property():
1912
ls = threading.local()
1913
1914
def fget(_):
1915
try:
1916
return ls.var
1917
except AttributeError:
1918
raise RuntimeError("Request context not initialized.")
1919
1920
def fset(_, value):
1921
ls.var = value
1922
1923
def fdel(_):
1924
del ls.var
1925
1926
return property(fget, fset, fdel, 'Thread-local property')
1927
1928
1929
class LocalRequest(BaseRequest):
1930
""" A thread-local subclass of :class:`BaseRequest` with a different
1931
set of attributes for each thread. There is usually only one global
1932
instance of this class (:data:`request`). If accessed during a
1933
request/response cycle, this instance always refers to the *current*
1934
request (even on a multithreaded server). """
1935
bind = BaseRequest.__init__
1936
environ = _local_property()
1937
1938
1939
class LocalResponse(BaseResponse):
1940
""" A thread-local subclass of :class:`BaseResponse` with a different
1941
set of attributes for each thread. There is usually only one global
1942
instance of this class (:data:`response`). Its attributes are used
1943
to build the HTTP response at the end of the request/response cycle.
1944
"""
1945
bind = BaseResponse.__init__
1946
_status_line = _local_property()
1947
_status_code = _local_property()
1948
_cookies = _local_property()
1949
_headers = _local_property()
1950
body = _local_property()
1951
1952
1953
Request = BaseRequest
1954
Response = BaseResponse
1955
1956
1957
class HTTPResponse(Response, BottleException):
1958
""" A subclass of :class:`Response` that can be raised or returned from request
1959
handlers to short-curcuit request processing and override changes made to the
1960
global :data:`request` object. This bypasses error handlers, even if the status
1961
code indicates an error. Return or raise :class:`HTTPError` to trigger error
1962
handlers.
1963
"""
1964
1965
def __init__(self, body='', status=None, headers=None, **more_headers):
1966
super(HTTPResponse, self).__init__(body, status, headers, **more_headers)
1967
1968
def apply(self, other):
1969
""" Copy the state of this response to a different :class:`Response` object. """
1970
other._status_code = self._status_code
1971
other._status_line = self._status_line
1972
other._headers = self._headers
1973
other._cookies = self._cookies
1974
other.body = self.body
1975
1976
1977
class HTTPError(HTTPResponse):
1978
""" A subclass of :class:`HTTPResponse` that triggers error handlers. """
1979
1980
default_status = 500
1981
1982
def __init__(self,
1983
status=None,
1984
body=None,
1985
exception=None,
1986
traceback=None, **more_headers):
1987
self.exception = exception
1988
self.traceback = traceback
1989
super(HTTPError, self).__init__(body, status, **more_headers)
1990
1991
###############################################################################
1992
# Plugins ######################################################################
1993
###############################################################################
1994
1995
1996
class PluginError(BottleException):
1997
pass
1998
1999
2000
class JSONPlugin(object):
2001
name = 'json'
2002
api = 2
2003
2004
def __init__(self, json_dumps=json_dumps):
2005
self.json_dumps = json_dumps
2006
2007
def setup(self, app):
2008
app.config._define('json.enable', default=True, validate=bool,
2009
help="Enable or disable automatic dict->json filter.")
2010
app.config._define('json.ascii', default=False, validate=bool,
2011
help="Use only 7-bit ASCII characters in output.")
2012
app.config._define('json.indent', default=True, validate=bool,
2013
help="Add whitespace to make json more readable.")
2014
app.config._define('json.dump_func', default=None,
2015
help="If defined, use this function to transform"
2016
" dict into json. The other options no longer"
2017
" apply.")
2018
2019
def apply(self, callback, route):
2020
dumps = self.json_dumps
2021
if not self.json_dumps: return callback
2022
2023
@functools.wraps(callback)
2024
def wrapper(*a, **ka):
2025
try:
2026
rv = callback(*a, **ka)
2027
except HTTPResponse as resp:
2028
rv = resp
2029
2030
if isinstance(rv, dict):
2031
#Attempt to serialize, raises exception on failure
2032
json_response = dumps(rv)
2033
#Set content type only if serialization successful
2034
response.content_type = 'application/json'
2035
return json_response
2036
elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict):
2037
rv.body = dumps(rv.body)
2038
rv.content_type = 'application/json'
2039
return rv
2040
2041
return wrapper
2042
2043
2044
class TemplatePlugin(object):
2045
""" This plugin applies the :func:`view` decorator to all routes with a
2046
`template` config parameter. If the parameter is a tuple, the second
2047
element must be a dict with additional options (e.g. `template_engine`)
2048
or default variables for the template. """
2049
name = 'template'
2050
api = 2
2051
2052
def setup(self, app):
2053
app.tpl = self
2054
2055
def apply(self, callback, route):
2056
conf = route.config.get('template')
2057
if isinstance(conf, (tuple, list)) and len(conf) == 2:
2058
return view(conf[0], **conf[1])(callback)
2059
elif isinstance(conf, str):
2060
return view(conf)(callback)
2061
else:
2062
return callback
2063
2064
2065
#: Not a plugin, but part of the plugin API. TODO: Find a better place.
2066
class _ImportRedirect(object):
2067
def __init__(self, name, impmask):
2068
""" Create a virtual package that redirects imports (see PEP 302). """
2069
self.name = name
2070
self.impmask = impmask
2071
self.module = sys.modules.setdefault(name, new_module(name))
2072
self.module.__dict__.update({
2073
'__file__': __file__,
2074
'__path__': [],
2075
'__all__': [],
2076
'__loader__': self
2077
})
2078
sys.meta_path.append(self)
2079
2080
def find_spec(self, fullname, path, target=None):
2081
if '.' not in fullname: return
2082
if fullname.rsplit('.', 1)[0] != self.name: return
2083
from importlib.util import spec_from_loader
2084
return spec_from_loader(fullname, self)
2085
2086
def find_module(self, fullname, path=None):
2087
if '.' not in fullname: return
2088
if fullname.rsplit('.', 1)[0] != self.name: return
2089
return self
2090
2091
def create_module(self, spec):
2092
return self.load_module(spec.name)
2093
2094
def exec_module(self, module):
2095
pass # This probably breaks importlib.reload() :/
2096
2097
def load_module(self, fullname):
2098
if fullname in sys.modules: return sys.modules[fullname]
2099
modname = fullname.rsplit('.', 1)[1]
2100
realname = self.impmask % modname
2101
__import__(realname)
2102
module = sys.modules[fullname] = sys.modules[realname]
2103
setattr(self.module, modname, module)
2104
module.__loader__ = self
2105
return module
2106
2107
###############################################################################
2108
# Common Utilities #############################################################
2109
###############################################################################
2110
2111
2112
class MultiDict(DictMixin):
2113
""" This dict stores multiple values per key, but behaves exactly like a
2114
normal dict in that it returns only the newest value for any given key.
2115
There are special methods available to access the full list of values.
2116
"""
2117
2118
def __init__(self, *a, **k):
2119
self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items())
2120
2121
def __len__(self):
2122
return len(self.dict)
2123
2124
def __iter__(self):
2125
return iter(self.dict)
2126
2127
def __contains__(self, key):
2128
return key in self.dict
2129
2130
def __delitem__(self, key):
2131
del self.dict[key]
2132
2133
def __getitem__(self, key):
2134
return self.dict[key][-1]
2135
2136
def __setitem__(self, key, value):
2137
self.append(key, value)
2138
2139
def keys(self):
2140
return self.dict.keys()
2141
2142
if py3k:
2143
2144
def values(self):
2145
return (v[-1] for v in self.dict.values())
2146
2147
def items(self):
2148
return ((k, v[-1]) for k, v in self.dict.items())
2149
2150
def allitems(self):
2151
return ((k, v) for k, vl in self.dict.items() for v in vl)
2152
2153
iterkeys = keys
2154
itervalues = values
2155
iteritems = items
2156
iterallitems = allitems
2157
2158
else:
2159
2160
def values(self):
2161
return [v[-1] for v in self.dict.values()]
2162
2163
def items(self):
2164
return [(k, v[-1]) for k, v in self.dict.items()]
2165
2166
def iterkeys(self):
2167
return self.dict.iterkeys()
2168
2169
def itervalues(self):
2170
return (v[-1] for v in self.dict.itervalues())
2171
2172
def iteritems(self):
2173
return ((k, v[-1]) for k, v in self.dict.iteritems())
2174
2175
def iterallitems(self):
2176
return ((k, v) for k, vl in self.dict.iteritems() for v in vl)
2177
2178
def allitems(self):
2179
return [(k, v) for k, vl in self.dict.iteritems() for v in vl]
2180
2181
def get(self, key, default=None, index=-1, type=None):
2182
""" Return the most recent value for a key.
2183
2184
:param default: The default value to be returned if the key is not
2185
present or the type conversion fails.
2186
:param index: An index for the list of available values.
2187
:param type: If defined, this callable is used to cast the value
2188
into a specific type. Exception are suppressed and result in
2189
the default value to be returned.
2190
"""
2191
try:
2192
val = self.dict[key][index]
2193
return type(val) if type else val
2194
except Exception:
2195
pass
2196
return default
2197
2198
def append(self, key, value):
2199
""" Add a new value to the list of values for this key. """
2200
self.dict.setdefault(key, []).append(value)
2201
2202
def replace(self, key, value):
2203
""" Replace the list of values with a single value. """
2204
self.dict[key] = [value]
2205
2206
def getall(self, key):
2207
""" Return a (possibly empty) list of values for a key. """
2208
return self.dict.get(key) or []
2209
2210
#: Aliases for WTForms to mimic other multi-dict APIs (Django)
2211
getone = get
2212
getlist = getall
2213
2214
2215
class FormsDict(MultiDict):
2216
""" This :class:`MultiDict` subclass is used to store request form data.
2217
Additionally to the normal dict-like item access methods (which return
2218
unmodified data as native strings), this container also supports
2219
attribute-like access to its values. Attributes are automatically de-
2220
or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing
2221
attributes default to an empty string. """
2222
2223
#: Encoding used for attribute values.
2224
input_encoding = 'utf8'
2225
#: If true (default), unicode strings are first encoded with `latin1`
2226
#: and then decoded to match :attr:`input_encoding`.
2227
recode_unicode = True
2228
2229
def _fix(self, s, encoding=None):
2230
if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI
2231
return s.encode('latin1').decode(encoding or self.input_encoding)
2232
elif isinstance(s, bytes): # Python 2 WSGI
2233
return s.decode(encoding or self.input_encoding)
2234
else:
2235
return s
2236
2237
def decode(self, encoding=None):
2238
""" Returns a copy with all keys and values de- or recoded to match
2239
:attr:`input_encoding`. Some libraries (e.g. WTForms) want a
2240
unicode dictionary. """
2241
copy = FormsDict()
2242
enc = copy.input_encoding = encoding or self.input_encoding
2243
copy.recode_unicode = False
2244
for key, value in self.allitems():
2245
copy.append(self._fix(key, enc), self._fix(value, enc))
2246
return copy
2247
2248
def getunicode(self, name, default=None, encoding=None):
2249
""" Return the value as a unicode string, or the default. """
2250
try:
2251
return self._fix(self[name], encoding)
2252
except (UnicodeError, KeyError):
2253
return default
2254
2255
def __getattr__(self, name, default=unicode()):
2256
# Without this guard, pickle generates a cryptic TypeError:
2257
if name.startswith('__') and name.endswith('__'):
2258
return super(FormsDict, self).__getattr__(name)
2259
return self.getunicode(name, default=default)
2260
2261
class HeaderDict(MultiDict):
2262
""" A case-insensitive version of :class:`MultiDict` that defaults to
2263
replace the old value instead of appending it. """
2264
2265
def __init__(self, *a, **ka):
2266
self.dict = {}
2267
if a or ka: self.update(*a, **ka)
2268
2269
def __contains__(self, key):
2270
return _hkey(key) in self.dict
2271
2272
def __delitem__(self, key):
2273
del self.dict[_hkey(key)]
2274
2275
def __getitem__(self, key):
2276
return self.dict[_hkey(key)][-1]
2277
2278
def __setitem__(self, key, value):
2279
self.dict[_hkey(key)] = [_hval(value)]
2280
2281
def append(self, key, value):
2282
self.dict.setdefault(_hkey(key), []).append(_hval(value))
2283
2284
def replace(self, key, value):
2285
self.dict[_hkey(key)] = [_hval(value)]
2286
2287
def getall(self, key):
2288
return self.dict.get(_hkey(key)) or []
2289
2290
def get(self, key, default=None, index=-1):
2291
return MultiDict.get(self, _hkey(key), default, index)
2292
2293
def filter(self, names):
2294
for name in (_hkey(n) for n in names):
2295
if name in self.dict:
2296
del self.dict[name]
2297
2298
2299
class WSGIHeaderDict(DictMixin):
2300
""" This dict-like class wraps a WSGI environ dict and provides convenient
2301
access to HTTP_* fields. Keys and values are native strings
2302
(2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI
2303
environment contains non-native string values, these are de- or encoded
2304
using a lossless 'latin1' character set.
2305
2306
The API will remain stable even on changes to the relevant PEPs.
2307
Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one
2308
that uses non-native strings.)
2309
"""
2310
#: List of keys that do not have a ``HTTP_`` prefix.
2311
cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH')
2312
2313
def __init__(self, environ):
2314
self.environ = environ
2315
2316
def _ekey(self, key):
2317
""" Translate header field name to CGI/WSGI environ key. """
2318
key = key.replace('-', '_').upper()
2319
if key in self.cgikeys:
2320
return key
2321
return 'HTTP_' + key
2322
2323
def raw(self, key, default=None):
2324
""" Return the header value as is (may be bytes or unicode). """
2325
return self.environ.get(self._ekey(key), default)
2326
2327
def __getitem__(self, key):
2328
val = self.environ[self._ekey(key)]
2329
if py3k:
2330
if isinstance(val, unicode):
2331
val = val.encode('latin1').decode('utf8')
2332
else:
2333
val = val.decode('utf8')
2334
return val
2335
2336
def __setitem__(self, key, value):
2337
raise TypeError("%s is read-only." % self.__class__)
2338
2339
def __delitem__(self, key):
2340
raise TypeError("%s is read-only." % self.__class__)
2341
2342
def __iter__(self):
2343
for key in self.environ:
2344
if key[:5] == 'HTTP_':
2345
yield _hkey(key[5:])
2346
elif key in self.cgikeys:
2347
yield _hkey(key)
2348
2349
def keys(self):
2350
return [x for x in self]
2351
2352
def __len__(self):
2353
return len(self.keys())
2354
2355
def __contains__(self, key):
2356
return self._ekey(key) in self.environ
2357
2358
_UNSET = object()
2359
2360
class ConfigDict(dict):
2361
""" A dict-like configuration storage with additional support for
2362
namespaces, validators, meta-data and overlays.
2363
2364
This dict-like class is heavily optimized for read access.
2365
Read-only methods and item access should be as fast as a native dict.
2366
"""
2367
2368
__slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__')
2369
2370
def __init__(self):
2371
self._meta = {}
2372
self._change_listener = []
2373
#: Weak references of overlays that need to be kept in sync.
2374
self._overlays = []
2375
#: Config that is the source for this overlay.
2376
self._source = None
2377
#: Keys of values copied from the source (values we do not own)
2378
self._virtual_keys = set()
2379
2380
def load_module(self, name, squash=True):
2381
"""Load values from a Python module.
2382
2383
Import a python module by name and add all upper-case module-level
2384
variables to this config dict.
2385
2386
:param name: Module name to import and load.
2387
:param squash: If true (default), nested dicts are assumed to
2388
represent namespaces and flattened (see :meth:`load_dict`).
2389
"""
2390
config_obj = load(name)
2391
obj = {key: getattr(config_obj, key)
2392
for key in dir(config_obj) if key.isupper()}
2393
2394
if squash:
2395
self.load_dict(obj)
2396
else:
2397
self.update(obj)
2398
return self
2399
2400
def load_config(self, filename, **options):
2401
""" Load values from ``*.ini`` style config files using configparser.
2402
2403
INI style sections (e.g. ``[section]``) are used as namespace for
2404
all keys within that section. Both section and key names may contain
2405
dots as namespace separators and are converted to lower-case.
2406
2407
The special sections ``[bottle]`` and ``[ROOT]`` refer to the root
2408
namespace and the ``[DEFAULT]`` section defines default values for all
2409
other sections.
2410
2411
:param filename: The path of a config file, or a list of paths.
2412
:param options: All keyword parameters are passed to the underlying
2413
:class:`python:configparser.ConfigParser` constructor call.
2414
2415
"""
2416
options.setdefault('allow_no_value', True)
2417
if py3k:
2418
options.setdefault('interpolation',
2419
configparser.ExtendedInterpolation())
2420
conf = configparser.ConfigParser(**options)
2421
conf.read(filename)
2422
for section in conf.sections():
2423
for key in conf.options(section):
2424
value = conf.get(section, key)
2425
if section not in ('bottle', 'ROOT'):
2426
key = section + '.' + key
2427
self[key.lower()] = value
2428
return self
2429
2430
def load_dict(self, source, namespace=''):
2431
""" Load values from a dictionary structure. Nesting can be used to
2432
represent namespaces.
2433
2434
>>> c = ConfigDict()
2435
>>> c.load_dict({'some': {'namespace': {'key': 'value'} } })
2436
{'some.namespace.key': 'value'}
2437
"""
2438
for key, value in source.items():
2439
if isinstance(key, basestring):
2440
nskey = (namespace + '.' + key).strip('.')
2441
if isinstance(value, dict):
2442
self.load_dict(value, namespace=nskey)
2443
else:
2444
self[nskey] = value
2445
else:
2446
raise TypeError('Key has type %r (not a string)' % type(key))
2447
return self
2448
2449
def update(self, *a, **ka):
2450
""" If the first parameter is a string, all keys are prefixed with this
2451
namespace. Apart from that it works just as the usual dict.update().
2452
2453
>>> c = ConfigDict()
2454
>>> c.update('some.namespace', key='value')
2455
"""
2456
prefix = ''
2457
if a and isinstance(a[0], basestring):
2458
prefix = a[0].strip('.') + '.'
2459
a = a[1:]
2460
for key, value in dict(*a, **ka).items():
2461
self[prefix + key] = value
2462
2463
def setdefault(self, key, value=None):
2464
if key not in self:
2465
self[key] = value
2466
return self[key]
2467
2468
def __setitem__(self, key, value):
2469
if not isinstance(key, basestring):
2470
raise TypeError('Key has type %r (not a string)' % type(key))
2471
2472
self._virtual_keys.discard(key)
2473
2474
value = self.meta_get(key, 'filter', lambda x: x)(value)
2475
if key in self and self[key] is value:
2476
return
2477
2478
self._on_change(key, value)
2479
dict.__setitem__(self, key, value)
2480
2481
for overlay in self._iter_overlays():
2482
overlay._set_virtual(key, value)
2483
2484
def __delitem__(self, key):
2485
if key not in self:
2486
raise KeyError(key)
2487
if key in self._virtual_keys:
2488
raise KeyError("Virtual keys cannot be deleted: %s" % key)
2489
2490
if self._source and key in self._source:
2491
# Not virtual, but present in source -> Restore virtual value
2492
dict.__delitem__(self, key)
2493
self._set_virtual(key, self._source[key])
2494
else: # not virtual, not present in source. This is OUR value
2495
self._on_change(key, None)
2496
dict.__delitem__(self, key)
2497
for overlay in self._iter_overlays():
2498
overlay._delete_virtual(key)
2499
2500
def _set_virtual(self, key, value):
2501
""" Recursively set or update virtual keys. """
2502
if key in self and key not in self._virtual_keys:
2503
return # Do nothing for non-virtual keys.
2504
2505
self._virtual_keys.add(key)
2506
if key in self and self[key] is not value:
2507
self._on_change(key, value)
2508
dict.__setitem__(self, key, value)
2509
for overlay in self._iter_overlays():
2510
overlay._set_virtual(key, value)
2511
2512
def _delete_virtual(self, key):
2513
""" Recursively delete virtual entry. """
2514
if key not in self._virtual_keys:
2515
return # Do nothing for non-virtual keys.
2516
2517
if key in self:
2518
self._on_change(key, None)
2519
dict.__delitem__(self, key)
2520
self._virtual_keys.discard(key)
2521
for overlay in self._iter_overlays():
2522
overlay._delete_virtual(key)
2523
2524
def _on_change(self, key, value):
2525
for cb in self._change_listener:
2526
if cb(self, key, value):
2527
return True
2528
2529
def _add_change_listener(self, func):
2530
self._change_listener.append(func)
2531
return func
2532
2533
def meta_get(self, key, metafield, default=None):
2534
""" Return the value of a meta field for a key. """
2535
return self._meta.get(key, {}).get(metafield, default)
2536
2537
def meta_set(self, key, metafield, value):
2538
""" Set the meta field for a key to a new value.
2539
2540
Meta-fields are shared between all members of an overlay tree.
2541
"""
2542
self._meta.setdefault(key, {})[metafield] = value
2543
2544
def meta_list(self, key):
2545
""" Return an iterable of meta field names defined for a key. """
2546
return self._meta.get(key, {}).keys()
2547
2548
def _define(self, key, default=_UNSET, help=_UNSET, validate=_UNSET):
2549
""" (Unstable) Shortcut for plugins to define own config parameters. """
2550
if default is not _UNSET:
2551
self.setdefault(key, default)
2552
if help is not _UNSET:
2553
self.meta_set(key, 'help', help)
2554
if validate is not _UNSET:
2555
self.meta_set(key, 'validate', validate)
2556
2557
def _iter_overlays(self):
2558
for ref in self._overlays:
2559
overlay = ref()
2560
if overlay is not None:
2561
yield overlay
2562
2563
def _make_overlay(self):
2564
""" (Unstable) Create a new overlay that acts like a chained map: Values
2565
missing in the overlay are copied from the source map. Both maps
2566
share the same meta entries.
2567
2568
Entries that were copied from the source are called 'virtual'. You
2569
can not delete virtual keys, but overwrite them, which turns them
2570
into non-virtual entries. Setting keys on an overlay never affects
2571
its source, but may affect any number of child overlays.
2572
2573
Other than collections.ChainMap or most other implementations, this
2574
approach does not resolve missing keys on demand, but instead
2575
actively copies all values from the source to the overlay and keeps
2576
track of virtual and non-virtual keys internally. This removes any
2577
lookup-overhead. Read-access is as fast as a build-in dict for both
2578
virtual and non-virtual keys.
2579
2580
Changes are propagated recursively and depth-first. A failing
2581
on-change handler in an overlay stops the propagation of virtual
2582
values and may result in an partly updated tree. Take extra care
2583
here and make sure that on-change handlers never fail.
2584
2585
Used by Route.config
2586
"""
2587
# Cleanup dead references
2588
self._overlays[:] = [ref for ref in self._overlays if ref() is not None]
2589
2590
overlay = ConfigDict()
2591
overlay._meta = self._meta
2592
overlay._source = self
2593
self._overlays.append(weakref.ref(overlay))
2594
for key in self:
2595
overlay._set_virtual(key, self[key])
2596
return overlay
2597
2598
2599
2600
2601
class AppStack(list):
2602
""" A stack-like list. Calling it returns the head of the stack. """
2603
2604
def __call__(self):
2605
""" Return the current default application. """
2606
return self.default
2607
2608
def push(self, value=None):
2609
""" Add a new :class:`Bottle` instance to the stack """
2610
if not isinstance(value, Bottle):
2611
value = Bottle()
2612
self.append(value)
2613
return value
2614
new_app = push
2615
2616
@property
2617
def default(self):
2618
try:
2619
return self[-1]
2620
except IndexError:
2621
return self.push()
2622
2623
2624
class WSGIFileWrapper(object):
2625
def __init__(self, fp, buffer_size=1024 * 64):
2626
self.fp, self.buffer_size = fp, buffer_size
2627
for attr in 'fileno', 'close', 'read', 'readlines', 'tell', 'seek':
2628
if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr))
2629
2630
def __iter__(self):
2631
buff, read = self.buffer_size, self.read
2632
part = read(buff)
2633
while part:
2634
yield part
2635
part = read(buff)
2636
2637
2638
class _closeiter(object):
2639
""" This only exists to be able to attach a .close method to iterators that
2640
do not support attribute assignment (most of itertools). """
2641
2642
def __init__(self, iterator, close=None):
2643
self.iterator = iterator
2644
self.close_callbacks = makelist(close)
2645
2646
def __iter__(self):
2647
return iter(self.iterator)
2648
2649
def close(self):
2650
for func in self.close_callbacks:
2651
func()
2652
2653
2654
class ResourceManager(object):
2655
""" This class manages a list of search paths and helps to find and open
2656
application-bound resources (files).
2657
2658
:param base: default value for :meth:`add_path` calls.
2659
:param opener: callable used to open resources.
2660
:param cachemode: controls which lookups are cached. One of 'all',
2661
'found' or 'none'.
2662
"""
2663
2664
def __init__(self, base='./', opener=open, cachemode='all'):
2665
self.opener = opener
2666
self.base = base
2667
self.cachemode = cachemode
2668
2669
#: A list of search paths. See :meth:`add_path` for details.
2670
self.path = []
2671
#: A cache for resolved paths. ``res.cache.clear()`` clears the cache.
2672
self.cache = {}
2673
2674
def add_path(self, path, base=None, index=None, create=False):
2675
""" Add a new path to the list of search paths. Return False if the
2676
path does not exist.
2677
2678
:param path: The new search path. Relative paths are turned into
2679
an absolute and normalized form. If the path looks like a file
2680
(not ending in `/`), the filename is stripped off.
2681
:param base: Path used to absolutize relative search paths.
2682
Defaults to :attr:`base` which defaults to ``os.getcwd()``.
2683
:param index: Position within the list of search paths. Defaults
2684
to last index (appends to the list).
2685
2686
The `base` parameter makes it easy to reference files installed
2687
along with a python module or package::
2688
2689
res.add_path('./resources/', __file__)
2690
"""
2691
base = os.path.abspath(os.path.dirname(base or self.base))
2692
path = os.path.abspath(os.path.join(base, os.path.dirname(path)))
2693
path += os.sep
2694
if path in self.path:
2695
self.path.remove(path)
2696
if create and not os.path.isdir(path):
2697
os.makedirs(path)
2698
if index is None:
2699
self.path.append(path)
2700
else:
2701
self.path.insert(index, path)
2702
self.cache.clear()
2703
return os.path.exists(path)
2704
2705
def __iter__(self):
2706
""" Iterate over all existing files in all registered paths. """
2707
search = self.path[:]
2708
while search:
2709
path = search.pop()
2710
if not os.path.isdir(path): continue
2711
for name in os.listdir(path):
2712
full = os.path.join(path, name)
2713
if os.path.isdir(full): search.append(full)
2714
else: yield full
2715
2716
def lookup(self, name):
2717
""" Search for a resource and return an absolute file path, or `None`.
2718
2719
The :attr:`path` list is searched in order. The first match is
2720
returned. Symlinks are followed. The result is cached to speed up
2721
future lookups. """
2722
if name not in self.cache or DEBUG:
2723
for path in self.path:
2724
fpath = os.path.join(path, name)
2725
if os.path.isfile(fpath):
2726
if self.cachemode in ('all', 'found'):
2727
self.cache[name] = fpath
2728
return fpath
2729
if self.cachemode == 'all':
2730
self.cache[name] = None
2731
return self.cache[name]
2732
2733
def open(self, name, mode='r', *args, **kwargs):
2734
""" Find a resource and return a file object, or raise IOError. """
2735
fname = self.lookup(name)
2736
if not fname: raise IOError("Resource %r not found." % name)
2737
return self.opener(fname, mode=mode, *args, **kwargs)
2738
2739
2740
class FileUpload(object):
2741
def __init__(self, fileobj, name, filename, headers=None):
2742
""" Wrapper for a single file uploaded via ``multipart/form-data``. """
2743
#: Open file(-like) object (BytesIO buffer or temporary file)
2744
self.file = fileobj
2745
#: Name of the upload form field
2746
self.name = name
2747
#: Raw filename as sent by the client (may contain unsafe characters)
2748
self.raw_filename = filename
2749
#: A :class:`HeaderDict` with additional headers (e.g. content-type)
2750
self.headers = HeaderDict(headers) if headers else HeaderDict()
2751
2752
content_type = HeaderProperty('Content-Type')
2753
content_length = HeaderProperty('Content-Length', reader=int, default=-1)
2754
2755
def get_header(self, name, default=None):
2756
""" Return the value of a header within the multipart part. """
2757
return self.headers.get(name, default)
2758
2759
@cached_property
2760
def filename(self):
2761
""" Name of the file on the client file system, but normalized to ensure
2762
file system compatibility. An empty filename is returned as 'empty'.
2763
2764
Only ASCII letters, digits, dashes, underscores and dots are
2765
allowed in the final filename. Accents are removed, if possible.
2766
Whitespace is replaced by a single dash. Leading or tailing dots
2767
or dashes are removed. The filename is limited to 255 characters.
2768
"""
2769
fname = self.raw_filename
2770
if not isinstance(fname, unicode):
2771
fname = fname.decode('utf8', 'ignore')
2772
fname = normalize('NFKD', fname)
2773
fname = fname.encode('ASCII', 'ignore').decode('ASCII')
2774
fname = os.path.basename(fname.replace('\\', os.path.sep))
2775
fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip()
2776
fname = re.sub(r'[-\s]+', '-', fname).strip('.-')
2777
return fname[:255] or 'empty'
2778
2779
def _copy_file(self, fp, chunk_size=2 ** 16):
2780
read, write, offset = self.file.read, fp.write, self.file.tell()
2781
while 1:
2782
buf = read(chunk_size)
2783
if not buf: break
2784
write(buf)
2785
self.file.seek(offset)
2786
2787
def save(self, destination, overwrite=False, chunk_size=2 ** 16):
2788
""" Save file to disk or copy its content to an open file(-like) object.
2789
If *destination* is a directory, :attr:`filename` is added to the
2790
path. Existing files are not overwritten by default (IOError).
2791
2792
:param destination: File path, directory or file(-like) object.
2793
:param overwrite: If True, replace existing files. (default: False)
2794
:param chunk_size: Bytes to read at a time. (default: 64kb)
2795
"""
2796
if isinstance(destination, basestring): # Except file-likes here
2797
if os.path.isdir(destination):
2798
destination = os.path.join(destination, self.filename)
2799
if not overwrite and os.path.exists(destination):
2800
raise IOError('File exists.')
2801
with open(destination, 'wb') as fp:
2802
self._copy_file(fp, chunk_size)
2803
else:
2804
self._copy_file(destination, chunk_size)
2805
2806
###############################################################################
2807
# Application Helper ###########################################################
2808
###############################################################################
2809
2810
2811
def abort(code=500, text='Unknown Error.'):
2812
""" Aborts execution and causes a HTTP error. """
2813
raise HTTPError(code, text)
2814
2815
2816
def redirect(url, code=None):
2817
""" Aborts execution and causes a 303 or 302 redirect, depending on
2818
the HTTP protocol version. """
2819
if not code:
2820
code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302
2821
res = response.copy(cls=HTTPResponse)
2822
res.status = code
2823
res.body = ""
2824
res.set_header('Location', urljoin(request.url, url))
2825
raise res
2826
2827
2828
def _rangeiter(fp, offset, limit, bufsize=1024 * 1024):
2829
""" Yield chunks from a range in a file. """
2830
fp.seek(offset)
2831
while limit > 0:
2832
part = fp.read(min(limit, bufsize))
2833
if not part:
2834
break
2835
limit -= len(part)
2836
yield part
2837
2838
2839
def static_file(filename, root,
2840
mimetype=True,
2841
download=False,
2842
charset='UTF-8',
2843
etag=None,
2844
headers=None):
2845
""" Open a file in a safe way and return an instance of :exc:`HTTPResponse`
2846
that can be sent back to the client.
2847
2848
:param filename: Name or path of the file to send, relative to ``root``.
2849
:param root: Root path for file lookups. Should be an absolute directory
2850
path.
2851
:param mimetype: Provide the content-type header (default: guess from
2852
file extension)
2853
:param download: If True, ask the browser to open a `Save as...` dialog
2854
instead of opening the file with the associated program. You can
2855
specify a custom filename as a string. If not specified, the
2856
original filename is used (default: False).
2857
:param charset: The charset for files with a ``text/*`` mime-type.
2858
(default: UTF-8)
2859
:param etag: Provide a pre-computed ETag header. If set to ``False``,
2860
ETag handling is disabled. (default: auto-generate ETag header)
2861
:param headers: Additional headers dict to add to the response.
2862
2863
While checking user input is always a good idea, this function provides
2864
additional protection against malicious ``filename`` parameters from
2865
breaking out of the ``root`` directory and leaking sensitive information
2866
to an attacker.
2867
2868
Read-protected files or files outside of the ``root`` directory are
2869
answered with ``403 Access Denied``. Missing files result in a
2870
``404 Not Found`` response. Conditional requests (``If-Modified-Since``,
2871
``If-None-Match``) are answered with ``304 Not Modified`` whenever
2872
possible. ``HEAD`` and ``Range`` requests (used by download managers to
2873
check or continue partial downloads) are also handled automatically.
2874
"""
2875
2876
root = os.path.join(os.path.abspath(root), '')
2877
filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
2878
headers = headers.copy() if headers else {}
2879
getenv = request.environ.get
2880
2881
if not filename.startswith(root):
2882
return HTTPError(403, "Access denied.")
2883
if not os.path.exists(filename) or not os.path.isfile(filename):
2884
return HTTPError(404, "File does not exist.")
2885
if not os.access(filename, os.R_OK):
2886
return HTTPError(403, "You do not have permission to access this file.")
2887
2888
if mimetype is True:
2889
name = download if isinstance(download, str) else filename
2890
mimetype, encoding = mimetypes.guess_type(name)
2891
if encoding == 'gzip':
2892
mimetype = 'application/gzip'
2893
elif encoding: # e.g. bzip2 -> application/x-bzip2
2894
mimetype = 'application/x-' + encoding
2895
2896
if charset and mimetype and 'charset=' not in mimetype \
2897
and (mimetype[:5] == 'text/' or mimetype == 'application/javascript'):
2898
mimetype += '; charset=%s' % charset
2899
2900
if mimetype:
2901
headers['Content-Type'] = mimetype
2902
2903
if download is True:
2904
download = os.path.basename(filename)
2905
2906
if download:
2907
download = download.replace('"','')
2908
headers['Content-Disposition'] = 'attachment; filename="%s"' % download
2909
2910
stats = os.stat(filename)
2911
headers['Content-Length'] = clen = stats.st_size
2912
headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, usegmt=True)
2913
headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)
2914
2915
if etag is None:
2916
etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
2917
clen, filename)
2918
etag = hashlib.sha1(tob(etag)).hexdigest()
2919
2920
if etag:
2921
headers['ETag'] = etag
2922
check = getenv('HTTP_IF_NONE_MATCH')
2923
if check and check == etag:
2924
return HTTPResponse(status=304, **headers)
2925
2926
ims = getenv('HTTP_IF_MODIFIED_SINCE')
2927
if ims:
2928
ims = parse_date(ims.split(";")[0].strip())
2929
if ims is not None and ims >= int(stats.st_mtime):
2930
return HTTPResponse(status=304, **headers)
2931
2932
body = '' if request.method == 'HEAD' else open(filename, 'rb')
2933
2934
headers["Accept-Ranges"] = "bytes"
2935
range_header = getenv('HTTP_RANGE')
2936
if range_header:
2937
ranges = list(parse_range_header(range_header, clen))
2938
if not ranges:
2939
return HTTPError(416, "Requested Range Not Satisfiable")
2940
offset, end = ranges[0]
2941
rlen = end - offset
2942
headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
2943
headers["Content-Length"] = str(rlen)
2944
if body: body = _closeiter(_rangeiter(body, offset, rlen), body.close)
2945
return HTTPResponse(body, status=206, **headers)
2946
return HTTPResponse(body, **headers)
2947
2948
###############################################################################
2949
# HTTP Utilities and MISC (TODO) ###############################################
2950
###############################################################################
2951
2952
2953
def debug(mode=True):
2954
""" Change the debug level.
2955
There is only one debug level supported at the moment."""
2956
global DEBUG
2957
if mode: warnings.simplefilter('default')
2958
DEBUG = bool(mode)
2959
2960
2961
def http_date(value):
2962
if isinstance(value, basestring):
2963
return value
2964
if isinstance(value, datetime):
2965
# aware datetime.datetime is converted to UTC time
2966
# naive datetime.datetime is treated as UTC time
2967
value = value.utctimetuple()
2968
elif isinstance(value, datedate):
2969
# datetime.date is naive, and is treated as UTC time
2970
value = value.timetuple()
2971
if not isinstance(value, (int, float)):
2972
# convert struct_time in UTC to UNIX timestamp
2973
value = calendar.timegm(value)
2974
return email.utils.formatdate(value, usegmt=True)
2975
2976
2977
def parse_date(ims):
2978
""" Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """
2979
try:
2980
ts = email.utils.parsedate_tz(ims)
2981
return calendar.timegm(ts[:8] + (0, )) - (ts[9] or 0)
2982
except (TypeError, ValueError, IndexError, OverflowError):
2983
return None
2984
2985
2986
def parse_auth(header):
2987
""" Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None"""
2988
try:
2989
method, data = header.split(None, 1)
2990
if method.lower() == 'basic':
2991
user, pwd = touni(base64.b64decode(tob(data))).split(':', 1)
2992
return user, pwd
2993
except (KeyError, ValueError):
2994
return None
2995
2996
2997
def parse_range_header(header, maxlen=0):
2998
""" Yield (start, end) ranges parsed from a HTTP Range header. Skip
2999
unsatisfiable ranges. The end index is non-inclusive."""
3000
if not header or header[:6] != 'bytes=': return
3001
ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r]
3002
for start, end in ranges:
3003
try:
3004
if not start: # bytes=-100 -> last 100 bytes
3005
start, end = max(0, maxlen - int(end)), maxlen
3006
elif not end: # bytes=100- -> all but the first 99 bytes
3007
start, end = int(start), maxlen
3008
else: # bytes=100-200 -> bytes 100-200 (inclusive)
3009
start, end = int(start), min(int(end) + 1, maxlen)
3010
if 0 <= start < end <= maxlen:
3011
yield start, end
3012
except ValueError:
3013
pass
3014
3015
3016
#: Header tokenizer used by _parse_http_header()
3017
_hsplit = re.compile('(?:(?:"((?:[^"\\\\]|\\\\.)*)")|([^;,=]+))([;,=]?)').findall
3018
3019
def _parse_http_header(h):
3020
""" Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values
3021
and parameters. For non-standard or broken input, this implementation may return partial results.
3022
:param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``)
3023
:return: List of (value, params) tuples. The second element is a (possibly empty) dict.
3024
"""
3025
values = []
3026
if '"' not in h: # INFO: Fast path without regexp (~2x faster)
3027
for value in h.split(','):
3028
parts = value.split(';')
3029
values.append((parts[0].strip(), {}))
3030
for attr in parts[1:]:
3031
name, value = attr.split('=', 1)
3032
values[-1][1][name.strip().lower()] = value.strip()
3033
else:
3034
lop, key, attrs = ',', None, {}
3035
for quoted, plain, tok in _hsplit(h):
3036
value = plain.strip() if plain else quoted.replace('\\"', '"')
3037
if lop == ',':
3038
attrs = {}
3039
values.append((value, attrs))
3040
elif lop == ';':
3041
if tok == '=':
3042
key = value
3043
else:
3044
attrs[value.strip().lower()] = ''
3045
elif lop == '=' and key:
3046
attrs[key.strip().lower()] = value
3047
key = None
3048
lop = tok
3049
return values
3050
3051
3052
def _parse_qsl(qs):
3053
r = []
3054
for pair in qs.split('&'):
3055
if not pair: continue
3056
nv = pair.split('=', 1)
3057
if len(nv) != 2: nv.append('')
3058
key = urlunquote(nv[0].replace('+', ' '))
3059
value = urlunquote(nv[1].replace('+', ' '))
3060
r.append((key, value))
3061
return r
3062
3063
3064
def _lscmp(a, b):
3065
""" Compares two strings in a cryptographically safe way:
3066
Runtime is not affected by length of common prefix. """
3067
return not sum(0 if x == y else 1
3068
for x, y in zip(a, b)) and len(a) == len(b)
3069
3070
3071
def cookie_encode(data, key, digestmod=None):
3072
""" Encode and sign a pickle-able object. Return a (byte) string """
3073
depr(0, 13, "cookie_encode() will be removed soon.",
3074
"Do not use this API directly.")
3075
digestmod = digestmod or hashlib.sha256
3076
msg = base64.b64encode(pickle.dumps(data, -1))
3077
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest())
3078
return tob('!') + sig + tob('?') + msg
3079
3080
3081
def cookie_decode(data, key, digestmod=None):
3082
""" Verify and decode an encoded string. Return an object or None."""
3083
depr(0, 13, "cookie_decode() will be removed soon.",
3084
"Do not use this API directly.")
3085
data = tob(data)
3086
if cookie_is_encoded(data):
3087
sig, msg = data.split(tob('?'), 1)
3088
digestmod = digestmod or hashlib.sha256
3089
hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest()
3090
if _lscmp(sig[1:], base64.b64encode(hashed)):
3091
return pickle.loads(base64.b64decode(msg))
3092
return None
3093
3094
3095
def cookie_is_encoded(data):
3096
""" Return True if the argument looks like a encoded cookie."""
3097
depr(0, 13, "cookie_is_encoded() will be removed soon.",
3098
"Do not use this API directly.")
3099
return bool(data.startswith(tob('!')) and tob('?') in data)
3100
3101
3102
def html_escape(string):
3103
""" Escape HTML special characters ``&<>`` and quotes ``'"``. """
3104
return string.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')\
3105
.replace('"', '&quot;').replace("'", '&#039;')
3106
3107
3108
def html_quote(string):
3109
""" Escape and quote a string to be used as an HTTP attribute."""
3110
return '"%s"' % html_escape(string).replace('\n', '&#10;')\
3111
.replace('\r', '&#13;').replace('\t', '&#9;')
3112
3113
3114
def yieldroutes(func):
3115
""" Return a generator for routes that match the signature (name, args)
3116
of the func parameter. This may yield more than one route if the function
3117
takes optional keyword arguments. The output is best described by example::
3118
3119
a() -> '/a'
3120
b(x, y) -> '/b/<x>/<y>'
3121
c(x, y=5) -> '/c/<x>' and '/c/<x>/<y>'
3122
d(x=5, y=6) -> '/d' and '/d/<x>' and '/d/<x>/<y>'
3123
"""
3124
path = '/' + func.__name__.replace('__', '/').lstrip('/')
3125
spec = getargspec(func)
3126
argc = len(spec[0]) - len(spec[3] or [])
3127
path += ('/<%s>' * argc) % tuple(spec[0][:argc])
3128
yield path
3129
for arg in spec[0][argc:]:
3130
path += '/<%s>' % arg
3131
yield path
3132
3133
3134
def path_shift(script_name, path_info, shift=1):
3135
""" Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa.
3136
3137
:return: The modified paths.
3138
:param script_name: The SCRIPT_NAME path.
3139
:param script_name: The PATH_INFO path.
3140
:param shift: The number of path fragments to shift. May be negative to
3141
change the shift direction. (default: 1)
3142
"""
3143
if shift == 0: return script_name, path_info
3144
pathlist = path_info.strip('/').split('/')
3145
scriptlist = script_name.strip('/').split('/')
3146
if pathlist and pathlist[0] == '': pathlist = []
3147
if scriptlist and scriptlist[0] == '': scriptlist = []
3148
if 0 < shift <= len(pathlist):
3149
moved = pathlist[:shift]
3150
scriptlist = scriptlist + moved
3151
pathlist = pathlist[shift:]
3152
elif 0 > shift >= -len(scriptlist):
3153
moved = scriptlist[shift:]
3154
pathlist = moved + pathlist
3155
scriptlist = scriptlist[:shift]
3156
else:
3157
empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO'
3158
raise AssertionError("Cannot shift. Nothing left from %s" % empty)
3159
new_script_name = '/' + '/'.join(scriptlist)
3160
new_path_info = '/' + '/'.join(pathlist)
3161
if path_info.endswith('/') and pathlist: new_path_info += '/'
3162
return new_script_name, new_path_info
3163
3164
3165
def auth_basic(check, realm="private", text="Access denied"):
3166
""" Callback decorator to require HTTP auth (basic).
3167
TODO: Add route(check_auth=...) parameter. """
3168
3169
def decorator(func):
3170
3171
@functools.wraps(func)
3172
def wrapper(*a, **ka):
3173
user, password = request.auth or (None, None)
3174
if user is None or not check(user, password):
3175
err = HTTPError(401, text)
3176
err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm)
3177
return err
3178
return func(*a, **ka)
3179
3180
return wrapper
3181
3182
return decorator
3183
3184
# Shortcuts for common Bottle methods.
3185
# They all refer to the current default application.
3186
3187
3188
def make_default_app_wrapper(name):
3189
""" Return a callable that relays calls to the current default app. """
3190
3191
@functools.wraps(getattr(Bottle, name))
3192
def wrapper(*a, **ka):
3193
return getattr(app(), name)(*a, **ka)
3194
3195
return wrapper
3196
3197
3198
route = make_default_app_wrapper('route')
3199
get = make_default_app_wrapper('get')
3200
post = make_default_app_wrapper('post')
3201
put = make_default_app_wrapper('put')
3202
delete = make_default_app_wrapper('delete')
3203
patch = make_default_app_wrapper('patch')
3204
error = make_default_app_wrapper('error')
3205
mount = make_default_app_wrapper('mount')
3206
hook = make_default_app_wrapper('hook')
3207
install = make_default_app_wrapper('install')
3208
uninstall = make_default_app_wrapper('uninstall')
3209
url = make_default_app_wrapper('get_url')
3210
3211
3212
###############################################################################
3213
# Multipart Handling ###########################################################
3214
###############################################################################
3215
# cgi.FieldStorage was deprecated in Python 3.11 and removed in 3.13
3216
# This implementation is based on https://github.com/defnull/multipart/
3217
3218
3219
class MultipartError(HTTPError):
3220
def __init__(self, msg):
3221
HTTPError.__init__(self, 400, "MultipartError: " + msg)
3222
3223
3224
class _MultipartParser(object):
3225
def __init__(
3226
self,
3227
stream,
3228
boundary,
3229
content_length=-1,
3230
disk_limit=2 ** 30,
3231
mem_limit=2 ** 20,
3232
memfile_limit=2 ** 18,
3233
buffer_size=2 ** 16,
3234
charset="latin1",
3235
):
3236
self.stream = stream
3237
self.boundary = boundary
3238
self.content_length = content_length
3239
self.disk_limit = disk_limit
3240
self.memfile_limit = memfile_limit
3241
self.mem_limit = min(mem_limit, self.disk_limit)
3242
self.buffer_size = min(buffer_size, self.mem_limit)
3243
self.charset = charset
3244
3245
if not boundary:
3246
raise MultipartError("No boundary.")
3247
3248
if self.buffer_size - 6 < len(boundary): # "--boundary--\r\n"
3249
raise MultipartError("Boundary does not fit into buffer_size.")
3250
3251
def _lineiter(self):
3252
""" Iterate over a binary file-like object (crlf terminated) line by
3253
line. Each line is returned as a (line, crlf) tuple. Lines larger
3254
than buffer_size are split into chunks where all but the last chunk
3255
has an empty string instead of crlf. Maximum chunk size is twice the
3256
buffer size.
3257
"""
3258
3259
read = self.stream.read
3260
maxread, maxbuf = self.content_length, self.buffer_size
3261
partial = b"" # Contains the last (partial) line
3262
3263
while True:
3264
chunk = read(maxbuf if maxread < 0 else min(maxbuf, maxread))
3265
maxread -= len(chunk)
3266
if not chunk:
3267
if partial:
3268
yield partial, b''
3269
break
3270
3271
if partial:
3272
chunk = partial + chunk
3273
3274
scanpos = 0
3275
while True:
3276
i = chunk.find(b'\r\n', scanpos)
3277
if i >= 0:
3278
yield chunk[scanpos:i], b'\r\n'
3279
scanpos = i + 2
3280
else: # CRLF not found
3281
partial = chunk[scanpos:] if scanpos else chunk
3282
break
3283
3284
if len(partial) > maxbuf:
3285
yield partial[:-1], b""
3286
partial = partial[-1:]
3287
3288
def parse(self):
3289
""" Return a MultiPart iterator. Can only be called once. """
3290
3291
lines, line = self._lineiter(), ""
3292
separator = b"--" + tob(self.boundary)
3293
terminator = separator + b"--"
3294
mem_used, disk_used = 0, 0 # Track used resources to prevent DoS
3295
is_tail = False # True if the last line was incomplete (cutted)
3296
3297
# Consume first boundary. Ignore any preamble, as required by RFC
3298
# 2046, section 5.1.1.
3299
for line, nl in lines:
3300
if line in (separator, terminator):
3301
break
3302
else:
3303
raise MultipartError("Stream does not contain boundary")
3304
3305
# First line is termainating boundary -> empty multipart stream
3306
if line == terminator:
3307
for _ in lines:
3308
raise MultipartError("Found data after empty multipart stream")
3309
return
3310
3311
part_options = {
3312
"buffer_size": self.buffer_size,
3313
"memfile_limit": self.memfile_limit,
3314
"charset": self.charset,
3315
}
3316
part = _MultipartPart(**part_options)
3317
3318
for line, nl in lines:
3319
if not is_tail and (line == separator or line == terminator):
3320
part.finish()
3321
if part.is_buffered():
3322
mem_used += part.size
3323
else:
3324
disk_used += part.size
3325
yield part
3326
if line == terminator:
3327
break
3328
part = _MultipartPart(**part_options)
3329
else:
3330
is_tail = not nl # The next line continues this one
3331
try:
3332
part.feed(line, nl)
3333
if part.is_buffered():
3334
if part.size + mem_used > self.mem_limit:
3335
raise MultipartError("Memory limit reached.")
3336
elif part.size + disk_used > self.disk_limit:
3337
raise MultipartError("Disk limit reached.")
3338
except MultipartError:
3339
part.close()
3340
raise
3341
else:
3342
part.close()
3343
3344
if line != terminator:
3345
raise MultipartError("Unexpected end of multipart stream.")
3346
3347
3348
class _MultipartPart(object):
3349
def __init__(self, buffer_size=2 ** 16, memfile_limit=2 ** 18, charset="latin1"):
3350
self.headerlist = []
3351
self.headers = None
3352
self.file = False
3353
self.size = 0
3354
self._buf = b""
3355
self.disposition = None
3356
self.name = None
3357
self.filename = None
3358
self.content_type = None
3359
self.charset = charset
3360
self.memfile_limit = memfile_limit
3361
self.buffer_size = buffer_size
3362
3363
def feed(self, line, nl=""):
3364
if self.file:
3365
return self.write_body(line, nl)
3366
return self.write_header(line, nl)
3367
3368
def write_header(self, line, nl):
3369
line = line.decode(self.charset)
3370
3371
if not nl:
3372
raise MultipartError("Unexpected end of line in header.")
3373
3374
if not line.strip(): # blank line -> end of header segment
3375
self.finish_header()
3376
elif line[0] in " \t" and self.headerlist:
3377
name, value = self.headerlist.pop()
3378
self.headerlist.append((name, value + line.strip()))
3379
else:
3380
if ":" not in line:
3381
raise MultipartError("Syntax error in header: No colon.")
3382
3383
name, value = line.split(":", 1)
3384
self.headerlist.append((name.strip(), value.strip()))
3385
3386
def write_body(self, line, nl):
3387
if not line and not nl:
3388
return # This does not even flush the buffer
3389
3390
self.size += len(line) + len(self._buf)
3391
self.file.write(self._buf + line)
3392
self._buf = nl
3393
3394
if self.content_length > 0 and self.size > self.content_length:
3395
raise MultipartError("Size of body exceeds Content-Length header.")
3396
3397
if self.size > self.memfile_limit and isinstance(self.file, BytesIO):
3398
self.file, old = NamedTemporaryFile(mode="w+b"), self.file
3399
old.seek(0)
3400
3401
copied, maxcopy, chunksize = 0, self.size, self.buffer_size
3402
read, write = old.read, self.file.write
3403
while copied < maxcopy:
3404
chunk = read(min(chunksize, maxcopy - copied))
3405
write(chunk)
3406
copied += len(chunk)
3407
3408
def finish_header(self):
3409
self.file = BytesIO()
3410
self.headers = HeaderDict(self.headerlist)
3411
content_disposition = self.headers.get("Content-Disposition")
3412
content_type = self.headers.get("Content-Type")
3413
3414
if not content_disposition:
3415
raise MultipartError("Content-Disposition header is missing.")
3416
3417
self.disposition, self.options = _parse_http_header(content_disposition)[0]
3418
self.name = self.options.get("name")
3419
if "filename" in self.options:
3420
self.filename = self.options.get("filename")
3421
if self.filename[1:3] == ":\\" or self.filename[:2] == "\\\\":
3422
self.filename = self.filename.split("\\")[-1] # ie6 bug
3423
3424
self.content_type, options = _parse_http_header(content_type)[0] if content_type else (None, {})
3425
self.charset = options.get("charset") or self.charset
3426
3427
self.content_length = int(self.headers.get("Content-Length", "-1"))
3428
3429
def finish(self):
3430
if not self.file:
3431
raise MultipartError("Incomplete part: Header section not closed.")
3432
self.file.seek(0)
3433
3434
def is_buffered(self):
3435
""" Return true if the data is fully buffered in memory."""
3436
return isinstance(self.file, BytesIO)
3437
3438
@property
3439
def value(self):
3440
""" Data decoded with the specified charset """
3441
3442
return self.raw.decode(self.charset)
3443
3444
@property
3445
def raw(self):
3446
""" Data without decoding """
3447
pos = self.file.tell()
3448
self.file.seek(0)
3449
3450
try:
3451
return self.file.read()
3452
finally:
3453
self.file.seek(pos)
3454
3455
def close(self):
3456
if self.file:
3457
self.file.close()
3458
self.file = False
3459
3460
###############################################################################
3461
# Server Adapter ###############################################################
3462
###############################################################################
3463
3464
# Before you edit or add a server adapter, please read:
3465
# - https://github.com/bottlepy/bottle/pull/647#issuecomment-60152870
3466
# - https://github.com/bottlepy/bottle/pull/865#issuecomment-242795341
3467
3468
class ServerAdapter(object):
3469
quiet = False
3470
3471
def __init__(self, host='127.0.0.1', port=8080, **options):
3472
self.options = options
3473
self.host = host
3474
self.port = int(port)
3475
3476
def run(self, handler): # pragma: no cover
3477
pass
3478
3479
def __repr__(self):
3480
args = ', '.join('%s=%s' % (k, repr(v))
3481
for k, v in self.options.items())
3482
return "%s(%s)" % (self.__class__.__name__, args)
3483
3484
3485
class CGIServer(ServerAdapter):
3486
quiet = True
3487
3488
def run(self, handler): # pragma: no cover
3489
from wsgiref.handlers import CGIHandler
3490
3491
def fixed_environ(environ, start_response):
3492
environ.setdefault('PATH_INFO', '')
3493
return handler(environ, start_response)
3494
3495
CGIHandler().run(fixed_environ)
3496
3497
3498
class FlupFCGIServer(ServerAdapter):
3499
def run(self, handler): # pragma: no cover
3500
import flup.server.fcgi
3501
self.options.setdefault('bindAddress', (self.host, self.port))
3502
flup.server.fcgi.WSGIServer(handler, **self.options).run()
3503
3504
3505
class WSGIRefServer(ServerAdapter):
3506
def run(self, app): # pragma: no cover
3507
from wsgiref.simple_server import make_server
3508
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer
3509
import socket
3510
3511
class FixedHandler(WSGIRequestHandler):
3512
def address_string(self): # Prevent reverse DNS lookups please.
3513
return self.client_address[0]
3514
3515
def log_request(*args, **kw):
3516
if not self.quiet:
3517
return WSGIRequestHandler.log_request(*args, **kw)
3518
3519
handler_cls = self.options.get('handler_class', FixedHandler)
3520
server_cls = self.options.get('server_class', WSGIServer)
3521
3522
if ':' in self.host: # Fix wsgiref for IPv6 addresses.
3523
if getattr(server_cls, 'address_family') == socket.AF_INET:
3524
3525
class server_cls(server_cls):
3526
address_family = socket.AF_INET6
3527
3528
self.srv = make_server(self.host, self.port, app, server_cls,
3529
handler_cls)
3530
self.port = self.srv.server_port # update port actual port (0 means random)
3531
try:
3532
self.srv.serve_forever()
3533
except KeyboardInterrupt:
3534
self.srv.server_close() # Prevent ResourceWarning: unclosed socket
3535
raise
3536
3537
3538
class CherryPyServer(ServerAdapter):
3539
def run(self, handler): # pragma: no cover
3540
depr(0, 13, "The wsgi server part of cherrypy was split into a new "
3541
"project called 'cheroot'.", "Use the 'cheroot' server "
3542
"adapter instead of cherrypy.")
3543
from cherrypy import wsgiserver # This will fail for CherryPy >= 9
3544
3545
self.options['bind_addr'] = (self.host, self.port)
3546
self.options['wsgi_app'] = handler
3547
3548
certfile = self.options.get('certfile')
3549
if certfile:
3550
del self.options['certfile']
3551
keyfile = self.options.get('keyfile')
3552
if keyfile:
3553
del self.options['keyfile']
3554
3555
server = wsgiserver.CherryPyWSGIServer(**self.options)
3556
if certfile:
3557
server.ssl_certificate = certfile
3558
if keyfile:
3559
server.ssl_private_key = keyfile
3560
3561
try:
3562
server.start()
3563
finally:
3564
server.stop()
3565
3566
3567
class CherootServer(ServerAdapter):
3568
def run(self, handler): # pragma: no cover
3569
from cheroot import wsgi
3570
from cheroot.ssl import builtin
3571
self.options['bind_addr'] = (self.host, self.port)
3572
self.options['wsgi_app'] = handler
3573
certfile = self.options.pop('certfile', None)
3574
keyfile = self.options.pop('keyfile', None)
3575
chainfile = self.options.pop('chainfile', None)
3576
server = wsgi.Server(**self.options)
3577
if certfile and keyfile:
3578
server.ssl_adapter = builtin.BuiltinSSLAdapter(
3579
certfile, keyfile, chainfile)
3580
try:
3581
server.start()
3582
finally:
3583
server.stop()
3584
3585
3586
class WaitressServer(ServerAdapter):
3587
def run(self, handler):
3588
from waitress import serve
3589
serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options)
3590
3591
3592
class PasteServer(ServerAdapter):
3593
def run(self, handler): # pragma: no cover
3594
from paste import httpserver
3595
from paste.translogger import TransLogger
3596
handler = TransLogger(handler, setup_console_handler=(not self.quiet))
3597
httpserver.serve(handler,
3598
host=self.host,
3599
port=str(self.port), **self.options)
3600
3601
3602
class MeinheldServer(ServerAdapter):
3603
def run(self, handler):
3604
from meinheld import server
3605
server.listen((self.host, self.port))
3606
server.run(handler)
3607
3608
3609
class FapwsServer(ServerAdapter):
3610
""" Extremely fast webserver using libev. See https://github.com/william-os4y/fapws3 """
3611
3612
def run(self, handler): # pragma: no cover
3613
depr(0, 13, "fapws3 is not maintained and support will be dropped.")
3614
import fapws._evwsgi as evwsgi
3615
from fapws import base, config
3616
port = self.port
3617
if float(config.SERVER_IDENT[-2:]) > 0.4:
3618
# fapws3 silently changed its API in 0.5
3619
port = str(port)
3620
evwsgi.start(self.host, port)
3621
# fapws3 never releases the GIL. Complain upstream. I tried. No luck.
3622
if 'BOTTLE_CHILD' in os.environ and not self.quiet:
3623
_stderr("WARNING: Auto-reloading does not work with Fapws3.")
3624
_stderr(" (Fapws3 breaks python thread support)")
3625
evwsgi.set_base_module(base)
3626
3627
def app(environ, start_response):
3628
environ['wsgi.multiprocess'] = False
3629
return handler(environ, start_response)
3630
3631
evwsgi.wsgi_cb(('', app))
3632
evwsgi.run()
3633
3634
3635
class TornadoServer(ServerAdapter):
3636
""" The super hyped asynchronous server by facebook. Untested. """
3637
3638
def run(self, handler): # pragma: no cover
3639
import tornado.wsgi, tornado.httpserver, tornado.ioloop
3640
container = tornado.wsgi.WSGIContainer(handler)
3641
server = tornado.httpserver.HTTPServer(container)
3642
server.listen(port=self.port, address=self.host)
3643
tornado.ioloop.IOLoop.instance().start()
3644
3645
3646
class AppEngineServer(ServerAdapter):
3647
""" Adapter for Google App Engine. """
3648
quiet = True
3649
3650
def run(self, handler):
3651
depr(0, 13, "AppEngineServer no longer required",
3652
"Configure your application directly in your app.yaml")
3653
from google.appengine.ext.webapp import util
3654
# A main() function in the handler script enables 'App Caching'.
3655
# Lets makes sure it is there. This _really_ improves performance.
3656
module = sys.modules.get('__main__')
3657
if module and not hasattr(module, 'main'):
3658
module.main = lambda: util.run_wsgi_app(handler)
3659
util.run_wsgi_app(handler)
3660
3661
3662
class TwistedServer(ServerAdapter):
3663
""" Untested. """
3664
3665
def run(self, handler):
3666
from twisted.web import server, wsgi
3667
from twisted.python.threadpool import ThreadPool
3668
from twisted.internet import reactor
3669
thread_pool = ThreadPool()
3670
thread_pool.start()
3671
reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop)
3672
factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler))
3673
reactor.listenTCP(self.port, factory, interface=self.host)
3674
if not reactor.running:
3675
reactor.run()
3676
3677
3678
class DieselServer(ServerAdapter):
3679
""" Untested. """
3680
3681
def run(self, handler):
3682
depr(0, 13, "Diesel is not tested or supported and will be removed.")
3683
from diesel.protocols.wsgi import WSGIApplication
3684
app = WSGIApplication(handler, port=self.port)
3685
app.run()
3686
3687
3688
class GeventServer(ServerAdapter):
3689
""" Untested. Options:
3690
3691
* See gevent.wsgi.WSGIServer() documentation for more options.
3692
"""
3693
3694
def run(self, handler):
3695
from gevent import pywsgi, local
3696
if not isinstance(threading.local(), local.local):
3697
msg = "Bottle requires gevent.monkey.patch_all() (before import)"
3698
raise RuntimeError(msg)
3699
if self.quiet:
3700
self.options['log'] = None
3701
address = (self.host, self.port)
3702
server = pywsgi.WSGIServer(address, handler, **self.options)
3703
if 'BOTTLE_CHILD' in os.environ:
3704
import signal
3705
signal.signal(signal.SIGINT, lambda s, f: server.stop())
3706
server.serve_forever()
3707
3708
3709
class GunicornServer(ServerAdapter):
3710
""" Untested. See http://gunicorn.org/configure.html for options. """
3711
3712
def run(self, handler):
3713
from gunicorn.app.base import BaseApplication
3714
3715
if self.host.startswith("unix:"):
3716
config = {'bind': self.host}
3717
else:
3718
config = {'bind': "%s:%d" % (self.host, self.port)}
3719
3720
config.update(self.options)
3721
3722
class GunicornApplication(BaseApplication):
3723
def load_config(self):
3724
for key, value in config.items():
3725
self.cfg.set(key, value)
3726
3727
def load(self):
3728
return handler
3729
3730
GunicornApplication().run()
3731
3732
3733
class EventletServer(ServerAdapter):
3734
""" Untested. Options:
3735
3736
* `backlog` adjust the eventlet backlog parameter which is the maximum
3737
number of queued connections. Should be at least 1; the maximum
3738
value is system-dependent.
3739
* `family`: (default is 2) socket family, optional. See socket
3740
documentation for available families.
3741
"""
3742
3743
def run(self, handler):
3744
from eventlet import wsgi, listen, patcher
3745
if not patcher.is_monkey_patched(os):
3746
msg = "Bottle requires eventlet.monkey_patch() (before import)"
3747
raise RuntimeError(msg)
3748
socket_args = {}
3749
for arg in ('backlog', 'family'):
3750
try:
3751
socket_args[arg] = self.options.pop(arg)
3752
except KeyError:
3753
pass
3754
address = (self.host, self.port)
3755
try:
3756
wsgi.server(listen(address, **socket_args), handler,
3757
log_output=(not self.quiet))
3758
except TypeError:
3759
# Fallback, if we have old version of eventlet
3760
wsgi.server(listen(address), handler)
3761
3762
3763
class BjoernServer(ServerAdapter):
3764
""" Fast server written in C: https://github.com/jonashaag/bjoern """
3765
3766
def run(self, handler):
3767
from bjoern import run
3768
run(handler, self.host, self.port, reuse_port=True)
3769
3770
class AsyncioServerAdapter(ServerAdapter):
3771
""" Extend ServerAdapter for adding custom event loop """
3772
def get_event_loop(self):
3773
pass
3774
3775
class AiohttpServer(AsyncioServerAdapter):
3776
""" Asynchronous HTTP client/server framework for asyncio
3777
https://pypi.python.org/pypi/aiohttp/
3778
https://pypi.org/project/aiohttp-wsgi/
3779
"""
3780
3781
def get_event_loop(self):
3782
import asyncio
3783
return asyncio.new_event_loop()
3784
3785
def run(self, handler):
3786
import asyncio
3787
from aiohttp_wsgi.wsgi import serve
3788
self.loop = self.get_event_loop()
3789
asyncio.set_event_loop(self.loop)
3790
3791
if 'BOTTLE_CHILD' in os.environ:
3792
import signal
3793
signal.signal(signal.SIGINT, lambda s, f: self.loop.stop())
3794
3795
serve(handler, host=self.host, port=self.port)
3796
3797
3798
class AiohttpUVLoopServer(AiohttpServer):
3799
"""uvloop
3800
https://github.com/MagicStack/uvloop
3801
"""
3802
def get_event_loop(self):
3803
import uvloop
3804
return uvloop.new_event_loop()
3805
3806
class AutoServer(ServerAdapter):
3807
""" Untested. """
3808
adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer,
3809
CherootServer, WSGIRefServer]
3810
3811
def run(self, handler):
3812
for sa in self.adapters:
3813
try:
3814
return sa(self.host, self.port, **self.options).run(handler)
3815
except ImportError:
3816
pass
3817
3818
3819
server_names = {
3820
'cgi': CGIServer,
3821
'flup': FlupFCGIServer,
3822
'wsgiref': WSGIRefServer,
3823
'waitress': WaitressServer,
3824
'cherrypy': CherryPyServer,
3825
'cheroot': CherootServer,
3826
'paste': PasteServer,
3827
'fapws3': FapwsServer,
3828
'tornado': TornadoServer,
3829
'gae': AppEngineServer,
3830
'twisted': TwistedServer,
3831
'diesel': DieselServer,
3832
'meinheld': MeinheldServer,
3833
'gunicorn': GunicornServer,
3834
'eventlet': EventletServer,
3835
'gevent': GeventServer,
3836
'bjoern': BjoernServer,
3837
'aiohttp': AiohttpServer,
3838
'uvloop': AiohttpUVLoopServer,
3839
'auto': AutoServer,
3840
}
3841
3842
###############################################################################
3843
# Application Control ##########################################################
3844
###############################################################################
3845
3846
3847
def load(target, **namespace):
3848
""" Import a module or fetch an object from a module.
3849
3850
* ``package.module`` returns `module` as a module object.
3851
* ``pack.mod:name`` returns the module variable `name` from `pack.mod`.
3852
* ``pack.mod:func()`` calls `pack.mod.func()` and returns the result.
3853
3854
The last form accepts not only function calls, but any type of
3855
expression. Keyword arguments passed to this function are available as
3856
local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``
3857
"""
3858
module, target = target.split(":", 1) if ':' in target else (target, None)
3859
if module not in sys.modules: __import__(module)
3860
if not target: return sys.modules[module]
3861
if target.isalnum(): return getattr(sys.modules[module], target)
3862
package_name = module.split('.')[0]
3863
namespace[package_name] = sys.modules[package_name]
3864
return eval('%s.%s' % (module, target), namespace)
3865
3866
3867
def load_app(target):
3868
""" Load a bottle application from a module and make sure that the import
3869
does not affect the current default application, but returns a separate
3870
application object. See :func:`load` for the target parameter. """
3871
global NORUN
3872
NORUN, nr_old = True, NORUN
3873
tmp = default_app.push() # Create a new "default application"
3874
try:
3875
rv = load(target) # Import the target module
3876
return rv if callable(rv) else tmp
3877
finally:
3878
default_app.remove(tmp) # Remove the temporary added default application
3879
NORUN = nr_old
3880
3881
3882
_debug = debug
3883
3884
3885
def run(app=None,
3886
server='wsgiref',
3887
host='127.0.0.1',
3888
port=8080,
3889
interval=1,
3890
reloader=False,
3891
quiet=False,
3892
plugins=None,
3893
debug=None,
3894
config=None, **kargs):
3895
""" Start a server instance. This method blocks until the server terminates.
3896
3897
:param app: WSGI application or target string supported by
3898
:func:`load_app`. (default: :func:`default_app`)
3899
:param server: Server adapter to use. See :data:`server_names` keys
3900
for valid names or pass a :class:`ServerAdapter` subclass.
3901
(default: `wsgiref`)
3902
:param host: Server address to bind to. Pass ``0.0.0.0`` to listens on
3903
all interfaces including the external one. (default: 127.0.0.1)
3904
:param port: Server port to bind to. Values below 1024 require root
3905
privileges. (default: 8080)
3906
:param reloader: Start auto-reloading server? (default: False)
3907
:param interval: Auto-reloader interval in seconds (default: 1)
3908
:param quiet: Suppress output to stdout and stderr? (default: False)
3909
:param options: Options passed to the server adapter.
3910
"""
3911
if NORUN: return
3912
if reloader and not os.environ.get('BOTTLE_CHILD'):
3913
import subprocess
3914
fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
3915
environ = os.environ.copy()
3916
environ['BOTTLE_CHILD'] = 'true'
3917
environ['BOTTLE_LOCKFILE'] = lockfile
3918
args = [sys.executable] + sys.argv
3919
# If a package was loaded with `python -m`, then `sys.argv` needs to be
3920
# restored to the original value, or imports might break. See #1336
3921
if getattr(sys.modules.get('__main__'), '__package__', None):
3922
args[1:1] = ["-m", sys.modules['__main__'].__package__]
3923
3924
try:
3925
os.close(fd) # We never write to this file
3926
while os.path.exists(lockfile):
3927
p = subprocess.Popen(args, env=environ)
3928
while p.poll() is None:
3929
os.utime(lockfile, None) # Tell child we are still alive
3930
time.sleep(interval)
3931
if p.returncode == 3: # Child wants to be restarted
3932
continue
3933
sys.exit(p.returncode)
3934
except KeyboardInterrupt:
3935
pass
3936
finally:
3937
if os.path.exists(lockfile):
3938
os.unlink(lockfile)
3939
return
3940
3941
try:
3942
if debug is not None: _debug(debug)
3943
app = app or default_app()
3944
if isinstance(app, basestring):
3945
app = load_app(app)
3946
if not callable(app):
3947
raise ValueError("Application is not callable: %r" % app)
3948
3949
for plugin in plugins or []:
3950
if isinstance(plugin, basestring):
3951
plugin = load(plugin)
3952
app.install(plugin)
3953
3954
if config:
3955
app.config.update(config)
3956
3957
if server in server_names:
3958
server = server_names.get(server)
3959
if isinstance(server, basestring):
3960
server = load(server)
3961
if isinstance(server, type):
3962
server = server(host=host, port=port, **kargs)
3963
if not isinstance(server, ServerAdapter):
3964
raise ValueError("Unknown or unsupported server: %r" % server)
3965
3966
server.quiet = server.quiet or quiet
3967
if not server.quiet:
3968
_stderr("Bottle v%s server starting up (using %s)..." %
3969
(__version__, repr(server)))
3970
if server.host.startswith("unix:"):
3971
_stderr("Listening on %s" % server.host)
3972
else:
3973
_stderr("Listening on http://%s:%d/" %
3974
(server.host, server.port))
3975
_stderr("Hit Ctrl-C to quit.\n")
3976
3977
if reloader:
3978
lockfile = os.environ.get('BOTTLE_LOCKFILE')
3979
bgcheck = FileCheckerThread(lockfile, interval)
3980
with bgcheck:
3981
server.run(app)
3982
if bgcheck.status == 'reload':
3983
sys.exit(3)
3984
else:
3985
server.run(app)
3986
except KeyboardInterrupt:
3987
pass
3988
except (SystemExit, MemoryError):
3989
raise
3990
except:
3991
if not reloader: raise
3992
if not getattr(server, 'quiet', quiet):
3993
print_exc()
3994
time.sleep(interval)
3995
sys.exit(3)
3996
3997
3998
class FileCheckerThread(threading.Thread):
3999
""" Interrupt main-thread as soon as a changed module file is detected,
4000
the lockfile gets deleted or gets too old. """
4001
4002
def __init__(self, lockfile, interval):
4003
threading.Thread.__init__(self)
4004
self.daemon = True
4005
self.lockfile, self.interval = lockfile, interval
4006
#: Is one of 'reload', 'error' or 'exit'
4007
self.status = None
4008
4009
def run(self):
4010
exists = os.path.exists
4011
mtime = lambda p: os.stat(p).st_mtime
4012
files = dict()
4013
4014
for module in list(sys.modules.values()):
4015
path = getattr(module, '__file__', '') or ''
4016
if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
4017
if path and exists(path): files[path] = mtime(path)
4018
4019
while not self.status:
4020
if not exists(self.lockfile)\
4021
or mtime(self.lockfile) < time.time() - self.interval - 5:
4022
self.status = 'error'
4023
thread.interrupt_main()
4024
for path, lmtime in list(files.items()):
4025
if not exists(path) or mtime(path) > lmtime:
4026
self.status = 'reload'
4027
thread.interrupt_main()
4028
break
4029
time.sleep(self.interval)
4030
4031
def __enter__(self):
4032
self.start()
4033
4034
def __exit__(self, exc_type, *_):
4035
if not self.status: self.status = 'exit' # silent exit
4036
self.join()
4037
return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)
4038
4039
###############################################################################
4040
# Template Adapters ############################################################
4041
###############################################################################
4042
4043
4044
class TemplateError(BottleException):
4045
pass
4046
4047
4048
class BaseTemplate(object):
4049
""" Base class and minimal API for template adapters """
4050
extensions = ['tpl', 'html', 'thtml', 'stpl']
4051
settings = {} #used in prepare()
4052
defaults = {} #used in render()
4053
4054
def __init__(self,
4055
source=None,
4056
name=None,
4057
lookup=None,
4058
encoding='utf8', **settings):
4059
""" Create a new template.
4060
If the source parameter (str or buffer) is missing, the name argument
4061
is used to guess a template filename. Subclasses can assume that
4062
self.source and/or self.filename are set. Both are strings.
4063
The lookup, encoding and settings parameters are stored as instance
4064
variables.
4065
The lookup parameter stores a list containing directory paths.
4066
The encoding parameter should be used to decode byte strings or files.
4067
The settings parameter contains a dict for engine-specific settings.
4068
"""
4069
self.name = name
4070
self.source = source.read() if hasattr(source, 'read') else source
4071
self.filename = source.filename if hasattr(source, 'filename') else None
4072
self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
4073
self.encoding = encoding
4074
self.settings = self.settings.copy() # Copy from class variable
4075
self.settings.update(settings) # Apply
4076
if not self.source and self.name:
4077
self.filename = self.search(self.name, self.lookup)
4078
if not self.filename:
4079
raise TemplateError('Template %s not found.' % repr(name))
4080
if not self.source and not self.filename:
4081
raise TemplateError('No template specified.')
4082
self.prepare(**self.settings)
4083
4084
@classmethod
4085
def search(cls, name, lookup=None):
4086
""" Search name in all directories specified in lookup.
4087
First without, then with common extensions. Return first hit. """
4088
if not lookup:
4089
raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.")
4090
4091
if os.path.isabs(name):
4092
raise depr(0, 12, "Use of absolute path for template name.",
4093
"Refer to templates with names or paths relative to the lookup path.")
4094
4095
for spath in lookup:
4096
spath = os.path.abspath(spath) + os.sep
4097
fname = os.path.abspath(os.path.join(spath, name))
4098
if not fname.startswith(spath): continue
4099
if os.path.isfile(fname): return fname
4100
for ext in cls.extensions:
4101
if os.path.isfile('%s.%s' % (fname, ext)):
4102
return '%s.%s' % (fname, ext)
4103
4104
@classmethod
4105
def global_config(cls, key, *args):
4106
""" This reads or sets the global settings stored in class.settings. """
4107
if args:
4108
cls.settings = cls.settings.copy() # Make settings local to class
4109
cls.settings[key] = args[0]
4110
else:
4111
return cls.settings[key]
4112
4113
def prepare(self, **options):
4114
""" Run preparations (parsing, caching, ...).
4115
It should be possible to call this again to refresh a template or to
4116
update settings.
4117
"""
4118
raise NotImplementedError
4119
4120
def render(self, *args, **kwargs):
4121
""" Render the template with the specified local variables and return
4122
a single byte or unicode string. If it is a byte string, the encoding
4123
must match self.encoding. This method must be thread-safe!
4124
Local variables may be provided in dictionaries (args)
4125
or directly, as keywords (kwargs).
4126
"""
4127
raise NotImplementedError
4128
4129
4130
class MakoTemplate(BaseTemplate):
4131
def prepare(self, **options):
4132
from mako.template import Template
4133
from mako.lookup import TemplateLookup
4134
options.update({'input_encoding': self.encoding})
4135
options.setdefault('format_exceptions', bool(DEBUG))
4136
lookup = TemplateLookup(directories=self.lookup, **options)
4137
if self.source:
4138
self.tpl = Template(self.source, lookup=lookup, **options)
4139
else:
4140
self.tpl = Template(uri=self.name,
4141
filename=self.filename,
4142
lookup=lookup, **options)
4143
4144
def render(self, *args, **kwargs):
4145
for dictarg in args:
4146
kwargs.update(dictarg)
4147
_defaults = self.defaults.copy()
4148
_defaults.update(kwargs)
4149
return self.tpl.render(**_defaults)
4150
4151
4152
class CheetahTemplate(BaseTemplate):
4153
def prepare(self, **options):
4154
from Cheetah.Template import Template
4155
self.context = threading.local()
4156
self.context.vars = {}
4157
options['searchList'] = [self.context.vars]
4158
if self.source:
4159
self.tpl = Template(source=self.source, **options)
4160
else:
4161
self.tpl = Template(file=self.filename, **options)
4162
4163
def render(self, *args, **kwargs):
4164
for dictarg in args:
4165
kwargs.update(dictarg)
4166
self.context.vars.update(self.defaults)
4167
self.context.vars.update(kwargs)
4168
out = str(self.tpl)
4169
self.context.vars.clear()
4170
return out
4171
4172
4173
class Jinja2Template(BaseTemplate):
4174
def prepare(self, filters=None, tests=None, globals={}, **kwargs):
4175
from jinja2 import Environment, FunctionLoader
4176
self.env = Environment(loader=FunctionLoader(self.loader), **kwargs)
4177
if filters: self.env.filters.update(filters)
4178
if tests: self.env.tests.update(tests)
4179
if globals: self.env.globals.update(globals)
4180
if self.source:
4181
self.tpl = self.env.from_string(self.source)
4182
else:
4183
self.tpl = self.env.get_template(self.name)
4184
4185
def render(self, *args, **kwargs):
4186
for dictarg in args:
4187
kwargs.update(dictarg)
4188
_defaults = self.defaults.copy()
4189
_defaults.update(kwargs)
4190
return self.tpl.render(**_defaults)
4191
4192
def loader(self, name):
4193
if name == self.filename:
4194
fname = name
4195
else:
4196
fname = self.search(name, self.lookup)
4197
if not fname: return
4198
with open(fname, "rb") as f:
4199
return (f.read().decode(self.encoding), fname, lambda: False)
4200
4201
4202
class SimpleTemplate(BaseTemplate):
4203
def prepare(self,
4204
escape_func=html_escape,
4205
noescape=False,
4206
syntax=None, **ka):
4207
self.cache = {}
4208
enc = self.encoding
4209
self._str = lambda x: touni(x, enc)
4210
self._escape = lambda x: escape_func(touni(x, enc))
4211
self.syntax = syntax
4212
if noescape:
4213
self._str, self._escape = self._escape, self._str
4214
4215
@cached_property
4216
def co(self):
4217
return compile(self.code, self.filename or '<string>', 'exec')
4218
4219
@cached_property
4220
def code(self):
4221
source = self.source
4222
if not source:
4223
with open(self.filename, 'rb') as f:
4224
source = f.read()
4225
try:
4226
source, encoding = touni(source), 'utf8'
4227
except UnicodeError:
4228
raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.')
4229
parser = StplParser(source, encoding=encoding, syntax=self.syntax)
4230
code = parser.translate()
4231
self.encoding = parser.encoding
4232
return code
4233
4234
def _rebase(self, _env, _name=None, **kwargs):
4235
_env['_rebase'] = (_name, kwargs)
4236
4237
def _include(self, _env, _name=None, **kwargs):
4238
env = _env.copy()
4239
env.update(kwargs)
4240
if _name not in self.cache:
4241
self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax)
4242
return self.cache[_name].execute(env['_stdout'], env)
4243
4244
def execute(self, _stdout, kwargs):
4245
env = self.defaults.copy()
4246
env.update(kwargs)
4247
env.update({
4248
'_stdout': _stdout,
4249
'_printlist': _stdout.extend,
4250
'include': functools.partial(self._include, env),
4251
'rebase': functools.partial(self._rebase, env),
4252
'_rebase': None,
4253
'_str': self._str,
4254
'_escape': self._escape,
4255
'get': env.get,
4256
'setdefault': env.setdefault,
4257
'defined': env.__contains__
4258
})
4259
exec(self.co, env)
4260
if env.get('_rebase'):
4261
subtpl, rargs = env.pop('_rebase')
4262
rargs['base'] = ''.join(_stdout) #copy stdout
4263
del _stdout[:] # clear stdout
4264
return self._include(env, subtpl, **rargs)
4265
return env
4266
4267
def render(self, *args, **kwargs):
4268
""" Render the template using keyword arguments as local variables. """
4269
env = {}
4270
stdout = []
4271
for dictarg in args:
4272
env.update(dictarg)
4273
env.update(kwargs)
4274
self.execute(stdout, env)
4275
return ''.join(stdout)
4276
4277
4278
class StplSyntaxError(TemplateError):
4279
pass
4280
4281
4282
class StplParser(object):
4283
""" Parser for stpl templates. """
4284
_re_cache = {} #: Cache for compiled re patterns
4285
4286
# This huge pile of voodoo magic splits python code into 8 different tokens.
4287
# We use the verbose (?x) regex mode to make this more manageable
4288
4289
_re_tok = r'''(
4290
[urbURB]*
4291
(?: ''(?!')
4292
|""(?!")
4293
|'{6}
4294
|"{6}
4295
|'(?:[^\\']|\\.)+?'
4296
|"(?:[^\\"]|\\.)+?"
4297
|'{3}(?:[^\\]|\\.|\n)+?'{3}
4298
|"{3}(?:[^\\]|\\.|\n)+?"{3}
4299
)
4300
)'''
4301
4302
_re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later
4303
4304
_re_tok += r'''
4305
# 2: Comments (until end of line, but not the newline itself)
4306
|(\#.*)
4307
4308
# 3: Open and close (4) grouping tokens
4309
|([\[\{\(])
4310
|([\]\}\)])
4311
4312
# 5,6: Keywords that start or continue a python block (only start of line)
4313
|^([\ \t]*(?:if|for|while|with|try|def|class)\b)
4314
|^([\ \t]*(?:elif|else|except|finally)\b)
4315
4316
# 7: Our special 'end' keyword (but only if it stands alone)
4317
|((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#))
4318
4319
# 8: A customizable end-of-code-block template token (only end of line)
4320
|(%(block_close)s[\ \t]*(?=\r?$))
4321
4322
# 9: And finally, a single newline. The 10th token is 'everything else'
4323
|(\r?\n)
4324
'''
4325
4326
# Match the start tokens of code areas in a template
4327
_re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))'''
4328
# Match inline statements (may contain python strings)
4329
_re_inl = r'''%%(inline_start)s((?:%s|[^'"\n])*?)%%(inline_end)s''' % _re_inl
4330
4331
# add the flag in front of the regexp to avoid Deprecation warning (see Issue #949)
4332
# verbose and dot-matches-newline mode
4333
_re_tok = '(?mx)' + _re_tok
4334
_re_inl = '(?mx)' + _re_inl
4335
4336
4337
default_syntax = '<% %> % {{ }}'
4338
4339
def __init__(self, source, syntax=None, encoding='utf8'):
4340
self.source, self.encoding = touni(source, encoding), encoding
4341
self.set_syntax(syntax or self.default_syntax)
4342
self.code_buffer, self.text_buffer = [], []
4343
self.lineno, self.offset = 1, 0
4344
self.indent, self.indent_mod = 0, 0
4345
self.paren_depth = 0
4346
4347
def get_syntax(self):
4348
""" Tokens as a space separated string (default: <% %> % {{ }}) """
4349
return self._syntax
4350
4351
def set_syntax(self, syntax):
4352
self._syntax = syntax
4353
self._tokens = syntax.split()
4354
if syntax not in self._re_cache:
4355
names = 'block_start block_close line_start inline_start inline_end'
4356
etokens = map(re.escape, self._tokens)
4357
pattern_vars = dict(zip(names.split(), etokens))
4358
patterns = (self._re_split, self._re_tok, self._re_inl)
4359
patterns = [re.compile(p % pattern_vars) for p in patterns]
4360
self._re_cache[syntax] = patterns
4361
self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax]
4362
4363
syntax = property(get_syntax, set_syntax)
4364
4365
def translate(self):
4366
if self.offset: raise RuntimeError('Parser is a one time instance.')
4367
while True:
4368
m = self.re_split.search(self.source, pos=self.offset)
4369
if m:
4370
text = self.source[self.offset:m.start()]
4371
self.text_buffer.append(text)
4372
self.offset = m.end()
4373
if m.group(1): # Escape syntax
4374
line, sep, _ = self.source[self.offset:].partition('\n')
4375
self.text_buffer.append(self.source[m.start():m.start(1)] +
4376
m.group(2) + line + sep)
4377
self.offset += len(line + sep)
4378
continue
4379
self.flush_text()
4380
self.offset += self.read_code(self.source[self.offset:],
4381
multiline=bool(m.group(4)))
4382
else:
4383
break
4384
self.text_buffer.append(self.source[self.offset:])
4385
self.flush_text()
4386
return ''.join(self.code_buffer)
4387
4388
def read_code(self, pysource, multiline):
4389
code_line, comment = '', ''
4390
offset = 0
4391
while True:
4392
m = self.re_tok.search(pysource, pos=offset)
4393
if not m:
4394
code_line += pysource[offset:]
4395
offset = len(pysource)
4396
self.write_code(code_line.strip(), comment)
4397
break
4398
code_line += pysource[offset:m.start()]
4399
offset = m.end()
4400
_str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups()
4401
if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c
4402
code_line += _blk1 or _blk2
4403
continue
4404
if _str: # Python string
4405
code_line += _str
4406
elif _com: # Python comment (up to EOL)
4407
comment = _com
4408
if multiline and _com.strip().endswith(self._tokens[1]):
4409
multiline = False # Allow end-of-block in comments
4410
elif _po: # open parenthesis
4411
self.paren_depth += 1
4412
code_line += _po
4413
elif _pc: # close parenthesis
4414
if self.paren_depth > 0:
4415
# we could check for matching parentheses here, but it's
4416
# easier to leave that to python - just check counts
4417
self.paren_depth -= 1
4418
code_line += _pc
4419
elif _blk1: # Start-block keyword (if/for/while/def/try/...)
4420
code_line = _blk1
4421
self.indent += 1
4422
self.indent_mod -= 1
4423
elif _blk2: # Continue-block keyword (else/elif/except/...)
4424
code_line = _blk2
4425
self.indent_mod -= 1
4426
elif _cend: # The end-code-block template token (usually '%>')
4427
if multiline: multiline = False
4428
else: code_line += _cend
4429
elif _end:
4430
self.indent -= 1
4431
self.indent_mod += 1
4432
else: # \n
4433
self.write_code(code_line.strip(), comment)
4434
self.lineno += 1
4435
code_line, comment, self.indent_mod = '', '', 0
4436
if not multiline:
4437
break
4438
4439
return offset
4440
4441
def flush_text(self):
4442
text = ''.join(self.text_buffer)
4443
del self.text_buffer[:]
4444
if not text: return
4445
parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent
4446
for m in self.re_inl.finditer(text):
4447
prefix, pos = text[pos:m.start()], m.end()
4448
if prefix:
4449
parts.append(nl.join(map(repr, prefix.splitlines(True))))
4450
if prefix.endswith('\n'): parts[-1] += nl
4451
parts.append(self.process_inline(m.group(1).strip()))
4452
if pos < len(text):
4453
prefix = text[pos:]
4454
lines = prefix.splitlines(True)
4455
if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3]
4456
elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4]
4457
parts.append(nl.join(map(repr, lines)))
4458
code = '_printlist((%s,))' % ', '.join(parts)
4459
self.lineno += code.count('\n') + 1
4460
self.write_code(code)
4461
4462
@staticmethod
4463
def process_inline(chunk):
4464
if chunk[0] == '!': return '_str(%s)' % chunk[1:]
4465
return '_escape(%s)' % chunk
4466
4467
def write_code(self, line, comment=''):
4468
code = ' ' * (self.indent + self.indent_mod)
4469
code += line.lstrip() + comment + '\n'
4470
self.code_buffer.append(code)
4471
4472
4473
def template(*args, **kwargs):
4474
"""
4475
Get a rendered template as a string iterator.
4476
You can use a name, a filename or a template string as first parameter.
4477
Template rendering arguments can be passed as dictionaries
4478
or directly (as keyword arguments).
4479
"""
4480
tpl = args[0] if args else None
4481
for dictarg in args[1:]:
4482
kwargs.update(dictarg)
4483
adapter = kwargs.pop('template_adapter', SimpleTemplate)
4484
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
4485
tplid = (id(lookup), tpl)
4486
if tplid not in TEMPLATES or DEBUG:
4487
settings = kwargs.pop('template_settings', {})
4488
if isinstance(tpl, adapter):
4489
TEMPLATES[tplid] = tpl
4490
if settings: TEMPLATES[tplid].prepare(**settings)
4491
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
4492
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
4493
else:
4494
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
4495
if not TEMPLATES[tplid]:
4496
abort(500, 'Template (%s) not found' % tpl)
4497
return TEMPLATES[tplid].render(kwargs)
4498
4499
4500
mako_template = functools.partial(template, template_adapter=MakoTemplate)
4501
cheetah_template = functools.partial(template,
4502
template_adapter=CheetahTemplate)
4503
jinja2_template = functools.partial(template, template_adapter=Jinja2Template)
4504
4505
4506
def view(tpl_name, **defaults):
4507
""" Decorator: renders a template for a handler.
4508
The handler can control its behavior like that:
4509
4510
- return a dict of template vars to fill out the template
4511
- return something other than a dict and the view decorator will not
4512
process the template, but return the handler result as is.
4513
This includes returning a HTTPResponse(dict) to get,
4514
for instance, JSON with autojson or other castfilters.
4515
"""
4516
4517
def decorator(func):
4518
4519
@functools.wraps(func)
4520
def wrapper(*args, **kwargs):
4521
result = func(*args, **kwargs)
4522
if isinstance(result, (dict, DictMixin)):
4523
tplvars = defaults.copy()
4524
tplvars.update(result)
4525
return template(tpl_name, **tplvars)
4526
elif result is None:
4527
return template(tpl_name, **defaults)
4528
return result
4529
4530
return wrapper
4531
4532
return decorator
4533
4534
4535
mako_view = functools.partial(view, template_adapter=MakoTemplate)
4536
cheetah_view = functools.partial(view, template_adapter=CheetahTemplate)
4537
jinja2_view = functools.partial(view, template_adapter=Jinja2Template)
4538
4539
###############################################################################
4540
# Constants and Globals ########################################################
4541
###############################################################################
4542
4543
TEMPLATE_PATH = ['./', './views/']
4544
TEMPLATES = {}
4545
DEBUG = False
4546
NORUN = False # If set, run() does nothing. Used by load_app()
4547
4548
#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
4549
HTTP_CODES = httplib.responses.copy()
4550
HTTP_CODES[418] = "I'm a teapot" # RFC 2324
4551
HTTP_CODES[428] = "Precondition Required"
4552
HTTP_CODES[429] = "Too Many Requests"
4553
HTTP_CODES[431] = "Request Header Fields Too Large"
4554
HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725
4555
HTTP_CODES[511] = "Network Authentication Required"
4556
_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v))
4557
for (k, v) in HTTP_CODES.items())
4558
4559
#: The default template used for error pages. Override with @error()
4560
ERROR_PAGE_TEMPLATE = """
4561
%%try:
4562
%%from %s import DEBUG, request
4563
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
4564
<html>
4565
<head>
4566
<title>Error: {{e.status}}</title>
4567
<style type="text/css">
4568
html {background-color: #eee; font-family: sans-serif;}
4569
body {background-color: #fff; border: 1px solid #ddd;
4570
padding: 15px; margin: 15px;}
4571
pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}
4572
</style>
4573
</head>
4574
<body>
4575
<h1>Error: {{e.status}}</h1>
4576
<p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt>
4577
caused an error:</p>
4578
<pre>{{e.body}}</pre>
4579
%%if DEBUG and e.exception:
4580
<h2>Exception:</h2>
4581
%%try:
4582
%%exc = repr(e.exception)
4583
%%except:
4584
%%exc = '<unprintable %%s object>' %% type(e.exception).__name__
4585
%%end
4586
<pre>{{exc}}</pre>
4587
%%end
4588
%%if DEBUG and e.traceback:
4589
<h2>Traceback:</h2>
4590
<pre>{{e.traceback}}</pre>
4591
%%end
4592
</body>
4593
</html>
4594
%%except ImportError:
4595
<b>ImportError:</b> Could not generate the error page. Please add bottle to
4596
the import path.
4597
%%end
4598
""" % __name__
4599
4600
#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
4601
#: request callback, this instance always refers to the *current* request
4602
#: (even on a multi-threaded server).
4603
request = LocalRequest()
4604
4605
#: A thread-safe instance of :class:`LocalResponse`. It is used to change the
4606
#: HTTP response for the *current* request.
4607
response = LocalResponse()
4608
4609
#: A thread-safe namespace. Not used by Bottle.
4610
local = threading.local()
4611
4612
# Initialize app stack (create first empty Bottle app now deferred until needed)
4613
# BC: 0.6.4 and needed for run()
4614
apps = app = default_app = AppStack()
4615
4616
#: A virtual package that redirects import statements.
4617
#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
4618
ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else
4619
__name__ + ".ext", 'bottle_%s').module
4620
4621
4622
def _main(argv): # pragma: no coverage
4623
args, parser = _cli_parse(argv)
4624
4625
def _cli_error(cli_msg):
4626
parser.print_help()
4627
_stderr('\nError: %s\n' % cli_msg)
4628
sys.exit(1)
4629
4630
if args.version:
4631
print('Bottle %s' % __version__)
4632
sys.exit(0)
4633
if not args.app:
4634
_cli_error("No application entry point specified.")
4635
4636
sys.path.insert(0, '.')
4637
sys.modules.setdefault('bottle', sys.modules['__main__'])
4638
4639
host, port = (args.bind or 'localhost'), 8080
4640
if ':' in host and host.rfind(']') < host.rfind(':'):
4641
host, port = host.rsplit(':', 1)
4642
host = host.strip('[]')
4643
4644
config = ConfigDict()
4645
4646
for cfile in args.conf or []:
4647
try:
4648
if cfile.endswith('.json'):
4649
with open(cfile, 'rb') as fp:
4650
config.load_dict(json_loads(fp.read()))
4651
else:
4652
config.load_config(cfile)
4653
except configparser.Error as parse_error:
4654
_cli_error(parse_error)
4655
except IOError:
4656
_cli_error("Unable to read config file %r" % cfile)
4657
except (UnicodeError, TypeError, ValueError) as error:
4658
_cli_error("Unable to parse config file %r: %s" % (cfile, error))
4659
4660
for cval in args.param or []:
4661
if '=' in cval:
4662
config.update((cval.split('=', 1),))
4663
else:
4664
config[cval] = True
4665
4666
run(args.app,
4667
host=host,
4668
port=int(port),
4669
server=args.server,
4670
reloader=args.reload,
4671
plugins=args.plugin,
4672
debug=args.debug,
4673
config=config)
4674
4675
4676
def main():
4677
_main(sys.argv)
4678
4679
4680
if __name__ == '__main__': # pragma: no coverage
4681
main()
4682
4683