"
@@ -380,7 +380,6 @@ def serve(path=localFile, port=8080, root=None):
cov = coverage(data_file=path)
cov.load()
- import cherrypy
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
'environment': 'production',
diff --git a/lib/cherrypy/lib/cpstats.py b/lib/cherrypy/lib/cpstats.py
index b2debd8..ae9f747 100644
--- a/lib/cherrypy/lib/cpstats.py
+++ b/lib/cherrypy/lib/cpstats.py
@@ -193,6 +193,8 @@
import threading
import time
+import six
+
import cherrypy
from cherrypy._cpcompat import json
@@ -248,7 +250,9 @@ def extrapolate_statistics(scope):
'Requests': {},
})
-proc_time = lambda s: time.time() - s['Start Time']
+
+def proc_time(s):
+ return time.time() - s['Start Time']
class ByteCountWrapper(object):
@@ -294,7 +298,8 @@ def next(self):
return data
-average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
+def average_uriset_time(s):
+ return s['Count'] and (s['Sum'] / s['Count']) or 0
def _get_threading_ident():
@@ -402,8 +407,13 @@ def record_stop(
missing = object()
-locale_date = lambda v: time.strftime('%c', time.gmtime(v))
-iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
+
+def locale_date(v):
+ return time.strftime('%c', time.gmtime(v))
+
+
+def iso_format(v):
+ return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
def pause_resume(ns):
@@ -603,12 +613,7 @@ def get_dict_collection(self, v, formatting):
"""Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict.
headers = []
- try:
- # python2
- vals = v.itervalues()
- except AttributeError:
- # python3
- vals = v.values()
+ vals = six.itervalues(v)
for record in vals:
for k3 in record:
format = formatting.get(k3, missing)
diff --git a/lib/cherrypy/lib/cptools.py b/lib/cherrypy/lib/cptools.py
index 2ad5a44..1c07963 100644
--- a/lib/cherrypy/lib/cptools.py
+++ b/lib/cherrypy/lib/cptools.py
@@ -5,6 +5,7 @@
from hashlib import md5
import six
+from six.moves import urllib
import cherrypy
from cherrypy._cpcompat import text_or_bytes
@@ -195,10 +196,8 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
if lbase is not None:
base = lbase.split(',')[0]
if not base:
- base = request.headers.get('Host', '127.0.0.1')
- port = request.local.port
- if port != 80:
- base += ':%s' % port
+ default = urllib.parse.urlparse(request.base).netloc
+ base = request.headers.get('Host', default)
if base.find('://') == -1:
# add http:// or https:// if needed
@@ -212,8 +211,8 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
if xff:
if remote == 'X-Forwarded-For':
- # Bug #1268
- xff = xff.split(',')[0].strip()
+ # Grab the first IP in a comma-separated list. Ref #1268.
+ xff = next(ip.strip() for ip in xff.split(','))
request.remote.ip = xff
@@ -240,7 +239,9 @@ def response_headers(headers=None, debug=False):
'TOOLS.RESPONSE_HEADERS')
for name, value in (headers or []):
cherrypy.serving.response.headers[name] = value
-response_headers.failsafe = True # noqa: E305
+
+
+response_headers.failsafe = True
def referer(pattern, accept=True, accept_missing=False, error=403,
@@ -409,7 +410,9 @@ def session_auth(**kwargs):
for k, v in kwargs.items():
setattr(sa, k, v)
return sa.run()
-session_auth.__doc__ = ( # noqa: E305
+
+
+session_auth.__doc__ = (
"""Session authentication hook.
Any attribute of the SessionAuth class may be overridden via a keyword arg
@@ -586,26 +589,13 @@ def accept(media=None, debug=False):
class MonitoredHeaderMap(_httputil.HeaderMap):
- def __init__(self):
- self.accessed_headers = set()
-
- def __getitem__(self, key):
+ def transform_key(self, key):
self.accessed_headers.add(key)
- return _httputil.HeaderMap.__getitem__(self, key)
+ return super(MonitoredHeaderMap, self).transform_key(key)
- def __contains__(self, key):
- self.accessed_headers.add(key)
- return _httputil.HeaderMap.__contains__(self, key)
-
- def get(self, key, default=None):
- self.accessed_headers.add(key)
- return _httputil.HeaderMap.get(self, key, default=default)
-
- if hasattr({}, 'has_key'):
- # Python 2
- def has_key(self, key):
- self.accessed_headers.add(key)
- return _httputil.HeaderMap.has_key(self, key) # noqa: W601
+ def __init__(self):
+ self.accessed_headers = set()
+ super(MonitoredHeaderMap, self).__init__()
def autovary(ignore=None, debug=False):
diff --git a/lib/cherrypy/lib/encoding.py b/lib/cherrypy/lib/encoding.py
index 72e58f9..3d001ca 100644
--- a/lib/cherrypy/lib/encoding.py
+++ b/lib/cherrypy/lib/encoding.py
@@ -5,7 +5,7 @@
import six
import cherrypy
-from cherrypy._cpcompat import text_or_bytes, ntob
+from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header
@@ -220,19 +220,7 @@ def __call__(self, *args, **kwargs):
response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs)
- if isinstance(self.body, text_or_bytes):
- # strings get wrapped in a list because iterating over a single
- # item list is much faster than iterating over every character
- # in a long string.
- if self.body:
- self.body = [self.body]
- else:
- # [''] doesn't evaluate to False, so replace it with [].
- self.body = []
- elif hasattr(self.body, 'read'):
- self.body = file_generator(self.body)
- elif self.body is None:
- self.body = []
+ self.body = prepare_iter(self.body)
ct = response.headers.elements('Content-Type')
if self.debug:
@@ -269,6 +257,29 @@ def __call__(self, *args, **kwargs):
return self.body
+
+def prepare_iter(value):
+ """
+ Ensure response body is iterable and resolves to False when empty.
+ """
+ if isinstance(value, text_or_bytes):
+ # strings get wrapped in a list because iterating over a single
+ # item list is much faster than iterating over every character
+ # in a long string.
+ if value:
+ value = [value]
+ else:
+ # [''] doesn't evaluate to False, so replace it with [].
+ value = []
+ # Don't use isinstance here; io.IOBase which has an ABC takes
+ # 1000 times as long as, say, isinstance(value, str)
+ elif hasattr(value, 'read'):
+ value = file_generator(value)
+ elif value is None:
+ value = []
+ return value
+
+
# GZIP
@@ -277,15 +288,15 @@ def compress(body, compress_level):
import zlib
# See http://www.gzip.org/zlib/rfc-gzip.html
- yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
- yield ntob('\x08') # CM: compression method
- yield ntob('\x00') # FLG: none set
+ yield b'\x1f\x8b' # ID1 and ID2: gzip marker
+ yield b'\x08' # CM: compression method
+ yield b'\x00' # FLG: none set
# MTIME: 4 bytes
yield struct.pack('
'
-__credits__ = """
- Peter van Kampen for its recipe which implement most of Digest
- authentication:
- http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
-"""
-
-__license__ = """
-Copyright (c) 2005, Tiago Cogumbreiro
-All rights reserved.
-
-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.
- * Neither the name of Sylvain Hellegouarch nor the names of his
- contributors may 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.
-"""
-
-__all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
- 'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
- 'calculateNonce', 'SUPPORTED_QOP')
-
-##########################################################################
-
-MD5 = 'MD5'
-MD5_SESS = 'MD5-sess'
-AUTH = 'auth'
-AUTH_INT = 'auth-int'
-
-SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
-SUPPORTED_QOP = (AUTH, AUTH_INT)
-
-##########################################################################
-# doAuth
-#
-DIGEST_AUTH_ENCODERS = {
- MD5: lambda val: md5(ntob(val)).hexdigest(),
- MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
- # SHA: lambda val: sha.new(ntob(val)).hexdigest (),
-}
-
-
-def calculateNonce(realm, algorithm=MD5):
- """This is an auxaliary function that calculates 'nonce' value. It is used
- to handle sessions."""
-
- global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
- assert algorithm in SUPPORTED_ALGORITHM
-
- try:
- encoder = DIGEST_AUTH_ENCODERS[algorithm]
- except KeyError:
- raise NotImplementedError('The chosen algorithm (%s) does not have '
- 'an implementation yet' % algorithm)
-
- return encoder('%d:%s' % (time.time(), realm))
-
-
-def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
- """Challenges the client for a Digest authentication."""
- global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
- assert algorithm in SUPPORTED_ALGORITHM
- assert qop in SUPPORTED_QOP
-
- if nonce is None:
- nonce = calculateNonce(realm, algorithm)
-
- return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
- realm, nonce, algorithm, qop
- )
-
-
-def basicAuth(realm):
- """Challengenes the client for a Basic authentication."""
- assert '"' not in realm, "Realms cannot contain the \" (quote) character."
-
- return 'Basic realm="%s"' % realm
-
-
-def doAuth(realm):
- """'doAuth' function returns the challenge string b giving priority over
- Digest and fallback to Basic authentication when the browser doesn't
- support the first one.
-
- This should be set in the HTTP header under the key 'WWW-Authenticate'."""
-
- return digestAuth(realm) + ' ' + basicAuth(realm)
-
-
-##########################################################################
-# Parse authorization parameters
-#
-def _parseDigestAuthorization(auth_params):
- # Convert the auth params to a dict
- items = parse_http_list(auth_params)
- params = parse_keqv_list(items)
-
- # Now validate the params
-
- # Check for required parameters
- required = ['username', 'realm', 'nonce', 'uri', 'response']
- for k in required:
- if k not in params:
- return None
-
- # If qop is sent then cnonce and nc MUST be present
- if 'qop' in params and not ('cnonce' in params and 'nc' in params):
- return None
-
- # If qop is not sent, neither cnonce nor nc can be present
- if ('cnonce' in params or 'nc' in params) and 'qop' not in params:
- return None
-
- return params
-
-
-def _parseBasicAuthorization(auth_params):
- username, password = base64_decode(auth_params).split(':', 1)
- return {'username': username, 'password': password}
-
-AUTH_SCHEMES = { # noqa: E305
- 'basic': _parseBasicAuthorization,
- 'digest': _parseDigestAuthorization,
-}
-
-
-def parseAuthorization(credentials):
- """parseAuthorization will convert the value of the 'Authorization' key in
- the HTTP header to a map itself. If the parsing fails 'None' is returned.
- """
-
- global AUTH_SCHEMES
-
- auth_scheme, auth_params = credentials.split(' ', 1)
- auth_scheme = auth_scheme.lower()
-
- parser = AUTH_SCHEMES[auth_scheme]
- params = parser(auth_params)
-
- if params is None:
- return
-
- assert 'auth_scheme' not in params
- params['auth_scheme'] = auth_scheme
- return params
-
-
-##########################################################################
-# Check provided response for a valid password
-#
-def md5SessionKey(params, password):
- """
- If the "algorithm" directive's value is "MD5-sess", then A1
- [the session key] is calculated only once - on the first request by the
- client following receipt of a WWW-Authenticate challenge from the server.
-
- This creates a 'session key' for the authentication of subsequent
- requests and responses which is different for each "authentication
- session", thus limiting the amount of material hashed with any one
- key.
-
- Because the server need only use the hash of the user
- credentials in order to create the A1 value, this construction could
- be used in conjunction with a third party authentication service so
- that the web server would not need the actual password value. The
- specification of such a protocol is beyond the scope of this
- specification.
-"""
-
- keys = ('username', 'realm', 'nonce', 'cnonce')
- params_copy = {}
- for key in keys:
- params_copy[key] = params[key]
-
- params_copy['algorithm'] = MD5_SESS
- return _A1(params_copy, password)
-
-
-def _A1(params, password):
- algorithm = params.get('algorithm', MD5)
- H = DIGEST_AUTH_ENCODERS[algorithm]
-
- if algorithm == MD5:
- # If the "algorithm" directive's value is "MD5" or is
- # unspecified, then A1 is:
- # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
- return '%s:%s:%s' % (params['username'], params['realm'], password)
-
- elif algorithm == MD5_SESS:
-
- # This is A1 if qop is set
- # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
- # ":" unq(nonce-value) ":" unq(cnonce-value)
- h_a1 = H('%s:%s:%s' % (params['username'], params['realm'], password))
- return '%s:%s:%s' % (h_a1, params['nonce'], params['cnonce'])
-
-
-def _A2(params, method, kwargs):
- # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
- # A2 = Method ":" digest-uri-value
-
- qop = params.get('qop', 'auth')
- if qop == 'auth':
- return method + ':' + params['uri']
- elif qop == 'auth-int':
- # If the "qop" value is "auth-int", then A2 is:
- # A2 = Method ":" digest-uri-value ":" H(entity-body)
- entity_body = kwargs.get('entity_body', '')
- H = kwargs['H']
-
- return '%s:%s:%s' % (
- method,
- params['uri'],
- H(entity_body)
- )
-
- else:
- raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
-
-
-def _computeDigestResponse(auth_map, password, method='GET', A1=None,
- **kwargs):
- """
- Generates a response respecting the algorithm defined in RFC 2617
- """
- params = auth_map
-
- algorithm = params.get('algorithm', MD5)
-
- H = DIGEST_AUTH_ENCODERS[algorithm]
- KD = lambda secret, data: H(secret + ':' + data)
-
- qop = params.get('qop', None)
-
- H_A2 = H(_A2(params, method, kwargs))
-
- if algorithm == MD5_SESS and A1 is not None:
- H_A1 = H(A1)
- else:
- H_A1 = H(_A1(params, password))
-
- if qop in ('auth', 'auth-int'):
- # If the "qop" value is "auth" or "auth-int":
- # request-digest = <"> < KD ( H(A1), unq(nonce-value)
- # ":" nc-value
- # ":" unq(cnonce-value)
- # ":" unq(qop-value)
- # ":" H(A2)
- # ) <">
- request = '%s:%s:%s:%s:%s' % (
- params['nonce'],
- params['nc'],
- params['cnonce'],
- params['qop'],
- H_A2,
- )
- elif qop is None:
- # If the "qop" directive is not present (this construction is
- # for compatibility with RFC 2069):
- # request-digest =
- # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
- request = '%s:%s' % (params['nonce'], H_A2)
-
- return KD(H_A1, request)
-
-
-def _checkDigestResponse(auth_map, password, method='GET', A1=None, **kwargs):
- """This function is used to verify the response given by the client when
- he tries to authenticate.
- Optional arguments:
- entity_body - when 'qop' is set to 'auth-int' you MUST provide the
- raw data you are going to send to the client (usually the
- HTML page.
- request_uri - the uri from the request line compared with the 'uri'
- directive of the authorization map. They must represent
- the same resource (unused at this time).
- """
-
- if auth_map['realm'] != kwargs.get('realm', None):
- return False
-
- response = _computeDigestResponse(
- auth_map, password, method, A1, **kwargs)
-
- return response == auth_map['response']
-
-
-def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
- **kwargs):
- # Note that the Basic response doesn't provide the realm value so we cannot
- # test it
- pass_through = lambda password, username=None: password
- encrypt = encrypt or pass_through
- try:
- candidate = encrypt(auth_map['password'], auth_map['username'])
- except TypeError:
- # if encrypt only takes one parameter, it's the password
- candidate = encrypt(auth_map['password'])
- return candidate == password
-
-AUTH_RESPONSES = { # noqa: E305
- 'basic': _checkBasicResponse,
- 'digest': _checkDigestResponse,
-}
-
-
-def checkResponse(auth_map, password, method='GET', encrypt=None, **kwargs):
- """'checkResponse' compares the auth_map with the password and optionally
- other arguments that each implementation might need.
-
- If the response is of type 'Basic' then the function has the following
- signature::
-
- checkBasicResponse(auth_map, password) -> bool
-
- If the response is of type 'Digest' then the function has the following
- signature::
-
- checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
-
- The 'A1' argument is only used in MD5_SESS algorithm based responses.
- Check md5SessionKey() for more info.
- """
- checker = AUTH_RESPONSES[auth_map['auth_scheme']]
- return checker(auth_map, password, method=method, encrypt=encrypt,
- **kwargs)
diff --git a/lib/cherrypy/lib/httputil.py b/lib/cherrypy/lib/httputil.py
index 1eb3e64..59bcc74 100644
--- a/lib/cherrypy/lib/httputil.py
+++ b/lib/cherrypy/lib/httputil.py
@@ -12,17 +12,15 @@
import re
from binascii import b2a_base64
from cgi import parse_header
-try:
- # Python 3
- from email.header import decode_header
-except ImportError:
- from email.Header import decode_header
+from email.header import decode_header
import six
+from six.moves import range, builtins, map
+from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
-from cherrypy._cpcompat import BaseHTTPRequestHandler, ntob, ntou
-from cherrypy._cpcompat import text_or_bytes, iteritems
-from cherrypy._cpcompat import reversed, sorted, unquote_qs
+import cherrypy
+from cherrypy._cpcompat import ntob, ntou
+from cherrypy._cpcompat import unquote_plus
response_codes = BaseHTTPRequestHandler.responses.copy()
@@ -40,7 +38,7 @@
def urljoin(*atoms):
- """Return the given path \*atoms, joined into a single URL.
+ r"""Return the given path \*atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
@@ -58,11 +56,11 @@ def urljoin_bytes(*atoms):
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
"""
- url = ntob('/').join([x for x in atoms if x])
- while ntob('//') in url:
- url = url.replace(ntob('//'), ntob('/'))
+ url = b'/'.join([x for x in atoms if x])
+ while b'//' in url:
+ url = url.replace(b'//', b'/')
# Special-case the final url of "", and return "/" instead.
- return url or ntob('/')
+ return url or b'/'
def protocol_from_http(protocol_str):
@@ -139,13 +137,13 @@ def __init__(self, value, params=None):
self.params = params
def __cmp__(self, other):
- return cmp(self.value, other.value) # noqa: F821
+ return builtins.cmp(self.value, other.value)
def __lt__(self, other):
return self.value < other.value
def __str__(self):
- p = [';%s=%s' % (k, v) for k, v in iteritems(self.params)]
+ p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)]
return str('%s%s' % (self.value, ''.join(p)))
def __bytes__(self):
@@ -204,12 +202,26 @@ def qvalue(self):
val = self.params.get('q', '1')
if isinstance(val, HeaderElement):
val = val.value
- return float(val)
+ try:
+ return float(val)
+ except ValueError as val_err:
+ """Fail client requests with invalid quality value.
+
+ Ref: https://github.com/cherrypy/cherrypy/issues/1370
+ """
+ six.raise_from(
+ cherrypy.HTTPError(
+ 400,
+ 'Malformed HTTP header: `{}`'.
+ format(str(self)),
+ ),
+ val_err,
+ )
def __cmp__(self, other):
- diff = cmp(self.qvalue, other.qvalue) # noqa: F821
+ diff = builtins.cmp(self.qvalue, other.qvalue)
if diff == 0:
- diff = cmp(str(self), str(other)) # noqa: F821
+ diff = builtins.cmp(str(self), str(other))
return diff
def __lt__(self, other):
@@ -218,8 +230,11 @@ def __lt__(self, other):
else:
return self.qvalue < other.qvalue
-RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') # noqa: E305
-def header_elements(fieldname, fieldvalue): # noqa: E302
+
+RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
+
+
+def header_elements(fieldname, fieldvalue):
"""Return a sorted HeaderElement list from a comma-separated header string.
"""
if not fieldvalue:
@@ -237,7 +252,12 @@ def header_elements(fieldname, fieldvalue): # noqa: E302
def decode_TEXT(value):
- r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
+ r"""
+ Decode :rfc:`2047` TEXT
+
+ >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1')
+ True
+ """
atoms = decode_header(value)
decodedvalue = ''
for atom, charset in atoms:
@@ -247,31 +267,41 @@ def decode_TEXT(value):
return decodedvalue
+def decode_TEXT_maybe(value):
+ """
+ Decode the text but only if '=?' appears in it.
+ """
+ return decode_TEXT(value) if '=?' in value else value
+
+
def valid_status(status):
"""Return legal HTTP status Code, Reason-phrase and Message.
- The status arg must be an int, or a str that begins with an int.
+ The status arg must be an int, a str that begins with an int
+ or the constant from ``http.client`` stdlib module.
+
+ If status has no reason-phrase is supplied, a default reason-
+ phrase will be provided.
- If status is an int, or a str and no reason-phrase is supplied,
- a default reason-phrase will be provided.
+ >>> from six.moves import http_client
+ >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
+ >>> valid_status(http_client.ACCEPTED) == (
+ ... int(http_client.ACCEPTED),
+ ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED]
+ True
"""
if not status:
status = 200
- status = str(status)
- parts = status.split(' ', 1)
- if len(parts) == 1:
- # No reason supplied.
- code, = parts
- reason = None
- else:
- code, reason = parts
- reason = reason.strip()
+ code, reason = status, None
+ if isinstance(status, six.string_types):
+ code, _, reason = status.partition(' ')
+ reason = reason.strip() or None
try:
code = int(code)
- except ValueError:
+ except (TypeError, ValueError):
raise ValueError('Illegal response status from server '
'(%s is non-numeric).' % repr(code))
@@ -329,8 +359,8 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
else:
continue
if len(nv[1]) or keep_blank_values:
- name = unquote_qs(nv[0], encoding)
- value = unquote_qs(nv[1], encoding)
+ name = unquote_plus(nv[0], encoding, errors='strict')
+ value = unquote_plus(nv[1], encoding, errors='strict')
if name in d:
if not isinstance(d[name], list):
d[name] = [d[name]]
@@ -360,53 +390,77 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
return pm
-class CaseInsensitiveDict(dict):
+####
+# Inlined from jaraco.collections 1.5.2
+# Ref #1673
+class KeyTransformingDict(dict):
+ """
+ A dict subclass that transforms the keys before they're used.
+ Subclasses may override the default transform_key to customize behavior.
+ """
+ @staticmethod
+ def transform_key(key):
+ return key
- """A case-insensitive dict subclass.
+ def __init__(self, *args, **kargs):
+ super(KeyTransformingDict, self).__init__()
+ # build a dictionary using the default constructs
+ d = dict(*args, **kargs)
+ # build this dictionary using transformed keys.
+ for item in d.items():
+ self.__setitem__(*item)
- Each key is changed on entry to str(key).title().
- """
+ def __setitem__(self, key, val):
+ key = self.transform_key(key)
+ super(KeyTransformingDict, self).__setitem__(key, val)
def __getitem__(self, key):
- return dict.__getitem__(self, str(key).title())
+ key = self.transform_key(key)
+ return super(KeyTransformingDict, self).__getitem__(key)
- def __setitem__(self, key, value):
- dict.__setitem__(self, str(key).title(), value)
+ def __contains__(self, key):
+ key = self.transform_key(key)
+ return super(KeyTransformingDict, self).__contains__(key)
def __delitem__(self, key):
- dict.__delitem__(self, str(key).title())
+ key = self.transform_key(key)
+ return super(KeyTransformingDict, self).__delitem__(key)
- def __contains__(self, key):
- return dict.__contains__(self, str(key).title())
+ def get(self, key, *args, **kwargs):
+ key = self.transform_key(key)
+ return super(KeyTransformingDict, self).get(key, *args, **kwargs)
- def get(self, key, default=None):
- return dict.get(self, str(key).title(), default)
+ def setdefault(self, key, *args, **kwargs):
+ key = self.transform_key(key)
+ return super(KeyTransformingDict, self).setdefault(
+ key, *args, **kwargs)
- if hasattr({}, 'has_key'):
- def has_key(self, key):
- return str(key).title() in self
+ def pop(self, key, *args, **kwargs):
+ key = self.transform_key(key)
+ return super(KeyTransformingDict, self).pop(key, *args, **kwargs)
- def update(self, E):
- for k in E.keys():
- self[str(k).title()] = E[k]
+ def matching_key_for(self, key):
+ """
+ Given a key, return the actual key stored in self that matches.
+ Raise KeyError if the key isn't found.
+ """
+ try:
+ return next(e_key for e_key in self.keys() if e_key == key)
+ except StopIteration:
+ raise KeyError(key)
+####
- @classmethod
- def fromkeys(cls, seq, value=None):
- newdict = cls()
- for k in seq:
- newdict[str(k).title()] = value
- return newdict
- def setdefault(self, key, x=None):
- key = str(key).title()
- try:
- return self[key]
- except KeyError:
- self[key] = x
- return x
+class CaseInsensitiveDict(KeyTransformingDict):
- def pop(self, key, default):
- return dict.pop(self, str(key).title(), default)
+ """A case-insensitive dict subclass.
+
+ Each key is changed on entry to str(key).title().
+ """
+
+ @staticmethod
+ def transform_key(key):
+ return str(key).title()
# TEXT =
@@ -415,9 +469,9 @@ def pop(self, key, default):
# field continuation. It is expected that the folding LWS will be
# replaced with a single SP before interpretation of the TEXT value."
if str == bytes:
- header_translate_table = ''.join([chr(i) for i in six.moves.xrange(256)])
+ header_translate_table = ''.join([chr(i) for i in range(256)])
header_translate_deletechars = ''.join(
- [chr(i) for i in six.moves.xrange(32)]) + chr(127)
+ [chr(i) for i in range(32)]) + chr(127)
else:
header_translate_table = None
header_translate_deletechars = bytes(range(32)) + bytes([127])
@@ -464,23 +518,21 @@ def encode_header_items(cls, header_items):
transmitting on the wire for HTTP.
"""
for k, v in header_items:
- if isinstance(k, six.text_type):
- k = cls.encode(k)
+ if not isinstance(v, six.string_types) and \
+ not isinstance(v, six.binary_type):
+ v = six.text_type(v)
- if not isinstance(v, text_or_bytes):
- v = str(v)
+ yield tuple(map(cls.encode_header_item, (k, v)))
- if isinstance(v, six.text_type):
- v = cls.encode(v)
-
- # See header_translate_* constants above.
- # Replace only if you really know what you're doing.
- k = k.translate(header_translate_table,
- header_translate_deletechars)
- v = v.translate(header_translate_table,
- header_translate_deletechars)
+ @classmethod
+ def encode_header_item(cls, item):
+ if isinstance(item, six.text_type):
+ item = cls.encode(item)
- yield (k, v)
+ # See header_translate_* constants above.
+ # Replace only if you really know what you're doing.
+ return item.translate(
+ header_translate_table, header_translate_deletechars)
@classmethod
def encode(cls, v):
@@ -498,7 +550,7 @@ def encode(cls, v):
# because we never want to fold lines--folding has
# been deprecated by the HTTP working group.
v = b2a_base64(v.encode('utf-8'))
- return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
+ return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=')
raise ValueError('Could not encode header part %r using '
'any of the encodings %r.' %
diff --git a/lib/cherrypy/lib/jsontools.py b/lib/cherrypy/lib/jsontools.py
index 91ea74e..4868309 100644
--- a/lib/cherrypy/lib/jsontools.py
+++ b/lib/cherrypy/lib/jsontools.py
@@ -34,9 +34,6 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
request header, or it will raise "411 Length Required". If for any
other reason the request entity cannot be deserialized from JSON,
it will raise "400 Bad Request: Invalid JSON document".
-
- You must be using Python 2.6 or greater, or have the 'simplejson'
- package importable; otherwise, ValueError is raised during processing.
"""
request = cherrypy.serving.request
if isinstance(content_type, text_or_bytes):
@@ -72,9 +69,6 @@ def json_out(content_type='application/json', debug=False,
Provide your own handler to use a custom encoder. For example
cherrypy.config['tools.json_out.handler'] = , or
@json_out(handler=function).
-
- You must be using Python 2.6 or greater, or have the 'simplejson'
- package importable; otherwise, ValueError is raised during processing.
"""
request = cherrypy.serving.request
# request.handler may be set to None by e.g. the caching tool
diff --git a/lib/cherrypy/lib/lockfile.py b/lib/cherrypy/lib/lockfile.py
deleted file mode 100644
index 336b558..0000000
--- a/lib/cherrypy/lib/lockfile.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-Platform-independent file locking. Inspired by and modeled after zc.lockfile.
-"""
-
-import os
-
-try:
- import msvcrt
-except ImportError:
- pass
-
-try:
- import fcntl
-except ImportError:
- pass
-
-
-class LockError(Exception):
-
- 'Could not obtain a lock'
-
- msg = 'Unable to lock %r'
-
- def __init__(self, path):
- super(LockError, self).__init__(self.msg % path)
-
-
-class UnlockError(LockError):
-
- 'Could not release a lock'
-
- msg = 'Unable to unlock %r'
-
-
-# first, a default, naive locking implementation
-class LockFile(object):
-
- """
- A default, naive locking implementation. Always fails if the file
- already exists.
- """
-
- def __init__(self, path):
- self.path = path
- try:
- fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
- except OSError:
- raise LockError(self.path)
- os.close(fd)
-
- def release(self):
- os.remove(self.path)
-
- def remove(self):
- pass
-
-
-class SystemLockFile(object):
-
- """
- An abstract base class for platform-specific locking.
- """
-
- def __init__(self, path):
- self.path = path
-
- try:
- # Open lockfile for writing without truncation:
- self.fp = open(path, 'r+')
- except IOError:
- # If the file doesn't exist, IOError is raised; Use a+ instead.
- # Note that there may be a race here. Multiple processes
- # could fail on the r+ open and open the file a+, but only
- # one will get the the lock and write a pid.
- self.fp = open(path, 'a+')
-
- try:
- self._lock_file()
- except:
- self.fp.seek(1)
- self.fp.close()
- del self.fp
- raise
-
- self.fp.write(' %s\n' % os.getpid())
- self.fp.truncate()
- self.fp.flush()
-
- def release(self):
- if not hasattr(self, 'fp'):
- return
- self._unlock_file()
- self.fp.close()
- del self.fp
-
- def remove(self):
- """
- Attempt to remove the file
- """
- try:
- os.remove(self.path)
- except:
- pass
-
- def _unlock_file(self):
- """Attempt to obtain the lock on self.fp. Raise UnlockError if not
- released."""
-
-
-class WindowsLockFile(SystemLockFile):
-
- def _lock_file(self):
- # Lock just the first byte
- try:
- msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
- except IOError:
- raise LockError(self.fp.name)
-
- def _unlock_file(self):
- try:
- self.fp.seek(0)
- msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
- except IOError:
- raise UnlockError(self.fp.name)
-
-if 'msvcrt' in globals(): # noqa: E305
- LockFile = WindowsLockFile # noqa: F811
-
-
-class UnixLockFile(SystemLockFile):
-
- def _lock_file(self):
- flags = fcntl.LOCK_EX | fcntl.LOCK_NB
- try:
- fcntl.flock(self.fp.fileno(), flags)
- except IOError:
- raise LockError(self.fp.name)
-
- # no need to implement _unlock_file, it will be unlocked on close()
-
-if 'fcntl' in globals(): # noqa: E305
- LockFile = UnixLockFile
diff --git a/lib/cherrypy/lib/profiler.py b/lib/cherrypy/lib/profiler.py
index 94b8798..fccf2eb 100644
--- a/lib/cherrypy/lib/profiler.py
+++ b/lib/cherrypy/lib/profiler.py
@@ -51,7 +51,11 @@ def new_func_strip_path(func_name):
"""
filename, line, name = func_name
if filename.endswith('__init__.py'):
- return os.path.basename(filename[:-12]) + filename[-12:], line, name
+ return (
+ os.path.basename(filename[:-12]) + filename[-12:],
+ line,
+ name,
+ )
return os.path.basename(filename), line, name
pstats.func_strip_path = new_func_strip_path
diff --git a/lib/cherrypy/lib/reprconf.py b/lib/cherrypy/lib/reprconf.py
index 0c7400a..fc75849 100644
--- a/lib/cherrypy/lib/reprconf.py
+++ b/lib/cherrypy/lib/reprconf.py
@@ -18,35 +18,12 @@
and the handler must be either a callable or a context manager.
"""
-try:
- # Python 3.0+
- from configparser import ConfigParser
-except ImportError:
- from ConfigParser import ConfigParser
-
-try:
- text_or_bytes
-except NameError:
- text_or_bytes = str
-
-try:
- # Python 3
- import builtins
-except ImportError:
- # Python 2
- import __builtin__ as builtins
-
-import operator as _operator
-import sys
-
+from cherrypy._cpcompat import text_or_bytes
+from six.moves import configparser
+from six.moves import builtins
-def as_dict(config):
- """Return a dict from 'config' whether it is a dict, file, or filename."""
- if isinstance(config, text_or_bytes):
- config = Parser().dict_from_file(config)
- elif hasattr(config, 'read'):
- config = Parser().dict_from_file(config)
- return config
+import operator
+import sys
class NamespaceSet(dict):
@@ -85,9 +62,9 @@ def __call__(self, config):
# I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement:
- # for ns, handler in self.iteritems():
+ # for ns, handler in six.iteritems(self):
# with handler as callable:
- # for k, v in ns_confs.get(ns, {}).iteritems():
+ # for k, v in six.iteritems(ns_confs.get(ns, {})):
# callable(k, v)
for ns, handler in self.items():
exit = getattr(handler, '__exit__', None)
@@ -98,7 +75,7 @@ def __call__(self, config):
try:
for k, v in ns_confs.get(ns, {}).items():
callable(k, v)
- except:
+ except Exception:
# The exceptional case is handled here
no_exc = False
if exit is None:
@@ -149,16 +126,8 @@ def reset(self):
dict.update(self, self.defaults)
def update(self, config):
- """Update self from a dict, file or filename."""
- if isinstance(config, text_or_bytes):
- # Filename
- config = Parser().dict_from_file(config)
- elif hasattr(config, 'read'):
- # Open file object
- config = Parser().dict_from_file(config)
- else:
- config = config.copy()
- self._apply(config)
+ """Update self from a dict, file, or filename."""
+ self._apply(Parser.load(config))
def _apply(self, config):
"""Update self from a dict."""
@@ -177,7 +146,7 @@ def __setitem__(self, k, v):
self.namespaces({k: v})
-class Parser(ConfigParser):
+class Parser(configparser.ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that
raises an exception if the file cannot be read.
@@ -227,6 +196,17 @@ def dict_from_file(self, file):
self.read(file)
return self.as_dict()
+ @classmethod
+ def load(self, input):
+ """Resolve 'input' to dict from a dict, file, or filename."""
+ is_file = (
+ # Filename
+ isinstance(input, text_or_bytes)
+ # Open file object
+ or hasattr(input, 'read')
+ )
+ return Parser().dict_from_file(input) if is_file else input.copy()
+
# public domain "unrepr" implementation, found on the web and then improved.
@@ -471,6 +451,8 @@ def build_Name(self, o):
def build_NameConstant(self, o):
return o.value
+ build_Constant = build_NameConstant # Python 3.8 change
+
def build_UnaryOp(self, o):
op, operand = map(self.build, [o.op, o.operand])
return op(operand)
@@ -480,13 +462,13 @@ def build_BinOp(self, o):
return op(left, right)
def build_Add(self, o):
- return _operator.add
+ return operator.add
def build_Mult(self, o):
- return _operator.mul
+ return operator.mul
def build_USub(self, o):
- return _operator.neg
+ return operator.neg
def build_Attribute(self, o):
parent = self.build(o.value)
diff --git a/lib/cherrypy/lib/sessions.py b/lib/cherrypy/lib/sessions.py
index 9a763dc..5b49ee1 100644
--- a/lib/cherrypy/lib/sessions.py
+++ b/lib/cherrypy/lib/sessions.py
@@ -57,6 +57,17 @@
data for that id. Therefore, if you never save any session data,
**you will get a new session id for every request**.
+A side effect of CherryPy overwriting unrecognised session ids is that if you
+have multiple, separate CherryPy applications running on a single domain (e.g.
+on different ports), each app will overwrite the other's session id because by
+default they use the same cookie name (``"session_id"``) but do not recognise
+each others sessions. It is therefore a good idea to use a different name for
+each, for example::
+
+ [/]
+ ...
+ tools.sessions.name = "my_app_session_id"
+
================
Sharing Sessions
================
@@ -94,14 +105,24 @@
import os
import time
import threading
+import binascii
+
+import six
+from six.moves import cPickle as pickle
+import contextlib2
+
+import zc.lockfile
import cherrypy
-from cherrypy._cpcompat import copyitems, pickle, random20
from cherrypy.lib import httputil
-from cherrypy.lib import lockfile
from cherrypy.lib import locking
from cherrypy.lib import is_iterator
+
+if six.PY2:
+ FileNotFoundError = OSError
+
+
missing = object()
@@ -114,14 +135,16 @@ class Session(object):
id_observers = None
"A list of callbacks to which to pass new id's."
- def _get_id(self):
+ @property
+ def id(self):
+ """Return the current session id."""
return self._id
- def _set_id(self, value):
+ @id.setter
+ def id(self, value):
self._id = value
for o in self.id_observers:
o(value)
- id = property(_get_id, _set_id, doc='The current session ID.')
timeout = 60
'Number of minutes after which to delete session data.'
@@ -235,7 +258,7 @@ def clean_up(self):
def generate_id(self):
"""Return a new session id."""
- return random20()
+ return binascii.hexlify(os.urandom(20)).decode('ascii')
def save(self):
"""Save session data."""
@@ -334,13 +357,6 @@ def __contains__(self, key):
self.load()
return key in self._data
- if hasattr({}, 'has_key'):
- def has_key(self, key):
- """D.has_key(k) -> True if D has a key k, else False."""
- if not self.loaded:
- self.load()
- return key in self._data
-
def get(self, key, default=None):
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
if not self.loaded:
@@ -394,7 +410,7 @@ def clean_up(self):
"""Clean up expired sessions."""
now = self.now()
- for _id, (data, expiration_time) in copyitems(self.cache):
+ for _id, (data, expiration_time) in list(six.iteritems(self.cache)):
if expiration_time <= now:
try:
del self.cache[_id]
@@ -409,7 +425,11 @@ def clean_up(self):
# added to remove obsolete lock objects
for _id in list(self.locks):
- if _id not in self.cache and self.locks[_id].acquire(blocking=False):
+ locked = (
+ _id not in self.cache
+ and self.locks[_id].acquire(blocking=False)
+ )
+ if locked:
lock = self.locks.pop(_id)
lock.release()
@@ -470,7 +490,9 @@ def __init__(self, id=None, **kwargs):
if isinstance(self.lock_timeout, (int, float)):
self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
- raise ValueError('Lock timeout must be numeric seconds or a timedelta instance.')
+ raise ValueError(
+ 'Lock timeout must be numeric seconds or a timedelta instance.'
+ )
@classmethod
def setup(cls, **kwargs):
@@ -538,8 +560,8 @@ def acquire_lock(self, path=None):
checker = locking.LockChecker(self.id, self.lock_timeout)
while not checker.expired():
try:
- self.lock = lockfile.LockFile(path)
- except lockfile.LockError:
+ self.lock = zc.lockfile.LockFile(path)
+ except zc.lockfile.LockError:
time.sleep(0.1)
else:
break
@@ -549,8 +571,9 @@ def acquire_lock(self, path=None):
def release_lock(self, path=None):
"""Release the lock on the currently-loaded session data."""
- self.lock.release()
- self.lock.remove()
+ self.lock.close()
+ with contextlib2.suppress(FileNotFoundError):
+ os.remove(self.lock._path)
self.locked = False
def clean_up(self):
@@ -558,7 +581,11 @@ def clean_up(self):
now = self.now()
# Iterate over all session files in self.storage_path
for fname in os.listdir(self.storage_path):
- if fname.startswith(self.SESSION_PREFIX) and not fname.endswith(self.LOCK_SUFFIX):
+ have_session = (
+ fname.startswith(self.SESSION_PREFIX)
+ and not fname.endswith(self.LOCK_SUFFIX)
+ )
+ if have_session:
# We have a session file: lock and load it and check
# if it's expired. If it fails, nevermind.
path = os.path.join(self.storage_path, fname)
@@ -594,7 +621,7 @@ class MemcachedSession(Session):
# Wrap all .get and .set operations in a single lock.
mc_lock = threading.RLock()
- # This is a seperate set of locks per session id.
+ # This is a separate set of locks per session id.
locks = {}
servers = ['127.0.0.1:11211']
@@ -682,7 +709,9 @@ def save():
if is_iterator(response.body):
response.collapse_body()
cherrypy.session.save()
-save.failsafe = True # noqa: E305
+
+
+save.failsafe = True
def close():
@@ -693,7 +722,9 @@ def close():
sess.release_lock()
if sess.debug:
cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
-close.failsafe = True # noqa: E305
+
+
+close.failsafe = True
close.priority = 90
@@ -885,3 +916,4 @@ def expire():
one_year = 60 * 60 * 24 * 365
e = time.time() - one_year
cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
+ cherrypy.serving.response.cookie[name].pop('max-age', None)
diff --git a/lib/cherrypy/lib/static.py b/lib/cherrypy/lib/static.py
index acedf51..da9d937 100644
--- a/lib/cherrypy/lib/static.py
+++ b/lib/cherrypy/lib/static.py
@@ -1,23 +1,32 @@
+"""Module with helpers for serving static files."""
+
import os
+import platform
import re
import stat
import mimetypes
-try:
- from io import UnsupportedOperation
-except ImportError:
- UnsupportedOperation = object()
+from email.generator import _make_boundary as make_boundary
+from io import UnsupportedOperation
+
+from six.moves import urllib
import cherrypy
-from cherrypy._cpcompat import ntob, unquote
+from cherrypy._cpcompat import ntob
from cherrypy.lib import cptools, httputil, file_generator_limited
-mimetypes.init()
-mimetypes.types_map['.dwg'] = 'image/x-dwg'
-mimetypes.types_map['.ico'] = 'image/x-icon'
-mimetypes.types_map['.bz2'] = 'application/x-bzip2'
-mimetypes.types_map['.gz'] = 'application/x-gzip'
+def _setup_mimetypes():
+ """Pre-initialize global mimetype map."""
+ if not mimetypes.inited:
+ mimetypes.init()
+ mimetypes.types_map['.dwg'] = 'image/x-dwg'
+ mimetypes.types_map['.ico'] = 'image/x-icon'
+ mimetypes.types_map['.bz2'] = 'application/x-bzip2'
+ mimetypes.types_map['.gz'] = 'application/x-gzip'
+
+
+_setup_mimetypes()
def serve_file(path, content_type=None, disposition=None, name=None,
@@ -33,7 +42,6 @@ def serve_file(path, content_type=None, disposition=None, name=None,
to the basename of path. If disposition is None, no Content-Disposition
header will be written.
"""
-
response = cherrypy.serving.response
# If path is relative, users should fix it by making path absolute.
@@ -98,7 +106,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
- debug=False, filesize=None):
+ debug=False):
"""Set status, headers, and body in order to serve the given file object.
The Content-Type header will be set to the content_type arg, if provided.
@@ -115,7 +123,6 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
serve_fileobj(), expecting that the data would be served starting from that
position.
"""
-
response = cherrypy.serving.response
try:
@@ -123,7 +130,7 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
except AttributeError:
if debug:
cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
- content_length = filesize
+ content_length = None
except UnsupportedOperation:
content_length = None
else:
@@ -144,7 +151,7 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
cd = disposition
else:
cd = '%s; filename="%s"' % (disposition, name)
- response.headers["Content-Disposition"] = cd
+ response.headers['Content-Disposition'] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@@ -188,12 +195,6 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
else:
# Return a multipart/byteranges response.
response.status = '206 Partial Content'
- try:
- # Python 3
- from email.generator import _make_boundary as make_boundary
- except ImportError:
- # Python 2
- from mimetools import choose_boundary as make_boundary
boundary = make_boundary()
ct = 'multipart/byteranges; boundary=%s' % boundary
response.headers['Content-Type'] = ct
@@ -203,7 +204,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
def file_ranges():
# Apache compatibility:
- yield ntob('\r\n')
+ yield b'\r\n'
for start, stop in r:
if debug:
@@ -222,12 +223,12 @@ def file_ranges():
gen = file_generator_limited(fileobj, stop - start)
for chunk in gen:
yield chunk
- yield ntob('\r\n')
+ yield b'\r\n'
# Final boundary
yield ntob('--' + boundary + '--', 'ascii')
# Apache compatibility:
- yield ntob('\r\n')
+ yield b'\r\n'
response.body = file_ranges()
return response.body
else:
@@ -318,7 +319,15 @@ def staticdir(section, dir, root='', match='', content_types=None, index='',
section = '/'
section = section.rstrip(r'\/')
branch = request.path_info[len(section) + 1:]
- branch = unquote(branch.lstrip(r'\/'))
+ branch = urllib.parse.unquote(branch.lstrip(r'\/'))
+
+ # Requesting a file in sub-dir of the staticdir results
+ # in mixing of delimiter styles, e.g. C:\static\js/script.js.
+ # Windows accepts this form except not when the path is
+ # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js.
+ # http://bit.ly/1vdioCX
+ if platform.system() == 'Windows':
+ branch = branch.replace('/', '\\')
# If branch is "", filename will end in a slash
filename = os.path.join(dir, branch)
diff --git a/lib/cherrypy/lib/xmlrpcutil.py b/lib/cherrypy/lib/xmlrpcutil.py
index 9fc9564..ddaac86 100644
--- a/lib/cherrypy/lib/xmlrpcutil.py
+++ b/lib/cherrypy/lib/xmlrpcutil.py
@@ -1,21 +1,19 @@
+"""XML-RPC tool helpers."""
import sys
+from six.moves.xmlrpc_client import (
+ loads as xmlrpc_loads, dumps as xmlrpc_dumps,
+ Fault as XMLRPCFault
+)
+
import cherrypy
from cherrypy._cpcompat import ntob
-def get_xmlrpclib():
- try:
- import xmlrpc.client as x
- except ImportError:
- import xmlrpclib as x
- return x
-
-
def process_body():
"""Return (params, method) from request body."""
try:
- return get_xmlrpclib().loads(cherrypy.request.body.read())
+ return xmlrpc_loads(cherrypy.request.body.read())
except Exception:
return ('ERROR PARAMS', ), 'ERRORMETHOD'
@@ -31,9 +29,10 @@ def patched_path(path):
def _set_response(body):
+ """Set up HTTP status, headers and body within CherryPy."""
# The XML-RPC spec (http://www.xmlrpc.com/spec) says:
# "Unless there's a lower-level error, always return 200 OK."
- # Since Python's xmlrpclib interprets a non-200 response
+ # Since Python's xmlrpc_client interprets a non-200 response
# as a "Protocol Error", we'll just return 200 every time.
response = cherrypy.response
response.status = '200 OK'
@@ -43,15 +42,20 @@ def _set_response(body):
def respond(body, encoding='utf-8', allow_none=0):
- xmlrpclib = get_xmlrpclib()
- if not isinstance(body, xmlrpclib.Fault):
+ """Construct HTTP response body."""
+ if not isinstance(body, XMLRPCFault):
body = (body,)
- _set_response(xmlrpclib.dumps(body, methodresponse=1,
- encoding=encoding,
- allow_none=allow_none))
+
+ _set_response(
+ xmlrpc_dumps(
+ body, methodresponse=1,
+ encoding=encoding,
+ allow_none=allow_none
+ )
+ )
def on_error(*args, **kwargs):
+ """Construct HTTP response body for an error response."""
body = str(sys.exc_info()[1])
- xmlrpclib = get_xmlrpclib()
- _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))
+ _set_response(xmlrpc_dumps(XMLRPCFault(1, body)))
diff --git a/lib/cherrypy/process/__init__.py b/lib/cherrypy/process/__init__.py
index 97f91ce..f242d22 100644
--- a/lib/cherrypy/process/__init__.py
+++ b/lib/cherrypy/process/__init__.py
@@ -10,5 +10,8 @@
for each class.
"""
-from cherrypy.process.wspbus import bus # noqa
-from cherrypy.process import plugins, servers # noqa
+from .wspbus import bus
+from . import plugins, servers
+
+
+__all__ = ('bus', 'plugins', 'servers')
diff --git a/lib/cherrypy/process/plugins.py b/lib/cherrypy/process/plugins.py
index a94c3cd..8c246c8 100644
--- a/lib/cherrypy/process/plugins.py
+++ b/lib/cherrypy/process/plugins.py
@@ -7,7 +7,9 @@
import time
import threading
-from cherrypy._cpcompat import text_or_bytes, get_thread_ident
+from six.moves import _thread
+
+from cherrypy._cpcompat import text_or_bytes
from cherrypy._cpcompat import ntob, Timer
# _module__file__base is used by Autoreload to make
@@ -220,7 +222,8 @@ class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows.
- Special thanks to `Gavin Baker `_
+ Special thanks to `Gavin Baker
+ `_
"""
def __init__(self, bus, umask=None, uid=None, gid=None):
@@ -230,10 +233,13 @@ def __init__(self, bus, umask=None, uid=None, gid=None):
self.gid = gid
self.umask = umask
- def _get_uid(self):
+ @property
+ def uid(self):
+ """The uid under which to run. Availability: Unix."""
return self._uid
- def _set_uid(self, val):
+ @uid.setter
+ def uid(self, val):
if val is not None:
if pwd is None:
self.bus.log('pwd module not available; ignoring uid.',
@@ -242,13 +248,14 @@ def _set_uid(self, val):
elif isinstance(val, text_or_bytes):
val = pwd.getpwnam(val)[2]
self._uid = val
- uid = property(_get_uid, _set_uid,
- doc='The uid under which to run. Availability: Unix.')
- def _get_gid(self):
+ @property
+ def gid(self):
+ """The gid under which to run. Availability: Unix."""
return self._gid
- def _set_gid(self, val):
+ @gid.setter
+ def gid(self, val):
if val is not None:
if grp is None:
self.bus.log('grp module not available; ignoring gid.',
@@ -257,13 +264,18 @@ def _set_gid(self, val):
elif isinstance(val, text_or_bytes):
val = grp.getgrnam(val)[2]
self._gid = val
- gid = property(_get_gid, _set_gid,
- doc='The gid under which to run. Availability: Unix.')
- def _get_umask(self):
+ @property
+ def umask(self):
+ """The default permission mode for newly created files and directories.
+
+ Usually expressed in octal format, for example, ``0644``.
+ Availability: Unix, Windows.
+ """
return self._umask
- def _set_umask(self, val):
+ @umask.setter
+ def umask(self, val):
if val is not None:
try:
os.umask
@@ -272,15 +284,6 @@ def _set_umask(self, val):
level=30)
val = None
self._umask = val
- umask = property(
- _get_umask,
- _set_umask,
- doc="""The default permission mode for newly created files and
- directories.
-
- Usually expressed in octal format, for example, ``0644``.
- Availability: Unix, Windows.
- """)
def start(self):
# uid/gid
@@ -344,7 +347,7 @@ class Daemonizer(SimplePlugin):
process still return proper exit codes. Therefore, if you use this
plugin to daemonize, don't use the return code as an accurate indicator
of whether the process fully started. In fact, that return code only
- indicates if the process succesfully finished the first fork.
+ indicates if the process successfully finished the first fork.
"""
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
@@ -369,6 +372,15 @@ def start(self):
'Daemonizing now may cause strange failures.' %
threading.enumerate(), level=30)
+ self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log)
+
+ self.finalized = True
+ start.priority = 65
+
+ @staticmethod
+ def daemonize(
+ stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
+ logger=lambda msg: None):
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
@@ -377,40 +389,29 @@ def start(self):
sys.stdout.flush()
sys.stderr.flush()
- # Do first fork.
- try:
- pid = os.fork()
- if pid == 0:
- # This is the child process. Continue.
- pass
- else:
- # This is the first parent. Exit, now that we've forked.
- self.bus.log('Forking once.')
- os._exit(0)
- except OSError:
- # Python raises OSError rather than returning negative numbers.
- exc = sys.exc_info()[1]
- sys.exit('%s: fork #1 failed: (%d) %s\n'
- % (sys.argv[0], exc.errno, exc.strerror))
-
- os.setsid()
-
- # Do second fork
- try:
- pid = os.fork()
- if pid > 0:
- self.bus.log('Forking twice.')
- os._exit(0) # Exit second parent
- except OSError:
- exc = sys.exc_info()[1]
- sys.exit('%s: fork #2 failed: (%d) %s\n'
- % (sys.argv[0], exc.errno, exc.strerror))
+ error_tmpl = (
+ '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n'
+ )
+
+ for fork in range(2):
+ msg = ['Forking once.', 'Forking twice.'][fork]
+ try:
+ pid = os.fork()
+ if pid > 0:
+ # This is the parent; exit.
+ logger(msg)
+ os._exit(0)
+ except OSError as exc:
+ # Python raises OSError rather than returning negative numbers.
+ sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1))
+ if fork == 0:
+ os.setsid()
os.umask(0)
- si = open(self.stdin, 'r')
- so = open(self.stdout, 'a+')
- se = open(self.stderr, 'a+')
+ si = open(stdin, 'r')
+ so = open(stdout, 'a+')
+ se = open(stderr, 'a+')
# os.dup2(fd, fd2) will close fd2 if necessary,
# so we don't explicitly close stdin/out/err.
@@ -419,9 +420,7 @@ def start(self):
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
- self.bus.log('Daemonized to PID: %s' % os.getpid())
- self.finalized = True
- start.priority = 65
+ logger('Daemonized to PID: %s' % os.getpid())
class PIDFile(SimplePlugin):
@@ -449,7 +448,7 @@ def exit(self):
self.bus.log('PID file removed: %r.' % self.pidfile)
except (KeyboardInterrupt, SystemExit):
raise
- except:
+ except Exception:
pass
@@ -628,23 +627,40 @@ def start(self):
def sysfiles(self):
"""Return a Set of sys.modules filenames to monitor."""
- files = set()
- for k, m in list(sys.modules.items()):
- if re.match(self.match, k):
- if (
- hasattr(m, '__loader__') and
- hasattr(m.__loader__, 'archive')
- ):
- f = m.__loader__.archive
- else:
- f = getattr(m, '__file__', None)
- if f is not None and not os.path.isabs(f):
- # ensure absolute paths so a os.chdir() in the app
- # doesn't break me
- f = os.path.normpath(
- os.path.join(_module__file__base, f))
- files.add(f)
- return files
+ search_mod_names = filter(re.compile(self.match).match, sys.modules)
+ mods = map(sys.modules.get, search_mod_names)
+ return set(filter(None, map(self._file_for_module, mods)))
+
+ @classmethod
+ def _file_for_module(cls, module):
+ """Return the relevant file for the module."""
+ return (
+ cls._archive_for_zip_module(module)
+ or cls._file_for_file_module(module)
+ )
+
+ @staticmethod
+ def _archive_for_zip_module(module):
+ """Return the archive filename for the module if relevant."""
+ try:
+ return module.__loader__.archive
+ except AttributeError:
+ pass
+
+ @classmethod
+ def _file_for_file_module(cls, module):
+ """Return the file for the module."""
+ try:
+ return module.__file__ and cls._make_absolute(module.__file__)
+ except AttributeError:
+ pass
+
+ @staticmethod
+ def _make_absolute(filename):
+ """Ensure filename is absolute to avoid effect of os.chdir."""
+ return filename if os.path.isabs(filename) else (
+ os.path.normpath(os.path.join(_module__file__base, filename))
+ )
def run(self):
"""Reload the process if registered files have been modified."""
@@ -713,7 +729,7 @@ def acquire_thread(self):
If the current thread has already been seen, any 'start_thread'
listeners will not be run again.
"""
- thread_ident = get_thread_ident()
+ thread_ident = _thread.get_ident()
if thread_ident not in self.threads:
# We can't just use get_ident as the thread ID
# because some platforms reuse thread ID's.
@@ -723,7 +739,7 @@ def acquire_thread(self):
def release_thread(self):
"""Release the current thread and run 'stop_thread' listeners."""
- thread_ident = get_thread_ident()
+ thread_ident = _thread.get_ident()
i = self.threads.pop(thread_ident, None)
if i is not None:
self.bus.publish('stop_thread', i)
diff --git a/lib/cherrypy/process/servers.py b/lib/cherrypy/process/servers.py
index 1013f24..dcb34de 100644
--- a/lib/cherrypy/process/servers.py
+++ b/lib/cherrypy/process/servers.py
@@ -1,4 +1,4 @@
-"""
+r"""
Starting in CherryPy 3.1, cherrypy.server is implemented as an
:ref:`Engine Plugin`. It's an instance of
:class:`cherrypy._cpserver.Server`, which is a subclass of
@@ -232,7 +232,7 @@ def _start_http_thread(self):
self.interrupt = sys.exc_info()[1]
self.bus.exit()
raise
- except:
+ except Exception:
self.interrupt = sys.exc_info()[1]
self.bus.log('Error in HTTP server: shutting down',
traceback=True, level=40)
@@ -246,13 +246,18 @@ def wait(self):
raise self.interrupt
time.sleep(.1)
- # Wait for port to be occupied
- if not os.environ.get('LISTEN_PID', None):
- # Wait for port to be occupied if not running via socket-activation
- # (for socket-activation the port will be managed by systemd )
- if isinstance(self.bind_addr, tuple):
- with _safe_wait(*self.bound_addr):
- portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
+ # bypass check when LISTEN_PID is set
+ if os.environ.get('LISTEN_PID', None):
+ return
+
+ # bypass check when running via socket-activation
+ # (for socket-activation the port will be managed by systemd)
+ if not isinstance(self.bind_addr, tuple):
+ return
+
+ # wait for port to be occupied
+ with _safe_wait(*self.bound_addr):
+ portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
@property
def bound_addr(self):
diff --git a/lib/cherrypy/process/win32.py b/lib/cherrypy/process/win32.py
index 74b5067..096b027 100644
--- a/lib/cherrypy/process/win32.py
+++ b/lib/cherrypy/process/win32.py
@@ -90,14 +90,15 @@ def _get_state_event(self, state):
self.events[state] = event
return event
- def _get_state(self):
+ @property
+ def state(self):
return self._state
- def _set_state(self, value):
+ @state.setter
+ def state(self, value):
self._state = value
event = self._get_state_event(value)
win32event.PulseEvent(event)
- state = property(_get_state, _set_state)
def wait(self, state, interval=0.1, channel=None):
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
@@ -137,7 +138,8 @@ def key_for(self, obj):
return key
raise ValueError('The given object could not be found: %r' % obj)
-control_codes = _ControlCodes({'graceful': 138}) # noqa: E305
+
+control_codes = _ControlCodes({'graceful': 138})
def signal_child(service, command):
diff --git a/lib/cherrypy/process/wspbus.py b/lib/cherrypy/process/wspbus.py
index 634859c..d91dba4 100644
--- a/lib/cherrypy/process/wspbus.py
+++ b/lib/cherrypy/process/wspbus.py
@@ -1,4 +1,4 @@
-"""An implementation of the Web Site Process Bus.
+r"""An implementation of the Web Site Process Bus.
This module is completely standalone, depending only on the stdlib.
@@ -78,11 +78,11 @@
import time
import traceback as _traceback
import warnings
+import subprocess
+import functools
import six
-from cherrypy._cpcompat import _args_from_interpreter_flags
-
# Here I save the value of os.getcwd(), which, if I am imported early enough,
# will be the directory from which the startup script was run. This is needed
@@ -94,13 +94,13 @@
class ChannelFailures(Exception):
+ """Exception raised during errors on Bus.publish()."""
- """Exception raised when errors occur in a listener during Bus.publish().
- """
delimiter = '\n'
def __init__(self, *args, **kwargs):
- super(Exception, self).__init__(*args, **kwargs)
+ """Initialize ChannelFailures errors wrapper."""
+ super(ChannelFailures, self).__init__(*args, **kwargs)
self._exceptions = list()
def handle_exception(self):
@@ -112,12 +112,14 @@ def get_instances(self):
return self._exceptions[:]
def __str__(self):
+ """Render the list of errors, which happened in channel."""
exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings)
__repr__ = __str__
def __bool__(self):
+ """Determine whether any error happened in channel."""
return bool(self._exceptions)
__nonzero__ = __bool__
@@ -136,7 +138,9 @@ def __setattr__(self, key, value):
if isinstance(value, self.State):
value.name = key
object.__setattr__(self, key, value)
-states = _StateEnum() # noqa: E305
+
+
+states = _StateEnum()
states.STOPPED = states.State()
states.STARTING = states.State()
states.STARTED = states.State()
@@ -156,7 +160,6 @@ def __setattr__(self, key, value):
class Bus(object):
-
"""Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even
@@ -172,6 +175,7 @@ class Bus(object):
max_cloexec_files = max_files
def __init__(self):
+ """Initialize pub/sub bus."""
self.execv = False
self.state = states.STOPPED
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
@@ -181,8 +185,19 @@ def __init__(self):
)
self._priorities = {}
- def subscribe(self, channel, callback, priority=None):
- """Add the given callback at the given channel (if not present)."""
+ def subscribe(self, channel, callback=None, priority=None):
+ """Add the given callback at the given channel (if not present).
+
+ If callback is None, return a partial suitable for decorating
+ the callback.
+ """
+ if callback is None:
+ return functools.partial(
+ self.subscribe,
+ channel,
+ priority=priority,
+ )
+
ch_listeners = self.listeners.setdefault(channel, set())
ch_listeners.add(callback)
@@ -221,7 +236,7 @@ def publish(self, channel, *args, **kwargs):
if exc and e.code == 0:
e.code = 1
raise
- except:
+ except Exception:
exc.handle_exception()
if channel == 'log':
# Assume any further messages to 'log' will fail.
@@ -234,7 +249,7 @@ def publish(self, channel, *args, **kwargs):
return output
def _clean_exit(self):
- """An atexit handler which asserts the Bus is not running."""
+ """Assert that the Bus is not running in atexit handler callback."""
if self.state != states.EXITING:
warnings.warn(
'The main thread is exiting, but the Bus is in the %r state; '
@@ -255,13 +270,13 @@ def start(self):
self.log('Bus STARTED')
except (KeyboardInterrupt, SystemExit):
raise
- except:
+ except Exception:
self.log('Shutting down due to error in start listener:',
level=40, traceback=True)
e_info = sys.exc_info()[1]
try:
self.exit()
- except:
+ except Exception:
# Any stop/exit errors will be logged inside publish().
pass
# Re-raise the original error
@@ -280,7 +295,7 @@ def exit(self):
# This isn't strictly necessary, but it's better than seeing
# "Waiting for child threads to terminate..." and then nothing.
self.log('Bus EXITED')
- except:
+ except Exception:
# This method is often called asynchronously (whether thread,
# signal handler, console handler, or atexit handler), so we
# can't just let exceptions propagate out unhandled.
@@ -343,7 +358,8 @@ def block(self, interval=0.1):
if (
t != threading.currentThread() and
not isinstance(t, threading._MainThread) and
- # Note that any dummy (external) threads are always daemonic.
+ # Note that any dummy (external) threads are
+ # always daemonic.
not t.daemon
):
self.log('Waiting for thread %s.' % t.getName())
@@ -359,23 +375,9 @@ def wait(self, state, interval=0.1, channel=None):
else:
states = [state]
- def _wait():
- while self.state not in states:
- time.sleep(interval)
- self.publish(channel)
-
- # From http://psyco.sourceforge.net/psycoguide/bugs.html:
- # "The compiled machine code does not include the regular polling
- # done by Python, meaning that a KeyboardInterrupt will not be
- # detected before execution comes back to the regular Python
- # interpreter. Your program cannot be interrupted if caught
- # into an infinite Psyco-compiled loop."
- try:
- sys.modules['psyco'].cannotcompile(_wait)
- except (KeyError, AttributeError):
- pass
-
- _wait()
+ while self.state not in states:
+ time.sleep(interval)
+ self.publish(channel)
def _do_execv(self):
"""Re-execute the current process.
@@ -407,7 +409,7 @@ def _do_execv(self):
@staticmethod
def _get_interpreter_argv():
- """Retrieve current Python interpreter's arguments
+ """Retrieve current Python interpreter's arguments.
Returns empty tuple in case of frozen mode, uses built-in arguments
reproduction function otherwise.
@@ -421,11 +423,11 @@ def _get_interpreter_argv():
"""
return ([]
if getattr(sys, 'frozen', False)
- else _args_from_interpreter_flags())
+ else subprocess._args_from_interpreter_flags())
@staticmethod
def _get_true_argv():
- """Retrieves all real arguments of the python interpreter
+ """Retrieve all real arguments of the python interpreter.
...even those not listed in ``sys.argv``
@@ -433,14 +435,16 @@ def _get_true_argv():
:seealso: http://stackoverflow.com/a/6683222/595220
:seealso: http://stackoverflow.com/a/28414807/595220
"""
-
try:
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
argv = ctypes.POINTER(char_p)()
argc = ctypes.c_int()
- ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(argc), ctypes.byref(argv))
+ ctypes.pythonapi.Py_GetArgcArgv(
+ ctypes.byref(argc),
+ ctypes.byref(argv),
+ )
_argv = argv[:argc.value]
@@ -465,7 +469,7 @@ def _get_true_argv():
try:
c_ind = _argv.index('-c')
- if m_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
+ if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
is_command = True
except (IndexError, ValueError):
c_ind = None
@@ -500,7 +504,7 @@ def _get_true_argv():
:seealso: https://github.com/cherrypy/cherrypy/issues/1506
:seealso: https://github.com/cherrypy/cherrypy/issues/1512
- :ref: https://chromium.googlesource.com/infra/infra/+/69eb0279c12bcede5937ce9298020dd4581e38dd%5E!/
+ :ref: http://bit.ly/2gK6bXK
"""
raise NotImplementedError
else:
@@ -508,7 +512,8 @@ def _get_true_argv():
@staticmethod
def _extend_pythonpath(env):
- """
+ """Prepend current working dir to PATH environment variable if needed.
+
If sys.path[0] is an empty string, the interpreter was likely
invoked with -m and the effective path is about to change on
re-exec. Add the current directory to $PYTHONPATH to ensure
@@ -581,4 +586,5 @@ def log(self, msg='', level=20, traceback=False):
msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
self.publish('log', msg, level)
-bus = Bus() # noqa: E305
+
+bus = Bus()
diff --git a/lib/cherrypy/scaffold/__init__.py b/lib/cherrypy/scaffold/__init__.py
index 52b40b3..bcddba2 100644
--- a/lib/cherrypy/scaffold/__init__.py
+++ b/lib/cherrypy/scaffold/__init__.py
@@ -21,9 +21,11 @@
@cherrypy.config(**{'tools.log_tracebacks.on': True})
class Root:
+ """Declaration of the CherryPy app URI structure."""
@cherrypy.expose
def index(self):
+ """Render HTML-template at the root path of the web-app."""
return """
Try some other path,
or a default path.
@@ -34,10 +36,12 @@ def index(self):
@cherrypy.expose
def default(self, *args, **kwargs):
+ """Render catch-all args and kwargs."""
return 'args: %s kwargs: %s' % (args, kwargs)
@cherrypy.expose
def other(self, a=2, b='bananas', c=None):
+ """Render number of fruits based on third argument."""
cherrypy.response.headers['Content-Type'] = 'text/plain'
if c is None:
return 'Have %d %s.' % (int(a), b)
diff --git a/lib/cherrypy/scaffold/apache-fcgi.conf b/lib/cherrypy/scaffold/apache-fcgi.conf
index 922398e..6e4f144 100644
--- a/lib/cherrypy/scaffold/apache-fcgi.conf
+++ b/lib/cherrypy/scaffold/apache-fcgi.conf
@@ -19,4 +19,4 @@ RewriteRule ^(.*)$ /fastcgi.pyc [L]
# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot.
# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this
# filename will be handled by this external FastCGI application.
-FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088
\ No newline at end of file
+FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088
diff --git a/lib/cherrypy/scaffold/example.conf b/lib/cherrypy/scaffold/example.conf
index 93a6e53..63250fe 100644
--- a/lib/cherrypy/scaffold/example.conf
+++ b/lib/cherrypy/scaffold/example.conf
@@ -1,3 +1,3 @@
[/]
log.error_file: "error.log"
-log.access_file: "access.log"
\ No newline at end of file
+log.access_file: "access.log"
diff --git a/lib/cherrypy/scaffold/static/made_with_cherrypy_small.png b/lib/cherrypy/scaffold/static/made_with_cherrypy_small.png
index c3aafeed952190f5da9982bb359aa75b107ff079..724f9d72d9ca5aede0b788fc1216286aa967e212 100644
GIT binary patch
literal 6347
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
N002ovPDHLkV1nY?RmA`R
literal 7455
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'),
- esc('') + ntob('(.*)') + esc('
'))
+ esc('') + b'(.*)' + esc('
'))
m = re.match(epage, self.body, re.DOTALL)
if not m:
self._handlewebError(
@@ -387,7 +390,9 @@ def _test_method_sorter(_, x, y):
if x < y:
return -1
return 0
-unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter # noqa: E305
+
+
+unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter
def setup_client():
@@ -452,11 +457,11 @@ def start(self, imports=None):
args = [
'-m',
- 'cherrypy.__main__', # __main__ is needed for `-m` in Python 2.6
+ 'cherrypy',
'-c', self.config_file,
'-p', self.pid_file,
]
- """
+ r"""
Command for running cherryd server with autoreload enabled
Using
diff --git a/lib/cherrypy/test/logtest.py b/lib/cherrypy/test/logtest.py
index 5361cfb..ed8f154 100644
--- a/lib/cherrypy/test/logtest.py
+++ b/lib/cherrypy/test/logtest.py
@@ -2,6 +2,7 @@
import sys
import time
+from uuid import UUID
import six
@@ -46,7 +47,7 @@ class LogCase(object):
logfile = None
lastmarker = None
- markerPrefix = ntob('test suite marker: ')
+ markerPrefix = b'test suite marker: '
def _handleLogError(self, msg, data, marker, pattern):
print('')
@@ -161,6 +162,33 @@ def assertNotInLog(self, line, marker=None):
msg = '%r found in log' % line
self._handleLogError(msg, data, marker, line)
+ def assertValidUUIDv4(self, marker=None):
+ """Fail if the given UUIDv4 is not valid.
+
+ The log will be searched from the given marker to the next marker.
+ If marker is None, self.lastmarker is used. If the log hasn't
+ been marked (using self.markLog), the entire log will be searched.
+ """
+ data = self._read_marked_region(marker)
+ data = [
+ chunk.decode('utf-8').rstrip('\n').rstrip('\r')
+ for chunk in data
+ ]
+ for log_chunk in data:
+ try:
+ uuid_log = data[-1]
+ uuid_obj = UUID(uuid_log, version=4)
+ except (TypeError, ValueError):
+ pass # it might be in other chunk
+ else:
+ if str(uuid_obj) == uuid_log:
+ return
+ msg = '%r is not a valid UUIDv4' % uuid_log
+ self._handleLogError(msg, data, marker, log_chunk)
+
+ msg = 'UUIDv4 not found in log'
+ self._handleLogError(msg, data, marker, log_chunk)
+
def assertLog(self, sliceargs, lines, marker=None):
"""Fail if log.readlines()[sliceargs] is not contained in 'lines'.
diff --git a/lib/cherrypy/test/modpy.py b/lib/cherrypy/test/modpy.py
index 6da9c53..7c288d2 100644
--- a/lib/cherrypy/test/modpy.py
+++ b/lib/cherrypy/test/modpy.py
@@ -37,6 +37,7 @@
import os
import re
+import cherrypy
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
@@ -132,7 +133,6 @@ def wsgisetup(req):
loaded = True
options = req.get_options()
- import cherrypy
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.log'),
'environment': 'test_suite',
@@ -155,7 +155,6 @@ def cpmodpysetup(req):
loaded = True
options = req.get_options()
- import cherrypy
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.log'),
'environment': 'test_suite',
diff --git a/lib/cherrypy/test/modwsgi.py b/lib/cherrypy/test/modwsgi.py
index cd40ad5..f558e22 100644
--- a/lib/cherrypy/test/modwsgi.py
+++ b/lib/cherrypy/test/modwsgi.py
@@ -39,7 +39,10 @@
import portend
-from cherrypy.test import helper, webtest
+from cheroot.test import webtest
+
+import cherrypy
+from cherrypy.test import helper
curdir = os.path.abspath(os.path.dirname(__file__))
@@ -87,7 +90,7 @@ def read_process(cmd, args=''):
WSGIScriptAlias / "%(curdir)s/modwsgi.py"
SetEnv testmod %(testmod)s
-"""
+""" # noqa E501
class ModWSGISupervisor(helper.Supervisor):
@@ -134,7 +137,6 @@ def stop(self):
def application(environ, start_response):
- import cherrypy
global loaded
if not loaded:
loaded = True
diff --git a/lib/cherrypy/test/sessiondemo.py b/lib/cherrypy/test/sessiondemo.py
old mode 100644
new mode 100755
index 2aac468..8226c1b
--- a/lib/cherrypy/test/sessiondemo.py
+++ b/lib/cherrypy/test/sessiondemo.py
@@ -4,9 +4,11 @@
import calendar
from datetime import datetime
import sys
+
+import six
+
import cherrypy
from cherrypy.lib import sessions
-from cherrypy._cpcompat import copyitems
page = """
@@ -93,7 +95,7 @@
Python Version: | %(pyversion)s |
-"""
+""" # noqa E501
class Root(object):
@@ -121,7 +123,7 @@ def page(self):
'changemsg': '
'.join(changemsg),
'respcookie': cherrypy.response.cookie.output(),
'reqcookie': cherrypy.request.cookie.output(),
- 'sessiondata': copyitems(cherrypy.session),
+ 'sessiondata': list(six.iteritems(cherrypy.session)),
'servertime': (
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
),
diff --git a/lib/cherrypy/test/static/dirback.jpg b/lib/cherrypy/test/static/dirback.jpg
index 530e6d6a386fc097f3a1dbabbde2d80fec1175ac..80403dc227c19b9192158420f5288121e7f2669a 100644
GIT binary patch
literal 16585
zcmbt*XHZjJ)b2?k2_^L2jDScNBy`k7T0p=M0wRb+K@7cviX{oX3JM58C3F%%kX|fw
z5dlLN5D*kZq^Jnkxp}{D=Ki>U@6JpnIg{CEPS#%Kd7icQ-|W8w1Z^y>EddY+06?4<
zus;i!0bE@0|F5;*2?!xTykIW~AOwJf0I(3S-viJ900R2&M*nw#zz_h6;9UD}{oktq
z00u#zT!H`y0)}wG!7$D@Fz1&b&OQfqgcY!mqbD6Q2^TJggi;Ide4!Q9b&W1TMHF?-
z9(Rs-eshfJ9$OUpe|F?Noe+3`2H=DI&nF?^1n~Vu^T3ZI_>QeL%Ggu5I;IpTZh4}j
zErN>A74Ka=3t6Y@@U`te4t|Oc`i2Y_T}~n>kkZEjrY%(L_%GbVO&pJvv{SDqa4YBK
z5&2%mC`qp}RUW)|Ubt6?15(cfiRZQa;fbHlOlr-)`MS7++VnzaFzA)aK&`Fen9kDQ
z+kcsb)T3c1K7A#*YP<|j4a$Eh>3NhV-oxY(Ee%wkoA=S_|q6=&SAAFi>&SkYW|
zrdLsciEwk?#yPza$wULmwArF;OOty)zC5``q!HsK=ImsZETR_7=vK
zVTOAc<6>7qpKTW_6uj=Ui3bn8qQ_FrB%!CQVbdPERX=rF^iU0(c-cn;P
zcU`#>PCOtGMofiNqAW9Q%R(<9&d5R#zmrxKX~;$cBEJFm)5*=
zw1-rLfYxcWDUlB`;TZh{)o*P&QW>O-&`(Q*3TUcAS=rY`3(Y#z@YAnx1jm^m>D$4@
zrP@$Idzo|44~BH2CT`_};t)v0L(f*obO7mYeHOO@3|>%3pRgldM3UdPl{Nf6df814
zG#pYUED(yP6i8U0E*{%muM+Ly>!g{gQ3b$eshMwC4tQ_Ibyn78`K@e?#C@RkZK=gR
z5LyRf=~zlg^+2Q41g{vT@%eC_q~P*kFG0{W*;?RqN)fq9Wafu+4$_pZKI3UYJM?J9
zSQV
SJEmU<`i+7emFb-^G~Of*#XN-M*um9BSD8Hdrd}
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
Sse&CiQ+RTS)_Y=q`u_k!0j`Vy
literal 18238
zcmb5VRajfk7cQI-+=Dy8ixwxi1qsEiMSnO1cPq5GyESOh;1mg3+T!jWq{RyqC|W9%
z9{%UzeAnOF&)yfapS5P)tasMT`_8|$f7<|ZEp@m$00;yEG#?+pzYTyY00)GPjSa$i
z{NUi=;NlVE<2@P~5fK3~n2dq~Oa=y1(lF6dQZZ12!E|hN49v`|tgMu@?40Z@oJ=gN
zEdL7ve00Ub#UsVXCuN}mQ?dO2wtsy9Fg}n7%K-#r2VjALATaRX5P$&y06iuP1pGe(
zVu7#$IQWnM6vzQU5EcmcF>P!tAT|gH06sc`*eFDl*+mU(eKN5rBg;6%R1EF1TKac<
zqvkJEji@-q%P;Mt2NoXv>HlwF(Ep1J_@6%r8|Q!1g8w(?W5oZ@fM7NeWvqYe0OH5t
z$7#R-MZj-6JTiY9Z~0Z+2Qq&pR9Op8Meoo~Tq;rKtSEkl_E3tu
zCM=B{oR-z5Uw`_JYl%ol9qWg4hL^5pGP6&Xaz}Gvz%!&qN_iet7@9s1g4FTs1rcyp^&2O1O0JGrrK!FN<9$
zFaN_y?{ufh+PM3#bOK!}lwQ%FTrC@6@S*hL$gzbk@vk&_6TzV$=9zSvcPx`)g%Z`E
zw9nPZ=kj`vMTrfHVN!y#1WYKUF
zj4(lda${I45pXf3jR)GbdcD}^(3KblfRCV(EVb~COmu5+WNPHI7FXXir>o9V{x%&h(ax_sg_y%QzJ%}NlS&ks%Lf4jKsQ;u@iEV@l;JHO;12~U$u
zS8INEphU1F>2A?L_7C%!<@oa5kGCwly%>COoL_!2jz`37iim*OwT)OV>Sz4}G;A2-3kO>J>M0ck4hhyxomHl^Z!F`Ql|O5-8UaRe
zh~M(bA+I*cI3s87UzM2vUV604T~t5ye3GYzh96Rv2)q+C9$^-?Ngt>+NEQ3h;-~g(
z7fU=U+6^~rHJ>A^#y=6X<*J!rZnL3d8o1z3jyR@o^x_u1GhhjsC7e!TJ5cq|v*tsa
zn=^TX2flrjqj%F;=ItpfveDqu%>OTOM$%6!)LpFiwB5>Bc3M^_6;g`99t(UXJ*kCT
z6#duMU-tVaSgK#-R)}&F=il(`T5ay2(`aR^a~uUcnEemS71L!hnq@~DLF{SyElQY}xo`%f_Ce-h{r1KsFmBDo`OeTCuq)R}Kp$Xyk$p%c
zj6Zh+moU?da$7i|)BbMeOAYq7gJUlPtKDMwB-=S4A}iSYSfK%Qt~&$Dg_PeOsFL?s
z%q@Z>mAfeWb&FpNotm)5SZYYEDN~Aa?z;imra+p#hk@>;Pe>=>LSLAP6ec9?
znF4Xe3{5glCE?9@wwet@^s>6O@7%R#TIv755ey5OJXj+?3b=9%baafY771!*R
z4A>7Xw{5y~cPm=3DaIZsME0rEw51{LUlcT8++weVQ2Ra>Ttm$V5zuzuPwbPw2NqpYX%ogL^;4(E1nQAGG5_@~6MmV_F
zRp2+(gViWH57=?pVG=V-EKB1iD%y<8FJBD4_LGdOT?m#O!aU^a{~90h+Zq#8j$#Hd
zIeh*BqriVtf~in?Gd`qwNUbE7SGmLUVpxhkEy+&|z2j4D5`^v|XJ*=Yn)Ce+{y2@R
z@x7G3
zwHUZvl7P~uOMVv}p-rz3kJC*oOi0mz{N|>c(j*vmF+@Zs0K+MFfV!**Q1QHrzX#D3
zFCIYGRl!1(^v(q9cS9esMUh7$)!;AfD3QWz9o-TT+fYqQ^I*#>Y}7>s34CB1A2w2S
z*IT%tkjQ6;>)mSssoe!U-SX*(6_kHN@+R3e8H^+lOI+ElnFk4nR+32%zURZ!EXt1R
z8r`wfL92(vDDp+Zh3%+BvTvh32}h&__BVnVnZY;ny@N4l1nS9~K2|6w=XlIy=zte7
zC^-gw1M974G|CA@*#7a8>Y2{1Sag^kTNFR&NYOQ_V4IJ_
zqcE8@sTnt41M}2BxHJ7Lryj*oWm#tSM6L
z3(>Db0IKlagp|@CB_eIf<7hqH(!9zLPYBS%*DOt!QcsAPrXV6ow@s~$58Qf&a?J5l
zQzL(&Hz@jgqB9!&T=Jm0Jc9!mt)0lKchR_mAE2V_e&Jdkl%xn=4MtnsTJpQk;3=h&
zUqX9#chdSCmt_%hx>(3%?)US~`cnPPKN_#mmK7IsgymP;W
zHV!bFmDjFQ%$Ikc16$1pNnf`jOHU20Pru&&1Mm$m$};J)Tj@tEawb32-is9M?HaTD
za@6@K_pv58aoiYJ!Q$g--`@QLptqNL)wq+|I-KBX$7+&sNRMX;t)0nEGL4=~Ok+y=
z&R>jrglk6ax=Cr5`1GU#4X^S6OzMF46wlD!*RPn@DzB7Q)
zo~<*h)q+K)qSp3OMW58!lar(>evLs691*Yg)e?H7WF-W|-@^?>szDv}&{yY0%W}dj
z!6bvW!<22POOJw7;RWej84qp(s{YJOlS|lcqC4!2)2*r$x~P+F78kGX|2;o9usw<>
zTD5gr6YNa{d2ywS%R5AWk7B4n4SLJZUu~S`b}4~rw+*nt2zkOenr3kTK}7ViomfIN
z>-UqlJ+WpZftDWCb$oFFwDy_$L;HrU*~3cOP?DDr%dkm?Old^9xrC+lmn2POl-M{E
zO}s;!AtTEp9K6jmLmZr#oTQ{z>&b-WQPDMD+B^je`LSg2!W
zUAVnN#^&fx9VPP1X0;5LWgqHF;cTYogz3`F$rWaix1Jm7zVX@G;TVo5QpjH^TtUch
zUV)kjf{d>*Gbmz@E_2FUcc#~;Lf6=%r`?T%x0K7`X>4ZtvVTlNG6K;uf;cNv0%rjg
z^kYZ$#6pl4@L+|+8n{qA$juC}t^6ioVZ6qxFd>bIvbEUMct}LLx(-_zmtNk;hKmkJdctlDyvCA!{0dt~6?;CmK
zTNj9u@=$Ux
zyU*i2&Jv_1E*cx>h5Ks;wN7lla;I6EdidtV@5M67?|HUf(&JQ+s10pjU=hR(r~GO9
zX~wPW0X0rHANz&m&!@etcd>3^J5-j2ihx5?XyHXa;yyZ~og@f*kzIC*p)*@mg@s}(bvE3XM|4c?(#A!q>
zNMDH^*Q3hwySpw}TfM6Y;5*ByJN6SPp>sTCL2fy(fgSPLF(@h8kh?@>W*RrFEi~NL
zZH1aWXPl%vSIJCTGPD+;>r}J>yM&PQ&Yq%mg*78&Q6wbfjG(#<02r$z%mj8KF^A#_
zmDXx=D1O5j)M7fB9>6yM$8cI$YyLh~0blmVr@n)sQ&v3ZrC+2ajw$#LUhigQe-y*z
z@#iQXt*ZScuykkoX1{Im*Q@H==Veifx1@^rmYZ)6T&s1qvAmO&0Rn?xv%*@?UQ*_&
z9S*&g>l2Zt!qL!({Tc?FFvCg$lBmCaB-*3qB0XQB7DyA9Xz1bi2bByN``3;J%TL-n
zNoLDaGFG3xMRu7s*v7m3=B0;hVUq9Ebc<8(0&Yyode6#3f7epBVD~sH(CF=qXL080
zu4FZq*o+$R7W~-Zb`rBP*=hRFU6$g2|CLSi%+EpXK(RbcGX6jORW7BkP$o_@!-`Pq
zb%&P~uZUV$2y_+dp>DB*&~MAuA=D5zyQq2m78P7_=8N5R4z)@X8t;?hclEgDJW8oN
zOG4`|AhjYkBZc+sO`(S&E5
z;#pQJl=`A)S9?q}&W_6rIx6&WgZf>D-eBUAgttXI?y=cSR|G@)>!}GO+BiB&R-h?KaKHwmM5~I@)>=iXaP|-Djy$12wGnv;PfWuUcKkCALBF2rC(0^E2LL(4hedCIyQ}K)-HE$0HwP
zkSKbzh-VVrvglS&;Uq+St0}V1oAtWZAmR(9-V@#k+z^&;9609_cB3Z;%WeR&$~&{d
zc+x%GarD)~NgYT8H=pVkyx63d`)4z9abBm?MtB9p(d1e}v5Zg0E`9?$ooiZ^X&H>@
zqff3lSi0(?1C|if{VH)6!!<_@8olA_u+mA(vS!fDXrP;O@1u&SA-R|c4tpVCKqu1D
z1?~iGc~9*AeeNtOxXe2TWjQf1Cy?b_bFDvmS+j!Oy+I*Uajeo6W_S9Ob{6)L?hI{T
z8_q;2{fTOliTIG#pxr6ztH0R|TG7AZZ0nX^^-gPMdhV?%<*SfiHgCW~hwY--7kz>>
zAG#azjeT%4*hYRgiWhwaT>?8&58JTL5P$lPip0MesKx9QIs6w@VU_HlaM|zvmf$VY
zXewM8;%RtgE8i7UZjyq%ds^)>jY&(&eP!0`Z-n%`Zo{u>YqR@qowOum<@@@NA!pq8
zE_#{&xXc-6;%Ng6{YsZ2Lf9gvi)6X#Y|l5?F)D)VH2(SBv7xC^niHFuHh#exoFF~w
z{&z(i7%MKGEbL+|(|D4jn1Jyo&RG$4yGJ{3pE5OoB{d+-kF!8#-;eRc;IhY-0iV-Mt<--dW4M#(o2pKxrpodD*#4_^XCk@Bk+5lTaj=auGBh(g4$6UYJS
z6;S&pZr#UhTRCF+Z3W?4N_@arr{Idv3trI*OB>jYo~NK{eL>H?IeOf!&vDX%)1_2*
z<`+F8H@BYhAcBceXo}a4y@(;^OFXB6+Qc)8n*V>moh