Update cherrypy-18.6.1

This commit is contained in:
JonnyWong16 2021-10-14 21:17:18 -07:00
parent b3ae6bd695
commit ebffd124f6
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
57 changed files with 1269 additions and 1509 deletions

View file

@ -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' %

View file

@ -18,16 +18,10 @@ 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.
""" """
@ -35,7 +29,8 @@ if six.PY3:
# 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'):
def ntou(n, encoding='ISO-8859-1'):
"""Return the given native string as a unicode string with the given """Return the given native string as a unicode string with the given
encoding. encoding.
""" """
@ -43,49 +38,13 @@ if six.PY3:
# In Python 3, the native string type is unicode # In Python 3, the native string type is unicode
return n return n
def tonative(n, encoding='ISO-8859-1'):
def tonative(n, encoding='ISO-8859-1'):
"""Return the given string as a native string in the given encoding.""" """Return the given string as a native string in the given encoding."""
# In Python 3, the native string type is unicode # In Python 3, the native string type is unicode
if isinstance(n, bytes): if isinstance(n, bytes):
return n.decode(encoding) return n.decode(encoding)
return n return n
else:
# 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
encoding.
"""
assert_native(n)
# 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)
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)

View file

@ -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

View file

@ -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,7 +248,6 @@ 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,
@ -270,7 +263,6 @@ 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)
@ -292,23 +284,6 @@ class LogManager(object):
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)."""

View file

@ -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

View file

@ -115,10 +115,16 @@ 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: import cheroot.server
def unquote_plus(bs):
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.lib import httputil
def unquote_plus(bs):
"""Bytes version of urllib.parse.unquote_plus.""" """Bytes version of urllib.parse.unquote_plus."""
bs = bs.replace(b'+', b' ') bs = bs.replace(b'+', b' ')
atoms = bs.split(b'%') atoms = bs.split(b'%')
@ -131,13 +137,6 @@ except ImportError:
pass pass
return b''.join(atoms) return b''.join(atoms)
import six
import cheroot.server
import cherrypy
from cherrypy._cpcompat import ntou, unquote
from cherrypy.lib import httputil
# ------------------------------- Processors -------------------------------- # # ------------------------------- Processors -------------------------------- #
@ -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]]

View file

@ -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
# the same hookpoint fail. We will still log the failure,
# but proceed on to the next hook. The only way
# to stop all processing from one of these hooks is
# to raise SystemExit and stop the whole server.
if exc is None or hook.failsafe:
try: try:
hook() hook()
except (KeyboardInterrupt, SystemExit): except quiet_errors:
cls.run_hooks(safe)
raise raise
except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
cherrypy.InternalRedirect):
exc = sys.exc_info()[1]
except Exception: except Exception:
exc = sys.exc_info()[1]
cherrypy.log(traceback=True, severity=40) cherrypy.log(traceback=True, severity=40)
if exc: cls.run_hooks(safe)
raise exc raise
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))

View file

@ -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."""

View file

@ -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]

View file

@ -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):
# Python 2/WSGI u.0: all strings MUST be of type unicode
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['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip('/')):] environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
return app(environ, start_response) return app(environ, start_response)

View file

@ -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.

View file

@ -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,6 +83,9 @@ 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:
.. code-block:: python
@cherrypy.popargs('year', 'month', 'day') @cherrypy.popargs('year', 'month', 'day')
class Blog: class Blog:
def index(self, year=None, month=None, day=None): def index(self, year=None, month=None, day=None):
@ -95,10 +94,13 @@ def popargs(*args, **kwargs):
#will fill in the appropriate parameters. #will fill in the appropriate parameters.
def create(self): def create(self):
#This link will still be available at /create. Defined functions #This link will still be available at /create.
#take precedence over arguments. #Defined functions take precedence over arguments.
Or as a member of a class: Or as a member of a class:
.. code-block:: python
class Blog: class Blog:
_cp_dispatch = cherrypy.popargs('year', 'month', 'day') _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
#... #...
@ -107,6 +109,8 @@ def popargs(*args, **kwargs):
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:
.. code-block:: python
class DayHandler: class DayHandler:
def index(self, year, month, day): def index(self, year, month, day):
#Do something with this day; probably list entries #Do something with this day; probably list entries

25
lib/cherrypy/_json.py Normal file
View 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')

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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):

View file

@ -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)

View file

@ -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))
# 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 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'')

View file

@ -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.

View file

@ -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,

View file

@ -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)

View file

@ -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):

View file

@ -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')

View file

@ -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
) )

View file

@ -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

View file

@ -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()

View file

@ -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):

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'
), ),

View file

@ -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,7 +90,6 @@ 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 % (

View file

@ -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():
"""Return a wspbus instance."""
return wspbus.Bus()
@pytest.fixture
def log_tracker(bus):
"""Return an instance of bus log tracker."""
class LogTracker: # pylint: disable=too-few-public-methods
"""Bus log tracker."""
log_entries = []
def __init__(self, bus):
def logit(msg, level): # pylint: disable=unused-argument
self.log_entries.append(msg)
bus.subscribe('log', logit)
return LogTracker(bus)
@pytest.fixture
def listener():
"""Return an instance of bus response tracker."""
class Listner: # pylint: disable=too-few-public-methods
"""Bus handler return value tracker."""
responses = []
def get_listener(self, channel, index): def get_listener(self, channel, index):
"""Return an argument tracking listener."""
def listener(arg=None): def listener(arg=None):
self.responses.append(msg % (index, channel, arg)) self.responses.append(msg % (index, channel, arg))
return listener return listener
def test_builtin_channels(self): return Listner()
b = wspbus.Bus()
self.responses, expected = [], []
for channel in b.listeners: def test_builtin_channels(bus, listener):
"""Test that built-in channels trigger corresponding listeners."""
expected = []
for channel in bus.listeners:
for index, priority in enumerate([100, 50, 0, 51]): for index, priority in enumerate([100, 50, 0, 51]):
b.subscribe(channel, bus.subscribe(
self.get_listener(channel, index), priority) channel,
listener.get_listener(channel, index),
priority,
)
for channel in b.listeners: for channel in bus.listeners:
b.publish(channel) bus.publish(channel)
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
b.publish(channel, arg=79347) bus.publish(channel, arg=79347)
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
self.assertEqual(self.responses, expected) assert listener.responses == expected
def test_custom_channels(self):
b = wspbus.Bus()
self.responses, expected = [], [] def test_custom_channels(bus, listener):
"""Test that custom pub-sub channels work as built-in ones."""
expected = []
custom_listeners = ('hugh', 'louis', 'dewey') custom_listeners = ('hugh', 'louis', 'dewey')
for channel in custom_listeners: for channel in custom_listeners:
for index, priority in enumerate([None, 10, 60, 40]): for index, priority in enumerate([None, 10, 60, 40]):
b.subscribe(channel, bus.subscribe(
self.get_listener(channel, index), priority) channel,
listener.get_listener(channel, index),
priority,
)
for channel in custom_listeners: for channel in custom_listeners:
b.publish(channel, 'ah so') bus.publish(channel, 'ah so')
expected.extend([msg % (i, channel, 'ah so') expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2))
for i in (1, 3, 0, 2)]) bus.publish(channel)
b.publish(channel) expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2))
expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
self.assertEqual(self.responses, expected) assert listener.responses == expected
def test_listener_errors(self):
b = wspbus.Bus()
self.responses, expected = [], [] def test_listener_errors(bus, listener):
channels = [c for c in b.listeners if c != 'log'] """Test that unhandled exceptions raise channel failures."""
expected = []
channels = [c for c in bus.listeners if c != 'log']
for channel in channels: for channel in channels:
b.subscribe(channel, self.get_listener(channel, 1)) bus.subscribe(channel, listener.get_listener(channel, 1))
# This will break since the lambda takes no args. # This will break since the lambda takes no args.
b.subscribe(channel, lambda: None, priority=20) bus.subscribe(channel, lambda: None, priority=20)
for channel in channels: for channel in channels:
self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) with pytest.raises(wspbus.ChannelFailures):
bus.publish(channel, 123)
expected.append(msg % (1, channel, 123)) expected.append(msg % (1, channel, 123))
self.assertEqual(self.responses, expected) assert listener.responses == expected
class BusMethodTests(unittest.TestCase): def test_start(bus, listener, log_tracker):
"""Test that bus start sequence calls all listeners."""
def log(self, bus):
self._log_entries = []
def logit(msg, level):
self._log_entries.append(msg)
bus.subscribe('log', logit)
def assertLog(self, entries):
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):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3 num = 3
for index in range(num): for index in range(num):
b.subscribe('start', self.get_listener('start', index)) bus.subscribe('start', listener.get_listener('start', index))
b.start() bus.start()
try: try:
# The start method MUST call all 'start' listeners. # The start method MUST call all 'start' listeners.
self.assertEqual( assert (
set(self.responses), set(listener.responses) ==
set([msg % (i, 'start', None) for i in range(num)])) set(msg % (i, 'start', None) for i in range(num)))
# The start method MUST move the state to STARTED # The start method MUST move the state to STARTED
# (or EXITING, if errors occur) # (or EXITING, if errors occur)
self.assertEqual(b.state, b.states.STARTED) assert bus.state == bus.states.STARTED
# The start method MUST log its states. # The start method MUST log its states.
self.assertLog(['Bus STARTING', 'Bus STARTED']) assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED']
finally: finally:
# Exit so the atexit handler doesn't complain. # Exit so the atexit handler doesn't complain.
b.exit() bus.exit()
def test_stop(self):
b = wspbus.Bus()
self.log(b)
self.responses = [] def test_stop(bus, listener, log_tracker):
"""Test that bus stop sequence calls all listeners."""
num = 3 num = 3
for index in range(num):
b.subscribe('stop', self.get_listener('stop', index))
b.stop() for index in range(num):
bus.subscribe('stop', listener.get_listener('stop', index))
bus.stop()
# The stop method MUST call all 'stop' listeners. # The stop method MUST call all 'stop' listeners.
self.assertEqual(set(self.responses), assert (set(listener.responses) ==
set([msg % (i, 'stop', None) for i in range(num)])) set(msg % (i, 'stop', None) for i in range(num)))
# The stop method MUST move the state to STOPPED # The stop method MUST move the state to STOPPED
self.assertEqual(b.state, b.states.STOPPED) assert bus.state == bus.states.STOPPED
# The stop method MUST log its states. # The stop method MUST log its states.
self.assertLog(['Bus STOPPING', 'Bus STOPPED']) assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED']
def test_graceful(self):
b = wspbus.Bus()
self.log(b)
self.responses = [] def test_graceful(bus, listener, log_tracker):
"""Test that bus graceful state triggers all listeners."""
num = 3 num = 3
for index in range(num):
b.subscribe('graceful', self.get_listener('graceful', index))
b.graceful() for index in range(num):
bus.subscribe('graceful', listener.get_listener('graceful', index))
bus.graceful()
# The graceful method MUST call all 'graceful' listeners. # The graceful method MUST call all 'graceful' listeners.
self.assertEqual( assert (
set(self.responses), set(listener.responses) ==
set([msg % (i, 'graceful', None) for i in range(num)])) set(msg % (i, 'graceful', None) for i in range(num)))
# The graceful method MUST log its states. # The graceful method MUST log its states.
self.assertLog(['Bus graceful']) assert log_tracker.log_entries == ['Bus graceful']
def test_exit(self):
b = wspbus.Bus()
self.log(b)
self.responses = [] def test_exit(bus, listener, log_tracker):
"""Test that bus exit sequence is correct."""
num = 3 num = 3
for index in range(num):
b.subscribe('stop', self.get_listener('stop', index))
b.subscribe('exit', self.get_listener('exit', index))
b.exit() 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, # The exit method MUST call all 'stop' listeners,
# and then all 'exit' listeners. # and then all 'exit' listeners.
self.assertEqual(set(self.responses), assert (set(listener.responses) ==
set([msg % (i, 'stop', None) for i in range(num)] + set([msg % (i, 'stop', None) for i in range(num)] +
[msg % (i, 'exit', None) for i in range(num)])) [msg % (i, 'exit', None) for i in range(num)]))
# The exit method MUST move the state to EXITING # The exit method MUST move the state to EXITING
self.assertEqual(b.state, b.states.EXITING) assert bus.state == bus.states.EXITING
# The exit method MUST log its states. # The exit method MUST log its states.
self.assertLog( 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):
"""Test that bus wait awaits for states."""
def f(method): # pylint: disable=invalid-name
time.sleep(0.2) time.sleep(0.2)
getattr(b, method)() 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]),
]: ]
for method, states in flow:
threading.Thread(target=f, args=(method,)).start() threading.Thread(target=f, args=(method,)).start()
b.wait(states) bus.wait(states)
# The wait method MUST wait for the given state(s). # The wait method MUST wait for the given state(s).
if b.state not in states: assert bus.state in states, 'State %r not in %r' % (bus.state, states)
self.fail('State %r not in %r' % (b.state, states))
def test_block(self):
b = wspbus.Bus()
self.log(b)
def f(): @pytest.mark.xfail(CI_ON_MACOS, reason='continuous integration on macOS fails')
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) time.sleep(0.2)
b.exit() bus.exit()
def g(): def g(): # pylint: disable=invalid-name
time.sleep(0.4) time.sleep(0.4)
threading.Thread(target=f).start() threading.Thread(target=f).start()
threading.Thread(target=g).start() threading.Thread(target=g).start()
threads = [t for t in threading.enumerate() if not t.daemon] threads = [t for t in threading.enumerate() if not t.daemon]
self.assertEqual(len(threads), 3) assert len(threads) == 3
b.block() bus.block()
# The block method MUST wait for the EXITING state. # The block method MUST wait for the EXITING state.
self.assertEqual(b.state, b.states.EXITING) assert bus.state == bus.states.EXITING
# The block method MUST wait for ALL non-main, non-daemon threads to # The block method MUST wait for ALL non-main, non-daemon threads to
# finish. # finish.
threads = [t for t in threading.enumerate() if not t.daemon] threads = [t for t in threading.enumerate() if not t.daemon]
self.assertEqual(len(threads), 1) assert len(threads) == 1
# The last message will mention an indeterminable thread name; ignore # The last message will mention an indeterminable thread name; ignore
# it # it
self.assertEqual(self._log_entries[:-1], expected_bus_messages = [
['Bus STOPPING', 'Bus STOPPED', 'Bus STOPPING',
'Bus EXITING', 'Bus EXITED', 'Bus STOPPED',
'Waiting for child threads to terminate...']) 'Bus EXITING',
'Bus EXITED',
'Waiting for child threads to terminate...',
]
bus_msg_num = len(expected_bus_messages)
def test_start_with_callback(self): # If the last message mentions an indeterminable thread name then ignore it
b = wspbus.Bus() assert log_tracker.log_entries[:bus_msg_num] == expected_bus_messages
self.log(b) 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: try:
events = [] events = []
def f(*args, **kwargs): def f(*args, **kwargs): # pylint: disable=invalid-name
events.append(('f', args, kwargs)) events.append(('f', args, kwargs))
def g(): def g(): # pylint: disable=invalid-name
events.append('g') events.append('g')
b.subscribe('start', g) bus.subscribe('start', g)
b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
# Give wait() time to run f() # Give wait() time to run f()
time.sleep(0.2) time.sleep(0.2)
# The callback method MUST wait for the STARTED state. # The callback method MUST wait for the STARTED state.
self.assertEqual(b.state, b.states.STARTED) assert bus.state == bus.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): # The callback method MUST run after all start methods.
b = wspbus.Bus() assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})]
self.log(b) finally:
self.assertLog([]) 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. # Try a normal message.
expected = [] expected = []
for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
b.log(msg) bus.log(msg_)
expected.append(msg) expected.append(msg_)
self.assertLog(expected) assert log_tracker.log_entries == expected
# Try an error message # Try an error message
try: try:
foo foo
except NameError: except NameError:
b.log('You are lost and gone forever', traceback=True) bus.log('You are lost and gone forever', traceback=True)
lastmsg = self._log_entries[-1] lastmsg = log_tracker.log_entries[-1]
if 'Traceback' not in lastmsg or 'NameError' not in lastmsg: assert 'Traceback' in lastmsg and 'NameError' in lastmsg, (
self.fail('Last log message %r did not contain ' 'Last log message %r did not contain '
'the expected traceback.' % lastmsg) 'the expected traceback.' % lastmsg
)
else: else:
self.fail('NameError was not raised as expected.') pytest.fail('NameError was not raised as expected.')
if __name__ == '__main__':
unittest.main()

View file

@ -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.

View file

@ -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&amp;&lt;&gt;"aa'""",
compat.escape_html("""xx&<>"aa'"""),
)

View file

@ -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=[

View file

@ -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,7 +434,6 @@ 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)
@ -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

View file

@ -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'

View file

@ -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):
""" """

View file

@ -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

View file

@ -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,13 +103,11 @@ 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',

View file

@ -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)

View file

@ -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:

View file

@ -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})

View file

@ -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'
assert resp.text == expected_body
assert resp.status_code == 200
intro = '%s - - [' % host
log_tracker.assertLog(-1, intro)
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 test_normal_yield(log_tracker, server):
log_tracker.markLog()
host = webtest.interface(webtest.WebCase.HOST)
port = webtest.WebCase.PORT
resp = requests.get(
'http://%s:%s/as_yield' % (host, port),
headers={
'User-Agent': '',
},
)
expected_body = 'content'
assert resp.text == expected_body
assert resp.status_code == 200
intro = '%s - - [' % host
log_tracker.assertLog(-1, intro)
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_yield HTTP/1.1" 200 %s "" ""'
% content_length,
)
def test_custom_log_format(log_tracker, monkeypatch, server):
"""Test a customized access_log_format string, which is a """Test a customized access_log_format string, which is a
feature of _cplogging.LogManager.access().""" feature of _cplogging.LogManager.access()."""
self.markLog() monkeypatch.setattr(
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(
'cherrypy._cplogging.LogManager.access_log_format', 'cherrypy._cplogging.LogManager.access_log_format',
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}' '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}',
if six.PY3 else
'%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
) )
def testTimezLogFormat(self): 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',
)
def test_timez_log_format(log_tracker, monkeypatch, server):
"""Test a customized access_log_format string, which is a """Test a customized access_log_format string, which is a
feature of _cplogging.LogManager.access().""" feature of _cplogging.LogManager.access()."""
self.markLog() monkeypatch.setattr(
'cherrypy._cplogging.LogManager.access_log_format',
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}',
)
log_tracker.markLog()
expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime()) expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
with mock.patch( monkeypatch.setattr(
'cherrypy._cplogging.LazyRfc3339UtcTime', 'cherrypy._cplogging.LazyRfc3339UtcTime',
lambda: expected_time): lambda: expected_time,
self.getPage('/as_string', headers=[('Referer', 'REFERER'), )
('User-Agent', 'USERAGENT'), host = webtest.interface(webtest.WebCase.HOST)
('Host', 'HOST')]) port = webtest.WebCase.PORT
requests.get(
self.assertLog(-1, '%s - - ' % self.interface()) 'http://%s:%s/as_string' % (host, port),
self.assertLog(-1, expected_time) headers={
self.assertLog(-1, ' "GET /as_string HTTP/1.1" ' 'Referer': 'REFERER',
'200 7 "REFERER" "USERAGENT" HOST') 'User-Agent': 'USERAGENT',
'Host': 'HOST',
@mock.patch( },
'cherrypy._cplogging.LogManager.access_log_format',
'{i}' if six.PY3 else '%(i)s'
) )
def testUUIDv4ParameterLogFormat(self):
"""Test rendering of UUID4 within access log."""
self.markLog()
self.getPage('/as_string')
self.assertValidUUIDv4()
def testEscapedOutput(self): 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. # Test unicode in access log pieces.
self.markLog() log_tracker.markLog()
self.getPage('/uni_code') host = webtest.interface(webtest.WebCase.HOST)
self.assertStatus(200) port = webtest.WebCase.PORT
if six.PY3: resp = requests.get('http://%s:%s/uni_code' % (host, port))
# The repr of a bytestring in six.PY3 includes a b'' prefix assert resp.status_code == 200
self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) # The repr of a bytestring includes a b'' prefix
else: log_tracker.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
# Test the erebos value. Included inline for your enlightenment. # Test the erebos value. Included inline for your enlightenment.
# Note the 'r' prefix--those backslashes are literals. # Note the 'r' prefix--those backslashes are literals.
self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') log_tracker.assertLog(
-1,
r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82',
)
# Test backslashes in output. # Test backslashes in output.
self.markLog() log_tracker.markLog()
self.getPage('/slashes') resp = requests.get('http://%s:%s/slashes' % (host, port))
self.assertStatus(200) assert resp.status_code == 200
if six.PY3: log_tracker.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
else:
self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
# Test whitespace in output. # Test whitespace in output.
self.markLog() log_tracker.markLog()
self.getPage('/whitespace') resp = requests.get('http://%s:%s/whitespace' % (host, port))
self.assertStatus(200) assert resp.status_code == 200
# Again, note the 'r' prefix. # Again, note the 'r' prefix.
self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') log_tracker.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
class ErrorLogTests(helper.CPWebCase, logtest.LogCase): def test_tracebacks(server, caplog):
setup_server = staticmethod(setup_server) 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))
logfile = error_log rec = caplog.records[0]
exc_cls, exc_msg = rec.exc_info[0], rec.message
def testTracebacks(self): assert 'raise ValueError()' in resp.text
# Test that tracebacks get written to the error log. assert 'HTTP' in exc_msg
self.markLog() assert exc_cls is ValueError
ignore = helper.webtest.ignored_exceptions
ignore.append(ValueError)
try:
self.getPage('/error')
self.assertInBody('raise ValueError()')
self.assertLog(0, 'HTTP')
self.assertLog(1, 'Traceback (most recent call last):')
self.assertLog(-2, 'raise ValueError()')
finally:
ignore.pop()

View file

@ -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

View file

@ -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):

View file

@ -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,63 +401,95 @@ 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,
request=request,
)
return locals()
@pytest.fixture
def memcached_configured(
memcached_instance, monkeypatch,
memcached_client_present,
):
server = 'localhost:{port}'.format_map(memcached_instance)
monkeypatch.setattr(
sessions.MemcachedSession,
'servers',
[server],
)
@pytest.mark.skipif(
platform.system() == 'Windows',
reason='pytest-services helper does not work under Windows',
)
@pytest.mark.usefixtures('memcached_configured')
class MemcachedSessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server) setup_server = staticmethod(setup_server)
def test_0_Session(self): def test_0_Session(self):
self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession') self.getPage(
'/set_session_cls/cherrypy.lib.sessions.MemcachedSession'
)
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('/length', self.cookies) self.getPage('/length', self.cookies)
self.assertErrorPage(500) self.assertErrorPage(500)
self.assertInBody('NotImplementedError') assert b'NotImplementedError' in self.body
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(1.25) time.sleep(1.25)
self.getPage('/') self.getPage('/')
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'
# Test session delete # Test session delete
self.getPage('/delete', self.cookies) self.getPage('/delete', self.cookies)
self.assertBody('done') assert self.body == b'done'
def test_1_Concurrency(self): def test_1_Concurrency(self):
client_thread_count = 5 client_thread_count = 5
@ -462,7 +497,7 @@ else:
# 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 = {}
@ -490,13 +525,13 @@ else:
hitcount = max(data_dict.values()) hitcount = max(data_dict.values())
expected = 1 + (client_thread_count * request_count) expected = 1 + (client_thread_count * request_count)
self.assertEqual(hitcount, expected) 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('memcached') assert self.body == b'MemcachedSession'
def test_5_Error_paths(self): def test_5_Error_paths(self):
self.getPage('/unknown/page') self.getPage('/unknown/page')

View file

@ -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,9 +431,7 @@ test_case_name: "test_signal_handler_unsubscribe"
) )
class WaitTests(unittest.TestCase): def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name
def test_safe_wait_INADDR_ANY(self):
""" """
Wait on INADDR_ANY should not raise IOError Wait on INADDR_ANY should not raise IOError
@ -458,16 +454,18 @@ class WaitTests(unittest.TestCase):
inaddr_any = '0.0.0.0' inaddr_any = '0.0.0.0'
# Wait on the free port that's unbound # Wait on the free port that's unbound
with warnings.catch_warnings(record=True) as w: 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): with servers._safe_wait(inaddr_any, free_port):
portend.occupied(inaddr_any, free_port, timeout=1) portend.occupied(inaddr_any, free_port, timeout=1)
self.assertEqual(len(w), 1) assert len(warnings) == 1
self.assertTrue(isinstance(w[0], warnings.WarningMessage))
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 # The wait should still raise an IO error if INADDR_ANY was
# not supplied. # not supplied.
with pytest.raises(IOError): with pytest.raises(IOError):
# pylint: disable=protected-access
with servers._safe_wait('127.0.0.1', free_port): with servers._safe_wait('127.0.0.1', free_port):
portend.occupied('127.0.0.1', free_port, timeout=1) portend.occupied('127.0.0.1', free_port, timeout=1)

View file

@ -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,30 +429,26 @@ 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')

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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():