Translation of UI (german and english)
Bugfix for feeds
- removed categories related and up
- load new books now working
- category random now working
login page is free of non accessible elements
boolean custom column is vivible in UI
books with only with certain languages can be shown
book shelfs can be deleted from UI
Anonymous user view is more resticted
Added browse of series in sidebar
Dependencys in vendor folder are updated to newer versions (licencs files are now present)
Bugfix editing Authors names
Made upload on windows working
@@ -0,0 +1,54 @@
|
||||
Werkzeug is written and maintained by the Werkzeug Team and various
|
||||
contributors:
|
||||
|
||||
Project Leader / Developer:
|
||||
|
||||
- Armin Ronacher <armin.ronacher@active-4.com>
|
||||
|
||||
- Georg Brandl
|
||||
- Leif K-Brooks <eurleif@gmail.com>
|
||||
- Thomas Johansson
|
||||
- Marian Sigler
|
||||
- Ronny Pfannschmidt
|
||||
- Noah Slater <nslater@tumbolia.org>
|
||||
- Alec Thomas
|
||||
- Shannon Behrens
|
||||
- Christoph Rauch
|
||||
- Clemens Hermann
|
||||
- Jason Kirtland
|
||||
- Ali Afshar
|
||||
- Christopher Grebs <cg@webshox.org>
|
||||
- Sean Cazzell <seancazzell@gmail.com>
|
||||
- Florent Xicluna
|
||||
- Kyle Dawkins
|
||||
- Pedro Algarvio
|
||||
- Zahari Petkov
|
||||
- Ludvig Ericson
|
||||
- Kenneth Reitz
|
||||
- Daniel Neuhäuser
|
||||
- Markus Unterwaditzer
|
||||
- Joe Esposito <joe@joeyespo.com>
|
||||
- Cédric Krier
|
||||
- Lars Holm Nielsen
|
||||
|
||||
Contributors of code for werkzeug/examples are:
|
||||
|
||||
- Itay Neeman <itay@neeman.net>
|
||||
|
||||
The SSL related parts of the Werkzeug development server are partially
|
||||
taken from Paste. Same thing is true for the range support which comes
|
||||
from WebOb which is a Paste project. The original code is MIT licensed which
|
||||
is largely compatible with the modfied BSD license. The following copyrights
|
||||
apply:
|
||||
|
||||
- (c) 2005 Ian Bicking and contributors
|
||||
- (c) 2005 Clark C. Evans
|
||||
|
||||
The rename() function from the posixemulation was taken almost unmodified
|
||||
from the Trac project's utility module. The original code is BSD licensed
|
||||
with the following copyrights from that module:
|
||||
|
||||
- (c) 2003-2009 Edgewall Software
|
||||
- (c) 2003-2006 Jonas Borgström <jonas@edgewall.com>
|
||||
- (c) 2006 Matthew Good <trac@matt-good.net>
|
||||
- (c) 2005-2006 Christian Boos <cboos@neuf.fr>
|
||||
@@ -0,0 +1,29 @@
|
||||
Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* The names of the contributors may not be used to endorse or
|
||||
promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -11,7 +11,7 @@
|
||||
library.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from types import ModuleType
|
||||
@@ -20,7 +20,7 @@ import sys
|
||||
from werkzeug._compat import iteritems
|
||||
|
||||
# the version. Usually set automatically by a script.
|
||||
__version__ = '0.9.1'
|
||||
__version__ = '0.11.11-dev'
|
||||
|
||||
|
||||
# This import magic raises concerns quite often which is why the implementation
|
||||
@@ -39,66 +39,63 @@ __version__ = '0.9.1'
|
||||
|
||||
# import mapping to objects in other modules
|
||||
all_by_module = {
|
||||
'werkzeug.debug': ['DebuggedApplication'],
|
||||
'werkzeug.local': ['Local', 'LocalManager', 'LocalProxy',
|
||||
'LocalStack', 'release_local'],
|
||||
'werkzeug.serving': ['run_simple'],
|
||||
'werkzeug.test': ['Client', 'EnvironBuilder', 'create_environ',
|
||||
'run_wsgi_app'],
|
||||
'werkzeug.testapp': ['test_app'],
|
||||
'werkzeug.exceptions': ['abort', 'Aborter'],
|
||||
'werkzeug.urls': ['url_decode', 'url_encode', 'url_quote',
|
||||
'url_quote_plus', 'url_unquote',
|
||||
'url_unquote_plus', 'url_fix', 'Href',
|
||||
'iri_to_uri', 'uri_to_iri'],
|
||||
'werkzeug.formparser': ['parse_form_data'],
|
||||
'werkzeug.utils': ['escape', 'environ_property',
|
||||
'append_slash_redirect', 'redirect',
|
||||
'cached_property', 'import_string',
|
||||
'dump_cookie', 'parse_cookie', 'unescape',
|
||||
'format_string', 'find_modules', 'header_property',
|
||||
'html', 'xhtml', 'HTMLBuilder',
|
||||
'validate_arguments', 'ArgumentValidationError',
|
||||
'bind_arguments', 'secure_filename'],
|
||||
'werkzeug.wsgi': ['get_current_url', 'get_host', 'pop_path_info',
|
||||
'peek_path_info', 'SharedDataMiddleware',
|
||||
'DispatcherMiddleware', 'ClosingIterator',
|
||||
'FileWrapper', 'make_line_iter', 'LimitedStream',
|
||||
'responder', 'wrap_file', 'extract_path_info'],
|
||||
'werkzeug.debug': ['DebuggedApplication'],
|
||||
'werkzeug.local': ['Local', 'LocalManager', 'LocalProxy', 'LocalStack',
|
||||
'release_local'],
|
||||
'werkzeug.serving': ['run_simple'],
|
||||
'werkzeug.test': ['Client', 'EnvironBuilder', 'create_environ',
|
||||
'run_wsgi_app'],
|
||||
'werkzeug.testapp': ['test_app'],
|
||||
'werkzeug.exceptions': ['abort', 'Aborter'],
|
||||
'werkzeug.urls': ['url_decode', 'url_encode', 'url_quote',
|
||||
'url_quote_plus', 'url_unquote', 'url_unquote_plus',
|
||||
'url_fix', 'Href', 'iri_to_uri', 'uri_to_iri'],
|
||||
'werkzeug.formparser': ['parse_form_data'],
|
||||
'werkzeug.utils': ['escape', 'environ_property', 'append_slash_redirect',
|
||||
'redirect', 'cached_property', 'import_string',
|
||||
'dump_cookie', 'parse_cookie', 'unescape',
|
||||
'format_string', 'find_modules', 'header_property',
|
||||
'html', 'xhtml', 'HTMLBuilder', 'validate_arguments',
|
||||
'ArgumentValidationError', 'bind_arguments',
|
||||
'secure_filename'],
|
||||
'werkzeug.wsgi': ['get_current_url', 'get_host', 'pop_path_info',
|
||||
'peek_path_info', 'SharedDataMiddleware',
|
||||
'DispatcherMiddleware', 'ClosingIterator', 'FileWrapper',
|
||||
'make_line_iter', 'LimitedStream', 'responder',
|
||||
'wrap_file', 'extract_path_info'],
|
||||
'werkzeug.datastructures': ['MultiDict', 'CombinedMultiDict', 'Headers',
|
||||
'EnvironHeaders', 'ImmutableList',
|
||||
'ImmutableDict', 'ImmutableMultiDict',
|
||||
'TypeConversionDict', 'ImmutableTypeConversionDict',
|
||||
'Accept', 'MIMEAccept', 'CharsetAccept',
|
||||
'LanguageAccept', 'RequestCacheControl',
|
||||
'ResponseCacheControl', 'ETags', 'HeaderSet',
|
||||
'WWWAuthenticate', 'Authorization',
|
||||
'FileMultiDict', 'CallbackDict', 'FileStorage',
|
||||
'OrderedMultiDict', 'ImmutableOrderedMultiDict'],
|
||||
'EnvironHeaders', 'ImmutableList',
|
||||
'ImmutableDict', 'ImmutableMultiDict',
|
||||
'TypeConversionDict',
|
||||
'ImmutableTypeConversionDict', 'Accept',
|
||||
'MIMEAccept', 'CharsetAccept',
|
||||
'LanguageAccept', 'RequestCacheControl',
|
||||
'ResponseCacheControl', 'ETags', 'HeaderSet',
|
||||
'WWWAuthenticate', 'Authorization',
|
||||
'FileMultiDict', 'CallbackDict', 'FileStorage',
|
||||
'OrderedMultiDict', 'ImmutableOrderedMultiDict'
|
||||
],
|
||||
'werkzeug.useragents': ['UserAgent'],
|
||||
'werkzeug.http': ['parse_etags', 'parse_date', 'http_date',
|
||||
'cookie_date', 'parse_cache_control_header',
|
||||
'is_resource_modified', 'parse_accept_header',
|
||||
'parse_set_header', 'quote_etag', 'unquote_etag',
|
||||
'generate_etag', 'dump_header',
|
||||
'parse_list_header', 'parse_dict_header',
|
||||
'parse_authorization_header',
|
||||
'parse_www_authenticate_header',
|
||||
'remove_entity_headers', 'is_entity_header',
|
||||
'remove_hop_by_hop_headers', 'parse_options_header',
|
||||
'dump_options_header', 'is_hop_by_hop_header',
|
||||
'unquote_header_value',
|
||||
'quote_header_value', 'HTTP_STATUS_CODES'],
|
||||
'werkzeug.wrappers': ['BaseResponse', 'BaseRequest', 'Request',
|
||||
'Response', 'AcceptMixin', 'ETagRequestMixin',
|
||||
'ETagResponseMixin', 'ResponseStreamMixin',
|
||||
'CommonResponseDescriptorsMixin',
|
||||
'UserAgentMixin', 'AuthorizationMixin',
|
||||
'WWWAuthenticateMixin',
|
||||
'CommonRequestDescriptorsMixin'],
|
||||
'werkzeug.security': ['generate_password_hash', 'check_password_hash'],
|
||||
'werkzeug.http': ['parse_etags', 'parse_date', 'http_date', 'cookie_date',
|
||||
'parse_cache_control_header', 'is_resource_modified',
|
||||
'parse_accept_header', 'parse_set_header', 'quote_etag',
|
||||
'unquote_etag', 'generate_etag', 'dump_header',
|
||||
'parse_list_header', 'parse_dict_header',
|
||||
'parse_authorization_header',
|
||||
'parse_www_authenticate_header', 'remove_entity_headers',
|
||||
'is_entity_header', 'remove_hop_by_hop_headers',
|
||||
'parse_options_header', 'dump_options_header',
|
||||
'is_hop_by_hop_header', 'unquote_header_value',
|
||||
'quote_header_value', 'HTTP_STATUS_CODES'],
|
||||
'werkzeug.wrappers': ['BaseResponse', 'BaseRequest', 'Request', 'Response',
|
||||
'AcceptMixin', 'ETagRequestMixin',
|
||||
'ETagResponseMixin', 'ResponseStreamMixin',
|
||||
'CommonResponseDescriptorsMixin', 'UserAgentMixin',
|
||||
'AuthorizationMixin', 'WWWAuthenticateMixin',
|
||||
'CommonRequestDescriptorsMixin'],
|
||||
'werkzeug.security': ['generate_password_hash', 'check_password_hash'],
|
||||
# the undocumented easteregg ;-)
|
||||
'werkzeug._internal': ['_easteregg']
|
||||
'werkzeug._internal': ['_easteregg']
|
||||
}
|
||||
|
||||
# modules that should be imported when accessed as attributes of werkzeug
|
||||
@@ -112,6 +109,7 @@ for module, items in iteritems(all_by_module):
|
||||
|
||||
|
||||
class module(ModuleType):
|
||||
|
||||
"""Automatically import objects from the modules."""
|
||||
|
||||
def __getattr__(self, name):
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
# flake8: noqa
|
||||
# This whole file is full of lint errors
|
||||
import codecs
|
||||
import sys
|
||||
import operator
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
@@ -8,6 +13,7 @@ except ImportError:
|
||||
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
WIN = sys.platform.startswith('win')
|
||||
|
||||
_identity = lambda x: x
|
||||
|
||||
@@ -16,7 +22,6 @@ if PY2:
|
||||
text_type = unicode
|
||||
string_types = (str, unicode)
|
||||
integer_types = (int, long)
|
||||
int_to_byte = chr
|
||||
|
||||
iterkeys = lambda d, *args, **kwargs: d.iterkeys(*args, **kwargs)
|
||||
itervalues = lambda d, *args, **kwargs: d.itervalues(*args, **kwargs)
|
||||
@@ -25,7 +30,8 @@ if PY2:
|
||||
iterlists = lambda d, *args, **kwargs: d.iterlists(*args, **kwargs)
|
||||
iterlistvalues = lambda d, *args, **kwargs: d.iterlistvalues(*args, **kwargs)
|
||||
|
||||
iter_bytes = lambda x: iter(x)
|
||||
int_to_byte = chr
|
||||
iter_bytes = iter
|
||||
|
||||
exec('def reraise(tp, value, tb=None):\n raise tp, value, tb')
|
||||
|
||||
@@ -67,7 +73,7 @@ if PY2:
|
||||
NativeStringIO = BytesIO
|
||||
|
||||
def make_literal_wrapper(reference):
|
||||
return lambda x: x
|
||||
return _identity
|
||||
|
||||
def normalize_string_tuple(tup):
|
||||
"""Normalizes a string tuple to a common type. Following Python 2
|
||||
@@ -82,19 +88,19 @@ if PY2:
|
||||
leave it as unicode.
|
||||
"""
|
||||
try:
|
||||
return str(s)
|
||||
return to_native(s)
|
||||
except UnicodeError:
|
||||
return s
|
||||
|
||||
wsgi_get_bytes = _identity
|
||||
|
||||
def wsgi_decoding_dance(s, charset='utf-8', errors='replace'):
|
||||
return s.decode(charset)
|
||||
return s.decode(charset, errors)
|
||||
|
||||
def wsgi_encoding_dance(s, charset='utf-8', errors='replace'):
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
return s.encode(charset)
|
||||
return s.encode(charset, errors)
|
||||
|
||||
def to_bytes(x, charset=sys.getdefaultencoding(), errors='strict'):
|
||||
if x is None:
|
||||
@@ -124,9 +130,7 @@ else:
|
||||
iterlistvalues = lambda d, *args, **kwargs: iter(d.listvalues(*args, **kwargs))
|
||||
|
||||
int_to_byte = operator.methodcaller('to_bytes', 1, 'big')
|
||||
|
||||
def iter_bytes(b):
|
||||
return map(int_to_byte, b)
|
||||
iter_bytes = functools.partial(map, int_to_byte)
|
||||
|
||||
def reraise(tp, value, tb=None):
|
||||
if value.__traceback__ is not tb:
|
||||
@@ -146,10 +150,12 @@ else:
|
||||
from io import StringIO, BytesIO
|
||||
NativeStringIO = StringIO
|
||||
|
||||
_latin1_encode = operator.methodcaller('encode', 'latin1')
|
||||
|
||||
def make_literal_wrapper(reference):
|
||||
if isinstance(reference, text_type):
|
||||
return lambda x: x
|
||||
return lambda x: x.encode('latin1')
|
||||
return _identity
|
||||
return _latin1_encode
|
||||
|
||||
def normalize_string_tuple(tup):
|
||||
"""Ensures that all types in the tuple are either strings
|
||||
@@ -160,26 +166,24 @@ else:
|
||||
for arg in tupiter:
|
||||
if isinstance(arg, text_type) != is_text:
|
||||
raise TypeError('Cannot mix str and bytes arguments (got %s)'
|
||||
% repr(tup))
|
||||
% repr(tup))
|
||||
return tup
|
||||
|
||||
try_coerce_native = _identity
|
||||
|
||||
def wsgi_get_bytes(s):
|
||||
return s.encode('latin1')
|
||||
wsgi_get_bytes = _latin1_encode
|
||||
|
||||
def wsgi_decoding_dance(s, charset='utf-8', errors='replace'):
|
||||
return s.encode('latin1').decode(charset, errors)
|
||||
|
||||
def wsgi_encoding_dance(s, charset='utf-8', errors='replace'):
|
||||
if isinstance(s, bytes):
|
||||
return s.decode('latin1', errors)
|
||||
return s.encode(charset).decode('latin1', errors)
|
||||
if isinstance(s, text_type):
|
||||
s = s.encode(charset)
|
||||
return s.decode('latin1', errors)
|
||||
|
||||
def to_bytes(x, charset=sys.getdefaultencoding(), errors='strict'):
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, (bytes, bytearray, memoryview)):
|
||||
if isinstance(x, (bytes, bytearray, memoryview)): # noqa
|
||||
return bytes(x)
|
||||
if isinstance(x, str):
|
||||
return x.encode(charset, errors)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module provides internally used helpers and constants.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
@@ -16,7 +16,7 @@ from datetime import datetime, date
|
||||
from itertools import chain
|
||||
|
||||
from werkzeug._compat import iter_bytes, text_type, BytesIO, int_to_byte, \
|
||||
range_type, to_native
|
||||
range_type, integer_types
|
||||
|
||||
|
||||
_logger = None
|
||||
@@ -31,10 +31,10 @@ _legal_cookie_chars = (string.ascii_letters +
|
||||
u"!#$%&'*+-.^_`|~:").encode('ascii')
|
||||
|
||||
_cookie_quoting_map = {
|
||||
b',' : b'\\054',
|
||||
b';' : b'\\073',
|
||||
b'"' : b'\\"',
|
||||
b'\\' : b'\\\\',
|
||||
b',': b'\\054',
|
||||
b';': b'\\073',
|
||||
b'"': b'\\"',
|
||||
b'\\': b'\\\\',
|
||||
}
|
||||
for _i in chain(range_type(32), range_type(127, 256)):
|
||||
_cookie_quoting_map[int_to_byte(_i)] = ('\\%03o' % _i).encode('latin1')
|
||||
@@ -141,12 +141,12 @@ def _parse_signature(func):
|
||||
if vararg_var is not None:
|
||||
new_args.extend(extra_positional)
|
||||
extra_positional = ()
|
||||
if kwargs and not kwarg_var is not None:
|
||||
if kwargs and kwarg_var is None:
|
||||
extra.update(kwargs)
|
||||
kwargs = {}
|
||||
|
||||
return new_args, kwargs, missing, extra, extra_positional, \
|
||||
arguments, vararg_var, kwarg_var
|
||||
arguments, vararg_var, kwarg_var
|
||||
_signature_cache[func] = parse
|
||||
return parse
|
||||
|
||||
@@ -157,7 +157,7 @@ def _date_to_unix(arg):
|
||||
"""
|
||||
if isinstance(arg, datetime):
|
||||
arg = arg.utctimetuple()
|
||||
elif isinstance(arg, (int, long, float)):
|
||||
elif isinstance(arg, integer_types + (float,)):
|
||||
return int(arg)
|
||||
year, month, day, hour, minute, second = arg[:6]
|
||||
days = date(year, month, 1).toordinal() - _epoch_ord + day - 1
|
||||
@@ -168,6 +168,7 @@ def _date_to_unix(arg):
|
||||
|
||||
|
||||
class _DictAccessorProperty(object):
|
||||
|
||||
"""Baseclass for `environ_property` and `header_property`."""
|
||||
read_only = False
|
||||
|
||||
@@ -257,7 +258,7 @@ def _cookie_unquote(b):
|
||||
k = q_match.start(0)
|
||||
if q_match and (not o_match or k < j):
|
||||
_push(b[i:k])
|
||||
_push(b[k + 1])
|
||||
_push(b[k + 1:k + 2])
|
||||
i = k + 2
|
||||
else:
|
||||
_push(b[i:j])
|
||||
@@ -337,8 +338,8 @@ def _make_cookie_domain(domain):
|
||||
if b'.' in domain:
|
||||
return domain
|
||||
raise ValueError(
|
||||
'Setting \'domain\' for a cookie on a server running localy (ex: '
|
||||
'localhost) is not supportted by complying browsers. You should '
|
||||
'Setting \'domain\' for a cookie on a server running locally (ex: '
|
||||
'localhost) is not supported by complying browsers. You should '
|
||||
'have something like: \'127.0.0.1 localhost dev.localhost\' on '
|
||||
'your hosts file and then point your server to run on '
|
||||
'\'dev.localhost\' and also set \'domain\' for \'dev.localhost\''
|
||||
@@ -383,6 +384,7 @@ krEDuNoJCHNlZYhKpvw4mspVWxqo415n8cD62N9+EfHrAvqQnINStetek7RY2Urv8nxsnGaZfRr/
|
||||
nhXbJ6m/yl1LzYqscDZA9QHLNbdaSTTr+kFg3bC0iYbX/eQy0Bv3h4B50/SGYzKAXkCeOLI3bcAt
|
||||
mj2Z/FM1vQWgDynsRwNvrWnJHlespkrp8+vO1jNaibm+PhqXPPv30YwDZ6jApe3wUjFQobghvW9p
|
||||
7f2zLkGNv8b191cD/3vs9Q833z8t''').splitlines()])
|
||||
|
||||
def easteregged(environ, start_response):
|
||||
def injecting_start_response(status, headers, exc_info=None):
|
||||
headers.append(('X-Powered-By', 'Werkzeug'))
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import threading
|
||||
from itertools import chain
|
||||
|
||||
from werkzeug._internal import _log
|
||||
from werkzeug._compat import PY2, iteritems, text_type
|
||||
|
||||
|
||||
def _iter_module_files():
|
||||
"""This iterates over all relevant Python files. It goes through all
|
||||
loaded files from modules, all files in folders of already loaded modules
|
||||
as well as all files reachable through a package.
|
||||
"""
|
||||
# The list call is necessary on Python 3 in case the module
|
||||
# dictionary modifies during iteration.
|
||||
for module in list(sys.modules.values()):
|
||||
if module is None:
|
||||
continue
|
||||
filename = getattr(module, '__file__', None)
|
||||
if filename:
|
||||
old = None
|
||||
while not os.path.isfile(filename):
|
||||
old = filename
|
||||
filename = os.path.dirname(filename)
|
||||
if filename == old:
|
||||
break
|
||||
else:
|
||||
if filename[-4:] in ('.pyc', '.pyo'):
|
||||
filename = filename[:-1]
|
||||
yield filename
|
||||
|
||||
|
||||
def _find_observable_paths(extra_files=None):
|
||||
"""Finds all paths that should be observed."""
|
||||
rv = set(os.path.abspath(x) for x in sys.path)
|
||||
|
||||
for filename in extra_files or ():
|
||||
rv.add(os.path.dirname(os.path.abspath(filename)))
|
||||
|
||||
for module in list(sys.modules.values()):
|
||||
fn = getattr(module, '__file__', None)
|
||||
if fn is None:
|
||||
continue
|
||||
fn = os.path.abspath(fn)
|
||||
rv.add(os.path.dirname(fn))
|
||||
|
||||
return _find_common_roots(rv)
|
||||
|
||||
|
||||
def _find_common_roots(paths):
|
||||
"""Out of some paths it finds the common roots that need monitoring."""
|
||||
paths = [x.split(os.path.sep) for x in paths]
|
||||
root = {}
|
||||
for chunks in sorted(paths, key=len, reverse=True):
|
||||
node = root
|
||||
for chunk in chunks:
|
||||
node = node.setdefault(chunk, {})
|
||||
node.clear()
|
||||
|
||||
rv = set()
|
||||
|
||||
def _walk(node, path):
|
||||
for prefix, child in iteritems(node):
|
||||
_walk(child, path + (prefix,))
|
||||
if not node:
|
||||
rv.add('/'.join(path))
|
||||
_walk(root, ())
|
||||
return rv
|
||||
|
||||
|
||||
class ReloaderLoop(object):
|
||||
name = None
|
||||
|
||||
# monkeypatched by testsuite. wrapping with `staticmethod` is required in
|
||||
# case time.sleep has been replaced by a non-c function (e.g. by
|
||||
# `eventlet.monkey_patch`) before we get here
|
||||
_sleep = staticmethod(time.sleep)
|
||||
|
||||
def __init__(self, extra_files=None, interval=1):
|
||||
self.extra_files = set(os.path.abspath(x)
|
||||
for x in extra_files or ())
|
||||
self.interval = interval
|
||||
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
def restart_with_reloader(self):
|
||||
"""Spawn a new Python interpreter with the same arguments as this one,
|
||||
but running the reloader thread.
|
||||
"""
|
||||
while 1:
|
||||
_log('info', ' * Restarting with %s' % self.name)
|
||||
args = [sys.executable] + sys.argv
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
|
||||
|
||||
# a weird bug on windows. sometimes unicode strings end up in the
|
||||
# environment and subprocess.call does not like this, encode them
|
||||
# to latin1 and continue.
|
||||
if os.name == 'nt' and PY2:
|
||||
for key, value in iteritems(new_environ):
|
||||
if isinstance(value, text_type):
|
||||
new_environ[key] = value.encode('iso-8859-1')
|
||||
|
||||
exit_code = subprocess.call(args, env=new_environ,
|
||||
close_fds=False)
|
||||
if exit_code != 3:
|
||||
return exit_code
|
||||
|
||||
def trigger_reload(self, filename):
|
||||
self.log_reload(filename)
|
||||
sys.exit(3)
|
||||
|
||||
def log_reload(self, filename):
|
||||
filename = os.path.abspath(filename)
|
||||
_log('info', ' * Detected change in %r, reloading' % filename)
|
||||
|
||||
|
||||
class StatReloaderLoop(ReloaderLoop):
|
||||
name = 'stat'
|
||||
|
||||
def run(self):
|
||||
mtimes = {}
|
||||
while 1:
|
||||
for filename in chain(_iter_module_files(),
|
||||
self.extra_files):
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
continue
|
||||
elif mtime > old_time:
|
||||
self.trigger_reload(filename)
|
||||
self._sleep(self.interval)
|
||||
|
||||
|
||||
class WatchdogReloaderLoop(ReloaderLoop):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
ReloaderLoop.__init__(self, *args, **kwargs)
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
self.observable_paths = set()
|
||||
|
||||
def _check_modification(filename):
|
||||
if filename in self.extra_files:
|
||||
self.trigger_reload(filename)
|
||||
dirname = os.path.dirname(filename)
|
||||
if dirname.startswith(tuple(self.observable_paths)):
|
||||
if filename.endswith(('.pyc', '.pyo')):
|
||||
self.trigger_reload(filename[:-1])
|
||||
elif filename.endswith('.py'):
|
||||
self.trigger_reload(filename)
|
||||
|
||||
class _CustomHandler(FileSystemEventHandler):
|
||||
|
||||
def on_created(self, event):
|
||||
_check_modification(event.src_path)
|
||||
|
||||
def on_modified(self, event):
|
||||
_check_modification(event.src_path)
|
||||
|
||||
def on_moved(self, event):
|
||||
_check_modification(event.src_path)
|
||||
_check_modification(event.dest_path)
|
||||
|
||||
def on_deleted(self, event):
|
||||
_check_modification(event.src_path)
|
||||
|
||||
reloader_name = Observer.__name__.lower()
|
||||
if reloader_name.endswith('observer'):
|
||||
reloader_name = reloader_name[:-8]
|
||||
reloader_name += ' reloader'
|
||||
|
||||
self.name = reloader_name
|
||||
|
||||
self.observer_class = Observer
|
||||
self.event_handler = _CustomHandler()
|
||||
self.should_reload = False
|
||||
|
||||
def trigger_reload(self, filename):
|
||||
# This is called inside an event handler, which means throwing
|
||||
# SystemExit has no effect.
|
||||
# https://github.com/gorakhargosh/watchdog/issues/294
|
||||
self.should_reload = True
|
||||
self.log_reload(filename)
|
||||
|
||||
def run(self):
|
||||
watches = {}
|
||||
observer = self.observer_class()
|
||||
observer.start()
|
||||
|
||||
while not self.should_reload:
|
||||
to_delete = set(watches)
|
||||
paths = _find_observable_paths(self.extra_files)
|
||||
for path in paths:
|
||||
if path not in watches:
|
||||
try:
|
||||
watches[path] = observer.schedule(
|
||||
self.event_handler, path, recursive=True)
|
||||
except OSError:
|
||||
# Clear this path from list of watches We don't want
|
||||
# the same error message showing again in the next
|
||||
# iteration.
|
||||
watches[path] = None
|
||||
to_delete.discard(path)
|
||||
for path in to_delete:
|
||||
watch = watches.pop(path, None)
|
||||
if watch is not None:
|
||||
observer.unschedule(watch)
|
||||
self.observable_paths = paths
|
||||
self._sleep(self.interval)
|
||||
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
reloader_loops = {
|
||||
'stat': StatReloaderLoop,
|
||||
'watchdog': WatchdogReloaderLoop,
|
||||
}
|
||||
|
||||
try:
|
||||
__import__('watchdog.observers')
|
||||
except ImportError:
|
||||
reloader_loops['auto'] = reloader_loops['stat']
|
||||
else:
|
||||
reloader_loops['auto'] = reloader_loops['watchdog']
|
||||
|
||||
|
||||
def run_with_reloader(main_func, extra_files=None, interval=1,
|
||||
reloader_type='auto'):
|
||||
"""Run the given function in an independent python interpreter."""
|
||||
import signal
|
||||
reloader = reloader_loops[reloader_type](extra_files, interval)
|
||||
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
||||
try:
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
t = threading.Thread(target=main_func, args=())
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
reloader.run()
|
||||
else:
|
||||
sys.exit(reloader.restart_with_reloader())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -11,6 +11,6 @@
|
||||
This file itself is mostly for informational purposes and to tell the
|
||||
Python interpreter that `contrib` is a package.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
updated=post.last_update, published=post.pub_date)
|
||||
return feed.get_response()
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from datetime import datetime
|
||||
@@ -44,11 +44,15 @@ def _make_text_block(name, content, content_type=None):
|
||||
|
||||
def format_iso8601(obj):
|
||||
"""Format a datetime object for iso8601"""
|
||||
return obj.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
iso8601 = obj.isoformat()
|
||||
if obj.tzinfo:
|
||||
return iso8601
|
||||
return iso8601 + 'Z'
|
||||
|
||||
|
||||
@implements_to_string
|
||||
class AtomFeed(object):
|
||||
|
||||
"""A helper class that creates Atom feeds.
|
||||
|
||||
:param title: the title of the feed. Required.
|
||||
@@ -61,6 +65,7 @@ class AtomFeed(object):
|
||||
:param updated: the time the feed was modified the last time. Must
|
||||
be a :class:`datetime.datetime` object. If not
|
||||
present the latest entry's `updated` is used.
|
||||
Treated as UTC if naive datetime.
|
||||
:param feed_url: the URL to the feed. Should be the URL that was
|
||||
requested.
|
||||
:param author: the author of the feed. Must be either a string (the
|
||||
@@ -167,13 +172,13 @@ class AtomFeed(object):
|
||||
yield u' <id>%s</id>\n' % escape(self.id)
|
||||
yield u' <updated>%s</updated>\n' % format_iso8601(self.updated)
|
||||
if self.url:
|
||||
yield u' <link href="%s" />\n' % escape(self.url, True)
|
||||
yield u' <link href="%s" />\n' % escape(self.url)
|
||||
if self.feed_url:
|
||||
yield u' <link href="%s" rel="self" />\n' % \
|
||||
escape(self.feed_url, True)
|
||||
escape(self.feed_url)
|
||||
for link in self.links:
|
||||
yield u' <link %s/>\n' % ''.join('%s="%s" ' % \
|
||||
(k, escape(link[k], True)) for k in link)
|
||||
yield u' <link %s/>\n' % ''.join('%s="%s" ' %
|
||||
(k, escape(link[k])) for k in link)
|
||||
for author in self.author:
|
||||
yield u' <author>\n'
|
||||
yield u' <name>%s</name>\n' % escape(author['name'])
|
||||
@@ -196,9 +201,9 @@ class AtomFeed(object):
|
||||
if generator_name or generator_url or generator_version:
|
||||
tmp = [u' <generator']
|
||||
if generator_url:
|
||||
tmp.append(u' uri="%s"' % escape(generator_url, True))
|
||||
tmp.append(u' uri="%s"' % escape(generator_url))
|
||||
if generator_version:
|
||||
tmp.append(u' version="%s"' % escape(generator_version, True))
|
||||
tmp.append(u' version="%s"' % escape(generator_version))
|
||||
tmp.append(u'>%s</generator>\n' % escape(generator_name))
|
||||
yield u''.join(tmp)
|
||||
for entry in self.entries:
|
||||
@@ -224,6 +229,7 @@ class AtomFeed(object):
|
||||
|
||||
@implements_to_string
|
||||
class FeedEntry(object):
|
||||
|
||||
"""Represents a single entry in a feed.
|
||||
|
||||
:param title: the title of the entry. Required.
|
||||
@@ -239,7 +245,8 @@ class FeedEntry(object):
|
||||
:param id: a globally unique id for the entry. Must be an URI. If
|
||||
not present the URL is used, but one of both is required.
|
||||
:param updated: the time the entry was modified the last time. Must
|
||||
be a :class:`datetime.datetime` object. Required.
|
||||
be a :class:`datetime.datetime` object. Treated as
|
||||
UTC if naive datetime. Required.
|
||||
:param author: the author of the entry. Must be either a string (the
|
||||
name) or a dict with name (required) and uri or
|
||||
email (both optional). Can be a list of (may be
|
||||
@@ -247,7 +254,8 @@ class FeedEntry(object):
|
||||
multiple authors. Required if the feed does not have an
|
||||
author element.
|
||||
:param published: the time the entry was initially published. Must
|
||||
be a :class:`datetime.datetime` object.
|
||||
be a :class:`datetime.datetime` object. Treated as
|
||||
UTC if naive datetime.
|
||||
:param rights: copyright information for the entry.
|
||||
:param rights_type: the type attribute for the rights element. One of
|
||||
``'html'``, ``'text'`` or ``'xhtml'``. Default is
|
||||
@@ -307,7 +315,7 @@ class FeedEntry(object):
|
||||
"""Yields pieces of ATOM XML."""
|
||||
base = ''
|
||||
if self.xml_base:
|
||||
base = ' xml:base="%s"' % escape(self.xml_base, True)
|
||||
base = ' xml:base="%s"' % escape(self.xml_base)
|
||||
yield u'<entry%s>\n' % base
|
||||
yield u' ' + _make_text_block('title', self.title, self.title_type)
|
||||
yield u' <id>%s</id>\n' % escape(self.id)
|
||||
@@ -326,11 +334,11 @@ class FeedEntry(object):
|
||||
yield u' <email>%s</email>\n' % escape(author['email'])
|
||||
yield u' </author>\n'
|
||||
for link in self.links:
|
||||
yield u' <link %s/>\n' % ''.join('%s="%s" ' % \
|
||||
(k, escape(link[k], True)) for k in link)
|
||||
yield u' <link %s/>\n' % ''.join('%s="%s" ' %
|
||||
(k, escape(link[k])) for k in link)
|
||||
for category in self.categories:
|
||||
yield u' <category %s/>\n' % ''.join('%s="%s" ' % \
|
||||
(k, escape(category[k], True)) for k in category)
|
||||
yield u' <category %s/>\n' % ''.join('%s="%s" ' %
|
||||
(k, escape(category[k])) for k in category)
|
||||
if self.summary:
|
||||
yield u' ' + _make_text_block('summary', self.summary,
|
||||
self.summary_type)
|
||||
|
||||
@@ -53,20 +53,22 @@
|
||||
you have access to it (either as a module global you can import or you just
|
||||
put it into your WSGI application).
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import errno
|
||||
import tempfile
|
||||
from hashlib import md5
|
||||
from time import time
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
import pickle
|
||||
|
||||
from werkzeug._compat import iteritems, string_types, text_type
|
||||
from werkzeug._compat import iteritems, string_types, text_type, \
|
||||
integer_types, to_native
|
||||
from werkzeug.posixemulation import rename
|
||||
|
||||
|
||||
@@ -81,48 +83,48 @@ def _items(mappingorseq):
|
||||
... assert k*k == v
|
||||
|
||||
"""
|
||||
if hasattr(mappingorseq, "iteritems"):
|
||||
return mappingorseq.iteritems()
|
||||
elif hasattr(mappingorseq, "items"):
|
||||
return mappingorseq.items()
|
||||
if hasattr(mappingorseq, 'items'):
|
||||
return iteritems(mappingorseq)
|
||||
return mappingorseq
|
||||
|
||||
|
||||
class BaseCache(object):
|
||||
|
||||
"""Baseclass for the cache systems. All the cache systems implement this
|
||||
API or a superset of it.
|
||||
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`set`.
|
||||
:param default_timeout: the default timeout (in seconds) that is used if no
|
||||
timeout is specified on :meth:`set`. A timeout of 0
|
||||
indicates that the cache never expires.
|
||||
"""
|
||||
|
||||
def __init__(self, default_timeout=300):
|
||||
self.default_timeout = default_timeout
|
||||
|
||||
def get(self, key):
|
||||
"""Looks up key in the cache and returns the value for it.
|
||||
If the key does not exist `None` is returned instead.
|
||||
"""Look up key in the cache and return the value for it.
|
||||
|
||||
:param key: the key to be looked up.
|
||||
:returns: The value if it exists and is readable, else ``None``.
|
||||
"""
|
||||
return None
|
||||
|
||||
def delete(self, key):
|
||||
"""Deletes `key` from the cache. If it does not exist in the cache
|
||||
nothing happens.
|
||||
"""Delete `key` from the cache.
|
||||
|
||||
:param key: the key to delete.
|
||||
:returns: Whether the key existed and has been deleted.
|
||||
:rtype: boolean
|
||||
"""
|
||||
pass
|
||||
return True
|
||||
|
||||
def get_many(self, *keys):
|
||||
"""Returns a list of values for the given keys.
|
||||
For each key a item in the list is created. Example::
|
||||
For each key a item in the list is created::
|
||||
|
||||
foo, bar = cache.get_many("foo", "bar")
|
||||
|
||||
If a key can't be looked up `None` is returned for that key
|
||||
instead.
|
||||
Has the same error handling as :meth:`get`.
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
@@ -130,7 +132,7 @@ class BaseCache(object):
|
||||
return map(self.get, keys)
|
||||
|
||||
def get_dict(self, *keys):
|
||||
"""Works like :meth:`get_many` but returns a dict::
|
||||
"""Like :meth:`get_many` but return a dict::
|
||||
|
||||
d = cache.get_dict("foo", "bar")
|
||||
foo = d["foo"]
|
||||
@@ -142,15 +144,20 @@ class BaseCache(object):
|
||||
return dict(zip(keys, self.get_many(*keys)))
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
"""Adds a new key/value to the cache (overwrites value, if key already
|
||||
"""Add a new key/value to the cache (overwrites value, if key already
|
||||
exists in the cache).
|
||||
|
||||
:param key: the key to set
|
||||
:param value: the value for the key
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
it uses the default timeout). A timeout of 0 idicates
|
||||
that the cache never expires.
|
||||
:returns: ``True`` if key has been updated, ``False`` for backend
|
||||
errors. Pickling errors, however, will raise a subclass of
|
||||
``pickle.PickleError``.
|
||||
:rtype: boolean
|
||||
"""
|
||||
pass
|
||||
return True
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
"""Works like :meth:`set` but does not overwrite the values of already
|
||||
@@ -159,34 +166,62 @@ class BaseCache(object):
|
||||
:param key: the key to set
|
||||
:param value: the value for the key
|
||||
:param timeout: the cache timeout for the key or the default
|
||||
timeout if not specified.
|
||||
timeout if not specified. A timeout of 0 indicates
|
||||
that the cache never expires.
|
||||
:returns: Same as :meth:`set`, but also ``False`` for already
|
||||
existing keys.
|
||||
:rtype: boolean
|
||||
"""
|
||||
pass
|
||||
return True
|
||||
|
||||
def set_many(self, mapping, timeout=None):
|
||||
"""Sets multiple keys and values from a mapping.
|
||||
|
||||
:param mapping: a mapping with the keys/values to set.
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
it uses the default timeout). A timeout of 0
|
||||
indicates tht the cache never expires.
|
||||
:returns: Whether all given keys have been set.
|
||||
:rtype: boolean
|
||||
"""
|
||||
rv = True
|
||||
for key, value in _items(mapping):
|
||||
self.set(key, value, timeout)
|
||||
if not self.set(key, value, timeout):
|
||||
rv = False
|
||||
return rv
|
||||
|
||||
def delete_many(self, *keys):
|
||||
"""Deletes multiple keys at once.
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
:returns: Whether all given keys have been deleted.
|
||||
:rtype: boolean
|
||||
"""
|
||||
for key in keys:
|
||||
self.delete(key)
|
||||
return all(self.delete(key) for key in keys)
|
||||
|
||||
def has(self, key):
|
||||
"""Checks if a key exists in the cache without returning it. This is a
|
||||
cheap operation that bypasses loading the actual data on the backend.
|
||||
|
||||
This method is optional and may not be implemented on all caches.
|
||||
|
||||
:param key: the key to check
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'%s doesn\'t have an efficient implementation of `has`. That '
|
||||
'means it is impossible to check whether a key exists without '
|
||||
'fully loading the key\'s data. Consider using `self.get` '
|
||||
'explicitly if you don\'t care about performance.'
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
"""Clears the cache. Keep in mind that not all caches support
|
||||
completely clearing the cache.
|
||||
:returns: Whether the cache has been cleared.
|
||||
:rtype: boolean
|
||||
"""
|
||||
pass
|
||||
return True
|
||||
|
||||
def inc(self, key, delta=1):
|
||||
"""Increments the value of a key by `delta`. If the key does
|
||||
@@ -196,8 +231,10 @@ class BaseCache(object):
|
||||
|
||||
:param key: the key to increment.
|
||||
:param delta: the delta to add.
|
||||
:returns: The new value or ``None`` for backend errors.
|
||||
"""
|
||||
self.set(key, (self.get(key) or 0) + delta)
|
||||
value = (self.get(key) or 0) + delta
|
||||
return value if self.set(key, value) else None
|
||||
|
||||
def dec(self, key, delta=1):
|
||||
"""Decrements the value of a key by `delta`. If the key does
|
||||
@@ -207,11 +244,14 @@ class BaseCache(object):
|
||||
|
||||
:param key: the key to increment.
|
||||
:param delta: the delta to subtract.
|
||||
:returns: The new value or `None` for backend errors.
|
||||
"""
|
||||
self.set(key, (self.get(key) or 0) - delta)
|
||||
value = (self.get(key) or 0) - delta
|
||||
return value if self.set(key, value) else None
|
||||
|
||||
|
||||
class NullCache(BaseCache):
|
||||
|
||||
"""A cache that doesn't cache. This can be useful for unit testing.
|
||||
|
||||
:param default_timeout: a dummy parameter that is ignored but exists
|
||||
@@ -220,6 +260,7 @@ class NullCache(BaseCache):
|
||||
|
||||
|
||||
class SimpleCache(BaseCache):
|
||||
|
||||
"""Simple memory cache for single process environments. This class exists
|
||||
mainly for the development server and is not 100% thread safe. It tries
|
||||
to use as many atomic operations as possible and no locks for simplicity
|
||||
@@ -228,7 +269,8 @@ class SimpleCache(BaseCache):
|
||||
:param threshold: the maximum number of items the cache stores before
|
||||
it starts deleting some.
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
specified on :meth:`~BaseCache.set`. A timeout of
|
||||
0 indicates that the cache never expires.
|
||||
"""
|
||||
|
||||
def __init__(self, threshold=500, default_timeout=300):
|
||||
@@ -240,38 +282,60 @@ class SimpleCache(BaseCache):
|
||||
def _prune(self):
|
||||
if len(self._cache) > self._threshold:
|
||||
now = time()
|
||||
toremove = []
|
||||
for idx, (key, (expires, _)) in enumerate(self._cache.items()):
|
||||
if expires <= now or idx % 3 == 0:
|
||||
self._cache.pop(key, None)
|
||||
if (expires != 0 and expires <= now) or idx % 3 == 0:
|
||||
toremove.append(key)
|
||||
for key in toremove:
|
||||
self._cache.pop(key, None)
|
||||
|
||||
def _get_expiration(self, timeout):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
if timeout > 0:
|
||||
timeout = time() + timeout
|
||||
return timeout
|
||||
|
||||
def get(self, key):
|
||||
expires, value = self._cache.get(key, (0, None))
|
||||
if expires > time():
|
||||
return pickle.loads(value)
|
||||
try:
|
||||
expires, value = self._cache[key]
|
||||
if expires == 0 or expires > time():
|
||||
return pickle.loads(value)
|
||||
except (KeyError, pickle.PickleError):
|
||||
return None
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
expires = self._get_expiration(timeout)
|
||||
self._prune()
|
||||
self._cache[key] = (time() + timeout, pickle.dumps(value,
|
||||
pickle.HIGHEST_PROTOCOL))
|
||||
self._cache[key] = (expires, pickle.dumps(value,
|
||||
pickle.HIGHEST_PROTOCOL))
|
||||
return True
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
if len(self._cache) > self._threshold:
|
||||
self._prune()
|
||||
item = (time() + timeout, pickle.dumps(value,
|
||||
pickle.HIGHEST_PROTOCOL))
|
||||
expires = self._get_expiration(timeout)
|
||||
self._prune()
|
||||
item = (expires, pickle.dumps(value,
|
||||
pickle.HIGHEST_PROTOCOL))
|
||||
if key in self._cache:
|
||||
return False
|
||||
self._cache.setdefault(key, item)
|
||||
return True
|
||||
|
||||
def delete(self, key):
|
||||
self._cache.pop(key, None)
|
||||
return self._cache.pop(key, None) is not None
|
||||
|
||||
def has(self, key):
|
||||
try:
|
||||
expires, value = self._cache[key]
|
||||
return expires == 0 or expires > time()
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
_test_memcached_key = re.compile(r'[^\x00-\x21\xff]{1,250}$').match
|
||||
|
||||
|
||||
class MemcachedCache(BaseCache):
|
||||
|
||||
"""A cache that uses memcached as backend.
|
||||
|
||||
The first argument can either be an object that resembles the API of a
|
||||
@@ -279,6 +343,13 @@ class MemcachedCache(BaseCache):
|
||||
event that a tuple/list is passed, Werkzeug tries to import the best
|
||||
available memcache library.
|
||||
|
||||
This cache looks into the following packages/modules to find bindings for
|
||||
memcached:
|
||||
|
||||
- ``pylibmc``
|
||||
- ``google.appengine.api.memcached``
|
||||
- ``memcached``
|
||||
|
||||
Implementation notes: This cache backend works around some limitations in
|
||||
memcached to simplify the interface. For example unicode keys are encoded
|
||||
to utf-8 on the fly. Methods such as :meth:`~BaseCache.get_dict` return
|
||||
@@ -289,7 +360,8 @@ class MemcachedCache(BaseCache):
|
||||
:param servers: a list or tuple of server addresses or alternatively
|
||||
a :class:`memcache.Client` or a compatible client.
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
specified on :meth:`~BaseCache.set`. A timeout of
|
||||
0 indicates taht the cache never expires.
|
||||
:param key_prefix: a prefix that is added before all keys. This makes it
|
||||
possible to use the same memcached server for different
|
||||
applications. Keep in mind that
|
||||
@@ -310,15 +382,25 @@ class MemcachedCache(BaseCache):
|
||||
# client.
|
||||
self._client = servers
|
||||
|
||||
self.key_prefix = key_prefix
|
||||
self.key_prefix = to_native(key_prefix)
|
||||
|
||||
def get(self, key):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
def _normalize_key(self, key):
|
||||
key = to_native(key, 'utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
return key
|
||||
|
||||
def _normalize_timeout(self, timeout):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
if timeout > 0:
|
||||
timeout = int(time()) + timeout
|
||||
return timeout
|
||||
|
||||
def get(self, key):
|
||||
key = self._normalize_key(key)
|
||||
# memcached doesn't support keys longer than that. Because often
|
||||
# checks for so long keys can occour because it's tested from user
|
||||
# checks for so long keys can occur because it's tested from user
|
||||
# submitted data etc we fail silently for getting.
|
||||
if _test_memcached_key(key):
|
||||
return self._client.get(key)
|
||||
@@ -327,13 +409,9 @@ class MemcachedCache(BaseCache):
|
||||
key_mapping = {}
|
||||
have_encoded_keys = False
|
||||
for key in keys:
|
||||
if isinstance(key, unicode):
|
||||
encoded_key = key.encode('utf-8')
|
||||
encoded_key = self._normalize_key(key)
|
||||
if not isinstance(key, str):
|
||||
have_encoded_keys = True
|
||||
else:
|
||||
encoded_key = key
|
||||
if self.key_prefix:
|
||||
encoded_key = self.key_prefix + encoded_key
|
||||
if _test_memcached_key(key):
|
||||
key_mapping[encoded_key] = key
|
||||
d = rv = self._client.get_multi(key_mapping.keys())
|
||||
@@ -348,74 +426,58 @@ class MemcachedCache(BaseCache):
|
||||
return rv
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
self._client.add(key, value, timeout)
|
||||
key = self._normalize_key(key)
|
||||
timeout = self._normalize_timeout(timeout)
|
||||
return self._client.add(key, value, timeout)
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
self._client.set(key, value, timeout)
|
||||
key = self._normalize_key(key)
|
||||
timeout = self._normalize_timeout(timeout)
|
||||
return self._client.set(key, value, timeout)
|
||||
|
||||
def get_many(self, *keys):
|
||||
d = self.get_dict(*keys)
|
||||
return [d[key] for key in keys]
|
||||
|
||||
def set_many(self, mapping, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
new_mapping = {}
|
||||
for key, value in _items(mapping):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
key = self._normalize_key(key)
|
||||
new_mapping[key] = value
|
||||
self._client.set_multi(new_mapping, timeout)
|
||||
|
||||
timeout = self._normalize_timeout(timeout)
|
||||
failed_keys = self._client.set_multi(new_mapping, timeout)
|
||||
return not failed_keys
|
||||
|
||||
def delete(self, key):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
key = self._normalize_key(key)
|
||||
if _test_memcached_key(key):
|
||||
self._client.delete(key)
|
||||
return self._client.delete(key)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
new_keys = []
|
||||
for key in keys:
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
key = self._normalize_key(key)
|
||||
if _test_memcached_key(key):
|
||||
new_keys.append(key)
|
||||
self._client.delete_multi(new_keys)
|
||||
return self._client.delete_multi(new_keys)
|
||||
|
||||
def has(self, key):
|
||||
key = self._normalize_key(key)
|
||||
if _test_memcached_key(key):
|
||||
return self._client.append(key, '')
|
||||
return False
|
||||
|
||||
def clear(self):
|
||||
self._client.flush_all()
|
||||
return self._client.flush_all()
|
||||
|
||||
def inc(self, key, delta=1):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
self._client.incr(key, delta)
|
||||
key = self._normalize_key(key)
|
||||
return self._client.incr(key, delta)
|
||||
|
||||
def dec(self, key, delta=1):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
key = self.key_prefix + key
|
||||
self._client.decr(key, delta)
|
||||
key = self._normalize_key(key)
|
||||
return self._client.decr(key, delta)
|
||||
|
||||
def import_preferred_memcache_lib(self, servers):
|
||||
"""Returns an initialized memcache client. Used by the constructor."""
|
||||
@@ -446,6 +508,7 @@ GAEMemcachedCache = MemcachedCache
|
||||
|
||||
|
||||
class RedisCache(BaseCache):
|
||||
|
||||
"""Uses the Redis key-value store as a cache backend.
|
||||
|
||||
The first argument can be either a string denoting address of the Redis
|
||||
@@ -465,46 +528,66 @@ class RedisCache(BaseCache):
|
||||
.. versionchanged:: 0.8.3
|
||||
This cache backend now supports password authentication.
|
||||
|
||||
.. versionchanged:: 0.10
|
||||
``**kwargs`` is now passed to the redis object.
|
||||
|
||||
:param host: address of the Redis server or an object which API is
|
||||
compatible with the official Python Redis client (redis-py).
|
||||
:param port: port number on which Redis server listens for connections.
|
||||
:param password: password authentication for the Redis server.
|
||||
:param db: db (zero-based numeric index) on Redis Server to connect.
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
specified on :meth:`~BaseCache.set`. A timeout of
|
||||
0 indicates that the cache never expires.
|
||||
:param key_prefix: A prefix that should be added to all keys.
|
||||
|
||||
Any additional keyword arguments will be passed to ``redis.Redis``.
|
||||
"""
|
||||
|
||||
def __init__(self, host='localhost', port=6379, password=None,
|
||||
db=0, default_timeout=300, key_prefix=None):
|
||||
db=0, default_timeout=300, key_prefix=None, **kwargs):
|
||||
BaseCache.__init__(self, default_timeout)
|
||||
if isinstance(host, string_types):
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
raise RuntimeError('no redis module found')
|
||||
self._client = redis.Redis(host=host, port=port, password=password, db=db)
|
||||
if kwargs.get('decode_responses', None):
|
||||
raise ValueError('decode_responses is not supported by '
|
||||
'RedisCache.')
|
||||
self._client = redis.Redis(host=host, port=port, password=password,
|
||||
db=db, **kwargs)
|
||||
else:
|
||||
self._client = host
|
||||
self.key_prefix = key_prefix or ''
|
||||
|
||||
def _get_expiration(self, timeout):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
if timeout == 0:
|
||||
timeout = -1
|
||||
return timeout
|
||||
|
||||
def dump_object(self, value):
|
||||
"""Dumps an object into a string for redis. By default it serializes
|
||||
integers as regular string and pickle dumps everything else.
|
||||
"""
|
||||
t = type(value)
|
||||
if t is int or t is long:
|
||||
return str(value)
|
||||
return '!' + pickle.dumps(value)
|
||||
if t in integer_types:
|
||||
return str(value).encode('ascii')
|
||||
return b'!' + pickle.dumps(value)
|
||||
|
||||
def load_object(self, value):
|
||||
"""The reversal of :meth:`dump_object`. This might be callde with
|
||||
"""The reversal of :meth:`dump_object`. This might be called with
|
||||
None.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if value.startswith('!'):
|
||||
return pickle.loads(value[1:])
|
||||
if value.startswith(b'!'):
|
||||
try:
|
||||
return pickle.loads(value[1:])
|
||||
except pickle.PickleError:
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
@@ -520,54 +603,71 @@ class RedisCache(BaseCache):
|
||||
return [self.load_object(x) for x in self._client.mget(keys)]
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
timeout = self._get_expiration(timeout)
|
||||
dump = self.dump_object(value)
|
||||
self._client.setex(self.key_prefix + key, dump, timeout)
|
||||
if timeout == -1:
|
||||
result = self._client.set(name=self.key_prefix + key,
|
||||
value=dump)
|
||||
else:
|
||||
result = self._client.setex(name=self.key_prefix + key,
|
||||
value=dump, time=timeout)
|
||||
return result
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
timeout = self._get_expiration(timeout)
|
||||
dump = self.dump_object(value)
|
||||
added = self._client.setnx(self.key_prefix + key, dump)
|
||||
if added:
|
||||
self._client.expire(self.key_prefix + key, timeout)
|
||||
return (
|
||||
self._client.setnx(name=self.key_prefix + key, value=dump) and
|
||||
self._client.expire(name=self.key_prefix + key, time=timeout)
|
||||
)
|
||||
|
||||
def set_many(self, mapping, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
pipe = self._client.pipeline()
|
||||
timeout = self._get_expiration(timeout)
|
||||
# Use transaction=False to batch without calling redis MULTI
|
||||
# which is not supported by twemproxy
|
||||
pipe = self._client.pipeline(transaction=False)
|
||||
|
||||
for key, value in _items(mapping):
|
||||
dump = self.dump_object(value)
|
||||
pipe.setex(self.key_prefix + key, dump, timeout)
|
||||
pipe.execute()
|
||||
if timeout == -1:
|
||||
pipe.set(name=self.key_prefix + key, value=dump)
|
||||
else:
|
||||
pipe.setex(name=self.key_prefix + key, value=dump,
|
||||
time=timeout)
|
||||
return pipe.execute()
|
||||
|
||||
def delete(self, key):
|
||||
self._client.delete(self.key_prefix + key)
|
||||
return self._client.delete(self.key_prefix + key)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
if not keys:
|
||||
return
|
||||
if self.key_prefix:
|
||||
keys = [self.key_prefix + key for key in keys]
|
||||
self._client.delete(*keys)
|
||||
return self._client.delete(*keys)
|
||||
|
||||
def has(self, key):
|
||||
return self._client.exists(self.key_prefix + key)
|
||||
|
||||
def clear(self):
|
||||
status = False
|
||||
if self.key_prefix:
|
||||
keys = self._client.keys(self.key_prefix + '*')
|
||||
if keys:
|
||||
self._client.delete(*keys)
|
||||
status = self._client.delete(*keys)
|
||||
else:
|
||||
self._client.flushdb()
|
||||
status = self._client.flushdb()
|
||||
return status
|
||||
|
||||
def inc(self, key, delta=1):
|
||||
return self._client.incr(self.key_prefix + key, delta)
|
||||
return self._client.incr(name=self.key_prefix + key, amount=delta)
|
||||
|
||||
def dec(self, key, delta=1):
|
||||
return self._client.decr(self.key_prefix + key, delta)
|
||||
return self._client.decr(name=self.key_prefix + key, amount=delta)
|
||||
|
||||
|
||||
class FileSystemCache(BaseCache):
|
||||
|
||||
"""A cache that stores the items on the file system. This cache depends
|
||||
on being the only user of the `cache_dir`. Make absolutely sure that
|
||||
nobody but this cache stores files there or otherwise the cache will
|
||||
@@ -577,20 +677,26 @@ class FileSystemCache(BaseCache):
|
||||
:param threshold: the maximum number of items the cache stores before
|
||||
it starts deleting some.
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
specified on :meth:`~BaseCache.set`. A timeout of
|
||||
0 indicates that the cache never expires.
|
||||
:param mode: the file mode wanted for the cache files, default 0600
|
||||
"""
|
||||
|
||||
#: used for temporary files by the FileSystemCache
|
||||
_fs_transaction_suffix = '.__wz_cache'
|
||||
|
||||
def __init__(self, cache_dir, threshold=500, default_timeout=300, mode=0o600):
|
||||
def __init__(self, cache_dir, threshold=500, default_timeout=300,
|
||||
mode=0o600):
|
||||
BaseCache.__init__(self, default_timeout)
|
||||
self._path = cache_dir
|
||||
self._threshold = threshold
|
||||
self._mode = mode
|
||||
if not os.path.exists(self._path):
|
||||
|
||||
try:
|
||||
os.makedirs(self._path)
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def _list_dir(self):
|
||||
"""return a list of (fully qualified) cache filenames
|
||||
@@ -602,77 +708,88 @@ class FileSystemCache(BaseCache):
|
||||
entries = self._list_dir()
|
||||
if len(entries) > self._threshold:
|
||||
now = time()
|
||||
for idx, fname in enumerate(entries):
|
||||
remove = False
|
||||
f = None
|
||||
try:
|
||||
try:
|
||||
f = open(fname, 'rb')
|
||||
try:
|
||||
for idx, fname in enumerate(entries):
|
||||
remove = False
|
||||
with open(fname, 'rb') as f:
|
||||
expires = pickle.load(f)
|
||||
remove = expires <= now or idx % 3 == 0
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
if remove:
|
||||
try:
|
||||
remove = (expires != 0 and expires <= now) or idx % 3 == 0
|
||||
|
||||
if remove:
|
||||
os.remove(fname)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
for fname in self._list_dir():
|
||||
try:
|
||||
os.remove(fname)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_filename(self, key):
|
||||
if isinstance(key, text_type):
|
||||
key = key.encode('utf-8') #XXX unicode review
|
||||
key = key.encode('utf-8') # XXX unicode review
|
||||
hash = md5(key).hexdigest()
|
||||
return os.path.join(self._path, hash)
|
||||
|
||||
def get(self, key):
|
||||
filename = self._get_filename(key)
|
||||
try:
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
if pickle.load(f) >= time():
|
||||
with open(filename, 'rb') as f:
|
||||
pickle_time = pickle.load(f)
|
||||
if pickle_time == 0 or pickle_time >= time():
|
||||
return pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
os.remove(filename)
|
||||
except Exception:
|
||||
else:
|
||||
os.remove(filename)
|
||||
return None
|
||||
except (IOError, OSError, pickle.PickleError):
|
||||
return None
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
filename = self._get_filename(key)
|
||||
if not os.path.exists(filename):
|
||||
self.set(key, value, timeout)
|
||||
return self.set(key, value, timeout)
|
||||
return False
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
timeout = int(time() + self.default_timeout)
|
||||
elif timeout != 0:
|
||||
timeout = int(time() + timeout)
|
||||
filename = self._get_filename(key)
|
||||
self._prune()
|
||||
try:
|
||||
fd, tmp = tempfile.mkstemp(suffix=self._fs_transaction_suffix,
|
||||
dir=self._path)
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
pickle.dump(int(time() + timeout), f, 1)
|
||||
with os.fdopen(fd, 'wb') as f:
|
||||
pickle.dump(timeout, f, 1)
|
||||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
||||
finally:
|
||||
f.close()
|
||||
rename(tmp, filename)
|
||||
os.chmod(filename, self._mode)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def delete(self, key):
|
||||
try:
|
||||
os.remove(self._get_filename(key))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def has(self, key):
|
||||
filename = self._get_filename(key)
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
pickle_time = pickle.load(f)
|
||||
if pickle_time == 0 or pickle_time >= time():
|
||||
return True
|
||||
else:
|
||||
os.remove(filename)
|
||||
return False
|
||||
except (IOError, OSError, pickle.PickleError):
|
||||
return False
|
||||
|
||||
@@ -22,11 +22,13 @@ except ImportError:
|
||||
from urllib.parse import unquote
|
||||
|
||||
from werkzeug.http import parse_options_header, parse_cache_control_header, \
|
||||
parse_set_header
|
||||
parse_set_header
|
||||
from werkzeug.useragents import UserAgent
|
||||
from werkzeug.datastructures import Headers, ResponseCacheControl
|
||||
|
||||
|
||||
class CGIRootFix(object):
|
||||
|
||||
"""Wrap the application in this middleware if you are using FastCGI or CGI
|
||||
and you have problems with your app root being set to the cgi script's path
|
||||
instead of the path users are going to visit
|
||||
@@ -52,7 +54,7 @@ class CGIRootFix(object):
|
||||
if 'SERVER_SOFTWARE' not in environ or \
|
||||
environ['SERVER_SOFTWARE'] < 'lighttpd/1.4.28':
|
||||
environ['PATH_INFO'] = environ.get('SCRIPT_NAME', '') + \
|
||||
environ.get('PATH_INFO', '')
|
||||
environ.get('PATH_INFO', '')
|
||||
environ['SCRIPT_NAME'] = self.app_root.strip('/')
|
||||
return self.app(environ, start_response)
|
||||
|
||||
@@ -61,6 +63,7 @@ LighttpdCGIRootFix = CGIRootFix
|
||||
|
||||
|
||||
class PathInfoFromRequestUriFix(object):
|
||||
|
||||
"""On windows environment variables are limited to the system charset
|
||||
which makes it impossible to store the `PATH_INFO` variable in the
|
||||
environment without loss of information on some systems.
|
||||
@@ -92,9 +95,14 @@ class PathInfoFromRequestUriFix(object):
|
||||
|
||||
|
||||
class ProxyFix(object):
|
||||
|
||||
"""This middleware can be applied to add HTTP proxy support to an
|
||||
application that was not designed with HTTP proxies in mind. It
|
||||
sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers.
|
||||
sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers. While
|
||||
Werkzeug-based applications already can use
|
||||
:py:func:`werkzeug.wsgi.get_host` to retrieve the current host even if
|
||||
behind proxy setups, this middleware can be used for applications which
|
||||
access the WSGI environment directly.
|
||||
|
||||
If you have more than one proxy server in front of your app, set
|
||||
`num_proxies` accordingly.
|
||||
@@ -145,6 +153,7 @@ class ProxyFix(object):
|
||||
|
||||
|
||||
class HeaderRewriterFix(object):
|
||||
|
||||
"""This middleware can remove response headers and add others. This
|
||||
is for example useful to remove the `Date` header from responses if you
|
||||
are using a server that adds that header, no matter if it's present or
|
||||
@@ -177,6 +186,7 @@ class HeaderRewriterFix(object):
|
||||
|
||||
|
||||
class InternetExplorerFix(object):
|
||||
|
||||
"""This middleware fixes a couple of bugs with Microsoft Internet
|
||||
Explorer. Currently the following fixes are applied:
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ r"""
|
||||
|
||||
.. _greenlet: http://codespeak.net/py/dist/greenlet.html
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
try:
|
||||
@@ -64,6 +64,7 @@ def _newline(reference_string):
|
||||
|
||||
@implements_iterator
|
||||
class IterIO(object):
|
||||
|
||||
"""Instances of this object implement an interface compatible with the
|
||||
standard Python :class:`file` object. Streams are either read-only or
|
||||
write-only depending on how the object is created.
|
||||
@@ -160,6 +161,7 @@ class IterIO(object):
|
||||
|
||||
|
||||
class IterI(IterIO):
|
||||
|
||||
"""Convert an stream into an iterator."""
|
||||
|
||||
def __new__(cls, func, sentinel=''):
|
||||
@@ -214,6 +216,7 @@ class IterI(IterIO):
|
||||
|
||||
|
||||
class IterO(IterIO):
|
||||
|
||||
"""Iter output. Wrap an iterator and give it a stream like interface."""
|
||||
|
||||
def __new__(cls, gen, sentinel=''):
|
||||
@@ -305,7 +308,10 @@ class IterO(IterIO):
|
||||
nl_pos = self._buf.find(_newline(self._buf), self.pos)
|
||||
buf = []
|
||||
try:
|
||||
pos = self.pos
|
||||
if self._buf is None:
|
||||
pos = self.pos
|
||||
else:
|
||||
pos = len(self._buf)
|
||||
while nl_pos < 0:
|
||||
item = next(self._gen)
|
||||
local_pos = item.find(_newline(item))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Addon module that allows to create a JavaScript function from a map
|
||||
that generates rules.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
try:
|
||||
@@ -26,13 +26,13 @@ from werkzeug._compat import iteritems
|
||||
def render_template(name_parts, rules, converters):
|
||||
result = u''
|
||||
if name_parts:
|
||||
for idx in xrange(0, len(name_parts) - 1):
|
||||
for idx in range(0, len(name_parts) - 1):
|
||||
name = u'.'.join(name_parts[:idx + 1])
|
||||
result += u"if (typeof %s === 'undefined') %s = {}\n" % (name, name)
|
||||
result += '%s = ' % '.'.join(name_parts)
|
||||
result += """(function (server_name, script_name, subdomain, url_scheme) {
|
||||
var converters = %(converters)s;
|
||||
var rules = $rules;
|
||||
var converters = [%(converters)s];
|
||||
var rules = %(rules)s;
|
||||
function in_array(array, value) {
|
||||
if (array.indexOf != undefined) {
|
||||
return array.indexOf(value) != -1;
|
||||
@@ -163,7 +163,9 @@ def render_template(name_parts, rules, converters):
|
||||
+ '/' + lstrip(rv.path, '/');
|
||||
}
|
||||
};
|
||||
})""" % {'converters': u', '.join(converters)}
|
||||
})""" % {'converters': u', '.join(converters),
|
||||
'rules': rules}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
.. _Trac: http://trac.edgewall.org/
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from warnings import warn
|
||||
@@ -18,6 +18,7 @@ from werkzeug.wsgi import LimitedStream
|
||||
|
||||
|
||||
class StreamLimitMiddleware(object):
|
||||
|
||||
"""Limits the input stream to a given number of bytes. This is useful if
|
||||
you have a WSGI application that reads form data into memory (django for
|
||||
example) and you don't want users to harm the server by uploading tons of
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
It's strongly recommended to use it during development.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from urlparse import urlparse
|
||||
@@ -29,17 +29,19 @@ from werkzeug._compat import string_types
|
||||
|
||||
|
||||
class WSGIWarning(Warning):
|
||||
|
||||
"""Warning class for WSGI warnings."""
|
||||
|
||||
|
||||
class HTTPWarning(Warning):
|
||||
|
||||
"""Warning class for HTTP warnings."""
|
||||
|
||||
|
||||
def check_string(context, obj, stacklevel=3):
|
||||
if type(obj) is not str:
|
||||
warn(WSGIWarning('%s requires bytestrings, got %s' %
|
||||
(context, obj.__class__.__name__)))
|
||||
(context, obj.__class__.__name__)))
|
||||
|
||||
|
||||
class InputStream(object):
|
||||
@@ -160,7 +162,7 @@ class GuardedIterator(object):
|
||||
if key not in ('expires', 'content-location') and \
|
||||
is_entity_header(key):
|
||||
warn(HTTPWarning('entity header %r found in 304 '
|
||||
'response' % key))
|
||||
'response' % key))
|
||||
if bytes_sent:
|
||||
warn(HTTPWarning('304 responses must not have a body'))
|
||||
elif 100 <= status_code < 200 or status_code == 204:
|
||||
@@ -184,6 +186,7 @@ class GuardedIterator(object):
|
||||
|
||||
|
||||
class LintMiddleware(object):
|
||||
|
||||
"""This middleware wraps an application and warns on common errors.
|
||||
Among other thing it currently checks for the following problems:
|
||||
|
||||
@@ -220,7 +223,7 @@ class LintMiddleware(object):
|
||||
'wsgi.run_once'):
|
||||
if key not in environ:
|
||||
warn(WSGIWarning('required environment key %r not found'
|
||||
% key), stacklevel=3)
|
||||
% key), stacklevel=3)
|
||||
if environ['wsgi.version'] != (1, 0):
|
||||
warn(WSGIWarning('environ is not a WSGI 1.0 environ'),
|
||||
stacklevel=3)
|
||||
@@ -234,7 +237,6 @@ class LintMiddleware(object):
|
||||
warn(WSGIWarning('PATH_INFO does not start with a slash: %r'
|
||||
% path_info), stacklevel=3)
|
||||
|
||||
|
||||
def check_start_response(self, status, headers, exc_info):
|
||||
check_string('status', status)
|
||||
status_code = status.split(None, 1)[0]
|
||||
@@ -261,7 +263,7 @@ class LintMiddleware(object):
|
||||
if name.lower() == 'status':
|
||||
warn(WSGIWarning('The status header is not supported due to '
|
||||
'conflicts with the CGI spec.'),
|
||||
stacklevel=3)
|
||||
stacklevel=3)
|
||||
|
||||
if exc_info is not None and not isinstance(exc_info, tuple):
|
||||
warn(WSGIWarning('invalid value for exc_info'), stacklevel=3)
|
||||
@@ -274,7 +276,10 @@ class LintMiddleware(object):
|
||||
def check_headers(self, headers):
|
||||
etag = headers.get('etag')
|
||||
if etag is not None:
|
||||
if etag.startswith('w/'):
|
||||
if etag.startswith(('W/', 'w/')):
|
||||
if etag.startswith('w/'):
|
||||
warn(HTTPWarning('weak etag indicator should be upcase.'),
|
||||
stacklevel=4)
|
||||
etag = etag[2:]
|
||||
if not (etag[:1] == etag[-1:] == '"'):
|
||||
warn(HTTPWarning('unquoted etag emitted.'), stacklevel=4)
|
||||
@@ -314,7 +319,7 @@ class LintMiddleware(object):
|
||||
def checking_start_response(*args, **kwargs):
|
||||
if len(args) not in (2, 3):
|
||||
warn(WSGIWarning('Invalid number of arguments: %s, expected '
|
||||
'2 or 3' % len(args), stacklevel=2))
|
||||
'2 or 3' % len(args), stacklevel=2))
|
||||
if kwargs:
|
||||
warn(WSGIWarning('no keyword arguments allowed.'))
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
from werkzeug.contrib.profiler import ProfilerMiddleware
|
||||
app = ProfilerMiddleware(app)
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys, time, os.path
|
||||
import sys
|
||||
import time
|
||||
import os.path
|
||||
try:
|
||||
try:
|
||||
from cProfile import Profile
|
||||
@@ -29,6 +31,7 @@ except ImportError:
|
||||
|
||||
|
||||
class MergeStream(object):
|
||||
|
||||
"""An object that redirects `write` calls to multiple streams.
|
||||
Use this to log to both `sys.stdout` and a file::
|
||||
|
||||
@@ -48,6 +51,7 @@ class MergeStream(object):
|
||||
|
||||
|
||||
class ProfilerMiddleware(object):
|
||||
|
||||
"""Simple profiler middleware. Wraps a WSGI application and profiles
|
||||
a request. This intentionally buffers the response so that timings are
|
||||
more exact.
|
||||
@@ -97,17 +101,18 @@ class ProfilerMiddleware(object):
|
||||
p = Profile()
|
||||
start = time.time()
|
||||
p.runcall(runapp)
|
||||
body = ''.join(response_body)
|
||||
body = b''.join(response_body)
|
||||
elapsed = time.time() - start
|
||||
|
||||
if self._profile_dir is not None:
|
||||
prof_filename = os.path.join(self._profile_dir,
|
||||
'%s.%s.%06dms.%d.prof' % (
|
||||
environ['REQUEST_METHOD'],
|
||||
environ.get('PATH_INFO').strip('/').replace('/', '.') or 'root',
|
||||
elapsed * 1000.0,
|
||||
time.time()
|
||||
))
|
||||
'%s.%s.%06dms.%d.prof' % (
|
||||
environ['REQUEST_METHOD'],
|
||||
environ.get('PATH_INFO').strip(
|
||||
'/').replace('/', '.') or 'root',
|
||||
elapsed * 1000.0,
|
||||
time.time()
|
||||
))
|
||||
p.dump_stats(prof_filename)
|
||||
|
||||
else:
|
||||
|
||||
@@ -85,7 +85,7 @@ r"""
|
||||
request.client_session.save_cookie(response)
|
||||
return response(environ, start_response)
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import pickle
|
||||
@@ -103,10 +103,12 @@ from werkzeug._compat import to_native
|
||||
|
||||
|
||||
class UnquoteError(Exception):
|
||||
|
||||
"""Internal exception used to signal failures on quoting."""
|
||||
|
||||
|
||||
class SecureCookie(ModificationTrackingDict):
|
||||
|
||||
"""Represents a secure cookie. You can subclass this class and provide
|
||||
an alternative mac method. The import thing is that the mac method
|
||||
is a function with a similar interface to the hashlib. Required
|
||||
@@ -248,7 +250,7 @@ class SecureCookie(ModificationTrackingDict):
|
||||
mac = hmac(secret_key, None, cls.hash_method)
|
||||
for item in data.split(b'&'):
|
||||
mac.update(b'|' + item)
|
||||
if not b'=' in item:
|
||||
if b'=' not in item:
|
||||
items = None
|
||||
break
|
||||
key, value = item.split(b'=', 1)
|
||||
|
||||
@@ -48,12 +48,11 @@ r"""
|
||||
response.set_cookie('cookie_name', request.session.sid)
|
||||
return response(environ, start_response)
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from os import path
|
||||
from time import time
|
||||
@@ -66,6 +65,7 @@ from werkzeug.utils import dump_cookie, parse_cookie
|
||||
from werkzeug.wsgi import ClosingIterator
|
||||
from werkzeug.posixemulation import rename
|
||||
from werkzeug._compat import PY2, text_type
|
||||
from werkzeug.filesystem import get_filesystem_encoding
|
||||
|
||||
|
||||
_sha1_re = re.compile(r'^[a-f0-9]{40}$')
|
||||
@@ -74,7 +74,7 @@ _sha1_re = re.compile(r'^[a-f0-9]{40}$')
|
||||
def _urandom():
|
||||
if hasattr(os, 'urandom'):
|
||||
return os.urandom(30)
|
||||
return random()
|
||||
return text_type(random()).encode('ascii')
|
||||
|
||||
|
||||
def generate_key(salt=None):
|
||||
@@ -112,6 +112,7 @@ class ModificationTrackingDict(CallbackDict):
|
||||
|
||||
|
||||
class Session(ModificationTrackingDict):
|
||||
|
||||
"""Subclass of a dict that keeps track of direct object changes. Changes
|
||||
in mutable structures are not tracked, for those you have to set
|
||||
`modified` to `True` by hand.
|
||||
@@ -142,6 +143,7 @@ class Session(ModificationTrackingDict):
|
||||
|
||||
|
||||
class SessionStore(object):
|
||||
|
||||
"""Baseclass for all session stores. The Werkzeug contrib module does not
|
||||
implement any useful stores besides the filesystem store, application
|
||||
developers are encouraged to create their own stores.
|
||||
@@ -191,6 +193,7 @@ _fs_transaction_suffix = '.__wz_sess'
|
||||
|
||||
|
||||
class FilesystemSessionStore(SessionStore):
|
||||
|
||||
"""Simple example session store that saves sessions on the filesystem.
|
||||
This store works best on POSIX systems and Windows Vista / Windows
|
||||
Server 2008 and newer.
|
||||
@@ -220,7 +223,7 @@ class FilesystemSessionStore(SessionStore):
|
||||
self.path = path
|
||||
if isinstance(filename_template, text_type) and PY2:
|
||||
filename_template = filename_template.encode(
|
||||
sys.getfilesystemencoding() or 'utf-8')
|
||||
get_filesystem_encoding())
|
||||
assert not filename_template.endswith(_fs_transaction_suffix), \
|
||||
'filename templates may not end with %s' % _fs_transaction_suffix
|
||||
self.filename_template = filename_template
|
||||
@@ -232,7 +235,7 @@ class FilesystemSessionStore(SessionStore):
|
||||
# you might reconfigure the session object to have a more
|
||||
# arbitrary string.
|
||||
if isinstance(sid, text_type) and PY2:
|
||||
sid = sid.encode(sys.getfilesystemencoding() or 'utf-8')
|
||||
sid = sid.encode(get_filesystem_encoding())
|
||||
return path.join(self.path, self.filename_template % sid)
|
||||
|
||||
def save(self, session):
|
||||
@@ -296,6 +299,7 @@ class FilesystemSessionStore(SessionStore):
|
||||
|
||||
|
||||
class SessionMiddleware(object):
|
||||
|
||||
"""A simple middleware that puts the session object of a store provided
|
||||
into the WSGI environ. It automatically sets cookies and restores
|
||||
sessions.
|
||||
@@ -339,10 +343,10 @@ class SessionMiddleware(object):
|
||||
if session.should_save:
|
||||
self.store.save(session)
|
||||
headers.append(('Set-Cookie', dump_cookie(self.cookie_name,
|
||||
session.sid, self.cookie_age,
|
||||
self.cookie_expires, self.cookie_path,
|
||||
self.cookie_domain, self.cookie_secure,
|
||||
self.cookie_httponly)))
|
||||
session.sid, self.cookie_age,
|
||||
self.cookie_expires, self.cookie_path,
|
||||
self.cookie_domain, self.cookie_secure,
|
||||
self.cookie_httponly)))
|
||||
return start_response(status, headers, exc_info)
|
||||
return ClosingIterator(self.app(environ, injecting_start_response),
|
||||
lambda: self.store.save_if_modified(session))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
A response wrapper which adds various cached attributes for
|
||||
simplified assertions on various content types.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from werkzeug.utils import cached_property, import_string
|
||||
@@ -21,6 +21,7 @@ warn(DeprecationWarning('werkzeug.contrib.testtools is deprecated and '
|
||||
|
||||
|
||||
class ContentAccessors(object):
|
||||
|
||||
"""
|
||||
A mixin class for response objects that provides a couple of useful
|
||||
accessors for unittesting.
|
||||
@@ -50,7 +51,7 @@ class ContentAccessors(object):
|
||||
from lxml.html import fromstring
|
||||
except ImportError:
|
||||
fromstring = etree.HTML
|
||||
if self.mimetype=='text/html':
|
||||
if self.mimetype == 'text/html':
|
||||
return fromstring(self.data)
|
||||
return etree.XML(self.data)
|
||||
lxml = cached_property(lxml)
|
||||
@@ -68,4 +69,5 @@ class ContentAccessors(object):
|
||||
|
||||
|
||||
class TestResponse(Response, ContentAccessors):
|
||||
|
||||
"""Pass this to `werkzeug.test.Client` for easier unittesting."""
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
Afterwards this request object provides the extra functionality of the
|
||||
:class:`JSONRequestMixin`.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import codecs
|
||||
@@ -42,6 +42,7 @@ def is_known_charset(charset):
|
||||
|
||||
|
||||
class JSONRequestMixin(object):
|
||||
|
||||
"""Add json method to a request object. This will parse the input data
|
||||
through simplejson if possible.
|
||||
|
||||
@@ -55,12 +56,13 @@ class JSONRequestMixin(object):
|
||||
if 'json' not in self.environ.get('CONTENT_TYPE', ''):
|
||||
raise BadRequest('Not a JSON request')
|
||||
try:
|
||||
return loads(self.data)
|
||||
return loads(self.data.decode(self.charset, self.encoding_errors))
|
||||
except Exception:
|
||||
raise BadRequest('Unable to read JSON request')
|
||||
|
||||
|
||||
class ProtobufRequestMixin(object):
|
||||
|
||||
"""Add protobuf parsing method to a request object. This will parse the
|
||||
input data through `protobuf`_ if possible.
|
||||
|
||||
@@ -95,6 +97,7 @@ class ProtobufRequestMixin(object):
|
||||
|
||||
|
||||
class RoutingArgsRequestMixin(object):
|
||||
|
||||
"""This request mixin adds support for the wsgiorg routing args
|
||||
`specification`_.
|
||||
|
||||
@@ -137,6 +140,7 @@ class RoutingArgsRequestMixin(object):
|
||||
|
||||
|
||||
class ReverseSlashBehaviorRequestMixin(object):
|
||||
|
||||
"""This mixin reverses the trailing slash behavior of :attr:`script_root`
|
||||
and :attr:`path`. This makes it possible to use :func:`~urlparse.urljoin`
|
||||
directly on the paths.
|
||||
@@ -177,6 +181,7 @@ class ReverseSlashBehaviorRequestMixin(object):
|
||||
|
||||
|
||||
class DynamicCharsetRequestMixin(object):
|
||||
|
||||
""""If this mixin is mixed into a request class it will provide
|
||||
a dynamic `charset` attribute. This means that if the charset is
|
||||
transmitted in the content type headers it's used from there.
|
||||
@@ -233,6 +238,7 @@ class DynamicCharsetRequestMixin(object):
|
||||
|
||||
|
||||
class DynamicCharsetResponseMixin(object):
|
||||
|
||||
"""If this mixin is mixed into a response class it will provide
|
||||
a dynamic `charset` attribute. This means that if the charset is
|
||||
looked up and stored in the `Content-Type` header and updates
|
||||
|
||||
@@ -5,18 +5,20 @@
|
||||
|
||||
This module provides mixins and classes with an immutable interface.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
import codecs
|
||||
import mimetypes
|
||||
from copy import deepcopy
|
||||
from itertools import repeat
|
||||
|
||||
from werkzeug._internal import _missing, _empty_stream
|
||||
from werkzeug._compat import iterkeys, itervalues, iteritems, iterlists, \
|
||||
PY2, text_type, integer_types, string_types, make_literal_wrapper
|
||||
PY2, text_type, integer_types, string_types, make_literal_wrapper, \
|
||||
to_native
|
||||
from werkzeug.filesystem import get_filesystem_encoding
|
||||
|
||||
|
||||
_locale_delim_re = re.compile(r'[_-]')
|
||||
@@ -48,12 +50,13 @@ def iter_multi_items(mapping):
|
||||
def native_itermethods(names):
|
||||
if not PY2:
|
||||
return lambda x: x
|
||||
|
||||
def setmethod(cls, name):
|
||||
itermethod = getattr(cls, name)
|
||||
setattr(cls, 'iter%s' % name, itermethod)
|
||||
listmethod = lambda self, *a, **kw: list(itermethod(self, *a, **kw))
|
||||
listmethod.__doc__ = \
|
||||
'Like :py:meth:`iter%s`, but returns a list.' % name
|
||||
'Like :py:meth:`iter%s`, but returns a list.' % name
|
||||
setattr(cls, name, listmethod)
|
||||
|
||||
def wrap(cls):
|
||||
@@ -64,6 +67,7 @@ def native_itermethods(names):
|
||||
|
||||
|
||||
class ImmutableListMixin(object):
|
||||
|
||||
"""Makes a :class:`list` immutable.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -119,6 +123,7 @@ class ImmutableListMixin(object):
|
||||
|
||||
|
||||
class ImmutableList(ImmutableListMixin, list):
|
||||
|
||||
"""An immutable :class:`list`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -129,11 +134,12 @@ class ImmutableList(ImmutableListMixin, list):
|
||||
def __repr__(self):
|
||||
return '%s(%s)' % (
|
||||
self.__class__.__name__,
|
||||
dict.__repr__(self),
|
||||
list.__repr__(self),
|
||||
)
|
||||
|
||||
|
||||
class ImmutableDictMixin(object):
|
||||
|
||||
"""Makes a :class:`dict` immutable.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -183,6 +189,7 @@ class ImmutableDictMixin(object):
|
||||
|
||||
|
||||
class ImmutableMultiDictMixin(ImmutableDictMixin):
|
||||
|
||||
"""Makes a :class:`MultiDict` immutable.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -213,6 +220,7 @@ class ImmutableMultiDictMixin(ImmutableDictMixin):
|
||||
|
||||
|
||||
class UpdateDictMixin(object):
|
||||
|
||||
"""Makes dicts call `self.on_update` on modifications.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -257,6 +265,7 @@ class UpdateDictMixin(object):
|
||||
|
||||
|
||||
class TypeConversionDict(dict):
|
||||
|
||||
"""Works like a regular dict but the :meth:`get` method can perform
|
||||
type conversions. :class:`MultiDict` and :class:`CombinedMultiDict`
|
||||
are subclasses of this class and provide the same feature.
|
||||
@@ -295,6 +304,7 @@ class TypeConversionDict(dict):
|
||||
|
||||
|
||||
class ImmutableTypeConversionDict(ImmutableDictMixin, TypeConversionDict):
|
||||
|
||||
"""Works like a :class:`TypeConversionDict` but does not support
|
||||
modifications.
|
||||
|
||||
@@ -314,6 +324,7 @@ class ImmutableTypeConversionDict(ImmutableDictMixin, TypeConversionDict):
|
||||
|
||||
@native_itermethods(['keys', 'values', 'items', 'lists', 'listvalues'])
|
||||
class MultiDict(TypeConversionDict):
|
||||
|
||||
"""A :class:`MultiDict` is a dictionary subclass customized to deal with
|
||||
multiple values for the same key which is for example used by the parsing
|
||||
functions in the wrappers. This is necessary because some HTML form
|
||||
@@ -361,6 +372,8 @@ class MultiDict(TypeConversionDict):
|
||||
tmp = {}
|
||||
for key, value in iteritems(mapping):
|
||||
if isinstance(value, (tuple, list)):
|
||||
if len(value) == 0:
|
||||
continue
|
||||
value = list(value)
|
||||
else:
|
||||
value = [value]
|
||||
@@ -387,7 +400,9 @@ class MultiDict(TypeConversionDict):
|
||||
:raise KeyError: if the key does not exist.
|
||||
"""
|
||||
if key in self:
|
||||
return dict.__getitem__(self, key)[0]
|
||||
lst = dict.__getitem__(self, key)
|
||||
if len(lst) > 0:
|
||||
return lst[0]
|
||||
raise exceptions.BadRequestKeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
@@ -537,6 +552,10 @@ class MultiDict(TypeConversionDict):
|
||||
"""Return a shallow copy of this object."""
|
||||
return self.__class__(self)
|
||||
|
||||
def deepcopy(self, memo=None):
|
||||
"""Return a deep copy of this object."""
|
||||
return self.__class__(deepcopy(self.to_dict(flat=False), memo))
|
||||
|
||||
def to_dict(self, flat=True):
|
||||
"""Return the contents as regular dict. If `flat` is `True` the
|
||||
returned dict will only have the first item present, if `flat` is
|
||||
@@ -552,7 +571,23 @@ class MultiDict(TypeConversionDict):
|
||||
return dict(self.lists())
|
||||
|
||||
def update(self, other_dict):
|
||||
"""update() extends rather than replaces existing key lists."""
|
||||
"""update() extends rather than replaces existing key lists:
|
||||
|
||||
>>> a = MultiDict({'x': 1})
|
||||
>>> b = MultiDict({'x': 2, 'y': 3})
|
||||
>>> a.update(b)
|
||||
>>> a
|
||||
MultiDict([('y', 3), ('x', 1), ('x', 2)])
|
||||
|
||||
If the value list for a key in ``other_dict`` is empty, no new values
|
||||
will be added to the dict and the key will not be created:
|
||||
|
||||
>>> x = {'empty_list': []}
|
||||
>>> y = MultiDict()
|
||||
>>> y.update(x)
|
||||
>>> y
|
||||
MultiDict([])
|
||||
"""
|
||||
for key, value in iter_multi_items(other_dict):
|
||||
MultiDict.add(self, key, value)
|
||||
|
||||
@@ -605,11 +640,15 @@ class MultiDict(TypeConversionDict):
|
||||
def __copy__(self):
|
||||
return self.copy()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return self.deepcopy(memo=memo)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, list(iteritems(self, multi=True)))
|
||||
|
||||
|
||||
class _omd_bucket(object):
|
||||
|
||||
"""Wraps values in the :class:`OrderedMultiDict`. This makes it
|
||||
possible to keep an order over multiple different keys. It requires
|
||||
a lot of extra memory and slows down access a lot, but makes it
|
||||
@@ -642,6 +681,7 @@ class _omd_bucket(object):
|
||||
|
||||
@native_itermethods(['keys', 'values', 'items', 'lists', 'listvalues'])
|
||||
class OrderedMultiDict(MultiDict):
|
||||
|
||||
"""Works like a regular :class:`MultiDict` but preserves the
|
||||
order of the fields. To convert the ordered multi dict into a
|
||||
list you can use the :meth:`items` method and pass it ``multi=True``.
|
||||
@@ -818,7 +858,7 @@ class OrderedMultiDict(MultiDict):
|
||||
|
||||
def _options_header_vkw(value, kw):
|
||||
return dump_options_header(value, dict((k.replace('_', '-'), v)
|
||||
for k, v in kw.items()))
|
||||
for k, v in kw.items()))
|
||||
|
||||
|
||||
def _unicodify_header_value(value):
|
||||
@@ -831,6 +871,7 @@ def _unicodify_header_value(value):
|
||||
|
||||
@native_itermethods(['keys', 'values', 'items'])
|
||||
class Headers(object):
|
||||
|
||||
"""An object that stores some headers. It has a dict-like interface
|
||||
but is ordered and can store the same keys multiple times.
|
||||
|
||||
@@ -892,7 +933,7 @@ class Headers(object):
|
||||
|
||||
def __eq__(self, other):
|
||||
return other.__class__ is self.__class__ and \
|
||||
set(other._list) == set(self._list)
|
||||
set(other._list) == set(self._list)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@@ -1091,7 +1132,7 @@ class Headers(object):
|
||||
raise TypeError('Value should be unicode.')
|
||||
if u'\n' in value or u'\r' in value:
|
||||
raise ValueError('Detected newline in header value. This is '
|
||||
'a potential security problem')
|
||||
'a potential security problem')
|
||||
|
||||
def add_header(self, _key, _value, **_kw):
|
||||
"""Add a new header tuple to the list.
|
||||
@@ -1182,7 +1223,7 @@ class Headers(object):
|
||||
:return: list
|
||||
"""
|
||||
if PY2:
|
||||
return [(k, v.encode('latin1')) for k, v in self]
|
||||
return [(to_native(k), v.encode('latin1')) for k, v in self]
|
||||
return list(self)
|
||||
|
||||
def copy(self):
|
||||
@@ -1207,6 +1248,7 @@ class Headers(object):
|
||||
|
||||
|
||||
class ImmutableHeadersMixin(object):
|
||||
|
||||
"""Makes a :class:`Headers` immutable. We do not mark them as
|
||||
hashable though since the only usecase for this datastructure
|
||||
in Werkzeug is a view on a mutable structure.
|
||||
@@ -1244,6 +1286,7 @@ class ImmutableHeadersMixin(object):
|
||||
|
||||
|
||||
class EnvironHeaders(ImmutableHeadersMixin, Headers):
|
||||
|
||||
"""Read only version of the headers from a WSGI environment. This
|
||||
provides the same interface as `Headers` and is constructed from
|
||||
a WSGI environment.
|
||||
@@ -1289,6 +1332,7 @@ class EnvironHeaders(ImmutableHeadersMixin, Headers):
|
||||
|
||||
@native_itermethods(['keys', 'values', 'items', 'lists', 'listvalues'])
|
||||
class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
|
||||
"""A read only :class:`MultiDict` that you can pass multiple :class:`MultiDict`
|
||||
instances as sequence and it will combine the return values of all wrapped
|
||||
dicts:
|
||||
@@ -1345,11 +1389,20 @@ class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
rv.extend(d.getlist(key, type))
|
||||
return rv
|
||||
|
||||
def keys(self):
|
||||
def _keys_impl(self):
|
||||
"""This function exists so __len__ can be implemented more efficiently,
|
||||
saving one list creation from an iterator.
|
||||
|
||||
Using this for Python 2's ``dict.keys`` behavior would be useless since
|
||||
`dict.keys` in Python 2 returns a list, while we have a set here.
|
||||
"""
|
||||
rv = set()
|
||||
for d in self.dicts:
|
||||
rv.update(d.keys())
|
||||
return iter(rv)
|
||||
rv.update(iterkeys(d))
|
||||
return rv
|
||||
|
||||
def keys(self):
|
||||
return iter(self._keys_impl())
|
||||
|
||||
__iter__ = keys
|
||||
|
||||
@@ -1397,7 +1450,7 @@ class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
return rv
|
||||
|
||||
def __len__(self):
|
||||
return len(self.keys())
|
||||
return len(self._keys_impl())
|
||||
|
||||
def __contains__(self, key):
|
||||
for d in self.dicts:
|
||||
@@ -1412,6 +1465,7 @@ class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
|
||||
|
||||
class FileMultiDict(MultiDict):
|
||||
|
||||
"""A special :class:`MultiDict` that has convenience methods to add
|
||||
files to it. This is used for :class:`EnvironBuilder` and generally
|
||||
useful for unittesting.
|
||||
@@ -1437,13 +1491,14 @@ class FileMultiDict(MultiDict):
|
||||
file = open(file, 'rb')
|
||||
if filename and content_type is None:
|
||||
content_type = mimetypes.guess_type(filename)[0] or \
|
||||
'application/octet-stream'
|
||||
'application/octet-stream'
|
||||
value = FileStorage(file, filename, name, content_type)
|
||||
|
||||
self.add(name, value)
|
||||
|
||||
|
||||
class ImmutableDict(ImmutableDictMixin, dict):
|
||||
|
||||
"""An immutable :class:`dict`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -1467,6 +1522,7 @@ class ImmutableDict(ImmutableDictMixin, dict):
|
||||
|
||||
|
||||
class ImmutableMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
|
||||
"""An immutable :class:`MultiDict`.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
@@ -1484,6 +1540,7 @@ class ImmutableMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
|
||||
|
||||
class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict):
|
||||
|
||||
"""An immutable :class:`OrderedMultiDict`.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
@@ -1505,6 +1562,7 @@ class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict):
|
||||
|
||||
@native_itermethods(['values'])
|
||||
class Accept(ImmutableList):
|
||||
|
||||
"""An :class:`Accept` object is just a list subclass for lists of
|
||||
``(value, quality)`` tuples. It is automatically sorted by quality.
|
||||
|
||||
@@ -1641,7 +1699,8 @@ class Accept(ImmutableList):
|
||||
for client_item, quality in self:
|
||||
if quality <= best_quality:
|
||||
break
|
||||
if self._value_matches(server_item, client_item):
|
||||
if self._value_matches(server_item, client_item) \
|
||||
and quality > 0:
|
||||
best_quality = quality
|
||||
result = server_item
|
||||
return result
|
||||
@@ -1654,6 +1713,7 @@ class Accept(ImmutableList):
|
||||
|
||||
|
||||
class MIMEAccept(Accept):
|
||||
|
||||
"""Like :class:`Accept` but with special methods and behavior for
|
||||
mimetypes.
|
||||
"""
|
||||
@@ -1708,6 +1768,7 @@ class MIMEAccept(Accept):
|
||||
|
||||
|
||||
class LanguageAccept(Accept):
|
||||
|
||||
"""Like :class:`Accept` but with normalization for languages."""
|
||||
|
||||
def _value_matches(self, value, item):
|
||||
@@ -1717,6 +1778,7 @@ class LanguageAccept(Accept):
|
||||
|
||||
|
||||
class CharsetAccept(Accept):
|
||||
|
||||
"""Like :class:`Accept` but with normalization for charsets."""
|
||||
|
||||
def _value_matches(self, value, item):
|
||||
@@ -1738,6 +1800,7 @@ def cache_property(key, empty, type):
|
||||
|
||||
|
||||
class _CacheControl(UpdateDictMixin, dict):
|
||||
|
||||
"""Subclass of a dict that stores values for a Cache-Control header. It
|
||||
has accessors for all the cache-control directives specified in RFC 2616.
|
||||
The class does not differentiate between request and response directives.
|
||||
@@ -1822,13 +1885,16 @@ class _CacheControl(UpdateDictMixin, dict):
|
||||
return self.to_header()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %r>' % (
|
||||
return '<%s %s>' % (
|
||||
self.__class__.__name__,
|
||||
self.to_header()
|
||||
" ".join(
|
||||
"%s=%r" % (k, v) for k, v in sorted(self.items())
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class RequestCacheControl(ImmutableDictMixin, _CacheControl):
|
||||
|
||||
"""A cache control for requests. This is immutable and gives access
|
||||
to all the request-relevant cache control headers.
|
||||
|
||||
@@ -1849,6 +1915,7 @@ class RequestCacheControl(ImmutableDictMixin, _CacheControl):
|
||||
|
||||
|
||||
class ResponseCacheControl(_CacheControl):
|
||||
|
||||
"""A cache control for responses. Unlike :class:`RequestCacheControl`
|
||||
this is mutable and gives access to response-relevant cache control
|
||||
headers.
|
||||
@@ -1876,6 +1943,7 @@ _CacheControl.cache_property = staticmethod(cache_property)
|
||||
|
||||
|
||||
class CallbackDict(UpdateDictMixin, dict):
|
||||
|
||||
"""A dict that calls a function passed every time something is changed.
|
||||
The function is passed the dict instance.
|
||||
"""
|
||||
@@ -1892,6 +1960,7 @@ class CallbackDict(UpdateDictMixin, dict):
|
||||
|
||||
|
||||
class HeaderSet(object):
|
||||
|
||||
"""Similar to the :class:`ETags` class this implements a set-like structure.
|
||||
Unlike :class:`ETags` this is case insensitive and used for vary, allow, and
|
||||
content-language headers.
|
||||
@@ -2045,6 +2114,7 @@ class HeaderSet(object):
|
||||
|
||||
|
||||
class ETags(object):
|
||||
|
||||
"""A set that can be used to check if one etag is present in a collection
|
||||
of etags.
|
||||
"""
|
||||
@@ -2094,7 +2164,7 @@ class ETags(object):
|
||||
return '*'
|
||||
return ', '.join(
|
||||
['"%s"' % x for x in self._strong] +
|
||||
['w/"%s"' % x for x in self._weak]
|
||||
['W/"%s"' % x for x in self._weak]
|
||||
)
|
||||
|
||||
def __call__(self, etag=None, data=None, include_weak=False):
|
||||
@@ -2107,9 +2177,11 @@ class ETags(object):
|
||||
return True
|
||||
return etag in self._strong
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
return bool(self.star_tag or self._strong or self._weak)
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __str__(self):
|
||||
return self.to_header()
|
||||
|
||||
@@ -2124,6 +2196,7 @@ class ETags(object):
|
||||
|
||||
|
||||
class IfRange(object):
|
||||
|
||||
"""Very simple object that represents the `If-Range` header in parsed
|
||||
form. It will either have neither a etag or date or one of either but
|
||||
never both.
|
||||
@@ -2154,6 +2227,7 @@ class IfRange(object):
|
||||
|
||||
|
||||
class Range(object):
|
||||
|
||||
"""Represents a range header. All the methods are only supporting bytes
|
||||
as unit. It does store multiple ranges but :meth:`range_for_length` will
|
||||
only work if only one range is provided.
|
||||
@@ -2209,6 +2283,7 @@ class Range(object):
|
||||
|
||||
|
||||
class ContentRange(object):
|
||||
|
||||
"""Represents the content range header.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
@@ -2223,6 +2298,7 @@ class ContentRange(object):
|
||||
def _callback_property(name):
|
||||
def fget(self):
|
||||
return getattr(self, name)
|
||||
|
||||
def fset(self, value):
|
||||
setattr(self, name, value)
|
||||
if self.on_update is not None:
|
||||
@@ -2285,6 +2361,7 @@ class ContentRange(object):
|
||||
|
||||
|
||||
class Authorization(ImmutableDictMixin, dict):
|
||||
|
||||
"""Represents an `Authorization` header sent by the client. You should
|
||||
not create this kind of object yourself but use it when it's returned by
|
||||
the `parse_authorization_header` function.
|
||||
@@ -2345,10 +2422,11 @@ class Authorization(ImmutableDictMixin, dict):
|
||||
|
||||
|
||||
class WWWAuthenticate(UpdateDictMixin, dict):
|
||||
|
||||
"""Provides simple access to `WWW-Authenticate` headers."""
|
||||
|
||||
#: list of keys that require quoting in the generated header
|
||||
_require_quoting = frozenset(['domain', 'nonce', 'opaque', 'realm'])
|
||||
_require_quoting = frozenset(['domain', 'nonce', 'opaque', 'realm', 'qop'])
|
||||
|
||||
def __init__(self, auth_type=None, values=None, on_update=None):
|
||||
dict.__init__(self, values or ())
|
||||
@@ -2389,7 +2467,7 @@ class WWWAuthenticate(UpdateDictMixin, dict):
|
||||
auth_type = d.pop('__auth_type__', None) or 'basic'
|
||||
return '%s %s' % (auth_type.title(), ', '.join([
|
||||
'%s=%s' % (key, quote_header_value(value,
|
||||
allow_token=key not in self._require_quoting))
|
||||
allow_token=key not in self._require_quoting))
|
||||
for key, value in iteritems(d)
|
||||
]))
|
||||
|
||||
@@ -2412,6 +2490,7 @@ class WWWAuthenticate(UpdateDictMixin, dict):
|
||||
For more information have a look at the sourcecode to see how the
|
||||
regular properties (:attr:`realm` etc.) are implemented.
|
||||
"""
|
||||
|
||||
def _set_value(self, value):
|
||||
if value is None:
|
||||
self.pop(name, None)
|
||||
@@ -2462,6 +2541,7 @@ class WWWAuthenticate(UpdateDictMixin, dict):
|
||||
val = self.get('stale')
|
||||
if val is not None:
|
||||
return val.lower() == 'true'
|
||||
|
||||
def _set_stale(self, value):
|
||||
if value is None:
|
||||
self.pop('stale', None)
|
||||
@@ -2479,6 +2559,7 @@ class WWWAuthenticate(UpdateDictMixin, dict):
|
||||
|
||||
|
||||
class FileStorage(object):
|
||||
|
||||
"""The :class:`FileStorage` class is a thin wrapper over incoming files.
|
||||
It is used by the request object to represent uploaded files. All the
|
||||
attributes of the wrapper stream are proxied by the file storage so
|
||||
@@ -2506,7 +2587,7 @@ class FileStorage(object):
|
||||
# This might not be if the name attribute is bytes due to the
|
||||
# file being opened from the bytes API.
|
||||
if not PY2 and isinstance(filename, bytes):
|
||||
filename = filename.decode(sys.getfilesystemencoding(),
|
||||
filename = filename.decode(get_filesystem_encoding(),
|
||||
'replace')
|
||||
|
||||
self.filename = filename
|
||||
@@ -2535,15 +2616,15 @@ class FileStorage(object):
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
"""Like :attr:`content_type` but without parameters (eg, without
|
||||
charset, type etc.). For example if the content
|
||||
type is ``text/html; charset=utf-8`` the mimetype would be
|
||||
"""Like :attr:`content_type`, but without parameters (eg, without
|
||||
charset, type etc.) and always lowercase. For example if the content
|
||||
type is ``text/HTML; charset=utf-8`` the mimetype would be
|
||||
``'text/html'``.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
self._parse_content_type()
|
||||
return self._parsed_content_type[0]
|
||||
return self._parsed_content_type[0].lower()
|
||||
|
||||
@property
|
||||
def mimetype_params(self):
|
||||
@@ -2590,6 +2671,7 @@ class FileStorage(object):
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.filename)
|
||||
__bool__ = __nonzero__
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.stream, name)
|
||||
@@ -2607,6 +2689,6 @@ class FileStorage(object):
|
||||
|
||||
# circular dependencies
|
||||
from werkzeug.http import dump_options_header, dump_header, generate_etag, \
|
||||
quote_header_value, parse_set_header, unquote_etag, quote_etag, \
|
||||
parse_options_header, http_date, is_byte_range_valid
|
||||
quote_header_value, parse_set_header, unquote_etag, quote_etag, \
|
||||
parse_options_header, http_date, is_byte_range_valid
|
||||
from werkzeug import exceptions
|
||||
|
||||
@@ -5,24 +5,103 @@
|
||||
|
||||
WSGI application traceback debugger.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import json
|
||||
import time
|
||||
import getpass
|
||||
import hashlib
|
||||
import mimetypes
|
||||
from itertools import chain
|
||||
from os.path import join, dirname, basename, isfile
|
||||
from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response
|
||||
from werkzeug.http import parse_cookie
|
||||
from werkzeug.debug.tbtools import get_current_traceback, render_console_html
|
||||
from werkzeug.debug.console import Console
|
||||
from werkzeug.security import gen_salt
|
||||
from werkzeug._internal import _log
|
||||
from werkzeug._compat import text_type
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
#: import this here because it once was documented as being available
|
||||
#: from this module. In case there are users left ...
|
||||
from werkzeug.debug.repr import debug_repr
|
||||
from werkzeug.debug.repr import debug_repr # noqa
|
||||
|
||||
|
||||
# A week
|
||||
PIN_TIME = 60 * 60 * 24 * 7
|
||||
|
||||
|
||||
def hash_pin(pin):
|
||||
if isinstance(pin, text_type):
|
||||
pin = pin.encode('utf-8', 'replace')
|
||||
return hashlib.md5(pin + b'shittysalt').hexdigest()[:12]
|
||||
|
||||
|
||||
_machine_id = None
|
||||
|
||||
|
||||
def get_machine_id():
|
||||
global _machine_id
|
||||
rv = _machine_id
|
||||
if rv is not None:
|
||||
return rv
|
||||
|
||||
def _generate():
|
||||
# Potential sources of secret information on linux. The machine-id
|
||||
# is stable across boots, the boot id is not
|
||||
for filename in '/etc/machine-id', '/proc/sys/kernel/random/boot_id':
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
return f.readline().strip()
|
||||
except IOError:
|
||||
continue
|
||||
|
||||
# On OS X we can use the computer's serial number assuming that
|
||||
# ioreg exists and can spit out that information.
|
||||
try:
|
||||
# Also catch import errors: subprocess may not be available, e.g.
|
||||
# Google App Engine
|
||||
# See https://github.com/pallets/werkzeug/issues/925
|
||||
from subprocess import Popen, PIPE
|
||||
dump = Popen(['ioreg', '-c', 'IOPlatformExpertDevice', '-d', '2'],
|
||||
stdout=PIPE).communicate()[0]
|
||||
match = re.search(b'"serial-number" = <([^>]+)', dump)
|
||||
if match is not None:
|
||||
return match.group(1)
|
||||
except (OSError, ImportError):
|
||||
pass
|
||||
|
||||
# On Windows we can use winreg to get the machine guid
|
||||
wr = None
|
||||
try:
|
||||
import winreg as wr
|
||||
except ImportError:
|
||||
try:
|
||||
import _winreg as wr
|
||||
except ImportError:
|
||||
pass
|
||||
if wr is not None:
|
||||
try:
|
||||
with wr.OpenKey(wr.HKEY_LOCAL_MACHINE,
|
||||
'SOFTWARE\\Microsoft\\Cryptography', 0,
|
||||
wr.KEY_READ | wr.KEY_WOW64_64KEY) as rk:
|
||||
return wr.QueryValueEx(rk, 'MachineGuid')[0]
|
||||
except WindowsError:
|
||||
pass
|
||||
|
||||
_machine_id = rv = _generate()
|
||||
return rv
|
||||
|
||||
|
||||
class _ConsoleFrame(object):
|
||||
|
||||
"""Helper class so that we can reuse the frame console code for the
|
||||
standalone console.
|
||||
"""
|
||||
@@ -32,6 +111,90 @@ class _ConsoleFrame(object):
|
||||
self.id = 0
|
||||
|
||||
|
||||
def get_pin_and_cookie_name(app):
|
||||
"""Given an application object this returns a semi-stable 9 digit pin
|
||||
code and a random key. The hope is that this is stable between
|
||||
restarts to not make debugging particularly frustrating. If the pin
|
||||
was forcefully disabled this returns `None`.
|
||||
|
||||
Second item in the resulting tuple is the cookie name for remembering.
|
||||
"""
|
||||
pin = os.environ.get('WERKZEUG_DEBUG_PIN')
|
||||
rv = None
|
||||
num = None
|
||||
|
||||
# Pin was explicitly disabled
|
||||
if pin == 'off':
|
||||
return None, None
|
||||
|
||||
# Pin was provided explicitly
|
||||
if pin is not None and pin.replace('-', '').isdigit():
|
||||
# If there are separators in the pin, return it directly
|
||||
if '-' in pin:
|
||||
rv = pin
|
||||
else:
|
||||
num = pin
|
||||
|
||||
modname = getattr(app, '__module__',
|
||||
getattr(app.__class__, '__module__'))
|
||||
|
||||
try:
|
||||
# `getpass.getuser()` imports the `pwd` module,
|
||||
# which does not exist in the Google App Engine sandbox.
|
||||
username = getpass.getuser()
|
||||
except ImportError:
|
||||
username = None
|
||||
|
||||
mod = sys.modules.get(modname)
|
||||
|
||||
# This information only exists to make the cookie unique on the
|
||||
# computer, not as a security feature.
|
||||
probably_public_bits = [
|
||||
username,
|
||||
modname,
|
||||
getattr(app, '__name__', getattr(app.__class__, '__name__')),
|
||||
getattr(mod, '__file__', None),
|
||||
]
|
||||
|
||||
# This information is here to make it harder for an attacker to
|
||||
# guess the cookie name. They are unlikely to be contained anywhere
|
||||
# within the unauthenticated debug page.
|
||||
private_bits = [
|
||||
str(uuid.getnode()),
|
||||
get_machine_id(),
|
||||
]
|
||||
|
||||
h = hashlib.md5()
|
||||
for bit in chain(probably_public_bits, private_bits):
|
||||
if not bit:
|
||||
continue
|
||||
if isinstance(bit, text_type):
|
||||
bit = bit.encode('utf-8')
|
||||
h.update(bit)
|
||||
h.update(b'cookiesalt')
|
||||
|
||||
cookie_name = '__wzd' + h.hexdigest()[:20]
|
||||
|
||||
# If we need to generate a pin we salt it a bit more so that we don't
|
||||
# end up with the same value and generate out 9 digits
|
||||
if num is None:
|
||||
h.update(b'pinsalt')
|
||||
num = ('%09d' % int(h.hexdigest(), 16))[:9]
|
||||
|
||||
# Format the pincode in groups of digits for easier remembering if
|
||||
# we don't have a result yet.
|
||||
if rv is None:
|
||||
for group_size in 5, 4, 3:
|
||||
if len(num) % group_size == 0:
|
||||
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
|
||||
for x in range(0, len(num), group_size))
|
||||
break
|
||||
else:
|
||||
rv = num
|
||||
|
||||
return rv, cookie_name
|
||||
|
||||
|
||||
class DebuggedApplication(object):
|
||||
"""Enables debugging support for a given application::
|
||||
|
||||
@@ -58,19 +221,19 @@ class DebuggedApplication(object):
|
||||
:param show_hidden_frames: by default hidden traceback frames are skipped.
|
||||
You can show them by setting this parameter
|
||||
to `True`.
|
||||
:param pin_security: can be used to disable the pin based security system.
|
||||
:param pin_logging: enables the logging of the pin system.
|
||||
"""
|
||||
|
||||
# this class is public
|
||||
__module__ = 'werkzeug'
|
||||
|
||||
def __init__(self, app, evalex=False, request_key='werkzeug.request',
|
||||
console_path='/console', console_init_func=None,
|
||||
show_hidden_frames=False, lodgeit_url=None):
|
||||
show_hidden_frames=False, lodgeit_url=None,
|
||||
pin_security=True, pin_logging=True):
|
||||
if lodgeit_url is not None:
|
||||
from warnings import warn
|
||||
warn(DeprecationWarning('Werkzeug now pastes into gists.'))
|
||||
if not console_init_func:
|
||||
console_init_func = dict
|
||||
console_init_func = None
|
||||
self.app = app
|
||||
self.evalex = evalex
|
||||
self.frames = {}
|
||||
@@ -80,6 +243,39 @@ class DebuggedApplication(object):
|
||||
self.console_init_func = console_init_func
|
||||
self.show_hidden_frames = show_hidden_frames
|
||||
self.secret = gen_salt(20)
|
||||
self._failed_pin_auth = 0
|
||||
|
||||
self.pin_logging = pin_logging
|
||||
if pin_security:
|
||||
# Print out the pin for the debugger on standard out.
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true' and \
|
||||
pin_logging:
|
||||
_log('warning', ' * Debugger is active!')
|
||||
if self.pin is None:
|
||||
_log('warning', ' * Debugger pin disabled. '
|
||||
'DEBUGGER UNSECURED!')
|
||||
else:
|
||||
_log('info', ' * Debugger pin code: %s' % self.pin)
|
||||
else:
|
||||
self.pin = None
|
||||
|
||||
def _get_pin(self):
|
||||
if not hasattr(self, '_pin'):
|
||||
self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
|
||||
return self._pin
|
||||
|
||||
def _set_pin(self, value):
|
||||
self._pin = value
|
||||
|
||||
pin = property(_get_pin, _set_pin)
|
||||
del _get_pin, _set_pin
|
||||
|
||||
@property
|
||||
def pin_cookie_name(self):
|
||||
"""The name of the pin cookie."""
|
||||
if not hasattr(self, '_pin_cookie'):
|
||||
self._pin, self._pin_cookie = get_pin_and_cookie_name(self.app)
|
||||
return self._pin_cookie
|
||||
|
||||
def debug_application(self, environ, start_response):
|
||||
"""Run the application and conserve the traceback frames."""
|
||||
@@ -93,9 +289,9 @@ class DebuggedApplication(object):
|
||||
except Exception:
|
||||
if hasattr(app_iter, 'close'):
|
||||
app_iter.close()
|
||||
traceback = get_current_traceback(skip=1, show_hidden_frames=
|
||||
self.show_hidden_frames,
|
||||
ignore_system_exceptions=True)
|
||||
traceback = get_current_traceback(
|
||||
skip=1, show_hidden_frames=self.show_hidden_frames,
|
||||
ignore_system_exceptions=True)
|
||||
for frame in traceback.frames:
|
||||
self.frames[frame.id] = frame
|
||||
self.tracebacks[traceback.id] = traceback
|
||||
@@ -117,9 +313,11 @@ class DebuggedApplication(object):
|
||||
'response at a point where response headers were already '
|
||||
'sent.\n')
|
||||
else:
|
||||
is_trusted = bool(self.check_pin_trust(environ))
|
||||
yield traceback.render_full(evalex=self.evalex,
|
||||
evalex_trusted=is_trusted,
|
||||
secret=self.secret) \
|
||||
.encode('utf-8', 'replace')
|
||||
.encode('utf-8', 'replace')
|
||||
|
||||
traceback.log(environ['wsgi.errors'])
|
||||
|
||||
@@ -130,8 +328,15 @@ class DebuggedApplication(object):
|
||||
def display_console(self, request):
|
||||
"""Display a standalone shell."""
|
||||
if 0 not in self.frames:
|
||||
self.frames[0] = _ConsoleFrame(self.console_init_func())
|
||||
return Response(render_console_html(secret=self.secret),
|
||||
if self.console_init_func is None:
|
||||
ns = {}
|
||||
else:
|
||||
ns = dict(self.console_init_func())
|
||||
ns.setdefault('app', self.app)
|
||||
self.frames[0] = _ConsoleFrame(ns)
|
||||
is_trusted = bool(self.check_pin_trust(request.environ))
|
||||
return Response(render_console_html(secret=self.secret,
|
||||
evalex_trusted=is_trusted),
|
||||
mimetype='text/html')
|
||||
|
||||
def paste_traceback(self, request, traceback):
|
||||
@@ -139,10 +344,6 @@ class DebuggedApplication(object):
|
||||
rv = traceback.paste()
|
||||
return Response(json.dumps(rv), mimetype='application/json')
|
||||
|
||||
def get_source(self, request, frame):
|
||||
"""Render the source viewer."""
|
||||
return Response(frame.render_source(), mimetype='text/html')
|
||||
|
||||
def get_resource(self, request, filename):
|
||||
"""Return a static resource from the shared folder."""
|
||||
filename = join(dirname(__file__), 'shared', basename(filename))
|
||||
@@ -156,6 +357,83 @@ class DebuggedApplication(object):
|
||||
f.close()
|
||||
return Response('Not Found', status=404)
|
||||
|
||||
def check_pin_trust(self, environ):
|
||||
"""Checks if the request passed the pin test. This returns `True` if the
|
||||
request is trusted on a pin/cookie basis and returns `False` if not.
|
||||
Additionally if the cookie's stored pin hash is wrong it will return
|
||||
`None` so that appropriate action can be taken.
|
||||
"""
|
||||
if self.pin is None:
|
||||
return True
|
||||
val = parse_cookie(environ).get(self.pin_cookie_name)
|
||||
if not val or '|' not in val:
|
||||
return False
|
||||
ts, pin_hash = val.split('|', 1)
|
||||
if not ts.isdigit():
|
||||
return False
|
||||
if pin_hash != hash_pin(self.pin):
|
||||
return None
|
||||
return (time.time() - PIN_TIME) < int(ts)
|
||||
|
||||
def _fail_pin_auth(self):
|
||||
time.sleep(self._failed_pin_auth > 5 and 5.0 or 0.5)
|
||||
self._failed_pin_auth += 1
|
||||
|
||||
def pin_auth(self, request):
|
||||
"""Authenticates with the pin."""
|
||||
exhausted = False
|
||||
auth = False
|
||||
trust = self.check_pin_trust(request.environ)
|
||||
|
||||
# If the trust return value is `None` it means that the cookie is
|
||||
# set but the stored pin hash value is bad. This means that the
|
||||
# pin was changed. In this case we count a bad auth and unset the
|
||||
# cookie. This way it becomes harder to guess the cookie name
|
||||
# instead of the pin as we still count up failures.
|
||||
bad_cookie = False
|
||||
if trust is None:
|
||||
self._fail_pin_auth()
|
||||
bad_cookie = True
|
||||
|
||||
# If we're trusted, we're authenticated.
|
||||
elif trust:
|
||||
auth = True
|
||||
|
||||
# If we failed too many times, then we're locked out.
|
||||
elif self._failed_pin_auth > 10:
|
||||
exhausted = True
|
||||
|
||||
# Otherwise go through pin based authentication
|
||||
else:
|
||||
entered_pin = request.args.get('pin')
|
||||
if entered_pin.strip().replace('-', '') == \
|
||||
self.pin.replace('-', ''):
|
||||
self._failed_pin_auth = 0
|
||||
auth = True
|
||||
else:
|
||||
self._fail_pin_auth()
|
||||
|
||||
rv = Response(json.dumps({
|
||||
'auth': auth,
|
||||
'exhausted': exhausted,
|
||||
}), mimetype='application/json')
|
||||
if auth:
|
||||
rv.set_cookie(self.pin_cookie_name, '%s|%s' % (
|
||||
int(time.time()),
|
||||
hash_pin(self.pin)
|
||||
), httponly=True)
|
||||
elif bad_cookie:
|
||||
rv.delete_cookie(self.pin_cookie_name)
|
||||
return rv
|
||||
|
||||
def log_pin_request(self):
|
||||
"""Log the pin if needed."""
|
||||
if self.pin_logging and self.pin is not None:
|
||||
_log('info', ' * To enable the debugger you need to '
|
||||
'enter the security pin:')
|
||||
_log('info', ' * Debugger pin code: %s' % self.pin)
|
||||
return Response('')
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Dispatch the requests."""
|
||||
# important: don't ever access a function here that reads the incoming
|
||||
@@ -172,14 +450,17 @@ class DebuggedApplication(object):
|
||||
if cmd == 'resource' and arg:
|
||||
response = self.get_resource(request, arg)
|
||||
elif cmd == 'paste' and traceback is not None and \
|
||||
secret == self.secret:
|
||||
secret == self.secret:
|
||||
response = self.paste_traceback(request, traceback)
|
||||
elif cmd == 'source' and frame and self.secret == secret:
|
||||
response = self.get_source(request, frame)
|
||||
elif self.evalex and cmd is not None and frame is not None and \
|
||||
self.secret == secret:
|
||||
elif cmd == 'pinauth' and secret == self.secret:
|
||||
response = self.pin_auth(request)
|
||||
elif cmd == 'printpin' and secret == self.secret:
|
||||
response = self.log_pin_request()
|
||||
elif self.evalex and cmd is not None and frame is not None \
|
||||
and self.secret == secret and \
|
||||
self.check_pin_trust(environ):
|
||||
response = self.execute_command(request, cmd, frame)
|
||||
elif self.evalex and self.console_path is not None and \
|
||||
request.path == self.console_path:
|
||||
request.path == self.console_path:
|
||||
response = self.display_console(request)
|
||||
return response(environ, start_response)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
Interactive console support.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
import sys
|
||||
@@ -21,6 +21,7 @@ _local = Local()
|
||||
|
||||
|
||||
class HTMLStringO(object):
|
||||
|
||||
"""A StringO version that HTML escapes on write."""
|
||||
|
||||
def __init__(self):
|
||||
@@ -63,6 +64,7 @@ class HTMLStringO(object):
|
||||
|
||||
|
||||
class ThreadedStream(object):
|
||||
|
||||
"""Thread-local wrapper for sys.stdout for the interactive console."""
|
||||
|
||||
def push():
|
||||
@@ -136,6 +138,7 @@ class _ConsoleLoader(object):
|
||||
|
||||
def _wrap_compiler(console):
|
||||
compile = console.compile
|
||||
|
||||
def func(source, filename, symbol):
|
||||
code = compile(source, filename, symbol)
|
||||
console.loader.register(code, source)
|
||||
@@ -162,7 +165,7 @@ class _InteractiveConsole(code.InteractiveInterpreter):
|
||||
try:
|
||||
source_to_eval = ''.join(self.buffer + [source])
|
||||
if code.InteractiveInterpreter.runsource(self,
|
||||
source_to_eval, '<debugger>', 'single'):
|
||||
source_to_eval, '<debugger>', 'single'):
|
||||
self.more = True
|
||||
self.buffer.append(source)
|
||||
else:
|
||||
@@ -193,6 +196,7 @@ class _InteractiveConsole(code.InteractiveInterpreter):
|
||||
|
||||
|
||||
class Console(object):
|
||||
|
||||
"""An interactive console."""
|
||||
|
||||
def __init__(self, globals=None, locals=None):
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Together with the CSS and JavaScript files of the debugger this gives
|
||||
a colorful and more compact output.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
import sys
|
||||
@@ -19,7 +19,7 @@ import codecs
|
||||
from traceback import format_exception_only
|
||||
try:
|
||||
from collections import deque
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError: # pragma: no cover
|
||||
deque = None
|
||||
from werkzeug.utils import escape
|
||||
from werkzeug._compat import iteritems, PY2, text_type, integer_types, \
|
||||
@@ -64,6 +64,7 @@ def dump(obj=missing):
|
||||
|
||||
|
||||
class _Helper(object):
|
||||
|
||||
"""Displays an HTML version of the normal help, for the interactive
|
||||
debugger only because it requires a patched sys.stdout.
|
||||
"""
|
||||
@@ -84,7 +85,7 @@ class _Helper(object):
|
||||
if len(paragraphs) > 1:
|
||||
title = paragraphs[0]
|
||||
text = '\n\n'.join(paragraphs[1:])
|
||||
else: # pragma: no cover
|
||||
else: # pragma: no cover
|
||||
title = 'Help'
|
||||
text = paragraphs[0]
|
||||
sys.stdout._write(HELP_HTML % {'title': title, 'text': text})
|
||||
@@ -218,7 +219,7 @@ class DebugReprGenerator(object):
|
||||
def fallback_repr(self):
|
||||
try:
|
||||
info = ''.join(format_exception_only(*sys.exc_info()[:2]))
|
||||
except Exception: # pragma: no cover
|
||||
except Exception: # pragma: no cover
|
||||
info = '?'
|
||||
if PY2:
|
||||
info = info.decode('utf-8', 'ignore')
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
$(function() {
|
||||
var sourceView = null;
|
||||
if (!EVALEX_TRUSTED) {
|
||||
initPinBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* if we are in console mode, show the console.
|
||||
@@ -10,17 +12,18 @@ $(function() {
|
||||
|
||||
$('div.traceback div.frame').each(function() {
|
||||
var
|
||||
target = $('pre', this)
|
||||
.click(function() {
|
||||
sourceButton.click();
|
||||
}),
|
||||
consoleNode = null, source = null,
|
||||
target = $('pre', this),
|
||||
consoleNode = null,
|
||||
frameID = this.id.substring(6);
|
||||
|
||||
target.click(function() {
|
||||
$(this).parent().toggleClass('expanded');
|
||||
});
|
||||
|
||||
/**
|
||||
* Add an interactive console to the frames
|
||||
*/
|
||||
if (EVALEX)
|
||||
if (EVALEX && target.is('.current')) {
|
||||
$('<img src="?__debugger__=yes&cmd=resource&f=console.png">')
|
||||
.attr('title', 'Open an interactive python shell in this frame')
|
||||
.click(function() {
|
||||
@@ -28,36 +31,7 @@ $(function() {
|
||||
return false;
|
||||
})
|
||||
.prependTo(target);
|
||||
|
||||
/**
|
||||
* Show sourcecode
|
||||
*/
|
||||
var sourceButton = $('<img src="?__debugger__=yes&cmd=resource&f=source.png">')
|
||||
.attr('title', 'Display the sourcecode for this frame')
|
||||
.click(function() {
|
||||
if (!sourceView)
|
||||
$('h2', sourceView =
|
||||
$('<div class="box"><h2>View Source</h2><div class="sourceview">' +
|
||||
'<table></table></div>')
|
||||
.insertBefore('div.explanation'))
|
||||
.css('cursor', 'pointer')
|
||||
.click(function() {
|
||||
sourceView.slideUp('fast');
|
||||
});
|
||||
$.get('', {__debugger__: 'yes', cmd:
|
||||
'source', frm: frameID, s: SECRET}, function(data) {
|
||||
$('table', sourceView)
|
||||
.replaceWith(data);
|
||||
if (!sourceView.is(':visible'))
|
||||
sourceView.slideDown('fast', function() {
|
||||
focusSourceBlock();
|
||||
});
|
||||
else
|
||||
focusSourceBlock();
|
||||
});
|
||||
return false;
|
||||
})
|
||||
.prependTo(target);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -118,11 +92,57 @@ $(function() {
|
||||
plainTraceback.replaceWith($('<pre>').text(plainTraceback.text()));
|
||||
});
|
||||
|
||||
function initPinBox() {
|
||||
$('.pin-prompt form').submit(function(evt) {
|
||||
evt.preventDefault();
|
||||
var pin = this.pin.value;
|
||||
var btn = this.btn;
|
||||
btn.disabled = true;
|
||||
$.ajax({
|
||||
dataType: 'json',
|
||||
url: document.location.pathname,
|
||||
data: {__debugger__: 'yes', cmd: 'pinauth', pin: pin,
|
||||
s: SECRET},
|
||||
success: function(data) {
|
||||
btn.disabled = false;
|
||||
if (data.auth) {
|
||||
EVALEX_TRUSTED = true;
|
||||
$('.pin-prompt').fadeOut();
|
||||
} else {
|
||||
if (data.exhausted) {
|
||||
alert('Error: too many attempts. Restart server to retry.');
|
||||
} else {
|
||||
alert('Error: incorrect pin');
|
||||
}
|
||||
}
|
||||
console.log(data);
|
||||
},
|
||||
error: function() {
|
||||
btn.disabled = false;
|
||||
alert('Error: Could not verify PIN. Network error?');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function promptForPin() {
|
||||
if (!EVALEX_TRUSTED) {
|
||||
$.ajax({
|
||||
url: document.location.pathname,
|
||||
data: {__debugger__: 'yes', cmd: 'printpin', s: SECRET}
|
||||
});
|
||||
$('.pin-prompt').fadeIn(function() {
|
||||
$('.pin-prompt input[name="pin"]').focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper function for shell initialization
|
||||
*/
|
||||
function openShell(consoleNode, target, frameID) {
|
||||
promptForPin();
|
||||
if (consoleNode)
|
||||
return consoleNode.slideToggle('fast');
|
||||
consoleNode = $('<pre class="console">')
|
||||
@@ -183,19 +203,3 @@ function openShell(consoleNode, target, frameID) {
|
||||
command.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the current block in the source view.
|
||||
*/
|
||||
function focusSourceBlock() {
|
||||
var tmp, line = $('table.source tr.current');
|
||||
for (var i = 0; i < 7; i++) {
|
||||
tmp = line.prev();
|
||||
if (!(tmp && tmp.is('.in-frame')))
|
||||
break
|
||||
line = tmp;
|
||||
}
|
||||
var container = $('div.sourceview');
|
||||
|
||||
container.scrollTop(line.offset().top - container.offset().top + container.scrollTop());
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ h1, h2, h3 { font-family: 'Ubuntu', 'Lucida Grande', 'Lucida Sans Unicode',
|
||||
|
||||
input { background-color: #fff; margin: 0; text-align: left;
|
||||
outline: none !important; }
|
||||
input[type="submit"] { padding: 3px 6px; }
|
||||
a { color: #11557C; }
|
||||
a:hover { color: #177199; }
|
||||
pre, code, table.source,
|
||||
pre, code,
|
||||
textarea { font-family: 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono',
|
||||
monospace; font-size: 14px; }
|
||||
|
||||
@@ -43,15 +44,20 @@ div.traceback ul { list-style: none; margin: 0; padding: 0 0 0 1em; }
|
||||
div.traceback h4 { font-size: 13px; font-weight: normal; margin: 0.7em 0 0.1em 0; }
|
||||
div.traceback pre { margin: 0; padding: 5px 0 3px 15px;
|
||||
background-color: #E8EFF0; border: 1px solid #D3E7E9; }
|
||||
div.traceback pre,
|
||||
div.box table.source { white-space: pre-wrap; /* css-3 should we be so lucky... */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 ?? */
|
||||
white-space: -o-pre-wrap; /* Opera 7 ?? */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
_white-space: pre; /* IE only hack to re-specify in
|
||||
addition to word-wrap */ }
|
||||
div.traceback pre:hover { background-color: #DDECEE; color: black; cursor: pointer; }
|
||||
div.traceback div.source.expanded pre + pre { border-top: none; }
|
||||
|
||||
div.traceback span.ws { display: none; }
|
||||
div.traceback pre.before, div.traceback pre.after { display: none; background: white; }
|
||||
div.traceback div.source.expanded pre.before,
|
||||
div.traceback div.source.expanded pre.after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.traceback div.source.expanded span.ws {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.traceback blockquote { margin: 1em 0 0 0; padding: 0; }
|
||||
div.traceback img { float: right; padding: 2px; margin: -3px 2px 0 0; display: none; }
|
||||
div.traceback img:hover { background-color: #ddd; cursor: pointer;
|
||||
@@ -105,9 +111,32 @@ pre.console div.box pre.help:hover { cursor: default; }
|
||||
pre.console table tr { vertical-align: top; }
|
||||
div.console { border: 1px solid #ccc; padding: 4px; background-color: #fafafa; }
|
||||
|
||||
div.box table.source { border-collapse: collapse; width: 100%; background: #E8EFF0 }
|
||||
div.box table.source td { border-top: 1px solid #E8EFF0; padding: 4px 0 4px 10px; }
|
||||
div.box table.source td.lineno { color: #999; padding-right: 10px; width: 1px; }
|
||||
div.box table.source tr.in-frame { background-color: white; }
|
||||
div.box table.source tr.current { background-color: #EEF7F8; color: #23707E; }
|
||||
div.sourceview { max-height: 400px; overflow: auto; border: 1px solid #ccc; }
|
||||
div.traceback pre, div.console pre {
|
||||
white-space: pre-wrap; /* css-3 should we be so lucky... */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 ?? */
|
||||
white-space: -o-pre-wrap; /* Opera 7 ?? */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
_white-space: pre; /* IE only hack to re-specify in
|
||||
addition to word-wrap */
|
||||
}
|
||||
|
||||
|
||||
div.pin-prompt {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
div.pin-prompt .inner {
|
||||
background: #eee;
|
||||
padding: 10px 50px;
|
||||
width: 350px;
|
||||
margin: 10% auto 0 auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module provides various traceback related utility functions.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
import re
|
||||
@@ -20,13 +20,15 @@ from tokenize import TokenError
|
||||
|
||||
from werkzeug.utils import cached_property, escape
|
||||
from werkzeug.debug.console import Console
|
||||
from werkzeug._compat import range_type, PY2, text_type, string_types
|
||||
from werkzeug._compat import range_type, PY2, text_type, string_types, \
|
||||
to_native, to_unicode
|
||||
from werkzeug.filesystem import get_filesystem_encoding
|
||||
|
||||
|
||||
_coding_re = re.compile(r'coding[:=]\s*([-\w.]+)')
|
||||
_line_re = re.compile(r'^(.*?)$(?m)')
|
||||
_coding_re = re.compile(br'coding[:=]\s*([-\w.]+)')
|
||||
_line_re = re.compile(br'^(.*?)$(?m)')
|
||||
_funcdef_re = re.compile(r'^(\s*def\s)|(.*(?<!\w)lambda(:|\s))|^(\s*@)')
|
||||
UTF8_COOKIE = '\xef\xbb\xbf'
|
||||
UTF8_COOKIE = b'\xef\xbb\xbf'
|
||||
|
||||
system_exceptions = (SystemExit, KeyboardInterrupt)
|
||||
try:
|
||||
@@ -41,17 +43,20 @@ HEADER = u'''\
|
||||
<html>
|
||||
<head>
|
||||
<title>%(title)s // Werkzeug Debugger</title>
|
||||
<link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css" type="text/css">
|
||||
<!-- We need to make sure this has a favicon so that the debugger does not by
|
||||
accident trigger a request to /favicon.ico which might change the application
|
||||
state. -->
|
||||
<link rel="shortcut icon" href="?__debugger__=yes&cmd=resource&f=console.png">
|
||||
<script type="text/javascript" src="?__debugger__=yes&cmd=resource&f=jquery.js"></script>
|
||||
<script type="text/javascript" src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>
|
||||
<link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css"
|
||||
type="text/css">
|
||||
<!-- We need to make sure this has a favicon so that the debugger does
|
||||
not by accident trigger a request to /favicon.ico which might
|
||||
change the application state. -->
|
||||
<link rel="shortcut icon"
|
||||
href="?__debugger__=yes&cmd=resource&f=console.png">
|
||||
<script src="?__debugger__=yes&cmd=resource&f=jquery.js"></script>
|
||||
<script src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>
|
||||
<script type="text/javascript">
|
||||
var TRACEBACK = %(traceback_id)d,
|
||||
CONSOLE_MODE = %(console)s,
|
||||
EVALEX = %(evalex)s,
|
||||
EVALEX_TRUSTED = %(evalex_trusted)s,
|
||||
SECRET = "%(secret)s";
|
||||
</script>
|
||||
</head>
|
||||
@@ -64,6 +69,21 @@ FOOTER = u'''\
|
||||
friendly Werkzeug powered traceback interpreter.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pin-prompt">
|
||||
<div class="inner">
|
||||
<h3>Console Locked</h3>
|
||||
<p>
|
||||
The console is locked and needs to be unlocked by entering the PIN.
|
||||
You can find the PIN printed out on the standard output of your
|
||||
shell that runs the server.
|
||||
<form>
|
||||
<p>PIN:
|
||||
<input type=text name=pin size=14>
|
||||
<input type=submit name=btn value="Confirm Pin">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
@@ -124,12 +144,10 @@ FRAME_HTML = u'''\
|
||||
<h4>File <cite class="filename">"%(filename)s"</cite>,
|
||||
line <em class="line">%(lineno)s</em>,
|
||||
in <code class="function">%(function_name)s</code></h4>
|
||||
<pre>%(current_line)s</pre>
|
||||
<div class="source">%(lines)s</div>
|
||||
</div>
|
||||
'''
|
||||
|
||||
SOURCE_TABLE_HTML = u'<table class=source>%s</table>'
|
||||
|
||||
SOURCE_LINE_HTML = u'''\
|
||||
<tr class="%(classes)s">
|
||||
<td class=lineno>%(lineno)s</td>
|
||||
@@ -138,13 +156,14 @@ SOURCE_LINE_HTML = u'''\
|
||||
'''
|
||||
|
||||
|
||||
def render_console_html(secret):
|
||||
def render_console_html(secret, evalex_trusted=True):
|
||||
return CONSOLE_HTML % {
|
||||
'evalex': 'true',
|
||||
'evalex_trusted': evalex_trusted and 'true' or 'false',
|
||||
'console': 'true',
|
||||
'title': 'Console',
|
||||
'secret': secret,
|
||||
'traceback_id': -1
|
||||
'traceback_id': -1
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +288,7 @@ class Traceback(object):
|
||||
logfile = sys.stderr
|
||||
tb = self.plaintext.rstrip() + u'\n'
|
||||
if PY2:
|
||||
tb.encode('utf-8', 'replace')
|
||||
tb = tb.encode('utf-8', 'replace')
|
||||
logfile.write(tb)
|
||||
|
||||
def paste(self):
|
||||
@@ -327,17 +346,19 @@ class Traceback(object):
|
||||
'description': description_wrapper % escape(self.exception)
|
||||
}
|
||||
|
||||
def render_full(self, evalex=False, secret=None):
|
||||
def render_full(self, evalex=False, secret=None,
|
||||
evalex_trusted=True):
|
||||
"""Render the Full HTML page with the traceback info."""
|
||||
exc = escape(self.exception)
|
||||
return PAGE_HTML % {
|
||||
'evalex': evalex and 'true' or 'false',
|
||||
'evalex_trusted': evalex_trusted and 'true' or 'false',
|
||||
'console': 'false',
|
||||
'title': exc,
|
||||
'exception': exc,
|
||||
'exception_type': escape(self.exception_type),
|
||||
'summary': self.render_summary(include_title=False),
|
||||
'plaintext': self.plaintext,
|
||||
'plaintext': escape(self.plaintext),
|
||||
'plaintext_cs': re.sub('-{2,}', '-', self.plaintext),
|
||||
'traceback_id': self.id,
|
||||
'secret': secret
|
||||
@@ -363,6 +384,7 @@ class Traceback(object):
|
||||
|
||||
|
||||
class Frame(object):
|
||||
|
||||
"""A single frame in a traceback."""
|
||||
|
||||
def __init__(self, exc_type, exc_value, tb):
|
||||
@@ -377,7 +399,7 @@ class Frame(object):
|
||||
# if it's a file on the file system resolve the real filename.
|
||||
if os.path.isfile(fn):
|
||||
fn = os.path.realpath(fn)
|
||||
self.filename = fn
|
||||
self.filename = to_unicode(fn, get_filesystem_encoding())
|
||||
self.module = self.globals.get('__name__')
|
||||
self.loader = self.globals.get('__loader__')
|
||||
self.code = tb.tb_frame.f_code
|
||||
@@ -399,9 +421,29 @@ class Frame(object):
|
||||
'filename': escape(self.filename),
|
||||
'lineno': self.lineno,
|
||||
'function_name': escape(self.function_name),
|
||||
'current_line': escape(self.current_line.strip())
|
||||
'lines': self.render_line_context(),
|
||||
}
|
||||
|
||||
def render_line_context(self):
|
||||
before, current, after = self.get_context_lines()
|
||||
rv = []
|
||||
|
||||
def render_line(line, cls):
|
||||
line = line.expandtabs().rstrip()
|
||||
stripped_line = line.strip()
|
||||
prefix = len(line) - len(stripped_line)
|
||||
rv.append(
|
||||
'<pre class="line %s"><span class="ws">%s</span>%s</pre>' % (
|
||||
cls, ' ' * prefix, escape(stripped_line) or ' '))
|
||||
|
||||
for line in before:
|
||||
render_line(line, 'before')
|
||||
render_line(current, 'current')
|
||||
for line in after:
|
||||
render_line(line, 'after')
|
||||
|
||||
return '\n'.join(rv)
|
||||
|
||||
def get_annotated_lines(self):
|
||||
"""Helper function that returns lines with extra information."""
|
||||
lines = [Line(idx + 1, x) for idx, x in enumerate(self.sourcelines)]
|
||||
@@ -429,15 +471,10 @@ class Frame(object):
|
||||
|
||||
return lines
|
||||
|
||||
def render_source(self):
|
||||
"""Render the sourcecode."""
|
||||
return SOURCE_TABLE_HTML % u'\n'.join(line.render() for line in
|
||||
self.get_annotated_lines())
|
||||
|
||||
def eval(self, code, mode='single'):
|
||||
"""Evaluate code in the context of the frame."""
|
||||
if isinstance(code, string_types):
|
||||
if PY2 and isinstance(code, unicode):
|
||||
if PY2 and isinstance(code, unicode): # noqa
|
||||
code = UTF8_COOKIE + code.encode('utf-8')
|
||||
code = compile(code, '<interactive>', mode)
|
||||
return eval(code, self.globals, self.locals)
|
||||
@@ -460,7 +497,8 @@ class Frame(object):
|
||||
|
||||
if source is None:
|
||||
try:
|
||||
f = open(self.filename)
|
||||
f = open(to_native(self.filename, get_filesystem_encoding()),
|
||||
mode='rb')
|
||||
except IOError:
|
||||
return []
|
||||
try:
|
||||
@@ -479,7 +517,7 @@ class Frame(object):
|
||||
source = source[3:]
|
||||
else:
|
||||
for idx, match in enumerate(_line_re.finditer(source)):
|
||||
match = _line_re.search(match.group())
|
||||
match = _coding_re.search(match.group())
|
||||
if match is not None:
|
||||
charset = match.group(1)
|
||||
break
|
||||
@@ -487,6 +525,7 @@ class Frame(object):
|
||||
break
|
||||
|
||||
# on broken cookies we fall back to utf-8 too
|
||||
charset = to_native(charset)
|
||||
try:
|
||||
codecs.lookup(charset)
|
||||
except LookupError:
|
||||
@@ -494,6 +533,15 @@ class Frame(object):
|
||||
|
||||
return source.decode(charset, 'replace').splitlines()
|
||||
|
||||
def get_context_lines(self, context=5):
|
||||
before = self.sourcelines[self.lineno - context - 1:self.lineno - 1]
|
||||
past = self.sourcelines[self.lineno:self.lineno + context]
|
||||
return (
|
||||
before,
|
||||
self.current_line,
|
||||
past,
|
||||
)
|
||||
|
||||
@property
|
||||
def current_line(self):
|
||||
try:
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
return e
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
@@ -66,13 +66,14 @@ werkzeug.exceptions = sys.modules[__name__]
|
||||
|
||||
from werkzeug._internal import _get_environ
|
||||
from werkzeug._compat import iteritems, integer_types, text_type, \
|
||||
implements_to_string
|
||||
implements_to_string
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
|
||||
@implements_to_string
|
||||
class HTTPException(Exception):
|
||||
|
||||
"""
|
||||
Baseclass for all HTTP exceptions. This exception can be called as WSGI
|
||||
application to render a default error page or you can catch the subclasses
|
||||
@@ -94,6 +95,7 @@ class HTTPException(Exception):
|
||||
also is a subclass of `BadRequest`.
|
||||
"""
|
||||
class newcls(cls, exception):
|
||||
|
||||
def __init__(self, arg=None, *args, **kwargs):
|
||||
cls.__init__(self, *args, **kwargs)
|
||||
exception.__init__(self, arg)
|
||||
@@ -161,6 +163,7 @@ class HTTPException(Exception):
|
||||
|
||||
|
||||
class BadRequest(HTTPException):
|
||||
|
||||
"""*400* `Bad Request`
|
||||
|
||||
Raise if the browser sends something to the application the application
|
||||
@@ -174,6 +177,7 @@ class BadRequest(HTTPException):
|
||||
|
||||
|
||||
class ClientDisconnected(BadRequest):
|
||||
|
||||
"""Internal exception that is raised if Werkzeug detects a disconnected
|
||||
client. Since the client is already gone at that point attempting to
|
||||
send the error message to the client might not work and might ultimately
|
||||
@@ -181,7 +185,7 @@ class ClientDisconnected(BadRequest):
|
||||
it is silenced by default as far as Werkzeug is concerned.
|
||||
|
||||
Since disconnections cannot be reliably detected and are unspecified
|
||||
by WSGI to a large extend this might or might not be raised if a client
|
||||
by WSGI to a large extent this might or might not be raised if a client
|
||||
is gone.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
@@ -189,6 +193,7 @@ class ClientDisconnected(BadRequest):
|
||||
|
||||
|
||||
class SecurityError(BadRequest):
|
||||
|
||||
"""Raised if something triggers a security error. This is otherwise
|
||||
exactly like a bad request error.
|
||||
|
||||
@@ -196,7 +201,16 @@ class SecurityError(BadRequest):
|
||||
"""
|
||||
|
||||
|
||||
class BadHost(BadRequest):
|
||||
|
||||
"""Raised if the submitted host is badly formatted.
|
||||
|
||||
.. versionadded:: 0.11.2
|
||||
"""
|
||||
|
||||
|
||||
class Unauthorized(HTTPException):
|
||||
|
||||
"""*401* `Unauthorized`
|
||||
|
||||
Raise if the user is not authorized. Also used if you want to use HTTP
|
||||
@@ -212,6 +226,7 @@ class Unauthorized(HTTPException):
|
||||
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
|
||||
"""*403* `Forbidden`
|
||||
|
||||
Raise if the user doesn't have the permission for the requested resource
|
||||
@@ -225,6 +240,7 @@ class Forbidden(HTTPException):
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
|
||||
"""*404* `Not Found`
|
||||
|
||||
Raise if a resource does not exist and never existed.
|
||||
@@ -238,6 +254,7 @@ class NotFound(HTTPException):
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPException):
|
||||
|
||||
"""*405* `Method Not Allowed`
|
||||
|
||||
Raise if the server used a method the resource does not handle. For
|
||||
@@ -264,6 +281,7 @@ class MethodNotAllowed(HTTPException):
|
||||
|
||||
|
||||
class NotAcceptable(HTTPException):
|
||||
|
||||
"""*406* `Not Acceptable`
|
||||
|
||||
Raise if the server can't return any content conforming to the
|
||||
@@ -280,6 +298,7 @@ class NotAcceptable(HTTPException):
|
||||
|
||||
|
||||
class RequestTimeout(HTTPException):
|
||||
|
||||
"""*408* `Request Timeout`
|
||||
|
||||
Raise to signalize a timeout.
|
||||
@@ -292,6 +311,7 @@ class RequestTimeout(HTTPException):
|
||||
|
||||
|
||||
class Conflict(HTTPException):
|
||||
|
||||
"""*409* `Conflict`
|
||||
|
||||
Raise to signal that a request cannot be completed because it conflicts
|
||||
@@ -307,19 +327,21 @@ class Conflict(HTTPException):
|
||||
|
||||
|
||||
class Gone(HTTPException):
|
||||
|
||||
"""*410* `Gone`
|
||||
|
||||
Raise if a resource existed previously and went away without new location.
|
||||
"""
|
||||
code = 410
|
||||
description = (
|
||||
'The requested URL is no longer available on this server and '
|
||||
'there is no forwarding address.</p><p>If you followed a link '
|
||||
'from a foreign page, please contact the author of this page.'
|
||||
'The requested URL is no longer available on this server and there '
|
||||
'is no forwarding address. If you followed a link from a foreign '
|
||||
'page, please contact the author of this page.'
|
||||
)
|
||||
|
||||
|
||||
class LengthRequired(HTTPException):
|
||||
|
||||
"""*411* `Length Required`
|
||||
|
||||
Raise if the browser submitted data but no ``Content-Length`` header which
|
||||
@@ -333,6 +355,7 @@ class LengthRequired(HTTPException):
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPException):
|
||||
|
||||
"""*412* `Precondition Failed`
|
||||
|
||||
Status code used in combination with ``If-Match``, ``If-None-Match``, or
|
||||
@@ -346,6 +369,7 @@ class PreconditionFailed(HTTPException):
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPException):
|
||||
|
||||
"""*413* `Request Entity Too Large`
|
||||
|
||||
The status code one should return if the data submitted exceeded a given
|
||||
@@ -358,6 +382,7 @@ class RequestEntityTooLarge(HTTPException):
|
||||
|
||||
|
||||
class RequestURITooLarge(HTTPException):
|
||||
|
||||
"""*414* `Request URI Too Large`
|
||||
|
||||
Like *413* but for too long URLs.
|
||||
@@ -370,6 +395,7 @@ class RequestURITooLarge(HTTPException):
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPException):
|
||||
|
||||
"""*415* `Unsupported Media Type`
|
||||
|
||||
The status code returned if the server is unable to handle the media type
|
||||
@@ -383,6 +409,7 @@ class UnsupportedMediaType(HTTPException):
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPException):
|
||||
|
||||
"""*416* `Requested Range Not Satisfiable`
|
||||
|
||||
The client asked for a part of the file that lies beyond the end
|
||||
@@ -397,6 +424,7 @@ class RequestedRangeNotSatisfiable(HTTPException):
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPException):
|
||||
|
||||
"""*417* `Expectation Failed`
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header.
|
||||
@@ -410,6 +438,7 @@ class ExpectationFailed(HTTPException):
|
||||
|
||||
|
||||
class ImATeapot(HTTPException):
|
||||
|
||||
"""*418* `I'm a teapot`
|
||||
|
||||
The server should return this if it is a teapot and someone attempted
|
||||
@@ -424,6 +453,7 @@ class ImATeapot(HTTPException):
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPException):
|
||||
|
||||
"""*422* `Unprocessable Entity`
|
||||
|
||||
Used if the request is well formed, but the instructions are otherwise
|
||||
@@ -437,6 +467,7 @@ class UnprocessableEntity(HTTPException):
|
||||
|
||||
|
||||
class PreconditionRequired(HTTPException):
|
||||
|
||||
"""*428* `Precondition Required`
|
||||
|
||||
The server requires this request to be conditional, typically to prevent
|
||||
@@ -455,6 +486,7 @@ class PreconditionRequired(HTTPException):
|
||||
|
||||
|
||||
class TooManyRequests(HTTPException):
|
||||
|
||||
"""*429* `Too Many Requests`
|
||||
|
||||
The server is limiting the rate at which this user receives responses, and
|
||||
@@ -470,6 +502,7 @@ class TooManyRequests(HTTPException):
|
||||
|
||||
|
||||
class RequestHeaderFieldsTooLarge(HTTPException):
|
||||
|
||||
"""*431* `Request Header Fields Too Large`
|
||||
|
||||
The server refuses to process the request because the header fields are too
|
||||
@@ -483,6 +516,7 @@ class RequestHeaderFieldsTooLarge(HTTPException):
|
||||
|
||||
|
||||
class InternalServerError(HTTPException):
|
||||
|
||||
"""*500* `Internal Server Error`
|
||||
|
||||
Raise if an internal server error occurred. This is a good fallback if an
|
||||
@@ -497,6 +531,7 @@ class InternalServerError(HTTPException):
|
||||
|
||||
|
||||
class NotImplemented(HTTPException):
|
||||
|
||||
"""*501* `Not Implemented`
|
||||
|
||||
Raise if the application does not support the action requested by the
|
||||
@@ -510,6 +545,7 @@ class NotImplemented(HTTPException):
|
||||
|
||||
|
||||
class BadGateway(HTTPException):
|
||||
|
||||
"""*502* `Bad Gateway`
|
||||
|
||||
If you do proxying in your application you should return this status code
|
||||
@@ -524,6 +560,7 @@ class BadGateway(HTTPException):
|
||||
|
||||
|
||||
class ServiceUnavailable(HTTPException):
|
||||
|
||||
"""*503* `Service Unavailable`
|
||||
|
||||
Status code you should return if a service is temporarily unavailable.
|
||||
@@ -536,22 +573,55 @@ class ServiceUnavailable(HTTPException):
|
||||
)
|
||||
|
||||
|
||||
class GatewayTimeout(HTTPException):
|
||||
|
||||
"""*504* `Gateway Timeout`
|
||||
|
||||
Status code you should return if a connection to an upstream server
|
||||
times out.
|
||||
"""
|
||||
code = 504
|
||||
description = (
|
||||
'The connection to an upstream server timed out.'
|
||||
)
|
||||
|
||||
|
||||
class HTTPVersionNotSupported(HTTPException):
|
||||
|
||||
"""*505* `HTTP Version Not Supported`
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
code = 505
|
||||
description = (
|
||||
'The server does not support the HTTP protocol version used in the '
|
||||
'request.'
|
||||
)
|
||||
|
||||
|
||||
default_exceptions = {}
|
||||
__all__ = ['HTTPException']
|
||||
|
||||
|
||||
def _find_exceptions():
|
||||
for name, obj in iteritems(globals()):
|
||||
try:
|
||||
if getattr(obj, 'code', None) is not None:
|
||||
default_exceptions[obj.code] = obj
|
||||
__all__.append(obj.__name__)
|
||||
except TypeError: # pragma: no cover
|
||||
is_http_exception = issubclass(obj, HTTPException)
|
||||
except TypeError:
|
||||
is_http_exception = False
|
||||
if not is_http_exception or obj.code is None:
|
||||
continue
|
||||
__all__.append(obj.__name__)
|
||||
old_obj = default_exceptions.get(obj.code, None)
|
||||
if old_obj is not None and issubclass(obj, old_obj):
|
||||
continue
|
||||
default_exceptions[obj.code] = obj
|
||||
_find_exceptions()
|
||||
del _find_exceptions
|
||||
|
||||
|
||||
class Aborter(object):
|
||||
|
||||
"""
|
||||
When passed a dict of code -> exception items it can be used as
|
||||
callable that raises exceptions. If the first argument to the
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.filesystem
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Various utilities for the local filesystem.
|
||||
|
||||
:copyright: (c) 2015 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
# We do not trust traditional unixes.
|
||||
has_likely_buggy_unicode_filesystem = \
|
||||
sys.platform.startswith('linux') or 'bsd' in sys.platform
|
||||
|
||||
|
||||
def _is_ascii_encoding(encoding):
|
||||
"""
|
||||
Given an encoding this figures out if the encoding is actually ASCII (which
|
||||
is something we don't actually want in most cases). This is necessary
|
||||
because ASCII comes under many names such as ANSI_X3.4-1968.
|
||||
"""
|
||||
if encoding is None:
|
||||
return False
|
||||
try:
|
||||
return codecs.lookup(encoding).name == 'ascii'
|
||||
except LookupError:
|
||||
return False
|
||||
|
||||
|
||||
class BrokenFilesystemWarning(RuntimeWarning, UnicodeWarning):
|
||||
'''The warning used by Werkzeug to signal a broken filesystem. Will only be
|
||||
used once per runtime.'''
|
||||
|
||||
|
||||
_warned_about_filesystem_encoding = False
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
"""
|
||||
Returns the filesystem encoding that should be used. Note that this is
|
||||
different from the Python understanding of the filesystem encoding which
|
||||
might be deeply flawed. Do not use this value against Python's unicode APIs
|
||||
because it might be different. See :ref:`filesystem-encoding` for the exact
|
||||
behavior.
|
||||
|
||||
The concept of a filesystem encoding in generally is not something you
|
||||
should rely on. As such if you ever need to use this function except for
|
||||
writing wrapper code reconsider.
|
||||
"""
|
||||
global _warned_about_filesystem_encoding
|
||||
rv = sys.getfilesystemencoding()
|
||||
if has_likely_buggy_unicode_filesystem and not rv \
|
||||
or _is_ascii_encoding(rv):
|
||||
if not _warned_about_filesystem_encoding:
|
||||
warnings.warn(
|
||||
'Detected a misconfigured UNIX filesystem: Will use UTF-8 as '
|
||||
'filesystem encoding instead of {0!r}'.format(rv),
|
||||
BrokenFilesystemWarning)
|
||||
_warned_about_filesystem_encoding = True
|
||||
return 'utf-8'
|
||||
return rv
|
||||
@@ -6,7 +6,7 @@
|
||||
This module implements the form parsing. It supports url-encoded forms
|
||||
as well as non-nested multipart uploads.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
@@ -19,7 +19,7 @@ from functools import update_wrapper
|
||||
from werkzeug._compat import to_native, text_type
|
||||
from werkzeug.urls import url_decode_stream
|
||||
from werkzeug.wsgi import make_line_iter, \
|
||||
get_input_stream, get_content_length
|
||||
get_input_stream, get_content_length
|
||||
from werkzeug.datastructures import Headers, FileStorage, MultiDict
|
||||
from werkzeug.http import parse_options_header
|
||||
|
||||
@@ -94,6 +94,7 @@ def parse_form_data(environ, stream_factory=None, charset='utf-8',
|
||||
|
||||
def exhaust_stream(f):
|
||||
"""Helper decorator for methods that exhausts the stream on return."""
|
||||
|
||||
def wrapper(self, stream, *args, **kwargs):
|
||||
try:
|
||||
return f(self, stream, *args, **kwargs)
|
||||
@@ -110,6 +111,7 @@ def exhaust_stream(f):
|
||||
|
||||
|
||||
class FormDataParser(object):
|
||||
|
||||
"""This class implements parsing of form data for Werkzeug. By itself
|
||||
it can parse multipart and url encoded form data. It can be subclassed
|
||||
and extended but for most mimetypes it is a better idea to use the
|
||||
@@ -203,6 +205,8 @@ class FormDataParser(object):
|
||||
max_form_memory_size=self.max_form_memory_size,
|
||||
cls=self.cls)
|
||||
boundary = options.get('boundary')
|
||||
if boundary is None:
|
||||
raise ValueError('Missing boundary')
|
||||
if isinstance(boundary, text_type):
|
||||
boundary = boundary.encode('ascii')
|
||||
form, files = parser.parse(stream, boundary, content_length)
|
||||
@@ -361,14 +365,14 @@ class MultiPartParser(object):
|
||||
self.fail('Missing boundary')
|
||||
if not is_valid_multipart_boundary(boundary):
|
||||
self.fail('Invalid boundary: %s' % boundary)
|
||||
if len(boundary) > self.buffer_size: # pragma: no cover
|
||||
if len(boundary) > self.buffer_size: # pragma: no cover
|
||||
# this should never happen because we check for a minimum size
|
||||
# of 1024 and boundaries may not be longer than 200. The only
|
||||
# situation when this happens is for non debug builds where
|
||||
# the assert is skipped.
|
||||
self.fail('Boundary longer than buffer size')
|
||||
|
||||
def parse_lines(self, file, boundary, content_length):
|
||||
def parse_lines(self, file, boundary, content_length, cap_at_buffer=True):
|
||||
"""Generate parts of
|
||||
``('begin_form', (headers, name))``
|
||||
``('begin_file', (headers, name, filename))``
|
||||
@@ -383,7 +387,8 @@ class MultiPartParser(object):
|
||||
last_part = next_part + b'--'
|
||||
|
||||
iterator = chain(make_line_iter(file, limit=content_length,
|
||||
buffer_size=self.buffer_size),
|
||||
buffer_size=self.buffer_size,
|
||||
cap_at_buffer=cap_at_buffer),
|
||||
_empty_string_iter)
|
||||
|
||||
terminator = self._find_terminator(iterator)
|
||||
@@ -455,7 +460,7 @@ class MultiPartParser(object):
|
||||
cutoff = -1
|
||||
yield _cont, line[:cutoff]
|
||||
|
||||
else: # pragma: no cover
|
||||
else: # pragma: no cover
|
||||
raise ValueError('unexpected end of part')
|
||||
|
||||
# if we have a leftover in the buffer that is not a newline
|
||||
@@ -508,7 +513,7 @@ class MultiPartParser(object):
|
||||
part_charset = self.get_part_charset(headers)
|
||||
yield ('form',
|
||||
(name, b''.join(container).decode(
|
||||
part_charset, self.errors)))
|
||||
part_charset, self.errors)))
|
||||
|
||||
def parse(self, file, boundary, content_length):
|
||||
formstream, filestream = tee(
|
||||
|
||||
@@ -13,40 +13,58 @@
|
||||
module.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
from time import time, gmtime
|
||||
try:
|
||||
from email.utils import parsedate_tz
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError: # pragma: no cover
|
||||
from email.Utils import parsedate_tz
|
||||
try:
|
||||
from urllib2 import parse_http_list as _parse_list_header
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError: # pragma: no cover
|
||||
from urllib.request import parse_http_list as _parse_list_header
|
||||
from datetime import datetime, timedelta
|
||||
from hashlib import md5
|
||||
import base64
|
||||
|
||||
from werkzeug._internal import _cookie_quote, _make_cookie_domain, \
|
||||
_cookie_parse_impl
|
||||
_cookie_parse_impl
|
||||
from werkzeug._compat import to_unicode, iteritems, text_type, \
|
||||
string_types, try_coerce_native, to_bytes, PY2, \
|
||||
integer_types
|
||||
string_types, try_coerce_native, to_bytes, PY2, \
|
||||
integer_types
|
||||
|
||||
|
||||
# incorrect
|
||||
_cookie_charset = 'latin1'
|
||||
_accept_re = re.compile(r'([^\s;,]+)(?:[^,]*?;\s*q=(\d*(?:\.\d+)?))?')
|
||||
# for explanation of "media-range", etc. see Sections 5.3.{1,2} of RFC 7231
|
||||
_accept_re = re.compile(
|
||||
r'''( # media-range capturing-parenthesis
|
||||
[^\s;,]+ # type/subtype
|
||||
(?:[ \t]*;[ \t]* # ";"
|
||||
(?: # parameter non-capturing-parenthesis
|
||||
[^\s;,q][^\s;,]* # token that doesn't start with "q"
|
||||
| # or
|
||||
q[^\s;,=][^\s;,]* # token that is more than just "q"
|
||||
)
|
||||
)* # zero or more parameters
|
||||
) # end of media-range
|
||||
(?:[ \t]*;[ \t]*q= # weight is a "q" parameter
|
||||
(\d*(?:\.\d+)?) # qvalue capturing-parentheses
|
||||
[^,]* # "extension" accept params: who cares?
|
||||
)? # accept params are optional
|
||||
''', re.VERBOSE)
|
||||
_token_chars = frozenset("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
'^_`abcdefghijklmnopqrstuvwxyz|~')
|
||||
_etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
|
||||
_unsafe_header_chars = set('()<>@,;:\"/[]?={} \t')
|
||||
_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
|
||||
_option_header_piece_re = re.compile(r';\s*(%s|[^\s;=]+)\s*(?:=\s*(%s|[^;]+))?\s*' %
|
||||
(_quoted_string_re, _quoted_string_re))
|
||||
_option_header_piece_re = re.compile(
|
||||
r';\s*(%s|[^\s;,=]+)\s*(?:=\s*(%s|[^;,]+)?)?\s*' %
|
||||
(_quoted_string_re, _quoted_string_re)
|
||||
)
|
||||
_option_header_start_mime_type = re.compile(r',\s*([^;,\s]+)([;,]\s*.+)?')
|
||||
|
||||
_entity_headers = frozenset([
|
||||
'allow', 'content-encoding', 'content-language', 'content-length',
|
||||
@@ -98,15 +116,15 @@ HTTP_STATUS_CODES = {
|
||||
415: 'Unsupported Media Type',
|
||||
416: 'Requested Range Not Satisfiable',
|
||||
417: 'Expectation Failed',
|
||||
418: 'I\'m a teapot', # see RFC 2324
|
||||
418: 'I\'m a teapot', # see RFC 2324
|
||||
422: 'Unprocessable Entity',
|
||||
423: 'Locked',
|
||||
424: 'Failed Dependency',
|
||||
426: 'Upgrade Required',
|
||||
428: 'Precondition Required', # see RFC 6585
|
||||
428: 'Precondition Required', # see RFC 6585
|
||||
429: 'Too Many Requests',
|
||||
431: 'Request Header Fields Too Large',
|
||||
449: 'Retry With', # proprietary MS extension
|
||||
449: 'Retry With', # proprietary MS extension
|
||||
500: 'Internal Server Error',
|
||||
501: 'Not Implemented',
|
||||
502: 'Bad Gateway',
|
||||
@@ -124,7 +142,7 @@ def wsgi_to_bytes(data):
|
||||
"""
|
||||
if isinstance(data, bytes):
|
||||
return data
|
||||
return data.encode('latin1') #XXX: utf8 fallback?
|
||||
return data.encode('latin1') # XXX: utf8 fallback?
|
||||
|
||||
|
||||
def bytes_to_wsgi(data):
|
||||
@@ -287,7 +305,7 @@ def parse_dict_header(value, cls=dict):
|
||||
"""
|
||||
result = cls()
|
||||
if not isinstance(value, text_type):
|
||||
#XXX: validate
|
||||
# XXX: validate
|
||||
value = bytes_to_wsgi(value)
|
||||
for item in _parse_list_header(value):
|
||||
if '=' not in item:
|
||||
@@ -300,7 +318,7 @@ def parse_dict_header(value, cls=dict):
|
||||
return result
|
||||
|
||||
|
||||
def parse_options_header(value):
|
||||
def parse_options_header(value, multiple=False):
|
||||
"""Parse a ``Content-Type`` like header into a tuple with the content
|
||||
type and the options:
|
||||
|
||||
@@ -314,23 +332,42 @@ def parse_options_header(value):
|
||||
.. versionadded:: 0.5
|
||||
|
||||
:param value: the header to parse.
|
||||
:return: (str, options)
|
||||
:param multiple: Whether try to parse and return multiple MIME types
|
||||
:return: (mimetype, options) or (mimetype, options, mimetype, options, …)
|
||||
if multiple=True
|
||||
"""
|
||||
def _tokenize(string):
|
||||
for match in _option_header_piece_re.finditer(string):
|
||||
key, value = match.groups()
|
||||
key = unquote_header_value(key)
|
||||
if value is not None:
|
||||
value = unquote_header_value(value, key == 'filename')
|
||||
yield key, value
|
||||
|
||||
if not value:
|
||||
return '', {}
|
||||
|
||||
parts = _tokenize(';' + value)
|
||||
name = next(parts)[0]
|
||||
extra = dict(parts)
|
||||
return name, extra
|
||||
result = []
|
||||
|
||||
value = "," + value.replace("\n", ",")
|
||||
while value:
|
||||
match = _option_header_start_mime_type.match(value)
|
||||
if not match:
|
||||
break
|
||||
result.append(match.group(1)) # mimetype
|
||||
options = {}
|
||||
# Parse options
|
||||
rest = match.group(2)
|
||||
while rest:
|
||||
optmatch = _option_header_piece_re.match(rest)
|
||||
if not optmatch:
|
||||
break
|
||||
option, option_value = optmatch.groups()
|
||||
option = unquote_header_value(option)
|
||||
if option_value is not None:
|
||||
option_value = unquote_header_value(
|
||||
option_value,
|
||||
option == 'filename')
|
||||
options[option] = option_value
|
||||
rest = rest[optmatch.end():]
|
||||
result.append(options)
|
||||
if multiple is False:
|
||||
return tuple(result)
|
||||
value = rest
|
||||
|
||||
return tuple(result) if result else ('', {})
|
||||
|
||||
|
||||
def parse_accept_header(value, cls=None):
|
||||
@@ -440,14 +477,14 @@ def parse_authorization_header(value):
|
||||
if auth_type == b'basic':
|
||||
try:
|
||||
username, password = base64.b64decode(auth_info).split(b':', 1)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return
|
||||
return Authorization('basic', {'username': bytes_to_wsgi(username),
|
||||
'password': bytes_to_wsgi(password)})
|
||||
elif auth_type == b'digest':
|
||||
auth_map = parse_dict_header(auth_info)
|
||||
for key in 'username', 'realm', 'nonce', 'uri', 'response':
|
||||
if not key in auth_map:
|
||||
if key not in auth_map:
|
||||
return
|
||||
if 'qop' in auth_map:
|
||||
if not auth_map.get('nc') or not auth_map.get('cnonce'):
|
||||
@@ -589,14 +626,14 @@ def quote_etag(etag, weak=False):
|
||||
raise ValueError('invalid etag')
|
||||
etag = '"%s"' % etag
|
||||
if weak:
|
||||
etag = 'w/' + etag
|
||||
etag = 'W/' + etag
|
||||
return etag
|
||||
|
||||
|
||||
def unquote_etag(etag):
|
||||
"""Unquote a single etag:
|
||||
|
||||
>>> unquote_etag('w/"bar"')
|
||||
>>> unquote_etag('W/"bar"')
|
||||
('bar', True)
|
||||
>>> unquote_etag('"bar"')
|
||||
('bar', False)
|
||||
@@ -608,7 +645,7 @@ def unquote_etag(etag):
|
||||
return None, None
|
||||
etag = etag.strip()
|
||||
weak = False
|
||||
if etag[:2] in ('w/', 'W/'):
|
||||
if etag.startswith(('W/', 'w/')):
|
||||
weak = True
|
||||
etag = etag[2:]
|
||||
if etag[:1] == etag[-1:] == '"':
|
||||
@@ -678,7 +715,7 @@ def parse_date(value):
|
||||
elif year >= 69 and year <= 99:
|
||||
year += 1900
|
||||
return datetime(*((year,) + t[1:7])) - \
|
||||
timedelta(seconds=t[-1] or 0)
|
||||
timedelta(seconds=t[-1] or 0)
|
||||
except (ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
@@ -762,7 +799,11 @@ def is_resource_modified(environ, etag=None, data=None, last_modified=None):
|
||||
if etag:
|
||||
if_none_match = parse_etags(environ.get('HTTP_IF_NONE_MATCH'))
|
||||
if if_none_match:
|
||||
unmodified = if_none_match.contains_raw(etag)
|
||||
# http://tools.ietf.org/html/rfc7232#section-3.2
|
||||
# "A recipient MUST use the weak comparison function when comparing
|
||||
# entity-tags for If-None-Match"
|
||||
etag, _ = unquote_etag(etag)
|
||||
unmodified = if_none_match.contains_weak(etag)
|
||||
|
||||
return not unmodified
|
||||
|
||||
@@ -969,12 +1010,13 @@ def is_byte_range_valid(start, stop, length):
|
||||
|
||||
# circular dependency fun
|
||||
from werkzeug.datastructures import Accept, HeaderSet, ETags, Authorization, \
|
||||
WWWAuthenticate, TypeConversionDict, IfRange, Range, ContentRange, \
|
||||
RequestCacheControl
|
||||
WWWAuthenticate, TypeConversionDict, IfRange, Range, ContentRange, \
|
||||
RequestCacheControl
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
# backwards compatible imports
|
||||
from werkzeug.datastructures import MIMEAccept, CharsetAccept, \
|
||||
LanguageAccept, Headers
|
||||
from werkzeug.datastructures import ( # noqa
|
||||
MIMEAccept, CharsetAccept, LanguageAccept, Headers
|
||||
)
|
||||
from werkzeug.urls import iri_to_uri
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
|
||||
This module implements context-local objects.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import copy
|
||||
from functools import update_wrapper
|
||||
from werkzeug.wsgi import ClosingIterator
|
||||
from werkzeug._compat import PY2, implements_bool
|
||||
@@ -37,7 +38,7 @@ def release_local(local):
|
||||
False
|
||||
|
||||
With this function one can release :class:`Local` objects as well
|
||||
as :class:`StackLocal` objects. However it is not possible to
|
||||
as :class:`LocalStack` objects. However it is not possible to
|
||||
release data held by proxies that way, one always has to retain
|
||||
a reference to the underlying local object in order to be able
|
||||
to release it.
|
||||
@@ -86,6 +87,7 @@ class Local(object):
|
||||
|
||||
|
||||
class LocalStack(object):
|
||||
|
||||
"""This class works similar to a :class:`Local` but keeps a stack
|
||||
of objects instead. This is best explained with an example::
|
||||
|
||||
@@ -120,6 +122,7 @@ class LocalStack(object):
|
||||
|
||||
def _get__ident_func__(self):
|
||||
return self._local.__ident_func__
|
||||
|
||||
def _set__ident_func__(self, value):
|
||||
object.__setattr__(self._local, '__ident_func__', value)
|
||||
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
|
||||
@@ -166,6 +169,7 @@ class LocalStack(object):
|
||||
|
||||
|
||||
class LocalManager(object):
|
||||
|
||||
"""Local objects cannot manage themselves. For that you need a local
|
||||
manager. You can pass a local manager multiple locals or add them later
|
||||
by appending them to `manager.locals`. Everytime the manager cleans up
|
||||
@@ -203,7 +207,7 @@ class LocalManager(object):
|
||||
scoped sessions) to the Werkzeug locals.
|
||||
|
||||
.. versionchanged:: 0.7
|
||||
Yu can pass a different ident function to the local manager that
|
||||
You can pass a different ident function to the local manager that
|
||||
will then be propagated to all the locals passed to the
|
||||
constructor.
|
||||
"""
|
||||
@@ -248,6 +252,7 @@ class LocalManager(object):
|
||||
|
||||
@implements_bool
|
||||
class LocalProxy(object):
|
||||
|
||||
"""Acts as a proxy for a werkzeug local. Forwards all operations to
|
||||
a proxied object. The only operations not supported for forwarding
|
||||
are right handed operands and any kind of assignment.
|
||||
@@ -322,7 +327,7 @@ class LocalProxy(object):
|
||||
|
||||
def __unicode__(self):
|
||||
try:
|
||||
return unicode(self._get_current_object())
|
||||
return unicode(self._get_current_object()) # noqa
|
||||
except RuntimeError:
|
||||
return repr(self)
|
||||
|
||||
@@ -361,7 +366,7 @@ class LocalProxy(object):
|
||||
__ne__ = lambda x, o: x._get_current_object() != o
|
||||
__gt__ = lambda x, o: x._get_current_object() > o
|
||||
__ge__ = lambda x, o: x._get_current_object() >= o
|
||||
__cmp__ = lambda x, o: cmp(x._get_current_object(), o)
|
||||
__cmp__ = lambda x, o: cmp(x._get_current_object(), o) # noqa
|
||||
__hash__ = lambda x: hash(x._get_current_object())
|
||||
__call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)
|
||||
__len__ = lambda x: len(x._get_current_object())
|
||||
@@ -388,7 +393,7 @@ class LocalProxy(object):
|
||||
__invert__ = lambda x: ~(x._get_current_object())
|
||||
__complex__ = lambda x: complex(x._get_current_object())
|
||||
__int__ = lambda x: int(x._get_current_object())
|
||||
__long__ = lambda x: long(x._get_current_object())
|
||||
__long__ = lambda x: long(x._get_current_object()) # noqa
|
||||
__float__ = lambda x: float(x._get_current_object())
|
||||
__oct__ = lambda x: oct(x._get_current_object())
|
||||
__hex__ = lambda x: hex(x._get_current_object())
|
||||
@@ -407,3 +412,5 @@ class LocalProxy(object):
|
||||
__rfloordiv__ = lambda x, o: o // x._get_current_object()
|
||||
__rmod__ = lambda x, o: o % x._get_current_object()
|
||||
__rdivmod__ = lambda x, o: x._get_current_object().__rdivmod__(o)
|
||||
__copy__ = lambda x: copy.copy(x._get_current_object())
|
||||
__deepcopy__ = lambda x, memo: copy.deepcopy(x._get_current_object(), memo)
|
||||
|
||||
@@ -14,7 +14,7 @@ r"""
|
||||
This module was introduced in 0.6.1 and is not a public interface.
|
||||
It might become one in later versions of Werkzeug.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
@@ -23,9 +23,12 @@ import errno
|
||||
import time
|
||||
import random
|
||||
|
||||
from ._compat import to_unicode
|
||||
from .filesystem import get_filesystem_encoding
|
||||
|
||||
|
||||
can_rename_open_file = False
|
||||
if os.name == 'nt': # pragma: no cover
|
||||
if os.name == 'nt': # pragma: no cover
|
||||
_rename = lambda src, dst: False
|
||||
_rename_atomic = lambda src, dst: False
|
||||
|
||||
@@ -37,17 +40,15 @@ if os.name == 'nt': # pragma: no cover
|
||||
_MoveFileEx = ctypes.windll.kernel32.MoveFileExW
|
||||
|
||||
def _rename(src, dst):
|
||||
if not isinstance(src, unicode):
|
||||
src = unicode(src, sys.getfilesystemencoding())
|
||||
if not isinstance(dst, unicode):
|
||||
dst = unicode(dst, sys.getfilesystemencoding())
|
||||
src = to_unicode(src, get_filesystem_encoding())
|
||||
dst = to_unicode(dst, get_filesystem_encoding())
|
||||
if _rename_atomic(src, dst):
|
||||
return True
|
||||
retry = 0
|
||||
rv = False
|
||||
while not rv and retry < 100:
|
||||
rv = _MoveFileEx(src, dst, _MOVEFILE_REPLACE_EXISTING |
|
||||
_MOVEFILE_WRITE_THROUGH)
|
||||
_MOVEFILE_WRITE_THROUGH)
|
||||
if not rv:
|
||||
time.sleep(0.001)
|
||||
retry += 1
|
||||
|
||||
@@ -92,24 +92,25 @@
|
||||
method is raised.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import difflib
|
||||
import re
|
||||
import uuid
|
||||
import posixpath
|
||||
from pprint import pformat
|
||||
try:
|
||||
from urlparse import urljoin
|
||||
except ImportError:
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from werkzeug.urls import url_encode, url_quote
|
||||
from pprint import pformat
|
||||
from threading import Lock
|
||||
|
||||
from werkzeug.urls import url_encode, url_quote, url_join
|
||||
from werkzeug.utils import redirect, format_string
|
||||
from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed
|
||||
from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed, \
|
||||
BadHost
|
||||
from werkzeug._internal import _get_environ, _encode_idna
|
||||
from werkzeug._compat import itervalues, iteritems, to_unicode, to_bytes, \
|
||||
text_type, string_types, native_string_result, \
|
||||
implements_to_string, wsgi_decoding_dance
|
||||
text_type, string_types, native_string_result, \
|
||||
implements_to_string, wsgi_decoding_dance
|
||||
from werkzeug.datastructures import ImmutableDict, MultiDict
|
||||
|
||||
|
||||
@@ -135,7 +136,7 @@ _converter_args_re = re.compile(r'''
|
||||
\w+|
|
||||
[urUR]?(?P<stringval>"[^"]*?"|'[^']*')
|
||||
)\s*,
|
||||
''', re.VERBOSE|re.UNICODE)
|
||||
''', re.VERBOSE | re.UNICODE)
|
||||
|
||||
|
||||
_PYTHON_CONSTANTS = {
|
||||
@@ -210,6 +211,7 @@ def parse_rule(rule):
|
||||
|
||||
|
||||
class RoutingException(Exception):
|
||||
|
||||
"""Special exceptions that require the application to redirect, notifying
|
||||
about missing urls, etc.
|
||||
|
||||
@@ -218,6 +220,7 @@ class RoutingException(Exception):
|
||||
|
||||
|
||||
class RequestRedirect(HTTPException, RoutingException):
|
||||
|
||||
"""Raise if the map requests a redirect. This is for example the case if
|
||||
`strict_slashes` are activated and an url that requires a trailing slash.
|
||||
|
||||
@@ -234,10 +237,12 @@ class RequestRedirect(HTTPException, RoutingException):
|
||||
|
||||
|
||||
class RequestSlash(RoutingException):
|
||||
|
||||
"""Internal exception."""
|
||||
|
||||
|
||||
class RequestAliasRedirect(RoutingException):
|
||||
|
||||
"""This rule is an alias and wants to redirect to the canonical URL."""
|
||||
|
||||
def __init__(self, matched_values):
|
||||
@@ -245,24 +250,71 @@ class RequestAliasRedirect(RoutingException):
|
||||
|
||||
|
||||
class BuildError(RoutingException, LookupError):
|
||||
|
||||
"""Raised if the build system cannot find a URL for an endpoint with the
|
||||
values provided.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint, values, method):
|
||||
def __init__(self, endpoint, values, method, adapter=None):
|
||||
LookupError.__init__(self, endpoint, values, method)
|
||||
self.endpoint = endpoint
|
||||
self.values = values
|
||||
self.method = method
|
||||
self.suggested = self.closest_rule(adapter)
|
||||
|
||||
def closest_rule(self, adapter):
|
||||
def score_rule(rule):
|
||||
return sum([
|
||||
0.98 * difflib.SequenceMatcher(
|
||||
None, rule.endpoint, self.endpoint
|
||||
).ratio(),
|
||||
0.01 * bool(set(self.values or ()).issubset(rule.arguments)),
|
||||
0.01 * bool(rule.methods and self.method in rule.methods)
|
||||
])
|
||||
|
||||
if adapter and adapter.map._rules:
|
||||
return max(adapter.map._rules, key=score_rule)
|
||||
else:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
message = []
|
||||
message.append("Could not build url for endpoint %r" % self.endpoint)
|
||||
if self.method:
|
||||
message.append(" (%r)" % self.method)
|
||||
if self.values:
|
||||
message.append(" with values %r" % sorted(self.values.keys()))
|
||||
message.append(".")
|
||||
if self.suggested:
|
||||
if self.endpoint == self.suggested.endpoint:
|
||||
if self.method and self.method not in self.suggested.methods:
|
||||
message.append(" Did you mean to use methods %r?" % sorted(
|
||||
self.suggested.methods
|
||||
))
|
||||
missing_values = self.suggested.arguments.union(
|
||||
set(self.suggested.defaults or ())
|
||||
) - set(self.values.keys())
|
||||
if missing_values:
|
||||
message.append(
|
||||
" Did you forget to specify values %r?" %
|
||||
sorted(missing_values)
|
||||
)
|
||||
else:
|
||||
message.append(
|
||||
" Did you mean %r instead?" % self.suggested.endpoint
|
||||
)
|
||||
return "".join(message)
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
|
||||
"""Validation error. If a rule converter raises this exception the rule
|
||||
does not match the current URL and the next URL is tried.
|
||||
"""
|
||||
|
||||
|
||||
class RuleFactory(object):
|
||||
|
||||
"""As soon as you have more complex URL setups it's a good idea to use rule
|
||||
factories to avoid repetitive tasks. Some of them are builtin, others can
|
||||
be added by subclassing `RuleFactory` and overriding `get_rules`.
|
||||
@@ -275,6 +327,7 @@ class RuleFactory(object):
|
||||
|
||||
|
||||
class Subdomain(RuleFactory):
|
||||
|
||||
"""All URLs provided by this factory have the subdomain set to a
|
||||
specific domain. For example if you want to use the subdomain for
|
||||
the current language this can be a good setup::
|
||||
@@ -306,6 +359,7 @@ class Subdomain(RuleFactory):
|
||||
|
||||
|
||||
class Submount(RuleFactory):
|
||||
|
||||
"""Like `Subdomain` but prefixes the URL rule with a given string::
|
||||
|
||||
url_map = Map([
|
||||
@@ -332,6 +386,7 @@ class Submount(RuleFactory):
|
||||
|
||||
|
||||
class EndpointPrefix(RuleFactory):
|
||||
|
||||
"""Prefixes all endpoints (which must be strings for this factory) with
|
||||
another string. This can be useful for sub applications::
|
||||
|
||||
@@ -357,6 +412,7 @@ class EndpointPrefix(RuleFactory):
|
||||
|
||||
|
||||
class RuleTemplate(object):
|
||||
|
||||
"""Returns copies of the rules wrapped and expands string templates in
|
||||
the endpoint, rule, defaults or subdomain sections.
|
||||
|
||||
@@ -383,6 +439,7 @@ class RuleTemplate(object):
|
||||
|
||||
|
||||
class RuleTemplateFactory(RuleFactory):
|
||||
|
||||
"""A factory that fills in template variables into rules. Used by
|
||||
`RuleTemplate` internally.
|
||||
|
||||
@@ -421,6 +478,7 @@ class RuleTemplateFactory(RuleFactory):
|
||||
|
||||
@implements_to_string
|
||||
class Rule(RuleFactory):
|
||||
|
||||
"""A Rule represents one URL pattern. There are some options for `Rule`
|
||||
that change the way it behaves and are passed to the `Rule` constructor.
|
||||
Note that besides the rule-string all arguments *must* be keyword arguments
|
||||
@@ -565,14 +623,35 @@ class Rule(RuleFactory):
|
||||
self._trace = self._converters = self._regex = self._weights = None
|
||||
|
||||
def empty(self):
|
||||
"""Return an unbound copy of this rule. This can be useful if you
|
||||
want to reuse an already bound URL for another map."""
|
||||
"""
|
||||
Return an unbound copy of this rule.
|
||||
|
||||
This can be useful if want to reuse an already bound URL for another
|
||||
map. See ``get_empty_kwargs`` to override what keyword arguments are
|
||||
provided to the new copy.
|
||||
"""
|
||||
return type(self)(self.rule, **self.get_empty_kwargs())
|
||||
|
||||
def get_empty_kwargs(self):
|
||||
"""
|
||||
Provides kwargs for instantiating empty copy with empty()
|
||||
|
||||
Use this method to provide custom keyword arguments to the subclass of
|
||||
``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
|
||||
has custom keyword arguments that are needed at instantiation.
|
||||
|
||||
Must return a ``dict`` that will be provided as kwargs to the new
|
||||
instance of ``Rule``, following the initial ``self.rule`` value which
|
||||
is always provided as the first, required positional argument.
|
||||
"""
|
||||
defaults = None
|
||||
if self.defaults:
|
||||
defaults = dict(self.defaults)
|
||||
return Rule(self.rule, defaults, self.subdomain, self.methods,
|
||||
self.build_only, self.endpoint, self.strict_slashes,
|
||||
self.redirect_to, self.alias, self.host)
|
||||
return dict(defaults=defaults, subdomain=self.subdomain,
|
||||
methods=self.methods, build_only=self.build_only,
|
||||
endpoint=self.endpoint, strict_slashes=self.strict_slashes,
|
||||
redirect_to=self.redirect_to, alias=self.alias,
|
||||
host=self.host)
|
||||
|
||||
def get_rules(self, map):
|
||||
yield self
|
||||
@@ -606,7 +685,7 @@ class Rule(RuleFactory):
|
||||
|
||||
.. versionadded:: 0.9
|
||||
"""
|
||||
if not converter_name in self.map.converters:
|
||||
if converter_name not in self.map.converters:
|
||||
raise LookupError('the converter %r does not exist' % converter_name)
|
||||
return self.map.converters[converter_name](self.map, *args, **kwargs)
|
||||
|
||||
@@ -657,8 +736,8 @@ class Rule(RuleFactory):
|
||||
return
|
||||
regex = r'^%s%s$' % (
|
||||
u''.join(regex_parts),
|
||||
(not self.is_leaf or not self.strict_slashes) and \
|
||||
'(?<!/)(?P<__suffix__>/?)' or ''
|
||||
(not self.is_leaf or not self.strict_slashes) and
|
||||
'(?<!/)(?P<__suffix__>/?)' or ''
|
||||
)
|
||||
self._regex = re.compile(regex, re.UNICODE)
|
||||
|
||||
@@ -732,8 +811,8 @@ class Rule(RuleFactory):
|
||||
|
||||
if query_vars:
|
||||
url += u'?' + url_encode(query_vars, charset=self.map.charset,
|
||||
sort=self.map.sort_parameters,
|
||||
key=self.map.sort_key)
|
||||
sort=self.map.sort_parameters,
|
||||
key=self.map.sort_key)
|
||||
|
||||
return domain_part, url
|
||||
|
||||
@@ -743,8 +822,8 @@ class Rule(RuleFactory):
|
||||
:internal:
|
||||
"""
|
||||
return not self.build_only and self.defaults and \
|
||||
self.endpoint == rule.endpoint and self != rule and \
|
||||
self.arguments == rule.arguments
|
||||
self.endpoint == rule.endpoint and self != rule and \
|
||||
self.arguments == rule.arguments
|
||||
|
||||
def suitable_for(self, values, method=None):
|
||||
"""Check if the dict of values has enough data for url generation.
|
||||
@@ -796,11 +875,11 @@ class Rule(RuleFactory):
|
||||
:internal:
|
||||
"""
|
||||
return self.alias and 1 or 0, -len(self.arguments), \
|
||||
-len(self.defaults or ())
|
||||
-len(self.defaults or ())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ is other.__class__ and \
|
||||
self._trace == other._trace
|
||||
self._trace == other._trace
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
@@ -821,13 +900,15 @@ class Rule(RuleFactory):
|
||||
return u'<%s %s%s -> %s>' % (
|
||||
self.__class__.__name__,
|
||||
repr((u''.join(tmp)).lstrip(u'|')).lstrip(u'u'),
|
||||
self.methods is not None and u' (%s)' % \
|
||||
u', '.join(self.methods) or u'',
|
||||
self.methods is not None
|
||||
and u' (%s)' % u', '.join(self.methods)
|
||||
or u'',
|
||||
self.endpoint
|
||||
)
|
||||
|
||||
|
||||
class BaseConverter(object):
|
||||
|
||||
"""Base class for all converters."""
|
||||
regex = '[^/]+'
|
||||
weight = 100
|
||||
@@ -843,6 +924,7 @@ class BaseConverter(object):
|
||||
|
||||
|
||||
class UnicodeConverter(BaseConverter):
|
||||
|
||||
"""This converter is the default converter and accepts any string but
|
||||
only one path segment. Thus the string can not include a slash.
|
||||
|
||||
@@ -877,6 +959,7 @@ class UnicodeConverter(BaseConverter):
|
||||
|
||||
|
||||
class AnyConverter(BaseConverter):
|
||||
|
||||
"""Matches one of the items provided. Items can either be Python
|
||||
identifiers or strings::
|
||||
|
||||
@@ -893,6 +976,7 @@ class AnyConverter(BaseConverter):
|
||||
|
||||
|
||||
class PathConverter(BaseConverter):
|
||||
|
||||
"""Like the default :class:`UnicodeConverter`, but it also matches
|
||||
slashes. This is useful for wikis and similar applications::
|
||||
|
||||
@@ -906,6 +990,7 @@ class PathConverter(BaseConverter):
|
||||
|
||||
|
||||
class NumberConverter(BaseConverter):
|
||||
|
||||
"""Baseclass for `IntegerConverter` and `FloatConverter`.
|
||||
|
||||
:internal:
|
||||
@@ -935,6 +1020,7 @@ class NumberConverter(BaseConverter):
|
||||
|
||||
|
||||
class IntegerConverter(NumberConverter):
|
||||
|
||||
"""This converter only accepts integer values::
|
||||
|
||||
Rule('/page/<int:page>')
|
||||
@@ -954,6 +1040,7 @@ class IntegerConverter(NumberConverter):
|
||||
|
||||
|
||||
class FloatConverter(NumberConverter):
|
||||
|
||||
"""This converter only accepts floating point values::
|
||||
|
||||
Rule('/probability/<float:probability>')
|
||||
@@ -971,6 +1058,26 @@ class FloatConverter(NumberConverter):
|
||||
NumberConverter.__init__(self, map, 0, min, max)
|
||||
|
||||
|
||||
class UUIDConverter(BaseConverter):
|
||||
|
||||
"""This converter only accepts UUID strings::
|
||||
|
||||
Rule('/object/<uuid:identifier>')
|
||||
|
||||
.. versionadded:: 0.10
|
||||
|
||||
:param map: the :class:`Map`.
|
||||
"""
|
||||
regex = r'[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-' \
|
||||
r'[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}'
|
||||
|
||||
def to_python(self, value):
|
||||
return uuid.UUID(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return str(value)
|
||||
|
||||
|
||||
#: the default converter mapping for the map.
|
||||
DEFAULT_CONVERTERS = {
|
||||
'default': UnicodeConverter,
|
||||
@@ -978,11 +1085,13 @@ DEFAULT_CONVERTERS = {
|
||||
'any': AnyConverter,
|
||||
'path': PathConverter,
|
||||
'int': IntegerConverter,
|
||||
'float': FloatConverter
|
||||
'float': FloatConverter,
|
||||
'uuid': UUIDConverter,
|
||||
}
|
||||
|
||||
|
||||
class Map(object):
|
||||
|
||||
"""The map class stores all the URL rules and some configuration
|
||||
parameters. Some of the configuration values are only stored on the
|
||||
`Map` instance since those affect all rules, others are just defaults
|
||||
@@ -1027,6 +1136,7 @@ class Map(object):
|
||||
self._rules = []
|
||||
self._rules_by_endpoint = {}
|
||||
self._remap = True
|
||||
self._remap_lock = Lock()
|
||||
|
||||
self.default_subdomain = default_subdomain
|
||||
self.charset = charset
|
||||
@@ -1123,7 +1233,10 @@ class Map(object):
|
||||
subdomain = self.default_subdomain
|
||||
if script_name is None:
|
||||
script_name = '/'
|
||||
server_name = _encode_idna(server_name)
|
||||
try:
|
||||
server_name = _encode_idna(server_name)
|
||||
except UnicodeError:
|
||||
raise BadHost()
|
||||
return MapAdapter(self, server_name, script_name, subdomain,
|
||||
url_scheme, path_info, default_method, query_args)
|
||||
|
||||
@@ -1162,24 +1275,31 @@ class Map(object):
|
||||
:param subdomain: optionally the current subdomain (see above).
|
||||
"""
|
||||
environ = _get_environ(environ)
|
||||
|
||||
if 'HTTP_HOST' in environ:
|
||||
wsgi_server_name = environ['HTTP_HOST']
|
||||
|
||||
if environ['wsgi.url_scheme'] == 'http' \
|
||||
and wsgi_server_name.endswith(':80'):
|
||||
wsgi_server_name = wsgi_server_name[:-3]
|
||||
elif environ['wsgi.url_scheme'] == 'https' \
|
||||
and wsgi_server_name.endswith(':443'):
|
||||
wsgi_server_name = wsgi_server_name[:-4]
|
||||
else:
|
||||
wsgi_server_name = environ['SERVER_NAME']
|
||||
|
||||
if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
|
||||
in (('https', '443'), ('http', '80')):
|
||||
wsgi_server_name += ':' + environ['SERVER_PORT']
|
||||
|
||||
wsgi_server_name = wsgi_server_name.lower()
|
||||
|
||||
if server_name is None:
|
||||
if 'HTTP_HOST' in environ:
|
||||
server_name = environ['HTTP_HOST']
|
||||
else:
|
||||
server_name = environ['SERVER_NAME']
|
||||
if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
|
||||
in (('https', '443'), ('http', '80')):
|
||||
server_name += ':' + environ['SERVER_PORT']
|
||||
elif subdomain is None and not self.host_matching:
|
||||
server_name = wsgi_server_name
|
||||
else:
|
||||
server_name = server_name.lower()
|
||||
if 'HTTP_HOST' in environ:
|
||||
wsgi_server_name = environ.get('HTTP_HOST')
|
||||
else:
|
||||
wsgi_server_name = environ.get('SERVER_NAME')
|
||||
if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
|
||||
in (('https', '443'), ('http', '80')):
|
||||
wsgi_server_name += ':' + environ['SERVER_PORT']
|
||||
wsgi_server_name = wsgi_server_name.lower()
|
||||
|
||||
if subdomain is None and not self.host_matching:
|
||||
cur_server_name = wsgi_server_name.split('.')
|
||||
real_server_name = server_name.split('.')
|
||||
offset = -len(real_server_name)
|
||||
@@ -1210,7 +1330,13 @@ class Map(object):
|
||||
"""Called before matching and building to keep the compiled rules
|
||||
in the correct order after things changed.
|
||||
"""
|
||||
if self._remap:
|
||||
if not self._remap:
|
||||
return
|
||||
|
||||
with self._remap_lock:
|
||||
if not self._remap:
|
||||
return
|
||||
|
||||
self._rules.sort(key=lambda x: x.match_compare_key())
|
||||
for rules in itervalues(self._rules_by_endpoint):
|
||||
rules.sort(key=lambda x: x.build_compare_key())
|
||||
@@ -1222,6 +1348,7 @@ class Map(object):
|
||||
|
||||
|
||||
class MapAdapter(object):
|
||||
|
||||
"""Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
|
||||
the URL matching and building based on runtime information.
|
||||
"""
|
||||
@@ -1382,8 +1509,10 @@ class MapAdapter(object):
|
||||
query_args = self.query_args
|
||||
method = (method or self.default_method).upper()
|
||||
|
||||
path = u'%s|/%s' % (self.map.host_matching and self.server_name or
|
||||
self.subdomain, path_info.lstrip('/'))
|
||||
path = u'%s|%s' % (
|
||||
self.map.host_matching and self.server_name or self.subdomain,
|
||||
path_info and '/%s' % path_info.lstrip('/')
|
||||
)
|
||||
|
||||
have_match_for = set()
|
||||
for rule in self.map._rules:
|
||||
@@ -1391,7 +1520,8 @@ class MapAdapter(object):
|
||||
rv = rule.match(path)
|
||||
except RequestSlash:
|
||||
raise RequestRedirect(self.make_redirect_url(
|
||||
path_info + '/', query_args))
|
||||
url_quote(path_info, self.map.charset,
|
||||
safe='/:|+') + '/', query_args))
|
||||
except RequestAliasRedirect as e:
|
||||
raise RequestRedirect(self.make_alias_redirect_url(
|
||||
path, rule.endpoint, e.matched_values, method, query_args))
|
||||
@@ -1416,8 +1546,8 @@ class MapAdapter(object):
|
||||
rule.redirect_to)
|
||||
else:
|
||||
redirect_url = rule.redirect_to(self, **rv)
|
||||
raise RequestRedirect(str(urljoin('%s://%s%s%s' % (
|
||||
self.url_scheme,
|
||||
raise RequestRedirect(str(url_join('%s://%s%s%s' % (
|
||||
self.url_scheme or 'http',
|
||||
self.subdomain and self.subdomain + '.' or '',
|
||||
self.server_name,
|
||||
self.script_name
|
||||
@@ -1512,11 +1642,10 @@ class MapAdapter(object):
|
||||
if query_args:
|
||||
suffix = '?' + self.encode_query_args(query_args)
|
||||
return str('%s://%s/%s%s' % (
|
||||
self.url_scheme,
|
||||
self.url_scheme or 'http',
|
||||
self.get_host(domain_part),
|
||||
posixpath.join(self.script_name[:-1].lstrip('/'),
|
||||
url_quote(path_info.lstrip('/'), self.map.charset,
|
||||
safe='/:|+')),
|
||||
path_info.lstrip('/')),
|
||||
suffix
|
||||
))
|
||||
|
||||
@@ -1527,7 +1656,7 @@ class MapAdapter(object):
|
||||
if query_args:
|
||||
url += '?' + self.encode_query_args(query_args)
|
||||
assert url != path, 'detected invalid alias setting. No canonical ' \
|
||||
'URL found'
|
||||
'URL found'
|
||||
return url
|
||||
|
||||
def _partial_build(self, endpoint, values, method, append_unknown):
|
||||
@@ -1585,6 +1714,13 @@ class MapAdapter(object):
|
||||
>>> urls.build("index", {'q': 'My Searchstring'})
|
||||
'/?q=My+Searchstring'
|
||||
|
||||
When processing those additional values, lists are furthermore
|
||||
interpreted as multiple values (as per
|
||||
:py:class:`werkzeug.datastructures.MultiDict`):
|
||||
|
||||
>>> urls.build("index", {'q': ['a', 'b', 'c']})
|
||||
'/?q=a&q=b&q=c'
|
||||
|
||||
If a rule does not exist when building a `BuildError` exception is
|
||||
raised.
|
||||
|
||||
@@ -1600,7 +1736,9 @@ class MapAdapter(object):
|
||||
appended to the URL as query parameters.
|
||||
:param method: the HTTP method for the rule if there are different
|
||||
URLs for different methods on the same endpoint.
|
||||
:param force_external: enforce full canonical external URLs.
|
||||
:param force_external: enforce full canonical external URLs. If the URL
|
||||
scheme is not provided, this will generate
|
||||
a protocol-relative URL.
|
||||
:param append_unknown: unknown parameters are appended to the generated
|
||||
URL as query string argument. Disable this
|
||||
if you want the builder to ignore those.
|
||||
@@ -1608,7 +1746,7 @@ class MapAdapter(object):
|
||||
self.map.update()
|
||||
if values:
|
||||
if isinstance(values, MultiDict):
|
||||
valueiter = values.iteritems(multi=True)
|
||||
valueiter = iteritems(values, multi=True)
|
||||
else:
|
||||
valueiter = iteritems(values)
|
||||
values = dict((k, v) for k, v in valueiter if v is not None)
|
||||
@@ -1617,7 +1755,7 @@ class MapAdapter(object):
|
||||
|
||||
rv = self._partial_build(endpoint, values, method, append_unknown)
|
||||
if rv is None:
|
||||
raise BuildError(endpoint, values, method)
|
||||
raise BuildError(endpoint, values, method, self)
|
||||
domain_part, path = rv
|
||||
|
||||
host = self.get_host(domain_part)
|
||||
@@ -1625,10 +1763,11 @@ class MapAdapter(object):
|
||||
# shortcut this.
|
||||
if not force_external and (
|
||||
(self.map.host_matching and host == self.server_name) or
|
||||
(not self.map.host_matching and domain_part == self.subdomain)):
|
||||
return str(urljoin(self.script_name, './' + path.lstrip('/')))
|
||||
return str('%s://%s%s/%s' % (
|
||||
self.url_scheme,
|
||||
(not self.map.host_matching and domain_part == self.subdomain)
|
||||
):
|
||||
return str(url_join(self.script_name, './' + path.lstrip('/')))
|
||||
return str('%s//%s%s/%s' % (
|
||||
self.url_scheme + ':' if self.url_scheme else '',
|
||||
host,
|
||||
self.script_name[:-1],
|
||||
path.lstrip('/')
|
||||
|
||||
@@ -67,7 +67,7 @@ r'''
|
||||
or as named parameters, pretty much like Python function calls.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
'''
|
||||
from __future__ import print_function
|
||||
@@ -88,7 +88,7 @@ argument_types = {
|
||||
|
||||
|
||||
converters = {
|
||||
'boolean': lambda x: x.lower() in ('1', 'true', 'yes', 'on'),
|
||||
'boolean': lambda x: x.lower() in ('1', 'true', 'yes', 'on'),
|
||||
'string': str,
|
||||
'integer': int,
|
||||
'float': float
|
||||
@@ -194,8 +194,7 @@ def find_actions(namespace, action_prefix):
|
||||
|
||||
def print_usage(actions):
|
||||
"""Print the usage information. (Help screen)"""
|
||||
actions = actions.items()
|
||||
actions.sort()
|
||||
actions = sorted(iteritems(actions))
|
||||
print('usage: %s <action> [<options>]' % basename(sys.argv[0]))
|
||||
print(' %s --help' % basename(sys.argv[0]))
|
||||
print()
|
||||
@@ -260,6 +259,7 @@ def make_shell(init_func=None, banner=None, use_ipython=True):
|
||||
banner = 'Interactive Werkzeug Shell'
|
||||
if init_func is None:
|
||||
init_func = dict
|
||||
|
||||
def action(ipython=use_ipython):
|
||||
"""Start a new interactive python session."""
|
||||
namespace = init_func()
|
||||
@@ -310,7 +310,9 @@ def make_runserver(app_factory, hostname='localhost', port=5000,
|
||||
"""Start a new development server."""
|
||||
from werkzeug.serving import run_simple
|
||||
app = app_factory()
|
||||
run_simple(hostname, port, app, reloader, debugger, evalex,
|
||||
extra_files, 1, threaded, processes,
|
||||
run_simple(hostname, port, app,
|
||||
use_reloader=reloader, use_debugger=debugger,
|
||||
use_evalex=evalex, extra_files=extra_files,
|
||||
reloader_interval=1, threaded=threaded, processes=processes,
|
||||
static_files=static_files, ssl_context=ssl_context)
|
||||
return action
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
Security related helpers such as secure password hashing tools.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
@@ -19,7 +19,7 @@ from operator import xor
|
||||
from itertools import starmap
|
||||
|
||||
from werkzeug._compat import range_type, PY2, text_type, izip, to_bytes, \
|
||||
string_types, to_native
|
||||
string_types, to_native
|
||||
|
||||
|
||||
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
@@ -27,7 +27,7 @@ DEFAULT_PBKDF2_ITERATIONS = 1000
|
||||
|
||||
|
||||
_pack_int = Struct('>I').pack
|
||||
_builtin_safe_str_cmp = getattr(hmac, 'digest_compare', None)
|
||||
_builtin_safe_str_cmp = getattr(hmac, 'compare_digest', None)
|
||||
_sys_rng = SystemRandom()
|
||||
_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep]
|
||||
if sep not in (None, '/'))
|
||||
@@ -48,28 +48,31 @@ _hash_funcs = _find_hashlib_algorithms()
|
||||
|
||||
def pbkdf2_hex(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS,
|
||||
keylen=None, hashfunc=None):
|
||||
"""Like :func:`pbkdf2_bin` but returns a hex encoded string.
|
||||
"""Like :func:`pbkdf2_bin`, but returns a hex-encoded string.
|
||||
|
||||
.. versionadded:: 0.9
|
||||
|
||||
:param data: the data to derive.
|
||||
:param salt: the salt for the derivation.
|
||||
:param iterations: the number of iterations.
|
||||
:param keylen: the length of the resulting key. If not provided
|
||||
:param keylen: the length of the resulting key. If not provided,
|
||||
the digest size will be used.
|
||||
:param hashfunc: the hash function to use. This can either be the
|
||||
string name of a known hash function or a function
|
||||
string name of a known hash function, or a function
|
||||
from the hashlib module. Defaults to sha1.
|
||||
"""
|
||||
rv = pbkdf2_bin(data, salt, iterations, keylen, hashfunc)
|
||||
return to_native(codecs.encode(rv, 'hex_codec'))
|
||||
|
||||
|
||||
_has_native_pbkdf2 = hasattr(hashlib, 'pbkdf2_hmac')
|
||||
|
||||
|
||||
def pbkdf2_bin(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS,
|
||||
keylen=None, hashfunc=None):
|
||||
"""Returns a binary digest for the PBKDF2 hash algorithm of `data`
|
||||
with the given `salt`. It iterates `iterations` time and produces a
|
||||
key of `keylen` bytes. By default SHA-1 is used as hash function,
|
||||
with the given `salt`. It iterates `iterations` times and produces a
|
||||
key of `keylen` bytes. By default, SHA-1 is used as hash function;
|
||||
a different hashlib `hashfunc` can be provided.
|
||||
|
||||
.. versionadded:: 0.9
|
||||
@@ -87,10 +90,23 @@ def pbkdf2_bin(data, salt, iterations=DEFAULT_PBKDF2_ITERATIONS,
|
||||
hashfunc = _hash_funcs[hashfunc]
|
||||
elif not hashfunc:
|
||||
hashfunc = hashlib.sha1
|
||||
data = to_bytes(data)
|
||||
salt = to_bytes(salt)
|
||||
mac = hmac.HMAC(to_bytes(data), None, hashfunc)
|
||||
|
||||
# If we're on Python with pbkdf2_hmac we can try to use it for
|
||||
# compatible digests.
|
||||
if _has_native_pbkdf2:
|
||||
_test_hash = hashfunc()
|
||||
if hasattr(_test_hash, 'name') and \
|
||||
_test_hash.name in _hash_funcs:
|
||||
return hashlib.pbkdf2_hmac(_test_hash.name,
|
||||
data, salt, iterations,
|
||||
keylen)
|
||||
|
||||
mac = hmac.HMAC(data, None, hashfunc)
|
||||
if not keylen:
|
||||
keylen = mac.digest_size
|
||||
|
||||
def _pseudorandom(x, mac=mac):
|
||||
h = mac.copy()
|
||||
h.update(x)
|
||||
@@ -109,28 +125,36 @@ def safe_str_cmp(a, b):
|
||||
"""This function compares strings in somewhat constant time. This
|
||||
requires that the length of at least one string is known in advance.
|
||||
|
||||
Returns `True` if the two strings are equal or `False` if they are not.
|
||||
Returns `True` if the two strings are equal, or `False` if they are not.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
if isinstance(a, text_type):
|
||||
a = a.encode('utf-8')
|
||||
if isinstance(b, text_type):
|
||||
b = b.encode('utf-8')
|
||||
|
||||
if _builtin_safe_str_cmp is not None:
|
||||
return _builtin_safe_str_cmp(a, b)
|
||||
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
|
||||
rv = 0
|
||||
if isinstance(a, bytes) and isinstance(b, bytes) and not PY2:
|
||||
for x, y in izip(a, b):
|
||||
rv |= x ^ y
|
||||
else:
|
||||
if PY2:
|
||||
for x, y in izip(a, b):
|
||||
rv |= ord(x) ^ ord(y)
|
||||
else:
|
||||
for x, y in izip(a, b):
|
||||
rv |= x ^ y
|
||||
|
||||
return rv == 0
|
||||
|
||||
|
||||
def gen_salt(length):
|
||||
"""Generate a random string of SALT_CHARS with specified ``length``."""
|
||||
if length <= 0:
|
||||
raise ValueError('requested salt of length <= 0')
|
||||
raise ValueError('Salt length must be positive')
|
||||
return ''.join(_sys_rng.choice(SALT_CHARS) for _ in range_type(length))
|
||||
|
||||
|
||||
@@ -196,11 +220,11 @@ def generate_password_hash(password, method='pbkdf2:sha1', salt_length=8):
|
||||
pbkdf2:sha1:2000$salt$hash
|
||||
pbkdf2:sha1$salt$hash
|
||||
|
||||
:param password: the password to hash
|
||||
:param method: the hash method to use (one that hashlib supports), can
|
||||
optionally be in the format ``pbpdf2:<method>[:iterations]``
|
||||
:param password: the password to hash.
|
||||
:param method: the hash method to use (one that hashlib supports). Can
|
||||
optionally be in the format ``pbkdf2:<method>[:iterations]``
|
||||
to enable PBKDF2.
|
||||
:param salt_length: the length of the salt in letters
|
||||
:param salt_length: the length of the salt in letters.
|
||||
"""
|
||||
salt = method != 'plain' and gen_salt(salt_length) or ''
|
||||
h, actual_method = _hash_internal(method, salt, password)
|
||||
@@ -215,8 +239,8 @@ def check_password_hash(pwhash, password):
|
||||
Returns `True` if the password matched, `False` otherwise.
|
||||
|
||||
:param pwhash: a hashed string like returned by
|
||||
:func:`generate_password_hash`
|
||||
:param password: the plaintext password to compare against the hash
|
||||
:func:`generate_password_hash`.
|
||||
:param password: the plaintext password to compare against the hash.
|
||||
"""
|
||||
if pwhash.count('$') < 2:
|
||||
return False
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
instead of a simple start file.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
@@ -40,14 +40,26 @@ from __future__ import with_statement
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
import thread
|
||||
import ssl
|
||||
except ImportError:
|
||||
import _thread as thread
|
||||
class _SslDummy(object):
|
||||
def __getattr__(self, name):
|
||||
raise RuntimeError('SSL support unavailable')
|
||||
ssl = _SslDummy()
|
||||
|
||||
|
||||
def _get_openssl_crypto_module():
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
except ImportError:
|
||||
raise TypeError('Using ad-hoc certificates requires the pyOpenSSL '
|
||||
'library.')
|
||||
else:
|
||||
return crypto
|
||||
|
||||
|
||||
try:
|
||||
from SocketServer import ThreadingMixIn, ForkingMixIn
|
||||
@@ -56,15 +68,20 @@ except ImportError:
|
||||
from socketserver import ThreadingMixIn, ForkingMixIn
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
|
||||
# important: do not use relative imports here or python -m will break
|
||||
import werkzeug
|
||||
from werkzeug._internal import _log
|
||||
from werkzeug._compat import iteritems, PY2, reraise, text_type, \
|
||||
wsgi_encoding_dance
|
||||
from werkzeug._compat import PY2, reraise, wsgi_encoding_dance
|
||||
from werkzeug.urls import url_parse, url_unquote
|
||||
from werkzeug.exceptions import InternalServerError, BadRequest
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
|
||||
|
||||
LISTEN_QUEUE = 128
|
||||
can_open_by_fd = hasattr(socket, 'fromfd')
|
||||
|
||||
|
||||
class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
|
||||
"""A request handler that implements WSGI dispatching."""
|
||||
|
||||
@property
|
||||
@@ -88,8 +105,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
'wsgi.multithread': self.server.multithread,
|
||||
'wsgi.multiprocess': self.server.multiprocess,
|
||||
'wsgi.run_once': False,
|
||||
'werkzeug.server.shutdown':
|
||||
shutdown_server,
|
||||
'werkzeug.server.shutdown': shutdown_server,
|
||||
'SERVER_SOFTWARE': self.server_version,
|
||||
'REQUEST_METHOD': self.command,
|
||||
'SCRIPT_NAME': '',
|
||||
@@ -97,8 +113,8 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
'QUERY_STRING': wsgi_encoding_dance(request_url.query),
|
||||
'CONTENT_TYPE': self.headers.get('Content-Type', ''),
|
||||
'CONTENT_LENGTH': self.headers.get('Content-Length', ''),
|
||||
'REMOTE_ADDR': self.client_address[0],
|
||||
'REMOTE_PORT': self.client_address[1],
|
||||
'REMOTE_ADDR': self.address_string(),
|
||||
'REMOTE_PORT': self.port_integer(),
|
||||
'SERVER_NAME': self.server.server_address[0],
|
||||
'SERVER_PORT': str(self.server.server_address[1]),
|
||||
'SERVER_PROTOCOL': self.request_version
|
||||
@@ -109,7 +125,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
|
||||
environ[key] = value
|
||||
|
||||
if request_url.netloc:
|
||||
if request_url.scheme and request_url.netloc:
|
||||
environ['HTTP_HOST'] = request_url.netloc
|
||||
|
||||
return environ
|
||||
@@ -118,7 +134,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
if self.headers.get('Expect', '').lower().strip() == '100-continue':
|
||||
self.wfile.write(b'HTTP/1.1 100 Continue\r\n\r\n')
|
||||
|
||||
environ = self.make_environ()
|
||||
self.environ = environ = self.make_environ()
|
||||
headers_set = []
|
||||
headers_sent = []
|
||||
|
||||
@@ -145,7 +161,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
self.send_header('Date', self.date_time_string())
|
||||
self.end_headers()
|
||||
|
||||
assert type(data) is bytes, 'applications must write bytes'
|
||||
assert isinstance(data, bytes), 'applications must write bytes'
|
||||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
|
||||
@@ -249,6 +265,9 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
def address_string(self):
|
||||
return self.client_address[0]
|
||||
|
||||
def port_integer(self):
|
||||
return self.client_address[1]
|
||||
|
||||
def log_request(self, code='-', size='-'):
|
||||
self.log('info', '"%s" %s %s', self.requestline, code, size)
|
||||
|
||||
@@ -270,14 +289,14 @@ BaseRequestHandler = WSGIRequestHandler
|
||||
|
||||
def generate_adhoc_ssl_pair(cn=None):
|
||||
from random import random
|
||||
from OpenSSL import crypto
|
||||
crypto = _get_openssl_crypto_module()
|
||||
|
||||
# pretty damn sure that this is not actually accepted by anyone
|
||||
if cn is None:
|
||||
cn = '*'
|
||||
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(int(random() * sys.maxint))
|
||||
cert.set_serial_number(int(random() * sys.maxsize))
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365)
|
||||
|
||||
@@ -290,7 +309,7 @@ def generate_adhoc_ssl_pair(cn=None):
|
||||
issuer.O = 'Self-Signed'
|
||||
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 768)
|
||||
pkey.generate_key(crypto.TYPE_RSA, 1024)
|
||||
cert.set_pubkey(pkey)
|
||||
cert.sign(pkey, 'md5')
|
||||
|
||||
@@ -323,9 +342,9 @@ def make_ssl_devcert(base_path, host=None, cn=None):
|
||||
cert_file = base_path + '.crt'
|
||||
pkey_file = base_path + '.key'
|
||||
|
||||
with open(cert_file, 'w') as f:
|
||||
with open(cert_file, 'wb') as f:
|
||||
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
with open(pkey_file, 'w') as f:
|
||||
with open(pkey_file, 'wb') as f:
|
||||
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
|
||||
return cert_file, pkey_file
|
||||
@@ -333,48 +352,76 @@ def make_ssl_devcert(base_path, host=None, cn=None):
|
||||
|
||||
def generate_adhoc_ssl_context():
|
||||
"""Generates an adhoc SSL context for the development server."""
|
||||
from OpenSSL import SSL
|
||||
crypto = _get_openssl_crypto_module()
|
||||
import tempfile
|
||||
import atexit
|
||||
|
||||
cert, pkey = generate_adhoc_ssl_pair()
|
||||
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
||||
ctx.use_privatekey(pkey)
|
||||
ctx.use_certificate(cert)
|
||||
cert_handle, cert_file = tempfile.mkstemp()
|
||||
pkey_handle, pkey_file = tempfile.mkstemp()
|
||||
atexit.register(os.remove, pkey_file)
|
||||
atexit.register(os.remove, cert_file)
|
||||
|
||||
os.write(cert_handle, crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
os.write(pkey_handle, crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
os.close(cert_handle)
|
||||
os.close(pkey_handle)
|
||||
ctx = load_ssl_context(cert_file, pkey_file)
|
||||
return ctx
|
||||
|
||||
|
||||
def load_ssl_context(cert_file, pkey_file):
|
||||
"""Loads an SSL context from a certificate and private key file."""
|
||||
from OpenSSL import SSL
|
||||
ctx = SSL.Context(SSL.SSLv23_METHOD)
|
||||
ctx.use_certificate_file(cert_file)
|
||||
ctx.use_privatekey_file(pkey_file)
|
||||
def load_ssl_context(cert_file, pkey_file=None, protocol=None):
|
||||
"""Loads SSL context from cert/private key files and optional protocol.
|
||||
Many parameters are directly taken from the API of
|
||||
:py:class:`ssl.SSLContext`.
|
||||
|
||||
:param cert_file: Path of the certificate to use.
|
||||
:param pkey_file: Path of the private key to use. If not given, the key
|
||||
will be obtained from the certificate file.
|
||||
:param protocol: One of the ``PROTOCOL_*`` constants in the stdlib ``ssl``
|
||||
module. Defaults to ``PROTOCOL_SSLv23``.
|
||||
"""
|
||||
if protocol is None:
|
||||
protocol = ssl.PROTOCOL_SSLv23
|
||||
ctx = _SSLContext(protocol)
|
||||
ctx.load_cert_chain(cert_file, pkey_file)
|
||||
return ctx
|
||||
|
||||
|
||||
class _SSLContext(object):
|
||||
|
||||
'''A dummy class with a small subset of Python3's ``ssl.SSLContext``, only
|
||||
intended to be used with and by Werkzeug.'''
|
||||
|
||||
def __init__(self, protocol):
|
||||
self._protocol = protocol
|
||||
self._certfile = None
|
||||
self._keyfile = None
|
||||
self._password = None
|
||||
|
||||
def load_cert_chain(self, certfile, keyfile=None, password=None):
|
||||
self._certfile = certfile
|
||||
self._keyfile = keyfile or certfile
|
||||
self._password = password
|
||||
|
||||
def wrap_socket(self, sock, **kwargs):
|
||||
return ssl.wrap_socket(sock, keyfile=self._keyfile,
|
||||
certfile=self._certfile,
|
||||
ssl_version=self._protocol, **kwargs)
|
||||
|
||||
|
||||
def is_ssl_error(error=None):
|
||||
"""Checks if the given error (or the current one) is an SSL error."""
|
||||
exc_types = (ssl.SSLError,)
|
||||
try:
|
||||
from OpenSSL.SSL import Error
|
||||
exc_types += (Error,)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if error is None:
|
||||
error = sys.exc_info()[1]
|
||||
from OpenSSL import SSL
|
||||
return isinstance(error, SSL.Error)
|
||||
|
||||
|
||||
class _SSLConnectionFix(object):
|
||||
"""Wrapper around SSL connection to provide a working makefile()."""
|
||||
|
||||
def __init__(self, con):
|
||||
self._con = con
|
||||
|
||||
def makefile(self, mode, bufsize):
|
||||
return socket._fileobject(self._con, mode, bufsize)
|
||||
|
||||
def __getattr__(self, attrib):
|
||||
return getattr(self._con, attrib)
|
||||
|
||||
def shutdown(self, arg=None):
|
||||
try:
|
||||
self._con.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
return isinstance(error, exc_types)
|
||||
|
||||
|
||||
def select_ip_version(host, port):
|
||||
@@ -383,46 +430,62 @@ def select_ip_version(host, port):
|
||||
# and various operating systems. Probably this code also is
|
||||
# not supposed to work, but I can't come up with any other
|
||||
# ways to implement this.
|
||||
##try:
|
||||
## info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
||||
## socket.SOCK_STREAM, 0,
|
||||
## socket.AI_PASSIVE)
|
||||
## if info:
|
||||
## return info[0][0]
|
||||
##except socket.gaierror:
|
||||
## pass
|
||||
# try:
|
||||
# info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
||||
# socket.SOCK_STREAM, 0,
|
||||
# socket.AI_PASSIVE)
|
||||
# if info:
|
||||
# return info[0][0]
|
||||
# except socket.gaierror:
|
||||
# pass
|
||||
if ':' in host and hasattr(socket, 'AF_INET6'):
|
||||
return socket.AF_INET6
|
||||
return socket.AF_INET
|
||||
|
||||
|
||||
class BaseWSGIServer(HTTPServer, object):
|
||||
|
||||
"""Simple single-threaded, single-process WSGI server."""
|
||||
multithread = False
|
||||
multiprocess = False
|
||||
request_queue_size = 128
|
||||
request_queue_size = LISTEN_QUEUE
|
||||
|
||||
def __init__(self, host, port, app, handler=None,
|
||||
passthrough_errors=False, ssl_context=None):
|
||||
passthrough_errors=False, ssl_context=None, fd=None):
|
||||
if handler is None:
|
||||
handler = WSGIRequestHandler
|
||||
|
||||
self.address_family = select_ip_version(host, port)
|
||||
|
||||
if fd is not None:
|
||||
real_sock = socket.fromfd(fd, self.address_family,
|
||||
socket.SOCK_STREAM)
|
||||
port = 0
|
||||
HTTPServer.__init__(self, (host, int(port)), handler)
|
||||
self.app = app
|
||||
self.passthrough_errors = passthrough_errors
|
||||
self.shutdown_signal = False
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
# Patch in the original socket.
|
||||
if fd is not None:
|
||||
self.socket.close()
|
||||
self.socket = real_sock
|
||||
self.server_address = self.socket.getsockname()
|
||||
|
||||
if ssl_context is not None:
|
||||
try:
|
||||
from OpenSSL import tsafe
|
||||
except ImportError:
|
||||
raise TypeError('SSL is not available if the OpenSSL '
|
||||
'library is not installed.')
|
||||
if isinstance(ssl_context, tuple):
|
||||
ssl_context = load_ssl_context(*ssl_context)
|
||||
if ssl_context == 'adhoc':
|
||||
ssl_context = generate_adhoc_ssl_context()
|
||||
self.socket = tsafe.Connection(ssl_context, self.socket)
|
||||
# If we are on Python 2 the return value from socket.fromfd
|
||||
# is an internal socket object but what we need for ssl wrap
|
||||
# is the wrapper around it :(
|
||||
sock = self.socket
|
||||
if PY2 and not isinstance(sock, socket.socket):
|
||||
sock = socket.socket(sock.family, sock.type, sock.proto, sock)
|
||||
self.socket = ssl_context.wrap_socket(sock, server_side=True)
|
||||
self.ssl_context = ssl_context
|
||||
else:
|
||||
self.ssl_context = None
|
||||
@@ -436,39 +499,40 @@ class BaseWSGIServer(HTTPServer, object):
|
||||
HTTPServer.serve_forever(self)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.server_close()
|
||||
|
||||
def handle_error(self, request, client_address):
|
||||
if self.passthrough_errors:
|
||||
raise
|
||||
else:
|
||||
return HTTPServer.handle_error(self, request, client_address)
|
||||
return HTTPServer.handle_error(self, request, client_address)
|
||||
|
||||
def get_request(self):
|
||||
con, info = self.socket.accept()
|
||||
if self.ssl_context is not None:
|
||||
con = _SSLConnectionFix(con)
|
||||
return con, info
|
||||
|
||||
|
||||
class ThreadedWSGIServer(ThreadingMixIn, BaseWSGIServer):
|
||||
|
||||
"""A WSGI server that does threading."""
|
||||
multithread = True
|
||||
|
||||
|
||||
class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer):
|
||||
|
||||
"""A WSGI server that does forking."""
|
||||
multiprocess = True
|
||||
|
||||
def __init__(self, host, port, app, processes=40, handler=None,
|
||||
passthrough_errors=False, ssl_context=None):
|
||||
passthrough_errors=False, ssl_context=None, fd=None):
|
||||
BaseWSGIServer.__init__(self, host, port, app, handler,
|
||||
passthrough_errors, ssl_context)
|
||||
passthrough_errors, ssl_context, fd)
|
||||
self.max_children = processes
|
||||
|
||||
|
||||
def make_server(host, port, app=None, threaded=False, processes=1,
|
||||
def make_server(host=None, port=None, app=None, threaded=False, processes=1,
|
||||
request_handler=None, passthrough_errors=False,
|
||||
ssl_context=None):
|
||||
ssl_context=None, fd=None):
|
||||
"""Create a new server instance that is either threaded, or forks
|
||||
or just processes one request after another.
|
||||
"""
|
||||
@@ -477,156 +541,32 @@ def make_server(host, port, app=None, threaded=False, processes=1,
|
||||
"multi process server.")
|
||||
elif threaded:
|
||||
return ThreadedWSGIServer(host, port, app, request_handler,
|
||||
passthrough_errors, ssl_context)
|
||||
passthrough_errors, ssl_context, fd=fd)
|
||||
elif processes > 1:
|
||||
return ForkingWSGIServer(host, port, app, processes, request_handler,
|
||||
passthrough_errors, ssl_context)
|
||||
passthrough_errors, ssl_context, fd=fd)
|
||||
else:
|
||||
return BaseWSGIServer(host, port, app, request_handler,
|
||||
passthrough_errors, ssl_context)
|
||||
passthrough_errors, ssl_context, fd=fd)
|
||||
|
||||
|
||||
def _iter_module_files():
|
||||
# The list call is necessary on Python 3 in case the module
|
||||
# dictionary modifies during iteration.
|
||||
for module in list(sys.modules.values()):
|
||||
filename = getattr(module, '__file__', None)
|
||||
if filename:
|
||||
old = None
|
||||
while not os.path.isfile(filename):
|
||||
old = filename
|
||||
filename = os.path.dirname(filename)
|
||||
if filename == old:
|
||||
break
|
||||
else:
|
||||
if filename[-4:] in ('.pyc', '.pyo'):
|
||||
filename = filename[:-1]
|
||||
yield filename
|
||||
def is_running_from_reloader():
|
||||
"""Checks if the application is running from within the Werkzeug
|
||||
reloader subprocess.
|
||||
|
||||
|
||||
def _reloader_stat_loop(extra_files=None, interval=1):
|
||||
"""When this function is run from the main thread, it will force other
|
||||
threads to exit when any modules currently loaded change.
|
||||
|
||||
Copyright notice. This function is based on the autoreload.py from
|
||||
the CherryPy trac which originated from WSGIKit which is now dead.
|
||||
|
||||
:param extra_files: a list of additional files it should watch.
|
||||
.. versionadded:: 0.10
|
||||
"""
|
||||
from itertools import chain
|
||||
mtimes = {}
|
||||
while 1:
|
||||
for filename in chain(_iter_module_files(), extra_files or ()):
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
continue
|
||||
elif mtime > old_time:
|
||||
_log('info', ' * Detected change in %r, reloading' % filename)
|
||||
sys.exit(3)
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
def _reloader_inotify(extra_files=None, interval=None):
|
||||
# Mutated by inotify loop when changes occur.
|
||||
changed = [False]
|
||||
|
||||
# Setup inotify watches
|
||||
from pyinotify import WatchManager, Notifier
|
||||
|
||||
# this API changed at one point, support both
|
||||
try:
|
||||
from pyinotify import EventsCodes as ec
|
||||
ec.IN_ATTRIB
|
||||
except (ImportError, AttributeError):
|
||||
import pyinotify as ec
|
||||
|
||||
wm = WatchManager()
|
||||
mask = ec.IN_DELETE_SELF | ec.IN_MOVE_SELF | ec.IN_MODIFY | ec.IN_ATTRIB
|
||||
|
||||
def signal_changed(event):
|
||||
if changed[0]:
|
||||
return
|
||||
_log('info', ' * Detected change in %r, reloading' % event.path)
|
||||
changed[:] = [True]
|
||||
|
||||
for fname in extra_files or ():
|
||||
wm.add_watch(fname, mask, signal_changed)
|
||||
|
||||
# ... And now we wait...
|
||||
notif = Notifier(wm)
|
||||
try:
|
||||
while not changed[0]:
|
||||
# always reiterate through sys.modules, adding them
|
||||
for fname in _iter_module_files():
|
||||
wm.add_watch(fname, mask, signal_changed)
|
||||
notif.process_events()
|
||||
if notif.check_events(timeout=interval):
|
||||
notif.read_events()
|
||||
# TODO Set timeout to something small and check parent liveliness
|
||||
finally:
|
||||
notif.stop()
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
# currently we always use the stat loop reloader for the simple reason
|
||||
# that the inotify one does not respond to added files properly. Also
|
||||
# it's quite buggy and the API is a mess.
|
||||
reloader_loop = _reloader_stat_loop
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
"""Spawn a new Python interpreter with the same arguments as this one,
|
||||
but running the reloader thread.
|
||||
"""
|
||||
while 1:
|
||||
_log('info', ' * Restarting with reloader')
|
||||
args = [sys.executable] + sys.argv
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
|
||||
|
||||
# a weird bug on windows. sometimes unicode strings end up in the
|
||||
# environment and subprocess.call does not like this, encode them
|
||||
# to latin1 and continue.
|
||||
if os.name == 'nt' and PY2:
|
||||
for key, value in iteritems(new_environ):
|
||||
if isinstance(value, text_type):
|
||||
new_environ[key] = value.encode('iso-8859-1')
|
||||
|
||||
exit_code = subprocess.call(args, env=new_environ)
|
||||
if exit_code != 3:
|
||||
return exit_code
|
||||
|
||||
|
||||
def run_with_reloader(main_func, extra_files=None, interval=1):
|
||||
"""Run the given function in an independent python interpreter."""
|
||||
import signal
|
||||
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
thread.start_new_thread(main_func, ())
|
||||
try:
|
||||
reloader_loop(extra_files, interval)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
try:
|
||||
sys.exit(restart_with_reloader())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
|
||||
|
||||
|
||||
def run_simple(hostname, port, application, use_reloader=False,
|
||||
use_debugger=False, use_evalex=True,
|
||||
extra_files=None, reloader_interval=1, threaded=False,
|
||||
extra_files=None, reloader_interval=1,
|
||||
reloader_type='auto', threaded=False,
|
||||
processes=1, request_handler=None, static_files=None,
|
||||
passthrough_errors=False, ssl_context=None):
|
||||
"""Start an application using wsgiref and with an optional reloader. This
|
||||
wraps `wsgiref` to fix the wrong default reporting of the multithreaded
|
||||
WSGI variable and adds optional multithreading and fork support.
|
||||
"""Start a WSGI application. Optional features include a reloader,
|
||||
multithreading and fork support.
|
||||
|
||||
This function has a command-line interface too::
|
||||
|
||||
@@ -646,6 +586,11 @@ def run_simple(hostname, port, application, use_reloader=False,
|
||||
.. versionadded:: 0.9
|
||||
Added command-line interface.
|
||||
|
||||
.. versionadded:: 0.10
|
||||
Improved the reloader and added support for changing the backend
|
||||
through the `reloader_type` parameter. See :ref:`reloader`
|
||||
for more information.
|
||||
|
||||
:param hostname: The host for the application. eg: ``'localhost'``
|
||||
:param port: The port for the server. eg: ``8080``
|
||||
:param application: the WSGI application to execute
|
||||
@@ -657,6 +602,10 @@ def run_simple(hostname, port, application, use_reloader=False,
|
||||
additionally to the modules. For example configuration
|
||||
files.
|
||||
:param reloader_interval: the interval for the reloader in seconds.
|
||||
:param reloader_type: the type of reloader to use. The default is
|
||||
auto detection. Valid values are ``'stat'`` and
|
||||
``'watchdog'``. See :ref:`reloader` for more
|
||||
information.
|
||||
:param threaded: should the process handle each request in a separate
|
||||
thread?
|
||||
:param processes: if greater than 1 then handle each request in a new process
|
||||
@@ -673,11 +622,11 @@ def run_simple(hostname, port, application, use_reloader=False,
|
||||
:param passthrough_errors: set this to `True` to disable the error catching.
|
||||
This means that the server will die on errors but
|
||||
it can be useful to hook debuggers in (pdb etc.)
|
||||
:param ssl_context: an SSL context for the connection. Either an OpenSSL
|
||||
context, a tuple in the form ``(cert_file, pkey_file)``,
|
||||
the string ``'adhoc'`` if the server should
|
||||
automatically create one, or `None` to disable SSL
|
||||
(which is the default).
|
||||
:param ssl_context: an SSL context for the connection. Either an
|
||||
:class:`ssl.SSLContext`, a tuple in the form
|
||||
``(cert_file, pkey_file)``, the string ``'adhoc'`` if
|
||||
the server should automatically create one, or ``None``
|
||||
to disable SSL (which is the default).
|
||||
"""
|
||||
if use_debugger:
|
||||
from werkzeug.debug import DebuggedApplication
|
||||
@@ -686,29 +635,72 @@ def run_simple(hostname, port, application, use_reloader=False,
|
||||
from werkzeug.wsgi import SharedDataMiddleware
|
||||
application = SharedDataMiddleware(application, static_files)
|
||||
|
||||
def inner():
|
||||
make_server(hostname, port, application, threaded,
|
||||
processes, request_handler,
|
||||
passthrough_errors, ssl_context).serve_forever()
|
||||
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
|
||||
display_hostname = hostname != '*' and hostname or 'localhost'
|
||||
def log_startup(sock):
|
||||
display_hostname = hostname not in ('', '*') and hostname or 'localhost'
|
||||
if ':' in display_hostname:
|
||||
display_hostname = '[%s]' % display_hostname
|
||||
_log('info', ' * Running on %s://%s:%d/', ssl_context is None
|
||||
and 'http' or 'https', display_hostname, port)
|
||||
quit_msg = '(Press CTRL+C to quit)'
|
||||
port = sock.getsockname()[1]
|
||||
_log('info', ' * Running on %s://%s:%d/ %s',
|
||||
ssl_context is None and 'http' or 'https',
|
||||
display_hostname, port, quit_msg)
|
||||
|
||||
def inner():
|
||||
try:
|
||||
fd = int(os.environ['WERKZEUG_SERVER_FD'])
|
||||
except (LookupError, ValueError):
|
||||
fd = None
|
||||
srv = make_server(hostname, port, application, threaded,
|
||||
processes, request_handler,
|
||||
passthrough_errors, ssl_context,
|
||||
fd=fd)
|
||||
if fd is None:
|
||||
log_startup(srv.socket)
|
||||
srv.serve_forever()
|
||||
|
||||
if use_reloader:
|
||||
# Create and destroy a socket so that any exceptions are raised before
|
||||
# we spawn a separate Python interpreter and lose this ability.
|
||||
address_family = select_ip_version(hostname, port)
|
||||
test_socket = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
test_socket.bind((hostname, port))
|
||||
test_socket.close()
|
||||
run_with_reloader(inner, extra_files, reloader_interval)
|
||||
# If we're not running already in the subprocess that is the
|
||||
# reloader we want to open up a socket early to make sure the
|
||||
# port is actually available.
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
|
||||
if port == 0 and not can_open_by_fd:
|
||||
raise ValueError('Cannot bind to a random port with enabled '
|
||||
'reloader if the Python interpreter does '
|
||||
'not support socket opening by fd.')
|
||||
|
||||
# Create and destroy a socket so that any exceptions are
|
||||
# raised before we spawn a separate Python interpreter and
|
||||
# lose this ability.
|
||||
address_family = select_ip_version(hostname, port)
|
||||
s = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind((hostname, port))
|
||||
if hasattr(s, 'set_inheritable'):
|
||||
s.set_inheritable(True)
|
||||
|
||||
# If we can open the socket by file descriptor, then we can just
|
||||
# reuse this one and our socket will survive the restarts.
|
||||
if can_open_by_fd:
|
||||
os.environ['WERKZEUG_SERVER_FD'] = str(s.fileno())
|
||||
s.listen(LISTEN_QUEUE)
|
||||
log_startup(s)
|
||||
else:
|
||||
s.close()
|
||||
|
||||
from ._reloader import run_with_reloader
|
||||
run_with_reloader(inner, extra_files, reloader_interval,
|
||||
reloader_type)
|
||||
else:
|
||||
inner()
|
||||
|
||||
|
||||
def run_with_reloader(*args, **kwargs):
|
||||
# People keep using undocumented APIs. Do not use this function
|
||||
# please, we do not guarantee that it continues working.
|
||||
from ._reloader import run_with_reloader
|
||||
return run_with_reloader(*args, **kwargs)
|
||||
|
||||
|
||||
def main():
|
||||
'''A simple command-line interface for :py:func:`run_simple`.'''
|
||||
|
||||
@@ -716,7 +708,8 @@ def main():
|
||||
import optparse
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
parser = optparse.OptionParser(usage='Usage: %prog [options] app_module:app_object')
|
||||
parser = optparse.OptionParser(
|
||||
usage='Usage: %prog [options] app_module:app_object')
|
||||
parser.add_option('-b', '--bind', dest='address',
|
||||
help='The hostname:port the app should listen on.')
|
||||
parser.add_option('-d', '--debug', dest='use_debugger',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module implements a client to WSGI applications for testing.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
@@ -22,20 +22,20 @@ except ImportError:
|
||||
from urllib.request import Request as U2Request
|
||||
try:
|
||||
from http.cookiejar import CookieJar
|
||||
except ImportError: # Py2
|
||||
except ImportError: # Py2
|
||||
from cookielib import CookieJar
|
||||
|
||||
from werkzeug._compat import iterlists, iteritems, itervalues, to_native, \
|
||||
string_types, text_type, reraise, wsgi_encoding_dance, \
|
||||
make_literal_wrapper
|
||||
from werkzeug._compat import iterlists, iteritems, itervalues, to_bytes, \
|
||||
string_types, text_type, reraise, wsgi_encoding_dance, \
|
||||
make_literal_wrapper
|
||||
from werkzeug._internal import _empty_stream, _get_environ
|
||||
from werkzeug.wrappers import BaseRequest
|
||||
from werkzeug.urls import url_encode, url_fix, iri_to_uri, url_unquote, \
|
||||
url_unparse, url_parse
|
||||
url_unparse, url_parse
|
||||
from werkzeug.wsgi import get_host, get_current_url, ClosingIterator
|
||||
from werkzeug.utils import dump_cookie
|
||||
from werkzeug.datastructures import FileMultiDict, MultiDict, \
|
||||
CombinedMultiDict, Headers, FileStorage
|
||||
CombinedMultiDict, Headers, FileStorage
|
||||
|
||||
|
||||
def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
|
||||
@@ -97,11 +97,12 @@ def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
|
||||
break
|
||||
write_binary(chunk)
|
||||
else:
|
||||
if isinstance(value, string_types):
|
||||
value = to_native(value, charset)
|
||||
else:
|
||||
if not isinstance(value, string_types):
|
||||
value = str(value)
|
||||
write('\r\n\r\n' + value)
|
||||
|
||||
value = to_bytes(value, charset)
|
||||
write('\r\n\r\n')
|
||||
write_binary(value)
|
||||
write('\r\n')
|
||||
write('--%s--\r\n' % boundary)
|
||||
|
||||
@@ -128,6 +129,7 @@ def File(fd, filename=None, mimetype=None):
|
||||
|
||||
|
||||
class _TestCookieHeaders(object):
|
||||
|
||||
"""A headers adapter for cookielib
|
||||
"""
|
||||
|
||||
@@ -151,6 +153,7 @@ class _TestCookieHeaders(object):
|
||||
|
||||
|
||||
class _TestCookieResponse(object):
|
||||
|
||||
"""Something that looks like a httplib.HTTPResponse, but is actually just an
|
||||
adapter for our test responses to make them available for cookielib.
|
||||
"""
|
||||
@@ -163,6 +166,7 @@ class _TestCookieResponse(object):
|
||||
|
||||
|
||||
class _TestCookieJar(CookieJar):
|
||||
|
||||
"""A cookielib.CookieJar modified to inject and read cookie headers from
|
||||
and to wsgi environments, and wsgi application responses.
|
||||
"""
|
||||
@@ -206,6 +210,7 @@ def _iter_data(data):
|
||||
|
||||
|
||||
class EnvironBuilder(object):
|
||||
|
||||
"""This class can be used to conveniently create a WSGI environment
|
||||
for testing purposes. It can be used to quickly create WSGI environments
|
||||
or request objects from arbitrary data.
|
||||
@@ -303,7 +308,8 @@ class EnvironBuilder(object):
|
||||
elif not isinstance(headers, Headers):
|
||||
headers = Headers(headers)
|
||||
self.headers = headers
|
||||
self.content_type = content_type
|
||||
if content_type is not None:
|
||||
self.content_type = content_type
|
||||
if errors_stream is None:
|
||||
errors_stream = sys.stderr
|
||||
self.errors_stream = errors_stream
|
||||
@@ -377,9 +383,9 @@ class EnvironBuilder(object):
|
||||
def _get_content_type(self):
|
||||
ct = self.headers.get('Content-Type')
|
||||
if ct is None and not self._input_stream:
|
||||
if self.method in ('POST', 'PUT', 'PATCH'):
|
||||
if self._files:
|
||||
return 'multipart/form-data'
|
||||
if self._files:
|
||||
return 'multipart/form-data'
|
||||
elif self._form:
|
||||
return 'application/x-www-form-urlencoded'
|
||||
return None
|
||||
return ct
|
||||
@@ -413,6 +419,7 @@ class EnvironBuilder(object):
|
||||
|
||||
def form_property(name, storage, doc):
|
||||
key = '_' + name
|
||||
|
||||
def getter(self):
|
||||
if self._input_stream is not None:
|
||||
raise AttributeError('an input stream is defined')
|
||||
@@ -420,7 +427,9 @@ class EnvironBuilder(object):
|
||||
if rv is None:
|
||||
rv = storage()
|
||||
setattr(self, key, rv)
|
||||
|
||||
return rv
|
||||
|
||||
def setter(self, value):
|
||||
self._input_stream = None
|
||||
setattr(self, key, value)
|
||||
@@ -534,7 +543,7 @@ class EnvironBuilder(object):
|
||||
stream_encode_multipart(values, charset=self.charset)
|
||||
content_type += '; boundary="%s"' % boundary
|
||||
elif content_type == 'application/x-www-form-urlencoded':
|
||||
#py2v3 review
|
||||
# XXX: py2v3 review
|
||||
values = url_encode(self.form, charset=self.charset)
|
||||
values = values.encode('ascii')
|
||||
content_length = len(values)
|
||||
@@ -588,6 +597,7 @@ class EnvironBuilder(object):
|
||||
|
||||
|
||||
class ClientRedirectError(Exception):
|
||||
|
||||
"""
|
||||
If a redirect loop is detected when using follow_redirects=True with
|
||||
the :cls:`Client`, then this exception is raised.
|
||||
@@ -595,6 +605,7 @@ class ClientRedirectError(Exception):
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
"""This class allows to send requests to a wrapped application.
|
||||
|
||||
The response wrapper can be a class or factory function that takes
|
||||
@@ -678,6 +689,12 @@ class Client(object):
|
||||
raise RuntimeError('%r does not support redirect to '
|
||||
'external targets' % self.__class__)
|
||||
|
||||
status_code = int(response[1].split(None, 1)[0])
|
||||
if status_code == 307:
|
||||
method = environ['REQUEST_METHOD']
|
||||
else:
|
||||
method = 'GET'
|
||||
|
||||
# For redirect handling we temporarily disable the response
|
||||
# wrapper. This is not threadsafe but not a real concern
|
||||
# since the test client must not be shared anyways.
|
||||
@@ -686,7 +703,7 @@ class Client(object):
|
||||
try:
|
||||
return self.open(path=script_root, base_url=base_url,
|
||||
query_string=qs, as_tuple=True,
|
||||
buffered=buffered)
|
||||
buffered=buffered, method=method)
|
||||
finally:
|
||||
self.response_wrapper = old_response_wrapper
|
||||
|
||||
@@ -746,7 +763,8 @@ class Client(object):
|
||||
raise ClientRedirectError('loop detected')
|
||||
redirect_chain.append(new_redirect_entry)
|
||||
environ, response = self.resolve_redirect(response, new_location,
|
||||
environ, buffered=buffered)
|
||||
environ,
|
||||
buffered=buffered)
|
||||
|
||||
if self.response_wrapper is not None:
|
||||
response = self.response_wrapper(*response)
|
||||
@@ -850,29 +868,29 @@ def run_wsgi_app(app, environ, buffered=False):
|
||||
response[:] = [status, headers]
|
||||
return buffer.append
|
||||
|
||||
app_iter = app(environ, start_response)
|
||||
app_rv = app(environ, start_response)
|
||||
close_func = getattr(app_rv, 'close', None)
|
||||
app_iter = iter(app_rv)
|
||||
|
||||
# when buffering we emit the close call early and convert the
|
||||
# application iterator into a regular list
|
||||
if buffered:
|
||||
close_func = getattr(app_iter, 'close', None)
|
||||
try:
|
||||
app_iter = list(app_iter)
|
||||
finally:
|
||||
if close_func is not None:
|
||||
close_func()
|
||||
|
||||
# otherwise we iterate the application iter until we have
|
||||
# a response, chain the already received data with the already
|
||||
# collected data and wrap it in a new `ClosingIterator` if
|
||||
# we have a close callable.
|
||||
# otherwise we iterate the application iter until we have a response, chain
|
||||
# the already received data with the already collected data and wrap it in
|
||||
# a new `ClosingIterator` if we need to restore a `close` callable from the
|
||||
# original return value.
|
||||
else:
|
||||
while not response:
|
||||
buffer.append(next(app_iter))
|
||||
if buffer:
|
||||
close_func = getattr(app_iter, 'close', None)
|
||||
app_iter = chain(buffer, app_iter)
|
||||
if close_func is not None:
|
||||
app_iter = ClosingIterator(app_iter, close_func)
|
||||
if close_func is not None and app_iter is not app_rv:
|
||||
app_iter = ClosingIterator(app_iter, close_func)
|
||||
|
||||
return app_iter, response[0], Headers(response[1])
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Provide a small test application that can be used to test a WSGI server
|
||||
and check it for WSGI compliance.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
@@ -17,8 +17,8 @@ from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response
|
||||
from werkzeug.utils import escape
|
||||
import base64
|
||||
|
||||
logo = Response(base64.b64decode(
|
||||
'''R0lGODlhoACgAOMIAAEDACwpAEpCAGdgAJaKAM28AOnVAP3rAP/////////
|
||||
logo = Response(base64.b64decode('''
|
||||
R0lGODlhoACgAOMIAAEDACwpAEpCAGdgAJaKAM28AOnVAP3rAP/////////
|
||||
//////////////////////yH5BAEKAAgALAAAAACgAKAAAAT+EMlJq704680R+F0ojmRpnuj0rWnrv
|
||||
nB8rbRs33gu0bzu/0AObxgsGn3D5HHJbCUFyqZ0ukkSDlAidctNFg7gbI9LZlrBaHGtzAae0eloe25
|
||||
7w9EDOX2fst/xenyCIn5/gFqDiVVDV4aGeYiKkhSFjnCQY5OTlZaXgZp8nJ2ekaB0SQOjqphrpnOiq
|
||||
@@ -147,7 +147,7 @@ def iter_sys_path():
|
||||
for item in sys.path:
|
||||
path = os.path.join(cwd, item or os.path.curdir)
|
||||
yield strip(os.path.normpath(path)), \
|
||||
not os.path.isdir(path), path != item
|
||||
not os.path.isdir(path), path != item
|
||||
|
||||
|
||||
def render_testapp(req):
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Contains all test Werkzeug tests.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
import shutil
|
||||
import tempfile
|
||||
import atexit
|
||||
|
||||
from werkzeug.utils import find_modules
|
||||
from werkzeug._compat import text_type, integer_types, reraise
|
||||
|
||||
|
||||
def get_temporary_directory():
|
||||
directory = tempfile.mkdtemp()
|
||||
@atexit.register
|
||||
def remove_directory():
|
||||
try:
|
||||
shutil.rmtree(directory)
|
||||
except EnvironmentError:
|
||||
pass
|
||||
return directory
|
||||
|
||||
|
||||
def iter_suites(package):
|
||||
"""Yields all testsuites."""
|
||||
for module in find_modules(package, include_packages=True):
|
||||
mod = __import__(module, fromlist=['*'])
|
||||
if hasattr(mod, 'suite'):
|
||||
yield mod.suite()
|
||||
|
||||
|
||||
def find_all_tests(suite):
|
||||
"""Yields all the tests and their names from a given suite."""
|
||||
suites = [suite]
|
||||
while suites:
|
||||
s = suites.pop()
|
||||
try:
|
||||
suites.extend(s)
|
||||
except TypeError:
|
||||
yield s, '%s.%s.%s' % (
|
||||
s.__class__.__module__,
|
||||
s.__class__.__name__,
|
||||
s._testMethodName
|
||||
)
|
||||
|
||||
|
||||
class WerkzeugTestCase(unittest.TestCase):
|
||||
"""Baseclass for all the tests that Werkzeug uses. Use these
|
||||
methods for testing instead of the camelcased ones in the
|
||||
baseclass for consistency.
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def setUp(self):
|
||||
self.setup()
|
||||
|
||||
def tearDown(self):
|
||||
unittest.TestCase.tearDown(self)
|
||||
self.teardown()
|
||||
|
||||
def assert_line_equal(self, x, y):
|
||||
assert x == y, "lines not equal\n a = %r\n b = %r" % (x, y)
|
||||
|
||||
def assert_equal(self, x, y, msg=None):
|
||||
return self.assertEqual(x, y, msg)
|
||||
|
||||
def assert_not_equal(self, x, y):
|
||||
return self.assertNotEqual(x, y)
|
||||
|
||||
def assert_raises(self, exc_type, callable=None, *args, **kwargs):
|
||||
catcher = _ExceptionCatcher(self, exc_type)
|
||||
if callable is None:
|
||||
return catcher
|
||||
with catcher:
|
||||
callable(*args, **kwargs)
|
||||
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
def assertIsNone(self, x):
|
||||
assert x is None, "%r is not None" % (x,)
|
||||
|
||||
def assertIsNotNone(self, x):
|
||||
assert x is not None, "%r is None" % (x, )
|
||||
|
||||
def assertIn(self, x, y):
|
||||
assert x in y, "%r not in %r" % (x, y)
|
||||
|
||||
def assertNotIn(self, x, y):
|
||||
assert x not in y, "%r in %r" % (x, y)
|
||||
|
||||
def assertIsInstance(self, x, y):
|
||||
assert isinstance(x, y), "not isinstance(%r, %r)" % (x, y)
|
||||
|
||||
def assertIs(self, x, y):
|
||||
assert x is y, "%r is not %r" % (x, y)
|
||||
|
||||
def assertIsNot(self, x, y):
|
||||
assert x is not y, "%r is %r" % (x, y)
|
||||
|
||||
def assertSequenceEqual(self, x, y):
|
||||
self.assertEqual(x, y)
|
||||
|
||||
def assertRaisesRegex(self, exc_type, regex, *args, **kwargs):
|
||||
catcher = _ExceptionCatcher(self, exc_type)
|
||||
if not args:
|
||||
return catcher
|
||||
elif callable(args[0]):
|
||||
with catcher:
|
||||
args[0](*args[1:], **kwargs)
|
||||
if args[0] is not None:
|
||||
assert re.search(args[0], catcher.exc_value[0])
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
elif sys.version_info[0] == 2:
|
||||
def assertRaisesRegex(self, *args, **kwargs):
|
||||
return self.assertRaisesRegexp(*args, **kwargs)
|
||||
|
||||
def assert_is_none(self, x):
|
||||
self.assertIsNone(x)
|
||||
|
||||
def assert_is_not_none(self, x):
|
||||
self.assertIsNotNone(x)
|
||||
|
||||
def assert_in(self, x, y):
|
||||
self.assertIn(x, y)
|
||||
|
||||
def assert_is_instance(self, x, y):
|
||||
self.assertIsInstance(x, y)
|
||||
|
||||
def assert_not_in(self, x, y):
|
||||
self.assertNotIn(x, y)
|
||||
|
||||
def assert_is(self, x, y):
|
||||
self.assertIs(x, y)
|
||||
|
||||
def assert_is_not(self, x, y):
|
||||
self.assertIsNot(x, y)
|
||||
|
||||
def assert_true(self, x):
|
||||
self.assertTrue(x)
|
||||
|
||||
def assert_false(self, x):
|
||||
self.assertFalse(x)
|
||||
|
||||
def assert_raises_regex(self, *args, **kwargs):
|
||||
return self.assertRaisesRegex(*args, **kwargs)
|
||||
|
||||
def assert_sequence_equal(self, x, y):
|
||||
self.assertSequenceEqual(x, y)
|
||||
|
||||
def assert_strict_equal(self, x, y):
|
||||
'''Stricter version of assert_equal that doesn't do implicit conversion
|
||||
between unicode and strings'''
|
||||
self.assert_equal(x, y)
|
||||
assert issubclass(type(x), type(y)) or issubclass(type(y), type(x)), \
|
||||
'%s != %s' % (type(x), type(y))
|
||||
if isinstance(x, (bytes, text_type, integer_types)) or x is None:
|
||||
return
|
||||
elif isinstance(x, dict) or isinstance(y, dict):
|
||||
x = sorted(x.items())
|
||||
y = sorted(y.items())
|
||||
elif isinstance(x, set) or isinstance(y, set):
|
||||
x = sorted(x)
|
||||
y = sorted(y)
|
||||
rx, ry = repr(x), repr(y)
|
||||
if rx != ry:
|
||||
rx = rx[:200] + (rx[200:] and '...')
|
||||
ry = ry[:200] + (ry[200:] and '...')
|
||||
raise AssertionError(rx, ry)
|
||||
assert repr(x) == repr(y), repr((x, y))[:200]
|
||||
|
||||
|
||||
class _ExceptionCatcher(object):
|
||||
|
||||
def __init__(self, test_case, exc_type):
|
||||
self.test_case = test_case
|
||||
self.exc_type = exc_type
|
||||
self.exc_value = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
exception_name = self.exc_type.__name__
|
||||
if exc_type is None:
|
||||
self.test_case.fail('Expected exception of type %r' %
|
||||
exception_name)
|
||||
elif not issubclass(exc_type, self.exc_type):
|
||||
reraise(exc_type, exc_value, tb)
|
||||
self.exc_value = exc_value
|
||||
return True
|
||||
|
||||
|
||||
class BetterLoader(unittest.TestLoader):
|
||||
"""A nicer loader that solves two problems. First of all we are setting
|
||||
up tests from different sources and we're doing this programmatically
|
||||
which breaks the default loading logic so this is required anyways.
|
||||
Secondly this loader has a nicer interpolation for test names than the
|
||||
default one so you can just do ``run-tests.py ViewTestCase`` and it
|
||||
will work.
|
||||
"""
|
||||
|
||||
def getRootSuite(self):
|
||||
return suite()
|
||||
|
||||
def loadTestsFromName(self, name, module=None):
|
||||
root = self.getRootSuite()
|
||||
if name == 'suite':
|
||||
return root
|
||||
|
||||
all_tests = []
|
||||
for testcase, testname in find_all_tests(root):
|
||||
if testname == name or \
|
||||
testname.endswith('.' + name) or \
|
||||
('.' + name + '.') in testname or \
|
||||
testname.startswith(name + '.'):
|
||||
all_tests.append(testcase)
|
||||
|
||||
if not all_tests:
|
||||
raise LookupError('could not find test case for "%s"' % name)
|
||||
|
||||
if len(all_tests) == 1:
|
||||
return all_tests[0]
|
||||
rv = unittest.TestSuite()
|
||||
for test in all_tests:
|
||||
rv.addTest(test)
|
||||
return rv
|
||||
|
||||
|
||||
def suite():
|
||||
"""A testsuite that has all the Flask tests. You can use this
|
||||
function to integrate the Flask tests into your own testsuite
|
||||
in case you want to test that monkeypatches to Flask do not
|
||||
break it.
|
||||
"""
|
||||
suite = unittest.TestSuite()
|
||||
for other_suite in iter_suites(__name__):
|
||||
suite.addTest(other_suite)
|
||||
return suite
|
||||
|
||||
|
||||
def main():
|
||||
"""Runs the testsuite as command line application."""
|
||||
try:
|
||||
unittest.main(testLoader=BetterLoader(), defaultTest='suite')
|
||||
except Exception:
|
||||
import sys
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
@@ -1,40 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.compat
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Ensure that old stuff does not break on update.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
import warnings
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.test import create_environ
|
||||
|
||||
|
||||
class CompatTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_old_imports(self):
|
||||
from werkzeug.utils import Headers, MultiDict, CombinedMultiDict, \
|
||||
Headers, EnvironHeaders
|
||||
from werkzeug.http import Accept, MIMEAccept, CharsetAccept, \
|
||||
LanguageAccept, ETags, HeaderSet, WWWAuthenticate, \
|
||||
Authorization
|
||||
|
||||
def test_exposed_werkzeug_mod(self):
|
||||
import werkzeug
|
||||
for key in werkzeug.__all__:
|
||||
# deprecated, skip it
|
||||
if key in ('templates', 'Template'):
|
||||
continue
|
||||
getattr(werkzeug, key)
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(CompatTestCase))
|
||||
return suite
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.contrib
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the contrib modules.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
from werkzeug.testsuite import iter_suites
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
for other_suite in iter_suites(__name__):
|
||||
suite.addTest(other_suite)
|
||||
return suite
|
||||
@@ -1,171 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.cache
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the cache system
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.contrib import cache
|
||||
|
||||
try:
|
||||
import redis
|
||||
try:
|
||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||
cache.RedisCache(key_prefix='werkzeug-test-case:')._client.set('test','connection')
|
||||
except RedisConnectionError:
|
||||
redis = None
|
||||
except ImportError:
|
||||
redis = None
|
||||
|
||||
|
||||
class SimpleCacheTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_get_dict(self):
|
||||
c = cache.SimpleCache()
|
||||
c.set('a', 'a')
|
||||
c.set('b', 'b')
|
||||
d = c.get_dict('a', 'b')
|
||||
assert 'a' in d
|
||||
assert 'a' == d['a']
|
||||
assert 'b' in d
|
||||
assert 'b' == d['b']
|
||||
|
||||
def test_set_many(self):
|
||||
c = cache.SimpleCache()
|
||||
c.set_many({0: 0, 1: 1, 2: 4})
|
||||
assert c.get(2) == 4
|
||||
c.set_many((i, i*i) for i in range(3))
|
||||
assert c.get(2) == 4
|
||||
|
||||
|
||||
class FileSystemCacheTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_set_get(self):
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
c = cache.FileSystemCache(cache_dir=tmp_dir)
|
||||
for i in range(3):
|
||||
c.set(str(i), i * i)
|
||||
for i in range(3):
|
||||
result = c.get(str(i))
|
||||
assert result == i * i
|
||||
finally:
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
def test_filesystemcache_prune(self):
|
||||
THRESHOLD = 13
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
c = cache.FileSystemCache(cache_dir=tmp_dir, threshold=THRESHOLD)
|
||||
for i in range(2 * THRESHOLD):
|
||||
c.set(str(i), i)
|
||||
cache_files = os.listdir(tmp_dir)
|
||||
shutil.rmtree(tmp_dir)
|
||||
assert len(cache_files) <= THRESHOLD
|
||||
|
||||
|
||||
def test_filesystemcache_clear(self):
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
c = cache.FileSystemCache(cache_dir=tmp_dir)
|
||||
c.set('foo', 'bar')
|
||||
cache_files = os.listdir(tmp_dir)
|
||||
assert len(cache_files) == 1
|
||||
c.clear()
|
||||
cache_files = os.listdir(tmp_dir)
|
||||
assert len(cache_files) == 0
|
||||
shutil.rmtree(tmp_dir)
|
||||
|
||||
|
||||
class RedisCacheTestCase(WerkzeugTestCase):
|
||||
|
||||
def make_cache(self):
|
||||
return cache.RedisCache(key_prefix='werkzeug-test-case:')
|
||||
|
||||
def teardown(self):
|
||||
self.make_cache().clear()
|
||||
|
||||
def test_compat(self):
|
||||
c = self.make_cache()
|
||||
c._client.set(c.key_prefix + 'foo', 'Awesome')
|
||||
self.assert_equal(c.get('foo'), 'Awesome')
|
||||
c._client.set(c.key_prefix + 'foo', '42')
|
||||
self.assert_equal(c.get('foo'), 42)
|
||||
|
||||
def test_get_set(self):
|
||||
c = self.make_cache()
|
||||
c.set('foo', ['bar'])
|
||||
assert c.get('foo') == ['bar']
|
||||
|
||||
def test_get_many(self):
|
||||
c = self.make_cache()
|
||||
c.set('foo', ['bar'])
|
||||
c.set('spam', 'eggs')
|
||||
assert c.get_many('foo', 'spam') == [['bar'], 'eggs']
|
||||
|
||||
def test_set_many(self):
|
||||
c = self.make_cache()
|
||||
c.set_many({'foo': 'bar', 'spam': ['eggs']})
|
||||
assert c.get('foo') == 'bar'
|
||||
assert c.get('spam') == ['eggs']
|
||||
|
||||
def test_expire(self):
|
||||
c = self.make_cache()
|
||||
c.set('foo', 'bar', 1)
|
||||
time.sleep(2)
|
||||
assert c.get('foo') is None
|
||||
|
||||
def test_add(self):
|
||||
c = self.make_cache()
|
||||
# sanity check that add() works like set()
|
||||
c.add('foo', 'bar')
|
||||
assert c.get('foo') == 'bar'
|
||||
c.add('foo', 'qux')
|
||||
assert c.get('foo') == 'bar'
|
||||
|
||||
def test_delete(self):
|
||||
c = self.make_cache()
|
||||
c.add('foo', 'bar')
|
||||
assert c.get('foo') == 'bar'
|
||||
c.delete('foo')
|
||||
assert c.get('foo') is None
|
||||
|
||||
def test_delete_many(self):
|
||||
c = self.make_cache()
|
||||
c.add('foo', 'bar')
|
||||
c.add('spam', 'eggs')
|
||||
c.delete_many('foo', 'spam')
|
||||
assert c.get('foo') is None
|
||||
assert c.get('spam') is None
|
||||
|
||||
def test_inc_dec(self):
|
||||
c = self.make_cache()
|
||||
c.set('foo', 1)
|
||||
assert c.inc('foo') == 2
|
||||
assert c.dec('foo') == 1
|
||||
c.delete('foo')
|
||||
|
||||
|
||||
def test_true_false(self):
|
||||
c = self.make_cache()
|
||||
c.set('foo', True)
|
||||
assert c.get('foo') == True
|
||||
c.set('bar', False)
|
||||
assert c.get('bar') == False
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(SimpleCacheTestCase))
|
||||
suite.addTest(unittest.makeSuite(FileSystemCacheTestCase))
|
||||
if redis is not None:
|
||||
suite.addTest(unittest.makeSuite(RedisCacheTestCase))
|
||||
return suite
|
||||
@@ -1,193 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.fixers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Server / Browser fixers.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.datastructures import ResponseCacheControl
|
||||
from werkzeug.http import parse_cache_control_header
|
||||
|
||||
from werkzeug.test import create_environ, Client
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.contrib import fixers
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
|
||||
@Request.application
|
||||
def path_check_app(request):
|
||||
return Response('PATH_INFO: %s\nSCRIPT_NAME: %s' % (
|
||||
request.environ.get('PATH_INFO', ''),
|
||||
request.environ.get('SCRIPT_NAME', '')
|
||||
))
|
||||
|
||||
|
||||
class ServerFixerTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_cgi_root_fix(self):
|
||||
app = fixers.CGIRootFix(path_check_app)
|
||||
response = Response.from_app(app, dict(create_environ(),
|
||||
SCRIPT_NAME='/foo',
|
||||
PATH_INFO='/bar',
|
||||
SERVER_SOFTWARE='lighttpd/1.4.27'
|
||||
))
|
||||
self.assert_equal(response.get_data(),
|
||||
b'PATH_INFO: /foo/bar\nSCRIPT_NAME: ')
|
||||
|
||||
def test_cgi_root_fix_custom_app_root(self):
|
||||
app = fixers.CGIRootFix(path_check_app, app_root='/baz/poop/')
|
||||
response = Response.from_app(app, dict(create_environ(),
|
||||
SCRIPT_NAME='/foo',
|
||||
PATH_INFO='/bar'
|
||||
))
|
||||
self.assert_equal(response.get_data(), b'PATH_INFO: /foo/bar\nSCRIPT_NAME: baz/poop')
|
||||
|
||||
def test_path_info_from_request_uri_fix(self):
|
||||
app = fixers.PathInfoFromRequestUriFix(path_check_app)
|
||||
for key in 'REQUEST_URI', 'REQUEST_URL', 'UNENCODED_URL':
|
||||
env = dict(create_environ(), SCRIPT_NAME='/test', PATH_INFO='/?????')
|
||||
env[key] = '/test/foo%25bar?drop=this'
|
||||
response = Response.from_app(app, env)
|
||||
self.assert_equal(response.get_data(), b'PATH_INFO: /foo%bar\nSCRIPT_NAME: /test')
|
||||
|
||||
def test_proxy_fix(self):
|
||||
@Request.application
|
||||
def app(request):
|
||||
return Response('%s|%s' % (
|
||||
request.remote_addr,
|
||||
# do not use request.host as this fixes too :)
|
||||
request.environ['HTTP_HOST']
|
||||
))
|
||||
app = fixers.ProxyFix(app, num_proxies=2)
|
||||
environ = dict(create_environ(),
|
||||
HTTP_X_FORWARDED_PROTO="https",
|
||||
HTTP_X_FORWARDED_HOST='example.com',
|
||||
HTTP_X_FORWARDED_FOR='1.2.3.4, 5.6.7.8',
|
||||
REMOTE_ADDR='127.0.0.1',
|
||||
HTTP_HOST='fake'
|
||||
)
|
||||
|
||||
response = Response.from_app(app, environ)
|
||||
|
||||
self.assert_equal(response.get_data(), b'1.2.3.4|example.com')
|
||||
|
||||
# And we must check that if it is a redirection it is
|
||||
# correctly done:
|
||||
|
||||
redirect_app = redirect('/foo/bar.hml')
|
||||
response = Response.from_app(redirect_app, environ)
|
||||
|
||||
wsgi_headers = response.get_wsgi_headers(environ)
|
||||
assert wsgi_headers['Location'] == 'https://example.com/foo/bar.hml'
|
||||
|
||||
def test_proxy_fix_weird_enum(self):
|
||||
@fixers.ProxyFix
|
||||
@Request.application
|
||||
def app(request):
|
||||
return Response(request.remote_addr)
|
||||
environ = dict(create_environ(),
|
||||
HTTP_X_FORWARDED_FOR=',',
|
||||
REMOTE_ADDR='127.0.0.1',
|
||||
)
|
||||
|
||||
response = Response.from_app(app, environ)
|
||||
self.assert_strict_equal(response.get_data(), b'127.0.0.1')
|
||||
|
||||
def test_header_rewriter_fix(self):
|
||||
@Request.application
|
||||
def application(request):
|
||||
return Response("", headers=[
|
||||
('X-Foo', 'bar')
|
||||
])
|
||||
application = fixers.HeaderRewriterFix(application, ('X-Foo',), (('X-Bar', '42'),))
|
||||
response = Response.from_app(application, create_environ())
|
||||
assert response.headers['Content-Type'] == 'text/plain; charset=utf-8'
|
||||
assert 'X-Foo' not in response.headers
|
||||
assert response.headers['X-Bar'] == '42'
|
||||
|
||||
|
||||
class BrowserFixerTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_ie_fixes(self):
|
||||
@fixers.InternetExplorerFix
|
||||
@Request.application
|
||||
def application(request):
|
||||
response = Response('binary data here', mimetype='application/vnd.ms-excel')
|
||||
response.headers['Vary'] = 'Cookie'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=foo.xls'
|
||||
return response
|
||||
|
||||
c = Client(application, Response)
|
||||
response = c.get('/', headers=[
|
||||
('User-Agent', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)')
|
||||
])
|
||||
|
||||
# IE gets no vary
|
||||
self.assert_equal(response.get_data(), b'binary data here')
|
||||
assert 'vary' not in response.headers
|
||||
assert response.headers['content-disposition'] == 'attachment; filename=foo.xls'
|
||||
assert response.headers['content-type'] == 'application/vnd.ms-excel'
|
||||
|
||||
# other browsers do
|
||||
c = Client(application, Response)
|
||||
response = c.get('/')
|
||||
self.assert_equal(response.get_data(), b'binary data here')
|
||||
assert 'vary' in response.headers
|
||||
|
||||
cc = ResponseCacheControl()
|
||||
cc.no_cache = True
|
||||
|
||||
@fixers.InternetExplorerFix
|
||||
@Request.application
|
||||
def application(request):
|
||||
response = Response('binary data here', mimetype='application/vnd.ms-excel')
|
||||
response.headers['Pragma'] = ', '.join(pragma)
|
||||
response.headers['Cache-Control'] = cc.to_header()
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=foo.xls'
|
||||
return response
|
||||
|
||||
|
||||
# IE has no pragma or cache control
|
||||
pragma = ('no-cache',)
|
||||
c = Client(application, Response)
|
||||
response = c.get('/', headers=[
|
||||
('User-Agent', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)')
|
||||
])
|
||||
self.assert_equal(response.get_data(), b'binary data here')
|
||||
assert 'pragma' not in response.headers
|
||||
assert 'cache-control' not in response.headers
|
||||
assert response.headers['content-disposition'] == 'attachment; filename=foo.xls'
|
||||
|
||||
# IE has simplified pragma
|
||||
pragma = ('no-cache', 'x-foo')
|
||||
cc.proxy_revalidate = True
|
||||
response = c.get('/', headers=[
|
||||
('User-Agent', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)')
|
||||
])
|
||||
self.assert_equal(response.get_data(), b'binary data here')
|
||||
assert response.headers['pragma'] == 'x-foo'
|
||||
assert response.headers['cache-control'] == 'proxy-revalidate'
|
||||
assert response.headers['content-disposition'] == 'attachment; filename=foo.xls'
|
||||
|
||||
# regular browsers get everything
|
||||
response = c.get('/')
|
||||
self.assert_equal(response.get_data(), b'binary data here')
|
||||
assert response.headers['pragma'] == 'no-cache, x-foo'
|
||||
cc = parse_cache_control_header(response.headers['cache-control'],
|
||||
cls=ResponseCacheControl)
|
||||
assert cc.no_cache
|
||||
assert cc.proxy_revalidate
|
||||
assert response.headers['content-disposition'] == 'attachment; filename=foo.xls'
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(ServerFixerTestCase))
|
||||
suite.addTest(unittest.makeSuite(BrowserFixerTestCase))
|
||||
return suite
|
||||
@@ -1,184 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.iterio
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the iterio object.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
from functools import partial
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.contrib.iterio import IterIO, greenlet
|
||||
|
||||
|
||||
class IterOTestSuite(WerkzeugTestCase):
|
||||
|
||||
def test_basic_native(self):
|
||||
io = IterIO(["Hello", "World", "1", "2", "3"])
|
||||
self.assert_equal(io.tell(), 0)
|
||||
self.assert_equal(io.read(2), "He")
|
||||
self.assert_equal(io.tell(), 2)
|
||||
self.assert_equal(io.read(3), "llo")
|
||||
self.assert_equal(io.tell(), 5)
|
||||
io.seek(0)
|
||||
self.assert_equal(io.read(5), "Hello")
|
||||
self.assert_equal(io.tell(), 5)
|
||||
self.assert_equal(io._buf, "Hello")
|
||||
self.assert_equal(io.read(), "World123")
|
||||
self.assert_equal(io.tell(), 13)
|
||||
io.close()
|
||||
assert io.closed
|
||||
|
||||
io = IterIO(["Hello\n", "World!"])
|
||||
self.assert_equal(io.readline(), 'Hello\n')
|
||||
self.assert_equal(io._buf, 'Hello\n')
|
||||
self.assert_equal(io.read(), 'World!')
|
||||
self.assert_equal(io._buf, 'Hello\nWorld!')
|
||||
self.assert_equal(io.tell(), 12)
|
||||
io.seek(0)
|
||||
self.assert_equal(io.readlines(), ['Hello\n', 'World!'])
|
||||
|
||||
io = IterIO(["foo\n", "bar"])
|
||||
io.seek(-4, 2)
|
||||
self.assert_equal(io.read(4), '\nbar')
|
||||
|
||||
self.assert_raises(IOError, io.seek, 2, 100)
|
||||
io.close()
|
||||
self.assert_raises(ValueError, io.read)
|
||||
|
||||
def test_basic_bytes(self):
|
||||
io = IterIO([b"Hello", b"World", b"1", b"2", b"3"])
|
||||
self.assert_equal(io.tell(), 0)
|
||||
self.assert_equal(io.read(2), b"He")
|
||||
self.assert_equal(io.tell(), 2)
|
||||
self.assert_equal(io.read(3), b"llo")
|
||||
self.assert_equal(io.tell(), 5)
|
||||
io.seek(0)
|
||||
self.assert_equal(io.read(5), b"Hello")
|
||||
self.assert_equal(io.tell(), 5)
|
||||
self.assert_equal(io._buf, b"Hello")
|
||||
self.assert_equal(io.read(), b"World123")
|
||||
self.assert_equal(io.tell(), 13)
|
||||
io.close()
|
||||
assert io.closed
|
||||
|
||||
io = IterIO([b"Hello\n", b"World!"])
|
||||
self.assert_equal(io.readline(), b'Hello\n')
|
||||
self.assert_equal(io._buf, b'Hello\n')
|
||||
self.assert_equal(io.read(), b'World!')
|
||||
self.assert_equal(io._buf, b'Hello\nWorld!')
|
||||
self.assert_equal(io.tell(), 12)
|
||||
io.seek(0)
|
||||
self.assert_equal(io.readlines(), [b'Hello\n', b'World!'])
|
||||
|
||||
io = IterIO([b"foo\n", b"bar"])
|
||||
io.seek(-4, 2)
|
||||
self.assert_equal(io.read(4), b'\nbar')
|
||||
|
||||
self.assert_raises(IOError, io.seek, 2, 100)
|
||||
io.close()
|
||||
self.assert_raises(ValueError, io.read)
|
||||
|
||||
def test_basic_unicode(self):
|
||||
io = IterIO([u"Hello", u"World", u"1", u"2", u"3"])
|
||||
self.assert_equal(io.tell(), 0)
|
||||
self.assert_equal(io.read(2), u"He")
|
||||
self.assert_equal(io.tell(), 2)
|
||||
self.assert_equal(io.read(3), u"llo")
|
||||
self.assert_equal(io.tell(), 5)
|
||||
io.seek(0)
|
||||
self.assert_equal(io.read(5), u"Hello")
|
||||
self.assert_equal(io.tell(), 5)
|
||||
self.assert_equal(io._buf, u"Hello")
|
||||
self.assert_equal(io.read(), u"World123")
|
||||
self.assert_equal(io.tell(), 13)
|
||||
io.close()
|
||||
assert io.closed
|
||||
|
||||
io = IterIO([u"Hello\n", u"World!"])
|
||||
self.assert_equal(io.readline(), u'Hello\n')
|
||||
self.assert_equal(io._buf, u'Hello\n')
|
||||
self.assert_equal(io.read(), u'World!')
|
||||
self.assert_equal(io._buf, u'Hello\nWorld!')
|
||||
self.assert_equal(io.tell(), 12)
|
||||
io.seek(0)
|
||||
self.assert_equal(io.readlines(), [u'Hello\n', u'World!'])
|
||||
|
||||
io = IterIO([u"foo\n", u"bar"])
|
||||
io.seek(-4, 2)
|
||||
self.assert_equal(io.read(4), u'\nbar')
|
||||
|
||||
self.assert_raises(IOError, io.seek, 2, 100)
|
||||
io.close()
|
||||
self.assert_raises(ValueError, io.read)
|
||||
|
||||
def test_sentinel_cases(self):
|
||||
io = IterIO([])
|
||||
self.assert_strict_equal(io.read(), '')
|
||||
io = IterIO([], b'')
|
||||
self.assert_strict_equal(io.read(), b'')
|
||||
io = IterIO([], u'')
|
||||
self.assert_strict_equal(io.read(), u'')
|
||||
|
||||
io = IterIO([])
|
||||
self.assert_strict_equal(io.read(), '')
|
||||
io = IterIO([b''])
|
||||
self.assert_strict_equal(io.read(), b'')
|
||||
io = IterIO([u''])
|
||||
self.assert_strict_equal(io.read(), u'')
|
||||
|
||||
io = IterIO([])
|
||||
self.assert_strict_equal(io.readline(), '')
|
||||
io = IterIO([], b'')
|
||||
self.assert_strict_equal(io.readline(), b'')
|
||||
io = IterIO([], u'')
|
||||
self.assert_strict_equal(io.readline(), u'')
|
||||
|
||||
io = IterIO([])
|
||||
self.assert_strict_equal(io.readline(), '')
|
||||
io = IterIO([b''])
|
||||
self.assert_strict_equal(io.readline(), b'')
|
||||
io = IterIO([u''])
|
||||
self.assert_strict_equal(io.readline(), u'')
|
||||
|
||||
|
||||
class IterITestSuite(WerkzeugTestCase):
|
||||
|
||||
def test_basic(self):
|
||||
def producer(out):
|
||||
out.write('1\n')
|
||||
out.write('2\n')
|
||||
out.flush()
|
||||
out.write('3\n')
|
||||
iterable = IterIO(producer)
|
||||
self.assert_equal(next(iterable), '1\n2\n')
|
||||
self.assert_equal(next(iterable), '3\n')
|
||||
self.assert_raises(StopIteration, next, iterable)
|
||||
|
||||
def test_sentinel_cases(self):
|
||||
def producer_dummy_flush(out):
|
||||
out.flush()
|
||||
iterable = IterIO(producer_dummy_flush)
|
||||
self.assert_strict_equal(next(iterable), '')
|
||||
|
||||
def producer_empty(out):
|
||||
pass
|
||||
iterable = IterIO(producer_empty)
|
||||
self.assert_raises(StopIteration, next, iterable)
|
||||
|
||||
iterable = IterIO(producer_dummy_flush, b'')
|
||||
self.assert_strict_equal(next(iterable), b'')
|
||||
iterable = IterIO(producer_dummy_flush, u'')
|
||||
self.assert_strict_equal(next(iterable), u'')
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(IterOTestSuite))
|
||||
if greenlet is not None:
|
||||
suite.addTest(unittest.makeSuite(IterITestSuite))
|
||||
return suite
|
||||
@@ -1,64 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.securecookie
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the secure cookie.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug.utils import parse_cookie
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.contrib.securecookie import SecureCookie
|
||||
|
||||
|
||||
class SecureCookieTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_basic_support(self):
|
||||
c = SecureCookie(secret_key=b'foo')
|
||||
assert c.new
|
||||
assert not c.modified
|
||||
assert not c.should_save
|
||||
c['x'] = 42
|
||||
assert c.modified
|
||||
assert c.should_save
|
||||
s = c.serialize()
|
||||
|
||||
c2 = SecureCookie.unserialize(s, b'foo')
|
||||
assert c is not c2
|
||||
assert not c2.new
|
||||
assert not c2.modified
|
||||
assert not c2.should_save
|
||||
self.assert_equal(c2, c)
|
||||
|
||||
c3 = SecureCookie.unserialize(s, b'wrong foo')
|
||||
assert not c3.modified
|
||||
assert not c3.new
|
||||
self.assert_equal(c3, {})
|
||||
|
||||
def test_wrapper_support(self):
|
||||
req = Request.from_values()
|
||||
resp = Response()
|
||||
c = SecureCookie.load_cookie(req, secret_key=b'foo')
|
||||
assert c.new
|
||||
c['foo'] = 42
|
||||
self.assert_equal(c.secret_key, b'foo')
|
||||
c.save_cookie(resp)
|
||||
|
||||
req = Request.from_values(headers={
|
||||
'Cookie': 'session="%s"' % parse_cookie(resp.headers['set-cookie'])['session']
|
||||
})
|
||||
c2 = SecureCookie.load_cookie(req, secret_key=b'foo')
|
||||
assert not c2.new
|
||||
self.assert_equal(c2, c)
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(SecureCookieTestCase))
|
||||
return suite
|
||||
@@ -1,80 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.sessions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Added tests for the sessions.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
import shutil
|
||||
from tempfile import mkdtemp, gettempdir
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.contrib.sessions import FilesystemSessionStore
|
||||
|
||||
|
||||
|
||||
class SessionTestCase(WerkzeugTestCase):
|
||||
|
||||
def setup(self):
|
||||
self.session_folder = mkdtemp()
|
||||
|
||||
def teardown(self):
|
||||
shutil.rmtree(self.session_folder)
|
||||
|
||||
def test_default_tempdir(self):
|
||||
store = FilesystemSessionStore()
|
||||
assert store.path == gettempdir()
|
||||
|
||||
def test_basic_fs_sessions(self):
|
||||
store = FilesystemSessionStore(self.session_folder)
|
||||
x = store.new()
|
||||
assert x.new
|
||||
assert not x.modified
|
||||
x['foo'] = [1, 2, 3]
|
||||
assert x.modified
|
||||
store.save(x)
|
||||
|
||||
x2 = store.get(x.sid)
|
||||
assert not x2.new
|
||||
assert not x2.modified
|
||||
assert x2 is not x
|
||||
assert x2 == x
|
||||
x2['test'] = 3
|
||||
assert x2.modified
|
||||
assert not x2.new
|
||||
store.save(x2)
|
||||
|
||||
x = store.get(x.sid)
|
||||
store.delete(x)
|
||||
x2 = store.get(x.sid)
|
||||
# the session is not new when it was used previously.
|
||||
assert not x2.new
|
||||
|
||||
def test_renewing_fs_session(self):
|
||||
store = FilesystemSessionStore(self.session_folder, renew_missing=True)
|
||||
x = store.new()
|
||||
store.save(x)
|
||||
store.delete(x)
|
||||
x2 = store.get(x.sid)
|
||||
assert x2.new
|
||||
|
||||
def test_fs_session_lising(self):
|
||||
store = FilesystemSessionStore(self.session_folder, renew_missing=True)
|
||||
sessions = set()
|
||||
for x in range(10):
|
||||
sess = store.new()
|
||||
store.save(sess)
|
||||
sessions.add(sess.sid)
|
||||
|
||||
listed_sessions = set(store.list())
|
||||
assert sessions == listed_sessions
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(SessionTestCase))
|
||||
return suite
|
||||
@@ -1,97 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.contrib.wrappers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Added tests for the sessions.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug.contrib import wrappers
|
||||
from werkzeug import routing
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
|
||||
class WrappersTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_reverse_slash_behavior(self):
|
||||
class MyRequest(wrappers.ReverseSlashBehaviorRequestMixin, Request):
|
||||
pass
|
||||
req = MyRequest.from_values('/foo/bar', 'http://example.com/test')
|
||||
assert req.url == 'http://example.com/test/foo/bar'
|
||||
assert req.path == 'foo/bar'
|
||||
assert req.script_root == '/test/'
|
||||
|
||||
# make sure the routing system works with the slashes in
|
||||
# reverse order as well.
|
||||
map = routing.Map([routing.Rule('/foo/bar', endpoint='foo')])
|
||||
adapter = map.bind_to_environ(req.environ)
|
||||
assert adapter.match() == ('foo', {})
|
||||
adapter = map.bind(req.host, req.script_root)
|
||||
assert adapter.match(req.path) == ('foo', {})
|
||||
|
||||
def test_dynamic_charset_request_mixin(self):
|
||||
class MyRequest(wrappers.DynamicCharsetRequestMixin, Request):
|
||||
pass
|
||||
env = {'CONTENT_TYPE': 'text/html'}
|
||||
req = MyRequest(env)
|
||||
assert req.charset == 'latin1'
|
||||
|
||||
env = {'CONTENT_TYPE': 'text/html; charset=utf-8'}
|
||||
req = MyRequest(env)
|
||||
assert req.charset == 'utf-8'
|
||||
|
||||
env = {'CONTENT_TYPE': 'application/octet-stream'}
|
||||
req = MyRequest(env)
|
||||
assert req.charset == 'latin1'
|
||||
assert req.url_charset == 'latin1'
|
||||
|
||||
MyRequest.url_charset = 'utf-8'
|
||||
env = {'CONTENT_TYPE': 'application/octet-stream'}
|
||||
req = MyRequest(env)
|
||||
assert req.charset == 'latin1'
|
||||
assert req.url_charset == 'utf-8'
|
||||
|
||||
def return_ascii(x):
|
||||
return "ascii"
|
||||
env = {'CONTENT_TYPE': 'text/plain; charset=x-weird-charset'}
|
||||
req = MyRequest(env)
|
||||
req.unknown_charset = return_ascii
|
||||
assert req.charset == 'ascii'
|
||||
assert req.url_charset == 'utf-8'
|
||||
|
||||
def test_dynamic_charset_response_mixin(self):
|
||||
class MyResponse(wrappers.DynamicCharsetResponseMixin, Response):
|
||||
default_charset = 'utf-7'
|
||||
resp = MyResponse(mimetype='text/html')
|
||||
assert resp.charset == 'utf-7'
|
||||
resp.charset = 'utf-8'
|
||||
assert resp.charset == 'utf-8'
|
||||
assert resp.mimetype == 'text/html'
|
||||
assert resp.mimetype_params == {'charset': 'utf-8'}
|
||||
resp.mimetype_params['charset'] = 'iso-8859-15'
|
||||
assert resp.charset == 'iso-8859-15'
|
||||
resp.set_data(u'Hällo Wörld')
|
||||
assert b''.join(resp.iter_encoded()) == \
|
||||
u'Hällo Wörld'.encode('iso-8859-15')
|
||||
del resp.headers['content-type']
|
||||
try:
|
||||
resp.charset = 'utf-8'
|
||||
except TypeError as e:
|
||||
pass
|
||||
else:
|
||||
assert False, 'expected type error on charset setting without ct'
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(WrappersTestCase))
|
||||
return suite
|
||||
@@ -1,788 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.datastructures
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the functionality of the provided Werkzeug
|
||||
datastructures.
|
||||
|
||||
TODO:
|
||||
|
||||
- FileMultiDict
|
||||
- Immutable types undertested
|
||||
- Split up dict tests
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import unittest
|
||||
import pickle
|
||||
from contextlib import contextmanager
|
||||
from copy import copy
|
||||
|
||||
from werkzeug import datastructures
|
||||
from werkzeug._compat import iterkeys, itervalues, iteritems, iterlists, \
|
||||
iterlistvalues, text_type
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.exceptions import BadRequestKeyError
|
||||
|
||||
|
||||
class NativeItermethodsTestCase(WerkzeugTestCase):
|
||||
def test_basic(self):
|
||||
@datastructures.native_itermethods(['keys', 'values', 'items'])
|
||||
class StupidDict(object):
|
||||
def keys(self, multi=1):
|
||||
return iter(['a', 'b', 'c'] * multi)
|
||||
|
||||
def values(self, multi=1):
|
||||
return iter([1, 2, 3] * multi)
|
||||
|
||||
def items(self, multi=1):
|
||||
return iter(zip(iterkeys(self, multi=multi),
|
||||
itervalues(self, multi=multi)))
|
||||
|
||||
d = StupidDict()
|
||||
expected_keys = ['a', 'b', 'c']
|
||||
expected_values = [1, 2, 3]
|
||||
expected_items = list(zip(expected_keys, expected_values))
|
||||
|
||||
self.assert_equal(list(iterkeys(d)), expected_keys)
|
||||
self.assert_equal(list(itervalues(d)), expected_values)
|
||||
self.assert_equal(list(iteritems(d)), expected_items)
|
||||
|
||||
self.assert_equal(list(iterkeys(d, 2)), expected_keys * 2)
|
||||
self.assert_equal(list(itervalues(d, 2)), expected_values * 2)
|
||||
self.assert_equal(list(iteritems(d, 2)), expected_items * 2)
|
||||
|
||||
|
||||
class MutableMultiDictBaseTestCase(WerkzeugTestCase):
|
||||
storage_class = None
|
||||
|
||||
def test_pickle(self):
|
||||
cls = self.storage_class
|
||||
|
||||
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||
d = cls()
|
||||
d.setlist(b'foo', [1, 2, 3, 4])
|
||||
d.setlist(b'bar', b'foo bar baz'.split())
|
||||
s = pickle.dumps(d, protocol)
|
||||
ud = pickle.loads(s)
|
||||
self.assert_equal(type(ud), type(d))
|
||||
self.assert_equal(ud, d)
|
||||
self.assert_equal(pickle.loads(
|
||||
s.replace(b'werkzeug.datastructures', b'werkzeug')), d)
|
||||
ud[b'newkey'] = b'bla'
|
||||
self.assert_not_equal(ud, d)
|
||||
|
||||
def test_basic_interface(self):
|
||||
md = self.storage_class()
|
||||
assert isinstance(md, dict)
|
||||
|
||||
mapping = [('a', 1), ('b', 2), ('a', 2), ('d', 3),
|
||||
('a', 1), ('a', 3), ('d', 4), ('c', 3)]
|
||||
md = self.storage_class(mapping)
|
||||
|
||||
# simple getitem gives the first value
|
||||
self.assert_equal(md['a'], 1)
|
||||
self.assert_equal(md['c'], 3)
|
||||
with self.assert_raises(KeyError):
|
||||
md['e']
|
||||
self.assert_equal(md.get('a'), 1)
|
||||
|
||||
# list getitem
|
||||
self.assert_equal(md.getlist('a'), [1, 2, 1, 3])
|
||||
self.assert_equal(md.getlist('d'), [3, 4])
|
||||
# do not raise if key not found
|
||||
self.assert_equal(md.getlist('x'), [])
|
||||
|
||||
# simple setitem overwrites all values
|
||||
md['a'] = 42
|
||||
self.assert_equal(md.getlist('a'), [42])
|
||||
|
||||
# list setitem
|
||||
md.setlist('a', [1, 2, 3])
|
||||
self.assert_equal(md['a'], 1)
|
||||
self.assert_equal(md.getlist('a'), [1, 2, 3])
|
||||
|
||||
# verify that it does not change original lists
|
||||
l1 = [1, 2, 3]
|
||||
md.setlist('a', l1)
|
||||
del l1[:]
|
||||
self.assert_equal(md['a'], 1)
|
||||
|
||||
# setdefault, setlistdefault
|
||||
self.assert_equal(md.setdefault('u', 23), 23)
|
||||
self.assert_equal(md.getlist('u'), [23])
|
||||
del md['u']
|
||||
|
||||
md.setlist('u', [-1, -2])
|
||||
|
||||
# delitem
|
||||
del md['u']
|
||||
with self.assert_raises(KeyError):
|
||||
md['u']
|
||||
del md['d']
|
||||
self.assert_equal(md.getlist('d'), [])
|
||||
|
||||
# keys, values, items, lists
|
||||
self.assert_equal(list(sorted(md.keys())), ['a', 'b', 'c'])
|
||||
self.assert_equal(list(sorted(iterkeys(md))), ['a', 'b', 'c'])
|
||||
|
||||
self.assert_equal(list(sorted(itervalues(md))), [1, 2, 3])
|
||||
self.assert_equal(list(sorted(itervalues(md))), [1, 2, 3])
|
||||
|
||||
self.assert_equal(list(sorted(md.items())),
|
||||
[('a', 1), ('b', 2), ('c', 3)])
|
||||
self.assert_equal(list(sorted(md.items(multi=True))),
|
||||
[('a', 1), ('a', 2), ('a', 3), ('b', 2), ('c', 3)])
|
||||
self.assert_equal(list(sorted(iteritems(md))),
|
||||
[('a', 1), ('b', 2), ('c', 3)])
|
||||
self.assert_equal(list(sorted(iteritems(md, multi=True))),
|
||||
[('a', 1), ('a', 2), ('a', 3), ('b', 2), ('c', 3)])
|
||||
|
||||
self.assert_equal(list(sorted(md.lists())),
|
||||
[('a', [1, 2, 3]), ('b', [2]), ('c', [3])])
|
||||
self.assert_equal(list(sorted(iterlists(md))),
|
||||
[('a', [1, 2, 3]), ('b', [2]), ('c', [3])])
|
||||
|
||||
# copy method
|
||||
c = md.copy()
|
||||
self.assert_equal(c['a'], 1)
|
||||
self.assert_equal(c.getlist('a'), [1, 2, 3])
|
||||
|
||||
# copy method 2
|
||||
c = copy(md)
|
||||
self.assert_equal(c['a'], 1)
|
||||
self.assert_equal(c.getlist('a'), [1, 2, 3])
|
||||
|
||||
# update with a multidict
|
||||
od = self.storage_class([('a', 4), ('a', 5), ('y', 0)])
|
||||
md.update(od)
|
||||
self.assert_equal(md.getlist('a'), [1, 2, 3, 4, 5])
|
||||
self.assert_equal(md.getlist('y'), [0])
|
||||
|
||||
# update with a regular dict
|
||||
md = c
|
||||
od = {'a': 4, 'y': 0}
|
||||
md.update(od)
|
||||
self.assert_equal(md.getlist('a'), [1, 2, 3, 4])
|
||||
self.assert_equal(md.getlist('y'), [0])
|
||||
|
||||
# pop, poplist, popitem, popitemlist
|
||||
self.assert_equal(md.pop('y'), 0)
|
||||
assert 'y' not in md
|
||||
self.assert_equal(md.poplist('a'), [1, 2, 3, 4])
|
||||
assert 'a' not in md
|
||||
self.assert_equal(md.poplist('missing'), [])
|
||||
|
||||
# remaining: b=2, c=3
|
||||
popped = md.popitem()
|
||||
assert popped in [('b', 2), ('c', 3)]
|
||||
popped = md.popitemlist()
|
||||
assert popped in [('b', [2]), ('c', [3])]
|
||||
|
||||
# type conversion
|
||||
md = self.storage_class({'a': '4', 'b': ['2', '3']})
|
||||
self.assert_equal(md.get('a', type=int), 4)
|
||||
self.assert_equal(md.getlist('b', type=int), [2, 3])
|
||||
|
||||
# repr
|
||||
md = self.storage_class([('a', 1), ('a', 2), ('b', 3)])
|
||||
assert "('a', 1)" in repr(md)
|
||||
assert "('a', 2)" in repr(md)
|
||||
assert "('b', 3)" in repr(md)
|
||||
|
||||
# add and getlist
|
||||
md.add('c', '42')
|
||||
md.add('c', '23')
|
||||
self.assert_equal(md.getlist('c'), ['42', '23'])
|
||||
md.add('c', 'blah')
|
||||
self.assert_equal(md.getlist('c', type=int), [42, 23])
|
||||
|
||||
# setdefault
|
||||
md = self.storage_class()
|
||||
md.setdefault('x', []).append(42)
|
||||
md.setdefault('x', []).append(23)
|
||||
self.assert_equal(md['x'], [42, 23])
|
||||
|
||||
# to dict
|
||||
md = self.storage_class()
|
||||
md['foo'] = 42
|
||||
md.add('bar', 1)
|
||||
md.add('bar', 2)
|
||||
self.assert_equal(md.to_dict(), {'foo': 42, 'bar': 1})
|
||||
self.assert_equal(md.to_dict(flat=False), {'foo': [42], 'bar': [1, 2]})
|
||||
|
||||
# popitem from empty dict
|
||||
with self.assert_raises(KeyError):
|
||||
self.storage_class().popitem()
|
||||
|
||||
with self.assert_raises(KeyError):
|
||||
self.storage_class().popitemlist()
|
||||
|
||||
# key errors are of a special type
|
||||
with self.assert_raises(BadRequestKeyError):
|
||||
self.storage_class()[42]
|
||||
|
||||
# setlist works
|
||||
md = self.storage_class()
|
||||
md['foo'] = 42
|
||||
md.setlist('foo', [1, 2])
|
||||
self.assert_equal(md.getlist('foo'), [1, 2])
|
||||
|
||||
|
||||
class ImmutableDictBaseTestCase(WerkzeugTestCase):
|
||||
storage_class = None
|
||||
|
||||
def test_follows_dict_interface(self):
|
||||
cls = self.storage_class
|
||||
|
||||
data = {'foo': 1, 'bar': 2, 'baz': 3}
|
||||
d = cls(data)
|
||||
|
||||
self.assert_equal(d['foo'], 1)
|
||||
self.assert_equal(d['bar'], 2)
|
||||
self.assert_equal(d['baz'], 3)
|
||||
self.assert_equal(sorted(d.keys()), ['bar', 'baz', 'foo'])
|
||||
self.assert_true('foo' in d)
|
||||
self.assert_true('foox' not in d)
|
||||
self.assert_equal(len(d), 3)
|
||||
|
||||
def test_copies_are_mutable(self):
|
||||
cls = self.storage_class
|
||||
immutable = cls({'a': 1})
|
||||
with self.assert_raises(TypeError):
|
||||
immutable.pop('a')
|
||||
|
||||
mutable = immutable.copy()
|
||||
mutable.pop('a')
|
||||
self.assert_true('a' in immutable)
|
||||
self.assert_true(mutable is not immutable)
|
||||
self.assert_true(copy(immutable) is immutable)
|
||||
|
||||
def test_dict_is_hashable(self):
|
||||
cls = self.storage_class
|
||||
immutable = cls({'a': 1, 'b': 2})
|
||||
immutable2 = cls({'a': 2, 'b': 2})
|
||||
x = set([immutable])
|
||||
self.assert_true(immutable in x)
|
||||
self.assert_true(immutable2 not in x)
|
||||
x.discard(immutable)
|
||||
self.assert_true(immutable not in x)
|
||||
self.assert_true(immutable2 not in x)
|
||||
x.add(immutable2)
|
||||
self.assert_true(immutable not in x)
|
||||
self.assert_true(immutable2 in x)
|
||||
x.add(immutable)
|
||||
self.assert_true(immutable in x)
|
||||
self.assert_true(immutable2 in x)
|
||||
|
||||
|
||||
class ImmutableTypeConversionDictTestCase(ImmutableDictBaseTestCase):
|
||||
storage_class = datastructures.ImmutableTypeConversionDict
|
||||
|
||||
|
||||
class ImmutableMultiDictTestCase(ImmutableDictBaseTestCase):
|
||||
storage_class = datastructures.ImmutableMultiDict
|
||||
|
||||
def test_multidict_is_hashable(self):
|
||||
cls = self.storage_class
|
||||
immutable = cls({'a': [1, 2], 'b': 2})
|
||||
immutable2 = cls({'a': [1], 'b': 2})
|
||||
x = set([immutable])
|
||||
self.assert_true(immutable in x)
|
||||
self.assert_true(immutable2 not in x)
|
||||
x.discard(immutable)
|
||||
self.assert_true(immutable not in x)
|
||||
self.assert_true(immutable2 not in x)
|
||||
x.add(immutable2)
|
||||
self.assert_true(immutable not in x)
|
||||
self.assert_true(immutable2 in x)
|
||||
x.add(immutable)
|
||||
self.assert_true(immutable in x)
|
||||
self.assert_true(immutable2 in x)
|
||||
|
||||
|
||||
class ImmutableDictTestCase(ImmutableDictBaseTestCase):
|
||||
storage_class = datastructures.ImmutableDict
|
||||
|
||||
|
||||
class ImmutableOrderedMultiDictTestCase(ImmutableDictBaseTestCase):
|
||||
storage_class = datastructures.ImmutableOrderedMultiDict
|
||||
|
||||
def test_ordered_multidict_is_hashable(self):
|
||||
a = self.storage_class([('a', 1), ('b', 1), ('a', 2)])
|
||||
b = self.storage_class([('a', 1), ('a', 2), ('b', 1)])
|
||||
self.assert_not_equal(hash(a), hash(b))
|
||||
|
||||
|
||||
class MultiDictTestCase(MutableMultiDictBaseTestCase):
|
||||
storage_class = datastructures.MultiDict
|
||||
|
||||
def test_multidict_pop(self):
|
||||
make_d = lambda: self.storage_class({'foo': [1, 2, 3, 4]})
|
||||
d = make_d()
|
||||
self.assert_equal(d.pop('foo'), 1)
|
||||
assert not d
|
||||
d = make_d()
|
||||
self.assert_equal(d.pop('foo', 32), 1)
|
||||
assert not d
|
||||
d = make_d()
|
||||
self.assert_equal(d.pop('foos', 32), 32)
|
||||
assert d
|
||||
|
||||
with self.assert_raises(KeyError):
|
||||
d.pop('foos')
|
||||
|
||||
def test_setlistdefault(self):
|
||||
md = self.storage_class()
|
||||
self.assert_equal(md.setlistdefault('u', [-1, -2]), [-1, -2])
|
||||
self.assert_equal(md.getlist('u'), [-1, -2])
|
||||
self.assert_equal(md['u'], -1)
|
||||
|
||||
def test_iter_interfaces(self):
|
||||
mapping = [('a', 1), ('b', 2), ('a', 2), ('d', 3),
|
||||
('a', 1), ('a', 3), ('d', 4), ('c', 3)]
|
||||
md = self.storage_class(mapping)
|
||||
self.assert_equal(list(zip(md.keys(), md.listvalues())),
|
||||
list(md.lists()))
|
||||
self.assert_equal(list(zip(md, iterlistvalues(md))),
|
||||
list(iterlists(md)))
|
||||
self.assert_equal(list(zip(iterkeys(md), iterlistvalues(md))),
|
||||
list(iterlists(md)))
|
||||
|
||||
|
||||
class OrderedMultiDictTestCase(MutableMultiDictBaseTestCase):
|
||||
storage_class = datastructures.OrderedMultiDict
|
||||
|
||||
def test_ordered_interface(self):
|
||||
cls = self.storage_class
|
||||
|
||||
d = cls()
|
||||
assert not d
|
||||
d.add('foo', 'bar')
|
||||
self.assert_equal(len(d), 1)
|
||||
d.add('foo', 'baz')
|
||||
self.assert_equal(len(d), 1)
|
||||
self.assert_equal(list(iteritems(d)), [('foo', 'bar')])
|
||||
self.assert_equal(list(d), ['foo'])
|
||||
self.assert_equal(list(iteritems(d, multi=True)),
|
||||
[('foo', 'bar'), ('foo', 'baz')])
|
||||
del d['foo']
|
||||
assert not d
|
||||
self.assert_equal(len(d), 0)
|
||||
self.assert_equal(list(d), [])
|
||||
|
||||
d.update([('foo', 1), ('foo', 2), ('bar', 42)])
|
||||
d.add('foo', 3)
|
||||
self.assert_equal(d.getlist('foo'), [1, 2, 3])
|
||||
self.assert_equal(d.getlist('bar'), [42])
|
||||
self.assert_equal(list(iteritems(d)), [('foo', 1), ('bar', 42)])
|
||||
|
||||
expected = ['foo', 'bar']
|
||||
|
||||
self.assert_sequence_equal(list(d.keys()), expected)
|
||||
self.assert_sequence_equal(list(d), expected)
|
||||
self.assert_sequence_equal(list(iterkeys(d)), expected)
|
||||
|
||||
self.assert_equal(list(iteritems(d, multi=True)),
|
||||
[('foo', 1), ('foo', 2), ('bar', 42), ('foo', 3)])
|
||||
self.assert_equal(len(d), 2)
|
||||
|
||||
self.assert_equal(d.pop('foo'), 1)
|
||||
assert d.pop('blafasel', None) is None
|
||||
self.assert_equal(d.pop('blafasel', 42), 42)
|
||||
self.assert_equal(len(d), 1)
|
||||
self.assert_equal(d.poplist('bar'), [42])
|
||||
assert not d
|
||||
|
||||
d.get('missingkey') is None
|
||||
|
||||
d.add('foo', 42)
|
||||
d.add('foo', 23)
|
||||
d.add('bar', 2)
|
||||
d.add('foo', 42)
|
||||
self.assert_equal(d, datastructures.MultiDict(d))
|
||||
id = self.storage_class(d)
|
||||
self.assert_equal(d, id)
|
||||
d.add('foo', 2)
|
||||
assert d != id
|
||||
|
||||
d.update({'blah': [1, 2, 3]})
|
||||
self.assert_equal(d['blah'], 1)
|
||||
self.assert_equal(d.getlist('blah'), [1, 2, 3])
|
||||
|
||||
# setlist works
|
||||
d = self.storage_class()
|
||||
d['foo'] = 42
|
||||
d.setlist('foo', [1, 2])
|
||||
self.assert_equal(d.getlist('foo'), [1, 2])
|
||||
|
||||
with self.assert_raises(BadRequestKeyError):
|
||||
d.pop('missing')
|
||||
with self.assert_raises(BadRequestKeyError):
|
||||
d['missing']
|
||||
|
||||
# popping
|
||||
d = self.storage_class()
|
||||
d.add('foo', 23)
|
||||
d.add('foo', 42)
|
||||
d.add('foo', 1)
|
||||
self.assert_equal(d.popitem(), ('foo', 23))
|
||||
with self.assert_raises(BadRequestKeyError):
|
||||
d.popitem()
|
||||
assert not d
|
||||
|
||||
d.add('foo', 23)
|
||||
d.add('foo', 42)
|
||||
d.add('foo', 1)
|
||||
self.assert_equal(d.popitemlist(), ('foo', [23, 42, 1]))
|
||||
|
||||
with self.assert_raises(BadRequestKeyError):
|
||||
d.popitemlist()
|
||||
|
||||
def test_iterables(self):
|
||||
a = datastructures.MultiDict((("key_a", "value_a"),))
|
||||
b = datastructures.MultiDict((("key_b", "value_b"),))
|
||||
ab = datastructures.CombinedMultiDict((a,b))
|
||||
|
||||
self.assert_equal(sorted(ab.lists()), [('key_a', ['value_a']), ('key_b', ['value_b'])])
|
||||
self.assert_equal(sorted(ab.listvalues()), [['value_a'], ['value_b']])
|
||||
self.assert_equal(sorted(ab.keys()), ["key_a", "key_b"])
|
||||
|
||||
self.assert_equal(sorted(iterlists(ab)), [('key_a', ['value_a']), ('key_b', ['value_b'])])
|
||||
self.assert_equal(sorted(iterlistvalues(ab)), [['value_a'], ['value_b']])
|
||||
self.assert_equal(sorted(iterkeys(ab)), ["key_a", "key_b"])
|
||||
|
||||
|
||||
class CombinedMultiDictTestCase(WerkzeugTestCase):
|
||||
storage_class = datastructures.CombinedMultiDict
|
||||
|
||||
def test_basic_interface(self):
|
||||
d1 = datastructures.MultiDict([('foo', '1')])
|
||||
d2 = datastructures.MultiDict([('bar', '2'), ('bar', '3')])
|
||||
d = self.storage_class([d1, d2])
|
||||
|
||||
# lookup
|
||||
self.assert_equal(d['foo'], '1')
|
||||
self.assert_equal(d['bar'], '2')
|
||||
self.assert_equal(d.getlist('bar'), ['2', '3'])
|
||||
|
||||
self.assert_equal(sorted(d.items()),
|
||||
[('bar', '2'), ('foo', '1')])
|
||||
self.assert_equal(sorted(d.items(multi=True)),
|
||||
[('bar', '2'), ('bar', '3'), ('foo', '1')])
|
||||
assert 'missingkey' not in d
|
||||
assert 'foo' in d
|
||||
|
||||
# type lookup
|
||||
self.assert_equal(d.get('foo', type=int), 1)
|
||||
self.assert_equal(d.getlist('bar', type=int), [2, 3])
|
||||
|
||||
# get key errors for missing stuff
|
||||
with self.assert_raises(KeyError):
|
||||
d['missing']
|
||||
|
||||
# make sure that they are immutable
|
||||
with self.assert_raises(TypeError):
|
||||
d['foo'] = 'blub'
|
||||
|
||||
# copies are immutable
|
||||
d = d.copy()
|
||||
with self.assert_raises(TypeError):
|
||||
d['foo'] = 'blub'
|
||||
|
||||
# make sure lists merges
|
||||
md1 = datastructures.MultiDict((("foo", "bar"),))
|
||||
md2 = datastructures.MultiDict((("foo", "blafasel"),))
|
||||
x = self.storage_class((md1, md2))
|
||||
self.assert_equal(list(iterlists(x)), [('foo', ['bar', 'blafasel'])])
|
||||
|
||||
|
||||
class HeadersTestCase(WerkzeugTestCase):
|
||||
storage_class = datastructures.Headers
|
||||
|
||||
def test_basic_interface(self):
|
||||
headers = self.storage_class()
|
||||
headers.add('Content-Type', 'text/plain')
|
||||
headers.add('X-Foo', 'bar')
|
||||
assert 'x-Foo' in headers
|
||||
assert 'Content-type' in headers
|
||||
|
||||
headers['Content-Type'] = 'foo/bar'
|
||||
self.assert_equal(headers['Content-Type'], 'foo/bar')
|
||||
self.assert_equal(len(headers.getlist('Content-Type')), 1)
|
||||
|
||||
# list conversion
|
||||
self.assert_equal(headers.to_wsgi_list(), [
|
||||
('Content-Type', 'foo/bar'),
|
||||
('X-Foo', 'bar')
|
||||
])
|
||||
self.assert_equal(str(headers), (
|
||||
"Content-Type: foo/bar\r\n"
|
||||
"X-Foo: bar\r\n"
|
||||
"\r\n"))
|
||||
self.assert_equal(str(self.storage_class()), "\r\n")
|
||||
|
||||
# extended add
|
||||
headers.add('Content-Disposition', 'attachment', filename='foo')
|
||||
self.assert_equal(headers['Content-Disposition'],
|
||||
'attachment; filename=foo')
|
||||
|
||||
headers.add('x', 'y', z='"')
|
||||
self.assert_equal(headers['x'], r'y; z="\""')
|
||||
|
||||
def test_defaults_and_conversion(self):
|
||||
# defaults
|
||||
headers = self.storage_class([
|
||||
('Content-Type', 'text/plain'),
|
||||
('X-Foo', 'bar'),
|
||||
('X-Bar', '1'),
|
||||
('X-Bar', '2')
|
||||
])
|
||||
self.assert_equal(headers.getlist('x-bar'), ['1', '2'])
|
||||
self.assert_equal(headers.get('x-Bar'), '1')
|
||||
self.assert_equal(headers.get('Content-Type'), 'text/plain')
|
||||
|
||||
self.assert_equal(headers.setdefault('X-Foo', 'nope'), 'bar')
|
||||
self.assert_equal(headers.setdefault('X-Bar', 'nope'), '1')
|
||||
self.assert_equal(headers.setdefault('X-Baz', 'quux'), 'quux')
|
||||
self.assert_equal(headers.setdefault('X-Baz', 'nope'), 'quux')
|
||||
headers.pop('X-Baz')
|
||||
|
||||
# type conversion
|
||||
self.assert_equal(headers.get('x-bar', type=int), 1)
|
||||
self.assert_equal(headers.getlist('x-bar', type=int), [1, 2])
|
||||
|
||||
# list like operations
|
||||
self.assert_equal(headers[0], ('Content-Type', 'text/plain'))
|
||||
self.assert_equal(headers[:1], self.storage_class([('Content-Type', 'text/plain')]))
|
||||
del headers[:2]
|
||||
del headers[-1]
|
||||
self.assert_equal(headers, self.storage_class([('X-Bar', '1')]))
|
||||
|
||||
def test_copying(self):
|
||||
a = self.storage_class([('foo', 'bar')])
|
||||
b = a.copy()
|
||||
a.add('foo', 'baz')
|
||||
self.assert_equal(a.getlist('foo'), ['bar', 'baz'])
|
||||
self.assert_equal(b.getlist('foo'), ['bar'])
|
||||
|
||||
def test_popping(self):
|
||||
headers = self.storage_class([('a', 1)])
|
||||
self.assert_equal(headers.pop('a'), 1)
|
||||
self.assert_equal(headers.pop('b', 2), 2)
|
||||
|
||||
with self.assert_raises(KeyError):
|
||||
headers.pop('c')
|
||||
|
||||
def test_set_arguments(self):
|
||||
a = self.storage_class()
|
||||
a.set('Content-Disposition', 'useless')
|
||||
a.set('Content-Disposition', 'attachment', filename='foo')
|
||||
self.assert_equal(a['Content-Disposition'], 'attachment; filename=foo')
|
||||
|
||||
def test_reject_newlines(self):
|
||||
h = self.storage_class()
|
||||
|
||||
for variation in 'foo\nbar', 'foo\r\nbar', 'foo\rbar':
|
||||
with self.assert_raises(ValueError):
|
||||
h['foo'] = variation
|
||||
with self.assert_raises(ValueError):
|
||||
h.add('foo', variation)
|
||||
with self.assert_raises(ValueError):
|
||||
h.add('foo', 'test', option=variation)
|
||||
with self.assert_raises(ValueError):
|
||||
h.set('foo', variation)
|
||||
with self.assert_raises(ValueError):
|
||||
h.set('foo', 'test', option=variation)
|
||||
|
||||
def test_slicing(self):
|
||||
# there's nothing wrong with these being native strings
|
||||
# Headers doesn't care about the data types
|
||||
h = self.storage_class()
|
||||
h.set('X-Foo-Poo', 'bleh')
|
||||
h.set('Content-Type', 'application/whocares')
|
||||
h.set('X-Forwarded-For', '192.168.0.123')
|
||||
h[:] = [(k, v) for k, v in h if k.startswith(u'X-')]
|
||||
self.assert_equal(list(h), [
|
||||
('X-Foo-Poo', 'bleh'),
|
||||
('X-Forwarded-For', '192.168.0.123')
|
||||
])
|
||||
|
||||
def test_bytes_operations(self):
|
||||
h = self.storage_class()
|
||||
h.set('X-Foo-Poo', 'bleh')
|
||||
h.set('X-Whoops', b'\xff')
|
||||
|
||||
self.assert_equal(h.get('x-foo-poo', as_bytes=True), b'bleh')
|
||||
self.assert_equal(h.get('x-whoops', as_bytes=True), b'\xff')
|
||||
|
||||
|
||||
class EnvironHeadersTestCase(WerkzeugTestCase):
|
||||
storage_class = datastructures.EnvironHeaders
|
||||
|
||||
def test_basic_interface(self):
|
||||
# this happens in multiple WSGI servers because they
|
||||
# use a vary naive way to convert the headers;
|
||||
broken_env = {
|
||||
'HTTP_CONTENT_TYPE': 'text/html',
|
||||
'CONTENT_TYPE': 'text/html',
|
||||
'HTTP_CONTENT_LENGTH': '0',
|
||||
'CONTENT_LENGTH': '0',
|
||||
'HTTP_ACCEPT': '*',
|
||||
'wsgi.version': (1, 0)
|
||||
}
|
||||
headers = self.storage_class(broken_env)
|
||||
assert headers
|
||||
self.assert_equal(len(headers), 3)
|
||||
self.assert_equal(sorted(headers), [
|
||||
('Accept', '*'),
|
||||
('Content-Length', '0'),
|
||||
('Content-Type', 'text/html')
|
||||
])
|
||||
assert not self.storage_class({'wsgi.version': (1, 0)})
|
||||
self.assert_equal(len(self.storage_class({'wsgi.version': (1, 0)})), 0)
|
||||
|
||||
def test_return_type_is_unicode(self):
|
||||
# environ contains native strings; we return unicode
|
||||
headers = self.storage_class({
|
||||
'HTTP_FOO': '\xe2\x9c\x93',
|
||||
'CONTENT_TYPE': 'text/plain',
|
||||
})
|
||||
self.assert_equal(headers['Foo'], u"\xe2\x9c\x93")
|
||||
assert isinstance(headers['Foo'], text_type)
|
||||
assert isinstance(headers['Content-Type'], text_type)
|
||||
iter_output = dict(iter(headers))
|
||||
self.assert_equal(iter_output['Foo'], u"\xe2\x9c\x93")
|
||||
assert isinstance(iter_output['Foo'], text_type)
|
||||
assert isinstance(iter_output['Content-Type'], text_type)
|
||||
|
||||
def test_bytes_operations(self):
|
||||
foo_val = '\xff'
|
||||
h = self.storage_class({
|
||||
'HTTP_X_FOO': foo_val
|
||||
})
|
||||
|
||||
self.assert_equal(h.get('x-foo', as_bytes=True), b'\xff')
|
||||
self.assert_equal(h.get('x-foo'), u'\xff')
|
||||
|
||||
|
||||
class HeaderSetTestCase(WerkzeugTestCase):
|
||||
storage_class = datastructures.HeaderSet
|
||||
|
||||
def test_basic_interface(self):
|
||||
hs = self.storage_class()
|
||||
hs.add('foo')
|
||||
hs.add('bar')
|
||||
assert 'Bar' in hs
|
||||
self.assert_equal(hs.find('foo'), 0)
|
||||
self.assert_equal(hs.find('BAR'), 1)
|
||||
assert hs.find('baz') < 0
|
||||
hs.discard('missing')
|
||||
hs.discard('foo')
|
||||
assert hs.find('foo') < 0
|
||||
self.assert_equal(hs.find('bar'), 0)
|
||||
|
||||
with self.assert_raises(IndexError):
|
||||
hs.index('missing')
|
||||
|
||||
self.assert_equal(hs.index('bar'), 0)
|
||||
assert hs
|
||||
hs.clear()
|
||||
assert not hs
|
||||
|
||||
|
||||
class ImmutableListTestCase(WerkzeugTestCase):
|
||||
storage_class = datastructures.ImmutableList
|
||||
|
||||
def test_list_hashable(self):
|
||||
t = (1, 2, 3, 4)
|
||||
l = self.storage_class(t)
|
||||
self.assert_equal(hash(t), hash(l))
|
||||
self.assert_not_equal(t, l)
|
||||
|
||||
|
||||
def make_call_asserter(assert_equal_func, func=None):
|
||||
"""Utility to assert a certain number of function calls.
|
||||
|
||||
>>> assert_calls, func = make_call_asserter(self.assert_equal)
|
||||
>>> with assert_calls(2):
|
||||
func()
|
||||
func()
|
||||
"""
|
||||
|
||||
calls = [0]
|
||||
|
||||
@contextmanager
|
||||
def asserter(count, msg=None):
|
||||
calls[0] = 0
|
||||
yield
|
||||
assert_equal_func(calls[0], count, msg)
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
calls[0] += 1
|
||||
if func is not None:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return asserter, wrapped
|
||||
|
||||
|
||||
class CallbackDictTestCase(WerkzeugTestCase):
|
||||
storage_class = datastructures.CallbackDict
|
||||
|
||||
def test_callback_dict_reads(self):
|
||||
assert_calls, func = make_call_asserter(self.assert_equal)
|
||||
initial = {'a': 'foo', 'b': 'bar'}
|
||||
dct = self.storage_class(initial=initial, on_update=func)
|
||||
with assert_calls(0, 'callback triggered by read-only method'):
|
||||
# read-only methods
|
||||
dct['a']
|
||||
dct.get('a')
|
||||
self.assert_raises(KeyError, lambda: dct['x'])
|
||||
'a' in dct
|
||||
list(iter(dct))
|
||||
dct.copy()
|
||||
with assert_calls(0, 'callback triggered without modification'):
|
||||
# methods that may write but don't
|
||||
dct.pop('z', None)
|
||||
dct.setdefault('a')
|
||||
|
||||
def test_callback_dict_writes(self):
|
||||
assert_calls, func = make_call_asserter(self.assert_equal)
|
||||
initial = {'a': 'foo', 'b': 'bar'}
|
||||
dct = self.storage_class(initial=initial, on_update=func)
|
||||
with assert_calls(8, 'callback not triggered by write method'):
|
||||
# always-write methods
|
||||
dct['z'] = 123
|
||||
dct['z'] = 123 # must trigger again
|
||||
del dct['z']
|
||||
dct.pop('b', None)
|
||||
dct.setdefault('x')
|
||||
dct.popitem()
|
||||
dct.update([])
|
||||
dct.clear()
|
||||
with assert_calls(0, 'callback triggered by failed del'):
|
||||
self.assert_raises(KeyError, lambda: dct.__delitem__('x'))
|
||||
with assert_calls(0, 'callback triggered by failed pop'):
|
||||
self.assert_raises(KeyError, lambda: dct.pop('x'))
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(MultiDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(OrderedMultiDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(CombinedMultiDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(ImmutableTypeConversionDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(ImmutableMultiDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(ImmutableDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(ImmutableOrderedMultiDictTestCase))
|
||||
suite.addTest(unittest.makeSuite(HeadersTestCase))
|
||||
suite.addTest(unittest.makeSuite(EnvironHeadersTestCase))
|
||||
suite.addTest(unittest.makeSuite(HeaderSetTestCase))
|
||||
suite.addTest(unittest.makeSuite(NativeItermethodsTestCase))
|
||||
suite.addTest(unittest.makeSuite(CallbackDictTestCase))
|
||||
return suite
|
||||
@@ -1,172 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.debug
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests some debug utilities.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
import sys
|
||||
import re
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.debug.repr import debug_repr, DebugReprGenerator, \
|
||||
dump, helper
|
||||
from werkzeug.debug.console import HTMLStringO
|
||||
from werkzeug._compat import PY2
|
||||
|
||||
|
||||
class DebugReprTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_basic_repr(self):
|
||||
self.assert_equal(debug_repr([]), u'[]')
|
||||
self.assert_equal(debug_repr([1, 2]),
|
||||
u'[<span class="number">1</span>, <span class="number">2</span>]')
|
||||
self.assert_equal(debug_repr([1, 'test']),
|
||||
u'[<span class="number">1</span>, <span class="string">\'test\'</span>]')
|
||||
self.assert_equal(debug_repr([None]),
|
||||
u'[<span class="object">None</span>]')
|
||||
|
||||
def test_sequence_repr(self):
|
||||
self.assert_equal(debug_repr(list(range(20))), (
|
||||
u'[<span class="number">0</span>, <span class="number">1</span>, '
|
||||
u'<span class="number">2</span>, <span class="number">3</span>, '
|
||||
u'<span class="number">4</span>, <span class="number">5</span>, '
|
||||
u'<span class="number">6</span>, <span class="number">7</span>, '
|
||||
u'<span class="extended"><span class="number">8</span>, '
|
||||
u'<span class="number">9</span>, <span class="number">10</span>, '
|
||||
u'<span class="number">11</span>, <span class="number">12</span>, '
|
||||
u'<span class="number">13</span>, <span class="number">14</span>, '
|
||||
u'<span class="number">15</span>, <span class="number">16</span>, '
|
||||
u'<span class="number">17</span>, <span class="number">18</span>, '
|
||||
u'<span class="number">19</span></span>]'
|
||||
))
|
||||
|
||||
def test_mapping_repr(self):
|
||||
self.assert_equal(debug_repr({}), u'{}')
|
||||
self.assert_equal(debug_repr({'foo': 42}),
|
||||
u'{<span class="pair"><span class="key"><span class="string">\'foo\''
|
||||
u'</span></span>: <span class="value"><span class="number">42'
|
||||
u'</span></span></span>}')
|
||||
self.assert_equal(debug_repr(dict(zip(range(10), [None] * 10))),
|
||||
u'{<span class="pair"><span class="key"><span class="number">0</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">1</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">2</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">3</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="extended"><span class="pair"><span class="key"><span class="number">4</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">5</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">6</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">7</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">8</span></span>: <span class="value"><span class="object">None</span></span></span>, <span class="pair"><span class="key"><span class="number">9</span></span>: <span class="value"><span class="object">None</span></span></span></span>}')
|
||||
self.assert_equal(
|
||||
debug_repr((1, 'zwei', u'drei')),
|
||||
u'(<span class="number">1</span>, <span class="string">\''
|
||||
u'zwei\'</span>, <span class="string">%s\'drei\'</span>)' % ('u' if PY2 else ''))
|
||||
|
||||
def test_custom_repr(self):
|
||||
class Foo(object):
|
||||
def __repr__(self):
|
||||
return '<Foo 42>'
|
||||
self.assert_equal(debug_repr(Foo()),
|
||||
'<span class="object"><Foo 42></span>')
|
||||
|
||||
def test_list_subclass_repr(self):
|
||||
class MyList(list):
|
||||
pass
|
||||
self.assert_equal(
|
||||
debug_repr(MyList([1, 2])),
|
||||
u'<span class="module">werkzeug.testsuite.debug.</span>MyList(['
|
||||
u'<span class="number">1</span>, <span class="number">2</span>])')
|
||||
|
||||
def test_regex_repr(self):
|
||||
self.assert_equal(debug_repr(re.compile(r'foo\d')),
|
||||
u're.compile(<span class="string regex">r\'foo\\d\'</span>)')
|
||||
#XXX: no raw string here cause of a syntax bug in py3.3
|
||||
self.assert_equal(debug_repr(re.compile(u'foo\\d')),
|
||||
u're.compile(<span class="string regex">%sr\'foo\\d\'</span>)' %
|
||||
('u' if PY2 else ''))
|
||||
|
||||
def test_set_repr(self):
|
||||
self.assert_equal(debug_repr(frozenset('x')),
|
||||
u'frozenset([<span class="string">\'x\'</span>])')
|
||||
self.assert_equal(debug_repr(set('x')),
|
||||
u'set([<span class="string">\'x\'</span>])')
|
||||
|
||||
def test_recursive_repr(self):
|
||||
a = [1]
|
||||
a.append(a)
|
||||
self.assert_equal(debug_repr(a),
|
||||
u'[<span class="number">1</span>, [...]]')
|
||||
|
||||
def test_broken_repr(self):
|
||||
class Foo(object):
|
||||
def __repr__(self):
|
||||
raise Exception('broken!')
|
||||
|
||||
self.assert_equal(
|
||||
debug_repr(Foo()),
|
||||
u'<span class="brokenrepr"><broken repr (Exception: '
|
||||
u'broken!)></span>')
|
||||
|
||||
|
||||
class Foo(object):
|
||||
x = 42
|
||||
y = 23
|
||||
|
||||
def __init__(self):
|
||||
self.z = 15
|
||||
|
||||
|
||||
class DebugHelpersTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_object_dumping(self):
|
||||
drg = DebugReprGenerator()
|
||||
out = drg.dump_object(Foo())
|
||||
assert re.search('Details for werkzeug.testsuite.debug.Foo object at', out)
|
||||
assert re.search('<th>x.*<span class="number">42</span>(?s)', out)
|
||||
assert re.search('<th>y.*<span class="number">23</span>(?s)', out)
|
||||
assert re.search('<th>z.*<span class="number">15</span>(?s)', out)
|
||||
|
||||
out = drg.dump_object({'x': 42, 'y': 23})
|
||||
assert re.search('Contents of', out)
|
||||
assert re.search('<th>x.*<span class="number">42</span>(?s)', out)
|
||||
assert re.search('<th>y.*<span class="number">23</span>(?s)', out)
|
||||
|
||||
out = drg.dump_object({'x': 42, 'y': 23, 23: 11})
|
||||
assert not re.search('Contents of', out)
|
||||
|
||||
out = drg.dump_locals({'x': 42, 'y': 23})
|
||||
assert re.search('Local variables in frame', out)
|
||||
assert re.search('<th>x.*<span class="number">42</span>(?s)', out)
|
||||
assert re.search('<th>y.*<span class="number">23</span>(?s)', out)
|
||||
|
||||
def test_debug_dump(self):
|
||||
old = sys.stdout
|
||||
sys.stdout = HTMLStringO()
|
||||
try:
|
||||
dump([1, 2, 3])
|
||||
x = sys.stdout.reset()
|
||||
dump()
|
||||
y = sys.stdout.reset()
|
||||
finally:
|
||||
sys.stdout = old
|
||||
|
||||
self.assert_in('Details for list object at', x)
|
||||
self.assert_in('<span class="number">1</span>', x)
|
||||
self.assert_in('Local variables in frame', y)
|
||||
self.assert_in('<th>x', y)
|
||||
self.assert_in('<th>old', y)
|
||||
|
||||
def test_debug_help(self):
|
||||
old = sys.stdout
|
||||
sys.stdout = HTMLStringO()
|
||||
try:
|
||||
helper([1, 2, 3])
|
||||
x = sys.stdout.reset()
|
||||
finally:
|
||||
sys.stdout = old
|
||||
|
||||
self.assert_in('Help on list object', x)
|
||||
self.assert_in('__delitem__', x)
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(DebugReprTestCase))
|
||||
suite.addTest(unittest.makeSuite(DebugHelpersTestCase))
|
||||
return suite
|
||||
@@ -1,85 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The tests for the exception classes.
|
||||
|
||||
TODO:
|
||||
|
||||
- This is undertested. HTML is never checked
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import exceptions
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug._compat import text_type
|
||||
|
||||
|
||||
class ExceptionsTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_proxy_exception(self):
|
||||
orig_resp = Response('Hello World')
|
||||
try:
|
||||
exceptions.abort(orig_resp)
|
||||
except exceptions.HTTPException as e:
|
||||
resp = e.get_response({})
|
||||
else:
|
||||
self.fail('exception not raised')
|
||||
self.assert_true(resp is orig_resp)
|
||||
self.assert_equal(resp.get_data(), b'Hello World')
|
||||
|
||||
def test_aborter(self):
|
||||
abort = exceptions.abort
|
||||
self.assert_raises(exceptions.BadRequest, abort, 400)
|
||||
self.assert_raises(exceptions.Unauthorized, abort, 401)
|
||||
self.assert_raises(exceptions.Forbidden, abort, 403)
|
||||
self.assert_raises(exceptions.NotFound, abort, 404)
|
||||
self.assert_raises(exceptions.MethodNotAllowed, abort, 405, ['GET', 'HEAD'])
|
||||
self.assert_raises(exceptions.NotAcceptable, abort, 406)
|
||||
self.assert_raises(exceptions.RequestTimeout, abort, 408)
|
||||
self.assert_raises(exceptions.Gone, abort, 410)
|
||||
self.assert_raises(exceptions.LengthRequired, abort, 411)
|
||||
self.assert_raises(exceptions.PreconditionFailed, abort, 412)
|
||||
self.assert_raises(exceptions.RequestEntityTooLarge, abort, 413)
|
||||
self.assert_raises(exceptions.RequestURITooLarge, abort, 414)
|
||||
self.assert_raises(exceptions.UnsupportedMediaType, abort, 415)
|
||||
self.assert_raises(exceptions.UnprocessableEntity, abort, 422)
|
||||
self.assert_raises(exceptions.InternalServerError, abort, 500)
|
||||
self.assert_raises(exceptions.NotImplemented, abort, 501)
|
||||
self.assert_raises(exceptions.BadGateway, abort, 502)
|
||||
self.assert_raises(exceptions.ServiceUnavailable, abort, 503)
|
||||
|
||||
myabort = exceptions.Aborter({1: exceptions.NotFound})
|
||||
self.assert_raises(LookupError, myabort, 404)
|
||||
self.assert_raises(exceptions.NotFound, myabort, 1)
|
||||
|
||||
myabort = exceptions.Aborter(extra={1: exceptions.NotFound})
|
||||
self.assert_raises(exceptions.NotFound, myabort, 404)
|
||||
self.assert_raises(exceptions.NotFound, myabort, 1)
|
||||
|
||||
def test_exception_repr(self):
|
||||
exc = exceptions.NotFound()
|
||||
self.assert_equal(text_type(exc), '404: Not Found')
|
||||
self.assert_equal(repr(exc), "<NotFound '404: Not Found'>")
|
||||
|
||||
exc = exceptions.NotFound('Not There')
|
||||
self.assert_equal(text_type(exc), '404: Not Found')
|
||||
self.assert_equal(repr(exc), "<NotFound '404: Not Found'>")
|
||||
|
||||
def test_special_exceptions(self):
|
||||
exc = exceptions.MethodNotAllowed(['GET', 'HEAD', 'POST'])
|
||||
h = dict(exc.get_headers({}))
|
||||
self.assert_equal(h['Allow'], 'GET, HEAD, POST')
|
||||
self.assert_true('The method is not allowed' in exc.get_description())
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(ExceptionsTestCase))
|
||||
return suite
|
||||
@@ -1,400 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.formparser
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the form parsing facilities.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import unittest
|
||||
from os.path import join, dirname
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import formparser
|
||||
from werkzeug.test import create_environ, Client
|
||||
from werkzeug.wrappers import Request, Response
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.formparser import parse_form_data
|
||||
from werkzeug._compat import BytesIO
|
||||
|
||||
|
||||
@Request.application
|
||||
def form_data_consumer(request):
|
||||
result_object = request.args['object']
|
||||
if result_object == 'text':
|
||||
return Response(repr(request.form['text']))
|
||||
f = request.files[result_object]
|
||||
return Response(b'\n'.join((
|
||||
repr(f.filename).encode('ascii'),
|
||||
repr(f.name).encode('ascii'),
|
||||
repr(f.content_type).encode('ascii'),
|
||||
f.stream.read()
|
||||
)))
|
||||
|
||||
|
||||
def get_contents(filename):
|
||||
with open(filename, 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
class FormParserTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_limiting(self):
|
||||
data = b'foo=Hello+World&bar=baz'
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
method='POST')
|
||||
req.max_content_length = 400
|
||||
self.assert_strict_equal(req.form['foo'], u'Hello World')
|
||||
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
method='POST')
|
||||
req.max_form_memory_size = 7
|
||||
self.assert_raises(RequestEntityTooLarge, lambda: req.form['foo'])
|
||||
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='application/x-www-form-urlencoded',
|
||||
method='POST')
|
||||
req.max_form_memory_size = 400
|
||||
self.assert_strict_equal(req.form['foo'], u'Hello World')
|
||||
|
||||
data = (b'--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\n'
|
||||
b'Hello World\r\n'
|
||||
b'--foo\r\nContent-Disposition: form-field; name=bar\r\n\r\n'
|
||||
b'bar=baz\r\n--foo--')
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
req.max_content_length = 4
|
||||
self.assert_raises(RequestEntityTooLarge, lambda: req.form['foo'])
|
||||
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
req.max_content_length = 400
|
||||
self.assert_strict_equal(req.form['foo'], u'Hello World')
|
||||
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
req.max_form_memory_size = 7
|
||||
self.assert_raises(RequestEntityTooLarge, lambda: req.form['foo'])
|
||||
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
req.max_form_memory_size = 400
|
||||
self.assert_strict_equal(req.form['foo'], u'Hello World')
|
||||
|
||||
def test_parse_form_data_put_without_content(self):
|
||||
# A PUT without a Content-Type header returns empty data
|
||||
|
||||
# Both rfc1945 and rfc2616 (1.0 and 1.1) say "Any HTTP/[1.0/1.1] message
|
||||
# containing an entity-body SHOULD include a Content-Type header field
|
||||
# defining the media type of that body." In the case where either
|
||||
# headers are omitted, parse_form_data should still work.
|
||||
env = create_environ('/foo', 'http://example.org/', method='PUT')
|
||||
del env['CONTENT_TYPE']
|
||||
del env['CONTENT_LENGTH']
|
||||
|
||||
stream, form, files = formparser.parse_form_data(env)
|
||||
self.assert_strict_equal(stream.read(), b'')
|
||||
self.assert_strict_equal(len(form), 0)
|
||||
self.assert_strict_equal(len(files), 0)
|
||||
|
||||
def test_parse_form_data_get_without_content(self):
|
||||
env = create_environ('/foo', 'http://example.org/', method='GET')
|
||||
del env['CONTENT_TYPE']
|
||||
del env['CONTENT_LENGTH']
|
||||
|
||||
stream, form, files = formparser.parse_form_data(env)
|
||||
self.assert_strict_equal(stream.read(), b'')
|
||||
self.assert_strict_equal(len(form), 0)
|
||||
self.assert_strict_equal(len(files), 0)
|
||||
|
||||
def test_large_file(self):
|
||||
data = b'x' * (1024 * 600)
|
||||
req = Request.from_values(data={'foo': (BytesIO(data), 'test.txt')},
|
||||
method='POST')
|
||||
# make sure we have a real file here, because we expect to be
|
||||
# on the disk. > 1024 * 500
|
||||
self.assert_true(hasattr(req.files['foo'].stream, u'fileno'))
|
||||
# close file to prevent fds from leaking
|
||||
req.files['foo'].close()
|
||||
|
||||
def test_streaming_parse(self):
|
||||
data = b'x' * (1024 * 600)
|
||||
class StreamMPP(formparser.MultiPartParser):
|
||||
def parse(self, file, boundary, content_length):
|
||||
i = iter(self.parse_lines(file, boundary, content_length))
|
||||
one = next(i)
|
||||
two = next(i)
|
||||
return self.cls(()), {'one': one, 'two': two}
|
||||
class StreamFDP(formparser.FormDataParser):
|
||||
def _sf_parse_multipart(self, stream, mimetype,
|
||||
content_length, options):
|
||||
form, files = StreamMPP(
|
||||
self.stream_factory, self.charset, self.errors,
|
||||
max_form_memory_size=self.max_form_memory_size,
|
||||
cls=self.cls).parse(stream, options.get('boundary').encode('ascii'),
|
||||
content_length)
|
||||
return stream, form, files
|
||||
parse_functions = {}
|
||||
parse_functions.update(formparser.FormDataParser.parse_functions)
|
||||
parse_functions['multipart/form-data'] = _sf_parse_multipart
|
||||
class StreamReq(Request):
|
||||
form_data_parser_class = StreamFDP
|
||||
req = StreamReq.from_values(data={'foo': (BytesIO(data), 'test.txt')},
|
||||
method='POST')
|
||||
self.assert_strict_equal('begin_file', req.files['one'][0])
|
||||
self.assert_strict_equal(('foo', 'test.txt'), req.files['one'][1][1:])
|
||||
self.assert_strict_equal('cont', req.files['two'][0])
|
||||
self.assert_strict_equal(data, req.files['two'][1])
|
||||
|
||||
|
||||
class MultiPartTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_basic(self):
|
||||
resources = join(dirname(__file__), 'multipart')
|
||||
client = Client(form_data_consumer, Response)
|
||||
|
||||
repository = [
|
||||
('firefox3-2png1txt', '---------------------------186454651713519341951581030105', [
|
||||
(u'anchor.png', 'file1', 'image/png', 'file1.png'),
|
||||
(u'application_edit.png', 'file2', 'image/png', 'file2.png')
|
||||
], u'example text'),
|
||||
('firefox3-2pnglongtext', '---------------------------14904044739787191031754711748', [
|
||||
(u'accept.png', 'file1', 'image/png', 'file1.png'),
|
||||
(u'add.png', 'file2', 'image/png', 'file2.png')
|
||||
], u'--long text\r\n--with boundary\r\n--lookalikes--'),
|
||||
('opera8-2png1txt', '----------zEO9jQKmLc2Cq88c23Dx19', [
|
||||
(u'arrow_branch.png', 'file1', 'image/png', 'file1.png'),
|
||||
(u'award_star_bronze_1.png', 'file2', 'image/png', 'file2.png')
|
||||
], u'blafasel öäü'),
|
||||
('webkit3-2png1txt', '----WebKitFormBoundaryjdSFhcARk8fyGNy6', [
|
||||
(u'gtk-apply.png', 'file1', 'image/png', 'file1.png'),
|
||||
(u'gtk-no.png', 'file2', 'image/png', 'file2.png')
|
||||
], u'this is another text with ümläüts'),
|
||||
('ie6-2png1txt', '---------------------------7d91b03a20128', [
|
||||
(u'file1.png', 'file1', 'image/x-png', 'file1.png'),
|
||||
(u'file2.png', 'file2', 'image/x-png', 'file2.png')
|
||||
], u'ie6 sucks :-/')
|
||||
]
|
||||
|
||||
for name, boundary, files, text in repository:
|
||||
folder = join(resources, name)
|
||||
data = get_contents(join(folder, 'request.txt'))
|
||||
for filename, field, content_type, fsname in files:
|
||||
response = client.post('/?object=' + field, data=data, content_type=
|
||||
'multipart/form-data; boundary="%s"' % boundary,
|
||||
content_length=len(data))
|
||||
lines = response.get_data().split(b'\n', 3)
|
||||
self.assert_strict_equal(lines[0], repr(filename).encode('ascii'))
|
||||
self.assert_strict_equal(lines[1], repr(field).encode('ascii'))
|
||||
self.assert_strict_equal(lines[2], repr(content_type).encode('ascii'))
|
||||
self.assert_strict_equal(lines[3], get_contents(join(folder, fsname)))
|
||||
response = client.post('/?object=text', data=data, content_type=
|
||||
'multipart/form-data; boundary="%s"' % boundary,
|
||||
content_length=len(data))
|
||||
self.assert_strict_equal(response.get_data(), repr(text).encode('utf-8'))
|
||||
|
||||
def test_ie7_unc_path(self):
|
||||
client = Client(form_data_consumer, Response)
|
||||
data_file = join(dirname(__file__), 'multipart', 'ie7_full_path_request.txt')
|
||||
data = get_contents(data_file)
|
||||
boundary = '---------------------------7da36d1b4a0164'
|
||||
response = client.post('/?object=cb_file_upload_multiple', data=data, content_type=
|
||||
'multipart/form-data; boundary="%s"' % boundary, content_length=len(data))
|
||||
lines = response.get_data().split(b'\n', 3)
|
||||
self.assert_strict_equal(lines[0],
|
||||
repr(u'Sellersburg Town Council Meeting 02-22-2010doc.doc').encode('ascii'))
|
||||
|
||||
def test_end_of_file(self):
|
||||
# This test looks innocent but it was actually timeing out in
|
||||
# the Werkzeug 0.5 release version (#394)
|
||||
data = (
|
||||
b'--foo\r\n'
|
||||
b'Content-Disposition: form-data; name="test"; filename="test.txt"\r\n'
|
||||
b'Content-Type: text/plain\r\n\r\n'
|
||||
b'file contents and no end'
|
||||
)
|
||||
data = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
self.assert_true(not data.files)
|
||||
self.assert_true(not data.form)
|
||||
|
||||
def test_broken(self):
|
||||
data = (
|
||||
'--foo\r\n'
|
||||
'Content-Disposition: form-data; name="test"; filename="test.txt"\r\n'
|
||||
'Content-Transfer-Encoding: base64\r\n'
|
||||
'Content-Type: text/plain\r\n\r\n'
|
||||
'broken base 64'
|
||||
'--foo--'
|
||||
)
|
||||
_, form, files = formparser.parse_form_data(create_environ(data=data,
|
||||
method='POST', content_type='multipart/form-data; boundary=foo'))
|
||||
self.assert_true(not files)
|
||||
self.assert_true(not form)
|
||||
|
||||
self.assert_raises(ValueError, formparser.parse_form_data,
|
||||
create_environ(data=data, method='POST',
|
||||
content_type='multipart/form-data; boundary=foo'),
|
||||
silent=False)
|
||||
|
||||
def test_file_no_content_type(self):
|
||||
data = (
|
||||
b'--foo\r\n'
|
||||
b'Content-Disposition: form-data; name="test"; filename="test.txt"\r\n\r\n'
|
||||
b'file contents\r\n--foo--'
|
||||
)
|
||||
data = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
self.assert_equal(data.files['test'].filename, 'test.txt')
|
||||
self.assert_strict_equal(data.files['test'].read(), b'file contents')
|
||||
|
||||
def test_extra_newline(self):
|
||||
# this test looks innocent but it was actually timeing out in
|
||||
# the Werkzeug 0.5 release version (#394)
|
||||
data = (
|
||||
b'\r\n\r\n--foo\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"\r\n\r\n'
|
||||
b'a string\r\n'
|
||||
b'--foo--'
|
||||
)
|
||||
data = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
self.assert_true(not data.files)
|
||||
self.assert_strict_equal(data.form['foo'], u'a string')
|
||||
|
||||
def test_headers(self):
|
||||
data = (b'--foo\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"; filename="foo.txt"\r\n'
|
||||
b'X-Custom-Header: blah\r\n'
|
||||
b'Content-Type: text/plain; charset=utf-8\r\n\r\n'
|
||||
b'file contents, just the contents\r\n'
|
||||
b'--foo--')
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
foo = req.files['foo']
|
||||
self.assert_strict_equal(foo.mimetype, 'text/plain')
|
||||
self.assert_strict_equal(foo.mimetype_params, {'charset': 'utf-8'})
|
||||
self.assert_strict_equal(foo.headers['content-type'], foo.content_type)
|
||||
self.assert_strict_equal(foo.content_type, 'text/plain; charset=utf-8')
|
||||
self.assert_strict_equal(foo.headers['x-custom-header'], 'blah')
|
||||
|
||||
def test_nonstandard_line_endings(self):
|
||||
for nl in b'\n', b'\r', b'\r\n':
|
||||
data = nl.join((
|
||||
b'--foo',
|
||||
b'Content-Disposition: form-data; name=foo',
|
||||
b'',
|
||||
b'this is just bar',
|
||||
b'--foo',
|
||||
b'Content-Disposition: form-data; name=bar',
|
||||
b'',
|
||||
b'blafasel',
|
||||
b'--foo--'
|
||||
))
|
||||
req = Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; '
|
||||
'boundary=foo', method='POST')
|
||||
self.assert_strict_equal(req.form['foo'], u'this is just bar')
|
||||
self.assert_strict_equal(req.form['bar'], u'blafasel')
|
||||
|
||||
def test_failures(self):
|
||||
def parse_multipart(stream, boundary, content_length):
|
||||
parser = formparser.MultiPartParser(content_length)
|
||||
return parser.parse(stream, boundary, content_length)
|
||||
self.assert_raises(ValueError, parse_multipart, BytesIO(), b'broken ', 0)
|
||||
|
||||
data = b'--foo\r\n\r\nHello World\r\n--foo--'
|
||||
self.assert_raises(ValueError, parse_multipart, BytesIO(data), b'foo', len(data))
|
||||
|
||||
data = b'--foo\r\nContent-Disposition: form-field; name=foo\r\n' \
|
||||
b'Content-Transfer-Encoding: base64\r\n\r\nHello World\r\n--foo--'
|
||||
self.assert_raises(ValueError, parse_multipart, BytesIO(data), b'foo', len(data))
|
||||
|
||||
data = b'--foo\r\nContent-Disposition: form-field; name=foo\r\n\r\nHello World\r\n'
|
||||
self.assert_raises(ValueError, parse_multipart, BytesIO(data), b'foo', len(data))
|
||||
|
||||
x = formparser.parse_multipart_headers(['foo: bar\r\n', ' x test\r\n'])
|
||||
self.assert_strict_equal(x['foo'], 'bar\n x test')
|
||||
self.assert_raises(ValueError, formparser.parse_multipart_headers,
|
||||
['foo: bar\r\n', ' x test'])
|
||||
|
||||
def test_bad_newline_bad_newline_assumption(self):
|
||||
class ISORequest(Request):
|
||||
charset = 'latin1'
|
||||
contents = b'U2vlbmUgbORu'
|
||||
data = b'--foo\r\nContent-Disposition: form-data; name="test"\r\n' \
|
||||
b'Content-Transfer-Encoding: base64\r\n\r\n' + \
|
||||
contents + b'\r\n--foo--'
|
||||
req = ISORequest.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
self.assert_strict_equal(req.form['test'], u'Sk\xe5ne l\xe4n')
|
||||
|
||||
def test_empty_multipart(self):
|
||||
environ = {}
|
||||
data = b'--boundary--'
|
||||
environ['REQUEST_METHOD'] = 'POST'
|
||||
environ['CONTENT_TYPE'] = 'multipart/form-data; boundary=boundary'
|
||||
environ['CONTENT_LENGTH'] = str(len(data))
|
||||
environ['wsgi.input'] = BytesIO(data)
|
||||
stream, form, files = parse_form_data(environ, silent=False)
|
||||
rv = stream.read()
|
||||
self.assert_equal(rv, b'')
|
||||
self.assert_equal(form, MultiDict())
|
||||
self.assert_equal(files, MultiDict())
|
||||
|
||||
|
||||
class InternalFunctionsTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_line_parser(self):
|
||||
assert formparser._line_parse('foo') == ('foo', False)
|
||||
assert formparser._line_parse('foo\r\n') == ('foo', True)
|
||||
assert formparser._line_parse('foo\r') == ('foo', True)
|
||||
assert formparser._line_parse('foo\n') == ('foo', True)
|
||||
|
||||
def test_find_terminator(self):
|
||||
lineiter = iter(b'\n\n\nfoo\nbar\nbaz'.splitlines(True))
|
||||
find_terminator = formparser.MultiPartParser()._find_terminator
|
||||
line = find_terminator(lineiter)
|
||||
self.assert_equal(line, b'foo')
|
||||
self.assert_equal(list(lineiter), [b'bar\n', b'baz'])
|
||||
self.assert_equal(find_terminator([]), b'')
|
||||
self.assert_equal(find_terminator([b'']), b'')
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(FormParserTestCase))
|
||||
suite.addTest(unittest.makeSuite(MultiPartTestCase))
|
||||
suite.addTest(unittest.makeSuite(InternalFunctionsTestCase))
|
||||
return suite
|
||||
@@ -1,448 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.http
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
HTTP parsing utilities.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug._compat import itervalues, wsgi_encoding_dance
|
||||
|
||||
from werkzeug import http, datastructures
|
||||
from werkzeug.test import create_environ
|
||||
|
||||
|
||||
class HTTPUtilityTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_accept(self):
|
||||
a = http.parse_accept_header('en-us,ru;q=0.5')
|
||||
self.assert_equal(list(itervalues(a)), ['en-us', 'ru'])
|
||||
self.assert_equal(a.best, 'en-us')
|
||||
self.assert_equal(a.find('ru'), 1)
|
||||
self.assert_raises(ValueError, a.index, 'de')
|
||||
self.assert_equal(a.to_header(), 'en-us,ru;q=0.5')
|
||||
|
||||
def test_mime_accept(self):
|
||||
a = http.parse_accept_header('text/xml,application/xml,'
|
||||
'application/xhtml+xml,'
|
||||
'text/html;q=0.9,text/plain;q=0.8,'
|
||||
'image/png,*/*;q=0.5',
|
||||
datastructures.MIMEAccept)
|
||||
self.assert_raises(ValueError, lambda: a['missing'])
|
||||
self.assert_equal(a['image/png'], 1)
|
||||
self.assert_equal(a['text/plain'], 0.8)
|
||||
self.assert_equal(a['foo/bar'], 0.5)
|
||||
self.assert_equal(a[a.find('foo/bar')], ('*/*', 0.5))
|
||||
|
||||
def test_accept_matches(self):
|
||||
a = http.parse_accept_header('text/xml,application/xml,application/xhtml+xml,'
|
||||
'text/html;q=0.9,text/plain;q=0.8,'
|
||||
'image/png', datastructures.MIMEAccept)
|
||||
self.assert_equal(a.best_match(['text/html', 'application/xhtml+xml']),
|
||||
'application/xhtml+xml')
|
||||
self.assert_equal(a.best_match(['text/html']), 'text/html')
|
||||
self.assert_true(a.best_match(['foo/bar']) is None)
|
||||
self.assert_equal(a.best_match(['foo/bar', 'bar/foo'],
|
||||
default='foo/bar'), 'foo/bar')
|
||||
self.assert_equal(a.best_match(['application/xml', 'text/xml']), 'application/xml')
|
||||
|
||||
def test_charset_accept(self):
|
||||
a = http.parse_accept_header('ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
||||
datastructures.CharsetAccept)
|
||||
self.assert_equal(a['iso-8859-1'], a['iso8859-1'])
|
||||
self.assert_equal(a['iso-8859-1'], 1)
|
||||
self.assert_equal(a['UTF8'], 0.7)
|
||||
self.assert_equal(a['ebcdic'], 0.7)
|
||||
|
||||
def test_language_accept(self):
|
||||
a = http.parse_accept_header('de-AT,de;q=0.8,en;q=0.5',
|
||||
datastructures.LanguageAccept)
|
||||
self.assert_equal(a.best, 'de-AT')
|
||||
self.assert_true('de_AT' in a)
|
||||
self.assert_true('en' in a)
|
||||
self.assert_equal(a['de-at'], 1)
|
||||
self.assert_equal(a['en'], 0.5)
|
||||
|
||||
def test_set_header(self):
|
||||
hs = http.parse_set_header('foo, Bar, "Blah baz", Hehe')
|
||||
self.assert_true('blah baz' in hs)
|
||||
self.assert_true('foobar' not in hs)
|
||||
self.assert_true('foo' in hs)
|
||||
self.assert_equal(list(hs), ['foo', 'Bar', 'Blah baz', 'Hehe'])
|
||||
hs.add('Foo')
|
||||
self.assert_equal(hs.to_header(), 'foo, Bar, "Blah baz", Hehe')
|
||||
|
||||
def test_list_header(self):
|
||||
hl = http.parse_list_header('foo baz, blah')
|
||||
self.assert_equal(hl, ['foo baz', 'blah'])
|
||||
|
||||
def test_dict_header(self):
|
||||
d = http.parse_dict_header('foo="bar baz", blah=42')
|
||||
self.assert_equal(d, {'foo': 'bar baz', 'blah': '42'})
|
||||
|
||||
def test_cache_control_header(self):
|
||||
cc = http.parse_cache_control_header('max-age=0, no-cache')
|
||||
assert cc.max_age == 0
|
||||
assert cc.no_cache
|
||||
cc = http.parse_cache_control_header('private, community="UCI"', None,
|
||||
datastructures.ResponseCacheControl)
|
||||
assert cc.private
|
||||
assert cc['community'] == 'UCI'
|
||||
|
||||
c = datastructures.ResponseCacheControl()
|
||||
assert c.no_cache is None
|
||||
assert c.private is None
|
||||
c.no_cache = True
|
||||
assert c.no_cache == '*'
|
||||
c.private = True
|
||||
assert c.private == '*'
|
||||
del c.private
|
||||
assert c.private is None
|
||||
assert c.to_header() == 'no-cache'
|
||||
|
||||
def test_authorization_header(self):
|
||||
a = http.parse_authorization_header('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==')
|
||||
assert a.type == 'basic'
|
||||
assert a.username == 'Aladdin'
|
||||
assert a.password == 'open sesame'
|
||||
|
||||
a = http.parse_authorization_header('''Digest username="Mufasa",
|
||||
realm="testrealm@host.invalid",
|
||||
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
|
||||
uri="/dir/index.html",
|
||||
qop=auth,
|
||||
nc=00000001,
|
||||
cnonce="0a4f113b",
|
||||
response="6629fae49393a05397450978507c4ef1",
|
||||
opaque="5ccc069c403ebaf9f0171e9517f40e41"''')
|
||||
assert a.type == 'digest'
|
||||
assert a.username == 'Mufasa'
|
||||
assert a.realm == 'testrealm@host.invalid'
|
||||
assert a.nonce == 'dcd98b7102dd2f0e8b11d0f600bfb0c093'
|
||||
assert a.uri == '/dir/index.html'
|
||||
assert 'auth' in a.qop
|
||||
assert a.nc == '00000001'
|
||||
assert a.cnonce == '0a4f113b'
|
||||
assert a.response == '6629fae49393a05397450978507c4ef1'
|
||||
assert a.opaque == '5ccc069c403ebaf9f0171e9517f40e41'
|
||||
|
||||
a = http.parse_authorization_header('''Digest username="Mufasa",
|
||||
realm="testrealm@host.invalid",
|
||||
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
|
||||
uri="/dir/index.html",
|
||||
response="e257afa1414a3340d93d30955171dd0e",
|
||||
opaque="5ccc069c403ebaf9f0171e9517f40e41"''')
|
||||
assert a.type == 'digest'
|
||||
assert a.username == 'Mufasa'
|
||||
assert a.realm == 'testrealm@host.invalid'
|
||||
assert a.nonce == 'dcd98b7102dd2f0e8b11d0f600bfb0c093'
|
||||
assert a.uri == '/dir/index.html'
|
||||
assert a.response == 'e257afa1414a3340d93d30955171dd0e'
|
||||
assert a.opaque == '5ccc069c403ebaf9f0171e9517f40e41'
|
||||
|
||||
assert http.parse_authorization_header('') is None
|
||||
assert http.parse_authorization_header(None) is None
|
||||
assert http.parse_authorization_header('foo') is None
|
||||
|
||||
def test_www_authenticate_header(self):
|
||||
wa = http.parse_www_authenticate_header('Basic realm="WallyWorld"')
|
||||
assert wa.type == 'basic'
|
||||
assert wa.realm == 'WallyWorld'
|
||||
wa.realm = 'Foo Bar'
|
||||
assert wa.to_header() == 'Basic realm="Foo Bar"'
|
||||
|
||||
wa = http.parse_www_authenticate_header('''Digest
|
||||
realm="testrealm@host.com",
|
||||
qop="auth,auth-int",
|
||||
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
|
||||
opaque="5ccc069c403ebaf9f0171e9517f40e41"''')
|
||||
assert wa.type == 'digest'
|
||||
assert wa.realm == 'testrealm@host.com'
|
||||
assert 'auth' in wa.qop
|
||||
assert 'auth-int' in wa.qop
|
||||
assert wa.nonce == 'dcd98b7102dd2f0e8b11d0f600bfb0c093'
|
||||
assert wa.opaque == '5ccc069c403ebaf9f0171e9517f40e41'
|
||||
|
||||
wa = http.parse_www_authenticate_header('broken')
|
||||
assert wa.type == 'broken'
|
||||
|
||||
assert not http.parse_www_authenticate_header('').type
|
||||
assert not http.parse_www_authenticate_header('')
|
||||
|
||||
def test_etags(self):
|
||||
assert http.quote_etag('foo') == '"foo"'
|
||||
assert http.quote_etag('foo', True) == 'w/"foo"'
|
||||
assert http.unquote_etag('"foo"') == ('foo', False)
|
||||
assert http.unquote_etag('w/"foo"') == ('foo', True)
|
||||
es = http.parse_etags('"foo", "bar", w/"baz", blar')
|
||||
assert sorted(es) == ['bar', 'blar', 'foo']
|
||||
assert 'foo' in es
|
||||
assert 'baz' not in es
|
||||
assert es.contains_weak('baz')
|
||||
assert 'blar' in es
|
||||
assert es.contains_raw('w/"baz"')
|
||||
assert es.contains_raw('"foo"')
|
||||
assert sorted(es.to_header().split(', ')) == ['"bar"', '"blar"', '"foo"', 'w/"baz"']
|
||||
|
||||
def test_etags_nonzero(self):
|
||||
etags = http.parse_etags('w/"foo"')
|
||||
self.assert_true(bool(etags))
|
||||
self.assert_true(etags.contains_raw('w/"foo"'))
|
||||
|
||||
def test_parse_date(self):
|
||||
assert http.parse_date('Sun, 06 Nov 1994 08:49:37 GMT ') == datetime(1994, 11, 6, 8, 49, 37)
|
||||
assert http.parse_date('Sunday, 06-Nov-94 08:49:37 GMT') == datetime(1994, 11, 6, 8, 49, 37)
|
||||
assert http.parse_date(' Sun Nov 6 08:49:37 1994') == datetime(1994, 11, 6, 8, 49, 37)
|
||||
assert http.parse_date('foo') is None
|
||||
|
||||
def test_parse_date_overflows(self):
|
||||
assert http.parse_date(' Sun 02 Feb 1343 08:49:37 GMT') == datetime(1343, 2, 2, 8, 49, 37)
|
||||
assert http.parse_date('Thu, 01 Jan 1970 00:00:00 GMT') == datetime(1970, 1, 1, 0, 0)
|
||||
assert http.parse_date('Thu, 33 Jan 1970 00:00:00 GMT') is None
|
||||
|
||||
def test_remove_entity_headers(self):
|
||||
now = http.http_date()
|
||||
headers1 = [('Date', now), ('Content-Type', 'text/html'), ('Content-Length', '0')]
|
||||
headers2 = datastructures.Headers(headers1)
|
||||
|
||||
http.remove_entity_headers(headers1)
|
||||
assert headers1 == [('Date', now)]
|
||||
|
||||
http.remove_entity_headers(headers2)
|
||||
self.assert_equal(headers2, datastructures.Headers([(u'Date', now)]))
|
||||
|
||||
def test_remove_hop_by_hop_headers(self):
|
||||
headers1 = [('Connection', 'closed'), ('Foo', 'bar'),
|
||||
('Keep-Alive', 'wtf')]
|
||||
headers2 = datastructures.Headers(headers1)
|
||||
|
||||
http.remove_hop_by_hop_headers(headers1)
|
||||
assert headers1 == [('Foo', 'bar')]
|
||||
|
||||
http.remove_hop_by_hop_headers(headers2)
|
||||
assert headers2 == datastructures.Headers([('Foo', 'bar')])
|
||||
|
||||
def test_parse_options_header(self):
|
||||
assert http.parse_options_header(r'something; foo="other\"thing"') == \
|
||||
('something', {'foo': 'other"thing'})
|
||||
assert http.parse_options_header(r'something; foo="other\"thing"; meh=42') == \
|
||||
('something', {'foo': 'other"thing', 'meh': '42'})
|
||||
assert http.parse_options_header(r'something; foo="other\"thing"; meh=42; bleh') == \
|
||||
('something', {'foo': 'other"thing', 'meh': '42', 'bleh': None})
|
||||
assert http.parse_options_header('something; foo="other;thing"; meh=42; bleh') == \
|
||||
('something', {'foo': 'other;thing', 'meh': '42', 'bleh': None})
|
||||
assert http.parse_options_header('something; foo="otherthing"; meh=; bleh') == \
|
||||
('something', {'foo': 'otherthing', 'meh': None, 'bleh': None})
|
||||
|
||||
|
||||
|
||||
def test_dump_options_header(self):
|
||||
assert http.dump_options_header('foo', {'bar': 42}) == \
|
||||
'foo; bar=42'
|
||||
assert http.dump_options_header('foo', {'bar': 42, 'fizz': None}) in \
|
||||
('foo; bar=42; fizz', 'foo; fizz; bar=42')
|
||||
|
||||
def test_dump_header(self):
|
||||
assert http.dump_header([1, 2, 3]) == '1, 2, 3'
|
||||
assert http.dump_header([1, 2, 3], allow_token=False) == '"1", "2", "3"'
|
||||
assert http.dump_header({'foo': 'bar'}, allow_token=False) == 'foo="bar"'
|
||||
assert http.dump_header({'foo': 'bar'}) == 'foo=bar'
|
||||
|
||||
def test_is_resource_modified(self):
|
||||
env = create_environ()
|
||||
|
||||
# ignore POST
|
||||
env['REQUEST_METHOD'] = 'POST'
|
||||
assert not http.is_resource_modified(env, etag='testing')
|
||||
env['REQUEST_METHOD'] = 'GET'
|
||||
|
||||
# etagify from data
|
||||
self.assert_raises(TypeError, http.is_resource_modified, env,
|
||||
data='42', etag='23')
|
||||
env['HTTP_IF_NONE_MATCH'] = http.generate_etag(b'awesome')
|
||||
assert not http.is_resource_modified(env, data=b'awesome')
|
||||
|
||||
env['HTTP_IF_MODIFIED_SINCE'] = http.http_date(datetime(2008, 1, 1, 12, 30))
|
||||
assert not http.is_resource_modified(env,
|
||||
last_modified=datetime(2008, 1, 1, 12, 00))
|
||||
assert http.is_resource_modified(env,
|
||||
last_modified=datetime(2008, 1, 1, 13, 00))
|
||||
|
||||
def test_date_formatting(self):
|
||||
assert http.cookie_date(0) == 'Thu, 01-Jan-1970 00:00:00 GMT'
|
||||
assert http.cookie_date(datetime(1970, 1, 1)) == 'Thu, 01-Jan-1970 00:00:00 GMT'
|
||||
assert http.http_date(0) == 'Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
assert http.http_date(datetime(1970, 1, 1)) == 'Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
|
||||
def test_cookies(self):
|
||||
self.assert_strict_equal(
|
||||
dict(http.parse_cookie('dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cd'
|
||||
'c762809248d4beed; a=42')),
|
||||
{
|
||||
'CP': u'null*',
|
||||
'PHPSESSID': u'0a539d42abc001cdc762809248d4beed',
|
||||
'a': u'42',
|
||||
'dismiss-top': u'6'
|
||||
}
|
||||
)
|
||||
self.assert_strict_equal(
|
||||
set(http.dump_cookie('foo', 'bar baz blub', 360, httponly=True,
|
||||
sync_expires=False).split(u'; ')),
|
||||
set([u'HttpOnly', u'Max-Age=360', u'Path=/', u'foo="bar baz blub"'])
|
||||
)
|
||||
self.assert_strict_equal(dict(http.parse_cookie('fo234{=bar; blub=Blah')),
|
||||
{'fo234{': u'bar', 'blub': u'Blah'})
|
||||
|
||||
def test_cookie_quoting(self):
|
||||
val = http.dump_cookie("foo", "?foo")
|
||||
self.assert_strict_equal(val, 'foo="?foo"; Path=/')
|
||||
self.assert_strict_equal(dict(http.parse_cookie(val)), {'foo': u'?foo'})
|
||||
|
||||
self.assert_strict_equal(dict(http.parse_cookie(r'foo="foo\054bar"')),
|
||||
{'foo': u'foo,bar'})
|
||||
|
||||
def test_cookie_domain_resolving(self):
|
||||
val = http.dump_cookie('foo', 'bar', domain=u'\N{SNOWMAN}.com')
|
||||
self.assert_strict_equal(val, 'foo=bar; Domain=xn--n3h.com; Path=/')
|
||||
|
||||
def test_cookie_unicode_dumping(self):
|
||||
val = http.dump_cookie('foo', u'\N{SNOWMAN}')
|
||||
h = datastructures.Headers()
|
||||
h.add('Set-Cookie', val)
|
||||
self.assert_equal(h['Set-Cookie'], 'foo="\\342\\230\\203"; Path=/')
|
||||
|
||||
cookies = http.parse_cookie(h['Set-Cookie'])
|
||||
self.assert_equal(cookies['foo'], u'\N{SNOWMAN}')
|
||||
|
||||
def test_cookie_unicode_keys(self):
|
||||
# Yes, this is technically against the spec but happens
|
||||
val = http.dump_cookie(u'fö', u'fö')
|
||||
self.assert_equal(val, wsgi_encoding_dance(u'fö="f\\303\\266"; Path=/', 'utf-8'))
|
||||
cookies = http.parse_cookie(val)
|
||||
self.assert_equal(cookies[u'fö'], u'fö')
|
||||
|
||||
def test_cookie_unicode_parsing(self):
|
||||
# This is actually a correct test. This is what is being submitted
|
||||
# by firefox if you set an unicode cookie and we get the cookie sent
|
||||
# in on Python 3 under PEP 3333.
|
||||
cookies = http.parse_cookie(u'fö=fö')
|
||||
self.assert_equal(cookies[u'fö'], u'fö')
|
||||
|
||||
def test_cookie_domain_encoding(self):
|
||||
val = http.dump_cookie('foo', 'bar', domain=u'\N{SNOWMAN}.com')
|
||||
self.assert_strict_equal(val, 'foo=bar; Domain=xn--n3h.com; Path=/')
|
||||
|
||||
val = http.dump_cookie('foo', 'bar', domain=u'.\N{SNOWMAN}.com')
|
||||
self.assert_strict_equal(val, 'foo=bar; Domain=.xn--n3h.com; Path=/')
|
||||
|
||||
val = http.dump_cookie('foo', 'bar', domain=u'.foo.com')
|
||||
self.assert_strict_equal(val, 'foo=bar; Domain=.foo.com; Path=/')
|
||||
|
||||
|
||||
class RangeTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_if_range_parsing(self):
|
||||
rv = http.parse_if_range_header('"Test"')
|
||||
assert rv.etag == 'Test'
|
||||
assert rv.date is None
|
||||
assert rv.to_header() == '"Test"'
|
||||
|
||||
# weak information is dropped
|
||||
rv = http.parse_if_range_header('w/"Test"')
|
||||
assert rv.etag == 'Test'
|
||||
assert rv.date is None
|
||||
assert rv.to_header() == '"Test"'
|
||||
|
||||
# broken etags are supported too
|
||||
rv = http.parse_if_range_header('bullshit')
|
||||
assert rv.etag == 'bullshit'
|
||||
assert rv.date is None
|
||||
assert rv.to_header() == '"bullshit"'
|
||||
|
||||
rv = http.parse_if_range_header('Thu, 01 Jan 1970 00:00:00 GMT')
|
||||
assert rv.etag is None
|
||||
assert rv.date == datetime(1970, 1, 1)
|
||||
assert rv.to_header() == 'Thu, 01 Jan 1970 00:00:00 GMT'
|
||||
|
||||
for x in '', None:
|
||||
rv = http.parse_if_range_header(x)
|
||||
assert rv.etag is None
|
||||
assert rv.date is None
|
||||
assert rv.to_header() == ''
|
||||
|
||||
def test_range_parsing():
|
||||
rv = http.parse_range_header('bytes=52')
|
||||
assert rv is None
|
||||
|
||||
rv = http.parse_range_header('bytes=52-')
|
||||
assert rv.units == 'bytes'
|
||||
assert rv.ranges == [(52, None)]
|
||||
assert rv.to_header() == 'bytes=52-'
|
||||
|
||||
rv = http.parse_range_header('bytes=52-99')
|
||||
assert rv.units == 'bytes'
|
||||
assert rv.ranges == [(52, 100)]
|
||||
assert rv.to_header() == 'bytes=52-99'
|
||||
|
||||
rv = http.parse_range_header('bytes=52-99,-1000')
|
||||
assert rv.units == 'bytes'
|
||||
assert rv.ranges == [(52, 100), (-1000, None)]
|
||||
assert rv.to_header() == 'bytes=52-99,-1000'
|
||||
|
||||
rv = http.parse_range_header('bytes = 1 - 100')
|
||||
assert rv.units == 'bytes'
|
||||
assert rv.ranges == [(1, 101)]
|
||||
assert rv.to_header() == 'bytes=1-100'
|
||||
|
||||
rv = http.parse_range_header('AWesomes=0-999')
|
||||
assert rv.units == 'awesomes'
|
||||
assert rv.ranges == [(0, 1000)]
|
||||
assert rv.to_header() == 'awesomes=0-999'
|
||||
|
||||
def test_content_range_parsing():
|
||||
rv = http.parse_content_range_header('bytes 0-98/*')
|
||||
assert rv.units == 'bytes'
|
||||
assert rv.start == 0
|
||||
assert rv.stop == 99
|
||||
assert rv.length is None
|
||||
assert rv.to_header() == 'bytes 0-98/*'
|
||||
|
||||
rv = http.parse_content_range_header('bytes 0-98/*asdfsa')
|
||||
assert rv is None
|
||||
|
||||
rv = http.parse_content_range_header('bytes 0-99/100')
|
||||
assert rv.to_header() == 'bytes 0-99/100'
|
||||
rv.start = None
|
||||
rv.stop = None
|
||||
assert rv.units == 'bytes'
|
||||
assert rv.to_header() == 'bytes */100'
|
||||
|
||||
rv = http.parse_content_range_header('bytes */100')
|
||||
assert rv.start is None
|
||||
assert rv.stop is None
|
||||
assert rv.length == 100
|
||||
assert rv.units == 'bytes'
|
||||
|
||||
|
||||
class RegressionTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_best_match_works(self):
|
||||
# was a bug in 0.6
|
||||
rv = http.parse_accept_header('foo=,application/xml,application/xhtml+xml,'
|
||||
'text/html;q=0.9,text/plain;q=0.8,'
|
||||
'image/png,*/*;q=0.5',
|
||||
datastructures.MIMEAccept).best_match(['foo/bar'])
|
||||
self.assert_equal(rv, 'foo/bar')
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(HTTPUtilityTestCase))
|
||||
suite.addTest(unittest.makeSuite(RegressionTestCase))
|
||||
return suite
|
||||
@@ -1,81 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.internal
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Internal tests.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from datetime import datetime
|
||||
from warnings import filterwarnings, resetwarnings
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
from werkzeug import _internal as internal
|
||||
from werkzeug.test import create_environ
|
||||
|
||||
|
||||
class InternalTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_date_to_unix(self):
|
||||
assert internal._date_to_unix(datetime(1970, 1, 1)) == 0
|
||||
assert internal._date_to_unix(datetime(1970, 1, 1, 1, 0, 0)) == 3600
|
||||
assert internal._date_to_unix(datetime(1970, 1, 1, 1, 1, 1)) == 3661
|
||||
x = datetime(2010, 2, 15, 16, 15, 39)
|
||||
assert internal._date_to_unix(x) == 1266250539
|
||||
|
||||
def test_easteregg(self):
|
||||
req = Request.from_values('/?macgybarchakku')
|
||||
resp = Response.force_type(internal._easteregg(None), req)
|
||||
assert b'About Werkzeug' in resp.get_data()
|
||||
assert b'the Swiss Army knife of Python web development' in resp.get_data()
|
||||
|
||||
def test_wrapper_internals(self):
|
||||
req = Request.from_values(data={'foo': 'bar'}, method='POST')
|
||||
req._load_form_data()
|
||||
assert req.form.to_dict() == {'foo': 'bar'}
|
||||
|
||||
# second call does not break
|
||||
req._load_form_data()
|
||||
assert req.form.to_dict() == {'foo': 'bar'}
|
||||
|
||||
# check reprs
|
||||
assert repr(req) == "<Request 'http://localhost/' [POST]>"
|
||||
resp = Response()
|
||||
assert repr(resp) == '<Response 0 bytes [200 OK]>'
|
||||
resp.set_data('Hello World!')
|
||||
assert repr(resp) == '<Response 12 bytes [200 OK]>'
|
||||
resp.response = iter(['Test'])
|
||||
assert repr(resp) == '<Response streamed [200 OK]>'
|
||||
|
||||
# unicode data does not set content length
|
||||
response = Response([u'Hällo Wörld'])
|
||||
headers = response.get_wsgi_headers(create_environ())
|
||||
assert u'Content-Length' not in headers
|
||||
|
||||
response = Response([u'Hällo Wörld'.encode('utf-8')])
|
||||
headers = response.get_wsgi_headers(create_environ())
|
||||
assert u'Content-Length' in headers
|
||||
|
||||
# check for internal warnings
|
||||
filterwarnings('error', category=Warning)
|
||||
response = Response()
|
||||
environ = create_environ()
|
||||
response.response = 'What the...?'
|
||||
self.assert_raises(Warning, lambda: list(response.iter_encoded()))
|
||||
self.assert_raises(Warning, lambda: list(response.get_app_iter(environ)))
|
||||
response.direct_passthrough = True
|
||||
self.assert_raises(Warning, lambda: list(response.iter_encoded()))
|
||||
self.assert_raises(Warning, lambda: list(response.get_app_iter(environ)))
|
||||
resetwarnings()
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(InternalTestCase))
|
||||
return suite
|
||||
@@ -1,159 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.local
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Local and local proxy tests.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import time
|
||||
import unittest
|
||||
from threading import Thread
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import local
|
||||
|
||||
|
||||
class LocalTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_basic_local(self):
|
||||
l = local.Local()
|
||||
l.foo = 0
|
||||
values = []
|
||||
def value_setter(idx):
|
||||
time.sleep(0.01 * idx)
|
||||
l.foo = idx
|
||||
time.sleep(0.02)
|
||||
values.append(l.foo)
|
||||
threads = [Thread(target=value_setter, args=(x,))
|
||||
for x in [1, 2, 3]]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
time.sleep(0.2)
|
||||
assert sorted(values) == [1, 2, 3]
|
||||
|
||||
def delfoo():
|
||||
del l.foo
|
||||
delfoo()
|
||||
self.assert_raises(AttributeError, lambda: l.foo)
|
||||
self.assert_raises(AttributeError, delfoo)
|
||||
|
||||
local.release_local(l)
|
||||
|
||||
def test_local_release(self):
|
||||
loc = local.Local()
|
||||
loc.foo = 42
|
||||
local.release_local(loc)
|
||||
assert not hasattr(loc, 'foo')
|
||||
|
||||
ls = local.LocalStack()
|
||||
ls.push(42)
|
||||
local.release_local(ls)
|
||||
assert ls.top is None
|
||||
|
||||
def test_local_proxy(self):
|
||||
foo = []
|
||||
ls = local.LocalProxy(lambda: foo)
|
||||
ls.append(42)
|
||||
ls.append(23)
|
||||
ls[1:] = [1, 2, 3]
|
||||
assert foo == [42, 1, 2, 3]
|
||||
assert repr(foo) == repr(ls)
|
||||
assert foo[0] == 42
|
||||
foo += [1]
|
||||
assert list(foo) == [42, 1, 2, 3, 1]
|
||||
|
||||
def test_local_proxy_operations_math(self):
|
||||
foo = 2
|
||||
ls = local.LocalProxy(lambda: foo)
|
||||
assert ls + 1 == 3
|
||||
assert 1 + ls == 3
|
||||
assert ls - 1 == 1
|
||||
assert 1 - ls == -1
|
||||
assert ls * 1 == 2
|
||||
assert 1 * ls == 2
|
||||
assert ls / 1 == 2
|
||||
assert 1.0 / ls == 0.5
|
||||
assert ls // 1.0 == 2.0
|
||||
assert 1.0 // ls == 0.0
|
||||
assert ls % 2 == 0
|
||||
assert 2 % ls == 0
|
||||
|
||||
def test_local_proxy_operations_strings(self):
|
||||
foo = "foo"
|
||||
ls = local.LocalProxy(lambda: foo)
|
||||
assert ls + "bar" == "foobar"
|
||||
assert "bar" + ls == "barfoo"
|
||||
assert ls * 2 == "foofoo"
|
||||
|
||||
foo = "foo %s"
|
||||
assert ls % ("bar",) == "foo bar"
|
||||
|
||||
def test_local_stack(self):
|
||||
ident = local.get_ident()
|
||||
|
||||
ls = local.LocalStack()
|
||||
assert ident not in ls._local.__storage__
|
||||
assert ls.top is None
|
||||
ls.push(42)
|
||||
assert ident in ls._local.__storage__
|
||||
assert ls.top == 42
|
||||
ls.push(23)
|
||||
assert ls.top == 23
|
||||
ls.pop()
|
||||
assert ls.top == 42
|
||||
ls.pop()
|
||||
assert ls.top is None
|
||||
assert ls.pop() is None
|
||||
assert ls.pop() is None
|
||||
|
||||
proxy = ls()
|
||||
ls.push([1, 2])
|
||||
assert proxy == [1, 2]
|
||||
ls.push((1, 2))
|
||||
assert proxy == (1, 2)
|
||||
ls.pop()
|
||||
ls.pop()
|
||||
assert repr(proxy) == '<LocalProxy unbound>'
|
||||
|
||||
assert ident not in ls._local.__storage__
|
||||
|
||||
def test_local_proxies_with_callables(self):
|
||||
foo = 42
|
||||
ls = local.LocalProxy(lambda: foo)
|
||||
assert ls == 42
|
||||
foo = [23]
|
||||
ls.append(42)
|
||||
assert ls == [23, 42]
|
||||
assert foo == [23, 42]
|
||||
|
||||
def test_custom_idents(self):
|
||||
ident = 0
|
||||
loc = local.Local()
|
||||
stack = local.LocalStack()
|
||||
mgr = local.LocalManager([loc, stack], ident_func=lambda: ident)
|
||||
|
||||
loc.foo = 42
|
||||
stack.push({'foo': 42})
|
||||
ident = 1
|
||||
loc.foo = 23
|
||||
stack.push({'foo': 23})
|
||||
ident = 0
|
||||
assert loc.foo == 42
|
||||
assert stack.top['foo'] == 42
|
||||
stack.pop()
|
||||
assert stack.top is None
|
||||
ident = 1
|
||||
assert loc.foo == 23
|
||||
assert stack.top['foo'] == 23
|
||||
stack.pop()
|
||||
assert stack.top is None
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(LocalTestCase))
|
||||
return suite
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Hacky helper application to collect form data.
|
||||
"""
|
||||
from werkzeug.serving import run_simple
|
||||
from werkzeug.wrappers import Request, Response
|
||||
|
||||
|
||||
def copy_stream(request):
|
||||
from os import mkdir
|
||||
from time import time
|
||||
folder = 'request-%d' % time()
|
||||
mkdir(folder)
|
||||
environ = request.environ
|
||||
f = open(folder + '/request.txt', 'wb+')
|
||||
f.write(environ['wsgi.input'].read(int(environ['CONTENT_LENGTH'])))
|
||||
f.flush()
|
||||
f.seek(0)
|
||||
environ['wsgi.input'] = f
|
||||
request.stat_folder = folder
|
||||
|
||||
|
||||
def stats(request):
|
||||
copy_stream(request)
|
||||
f1 = request.files['file1']
|
||||
f2 = request.files['file2']
|
||||
text = request.form['text']
|
||||
f1.save(request.stat_folder + '/file1.bin')
|
||||
f2.save(request.stat_folder + '/file2.bin')
|
||||
open(request.stat_folder + '/text.txt', 'w').write(text.encode('utf-8'))
|
||||
return Response('Done.')
|
||||
|
||||
|
||||
def upload_file(request):
|
||||
return Response('''
|
||||
<h1>Upload File</h1>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="file1"><br>
|
||||
<input type="file" name="file2"><br>
|
||||
<textarea name="text"></textarea><br>
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
''', mimetype='text/html')
|
||||
|
||||
|
||||
def application(environ, start_responseonse):
|
||||
request = Request(environ)
|
||||
if request.method == 'POST':
|
||||
response = stats(request)
|
||||
else:
|
||||
response = upload_file(request)
|
||||
return response(environ, start_responseonse)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_simple('localhost', 5000, application, use_debugger=True)
|
||||
|
Before Width: | Height: | Size: 523 B |
|
Before Width: | Height: | Size: 703 B |
@@ -1 +0,0 @@
|
||||
example text
|
||||
|
Before Width: | Height: | Size: 781 B |
|
Before Width: | Height: | Size: 733 B |
@@ -1,3 +0,0 @@
|
||||
--long text
|
||||
--with boundary
|
||||
--lookalikes--
|
||||
|
Before Width: | Height: | Size: 523 B |
|
Before Width: | Height: | Size: 703 B |
@@ -1 +0,0 @@
|
||||
ie6 sucks :-/
|
||||
|
Before Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 733 B |
@@ -1 +0,0 @@
|
||||
blafasel öäü
|
||||
|
Before Width: | Height: | Size: 1002 B |
|
Before Width: | Height: | Size: 952 B |
@@ -1 +0,0 @@
|
||||
this is another text with ümläüts
|
||||
@@ -1 +0,0 @@
|
||||
FOUND
|
||||
@@ -1,673 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.routing
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Routing tests.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import routing as r
|
||||
from werkzeug.wrappers import Response
|
||||
from werkzeug.datastructures import ImmutableDict
|
||||
from werkzeug.test import create_environ
|
||||
|
||||
|
||||
class RoutingTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_basic_routing(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
r.Rule('/foo', endpoint='foo'),
|
||||
r.Rule('/bar/', endpoint='bar')
|
||||
])
|
||||
adapter = map.bind('example.org', '/')
|
||||
assert adapter.match('/') == ('index', {})
|
||||
assert adapter.match('/foo') == ('foo', {})
|
||||
assert adapter.match('/bar/') == ('bar', {})
|
||||
self.assert_raises(r.RequestRedirect, lambda: adapter.match('/bar'))
|
||||
self.assert_raises(r.NotFound, lambda: adapter.match('/blub'))
|
||||
|
||||
adapter = map.bind('example.org', '/test')
|
||||
try:
|
||||
adapter.match('/bar')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://example.org/test/bar/'
|
||||
else:
|
||||
self.fail('Expected request redirect')
|
||||
|
||||
adapter = map.bind('example.org', '/')
|
||||
try:
|
||||
adapter.match('/bar')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://example.org/bar/'
|
||||
else:
|
||||
self.fail('Expected request redirect')
|
||||
|
||||
adapter = map.bind('example.org', '/')
|
||||
try:
|
||||
adapter.match('/bar', query_args={'aha': 'muhaha'})
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://example.org/bar/?aha=muhaha'
|
||||
else:
|
||||
self.fail('Expected request redirect')
|
||||
|
||||
adapter = map.bind('example.org', '/')
|
||||
try:
|
||||
adapter.match('/bar', query_args='aha=muhaha')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://example.org/bar/?aha=muhaha'
|
||||
else:
|
||||
self.fail('Expected request redirect')
|
||||
|
||||
adapter = map.bind_to_environ(create_environ('/bar?foo=bar',
|
||||
'http://example.org/'))
|
||||
try:
|
||||
adapter.match()
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://example.org/bar/?foo=bar'
|
||||
else:
|
||||
self.fail('Expected request redirect')
|
||||
|
||||
def test_environ_defaults(self):
|
||||
environ = create_environ("/foo")
|
||||
self.assert_strict_equal(environ["PATH_INFO"], '/foo')
|
||||
m = r.Map([r.Rule("/foo", endpoint="foo"), r.Rule("/bar", endpoint="bar")])
|
||||
a = m.bind_to_environ(environ)
|
||||
self.assert_strict_equal(a.match("/foo"), ('foo', {}))
|
||||
self.assert_strict_equal(a.match(), ('foo', {}))
|
||||
self.assert_strict_equal(a.match("/bar"), ('bar', {}))
|
||||
self.assert_raises(r.NotFound, a.match, "/bars")
|
||||
|
||||
def test_environ_nonascii_pathinfo(self):
|
||||
environ = create_environ(u'/лошадь')
|
||||
m = r.Map([
|
||||
r.Rule(u'/', endpoint='index'),
|
||||
r.Rule(u'/лошадь', endpoint='horse')
|
||||
])
|
||||
a = m.bind_to_environ(environ)
|
||||
self.assert_strict_equal(a.match(u'/'), ('index', {}))
|
||||
self.assert_strict_equal(a.match(u'/лошадь'), ('horse', {}))
|
||||
self.assert_raises(r.NotFound, a.match, u'/барсук')
|
||||
|
||||
def test_basic_building(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
r.Rule('/foo', endpoint='foo'),
|
||||
r.Rule('/bar/<baz>', endpoint='bar'),
|
||||
r.Rule('/bar/<int:bazi>', endpoint='bari'),
|
||||
r.Rule('/bar/<float:bazf>', endpoint='barf'),
|
||||
r.Rule('/bar/<path:bazp>', endpoint='barp'),
|
||||
r.Rule('/hehe', endpoint='blah', subdomain='blah')
|
||||
])
|
||||
adapter = map.bind('example.org', '/', subdomain='blah')
|
||||
|
||||
assert adapter.build('index', {}) == 'http://example.org/'
|
||||
assert adapter.build('foo', {}) == 'http://example.org/foo'
|
||||
assert adapter.build('bar', {'baz': 'blub'}) == 'http://example.org/bar/blub'
|
||||
assert adapter.build('bari', {'bazi': 50}) == 'http://example.org/bar/50'
|
||||
assert adapter.build('barf', {'bazf': 0.815}) == 'http://example.org/bar/0.815'
|
||||
assert adapter.build('barp', {'bazp': 'la/di'}) == 'http://example.org/bar/la/di'
|
||||
assert adapter.build('blah', {}) == '/hehe'
|
||||
self.assert_raises(r.BuildError, lambda: adapter.build('urks'))
|
||||
|
||||
adapter = map.bind('example.org', '/test', subdomain='blah')
|
||||
assert adapter.build('index', {}) == 'http://example.org/test/'
|
||||
assert adapter.build('foo', {}) == 'http://example.org/test/foo'
|
||||
assert adapter.build('bar', {'baz': 'blub'}) == 'http://example.org/test/bar/blub'
|
||||
assert adapter.build('bari', {'bazi': 50}) == 'http://example.org/test/bar/50'
|
||||
assert adapter.build('barf', {'bazf': 0.815}) == 'http://example.org/test/bar/0.815'
|
||||
assert adapter.build('barp', {'bazp': 'la/di'}) == 'http://example.org/test/bar/la/di'
|
||||
assert adapter.build('blah', {}) == '/test/hehe'
|
||||
|
||||
def test_defaults(self):
|
||||
map = r.Map([
|
||||
r.Rule('/foo/', defaults={'page': 1}, endpoint='foo'),
|
||||
r.Rule('/foo/<int:page>', endpoint='foo')
|
||||
])
|
||||
adapter = map.bind('example.org', '/')
|
||||
|
||||
assert adapter.match('/foo/') == ('foo', {'page': 1})
|
||||
self.assert_raises(r.RequestRedirect, lambda: adapter.match('/foo/1'))
|
||||
assert adapter.match('/foo/2') == ('foo', {'page': 2})
|
||||
assert adapter.build('foo', {}) == '/foo/'
|
||||
assert adapter.build('foo', {'page': 1}) == '/foo/'
|
||||
assert adapter.build('foo', {'page': 2}) == '/foo/2'
|
||||
|
||||
def test_greedy(self):
|
||||
map = r.Map([
|
||||
r.Rule('/foo', endpoint='foo'),
|
||||
r.Rule('/<path:bar>', endpoint='bar'),
|
||||
r.Rule('/<path:bar>/<path:blub>', endpoint='bar')
|
||||
])
|
||||
adapter = map.bind('example.org', '/')
|
||||
|
||||
assert adapter.match('/foo') == ('foo', {})
|
||||
assert adapter.match('/blub') == ('bar', {'bar': 'blub'})
|
||||
assert adapter.match('/he/he') == ('bar', {'bar': 'he', 'blub': 'he'})
|
||||
|
||||
assert adapter.build('foo', {}) == '/foo'
|
||||
assert adapter.build('bar', {'bar': 'blub'}) == '/blub'
|
||||
assert adapter.build('bar', {'bar': 'blub', 'blub': 'bar'}) == '/blub/bar'
|
||||
|
||||
def test_path(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', defaults={'name': 'FrontPage'}, endpoint='page'),
|
||||
r.Rule('/Special', endpoint='special'),
|
||||
r.Rule('/<int:year>', endpoint='year'),
|
||||
r.Rule('/<path:name>', endpoint='page'),
|
||||
r.Rule('/<path:name>/edit', endpoint='editpage'),
|
||||
r.Rule('/<path:name>/silly/<path:name2>', endpoint='sillypage'),
|
||||
r.Rule('/<path:name>/silly/<path:name2>/edit', endpoint='editsillypage'),
|
||||
r.Rule('/Talk:<path:name>', endpoint='talk'),
|
||||
r.Rule('/User:<username>', endpoint='user'),
|
||||
r.Rule('/User:<username>/<path:name>', endpoint='userpage'),
|
||||
r.Rule('/Files/<path:file>', endpoint='files'),
|
||||
])
|
||||
adapter = map.bind('example.org', '/')
|
||||
|
||||
assert adapter.match('/') == ('page', {'name':'FrontPage'})
|
||||
self.assert_raises(r.RequestRedirect, lambda: adapter.match('/FrontPage'))
|
||||
assert adapter.match('/Special') == ('special', {})
|
||||
assert adapter.match('/2007') == ('year', {'year':2007})
|
||||
assert adapter.match('/Some/Page') == ('page', {'name':'Some/Page'})
|
||||
assert adapter.match('/Some/Page/edit') == ('editpage', {'name':'Some/Page'})
|
||||
assert adapter.match('/Foo/silly/bar') == ('sillypage', {'name':'Foo', 'name2':'bar'})
|
||||
assert adapter.match('/Foo/silly/bar/edit') == ('editsillypage', {'name':'Foo', 'name2':'bar'})
|
||||
assert adapter.match('/Talk:Foo/Bar') == ('talk', {'name':'Foo/Bar'})
|
||||
assert adapter.match('/User:thomas') == ('user', {'username':'thomas'})
|
||||
assert adapter.match('/User:thomas/projects/werkzeug') == \
|
||||
('userpage', {'username':'thomas', 'name':'projects/werkzeug'})
|
||||
assert adapter.match('/Files/downloads/werkzeug/0.2.zip') == \
|
||||
('files', {'file':'downloads/werkzeug/0.2.zip'})
|
||||
|
||||
def test_dispatch(self):
|
||||
env = create_environ('/')
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='root'),
|
||||
r.Rule('/foo/', endpoint='foo')
|
||||
])
|
||||
adapter = map.bind_to_environ(env)
|
||||
|
||||
raise_this = None
|
||||
def view_func(endpoint, values):
|
||||
if raise_this is not None:
|
||||
raise raise_this
|
||||
return Response(repr((endpoint, values)))
|
||||
dispatch = lambda p, q=False: Response.force_type(adapter.dispatch(view_func, p,
|
||||
catch_http_exceptions=q), env)
|
||||
|
||||
assert dispatch('/').data == b"('root', {})"
|
||||
assert dispatch('/foo').status_code == 301
|
||||
raise_this = r.NotFound()
|
||||
self.assert_raises(r.NotFound, lambda: dispatch('/bar'))
|
||||
assert dispatch('/bar', True).status_code == 404
|
||||
|
||||
def test_http_host_before_server_name(self):
|
||||
env = {
|
||||
'HTTP_HOST': 'wiki.example.com',
|
||||
'SERVER_NAME': 'web0.example.com',
|
||||
'SERVER_PORT': '80',
|
||||
'SCRIPT_NAME': '',
|
||||
'PATH_INFO': '',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'wsgi.url_scheme': 'http'
|
||||
}
|
||||
map = r.Map([r.Rule('/', endpoint='index', subdomain='wiki')])
|
||||
adapter = map.bind_to_environ(env, server_name='example.com')
|
||||
assert adapter.match('/') == ('index', {})
|
||||
assert adapter.build('index', force_external=True) == 'http://wiki.example.com/'
|
||||
assert adapter.build('index') == '/'
|
||||
|
||||
env['HTTP_HOST'] = 'admin.example.com'
|
||||
adapter = map.bind_to_environ(env, server_name='example.com')
|
||||
assert adapter.build('index') == 'http://wiki.example.com/'
|
||||
|
||||
def test_adapter_url_parameter_sorting(self):
|
||||
map = r.Map([r.Rule('/', endpoint='index')], sort_parameters=True,
|
||||
sort_key=lambda x: x[1])
|
||||
adapter = map.bind('localhost', '/')
|
||||
assert adapter.build('index', {'x': 20, 'y': 10, 'z': 30},
|
||||
force_external=True) == 'http://localhost/?y=10&x=20&z=30'
|
||||
|
||||
def test_request_direct_charset_bug(self):
|
||||
map = r.Map([r.Rule(u'/öäü/')])
|
||||
adapter = map.bind('localhost', '/')
|
||||
try:
|
||||
adapter.match(u'/öäü')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://localhost/%C3%B6%C3%A4%C3%BC/'
|
||||
else:
|
||||
self.fail('expected request redirect exception')
|
||||
|
||||
def test_request_redirect_default(self):
|
||||
map = r.Map([r.Rule(u'/foo', defaults={'bar': 42}),
|
||||
r.Rule(u'/foo/<int:bar>')])
|
||||
adapter = map.bind('localhost', '/')
|
||||
try:
|
||||
adapter.match(u'/foo/42')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://localhost/foo'
|
||||
else:
|
||||
self.fail('expected request redirect exception')
|
||||
|
||||
def test_request_redirect_default_subdomain(self):
|
||||
map = r.Map([r.Rule(u'/foo', defaults={'bar': 42}, subdomain='test'),
|
||||
r.Rule(u'/foo/<int:bar>', subdomain='other')])
|
||||
adapter = map.bind('localhost', '/', subdomain='other')
|
||||
try:
|
||||
adapter.match(u'/foo/42')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://test.localhost/foo'
|
||||
else:
|
||||
self.fail('expected request redirect exception')
|
||||
|
||||
def test_adapter_match_return_rule(self):
|
||||
rule = r.Rule('/foo/', endpoint='foo')
|
||||
map = r.Map([rule])
|
||||
adapter = map.bind('localhost', '/')
|
||||
assert adapter.match('/foo/', return_rule=True) == (rule, {})
|
||||
|
||||
def test_server_name_interpolation(self):
|
||||
server_name = 'example.invalid'
|
||||
map = r.Map([r.Rule('/', endpoint='index'),
|
||||
r.Rule('/', endpoint='alt', subdomain='alt')])
|
||||
|
||||
env = create_environ('/', 'http://%s/' % server_name)
|
||||
adapter = map.bind_to_environ(env, server_name=server_name)
|
||||
assert adapter.match() == ('index', {})
|
||||
|
||||
env = create_environ('/', 'http://alt.%s/' % server_name)
|
||||
adapter = map.bind_to_environ(env, server_name=server_name)
|
||||
assert adapter.match() == ('alt', {})
|
||||
|
||||
env = create_environ('/', 'http://%s/' % server_name)
|
||||
adapter = map.bind_to_environ(env, server_name='foo')
|
||||
assert adapter.subdomain == '<invalid>'
|
||||
|
||||
def test_rule_emptying(self):
|
||||
rule = r.Rule('/foo', {'meh': 'muh'}, 'x', ['POST'],
|
||||
False, 'x', True, None)
|
||||
rule2 = rule.empty()
|
||||
assert rule.__dict__ == rule2.__dict__
|
||||
rule.methods.add('GET')
|
||||
assert rule.__dict__ != rule2.__dict__
|
||||
rule.methods.discard('GET')
|
||||
rule.defaults['meh'] = 'aha'
|
||||
assert rule.__dict__ != rule2.__dict__
|
||||
|
||||
def test_rule_templates(self):
|
||||
testcase = r.RuleTemplate(
|
||||
[ r.Submount('/test/$app',
|
||||
[ r.Rule('/foo/', endpoint='handle_foo')
|
||||
, r.Rule('/bar/', endpoint='handle_bar')
|
||||
, r.Rule('/baz/', endpoint='handle_baz')
|
||||
]),
|
||||
r.EndpointPrefix('${app}',
|
||||
[ r.Rule('/${app}-blah', endpoint='bar')
|
||||
, r.Rule('/${app}-meh', endpoint='baz')
|
||||
]),
|
||||
r.Subdomain('$app',
|
||||
[ r.Rule('/blah', endpoint='x_bar')
|
||||
, r.Rule('/meh', endpoint='x_baz')
|
||||
])
|
||||
])
|
||||
|
||||
url_map = r.Map(
|
||||
[ testcase(app='test1')
|
||||
, testcase(app='test2')
|
||||
, testcase(app='test3')
|
||||
, testcase(app='test4')
|
||||
])
|
||||
|
||||
out = sorted([(x.rule, x.subdomain, x.endpoint)
|
||||
for x in url_map.iter_rules()])
|
||||
|
||||
assert out == ([
|
||||
('/blah', 'test1', 'x_bar'),
|
||||
('/blah', 'test2', 'x_bar'),
|
||||
('/blah', 'test3', 'x_bar'),
|
||||
('/blah', 'test4', 'x_bar'),
|
||||
('/meh', 'test1', 'x_baz'),
|
||||
('/meh', 'test2', 'x_baz'),
|
||||
('/meh', 'test3', 'x_baz'),
|
||||
('/meh', 'test4', 'x_baz'),
|
||||
('/test/test1/bar/', '', 'handle_bar'),
|
||||
('/test/test1/baz/', '', 'handle_baz'),
|
||||
('/test/test1/foo/', '', 'handle_foo'),
|
||||
('/test/test2/bar/', '', 'handle_bar'),
|
||||
('/test/test2/baz/', '', 'handle_baz'),
|
||||
('/test/test2/foo/', '', 'handle_foo'),
|
||||
('/test/test3/bar/', '', 'handle_bar'),
|
||||
('/test/test3/baz/', '', 'handle_baz'),
|
||||
('/test/test3/foo/', '', 'handle_foo'),
|
||||
('/test/test4/bar/', '', 'handle_bar'),
|
||||
('/test/test4/baz/', '', 'handle_baz'),
|
||||
('/test/test4/foo/', '', 'handle_foo'),
|
||||
('/test1-blah', '', 'test1bar'),
|
||||
('/test1-meh', '', 'test1baz'),
|
||||
('/test2-blah', '', 'test2bar'),
|
||||
('/test2-meh', '', 'test2baz'),
|
||||
('/test3-blah', '', 'test3bar'),
|
||||
('/test3-meh', '', 'test3baz'),
|
||||
('/test4-blah', '', 'test4bar'),
|
||||
('/test4-meh', '', 'test4baz')
|
||||
])
|
||||
|
||||
def test_non_string_parts(self):
|
||||
m = r.Map([
|
||||
r.Rule('/<foo>', endpoint='foo')
|
||||
])
|
||||
a = m.bind('example.com')
|
||||
self.assert_equal(a.build('foo', {'foo': 42}), '/42')
|
||||
|
||||
def test_complex_routing_rules(self):
|
||||
m = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
r.Rule('/<int:blub>', endpoint='an_int'),
|
||||
r.Rule('/<blub>', endpoint='a_string'),
|
||||
r.Rule('/foo/', endpoint='nested'),
|
||||
r.Rule('/foobar/', endpoint='nestedbar'),
|
||||
r.Rule('/foo/<path:testing>/', endpoint='nested_show'),
|
||||
r.Rule('/foo/<path:testing>/edit', endpoint='nested_edit'),
|
||||
r.Rule('/users/', endpoint='users', defaults={'page': 1}),
|
||||
r.Rule('/users/page/<int:page>', endpoint='users'),
|
||||
r.Rule('/foox', endpoint='foox'),
|
||||
r.Rule('/<path:bar>/<path:blub>', endpoint='barx_path_path')
|
||||
])
|
||||
a = m.bind('example.com')
|
||||
|
||||
assert a.match('/') == ('index', {})
|
||||
assert a.match('/42') == ('an_int', {'blub': 42})
|
||||
assert a.match('/blub') == ('a_string', {'blub': 'blub'})
|
||||
assert a.match('/foo/') == ('nested', {})
|
||||
assert a.match('/foobar/') == ('nestedbar', {})
|
||||
assert a.match('/foo/1/2/3/') == ('nested_show', {'testing': '1/2/3'})
|
||||
assert a.match('/foo/1/2/3/edit') == ('nested_edit', {'testing': '1/2/3'})
|
||||
assert a.match('/users/') == ('users', {'page': 1})
|
||||
assert a.match('/users/page/2') == ('users', {'page': 2})
|
||||
assert a.match('/foox') == ('foox', {})
|
||||
assert a.match('/1/2/3') == ('barx_path_path', {'bar': '1', 'blub': '2/3'})
|
||||
|
||||
assert a.build('index') == '/'
|
||||
assert a.build('an_int', {'blub': 42}) == '/42'
|
||||
assert a.build('a_string', {'blub': 'test'}) == '/test'
|
||||
assert a.build('nested') == '/foo/'
|
||||
assert a.build('nestedbar') == '/foobar/'
|
||||
assert a.build('nested_show', {'testing': '1/2/3'}) == '/foo/1/2/3/'
|
||||
assert a.build('nested_edit', {'testing': '1/2/3'}) == '/foo/1/2/3/edit'
|
||||
assert a.build('users', {'page': 1}) == '/users/'
|
||||
assert a.build('users', {'page': 2}) == '/users/page/2'
|
||||
assert a.build('foox') == '/foox'
|
||||
assert a.build('barx_path_path', {'bar': '1', 'blub': '2/3'}) == '/1/2/3'
|
||||
|
||||
def test_default_converters(self):
|
||||
class MyMap(r.Map):
|
||||
default_converters = r.Map.default_converters.copy()
|
||||
default_converters['foo'] = r.UnicodeConverter
|
||||
assert isinstance(r.Map.default_converters, ImmutableDict)
|
||||
m = MyMap([
|
||||
r.Rule('/a/<foo:a>', endpoint='a'),
|
||||
r.Rule('/b/<foo:b>', endpoint='b'),
|
||||
r.Rule('/c/<c>', endpoint='c')
|
||||
], converters={'bar': r.UnicodeConverter})
|
||||
a = m.bind('example.org', '/')
|
||||
assert a.match('/a/1') == ('a', {'a': '1'})
|
||||
assert a.match('/b/2') == ('b', {'b': '2'})
|
||||
assert a.match('/c/3') == ('c', {'c': '3'})
|
||||
assert 'foo' not in r.Map.default_converters
|
||||
|
||||
def test_build_append_unknown(self):
|
||||
map = r.Map([
|
||||
r.Rule('/bar/<float:bazf>', endpoint='barf')
|
||||
])
|
||||
adapter = map.bind('example.org', '/', subdomain='blah')
|
||||
assert adapter.build('barf', {'bazf': 0.815, 'bif' : 1.0}) == \
|
||||
'http://example.org/bar/0.815?bif=1.0'
|
||||
assert adapter.build('barf', {'bazf': 0.815, 'bif' : 1.0},
|
||||
append_unknown=False) == 'http://example.org/bar/0.815'
|
||||
|
||||
def test_method_fallback(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='index', methods=['GET']),
|
||||
r.Rule('/<name>', endpoint='hello_name', methods=['GET']),
|
||||
r.Rule('/select', endpoint='hello_select', methods=['POST']),
|
||||
r.Rule('/search_get', endpoint='search', methods=['GET']),
|
||||
r.Rule('/search_post', endpoint='search', methods=['POST'])
|
||||
])
|
||||
adapter = map.bind('example.com')
|
||||
assert adapter.build('index') == '/'
|
||||
assert adapter.build('index', method='GET') == '/'
|
||||
assert adapter.build('hello_name', {'name': 'foo'}) == '/foo'
|
||||
assert adapter.build('hello_select') == '/select'
|
||||
assert adapter.build('hello_select', method='POST') == '/select'
|
||||
assert adapter.build('search') == '/search_get'
|
||||
assert adapter.build('search', method='GET') == '/search_get'
|
||||
assert adapter.build('search', method='POST') == '/search_post'
|
||||
|
||||
def test_implicit_head(self):
|
||||
url_map = r.Map([
|
||||
r.Rule('/get', methods=['GET'], endpoint='a'),
|
||||
r.Rule('/post', methods=['POST'], endpoint='b')
|
||||
])
|
||||
adapter = url_map.bind('example.org')
|
||||
assert adapter.match('/get', method='HEAD') == ('a', {})
|
||||
self.assert_raises(r.MethodNotAllowed, adapter.match,
|
||||
'/post', method='HEAD')
|
||||
|
||||
def test_protocol_joining_bug(self):
|
||||
m = r.Map([r.Rule('/<foo>', endpoint='x')])
|
||||
a = m.bind('example.org')
|
||||
assert a.build('x', {'foo': 'x:y'}) == '/x:y'
|
||||
assert a.build('x', {'foo': 'x:y'}, force_external=True) == \
|
||||
'http://example.org/x:y'
|
||||
|
||||
def test_allowed_methods_querying(self):
|
||||
m = r.Map([r.Rule('/<foo>', methods=['GET', 'HEAD']),
|
||||
r.Rule('/foo', methods=['POST'])])
|
||||
a = m.bind('example.org')
|
||||
assert sorted(a.allowed_methods('/foo')) == ['GET', 'HEAD', 'POST']
|
||||
|
||||
def test_external_building_with_port(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
])
|
||||
adapter = map.bind('example.org:5000', '/')
|
||||
built_url = adapter.build('index', {}, force_external=True)
|
||||
assert built_url == 'http://example.org:5000/', built_url
|
||||
|
||||
def test_external_building_with_port_bind_to_environ(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
])
|
||||
adapter = map.bind_to_environ(
|
||||
create_environ('/', 'http://example.org:5000/'),
|
||||
server_name="example.org:5000"
|
||||
)
|
||||
built_url = adapter.build('index', {}, force_external=True)
|
||||
assert built_url == 'http://example.org:5000/', built_url
|
||||
|
||||
def test_external_building_with_port_bind_to_environ_wrong_servername(self):
|
||||
map = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
])
|
||||
environ = create_environ('/', 'http://example.org:5000/')
|
||||
adapter = map.bind_to_environ(environ, server_name="example.org")
|
||||
assert adapter.subdomain == '<invalid>'
|
||||
|
||||
def test_converter_parser(self):
|
||||
args, kwargs = r.parse_converter_args(u'test, a=1, b=3.0')
|
||||
|
||||
assert args == ('test',)
|
||||
assert kwargs == {'a': 1, 'b': 3.0 }
|
||||
|
||||
args, kwargs = r.parse_converter_args('')
|
||||
assert not args and not kwargs
|
||||
|
||||
args, kwargs = r.parse_converter_args('a, b, c,')
|
||||
assert args == ('a', 'b', 'c')
|
||||
assert not kwargs
|
||||
|
||||
args, kwargs = r.parse_converter_args('True, False, None')
|
||||
assert args == (True, False, None)
|
||||
|
||||
args, kwargs = r.parse_converter_args('"foo", u"bar"')
|
||||
assert args == ('foo', 'bar')
|
||||
|
||||
def test_alias_redirects(self):
|
||||
m = r.Map([
|
||||
r.Rule('/', endpoint='index'),
|
||||
r.Rule('/index.html', endpoint='index', alias=True),
|
||||
r.Rule('/users/', defaults={'page': 1}, endpoint='users'),
|
||||
r.Rule('/users/index.html', defaults={'page': 1}, alias=True,
|
||||
endpoint='users'),
|
||||
r.Rule('/users/page/<int:page>', endpoint='users'),
|
||||
r.Rule('/users/page-<int:page>.html', alias=True, endpoint='users'),
|
||||
])
|
||||
a = m.bind('example.com')
|
||||
|
||||
def ensure_redirect(path, new_url, args=None):
|
||||
try:
|
||||
a.match(path, query_args=args)
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://example.com' + new_url
|
||||
else:
|
||||
assert False, 'expected redirect'
|
||||
|
||||
ensure_redirect('/index.html', '/')
|
||||
ensure_redirect('/users/index.html', '/users/')
|
||||
ensure_redirect('/users/page-2.html', '/users/page/2')
|
||||
ensure_redirect('/users/page-1.html', '/users/')
|
||||
ensure_redirect('/users/page-1.html', '/users/?foo=bar', {'foo': 'bar'})
|
||||
|
||||
assert a.build('index') == '/'
|
||||
assert a.build('users', {'page': 1}) == '/users/'
|
||||
assert a.build('users', {'page': 2}) == '/users/page/2'
|
||||
|
||||
def test_double_defaults(self):
|
||||
for prefix in '', '/aaa':
|
||||
m = r.Map([
|
||||
r.Rule(prefix + '/', defaults={'foo': 1, 'bar': False}, endpoint='x'),
|
||||
r.Rule(prefix + '/<int:foo>', defaults={'bar': False}, endpoint='x'),
|
||||
r.Rule(prefix + '/bar/', defaults={'foo': 1, 'bar': True}, endpoint='x'),
|
||||
r.Rule(prefix + '/bar/<int:foo>', defaults={'bar': True}, endpoint='x')
|
||||
])
|
||||
a = m.bind('example.com')
|
||||
|
||||
assert a.match(prefix + '/') == ('x', {'foo': 1, 'bar': False})
|
||||
assert a.match(prefix + '/2') == ('x', {'foo': 2, 'bar': False})
|
||||
assert a.match(prefix + '/bar/') == ('x', {'foo': 1, 'bar': True})
|
||||
assert a.match(prefix + '/bar/2') == ('x', {'foo': 2, 'bar': True})
|
||||
|
||||
assert a.build('x', {'foo': 1, 'bar': False}) == prefix + '/'
|
||||
assert a.build('x', {'foo': 2, 'bar': False}) == prefix + '/2'
|
||||
assert a.build('x', {'bar': False}) == prefix + '/'
|
||||
assert a.build('x', {'foo': 1, 'bar': True}) == prefix + '/bar/'
|
||||
assert a.build('x', {'foo': 2, 'bar': True}) == prefix + '/bar/2'
|
||||
assert a.build('x', {'bar': True}) == prefix + '/bar/'
|
||||
|
||||
def test_host_matching(self):
|
||||
m = r.Map([
|
||||
r.Rule('/', endpoint='index', host='www.<domain>'),
|
||||
r.Rule('/', endpoint='files', host='files.<domain>'),
|
||||
r.Rule('/foo/', defaults={'page': 1}, host='www.<domain>', endpoint='x'),
|
||||
r.Rule('/<int:page>', host='files.<domain>', endpoint='x')
|
||||
], host_matching=True)
|
||||
|
||||
a = m.bind('www.example.com')
|
||||
assert a.match('/') == ('index', {'domain': 'example.com'})
|
||||
assert a.match('/foo/') == ('x', {'domain': 'example.com', 'page': 1})
|
||||
try:
|
||||
a.match('/foo')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://www.example.com/foo/'
|
||||
else:
|
||||
assert False, 'expected redirect'
|
||||
|
||||
a = m.bind('files.example.com')
|
||||
assert a.match('/') == ('files', {'domain': 'example.com'})
|
||||
assert a.match('/2') == ('x', {'domain': 'example.com', 'page': 2})
|
||||
try:
|
||||
a.match('/1')
|
||||
except r.RequestRedirect as e:
|
||||
assert e.new_url == 'http://www.example.com/foo/'
|
||||
else:
|
||||
assert False, 'expected redirect'
|
||||
|
||||
def test_server_name_casing(self):
|
||||
m = r.Map([
|
||||
r.Rule('/', endpoint='index', subdomain='foo')
|
||||
])
|
||||
|
||||
env = create_environ()
|
||||
env['SERVER_NAME'] = env['HTTP_HOST'] = 'FOO.EXAMPLE.COM'
|
||||
a = m.bind_to_environ(env, server_name='example.com')
|
||||
assert a.match('/') == ('index', {})
|
||||
|
||||
env = create_environ()
|
||||
env['SERVER_NAME'] = '127.0.0.1'
|
||||
env['SERVER_PORT'] = '5000'
|
||||
del env['HTTP_HOST']
|
||||
a = m.bind_to_environ(env, server_name='example.com')
|
||||
try:
|
||||
a.match()
|
||||
except r.NotFound:
|
||||
pass
|
||||
else:
|
||||
assert False, 'Expected not found exception'
|
||||
|
||||
def test_redirect_request_exception_code(self):
|
||||
exc = r.RequestRedirect('http://www.google.com/')
|
||||
exc.code = 307
|
||||
env = create_environ()
|
||||
self.assert_strict_equal(exc.get_response(env).status_code, exc.code)
|
||||
|
||||
def test_unicode_rules(self):
|
||||
m = r.Map([
|
||||
r.Rule(u'/войти/', endpoint='enter'),
|
||||
r.Rule(u'/foo+bar/', endpoint='foobar')
|
||||
])
|
||||
a = m.bind(u'☃.example.com')
|
||||
try:
|
||||
a.match(u'/войти')
|
||||
except r.RequestRedirect as e:
|
||||
self.assert_strict_equal(e.new_url, 'http://xn--n3h.example.com/'
|
||||
'%D0%B2%D0%BE%D0%B9%D1%82%D0%B8/')
|
||||
endpoint, values = a.match(u'/войти/')
|
||||
self.assert_strict_equal(endpoint, 'enter')
|
||||
self.assert_strict_equal(values, {})
|
||||
|
||||
try:
|
||||
a.match(u'/foo+bar')
|
||||
except r.RequestRedirect as e:
|
||||
self.assert_strict_equal(e.new_url, 'http://xn--n3h.example.com/'
|
||||
'foo+bar/')
|
||||
endpoint, values = a.match(u'/foo+bar/')
|
||||
self.assert_strict_equal(endpoint, 'foobar')
|
||||
self.assert_strict_equal(values, {})
|
||||
|
||||
url = a.build('enter', {}, force_external=True)
|
||||
self.assert_strict_equal(url, 'http://xn--n3h.example.com/%D0%B2%D0%BE%D0%B9%D1%82%D0%B8/')
|
||||
|
||||
url = a.build('foobar', {}, force_external=True)
|
||||
self.assert_strict_equal(url, 'http://xn--n3h.example.com/foo+bar/')
|
||||
|
||||
def test_map_repr(self):
|
||||
m = r.Map([
|
||||
r.Rule(u'/wat', endpoint='enter'),
|
||||
r.Rule(u'/woop', endpoint='foobar')
|
||||
])
|
||||
rv = repr(m)
|
||||
self.assert_strict_equal(rv,
|
||||
"Map([<Rule '/woop' -> foobar>, <Rule '/wat' -> enter>])")
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(RoutingTestCase))
|
||||
return suite
|
||||
@@ -1,97 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.security
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the security helpers.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug.security import check_password_hash, generate_password_hash, \
|
||||
safe_join, pbkdf2_hex
|
||||
|
||||
|
||||
class SecurityTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_password_hashing(self):
|
||||
hash0 = generate_password_hash('default')
|
||||
assert check_password_hash(hash0, 'default')
|
||||
assert hash0.startswith('pbkdf2:sha1:1000$')
|
||||
|
||||
hash1 = generate_password_hash('default', 'sha1')
|
||||
hash2 = generate_password_hash(u'default', method='sha1')
|
||||
assert hash1 != hash2
|
||||
assert check_password_hash(hash1, 'default')
|
||||
assert check_password_hash(hash2, 'default')
|
||||
assert hash1.startswith('sha1$')
|
||||
assert hash2.startswith('sha1$')
|
||||
|
||||
fakehash = generate_password_hash('default', method='plain')
|
||||
assert fakehash == 'plain$$default'
|
||||
assert check_password_hash(fakehash, 'default')
|
||||
|
||||
mhash = generate_password_hash(u'default', method='md5')
|
||||
assert mhash.startswith('md5$')
|
||||
assert check_password_hash(mhash, 'default')
|
||||
|
||||
legacy = 'md5$$c21f969b5f03d33d43e04f8f136e7682'
|
||||
assert check_password_hash(legacy, 'default')
|
||||
|
||||
legacy = u'md5$$c21f969b5f03d33d43e04f8f136e7682'
|
||||
assert check_password_hash(legacy, 'default')
|
||||
|
||||
def test_safe_join(self):
|
||||
assert safe_join('foo', 'bar/baz') == os.path.join('foo', 'bar/baz')
|
||||
assert safe_join('foo', '../bar/baz') is None
|
||||
if os.name == 'nt':
|
||||
assert safe_join('foo', 'foo\\bar') is None
|
||||
|
||||
def test_pbkdf2(self):
|
||||
def check(data, salt, iterations, keylen, expected):
|
||||
rv = pbkdf2_hex(data, salt, iterations, keylen)
|
||||
self.assert_equal(rv, expected)
|
||||
|
||||
# From RFC 6070
|
||||
check('password', 'salt', 1, None,
|
||||
'0c60c80f961f0e71f3a9b524af6012062fe037a6')
|
||||
check('password', 'salt', 1, 20,
|
||||
'0c60c80f961f0e71f3a9b524af6012062fe037a6')
|
||||
check('password', 'salt', 2, 20,
|
||||
'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957')
|
||||
check('password', 'salt', 4096, 20,
|
||||
'4b007901b765489abead49d926f721d065a429c1')
|
||||
check('passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt',
|
||||
4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038')
|
||||
check('pass\x00word', 'sa\x00lt', 4096, 16,
|
||||
'56fa6aa75548099dcc37d7f03425e0c3')
|
||||
# This one is from the RFC but it just takes for ages
|
||||
##check('password', 'salt', 16777216, 20,
|
||||
## 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984')
|
||||
|
||||
# From Crypt-PBKDF2
|
||||
check('password', 'ATHENA.MIT.EDUraeburn', 1, 16,
|
||||
'cdedb5281bb2f801565a1122b2563515')
|
||||
check('password', 'ATHENA.MIT.EDUraeburn', 1, 32,
|
||||
'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837')
|
||||
check('password', 'ATHENA.MIT.EDUraeburn', 2, 16,
|
||||
'01dbee7f4a9e243e988b62c73cda935d')
|
||||
check('password', 'ATHENA.MIT.EDUraeburn', 2, 32,
|
||||
'01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86')
|
||||
check('password', 'ATHENA.MIT.EDUraeburn', 1200, 32,
|
||||
'5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13')
|
||||
check('X' * 64, 'pass phrase equals block size', 1200, 32,
|
||||
'139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1')
|
||||
check('X' * 65, 'pass phrase exceeds block size', 1200, 32,
|
||||
'9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a')
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(SecurityTestCase))
|
||||
return suite
|
||||
@@ -1,117 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.serving
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Added serving tests.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
import time
|
||||
try:
|
||||
import httplib
|
||||
except ImportError:
|
||||
from http import client as httplib
|
||||
try:
|
||||
from urllib2 import urlopen, HTTPError
|
||||
except ImportError: # pragma: no cover
|
||||
from urllib.request import urlopen
|
||||
from urllib.error import HTTPError
|
||||
|
||||
import unittest
|
||||
from functools import update_wrapper
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import __version__ as version, serving
|
||||
from werkzeug.testapp import test_app
|
||||
from werkzeug._compat import StringIO
|
||||
from threading import Thread
|
||||
|
||||
|
||||
|
||||
real_make_server = serving.make_server
|
||||
|
||||
|
||||
def silencestderr(f):
|
||||
def new_func(*args, **kwargs):
|
||||
old_stderr = sys.stderr
|
||||
sys.stderr = StringIO()
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
sys.stderr = old_stderr
|
||||
return update_wrapper(new_func, f)
|
||||
|
||||
|
||||
def run_dev_server(application):
|
||||
servers = []
|
||||
|
||||
def tracking_make_server(*args, **kwargs):
|
||||
srv = real_make_server(*args, **kwargs)
|
||||
servers.append(srv)
|
||||
return srv
|
||||
serving.make_server = tracking_make_server
|
||||
try:
|
||||
t = Thread(target=serving.run_simple,
|
||||
args=('localhost', 0, application))
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
time.sleep(0.25)
|
||||
finally:
|
||||
serving.make_server = real_make_server
|
||||
if not servers:
|
||||
return None, None
|
||||
server, = servers
|
||||
ip, port = server.socket.getsockname()[:2]
|
||||
if ':' in ip:
|
||||
ip = '[%s]' % ip
|
||||
return server, '%s:%d' % (ip, port)
|
||||
|
||||
|
||||
class ServingTestCase(WerkzeugTestCase):
|
||||
|
||||
@silencestderr
|
||||
def test_serving(self):
|
||||
server, addr = run_dev_server(test_app)
|
||||
rv = urlopen('http://%s/?foo=bar&baz=blah' % addr).read()
|
||||
self.assert_in(b'WSGI Information', rv)
|
||||
self.assert_in(b'foo=bar&baz=blah', rv)
|
||||
self.assert_in(b'Werkzeug/' + version.encode('ascii'), rv)
|
||||
|
||||
@silencestderr
|
||||
def test_broken_app(self):
|
||||
def broken_app(environ, start_response):
|
||||
1 // 0
|
||||
server, addr = run_dev_server(broken_app)
|
||||
try:
|
||||
urlopen('http://%s/?foo=bar&baz=blah' % addr).read()
|
||||
except HTTPError as e:
|
||||
# In Python3 a 500 response causes an exception
|
||||
rv = e.read()
|
||||
assert b'Internal Server Error' in rv
|
||||
else:
|
||||
assert False, 'expected internal server error'
|
||||
|
||||
@silencestderr
|
||||
def test_absolute_requests(self):
|
||||
def asserting_app(environ, start_response):
|
||||
assert environ['HTTP_HOST'] == 'surelynotexisting.example.com:1337'
|
||||
assert environ['PATH_INFO'] == '/index.htm'
|
||||
assert environ['SERVER_PORT'] == addr.split(':')[1]
|
||||
start_response('200 OK', [('Content-Type', 'text/html')])
|
||||
return [b'YES']
|
||||
|
||||
server, addr = run_dev_server(asserting_app)
|
||||
conn = httplib.HTTPConnection(addr)
|
||||
conn.request('GET', 'http://surelynotexisting.example.com:1337/index.htm')
|
||||
res = conn.getresponse()
|
||||
assert res.read() == b'YES'
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(ServingTestCase))
|
||||
return suite
|
||||
@@ -1,401 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.test
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the testing tools.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from io import BytesIO
|
||||
from werkzeug._compat import iteritems, to_bytes
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug.wrappers import Request, Response, BaseResponse
|
||||
from werkzeug.test import Client, EnvironBuilder, create_environ, \
|
||||
ClientRedirectError, stream_encode_multipart, run_wsgi_app
|
||||
from werkzeug.utils import redirect
|
||||
from werkzeug.formparser import parse_form_data
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
|
||||
def cookie_app(environ, start_response):
|
||||
"""A WSGI application which sets a cookie, and returns as a ersponse any
|
||||
cookie which exists.
|
||||
"""
|
||||
response = Response(environ.get('HTTP_COOKIE', 'No Cookie'),
|
||||
mimetype='text/plain')
|
||||
response.set_cookie('test', 'test')
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def redirect_loop_app(environ, start_response):
|
||||
response = redirect('http://localhost/some/redirect/')
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def redirect_with_get_app(environ, start_response):
|
||||
req = Request(environ)
|
||||
if req.url not in ('http://localhost/',
|
||||
'http://localhost/first/request',
|
||||
'http://localhost/some/redirect/'):
|
||||
assert False, 'redirect_demo_app() did not expect URL "%s"' % req.url
|
||||
if '/some/redirect' not in req.url:
|
||||
response = redirect('http://localhost/some/redirect/')
|
||||
else:
|
||||
response = Response('current url: %s' % req.url)
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def redirect_with_post_app(environ, start_response):
|
||||
req = Request(environ)
|
||||
if req.url == 'http://localhost/some/redirect/':
|
||||
assert req.method == 'GET', 'request should be GET'
|
||||
assert not req.form, 'request should not have data'
|
||||
response = Response('current url: %s' % req.url)
|
||||
else:
|
||||
response = redirect('http://localhost/some/redirect/')
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def external_redirect_demo_app(environ, start_response):
|
||||
response = redirect('http://example.com/')
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def external_subdomain_redirect_demo_app(environ, start_response):
|
||||
if 'test.example.com' in environ['HTTP_HOST']:
|
||||
response = Response('redirected successfully to subdomain')
|
||||
else:
|
||||
response = redirect('http://test.example.com/login')
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
def multi_value_post_app(environ, start_response):
|
||||
req = Request(environ)
|
||||
assert req.form['field'] == 'val1', req.form['field']
|
||||
assert req.form.getlist('field') == ['val1', 'val2'], req.form.getlist('field')
|
||||
response = Response('ok')
|
||||
return response(environ, start_response)
|
||||
|
||||
|
||||
class TestTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_cookie_forging(self):
|
||||
c = Client(cookie_app)
|
||||
c.set_cookie('localhost', 'foo', 'bar')
|
||||
appiter, code, headers = c.open()
|
||||
self.assert_strict_equal(list(appiter), [b'foo=bar'])
|
||||
|
||||
def test_set_cookie_app(self):
|
||||
c = Client(cookie_app)
|
||||
appiter, code, headers = c.open()
|
||||
self.assert_in('Set-Cookie', dict(headers))
|
||||
|
||||
def test_cookiejar_stores_cookie(self):
|
||||
c = Client(cookie_app)
|
||||
appiter, code, headers = c.open()
|
||||
self.assert_in('test', c.cookie_jar._cookies['localhost.local']['/'])
|
||||
|
||||
def test_no_initial_cookie(self):
|
||||
c = Client(cookie_app)
|
||||
appiter, code, headers = c.open()
|
||||
self.assert_strict_equal(b''.join(appiter), b'No Cookie')
|
||||
|
||||
def test_resent_cookie(self):
|
||||
c = Client(cookie_app)
|
||||
c.open()
|
||||
appiter, code, headers = c.open()
|
||||
self.assert_strict_equal(b''.join(appiter), b'test=test')
|
||||
|
||||
def test_disable_cookies(self):
|
||||
c = Client(cookie_app, use_cookies=False)
|
||||
c.open()
|
||||
appiter, code, headers = c.open()
|
||||
self.assert_strict_equal(b''.join(appiter), b'No Cookie')
|
||||
|
||||
def test_cookie_for_different_path(self):
|
||||
c = Client(cookie_app)
|
||||
c.open('/path1')
|
||||
appiter, code, headers = c.open('/path2')
|
||||
self.assert_strict_equal(b''.join(appiter), b'test=test')
|
||||
|
||||
def test_environ_builder_basics(self):
|
||||
b = EnvironBuilder()
|
||||
self.assert_is_none(b.content_type)
|
||||
b.method = 'POST'
|
||||
self.assert_equal(b.content_type, 'application/x-www-form-urlencoded')
|
||||
b.files.add_file('test', BytesIO(b'test contents'), 'test.txt')
|
||||
self.assert_equal(b.files['test'].content_type, 'text/plain')
|
||||
self.assert_equal(b.content_type, 'multipart/form-data')
|
||||
b.form['test'] = 'normal value'
|
||||
|
||||
req = b.get_request()
|
||||
b.close()
|
||||
|
||||
self.assert_strict_equal(req.url, u'http://localhost/')
|
||||
self.assert_strict_equal(req.method, 'POST')
|
||||
self.assert_strict_equal(req.form['test'], u'normal value')
|
||||
self.assert_equal(req.files['test'].content_type, 'text/plain')
|
||||
self.assert_strict_equal(req.files['test'].filename, u'test.txt')
|
||||
self.assert_strict_equal(req.files['test'].read(), b'test contents')
|
||||
|
||||
def test_environ_builder_headers(self):
|
||||
b = EnvironBuilder(environ_base={'HTTP_USER_AGENT': 'Foo/0.1'},
|
||||
environ_overrides={'wsgi.version': (1, 1)})
|
||||
b.headers['X-Suck-My-Dick'] = 'very well sir'
|
||||
env = b.get_environ()
|
||||
self.assert_strict_equal(env['HTTP_USER_AGENT'], 'Foo/0.1')
|
||||
self.assert_strict_equal(env['HTTP_X_SUCK_MY_DICK'], 'very well sir')
|
||||
self.assert_strict_equal(env['wsgi.version'], (1, 1))
|
||||
|
||||
b.headers['User-Agent'] = 'Bar/1.0'
|
||||
env = b.get_environ()
|
||||
self.assert_strict_equal(env['HTTP_USER_AGENT'], 'Bar/1.0')
|
||||
|
||||
def test_environ_builder_paths(self):
|
||||
b = EnvironBuilder(path='/foo', base_url='http://example.com/')
|
||||
self.assert_strict_equal(b.base_url, 'http://example.com/')
|
||||
self.assert_strict_equal(b.path, '/foo')
|
||||
self.assert_strict_equal(b.script_root, '')
|
||||
self.assert_strict_equal(b.host, 'example.com')
|
||||
|
||||
b = EnvironBuilder(path='/foo', base_url='http://example.com/bar')
|
||||
self.assert_strict_equal(b.base_url, 'http://example.com/bar/')
|
||||
self.assert_strict_equal(b.path, '/foo')
|
||||
self.assert_strict_equal(b.script_root, '/bar')
|
||||
self.assert_strict_equal(b.host, 'example.com')
|
||||
|
||||
b.host = 'localhost'
|
||||
self.assert_strict_equal(b.base_url, 'http://localhost/bar/')
|
||||
b.base_url = 'http://localhost:8080/'
|
||||
self.assert_strict_equal(b.host, 'localhost:8080')
|
||||
self.assert_strict_equal(b.server_name, 'localhost')
|
||||
self.assert_strict_equal(b.server_port, 8080)
|
||||
|
||||
b.host = 'foo.invalid'
|
||||
b.url_scheme = 'https'
|
||||
b.script_root = '/test'
|
||||
env = b.get_environ()
|
||||
self.assert_strict_equal(env['SERVER_NAME'], 'foo.invalid')
|
||||
self.assert_strict_equal(env['SERVER_PORT'], '443')
|
||||
self.assert_strict_equal(env['SCRIPT_NAME'], '/test')
|
||||
self.assert_strict_equal(env['PATH_INFO'], '/foo')
|
||||
self.assert_strict_equal(env['HTTP_HOST'], 'foo.invalid')
|
||||
self.assert_strict_equal(env['wsgi.url_scheme'], 'https')
|
||||
self.assert_strict_equal(b.base_url, 'https://foo.invalid/test/')
|
||||
|
||||
def test_environ_builder_content_type(self):
|
||||
builder = EnvironBuilder()
|
||||
self.assert_is_none(builder.content_type)
|
||||
builder.method = 'POST'
|
||||
self.assert_equal(builder.content_type, 'application/x-www-form-urlencoded')
|
||||
builder.form['foo'] = 'bar'
|
||||
self.assert_equal(builder.content_type, 'application/x-www-form-urlencoded')
|
||||
builder.files.add_file('blafasel', BytesIO(b'foo'), 'test.txt')
|
||||
self.assert_equal(builder.content_type, 'multipart/form-data')
|
||||
req = builder.get_request()
|
||||
self.assert_strict_equal(req.form['foo'], u'bar')
|
||||
self.assert_strict_equal(req.files['blafasel'].read(), b'foo')
|
||||
|
||||
def test_environ_builder_stream_switch(self):
|
||||
d = MultiDict(dict(foo=u'bar', blub=u'blah', hu=u'hum'))
|
||||
for use_tempfile in False, True:
|
||||
stream, length, boundary = stream_encode_multipart(
|
||||
d, use_tempfile, threshold=150)
|
||||
self.assert_true(isinstance(stream, BytesIO) != use_tempfile)
|
||||
|
||||
form = parse_form_data({'wsgi.input': stream, 'CONTENT_LENGTH': str(length),
|
||||
'CONTENT_TYPE': 'multipart/form-data; boundary="%s"' %
|
||||
boundary})[1]
|
||||
self.assert_strict_equal(form, d)
|
||||
stream.close()
|
||||
|
||||
def test_create_environ(self):
|
||||
env = create_environ('/foo?bar=baz', 'http://example.org/')
|
||||
expected = {
|
||||
'wsgi.multiprocess': False,
|
||||
'wsgi.version': (1, 0),
|
||||
'wsgi.run_once': False,
|
||||
'wsgi.errors': sys.stderr,
|
||||
'wsgi.multithread': False,
|
||||
'wsgi.url_scheme': 'http',
|
||||
'SCRIPT_NAME': '',
|
||||
'CONTENT_TYPE': '',
|
||||
'CONTENT_LENGTH': '0',
|
||||
'SERVER_NAME': 'example.org',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'HTTP_HOST': 'example.org',
|
||||
'PATH_INFO': '/foo',
|
||||
'SERVER_PORT': '80',
|
||||
'SERVER_PROTOCOL': 'HTTP/1.1',
|
||||
'QUERY_STRING': 'bar=baz'
|
||||
}
|
||||
for key, value in iteritems(expected):
|
||||
self.assert_equal(env[key], value)
|
||||
self.assert_strict_equal(env['wsgi.input'].read(0), b'')
|
||||
self.assert_strict_equal(create_environ('/foo', 'http://example.com/')['SCRIPT_NAME'], '')
|
||||
|
||||
def test_file_closing(self):
|
||||
closed = []
|
||||
class SpecialInput(object):
|
||||
def read(self):
|
||||
return ''
|
||||
def close(self):
|
||||
closed.append(self)
|
||||
|
||||
env = create_environ(data={'foo': SpecialInput()})
|
||||
self.assert_strict_equal(len(closed), 1)
|
||||
builder = EnvironBuilder()
|
||||
builder.files.add_file('blah', SpecialInput())
|
||||
builder.close()
|
||||
self.assert_strict_equal(len(closed), 2)
|
||||
|
||||
def test_follow_redirect(self):
|
||||
env = create_environ('/', base_url='http://localhost')
|
||||
c = Client(redirect_with_get_app)
|
||||
appiter, code, headers = c.open(environ_overrides=env, follow_redirects=True)
|
||||
self.assert_strict_equal(code, '200 OK')
|
||||
self.assert_strict_equal(b''.join(appiter), b'current url: http://localhost/some/redirect/')
|
||||
|
||||
# Test that the :cls:`Client` is aware of user defined response wrappers
|
||||
c = Client(redirect_with_get_app, response_wrapper=BaseResponse)
|
||||
resp = c.get('/', follow_redirects=True)
|
||||
self.assert_strict_equal(resp.status_code, 200)
|
||||
self.assert_strict_equal(resp.data, b'current url: http://localhost/some/redirect/')
|
||||
|
||||
# test with URL other than '/' to make sure redirected URL's are correct
|
||||
c = Client(redirect_with_get_app, response_wrapper=BaseResponse)
|
||||
resp = c.get('/first/request', follow_redirects=True)
|
||||
self.assert_strict_equal(resp.status_code, 200)
|
||||
self.assert_strict_equal(resp.data, b'current url: http://localhost/some/redirect/')
|
||||
|
||||
def test_follow_external_redirect(self):
|
||||
env = create_environ('/', base_url='http://localhost')
|
||||
c = Client(external_redirect_demo_app)
|
||||
self.assert_raises(RuntimeError, lambda:
|
||||
c.get(environ_overrides=env, follow_redirects=True))
|
||||
|
||||
def test_follow_external_redirect_on_same_subdomain(self):
|
||||
env = create_environ('/', base_url='http://example.com')
|
||||
c = Client(external_subdomain_redirect_demo_app, allow_subdomain_redirects=True)
|
||||
c.get(environ_overrides=env, follow_redirects=True)
|
||||
|
||||
# check that this does not work for real external domains
|
||||
env = create_environ('/', base_url='http://localhost')
|
||||
self.assert_raises(RuntimeError, lambda:
|
||||
c.get(environ_overrides=env, follow_redirects=True))
|
||||
|
||||
# check that subdomain redirects fail if no `allow_subdomain_redirects` is applied
|
||||
c = Client(external_subdomain_redirect_demo_app)
|
||||
self.assert_raises(RuntimeError, lambda:
|
||||
c.get(environ_overrides=env, follow_redirects=True))
|
||||
|
||||
def test_follow_redirect_loop(self):
|
||||
c = Client(redirect_loop_app, response_wrapper=BaseResponse)
|
||||
with self.assert_raises(ClientRedirectError):
|
||||
resp = c.get('/', follow_redirects=True)
|
||||
|
||||
def test_follow_redirect_with_post(self):
|
||||
c = Client(redirect_with_post_app, response_wrapper=BaseResponse)
|
||||
resp = c.post('/', follow_redirects=True, data='foo=blub+hehe&blah=42')
|
||||
self.assert_strict_equal(resp.status_code, 200)
|
||||
self.assert_strict_equal(resp.data, b'current url: http://localhost/some/redirect/')
|
||||
|
||||
def test_path_info_script_name_unquoting(self):
|
||||
def test_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
return [environ['PATH_INFO'] + '\n' + environ['SCRIPT_NAME']]
|
||||
c = Client(test_app, response_wrapper=BaseResponse)
|
||||
resp = c.get('/foo%40bar')
|
||||
self.assert_strict_equal(resp.data, b'/foo@bar\n')
|
||||
c = Client(test_app, response_wrapper=BaseResponse)
|
||||
resp = c.get('/foo%40bar', 'http://localhost/bar%40baz')
|
||||
self.assert_strict_equal(resp.data, b'/foo@bar\n/bar@baz')
|
||||
|
||||
def test_multi_value_submit(self):
|
||||
c = Client(multi_value_post_app, response_wrapper=BaseResponse)
|
||||
data = {
|
||||
'field': ['val1','val2']
|
||||
}
|
||||
resp = c.post('/', data=data)
|
||||
self.assert_strict_equal(resp.status_code, 200)
|
||||
c = Client(multi_value_post_app, response_wrapper=BaseResponse)
|
||||
data = MultiDict({
|
||||
'field': ['val1', 'val2']
|
||||
})
|
||||
resp = c.post('/', data=data)
|
||||
self.assert_strict_equal(resp.status_code, 200)
|
||||
|
||||
def test_iri_support(self):
|
||||
b = EnvironBuilder(u'/föö-bar', base_url=u'http://☃.net/')
|
||||
self.assert_strict_equal(b.path, '/f%C3%B6%C3%B6-bar')
|
||||
self.assert_strict_equal(b.base_url, 'http://xn--n3h.net/')
|
||||
|
||||
def test_run_wsgi_apps(self):
|
||||
def simple_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/html')])
|
||||
return ['Hello World!']
|
||||
app_iter, status, headers = run_wsgi_app(simple_app, {})
|
||||
self.assert_strict_equal(status, '200 OK')
|
||||
self.assert_strict_equal(list(headers), [('Content-Type', 'text/html')])
|
||||
self.assert_strict_equal(app_iter, ['Hello World!'])
|
||||
|
||||
def yielding_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/html')])
|
||||
yield 'Hello '
|
||||
yield 'World!'
|
||||
app_iter, status, headers = run_wsgi_app(yielding_app, {})
|
||||
self.assert_strict_equal(status, '200 OK')
|
||||
self.assert_strict_equal(list(headers), [('Content-Type', 'text/html')])
|
||||
self.assert_strict_equal(list(app_iter), ['Hello ', 'World!'])
|
||||
|
||||
def test_multiple_cookies(self):
|
||||
@Request.application
|
||||
def test_app(request):
|
||||
response = Response(repr(sorted(request.cookies.items())))
|
||||
response.set_cookie(u'test1', b'foo')
|
||||
response.set_cookie(u'test2', b'bar')
|
||||
return response
|
||||
client = Client(test_app, Response)
|
||||
resp = client.get('/')
|
||||
self.assert_strict_equal(resp.data, b'[]')
|
||||
resp = client.get('/')
|
||||
self.assert_strict_equal(resp.data,
|
||||
to_bytes(repr([('test1', u'foo'), ('test2', u'bar')]), 'ascii'))
|
||||
|
||||
def test_correct_open_invocation_on_redirect(self):
|
||||
class MyClient(Client):
|
||||
counter = 0
|
||||
def open(self, *args, **kwargs):
|
||||
self.counter += 1
|
||||
env = kwargs.setdefault('environ_overrides', {})
|
||||
env['werkzeug._foo'] = self.counter
|
||||
return Client.open(self, *args, **kwargs)
|
||||
|
||||
@Request.application
|
||||
def test_app(request):
|
||||
return Response(str(request.environ['werkzeug._foo']))
|
||||
|
||||
c = MyClient(test_app, response_wrapper=Response)
|
||||
self.assert_strict_equal(c.get('/').data, b'1')
|
||||
self.assert_strict_equal(c.get('/').data, b'2')
|
||||
self.assert_strict_equal(c.get('/').data, b'3')
|
||||
|
||||
def test_correct_encoding(self):
|
||||
req = Request.from_values(u'/\N{SNOWMAN}', u'http://example.com/foo')
|
||||
self.assert_strict_equal(req.script_root, u'/foo')
|
||||
self.assert_strict_equal(req.path, u'/\N{SNOWMAN}')
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(TestTestCase))
|
||||
return suite
|
||||
@@ -1,300 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.urls
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
URL helper tests.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug.datastructures import OrderedMultiDict
|
||||
from werkzeug import urls
|
||||
from werkzeug._compat import text_type, NativeStringIO, BytesIO
|
||||
|
||||
|
||||
class URLsTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_replace(self):
|
||||
url = urls.url_parse('http://de.wikipedia.org/wiki/Troll')
|
||||
self.assert_strict_equal(url.replace(query='foo=bar'),
|
||||
urls.url_parse('http://de.wikipedia.org/wiki/Troll?foo=bar'))
|
||||
self.assert_strict_equal(url.replace(scheme='https'),
|
||||
urls.url_parse('https://de.wikipedia.org/wiki/Troll'))
|
||||
|
||||
def test_quoting(self):
|
||||
self.assert_strict_equal(urls.url_quote(u'\xf6\xe4\xfc'), '%C3%B6%C3%A4%C3%BC')
|
||||
self.assert_strict_equal(urls.url_unquote(urls.url_quote(u'#%="\xf6')), u'#%="\xf6')
|
||||
self.assert_strict_equal(urls.url_quote_plus('foo bar'), 'foo+bar')
|
||||
self.assert_strict_equal(urls.url_unquote_plus('foo+bar'), u'foo bar')
|
||||
self.assert_strict_equal(urls.url_encode({b'a': None, b'b': b'foo bar'}), 'b=foo+bar')
|
||||
self.assert_strict_equal(urls.url_encode({u'a': None, u'b': u'foo bar'}), 'b=foo+bar')
|
||||
self.assert_strict_equal(urls.url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffsklärung)'),
|
||||
'http://de.wikipedia.org/wiki/Elf%20(Begriffskl%C3%A4rung)')
|
||||
self.assert_strict_equal(urls.url_quote_plus(42), '42')
|
||||
self.assert_strict_equal(urls.url_quote(b'\xff'), '%FF')
|
||||
|
||||
def test_bytes_unquoting(self):
|
||||
self.assert_strict_equal(urls.url_unquote(urls.url_quote(
|
||||
u'#%="\xf6', charset='latin1'), charset=None), b'#%="\xf6')
|
||||
|
||||
def test_url_decoding(self):
|
||||
x = urls.url_decode(b'foo=42&bar=23&uni=H%C3%A4nsel')
|
||||
self.assert_strict_equal(x['foo'], u'42')
|
||||
self.assert_strict_equal(x['bar'], u'23')
|
||||
self.assert_strict_equal(x['uni'], u'Hänsel')
|
||||
|
||||
x = urls.url_decode(b'foo=42;bar=23;uni=H%C3%A4nsel', separator=b';')
|
||||
self.assert_strict_equal(x['foo'], u'42')
|
||||
self.assert_strict_equal(x['bar'], u'23')
|
||||
self.assert_strict_equal(x['uni'], u'Hänsel')
|
||||
|
||||
x = urls.url_decode(b'%C3%9Ch=H%C3%A4nsel', decode_keys=True)
|
||||
self.assert_strict_equal(x[u'Üh'], u'Hänsel')
|
||||
|
||||
def test_url_bytes_decoding(self):
|
||||
x = urls.url_decode(b'foo=42&bar=23&uni=H%C3%A4nsel', charset=None)
|
||||
self.assert_strict_equal(x[b'foo'], b'42')
|
||||
self.assert_strict_equal(x[b'bar'], b'23')
|
||||
self.assert_strict_equal(x[b'uni'], u'Hänsel'.encode('utf-8'))
|
||||
|
||||
def test_streamed_url_decoding(self):
|
||||
item1 = u'a' * 100000
|
||||
item2 = u'b' * 400
|
||||
string = ('a=%s&b=%s&c=%s' % (item1, item2, item2)).encode('ascii')
|
||||
gen = urls.url_decode_stream(BytesIO(string), limit=len(string),
|
||||
return_iterator=True)
|
||||
self.assert_strict_equal(next(gen), ('a', item1))
|
||||
self.assert_strict_equal(next(gen), ('b', item2))
|
||||
self.assert_strict_equal(next(gen), ('c', item2))
|
||||
self.assert_raises(StopIteration, lambda: next(gen))
|
||||
|
||||
def test_stream_decoding_string_fails(self):
|
||||
self.assert_raises(TypeError, urls.url_decode_stream, 'testing')
|
||||
|
||||
def test_url_encoding(self):
|
||||
self.assert_strict_equal(urls.url_encode({'foo': 'bar 45'}), 'foo=bar+45')
|
||||
d = {'foo': 1, 'bar': 23, 'blah': u'Hänsel'}
|
||||
self.assert_strict_equal(urls.url_encode(d, sort=True), 'bar=23&blah=H%C3%A4nsel&foo=1')
|
||||
self.assert_strict_equal(urls.url_encode(d, sort=True, separator=u';'), 'bar=23;blah=H%C3%A4nsel;foo=1')
|
||||
|
||||
def test_sorted_url_encode(self):
|
||||
self.assert_strict_equal(urls.url_encode({u"a": 42, u"b": 23, 1: 1, 2: 2},
|
||||
sort=True, key=lambda i: text_type(i[0])), '1=1&2=2&a=42&b=23')
|
||||
self.assert_strict_equal(urls.url_encode({u'A': 1, u'a': 2, u'B': 3, 'b': 4}, sort=True,
|
||||
key=lambda x: x[0].lower() + x[0]), 'A=1&a=2&B=3&b=4')
|
||||
|
||||
def test_streamed_url_encoding(self):
|
||||
out = NativeStringIO()
|
||||
urls.url_encode_stream({'foo': 'bar 45'}, out)
|
||||
self.assert_strict_equal(out.getvalue(), 'foo=bar+45')
|
||||
|
||||
d = {'foo': 1, 'bar': 23, 'blah': u'Hänsel'}
|
||||
out = NativeStringIO()
|
||||
urls.url_encode_stream(d, out, sort=True)
|
||||
self.assert_strict_equal(out.getvalue(), 'bar=23&blah=H%C3%A4nsel&foo=1')
|
||||
out = NativeStringIO()
|
||||
urls.url_encode_stream(d, out, sort=True, separator=u';')
|
||||
self.assert_strict_equal(out.getvalue(), 'bar=23;blah=H%C3%A4nsel;foo=1')
|
||||
|
||||
gen = urls.url_encode_stream(d, sort=True)
|
||||
self.assert_strict_equal(next(gen), 'bar=23')
|
||||
self.assert_strict_equal(next(gen), 'blah=H%C3%A4nsel')
|
||||
self.assert_strict_equal(next(gen), 'foo=1')
|
||||
self.assert_raises(StopIteration, lambda: next(gen))
|
||||
|
||||
def test_url_fixing(self):
|
||||
x = urls.url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffskl\xe4rung)')
|
||||
self.assert_line_equal(x, 'http://de.wikipedia.org/wiki/Elf%20(Begriffskl%C3%A4rung)')
|
||||
|
||||
x = urls.url_fix("http://just.a.test/$-_.+!*'(),")
|
||||
self.assert_equal(x, "http://just.a.test/$-_.+!*'(),")
|
||||
|
||||
def test_url_fixing_qs(self):
|
||||
x = urls.url_fix(b'http://example.com/?foo=%2f%2f')
|
||||
self.assert_line_equal(x, 'http://example.com/?foo=%2f%2f')
|
||||
|
||||
x = urls.url_fix('http://acronyms.thefreedictionary.com/Algebraic+Methods+of+Solving+the+Schr%C3%B6dinger+Equation')
|
||||
self.assert_equal(x, 'http://acronyms.thefreedictionary.com/Algebraic+Methods+of+Solving+the+Schr%C3%B6dinger+Equation')
|
||||
|
||||
def test_iri_support(self):
|
||||
self.assert_strict_equal(urls.uri_to_iri('http://xn--n3h.net/'),
|
||||
u'http://\u2603.net/')
|
||||
self.assert_strict_equal(
|
||||
urls.uri_to_iri(b'http://%C3%BCser:p%C3%A4ssword@xn--n3h.net/p%C3%A5th'),
|
||||
u'http://\xfcser:p\xe4ssword@\u2603.net/p\xe5th')
|
||||
self.assert_strict_equal(urls.iri_to_uri(u'http://☃.net/'), 'http://xn--n3h.net/')
|
||||
self.assert_strict_equal(
|
||||
urls.iri_to_uri(u'http://üser:pässword@☃.net/påth'),
|
||||
'http://%C3%BCser:p%C3%A4ssword@xn--n3h.net/p%C3%A5th')
|
||||
|
||||
self.assert_strict_equal(urls.uri_to_iri('http://test.com/%3Fmeh?foo=%26%2F'),
|
||||
u'http://test.com/%3Fmeh?foo=%26%2F')
|
||||
|
||||
# this should work as well, might break on 2.4 because of a broken
|
||||
# idna codec
|
||||
self.assert_strict_equal(urls.uri_to_iri(b'/foo'), u'/foo')
|
||||
self.assert_strict_equal(urls.iri_to_uri(u'/foo'), '/foo')
|
||||
|
||||
self.assert_strict_equal(urls.iri_to_uri(u'http://föö.com:8080/bam/baz'),
|
||||
'http://xn--f-1gaa.com:8080/bam/baz')
|
||||
|
||||
def test_ordered_multidict_encoding(self):
|
||||
d = OrderedMultiDict()
|
||||
d.add('foo', 1)
|
||||
d.add('foo', 2)
|
||||
d.add('foo', 3)
|
||||
d.add('bar', 0)
|
||||
d.add('foo', 4)
|
||||
self.assert_equal(urls.url_encode(d), 'foo=1&foo=2&foo=3&bar=0&foo=4')
|
||||
|
||||
def test_href(self):
|
||||
x = urls.Href('http://www.example.com/')
|
||||
self.assert_strict_equal(x(u'foo'), 'http://www.example.com/foo')
|
||||
self.assert_strict_equal(x.foo(u'bar'), 'http://www.example.com/foo/bar')
|
||||
self.assert_strict_equal(x.foo(u'bar', x=42), 'http://www.example.com/foo/bar?x=42')
|
||||
self.assert_strict_equal(x.foo(u'bar', class_=42), 'http://www.example.com/foo/bar?class=42')
|
||||
self.assert_strict_equal(x.foo(u'bar', {u'class': 42}), 'http://www.example.com/foo/bar?class=42')
|
||||
self.assert_raises(AttributeError, lambda: x.__blah__)
|
||||
|
||||
x = urls.Href('blah')
|
||||
self.assert_strict_equal(x.foo(u'bar'), 'blah/foo/bar')
|
||||
|
||||
self.assert_raises(TypeError, x.foo, {u"foo": 23}, x=42)
|
||||
|
||||
x = urls.Href('')
|
||||
self.assert_strict_equal(x('foo'), 'foo')
|
||||
|
||||
def test_href_url_join(self):
|
||||
x = urls.Href(u'test')
|
||||
self.assert_line_equal(x(u'foo:bar'), u'test/foo:bar')
|
||||
self.assert_line_equal(x(u'http://example.com/'), u'test/http://example.com/')
|
||||
self.assert_line_equal(x.a(), u'test/a')
|
||||
|
||||
def test_href_past_root(self):
|
||||
base_href = urls.Href('http://www.blagga.com/1/2/3')
|
||||
self.assert_strict_equal(base_href('../foo'), 'http://www.blagga.com/1/2/foo')
|
||||
self.assert_strict_equal(base_href('../../foo'), 'http://www.blagga.com/1/foo')
|
||||
self.assert_strict_equal(base_href('../../../foo'), 'http://www.blagga.com/foo')
|
||||
self.assert_strict_equal(base_href('../../../../foo'), 'http://www.blagga.com/foo')
|
||||
self.assert_strict_equal(base_href('../../../../../foo'), 'http://www.blagga.com/foo')
|
||||
self.assert_strict_equal(base_href('../../../../../../foo'), 'http://www.blagga.com/foo')
|
||||
|
||||
def test_url_unquote_plus_unicode(self):
|
||||
# was broken in 0.6
|
||||
self.assert_strict_equal(urls.url_unquote_plus(u'\x6d'), u'\x6d')
|
||||
self.assert_is(type(urls.url_unquote_plus(u'\x6d')), text_type)
|
||||
|
||||
def test_quoting_of_local_urls(self):
|
||||
rv = urls.iri_to_uri(u'/foo\x8f')
|
||||
self.assert_strict_equal(rv, '/foo%C2%8F')
|
||||
self.assert_is(type(rv), str)
|
||||
|
||||
def test_url_attributes(self):
|
||||
rv = urls.url_parse('http://foo%3a:bar%3a@[::1]:80/123?x=y#frag')
|
||||
self.assert_strict_equal(rv.scheme, 'http')
|
||||
self.assert_strict_equal(rv.auth, 'foo%3a:bar%3a')
|
||||
self.assert_strict_equal(rv.username, u'foo:')
|
||||
self.assert_strict_equal(rv.password, u'bar:')
|
||||
self.assert_strict_equal(rv.raw_username, 'foo%3a')
|
||||
self.assert_strict_equal(rv.raw_password, 'bar%3a')
|
||||
self.assert_strict_equal(rv.host, '::1')
|
||||
self.assert_equal(rv.port, 80)
|
||||
self.assert_strict_equal(rv.path, '/123')
|
||||
self.assert_strict_equal(rv.query, 'x=y')
|
||||
self.assert_strict_equal(rv.fragment, 'frag')
|
||||
|
||||
rv = urls.url_parse(u'http://\N{SNOWMAN}.com/')
|
||||
self.assert_strict_equal(rv.host, u'\N{SNOWMAN}.com')
|
||||
self.assert_strict_equal(rv.ascii_host, 'xn--n3h.com')
|
||||
|
||||
def test_url_attributes_bytes(self):
|
||||
rv = urls.url_parse(b'http://foo%3a:bar%3a@[::1]:80/123?x=y#frag')
|
||||
self.assert_strict_equal(rv.scheme, b'http')
|
||||
self.assert_strict_equal(rv.auth, b'foo%3a:bar%3a')
|
||||
self.assert_strict_equal(rv.username, u'foo:')
|
||||
self.assert_strict_equal(rv.password, u'bar:')
|
||||
self.assert_strict_equal(rv.raw_username, b'foo%3a')
|
||||
self.assert_strict_equal(rv.raw_password, b'bar%3a')
|
||||
self.assert_strict_equal(rv.host, b'::1')
|
||||
self.assert_equal(rv.port, 80)
|
||||
self.assert_strict_equal(rv.path, b'/123')
|
||||
self.assert_strict_equal(rv.query, b'x=y')
|
||||
self.assert_strict_equal(rv.fragment, b'frag')
|
||||
|
||||
def test_url_joining(self):
|
||||
self.assert_strict_equal(urls.url_join('/foo', '/bar'), '/bar')
|
||||
self.assert_strict_equal(urls.url_join('http://example.com/foo', '/bar'),
|
||||
'http://example.com/bar')
|
||||
self.assert_strict_equal(urls.url_join('file:///tmp/', 'test.html'),
|
||||
'file:///tmp/test.html')
|
||||
self.assert_strict_equal(urls.url_join('file:///tmp/x', 'test.html'),
|
||||
'file:///tmp/test.html')
|
||||
self.assert_strict_equal(urls.url_join('file:///tmp/x', '../../../x.html'),
|
||||
'file:///x.html')
|
||||
|
||||
def test_partial_unencoded_decode(self):
|
||||
ref = u'foo=정상처리'.encode('euc-kr')
|
||||
x = urls.url_decode(ref, charset='euc-kr')
|
||||
self.assert_strict_equal(x['foo'], u'정상처리')
|
||||
|
||||
def test_iri_to_uri_idempotence_ascii_only(self):
|
||||
uri = u'http://www.idempoten.ce'
|
||||
uri = urls.iri_to_uri(uri)
|
||||
self.assert_equal(urls.iri_to_uri(uri), uri)
|
||||
|
||||
def test_iri_to_uri_idempotence_non_ascii(self):
|
||||
uri = u'http://\N{SNOWMAN}/\N{SNOWMAN}'
|
||||
uri = urls.iri_to_uri(uri)
|
||||
self.assert_equal(urls.iri_to_uri(uri), uri)
|
||||
|
||||
def test_uri_to_iri_idempotence_ascii_only(self):
|
||||
uri = 'http://www.idempoten.ce'
|
||||
uri = urls.uri_to_iri(uri)
|
||||
self.assert_equal(urls.uri_to_iri(uri), uri)
|
||||
|
||||
def test_uri_to_iri_idempotence_non_ascii(self):
|
||||
uri = 'http://xn--n3h/%E2%98%83'
|
||||
uri = urls.uri_to_iri(uri)
|
||||
self.assert_equal(urls.uri_to_iri(uri), uri)
|
||||
|
||||
def test_iri_to_uri_to_iri(self):
|
||||
iri = u'http://föö.com/'
|
||||
uri = urls.iri_to_uri(iri)
|
||||
self.assert_equal(urls.uri_to_iri(uri), iri)
|
||||
|
||||
def test_uri_to_iri_to_uri(self):
|
||||
uri = 'http://xn--f-rgao.com/%C3%9E'
|
||||
iri = urls.uri_to_iri(uri)
|
||||
self.assert_equal(urls.iri_to_uri(iri), uri)
|
||||
|
||||
def test_uri_iri_normalization(self):
|
||||
uri = 'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93'
|
||||
iri = u'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713'
|
||||
|
||||
tests = [
|
||||
u'http://föñ.com/\N{BALLOT BOX}/fred?utf8=\u2713',
|
||||
u'http://xn--f-rgao.com/\u2610/fred?utf8=\N{CHECK MARK}',
|
||||
b'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93',
|
||||
u'http://xn--f-rgao.com/%E2%98%90/fred?utf8=%E2%9C%93',
|
||||
u'http://föñ.com/\u2610/fred?utf8=%E2%9C%93',
|
||||
b'http://xn--f-rgao.com/\xe2\x98\x90/fred?utf8=\xe2\x9c\x93',
|
||||
]
|
||||
|
||||
for test in tests:
|
||||
self.assert_equal(urls.uri_to_iri(test), iri)
|
||||
self.assert_equal(urls.iri_to_uri(test), uri)
|
||||
self.assert_equal(urls.uri_to_iri(urls.iri_to_uri(test)), iri)
|
||||
self.assert_equal(urls.iri_to_uri(urls.uri_to_iri(test)), uri)
|
||||
self.assert_equal(urls.uri_to_iri(urls.uri_to_iri(test)), iri)
|
||||
self.assert_equal(urls.iri_to_uri(urls.iri_to_uri(test)), uri)
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(URLsTestCase))
|
||||
return suite
|
||||
@@ -1,284 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.utils
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
General utilities.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import utils
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.http import parse_date, http_date
|
||||
from werkzeug.wrappers import BaseResponse
|
||||
from werkzeug.test import Client, run_wsgi_app
|
||||
from werkzeug._compat import text_type, implements_iterator
|
||||
|
||||
|
||||
class GeneralUtilityTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_redirect(self):
|
||||
resp = utils.redirect(u'/füübär')
|
||||
self.assert_in(b'/f%C3%BC%C3%BCb%C3%A4r', resp.get_data())
|
||||
self.assert_equal(resp.headers['Location'], '/f%C3%BC%C3%BCb%C3%A4r')
|
||||
self.assert_equal(resp.status_code, 302)
|
||||
|
||||
resp = utils.redirect(u'http://☃.net/', 307)
|
||||
self.assert_in(b'http://xn--n3h.net/', resp.get_data())
|
||||
self.assert_equal(resp.headers['Location'], 'http://xn--n3h.net/')
|
||||
self.assert_equal(resp.status_code, 307)
|
||||
|
||||
resp = utils.redirect('http://example.com/', 305)
|
||||
self.assert_equal(resp.headers['Location'], 'http://example.com/')
|
||||
self.assert_equal(resp.status_code, 305)
|
||||
|
||||
def test_redirect_no_unicode_header_keys(self):
|
||||
# Make sure all headers are native keys. This was a bug at one point
|
||||
# due to an incorrect conversion.
|
||||
resp = utils.redirect('http://example.com/', 305)
|
||||
for key, value in resp.headers.items():
|
||||
self.assert_equal(type(key), str)
|
||||
self.assert_equal(type(value), text_type)
|
||||
self.assert_equal(resp.headers['Location'], 'http://example.com/')
|
||||
self.assert_equal(resp.status_code, 305)
|
||||
|
||||
def test_redirect_xss(self):
|
||||
location = 'http://example.com/?xss="><script>alert(1)</script>'
|
||||
resp = utils.redirect(location)
|
||||
self.assert_not_in(b'<script>alert(1)</script>', resp.get_data())
|
||||
|
||||
location = 'http://example.com/?xss="onmouseover="alert(1)'
|
||||
resp = utils.redirect(location)
|
||||
self.assert_not_in(b'href="http://example.com/?xss="onmouseover="alert(1)"', resp.get_data())
|
||||
|
||||
def test_cached_property(self):
|
||||
foo = []
|
||||
class A(object):
|
||||
def prop(self):
|
||||
foo.append(42)
|
||||
return 42
|
||||
prop = utils.cached_property(prop)
|
||||
|
||||
a = A()
|
||||
p = a.prop
|
||||
q = a.prop
|
||||
self.assert_true(p == q == 42)
|
||||
self.assert_equal(foo, [42])
|
||||
|
||||
foo = []
|
||||
class A(object):
|
||||
def _prop(self):
|
||||
foo.append(42)
|
||||
return 42
|
||||
prop = utils.cached_property(_prop, name='prop')
|
||||
del _prop
|
||||
|
||||
a = A()
|
||||
p = a.prop
|
||||
q = a.prop
|
||||
self.assert_true(p == q == 42)
|
||||
self.assert_equal(foo, [42])
|
||||
|
||||
def test_environ_property(self):
|
||||
class A(object):
|
||||
environ = {'string': 'abc', 'number': '42'}
|
||||
|
||||
string = utils.environ_property('string')
|
||||
missing = utils.environ_property('missing', 'spam')
|
||||
read_only = utils.environ_property('number')
|
||||
number = utils.environ_property('number', load_func=int)
|
||||
broken_number = utils.environ_property('broken_number', load_func=int)
|
||||
date = utils.environ_property('date', None, parse_date, http_date,
|
||||
read_only=False)
|
||||
foo = utils.environ_property('foo')
|
||||
|
||||
a = A()
|
||||
self.assert_equal(a.string, 'abc')
|
||||
self.assert_equal(a.missing, 'spam')
|
||||
def test_assign():
|
||||
a.read_only = 'something'
|
||||
self.assert_raises(AttributeError, test_assign)
|
||||
self.assert_equal(a.number, 42)
|
||||
self.assert_equal(a.broken_number, None)
|
||||
self.assert_is_none(a.date)
|
||||
a.date = datetime(2008, 1, 22, 10, 0, 0, 0)
|
||||
self.assert_equal(a.environ['date'], 'Tue, 22 Jan 2008 10:00:00 GMT')
|
||||
|
||||
def test_escape(self):
|
||||
class Foo(str):
|
||||
def __html__(self):
|
||||
return text_type(self)
|
||||
self.assert_equal(utils.escape(None), '')
|
||||
self.assert_equal(utils.escape(42), '42')
|
||||
self.assert_equal(utils.escape('<>'), '<>')
|
||||
self.assert_equal(utils.escape('"foo"'), '"foo"')
|
||||
self.assert_equal(utils.escape(Foo('<foo>')), '<foo>')
|
||||
|
||||
def test_unescape(self):
|
||||
self.assert_equal(utils.unescape('<ä>'), u'<ä>')
|
||||
|
||||
def test_run_wsgi_app(self):
|
||||
def foo(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
yield '1'
|
||||
yield '2'
|
||||
yield '3'
|
||||
|
||||
app_iter, status, headers = run_wsgi_app(foo, {})
|
||||
self.assert_equal(status, '200 OK')
|
||||
self.assert_equal(list(headers), [('Content-Type', 'text/plain')])
|
||||
self.assert_equal(next(app_iter), '1')
|
||||
self.assert_equal(next(app_iter), '2')
|
||||
self.assert_equal(next(app_iter), '3')
|
||||
self.assert_raises(StopIteration, partial(next, app_iter))
|
||||
|
||||
got_close = []
|
||||
@implements_iterator
|
||||
class CloseIter(object):
|
||||
def __init__(self):
|
||||
self.iterated = False
|
||||
def __iter__(self):
|
||||
return self
|
||||
def close(self):
|
||||
got_close.append(None)
|
||||
def __next__(self):
|
||||
if self.iterated:
|
||||
raise StopIteration()
|
||||
self.iterated = True
|
||||
return 'bar'
|
||||
|
||||
def bar(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
return CloseIter()
|
||||
|
||||
app_iter, status, headers = run_wsgi_app(bar, {})
|
||||
self.assert_equal(status, '200 OK')
|
||||
self.assert_equal(list(headers), [('Content-Type', 'text/plain')])
|
||||
self.assert_equal(next(app_iter), 'bar')
|
||||
self.assert_raises(StopIteration, partial(next, app_iter))
|
||||
app_iter.close()
|
||||
|
||||
self.assert_equal(run_wsgi_app(bar, {}, True)[0], ['bar'])
|
||||
|
||||
self.assert_equal(len(got_close), 2)
|
||||
|
||||
def test_import_string(self):
|
||||
import cgi
|
||||
from werkzeug.debug import DebuggedApplication
|
||||
self.assert_is(utils.import_string('cgi.escape'), cgi.escape)
|
||||
self.assert_is(utils.import_string(u'cgi.escape'), cgi.escape)
|
||||
self.assert_is(utils.import_string('cgi:escape'), cgi.escape)
|
||||
self.assert_is_none(utils.import_string('XXXXXXXXXXXX', True))
|
||||
self.assert_is_none(utils.import_string('cgi.XXXXXXXXXXXX', True))
|
||||
self.assert_is(utils.import_string(u'cgi.escape'), cgi.escape)
|
||||
self.assert_is(utils.import_string(u'werkzeug.debug.DebuggedApplication'), DebuggedApplication)
|
||||
self.assert_raises(ImportError, utils.import_string, 'XXXXXXXXXXXXXXXX')
|
||||
self.assert_raises(ImportError, utils.import_string, 'cgi.XXXXXXXXXX')
|
||||
|
||||
def test_find_modules(self):
|
||||
self.assert_equal(list(utils.find_modules('werkzeug.debug')), \
|
||||
['werkzeug.debug.console', 'werkzeug.debug.repr',
|
||||
'werkzeug.debug.tbtools'])
|
||||
|
||||
def test_html_builder(self):
|
||||
html = utils.html
|
||||
xhtml = utils.xhtml
|
||||
self.assert_equal(html.p('Hello World'), '<p>Hello World</p>')
|
||||
self.assert_equal(html.a('Test', href='#'), '<a href="#">Test</a>')
|
||||
self.assert_equal(html.br(), '<br>')
|
||||
self.assert_equal(xhtml.br(), '<br />')
|
||||
self.assert_equal(html.img(src='foo'), '<img src="foo">')
|
||||
self.assert_equal(xhtml.img(src='foo'), '<img src="foo" />')
|
||||
self.assert_equal(html.html(
|
||||
html.head(
|
||||
html.title('foo'),
|
||||
html.script(type='text/javascript')
|
||||
)
|
||||
), '<html><head><title>foo</title><script type="text/javascript">'
|
||||
'</script></head></html>')
|
||||
self.assert_equal(html('<foo>'), '<foo>')
|
||||
self.assert_equal(html.input(disabled=True), '<input disabled>')
|
||||
self.assert_equal(xhtml.input(disabled=True), '<input disabled="disabled" />')
|
||||
self.assert_equal(html.input(disabled=''), '<input>')
|
||||
self.assert_equal(xhtml.input(disabled=''), '<input />')
|
||||
self.assert_equal(html.input(disabled=None), '<input>')
|
||||
self.assert_equal(xhtml.input(disabled=None), '<input />')
|
||||
self.assert_equal(html.script('alert("Hello World");'), '<script>' \
|
||||
'alert("Hello World");</script>')
|
||||
self.assert_equal(xhtml.script('alert("Hello World");'), '<script>' \
|
||||
'/*<![CDATA[*/alert("Hello World");/*]]>*/</script>')
|
||||
|
||||
def test_validate_arguments(self):
|
||||
take_none = lambda: None
|
||||
take_two = lambda a, b: None
|
||||
take_two_one_default = lambda a, b=0: None
|
||||
|
||||
self.assert_equal(utils.validate_arguments(take_two, (1, 2,), {}), ((1, 2), {}))
|
||||
self.assert_equal(utils.validate_arguments(take_two, (1,), {'b': 2}), ((1, 2), {}))
|
||||
self.assert_equal(utils.validate_arguments(take_two_one_default, (1,), {}), ((1, 0), {}))
|
||||
self.assert_equal(utils.validate_arguments(take_two_one_default, (1, 2), {}), ((1, 2), {}))
|
||||
|
||||
self.assert_raises(utils.ArgumentValidationError,
|
||||
utils.validate_arguments, take_two, (), {})
|
||||
|
||||
self.assert_equal(utils.validate_arguments(take_none, (1, 2,), {'c': 3}), ((), {}))
|
||||
self.assert_raises(utils.ArgumentValidationError,
|
||||
utils.validate_arguments, take_none, (1,), {}, drop_extra=False)
|
||||
self.assert_raises(utils.ArgumentValidationError,
|
||||
utils.validate_arguments, take_none, (), {'a': 1}, drop_extra=False)
|
||||
|
||||
def test_header_set_duplication_bug(self):
|
||||
headers = Headers([
|
||||
('Content-Type', 'text/html'),
|
||||
('Foo', 'bar'),
|
||||
('Blub', 'blah')
|
||||
])
|
||||
headers['blub'] = 'hehe'
|
||||
headers['blafasel'] = 'humm'
|
||||
self.assert_equal(headers, Headers([
|
||||
('Content-Type', 'text/html'),
|
||||
('Foo', 'bar'),
|
||||
('blub', 'hehe'),
|
||||
('blafasel', 'humm')
|
||||
]))
|
||||
|
||||
def test_append_slash_redirect(self):
|
||||
def app(env, sr):
|
||||
return utils.append_slash_redirect(env)(env, sr)
|
||||
client = Client(app, BaseResponse)
|
||||
response = client.get('foo', base_url='http://example.org/app')
|
||||
self.assert_equal(response.status_code, 301)
|
||||
self.assert_equal(response.headers['Location'], 'http://example.org/app/foo/')
|
||||
|
||||
def test_cached_property_doc(self):
|
||||
@utils.cached_property
|
||||
def foo():
|
||||
"""testing"""
|
||||
return 42
|
||||
self.assert_equal(foo.__doc__, 'testing')
|
||||
self.assert_equal(foo.__name__, 'foo')
|
||||
self.assert_equal(foo.__module__, __name__)
|
||||
|
||||
def test_secure_filename(self):
|
||||
self.assert_equal(utils.secure_filename('My cool movie.mov'),
|
||||
'My_cool_movie.mov')
|
||||
self.assert_equal(utils.secure_filename('../../../etc/passwd'),
|
||||
'etc_passwd')
|
||||
self.assert_equal(utils.secure_filename(u'i contain cool \xfcml\xe4uts.txt'),
|
||||
'i_contain_cool_umlauts.txt')
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(GeneralUtilityTestCase))
|
||||
return suite
|
||||
@@ -1,795 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.wrappers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests for the response and request objects.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
import pickle
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from werkzeug._compat import iteritems
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase
|
||||
|
||||
from werkzeug import wrappers
|
||||
from werkzeug.exceptions import SecurityError
|
||||
from werkzeug.wsgi import LimitedStream
|
||||
from werkzeug.datastructures import MultiDict, ImmutableOrderedMultiDict, \
|
||||
ImmutableList, ImmutableTypeConversionDict, CharsetAccept, \
|
||||
MIMEAccept, LanguageAccept, Accept, CombinedMultiDict
|
||||
from werkzeug.test import Client, create_environ, run_wsgi_app
|
||||
from werkzeug._compat import implements_iterator, text_type
|
||||
|
||||
|
||||
class RequestTestResponse(wrappers.BaseResponse):
|
||||
"""Subclass of the normal response class we use to test response
|
||||
and base classes. Has some methods to test if things in the
|
||||
response match.
|
||||
"""
|
||||
|
||||
def __init__(self, response, status, headers):
|
||||
wrappers.BaseResponse.__init__(self, response, status, headers)
|
||||
self.body_data = pickle.loads(self.get_data())
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.body_data[key]
|
||||
|
||||
|
||||
def request_demo_app(environ, start_response):
|
||||
request = wrappers.BaseRequest(environ)
|
||||
assert 'werkzeug.request' in environ
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
return [pickle.dumps({
|
||||
'args': request.args,
|
||||
'args_as_list': list(request.args.lists()),
|
||||
'form': request.form,
|
||||
'form_as_list': list(request.form.lists()),
|
||||
'environ': prepare_environ_pickle(request.environ),
|
||||
'data': request.get_data()
|
||||
})]
|
||||
|
||||
|
||||
def prepare_environ_pickle(environ):
|
||||
result = {}
|
||||
for key, value in iteritems(environ):
|
||||
try:
|
||||
pickle.dumps((key, value))
|
||||
except Exception:
|
||||
continue
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
class WrappersTestCase(WerkzeugTestCase):
|
||||
|
||||
def assert_environ(self, environ, method):
|
||||
self.assert_strict_equal(environ['REQUEST_METHOD'], method)
|
||||
self.assert_strict_equal(environ['PATH_INFO'], '/')
|
||||
self.assert_strict_equal(environ['SCRIPT_NAME'], '')
|
||||
self.assert_strict_equal(environ['SERVER_NAME'], 'localhost')
|
||||
self.assert_strict_equal(environ['wsgi.version'], (1, 0))
|
||||
self.assert_strict_equal(environ['wsgi.url_scheme'], 'http')
|
||||
|
||||
def test_base_request(self):
|
||||
client = Client(request_demo_app, RequestTestResponse)
|
||||
|
||||
# get requests
|
||||
response = client.get('/?foo=bar&foo=hehe')
|
||||
self.assert_strict_equal(response['args'], MultiDict([('foo', u'bar'), ('foo', u'hehe')]))
|
||||
self.assert_strict_equal(response['args_as_list'], [('foo', [u'bar', u'hehe'])])
|
||||
self.assert_strict_equal(response['form'], MultiDict())
|
||||
self.assert_strict_equal(response['form_as_list'], [])
|
||||
self.assert_strict_equal(response['data'], b'')
|
||||
self.assert_environ(response['environ'], 'GET')
|
||||
|
||||
# post requests with form data
|
||||
response = client.post('/?blub=blah', data='foo=blub+hehe&blah=42',
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
self.assert_strict_equal(response['args'], MultiDict([('blub', u'blah')]))
|
||||
self.assert_strict_equal(response['args_as_list'], [('blub', [u'blah'])])
|
||||
self.assert_strict_equal(response['form'], MultiDict([('foo', u'blub hehe'), ('blah', u'42')]))
|
||||
self.assert_strict_equal(response['data'], b'')
|
||||
# currently we do not guarantee that the values are ordered correctly
|
||||
# for post data.
|
||||
## self.assert_strict_equal(response['form_as_list'], [('foo', ['blub hehe']), ('blah', ['42'])])
|
||||
self.assert_environ(response['environ'], 'POST')
|
||||
|
||||
# patch requests with form data
|
||||
response = client.patch('/?blub=blah', data='foo=blub+hehe&blah=42',
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
self.assert_strict_equal(response['args'], MultiDict([('blub', u'blah')]))
|
||||
self.assert_strict_equal(response['args_as_list'], [('blub', [u'blah'])])
|
||||
self.assert_strict_equal(response['form'],
|
||||
MultiDict([('foo', u'blub hehe'), ('blah', u'42')]))
|
||||
self.assert_strict_equal(response['data'], b'')
|
||||
self.assert_environ(response['environ'], 'PATCH')
|
||||
|
||||
# post requests with json data
|
||||
json = b'{"foo": "bar", "blub": "blah"}'
|
||||
response = client.post('/?a=b', data=json, content_type='application/json')
|
||||
self.assert_strict_equal(response['data'], json)
|
||||
self.assert_strict_equal(response['args'], MultiDict([('a', u'b')]))
|
||||
self.assert_strict_equal(response['form'], MultiDict())
|
||||
|
||||
def test_query_string_is_bytes(self):
|
||||
req = wrappers.Request.from_values(u'/?foo=%2f')
|
||||
self.assert_strict_equal(req.query_string, b'foo=%2f')
|
||||
|
||||
def test_access_route(self):
|
||||
req = wrappers.Request.from_values(headers={
|
||||
'X-Forwarded-For': '192.168.1.2, 192.168.1.1'
|
||||
})
|
||||
req.environ['REMOTE_ADDR'] = '192.168.1.3'
|
||||
self.assert_equal(req.access_route, ['192.168.1.2', '192.168.1.1'])
|
||||
self.assert_strict_equal(req.remote_addr, '192.168.1.3')
|
||||
|
||||
req = wrappers.Request.from_values()
|
||||
req.environ['REMOTE_ADDR'] = '192.168.1.3'
|
||||
self.assert_strict_equal(list(req.access_route), ['192.168.1.3'])
|
||||
|
||||
def test_url_request_descriptors(self):
|
||||
req = wrappers.Request.from_values('/bar?foo=baz', 'http://example.com/test')
|
||||
self.assert_strict_equal(req.path, u'/bar')
|
||||
self.assert_strict_equal(req.full_path, u'/bar?foo=baz')
|
||||
self.assert_strict_equal(req.script_root, u'/test')
|
||||
self.assert_strict_equal(req.url, u'http://example.com/test/bar?foo=baz')
|
||||
self.assert_strict_equal(req.base_url, u'http://example.com/test/bar')
|
||||
self.assert_strict_equal(req.url_root, u'http://example.com/test/')
|
||||
self.assert_strict_equal(req.host_url, u'http://example.com/')
|
||||
self.assert_strict_equal(req.host, 'example.com')
|
||||
self.assert_strict_equal(req.scheme, 'http')
|
||||
|
||||
req = wrappers.Request.from_values('/bar?foo=baz', 'https://example.com/test')
|
||||
self.assert_strict_equal(req.scheme, 'https')
|
||||
|
||||
def test_url_request_descriptors_hosts(self):
|
||||
req = wrappers.Request.from_values('/bar?foo=baz', 'http://example.com/test')
|
||||
req.trusted_hosts = ['example.com']
|
||||
self.assert_strict_equal(req.path, u'/bar')
|
||||
self.assert_strict_equal(req.full_path, u'/bar?foo=baz')
|
||||
self.assert_strict_equal(req.script_root, u'/test')
|
||||
self.assert_strict_equal(req.url, u'http://example.com/test/bar?foo=baz')
|
||||
self.assert_strict_equal(req.base_url, u'http://example.com/test/bar')
|
||||
self.assert_strict_equal(req.url_root, u'http://example.com/test/')
|
||||
self.assert_strict_equal(req.host_url, u'http://example.com/')
|
||||
self.assert_strict_equal(req.host, 'example.com')
|
||||
self.assert_strict_equal(req.scheme, 'http')
|
||||
|
||||
req = wrappers.Request.from_values('/bar?foo=baz', 'https://example.com/test')
|
||||
self.assert_strict_equal(req.scheme, 'https')
|
||||
|
||||
req = wrappers.Request.from_values('/bar?foo=baz', 'http://example.com/test')
|
||||
req.trusted_hosts = ['example.org']
|
||||
self.assert_raises(SecurityError, lambda: req.url)
|
||||
self.assert_raises(SecurityError, lambda: req.base_url)
|
||||
self.assert_raises(SecurityError, lambda: req.url_root)
|
||||
self.assert_raises(SecurityError, lambda: req.host_url)
|
||||
self.assert_raises(SecurityError, lambda: req.host)
|
||||
|
||||
def test_authorization_mixin(self):
|
||||
request = wrappers.Request.from_values(headers={
|
||||
'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='
|
||||
})
|
||||
a = request.authorization
|
||||
self.assert_strict_equal(a.type, 'basic')
|
||||
self.assert_strict_equal(a.username, 'Aladdin')
|
||||
self.assert_strict_equal(a.password, 'open sesame')
|
||||
|
||||
def test_stream_only_mixing(self):
|
||||
request = wrappers.PlainRequest.from_values(
|
||||
data=b'foo=blub+hehe',
|
||||
content_type='application/x-www-form-urlencoded'
|
||||
)
|
||||
self.assert_equal(list(request.files.items()), [])
|
||||
self.assert_equal(list(request.form.items()), [])
|
||||
self.assert_raises(AttributeError, lambda: request.data)
|
||||
self.assert_strict_equal(request.stream.read(), b'foo=blub+hehe')
|
||||
|
||||
def test_base_response(self):
|
||||
# unicode
|
||||
response = wrappers.BaseResponse(u'öäü')
|
||||
self.assert_strict_equal(response.get_data(), u'öäü'.encode('utf-8'))
|
||||
|
||||
# writing
|
||||
response = wrappers.Response('foo')
|
||||
response.stream.write('bar')
|
||||
self.assert_strict_equal(response.get_data(), b'foobar')
|
||||
|
||||
# set cookie
|
||||
response = wrappers.BaseResponse()
|
||||
response.set_cookie('foo', 'bar', 60, 0, '/blub', 'example.org')
|
||||
self.assert_strict_equal(response.headers.to_wsgi_list(), [
|
||||
('Content-Type', 'text/plain; charset=utf-8'),
|
||||
('Set-Cookie', 'foo=bar; Domain=example.org; Expires=Thu, '
|
||||
'01-Jan-1970 00:00:00 GMT; Max-Age=60; Path=/blub')
|
||||
])
|
||||
|
||||
# delete cookie
|
||||
response = wrappers.BaseResponse()
|
||||
response.delete_cookie('foo')
|
||||
self.assert_strict_equal(response.headers.to_wsgi_list(), [
|
||||
('Content-Type', 'text/plain; charset=utf-8'),
|
||||
('Set-Cookie', 'foo=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/')
|
||||
])
|
||||
|
||||
# close call forwarding
|
||||
closed = []
|
||||
@implements_iterator
|
||||
class Iterable(object):
|
||||
def __next__(self):
|
||||
raise StopIteration()
|
||||
def __iter__(self):
|
||||
return self
|
||||
def close(self):
|
||||
closed.append(True)
|
||||
response = wrappers.BaseResponse(Iterable())
|
||||
response.call_on_close(lambda: closed.append(True))
|
||||
app_iter, status, headers = run_wsgi_app(response,
|
||||
create_environ(),
|
||||
buffered=True)
|
||||
self.assert_strict_equal(status, '200 OK')
|
||||
self.assert_strict_equal(''.join(app_iter), '')
|
||||
self.assert_strict_equal(len(closed), 2)
|
||||
|
||||
# with statement
|
||||
del closed[:]
|
||||
response = wrappers.BaseResponse(Iterable())
|
||||
with response:
|
||||
pass
|
||||
self.assert_equal(len(closed), 1)
|
||||
|
||||
def test_response_status_codes(self):
|
||||
response = wrappers.BaseResponse()
|
||||
response.status_code = 404
|
||||
self.assert_strict_equal(response.status, '404 NOT FOUND')
|
||||
response.status = '200 OK'
|
||||
self.assert_strict_equal(response.status_code, 200)
|
||||
response.status = '999 WTF'
|
||||
self.assert_strict_equal(response.status_code, 999)
|
||||
response.status_code = 588
|
||||
self.assert_strict_equal(response.status_code, 588)
|
||||
self.assert_strict_equal(response.status, '588 UNKNOWN')
|
||||
response.status = 'wtf'
|
||||
self.assert_strict_equal(response.status_code, 0)
|
||||
self.assert_strict_equal(response.status, '0 wtf')
|
||||
|
||||
def test_type_forcing(self):
|
||||
def wsgi_application(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'text/html')])
|
||||
return ['Hello World!']
|
||||
base_response = wrappers.BaseResponse('Hello World!', content_type='text/html')
|
||||
|
||||
class SpecialResponse(wrappers.Response):
|
||||
def foo(self):
|
||||
return 42
|
||||
|
||||
# good enough for this simple application, but don't ever use that in
|
||||
# real world examples!
|
||||
fake_env = {}
|
||||
|
||||
for orig_resp in wsgi_application, base_response:
|
||||
response = SpecialResponse.force_type(orig_resp, fake_env)
|
||||
assert response.__class__ is SpecialResponse
|
||||
self.assert_strict_equal(response.foo(), 42)
|
||||
self.assert_strict_equal(response.get_data(), b'Hello World!')
|
||||
self.assert_equal(response.content_type, 'text/html')
|
||||
|
||||
# without env, no arbitrary conversion
|
||||
self.assert_raises(TypeError, SpecialResponse.force_type, wsgi_application)
|
||||
|
||||
def test_accept_mixin(self):
|
||||
request = wrappers.Request({
|
||||
'HTTP_ACCEPT': 'text/xml,application/xml,application/xhtml+xml,'
|
||||
'text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
|
||||
'HTTP_ACCEPT_CHARSET': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
||||
'HTTP_ACCEPT_ENCODING': 'gzip,deflate',
|
||||
'HTTP_ACCEPT_LANGUAGE': 'en-us,en;q=0.5'
|
||||
})
|
||||
self.assert_equal(request.accept_mimetypes, MIMEAccept([
|
||||
('text/xml', 1), ('image/png', 1), ('application/xml', 1),
|
||||
('application/xhtml+xml', 1), ('text/html', 0.9),
|
||||
('text/plain', 0.8), ('*/*', 0.5)
|
||||
]))
|
||||
self.assert_strict_equal(request.accept_charsets, CharsetAccept([
|
||||
('ISO-8859-1', 1), ('utf-8', 0.7), ('*', 0.7)
|
||||
]))
|
||||
self.assert_strict_equal(request.accept_encodings, Accept([
|
||||
('gzip', 1), ('deflate', 1)]))
|
||||
self.assert_strict_equal(request.accept_languages, LanguageAccept([
|
||||
('en-us', 1), ('en', 0.5)]))
|
||||
|
||||
request = wrappers.Request({'HTTP_ACCEPT': ''})
|
||||
self.assert_strict_equal(request.accept_mimetypes, MIMEAccept())
|
||||
|
||||
def test_etag_request_mixin(self):
|
||||
request = wrappers.Request({
|
||||
'HTTP_CACHE_CONTROL': 'no-store, no-cache',
|
||||
'HTTP_IF_MATCH': 'w/"foo", bar, "baz"',
|
||||
'HTTP_IF_NONE_MATCH': 'w/"foo", bar, "baz"',
|
||||
'HTTP_IF_MODIFIED_SINCE': 'Tue, 22 Jan 2008 11:18:44 GMT',
|
||||
'HTTP_IF_UNMODIFIED_SINCE': 'Tue, 22 Jan 2008 11:18:44 GMT'
|
||||
})
|
||||
assert request.cache_control.no_store
|
||||
assert request.cache_control.no_cache
|
||||
|
||||
for etags in request.if_match, request.if_none_match:
|
||||
assert etags('bar')
|
||||
assert etags.contains_raw('w/"foo"')
|
||||
assert etags.contains_weak('foo')
|
||||
assert not etags.contains('foo')
|
||||
|
||||
self.assert_equal(request.if_modified_since, datetime(2008, 1, 22, 11, 18, 44))
|
||||
self.assert_equal(request.if_unmodified_since, datetime(2008, 1, 22, 11, 18, 44))
|
||||
|
||||
def test_user_agent_mixin(self):
|
||||
user_agents = [
|
||||
('Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.11) '
|
||||
'Gecko/20071127 Firefox/2.0.0.11', 'firefox', 'macos', '2.0.0.11',
|
||||
'en-US'),
|
||||
('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de-DE) Opera 8.54',
|
||||
'opera', 'windows', '8.54', 'de-DE'),
|
||||
('Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420 '
|
||||
'(KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3',
|
||||
'safari', 'iphone', '419.3', 'en'),
|
||||
('Bot Googlebot/2.1 ( http://www.googlebot.com/bot.html)',
|
||||
'google', None, '2.1', None)
|
||||
]
|
||||
for ua, browser, platform, version, lang in user_agents:
|
||||
request = wrappers.Request({'HTTP_USER_AGENT': ua})
|
||||
self.assert_strict_equal(request.user_agent.browser, browser)
|
||||
self.assert_strict_equal(request.user_agent.platform, platform)
|
||||
self.assert_strict_equal(request.user_agent.version, version)
|
||||
self.assert_strict_equal(request.user_agent.language, lang)
|
||||
assert bool(request.user_agent)
|
||||
self.assert_strict_equal(request.user_agent.to_header(), ua)
|
||||
self.assert_strict_equal(str(request.user_agent), ua)
|
||||
|
||||
request = wrappers.Request({'HTTP_USER_AGENT': 'foo'})
|
||||
assert not request.user_agent
|
||||
|
||||
def test_stream_wrapping(self):
|
||||
class LowercasingStream(object):
|
||||
def __init__(self, stream):
|
||||
self._stream = stream
|
||||
def read(self, size=-1):
|
||||
return self._stream.read(size).lower()
|
||||
def readline(self, size=-1):
|
||||
return self._stream.readline(size).lower()
|
||||
|
||||
data = b'foo=Hello+World'
|
||||
req = wrappers.Request.from_values('/', method='POST', data=data,
|
||||
content_type='application/x-www-form-urlencoded')
|
||||
req.stream = LowercasingStream(req.stream)
|
||||
self.assert_equal(req.form['foo'], 'hello world')
|
||||
|
||||
def test_etag_response_mixin(self):
|
||||
response = wrappers.Response('Hello World')
|
||||
self.assert_equal(response.get_etag(), (None, None))
|
||||
response.add_etag()
|
||||
self.assert_equal(response.get_etag(), ('b10a8db164e0754105b7a99be72e3fe5', False))
|
||||
assert not response.cache_control
|
||||
response.cache_control.must_revalidate = True
|
||||
response.cache_control.max_age = 60
|
||||
response.headers['Content-Length'] = len(response.get_data())
|
||||
assert response.headers['Cache-Control'] in ('must-revalidate, max-age=60',
|
||||
'max-age=60, must-revalidate')
|
||||
|
||||
assert 'date' not in response.headers
|
||||
env = create_environ()
|
||||
env.update({
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'HTTP_IF_NONE_MATCH': response.get_etag()[0]
|
||||
})
|
||||
response.make_conditional(env)
|
||||
assert 'date' in response.headers
|
||||
|
||||
# after the thing is invoked by the server as wsgi application
|
||||
# (we're emulating this here), there must not be any entity
|
||||
# headers left and the status code would have to be 304
|
||||
resp = wrappers.Response.from_app(response, env)
|
||||
self.assert_equal(resp.status_code, 304)
|
||||
assert not 'content-length' in resp.headers
|
||||
|
||||
# make sure date is not overriden
|
||||
response = wrappers.Response('Hello World')
|
||||
response.date = 1337
|
||||
d = response.date
|
||||
response.make_conditional(env)
|
||||
self.assert_equal(response.date, d)
|
||||
|
||||
# make sure content length is only set if missing
|
||||
response = wrappers.Response('Hello World')
|
||||
response.content_length = 999
|
||||
response.make_conditional(env)
|
||||
self.assert_equal(response.content_length, 999)
|
||||
|
||||
def test_etag_response_mixin_freezing(self):
|
||||
class WithFreeze(wrappers.ETagResponseMixin, wrappers.BaseResponse):
|
||||
pass
|
||||
class WithoutFreeze(wrappers.BaseResponse, wrappers.ETagResponseMixin):
|
||||
pass
|
||||
|
||||
response = WithFreeze('Hello World')
|
||||
response.freeze()
|
||||
self.assert_strict_equal(response.get_etag(),
|
||||
(text_type(wrappers.generate_etag(b'Hello World')), False))
|
||||
response = WithoutFreeze('Hello World')
|
||||
response.freeze()
|
||||
self.assert_equal(response.get_etag(), (None, None))
|
||||
response = wrappers.Response('Hello World')
|
||||
response.freeze()
|
||||
self.assert_equal(response.get_etag(), (None, None))
|
||||
|
||||
def test_authenticate_mixin(self):
|
||||
resp = wrappers.Response()
|
||||
resp.www_authenticate.type = 'basic'
|
||||
resp.www_authenticate.realm = 'Testing'
|
||||
self.assert_strict_equal(resp.headers['WWW-Authenticate'], u'Basic realm="Testing"')
|
||||
resp.www_authenticate.realm = None
|
||||
resp.www_authenticate.type = None
|
||||
assert 'WWW-Authenticate' not in resp.headers
|
||||
|
||||
def test_response_stream_mixin(self):
|
||||
response = wrappers.Response()
|
||||
response.stream.write('Hello ')
|
||||
response.stream.write('World!')
|
||||
self.assert_equal(response.response, ['Hello ', 'World!'])
|
||||
self.assert_equal(response.get_data(), b'Hello World!')
|
||||
|
||||
def test_common_response_descriptors_mixin(self):
|
||||
response = wrappers.Response()
|
||||
response.mimetype = 'text/html'
|
||||
self.assert_equal(response.mimetype, 'text/html')
|
||||
self.assert_equal(response.content_type, 'text/html; charset=utf-8')
|
||||
self.assert_equal(response.mimetype_params, {'charset': 'utf-8'})
|
||||
response.mimetype_params['x-foo'] = 'yep'
|
||||
del response.mimetype_params['charset']
|
||||
self.assert_equal(response.content_type, 'text/html; x-foo=yep')
|
||||
|
||||
now = datetime.utcnow().replace(microsecond=0)
|
||||
|
||||
assert response.content_length is None
|
||||
response.content_length = '42'
|
||||
self.assert_equal(response.content_length, 42)
|
||||
|
||||
for attr in 'date', 'age', 'expires':
|
||||
assert getattr(response, attr) is None
|
||||
setattr(response, attr, now)
|
||||
self.assert_equal(getattr(response, attr), now)
|
||||
|
||||
assert response.retry_after is None
|
||||
response.retry_after = now
|
||||
self.assert_equal(response.retry_after, now)
|
||||
|
||||
assert not response.vary
|
||||
response.vary.add('Cookie')
|
||||
response.vary.add('Content-Language')
|
||||
assert 'cookie' in response.vary
|
||||
self.assert_equal(response.vary.to_header(), 'Cookie, Content-Language')
|
||||
response.headers['Vary'] = 'Content-Encoding'
|
||||
self.assert_equal(response.vary.as_set(), set(['content-encoding']))
|
||||
|
||||
response.allow.update(['GET', 'POST'])
|
||||
self.assert_equal(response.headers['Allow'], 'GET, POST')
|
||||
|
||||
response.content_language.add('en-US')
|
||||
response.content_language.add('fr')
|
||||
self.assert_equal(response.headers['Content-Language'], 'en-US, fr')
|
||||
|
||||
def test_common_request_descriptors_mixin(self):
|
||||
request = wrappers.Request.from_values(content_type='text/html; charset=utf-8',
|
||||
content_length='23',
|
||||
headers={
|
||||
'Referer': 'http://www.example.com/',
|
||||
'Date': 'Sat, 28 Feb 2009 19:04:35 GMT',
|
||||
'Max-Forwards': '10',
|
||||
'Pragma': 'no-cache',
|
||||
'Content-Encoding': 'gzip',
|
||||
'Content-MD5': '9a3bc6dbc47a70db25b84c6e5867a072'
|
||||
})
|
||||
|
||||
self.assert_equal(request.content_type, 'text/html; charset=utf-8')
|
||||
self.assert_equal(request.mimetype, 'text/html')
|
||||
self.assert_equal(request.mimetype_params, {'charset': 'utf-8'})
|
||||
self.assert_equal(request.content_length, 23)
|
||||
self.assert_equal(request.referrer, 'http://www.example.com/')
|
||||
self.assert_equal(request.date, datetime(2009, 2, 28, 19, 4, 35))
|
||||
self.assert_equal(request.max_forwards, 10)
|
||||
self.assert_true('no-cache' in request.pragma)
|
||||
self.assert_equal(request.content_encoding, 'gzip')
|
||||
self.assert_equal(request.content_md5, '9a3bc6dbc47a70db25b84c6e5867a072')
|
||||
|
||||
def test_shallow_mode(self):
|
||||
request = wrappers.Request({'QUERY_STRING': 'foo=bar'}, shallow=True)
|
||||
self.assert_equal(request.args['foo'], 'bar')
|
||||
self.assert_raises(RuntimeError, lambda: request.form['foo'])
|
||||
|
||||
def test_form_parsing_failed(self):
|
||||
data = (
|
||||
b'--blah\r\n'
|
||||
)
|
||||
data = wrappers.Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST')
|
||||
assert not data.files
|
||||
assert not data.form
|
||||
|
||||
def test_file_closing(self):
|
||||
data = (b'--foo\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"; filename="foo.txt"\r\n'
|
||||
b'Content-Type: text/plain; charset=utf-8\r\n\r\n'
|
||||
b'file contents, just the contents\r\n'
|
||||
b'--foo--')
|
||||
req = wrappers.Request.from_values(
|
||||
input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST'
|
||||
)
|
||||
foo = req.files['foo']
|
||||
self.assert_equal(foo.mimetype, 'text/plain')
|
||||
self.assert_equal(foo.filename, 'foo.txt')
|
||||
|
||||
self.assert_equal(foo.closed, False)
|
||||
req.close()
|
||||
self.assert_equal(foo.closed, True)
|
||||
|
||||
def test_file_closing_with(self):
|
||||
data = (b'--foo\r\n'
|
||||
b'Content-Disposition: form-data; name="foo"; filename="foo.txt"\r\n'
|
||||
b'Content-Type: text/plain; charset=utf-8\r\n\r\n'
|
||||
b'file contents, just the contents\r\n'
|
||||
b'--foo--')
|
||||
req = wrappers.Request.from_values(
|
||||
input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='multipart/form-data; boundary=foo',
|
||||
method='POST'
|
||||
)
|
||||
with req:
|
||||
foo = req.files['foo']
|
||||
self.assert_equal(foo.mimetype, 'text/plain')
|
||||
self.assert_equal(foo.filename, 'foo.txt')
|
||||
|
||||
self.assert_equal(foo.closed, True)
|
||||
|
||||
def test_url_charset_reflection(self):
|
||||
req = wrappers.Request.from_values()
|
||||
req.charset = 'utf-7'
|
||||
self.assert_equal(req.url_charset, 'utf-7')
|
||||
|
||||
def test_response_streamed(self):
|
||||
r = wrappers.Response()
|
||||
assert not r.is_streamed
|
||||
r = wrappers.Response("Hello World")
|
||||
assert not r.is_streamed
|
||||
r = wrappers.Response(["foo", "bar"])
|
||||
assert not r.is_streamed
|
||||
def gen():
|
||||
if 0:
|
||||
yield None
|
||||
r = wrappers.Response(gen())
|
||||
assert r.is_streamed
|
||||
|
||||
def test_response_iter_wrapping(self):
|
||||
def uppercasing(iterator):
|
||||
for item in iterator:
|
||||
yield item.upper()
|
||||
def generator():
|
||||
yield 'foo'
|
||||
yield 'bar'
|
||||
req = wrappers.Request.from_values()
|
||||
resp = wrappers.Response(generator())
|
||||
del resp.headers['Content-Length']
|
||||
resp.response = uppercasing(resp.iter_encoded())
|
||||
actual_resp = wrappers.Response.from_app(resp, req.environ, buffered=True)
|
||||
self.assertEqual(actual_resp.get_data(), b'FOOBAR')
|
||||
|
||||
def test_response_freeze(self):
|
||||
def generate():
|
||||
yield "foo"
|
||||
yield "bar"
|
||||
resp = wrappers.Response(generate())
|
||||
resp.freeze()
|
||||
self.assert_equal(resp.response, [b'foo', b'bar'])
|
||||
self.assert_equal(resp.headers['content-length'], '6')
|
||||
|
||||
def test_other_method_payload(self):
|
||||
data = b'Hello World'
|
||||
req = wrappers.Request.from_values(input_stream=BytesIO(data),
|
||||
content_length=len(data),
|
||||
content_type='text/plain',
|
||||
method='WHAT_THE_FUCK')
|
||||
self.assert_equal(req.get_data(), data)
|
||||
self.assert_is_instance(req.stream, LimitedStream)
|
||||
|
||||
def test_urlfication(self):
|
||||
resp = wrappers.Response()
|
||||
resp.headers['Location'] = u'http://üser:pässword@☃.net/påth'
|
||||
resp.headers['Content-Location'] = u'http://☃.net/'
|
||||
headers = resp.get_wsgi_headers(create_environ())
|
||||
self.assert_equal(headers['location'], \
|
||||
'http://%C3%BCser:p%C3%A4ssword@xn--n3h.net/p%C3%A5th')
|
||||
self.assert_equal(headers['content-location'], 'http://xn--n3h.net/')
|
||||
|
||||
def test_new_response_iterator_behavior(self):
|
||||
req = wrappers.Request.from_values()
|
||||
resp = wrappers.Response(u'Hello Wörld!')
|
||||
|
||||
def get_content_length(resp):
|
||||
headers = resp.get_wsgi_headers(req.environ)
|
||||
return headers.get('content-length', type=int)
|
||||
|
||||
def generate_items():
|
||||
yield "Hello "
|
||||
yield u"Wörld!"
|
||||
|
||||
# werkzeug encodes when set to `data` now, which happens
|
||||
# if a string is passed to the response object.
|
||||
self.assert_equal(resp.response, [u'Hello Wörld!'.encode('utf-8')])
|
||||
self.assert_equal(resp.get_data(), u'Hello Wörld!'.encode('utf-8'))
|
||||
self.assert_equal(get_content_length(resp), 13)
|
||||
assert not resp.is_streamed
|
||||
assert resp.is_sequence
|
||||
|
||||
# try the same for manual assignment
|
||||
resp.set_data(u'Wörd')
|
||||
self.assert_equal(resp.response, [u'Wörd'.encode('utf-8')])
|
||||
self.assert_equal(resp.get_data(), u'Wörd'.encode('utf-8'))
|
||||
self.assert_equal(get_content_length(resp), 5)
|
||||
assert not resp.is_streamed
|
||||
assert resp.is_sequence
|
||||
|
||||
# automatic generator sequence conversion
|
||||
resp.response = generate_items()
|
||||
assert resp.is_streamed
|
||||
assert not resp.is_sequence
|
||||
self.assert_equal(resp.get_data(), u'Hello Wörld!'.encode('utf-8'))
|
||||
self.assert_equal(resp.response, [b'Hello ', u'Wörld!'.encode('utf-8')])
|
||||
assert not resp.is_streamed
|
||||
assert resp.is_sequence
|
||||
|
||||
# automatic generator sequence conversion
|
||||
resp.response = generate_items()
|
||||
resp.implicit_sequence_conversion = False
|
||||
assert resp.is_streamed
|
||||
assert not resp.is_sequence
|
||||
self.assert_raises(RuntimeError, lambda: resp.get_data())
|
||||
resp.make_sequence()
|
||||
self.assert_equal(resp.get_data(), u'Hello Wörld!'.encode('utf-8'))
|
||||
self.assert_equal(resp.response, [b'Hello ', u'Wörld!'.encode('utf-8')])
|
||||
assert not resp.is_streamed
|
||||
assert resp.is_sequence
|
||||
|
||||
# stream makes it a list no matter how the conversion is set
|
||||
for val in True, False:
|
||||
resp.implicit_sequence_conversion = val
|
||||
resp.response = ("foo", "bar")
|
||||
assert resp.is_sequence
|
||||
resp.stream.write('baz')
|
||||
self.assert_equal(resp.response, ['foo', 'bar', 'baz'])
|
||||
|
||||
def test_form_data_ordering(self):
|
||||
class MyRequest(wrappers.Request):
|
||||
parameter_storage_class = ImmutableOrderedMultiDict
|
||||
|
||||
req = MyRequest.from_values('/?foo=1&bar=0&foo=3')
|
||||
self.assert_equal(list(req.args), ['foo', 'bar'])
|
||||
self.assert_equal(list(req.args.items(multi=True)), [
|
||||
('foo', '1'),
|
||||
('bar', '0'),
|
||||
('foo', '3')
|
||||
])
|
||||
self.assert_is_instance(req.args, ImmutableOrderedMultiDict)
|
||||
self.assert_is_instance(req.values, CombinedMultiDict)
|
||||
self.assert_equal(req.values['foo'], '1')
|
||||
self.assert_equal(req.values.getlist('foo'), ['1', '3'])
|
||||
|
||||
def test_storage_classes(self):
|
||||
class MyRequest(wrappers.Request):
|
||||
dict_storage_class = dict
|
||||
list_storage_class = list
|
||||
parameter_storage_class = dict
|
||||
req = MyRequest.from_values('/?foo=baz', headers={
|
||||
'Cookie': 'foo=bar'
|
||||
})
|
||||
assert type(req.cookies) is dict
|
||||
self.assert_equal(req.cookies, {'foo': 'bar'})
|
||||
assert type(req.access_route) is list
|
||||
|
||||
assert type(req.args) is dict
|
||||
assert type(req.values) is CombinedMultiDict
|
||||
self.assert_equal(req.values['foo'], u'baz')
|
||||
|
||||
req = wrappers.Request.from_values(headers={
|
||||
'Cookie': 'foo=bar'
|
||||
})
|
||||
assert type(req.cookies) is ImmutableTypeConversionDict
|
||||
self.assert_equal(req.cookies, {'foo': 'bar'})
|
||||
assert type(req.access_route) is ImmutableList
|
||||
|
||||
MyRequest.list_storage_class = tuple
|
||||
req = MyRequest.from_values()
|
||||
assert type(req.access_route) is tuple
|
||||
|
||||
def test_response_headers_passthrough(self):
|
||||
headers = wrappers.Headers()
|
||||
resp = wrappers.Response(headers=headers)
|
||||
assert resp.headers is headers
|
||||
|
||||
def test_response_304_no_content_length(self):
|
||||
resp = wrappers.Response('Test', status=304)
|
||||
env = create_environ()
|
||||
assert 'content-length' not in resp.get_wsgi_headers(env)
|
||||
|
||||
def test_ranges(self):
|
||||
# basic range stuff
|
||||
req = wrappers.Request.from_values()
|
||||
assert req.range is None
|
||||
req = wrappers.Request.from_values(headers={'Range': 'bytes=0-499'})
|
||||
self.assert_equal(req.range.ranges, [(0, 500)])
|
||||
|
||||
resp = wrappers.Response()
|
||||
resp.content_range = req.range.make_content_range(1000)
|
||||
self.assert_equal(resp.content_range.units, 'bytes')
|
||||
self.assert_equal(resp.content_range.start, 0)
|
||||
self.assert_equal(resp.content_range.stop, 500)
|
||||
self.assert_equal(resp.content_range.length, 1000)
|
||||
self.assert_equal(resp.headers['Content-Range'], 'bytes 0-499/1000')
|
||||
|
||||
resp.content_range.unset()
|
||||
assert 'Content-Range' not in resp.headers
|
||||
|
||||
resp.headers['Content-Range'] = 'bytes 0-499/1000'
|
||||
self.assert_equal(resp.content_range.units, 'bytes')
|
||||
self.assert_equal(resp.content_range.start, 0)
|
||||
self.assert_equal(resp.content_range.stop, 500)
|
||||
self.assert_equal(resp.content_range.length, 1000)
|
||||
|
||||
def test_auto_content_length(self):
|
||||
resp = wrappers.Response('Hello World!')
|
||||
self.assert_equal(resp.content_length, 12)
|
||||
|
||||
resp = wrappers.Response(['Hello World!'])
|
||||
assert resp.content_length is None
|
||||
self.assert_equal(resp.get_wsgi_headers({})['Content-Length'], '12')
|
||||
|
||||
def test_disabled_auto_content_length(self):
|
||||
class MyResponse(wrappers.Response):
|
||||
automatically_set_content_length = False
|
||||
resp = MyResponse('Hello World!')
|
||||
self.assert_is_none(resp.content_length)
|
||||
|
||||
resp = MyResponse(['Hello World!'])
|
||||
self.assert_is_none(resp.content_length)
|
||||
self.assert_not_in('Content-Length', resp.get_wsgi_headers({}))
|
||||
|
||||
def test_location_header_autocorrect(self):
|
||||
env = create_environ()
|
||||
class MyResponse(wrappers.Response):
|
||||
autocorrect_location_header = False
|
||||
resp = MyResponse('Hello World!')
|
||||
resp.headers['Location'] = '/test'
|
||||
self.assert_equal(resp.get_wsgi_headers(env)['Location'], '/test')
|
||||
|
||||
resp = wrappers.Response('Hello World!')
|
||||
resp.headers['Location'] = '/test'
|
||||
self.assert_equal(resp.get_wsgi_headers(env)['Location'], 'http://localhost/test')
|
||||
|
||||
def test_modified_url_encoding(self):
|
||||
class ModifiedRequest(wrappers.Request):
|
||||
url_charset = 'euc-kr'
|
||||
|
||||
req = ModifiedRequest.from_values(u'/?foo=정상처리'.encode('euc-kr'))
|
||||
self.assert_strict_equal(req.args['foo'], u'정상처리')
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(WrappersTestCase))
|
||||
return suite
|
||||
@@ -1,352 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.testsuite.wsgi
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the WSGI utilities.
|
||||
|
||||
:copyright: (c) 2013 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import unittest
|
||||
from os import path
|
||||
from contextlib import closing
|
||||
|
||||
from werkzeug.testsuite import WerkzeugTestCase, get_temporary_directory
|
||||
|
||||
from werkzeug.wrappers import BaseResponse
|
||||
from werkzeug.exceptions import BadRequest, ClientDisconnected
|
||||
from werkzeug.test import Client, create_environ, run_wsgi_app
|
||||
from werkzeug import wsgi
|
||||
from werkzeug._compat import StringIO, BytesIO, NativeStringIO, to_native
|
||||
|
||||
|
||||
class WSGIUtilsTestCase(WerkzeugTestCase):
|
||||
|
||||
def test_shareddatamiddleware_get_file_loader(self):
|
||||
app = wsgi.SharedDataMiddleware(None, {})
|
||||
assert callable(app.get_file_loader('foo'))
|
||||
|
||||
def test_shared_data_middleware(self):
|
||||
def null_application(environ, start_response):
|
||||
start_response('404 NOT FOUND', [('Content-Type', 'text/plain')])
|
||||
yield b'NOT FOUND'
|
||||
|
||||
test_dir = get_temporary_directory()
|
||||
with open(path.join(test_dir, to_native(u'äöü', 'utf-8')), 'w') as test_file:
|
||||
test_file.write(u'FOUND')
|
||||
|
||||
app = wsgi.SharedDataMiddleware(null_application, {
|
||||
'/': path.join(path.dirname(__file__), 'res'),
|
||||
'/sources': path.join(path.dirname(__file__), 'res'),
|
||||
'/pkg': ('werkzeug.debug', 'shared'),
|
||||
'/foo': test_dir
|
||||
})
|
||||
|
||||
for p in '/test.txt', '/sources/test.txt', '/foo/äöü':
|
||||
app_iter, status, headers = run_wsgi_app(app, create_environ(p))
|
||||
self.assert_equal(status, '200 OK')
|
||||
with closing(app_iter) as app_iter:
|
||||
data = b''.join(app_iter).strip()
|
||||
self.assert_equal(data, b'FOUND')
|
||||
|
||||
app_iter, status, headers = run_wsgi_app(
|
||||
app, create_environ('/pkg/debugger.js'))
|
||||
with closing(app_iter) as app_iter:
|
||||
contents = b''.join(app_iter)
|
||||
self.assert_in(b'$(function() {', contents)
|
||||
|
||||
app_iter, status, headers = run_wsgi_app(
|
||||
app, create_environ('/missing'))
|
||||
self.assert_equal(status, '404 NOT FOUND')
|
||||
self.assert_equal(b''.join(app_iter).strip(), b'NOT FOUND')
|
||||
|
||||
|
||||
def test_get_host(self):
|
||||
env = {'HTTP_X_FORWARDED_HOST': 'example.org',
|
||||
'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'}
|
||||
self.assert_equal(wsgi.get_host(env), 'example.org')
|
||||
self.assert_equal(
|
||||
wsgi.get_host(create_environ('/', 'http://example.org')),
|
||||
'example.org')
|
||||
|
||||
def test_get_host_multiple_forwarded(self):
|
||||
env = {'HTTP_X_FORWARDED_HOST': 'example.com, example.org',
|
||||
'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'}
|
||||
self.assert_equal(wsgi.get_host(env), 'example.com')
|
||||
self.assert_equal(
|
||||
wsgi.get_host(create_environ('/', 'http://example.com')),
|
||||
'example.com')
|
||||
|
||||
def test_get_host_validation(self):
|
||||
env = {'HTTP_X_FORWARDED_HOST': 'example.org',
|
||||
'SERVER_NAME': 'bullshit', 'HOST_NAME': 'ignore me dammit'}
|
||||
self.assert_equal(wsgi.get_host(env, trusted_hosts=['.example.org']),
|
||||
'example.org')
|
||||
self.assert_raises(BadRequest, wsgi.get_host, env,
|
||||
trusted_hosts=['example.com'])
|
||||
|
||||
def test_responder(self):
|
||||
def foo(environ, start_response):
|
||||
return BaseResponse(b'Test')
|
||||
client = Client(wsgi.responder(foo), BaseResponse)
|
||||
response = client.get('/')
|
||||
self.assert_equal(response.status_code, 200)
|
||||
self.assert_equal(response.data, b'Test')
|
||||
|
||||
def test_pop_path_info(self):
|
||||
original_env = {'SCRIPT_NAME': '/foo', 'PATH_INFO': '/a/b///c'}
|
||||
|
||||
# regular path info popping
|
||||
def assert_tuple(script_name, path_info):
|
||||
self.assert_equal(env.get('SCRIPT_NAME'), script_name)
|
||||
self.assert_equal(env.get('PATH_INFO'), path_info)
|
||||
env = original_env.copy()
|
||||
pop = lambda: wsgi.pop_path_info(env)
|
||||
|
||||
assert_tuple('/foo', '/a/b///c')
|
||||
self.assert_equal(pop(), 'a')
|
||||
assert_tuple('/foo/a', '/b///c')
|
||||
self.assert_equal(pop(), 'b')
|
||||
assert_tuple('/foo/a/b', '///c')
|
||||
self.assert_equal(pop(), 'c')
|
||||
assert_tuple('/foo/a/b///c', '')
|
||||
self.assert_is_none(pop())
|
||||
|
||||
def test_peek_path_info(self):
|
||||
env = {
|
||||
'SCRIPT_NAME': '/foo',
|
||||
'PATH_INFO': '/aaa/b///c'
|
||||
}
|
||||
|
||||
self.assert_equal(wsgi.peek_path_info(env), 'aaa')
|
||||
self.assert_equal(wsgi.peek_path_info(env), 'aaa')
|
||||
self.assert_equal(wsgi.peek_path_info(env, charset=None), b'aaa')
|
||||
self.assert_equal(wsgi.peek_path_info(env, charset=None), b'aaa')
|
||||
|
||||
def test_path_info_and_script_name_fetching(self):
|
||||
env = create_environ(u'/\N{SNOWMAN}', u'http://example.com/\N{COMET}/')
|
||||
self.assert_equal(wsgi.get_path_info(env), u'/\N{SNOWMAN}')
|
||||
self.assert_equal(wsgi.get_path_info(env, charset=None), u'/\N{SNOWMAN}'.encode('utf-8'))
|
||||
self.assert_equal(wsgi.get_script_name(env), u'/\N{COMET}')
|
||||
self.assert_equal(wsgi.get_script_name(env, charset=None), u'/\N{COMET}'.encode('utf-8'))
|
||||
|
||||
def test_query_string_fetching(self):
|
||||
env = create_environ(u'/?\N{SNOWMAN}=\N{COMET}')
|
||||
qs = wsgi.get_query_string(env)
|
||||
self.assert_strict_equal(qs, '%E2%98%83=%E2%98%84')
|
||||
|
||||
def test_limited_stream(self):
|
||||
class RaisingLimitedStream(wsgi.LimitedStream):
|
||||
def on_exhausted(self):
|
||||
raise BadRequest('input stream exhausted')
|
||||
|
||||
io = BytesIO(b'123456')
|
||||
stream = RaisingLimitedStream(io, 3)
|
||||
self.assert_strict_equal(stream.read(), b'123')
|
||||
self.assert_raises(BadRequest, stream.read)
|
||||
|
||||
io = BytesIO(b'123456')
|
||||
stream = RaisingLimitedStream(io, 3)
|
||||
self.assert_strict_equal(stream.tell(), 0)
|
||||
self.assert_strict_equal(stream.read(1), b'1')
|
||||
self.assert_strict_equal(stream.tell(), 1)
|
||||
self.assert_strict_equal(stream.read(1), b'2')
|
||||
self.assert_strict_equal(stream.tell(), 2)
|
||||
self.assert_strict_equal(stream.read(1), b'3')
|
||||
self.assert_strict_equal(stream.tell(), 3)
|
||||
self.assert_raises(BadRequest, stream.read)
|
||||
|
||||
io = BytesIO(b'123456\nabcdefg')
|
||||
stream = wsgi.LimitedStream(io, 9)
|
||||
self.assert_strict_equal(stream.readline(), b'123456\n')
|
||||
self.assert_strict_equal(stream.readline(), b'ab')
|
||||
|
||||
io = BytesIO(b'123456\nabcdefg')
|
||||
stream = wsgi.LimitedStream(io, 9)
|
||||
self.assert_strict_equal(stream.readlines(), [b'123456\n', b'ab'])
|
||||
|
||||
io = BytesIO(b'123456\nabcdefg')
|
||||
stream = wsgi.LimitedStream(io, 9)
|
||||
self.assert_strict_equal(stream.readlines(2), [b'12'])
|
||||
self.assert_strict_equal(stream.readlines(2), [b'34'])
|
||||
self.assert_strict_equal(stream.readlines(), [b'56\n', b'ab'])
|
||||
|
||||
io = BytesIO(b'123456\nabcdefg')
|
||||
stream = wsgi.LimitedStream(io, 9)
|
||||
self.assert_strict_equal(stream.readline(100), b'123456\n')
|
||||
|
||||
io = BytesIO(b'123456\nabcdefg')
|
||||
stream = wsgi.LimitedStream(io, 9)
|
||||
self.assert_strict_equal(stream.readlines(100), [b'123456\n', b'ab'])
|
||||
|
||||
io = BytesIO(b'123456')
|
||||
stream = wsgi.LimitedStream(io, 3)
|
||||
self.assert_strict_equal(stream.read(1), b'1')
|
||||
self.assert_strict_equal(stream.read(1), b'2')
|
||||
self.assert_strict_equal(stream.read(), b'3')
|
||||
self.assert_strict_equal(stream.read(), b'')
|
||||
|
||||
io = BytesIO(b'123456')
|
||||
stream = wsgi.LimitedStream(io, 3)
|
||||
self.assert_strict_equal(stream.read(-1), b'123')
|
||||
|
||||
io = BytesIO(b'123456')
|
||||
stream = wsgi.LimitedStream(io, 0)
|
||||
self.assert_strict_equal(stream.read(-1), b'')
|
||||
|
||||
io = StringIO(u'123456')
|
||||
stream = wsgi.LimitedStream(io, 0)
|
||||
self.assert_strict_equal(stream.read(-1), u'')
|
||||
|
||||
io = StringIO(u'123\n456\n')
|
||||
stream = wsgi.LimitedStream(io, 8)
|
||||
self.assert_strict_equal(list(stream), [u'123\n', u'456\n'])
|
||||
|
||||
def test_limited_stream_disconnection(self):
|
||||
io = BytesIO(b'A bit of content')
|
||||
|
||||
# disconnect detection on out of bytes
|
||||
stream = wsgi.LimitedStream(io, 255)
|
||||
with self.assert_raises(ClientDisconnected):
|
||||
stream.read()
|
||||
|
||||
# disconnect detection because file close
|
||||
io = BytesIO(b'x' * 255)
|
||||
io.close()
|
||||
stream = wsgi.LimitedStream(io, 255)
|
||||
with self.assert_raises(ClientDisconnected):
|
||||
stream.read()
|
||||
|
||||
def test_path_info_extraction(self):
|
||||
x = wsgi.extract_path_info('http://example.com/app', '/app/hello')
|
||||
self.assert_equal(x, u'/hello')
|
||||
x = wsgi.extract_path_info('http://example.com/app',
|
||||
'https://example.com/app/hello')
|
||||
self.assert_equal(x, u'/hello')
|
||||
x = wsgi.extract_path_info('http://example.com/app/',
|
||||
'https://example.com/app/hello')
|
||||
self.assert_equal(x, u'/hello')
|
||||
x = wsgi.extract_path_info('http://example.com/app/',
|
||||
'https://example.com/app')
|
||||
self.assert_equal(x, u'/')
|
||||
x = wsgi.extract_path_info(u'http://☃.net/', u'/fööbär')
|
||||
self.assert_equal(x, u'/fööbär')
|
||||
x = wsgi.extract_path_info(u'http://☃.net/x', u'http://☃.net/x/fööbär')
|
||||
self.assert_equal(x, u'/fööbär')
|
||||
|
||||
env = create_environ(u'/fööbär', u'http://☃.net/x/')
|
||||
x = wsgi.extract_path_info(env, u'http://☃.net/x/fööbär')
|
||||
self.assert_equal(x, u'/fööbär')
|
||||
|
||||
x = wsgi.extract_path_info('http://example.com/app/',
|
||||
'https://example.com/a/hello')
|
||||
self.assert_is_none(x)
|
||||
x = wsgi.extract_path_info('http://example.com/app/',
|
||||
'https://example.com/app/hello',
|
||||
collapse_http_schemes=False)
|
||||
self.assert_is_none(x)
|
||||
|
||||
def test_get_host_fallback(self):
|
||||
self.assert_equal(wsgi.get_host({
|
||||
'SERVER_NAME': 'foobar.example.com',
|
||||
'wsgi.url_scheme': 'http',
|
||||
'SERVER_PORT': '80'
|
||||
}), 'foobar.example.com')
|
||||
self.assert_equal(wsgi.get_host({
|
||||
'SERVER_NAME': 'foobar.example.com',
|
||||
'wsgi.url_scheme': 'http',
|
||||
'SERVER_PORT': '81'
|
||||
}), 'foobar.example.com:81')
|
||||
|
||||
def test_get_current_url_unicode(self):
|
||||
env = create_environ()
|
||||
env['QUERY_STRING'] = 'foo=bar&baz=blah&meh=\xcf'
|
||||
rv = wsgi.get_current_url(env)
|
||||
self.assert_strict_equal(rv,
|
||||
u'http://localhost/?foo=bar&baz=blah&meh=\ufffd')
|
||||
|
||||
def test_multi_part_line_breaks(self):
|
||||
data = 'abcdef\r\nghijkl\r\nmnopqrstuvwxyz\r\nABCDEFGHIJK'
|
||||
test_stream = NativeStringIO(data)
|
||||
lines = list(wsgi.make_line_iter(test_stream, limit=len(data),
|
||||
buffer_size=16))
|
||||
self.assert_equal(lines, ['abcdef\r\n', 'ghijkl\r\n',
|
||||
'mnopqrstuvwxyz\r\n', 'ABCDEFGHIJK'])
|
||||
|
||||
data = 'abc\r\nThis line is broken by the buffer length.' \
|
||||
'\r\nFoo bar baz'
|
||||
test_stream = NativeStringIO(data)
|
||||
lines = list(wsgi.make_line_iter(test_stream, limit=len(data),
|
||||
buffer_size=24))
|
||||
self.assert_equal(lines, ['abc\r\n', 'This line is broken by the '
|
||||
'buffer length.\r\n', 'Foo bar baz'])
|
||||
|
||||
def test_multi_part_line_breaks_bytes(self):
|
||||
data = b'abcdef\r\nghijkl\r\nmnopqrstuvwxyz\r\nABCDEFGHIJK'
|
||||
test_stream = BytesIO(data)
|
||||
lines = list(wsgi.make_line_iter(test_stream, limit=len(data),
|
||||
buffer_size=16))
|
||||
self.assert_equal(lines, [b'abcdef\r\n', b'ghijkl\r\n',
|
||||
b'mnopqrstuvwxyz\r\n', b'ABCDEFGHIJK'])
|
||||
|
||||
data = b'abc\r\nThis line is broken by the buffer length.' \
|
||||
b'\r\nFoo bar baz'
|
||||
test_stream = BytesIO(data)
|
||||
lines = list(wsgi.make_line_iter(test_stream, limit=len(data),
|
||||
buffer_size=24))
|
||||
self.assert_equal(lines, [b'abc\r\n', b'This line is broken by the '
|
||||
b'buffer length.\r\n', b'Foo bar baz'])
|
||||
|
||||
def test_multi_part_line_breaks_problematic(self):
|
||||
data = 'abc\rdef\r\nghi'
|
||||
for x in range(1, 10):
|
||||
test_stream = NativeStringIO(data)
|
||||
lines = list(wsgi.make_line_iter(test_stream, limit=len(data),
|
||||
buffer_size=4))
|
||||
self.assert_equal(lines, ['abc\r', 'def\r\n', 'ghi'])
|
||||
|
||||
def test_iter_functions_support_iterators(self):
|
||||
data = ['abcdef\r\nghi', 'jkl\r\nmnopqrstuvwxyz\r', '\nABCDEFGHIJK']
|
||||
lines = list(wsgi.make_line_iter(data))
|
||||
self.assert_equal(lines, ['abcdef\r\n', 'ghijkl\r\n',
|
||||
'mnopqrstuvwxyz\r\n', 'ABCDEFGHIJK'])
|
||||
|
||||
def test_make_chunk_iter(self):
|
||||
data = [u'abcdefXghi', u'jklXmnopqrstuvwxyzX', u'ABCDEFGHIJK']
|
||||
rv = list(wsgi.make_chunk_iter(data, 'X'))
|
||||
self.assert_equal(rv, [u'abcdef', u'ghijkl', u'mnopqrstuvwxyz',
|
||||
u'ABCDEFGHIJK'])
|
||||
|
||||
data = u'abcdefXghijklXmnopqrstuvwxyzXABCDEFGHIJK'
|
||||
test_stream = StringIO(data)
|
||||
rv = list(wsgi.make_chunk_iter(test_stream, 'X', limit=len(data),
|
||||
buffer_size=4))
|
||||
self.assert_equal(rv, [u'abcdef', u'ghijkl', u'mnopqrstuvwxyz',
|
||||
u'ABCDEFGHIJK'])
|
||||
|
||||
def test_make_chunk_iter_bytes(self):
|
||||
data = [b'abcdefXghi', b'jklXmnopqrstuvwxyzX', b'ABCDEFGHIJK']
|
||||
rv = list(wsgi.make_chunk_iter(data, 'X'))
|
||||
self.assert_equal(rv, [b'abcdef', b'ghijkl', b'mnopqrstuvwxyz',
|
||||
b'ABCDEFGHIJK'])
|
||||
|
||||
data = b'abcdefXghijklXmnopqrstuvwxyzXABCDEFGHIJK'
|
||||
test_stream = BytesIO(data)
|
||||
rv = list(wsgi.make_chunk_iter(test_stream, 'X', limit=len(data),
|
||||
buffer_size=4))
|
||||
self.assert_equal(rv, [b'abcdef', b'ghijkl', b'mnopqrstuvwxyz',
|
||||
b'ABCDEFGHIJK'])
|
||||
|
||||
def test_lines_longer_buffer_size(self):
|
||||
data = '1234567890\n1234567890\n'
|
||||
for bufsize in range(1, 15):
|
||||
lines = list(wsgi.make_line_iter(NativeStringIO(data), limit=len(data),
|
||||
buffer_size=4))
|
||||
self.assert_equal(lines, ['1234567890\n', '1234567890\n'])
|
||||
|
||||
|
||||
def suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTest(unittest.makeSuite(WSGIUtilsTestCase))
|
||||
return suite
|
||||
@@ -3,16 +3,24 @@
|
||||
werkzeug.urls
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This module implements various URL related functions.
|
||||
``werkzeug.urls`` used to provide several wrapper functions for Python 2
|
||||
urlparse, whose main purpose were to work around the behavior of the Py2
|
||||
stdlib and its lack of unicode support. While this was already a somewhat
|
||||
inconvenient situation, it got even more complicated because Python 3's
|
||||
``urllib.parse`` actually does handle unicode properly. In other words,
|
||||
this module would wrap two libraries with completely different behavior. So
|
||||
now this module contains a 2-and-3-compatible backport of Python 3's
|
||||
``urllib.parse``, which is mostly API-compatible.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from werkzeug._compat import text_type, PY2, to_unicode, \
|
||||
to_native, implements_to_string, try_coerce_native, \
|
||||
normalize_string_tuple, make_literal_wrapper, \
|
||||
fix_tuple_repr
|
||||
to_native, implements_to_string, try_coerce_native, \
|
||||
normalize_string_tuple, make_literal_wrapper, \
|
||||
fix_tuple_repr
|
||||
from werkzeug._internal import _encode_idna, _decode_idna
|
||||
from werkzeug.datastructures import MultiDict, iter_multi_items
|
||||
from collections import namedtuple
|
||||
@@ -32,11 +40,15 @@ _hextobyte = dict(
|
||||
)
|
||||
|
||||
|
||||
_URLTuple = fix_tuple_repr(namedtuple('_URLTuple',
|
||||
['scheme', 'netloc', 'path', 'query', 'fragment']))
|
||||
_URLTuple = fix_tuple_repr(namedtuple(
|
||||
'_URLTuple',
|
||||
['scheme', 'netloc', 'path', 'query', 'fragment']
|
||||
))
|
||||
|
||||
|
||||
class _URLMixin(object):
|
||||
class BaseURL(_URLTuple):
|
||||
|
||||
'''Superclass of :py:class:`URL` and :py:class:`BytesURL`.'''
|
||||
__slots__ = ()
|
||||
|
||||
def replace(self, **kwargs):
|
||||
@@ -61,7 +73,10 @@ class _URLMixin(object):
|
||||
"""
|
||||
rv = self.host
|
||||
if rv is not None and isinstance(rv, text_type):
|
||||
rv = _encode_idna(rv)
|
||||
try:
|
||||
rv = _encode_idna(rv)
|
||||
except UnicodeError:
|
||||
rv = rv.encode('ascii', 'ignore')
|
||||
return to_native(rv, 'ascii', 'ignore')
|
||||
|
||||
@property
|
||||
@@ -174,6 +189,64 @@ class _URLMixin(object):
|
||||
"""
|
||||
return url_parse(uri_to_iri(self))
|
||||
|
||||
def get_file_location(self, pathformat=None):
|
||||
"""Returns a tuple with the location of the file in the form
|
||||
``(server, location)``. If the netloc is empty in the URL or
|
||||
points to localhost, it's represented as ``None``.
|
||||
|
||||
The `pathformat` by default is autodetection but needs to be set
|
||||
when working with URLs of a specific system. The supported values
|
||||
are ``'windows'`` when working with Windows or DOS paths and
|
||||
``'posix'`` when working with posix paths.
|
||||
|
||||
If the URL does not point to to a local file, the server and location
|
||||
are both represented as ``None``.
|
||||
|
||||
:param pathformat: The expected format of the path component.
|
||||
Currently ``'windows'`` and ``'posix'`` are
|
||||
supported. Defaults to ``None`` which is
|
||||
autodetect.
|
||||
"""
|
||||
if self.scheme != 'file':
|
||||
return None, None
|
||||
|
||||
path = url_unquote(self.path)
|
||||
host = self.netloc or None
|
||||
|
||||
if pathformat is None:
|
||||
if os.name == 'nt':
|
||||
pathformat = 'windows'
|
||||
else:
|
||||
pathformat = 'posix'
|
||||
|
||||
if pathformat == 'windows':
|
||||
if path[:1] == '/' and path[1:2].isalpha() and path[2:3] in '|:':
|
||||
path = path[1:2] + ':' + path[3:]
|
||||
windows_share = path[:3] in ('\\' * 3, '/' * 3)
|
||||
import ntpath
|
||||
path = ntpath.normpath(path)
|
||||
# Windows shared drives are represented as ``\\host\\directory``.
|
||||
# That results in a URL like ``file://///host/directory``, and a
|
||||
# path like ``///host/directory``. We need to special-case this
|
||||
# because the path contains the hostname.
|
||||
if windows_share and host is None:
|
||||
parts = path.lstrip('\\').split('\\', 1)
|
||||
if len(parts) == 2:
|
||||
host, path = parts
|
||||
else:
|
||||
host = parts[0]
|
||||
path = ''
|
||||
elif pathformat == 'posix':
|
||||
import posixpath
|
||||
path = posixpath.normpath(path)
|
||||
else:
|
||||
raise TypeError('Invalid path format %s' % repr(pathformat))
|
||||
|
||||
if host in ('127.0.0.1', '::1', 'localhost'):
|
||||
host = None
|
||||
|
||||
return host, path
|
||||
|
||||
def _split_netloc(self):
|
||||
if self._at in self.netloc:
|
||||
return self.netloc.split(self._at, 1)
|
||||
@@ -209,7 +282,8 @@ class _URLMixin(object):
|
||||
|
||||
|
||||
@implements_to_string
|
||||
class URL(_URLTuple, _URLMixin):
|
||||
class URL(BaseURL):
|
||||
|
||||
"""Represents a parsed URL. This behaves like a regular tuple but
|
||||
also has some extra attributes that give further insight into the
|
||||
URL.
|
||||
@@ -237,7 +311,7 @@ class URL(_URLTuple, _URLMixin):
|
||||
]))
|
||||
if auth:
|
||||
rv = '%s@%s' % (auth, rv)
|
||||
return rv.encode('ascii')
|
||||
return to_native(rv)
|
||||
|
||||
def encode(self, charset='utf-8', errors='replace'):
|
||||
"""Encodes the URL to a tuple made out of bytes. The charset is
|
||||
@@ -252,7 +326,8 @@ class URL(_URLTuple, _URLMixin):
|
||||
)
|
||||
|
||||
|
||||
class BytesURL(_URLTuple, _URLMixin):
|
||||
class BytesURL(BaseURL):
|
||||
|
||||
"""Represents a parsed URL in bytes."""
|
||||
__slots__ = ()
|
||||
_at = b'@'
|
||||
@@ -312,7 +387,7 @@ def _url_encode_impl(obj, charset, encode_keys, sort, key):
|
||||
key = text_type(key).encode(charset)
|
||||
if not isinstance(value, bytes):
|
||||
value = text_type(value).encode(charset)
|
||||
yield url_quote(key) + '=' + url_quote_plus(value)
|
||||
yield url_quote_plus(key) + '=' + url_quote_plus(value)
|
||||
|
||||
|
||||
def _url_unquote_legacy(value, unsafe=''):
|
||||
@@ -358,8 +433,8 @@ def url_parse(url, scheme=None, allow_fragments=True):
|
||||
if wdelim >= 0:
|
||||
delim = min(delim, wdelim)
|
||||
netloc, url = url[2:delim], url[delim:]
|
||||
if ((s('[') in netloc and s(']') not in netloc) or
|
||||
(s(']') in netloc and s('[') not in netloc)):
|
||||
if (s('[') in netloc and s(']') not in netloc) or \
|
||||
(s(']') in netloc and s('[') not in netloc):
|
||||
raise ValueError('Invalid IPv6 URL')
|
||||
|
||||
if allow_fragments and s('#') in url:
|
||||
@@ -371,12 +446,16 @@ def url_parse(url, scheme=None, allow_fragments=True):
|
||||
return result_type(scheme, netloc, url, query, fragment)
|
||||
|
||||
|
||||
def url_quote(string, charset='utf-8', errors='strict', safe='/:'):
|
||||
def url_quote(string, charset='utf-8', errors='strict', safe='/:', unsafe=''):
|
||||
"""URL encode a single string with a given encoding.
|
||||
|
||||
:param s: the string to quote.
|
||||
:param charset: the charset to be used.
|
||||
:param safe: an optional sequence of safe characters.
|
||||
:param unsafe: an optional sequence of unsafe characters.
|
||||
|
||||
.. versionadded:: 0.9.2
|
||||
The `unsafe` parameter was added.
|
||||
"""
|
||||
if not isinstance(string, (text_type, bytes, bytearray)):
|
||||
string = text_type(string)
|
||||
@@ -384,7 +463,9 @@ def url_quote(string, charset='utf-8', errors='strict', safe='/:'):
|
||||
string = string.encode(charset, errors)
|
||||
if isinstance(safe, text_type):
|
||||
safe = safe.encode(charset, errors)
|
||||
safe = frozenset(bytearray(safe) + _always_safe)
|
||||
if isinstance(unsafe, text_type):
|
||||
unsafe = unsafe.encode(charset, errors)
|
||||
safe = frozenset(bytearray(safe) + _always_safe) - frozenset(bytearray(unsafe))
|
||||
rv = bytearray()
|
||||
for char in bytearray(string):
|
||||
if char in safe:
|
||||
@@ -402,7 +483,7 @@ def url_quote_plus(string, charset='utf-8', errors='strict', safe=''):
|
||||
:param charset: The charset to be used.
|
||||
:param safe: An optional sequence of safe characters.
|
||||
"""
|
||||
return url_quote(string, charset, errors, safe + ' ').replace(' ', '+')
|
||||
return url_quote(string, charset, errors, safe + ' ', '+').replace(' ', '+')
|
||||
|
||||
|
||||
def url_unparse(components):
|
||||
@@ -485,10 +566,22 @@ def url_fix(s, charset='utf-8'):
|
||||
:param charset: The target charset for the URL if the url was given as
|
||||
unicode string.
|
||||
"""
|
||||
scheme, netloc, path, qs, anchor = url_parse(to_unicode(s, charset, 'replace'))
|
||||
path = url_quote(path, charset, safe='/%+$!*\'(),')
|
||||
qs = url_quote_plus(qs, charset, safe=':&%=+$!*\'(),')
|
||||
return to_native(url_unparse((scheme, netloc, path, qs, anchor)))
|
||||
# First step is to switch to unicode processing and to convert
|
||||
# backslashes (which are invalid in URLs anyways) to slashes. This is
|
||||
# consistent with what Chrome does.
|
||||
s = to_unicode(s, charset, 'replace').replace('\\', '/')
|
||||
|
||||
# For the specific case that we look like a malformed windows URL
|
||||
# we want to fix this up manually:
|
||||
if s.startswith('file://') and s[7:8].isalpha() and s[8:10] in (':/', '|/'):
|
||||
s = 'file:///' + s[7:]
|
||||
|
||||
url = url_parse(s)
|
||||
path = url_quote(url.path, charset, safe='/%+$!*\'(),')
|
||||
qs = url_quote_plus(url.query, charset, safe=':&%=+$!*\'(),')
|
||||
anchor = url_quote_plus(url.fragment, charset, safe=':&%=+$!*\'(),')
|
||||
return to_native(url_unparse((url.scheme, url.encode_netloc(),
|
||||
path, qs, anchor)))
|
||||
|
||||
|
||||
def uri_to_iri(uri, charset='utf-8', errors='replace'):
|
||||
@@ -516,14 +609,14 @@ def uri_to_iri(uri, charset='utf-8', errors='replace'):
|
||||
if isinstance(uri, tuple):
|
||||
uri = url_unparse(uri)
|
||||
uri = url_parse(to_unicode(uri, charset))
|
||||
path = url_unquote(uri.path, charset, errors, '/;?')
|
||||
query = url_unquote(uri.query, charset, errors, ';/?:@&=+,$')
|
||||
fragment = url_unquote(uri.fragment, charset, errors, ';/?:@&=+,$')
|
||||
path = url_unquote(uri.path, charset, errors, '%/;?')
|
||||
query = url_unquote(uri.query, charset, errors, '%;/?:@&=+,$#')
|
||||
fragment = url_unquote(uri.fragment, charset, errors, '%;/?:@&=+,$#')
|
||||
return url_unparse((uri.scheme, uri.decode_netloc(),
|
||||
path, query, fragment))
|
||||
|
||||
|
||||
def iri_to_uri(iri, charset='utf-8', errors='strict'):
|
||||
def iri_to_uri(iri, charset='utf-8', errors='strict', safe_conversion=False):
|
||||
r"""
|
||||
Converts any unicode based IRI to an acceptable ASCII URI. Werkzeug always
|
||||
uses utf-8 URLs internally because this is what browsers and HTTP do as
|
||||
@@ -537,16 +630,49 @@ def iri_to_uri(iri, charset='utf-8', errors='strict'):
|
||||
>>> iri_to_uri(u'http://üser:pässword@☃.net/påth')
|
||||
'http://%C3%BCser:p%C3%A4ssword@xn--n3h.net/p%C3%A5th'
|
||||
|
||||
There is a general problem with IRI and URI conversion with some
|
||||
protocols that appear in the wild that are in violation of the URI
|
||||
specification. In places where Werkzeug goes through a forced IRI to
|
||||
URI conversion it will set the `safe_conversion` flag which will
|
||||
not perform a conversion if the end result is already ASCII. This
|
||||
can mean that the return value is not an entirely correct URI but
|
||||
it will not destroy such invalid URLs in the process.
|
||||
|
||||
As an example consider the following two IRIs::
|
||||
|
||||
magnet:?xt=uri:whatever
|
||||
itms-services://?action=download-manifest
|
||||
|
||||
The internal representation after parsing of those URLs is the same
|
||||
and there is no way to reconstruct the original one. If safe
|
||||
conversion is enabled however this function becomes a noop for both of
|
||||
those strings as they both can be considered URIs.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
|
||||
.. versionchanged:: 0.9.6
|
||||
The `safe_conversion` parameter was added.
|
||||
|
||||
:param iri: The IRI to convert.
|
||||
:param charset: The charset for the URI.
|
||||
:param safe_conversion: indicates if a safe conversion should take place.
|
||||
For more information see the explanation above.
|
||||
"""
|
||||
if isinstance(iri, tuple):
|
||||
iri = url_unparse(iri)
|
||||
|
||||
if safe_conversion:
|
||||
try:
|
||||
native_iri = to_native(iri)
|
||||
ascii_iri = to_native(iri).encode('ascii')
|
||||
if ascii_iri.split() == [ascii_iri]:
|
||||
return native_iri
|
||||
except UnicodeError:
|
||||
pass
|
||||
|
||||
iri = url_parse(to_unicode(iri, charset, errors))
|
||||
|
||||
netloc = iri.encode_netloc().decode('ascii')
|
||||
netloc = iri.encode_netloc()
|
||||
path = url_quote(iri.path, charset, errors, '/:~+%')
|
||||
query = url_quote(iri.query, charset, errors, '%&[]:;$*()+,!?*/=')
|
||||
fragment = url_quote(iri.fragment, charset, errors, '=%&[]:;$()+,!?*/')
|
||||
@@ -789,6 +915,7 @@ def url_join(base, url, allow_fragments=True):
|
||||
|
||||
|
||||
class Href(object):
|
||||
|
||||
"""Implements a callable that constructs URLs with the given base. The
|
||||
function can be called with any number of positional and keyword
|
||||
arguments which than are used to assemble the URL. Works with URLs
|
||||
@@ -865,7 +992,7 @@ class Href(object):
|
||||
query = dict([(k.endswith('_') and k[:-1] or k, v)
|
||||
for k, v in query.items()])
|
||||
path = '/'.join([to_unicode(url_quote(x, self.charset), 'ascii')
|
||||
for x in path if x is not None]).lstrip('/')
|
||||
for x in path if x is not None]).lstrip('/')
|
||||
rv = self.base
|
||||
if path:
|
||||
if not rv.endswith('/'):
|
||||
|
||||
@@ -8,16 +8,18 @@
|
||||
browsers.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
|
||||
|
||||
class UserAgentParser(object):
|
||||
|
||||
"""A simple user agent parser. Used by the `UserAgent`."""
|
||||
|
||||
platforms = (
|
||||
('cros', 'chromeos'),
|
||||
('iphone|ios', 'iphone'),
|
||||
('ipad', 'ipad'),
|
||||
(r'darwin|mac|os\s*x', 'macos'),
|
||||
@@ -32,7 +34,8 @@ class UserAgentParser(object):
|
||||
('sco|unix_sv', 'sco'),
|
||||
('bsd', 'bsd'),
|
||||
('amiga', 'amiga'),
|
||||
('blackberry|playbook', 'blackberry')
|
||||
('blackberry|playbook', 'blackberry'),
|
||||
('symbian', 'symbian')
|
||||
)
|
||||
browsers = (
|
||||
('googlebot', 'google'),
|
||||
@@ -44,13 +47,13 @@ class UserAgentParser(object):
|
||||
('chrome', 'chrome'),
|
||||
('firefox|firebird|phoenix|iceweasel', 'firefox'),
|
||||
('galeon', 'galeon'),
|
||||
('safari', 'safari'),
|
||||
('safari|version', 'safari'),
|
||||
('webkit', 'webkit'),
|
||||
('camino', 'camino'),
|
||||
('konqueror', 'konqueror'),
|
||||
('k-meleon', 'kmeleon'),
|
||||
('netscape', 'netscape'),
|
||||
(r'msie|microsoft\s+internet\s+explorer', 'msie'),
|
||||
(r'msie|microsoft\s+internet\s+explorer|trident/.+? rv:', 'msie'),
|
||||
('lynx', 'lynx'),
|
||||
('links', 'links'),
|
||||
('seamonkey|mozilla', 'seamonkey')
|
||||
@@ -90,6 +93,7 @@ class UserAgentParser(object):
|
||||
|
||||
|
||||
class UserAgent(object):
|
||||
|
||||
"""Represents a user agent. Pass it a WSGI environment or a user agent
|
||||
string and you can inspect some of the details from the user agent
|
||||
string via the attributes. The following attributes exist:
|
||||
@@ -107,6 +111,7 @@ class UserAgent(object):
|
||||
- `amiga`
|
||||
- `android`
|
||||
- `bsd`
|
||||
- `chromeos`
|
||||
- `hpux`
|
||||
- `iphone`
|
||||
- `ipad`
|
||||
@@ -187,4 +192,4 @@ class UserAgent(object):
|
||||
# it afterwards. The class itself has the module set to this module so
|
||||
# pickle, inspect and similar modules treat the object as if it was really
|
||||
# implemented here.
|
||||
from werkzeug.wrappers import UserAgentMixin
|
||||
from werkzeug.wrappers import UserAgentMixin # noqa
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
them are used by the request and response wrappers but especially for
|
||||
middleware development it makes sense to use them without the wrappers.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
@@ -22,7 +22,7 @@ except ImportError:
|
||||
from werkzeug._compat import unichr, text_type, string_types, iteritems, \
|
||||
reraise, PY2
|
||||
from werkzeug._internal import _DictAccessorProperty, \
|
||||
_parse_signature, _missing
|
||||
_parse_signature, _missing
|
||||
|
||||
|
||||
_format_re = re.compile(r'\$(?:(%s)|\{(%s)\})' % (('[a-zA-Z_][a-zA-Z0-9_]*',) * 2))
|
||||
@@ -32,7 +32,8 @@ _windows_device_files = ('CON', 'AUX', 'COM1', 'COM2', 'COM3', 'COM4', 'LPT1',
|
||||
'LPT2', 'LPT3', 'PRN', 'NUL')
|
||||
|
||||
|
||||
class cached_property(object):
|
||||
class cached_property(property):
|
||||
|
||||
"""A decorator that converts a function into a lazy property. The
|
||||
function wrapped is called the first time to retrieve the result
|
||||
and then that calculated result is used the next time you access
|
||||
@@ -49,13 +50,11 @@ class cached_property(object):
|
||||
work.
|
||||
"""
|
||||
|
||||
# implementation detail: this property is implemented as non-data
|
||||
# descriptor. non-data descriptors are only invoked if there is
|
||||
# no entry with the same name in the instance's __dict__.
|
||||
# this allows us to completely get rid of the access function call
|
||||
# overhead. If one choses to invoke __get__ by hand the property
|
||||
# will still work as expected because the lookup logic is replicated
|
||||
# in __get__ for manual invocation.
|
||||
# implementation detail: A subclass of python's builtin property
|
||||
# decorator, we override __get__ to check for a cached value. If one
|
||||
# choses to invoke __get__ by hand the property will still work as
|
||||
# expected because the lookup logic is replicated in __get__ for
|
||||
# manual invocation.
|
||||
|
||||
def __init__(self, func, name=None, doc=None):
|
||||
self.__name__ = name or func.__name__
|
||||
@@ -63,6 +62,9 @@ class cached_property(object):
|
||||
self.__doc__ = doc or func.__doc__
|
||||
self.func = func
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj.__dict__[self.__name__] = value
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
if obj is None:
|
||||
return self
|
||||
@@ -74,6 +76,7 @@ class cached_property(object):
|
||||
|
||||
|
||||
class environ_property(_DictAccessorProperty):
|
||||
|
||||
"""Maps request attributes to environment variables. This works not only
|
||||
for the Werzeug request object, but also any other class with an
|
||||
environ attribute:
|
||||
@@ -101,6 +104,7 @@ class environ_property(_DictAccessorProperty):
|
||||
|
||||
|
||||
class header_property(_DictAccessorProperty):
|
||||
|
||||
"""Like `environ_property` but for headers."""
|
||||
|
||||
def lookup(self, obj):
|
||||
@@ -108,6 +112,7 @@ class header_property(_DictAccessorProperty):
|
||||
|
||||
|
||||
class HTMLBuilder(object):
|
||||
|
||||
"""Helper object for HTML generation.
|
||||
|
||||
Per default there are two instances of that class. The `html` one, and
|
||||
@@ -157,6 +162,7 @@ class HTMLBuilder(object):
|
||||
def __getattr__(self, tag):
|
||||
if tag[:2] == '__':
|
||||
raise AttributeError(tag)
|
||||
|
||||
def proxy(*children, **arguments):
|
||||
buffer = '<' + tag
|
||||
for key, value in iteritems(arguments):
|
||||
@@ -183,7 +189,7 @@ class HTMLBuilder(object):
|
||||
buffer += '>'
|
||||
|
||||
children_as_string = ''.join([text_type(x) for x in children
|
||||
if x is not None])
|
||||
if x is not None])
|
||||
|
||||
if children_as_string:
|
||||
if tag in self._plaintext_elements:
|
||||
@@ -207,7 +213,7 @@ xhtml = HTMLBuilder('xhtml')
|
||||
|
||||
|
||||
def get_content_type(mimetype, charset):
|
||||
"""Return the full content type string with charset for a mimetype.
|
||||
"""Returns the full content type string with charset for a mimetype.
|
||||
|
||||
If the mimetype represents text the charset will be appended as charset
|
||||
parameter, otherwise the mimetype is returned unchanged.
|
||||
@@ -219,7 +225,7 @@ def get_content_type(mimetype, charset):
|
||||
if mimetype.startswith('text/') or \
|
||||
mimetype == 'application/xml' or \
|
||||
(mimetype.startswith('application/') and
|
||||
mimetype.endswith('+xml')):
|
||||
mimetype.endswith('+xml')):
|
||||
mimetype += '; charset=' + charset
|
||||
return mimetype
|
||||
|
||||
@@ -250,7 +256,7 @@ def secure_filename(filename):
|
||||
to :func:`os.path.join`. The filename returned is an ASCII only string
|
||||
for maximum portability.
|
||||
|
||||
On windows system the function also makes sure that the file is not
|
||||
On windows systems the function also makes sure that the file is not
|
||||
named after one of the special device files.
|
||||
|
||||
>>> secure_filename("My cool movie.mov")
|
||||
@@ -335,8 +341,8 @@ def unescape(s):
|
||||
return _entity_re.sub(handle_match, s)
|
||||
|
||||
|
||||
def redirect(location, code=302):
|
||||
"""Return a response object (a WSGI application) that, if called,
|
||||
def redirect(location, code=302, Response=None):
|
||||
"""Returns a response object (a WSGI application) that, if called,
|
||||
redirects the client to the target location. Supported codes are 301,
|
||||
302, 303, 305, and 307. 300 is not supported because it's not a real
|
||||
redirect and 304 because it's the answer for a request with a request
|
||||
@@ -346,14 +352,24 @@ def redirect(location, code=302):
|
||||
The location can now be a unicode string that is encoded using
|
||||
the :func:`iri_to_uri` function.
|
||||
|
||||
.. versionadded:: 0.10
|
||||
The class used for the Response object can now be passed in.
|
||||
|
||||
:param location: the location the response should redirect to.
|
||||
:param code: the redirect status code. defaults to 302.
|
||||
:param class Response: a Response class to use when instantiating a
|
||||
response. The default is :class:`werkzeug.wrappers.Response` if
|
||||
unspecified.
|
||||
"""
|
||||
from werkzeug.wrappers import Response
|
||||
if Response is None:
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
display_location = escape(location)
|
||||
if isinstance(location, text_type):
|
||||
# Safe conversion is necessary here as we might redirect
|
||||
# to a broken URI scheme (for instance itms-services).
|
||||
from werkzeug.urls import iri_to_uri
|
||||
location = iri_to_uri(location)
|
||||
location = iri_to_uri(location, safe_conversion=True)
|
||||
response = Response(
|
||||
'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
|
||||
'<title>Redirecting...</title>\n'
|
||||
@@ -366,7 +382,7 @@ def redirect(location, code=302):
|
||||
|
||||
|
||||
def append_slash_redirect(environ, code=301):
|
||||
"""Redirect to the same URL but with a slash appended. The behavior
|
||||
"""Redirects to the same URL but with a slash appended. The behavior
|
||||
of this function is undefined if the path ends with a slash already.
|
||||
|
||||
:param environ: the WSGI environment for the request that triggers
|
||||
@@ -393,29 +409,32 @@ def import_string(import_name, silent=False):
|
||||
`None` is returned instead.
|
||||
:return: imported object
|
||||
"""
|
||||
#XXX: py3 review needed
|
||||
assert isinstance(import_name, string_types)
|
||||
# force the import name to automatically convert to strings
|
||||
import_name = str(import_name)
|
||||
# __import__ is not able to handle unicode strings in the fromlist
|
||||
# if the module is a package
|
||||
import_name = str(import_name).replace(':', '.')
|
||||
try:
|
||||
if ':' in import_name:
|
||||
module, obj = import_name.split(':', 1)
|
||||
elif '.' in import_name:
|
||||
module, obj = import_name.rsplit('.', 1)
|
||||
else:
|
||||
return __import__(import_name)
|
||||
# __import__ is not able to handle unicode strings in the fromlist
|
||||
# if the module is a package
|
||||
if PY2 and isinstance(obj, unicode):
|
||||
obj = obj.encode('utf-8')
|
||||
try:
|
||||
return getattr(__import__(module, None, None, [obj]), obj)
|
||||
except (ImportError, AttributeError):
|
||||
__import__(import_name)
|
||||
except ImportError:
|
||||
if '.' not in import_name:
|
||||
raise
|
||||
else:
|
||||
return sys.modules[import_name]
|
||||
|
||||
module_name, obj_name = import_name.rsplit('.', 1)
|
||||
try:
|
||||
module = __import__(module_name, None, None, [obj_name])
|
||||
except ImportError:
|
||||
# support importing modules not yet set up by the parent module
|
||||
# (or package for that matter)
|
||||
modname = module + '.' + obj
|
||||
__import__(modname)
|
||||
return sys.modules[modname]
|
||||
module = import_string(module_name)
|
||||
|
||||
try:
|
||||
return getattr(module, obj_name)
|
||||
except AttributeError as e:
|
||||
raise ImportError(e)
|
||||
|
||||
except ImportError as e:
|
||||
if not silent:
|
||||
reraise(
|
||||
@@ -425,7 +444,7 @@ def import_string(import_name, silent=False):
|
||||
|
||||
|
||||
def find_modules(import_path, include_packages=False, recursive=False):
|
||||
"""Find all the modules below a package. This can be useful to
|
||||
"""Finds all the modules below a package. This can be useful to
|
||||
automatically import all views / controllers so that their metaclasses /
|
||||
function decorators have a chance to register themselves on the
|
||||
application.
|
||||
@@ -457,7 +476,7 @@ def find_modules(import_path, include_packages=False, recursive=False):
|
||||
|
||||
|
||||
def validate_arguments(func, args, kwargs, drop_extra=True):
|
||||
"""Check if the function accepts the arguments and keyword arguments.
|
||||
"""Checks if the function accepts the arguments and keyword arguments.
|
||||
Returns a new ``(args, kwargs)`` tuple that can safely be passed to
|
||||
the function without causing a `TypeError` because the function signature
|
||||
is incompatible. If `drop_extra` is set to `True` (which is the default)
|
||||
@@ -543,6 +562,7 @@ def bind_arguments(func, args, kwargs):
|
||||
|
||||
|
||||
class ArgumentValidationError(ValueError):
|
||||
|
||||
"""Raised if :func:`validate_arguments` fails to validate"""
|
||||
|
||||
def __init__(self, missing=None, extra=None, extra_positional=None):
|
||||
@@ -551,12 +571,13 @@ class ArgumentValidationError(ValueError):
|
||||
self.extra_positional = extra_positional or []
|
||||
ValueError.__init__(self, 'function arguments invalid. ('
|
||||
'%d missing, %d additional)' % (
|
||||
len(self.missing),
|
||||
len(self.extra) + len(self.extra_positional)
|
||||
))
|
||||
len(self.missing),
|
||||
len(self.extra) + len(self.extra_positional)
|
||||
))
|
||||
|
||||
|
||||
class ImportStringError(ImportError):
|
||||
|
||||
"""Provides information about a failed :func:`import_string` attempt."""
|
||||
|
||||
#: String in dotted notation that failed to be imported.
|
||||
@@ -599,13 +620,9 @@ class ImportStringError(ImportError):
|
||||
self.exception)
|
||||
|
||||
|
||||
# circular dependencies
|
||||
from werkzeug.http import quote_header_value, unquote_header_value, \
|
||||
cookie_date
|
||||
|
||||
# DEPRECATED
|
||||
# these objects were previously in this module as well. we import
|
||||
# them here for backwards compatibility with old pickles.
|
||||
from werkzeug.datastructures import MultiDict, CombinedMultiDict, \
|
||||
Headers, EnvironHeaders
|
||||
from werkzeug.http import parse_cookie, dump_cookie
|
||||
from werkzeug.datastructures import ( # noqa
|
||||
MultiDict, CombinedMultiDict, Headers, EnvironHeaders)
|
||||
from werkzeug.http import parse_cookie, dump_cookie # noqa
|
||||
|
||||
@@ -17,35 +17,35 @@
|
||||
decoded into an unicode object if possible and if it makes sense.
|
||||
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from functools import update_wrapper
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from werkzeug.http import HTTP_STATUS_CODES, \
|
||||
parse_accept_header, parse_cache_control_header, parse_etags, \
|
||||
parse_date, generate_etag, is_resource_modified, unquote_etag, \
|
||||
quote_etag, parse_set_header, parse_authorization_header, \
|
||||
parse_www_authenticate_header, remove_entity_headers, \
|
||||
parse_options_header, dump_options_header, http_date, \
|
||||
parse_if_range_header, parse_cookie, dump_cookie, \
|
||||
parse_range_header, parse_content_range_header, dump_header
|
||||
parse_accept_header, parse_cache_control_header, parse_etags, \
|
||||
parse_date, generate_etag, is_resource_modified, unquote_etag, \
|
||||
quote_etag, parse_set_header, parse_authorization_header, \
|
||||
parse_www_authenticate_header, remove_entity_headers, \
|
||||
parse_options_header, dump_options_header, http_date, \
|
||||
parse_if_range_header, parse_cookie, dump_cookie, \
|
||||
parse_range_header, parse_content_range_header, dump_header
|
||||
from werkzeug.urls import url_decode, iri_to_uri, url_join
|
||||
from werkzeug.formparser import FormDataParser, default_stream_factory
|
||||
from werkzeug.utils import cached_property, environ_property, \
|
||||
header_property, get_content_type
|
||||
header_property, get_content_type
|
||||
from werkzeug.wsgi import get_current_url, get_host, \
|
||||
ClosingIterator, get_input_stream, get_content_length
|
||||
ClosingIterator, get_input_stream, get_content_length
|
||||
from werkzeug.datastructures import MultiDict, CombinedMultiDict, Headers, \
|
||||
EnvironHeaders, ImmutableMultiDict, ImmutableTypeConversionDict, \
|
||||
ImmutableList, MIMEAccept, CharsetAccept, LanguageAccept, \
|
||||
ResponseCacheControl, RequestCacheControl, CallbackDict, \
|
||||
ContentRange, iter_multi_items
|
||||
EnvironHeaders, ImmutableMultiDict, ImmutableTypeConversionDict, \
|
||||
ImmutableList, MIMEAccept, CharsetAccept, LanguageAccept, \
|
||||
ResponseCacheControl, RequestCacheControl, CallbackDict, \
|
||||
ContentRange, iter_multi_items
|
||||
from werkzeug._internal import _get_environ
|
||||
from werkzeug._compat import to_bytes, string_types, text_type, \
|
||||
integer_types, wsgi_decoding_dance, wsgi_get_bytes, \
|
||||
to_unicode, to_native
|
||||
integer_types, wsgi_decoding_dance, wsgi_get_bytes, \
|
||||
to_unicode, to_native, BytesIO
|
||||
|
||||
|
||||
def _run_wsgi_app(*args):
|
||||
@@ -86,6 +86,7 @@ def _iter_encoded(iterable, charset):
|
||||
|
||||
|
||||
class BaseRequest(object):
|
||||
|
||||
"""Very basic request object. This does not implement advanced stuff like
|
||||
entity tag parsing or cache controls. The request object is created with
|
||||
the WSGI environment as first argument and will add itself to the WSGI
|
||||
@@ -190,13 +191,17 @@ class BaseRequest(object):
|
||||
|
||||
#: Optionally a list of hosts that is trusted by this request. By default
|
||||
#: all hosts are trusted which means that whatever the client sends the
|
||||
#: host is will be accepted. This is the recommended setup as a webserver
|
||||
#: should manually be set up to not route invalid hosts to the application.
|
||||
#: host is will be accepted.
|
||||
#:
|
||||
#: This is the recommended setup as a webserver should manually be set up
|
||||
#: to only route correct hosts to the application, and remove the
|
||||
#: `X-Forwarded-Host` header if it is not being used (see
|
||||
#: :func:`werkzeug.wsgi.get_host`).
|
||||
#:
|
||||
#: .. versionadded:: 0.9
|
||||
trusted_hosts = None
|
||||
|
||||
#: Indicates weather the data descriptor should be allowed to read and
|
||||
#: Indicates whether the data descriptor should be allowed to read and
|
||||
#: buffer up the input stream. By default it's enabled.
|
||||
#:
|
||||
#: .. versionadded:: 0.9
|
||||
@@ -214,7 +219,7 @@ class BaseRequest(object):
|
||||
# in a debug session we don't want the repr to blow up.
|
||||
args = []
|
||||
try:
|
||||
args.append("'%s'" % self.url)
|
||||
args.append("'%s'" % to_native(self.url, self.url_charset))
|
||||
args.append('[%s]' % self.method)
|
||||
except Exception:
|
||||
args.append('(invalid WSGI environ)')
|
||||
@@ -254,6 +259,7 @@ class BaseRequest(object):
|
||||
"""
|
||||
from werkzeug.test import EnvironBuilder
|
||||
charset = kwargs.pop('charset', cls.charset)
|
||||
kwargs['charset'] = charset
|
||||
builder = EnvironBuilder(*args, **kwargs)
|
||||
try:
|
||||
return builder.get_request(cls)
|
||||
@@ -286,7 +292,7 @@ class BaseRequest(object):
|
||||
return update_wrapper(application, f)
|
||||
|
||||
def _get_file_stream(self, total_content_length, content_type, filename=None,
|
||||
content_length=None):
|
||||
content_length=None):
|
||||
"""Called to get a stream for the file upload.
|
||||
|
||||
This must provide a file-like class with `read()`, `readline()`
|
||||
@@ -335,7 +341,8 @@ class BaseRequest(object):
|
||||
"""Method used internally to retrieve submitted data. After calling
|
||||
this sets `form` and `files` on the request object to multi dicts
|
||||
filled with the incoming form data. As a matter of fact the input
|
||||
stream will be empty afterwards.
|
||||
stream will be empty afterwards. You can also call this method to
|
||||
force the parsing of the form data.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
@@ -350,8 +357,8 @@ class BaseRequest(object):
|
||||
content_length = get_content_length(self.environ)
|
||||
mimetype, options = parse_options_header(content_type)
|
||||
parser = self.make_form_data_parser()
|
||||
data = parser.parse(self.stream, mimetype,
|
||||
content_length, options)
|
||||
data = parser.parse(self._get_stream_for_parsing(),
|
||||
mimetype, content_length, options)
|
||||
else:
|
||||
data = (self.stream, self.parameter_storage_class(),
|
||||
self.parameter_storage_class())
|
||||
@@ -361,10 +368,22 @@ class BaseRequest(object):
|
||||
d = self.__dict__
|
||||
d['stream'], d['form'], d['files'] = data
|
||||
|
||||
def _get_stream_for_parsing(self):
|
||||
"""This is the same as accessing :attr:`stream` with the difference
|
||||
that if it finds cached data from calling :meth:`get_data` first it
|
||||
will create a new stream out of the cached data.
|
||||
|
||||
.. versionadded:: 0.9.3
|
||||
"""
|
||||
cached_data = getattr(self, '_cached_data', None)
|
||||
if cached_data is not None:
|
||||
return BytesIO(cached_data)
|
||||
return self.stream
|
||||
|
||||
def close(self):
|
||||
"""Closes associated resources of this request object. This
|
||||
closes all file handles explicitly. You can also use the request
|
||||
object in a with statement with will automatically close it.
|
||||
object in a with statement which will automatically close it.
|
||||
|
||||
.. versionadded:: 0.9
|
||||
"""
|
||||
@@ -394,9 +413,12 @@ class BaseRequest(object):
|
||||
_assert_not_shallow(self)
|
||||
return get_input_stream(self.environ)
|
||||
|
||||
input_stream = environ_property('wsgi.input', 'The WSGI input stream.\n'
|
||||
'In general it\'s a bad idea to use this one because you can easily '
|
||||
'read past the boundary. Use the :attr:`stream` instead.')
|
||||
input_stream = environ_property('wsgi.input', """
|
||||
The WSGI input stream.
|
||||
|
||||
In general it's a bad idea to use this one because you can easily read past
|
||||
the boundary. Use the :attr:`stream` instead.
|
||||
""")
|
||||
|
||||
@cached_property
|
||||
def args(self):
|
||||
@@ -415,9 +437,15 @@ class BaseRequest(object):
|
||||
if self.disable_data_descriptor:
|
||||
raise AttributeError('data descriptor is disabled')
|
||||
# XXX: this should eventually be deprecated.
|
||||
return self.get_data()
|
||||
|
||||
def get_data(self, cache=True, as_text=False):
|
||||
# We trigger form data parsing first which means that the descriptor
|
||||
# will not cache the data that would otherwise be .form or .files
|
||||
# data. This restores the behavior that was there in Werkzeug
|
||||
# before 0.9. New code should use :meth:`get_data` explicitly as
|
||||
# this will make behavior explicit.
|
||||
return self.get_data(parse_form_data=True)
|
||||
|
||||
def get_data(self, cache=True, as_text=False, parse_form_data=False):
|
||||
"""This reads the buffered incoming data from the client into one
|
||||
bytestring. By default this is cached but that behavior can be
|
||||
changed by setting `cache` to `False`.
|
||||
@@ -426,6 +454,17 @@ class BaseRequest(object):
|
||||
content length first as a client could send dozens of megabytes or more
|
||||
to cause memory problems on the server.
|
||||
|
||||
Note that if the form data was already parsed this method will not
|
||||
return anything as form data parsing does not cache the data like
|
||||
this method does. To implicitly invoke form data parsing function
|
||||
set `parse_form_data` to `True`. When this is done the return value
|
||||
of this method will be an empty string if the form parser handles
|
||||
the data. This generally is not necessary as if the whole data is
|
||||
cached (which is the default) the form parser will used the cached
|
||||
data to parse the form data. Please be generally aware of checking
|
||||
the content length first in any case before calling this method
|
||||
to avoid exhausting server memory.
|
||||
|
||||
If `as_text` is set to `True` the return value will be a decoded
|
||||
unicode string.
|
||||
|
||||
@@ -433,6 +472,8 @@ class BaseRequest(object):
|
||||
"""
|
||||
rv = getattr(self, '_cached_data', None)
|
||||
if rv is None:
|
||||
if parse_form_data:
|
||||
self._load_form_data()
|
||||
rv = self.stream.read()
|
||||
if cache:
|
||||
self._cached_data = rv
|
||||
@@ -517,38 +558,51 @@ class BaseRequest(object):
|
||||
|
||||
@cached_property
|
||||
def url(self):
|
||||
"""The reconstructed current URL"""
|
||||
"""The reconstructed current URL as IRI.
|
||||
See also: :attr:`trusted_hosts`.
|
||||
"""
|
||||
return get_current_url(self.environ,
|
||||
trusted_hosts=self.trusted_hosts)
|
||||
|
||||
@cached_property
|
||||
def base_url(self):
|
||||
"""Like :attr:`url` but without the querystring"""
|
||||
"""Like :attr:`url` but without the querystring
|
||||
See also: :attr:`trusted_hosts`.
|
||||
"""
|
||||
return get_current_url(self.environ, strip_querystring=True,
|
||||
trusted_hosts=self.trusted_hosts)
|
||||
|
||||
@cached_property
|
||||
def url_root(self):
|
||||
"""The full URL root (with hostname), this is the application root."""
|
||||
"""The full URL root (with hostname), this is the application
|
||||
root as IRI.
|
||||
See also: :attr:`trusted_hosts`.
|
||||
"""
|
||||
return get_current_url(self.environ, True,
|
||||
trusted_hosts=self.trusted_hosts)
|
||||
|
||||
@cached_property
|
||||
def host_url(self):
|
||||
"""Just the host with scheme."""
|
||||
"""Just the host with scheme as IRI.
|
||||
See also: :attr:`trusted_hosts`.
|
||||
"""
|
||||
return get_current_url(self.environ, host_only=True,
|
||||
trusted_hosts=self.trusted_hosts)
|
||||
|
||||
@cached_property
|
||||
def host(self):
|
||||
"""Just the host including the port if available."""
|
||||
"""Just the host including the port if available.
|
||||
See also: :attr:`trusted_hosts`.
|
||||
"""
|
||||
return get_host(self.environ, trusted_hosts=self.trusted_hosts)
|
||||
|
||||
query_string = environ_property('QUERY_STRING', '', read_only=True,
|
||||
load_func=wsgi_get_bytes, doc=
|
||||
'''The URL parameters as raw bytestring.''')
|
||||
method = environ_property('REQUEST_METHOD', 'GET', read_only=True, doc=
|
||||
'''The transmission method. (For example ``'GET'`` or ``'POST'``).''')
|
||||
query_string = environ_property(
|
||||
'QUERY_STRING', '', read_only=True,
|
||||
load_func=wsgi_get_bytes, doc='The URL parameters as raw bytestring.')
|
||||
method = environ_property(
|
||||
'REQUEST_METHOD', 'GET', read_only=True,
|
||||
load_func=lambda x: x.upper(),
|
||||
doc="The transmission method. (For example ``'GET'`` or ``'POST'``).")
|
||||
|
||||
@cached_property
|
||||
def access_route(self):
|
||||
@@ -594,10 +648,11 @@ class BaseRequest(object):
|
||||
is_run_once = environ_property('wsgi.run_once', doc='''
|
||||
boolean that is `True` if the application will be executed only
|
||||
once in a process lifetime. This is the case for CGI for example,
|
||||
but it's not guaranteed that the exeuction only happens one time.''')
|
||||
but it's not guaranteed that the execution only happens one time.''')
|
||||
|
||||
|
||||
class BaseResponse(object):
|
||||
|
||||
"""Base response class. The most important fact about a response object
|
||||
is that it's a regular WSGI application. It's initialized with a couple
|
||||
of response parameters (headers, body, status code etc.) and will start a
|
||||
@@ -633,7 +688,7 @@ class BaseResponse(object):
|
||||
To enforce a new type of already existing responses you can use the
|
||||
:meth:`force_type` method. This is useful if you're working with different
|
||||
subclasses of response objects and you want to post process them with a
|
||||
know interface.
|
||||
known interface.
|
||||
|
||||
Per default the request object will assume all the text data is `utf-8`
|
||||
encoded. Please refer to `the unicode chapter <unicode.txt>`_ for more
|
||||
@@ -647,9 +702,9 @@ class BaseResponse(object):
|
||||
Special note for `mimetype` and `content_type`: For most mime types
|
||||
`mimetype` and `content_type` work the same, the difference affects
|
||||
only 'text' mimetypes. If the mimetype passed with `mimetype` is a
|
||||
mimetype starting with `text/` it becomes a charset parameter defined
|
||||
with the charset of the response object. In contrast the
|
||||
`content_type` parameter is always added as header unmodified.
|
||||
mimetype starting with `text/`, the charset parameter of the response
|
||||
object is appended to it. In contrast the `content_type` parameter is
|
||||
always added as header unmodified.
|
||||
|
||||
.. versionchanged:: 0.5
|
||||
the `direct_passthrough` parameter was added.
|
||||
@@ -662,7 +717,7 @@ class BaseResponse(object):
|
||||
:param content_type: the content type for the request. See notice above.
|
||||
:param direct_passthrough: if set to `True` :meth:`iter_encoded` is not
|
||||
called before iteration which makes it
|
||||
possible to pass special iterators though
|
||||
possible to pass special iterators through
|
||||
unchanged (see :func:`wrap_file` for more
|
||||
details.)
|
||||
"""
|
||||
@@ -749,7 +804,7 @@ class BaseResponse(object):
|
||||
if self.is_sequence:
|
||||
body_info = '%d bytes' % sum(map(len, self.iter_encoded()))
|
||||
else:
|
||||
body_info = self.is_streamed and 'streamed' or 'likely-streamed'
|
||||
body_info = 'streamed' if self.is_streamed else 'likely-streamed'
|
||||
return '<%s %s [%s]>' % (
|
||||
self.__class__.__name__,
|
||||
body_info,
|
||||
@@ -811,6 +866,7 @@ class BaseResponse(object):
|
||||
|
||||
def _get_status_code(self):
|
||||
return self._status_code
|
||||
|
||||
def _set_status_code(self, code):
|
||||
self._status_code = code
|
||||
try:
|
||||
@@ -823,6 +879,7 @@ class BaseResponse(object):
|
||||
|
||||
def _get_status(self):
|
||||
return self._status
|
||||
|
||||
def _set_status(self, value):
|
||||
self._status = to_native(value)
|
||||
try:
|
||||
@@ -928,7 +985,6 @@ class BaseResponse(object):
|
||||
value of this method is used as application iterator unless
|
||||
:attr:`direct_passthrough` was activated.
|
||||
"""
|
||||
charset = self.charset
|
||||
if __debug__:
|
||||
_warn_if_string(self.response)
|
||||
# Encode in a separate function so that self.response is fetched
|
||||
@@ -956,8 +1012,8 @@ class BaseResponse(object):
|
||||
span the whole domain.
|
||||
"""
|
||||
self.headers.add('Set-Cookie', dump_cookie(key, value, max_age,
|
||||
expires, path, domain, secure, httponly,
|
||||
self.charset))
|
||||
expires, path, domain, secure, httponly,
|
||||
self.charset))
|
||||
|
||||
def delete_cookie(self, key, path='/', domain=None):
|
||||
"""Delete a cookie. Fails silently if key doesn't exist.
|
||||
@@ -982,7 +1038,7 @@ class BaseResponse(object):
|
||||
"""
|
||||
try:
|
||||
len(self.response)
|
||||
except TypeError:
|
||||
except (TypeError, AttributeError):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1073,7 +1129,10 @@ class BaseResponse(object):
|
||||
if location is not None:
|
||||
old_location = location
|
||||
if isinstance(location, text_type):
|
||||
location = iri_to_uri(location)
|
||||
# Safe conversion is necessary here as we might redirect
|
||||
# to a broken URI scheme (for instance itms-services).
|
||||
location = iri_to_uri(location, safe_conversion=True)
|
||||
|
||||
if self.autocorrect_location_header:
|
||||
current_url = get_current_url(environ, root_only=True)
|
||||
if isinstance(current_url, text_type):
|
||||
@@ -1104,7 +1163,8 @@ class BaseResponse(object):
|
||||
if self.automatically_set_content_length and \
|
||||
self.is_sequence and content_length is None and status != 304:
|
||||
try:
|
||||
content_length = sum(len(to_bytes(x, 'ascii')) for x in self.response)
|
||||
content_length = sum(len(to_bytes(x, 'ascii'))
|
||||
for x in self.response)
|
||||
except UnicodeError:
|
||||
# aha, something non-bytestringy in there, too bad, we
|
||||
# can't safely figure out the length of the response.
|
||||
@@ -1171,6 +1231,7 @@ class BaseResponse(object):
|
||||
|
||||
|
||||
class AcceptMixin(object):
|
||||
|
||||
"""A mixin for classes with an :attr:`~BaseResponse.environ` attribute
|
||||
to get all the HTTP accept headers as
|
||||
:class:`~werkzeug.datastructures.Accept` objects (or subclasses
|
||||
@@ -1214,6 +1275,7 @@ class AcceptMixin(object):
|
||||
|
||||
|
||||
class ETagRequestMixin(object):
|
||||
|
||||
"""Add entity tag and cache descriptors to a request object or object with
|
||||
a WSGI environment available as :attr:`~BaseRequest.environ`. This not
|
||||
only provides access to etags but also to the cache control header.
|
||||
@@ -1276,6 +1338,7 @@ class ETagRequestMixin(object):
|
||||
|
||||
|
||||
class UserAgentMixin(object):
|
||||
|
||||
"""Adds a `user_agent` attribute to the request object which contains the
|
||||
parsed user agent of the browser that triggered the request as a
|
||||
:class:`~werkzeug.useragents.UserAgent` object.
|
||||
@@ -1289,6 +1352,7 @@ class UserAgentMixin(object):
|
||||
|
||||
|
||||
class AuthorizationMixin(object):
|
||||
|
||||
"""Adds an :attr:`authorization` property that represents the parsed
|
||||
value of the `Authorization` header as
|
||||
:class:`~werkzeug.datastructures.Authorization` object.
|
||||
@@ -1302,6 +1366,7 @@ class AuthorizationMixin(object):
|
||||
|
||||
|
||||
class StreamOnlyMixin(object):
|
||||
|
||||
"""If mixed in before the request object this will change the bahavior
|
||||
of it to disable handling of form parsing. This disables the
|
||||
:attr:`files`, :attr:`form` attributes and will just provide a
|
||||
@@ -1315,6 +1380,7 @@ class StreamOnlyMixin(object):
|
||||
|
||||
|
||||
class ETagResponseMixin(object):
|
||||
|
||||
"""Adds extra functionality to a response object for etag and cache
|
||||
handling. This mixin requires an object with at least a `headers`
|
||||
object that implements a dict like interface similar to
|
||||
@@ -1367,7 +1433,7 @@ class ETagResponseMixin(object):
|
||||
# wsgiref.
|
||||
if 'date' not in self.headers:
|
||||
self.headers['Date'] = http_date()
|
||||
if 'content-length' not in self.headers:
|
||||
if self.automatically_set_content_length and 'content-length' not in self.headers:
|
||||
length = self.calculate_content_length()
|
||||
if length is not None:
|
||||
self.headers['Content-Length'] = length
|
||||
@@ -1422,6 +1488,7 @@ class ETagResponseMixin(object):
|
||||
if rv is None:
|
||||
rv = ContentRange(None, None, None, on_update=on_update)
|
||||
return rv
|
||||
|
||||
def _set_content_range(self, value):
|
||||
if not value:
|
||||
del self.headers['content-range']
|
||||
@@ -1440,6 +1507,7 @@ class ETagResponseMixin(object):
|
||||
|
||||
|
||||
class ResponseStream(object):
|
||||
|
||||
"""A file descriptor like object used by the :class:`ResponseStreamMixin` to
|
||||
represent the body of the stream. It directly pushes into the response
|
||||
iterable of the response object.
|
||||
@@ -1456,6 +1524,7 @@ class ResponseStream(object):
|
||||
raise ValueError('I/O operation on closed file')
|
||||
self.response._ensure_sequence(mutable=True)
|
||||
self.response.response.append(value)
|
||||
self.response.headers.pop('Content-Length', None)
|
||||
|
||||
def writelines(self, seq):
|
||||
for item in seq:
|
||||
@@ -1479,6 +1548,7 @@ class ResponseStream(object):
|
||||
|
||||
|
||||
class ResponseStreamMixin(object):
|
||||
|
||||
"""Mixin for :class:`BaseRequest` subclasses. Classes that inherit from
|
||||
this mixin will automatically get a :attr:`stream` property that provides
|
||||
a write-only interface to the response iterable.
|
||||
@@ -1491,6 +1561,7 @@ class ResponseStreamMixin(object):
|
||||
|
||||
|
||||
class CommonRequestDescriptorsMixin(object):
|
||||
|
||||
"""A mixin for :class:`BaseRequest` subclasses. Request objects that
|
||||
mix this class in will automatically get descriptors for a couple of
|
||||
HTTP headers with automatic type conversion.
|
||||
@@ -1550,13 +1621,13 @@ class CommonRequestDescriptorsMixin(object):
|
||||
|
||||
@property
|
||||
def mimetype(self):
|
||||
"""Like :attr:`content_type` but without parameters (eg, without
|
||||
charset, type etc.). For example if the content
|
||||
type is ``text/html; charset=utf-8`` the mimetype would be
|
||||
"""Like :attr:`content_type`, but without parameters (eg, without
|
||||
charset, type etc.) and always lowercase. For example if the content
|
||||
type is ``text/HTML; charset=utf-8`` the mimetype would be
|
||||
``'text/html'``.
|
||||
"""
|
||||
self._parse_content_type()
|
||||
return self._parsed_content_type[0]
|
||||
return self._parsed_content_type[0].lower()
|
||||
|
||||
@property
|
||||
def mimetype_params(self):
|
||||
@@ -1579,6 +1650,7 @@ class CommonRequestDescriptorsMixin(object):
|
||||
|
||||
|
||||
class CommonResponseDescriptorsMixin(object):
|
||||
|
||||
"""A mixin for :class:`BaseResponse` subclasses. Response objects that
|
||||
mix this class in will automatically get descriptors for a couple of
|
||||
HTTP headers with automatic type conversion.
|
||||
@@ -1667,6 +1739,7 @@ class CommonResponseDescriptorsMixin(object):
|
||||
elif value.isdigit():
|
||||
return datetime.utcnow() + timedelta(seconds=int(value))
|
||||
return parse_date(value)
|
||||
|
||||
def _set_retry_after(self, value):
|
||||
if value is None:
|
||||
if 'retry-after' in self.headers:
|
||||
@@ -1693,6 +1766,7 @@ class CommonResponseDescriptorsMixin(object):
|
||||
elif header_set:
|
||||
self.headers[name] = header_set.to_header()
|
||||
return parse_set_header(self.headers.get(name), on_update)
|
||||
|
||||
def fset(self, value):
|
||||
if not value:
|
||||
del self.headers[name]
|
||||
@@ -1724,6 +1798,7 @@ class CommonResponseDescriptorsMixin(object):
|
||||
|
||||
|
||||
class WWWAuthenticateMixin(object):
|
||||
|
||||
"""Adds a :attr:`www_authenticate` property to a response object."""
|
||||
|
||||
@property
|
||||
@@ -1741,6 +1816,7 @@ class WWWAuthenticateMixin(object):
|
||||
class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
|
||||
UserAgentMixin, AuthorizationMixin,
|
||||
CommonRequestDescriptorsMixin):
|
||||
|
||||
"""Full featured request object implementing the following mixins:
|
||||
|
||||
- :class:`AcceptMixin` for accept header parsing
|
||||
@@ -1752,6 +1828,7 @@ class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
|
||||
|
||||
|
||||
class PlainRequest(StreamOnlyMixin, Request):
|
||||
|
||||
"""A request object without special form parsing capabilities.
|
||||
|
||||
.. versionadded:: 0.9
|
||||
@@ -1761,6 +1838,7 @@ class PlainRequest(StreamOnlyMixin, Request):
|
||||
class Response(BaseResponse, ETagResponseMixin, ResponseStreamMixin,
|
||||
CommonResponseDescriptorsMixin,
|
||||
WWWAuthenticateMixin):
|
||||
|
||||
"""Full featured response object implementing the following mixins:
|
||||
|
||||
- :class:`ETagResponseMixin` for etag and cache control handling
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
This module implements WSGI related helpers.
|
||||
|
||||
:copyright: (c) 2013 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import posixpath
|
||||
import mimetypes
|
||||
from itertools import chain
|
||||
@@ -20,11 +19,12 @@ from datetime import datetime
|
||||
from functools import partial, update_wrapper
|
||||
|
||||
from werkzeug._compat import iteritems, text_type, string_types, \
|
||||
implements_iterator, make_literal_wrapper, to_unicode, to_bytes, \
|
||||
wsgi_get_bytes, try_coerce_native, PY2
|
||||
implements_iterator, make_literal_wrapper, to_unicode, to_bytes, \
|
||||
wsgi_get_bytes, try_coerce_native, PY2
|
||||
from werkzeug._internal import _empty_stream, _encode_idna
|
||||
from werkzeug.http import is_resource_modified, http_date
|
||||
from werkzeug.urls import uri_to_iri, url_quote, url_parse, url_join
|
||||
from werkzeug.filesystem import get_filesystem_encoding
|
||||
|
||||
|
||||
def responder(f):
|
||||
@@ -42,8 +42,8 @@ def responder(f):
|
||||
|
||||
def get_current_url(environ, root_only=False, strip_querystring=False,
|
||||
host_only=False, trusted_hosts=None):
|
||||
"""A handy helper function that recreates the full URL for the current
|
||||
request or parts of it. Here an example:
|
||||
"""A handy helper function that recreates the full URL as IRI for the
|
||||
current request or parts of it. Here an example:
|
||||
|
||||
>>> from werkzeug.test import create_environ
|
||||
>>> env = create_environ("/?param=foo", "http://localhost/script")
|
||||
@@ -60,6 +60,15 @@ def get_current_url(environ, root_only=False, strip_querystring=False,
|
||||
If the host is not in there it will raise a
|
||||
:exc:`~werkzeug.exceptions.SecurityError`.
|
||||
|
||||
Note that the string returned might contain unicode characters as the
|
||||
representation is an IRI not an URI. If you need an ASCII only
|
||||
representation you can use the :func:`~werkzeug.urls.iri_to_uri`
|
||||
function:
|
||||
|
||||
>>> from werkzeug.urls import iri_to_uri
|
||||
>>> iri_to_uri(get_current_url(env))
|
||||
'http://localhost/script/?param=foo'
|
||||
|
||||
:param environ: the WSGI environment to get the current URL from.
|
||||
:param root_only: set `True` if you only want the root URL.
|
||||
:param strip_querystring: set to `True` if you don't want the querystring.
|
||||
@@ -104,14 +113,20 @@ def host_is_trusted(hostname, trusted_list):
|
||||
hostname = hostname.rsplit(':', 1)[0]
|
||||
return _encode_idna(hostname)
|
||||
|
||||
hostname = _normalize(hostname)
|
||||
try:
|
||||
hostname = _normalize(hostname)
|
||||
except UnicodeError:
|
||||
return False
|
||||
for ref in trusted_list:
|
||||
if ref.startswith('.'):
|
||||
ref = ref[1:]
|
||||
suffix_match = True
|
||||
else:
|
||||
suffix_match = False
|
||||
ref = _normalize(ref)
|
||||
try:
|
||||
ref = _normalize(ref)
|
||||
except UnicodeError:
|
||||
return False
|
||||
if ref == hostname:
|
||||
return True
|
||||
if suffix_match and hostname.endswith('.' + ref):
|
||||
@@ -120,17 +135,20 @@ def host_is_trusted(hostname, trusted_list):
|
||||
|
||||
|
||||
def get_host(environ, trusted_hosts=None):
|
||||
"""Return the real host for the given WSGI environment. This takes care
|
||||
of the `X-Forwarded-Host` header. Optionally it verifies that the host
|
||||
is in a list of trusted hosts. If the host is not in there it will raise
|
||||
a :exc:`~werkzeug.exceptions.SecurityError`.
|
||||
"""Return the real host for the given WSGI environment. This first checks
|
||||
the `X-Forwarded-Host` header, then the normal `Host` header, and finally
|
||||
the `SERVER_NAME` environment variable (using the first one it finds).
|
||||
|
||||
Optionally it verifies that the host is in a list of trusted hosts.
|
||||
If the host is not in there it will raise a
|
||||
:exc:`~werkzeug.exceptions.SecurityError`.
|
||||
|
||||
:param environ: the WSGI environment to get the host of.
|
||||
:param trusted_hosts: a list of trusted hosts, see :func:`host_is_trusted`
|
||||
for more information.
|
||||
"""
|
||||
if 'HTTP_X_FORWARDED_HOST' in environ:
|
||||
rv = environ['HTTP_X_FORWARDED_HOST'].split(',')[0].strip()
|
||||
rv = environ['HTTP_X_FORWARDED_HOST'].split(',', 1)[0].strip()
|
||||
elif 'HTTP_HOST' in environ:
|
||||
rv = environ['HTTP_HOST']
|
||||
else:
|
||||
@@ -170,7 +188,7 @@ def get_input_stream(environ, safe_fallback=True):
|
||||
.. versionadded:: 0.9
|
||||
|
||||
:param environ: the WSGI environ to fetch the stream from.
|
||||
:param safe: indicates weather the function should use an empty
|
||||
:param safe: indicates whether the function should use an empty
|
||||
stream as safe fallback or just return the original
|
||||
WSGI input stream if it can't wrap it safely. The
|
||||
default is to return an empty string in those cases.
|
||||
@@ -179,7 +197,7 @@ def get_input_stream(environ, safe_fallback=True):
|
||||
content_length = get_content_length(environ)
|
||||
|
||||
# A wsgi extension that tells us if the input is terminated. In
|
||||
# that case we return the stream unchanged as we know we can savely
|
||||
# that case we return the stream unchanged as we know we can safely
|
||||
# read it until the end.
|
||||
if environ.get('wsgi.input_terminated'):
|
||||
return stream
|
||||
@@ -397,7 +415,7 @@ def extract_path_info(environ_or_baseurl, path_or_url, charset='utf-8',
|
||||
if scheme not in (u'http', u'https'):
|
||||
return None
|
||||
else:
|
||||
if not (base_scheme in (u'http', u'https') and \
|
||||
if not (base_scheme in (u'http', u'https') and
|
||||
base_scheme == cur_scheme):
|
||||
return None
|
||||
|
||||
@@ -414,6 +432,7 @@ def extract_path_info(environ_or_baseurl, path_or_url, charset='utf-8',
|
||||
|
||||
|
||||
class SharedDataMiddleware(object):
|
||||
|
||||
"""A WSGI middleware that provides static content for development
|
||||
environments or simple server setups. Usage is quite simple::
|
||||
|
||||
@@ -466,7 +485,7 @@ class SharedDataMiddleware(object):
|
||||
:param disallow: a list of :func:`~fnmatch.fnmatch` rules.
|
||||
:param fallback_mimetype: the fallback mimetype for unknown files.
|
||||
:param cache: enable or disable caching headers.
|
||||
:Param cache_timeout: the cache timeout in seconds for the headers.
|
||||
:param cache_timeout: the cache timeout in seconds for the headers.
|
||||
"""
|
||||
|
||||
def __init__(self, app, exports, disallow=None, cache=True,
|
||||
@@ -510,11 +529,12 @@ class SharedDataMiddleware(object):
|
||||
|
||||
def get_package_loader(self, package, package_path):
|
||||
from pkg_resources import DefaultProvider, ResourceManager, \
|
||||
get_provider
|
||||
get_provider
|
||||
loadtime = datetime.utcnow()
|
||||
provider = get_provider(package)
|
||||
manager = ResourceManager()
|
||||
filesystem_bound = isinstance(provider, DefaultProvider)
|
||||
|
||||
def loader(path):
|
||||
if path is None:
|
||||
return None, None
|
||||
@@ -545,7 +565,7 @@ class SharedDataMiddleware(object):
|
||||
|
||||
def generate_etag(self, mtime, file_size, real_filename):
|
||||
if not isinstance(real_filename, bytes):
|
||||
real_filename = real_filename.encode(sys.getfilesystemencoding())
|
||||
real_filename = real_filename.encode(get_filesystem_encoding())
|
||||
return 'wzsdm-%d-%s-%s' % (
|
||||
mktime(mtime.timetuple()),
|
||||
file_size,
|
||||
@@ -555,14 +575,14 @@ class SharedDataMiddleware(object):
|
||||
def __call__(self, environ, start_response):
|
||||
cleaned_path = get_path_info(environ)
|
||||
if PY2:
|
||||
cleaned_path = cleaned_path.encode(sys.getfilesystemencoding())
|
||||
cleaned_path = cleaned_path.encode(get_filesystem_encoding())
|
||||
# sanitize the path for non unix systems
|
||||
cleaned_path = cleaned_path.strip('/')
|
||||
for sep in os.sep, os.altsep:
|
||||
if sep and sep != '/':
|
||||
cleaned_path = cleaned_path.replace(sep, '/')
|
||||
path = '/'.join([''] + [x for x in cleaned_path.split('/')
|
||||
if x and x != '..'])
|
||||
path = '/' + '/'.join(x for x in cleaned_path.split('/')
|
||||
if x and x != '..')
|
||||
file_loader = None
|
||||
for search_path, loader in iteritems(self.exports):
|
||||
if search_path == path:
|
||||
@@ -608,6 +628,7 @@ class SharedDataMiddleware(object):
|
||||
|
||||
|
||||
class DispatcherMiddleware(object):
|
||||
|
||||
"""Allows one to mount middlewares or applications in a WSGI application.
|
||||
This is useful if you want to combine multiple WSGI applications::
|
||||
|
||||
@@ -628,9 +649,8 @@ class DispatcherMiddleware(object):
|
||||
if script in self.mounts:
|
||||
app = self.mounts[script]
|
||||
break
|
||||
items = script.split('/')
|
||||
script = '/'.join(items[:-1])
|
||||
path_info = '/%s%s' % (items[-1], path_info)
|
||||
script, last_item = script.rsplit('/', 1)
|
||||
path_info = '/%s%s' % (last_item, path_info)
|
||||
else:
|
||||
app = self.mounts.get(script, self.app)
|
||||
original_script_name = environ.get('SCRIPT_NAME', '')
|
||||
@@ -641,6 +661,7 @@ class DispatcherMiddleware(object):
|
||||
|
||||
@implements_iterator
|
||||
class ClosingIterator(object):
|
||||
|
||||
"""The WSGI specification requires that all middlewares and gateways
|
||||
respect the `close` callback of an iterator. Because it is useful to add
|
||||
another close action to a returned iterator and adding a custom iterator
|
||||
@@ -707,6 +728,7 @@ def wrap_file(environ, file, buffer_size=8192):
|
||||
|
||||
@implements_iterator
|
||||
class FileWrapper(object):
|
||||
|
||||
"""This class can be used to convert a :class:`file`-like object into
|
||||
an iterable. It yields `buffer_size` blocks until the file is fully
|
||||
read.
|
||||
@@ -762,7 +784,8 @@ def _make_chunk_iter(stream, limit, buffer_size):
|
||||
yield item
|
||||
|
||||
|
||||
def make_line_iter(stream, limit=None, buffer_size=10 * 1024):
|
||||
def make_line_iter(stream, limit=None, buffer_size=10 * 1024,
|
||||
cap_at_buffer=False):
|
||||
"""Safely iterates line-based over an input stream. If the input stream
|
||||
is not a :class:`LimitedStream` the `limit` parameter is mandatory.
|
||||
|
||||
@@ -786,6 +809,12 @@ def make_line_iter(stream, limit=None, buffer_size=10 * 1024):
|
||||
content length. Not necessary if the `stream`
|
||||
is a :class:`LimitedStream`.
|
||||
:param buffer_size: The optional buffer size.
|
||||
:param cap_at_buffer: if this is set chunks are split if they are longer
|
||||
than the buffer size. Internally this is implemented
|
||||
that the buffer size might be exhausted by a factor
|
||||
of two however.
|
||||
.. versionadded:: 0.11.10
|
||||
added support for the `cap_at_buffer` parameter.
|
||||
"""
|
||||
_iter = _make_chunk_iter(stream, limit, buffer_size)
|
||||
|
||||
@@ -809,11 +838,19 @@ def make_line_iter(stream, limit=None, buffer_size=10 * 1024):
|
||||
if not new_data:
|
||||
break
|
||||
new_buf = []
|
||||
buf_size = 0
|
||||
for item in chain(buffer, new_data.splitlines(True)):
|
||||
new_buf.append(item)
|
||||
buf_size += len(item)
|
||||
if item and item[-1:] in crlf:
|
||||
yield _join(new_buf)
|
||||
new_buf = []
|
||||
elif cap_at_buffer and buf_size >= buffer_size:
|
||||
rv = _join(new_buf)
|
||||
while len(rv) >= buffer_size:
|
||||
yield rv[:buffer_size]
|
||||
rv = rv[buffer_size:]
|
||||
new_buf = [rv]
|
||||
buffer = new_buf
|
||||
if buffer:
|
||||
yield _join(buffer)
|
||||
@@ -832,7 +869,8 @@ def make_line_iter(stream, limit=None, buffer_size=10 * 1024):
|
||||
yield previous
|
||||
|
||||
|
||||
def make_chunk_iter(stream, separator, limit=None, buffer_size=10 * 1024):
|
||||
def make_chunk_iter(stream, separator, limit=None, buffer_size=10 * 1024,
|
||||
cap_at_buffer=False):
|
||||
"""Works like :func:`make_line_iter` but accepts a separator
|
||||
which divides chunks. If you want newline based processing
|
||||
you should use :func:`make_line_iter` instead as it
|
||||
@@ -843,12 +881,19 @@ def make_chunk_iter(stream, separator, limit=None, buffer_size=10 * 1024):
|
||||
.. versionadded:: 0.9
|
||||
added support for iterators as input stream.
|
||||
|
||||
.. versionadded:: 0.11.10
|
||||
added support for the `cap_at_buffer` parameter.
|
||||
|
||||
:param stream: the stream or iterate to iterate over.
|
||||
:param separator: the separator that divides chunks.
|
||||
:param limit: the limit in bytes for the stream. (Usually
|
||||
content length. Not necessary if the `stream`
|
||||
is otherwise already limited).
|
||||
:param buffer_size: The optional buffer size.
|
||||
:param cap_at_buffer: if this is set chunks are split if they are longer
|
||||
than the buffer size. Internally this is implemented
|
||||
that the buffer size might be exhausted by a factor
|
||||
of two however.
|
||||
"""
|
||||
_iter = _make_chunk_iter(stream, limit, buffer_size)
|
||||
|
||||
@@ -873,12 +918,24 @@ def make_chunk_iter(stream, separator, limit=None, buffer_size=10 * 1024):
|
||||
break
|
||||
chunks = _split(new_data)
|
||||
new_buf = []
|
||||
buf_size = 0
|
||||
for item in chain(buffer, chunks):
|
||||
if item == separator:
|
||||
yield _join(new_buf)
|
||||
new_buf = []
|
||||
buf_size = 0
|
||||
else:
|
||||
buf_size += len(item)
|
||||
new_buf.append(item)
|
||||
|
||||
if cap_at_buffer and buf_size >= buffer_size:
|
||||
rv = _join(new_buf)
|
||||
while len(rv) >= buffer_size:
|
||||
yield rv[:buffer_size]
|
||||
rv = rv[buffer_size:]
|
||||
new_buf = [rv]
|
||||
buf_size = len(rv)
|
||||
|
||||
buffer = new_buf
|
||||
if buffer:
|
||||
yield _join(buffer)
|
||||
@@ -886,6 +943,7 @@ def make_chunk_iter(stream, separator, limit=None, buffer_size=10 * 1024):
|
||||
|
||||
@implements_iterator
|
||||
class LimitedStream(object):
|
||||
|
||||
"""Wraps a stream so that it doesn't read more than n bytes. If the
|
||||
stream is exhausted and the caller tries to get more bytes from it
|
||||
:func:`on_exhausted` is called which by default returns an empty
|
||||
|
||||