mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-07 05:31:15 -07:00
Update cherrypy-18.6.1
This commit is contained in:
parent
b3ae6bd695
commit
ebffd124f6
57 changed files with 1269 additions and 1509 deletions
|
@ -1,9 +1,7 @@
|
||||||
"""Checker for CherryPy sites and mounted apps."""
|
"""Checker for CherryPy sites and mounted apps."""
|
||||||
import os
|
import os
|
||||||
import warnings
|
import warnings
|
||||||
|
import builtins
|
||||||
import six
|
|
||||||
from six.moves import builtins
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
|
@ -70,14 +68,14 @@ class Checker(object):
|
||||||
|
|
||||||
def check_site_config_entries_in_app_config(self):
|
def check_site_config_entries_in_app_config(self):
|
||||||
"""Check for mounted Applications that have site-scoped config."""
|
"""Check for mounted Applications that have site-scoped config."""
|
||||||
for sn, app in six.iteritems(cherrypy.tree.apps):
|
for sn, app in cherrypy.tree.apps.items():
|
||||||
if not isinstance(app, cherrypy.Application):
|
if not isinstance(app, cherrypy.Application):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg = []
|
msg = []
|
||||||
for section, entries in six.iteritems(app.config):
|
for section, entries in app.config.items():
|
||||||
if section.startswith('/'):
|
if section.startswith('/'):
|
||||||
for key, value in six.iteritems(entries):
|
for key, value in entries.items():
|
||||||
for n in ('engine.', 'server.', 'tree.', 'checker.'):
|
for n in ('engine.', 'server.', 'tree.', 'checker.'):
|
||||||
if key.startswith(n):
|
if key.startswith(n):
|
||||||
msg.append('[%s] %s = %s' %
|
msg.append('[%s] %s = %s' %
|
||||||
|
|
|
@ -18,74 +18,33 @@ Instead, use unicode literals (from __future__) and bytes literals
|
||||||
and their .encode/.decode methods as needed.
|
and their .encode/.decode methods as needed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import http.client
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import six
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
|
|
||||||
if six.PY3:
|
def ntob(n, encoding='ISO-8859-1'):
|
||||||
def ntob(n, encoding='ISO-8859-1'):
|
"""Return the given native string as a byte string in the given
|
||||||
"""Return the given native string as a byte string in the given
|
encoding.
|
||||||
encoding.
|
"""
|
||||||
"""
|
assert_native(n)
|
||||||
assert_native(n)
|
# In Python 3, the native string type is unicode
|
||||||
# In Python 3, the native string type is unicode
|
return n.encode(encoding)
|
||||||
return n.encode(encoding)
|
|
||||||
|
|
||||||
def ntou(n, encoding='ISO-8859-1'):
|
|
||||||
"""Return the given native string as a unicode string with the given
|
|
||||||
encoding.
|
|
||||||
"""
|
|
||||||
assert_native(n)
|
|
||||||
# In Python 3, the native string type is unicode
|
|
||||||
return n
|
|
||||||
|
|
||||||
def tonative(n, encoding='ISO-8859-1'):
|
def ntou(n, encoding='ISO-8859-1'):
|
||||||
"""Return the given string as a native string in the given encoding."""
|
"""Return the given native string as a unicode string with the given
|
||||||
# In Python 3, the native string type is unicode
|
encoding.
|
||||||
if isinstance(n, bytes):
|
"""
|
||||||
return n.decode(encoding)
|
assert_native(n)
|
||||||
return n
|
# In Python 3, the native string type is unicode
|
||||||
else:
|
return n
|
||||||
# Python 2
|
|
||||||
def ntob(n, encoding='ISO-8859-1'):
|
|
||||||
"""Return the given native string as a byte string in the given
|
|
||||||
encoding.
|
|
||||||
"""
|
|
||||||
assert_native(n)
|
|
||||||
# In Python 2, the native string type is bytes. Assume it's already
|
|
||||||
# in the given encoding, which for ISO-8859-1 is almost always what
|
|
||||||
# was intended.
|
|
||||||
return n
|
|
||||||
|
|
||||||
def ntou(n, encoding='ISO-8859-1'):
|
|
||||||
"""Return the given native string as a unicode string with the given
|
def tonative(n, encoding='ISO-8859-1'):
|
||||||
encoding.
|
"""Return the given string as a native string in the given encoding."""
|
||||||
"""
|
# In Python 3, the native string type is unicode
|
||||||
assert_native(n)
|
if isinstance(n, bytes):
|
||||||
# In Python 2, the native string type is bytes.
|
|
||||||
# First, check for the special encoding 'escape'. The test suite uses
|
|
||||||
# this to signal that it wants to pass a string with embedded \uXXXX
|
|
||||||
# escapes, but without having to prefix it with u'' for Python 2,
|
|
||||||
# but no prefix for Python 3.
|
|
||||||
if encoding == 'escape':
|
|
||||||
return six.text_type( # unicode for Python 2
|
|
||||||
re.sub(r'\\u([0-9a-zA-Z]{4})',
|
|
||||||
lambda m: six.unichr(int(m.group(1), 16)),
|
|
||||||
n.decode('ISO-8859-1')))
|
|
||||||
# Assume it's already in the given encoding, which for ISO-8859-1
|
|
||||||
# is almost always what was intended.
|
|
||||||
return n.decode(encoding)
|
return n.decode(encoding)
|
||||||
|
return n
|
||||||
def tonative(n, encoding='ISO-8859-1'):
|
|
||||||
"""Return the given string as a native string in the given encoding."""
|
|
||||||
# In Python 2, the native string type is bytes.
|
|
||||||
if isinstance(n, six.text_type): # unicode for Python 2
|
|
||||||
return n.encode(encoding)
|
|
||||||
return n
|
|
||||||
|
|
||||||
|
|
||||||
def assert_native(n):
|
def assert_native(n):
|
||||||
|
@ -94,69 +53,7 @@ def assert_native(n):
|
||||||
|
|
||||||
|
|
||||||
# Some platforms don't expose HTTPSConnection, so handle it separately
|
# Some platforms don't expose HTTPSConnection, so handle it separately
|
||||||
HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None)
|
HTTPSConnection = getattr(http.client, 'HTTPSConnection', None)
|
||||||
|
|
||||||
|
|
||||||
def _unquote_plus_compat(string, encoding='utf-8', errors='replace'):
|
text_or_bytes = str, bytes
|
||||||
return urllib.parse.unquote_plus(string).decode(encoding, errors)
|
|
||||||
|
|
||||||
|
|
||||||
def _unquote_compat(string, encoding='utf-8', errors='replace'):
|
|
||||||
return urllib.parse.unquote(string).decode(encoding, errors)
|
|
||||||
|
|
||||||
|
|
||||||
def _quote_compat(string, encoding='utf-8', errors='replace'):
|
|
||||||
return urllib.parse.quote(string.encode(encoding, errors))
|
|
||||||
|
|
||||||
|
|
||||||
unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat
|
|
||||||
unquote = urllib.parse.unquote if six.PY3 else _unquote_compat
|
|
||||||
quote = urllib.parse.quote if six.PY3 else _quote_compat
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Prefer simplejson
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
json_decode = json.JSONDecoder().decode
|
|
||||||
_json_encode = json.JSONEncoder().iterencode
|
|
||||||
|
|
||||||
|
|
||||||
if six.PY3:
|
|
||||||
# Encode to bytes on Python 3
|
|
||||||
def json_encode(value):
|
|
||||||
for chunk in _json_encode(value):
|
|
||||||
yield chunk.encode('utf-8')
|
|
||||||
else:
|
|
||||||
json_encode = _json_encode
|
|
||||||
|
|
||||||
|
|
||||||
text_or_bytes = six.text_type, bytes
|
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 3):
|
|
||||||
Timer = threading.Timer
|
|
||||||
Event = threading.Event
|
|
||||||
else:
|
|
||||||
# Python 3.2 and earlier
|
|
||||||
Timer = threading._Timer
|
|
||||||
Event = threading._Event
|
|
||||||
|
|
||||||
# html module come in 3.2 version
|
|
||||||
try:
|
|
||||||
from html import escape
|
|
||||||
except ImportError:
|
|
||||||
from cgi import escape
|
|
||||||
|
|
||||||
|
|
||||||
# html module needed the argument quote=False because in cgi the default
|
|
||||||
# is False. With quote=True the results differ.
|
|
||||||
|
|
||||||
def escape_html(s, escape_quote=False):
|
|
||||||
"""Replace special characters "&", "<" and ">" to HTML-safe sequences.
|
|
||||||
|
|
||||||
When escape_quote=True, escape (') and (") chars.
|
|
||||||
"""
|
|
||||||
return escape(s, quote=escape_quote)
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ user:
|
||||||
POST should not raise this error
|
POST should not raise this error
|
||||||
305 Use Proxy Confirm with the user
|
305 Use Proxy Confirm with the user
|
||||||
307 Temporary Redirect Confirm with the user
|
307 Temporary Redirect Confirm with the user
|
||||||
|
308 Permanent Redirect No confirmation
|
||||||
===== ================================= ===========
|
===== ================================= ===========
|
||||||
|
|
||||||
However, browsers have historically implemented these restrictions poorly;
|
However, browsers have historically implemented these restrictions poorly;
|
||||||
|
@ -119,17 +120,15 @@ and not simply return an error message as a result.
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import urllib.parse
|
||||||
from sys import exc_info as _exc_info
|
from sys import exc_info as _exc_info
|
||||||
from traceback import format_exception as _format_exception
|
from traceback import format_exception as _format_exception
|
||||||
from xml.sax import saxutils
|
from xml.sax import saxutils
|
||||||
|
import html
|
||||||
import six
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
from more_itertools import always_iterable
|
from more_itertools import always_iterable
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import escape_html
|
|
||||||
from cherrypy._cpcompat import ntob
|
from cherrypy._cpcompat import ntob
|
||||||
from cherrypy._cpcompat import tonative
|
from cherrypy._cpcompat import tonative
|
||||||
from cherrypy._helper import classproperty
|
from cherrypy._helper import classproperty
|
||||||
|
@ -256,7 +255,7 @@ class HTTPRedirect(CherryPyException):
|
||||||
response = cherrypy.serving.response
|
response = cherrypy.serving.response
|
||||||
response.status = status = self.status
|
response.status = status = self.status
|
||||||
|
|
||||||
if status in (300, 301, 302, 303, 307):
|
if status in (300, 301, 302, 303, 307, 308):
|
||||||
response.headers['Content-Type'] = 'text/html;charset=utf-8'
|
response.headers['Content-Type'] = 'text/html;charset=utf-8'
|
||||||
# "The ... URI SHOULD be given by the Location field
|
# "The ... URI SHOULD be given by the Location field
|
||||||
# in the response."
|
# in the response."
|
||||||
|
@ -271,10 +270,11 @@ class HTTPRedirect(CherryPyException):
|
||||||
302: 'This resource resides temporarily at ',
|
302: 'This resource resides temporarily at ',
|
||||||
303: 'This resource can be found at ',
|
303: 'This resource can be found at ',
|
||||||
307: 'This resource has moved temporarily to ',
|
307: 'This resource has moved temporarily to ',
|
||||||
|
308: 'This resource has been moved to ',
|
||||||
}[status]
|
}[status]
|
||||||
msg += '<a href=%s>%s</a>.'
|
msg += '<a href=%s>%s</a>.'
|
||||||
msgs = [
|
msgs = [
|
||||||
msg % (saxutils.quoteattr(u), escape_html(u))
|
msg % (saxutils.quoteattr(u), html.escape(u, quote=False))
|
||||||
for u in self.urls
|
for u in self.urls
|
||||||
]
|
]
|
||||||
response.body = ntob('<br />\n'.join(msgs), 'utf-8')
|
response.body = ntob('<br />\n'.join(msgs), 'utf-8')
|
||||||
|
@ -496,11 +496,11 @@ def get_error_page(status, **kwargs):
|
||||||
if kwargs.get('version') is None:
|
if kwargs.get('version') is None:
|
||||||
kwargs['version'] = cherrypy.__version__
|
kwargs['version'] = cherrypy.__version__
|
||||||
|
|
||||||
for k, v in six.iteritems(kwargs):
|
for k, v in kwargs.items():
|
||||||
if v is None:
|
if v is None:
|
||||||
kwargs[k] = ''
|
kwargs[k] = ''
|
||||||
else:
|
else:
|
||||||
kwargs[k] = escape_html(kwargs[k])
|
kwargs[k] = html.escape(kwargs[k], quote=False)
|
||||||
|
|
||||||
# Use a custom template or callable for the error page?
|
# Use a custom template or callable for the error page?
|
||||||
pages = cherrypy.serving.request.error_page
|
pages = cherrypy.serving.request.error_page
|
||||||
|
@ -520,13 +520,13 @@ def get_error_page(status, **kwargs):
|
||||||
if cherrypy.lib.is_iterator(result):
|
if cherrypy.lib.is_iterator(result):
|
||||||
from cherrypy.lib.encoding import UTF8StreamEncoder
|
from cherrypy.lib.encoding import UTF8StreamEncoder
|
||||||
return UTF8StreamEncoder(result)
|
return UTF8StreamEncoder(result)
|
||||||
elif isinstance(result, six.text_type):
|
elif isinstance(result, str):
|
||||||
return result.encode('utf-8')
|
return result.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
if not isinstance(result, bytes):
|
if not isinstance(result, bytes):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'error page function did not '
|
'error page function did not '
|
||||||
'return a bytestring, six.text_type or an '
|
'return a bytestring, str or an '
|
||||||
'iterator - returned object of type %s.'
|
'iterator - returned object of type %s.'
|
||||||
% (type(result).__name__))
|
% (type(result).__name__))
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -113,8 +113,6 @@ import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy import _cperror
|
from cherrypy import _cperror
|
||||||
|
|
||||||
|
@ -155,11 +153,7 @@ class LogManager(object):
|
||||||
access_log = None
|
access_log = None
|
||||||
"""The actual :class:`logging.Logger` instance for access messages."""
|
"""The actual :class:`logging.Logger` instance for access messages."""
|
||||||
|
|
||||||
access_log_format = (
|
access_log_format = '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
||||||
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
|
||||||
if six.PY3 else
|
|
||||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
|
||||||
)
|
|
||||||
|
|
||||||
logger_root = None
|
logger_root = None
|
||||||
"""The "top-level" logger name.
|
"""The "top-level" logger name.
|
||||||
|
@ -254,8 +248,7 @@ class LogManager(object):
|
||||||
status = '-'
|
status = '-'
|
||||||
else:
|
else:
|
||||||
status = response.output_status.split(b' ', 1)[0]
|
status = response.output_status.split(b' ', 1)[0]
|
||||||
if six.PY3:
|
status = status.decode('ISO-8859-1')
|
||||||
status = status.decode('ISO-8859-1')
|
|
||||||
|
|
||||||
atoms = {'h': remote.name or remote.ip,
|
atoms = {'h': remote.name or remote.ip,
|
||||||
'l': '-',
|
'l': '-',
|
||||||
|
@ -270,45 +263,27 @@ class LogManager(object):
|
||||||
'i': request.unique_id,
|
'i': request.unique_id,
|
||||||
'z': LazyRfc3339UtcTime(),
|
'z': LazyRfc3339UtcTime(),
|
||||||
}
|
}
|
||||||
if six.PY3:
|
for k, v in atoms.items():
|
||||||
for k, v in atoms.items():
|
if not isinstance(v, str):
|
||||||
if not isinstance(v, str):
|
v = str(v)
|
||||||
v = str(v)
|
v = v.replace('"', '\\"').encode('utf8')
|
||||||
v = v.replace('"', '\\"').encode('utf8')
|
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
# and backslash for us. All we have to do is strip the quotes.
|
||||||
# and backslash for us. All we have to do is strip the quotes.
|
v = repr(v)[2:-1]
|
||||||
v = repr(v)[2:-1]
|
|
||||||
|
|
||||||
# in python 3.0 the repr of bytes (as returned by encode)
|
# in python 3.0 the repr of bytes (as returned by encode)
|
||||||
# uses double \'s. But then the logger escapes them yet, again
|
# uses double \'s. But then the logger escapes them yet, again
|
||||||
# resulting in quadruple slashes. Remove the extra one here.
|
# resulting in quadruple slashes. Remove the extra one here.
|
||||||
v = v.replace('\\\\', '\\')
|
v = v.replace('\\\\', '\\')
|
||||||
|
|
||||||
# Escape double-quote.
|
# Escape double-quote.
|
||||||
atoms[k] = v
|
atoms[k] = v
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.access_log.log(
|
self.access_log.log(
|
||||||
logging.INFO, self.access_log_format.format(**atoms))
|
logging.INFO, self.access_log_format.format(**atoms))
|
||||||
except Exception:
|
except Exception:
|
||||||
self(traceback=True)
|
self(traceback=True)
|
||||||
else:
|
|
||||||
for k, v in atoms.items():
|
|
||||||
if isinstance(v, six.text_type):
|
|
||||||
v = v.encode('utf8')
|
|
||||||
elif not isinstance(v, str):
|
|
||||||
v = str(v)
|
|
||||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
|
||||||
# and backslash for us. All we have to do is strip the quotes.
|
|
||||||
v = repr(v)[1:-1]
|
|
||||||
# Escape double-quote.
|
|
||||||
atoms[k] = v.replace('"', '\\"')
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.access_log.log(
|
|
||||||
logging.INFO, self.access_log_format % atoms)
|
|
||||||
except Exception:
|
|
||||||
self(traceback=True)
|
|
||||||
|
|
||||||
def time(self):
|
def time(self):
|
||||||
"""Return now() in Apache Common Log Format (no timezone)."""
|
"""Return now() in Apache Common Log Format (no timezone)."""
|
||||||
|
|
|
@ -61,8 +61,6 @@ import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from more_itertools import always_iterable
|
from more_itertools import always_iterable
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
@ -197,7 +195,7 @@ def handler(req):
|
||||||
path = req.uri
|
path = req.uri
|
||||||
qs = req.args or ''
|
qs = req.args or ''
|
||||||
reqproto = req.protocol
|
reqproto = req.protocol
|
||||||
headers = list(six.iteritems(req.headers_in))
|
headers = list(req.headers_in.copy().items())
|
||||||
rfile = _ReadOnlyRequest(req)
|
rfile = _ReadOnlyRequest(req)
|
||||||
prev = None
|
prev = None
|
||||||
|
|
||||||
|
|
|
@ -115,30 +115,29 @@ except ImportError:
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
try:
|
from urllib.parse import unquote
|
||||||
from urllib import unquote_plus
|
|
||||||
except ImportError:
|
|
||||||
def unquote_plus(bs):
|
|
||||||
"""Bytes version of urllib.parse.unquote_plus."""
|
|
||||||
bs = bs.replace(b'+', b' ')
|
|
||||||
atoms = bs.split(b'%')
|
|
||||||
for i in range(1, len(atoms)):
|
|
||||||
item = atoms[i]
|
|
||||||
try:
|
|
||||||
pct = int(item[:2], 16)
|
|
||||||
atoms[i] = bytes([pct]) + item[2:]
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return b''.join(atoms)
|
|
||||||
|
|
||||||
import six
|
|
||||||
import cheroot.server
|
import cheroot.server
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntou, unquote
|
from cherrypy._cpcompat import ntou
|
||||||
from cherrypy.lib import httputil
|
from cherrypy.lib import httputil
|
||||||
|
|
||||||
|
|
||||||
|
def unquote_plus(bs):
|
||||||
|
"""Bytes version of urllib.parse.unquote_plus."""
|
||||||
|
bs = bs.replace(b'+', b' ')
|
||||||
|
atoms = bs.split(b'%')
|
||||||
|
for i in range(1, len(atoms)):
|
||||||
|
item = atoms[i]
|
||||||
|
try:
|
||||||
|
pct = int(item[:2], 16)
|
||||||
|
atoms[i] = bytes([pct]) + item[2:]
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return b''.join(atoms)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------- Processors -------------------------------- #
|
# ------------------------------- Processors -------------------------------- #
|
||||||
|
|
||||||
def process_urlencoded(entity):
|
def process_urlencoded(entity):
|
||||||
|
@ -986,12 +985,6 @@ class RequestBody(Entity):
|
||||||
# add them in here.
|
# add them in here.
|
||||||
request_params = self.request_params
|
request_params = self.request_params
|
||||||
for key, value in self.params.items():
|
for key, value in self.params.items():
|
||||||
# Python 2 only: keyword arguments must be byte strings (type
|
|
||||||
# 'str').
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
if isinstance(key, six.text_type):
|
|
||||||
key = key.encode('ISO-8859-1')
|
|
||||||
|
|
||||||
if key in request_params:
|
if key in request_params:
|
||||||
if not isinstance(request_params[key], list):
|
if not isinstance(request_params[key], list):
|
||||||
request_params[key] = [request_params[key]]
|
request_params[key] = [request_params[key]]
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import collections
|
||||||
|
import operator
|
||||||
|
from http.cookies import SimpleCookie, CookieError
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import six
|
|
||||||
from six.moves.http_cookies import SimpleCookie, CookieError
|
|
||||||
|
|
||||||
from more_itertools import consume
|
from more_itertools import consume
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
@ -92,28 +92,36 @@ class HookMap(dict):
|
||||||
|
|
||||||
def run(self, point):
|
def run(self, point):
|
||||||
"""Execute all registered Hooks (callbacks) for the given point."""
|
"""Execute all registered Hooks (callbacks) for the given point."""
|
||||||
exc = None
|
self.run_hooks(iter(sorted(self[point])))
|
||||||
hooks = self[point]
|
|
||||||
hooks.sort()
|
@classmethod
|
||||||
|
def run_hooks(cls, hooks):
|
||||||
|
"""Execute the indicated hooks, trapping errors.
|
||||||
|
|
||||||
|
Hooks with ``.failsafe == True`` are guaranteed to run
|
||||||
|
even if others at the same hookpoint fail. In this case,
|
||||||
|
log the failure and proceed on to the next hook. The only
|
||||||
|
way to stop all processing from one of these hooks is
|
||||||
|
to raise a BaseException like SystemExit or
|
||||||
|
KeyboardInterrupt and stop the whole server.
|
||||||
|
"""
|
||||||
|
assert isinstance(hooks, collections.abc.Iterator)
|
||||||
|
quiet_errors = (
|
||||||
|
cherrypy.HTTPError,
|
||||||
|
cherrypy.HTTPRedirect,
|
||||||
|
cherrypy.InternalRedirect,
|
||||||
|
)
|
||||||
|
safe = filter(operator.attrgetter('failsafe'), hooks)
|
||||||
for hook in hooks:
|
for hook in hooks:
|
||||||
# Some hooks are guaranteed to run even if others at
|
try:
|
||||||
# the same hookpoint fail. We will still log the failure,
|
hook()
|
||||||
# but proceed on to the next hook. The only way
|
except quiet_errors:
|
||||||
# to stop all processing from one of these hooks is
|
cls.run_hooks(safe)
|
||||||
# to raise SystemExit and stop the whole server.
|
raise
|
||||||
if exc is None or hook.failsafe:
|
except Exception:
|
||||||
try:
|
cherrypy.log(traceback=True, severity=40)
|
||||||
hook()
|
cls.run_hooks(safe)
|
||||||
except (KeyboardInterrupt, SystemExit):
|
raise
|
||||||
raise
|
|
||||||
except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
|
|
||||||
cherrypy.InternalRedirect):
|
|
||||||
exc = sys.exc_info()[1]
|
|
||||||
except Exception:
|
|
||||||
exc = sys.exc_info()[1]
|
|
||||||
cherrypy.log(traceback=True, severity=40)
|
|
||||||
if exc:
|
|
||||||
raise exc
|
|
||||||
|
|
||||||
def __copy__(self):
|
def __copy__(self):
|
||||||
newmap = self.__class__()
|
newmap = self.__class__()
|
||||||
|
@ -141,7 +149,7 @@ def hooks_namespace(k, v):
|
||||||
# hookpoint per path (e.g. "hooks.before_handler.1").
|
# hookpoint per path (e.g. "hooks.before_handler.1").
|
||||||
# Little-known fact you only get from reading source ;)
|
# Little-known fact you only get from reading source ;)
|
||||||
hookpoint = k.split('.', 1)[0]
|
hookpoint = k.split('.', 1)[0]
|
||||||
if isinstance(v, six.string_types):
|
if isinstance(v, str):
|
||||||
v = cherrypy.lib.reprconf.attributes(v)
|
v = cherrypy.lib.reprconf.attributes(v)
|
||||||
if not isinstance(v, Hook):
|
if not isinstance(v, Hook):
|
||||||
v = Hook(v)
|
v = Hook(v)
|
||||||
|
@ -704,12 +712,6 @@ class Request(object):
|
||||||
'strings for this resource must be encoded with %r.' %
|
'strings for this resource must be encoded with %r.' %
|
||||||
self.query_string_encoding)
|
self.query_string_encoding)
|
||||||
|
|
||||||
# Python 2 only: keyword arguments must be byte strings (type 'str').
|
|
||||||
if six.PY2:
|
|
||||||
for key, value in p.items():
|
|
||||||
if isinstance(key, six.text_type):
|
|
||||||
del p[key]
|
|
||||||
p[key.encode(self.query_string_encoding)] = value
|
|
||||||
self.params.update(p)
|
self.params.update(p)
|
||||||
|
|
||||||
def process_headers(self):
|
def process_headers(self):
|
||||||
|
@ -786,11 +788,11 @@ class ResponseBody(object):
|
||||||
|
|
||||||
def __set__(self, obj, value):
|
def __set__(self, obj, value):
|
||||||
# Convert the given value to an iterable object.
|
# Convert the given value to an iterable object.
|
||||||
if isinstance(value, six.text_type):
|
if isinstance(value, str):
|
||||||
raise ValueError(self.unicode_err)
|
raise ValueError(self.unicode_err)
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
# every item in a list must be bytes...
|
# every item in a list must be bytes...
|
||||||
if any(isinstance(item, six.text_type) for item in value):
|
if any(isinstance(item, str) for item in value):
|
||||||
raise ValueError(self.unicode_err)
|
raise ValueError(self.unicode_err)
|
||||||
|
|
||||||
obj._body = encoding.prepare_iter(value)
|
obj._body = encoding.prepare_iter(value)
|
||||||
|
@ -903,9 +905,9 @@ class Response(object):
|
||||||
if cookie:
|
if cookie:
|
||||||
for line in cookie.split('\r\n'):
|
for line in cookie.split('\r\n'):
|
||||||
name, value = line.split(': ', 1)
|
name, value = line.split(': ', 1)
|
||||||
if isinstance(name, six.text_type):
|
if isinstance(name, str):
|
||||||
name = name.encode('ISO-8859-1')
|
name = name.encode('ISO-8859-1')
|
||||||
if isinstance(value, six.text_type):
|
if isinstance(value, str):
|
||||||
value = headers.encode(value)
|
value = headers.encode(value)
|
||||||
h.append((name, value))
|
h.append((name, value))
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
"""Manage HTTP servers with CherryPy."""
|
"""Manage HTTP servers with CherryPy."""
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib.reprconf import attributes
|
from cherrypy.lib.reprconf import attributes
|
||||||
from cherrypy._cpcompat import text_or_bytes
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
|
@ -116,21 +114,12 @@ class Server(ServerAdapter):
|
||||||
ssl_ciphers = None
|
ssl_ciphers = None
|
||||||
"""The ciphers list of SSL."""
|
"""The ciphers list of SSL."""
|
||||||
|
|
||||||
if six.PY3:
|
ssl_module = 'builtin'
|
||||||
ssl_module = 'builtin'
|
"""The name of a registered SSL adaptation module to use with
|
||||||
"""The name of a registered SSL adaptation module to use with
|
the builtin WSGI server. Builtin options are: 'builtin' (to
|
||||||
the builtin WSGI server. Builtin options are: 'builtin' (to
|
use the SSL library built into recent versions of Python).
|
||||||
use the SSL library built into recent versions of Python).
|
You may also register your own classes in the
|
||||||
You may also register your own classes in the
|
cheroot.server.ssl_adapters dict."""
|
||||||
cheroot.server.ssl_adapters dict."""
|
|
||||||
else:
|
|
||||||
ssl_module = 'pyopenssl'
|
|
||||||
"""The name of a registered SSL adaptation module to use with the
|
|
||||||
builtin WSGI server. Builtin options are 'builtin' (to use the SSL
|
|
||||||
library built into recent versions of Python) and 'pyopenssl' (to
|
|
||||||
use the PyOpenSSL project, which you must install separately). You
|
|
||||||
may also register your own classes in the cheroot.server.ssl_adapters
|
|
||||||
dict."""
|
|
||||||
|
|
||||||
statistics = False
|
statistics = False
|
||||||
"""Turns statistics-gathering on or off for aware HTTP servers."""
|
"""Turns statistics-gathering on or off for aware HTTP servers."""
|
||||||
|
|
|
@ -22,8 +22,6 @@ Tools may be implemented as any object with a namespace. The builtins
|
||||||
are generally either modules or instances of the tools.Tool class.
|
are generally either modules or instances of the tools.Tool class.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._helper import expose
|
from cherrypy._helper import expose
|
||||||
|
|
||||||
|
@ -37,14 +35,9 @@ def _getargs(func):
|
||||||
"""Return the names of all static arguments to the given function."""
|
"""Return the names of all static arguments to the given function."""
|
||||||
# Use this instead of importing inspect for less mem overhead.
|
# Use this instead of importing inspect for less mem overhead.
|
||||||
import types
|
import types
|
||||||
if six.PY3:
|
if isinstance(func, types.MethodType):
|
||||||
if isinstance(func, types.MethodType):
|
func = func.__func__
|
||||||
func = func.__func__
|
co = func.__code__
|
||||||
co = func.__code__
|
|
||||||
else:
|
|
||||||
if isinstance(func, types.MethodType):
|
|
||||||
func = func.im_func
|
|
||||||
co = func.func_code
|
|
||||||
return co.co_varnames[:co.co_argcount]
|
return co.co_varnames[:co.co_argcount]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntou
|
|
||||||
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
|
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
|
||||||
from cherrypy.lib import httputil, reprconf
|
from cherrypy.lib import httputil, reprconf
|
||||||
|
|
||||||
|
@ -289,8 +286,6 @@ class Tree(object):
|
||||||
# to '' (some WSGI servers always set SCRIPT_NAME to '').
|
# to '' (some WSGI servers always set SCRIPT_NAME to '').
|
||||||
# Try to look up the app using the full path.
|
# Try to look up the app using the full path.
|
||||||
env1x = environ
|
env1x = environ
|
||||||
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
|
||||||
env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
|
|
||||||
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
|
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
|
||||||
env1x.get('PATH_INFO', ''))
|
env1x.get('PATH_INFO', ''))
|
||||||
sn = self.script_name(path or '/')
|
sn = self.script_name(path or '/')
|
||||||
|
@ -302,12 +297,6 @@ class Tree(object):
|
||||||
|
|
||||||
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
|
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
|
||||||
environ = environ.copy()
|
environ = environ.copy()
|
||||||
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
environ['SCRIPT_NAME'] = sn
|
||||||
# Python 2/WSGI u.0: all strings MUST be of type unicode
|
environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
|
||||||
enc = environ[ntou('wsgi.url_encoding')]
|
|
||||||
environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
|
|
||||||
environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
|
|
||||||
else:
|
|
||||||
environ['SCRIPT_NAME'] = sn
|
|
||||||
environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
|
|
||||||
return app(environ, start_response)
|
return app(environ, start_response)
|
||||||
|
|
|
@ -10,8 +10,6 @@ still be translatable to bytes via the Latin-1 encoding!"
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy as _cherrypy
|
import cherrypy as _cherrypy
|
||||||
from cherrypy._cpcompat import ntou
|
from cherrypy._cpcompat import ntou
|
||||||
from cherrypy import _cperror
|
from cherrypy import _cperror
|
||||||
|
@ -25,10 +23,10 @@ def downgrade_wsgi_ux_to_1x(environ):
|
||||||
env1x = {}
|
env1x = {}
|
||||||
|
|
||||||
url_encoding = environ[ntou('wsgi.url_encoding')]
|
url_encoding = environ[ntou('wsgi.url_encoding')]
|
||||||
for k, v in list(environ.items()):
|
for k, v in environ.copy().items():
|
||||||
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
|
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
|
||||||
v = v.encode(url_encoding)
|
v = v.encode(url_encoding)
|
||||||
elif isinstance(v, six.text_type):
|
elif isinstance(v, str):
|
||||||
v = v.encode('ISO-8859-1')
|
v = v.encode('ISO-8859-1')
|
||||||
env1x[k.encode('ISO-8859-1')] = v
|
env1x[k.encode('ISO-8859-1')] = v
|
||||||
|
|
||||||
|
@ -177,10 +175,6 @@ class _TrappedResponse(object):
|
||||||
def __next__(self):
|
def __next__(self):
|
||||||
return self.trap(next, self.iter_response)
|
return self.trap(next, self.iter_response)
|
||||||
|
|
||||||
# todo: https://pythonhosted.org/six/#six.Iterator
|
|
||||||
if six.PY2:
|
|
||||||
next = __next__
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if hasattr(self.response, 'close'):
|
if hasattr(self.response, 'close'):
|
||||||
self.response.close()
|
self.response.close()
|
||||||
|
@ -198,7 +192,7 @@ class _TrappedResponse(object):
|
||||||
if not _cherrypy.request.show_tracebacks:
|
if not _cherrypy.request.show_tracebacks:
|
||||||
tb = ''
|
tb = ''
|
||||||
s, h, b = _cperror.bare_error(tb)
|
s, h, b = _cperror.bare_error(tb)
|
||||||
if six.PY3:
|
if True:
|
||||||
# What fun.
|
# What fun.
|
||||||
s = s.decode('ISO-8859-1')
|
s = s.decode('ISO-8859-1')
|
||||||
h = [
|
h = [
|
||||||
|
@ -238,9 +232,6 @@ class AppResponse(object):
|
||||||
def __init__(self, environ, start_response, cpapp):
|
def __init__(self, environ, start_response, cpapp):
|
||||||
self.cpapp = cpapp
|
self.cpapp = cpapp
|
||||||
try:
|
try:
|
||||||
if six.PY2:
|
|
||||||
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
|
||||||
environ = downgrade_wsgi_ux_to_1x(environ)
|
|
||||||
self.environ = environ
|
self.environ = environ
|
||||||
self.run()
|
self.run()
|
||||||
|
|
||||||
|
@ -262,7 +253,7 @@ class AppResponse(object):
|
||||||
raise TypeError(tmpl % v)
|
raise TypeError(tmpl % v)
|
||||||
outheaders.append((k, v))
|
outheaders.append((k, v))
|
||||||
|
|
||||||
if six.PY3:
|
if True:
|
||||||
# According to PEP 3333, when using Python 3, the response
|
# According to PEP 3333, when using Python 3, the response
|
||||||
# status and headers must be bytes masquerading as unicode;
|
# status and headers must be bytes masquerading as unicode;
|
||||||
# that is, they must be of type "str" but are restricted to
|
# that is, they must be of type "str" but are restricted to
|
||||||
|
@ -285,10 +276,6 @@ class AppResponse(object):
|
||||||
def __next__(self):
|
def __next__(self):
|
||||||
return next(self.iter_response)
|
return next(self.iter_response)
|
||||||
|
|
||||||
# todo: https://pythonhosted.org/six/#six.Iterator
|
|
||||||
if six.PY2:
|
|
||||||
next = __next__
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close and de-reference the current request and response. (Core)"""
|
"""Close and de-reference the current request and response. (Core)"""
|
||||||
streaming = _cherrypy.serving.response.stream
|
streaming = _cherrypy.serving.response.stream
|
||||||
|
@ -356,9 +343,6 @@ class AppResponse(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def recode_path_qs(self, path, qs):
|
def recode_path_qs(self, path, qs):
|
||||||
if not six.PY3:
|
|
||||||
return
|
|
||||||
|
|
||||||
# This isn't perfect; if the given PATH_INFO is in the
|
# This isn't perfect; if the given PATH_INFO is in the
|
||||||
# wrong encoding, it may fail to match the appropriate config
|
# wrong encoding, it may fail to match the appropriate config
|
||||||
# section URI. But meh.
|
# section URI. But meh.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Helper functions for CP apps."""
|
"""Helper functions for CP apps."""
|
||||||
|
|
||||||
import six
|
import urllib.parse
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
from cherrypy._cpcompat import text_or_bytes
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
|
|
||||||
|
@ -26,9 +25,6 @@ def expose(func=None, alias=None):
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
decoratable_types = types.FunctionType, types.MethodType, type,
|
decoratable_types = types.FunctionType, types.MethodType, type,
|
||||||
if six.PY2:
|
|
||||||
# Old-style classes are type types.ClassType.
|
|
||||||
decoratable_types += types.ClassType,
|
|
||||||
if isinstance(func, decoratable_types):
|
if isinstance(func, decoratable_types):
|
||||||
if alias is None:
|
if alias is None:
|
||||||
# @expose
|
# @expose
|
||||||
|
@ -87,53 +83,61 @@ def popargs(*args, **kwargs):
|
||||||
This decorator may be used in one of two ways:
|
This decorator may be used in one of two ways:
|
||||||
|
|
||||||
As a class decorator:
|
As a class decorator:
|
||||||
@cherrypy.popargs('year', 'month', 'day')
|
|
||||||
class Blog:
|
|
||||||
def index(self, year=None, month=None, day=None):
|
|
||||||
#Process the parameters here; any url like
|
|
||||||
#/, /2009, /2009/12, or /2009/12/31
|
|
||||||
#will fill in the appropriate parameters.
|
|
||||||
|
|
||||||
def create(self):
|
.. code-block:: python
|
||||||
#This link will still be available at /create. Defined functions
|
|
||||||
#take precedence over arguments.
|
@cherrypy.popargs('year', 'month', 'day')
|
||||||
|
class Blog:
|
||||||
|
def index(self, year=None, month=None, day=None):
|
||||||
|
#Process the parameters here; any url like
|
||||||
|
#/, /2009, /2009/12, or /2009/12/31
|
||||||
|
#will fill in the appropriate parameters.
|
||||||
|
|
||||||
|
def create(self):
|
||||||
|
#This link will still be available at /create.
|
||||||
|
#Defined functions take precedence over arguments.
|
||||||
|
|
||||||
Or as a member of a class:
|
Or as a member of a class:
|
||||||
class Blog:
|
|
||||||
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
|
.. code-block:: python
|
||||||
#...
|
|
||||||
|
class Blog:
|
||||||
|
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
|
||||||
|
#...
|
||||||
|
|
||||||
The handler argument may be used to mix arguments with built in functions.
|
The handler argument may be used to mix arguments with built in functions.
|
||||||
For instance, the following setup allows different activities at the
|
For instance, the following setup allows different activities at the
|
||||||
day, month, and year level:
|
day, month, and year level:
|
||||||
|
|
||||||
class DayHandler:
|
.. code-block:: python
|
||||||
def index(self, year, month, day):
|
|
||||||
#Do something with this day; probably list entries
|
|
||||||
|
|
||||||
def delete(self, year, month, day):
|
class DayHandler:
|
||||||
#Delete all entries for this day
|
def index(self, year, month, day):
|
||||||
|
#Do something with this day; probably list entries
|
||||||
|
|
||||||
@cherrypy.popargs('day', handler=DayHandler())
|
def delete(self, year, month, day):
|
||||||
class MonthHandler:
|
#Delete all entries for this day
|
||||||
def index(self, year, month):
|
|
||||||
#Do something with this month; probably list entries
|
|
||||||
|
|
||||||
def delete(self, year, month):
|
@cherrypy.popargs('day', handler=DayHandler())
|
||||||
#Delete all entries for this month
|
class MonthHandler:
|
||||||
|
def index(self, year, month):
|
||||||
|
#Do something with this month; probably list entries
|
||||||
|
|
||||||
@cherrypy.popargs('month', handler=MonthHandler())
|
def delete(self, year, month):
|
||||||
class YearHandler:
|
#Delete all entries for this month
|
||||||
def index(self, year):
|
|
||||||
#Do something with this year
|
|
||||||
|
|
||||||
#...
|
@cherrypy.popargs('month', handler=MonthHandler())
|
||||||
|
class YearHandler:
|
||||||
|
def index(self, year):
|
||||||
|
#Do something with this year
|
||||||
|
|
||||||
@cherrypy.popargs('year', handler=YearHandler())
|
|
||||||
class Root:
|
|
||||||
def index(self):
|
|
||||||
#...
|
#...
|
||||||
|
|
||||||
|
@cherrypy.popargs('year', handler=YearHandler())
|
||||||
|
class Root:
|
||||||
|
def index(self):
|
||||||
|
#...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Since keyword arg comes after *args, we have to process it ourselves
|
# Since keyword arg comes after *args, we have to process it ourselves
|
||||||
# for lower versions of python.
|
# for lower versions of python.
|
||||||
|
|
25
lib/cherrypy/_json.py
Normal file
25
lib/cherrypy/_json.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
"""
|
||||||
|
JSON support.
|
||||||
|
|
||||||
|
Expose preferred json module as json and provide encode/decode
|
||||||
|
convenience functions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prefer simplejson
|
||||||
|
import simplejson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['json', 'encode', 'decode']
|
||||||
|
|
||||||
|
|
||||||
|
decode = json.JSONDecoder().decode
|
||||||
|
_encode = json.JSONEncoder().iterencode
|
||||||
|
|
||||||
|
|
||||||
|
def encode(value):
|
||||||
|
"""Encode to bytes."""
|
||||||
|
for chunk in _encode(value):
|
||||||
|
yield chunk.encode('utf-8')
|
|
@ -70,6 +70,11 @@ class file_generator(object):
|
||||||
raise StopIteration()
|
raise StopIteration()
|
||||||
next = __next__
|
next = __next__
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""Close input on descturct."""
|
||||||
|
if hasattr(self.input, 'close'):
|
||||||
|
self.input.close()
|
||||||
|
|
||||||
|
|
||||||
def file_generator_limited(fileobj, count, chunk_size=65536):
|
def file_generator_limited(fileobj, count, chunk_size=65536):
|
||||||
"""Yield the given file object in chunks.
|
"""Yield the given file object in chunks.
|
||||||
|
|
|
@ -23,8 +23,7 @@ of plaintext passwords as the credentials store::
|
||||||
import time
|
import time
|
||||||
import functools
|
import functools
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from urllib.request import parse_http_list, parse_keqv_list
|
||||||
from six.moves.urllib.request import parse_http_list, parse_keqv_list
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntob, tonative
|
from cherrypy._cpcompat import ntob, tonative
|
||||||
|
|
|
@ -37,11 +37,8 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib import cptools, httputil
|
from cherrypy.lib import cptools, httputil
|
||||||
from cherrypy._cpcompat import Event
|
|
||||||
|
|
||||||
|
|
||||||
class Cache(object):
|
class Cache(object):
|
||||||
|
@ -82,7 +79,7 @@ class AntiStampedeCache(dict):
|
||||||
If timeout is None, no waiting is performed nor sentinels used.
|
If timeout is None, no waiting is performed nor sentinels used.
|
||||||
"""
|
"""
|
||||||
value = self.get(key)
|
value = self.get(key)
|
||||||
if isinstance(value, Event):
|
if isinstance(value, threading.Event):
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
# Ignore the other thread and recalc it ourselves.
|
# Ignore the other thread and recalc it ourselves.
|
||||||
if debug:
|
if debug:
|
||||||
|
@ -122,7 +119,7 @@ class AntiStampedeCache(dict):
|
||||||
"""Set the cached value for the given key."""
|
"""Set the cached value for the given key."""
|
||||||
existing = self.get(key)
|
existing = self.get(key)
|
||||||
dict.__setitem__(self, key, value)
|
dict.__setitem__(self, key, value)
|
||||||
if isinstance(existing, Event):
|
if isinstance(existing, threading.Event):
|
||||||
# Set Event.result so other threads waiting on it have
|
# Set Event.result so other threads waiting on it have
|
||||||
# immediate access without needing to poll the cache again.
|
# immediate access without needing to poll the cache again.
|
||||||
existing.result = value
|
existing.result = value
|
||||||
|
@ -199,8 +196,7 @@ class MemoryCache(Cache):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
# Must make a copy of expirations so it doesn't change size
|
# Must make a copy of expirations so it doesn't change size
|
||||||
# during iteration
|
# during iteration
|
||||||
items = list(six.iteritems(self.expirations))
|
for expiration_time, objects in self.expirations.copy().items():
|
||||||
for expiration_time, objects in items:
|
|
||||||
if expiration_time <= now:
|
if expiration_time <= now:
|
||||||
for obj_size, uri, sel_header_values in objects:
|
for obj_size, uri, sel_header_values in objects:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -25,8 +25,7 @@ import sys
|
||||||
import cgi
|
import cgi
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
|
import urllib.parse
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
|
|
|
@ -193,10 +193,8 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import json
|
from cherrypy._json import json
|
||||||
|
|
||||||
# ------------------------------- Statistics -------------------------------- #
|
# ------------------------------- Statistics -------------------------------- #
|
||||||
|
|
||||||
|
@ -207,7 +205,7 @@ if not hasattr(logging, 'statistics'):
|
||||||
def extrapolate_statistics(scope):
|
def extrapolate_statistics(scope):
|
||||||
"""Return an extrapolated copy of the given scope."""
|
"""Return an extrapolated copy of the given scope."""
|
||||||
c = {}
|
c = {}
|
||||||
for k, v in list(scope.items()):
|
for k, v in scope.copy().items():
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict):
|
||||||
v = extrapolate_statistics(v)
|
v = extrapolate_statistics(v)
|
||||||
elif isinstance(v, (list, tuple)):
|
elif isinstance(v, (list, tuple)):
|
||||||
|
@ -366,8 +364,8 @@ class StatsTool(cherrypy.Tool):
|
||||||
w['Bytes Written'] = cl
|
w['Bytes Written'] = cl
|
||||||
appstats['Total Bytes Written'] += cl
|
appstats['Total Bytes Written'] += cl
|
||||||
|
|
||||||
w['Response Status'] = getattr(
|
w['Response Status'] = \
|
||||||
resp, 'output_status', None) or resp.status
|
getattr(resp, 'output_status', resp.status).decode()
|
||||||
|
|
||||||
w['End Time'] = time.time()
|
w['End Time'] = time.time()
|
||||||
p = w['End Time'] - w['Start Time']
|
p = w['End Time'] - w['Start Time']
|
||||||
|
@ -613,7 +611,7 @@ table.stats2 th {
|
||||||
"""Return ([headers], [rows]) for the given collection."""
|
"""Return ([headers], [rows]) for the given collection."""
|
||||||
# E.g., the 'Requests' dict.
|
# E.g., the 'Requests' dict.
|
||||||
headers = []
|
headers = []
|
||||||
vals = six.itervalues(v)
|
vals = v.values()
|
||||||
for record in vals:
|
for record in vals:
|
||||||
for k3 in record:
|
for k3 in record:
|
||||||
format = formatting.get(k3, missing)
|
format = formatting.get(k3, missing)
|
||||||
|
@ -679,7 +677,7 @@ table.stats2 th {
|
||||||
def data(self):
|
def data(self):
|
||||||
s = extrapolate_statistics(logging.statistics)
|
s = extrapolate_statistics(logging.statistics)
|
||||||
cherrypy.response.headers['Content-Type'] = 'application/json'
|
cherrypy.response.headers['Content-Type'] = 'application/json'
|
||||||
return json.dumps(s, sort_keys=True, indent=4)
|
return json.dumps(s, sort_keys=True, indent=4).encode('utf-8')
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def pause(self, namespace):
|
def pause(self, namespace):
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
import urllib.parse
|
||||||
import six
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import text_or_bytes
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
|
@ -307,7 +305,7 @@ class SessionAuth(object):
|
||||||
|
|
||||||
def login_screen(self, from_page='..', username='', error_msg='',
|
def login_screen(self, from_page='..', username='', error_msg='',
|
||||||
**kwargs):
|
**kwargs):
|
||||||
return (six.text_type("""<html><body>
|
return (str("""<html><body>
|
||||||
Message: %(error_msg)s
|
Message: %(error_msg)s
|
||||||
<form method="post" action="do_login">
|
<form method="post" action="do_login">
|
||||||
Login: <input type="text" name="username" value="%(username)s" size="10" />
|
Login: <input type="text" name="username" value="%(username)s" size="10" />
|
||||||
|
@ -406,23 +404,22 @@ Message: %(error_msg)s
|
||||||
|
|
||||||
|
|
||||||
def session_auth(**kwargs):
|
def session_auth(**kwargs):
|
||||||
|
"""Session authentication hook.
|
||||||
|
|
||||||
|
Any attribute of the SessionAuth class may be overridden
|
||||||
|
via a keyword arg to this function:
|
||||||
|
|
||||||
|
""" + '\n '.join(
|
||||||
|
'{!s}: {!s}'.format(k, type(getattr(SessionAuth, k)).__name__)
|
||||||
|
for k in dir(SessionAuth)
|
||||||
|
if not k.startswith('__')
|
||||||
|
)
|
||||||
sa = SessionAuth()
|
sa = SessionAuth()
|
||||||
for k, v in kwargs.items():
|
for k, v in kwargs.items():
|
||||||
setattr(sa, k, v)
|
setattr(sa, k, v)
|
||||||
return sa.run()
|
return sa.run()
|
||||||
|
|
||||||
|
|
||||||
session_auth.__doc__ = (
|
|
||||||
"""Session authentication hook.
|
|
||||||
|
|
||||||
Any attribute of the SessionAuth class may be overridden via a keyword arg
|
|
||||||
to this function:
|
|
||||||
|
|
||||||
""" + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
|
|
||||||
for k in dir(SessionAuth) if not k.startswith('__')])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def log_traceback(severity=logging.ERROR, debug=False):
|
def log_traceback(severity=logging.ERROR, debug=False):
|
||||||
"""Write the last error's traceback to the cherrypy error log."""
|
"""Write the last error's traceback to the cherrypy error log."""
|
||||||
cherrypy.log('', 'HTTP', severity=severity, traceback=True)
|
cherrypy.log('', 'HTTP', severity=severity, traceback=True)
|
||||||
|
|
|
@ -2,8 +2,6 @@ import struct
|
||||||
import time
|
import time
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import text_or_bytes
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
from cherrypy.lib import file_generator
|
from cherrypy.lib import file_generator
|
||||||
|
@ -11,6 +9,10 @@ from cherrypy.lib import is_closable_iterator
|
||||||
from cherrypy.lib import set_vary_header
|
from cherrypy.lib import set_vary_header
|
||||||
|
|
||||||
|
|
||||||
|
_COMPRESSION_LEVEL_FAST = 1
|
||||||
|
_COMPRESSION_LEVEL_BEST = 9
|
||||||
|
|
||||||
|
|
||||||
def decode(encoding=None, default_encoding='utf-8'):
|
def decode(encoding=None, default_encoding='utf-8'):
|
||||||
"""Replace or extend the list of charsets used to decode a request entity.
|
"""Replace or extend the list of charsets used to decode a request entity.
|
||||||
|
|
||||||
|
@ -50,7 +52,7 @@ class UTF8StreamEncoder:
|
||||||
|
|
||||||
def __next__(self):
|
def __next__(self):
|
||||||
res = next(self._iterator)
|
res = next(self._iterator)
|
||||||
if isinstance(res, six.text_type):
|
if isinstance(res, str):
|
||||||
res = res.encode('utf-8')
|
res = res.encode('utf-8')
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -99,7 +101,7 @@ class ResponseEncoder:
|
||||||
|
|
||||||
def encoder(body):
|
def encoder(body):
|
||||||
for chunk in body:
|
for chunk in body:
|
||||||
if isinstance(chunk, six.text_type):
|
if isinstance(chunk, str):
|
||||||
chunk = chunk.encode(encoding, self.errors)
|
chunk = chunk.encode(encoding, self.errors)
|
||||||
yield chunk
|
yield chunk
|
||||||
self.body = encoder(self.body)
|
self.body = encoder(self.body)
|
||||||
|
@ -112,7 +114,7 @@ class ResponseEncoder:
|
||||||
self.attempted_charsets.add(encoding)
|
self.attempted_charsets.add(encoding)
|
||||||
body = []
|
body = []
|
||||||
for chunk in self.body:
|
for chunk in self.body:
|
||||||
if isinstance(chunk, six.text_type):
|
if isinstance(chunk, str):
|
||||||
try:
|
try:
|
||||||
chunk = chunk.encode(encoding, self.errors)
|
chunk = chunk.encode(encoding, self.errors)
|
||||||
except (LookupError, UnicodeError):
|
except (LookupError, UnicodeError):
|
||||||
|
@ -287,13 +289,29 @@ def compress(body, compress_level):
|
||||||
"""Compress 'body' at the given compress_level."""
|
"""Compress 'body' at the given compress_level."""
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
# See http://www.gzip.org/zlib/rfc-gzip.html
|
# See https://tools.ietf.org/html/rfc1952
|
||||||
yield b'\x1f\x8b' # ID1 and ID2: gzip marker
|
yield b'\x1f\x8b' # ID1 and ID2: gzip marker
|
||||||
yield b'\x08' # CM: compression method
|
yield b'\x08' # CM: compression method
|
||||||
yield b'\x00' # FLG: none set
|
yield b'\x00' # FLG: none set
|
||||||
# MTIME: 4 bytes
|
# MTIME: 4 bytes
|
||||||
yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
|
yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
|
||||||
yield b'\x02' # XFL: max compression, slowest algo
|
|
||||||
|
# RFC 1952, section 2.3.1:
|
||||||
|
#
|
||||||
|
# XFL (eXtra FLags)
|
||||||
|
# These flags are available for use by specific compression
|
||||||
|
# methods. The "deflate" method (CM = 8) sets these flags as
|
||||||
|
# follows:
|
||||||
|
#
|
||||||
|
# XFL = 2 - compressor used maximum compression,
|
||||||
|
# slowest algorithm
|
||||||
|
# XFL = 4 - compressor used fastest algorithm
|
||||||
|
if compress_level == _COMPRESSION_LEVEL_BEST:
|
||||||
|
yield b'\x02' # XFL: max compression, slowest algo
|
||||||
|
elif compress_level == _COMPRESSION_LEVEL_FAST:
|
||||||
|
yield b'\x04' # XFL: min compression, fastest algo
|
||||||
|
else:
|
||||||
|
yield b'\x00' # XFL: compression unset/tradeoff
|
||||||
yield b'\xff' # OS: unknown
|
yield b'\xff' # OS: unknown
|
||||||
|
|
||||||
crc = zlib.crc32(b'')
|
crc = zlib.crc32(b'')
|
||||||
|
|
|
@ -10,17 +10,17 @@ to a public caning.
|
||||||
import functools
|
import functools
|
||||||
import email.utils
|
import email.utils
|
||||||
import re
|
import re
|
||||||
|
import builtins
|
||||||
from binascii import b2a_base64
|
from binascii import b2a_base64
|
||||||
from cgi import parse_header
|
from cgi import parse_header
|
||||||
from email.header import decode_header
|
from email.header import decode_header
|
||||||
|
from http.server import BaseHTTPRequestHandler
|
||||||
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
import six
|
import jaraco.collections
|
||||||
from six.moves import range, builtins, map
|
|
||||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntob, ntou
|
from cherrypy._cpcompat import ntob, ntou
|
||||||
from cherrypy._cpcompat import unquote_plus
|
|
||||||
|
|
||||||
response_codes = BaseHTTPRequestHandler.responses.copy()
|
response_codes = BaseHTTPRequestHandler.responses.copy()
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ class HeaderElement(object):
|
||||||
return self.value < other.value
|
return self.value < other.value
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)]
|
p = [';%s=%s' % (k, v) for k, v in self.params.items()]
|
||||||
return str('%s%s' % (self.value, ''.join(p)))
|
return str('%s%s' % (self.value, ''.join(p)))
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
|
@ -209,14 +209,11 @@ class AcceptElement(HeaderElement):
|
||||||
|
|
||||||
Ref: https://github.com/cherrypy/cherrypy/issues/1370
|
Ref: https://github.com/cherrypy/cherrypy/issues/1370
|
||||||
"""
|
"""
|
||||||
six.raise_from(
|
raise cherrypy.HTTPError(
|
||||||
cherrypy.HTTPError(
|
400,
|
||||||
400,
|
'Malformed HTTP header: `{}`'.
|
||||||
'Malformed HTTP header: `{}`'.
|
format(str(self)),
|
||||||
format(str(self)),
|
) from val_err
|
||||||
),
|
|
||||||
val_err,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
diff = builtins.cmp(self.qvalue, other.qvalue)
|
diff = builtins.cmp(self.qvalue, other.qvalue)
|
||||||
|
@ -283,11 +280,11 @@ def valid_status(status):
|
||||||
If status has no reason-phrase is supplied, a default reason-
|
If status has no reason-phrase is supplied, a default reason-
|
||||||
phrase will be provided.
|
phrase will be provided.
|
||||||
|
|
||||||
>>> from six.moves import http_client
|
>>> import http.client
|
||||||
>>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
|
>>> from http.server import BaseHTTPRequestHandler
|
||||||
>>> valid_status(http_client.ACCEPTED) == (
|
>>> valid_status(http.client.ACCEPTED) == (
|
||||||
... int(http_client.ACCEPTED),
|
... int(http.client.ACCEPTED),
|
||||||
... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED]
|
... ) + BaseHTTPRequestHandler.responses[http.client.ACCEPTED]
|
||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -295,7 +292,7 @@ def valid_status(status):
|
||||||
status = 200
|
status = 200
|
||||||
|
|
||||||
code, reason = status, None
|
code, reason = status, None
|
||||||
if isinstance(status, six.string_types):
|
if isinstance(status, str):
|
||||||
code, _, reason = status.partition(' ')
|
code, _, reason = status.partition(' ')
|
||||||
reason = reason.strip() or None
|
reason = reason.strip() or None
|
||||||
|
|
||||||
|
@ -390,77 +387,19 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
|
||||||
return pm
|
return pm
|
||||||
|
|
||||||
|
|
||||||
####
|
class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict):
|
||||||
# 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
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
super(KeyTransformingDict, self).__setitem__(key, val)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
return super(KeyTransformingDict, self).__getitem__(key)
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
return super(KeyTransformingDict, self).__contains__(key)
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
return super(KeyTransformingDict, self).__delitem__(key)
|
|
||||||
|
|
||||||
def get(self, key, *args, **kwargs):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
return super(KeyTransformingDict, self).get(key, *args, **kwargs)
|
|
||||||
|
|
||||||
def setdefault(self, key, *args, **kwargs):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
return super(KeyTransformingDict, self).setdefault(
|
|
||||||
key, *args, **kwargs)
|
|
||||||
|
|
||||||
def pop(self, key, *args, **kwargs):
|
|
||||||
key = self.transform_key(key)
|
|
||||||
return super(KeyTransformingDict, self).pop(key, *args, **kwargs)
|
|
||||||
|
|
||||||
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)
|
|
||||||
####
|
|
||||||
|
|
||||||
|
|
||||||
class CaseInsensitiveDict(KeyTransformingDict):
|
|
||||||
|
|
||||||
"""A case-insensitive dict subclass.
|
"""A case-insensitive dict subclass.
|
||||||
|
|
||||||
Each key is changed on entry to str(key).title().
|
Each key is changed on entry to title case.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def transform_key(key):
|
def transform_key(key):
|
||||||
return str(key).title()
|
if key is None:
|
||||||
|
# TODO(#1830): why?
|
||||||
|
return 'None'
|
||||||
|
return key.title()
|
||||||
|
|
||||||
|
|
||||||
# TEXT = <any OCTET except CTLs, but including LWS>
|
# TEXT = <any OCTET except CTLs, but including LWS>
|
||||||
|
@ -499,9 +438,7 @@ class HeaderMap(CaseInsensitiveDict):
|
||||||
|
|
||||||
def elements(self, key):
|
def elements(self, key):
|
||||||
"""Return a sorted list of HeaderElements for the given header."""
|
"""Return a sorted list of HeaderElements for the given header."""
|
||||||
key = str(key).title()
|
return header_elements(self.transform_key(key), self.get(key))
|
||||||
value = self.get(key)
|
|
||||||
return header_elements(key, value)
|
|
||||||
|
|
||||||
def values(self, key):
|
def values(self, key):
|
||||||
"""Return a sorted list of HeaderElement.value for the given header."""
|
"""Return a sorted list of HeaderElement.value for the given header."""
|
||||||
|
@ -518,15 +455,14 @@ class HeaderMap(CaseInsensitiveDict):
|
||||||
transmitting on the wire for HTTP.
|
transmitting on the wire for HTTP.
|
||||||
"""
|
"""
|
||||||
for k, v in header_items:
|
for k, v in header_items:
|
||||||
if not isinstance(v, six.string_types) and \
|
if not isinstance(v, str) and not isinstance(v, bytes):
|
||||||
not isinstance(v, six.binary_type):
|
v = str(v)
|
||||||
v = six.text_type(v)
|
|
||||||
|
|
||||||
yield tuple(map(cls.encode_header_item, (k, v)))
|
yield tuple(map(cls.encode_header_item, (k, v)))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def encode_header_item(cls, item):
|
def encode_header_item(cls, item):
|
||||||
if isinstance(item, six.text_type):
|
if isinstance(item, str):
|
||||||
item = cls.encode(item)
|
item = cls.encode(item)
|
||||||
|
|
||||||
# See header_translate_* constants above.
|
# See header_translate_* constants above.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode
|
from cherrypy import _json as json
|
||||||
|
from cherrypy._cpcompat import text_or_bytes, ntou
|
||||||
|
|
||||||
|
|
||||||
def json_processor(entity):
|
def json_processor(entity):
|
||||||
|
@ -9,7 +10,7 @@ def json_processor(entity):
|
||||||
|
|
||||||
body = entity.fp.read()
|
body = entity.fp.read()
|
||||||
with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
|
with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
|
||||||
cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
|
cherrypy.serving.request.json = json.decode(body.decode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
|
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
|
||||||
|
@ -56,7 +57,7 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
|
||||||
|
|
||||||
def json_handler(*args, **kwargs):
|
def json_handler(*args, **kwargs):
|
||||||
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
|
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
|
||||||
return json_encode(value)
|
return json.encode(value)
|
||||||
|
|
||||||
|
|
||||||
def json_out(content_type='application/json', debug=False,
|
def json_out(content_type='application/json', debug=False,
|
||||||
|
|
|
@ -18,13 +18,13 @@ by adding a named handler to Config.namespaces. The name can be any string,
|
||||||
and the handler must be either a callable or a context manager.
|
and the handler must be either a callable or a context manager.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from cherrypy._cpcompat import text_or_bytes
|
import builtins
|
||||||
from six.moves import configparser
|
import configparser
|
||||||
from six.moves import builtins
|
|
||||||
|
|
||||||
import operator
|
import operator
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
|
|
||||||
|
|
||||||
class NamespaceSet(dict):
|
class NamespaceSet(dict):
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ class NamespaceSet(dict):
|
||||||
namespace removed) and the config value.
|
namespace removed) and the config value.
|
||||||
|
|
||||||
Namespace handlers may be any Python callable; they may also be
|
Namespace handlers may be any Python callable; they may also be
|
||||||
Python 2.5-style 'context managers', in which case their __enter__
|
context managers, in which case their __enter__
|
||||||
method should return a callable to be used as the handler.
|
method should return a callable to be used as the handler.
|
||||||
See cherrypy.tools (the Toolbox class) for an example.
|
See cherrypy.tools (the Toolbox class) for an example.
|
||||||
"""
|
"""
|
||||||
|
@ -61,10 +61,10 @@ class NamespaceSet(dict):
|
||||||
bucket[name] = config[k]
|
bucket[name] = config[k]
|
||||||
|
|
||||||
# I chose __enter__ and __exit__ so someday this could be
|
# I chose __enter__ and __exit__ so someday this could be
|
||||||
# rewritten using Python 2.5's 'with' statement:
|
# rewritten using 'with' statement:
|
||||||
# for ns, handler in six.iteritems(self):
|
# for ns, handler in self.items():
|
||||||
# with handler as callable:
|
# with handler as callable:
|
||||||
# for k, v in six.iteritems(ns_confs.get(ns, {})):
|
# for k, v in ns_confs.get(ns, {}).items():
|
||||||
# callable(k, v)
|
# callable(k, v)
|
||||||
for ns, handler in self.items():
|
for ns, handler in self.items():
|
||||||
exit = getattr(handler, '__exit__', None)
|
exit = getattr(handler, '__exit__', None)
|
||||||
|
@ -211,122 +211,7 @@ class Parser(configparser.ConfigParser):
|
||||||
# public domain "unrepr" implementation, found on the web and then improved.
|
# public domain "unrepr" implementation, found on the web and then improved.
|
||||||
|
|
||||||
|
|
||||||
class _Builder2:
|
class _Builder:
|
||||||
|
|
||||||
def build(self, o):
|
|
||||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
|
||||||
if m is None:
|
|
||||||
raise TypeError('unrepr does not recognize %s' %
|
|
||||||
repr(o.__class__.__name__))
|
|
||||||
return m(o)
|
|
||||||
|
|
||||||
def astnode(self, s):
|
|
||||||
"""Return a Python2 ast Node compiled from a string."""
|
|
||||||
try:
|
|
||||||
import compiler
|
|
||||||
except ImportError:
|
|
||||||
# Fallback to eval when compiler package is not available,
|
|
||||||
# e.g. IronPython 1.0.
|
|
||||||
return eval(s)
|
|
||||||
|
|
||||||
p = compiler.parse('__tempvalue__ = ' + s)
|
|
||||||
return p.getChildren()[1].getChildren()[0].getChildren()[1]
|
|
||||||
|
|
||||||
def build_Subscript(self, o):
|
|
||||||
expr, flags, subs = o.getChildren()
|
|
||||||
expr = self.build(expr)
|
|
||||||
subs = self.build(subs)
|
|
||||||
return expr[subs]
|
|
||||||
|
|
||||||
def build_CallFunc(self, o):
|
|
||||||
children = o.getChildren()
|
|
||||||
# Build callee from first child
|
|
||||||
callee = self.build(children[0])
|
|
||||||
# Build args and kwargs from remaining children
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
for child in children[1:]:
|
|
||||||
class_name = child.__class__.__name__
|
|
||||||
# None is ignored
|
|
||||||
if class_name == 'NoneType':
|
|
||||||
continue
|
|
||||||
# Keywords become kwargs
|
|
||||||
if class_name == 'Keyword':
|
|
||||||
kwargs.update(self.build(child))
|
|
||||||
# Everything else becomes args
|
|
||||||
else:
|
|
||||||
args.append(self.build(child))
|
|
||||||
|
|
||||||
return callee(*args, **kwargs)
|
|
||||||
|
|
||||||
def build_Keyword(self, o):
|
|
||||||
key, value_obj = o.getChildren()
|
|
||||||
value = self.build(value_obj)
|
|
||||||
kw_dict = {key: value}
|
|
||||||
return kw_dict
|
|
||||||
|
|
||||||
def build_List(self, o):
|
|
||||||
return map(self.build, o.getChildren())
|
|
||||||
|
|
||||||
def build_Const(self, o):
|
|
||||||
return o.value
|
|
||||||
|
|
||||||
def build_Dict(self, o):
|
|
||||||
d = {}
|
|
||||||
i = iter(map(self.build, o.getChildren()))
|
|
||||||
for el in i:
|
|
||||||
d[el] = i.next()
|
|
||||||
return d
|
|
||||||
|
|
||||||
def build_Tuple(self, o):
|
|
||||||
return tuple(self.build_List(o))
|
|
||||||
|
|
||||||
def build_Name(self, o):
|
|
||||||
name = o.name
|
|
||||||
if name == 'None':
|
|
||||||
return None
|
|
||||||
if name == 'True':
|
|
||||||
return True
|
|
||||||
if name == 'False':
|
|
||||||
return False
|
|
||||||
|
|
||||||
# See if the Name is a package or module. If it is, import it.
|
|
||||||
try:
|
|
||||||
return modules(name)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# See if the Name is in builtins.
|
|
||||||
try:
|
|
||||||
return getattr(builtins, name)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise TypeError('unrepr could not resolve the name %s' % repr(name))
|
|
||||||
|
|
||||||
def build_Add(self, o):
|
|
||||||
left, right = map(self.build, o.getChildren())
|
|
||||||
return left + right
|
|
||||||
|
|
||||||
def build_Mul(self, o):
|
|
||||||
left, right = map(self.build, o.getChildren())
|
|
||||||
return left * right
|
|
||||||
|
|
||||||
def build_Getattr(self, o):
|
|
||||||
parent = self.build(o.expr)
|
|
||||||
return getattr(parent, o.attrname)
|
|
||||||
|
|
||||||
def build_NoneType(self, o):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def build_UnarySub(self, o):
|
|
||||||
return -self.build(o.getChildren()[0])
|
|
||||||
|
|
||||||
def build_UnaryAdd(self, o):
|
|
||||||
return self.build(o.getChildren()[0])
|
|
||||||
|
|
||||||
|
|
||||||
class _Builder3:
|
|
||||||
|
|
||||||
def build(self, o):
|
def build(self, o):
|
||||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
||||||
|
@ -441,7 +326,6 @@ class _Builder3:
|
||||||
|
|
||||||
# See if the Name is in builtins.
|
# See if the Name is in builtins.
|
||||||
try:
|
try:
|
||||||
import builtins
|
|
||||||
return getattr(builtins, name)
|
return getattr(builtins, name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
@ -482,10 +366,7 @@ def unrepr(s):
|
||||||
"""Return a Python object compiled from a string."""
|
"""Return a Python object compiled from a string."""
|
||||||
if not s:
|
if not s:
|
||||||
return s
|
return s
|
||||||
if sys.version_info < (3, 0):
|
b = _Builder()
|
||||||
b = _Builder2()
|
|
||||||
else:
|
|
||||||
b = _Builder3()
|
|
||||||
obj = b.astnode(s)
|
obj = b.astnode(s)
|
||||||
return b.build(obj)
|
return b.build(obj)
|
||||||
|
|
||||||
|
|
|
@ -106,10 +106,7 @@ import os
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
import binascii
|
import binascii
|
||||||
|
import pickle
|
||||||
import six
|
|
||||||
from six.moves import cPickle as pickle
|
|
||||||
import contextlib2
|
|
||||||
|
|
||||||
import zc.lockfile
|
import zc.lockfile
|
||||||
|
|
||||||
|
@ -119,10 +116,6 @@ from cherrypy.lib import locking
|
||||||
from cherrypy.lib import is_iterator
|
from cherrypy.lib import is_iterator
|
||||||
|
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
FileNotFoundError = OSError
|
|
||||||
|
|
||||||
|
|
||||||
missing = object()
|
missing = object()
|
||||||
|
|
||||||
|
|
||||||
|
@ -410,7 +403,7 @@ class RamSession(Session):
|
||||||
"""Clean up expired sessions."""
|
"""Clean up expired sessions."""
|
||||||
|
|
||||||
now = self.now()
|
now = self.now()
|
||||||
for _id, (data, expiration_time) in list(six.iteritems(self.cache)):
|
for _id, (data, expiration_time) in self.cache.copy().items():
|
||||||
if expiration_time <= now:
|
if expiration_time <= now:
|
||||||
try:
|
try:
|
||||||
del self.cache[_id]
|
del self.cache[_id]
|
||||||
|
@ -572,8 +565,6 @@ class FileSession(Session):
|
||||||
def release_lock(self, path=None):
|
def release_lock(self, path=None):
|
||||||
"""Release the lock on the currently-loaded session data."""
|
"""Release the lock on the currently-loaded session data."""
|
||||||
self.lock.close()
|
self.lock.close()
|
||||||
with contextlib2.suppress(FileNotFoundError):
|
|
||||||
os.remove(self.lock._path)
|
|
||||||
self.locked = False
|
self.locked = False
|
||||||
|
|
||||||
def clean_up(self):
|
def clean_up(self):
|
||||||
|
@ -624,7 +615,7 @@ class MemcachedSession(Session):
|
||||||
# This is a separate set of locks per session id.
|
# This is a separate set of locks per session id.
|
||||||
locks = {}
|
locks = {}
|
||||||
|
|
||||||
servers = ['127.0.0.1:11211']
|
servers = ['localhost:11211']
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup(cls, **kwargs):
|
def setup(cls, **kwargs):
|
||||||
|
|
|
@ -5,12 +5,12 @@ import platform
|
||||||
import re
|
import re
|
||||||
import stat
|
import stat
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import urllib.parse
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
from email.generator import _make_boundary as make_boundary
|
from email.generator import _make_boundary as make_boundary
|
||||||
from io import UnsupportedOperation
|
from io import UnsupportedOperation
|
||||||
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntob
|
from cherrypy._cpcompat import ntob
|
||||||
from cherrypy.lib import cptools, httputil, file_generator_limited
|
from cherrypy.lib import cptools, httputil, file_generator_limited
|
||||||
|
@ -29,6 +29,30 @@ def _setup_mimetypes():
|
||||||
_setup_mimetypes()
|
_setup_mimetypes()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_content_disposition(disposition, file_name):
|
||||||
|
"""Create HTTP header for downloading a file with a UTF-8 filename.
|
||||||
|
|
||||||
|
This function implements the recommendations of :rfc:`6266#appendix-D`.
|
||||||
|
See this and related answers: https://stackoverflow.com/a/8996249/2173868.
|
||||||
|
"""
|
||||||
|
# As normalization algorithm for `unicodedata` is used composed form (NFC
|
||||||
|
# and NFKC) with compatibility equivalence criteria (NFK), so "NFKC" is the
|
||||||
|
# one. It first applies the compatibility decomposition, followed by the
|
||||||
|
# canonical composition. Should be displayed in the same manner, should be
|
||||||
|
# treated in the same way by applications such as alphabetizing names or
|
||||||
|
# searching, and may be substituted for each other.
|
||||||
|
# See: https://en.wikipedia.org/wiki/Unicode_equivalence.
|
||||||
|
ascii_name = (
|
||||||
|
unicodedata.normalize('NFKC', file_name).
|
||||||
|
encode('ascii', errors='ignore').decode()
|
||||||
|
)
|
||||||
|
header = '{}; filename="{}"'.format(disposition, ascii_name)
|
||||||
|
if ascii_name != file_name:
|
||||||
|
quoted_name = urllib.parse.quote(file_name)
|
||||||
|
header += '; filename*=UTF-8\'\'{}'.format(quoted_name)
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
def serve_file(path, content_type=None, disposition=None, name=None,
|
def serve_file(path, content_type=None, disposition=None, name=None,
|
||||||
debug=False):
|
debug=False):
|
||||||
"""Set status, headers, and body in order to serve the given path.
|
"""Set status, headers, and body in order to serve the given path.
|
||||||
|
@ -38,9 +62,10 @@ def serve_file(path, content_type=None, disposition=None, name=None,
|
||||||
of the 'path' argument.
|
of the 'path' argument.
|
||||||
|
|
||||||
If disposition is not None, the Content-Disposition header will be set
|
If disposition is not None, the Content-Disposition header will be set
|
||||||
to "<disposition>; filename=<name>". If name is None, it will be set
|
to "<disposition>; filename=<name>; filename*=utf-8''<name>"
|
||||||
to the basename of path. If disposition is None, no Content-Disposition
|
as described in :rfc:`6266#appendix-D`.
|
||||||
header will be written.
|
If name is None, it will be set to the basename of path.
|
||||||
|
If disposition is None, no Content-Disposition header will be written.
|
||||||
"""
|
"""
|
||||||
response = cherrypy.serving.response
|
response = cherrypy.serving.response
|
||||||
|
|
||||||
|
@ -93,7 +118,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
|
||||||
if disposition is not None:
|
if disposition is not None:
|
||||||
if name is None:
|
if name is None:
|
||||||
name = os.path.basename(path)
|
name = os.path.basename(path)
|
||||||
cd = '%s; filename="%s"' % (disposition, name)
|
cd = _make_content_disposition(disposition, name)
|
||||||
response.headers['Content-Disposition'] = cd
|
response.headers['Content-Disposition'] = cd
|
||||||
if debug:
|
if debug:
|
||||||
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
||||||
|
@ -112,9 +137,10 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
|
||||||
The Content-Type header will be set to the content_type arg, if provided.
|
The Content-Type header will be set to the content_type arg, if provided.
|
||||||
|
|
||||||
If disposition is not None, the Content-Disposition header will be set
|
If disposition is not None, the Content-Disposition header will be set
|
||||||
to "<disposition>; filename=<name>". If name is None, 'filename' will
|
to "<disposition>; filename=<name>; filename*=utf-8''<name>"
|
||||||
not be set. If disposition is None, no Content-Disposition header will
|
as described in :rfc:`6266#appendix-D`.
|
||||||
be written.
|
If name is None, 'filename' will not be set.
|
||||||
|
If disposition is None, no Content-Disposition header will be written.
|
||||||
|
|
||||||
CAUTION: If the request contains a 'Range' header, one or more seek()s will
|
CAUTION: If the request contains a 'Range' header, one or more seek()s will
|
||||||
be performed on the file object. This may cause undesired behavior if
|
be performed on the file object. This may cause undesired behavior if
|
||||||
|
@ -150,7 +176,7 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
|
||||||
if name is None:
|
if name is None:
|
||||||
cd = disposition
|
cd = disposition
|
||||||
else:
|
else:
|
||||||
cd = '%s; filename="%s"' % (disposition, name)
|
cd = _make_content_disposition(disposition, name)
|
||||||
response.headers['Content-Disposition'] = cd
|
response.headers['Content-Disposition'] = cd
|
||||||
if debug:
|
if debug:
|
||||||
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""XML-RPC tool helpers."""
|
"""XML-RPC tool helpers."""
|
||||||
import sys
|
import sys
|
||||||
|
from xmlrpc.client import (
|
||||||
from six.moves.xmlrpc_client import (
|
|
||||||
loads as xmlrpc_loads, dumps as xmlrpc_dumps,
|
loads as xmlrpc_loads, dumps as xmlrpc_dumps,
|
||||||
Fault as XMLRPCFault
|
Fault as XMLRPCFault
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,11 +6,10 @@ import signal as _signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
import _thread
|
||||||
from six.moves import _thread
|
|
||||||
|
|
||||||
from cherrypy._cpcompat import text_or_bytes
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
from cherrypy._cpcompat import ntob, Timer
|
from cherrypy._cpcompat import ntob
|
||||||
|
|
||||||
# _module__file__base is used by Autoreload to make
|
# _module__file__base is used by Autoreload to make
|
||||||
# absolute any filenames retrieved from sys.modules which are not
|
# absolute any filenames retrieved from sys.modules which are not
|
||||||
|
@ -367,7 +366,7 @@ class Daemonizer(SimplePlugin):
|
||||||
# "The general problem with making fork() work in a multi-threaded
|
# "The general problem with making fork() work in a multi-threaded
|
||||||
# world is what to do with all of the threads..."
|
# world is what to do with all of the threads..."
|
||||||
# So we check for active threads:
|
# So we check for active threads:
|
||||||
if threading.activeCount() != 1:
|
if threading.active_count() != 1:
|
||||||
self.bus.log('There are %r active threads. '
|
self.bus.log('There are %r active threads. '
|
||||||
'Daemonizing now may cause strange failures.' %
|
'Daemonizing now may cause strange failures.' %
|
||||||
threading.enumerate(), level=30)
|
threading.enumerate(), level=30)
|
||||||
|
@ -452,7 +451,7 @@ class PIDFile(SimplePlugin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PerpetualTimer(Timer):
|
class PerpetualTimer(threading.Timer):
|
||||||
|
|
||||||
"""A responsive subclass of threading.Timer whose run() method repeats.
|
"""A responsive subclass of threading.Timer whose run() method repeats.
|
||||||
|
|
||||||
|
@ -553,7 +552,7 @@ class Monitor(SimplePlugin):
|
||||||
if self.thread is None:
|
if self.thread is None:
|
||||||
self.thread = BackgroundTask(self.frequency, self.callback,
|
self.thread = BackgroundTask(self.frequency, self.callback,
|
||||||
bus=self.bus)
|
bus=self.bus)
|
||||||
self.thread.setName(threadname)
|
self.thread.name = threadname
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
self.bus.log('Started monitor thread %r.' % threadname)
|
self.bus.log('Started monitor thread %r.' % threadname)
|
||||||
else:
|
else:
|
||||||
|
@ -566,8 +565,8 @@ class Monitor(SimplePlugin):
|
||||||
self.bus.log('No thread running for %s.' %
|
self.bus.log('No thread running for %s.' %
|
||||||
self.name or self.__class__.__name__)
|
self.name or self.__class__.__name__)
|
||||||
else:
|
else:
|
||||||
if self.thread is not threading.currentThread():
|
if self.thread is not threading.current_thread():
|
||||||
name = self.thread.getName()
|
name = self.thread.name
|
||||||
self.thread.cancel()
|
self.thread.cancel()
|
||||||
if not self.thread.daemon:
|
if not self.thread.daemon:
|
||||||
self.bus.log('Joining %r' % name)
|
self.bus.log('Joining %r' % name)
|
||||||
|
@ -627,7 +626,10 @@ class Autoreloader(Monitor):
|
||||||
|
|
||||||
def sysfiles(self):
|
def sysfiles(self):
|
||||||
"""Return a Set of sys.modules filenames to monitor."""
|
"""Return a Set of sys.modules filenames to monitor."""
|
||||||
search_mod_names = filter(re.compile(self.match).match, sys.modules)
|
search_mod_names = filter(
|
||||||
|
re.compile(self.match).match,
|
||||||
|
list(sys.modules.keys()),
|
||||||
|
)
|
||||||
mods = map(sys.modules.get, search_mod_names)
|
mods = map(sys.modules.get, search_mod_names)
|
||||||
return set(filter(None, map(self._file_for_module, mods)))
|
return set(filter(None, map(self._file_for_module, mods)))
|
||||||
|
|
||||||
|
@ -690,7 +692,7 @@ class Autoreloader(Monitor):
|
||||||
filename)
|
filename)
|
||||||
self.thread.cancel()
|
self.thread.cancel()
|
||||||
self.bus.log('Stopped thread %r.' %
|
self.bus.log('Stopped thread %r.' %
|
||||||
self.thread.getName())
|
self.thread.name)
|
||||||
self.bus.restart()
|
self.bus.restart()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -178,7 +178,7 @@ class ServerAdapter(object):
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
t = threading.Thread(target=self._start_http_thread)
|
t = threading.Thread(target=self._start_http_thread)
|
||||||
t.setName('HTTPServer ' + t.getName())
|
t.name = 'HTTPServer ' + t.name
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
self.wait()
|
self.wait()
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.is_set:
|
if self.is_set:
|
||||||
self.bus.log('Handler for console events already set.', level=40)
|
self.bus.log('Handler for console events already set.', level=20)
|
||||||
return
|
return
|
||||||
|
|
||||||
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
|
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
|
||||||
|
@ -28,12 +28,12 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||||
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
|
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
|
||||||
win32api.GetLastError(), level=40)
|
win32api.GetLastError(), level=40)
|
||||||
else:
|
else:
|
||||||
self.bus.log('Set handler for console events.', level=40)
|
self.bus.log('Set handler for console events.', level=20)
|
||||||
self.is_set = True
|
self.is_set = True
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if not self.is_set:
|
if not self.is_set:
|
||||||
self.bus.log('Handler for console events already off.', level=40)
|
self.bus.log('Handler for console events already off.', level=20)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -46,7 +46,7 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||||
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
|
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
|
||||||
win32api.GetLastError(), level=40)
|
win32api.GetLastError(), level=40)
|
||||||
else:
|
else:
|
||||||
self.bus.log('Removed handler for console events.', level=40)
|
self.bus.log('Removed handler for console events.', level=20)
|
||||||
self.is_set = False
|
self.is_set = False
|
||||||
|
|
||||||
def handle(self, event):
|
def handle(self, event):
|
||||||
|
|
|
@ -81,7 +81,7 @@ import warnings
|
||||||
import subprocess
|
import subprocess
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
import six
|
from more_itertools import always_iterable
|
||||||
|
|
||||||
|
|
||||||
# Here I save the value of os.getcwd(), which, if I am imported early enough,
|
# Here I save the value of os.getcwd(), which, if I am imported early enough,
|
||||||
|
@ -356,13 +356,13 @@ class Bus(object):
|
||||||
# implemented as a windows service and in any other case
|
# implemented as a windows service and in any other case
|
||||||
# that another thread executes cherrypy.engine.exit()
|
# that another thread executes cherrypy.engine.exit()
|
||||||
if (
|
if (
|
||||||
t != threading.currentThread() and
|
t != threading.current_thread() and
|
||||||
not isinstance(t, threading._MainThread) and
|
not isinstance(t, threading._MainThread) and
|
||||||
# Note that any dummy (external) threads are
|
# Note that any dummy (external) threads are
|
||||||
# always daemonic.
|
# always daemonic.
|
||||||
not t.daemon
|
not t.daemon
|
||||||
):
|
):
|
||||||
self.log('Waiting for thread %s.' % t.getName())
|
self.log('Waiting for thread %s.' % t.name)
|
||||||
t.join()
|
t.join()
|
||||||
|
|
||||||
if self.execv:
|
if self.execv:
|
||||||
|
@ -370,10 +370,7 @@ class Bus(object):
|
||||||
|
|
||||||
def wait(self, state, interval=0.1, channel=None):
|
def wait(self, state, interval=0.1, channel=None):
|
||||||
"""Poll for the given state(s) at intervals; publish to channel."""
|
"""Poll for the given state(s) at intervals; publish to channel."""
|
||||||
if isinstance(state, (tuple, list)):
|
states = set(always_iterable(state))
|
||||||
states = state
|
|
||||||
else:
|
|
||||||
states = [state]
|
|
||||||
|
|
||||||
while self.state not in states:
|
while self.state not in states:
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
@ -436,7 +433,7 @@ class Bus(object):
|
||||||
:seealso: http://stackoverflow.com/a/28414807/595220
|
:seealso: http://stackoverflow.com/a/28414807/595220
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
|
char_p = ctypes.c_wchar_p
|
||||||
|
|
||||||
argv = ctypes.POINTER(char_p)()
|
argv = ctypes.POINTER(char_p)()
|
||||||
argc = ctypes.c_int()
|
argc = ctypes.c_int()
|
||||||
|
@ -573,7 +570,7 @@ class Bus(object):
|
||||||
self.wait(states.STARTED)
|
self.wait(states.STARTED)
|
||||||
func(*a, **kw)
|
func(*a, **kw)
|
||||||
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
|
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
|
||||||
t.setName('Bus Callback ' + t.getName())
|
t.name = 'Bus Callback ' + t.name
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
self.start()
|
self.start()
|
||||||
|
|
|
@ -10,10 +10,10 @@ import sys
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
import contextlib
|
||||||
|
|
||||||
import portend
|
import portend
|
||||||
import pytest
|
import pytest
|
||||||
import six
|
|
||||||
|
|
||||||
from cheroot.test import webtest
|
from cheroot.test import webtest
|
||||||
|
|
||||||
|
@ -93,8 +93,7 @@ class LocalSupervisor(Supervisor):
|
||||||
|
|
||||||
cherrypy.engine.exit()
|
cherrypy.engine.exit()
|
||||||
|
|
||||||
servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {})))
|
for name, server in getattr(cherrypy, 'servers', {}).copy().items():
|
||||||
for name, server in servers_copy:
|
|
||||||
server.unsubscribe()
|
server.unsubscribe()
|
||||||
del cherrypy.servers[name]
|
del cherrypy.servers[name]
|
||||||
|
|
||||||
|
@ -311,19 +310,12 @@ class CPWebCase(webtest.WebCase):
|
||||||
def exit(self):
|
def exit(self):
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
def getPage(self, url, headers=None, method='GET', body=None,
|
def getPage(self, url, *args, **kwargs):
|
||||||
protocol=None, raise_subcls=None):
|
"""Open the url.
|
||||||
"""Open the url. Return status, headers, body.
|
|
||||||
|
|
||||||
`raise_subcls` must be a tuple with the exceptions classes
|
|
||||||
or a single exception class that are not going to be considered
|
|
||||||
a socket.error regardless that they were are subclass of a
|
|
||||||
socket.error and therefore not considered for a connection retry.
|
|
||||||
"""
|
"""
|
||||||
if self.script_name:
|
if self.script_name:
|
||||||
url = httputil.urljoin(self.script_name, url)
|
url = httputil.urljoin(self.script_name, url)
|
||||||
return webtest.WebCase.getPage(self, url, headers, method, body,
|
return webtest.WebCase.getPage(self, url, *args, **kwargs)
|
||||||
protocol, raise_subcls)
|
|
||||||
|
|
||||||
def skip(self, msg='skipped '):
|
def skip(self, msg='skipped '):
|
||||||
pytest.skip(msg)
|
pytest.skip(msg)
|
||||||
|
@ -449,7 +441,7 @@ server.ssl_private_key: r'%s'
|
||||||
'extra': extra,
|
'extra': extra,
|
||||||
}
|
}
|
||||||
with io.open(self.config_file, 'w', encoding='utf-8') as f:
|
with io.open(self.config_file, 'w', encoding='utf-8') as f:
|
||||||
f.write(six.text_type(conf))
|
f.write(str(conf))
|
||||||
|
|
||||||
def start(self, imports=None):
|
def start(self, imports=None):
|
||||||
"""Start cherryd in a subprocess."""
|
"""Start cherryd in a subprocess."""
|
||||||
|
@ -523,20 +515,5 @@ server.ssl_private_key: r'%s'
|
||||||
self._proc.wait()
|
self._proc.wait()
|
||||||
|
|
||||||
def _join_daemon(self):
|
def _join_daemon(self):
|
||||||
try:
|
with contextlib.suppress(IOError):
|
||||||
try:
|
os.waitpid(self.get_pid(), 0)
|
||||||
# Mac, UNIX
|
|
||||||
os.wait()
|
|
||||||
except AttributeError:
|
|
||||||
# Windows
|
|
||||||
try:
|
|
||||||
pid = self.get_pid()
|
|
||||||
except IOError:
|
|
||||||
# Assume the subprocess deleted the pidfile on shutdown.
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
os.waitpid(pid, 0)
|
|
||||||
except OSError:
|
|
||||||
x = sys.exc_info()[1]
|
|
||||||
if x.args != (10, 'No child processes'):
|
|
||||||
raise
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import sys
|
||||||
import time
|
import time
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import six
|
import pytest
|
||||||
|
|
||||||
from cherrypy._cpcompat import text_or_bytes, ntob
|
from cherrypy._cpcompat import text_or_bytes
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -45,6 +45,7 @@ class LogCase(object):
|
||||||
unique enough from normal log output to use for marker identification.
|
unique enough from normal log output to use for marker identification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
interactive = False
|
||||||
logfile = None
|
logfile = None
|
||||||
lastmarker = None
|
lastmarker = None
|
||||||
markerPrefix = b'test suite marker: '
|
markerPrefix = b'test suite marker: '
|
||||||
|
@ -54,7 +55,7 @@ class LogCase(object):
|
||||||
print(' ERROR: %s' % msg)
|
print(' ERROR: %s' % msg)
|
||||||
|
|
||||||
if not self.interactive:
|
if not self.interactive:
|
||||||
raise self.failureException(msg)
|
raise pytest.fail(msg)
|
||||||
|
|
||||||
p = (' Show: '
|
p = (' Show: '
|
||||||
'[L]og [M]arker [P]attern; '
|
'[L]og [M]arker [P]attern; '
|
||||||
|
@ -86,7 +87,7 @@ class LogCase(object):
|
||||||
# return without raising the normal exception
|
# return without raising the normal exception
|
||||||
return
|
return
|
||||||
elif i == 'R':
|
elif i == 'R':
|
||||||
raise self.failureException(msg)
|
raise pytest.fail(msg)
|
||||||
elif i == 'X':
|
elif i == 'X':
|
||||||
self.exit()
|
self.exit()
|
||||||
sys.stdout.write(p + ' ')
|
sys.stdout.write(p + ' ')
|
||||||
|
@ -105,7 +106,9 @@ class LogCase(object):
|
||||||
self.lastmarker = key
|
self.lastmarker = key
|
||||||
|
|
||||||
open(self.logfile, 'ab+').write(
|
open(self.logfile, 'ab+').write(
|
||||||
ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8'))
|
b'%s%s\n'
|
||||||
|
% (self.markerPrefix, key.encode('utf-8'))
|
||||||
|
)
|
||||||
|
|
||||||
def _read_marked_region(self, marker=None):
|
def _read_marked_region(self, marker=None):
|
||||||
"""Return lines from self.logfile in the marked region.
|
"""Return lines from self.logfile in the marked region.
|
||||||
|
@ -121,7 +124,7 @@ class LogCase(object):
|
||||||
if marker is None:
|
if marker is None:
|
||||||
return open(logfile, 'rb').readlines()
|
return open(logfile, 'rb').readlines()
|
||||||
|
|
||||||
if isinstance(marker, six.text_type):
|
if isinstance(marker, str):
|
||||||
marker = marker.encode('utf-8')
|
marker = marker.encode('utf-8')
|
||||||
data = []
|
data = []
|
||||||
in_region = False
|
in_region = False
|
||||||
|
@ -201,7 +204,7 @@ class LogCase(object):
|
||||||
# Single arg. Use __getitem__ and allow lines to be str or list.
|
# Single arg. Use __getitem__ and allow lines to be str or list.
|
||||||
if isinstance(lines, (tuple, list)):
|
if isinstance(lines, (tuple, list)):
|
||||||
lines = lines[0]
|
lines = lines[0]
|
||||||
if isinstance(lines, six.text_type):
|
if isinstance(lines, str):
|
||||||
lines = lines.encode('utf-8')
|
lines = lines.encode('utf-8')
|
||||||
if lines not in data[sliceargs]:
|
if lines not in data[sliceargs]:
|
||||||
msg = '%r not found on log line %r' % (lines, sliceargs)
|
msg = '%r not found on log line %r' % (lines, sliceargs)
|
||||||
|
@ -221,7 +224,7 @@ class LogCase(object):
|
||||||
|
|
||||||
start, stop = sliceargs
|
start, stop = sliceargs
|
||||||
for line, logline in zip(lines, data[start:stop]):
|
for line, logline in zip(lines, data[start:stop]):
|
||||||
if isinstance(line, six.text_type):
|
if isinstance(line, str):
|
||||||
line = line.encode('utf-8')
|
line = line.encode('utf-8')
|
||||||
if line not in logline:
|
if line not in logline:
|
||||||
msg = '%r not found in log' % line
|
msg = '%r not found in log' % line
|
||||||
|
|
|
@ -9,18 +9,18 @@ create a symlink to them if needed.
|
||||||
KNOWN BUGS
|
KNOWN BUGS
|
||||||
==========
|
==========
|
||||||
|
|
||||||
##1. Apache processes Range headers automatically; CherryPy's truncated
|
1. Apache processes Range headers automatically; CherryPy's truncated
|
||||||
## output is then truncated again by Apache. See test_core.testRanges.
|
output is then truncated again by Apache. See test_core.testRanges.
|
||||||
## This was worked around in http://www.cherrypy.org/changeset/1319.
|
This was worked around in http://www.cherrypy.org/changeset/1319.
|
||||||
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
|
||||||
See test_core.testHTTPMethods.
|
See test_core.testHTTPMethods.
|
||||||
3. Max request header and body settings do not work with Apache.
|
3. Max request header and body settings do not work with Apache.
|
||||||
##4. Apache replaces status "reason phrases" automatically. For example,
|
4. Apache replaces status "reason phrases" automatically. For example,
|
||||||
## CherryPy may set "304 Not modified" but Apache will write out
|
CherryPy may set "304 Not modified" but Apache will write out
|
||||||
## "304 Not Modified" (capital "M").
|
"304 Not Modified" (capital "M").
|
||||||
##5. Apache does not allow custom error codes as per the spec.
|
5. Apache does not allow custom error codes as per the spec.
|
||||||
##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
|
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
|
||||||
## Request-URI too early.
|
Request-URI too early.
|
||||||
7. mod_wsgi will not read request bodies which use the "chunked"
|
7. mod_wsgi will not read request bodies which use the "chunked"
|
||||||
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
|
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
|
||||||
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
|
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
|
||||||
|
|
|
@ -5,8 +5,6 @@ import calendar
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib import sessions
|
from cherrypy.lib import sessions
|
||||||
|
|
||||||
|
@ -123,7 +121,7 @@ class Root(object):
|
||||||
'changemsg': '<br>'.join(changemsg),
|
'changemsg': '<br>'.join(changemsg),
|
||||||
'respcookie': cherrypy.response.cookie.output(),
|
'respcookie': cherrypy.response.cookie.output(),
|
||||||
'reqcookie': cherrypy.request.cookie.output(),
|
'reqcookie': cherrypy.request.cookie.output(),
|
||||||
'sessiondata': list(six.iteritems(cherrypy.session)),
|
'sessiondata': list(cherrypy.session.items()),
|
||||||
'servertime': (
|
'servertime': (
|
||||||
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
|
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib import auth_digest
|
from cherrypy.lib import auth_digest
|
||||||
|
@ -92,8 +90,7 @@ class DigestAuthTest(helper.CPWebCase):
|
||||||
'cnonce="1522e61005789929"')
|
'cnonce="1522e61005789929"')
|
||||||
|
|
||||||
encoded_user = username
|
encoded_user = username
|
||||||
if six.PY3:
|
encoded_user = encoded_user.encode('utf-8')
|
||||||
encoded_user = encoded_user.encode('utf-8')
|
|
||||||
encoded_user = encoded_user.decode('latin1')
|
encoded_user = encoded_user.decode('latin1')
|
||||||
auth_header = base_auth % (
|
auth_header = base_auth % (
|
||||||
encoded_user, realm, nonce, test_uri,
|
encoded_user, realm, nonce, test_uri,
|
||||||
|
|
|
@ -1,274 +1,327 @@
|
||||||
|
"""Publish-subscribe bus tests."""
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest.mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from cherrypy.process import wspbus
|
from cherrypy.process import wspbus
|
||||||
|
|
||||||
|
|
||||||
msg = 'Listener %d on channel %s: %s.'
|
CI_ON_MACOS = bool(os.getenv('CI')) and sys.platform == 'darwin'
|
||||||
|
msg = 'Listener %d on channel %s: %s.' # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
class PublishSubscribeTests(unittest.TestCase):
|
@pytest.fixture
|
||||||
|
def bus():
|
||||||
def get_listener(self, channel, index):
|
"""Return a wspbus instance."""
|
||||||
def listener(arg=None):
|
return wspbus.Bus()
|
||||||
self.responses.append(msg % (index, channel, arg))
|
|
||||||
return listener
|
|
||||||
|
|
||||||
def test_builtin_channels(self):
|
|
||||||
b = wspbus.Bus()
|
|
||||||
|
|
||||||
self.responses, expected = [], []
|
|
||||||
|
|
||||||
for channel in b.listeners:
|
|
||||||
for index, priority in enumerate([100, 50, 0, 51]):
|
|
||||||
b.subscribe(channel,
|
|
||||||
self.get_listener(channel, index), priority)
|
|
||||||
|
|
||||||
for channel in b.listeners:
|
|
||||||
b.publish(channel)
|
|
||||||
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
|
|
||||||
b.publish(channel, arg=79347)
|
|
||||||
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
|
|
||||||
|
|
||||||
self.assertEqual(self.responses, expected)
|
|
||||||
|
|
||||||
def test_custom_channels(self):
|
|
||||||
b = wspbus.Bus()
|
|
||||||
|
|
||||||
self.responses, expected = [], []
|
|
||||||
|
|
||||||
custom_listeners = ('hugh', 'louis', 'dewey')
|
|
||||||
for channel in custom_listeners:
|
|
||||||
for index, priority in enumerate([None, 10, 60, 40]):
|
|
||||||
b.subscribe(channel,
|
|
||||||
self.get_listener(channel, index), priority)
|
|
||||||
|
|
||||||
for channel in custom_listeners:
|
|
||||||
b.publish(channel, 'ah so')
|
|
||||||
expected.extend([msg % (i, channel, 'ah so')
|
|
||||||
for i in (1, 3, 0, 2)])
|
|
||||||
b.publish(channel)
|
|
||||||
expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
|
|
||||||
|
|
||||||
self.assertEqual(self.responses, expected)
|
|
||||||
|
|
||||||
def test_listener_errors(self):
|
|
||||||
b = wspbus.Bus()
|
|
||||||
|
|
||||||
self.responses, expected = [], []
|
|
||||||
channels = [c for c in b.listeners if c != 'log']
|
|
||||||
|
|
||||||
for channel in channels:
|
|
||||||
b.subscribe(channel, self.get_listener(channel, 1))
|
|
||||||
# This will break since the lambda takes no args.
|
|
||||||
b.subscribe(channel, lambda: None, priority=20)
|
|
||||||
|
|
||||||
for channel in channels:
|
|
||||||
self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
|
|
||||||
expected.append(msg % (1, channel, 123))
|
|
||||||
|
|
||||||
self.assertEqual(self.responses, expected)
|
|
||||||
|
|
||||||
|
|
||||||
class BusMethodTests(unittest.TestCase):
|
@pytest.fixture
|
||||||
|
def log_tracker(bus):
|
||||||
|
"""Return an instance of bus log tracker."""
|
||||||
|
class LogTracker: # pylint: disable=too-few-public-methods
|
||||||
|
"""Bus log tracker."""
|
||||||
|
|
||||||
def log(self, bus):
|
log_entries = []
|
||||||
self._log_entries = []
|
|
||||||
|
|
||||||
def logit(msg, level):
|
def __init__(self, bus):
|
||||||
self._log_entries.append(msg)
|
def logit(msg, level): # pylint: disable=unused-argument
|
||||||
bus.subscribe('log', logit)
|
self.log_entries.append(msg)
|
||||||
|
bus.subscribe('log', logit)
|
||||||
|
|
||||||
def assertLog(self, entries):
|
return LogTracker(bus)
|
||||||
self.assertEqual(self._log_entries, entries)
|
|
||||||
|
|
||||||
def get_listener(self, channel, index):
|
|
||||||
def listener(arg=None):
|
|
||||||
self.responses.append(msg % (index, channel, arg))
|
|
||||||
return listener
|
|
||||||
|
|
||||||
def test_start(self):
|
@pytest.fixture
|
||||||
b = wspbus.Bus()
|
def listener():
|
||||||
self.log(b)
|
"""Return an instance of bus response tracker."""
|
||||||
|
class Listner: # pylint: disable=too-few-public-methods
|
||||||
|
"""Bus handler return value tracker."""
|
||||||
|
|
||||||
self.responses = []
|
responses = []
|
||||||
num = 3
|
|
||||||
for index in range(num):
|
|
||||||
b.subscribe('start', self.get_listener('start', index))
|
|
||||||
|
|
||||||
b.start()
|
def get_listener(self, channel, index):
|
||||||
try:
|
"""Return an argument tracking listener."""
|
||||||
# The start method MUST call all 'start' listeners.
|
def listener(arg=None):
|
||||||
self.assertEqual(
|
self.responses.append(msg % (index, channel, arg))
|
||||||
set(self.responses),
|
return listener
|
||||||
set([msg % (i, 'start', None) for i in range(num)]))
|
|
||||||
# The start method MUST move the state to STARTED
|
|
||||||
# (or EXITING, if errors occur)
|
|
||||||
self.assertEqual(b.state, b.states.STARTED)
|
|
||||||
# The start method MUST log its states.
|
|
||||||
self.assertLog(['Bus STARTING', 'Bus STARTED'])
|
|
||||||
finally:
|
|
||||||
# Exit so the atexit handler doesn't complain.
|
|
||||||
b.exit()
|
|
||||||
|
|
||||||
def test_stop(self):
|
return Listner()
|
||||||
b = wspbus.Bus()
|
|
||||||
self.log(b)
|
|
||||||
|
|
||||||
self.responses = []
|
|
||||||
num = 3
|
|
||||||
for index in range(num):
|
|
||||||
b.subscribe('stop', self.get_listener('stop', index))
|
|
||||||
|
|
||||||
b.stop()
|
def test_builtin_channels(bus, listener):
|
||||||
|
"""Test that built-in channels trigger corresponding listeners."""
|
||||||
|
expected = []
|
||||||
|
|
||||||
# The stop method MUST call all 'stop' listeners.
|
for channel in bus.listeners:
|
||||||
self.assertEqual(set(self.responses),
|
for index, priority in enumerate([100, 50, 0, 51]):
|
||||||
set([msg % (i, 'stop', None) for i in range(num)]))
|
bus.subscribe(
|
||||||
# The stop method MUST move the state to STOPPED
|
channel,
|
||||||
self.assertEqual(b.state, b.states.STOPPED)
|
listener.get_listener(channel, index),
|
||||||
# The stop method MUST log its states.
|
priority,
|
||||||
self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
|
)
|
||||||
|
|
||||||
def test_graceful(self):
|
for channel in bus.listeners:
|
||||||
b = wspbus.Bus()
|
bus.publish(channel)
|
||||||
self.log(b)
|
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
|
||||||
|
bus.publish(channel, arg=79347)
|
||||||
|
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
|
||||||
|
|
||||||
self.responses = []
|
assert listener.responses == expected
|
||||||
num = 3
|
|
||||||
for index in range(num):
|
|
||||||
b.subscribe('graceful', self.get_listener('graceful', index))
|
|
||||||
|
|
||||||
b.graceful()
|
|
||||||
|
|
||||||
# The graceful method MUST call all 'graceful' listeners.
|
def test_custom_channels(bus, listener):
|
||||||
self.assertEqual(
|
"""Test that custom pub-sub channels work as built-in ones."""
|
||||||
set(self.responses),
|
expected = []
|
||||||
set([msg % (i, 'graceful', None) for i in range(num)]))
|
|
||||||
# The graceful method MUST log its states.
|
|
||||||
self.assertLog(['Bus graceful'])
|
|
||||||
|
|
||||||
def test_exit(self):
|
custom_listeners = ('hugh', 'louis', 'dewey')
|
||||||
b = wspbus.Bus()
|
for channel in custom_listeners:
|
||||||
self.log(b)
|
for index, priority in enumerate([None, 10, 60, 40]):
|
||||||
|
bus.subscribe(
|
||||||
|
channel,
|
||||||
|
listener.get_listener(channel, index),
|
||||||
|
priority,
|
||||||
|
)
|
||||||
|
|
||||||
self.responses = []
|
for channel in custom_listeners:
|
||||||
num = 3
|
bus.publish(channel, 'ah so')
|
||||||
for index in range(num):
|
expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2))
|
||||||
b.subscribe('stop', self.get_listener('stop', index))
|
bus.publish(channel)
|
||||||
b.subscribe('exit', self.get_listener('exit', index))
|
expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2))
|
||||||
|
|
||||||
b.exit()
|
assert listener.responses == expected
|
||||||
|
|
||||||
# The exit method MUST call all 'stop' listeners,
|
|
||||||
# and then all 'exit' listeners.
|
def test_listener_errors(bus, listener):
|
||||||
self.assertEqual(set(self.responses),
|
"""Test that unhandled exceptions raise channel failures."""
|
||||||
set([msg % (i, 'stop', None) for i in range(num)] +
|
expected = []
|
||||||
[msg % (i, 'exit', None) for i in range(num)]))
|
channels = [c for c in bus.listeners if c != 'log']
|
||||||
# The exit method MUST move the state to EXITING
|
|
||||||
self.assertEqual(b.state, b.states.EXITING)
|
for channel in channels:
|
||||||
# The exit method MUST log its states.
|
bus.subscribe(channel, listener.get_listener(channel, 1))
|
||||||
self.assertLog(
|
# This will break since the lambda takes no args.
|
||||||
|
bus.subscribe(channel, lambda: None, priority=20)
|
||||||
|
|
||||||
|
for channel in channels:
|
||||||
|
with pytest.raises(wspbus.ChannelFailures):
|
||||||
|
bus.publish(channel, 123)
|
||||||
|
expected.append(msg % (1, channel, 123))
|
||||||
|
|
||||||
|
assert listener.responses == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_start(bus, listener, log_tracker):
|
||||||
|
"""Test that bus start sequence calls all listeners."""
|
||||||
|
num = 3
|
||||||
|
for index in range(num):
|
||||||
|
bus.subscribe('start', listener.get_listener('start', index))
|
||||||
|
|
||||||
|
bus.start()
|
||||||
|
try:
|
||||||
|
# The start method MUST call all 'start' listeners.
|
||||||
|
assert (
|
||||||
|
set(listener.responses) ==
|
||||||
|
set(msg % (i, 'start', None) for i in range(num)))
|
||||||
|
# The start method MUST move the state to STARTED
|
||||||
|
# (or EXITING, if errors occur)
|
||||||
|
assert bus.state == bus.states.STARTED
|
||||||
|
# The start method MUST log its states.
|
||||||
|
assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED']
|
||||||
|
finally:
|
||||||
|
# Exit so the atexit handler doesn't complain.
|
||||||
|
bus.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_stop(bus, listener, log_tracker):
|
||||||
|
"""Test that bus stop sequence calls all listeners."""
|
||||||
|
num = 3
|
||||||
|
|
||||||
|
for index in range(num):
|
||||||
|
bus.subscribe('stop', listener.get_listener('stop', index))
|
||||||
|
|
||||||
|
bus.stop()
|
||||||
|
|
||||||
|
# The stop method MUST call all 'stop' listeners.
|
||||||
|
assert (set(listener.responses) ==
|
||||||
|
set(msg % (i, 'stop', None) for i in range(num)))
|
||||||
|
|
||||||
|
# The stop method MUST move the state to STOPPED
|
||||||
|
assert bus.state == bus.states.STOPPED
|
||||||
|
|
||||||
|
# The stop method MUST log its states.
|
||||||
|
assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED']
|
||||||
|
|
||||||
|
|
||||||
|
def test_graceful(bus, listener, log_tracker):
|
||||||
|
"""Test that bus graceful state triggers all listeners."""
|
||||||
|
num = 3
|
||||||
|
|
||||||
|
for index in range(num):
|
||||||
|
bus.subscribe('graceful', listener.get_listener('graceful', index))
|
||||||
|
|
||||||
|
bus.graceful()
|
||||||
|
|
||||||
|
# The graceful method MUST call all 'graceful' listeners.
|
||||||
|
assert (
|
||||||
|
set(listener.responses) ==
|
||||||
|
set(msg % (i, 'graceful', None) for i in range(num)))
|
||||||
|
|
||||||
|
# The graceful method MUST log its states.
|
||||||
|
assert log_tracker.log_entries == ['Bus graceful']
|
||||||
|
|
||||||
|
|
||||||
|
def test_exit(bus, listener, log_tracker):
|
||||||
|
"""Test that bus exit sequence is correct."""
|
||||||
|
num = 3
|
||||||
|
|
||||||
|
for index in range(num):
|
||||||
|
bus.subscribe('stop', listener.get_listener('stop', index))
|
||||||
|
bus.subscribe('exit', listener.get_listener('exit', index))
|
||||||
|
|
||||||
|
bus.exit()
|
||||||
|
|
||||||
|
# The exit method MUST call all 'stop' listeners,
|
||||||
|
# and then all 'exit' listeners.
|
||||||
|
assert (set(listener.responses) ==
|
||||||
|
set([msg % (i, 'stop', None) for i in range(num)] +
|
||||||
|
[msg % (i, 'exit', None) for i in range(num)]))
|
||||||
|
|
||||||
|
# The exit method MUST move the state to EXITING
|
||||||
|
assert bus.state == bus.states.EXITING
|
||||||
|
|
||||||
|
# The exit method MUST log its states.
|
||||||
|
assert (log_tracker.log_entries ==
|
||||||
['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
|
['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
|
||||||
|
|
||||||
def test_wait(self):
|
|
||||||
b = wspbus.Bus()
|
|
||||||
|
|
||||||
def f(method):
|
def test_wait(bus):
|
||||||
time.sleep(0.2)
|
"""Test that bus wait awaits for states."""
|
||||||
getattr(b, method)()
|
def f(method): # pylint: disable=invalid-name
|
||||||
|
time.sleep(0.2)
|
||||||
|
getattr(bus, method)()
|
||||||
|
|
||||||
for method, states in [('start', [b.states.STARTED]),
|
flow = [
|
||||||
('stop', [b.states.STOPPED]),
|
('start', [bus.states.STARTED]),
|
||||||
('start',
|
('stop', [bus.states.STOPPED]),
|
||||||
[b.states.STARTING, b.states.STARTED]),
|
('start', [bus.states.STARTING, bus.states.STARTED]),
|
||||||
('exit', [b.states.EXITING]),
|
('exit', [bus.states.EXITING]),
|
||||||
]:
|
]
|
||||||
threading.Thread(target=f, args=(method,)).start()
|
|
||||||
b.wait(states)
|
|
||||||
|
|
||||||
# The wait method MUST wait for the given state(s).
|
for method, states in flow:
|
||||||
if b.state not in states:
|
threading.Thread(target=f, args=(method,)).start()
|
||||||
self.fail('State %r not in %r' % (b.state, states))
|
bus.wait(states)
|
||||||
|
|
||||||
def test_block(self):
|
# The wait method MUST wait for the given state(s).
|
||||||
b = wspbus.Bus()
|
assert bus.state in states, 'State %r not in %r' % (bus.state, states)
|
||||||
self.log(b)
|
|
||||||
|
|
||||||
def f():
|
|
||||||
time.sleep(0.2)
|
|
||||||
b.exit()
|
|
||||||
|
|
||||||
def g():
|
|
||||||
time.sleep(0.4)
|
|
||||||
threading.Thread(target=f).start()
|
|
||||||
threading.Thread(target=g).start()
|
|
||||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
|
||||||
self.assertEqual(len(threads), 3)
|
|
||||||
|
|
||||||
b.block()
|
|
||||||
|
|
||||||
# The block method MUST wait for the EXITING state.
|
|
||||||
self.assertEqual(b.state, b.states.EXITING)
|
|
||||||
# The block method MUST wait for ALL non-main, non-daemon threads to
|
|
||||||
# finish.
|
|
||||||
threads = [t for t in threading.enumerate() if not t.daemon]
|
|
||||||
self.assertEqual(len(threads), 1)
|
|
||||||
# The last message will mention an indeterminable thread name; ignore
|
|
||||||
# it
|
|
||||||
self.assertEqual(self._log_entries[:-1],
|
|
||||||
['Bus STOPPING', 'Bus STOPPED',
|
|
||||||
'Bus EXITING', 'Bus EXITED',
|
|
||||||
'Waiting for child threads to terminate...'])
|
|
||||||
|
|
||||||
def test_start_with_callback(self):
|
|
||||||
b = wspbus.Bus()
|
|
||||||
self.log(b)
|
|
||||||
try:
|
|
||||||
events = []
|
|
||||||
|
|
||||||
def f(*args, **kwargs):
|
|
||||||
events.append(('f', args, kwargs))
|
|
||||||
|
|
||||||
def g():
|
|
||||||
events.append('g')
|
|
||||||
b.subscribe('start', g)
|
|
||||||
b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
|
|
||||||
# Give wait() time to run f()
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
# The callback method MUST wait for the STARTED state.
|
|
||||||
self.assertEqual(b.state, b.states.STARTED)
|
|
||||||
# The callback method MUST run after all start methods.
|
|
||||||
self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
|
|
||||||
finally:
|
|
||||||
b.exit()
|
|
||||||
|
|
||||||
def test_log(self):
|
|
||||||
b = wspbus.Bus()
|
|
||||||
self.log(b)
|
|
||||||
self.assertLog([])
|
|
||||||
|
|
||||||
# Try a normal message.
|
|
||||||
expected = []
|
|
||||||
for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
|
|
||||||
b.log(msg)
|
|
||||||
expected.append(msg)
|
|
||||||
self.assertLog(expected)
|
|
||||||
|
|
||||||
# Try an error message
|
|
||||||
try:
|
|
||||||
foo
|
|
||||||
except NameError:
|
|
||||||
b.log('You are lost and gone forever', traceback=True)
|
|
||||||
lastmsg = self._log_entries[-1]
|
|
||||||
if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
|
|
||||||
self.fail('Last log message %r did not contain '
|
|
||||||
'the expected traceback.' % lastmsg)
|
|
||||||
else:
|
|
||||||
self.fail('NameError was not raised as expected.')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
@pytest.mark.xfail(CI_ON_MACOS, reason='continuous integration on macOS fails')
|
||||||
unittest.main()
|
def test_wait_publishes_periodically(bus):
|
||||||
|
"""Test that wait publishes each tick."""
|
||||||
|
callback = unittest.mock.MagicMock()
|
||||||
|
bus.subscribe('main', callback)
|
||||||
|
|
||||||
|
def set_start():
|
||||||
|
time.sleep(0.05)
|
||||||
|
bus.start()
|
||||||
|
threading.Thread(target=set_start).start()
|
||||||
|
bus.wait(bus.states.STARTED, interval=0.01, channel='main')
|
||||||
|
assert callback.call_count > 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_block(bus, log_tracker):
|
||||||
|
"""Test that bus block waits for exiting."""
|
||||||
|
def f(): # pylint: disable=invalid-name
|
||||||
|
time.sleep(0.2)
|
||||||
|
bus.exit()
|
||||||
|
|
||||||
|
def g(): # pylint: disable=invalid-name
|
||||||
|
time.sleep(0.4)
|
||||||
|
|
||||||
|
threading.Thread(target=f).start()
|
||||||
|
threading.Thread(target=g).start()
|
||||||
|
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||||
|
assert len(threads) == 3
|
||||||
|
|
||||||
|
bus.block()
|
||||||
|
|
||||||
|
# The block method MUST wait for the EXITING state.
|
||||||
|
assert bus.state == bus.states.EXITING
|
||||||
|
|
||||||
|
# The block method MUST wait for ALL non-main, non-daemon threads to
|
||||||
|
# finish.
|
||||||
|
threads = [t for t in threading.enumerate() if not t.daemon]
|
||||||
|
assert len(threads) == 1
|
||||||
|
|
||||||
|
# The last message will mention an indeterminable thread name; ignore
|
||||||
|
# it
|
||||||
|
expected_bus_messages = [
|
||||||
|
'Bus STOPPING',
|
||||||
|
'Bus STOPPED',
|
||||||
|
'Bus EXITING',
|
||||||
|
'Bus EXITED',
|
||||||
|
'Waiting for child threads to terminate...',
|
||||||
|
]
|
||||||
|
bus_msg_num = len(expected_bus_messages)
|
||||||
|
|
||||||
|
# If the last message mentions an indeterminable thread name then ignore it
|
||||||
|
assert log_tracker.log_entries[:bus_msg_num] == expected_bus_messages
|
||||||
|
assert len(log_tracker.log_entries[bus_msg_num:]) <= 1, (
|
||||||
|
'No more than one extra log line with the thread name expected'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_with_callback(bus):
|
||||||
|
"""Test that callback fires on bus start."""
|
||||||
|
try:
|
||||||
|
events = []
|
||||||
|
|
||||||
|
def f(*args, **kwargs): # pylint: disable=invalid-name
|
||||||
|
events.append(('f', args, kwargs))
|
||||||
|
|
||||||
|
def g(): # pylint: disable=invalid-name
|
||||||
|
events.append('g')
|
||||||
|
bus.subscribe('start', g)
|
||||||
|
bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
|
||||||
|
|
||||||
|
# Give wait() time to run f()
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
# The callback method MUST wait for the STARTED state.
|
||||||
|
assert bus.state == bus.states.STARTED
|
||||||
|
|
||||||
|
# The callback method MUST run after all start methods.
|
||||||
|
assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})]
|
||||||
|
finally:
|
||||||
|
bus.exit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_log(bus, log_tracker):
|
||||||
|
"""Test that bus messages and errors are logged."""
|
||||||
|
assert log_tracker.log_entries == []
|
||||||
|
|
||||||
|
# Try a normal message.
|
||||||
|
expected = []
|
||||||
|
for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
|
||||||
|
bus.log(msg_)
|
||||||
|
expected.append(msg_)
|
||||||
|
assert log_tracker.log_entries == expected
|
||||||
|
|
||||||
|
# Try an error message
|
||||||
|
try:
|
||||||
|
foo
|
||||||
|
except NameError:
|
||||||
|
bus.log('You are lost and gone forever', traceback=True)
|
||||||
|
lastmsg = log_tracker.log_entries[-1]
|
||||||
|
assert 'Traceback' in lastmsg and 'NameError' in lastmsg, (
|
||||||
|
'Last log message %r did not contain '
|
||||||
|
'the expected traceback.' % lastmsg
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pytest.fail('NameError was not raised as expected.')
|
||||||
|
|
|
@ -3,9 +3,7 @@ from itertools import count
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
from six.moves import range
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -153,7 +151,7 @@ class CacheTest(helper.CPWebCase):
|
||||||
self.assertBody('visit #1')
|
self.assertBody('visit #1')
|
||||||
if trial != 0:
|
if trial != 0:
|
||||||
age = int(self.assertHeader('Age'))
|
age = int(self.assertHeader('Age'))
|
||||||
self.assert_(age >= elapsed)
|
assert age >= elapsed
|
||||||
elapsed = age
|
elapsed = age
|
||||||
|
|
||||||
# POST, PUT, DELETE should not be cached.
|
# POST, PUT, DELETE should not be cached.
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
"""Test Python 2/3 compatibility module."""
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import six
|
|
||||||
|
|
||||||
from cherrypy import _cpcompat as compat
|
|
||||||
|
|
||||||
|
|
||||||
class StringTester(unittest.TestCase):
|
|
||||||
"""Tests for string conversion."""
|
|
||||||
|
|
||||||
@pytest.mark.skipif(six.PY3, reason='Only useful on Python 2')
|
|
||||||
def test_ntob_non_native(self):
|
|
||||||
"""ntob should raise an Exception on unicode.
|
|
||||||
|
|
||||||
(Python 2 only)
|
|
||||||
|
|
||||||
See #1132 for discussion.
|
|
||||||
"""
|
|
||||||
self.assertRaises(TypeError, compat.ntob, 'fight')
|
|
||||||
|
|
||||||
|
|
||||||
class EscapeTester(unittest.TestCase):
|
|
||||||
"""Class to test escape_html function from _cpcompat."""
|
|
||||||
|
|
||||||
def test_escape_quote(self):
|
|
||||||
"""test_escape_quote - Verify the output for &<>"' chars."""
|
|
||||||
self.assertEqual(
|
|
||||||
"""xx&<>"aa'""",
|
|
||||||
compat.escape_html("""xx&<>"aa'"""),
|
|
||||||
)
|
|
|
@ -5,8 +5,6 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
@ -16,7 +14,7 @@ localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
def StringIOFromNative(x):
|
def StringIOFromNative(x):
|
||||||
return io.StringIO(six.text_type(x))
|
return io.StringIO(str(x))
|
||||||
|
|
||||||
|
|
||||||
def setup_server():
|
def setup_server():
|
||||||
|
@ -82,7 +80,7 @@ def setup_server():
|
||||||
|
|
||||||
def wrapper():
|
def wrapper():
|
||||||
params = cherrypy.request.params
|
params = cherrypy.request.params
|
||||||
for name, coercer in list(value.items()):
|
for name, coercer in value.copy().items():
|
||||||
try:
|
try:
|
||||||
params[name] = coercer(params[name])
|
params[name] = coercer(params[name])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -105,18 +103,12 @@ def setup_server():
|
||||||
def incr(self, num):
|
def incr(self, num):
|
||||||
return num + 1
|
return num + 1
|
||||||
|
|
||||||
if not six.PY3:
|
|
||||||
thing3 = "thing3: unicode('test', errors='ignore')"
|
|
||||||
else:
|
|
||||||
thing3 = ''
|
|
||||||
|
|
||||||
ioconf = StringIOFromNative("""
|
ioconf = StringIOFromNative("""
|
||||||
[/]
|
[/]
|
||||||
neg: -1234
|
neg: -1234
|
||||||
filename: os.path.join(sys.prefix, "hello.py")
|
filename: os.path.join(sys.prefix, "hello.py")
|
||||||
thing1: cherrypy.lib.httputil.response_codes[404]
|
thing1: cherrypy.lib.httputil.response_codes[404]
|
||||||
thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
|
thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
|
||||||
%s
|
|
||||||
complex: 3+2j
|
complex: 3+2j
|
||||||
mul: 6*3
|
mul: 6*3
|
||||||
ones: "11"
|
ones: "11"
|
||||||
|
@ -125,7 +117,7 @@ stradd: %%(ones)s + %%(twos)s + "33"
|
||||||
|
|
||||||
[/favicon.ico]
|
[/favicon.ico]
|
||||||
tools.staticfile.filename = %r
|
tools.staticfile.filename = %r
|
||||||
""" % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
|
""" % os.path.join(localDir, 'static/dirback.jpg'))
|
||||||
|
|
||||||
root = Root()
|
root = Root()
|
||||||
root.foo = Foo()
|
root.foo = Foo()
|
||||||
|
@ -203,10 +195,6 @@ class ConfigTests(helper.CPWebCase):
|
||||||
from cherrypy.tutorial import thing2
|
from cherrypy.tutorial import thing2
|
||||||
self.assertBody(repr(thing2))
|
self.assertBody(repr(thing2))
|
||||||
|
|
||||||
if not six.PY3:
|
|
||||||
self.getPage('/repr?key=thing3')
|
|
||||||
self.assertBody(repr(six.text_type('test')))
|
|
||||||
|
|
||||||
self.getPage('/repr?key=complex')
|
self.getPage('/repr?key=complex')
|
||||||
self.assertBody('(3+2j)')
|
self.assertBody('(3+2j)')
|
||||||
|
|
||||||
|
@ -233,8 +221,8 @@ class ConfigTests(helper.CPWebCase):
|
||||||
# the favicon in the page handler to be '../favicon.ico',
|
# the favicon in the page handler to be '../favicon.ico',
|
||||||
# but then overrode it in config to be './static/dirback.jpg'.
|
# but then overrode it in config to be './static/dirback.jpg'.
|
||||||
self.getPage('/favicon.ico')
|
self.getPage('/favicon.ico')
|
||||||
self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'),
|
with open(os.path.join(localDir, 'static/dirback.jpg'), 'rb') as tf:
|
||||||
'rb').read())
|
self.assertBody(tf.read())
|
||||||
|
|
||||||
def test_request_body_namespace(self):
|
def test_request_body_namespace(self):
|
||||||
self.getPage('/plain', method='POST', headers=[
|
self.getPage('/plain', method='POST', headers=[
|
||||||
|
|
|
@ -4,12 +4,8 @@ import errno
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import urllib.parse
|
||||||
import six
|
from http.client import BadStatusLine, HTTPConnection, NotConnected
|
||||||
from six.moves import urllib
|
|
||||||
from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from cheroot.test import webtest
|
from cheroot.test import webtest
|
||||||
|
|
||||||
|
@ -91,7 +87,7 @@ def setup_server():
|
||||||
body = [body]
|
body = [body]
|
||||||
newbody = []
|
newbody = []
|
||||||
for chunk in body:
|
for chunk in body:
|
||||||
if isinstance(chunk, six.text_type):
|
if isinstance(chunk, str):
|
||||||
chunk = chunk.encode('ISO-8859-1')
|
chunk = chunk.encode('ISO-8859-1')
|
||||||
newbody.append(chunk)
|
newbody.append(chunk)
|
||||||
return newbody
|
return newbody
|
||||||
|
@ -354,18 +350,17 @@ class PipelineTests(helper.CPWebCase):
|
||||||
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
||||||
conn._send_output()
|
conn._send_output()
|
||||||
response = conn.response_class(conn.sock, method='GET')
|
response = conn.response_class(conn.sock, method='GET')
|
||||||
|
msg = (
|
||||||
|
"Writing to timed out socket didn't fail as it should have: %s")
|
||||||
try:
|
try:
|
||||||
response.begin()
|
response.begin()
|
||||||
except Exception:
|
except Exception:
|
||||||
if not isinstance(sys.exc_info()[1],
|
if not isinstance(sys.exc_info()[1],
|
||||||
(socket.error, BadStatusLine)):
|
(socket.error, BadStatusLine)):
|
||||||
self.fail("Writing to timed out socket didn't fail"
|
self.fail(msg % sys.exc_info()[1])
|
||||||
' as it should have: %s' % sys.exc_info()[1])
|
|
||||||
else:
|
else:
|
||||||
if response.status != 408:
|
if response.status != 408:
|
||||||
self.fail("Writing to timed out socket didn't fail"
|
self.fail(msg % response.read())
|
||||||
' as it should have: %s' %
|
|
||||||
response.read())
|
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
@ -392,12 +387,10 @@ class PipelineTests(helper.CPWebCase):
|
||||||
except Exception:
|
except Exception:
|
||||||
if not isinstance(sys.exc_info()[1],
|
if not isinstance(sys.exc_info()[1],
|
||||||
(socket.error, BadStatusLine)):
|
(socket.error, BadStatusLine)):
|
||||||
self.fail("Writing to timed out socket didn't fail"
|
self.fail(msg % sys.exc_info()[1])
|
||||||
' as it should have: %s' % sys.exc_info()[1])
|
|
||||||
else:
|
else:
|
||||||
self.fail("Writing to timed out socket didn't fail"
|
if response.status != 408:
|
||||||
' as it should have: %s' %
|
self.fail(msg % response.read())
|
||||||
response.read())
|
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
@ -441,8 +434,7 @@ class PipelineTests(helper.CPWebCase):
|
||||||
# ``conn.sock``. Until that bug get's fixed we will
|
# ``conn.sock``. Until that bug get's fixed we will
|
||||||
# monkey patch the ``response`` instance.
|
# monkey patch the ``response`` instance.
|
||||||
# https://bugs.python.org/issue23377
|
# https://bugs.python.org/issue23377
|
||||||
if six.PY3:
|
response.fp = conn.sock.makefile('rb', 0)
|
||||||
response.fp = conn.sock.makefile('rb', 0)
|
|
||||||
response.begin()
|
response.begin()
|
||||||
body = response.read(13)
|
body = response.read(13)
|
||||||
self.assertEqual(response.status, 200)
|
self.assertEqual(response.status, 200)
|
||||||
|
@ -784,7 +776,6 @@ socket_reset_errors += [
|
||||||
class LimitedRequestQueueTests(helper.CPWebCase):
|
class LimitedRequestQueueTests(helper.CPWebCase):
|
||||||
setup_server = staticmethod(setup_upload_server)
|
setup_server = staticmethod(setup_upload_server)
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='#1535')
|
|
||||||
def test_queue_full(self):
|
def test_queue_full(self):
|
||||||
conns = []
|
conns = []
|
||||||
overflow_conn = None
|
overflow_conn = None
|
||||||
|
|
|
@ -6,8 +6,6 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntou
|
from cherrypy._cpcompat import ntou
|
||||||
from cherrypy import _cptools, tools
|
from cherrypy import _cptools, tools
|
||||||
|
@ -57,7 +55,7 @@ class CoreRequestHandlingTest(helper.CPWebCase):
|
||||||
"""
|
"""
|
||||||
def __init__(cls, name, bases, dct):
|
def __init__(cls, name, bases, dct):
|
||||||
type.__init__(cls, name, bases, dct)
|
type.__init__(cls, name, bases, dct)
|
||||||
for value in six.itervalues(dct):
|
for value in dct.values():
|
||||||
if isinstance(value, types.FunctionType):
|
if isinstance(value, types.FunctionType):
|
||||||
value.exposed = True
|
value.exposed = True
|
||||||
setattr(root, name.lower(), cls())
|
setattr(root, name.lower(), cls())
|
||||||
|
@ -387,6 +385,11 @@ class CoreRequestHandlingTest(helper.CPWebCase):
|
||||||
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||||
self.assertStatus(307)
|
self.assertStatus(307)
|
||||||
|
|
||||||
|
self.getPage('/redirect/by_code?code=308')
|
||||||
|
self.assertMatchesBody(
|
||||||
|
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
|
||||||
|
self.assertStatus(308)
|
||||||
|
|
||||||
self.getPage('/redirect/nomodify')
|
self.getPage('/redirect/nomodify')
|
||||||
self.assertBody('')
|
self.assertBody('')
|
||||||
self.assertStatus(304)
|
self.assertStatus(304)
|
||||||
|
@ -551,7 +554,7 @@ class CoreRequestHandlingTest(helper.CPWebCase):
|
||||||
self.assertStatus(206)
|
self.assertStatus(206)
|
||||||
ct = self.assertHeader('Content-Type')
|
ct = self.assertHeader('Content-Type')
|
||||||
expected_type = 'multipart/byteranges; boundary='
|
expected_type = 'multipart/byteranges; boundary='
|
||||||
self.assert_(ct.startswith(expected_type))
|
assert ct.startswith(expected_type)
|
||||||
boundary = ct[len(expected_type):]
|
boundary = ct[len(expected_type):]
|
||||||
expected_body = ('\r\n--%s\r\n'
|
expected_body = ('\r\n--%s\r\n'
|
||||||
'Content-type: text/html\r\n'
|
'Content-type: text/html\r\n'
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
|
||||||
|
@ -79,7 +77,7 @@ def setup_server():
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return six.text_type(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
@ -105,7 +103,7 @@ def setup_server():
|
||||||
return 'POST %d' % make_user(name)
|
return 'POST %d' % make_user(name)
|
||||||
|
|
||||||
def GET(self):
|
def GET(self):
|
||||||
return six.text_type(sorted(user_lookup.keys()))
|
return str(sorted(user_lookup.keys()))
|
||||||
|
|
||||||
def dynamic_dispatch(self, vpath):
|
def dynamic_dispatch(self, vpath):
|
||||||
try:
|
try:
|
||||||
|
@ -130,7 +128,7 @@ def setup_server():
|
||||||
"""
|
"""
|
||||||
Return the appropriate representation of the instance.
|
Return the appropriate representation of the instance.
|
||||||
"""
|
"""
|
||||||
return six.text_type(self.user)
|
return str(self.user)
|
||||||
|
|
||||||
def POST(self, name):
|
def POST(self, name):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,9 +3,8 @@
|
||||||
import gzip
|
import gzip
|
||||||
import io
|
import io
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from http.client import IncompleteRead
|
||||||
from six.moves.http_client import IncompleteRead
|
from urllib.parse import quote as url_quote
|
||||||
from six.moves.urllib.parse import quote as url_quote
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntob, ntou
|
from cherrypy._cpcompat import ntob, ntou
|
||||||
|
|
|
@ -6,13 +6,11 @@ import mimetypes
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
import urllib.parse
|
||||||
import six
|
from http.client import HTTPConnection
|
||||||
from six.moves.http_client import HTTPConnection
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import HTTPSConnection, quote
|
from cherrypy._cpcompat import HTTPSConnection
|
||||||
|
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
|
||||||
|
@ -36,7 +34,7 @@ def encode_filename(filename):
|
||||||
"""
|
"""
|
||||||
if is_ascii(filename):
|
if is_ascii(filename):
|
||||||
return 'filename', '"{filename}"'.format(**locals())
|
return 'filename', '"{filename}"'.format(**locals())
|
||||||
encoded = quote(filename, encoding='utf-8')
|
encoded = urllib.parse.quote(filename, encoding='utf-8')
|
||||||
return 'filename*', "'".join((
|
return 'filename*', "'".join((
|
||||||
'UTF-8',
|
'UTF-8',
|
||||||
'', # lang
|
'', # lang
|
||||||
|
@ -105,14 +103,12 @@ class HTTPTests(helper.CPWebCase):
|
||||||
count += 1
|
count += 1
|
||||||
else:
|
else:
|
||||||
if count:
|
if count:
|
||||||
if six.PY3:
|
curchar = chr(curchar)
|
||||||
curchar = chr(curchar)
|
|
||||||
summary.append('%s * %d' % (curchar, count))
|
summary.append('%s * %d' % (curchar, count))
|
||||||
count = 1
|
count = 1
|
||||||
curchar = c
|
curchar = c
|
||||||
if count:
|
if count:
|
||||||
if six.PY3:
|
curchar = chr(curchar)
|
||||||
curchar = chr(curchar)
|
|
||||||
summary.append('%s * %d' % (curchar, count))
|
summary.append('%s * %d' % (curchar, count))
|
||||||
return ', '.join(summary)
|
return ', '.join(summary)
|
||||||
|
|
||||||
|
@ -189,12 +185,14 @@ class HTTPTests(helper.CPWebCase):
|
||||||
self.assertBody(', '.join(parts))
|
self.assertBody(', '.join(parts))
|
||||||
|
|
||||||
def test_post_filename_with_special_characters(self):
|
def test_post_filename_with_special_characters(self):
|
||||||
'''Testing that we can handle filenames with special characters. This
|
"""Testing that we can handle filenames with special characters.
|
||||||
was reported as a bug in:
|
|
||||||
https://github.com/cherrypy/cherrypy/issues/1146/
|
This was reported as a bug in:
|
||||||
https://github.com/cherrypy/cherrypy/issues/1397/
|
|
||||||
https://github.com/cherrypy/cherrypy/issues/1694/
|
* https://github.com/cherrypy/cherrypy/issues/1146/
|
||||||
'''
|
* https://github.com/cherrypy/cherrypy/issues/1397/
|
||||||
|
* https://github.com/cherrypy/cherrypy/issues/1694/
|
||||||
|
"""
|
||||||
# We'll upload a bunch of files with differing names.
|
# We'll upload a bunch of files with differing names.
|
||||||
fnames = [
|
fnames = [
|
||||||
'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
|
'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Test helpers from ``cherrypy.lib.httputil`` module."""
|
"""Test helpers from ``cherrypy.lib.httputil`` module."""
|
||||||
import pytest
|
import pytest
|
||||||
from six.moves import http_client
|
import http.client
|
||||||
|
|
||||||
from cherrypy.lib import httputil
|
from cherrypy.lib import httputil
|
||||||
|
|
||||||
|
@ -49,12 +49,12 @@ EXPECTED_444 = (444, 'Non-existent reason', '')
|
||||||
(None, EXPECTED_200),
|
(None, EXPECTED_200),
|
||||||
(200, EXPECTED_200),
|
(200, EXPECTED_200),
|
||||||
('500', EXPECTED_500),
|
('500', EXPECTED_500),
|
||||||
(http_client.NOT_FOUND, EXPECTED_404),
|
(http.client.NOT_FOUND, EXPECTED_404),
|
||||||
('444 Non-existent reason', EXPECTED_444),
|
('444 Non-existent reason', EXPECTED_444),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def test_valid_status(status, expected_status):
|
def test_valid_status(status, expected_status):
|
||||||
"""Check valid int, string and http_client-constants
|
"""Check valid int, string and http.client-constants
|
||||||
statuses processing."""
|
statuses processing."""
|
||||||
assert httputil.valid_status(status) == expected_status
|
assert httputil.valid_status(status) == expected_status
|
||||||
|
|
||||||
|
@ -62,19 +62,20 @@ def test_valid_status(status, expected_status):
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'status_code,error_msg',
|
'status_code,error_msg',
|
||||||
[
|
[
|
||||||
('hey', "Illegal response status from server ('hey' is non-numeric)."),
|
(
|
||||||
|
'hey',
|
||||||
|
r"Illegal response status from server \('hey' is non-numeric\)."
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{'hey': 'hi'},
|
{'hey': 'hi'},
|
||||||
'Illegal response status from server '
|
r'Illegal response status from server '
|
||||||
"({'hey': 'hi'} is non-numeric).",
|
r"\(\{'hey': 'hi'\} is non-numeric\).",
|
||||||
),
|
),
|
||||||
(1, 'Illegal response status from server (1 is out of range).'),
|
(1, r'Illegal response status from server \(1 is out of range\).'),
|
||||||
(600, 'Illegal response status from server (600 is out of range).'),
|
(600, r'Illegal response status from server \(600 is out of range\).'),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def test_invalid_status(status_code, error_msg):
|
def test_invalid_status(status_code, error_msg):
|
||||||
"""Check that invalid status cause certain errors."""
|
"""Check that invalid status cause certain errors."""
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError, match=error_msg):
|
||||||
httputil.valid_status(status_code)
|
httputil.valid_status(status_code)
|
||||||
|
|
||||||
assert error_msg in str(excinfo)
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
|
||||||
|
@ -88,7 +86,7 @@ class IteratorTest(helper.CPWebCase):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def count(self, clsname):
|
def count(self, clsname):
|
||||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||||
return six.text_type(globals()[clsname].created)
|
return str(globals()[clsname].created)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def getall(self, clsname):
|
def getall(self, clsname):
|
||||||
|
@ -139,7 +137,7 @@ class IteratorTest(helper.CPWebCase):
|
||||||
headers = response.getheaders()
|
headers = response.getheaders()
|
||||||
for header_name, header_value in headers:
|
for header_name, header_value in headers:
|
||||||
if header_name.lower() == 'content-length':
|
if header_name.lower() == 'content-length':
|
||||||
expected = six.text_type(1024 * 16 * 256)
|
expected = str(1024 * 16 * 256)
|
||||||
assert header_value == expected, header_value
|
assert header_value == expected, header_value
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
from cherrypy._json import json
|
||||||
from cherrypy._cpcompat import json
|
|
||||||
|
|
||||||
|
|
||||||
json_out = cherrypy.config(**{'tools.json_out.on': True})
|
json_out = cherrypy.config(**{'tools.json_out.on': True})
|
||||||
|
|
|
@ -1,24 +1,51 @@
|
||||||
"""Basic tests for the CherryPy core: request handling."""
|
"""Basic tests for the CherryPy core: request handling."""
|
||||||
|
|
||||||
import os
|
import logging
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import six
|
from cheroot.test import webtest
|
||||||
|
import pytest
|
||||||
|
import requests # FIXME: Temporary using it directly, better switch
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntou
|
from cherrypy.test.logtest import LogCase
|
||||||
from cherrypy.test import helper, logtest
|
|
||||||
|
|
||||||
localDir = os.path.dirname(__file__)
|
|
||||||
access_log = os.path.join(localDir, 'access.log')
|
|
||||||
error_log = os.path.join(localDir, 'error.log')
|
|
||||||
|
|
||||||
# Some unicode strings.
|
# Some unicode strings.
|
||||||
tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape')
|
tartaros = u'\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2'
|
||||||
erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape')
|
erebos = u'\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com'
|
||||||
|
|
||||||
|
|
||||||
def setup_server():
|
@pytest.fixture
|
||||||
|
def access_log_file(tmp_path_factory):
|
||||||
|
return tmp_path_factory.mktemp('logs') / 'access.log'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def error_log_file(tmp_path_factory):
|
||||||
|
return tmp_path_factory.mktemp('logs') / 'access.log'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def server(configure_server):
|
||||||
|
cherrypy.engine.start()
|
||||||
|
cherrypy.engine.wait(cherrypy.engine.states.STARTED)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
shutdown_server()
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown_server():
|
||||||
|
cherrypy.engine.exit()
|
||||||
|
cherrypy.engine.block()
|
||||||
|
|
||||||
|
for name, server in getattr(cherrypy, 'servers', {}).copy().items():
|
||||||
|
server.unsubscribe()
|
||||||
|
del cherrypy.servers[name]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def configure_server(access_log_file, error_log_file):
|
||||||
class Root:
|
class Root:
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@ -58,152 +85,204 @@ def setup_server():
|
||||||
|
|
||||||
root = Root()
|
root = Root()
|
||||||
|
|
||||||
|
cherrypy.config.reset()
|
||||||
cherrypy.config.update({
|
cherrypy.config.update({
|
||||||
'log.error_file': error_log,
|
'server.socket_host': webtest.WebCase.HOST,
|
||||||
'log.access_file': access_log,
|
'server.socket_port': webtest.WebCase.PORT,
|
||||||
|
'server.protocol_version': webtest.WebCase.PROTOCOL,
|
||||||
|
'environment': 'test_suite',
|
||||||
|
})
|
||||||
|
cherrypy.config.update({
|
||||||
|
'log.error_file': str(error_log_file),
|
||||||
|
'log.access_file': str(access_log_file),
|
||||||
})
|
})
|
||||||
cherrypy.tree.mount(root)
|
cherrypy.tree.mount(root)
|
||||||
|
|
||||||
|
|
||||||
class AccessLogTests(helper.CPWebCase, logtest.LogCase):
|
@pytest.fixture
|
||||||
setup_server = staticmethod(setup_server)
|
def log_tracker(access_log_file):
|
||||||
|
class LogTracker(LogCase):
|
||||||
|
logfile = str(access_log_file)
|
||||||
|
return LogTracker()
|
||||||
|
|
||||||
logfile = access_log
|
|
||||||
|
|
||||||
def testNormalReturn(self):
|
def test_normal_return(log_tracker, server):
|
||||||
self.markLog()
|
log_tracker.markLog()
|
||||||
self.getPage('/as_string',
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
headers=[('Referer', 'http://www.cherrypy.org/'),
|
port = webtest.WebCase.PORT
|
||||||
('User-Agent', 'Mozilla/5.0')])
|
resp = requests.get(
|
||||||
self.assertBody('content')
|
'http://%s:%s/as_string' % (host, port),
|
||||||
self.assertStatus(200)
|
headers={
|
||||||
|
'Referer': 'http://www.cherrypy.org/',
|
||||||
intro = '%s - - [' % self.interface()
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
},
|
||||||
self.assertLog(-1, intro)
|
|
||||||
|
|
||||||
if [k for k, v in self.headers if k.lower() == 'content-length']:
|
|
||||||
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
|
|
||||||
'"http://www.cherrypy.org/" "Mozilla/5.0"'
|
|
||||||
% self.prefix())
|
|
||||||
else:
|
|
||||||
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
|
|
||||||
'"http://www.cherrypy.org/" "Mozilla/5.0"'
|
|
||||||
% self.prefix())
|
|
||||||
|
|
||||||
def testNormalYield(self):
|
|
||||||
self.markLog()
|
|
||||||
self.getPage('/as_yield')
|
|
||||||
self.assertBody('content')
|
|
||||||
self.assertStatus(200)
|
|
||||||
|
|
||||||
intro = '%s - - [' % self.interface()
|
|
||||||
|
|
||||||
self.assertLog(-1, intro)
|
|
||||||
if [k for k, v in self.headers if k.lower() == 'content-length']:
|
|
||||||
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
|
|
||||||
self.prefix())
|
|
||||||
else:
|
|
||||||
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
|
|
||||||
% self.prefix())
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
'cherrypy._cplogging.LogManager.access_log_format',
|
|
||||||
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}'
|
|
||||||
if six.PY3 else
|
|
||||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
|
|
||||||
)
|
)
|
||||||
def testCustomLogFormat(self):
|
expected_body = 'content'
|
||||||
"""Test a customized access_log_format string, which is a
|
assert resp.text == expected_body
|
||||||
feature of _cplogging.LogManager.access()."""
|
assert resp.status_code == 200
|
||||||
self.markLog()
|
|
||||||
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
|
|
||||||
('User-Agent', 'USERAGENT'),
|
|
||||||
('Host', 'HOST')])
|
|
||||||
self.assertLog(-1, '%s - - [' % self.interface())
|
|
||||||
self.assertLog(-1, '] "GET /as_string HTTP/1.1" '
|
|
||||||
'200 7 "REFERER" "USERAGENT" HOST')
|
|
||||||
|
|
||||||
@mock.patch(
|
intro = '%s - - [' % host
|
||||||
'cherrypy._cplogging.LogManager.access_log_format',
|
|
||||||
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}'
|
log_tracker.assertLog(-1, intro)
|
||||||
if six.PY3 else
|
|
||||||
'%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
|
content_length = len(expected_body)
|
||||||
|
if not any(
|
||||||
|
k for k, v in resp.headers.items()
|
||||||
|
if k.lower() == 'content-length'
|
||||||
|
):
|
||||||
|
content_length = '-'
|
||||||
|
|
||||||
|
log_tracker.assertLog(
|
||||||
|
-1,
|
||||||
|
'] "GET /as_string HTTP/1.1" 200 %s '
|
||||||
|
'"http://www.cherrypy.org/" "Mozilla/5.0"'
|
||||||
|
% content_length,
|
||||||
)
|
)
|
||||||
def testTimezLogFormat(self):
|
|
||||||
"""Test a customized access_log_format string, which is a
|
|
||||||
feature of _cplogging.LogManager.access()."""
|
|
||||||
self.markLog()
|
|
||||||
|
|
||||||
expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
|
|
||||||
with mock.patch(
|
|
||||||
'cherrypy._cplogging.LazyRfc3339UtcTime',
|
|
||||||
lambda: expected_time):
|
|
||||||
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
|
|
||||||
('User-Agent', 'USERAGENT'),
|
|
||||||
('Host', 'HOST')])
|
|
||||||
|
|
||||||
self.assertLog(-1, '%s - - ' % self.interface())
|
def test_normal_yield(log_tracker, server):
|
||||||
self.assertLog(-1, expected_time)
|
log_tracker.markLog()
|
||||||
self.assertLog(-1, ' "GET /as_string HTTP/1.1" '
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
'200 7 "REFERER" "USERAGENT" HOST')
|
port = webtest.WebCase.PORT
|
||||||
|
resp = requests.get(
|
||||||
@mock.patch(
|
'http://%s:%s/as_yield' % (host, port),
|
||||||
'cherrypy._cplogging.LogManager.access_log_format',
|
headers={
|
||||||
'{i}' if six.PY3 else '%(i)s'
|
'User-Agent': '',
|
||||||
|
},
|
||||||
)
|
)
|
||||||
def testUUIDv4ParameterLogFormat(self):
|
expected_body = 'content'
|
||||||
"""Test rendering of UUID4 within access log."""
|
assert resp.text == expected_body
|
||||||
self.markLog()
|
assert resp.status_code == 200
|
||||||
self.getPage('/as_string')
|
|
||||||
self.assertValidUUIDv4()
|
|
||||||
|
|
||||||
def testEscapedOutput(self):
|
intro = '%s - - [' % host
|
||||||
# Test unicode in access log pieces.
|
|
||||||
self.markLog()
|
|
||||||
self.getPage('/uni_code')
|
|
||||||
self.assertStatus(200)
|
|
||||||
if six.PY3:
|
|
||||||
# The repr of a bytestring in six.PY3 includes a b'' prefix
|
|
||||||
self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
|
|
||||||
else:
|
|
||||||
self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
|
|
||||||
# Test the erebos value. Included inline for your enlightenment.
|
|
||||||
# Note the 'r' prefix--those backslashes are literals.
|
|
||||||
self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
|
|
||||||
|
|
||||||
# Test backslashes in output.
|
log_tracker.assertLog(-1, intro)
|
||||||
self.markLog()
|
content_length = len(expected_body)
|
||||||
self.getPage('/slashes')
|
if not any(
|
||||||
self.assertStatus(200)
|
k for k, v in resp.headers.items()
|
||||||
if six.PY3:
|
if k.lower() == 'content-length'
|
||||||
self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
|
):
|
||||||
else:
|
content_length = '-'
|
||||||
self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
|
|
||||||
|
|
||||||
# Test whitespace in output.
|
log_tracker.assertLog(
|
||||||
self.markLog()
|
-1,
|
||||||
self.getPage('/whitespace')
|
'] "GET /as_yield HTTP/1.1" 200 %s "" ""'
|
||||||
self.assertStatus(200)
|
% content_length,
|
||||||
# Again, note the 'r' prefix.
|
)
|
||||||
self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
|
def test_custom_log_format(log_tracker, monkeypatch, server):
|
||||||
setup_server = staticmethod(setup_server)
|
"""Test a customized access_log_format string, which is a
|
||||||
|
feature of _cplogging.LogManager.access()."""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
'cherrypy._cplogging.LogManager.access_log_format',
|
||||||
|
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}',
|
||||||
|
)
|
||||||
|
log_tracker.markLog()
|
||||||
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
|
port = webtest.WebCase.PORT
|
||||||
|
requests.get(
|
||||||
|
'http://%s:%s/as_string' % (host, port),
|
||||||
|
headers={
|
||||||
|
'Referer': 'REFERER',
|
||||||
|
'User-Agent': 'USERAGENT',
|
||||||
|
'Host': 'HOST',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
log_tracker.assertLog(-1, '%s - - [' % host)
|
||||||
|
log_tracker.assertLog(
|
||||||
|
-1,
|
||||||
|
'] "GET /as_string HTTP/1.1" '
|
||||||
|
'200 7 "REFERER" "USERAGENT" HOST',
|
||||||
|
)
|
||||||
|
|
||||||
logfile = error_log
|
|
||||||
|
|
||||||
def testTracebacks(self):
|
def test_timez_log_format(log_tracker, monkeypatch, server):
|
||||||
# Test that tracebacks get written to the error log.
|
"""Test a customized access_log_format string, which is a
|
||||||
self.markLog()
|
feature of _cplogging.LogManager.access()."""
|
||||||
ignore = helper.webtest.ignored_exceptions
|
monkeypatch.setattr(
|
||||||
ignore.append(ValueError)
|
'cherrypy._cplogging.LogManager.access_log_format',
|
||||||
try:
|
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}',
|
||||||
self.getPage('/error')
|
)
|
||||||
self.assertInBody('raise ValueError()')
|
log_tracker.markLog()
|
||||||
self.assertLog(0, 'HTTP')
|
|
||||||
self.assertLog(1, 'Traceback (most recent call last):')
|
expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
|
||||||
self.assertLog(-2, 'raise ValueError()')
|
monkeypatch.setattr(
|
||||||
finally:
|
'cherrypy._cplogging.LazyRfc3339UtcTime',
|
||||||
ignore.pop()
|
lambda: expected_time,
|
||||||
|
)
|
||||||
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
|
port = webtest.WebCase.PORT
|
||||||
|
requests.get(
|
||||||
|
'http://%s:%s/as_string' % (host, port),
|
||||||
|
headers={
|
||||||
|
'Referer': 'REFERER',
|
||||||
|
'User-Agent': 'USERAGENT',
|
||||||
|
'Host': 'HOST',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
log_tracker.assertLog(-1, '%s - - ' % host)
|
||||||
|
log_tracker.assertLog(-1, expected_time)
|
||||||
|
log_tracker.assertLog(
|
||||||
|
-1,
|
||||||
|
' "GET /as_string HTTP/1.1" '
|
||||||
|
'200 7 "REFERER" "USERAGENT" HOST',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_UUIDv4_parameter_log_format(log_tracker, monkeypatch, server):
|
||||||
|
"""Test rendering of UUID4 within access log."""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
'cherrypy._cplogging.LogManager.access_log_format',
|
||||||
|
'{i}',
|
||||||
|
)
|
||||||
|
log_tracker.markLog()
|
||||||
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
|
port = webtest.WebCase.PORT
|
||||||
|
requests.get('http://%s:%s/as_string' % (host, port))
|
||||||
|
log_tracker.assertValidUUIDv4()
|
||||||
|
|
||||||
|
|
||||||
|
def test_escaped_output(log_tracker, server):
|
||||||
|
# Test unicode in access log pieces.
|
||||||
|
log_tracker.markLog()
|
||||||
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
|
port = webtest.WebCase.PORT
|
||||||
|
resp = requests.get('http://%s:%s/uni_code' % (host, port))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
# The repr of a bytestring includes a b'' prefix
|
||||||
|
log_tracker.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
|
||||||
|
# Test the erebos value. Included inline for your enlightenment.
|
||||||
|
# Note the 'r' prefix--those backslashes are literals.
|
||||||
|
log_tracker.assertLog(
|
||||||
|
-1,
|
||||||
|
r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test backslashes in output.
|
||||||
|
log_tracker.markLog()
|
||||||
|
resp = requests.get('http://%s:%s/slashes' % (host, port))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
log_tracker.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
|
||||||
|
|
||||||
|
# Test whitespace in output.
|
||||||
|
log_tracker.markLog()
|
||||||
|
resp = requests.get('http://%s:%s/whitespace' % (host, port))
|
||||||
|
assert resp.status_code == 200
|
||||||
|
# Again, note the 'r' prefix.
|
||||||
|
log_tracker.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
|
||||||
|
|
||||||
|
|
||||||
|
def test_tracebacks(server, caplog):
|
||||||
|
host = webtest.interface(webtest.WebCase.HOST)
|
||||||
|
port = webtest.WebCase.PORT
|
||||||
|
with caplog.at_level(logging.ERROR, logger='cherrypy.error'):
|
||||||
|
resp = requests.get('http://%s:%s/error' % (host, port))
|
||||||
|
|
||||||
|
rec = caplog.records[0]
|
||||||
|
exc_cls, exc_msg = rec.exc_info[0], rec.message
|
||||||
|
|
||||||
|
assert 'raise ValueError()' in resp.text
|
||||||
|
assert 'HTTP' in exc_msg
|
||||||
|
assert exc_cls is ValueError
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
import itertools
|
import itertools
|
||||||
import platform
|
import platform
|
||||||
import threading
|
import threading
|
||||||
|
from http.client import HTTPConnection
|
||||||
from six.moves.http_client import HTTPConnection
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import HTTPSConnection
|
from cherrypy._cpcompat import HTTPSConnection
|
||||||
|
|
|
@ -5,9 +5,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import uuid
|
import uuid
|
||||||
|
from http.client import IncompleteRead
|
||||||
import six
|
|
||||||
from six.moves.http_client import IncompleteRead
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import ntou
|
from cherrypy._cpcompat import ntou
|
||||||
|
@ -243,7 +241,7 @@ class RequestObjectTests(helper.CPWebCase):
|
||||||
|
|
||||||
def ifmatch(self):
|
def ifmatch(self):
|
||||||
val = cherrypy.request.headers['If-Match']
|
val = cherrypy.request.headers['If-Match']
|
||||||
assert isinstance(val, six.text_type)
|
assert isinstance(val, str)
|
||||||
cherrypy.response.headers['ETag'] = val
|
cherrypy.response.headers['ETag'] = val
|
||||||
return val
|
return val
|
||||||
|
|
||||||
|
@ -251,7 +249,7 @@ class RequestObjectTests(helper.CPWebCase):
|
||||||
|
|
||||||
def get_elements(self, headername):
|
def get_elements(self, headername):
|
||||||
e = cherrypy.request.headers.elements(headername)
|
e = cherrypy.request.headers.elements(headername)
|
||||||
return '\n'.join([six.text_type(x) for x in e])
|
return '\n'.join([str(x) for x in e])
|
||||||
|
|
||||||
class Method(Test):
|
class Method(Test):
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import socket
|
from http.client import HTTPConnection
|
||||||
import importlib
|
|
||||||
|
|
||||||
from six.moves.http_client import HTTPConnection
|
|
||||||
|
|
||||||
|
from distutils.spawn import find_executable
|
||||||
import pytest
|
import pytest
|
||||||
from path import Path
|
from path import Path
|
||||||
|
from more_itertools import consume
|
||||||
|
import portend
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy._cpcompat import (
|
from cherrypy._cpcompat import HTTPSConnection
|
||||||
json_decode,
|
|
||||||
HTTPSConnection,
|
|
||||||
)
|
|
||||||
from cherrypy.lib import sessions
|
from cherrypy.lib import sessions
|
||||||
from cherrypy.lib import reprconf
|
from cherrypy.lib import reprconf
|
||||||
from cherrypy.lib.httputil import response_codes
|
from cherrypy.lib.httputil import response_codes
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
from cherrypy import _json as json
|
||||||
|
|
||||||
localDir = os.path.dirname(__file__)
|
localDir = Path(__file__).dirname()
|
||||||
|
|
||||||
|
|
||||||
def http_methods_allowed(methods=['GET', 'HEAD']):
|
def http_methods_allowed(methods=['GET', 'HEAD']):
|
||||||
|
@ -48,9 +47,10 @@ def setup_server():
|
||||||
cherrypy.session.cache.clear()
|
cherrypy.session.cache.clear()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@cherrypy.tools.json_out()
|
||||||
def data(self):
|
def data(self):
|
||||||
cherrypy.session['aha'] = 'foo'
|
cherrypy.session['aha'] = 'foo'
|
||||||
return repr(cherrypy.session._data)
|
return cherrypy.session._data
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def testGen(self):
|
def testGen(self):
|
||||||
|
@ -142,14 +142,18 @@ def setup_server():
|
||||||
class SessionTest(helper.CPWebCase):
|
class SessionTest(helper.CPWebCase):
|
||||||
setup_server = staticmethod(setup_server)
|
setup_server = staticmethod(setup_server)
|
||||||
|
|
||||||
def tearDown(self):
|
@classmethod
|
||||||
# Clean up sessions.
|
def teardown_class(cls):
|
||||||
for fname in os.listdir(localDir):
|
"""Clean up sessions."""
|
||||||
if fname.startswith(sessions.FileSession.SESSION_PREFIX):
|
super(cls, cls).teardown_class()
|
||||||
path = Path(localDir) / fname
|
consume(
|
||||||
path.remove_p()
|
file.remove_p()
|
||||||
|
for file in localDir.listdir()
|
||||||
|
if file.basename().startswith(
|
||||||
|
sessions.FileSession.SESSION_PREFIX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='#1534')
|
|
||||||
def test_0_Session(self):
|
def test_0_Session(self):
|
||||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
||||||
self.getPage('/clear')
|
self.getPage('/clear')
|
||||||
|
@ -157,82 +161,81 @@ class SessionTest(helper.CPWebCase):
|
||||||
# Test that a normal request gets the same id in the cookies.
|
# Test that a normal request gets the same id in the cookies.
|
||||||
# Note: this wouldn't work if /data didn't load the session.
|
# Note: this wouldn't work if /data didn't load the session.
|
||||||
self.getPage('/data')
|
self.getPage('/data')
|
||||||
self.assertBody("{'aha': 'foo'}")
|
assert self.body == b'{"aha": "foo"}'
|
||||||
c = self.cookies[0]
|
c = self.cookies[0]
|
||||||
self.getPage('/data', self.cookies)
|
self.getPage('/data', self.cookies)
|
||||||
self.assertEqual(self.cookies[0], c)
|
self.cookies[0] == c
|
||||||
|
|
||||||
self.getPage('/testStr')
|
self.getPage('/testStr')
|
||||||
self.assertBody('1')
|
assert self.body == b'1'
|
||||||
cookie_parts = dict([p.strip().split('=')
|
cookie_parts = dict([p.strip().split('=')
|
||||||
for p in self.cookies[0][1].split(';')])
|
for p in self.cookies[0][1].split(';')])
|
||||||
# Assert there is an 'expires' param
|
# Assert there is an 'expires' param
|
||||||
self.assertEqual(set(cookie_parts.keys()),
|
expected_cookie_keys = {'session_id', 'expires', 'Path', 'Max-Age'}
|
||||||
set(['session_id', 'expires', 'Path']))
|
assert set(cookie_parts.keys()) == expected_cookie_keys
|
||||||
self.getPage('/testGen', self.cookies)
|
self.getPage('/testGen', self.cookies)
|
||||||
self.assertBody('2')
|
assert self.body == b'2'
|
||||||
self.getPage('/testStr', self.cookies)
|
self.getPage('/testStr', self.cookies)
|
||||||
self.assertBody('3')
|
assert self.body == b'3'
|
||||||
self.getPage('/data', self.cookies)
|
self.getPage('/data', self.cookies)
|
||||||
self.assertDictEqual(json_decode(self.body),
|
expected_data = {'counter': 3, 'aha': 'foo'}
|
||||||
{'counter': 3, 'aha': 'foo'})
|
assert json.decode(self.body.decode('utf-8')) == expected_data
|
||||||
self.getPage('/length', self.cookies)
|
self.getPage('/length', self.cookies)
|
||||||
self.assertBody('2')
|
assert self.body == b'2'
|
||||||
self.getPage('/delkey?key=counter', self.cookies)
|
self.getPage('/delkey?key=counter', self.cookies)
|
||||||
self.assertStatus(200)
|
assert self.status_code == 200
|
||||||
|
|
||||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
|
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
|
||||||
self.getPage('/testStr')
|
self.getPage('/testStr')
|
||||||
self.assertBody('1')
|
assert self.body == b'1'
|
||||||
self.getPage('/testGen', self.cookies)
|
self.getPage('/testGen', self.cookies)
|
||||||
self.assertBody('2')
|
assert self.body == b'2'
|
||||||
self.getPage('/testStr', self.cookies)
|
self.getPage('/testStr', self.cookies)
|
||||||
self.assertBody('3')
|
assert self.body == b'3'
|
||||||
self.getPage('/delkey?key=counter', self.cookies)
|
self.getPage('/delkey?key=counter', self.cookies)
|
||||||
self.assertStatus(200)
|
assert self.status_code == 200
|
||||||
|
|
||||||
# Wait for the session.timeout (1 second)
|
# Wait for the session.timeout (1 second)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
self.getPage('/')
|
self.getPage('/')
|
||||||
self.assertBody('1')
|
assert self.body == b'1'
|
||||||
self.getPage('/length', self.cookies)
|
self.getPage('/length', self.cookies)
|
||||||
self.assertBody('1')
|
assert self.body == b'1'
|
||||||
|
|
||||||
# Test session __contains__
|
# Test session __contains__
|
||||||
self.getPage('/keyin?key=counter', self.cookies)
|
self.getPage('/keyin?key=counter', self.cookies)
|
||||||
self.assertBody('True')
|
assert self.body == b'True'
|
||||||
cookieset1 = self.cookies
|
cookieset1 = self.cookies
|
||||||
|
|
||||||
# Make a new session and test __len__ again
|
# Make a new session and test __len__ again
|
||||||
self.getPage('/')
|
self.getPage('/')
|
||||||
self.getPage('/length', self.cookies)
|
self.getPage('/length', self.cookies)
|
||||||
self.assertBody('2')
|
assert self.body == b'2'
|
||||||
|
|
||||||
# Test session delete
|
# Test session delete
|
||||||
self.getPage('/delete', self.cookies)
|
self.getPage('/delete', self.cookies)
|
||||||
self.assertBody('done')
|
assert self.body == b'done'
|
||||||
self.getPage('/delete', cookieset1)
|
self.getPage('/delete', cookieset1)
|
||||||
self.assertBody('done')
|
assert self.body == b'done'
|
||||||
|
|
||||||
def f():
|
def f():
|
||||||
return [
|
return [
|
||||||
x
|
x
|
||||||
for x in os.listdir(localDir)
|
for x in os.listdir(localDir)
|
||||||
if x.startswith('session-')
|
if x.startswith('session-') and not x.endswith('.lock')
|
||||||
]
|
]
|
||||||
self.assertEqual(f(), [])
|
assert f() == []
|
||||||
|
|
||||||
# Wait for the cleanup thread to delete remaining session files
|
# Wait for the cleanup thread to delete remaining session files
|
||||||
self.getPage('/')
|
self.getPage('/')
|
||||||
self.assertNotEqual(f(), [])
|
assert f() != []
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
self.assertEqual(f(), [])
|
assert f() == []
|
||||||
|
|
||||||
def test_1_Ram_Concurrency(self):
|
def test_1_Ram_Concurrency(self):
|
||||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
||||||
self._test_Concurrency()
|
self._test_Concurrency()
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='#1306')
|
|
||||||
def test_2_File_Concurrency(self):
|
def test_2_File_Concurrency(self):
|
||||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
|
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
|
||||||
self._test_Concurrency()
|
self._test_Concurrency()
|
||||||
|
@ -243,7 +246,7 @@ class SessionTest(helper.CPWebCase):
|
||||||
|
|
||||||
# Get initial cookie
|
# Get initial cookie
|
||||||
self.getPage('/')
|
self.getPage('/')
|
||||||
self.assertBody('1')
|
assert self.body == b'1'
|
||||||
cookies = self.cookies
|
cookies = self.cookies
|
||||||
|
|
||||||
data_dict = {}
|
data_dict = {}
|
||||||
|
@ -285,13 +288,14 @@ class SessionTest(helper.CPWebCase):
|
||||||
|
|
||||||
for e in errors:
|
for e in errors:
|
||||||
print(e)
|
print(e)
|
||||||
self.assertEqual(hitcount, expected)
|
assert len(errors) == 0
|
||||||
|
assert hitcount == expected
|
||||||
|
|
||||||
def test_3_Redirect(self):
|
def test_3_Redirect(self):
|
||||||
# Start a new session
|
# Start a new session
|
||||||
self.getPage('/testStr')
|
self.getPage('/testStr')
|
||||||
self.getPage('/iredir', self.cookies)
|
self.getPage('/iredir', self.cookies)
|
||||||
self.assertBody('FileSession')
|
assert self.body == b'FileSession'
|
||||||
|
|
||||||
def test_4_File_deletion(self):
|
def test_4_File_deletion(self):
|
||||||
# Start a new session
|
# Start a new session
|
||||||
|
@ -319,9 +323,9 @@ class SessionTest(helper.CPWebCase):
|
||||||
# grab the cookie ID
|
# grab the cookie ID
|
||||||
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||||
self.getPage('/regen')
|
self.getPage('/regen')
|
||||||
self.assertBody('logged in')
|
assert self.body == b'logged in'
|
||||||
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||||
self.assertNotEqual(id1, id2)
|
assert id1 != id2
|
||||||
|
|
||||||
self.getPage('/testStr')
|
self.getPage('/testStr')
|
||||||
# grab the cookie ID
|
# grab the cookie ID
|
||||||
|
@ -332,8 +336,8 @@ class SessionTest(helper.CPWebCase):
|
||||||
'session_id=maliciousid; '
|
'session_id=maliciousid; '
|
||||||
'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
|
'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
|
||||||
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
|
||||||
self.assertNotEqual(id1, id2)
|
assert id1 != id2
|
||||||
self.assertNotEqual(id2, 'maliciousid')
|
assert id2 != 'maliciousid'
|
||||||
|
|
||||||
def test_7_session_cookies(self):
|
def test_7_session_cookies(self):
|
||||||
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
|
||||||
|
@ -343,18 +347,18 @@ class SessionTest(helper.CPWebCase):
|
||||||
cookie_parts = dict([p.strip().split('=')
|
cookie_parts = dict([p.strip().split('=')
|
||||||
for p in self.cookies[0][1].split(';')])
|
for p in self.cookies[0][1].split(';')])
|
||||||
# Assert there is no 'expires' param
|
# Assert there is no 'expires' param
|
||||||
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
|
assert set(cookie_parts.keys()) == {'temp', 'Path'}
|
||||||
id1 = cookie_parts['temp']
|
id1 = cookie_parts['temp']
|
||||||
self.assertEqual(list(sessions.RamSession.cache), [id1])
|
assert list(sessions.RamSession.cache) == [id1]
|
||||||
|
|
||||||
# Send another request in the same "browser session".
|
# Send another request in the same "browser session".
|
||||||
self.getPage('/session_cookie', self.cookies)
|
self.getPage('/session_cookie', self.cookies)
|
||||||
cookie_parts = dict([p.strip().split('=')
|
cookie_parts = dict([p.strip().split('=')
|
||||||
for p in self.cookies[0][1].split(';')])
|
for p in self.cookies[0][1].split(';')])
|
||||||
# Assert there is no 'expires' param
|
# Assert there is no 'expires' param
|
||||||
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
|
assert set(cookie_parts.keys()) == {'temp', 'Path'}
|
||||||
self.assertBody(id1)
|
assert self.body.decode('utf-8') == id1
|
||||||
self.assertEqual(list(sessions.RamSession.cache), [id1])
|
assert list(sessions.RamSession.cache) == [id1]
|
||||||
|
|
||||||
# Simulate a browser close by just not sending the cookies
|
# Simulate a browser close by just not sending the cookies
|
||||||
self.getPage('/session_cookie')
|
self.getPage('/session_cookie')
|
||||||
|
@ -362,12 +366,11 @@ class SessionTest(helper.CPWebCase):
|
||||||
cookie_parts = dict([p.strip().split('=')
|
cookie_parts = dict([p.strip().split('=')
|
||||||
for p in self.cookies[0][1].split(';')])
|
for p in self.cookies[0][1].split(';')])
|
||||||
# Assert there is no 'expires' param
|
# Assert there is no 'expires' param
|
||||||
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
|
assert set(cookie_parts.keys()) == {'temp', 'Path'}
|
||||||
# Assert a new id has been generated...
|
# Assert a new id has been generated...
|
||||||
id2 = cookie_parts['temp']
|
id2 = cookie_parts['temp']
|
||||||
self.assertNotEqual(id1, id2)
|
assert id1 != id2
|
||||||
self.assertEqual(set(sessions.RamSession.cache.keys()),
|
assert set(sessions.RamSession.cache.keys()) == {id1, id2}
|
||||||
set([id1, id2]))
|
|
||||||
|
|
||||||
# Wait for the session.timeout on both sessions
|
# Wait for the session.timeout on both sessions
|
||||||
time.sleep(2.5)
|
time.sleep(2.5)
|
||||||
|
@ -398,115 +401,147 @@ class SessionTest(helper.CPWebCase):
|
||||||
t.join()
|
t.join()
|
||||||
|
|
||||||
|
|
||||||
try:
|
def is_memcached_present():
|
||||||
importlib.import_module('memcache')
|
executable = find_executable('memcached')
|
||||||
|
return bool(executable)
|
||||||
|
|
||||||
host, port = '127.0.0.1', 11211
|
|
||||||
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
@pytest.fixture(scope='session')
|
||||||
socket.SOCK_STREAM):
|
def memcached_server_present():
|
||||||
af, socktype, proto, canonname, sa = res
|
is_memcached_present() or pytest.skip('memcached not available')
|
||||||
s = None
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def memcached_client_present():
|
||||||
|
pytest.importorskip('memcache')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def memcached_instance(request, watcher_getter, memcached_server_present):
|
||||||
|
"""
|
||||||
|
Start up an instance of memcached.
|
||||||
|
"""
|
||||||
|
|
||||||
|
port = portend.find_available_local_port()
|
||||||
|
|
||||||
|
def is_occupied():
|
||||||
try:
|
try:
|
||||||
s = socket.socket(af, socktype, proto)
|
portend.Checker().assert_free('localhost', port)
|
||||||
# See http://groups.google.com/group/cherrypy-users/
|
except Exception:
|
||||||
# browse_frm/thread/bbfe5eb39c904fe0
|
return True
|
||||||
s.settimeout(1.0)
|
return False
|
||||||
s.connect((host, port))
|
|
||||||
s.close()
|
|
||||||
except socket.error:
|
|
||||||
if s:
|
|
||||||
s.close()
|
|
||||||
raise
|
|
||||||
break
|
|
||||||
except (ImportError, socket.error):
|
|
||||||
class MemcachedSessionTest(helper.CPWebCase):
|
|
||||||
setup_server = staticmethod(setup_server)
|
|
||||||
|
|
||||||
def test(self):
|
proc = watcher_getter(
|
||||||
return self.skip('memcached not reachable ')
|
name='memcached',
|
||||||
else:
|
arguments=['-p', str(port)],
|
||||||
class MemcachedSessionTest(helper.CPWebCase):
|
checker=is_occupied,
|
||||||
setup_server = staticmethod(setup_server)
|
request=request,
|
||||||
|
)
|
||||||
|
return locals()
|
||||||
|
|
||||||
def test_0_Session(self):
|
|
||||||
self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession')
|
|
||||||
|
|
||||||
self.getPage('/testStr')
|
@pytest.fixture
|
||||||
self.assertBody('1')
|
def memcached_configured(
|
||||||
self.getPage('/testGen', self.cookies)
|
memcached_instance, monkeypatch,
|
||||||
self.assertBody('2')
|
memcached_client_present,
|
||||||
self.getPage('/testStr', self.cookies)
|
):
|
||||||
self.assertBody('3')
|
server = 'localhost:{port}'.format_map(memcached_instance)
|
||||||
self.getPage('/length', self.cookies)
|
monkeypatch.setattr(
|
||||||
self.assertErrorPage(500)
|
sessions.MemcachedSession,
|
||||||
self.assertInBody('NotImplementedError')
|
'servers',
|
||||||
self.getPage('/delkey?key=counter', self.cookies)
|
[server],
|
||||||
self.assertStatus(200)
|
)
|
||||||
|
|
||||||
# Wait for the session.timeout (1 second)
|
|
||||||
time.sleep(1.25)
|
|
||||||
self.getPage('/')
|
|
||||||
self.assertBody('1')
|
|
||||||
|
|
||||||
# Test session __contains__
|
@pytest.mark.skipif(
|
||||||
self.getPage('/keyin?key=counter', self.cookies)
|
platform.system() == 'Windows',
|
||||||
self.assertBody('True')
|
reason='pytest-services helper does not work under Windows',
|
||||||
|
)
|
||||||
|
@pytest.mark.usefixtures('memcached_configured')
|
||||||
|
class MemcachedSessionTest(helper.CPWebCase):
|
||||||
|
setup_server = staticmethod(setup_server)
|
||||||
|
|
||||||
# Test session delete
|
def test_0_Session(self):
|
||||||
self.getPage('/delete', self.cookies)
|
self.getPage(
|
||||||
self.assertBody('done')
|
'/set_session_cls/cherrypy.lib.sessions.MemcachedSession'
|
||||||
|
)
|
||||||
|
|
||||||
def test_1_Concurrency(self):
|
self.getPage('/testStr')
|
||||||
client_thread_count = 5
|
assert self.body == b'1'
|
||||||
request_count = 30
|
self.getPage('/testGen', self.cookies)
|
||||||
|
assert self.body == b'2'
|
||||||
|
self.getPage('/testStr', self.cookies)
|
||||||
|
assert self.body == b'3'
|
||||||
|
self.getPage('/length', self.cookies)
|
||||||
|
self.assertErrorPage(500)
|
||||||
|
assert b'NotImplementedError' in self.body
|
||||||
|
self.getPage('/delkey?key=counter', self.cookies)
|
||||||
|
assert self.status_code == 200
|
||||||
|
|
||||||
# Get initial cookie
|
# Wait for the session.timeout (1 second)
|
||||||
self.getPage('/')
|
time.sleep(1.25)
|
||||||
self.assertBody('1')
|
self.getPage('/')
|
||||||
cookies = self.cookies
|
assert self.body == b'1'
|
||||||
|
|
||||||
data_dict = {}
|
# Test session __contains__
|
||||||
|
self.getPage('/keyin?key=counter', self.cookies)
|
||||||
|
assert self.body == b'True'
|
||||||
|
|
||||||
def request(index):
|
# Test session delete
|
||||||
for i in range(request_count):
|
self.getPage('/delete', self.cookies)
|
||||||
self.getPage('/', cookies)
|
assert self.body == b'done'
|
||||||
# Uncomment the following line to prove threads overlap.
|
|
||||||
# sys.stdout.write("%d " % index)
|
|
||||||
if not self.body.isdigit():
|
|
||||||
self.fail(self.body)
|
|
||||||
data_dict[index] = int(self.body)
|
|
||||||
|
|
||||||
# Start <request_count> concurrent requests from
|
def test_1_Concurrency(self):
|
||||||
# each of <client_thread_count> clients
|
client_thread_count = 5
|
||||||
ts = []
|
request_count = 30
|
||||||
for c in range(client_thread_count):
|
|
||||||
data_dict[c] = 0
|
|
||||||
t = threading.Thread(target=request, args=(c,))
|
|
||||||
ts.append(t)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
for t in ts:
|
# Get initial cookie
|
||||||
t.join()
|
self.getPage('/')
|
||||||
|
assert self.body == b'1'
|
||||||
|
cookies = self.cookies
|
||||||
|
|
||||||
hitcount = max(data_dict.values())
|
data_dict = {}
|
||||||
expected = 1 + (client_thread_count * request_count)
|
|
||||||
self.assertEqual(hitcount, expected)
|
|
||||||
|
|
||||||
def test_3_Redirect(self):
|
def request(index):
|
||||||
# Start a new session
|
for i in range(request_count):
|
||||||
self.getPage('/testStr')
|
self.getPage('/', cookies)
|
||||||
self.getPage('/iredir', self.cookies)
|
# Uncomment the following line to prove threads overlap.
|
||||||
self.assertBody('memcached')
|
# sys.stdout.write("%d " % index)
|
||||||
|
if not self.body.isdigit():
|
||||||
|
self.fail(self.body)
|
||||||
|
data_dict[index] = int(self.body)
|
||||||
|
|
||||||
def test_5_Error_paths(self):
|
# Start <request_count> concurrent requests from
|
||||||
self.getPage('/unknown/page')
|
# each of <client_thread_count> clients
|
||||||
self.assertErrorPage(
|
ts = []
|
||||||
404, "The path '/unknown/page' was not found.")
|
for c in range(client_thread_count):
|
||||||
|
data_dict[c] = 0
|
||||||
|
t = threading.Thread(target=request, args=(c,))
|
||||||
|
ts.append(t)
|
||||||
|
t.start()
|
||||||
|
|
||||||
# Note: this path is *not* the same as above. The above
|
for t in ts:
|
||||||
# takes a normal route through the session code; this one
|
t.join()
|
||||||
# skips the session code's before_handler and only calls
|
|
||||||
# before_finalize (save) and on_end (close). So the session
|
hitcount = max(data_dict.values())
|
||||||
# code has to survive calling save/close without init.
|
expected = 1 + (client_thread_count * request_count)
|
||||||
self.getPage('/restricted', self.cookies, method='POST')
|
assert hitcount == expected
|
||||||
self.assertErrorPage(405, response_codes[405][1])
|
|
||||||
|
def test_3_Redirect(self):
|
||||||
|
# Start a new session
|
||||||
|
self.getPage('/testStr')
|
||||||
|
self.getPage('/iredir', self.cookies)
|
||||||
|
assert self.body == b'MemcachedSession'
|
||||||
|
|
||||||
|
def test_5_Error_paths(self):
|
||||||
|
self.getPage('/unknown/page')
|
||||||
|
self.assertErrorPage(
|
||||||
|
404, "The path '/unknown/page' was not found.")
|
||||||
|
|
||||||
|
# Note: this path is *not* the same as above. The above
|
||||||
|
# takes a normal route through the session code; this one
|
||||||
|
# skips the session code's before_handler and only calls
|
||||||
|
# before_finalize (save) and on_end (close). So the session
|
||||||
|
# code has to survive calling save/close without init.
|
||||||
|
self.getPage('/restricted', self.cookies, method='POST')
|
||||||
|
self.assertErrorPage(405, response_codes[405][1])
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
import unittest
|
from http.client import BadStatusLine
|
||||||
import warnings
|
|
||||||
|
|
||||||
from six.moves.http_client import BadStatusLine
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import portend
|
import portend
|
||||||
|
@ -13,6 +10,7 @@ import cherrypy
|
||||||
import cherrypy.process.servers
|
import cherrypy.process.servers
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
|
||||||
|
|
||||||
engine = cherrypy.engine
|
engine = cherrypy.engine
|
||||||
thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||||
|
|
||||||
|
@ -433,41 +431,41 @@ test_case_name: "test_signal_handler_unsubscribe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WaitTests(unittest.TestCase):
|
def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name
|
||||||
|
"""
|
||||||
|
Wait on INADDR_ANY should not raise IOError
|
||||||
|
|
||||||
def test_safe_wait_INADDR_ANY(self):
|
In cases where the loopback interface does not exist, CherryPy cannot
|
||||||
"""
|
effectively determine if a port binding to INADDR_ANY was effected.
|
||||||
Wait on INADDR_ANY should not raise IOError
|
In this situation, CherryPy should assume that it failed to detect
|
||||||
|
the binding (not that the binding failed) and only warn that it could
|
||||||
|
not verify it.
|
||||||
|
"""
|
||||||
|
# At such a time that CherryPy can reliably determine one or more
|
||||||
|
# viable IP addresses of the host, this test may be removed.
|
||||||
|
|
||||||
In cases where the loopback interface does not exist, CherryPy cannot
|
# Simulate the behavior we observe when no loopback interface is
|
||||||
effectively determine if a port binding to INADDR_ANY was effected.
|
# present by: finding a port that's not occupied, then wait on it.
|
||||||
In this situation, CherryPy should assume that it failed to detect
|
|
||||||
the binding (not that the binding failed) and only warn that it could
|
|
||||||
not verify it.
|
|
||||||
"""
|
|
||||||
# At such a time that CherryPy can reliably determine one or more
|
|
||||||
# viable IP addresses of the host, this test may be removed.
|
|
||||||
|
|
||||||
# Simulate the behavior we observe when no loopback interface is
|
free_port = portend.find_available_local_port()
|
||||||
# present by: finding a port that's not occupied, then wait on it.
|
|
||||||
|
|
||||||
free_port = portend.find_available_local_port()
|
servers = cherrypy.process.servers
|
||||||
|
|
||||||
servers = cherrypy.process.servers
|
inaddr_any = '0.0.0.0'
|
||||||
|
|
||||||
inaddr_any = '0.0.0.0'
|
# Wait on the free port that's unbound
|
||||||
|
with pytest.warns(
|
||||||
|
UserWarning,
|
||||||
|
match='Unable to verify that the server is bound on ',
|
||||||
|
) as warnings:
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
with servers._safe_wait(inaddr_any, free_port):
|
||||||
|
portend.occupied(inaddr_any, free_port, timeout=1)
|
||||||
|
assert len(warnings) == 1
|
||||||
|
|
||||||
# Wait on the free port that's unbound
|
# The wait should still raise an IO error if INADDR_ANY was
|
||||||
with warnings.catch_warnings(record=True) as w:
|
# not supplied.
|
||||||
with servers._safe_wait(inaddr_any, free_port):
|
with pytest.raises(IOError):
|
||||||
portend.occupied(inaddr_any, free_port, timeout=1)
|
# pylint: disable=protected-access
|
||||||
self.assertEqual(len(w), 1)
|
with servers._safe_wait('127.0.0.1', free_port):
|
||||||
self.assertTrue(isinstance(w[0], warnings.WarningMessage))
|
portend.occupied('127.0.0.1', free_port, timeout=1)
|
||||||
self.assertTrue(
|
|
||||||
'Unable to verify that the server is bound on ' in str(w[0]))
|
|
||||||
|
|
||||||
# The wait should still raise an IO error if INADDR_ANY was
|
|
||||||
# not supplied.
|
|
||||||
with pytest.raises(IOError):
|
|
||||||
with servers._safe_wait('127.0.0.1', free_port):
|
|
||||||
portend.occupied('127.0.0.1', free_port, timeout=1)
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import contextlib
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
import platform
|
import platform
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import urllib.parse
|
||||||
from six import text_type as str
|
import unittest.mock
|
||||||
from six.moves import urllib
|
from http.client import HTTPConnection
|
||||||
from six.moves.http_client import HTTPConnection
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import py.path
|
import py.path
|
||||||
|
import path
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.lib import static
|
from cherrypy.lib import static
|
||||||
|
@ -46,9 +46,9 @@ def ensure_unicode_filesystem():
|
||||||
tmpdir.remove()
|
tmpdir.remove()
|
||||||
|
|
||||||
|
|
||||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
curdir = path.Path(__file__).dirname()
|
||||||
has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
|
has_space_filepath = curdir / 'static' / 'has space.html'
|
||||||
bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log')
|
bigfile_filepath = curdir / 'static' / 'bigfile.log'
|
||||||
|
|
||||||
# The file size needs to be big enough such that half the size of it
|
# The file size needs to be big enough such that half the size of it
|
||||||
# won't be socket-buffered (or server-buffered) all in one go. See
|
# won't be socket-buffered (or server-buffered) all in one go. See
|
||||||
|
@ -58,6 +58,7 @@ BIGFILE_SIZE = 32 * MB
|
||||||
|
|
||||||
|
|
||||||
class StaticTest(helper.CPWebCase):
|
class StaticTest(helper.CPWebCase):
|
||||||
|
files_to_remove = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup_server():
|
def setup_server():
|
||||||
|
@ -96,6 +97,20 @@ class StaticTest(helper.CPWebCase):
|
||||||
f = io.BytesIO(b'Fee\nfie\nfo\nfum')
|
f = io.BytesIO(b'Fee\nfie\nfo\nfum')
|
||||||
return static.serve_fileobj(f, content_type='text/plain')
|
return static.serve_fileobj(f, content_type='text/plain')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def serve_file_utf8_filename(self):
|
||||||
|
return static.serve_file(
|
||||||
|
__file__,
|
||||||
|
disposition='attachment',
|
||||||
|
name='has_utf-8_character_☃.html')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def serve_fileobj_utf8_filename(self):
|
||||||
|
return static.serve_fileobj(
|
||||||
|
io.BytesIO('☃\nfie\nfo\nfum'.encode('utf-8')),
|
||||||
|
disposition='attachment',
|
||||||
|
name='has_utf-8_character_☃.html')
|
||||||
|
|
||||||
class Static:
|
class Static:
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@ -157,14 +172,13 @@ class StaticTest(helper.CPWebCase):
|
||||||
vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
|
vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
|
||||||
cherrypy.tree.graft(vhost)
|
cherrypy.tree.graft(vhost)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def teardown_server():
|
def teardown_class(cls):
|
||||||
for f in (has_space_filepath, bigfile_filepath):
|
super(cls, cls).teardown_class()
|
||||||
if os.path.exists(f):
|
files_to_remove = has_space_filepath, bigfile_filepath
|
||||||
try:
|
files_to_remove += tuple(cls.files_to_remove)
|
||||||
os.unlink(f)
|
for file in files_to_remove:
|
||||||
except Exception:
|
file.remove_p()
|
||||||
pass
|
|
||||||
|
|
||||||
def test_static(self):
|
def test_static(self):
|
||||||
self.getPage('/static/index.html')
|
self.getPage('/static/index.html')
|
||||||
|
@ -193,6 +207,22 @@ class StaticTest(helper.CPWebCase):
|
||||||
# we just check the content
|
# we just check the content
|
||||||
self.assertMatchesBody('^Dummy stylesheet')
|
self.assertMatchesBody('^Dummy stylesheet')
|
||||||
|
|
||||||
|
# Check a filename with utf-8 characters in it
|
||||||
|
ascii_fn = 'has_utf-8_character_.html'
|
||||||
|
url_quote_fn = 'has_utf-8_character_%E2%98%83.html' # %E2%98%83 == ☃
|
||||||
|
expected_content_disposition = (
|
||||||
|
'attachment; filename="{!s}"; filename*=UTF-8\'\'{!s}'.
|
||||||
|
format(ascii_fn, url_quote_fn)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.getPage('/serve_file_utf8_filename')
|
||||||
|
self.assertStatus('200 OK')
|
||||||
|
self.assertHeader('Content-Disposition', expected_content_disposition)
|
||||||
|
|
||||||
|
self.getPage('/serve_fileobj_utf8_filename')
|
||||||
|
self.assertStatus('200 OK')
|
||||||
|
self.assertHeader('Content-Disposition', expected_content_disposition)
|
||||||
|
|
||||||
@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only')
|
@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only')
|
||||||
def test_static_longpath(self):
|
def test_static_longpath(self):
|
||||||
"""Test serving of a file in subdir of a Windows long-path
|
"""Test serving of a file in subdir of a Windows long-path
|
||||||
|
@ -399,38 +429,34 @@ class StaticTest(helper.CPWebCase):
|
||||||
self.assertStatus(404)
|
self.assertStatus(404)
|
||||||
self.assertInBody("I couldn't find that thing")
|
self.assertInBody("I couldn't find that thing")
|
||||||
|
|
||||||
|
@unittest.mock.patch(
|
||||||
|
'http.client._contains_disallowed_url_pchar_re',
|
||||||
|
re.compile(r'[\n]'),
|
||||||
|
create=True,
|
||||||
|
)
|
||||||
def test_null_bytes(self):
|
def test_null_bytes(self):
|
||||||
self.getPage('/static/\x00')
|
self.getPage('/static/\x00')
|
||||||
self.assertStatus('404 Not Found')
|
self.assertStatus('404 Not Found')
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
@contextlib.contextmanager
|
def unicode_file(cls):
|
||||||
def unicode_file():
|
|
||||||
filename = ntou('Слава Україні.html', 'utf-8')
|
filename = ntou('Слава Україні.html', 'utf-8')
|
||||||
filepath = os.path.join(curdir, 'static', filename)
|
filepath = curdir / 'static' / filename
|
||||||
with io.open(filepath, 'w', encoding='utf-8') as strm:
|
with filepath.open('w', encoding='utf-8')as strm:
|
||||||
strm.write(ntou('Героям Слава!', 'utf-8'))
|
strm.write(ntou('Героям Слава!', 'utf-8'))
|
||||||
try:
|
cls.files_to_remove.append(filepath)
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
os.remove(filepath)
|
|
||||||
|
|
||||||
py27_on_windows = (
|
|
||||||
platform.system() == 'Windows' and
|
|
||||||
sys.version_info < (3,)
|
|
||||||
)
|
|
||||||
@pytest.mark.xfail(py27_on_windows, reason='#1544') # noqa: E301
|
|
||||||
def test_unicode(self):
|
def test_unicode(self):
|
||||||
ensure_unicode_filesystem()
|
ensure_unicode_filesystem()
|
||||||
with self.unicode_file():
|
self.unicode_file()
|
||||||
url = ntou('/static/Слава Україні.html', 'utf-8')
|
url = ntou('/static/Слава Україні.html', 'utf-8')
|
||||||
# quote function requires str
|
# quote function requires str
|
||||||
url = tonative(url, 'utf-8')
|
url = tonative(url, 'utf-8')
|
||||||
url = urllib.parse.quote(url)
|
url = urllib.parse.quote(url)
|
||||||
self.getPage(url)
|
self.getPage(url)
|
||||||
|
|
||||||
expected = ntou('Героям Слава!', 'utf-8')
|
expected = ntou('Героям Слава!', 'utf-8')
|
||||||
self.assertInBody(expected)
|
self.assertInBody(expected)
|
||||||
|
|
||||||
|
|
||||||
def error_page_404(status, message, traceback, version):
|
def error_page_404(status, message, traceback, version):
|
||||||
|
|
|
@ -7,10 +7,7 @@ import time
|
||||||
import types
|
import types
|
||||||
import unittest
|
import unittest
|
||||||
import operator
|
import operator
|
||||||
|
from http.client import IncompleteRead
|
||||||
import six
|
|
||||||
from six.moves import range, map
|
|
||||||
from six.moves.http_client import IncompleteRead
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy import tools
|
from cherrypy import tools
|
||||||
|
@ -18,6 +15,16 @@ from cherrypy._cpcompat import ntou
|
||||||
from cherrypy.test import helper, _test_decorators
|
from cherrypy.test import helper, _test_decorators
|
||||||
|
|
||||||
|
|
||||||
|
*PY_VER_MINOR, _ = PY_VER_PATCH = sys.version_info[:3]
|
||||||
|
# Refs:
|
||||||
|
# bugs.python.org/issue39389
|
||||||
|
# docs.python.org/3.7/whatsnew/changelog.html#python-3-7-7-release-candidate-1
|
||||||
|
# docs.python.org/3.8/whatsnew/changelog.html#python-3-8-2-release-candidate-1
|
||||||
|
HAS_GZIP_COMPRESSION_HEADER_FIXED = PY_VER_PATCH >= (3, 8, 2) or (
|
||||||
|
PY_VER_MINOR == (3, 7) and PY_VER_PATCH >= (3, 7, 7)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
timeout = 0.2
|
timeout = 0.2
|
||||||
europoundUnicode = ntou('\x80\xa3')
|
europoundUnicode = ntou('\x80\xa3')
|
||||||
|
|
||||||
|
@ -52,7 +59,7 @@ class ToolTests(helper.CPWebCase):
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
def makemap():
|
def makemap():
|
||||||
m = self._merged_args().get('map', {})
|
m = self._merged_args().get('map', {})
|
||||||
cherrypy.request.numerify_map = list(six.iteritems(m))
|
cherrypy.request.numerify_map = list(m.items())
|
||||||
cherrypy.request.hooks.attach('on_start_resource', makemap)
|
cherrypy.request.hooks.attach('on_start_resource', makemap)
|
||||||
|
|
||||||
def critical():
|
def critical():
|
||||||
|
@ -105,10 +112,7 @@ class ToolTests(helper.CPWebCase):
|
||||||
def __call__(self, scale):
|
def __call__(self, scale):
|
||||||
r = cherrypy.response
|
r = cherrypy.response
|
||||||
r.collapse_body()
|
r.collapse_body()
|
||||||
if six.PY3:
|
r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
|
||||||
r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
|
|
||||||
else:
|
|
||||||
r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
|
|
||||||
cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
|
cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
|
||||||
|
|
||||||
def stream_handler(next_handler, *args, **kwargs):
|
def stream_handler(next_handler, *args, **kwargs):
|
||||||
|
@ -179,7 +183,7 @@ class ToolTests(helper.CPWebCase):
|
||||||
"""
|
"""
|
||||||
def __init__(cls, name, bases, dct):
|
def __init__(cls, name, bases, dct):
|
||||||
type.__init__(cls, name, bases, dct)
|
type.__init__(cls, name, bases, dct)
|
||||||
for value in six.itervalues(dct):
|
for value in dct.values():
|
||||||
if isinstance(value, types.FunctionType):
|
if isinstance(value, types.FunctionType):
|
||||||
cherrypy.expose(value)
|
cherrypy.expose(value)
|
||||||
setattr(root, name.lower(), cls())
|
setattr(root, name.lower(), cls())
|
||||||
|
@ -346,7 +350,7 @@ class ToolTests(helper.CPWebCase):
|
||||||
self.getPage('/demo/err_in_onstart')
|
self.getPage('/demo/err_in_onstart')
|
||||||
self.assertErrorPage(502)
|
self.assertErrorPage(502)
|
||||||
tmpl = "AttributeError: 'str' object has no attribute '{attr}'"
|
tmpl = "AttributeError: 'str' object has no attribute '{attr}'"
|
||||||
expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems')
|
expected_msg = tmpl.format(attr='items')
|
||||||
self.assertInBody(expected_msg)
|
self.assertInBody(expected_msg)
|
||||||
|
|
||||||
def testCombinedTools(self):
|
def testCombinedTools(self):
|
||||||
|
@ -363,6 +367,13 @@ class ToolTests(helper.CPWebCase):
|
||||||
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')])
|
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')])
|
||||||
self.assertInBody(zbuf.getvalue()[:3])
|
self.assertInBody(zbuf.getvalue()[:3])
|
||||||
|
|
||||||
|
if not HAS_GZIP_COMPRESSION_HEADER_FIXED:
|
||||||
|
# NOTE: CherryPy adopts a fix from the CPython bug 39389
|
||||||
|
# NOTE: introducing a variable compression XFL flag that
|
||||||
|
# NOTE: was hardcoded to "best compression" before. And so
|
||||||
|
# NOTE: we can only test it on CPython versions that also
|
||||||
|
# NOTE: implement this fix.
|
||||||
|
return
|
||||||
zbuf = io.BytesIO()
|
zbuf = io.BytesIO()
|
||||||
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
|
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
|
||||||
zfile.write(expectedResult)
|
zfile.write(expectedResult)
|
||||||
|
@ -377,11 +388,7 @@ class ToolTests(helper.CPWebCase):
|
||||||
# but it proves the priority was changed.
|
# but it proves the priority was changed.
|
||||||
self.getPage('/decorated_euro/subpath',
|
self.getPage('/decorated_euro/subpath',
|
||||||
headers=[('Accept-Encoding', 'gzip')])
|
headers=[('Accept-Encoding', 'gzip')])
|
||||||
if six.PY3:
|
self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
|
||||||
self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
|
|
||||||
else:
|
|
||||||
self.assertInBody(''.join([chr((ord(x) + 3) % 256)
|
|
||||||
for x in zbuf.getvalue()]))
|
|
||||||
|
|
||||||
def testBareHooks(self):
|
def testBareHooks(self):
|
||||||
content = 'bit of a pain in me gulliver'
|
content = 'bit of a pain in me gulliver'
|
||||||
|
@ -429,7 +436,7 @@ class ToolTests(helper.CPWebCase):
|
||||||
@cherrypy.tools.register( # noqa: F811
|
@cherrypy.tools.register( # noqa: F811
|
||||||
'before_finalize', name='renamed', priority=60,
|
'before_finalize', name='renamed', priority=60,
|
||||||
)
|
)
|
||||||
def example():
|
def example(): # noqa: F811
|
||||||
pass
|
pass
|
||||||
self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool))
|
self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool))
|
||||||
self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize')
|
self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize')
|
||||||
|
@ -446,8 +453,8 @@ class SessionAuthTest(unittest.TestCase):
|
||||||
username and password were unicode.
|
username and password were unicode.
|
||||||
"""
|
"""
|
||||||
sa = cherrypy.lib.cptools.SessionAuth()
|
sa = cherrypy.lib.cptools.SessionAuth()
|
||||||
res = sa.login_screen(None, username=six.text_type('nobody'),
|
res = sa.login_screen(None, username=str('nobody'),
|
||||||
password=six.text_type('anypass'))
|
password=str('anypass'))
|
||||||
self.assertTrue(isinstance(res, bytes))
|
self.assertTrue(isinstance(res, bytes))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import sys
|
import sys
|
||||||
import imp
|
|
||||||
import types
|
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
|
||||||
|
@ -27,7 +23,7 @@ class TutorialTest(helper.CPWebCase):
|
||||||
"""
|
"""
|
||||||
target = 'cherrypy.tutorial.' + name
|
target = 'cherrypy.tutorial.' + name
|
||||||
if target in sys.modules:
|
if target in sys.modules:
|
||||||
module = imp.reload(sys.modules[target])
|
module = importlib.reload(sys.modules[target])
|
||||||
else:
|
else:
|
||||||
module = importlib.import_module(target)
|
module = importlib.import_module(target)
|
||||||
return module
|
return module
|
||||||
|
@ -39,8 +35,6 @@ class TutorialTest(helper.CPWebCase):
|
||||||
root = getattr(module, root_name)
|
root = getattr(module, root_name)
|
||||||
conf = getattr(module, 'tutconf')
|
conf = getattr(module, 'tutconf')
|
||||||
class_types = type,
|
class_types = type,
|
||||||
if six.PY2:
|
|
||||||
class_types += types.ClassType,
|
|
||||||
if isinstance(root, class_types):
|
if isinstance(root, class_types):
|
||||||
root = root()
|
root = root()
|
||||||
cherrypy.tree.mount(root, config=conf)
|
cherrypy.tree.mount(root, config=conf)
|
||||||
|
|
|
@ -2,8 +2,7 @@ import os
|
||||||
import socket
|
import socket
|
||||||
import atexit
|
import atexit
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from http.client import HTTPConnection
|
||||||
from six.moves.http_client import HTTPConnection
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
|
@ -1,54 +1,20 @@
|
||||||
import sys
|
import sys
|
||||||
|
import socket
|
||||||
|
|
||||||
import six
|
from xmlrpc.client import (
|
||||||
|
|
||||||
from six.moves.xmlrpc_client import (
|
|
||||||
DateTime, Fault,
|
DateTime, Fault,
|
||||||
ProtocolError, ServerProxy, SafeTransport
|
ServerProxy, SafeTransport
|
||||||
)
|
)
|
||||||
|
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from cherrypy import _cptools
|
from cherrypy import _cptools
|
||||||
from cherrypy.test import helper
|
from cherrypy.test import helper
|
||||||
|
|
||||||
if six.PY3:
|
HTTPSTransport = SafeTransport
|
||||||
HTTPSTransport = SafeTransport
|
|
||||||
|
|
||||||
# Python 3.0's SafeTransport still mistakenly checks for socket.ssl
|
# Python 3.0's SafeTransport still mistakenly checks for socket.ssl
|
||||||
import socket
|
if not hasattr(socket, 'ssl'):
|
||||||
if not hasattr(socket, 'ssl'):
|
socket.ssl = True
|
||||||
socket.ssl = True
|
|
||||||
else:
|
|
||||||
class HTTPSTransport(SafeTransport):
|
|
||||||
|
|
||||||
"""Subclass of SafeTransport to fix sock.recv errors (by using file).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def request(self, host, handler, request_body, verbose=0):
|
|
||||||
# issue XML-RPC request
|
|
||||||
h = self.make_connection(host)
|
|
||||||
if verbose:
|
|
||||||
h.set_debuglevel(1)
|
|
||||||
|
|
||||||
self.send_request(h, handler, request_body)
|
|
||||||
self.send_host(h, host)
|
|
||||||
self.send_user_agent(h)
|
|
||||||
self.send_content(h, request_body)
|
|
||||||
|
|
||||||
errcode, errmsg, headers = h.getreply()
|
|
||||||
if errcode != 200:
|
|
||||||
raise ProtocolError(host + handler, errcode, errmsg, headers)
|
|
||||||
|
|
||||||
self.verbose = verbose
|
|
||||||
|
|
||||||
# Here's where we differ from the superclass. It says:
|
|
||||||
# try:
|
|
||||||
# sock = h._conn.sock
|
|
||||||
# except AttributeError:
|
|
||||||
# sock = None
|
|
||||||
# return self._parse_response(h.getfile(), sock)
|
|
||||||
|
|
||||||
return self.parse_response(h.getfile())
|
|
||||||
|
|
||||||
|
|
||||||
def setup_server():
|
def setup_server():
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue