summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml20
-rw-r--r--CHANGES.txt75
-rw-r--r--CONTRIBUTING.txt2
-rw-r--r--README.txt9
-rw-r--r--cherrypy/__init__.py15
-rwxr-xr-xcherrypy/__main__.py4
-rw-r--r--cherrypy/_cpcompat.py30
-rw-r--r--cherrypy/_cpcompat_subprocess.py4
-rw-r--r--cherrypy/_cpconfig.py2
-rw-r--r--cherrypy/_cpdispatch.py5
-rw-r--r--cherrypy/_cplogging.py6
-rw-r--r--cherrypy/_cpreqbody.py2
-rw-r--r--cherrypy/_cptools.py4
-rw-r--r--cherrypy/_cpwsgi.py3
-rwxr-xr-xcherrypy/cherryd108
-rwxr-xr-xcherrypy/daemon.py106
-rw-r--r--cherrypy/lib/auth_digest.py3
-rw-r--r--cherrypy/lib/caching.py2
-rw-r--r--cherrypy/lib/cpstats.py18
-rw-r--r--cherrypy/lib/cptools.py10
-rw-r--r--cherrypy/lib/encoding.py2
-rw-r--r--cherrypy/lib/httpauth.py4
-rw-r--r--cherrypy/lib/httputil.py2
-rw-r--r--cherrypy/lib/profiler.py2
-rw-r--r--cherrypy/lib/reprconf.py42
-rw-r--r--cherrypy/lib/sessions.py2
-rw-r--r--cherrypy/lib/static.py5
-rw-r--r--cherrypy/process/plugins.py35
-rw-r--r--cherrypy/process/servers.py3
-rw-r--r--cherrypy/process/wspbus.py8
-rw-r--r--cherrypy/test/helper.py12
-rw-r--r--cherrypy/test/test_auth_basic.py4
-rw-r--r--cherrypy/test/test_bus.py3
-rw-r--r--cherrypy/test/test_config.py34
-rw-r--r--cherrypy/test/test_config_server.py2
-rw-r--r--cherrypy/test/test_conn.py57
-rw-r--r--cherrypy/test/test_core.py41
-rw-r--r--cherrypy/test/test_encoding.py2
-rw-r--r--cherrypy/test/test_http.py20
-rw-r--r--cherrypy/test/test_httpauth.py8
-rw-r--r--cherrypy/test/test_iterator.py2
-rw-r--r--cherrypy/test/test_logging.py5
-rw-r--r--cherrypy/test/test_mime.py2
-rw-r--r--cherrypy/test/test_objectmapping.py4
-rw-r--r--cherrypy/test/test_proxy.py2
-rw-r--r--cherrypy/test/test_request_obj.py26
-rw-r--r--cherrypy/test/test_states.py20
-rw-r--r--cherrypy/test/test_static.py12
-rw-r--r--cherrypy/test/test_wsgi_unix_socket.py106
-rw-r--r--cherrypy/test/test_xmlrpc.py2
-rw-r--r--cherrypy/test/webtest.py77
-rw-r--r--cherrypy/wsgiserver/ssl_pyopenssl.py4
-rw-r--r--cherrypy/wsgiserver/wsgiserver2.py124
-rw-r--r--cherrypy/wsgiserver/wsgiserver3.py67
-rw-r--r--docs/advanced.rst4
-rw-r--r--docs/basics.rst244
-rw-r--r--docs/conf.py2
-rw-r--r--docs/install.rst6
-rw-r--r--docs/tutorials.rst275
-rw-r--r--release.py62
-rw-r--r--setup.cfg26
-rw-r--r--setup.py13
-rw-r--r--tox.ini3
63 files changed, 1242 insertions, 562 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..18b91098
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,20 @@
+language: python
+python:
+- 2.6
+- 2.7
+- 3.2
+- 3.3
+- 3.4
+- 3.5
+script:
+- nosetests -s .
+deploy:
+ provider: pypi
+ on:
+ tags: true
+ all_branches: true
+ python: 3.5
+ user: jaraco
+ password:
+ secure: CQqUvtssQ4wmRluPcxh6m5lIXi83Qu9dAFAvZLS/+AQNIG78XECXv5xFTEdGSLX9yncKDpDKI3xRJeCKyO4OJUN0t6W1MRycY72+R63i9e9wPjfvtRqYH1TBT+no7jj/DHqXIrWSlpjRNAt4XXeSv7OzKWT4PmTNkNQSKyS1DWCmZGlbCKlV774Z9PbrfltflxL0V6DiX6ZfoY1THBO4rIopjMk/MPLgS5zvwLgXVbT9sK/DcPOgOq47iSLCs0oScbwiFzYW4DbcVZrBMv4ALtQTjk6ZEaBQ7KtKgsyxgi/ToVhjRxYg4rwvhjsyjixUdECLUqL3WgWfzW/lo82lhb79ERwhnjf1DvPNexlXhv9hHwHsFROpaOmM0nyDJsJg0rCNPVfO4SpBHEnd/ujlHO6yorHj0S54jZWqqDwD5gN19v3hEMT48Pc8uvazE9K1kMQbNXEzqn+SJjVB+DG7qK5Jm9Kk7ZC4R88hJAJNsR+SlFCXMGzkS9WUefUGLHQFfezZk43sMPIXMnh9d2XqCQo4QpUawdg3pwaTukFfyaHlK39CIHhZNas5D/UFL5spQPAAkH1IMcPILiSUwYYnXIJFWJIiulfEQalJroAQjrzvst/NVB8BbeYuCfmVLVOZw8Y6GOYONGgiXjT3nfmw/dN+uw+GY3EgAV5jl+fa434=
+ distributions: release
diff --git a/CHANGES.txt b/CHANGES.txt
index eea7f39b..a3c62069 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,76 @@
+5.2.1
-----
+
+* #1202: Add support for specifying a certificate authority when
+ serving SSL using the built-in SSL support.
+
+5.2.0
+-----
+
+* #1410: Moved hosting to Github (
+ `cherrypy/cherrypy <https://github.com/cherrypy/cherrypy>`_.
+
+5.1.0
+-----
+
+* Bugfix issue #1315 for ``test_HTTP11_pipelining`` test in Python 3.5
+* Bugfix issue #1382 regarding the keyword arguments support for Python 3
+ on the config file.
+* Bugfix issue #1406 for ``test_2_KeyboardInterrupt`` test in Python 3.5.
+ by monkey patching the HTTPRequest given a bug on CPython
+ that is affecting the testsuite (https://bugs.python.org/issue23377).
+* Add additional parameter ``raise_subcls`` to the tests helpers
+ `openURL` and ``CPWebCase.getPage`` to have finer control on
+ which exceptions can be raised.
+* Add support for direct keywords on the calls (e.g. ``foo=bar``) on
+ the config file under Python 3.
+* Add additional validation to determine if the process is running
+ as a daemon on ``cherrypy.process.plugins.SignalHandler`` to allow
+ the execution of the testsuite under CI tools.
+
+5.0.1
+-----
+
+* Bugfix for NameError following #94.
+
+5.0.0
+-----
+
+* Removed deprecated support for ``ssl_certificate`` and
+ ``ssl_private_key`` attributes and implicit construction
+ of SSL adapter on Python 2 WSGI servers.
+* Default SSL Adapter on Python 2 is the builtin SSL adapter,
+ matching Python 3 behavior.
+* Pull request #94: In proxy tool, defer to Host header for
+ resolving the base if no base is supplied.
+
+4.0.0
+-----
+
+* Drop support for Python 2.5 and earlier.
+* No longer build Windows installers by default.
+
+3.8.2
+-----
+
+* Pull Request #116: Correct InternalServerError when null bytes in
+ static file path. Now responds with 404 instead.
+
+3.8.0
+-----
+
+* Pull Request #96: Pass ``exc_info`` to logger as keyword rather than
+ formatting the error and injecting into the message.
+
+3.7.0
+-----
+
+* CherryPy daemon may now be invoked with ``python -m cherrypy`` in
+ addition to the ``cherryd`` script.
+* Issue #1298: Fix SSL handling on CPython 2.7 with builtin SSL module
+ and pyOpenSSL 0.14. This change will break PyPy for now.
+* Several documentation fixes.
+
3.6.0
-----
@@ -12,7 +84,6 @@
* Issue #1019: Allow logging host name in the access log.
* Pull Request #50: Fixed race condition in session cleanup.
------
3.5.0
-----
@@ -20,13 +91,11 @@
connections. This functionality was added to CherryPy 3.0, but
unintentionally lost in 3.1.
------
3.4.0
-----
* Miscellaneous quality improvements.
------
3.3.0
-----
diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt
index 6c653f75..c321e13a 100644
--- a/CONTRIBUTING.txt
+++ b/CONTRIBUTING.txt
@@ -3,4 +3,4 @@ CherryPy is a community-maintained, open-source project hosted at Bitbucket.
To contribute, first read `How to write the perfect pull request
<http://blog.jaraco.com/2014/04/how-to-write-perfect-pull-request.html>`_
and file your contribution with the `CherryPy Project page
-<https://bitbucket.org/cherrypy/cherrypy>`_.
+<https://github.com/cherrypy/cherrypy>`_.
diff --git a/README.txt b/README.txt
index d852128a..3c145146 100644
--- a/README.txt
+++ b/README.txt
@@ -1,10 +1,13 @@
-* To install, change to the directory where setup.py is located and type (python-2.3 or later needed):
+* To install, change to the directory where setup.py is located and type
+ (Python 2.6 or later needed):
python setup.py install
-* To learn how to use it, look at the examples under cherrypy/tutorial/ or go to http://www.cherrypy.org for more info.
+* To learn how to use it, look at the examples under cherrypy/tutorial/
+ or go to http://www.cherrypy.org for more info.
-* To run the regression tests, just go to the cherrypy/test/ directory and type:
+* To run the regression tests, just go to the cherrypy/test/ directory
+ and type:
nosetests -s ./
diff --git a/cherrypy/__init__.py b/cherrypy/__init__.py
index d90ffc7c..30a8cadf 100644
--- a/cherrypy/__init__.py
+++ b/cherrypy/__init__.py
@@ -56,10 +56,13 @@ with customized or extended components. The core API's are:
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
"""
-__version__ = "3.6.1"
+try:
+ import pkg_resources
+except ImportError:
+ pass
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
-from cherrypy._cpcompat import basestring, unicodestr, set
+from cherrypy._cpcompat import basestring, unicodestr
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
@@ -88,6 +91,12 @@ except ImportError:
engine = process.bus
+try:
+ __version__ = pkg_resources.require('cherrypy')[0].version
+except Exception:
+ __version__ = 'unknown'
+
+
# Timeout monitor. We add two channels to the engine
# to which cherrypy.Application will publish.
engine.listeners['before_request'] = set()
@@ -318,7 +327,7 @@ class _GlobalLogManager(_cplogging.LogManager):
"""Log the given message to the app.log or global log as appropriate.
"""
# Do NOT use try/except here. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/945
+ # https://github.com/cherrypy/cherrypy/issues/945
if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log
else:
diff --git a/cherrypy/__main__.py b/cherrypy/__main__.py
new file mode 100755
index 00000000..b1c9c012
--- /dev/null
+++ b/cherrypy/__main__.py
@@ -0,0 +1,4 @@
+import cherrypy.daemon
+
+if __name__ == '__main__':
+ cherrypy.daemon.run()
diff --git a/cherrypy/_cpcompat.py b/cherrypy/_cpcompat.py
index 8a98b38b..a73feb0b 100644
--- a/cherrypy/_cpcompat.py
+++ b/cherrypy/_cpcompat.py
@@ -111,11 +111,6 @@ def assert_native(n):
raise TypeError("n must be a native str (got %s)" % type(n).__name__)
try:
- set = set
-except NameError:
- from sets import Set as set
-
-try:
# Python 3.1+
from base64 import decodebytes as _base64_decodebytes
except ImportError:
@@ -137,17 +132,6 @@ def base64_decode(n, encoding='ISO-8859-1'):
else:
return b
-try:
- # Python 2.5+
- from hashlib import md5
-except ImportError:
- from md5 import new as md5
-
-try:
- # Python 2.5+
- from hashlib import sha1 as sha
-except ImportError:
- from sha import new as sha
try:
sorted = sorted
@@ -333,18 +317,10 @@ except ImportError:
# In Python 3, pickle is the sped-up C version.
import pickle
-try:
- os.urandom(20)
- import binascii
-
- def random20():
- return binascii.hexlify(os.urandom(20)).decode('ascii')
-except (AttributeError, NotImplementedError):
- import random
- # os.urandom not available until Python 2.4. Fall back to random.random.
+import binascii
- def random20():
- return sha('%s' % random.random()).hexdigest()
+def random20():
+ return binascii.hexlify(os.urandom(20)).decode('ascii')
try:
from _thread import get_ident as get_thread_ident
diff --git a/cherrypy/_cpcompat_subprocess.py b/cherrypy/_cpcompat_subprocess.py
index ce363725..517b8d44 100644
--- a/cherrypy/_cpcompat_subprocess.py
+++ b/cherrypy/_cpcompat_subprocess.py
@@ -883,7 +883,7 @@ class Popen(object):
startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = _subprocess.SW_HIDE
comspec = os.environ.get("COMSPEC", "cmd.exe")
- args = '{} /c "{}"'.format(comspec, args)
+ args = '{0} /c "{1}"'.format(comspec, args)
if (_subprocess.GetVersion() >= 0x80000000 or
os.path.basename(comspec).lower() == "command.com"):
# Win9x, or using command.com on NT. We need to
@@ -1029,7 +1029,7 @@ class Popen(object):
elif sig == signal.CTRL_BREAK_EVENT:
os.kill(self.pid, signal.CTRL_BREAK_EVENT)
else:
- raise ValueError("Unsupported signal: {}".format(sig))
+ raise ValueError("Unsupported signal: {0}".format(sig))
def terminate(self):
"""Terminates the process
diff --git a/cherrypy/_cpconfig.py b/cherrypy/_cpconfig.py
index c11bc1d1..00207723 100644
--- a/cherrypy/_cpconfig.py
+++ b/cherrypy/_cpconfig.py
@@ -119,7 +119,7 @@ style) context manager.
"""
import cherrypy
-from cherrypy._cpcompat import set, basestring
+from cherrypy._cpcompat import basestring
from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3
diff --git a/cherrypy/_cpdispatch.py b/cherrypy/_cpdispatch.py
index 1c2d7df8..2cb03c7e 100644
--- a/cherrypy/_cpdispatch.py
+++ b/cherrypy/_cpdispatch.py
@@ -18,7 +18,6 @@ except AttributeError:
classtype = type
import cherrypy
-from cherrypy._cpcompat import set
class PageHandler(object):
@@ -423,7 +422,7 @@ class Dispatcher(object):
object_trail.insert(
i + 1, ["default", defhandler, conf, segleft])
request.config = set_conf()
- # See https://bitbucket.org/cherrypy/cherrypy/issue/613
+ # See https://github.com/cherrypy/cherrypy/issues/613
request.is_index = path.endswith("/")
return defhandler, fullpath[fullpath_len - segleft:-1]
@@ -676,7 +675,7 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
result = next_dispatcher(path_info)
# Touch up staticdir config. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/614.
+ # https://github.com/cherrypy/cherrypy/issues/614.
section = request.config.get('tools.staticdir.section')
if section:
section = section[len(prefix):]
diff --git a/cherrypy/_cplogging.py b/cherrypy/_cplogging.py
index 554fd7ef..19d1d91e 100644
--- a/cherrypy/_cplogging.py
+++ b/cherrypy/_cplogging.py
@@ -209,9 +209,11 @@ class LogManager(object):
If ``traceback`` is True, the traceback of the current exception
(if any) will be appended to ``msg``.
"""
+ exc_info = None
if traceback:
- msg += _cperror.format_exc()
- self.error_log.log(severity, ' '.join((self.time(), context, msg)))
+ exc_info = _cperror._exc_info()
+
+ self.error_log.log(severity, ' '.join((self.time(), context, msg)), exc_info=exc_info)
def __call__(self, *args, **kwargs):
"""An alias for ``error``."""
diff --git a/cherrypy/_cpreqbody.py b/cherrypy/_cpreqbody.py
index d2dbbc92..13a8adb7 100644
--- a/cherrypy/_cpreqbody.py
+++ b/cherrypy/_cpreqbody.py
@@ -940,7 +940,7 @@ class RequestBody(Entity):
# Don't parse the request body at all if the client didn't provide
# a Content-Type header. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/790
+ # https://github.com/cherrypy/cherrypy/issues/790
default_content_type = ''
"""This defines a default ``Content-Type`` to use if no Content-Type header
is given. The empty string is used for RequestBody, which results in the
diff --git a/cherrypy/_cptools.py b/cherrypy/_cptools.py
index 06a56e87..af76ae06 100644
--- a/cherrypy/_cptools.py
+++ b/cherrypy/_cptools.py
@@ -271,7 +271,7 @@ class SessionTool(Tool):
body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX
progress meter
- (`issue <https://bitbucket.org/cherrypy/cherrypy/issue/630>`_).
+ (`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
When 'explicit' (or any other value), you need to call
cherrypy.session.acquire_lock() yourself before using
@@ -376,7 +376,7 @@ class XMLRPCController(object):
body = subhandler(*(vpath + rpcparams), **params)
else:
- # https://bitbucket.org/cherrypy/cherrypy/issue/533
+ # https://github.com/cherrypy/cherrypy/issues/533
# if a method is not found, an xmlrpclib.Fault should be returned
# raising an exception here will do that; see
# cherrypy.lib.xmlrpcutil.on_error
diff --git a/cherrypy/_cpwsgi.py b/cherrypy/_cpwsgi.py
index f6db68b0..a8068fb0 100644
--- a/cherrypy/_cpwsgi.py
+++ b/cherrypy/_cpwsgi.py
@@ -296,7 +296,8 @@ class AppResponse(object):
"""Create a Request object using environ."""
env = self.environ.get
- local = httputil.Host('', int(env('SERVER_PORT', 80)),
+ local = httputil.Host('',
+ int(env('SERVER_PORT', 80) or -1),
env('SERVER_NAME', ''))
remote = httputil.Host(env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
diff --git a/cherrypy/cherryd b/cherrypy/cherryd
index 5afb27ad..5d271c39 100755
--- a/cherrypy/cherryd
+++ b/cherrypy/cherryd
@@ -1,110 +1,6 @@
#! /usr/bin/env python
-"""The CherryPy daemon."""
-
-import sys
-
-import cherrypy
-from cherrypy.process import plugins, servers
-from cherrypy import Application
-
-
-def start(configfiles=None, daemonize=False, environment=None,
- fastcgi=False, scgi=False, pidfile=None, imports=None,
- cgi=False):
- """Subscribe all engine plugins and start the engine."""
- sys.path = [''] + sys.path
- for i in imports or []:
- exec("import %s" % i)
-
- for c in configfiles or []:
- cherrypy.config.update(c)
- # If there's only one app mounted, merge config into it.
- if len(cherrypy.tree.apps) == 1:
- for app in cherrypy.tree.apps.values():
- if isinstance(app, Application):
- app.merge(c)
-
- engine = cherrypy.engine
-
- if environment is not None:
- cherrypy.config.update({'environment': environment})
-
- # Only daemonize if asked to.
- if daemonize:
- # Don't print anything to stdout/sterr.
- cherrypy.config.update({'log.screen': False})
- plugins.Daemonizer(engine).subscribe()
-
- if pidfile:
- plugins.PIDFile(engine, pidfile).subscribe()
-
- if hasattr(engine, "signal_handler"):
- engine.signal_handler.subscribe()
- if hasattr(engine, "console_control_handler"):
- engine.console_control_handler.subscribe()
-
- if (fastcgi and (scgi or cgi)) or (scgi and cgi):
- cherrypy.log.error("You may only specify one of the cgi, fastcgi, and "
- "scgi options.", 'ENGINE')
- sys.exit(1)
- elif fastcgi or scgi or cgi:
- # Turn off autoreload when using *cgi.
- cherrypy.config.update({'engine.autoreload_on': False})
- # Turn off the default HTTP server (which is subscribed by default).
- cherrypy.server.unsubscribe()
-
- addr = cherrypy.server.bind_addr
- if fastcgi:
- f = servers.FlupFCGIServer(application=cherrypy.tree,
- bindAddress=addr)
- elif scgi:
- f = servers.FlupSCGIServer(application=cherrypy.tree,
- bindAddress=addr)
- else:
- f = servers.FlupCGIServer(application=cherrypy.tree,
- bindAddress=addr)
- s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
- s.subscribe()
-
- # Always start the engine; this will start all other services
- try:
- engine.start()
- except:
- # Assume the error has been logged already via bus.log.
- sys.exit(1)
- else:
- engine.block()
+import cherrypy.daemon
if __name__ == '__main__':
- from optparse import OptionParser
-
- p = OptionParser()
- p.add_option('-c', '--config', action="append", dest='config',
- help="specify config file(s)")
- p.add_option('-d', action="store_true", dest='daemonize',
- help="run the server as a daemon")
- p.add_option('-e', '--environment', dest='environment', default=None,
- help="apply the given config environment")
- p.add_option('-f', action="store_true", dest='fastcgi',
- help="start a fastcgi server instead of the default HTTP "
- "server")
- p.add_option('-s', action="store_true", dest='scgi',
- help="start a scgi server instead of the default HTTP server")
- p.add_option('-x', action="store_true", dest='cgi',
- help="start a cgi server instead of the default HTTP server")
- p.add_option('-i', '--import', action="append", dest='imports',
- help="specify modules to import")
- p.add_option('-p', '--pidfile', dest='pidfile', default=None,
- help="store the process id in the given file")
- p.add_option('-P', '--Path', action="append", dest='Path',
- help="add the given paths to sys.path")
- options, args = p.parse_args()
-
- if options.Path:
- for p in options.Path:
- sys.path.insert(0, p)
-
- start(options.config, options.daemonize,
- options.environment, options.fastcgi, options.scgi,
- options.pidfile, options.imports, options.cgi)
+ cherrypy.daemon.run()
diff --git a/cherrypy/daemon.py b/cherrypy/daemon.py
new file mode 100755
index 00000000..395a2e68
--- /dev/null
+++ b/cherrypy/daemon.py
@@ -0,0 +1,106 @@
+"""The CherryPy daemon."""
+
+import sys
+
+import cherrypy
+from cherrypy.process import plugins, servers
+from cherrypy import Application
+
+
+def start(configfiles=None, daemonize=False, environment=None,
+ fastcgi=False, scgi=False, pidfile=None, imports=None,
+ cgi=False):
+ """Subscribe all engine plugins and start the engine."""
+ sys.path = [''] + sys.path
+ for i in imports or []:
+ exec("import %s" % i)
+
+ for c in configfiles or []:
+ cherrypy.config.update(c)
+ # If there's only one app mounted, merge config into it.
+ if len(cherrypy.tree.apps) == 1:
+ for app in cherrypy.tree.apps.values():
+ if isinstance(app, Application):
+ app.merge(c)
+
+ engine = cherrypy.engine
+
+ if environment is not None:
+ cherrypy.config.update({'environment': environment})
+
+ # Only daemonize if asked to.
+ if daemonize:
+ # Don't print anything to stdout/sterr.
+ cherrypy.config.update({'log.screen': False})
+ plugins.Daemonizer(engine).subscribe()
+
+ if pidfile:
+ plugins.PIDFile(engine, pidfile).subscribe()
+
+ if hasattr(engine, "signal_handler"):
+ engine.signal_handler.subscribe()
+ if hasattr(engine, "console_control_handler"):
+ engine.console_control_handler.subscribe()
+
+ if (fastcgi and (scgi or cgi)) or (scgi and cgi):
+ cherrypy.log.error("You may only specify one of the cgi, fastcgi, and "
+ "scgi options.", 'ENGINE')
+ sys.exit(1)
+ elif fastcgi or scgi or cgi:
+ # Turn off autoreload when using *cgi.
+ cherrypy.config.update({'engine.autoreload_on': False})
+ # Turn off the default HTTP server (which is subscribed by default).
+ cherrypy.server.unsubscribe()
+
+ addr = cherrypy.server.bind_addr
+ cls = (
+ servers.FlupFCGIServer if fastcgi else
+ servers.FlupSCGIServer if scgi else
+ servers.FlupCGIServer
+ )
+ f = cls(application=cherrypy.tree, bindAddress=addr)
+ s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
+ s.subscribe()
+
+ # Always start the engine; this will start all other services
+ try:
+ engine.start()
+ except:
+ # Assume the error has been logged already via bus.log.
+ sys.exit(1)
+ else:
+ engine.block()
+
+
+def run():
+ from optparse import OptionParser
+
+ p = OptionParser()
+ p.add_option('-c', '--config', action="append", dest='config',
+ help="specify config file(s)")
+ p.add_option('-d', action="store_true", dest='daemonize',
+ help="run the server as a daemon")
+ p.add_option('-e', '--environment', dest='environment', default=None,
+ help="apply the given config environment")
+ p.add_option('-f', action="store_true", dest='fastcgi',
+ help="start a fastcgi server instead of the default HTTP "
+ "server")
+ p.add_option('-s', action="store_true", dest='scgi',
+ help="start a scgi server instead of the default HTTP server")
+ p.add_option('-x', action="store_true", dest='cgi',
+ help="start a cgi server instead of the default HTTP server")
+ p.add_option('-i', '--import', action="append", dest='imports',
+ help="specify modules to import")
+ p.add_option('-p', '--pidfile', dest='pidfile', default=None,
+ help="store the process id in the given file")
+ p.add_option('-P', '--Path', action="append", dest='Path',
+ help="add the given paths to sys.path")
+ options, args = p.parse_args()
+
+ if options.Path:
+ for p in options.Path:
+ sys.path.insert(0, p)
+
+ start(options.config, options.daemonize,
+ options.environment, options.fastcgi, options.scgi,
+ options.pidfile, options.imports, options.cgi)
diff --git a/cherrypy/lib/auth_digest.py b/cherrypy/lib/auth_digest.py
index e06535dc..e833ff77 100644
--- a/cherrypy/lib/auth_digest.py
+++ b/cherrypy/lib/auth_digest.py
@@ -23,10 +23,11 @@ __date__ = 'April 2009'
import time
+from hashlib import md5
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
import cherrypy
-from cherrypy._cpcompat import md5, ntob
+from cherrypy._cpcompat import ntob
md5_hex = lambda s: md5(ntob(s)).hexdigest()
qop_auth = 'auth'
diff --git a/cherrypy/lib/caching.py b/cherrypy/lib/caching.py
index fab6b569..375d5f0e 100644
--- a/cherrypy/lib/caching.py
+++ b/cherrypy/lib/caching.py
@@ -353,7 +353,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
return False
# Copy the response headers. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/721.
+ # https://github.com/cherrypy/cherrypy/issues/721.
response.headers = rh = httputil.HeaderMap()
for k in h:
dict.__setitem__(rh, k, dict.__getitem__(h, k))
diff --git a/cherrypy/lib/cpstats.py b/cherrypy/lib/cpstats.py
index a8661a14..4aeabd7d 100644
--- a/cherrypy/lib/cpstats.py
+++ b/cherrypy/lib/cpstats.py
@@ -210,6 +210,7 @@ def extrapolate_statistics(scope):
# -------------------- CherryPy Applications Statistics --------------------- #
+import sys
import threading
import time
@@ -294,6 +295,11 @@ class ByteCountWrapper(object):
average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
+def _get_threading_ident():
+ if sys.version_info >= (3, 3):
+ return threading.get_ident()
+ return threading._get_ident()
+
class StatsTool(cherrypy.Tool):
"""Record various information about the current request."""
@@ -322,7 +328,7 @@ class StatsTool(cherrypy.Tool):
appstats['Current Requests'] += 1
appstats['Total Requests'] += 1
- appstats['Requests'][threading._get_ident()] = {
+ appstats['Requests'][_get_threading_ident()] = {
'Bytes Read': None,
'Bytes Written': None,
# Use a lambda so the ip gets updated by tools.proxy later
@@ -339,7 +345,7 @@ class StatsTool(cherrypy.Tool):
debug=False, **kwargs):
"""Record the end of a request."""
resp = cherrypy.serving.response
- w = appstats['Requests'][threading._get_ident()]
+ w = appstats['Requests'][_get_threading_ident()]
r = cherrypy.request.rfile.bytes_read
w['Bytes Read'] = r
@@ -605,7 +611,13 @@ table.stats2 th {
"""Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict.
headers = []
- for record in v.itervalues():
+ try:
+ # python2
+ vals = v.itervalues()
+ except AttributeError:
+ # python3
+ vals = v.values()
+ for record in vals:
for k3 in record:
format = formatting.get(k3, missing)
if format is None:
diff --git a/cherrypy/lib/cptools.py b/cherrypy/lib/cptools.py
index f376282c..9be571ba 100644
--- a/cherrypy/lib/cptools.py
+++ b/cherrypy/lib/cptools.py
@@ -2,9 +2,10 @@
import logging
import re
+from hashlib import md5
import cherrypy
-from cherrypy._cpcompat import basestring, md5, set, unicodestr
+from cherrypy._cpcompat import basestring, unicodestr
from cherrypy.lib import httputil as _httputil
from cherrypy.lib import is_iterator
@@ -192,11 +193,10 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
if lbase is not None:
base = lbase.split(',')[0]
if not base:
+ base = request.headers.get('Host', '127.0.0.1')
port = request.local.port
- if port == 80:
- base = '127.0.0.1'
- else:
- base = '127.0.0.1:%s' % port
+ if port != 80:
+ base += ':%s' % port
if base.find("://") == -1:
# add http:// or https:// if needed
diff --git a/cherrypy/lib/encoding.py b/cherrypy/lib/encoding.py
index a4c2cbd6..fb688f8d 100644
--- a/cherrypy/lib/encoding.py
+++ b/cherrypy/lib/encoding.py
@@ -2,7 +2,7 @@ import struct
import time
import cherrypy
-from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr
+from cherrypy._cpcompat import basestring, BytesIO, ntob, unicodestr
from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header
diff --git a/cherrypy/lib/httpauth.py b/cherrypy/lib/httpauth.py
index 0897ea2a..6d519907 100644
--- a/cherrypy/lib/httpauth.py
+++ b/cherrypy/lib/httpauth.py
@@ -62,7 +62,9 @@ __all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
##########################################################################
import time
-from cherrypy._cpcompat import base64_decode, ntob, md5
+from hashlib import md5
+
+from cherrypy._cpcompat import base64_decode, ntob
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
MD5 = "MD5"
diff --git a/cherrypy/lib/httputil.py b/cherrypy/lib/httputil.py
index 69a18d45..f249e6dd 100644
--- a/cherrypy/lib/httputil.py
+++ b/cherrypy/lib/httputil.py
@@ -13,7 +13,7 @@ from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr
from cherrypy._cpcompat import reversed, sorted, unicodestr, unquote_qs
response_codes = BaseHTTPRequestHandler.responses.copy()
-# From https://bitbucket.org/cherrypy/cherrypy/issue/361
+# From https://github.com/cherrypy/cherrypy/issues/361
response_codes[500] = ('Internal Server Error',
'The server encountered an unexpected condition '
'which prevented it from fulfilling the request.')
diff --git a/cherrypy/lib/profiler.py b/cherrypy/lib/profiler.py
index 5dac386e..a3477454 100644
--- a/cherrypy/lib/profiler.py
+++ b/cherrypy/lib/profiler.py
@@ -8,7 +8,7 @@ You can profile any of your pages as follows::
from cherrypy.lib import profiler
class Root:
- p = profile.Profiler("/path/to/profile/dir")
+ p = profiler.Profiler("/path/to/profile/dir")
def index(self):
self.p.run(self._index)
diff --git a/cherrypy/lib/reprconf.py b/cherrypy/lib/reprconf.py
index 6e70b5ec..8af1f777 100644
--- a/cherrypy/lib/reprconf.py
+++ b/cherrypy/lib/reprconf.py
@@ -281,13 +281,14 @@ class _Builder2:
# 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
+ return kw_dict
def build_List(self, o):
return map(self.build, o.getChildren())
@@ -377,7 +378,39 @@ class _Builder3:
def build_Index(self, o):
return self.build(o.value)
+ def _build_call35(self, o):
+ """
+ Workaround for python 3.5 _ast.Call signature, docs found here
+ https://greentreesnakes.readthedocs.org/en/latest/nodes.html
+ """
+ import ast
+ callee = self.build(o.func)
+ args = []
+ if o.args is not None:
+ for a in o.args:
+ if isinstance(a, ast.Starred):
+ args.append(self.build(a.value))
+ else:
+ args.append(self.build(a))
+ kwargs = {}
+ for kw in o.keywords:
+ if kw.arg is None: # double asterix `**`
+ rst = self.build(kw.value)
+ if not isinstance(rst, dict):
+ raise TypeError("Invalid argument for call."
+ "Must be a mapping object.")
+ # give preference to the keys set directly from arg=value
+ for k, v in rst.items():
+ if k not in kwargs:
+ kwargs[k] = v
+ else: # defined on the call as: arg=value
+ kwargs[kw.arg] = self.build(kw.value)
+ return callee(*args, **kwargs)
+
def build_Call(self, o):
+ if sys.version_info >= (3, 5):
+ return self._build_call35(o)
+
callee = self.build(o.func)
if o.args is None:
@@ -388,13 +421,16 @@ class _Builder3:
if o.starargs is None:
starargs = ()
else:
- starargs = self.build(o.starargs)
+ starargs = tuple(self.build(o.starargs))
if o.kwargs is None:
kwargs = {}
else:
kwargs = self.build(o.kwargs)
-
+ if o.keywords is not None: # direct a=b keywords
+ for kw in o.keywords:
+ # preference because is a direct keyword against **kwargs
+ kwargs[kw.arg] = self.build(kw.value)
return callee(*(args + starargs), **kwargs)
def build_List(self, o):
diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py
index 37556363..e9605cb8 100644
--- a/cherrypy/lib/sessions.py
+++ b/cherrypy/lib/sessions.py
@@ -182,7 +182,7 @@ class Session(object):
cherrypy.log('Expired or malicious session %r; '
'making a new one' % id, 'TOOLS.SESSIONS')
# Expired or malicious session. Make a new one.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/709.
+ # See https://github.com/cherrypy/cherrypy/issues/709.
self.id = None
self.missing = True
self._regenerate()
diff --git a/cherrypy/lib/static.py b/cherrypy/lib/static.py
index a630dae6..6a78fc13 100644
--- a/cherrypy/lib/static.py
+++ b/cherrypy/lib/static.py
@@ -49,7 +49,10 @@ def serve_file(path, content_type=None, disposition=None, name=None,
try:
st = os.stat(path)
- except OSError:
+ except (OSError, TypeError, ValueError):
+ # OSError when file fails to stat
+ # TypeError on Python 2 when there's a null byte
+ # ValueError on Python 3 when there's a null byte
if debug:
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound()
diff --git a/cherrypy/process/plugins.py b/cherrypy/process/plugins.py
index c787ba92..0ec585c0 100644
--- a/cherrypy/process/plugins.py
+++ b/cherrypy/process/plugins.py
@@ -8,7 +8,7 @@ import time
import threading
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident
-from cherrypy._cpcompat import ntob, set, Timer, SetDaemonProperty
+from cherrypy._cpcompat import ntob, Timer, SetDaemonProperty
# _module__file__base is used by Autoreload to make
# absolute any filenames retrieved from sys.modules which are not
@@ -109,12 +109,35 @@ class SignalHandler(object):
self.handlers['SIGINT'] = self._jython_SIGINT_handler
self._previous_handlers = {}
+ # used to determine is the process is a daemon in `self._is_daemonized`
+ self._original_pid = os.getpid()
+
def _jython_SIGINT_handler(self, signum=None, frame=None):
# See http://bugs.jython.org/issue1313
self.bus.log('Keyboard Interrupt: shutting down bus')
self.bus.exit()
+ def _is_daemonized(self):
+ """Return boolean indicating if the current process is
+ running as a daemon.
+
+ The criteria to determine the `daemon` condition is to verify
+ if the current pid is not the same as the one that got used on
+ the initial construction of the plugin *and* the stdin is not
+ connected to a terminal.
+
+ The sole validation of the tty is not enough when the plugin
+ is executing inside other process like in a CI tool
+ (Buildbot, Jenkins).
+ """
+ if (self._original_pid != os.getpid() and
+ not os.isatty(sys.stdin.fileno())):
+ return True
+ else:
+ return False
+
+
def subscribe(self):
"""Subscribe self.handlers to signals."""
for sig, func in self.handlers.items():
@@ -180,13 +203,13 @@ class SignalHandler(object):
def handle_SIGHUP(self):
"""Restart if daemonized, else exit."""
- if os.isatty(sys.stdin.fileno()):
+ if self._is_daemonized():
+ self.bus.log("SIGHUP caught while daemonized. Restarting.")
+ self.bus.restart()
+ else:
# not daemonized (may be foreground or background)
self.bus.log("SIGHUP caught but not daemonized. Exiting.")
self.bus.exit()
- else:
- self.bus.log("SIGHUP caught while daemonized. Restarting.")
- self.bus.restart()
try:
@@ -200,7 +223,7 @@ class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows.
- Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
+ Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
"""
def __init__(self, bus, umask=None, uid=None, gid=None):
diff --git a/cherrypy/process/servers.py b/cherrypy/process/servers.py
index 6f8088bd..91ebf604 100644
--- a/cherrypy/process/servers.py
+++ b/cherrypy/process/servers.py
@@ -183,8 +183,7 @@ class ServerAdapter(object):
if not self.httpserver:
return ''
host, port = self.bind_addr
- if getattr(self.httpserver, 'ssl_certificate', None) or \
- getattr(self.httpserver, 'ssl_adapter', None):
+ if getattr(self.httpserver, 'ssl_adapter', None):
scheme = "https"
if port != 443:
host += ":%s" % port
diff --git a/cherrypy/process/wspbus.py b/cherrypy/process/wspbus.py
index 5409d038..9c9a2739 100644
--- a/cherrypy/process/wspbus.py
+++ b/cherrypy/process/wspbus.py
@@ -68,8 +68,6 @@ import time
import traceback as _traceback
import warnings
-from cherrypy._cpcompat import set
-
# Here I save the value of os.getcwd(), which, if I am imported early enough,
# will be the directory from which the startup script was run. This is needed
# by _do_execv(), to change back to the original directory before execv()ing a
@@ -87,7 +85,7 @@ class ChannelFailures(Exception):
def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4
- # See https://bitbucket.org/cherrypy/cherrypy/issue/959
+ # See https://github.com/cherrypy/cherrypy/issues/959
Exception.__init__(self, *args, **kwargs)
self._exceptions = list()
@@ -319,10 +317,10 @@ class Bus(object):
raise
# Waiting for ALL child threads to finish is necessary on OS X.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/581.
+ # See https://github.com/cherrypy/cherrypy/issues/581.
# It's also good to let them all shut down before allowing
# the main thread to call atexit handlers.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/751.
+ # See https://github.com/cherrypy/cherrypy/issues/751.
self.log("Waiting for child threads to terminate...")
for t in threading.enumerate():
# Validate the we're not trying to join the MainThread
diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py
index fd8bb914..adc225df 100644
--- a/cherrypy/test/helper.py
+++ b/cherrypy/test/helper.py
@@ -341,12 +341,18 @@ class CPWebCase(webtest.WebCase):
sys.exit()
def getPage(self, url, headers=None, method="GET", body=None,
- protocol=None):
- """Open the url. Return status, headers, body."""
+ protocol=None, raise_subcls=None):
+ """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:
url = httputil.urljoin(self.script_name, url)
return webtest.WebCase.getPage(self, url, headers, method, body,
- protocol)
+ protocol, raise_subcls)
def skip(self, msg='skipped '):
raise nose.SkipTest(msg)
diff --git a/cherrypy/test/test_auth_basic.py b/cherrypy/test/test_auth_basic.py
index 7ba11dfc..d03b1321 100644
--- a/cherrypy/test/test_auth_basic.py
+++ b/cherrypy/test/test_auth_basic.py
@@ -2,8 +2,10 @@
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+from hashlib import md5
+
import cherrypy
-from cherrypy._cpcompat import md5, ntob
+from cherrypy._cpcompat import ntob
from cherrypy.lib import auth_basic
from cherrypy.test import helper
diff --git a/cherrypy/test/test_bus.py b/cherrypy/test/test_bus.py
index 06ad6acb..6c927ffb 100644
--- a/cherrypy/test/test_bus.py
+++ b/cherrypy/test/test_bus.py
@@ -2,8 +2,7 @@ import threading
import time
import unittest
-import cherrypy
-from cherrypy._cpcompat import get_daemon, set
+from cherrypy._cpcompat import get_daemon
from cherrypy.process import wspbus
diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py
index f9831b12..0508f92c 100644
--- a/cherrypy/test/test_config.py
+++ b/cherrypy/test/test_config.py
@@ -269,3 +269,37 @@ class VariableSubstitutionTests(unittest.TestCase):
self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir")
self.assertEqual(cherrypy.config["my"]
["my.dir2"], "/some/dir/my/dir/dir2")
+
+
+class CallablesInConfigTest(unittest.TestCase):
+ setup_server = staticmethod(setup_server)
+
+
+ def test_call_with_literal_dict(self):
+ from textwrap import dedent
+ conf = dedent("""
+ [my]
+ value = dict(**{'foo': 'bar'})
+ """)
+ fp = compat.StringIO(conf)
+ cherrypy.config.update(fp)
+ self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'})
+
+ def test_call_with_kwargs(self):
+ from textwrap import dedent
+ conf = dedent("""
+ [my]
+ value = dict(foo="buzz", **cherrypy._test_dict)
+ """)
+ test_dict = {
+ "foo": "bar",
+ "bar": "foo",
+ "fizz": "buzz"
+ }
+ cherrypy._test_dict = test_dict
+ fp = compat.StringIO(conf)
+ cherrypy.config.update(fp)
+ test_dict['foo'] = 'buzz'
+ self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz')
+ self.assertEqual(cherrypy.config['my']['value'], test_dict)
+ del cherrypy._test_dict
diff --git a/cherrypy/test/test_config_server.py b/cherrypy/test/test_config_server.py
index 40504d8f..f4f574de 100644
--- a/cherrypy/test/test_config_server.py
+++ b/cherrypy/test/test_config_server.py
@@ -93,7 +93,7 @@ class ServerConfigTests(helper.CPWebCase):
self.getPage("/", headers=[('From', "x" * 500)])
self.assertStatus(413)
- # Test for https://bitbucket.org/cherrypy/cherrypy/issue/421
+ # Test for https://github.com/cherrypy/cherrypy/issues/421
# (Incorrect border condition in readline of SizeCheckWrapper).
# This hangs in rev 891 and earlier.
lines256 = "x" * 248
diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py
index ab830f3c..3e0cbc16 100644
--- a/cherrypy/test/test_conn.py
+++ b/cherrypy/test/test_conn.py
@@ -3,16 +3,23 @@
import socket
import sys
import time
-timeout = 1
+import errno
import cherrypy
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected
-from cherrypy._cpcompat import BadStatusLine, ntob, tonative, urlopen, unicodestr
+from cherrypy._cpcompat import (
+ BadStatusLine,
+ ntob,
+ tonative,
+ urlopen,
+ unicodestr,
+ py3k
+)
from cherrypy.test import webtest
-from cherrypy import _cperror
+timeout = 1
pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
@@ -198,7 +205,7 @@ class ConnectionCloseTests(helper.CPWebCase):
self.assertRaises(NotConnected, self.getPage, "/")
# Try HEAD. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/864.
+ # https://github.com/cherrypy/cherrypy/issues/864.
self.getPage("/stream", method='HEAD')
self.assertStatus('200 OK')
self.assertBody('')
@@ -434,6 +441,12 @@ class PipelineTests(helper.CPWebCase):
# Retrieve previous response
response = conn.response_class(conn.sock, method="GET")
+ # there is a bug in python3 regarding the buffering of
+ # ``conn.sock``. Until that bug get's fixed we will
+ # monkey patch the ``reponse`` instance.
+ # https://bugs.python.org/issue23377
+ if py3k:
+ response.fp = conn.sock.makefile("rb", 0)
response.begin()
body = response.read(13)
self.assertEqual(response.status, 200)
@@ -751,24 +764,31 @@ def setup_upload_server():
'server.accepted_queue_timeout': 0.1,
})
-import errno
-socket_reset_errors = []
-# Not all of these names will be defined for every platform.
-for _ in ("ECONNRESET", "WSAECONNRESET"):
- if _ in dir(errno):
- socket_reset_errors.append(getattr(errno, _))
+reset_names = 'ECONNRESET', 'WSAECONNRESET'
+socket_reset_errors = [
+ getattr(errno, name)
+ for name in reset_names
+ if hasattr(errno, name)
+]
+"reset error numbers available on this platform"
+
+socket_reset_errors += [
+ # Python 3.5 raises an http.client.RemoteDisconnected
+ # with this message
+ "Remote end closed connection without response",
+]
+
class LimitedRequestQueueTests(helper.CPWebCase):
- setup_server = staticmethod(setup_upload_server)
+ setup_server = staticmethod(setup_upload_server)
def test_queue_full(self):
conns = []
overflow_conn = None
-
+
try:
# Make 15 initial requests and leave them open, which should use
# all of wsgiserver's WorkerThreads and fill its Queue.
- import time
for i in range(15):
conn = self.HTTP_CONN(self.HOST, self.PORT)
conn.putrequest("POST", "/upload", skip_host=True)
@@ -777,7 +797,7 @@ class LimitedRequestQueueTests(helper.CPWebCase):
conn.putheader("Content-Length", "4")
conn.endheaders()
conns.append(conn)
-
+
# Now try a 16th conn, which should be closed by the server immediately.
overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
# Manually connect since httplib won't let us set a timeout
@@ -788,7 +808,7 @@ class LimitedRequestQueueTests(helper.CPWebCase):
overflow_conn.sock.settimeout(5)
overflow_conn.sock.connect(sa)
break
-
+
overflow_conn.putrequest("GET", "/", skip_host=True)
overflow_conn.putheader("Host", self.HOST)
overflow_conn.endheaders()
@@ -799,8 +819,11 @@ class LimitedRequestQueueTests(helper.CPWebCase):
if exc.args[0] in socket_reset_errors:
pass # Expected.
else:
- raise AssertionError("Overflow conn did not get RST. "
- "Got %s instead" % repr(exc.args))
+ tmpl = (
+ "Overflow conn did not get RST. "
+ "Got {exc.args!r} instead"
+ )
+ raise AssertionError(tmpl.format(**locals()))
except BadStatusLine:
# This is a special case in OS X. Linux and Windows will
# RST correctly.
diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py
index ae4728de..16a0219d 100644
--- a/cherrypy/test/test_core.py
+++ b/cherrypy/test/test_core.py
@@ -9,6 +9,7 @@ import cherrypy
from cherrypy._cpcompat import IncompleteRead, itervalues, ntob
from cherrypy import _cptools, tools
from cherrypy.lib import httputil, static
+from cherrypy.test._test_decorators import ExposeExamples
favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico")
@@ -41,10 +42,7 @@ class CoreRequestHandlingTest(helper.CPWebCase):
baseurl.exposed = True
root = Root()
-
- if sys.version_info >= (2, 5):
- from cherrypy.test._test_decorators import ExposeExamples
- root.expose_dec = ExposeExamples()
+ root.expose_dec = ExposeExamples()
class TestType(type):
@@ -250,11 +248,7 @@ class CoreRequestHandlingTest(helper.CPWebCase):
cherrypy.response.cookie[str(name)] = cookie.value
def multiple(self, names):
- for name in names:
- cookie = cherrypy.request.cookie[name]
- # Python2's SimpleCookie.__setitem__ won't take unicode
- # keys.
- cherrypy.response.cookie[str(name)] = cookie.value
+ list(map(self.single, names))
def append_headers(header_list, debug=False):
if debug:
@@ -409,7 +403,7 @@ class CoreRequestHandlingTest(helper.CPWebCase):
self.assertStatus(('302 Found', '303 See Other'))
# check injection protection
- # See https://bitbucket.org/cherrypy/cherrypy/issue/1003
+ # See https://github.com/cherrypy/cherrypy/issues/1003
self.getPage(
"/redirect/custom?"
"code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval")
@@ -554,21 +548,31 @@ class CoreRequestHandlingTest(helper.CPWebCase):
self.getPage("/favicon.ico")
self.assertBody(data)
+ def skip_if_bad_cookies(self):
+ """
+ cookies module fails to reject invalid cookies
+ https://github.com/cherrypy/cherrypy/issues/1405
+ """
+ cookies = sys.modules.get('http.cookies')
+ _is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False)
+ if not _is_legal_key(','):
+ return
+ issue = 'http://bugs.python.org/issue26302'
+ tmpl = "Broken cookies module ({issue})"
+ self.skip(tmpl.format(**locals()))
+
def testCookies(self):
- if sys.version_info >= (2, 5):
- header_value = lambda x: x
- else:
- header_value = lambda x: x + ';'
+ self.skip_if_bad_cookies()
self.getPage("/cookies/single?name=First",
[('Cookie', 'First=Dinsdale;')])
- self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
+ self.assertHeader('Set-Cookie', 'First=Dinsdale')
self.getPage("/cookies/multiple?names=First&names=Last",
[('Cookie', 'First=Dinsdale; Last=Piranha;'),
])
- self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
- self.assertHeader('Set-Cookie', header_value('Last=Piranha'))
+ self.assertHeader('Set-Cookie', 'First=Dinsdale')
+ self.assertHeader('Set-Cookie', 'Last=Piranha')
self.getPage("/cookies/single?name=Something-With%2CComma",
[('Cookie', 'Something-With,Comma=some-value')])
@@ -660,9 +664,6 @@ class CoreRequestHandlingTest(helper.CPWebCase):
self.assertBody('/page1')
def test_expose_decorator(self):
- if not sys.version_info >= (2, 5):
- return self.skip("skipped (Python 2.5+ only) ")
-
# Test @expose
self.getPage("/expose_dec/no_call")
self.assertStatus(200)
diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py
index 85869a9d..c6b4eb46 100644
--- a/cherrypy/test/test_encoding.py
+++ b/cherrypy/test/test_encoding.py
@@ -3,7 +3,7 @@ import gzip
import cherrypy
from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou
-europoundUnicode = ntou('\x80\xa3')
+europoundUnicode = ntou(r'\x00\xa3')
sing = ntou("\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?", 'escape')
sing8 = sing.encode('utf-8')
sing16 = sing.encode('utf-16')
diff --git a/cherrypy/test/test_http.py b/cherrypy/test/test_http.py
index ff96afa2..46ae226c 100644
--- a/cherrypy/test/test_http.py
+++ b/cherrypy/test/test_http.py
@@ -5,6 +5,8 @@ import mimetypes
import socket
import sys
+from mock import patch
+
import cherrypy
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob, py3k
@@ -35,12 +37,12 @@ from cherrypy.test import helper
class HTTPTests(helper.CPWebCase):
-
+
def make_connection(self):
if self.scheme == "https":
return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
- return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
def setup_server():
class Root:
@@ -78,7 +80,7 @@ class HTTPTests(helper.CPWebCase):
summary.append("%s * %d" % (curchar, count))
return ", ".join(summary)
post_multipart.exposed = True
-
+
@cherrypy.expose
def post_filename(self, myfile):
'''Return the name of the file which was uploaded.'''
@@ -111,7 +113,11 @@ class HTTPTests(helper.CPWebCase):
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
- c.request("POST", "/")
+ if hasattr(c, '_set_content_length'): # python 2.6 doesn't have it
+ with patch.object(c, '_set_content_length'):
+ c.request("POST", "/")
+ else:
+ c.request("POST", "/")
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
@@ -140,11 +146,11 @@ class HTTPTests(helper.CPWebCase):
self.status = str(response.status)
self.assertStatus(200)
self.assertBody(", ".join(["%s * 65536" % c for c in alphabet]))
-
+
def test_post_filename_with_commas(self):
'''Testing that we can handle filenames with commas. This was
reported as a bug in:
- https://bitbucket.org/cherrypy/cherrypy/issue/1146/'''
+ https://github.com/cherrypy/cherrypy/issues/1146/'''
# We'll upload a bunch of files with differing names.
for fname in ['boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv']:
files = [('myfile', fname, 'yunyeenyunyue')]
@@ -194,7 +200,7 @@ class HTTPTests(helper.CPWebCase):
c = self.make_connection()
c.putrequest('GET', '/')
c.putheader('Content-Type', 'text/plain')
- # See https://bitbucket.org/cherrypy/cherrypy/issue/941
+ # See https://github.com/cherrypy/cherrypy/issues/941
c._output(ntob('Re, 1.2.3.4#015#012'))
c.endheaders()
diff --git a/cherrypy/test/test_httpauth.py b/cherrypy/test/test_httpauth.py
index 98a300f0..8be48b6b 100644
--- a/cherrypy/test/test_httpauth.py
+++ b/cherrypy/test/test_httpauth.py
@@ -1,5 +1,7 @@
+from hashlib import md5, sha1
+
import cherrypy
-from cherrypy._cpcompat import md5, sha, ntob
+from cherrypy._cpcompat import ntob
from cherrypy.lib import httpauth
from cherrypy.test import helper
@@ -39,10 +41,10 @@ class HTTPAuthTest(helper.CPWebCase):
return {'test': 'test'}
def sha_password_encrypter(password):
- return sha(ntob(password)).hexdigest()
+ return sha1(ntob(password)).hexdigest()
def fetch_password(username):
- return sha(ntob('test')).hexdigest()
+ return sha1(ntob('test')).hexdigest()
conf = {
'/digest': {
diff --git a/cherrypy/test/test_iterator.py b/cherrypy/test/test_iterator.py
index dcf4bc94..c09a7cf9 100644
--- a/cherrypy/test/test_iterator.py
+++ b/cherrypy/test/test_iterator.py
@@ -4,7 +4,7 @@ from cherrypy._cpcompat import unicodestr
class IteratorBase(object):
created = 0
- datachunk = u'butternut squash' * 256
+ datachunk = 'butternut squash' * 256
@classmethod
def incr(cls):
diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py
index 0c029ad7..429b8f2f 100644
--- a/cherrypy/test/test_logging.py
+++ b/cherrypy/test/test_logging.py
@@ -169,7 +169,8 @@ class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
try:
self.getPage("/error")
self.assertInBody("raise ValueError()")
- self.assertLog(0, 'HTTP Traceback (most recent call last):')
- self.assertLog(-3, 'raise ValueError()')
+ self.assertLog(0, 'HTTP')
+ self.assertLog(1, 'Traceback (most recent call last):')
+ self.assertLog(-2, 'raise ValueError()')
finally:
ignore.pop()
diff --git a/cherrypy/test/test_mime.py b/cherrypy/test/test_mime.py
index f5f2b9fb..ca2f9c63 100644
--- a/cherrypy/test/test_mime.py
+++ b/cherrypy/test/test_mime.py
@@ -74,7 +74,7 @@ This is the <strong>HTML</strong> version
'--X',
# Test a param with more than one value.
# See
- # https://bitbucket.org/cherrypy/cherrypy/issue/1028
+ # https://github.com/cherrypy/cherrypy/issues/1028
'Content-Disposition: form-data; name="baz"',
'',
'111',
diff --git a/cherrypy/test/test_objectmapping.py b/cherrypy/test/test_objectmapping.py
index e80a7a71..a24e84e1 100644
--- a/cherrypy/test/test_objectmapping.py
+++ b/cherrypy/test/test_objectmapping.py
@@ -281,7 +281,7 @@ class ObjectMappingTest(helper.CPWebCase):
# Make sure /foobar maps to Root.foobar and not to the app
# mounted at /foo. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/573
+ # https://github.com/cherrypy/cherrypy/issues/573
self.getPage("/foobar")
self.assertBody("bar")
@@ -332,7 +332,7 @@ class ObjectMappingTest(helper.CPWebCase):
self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
# test that extra positional args raises an 404 Not Found
- # See https://bitbucket.org/cherrypy/cherrypy/issue/733.
+ # See https://github.com/cherrypy/cherrypy/issues/733.
self.getPage("/dir1/dir2/script_name/extra/stuff")
self.assertStatus(404)
diff --git a/cherrypy/test/test_proxy.py b/cherrypy/test/test_proxy.py
index 821a4e52..bdc5eada 100644
--- a/cherrypy/test/test_proxy.py
+++ b/cherrypy/test/test_proxy.py
@@ -131,7 +131,7 @@ class ProxyTest(helper.CPWebCase):
self.assertBody(expected)
# Test trailing slash (see
- # https://bitbucket.org/cherrypy/cherrypy/issue/562).
+ # https://github.com/cherrypy/cherrypy/issues/562).
self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')])
self.assertHeader('Location', "%s://www.example.test/xhost"
% self.scheme)
diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py
index d9989e97..58736b54 100644
--- a/cherrypy/test/test_request_obj.py
+++ b/cherrypy/test/test_request_obj.py
@@ -184,7 +184,7 @@ class RequestObjectTests(helper.CPWebCase):
return cherrypy.request.headers[headername]
def doubledheaders(self):
- # From https://bitbucket.org/cherrypy/cherrypy/issue/165:
+ # From https://github.com/cherrypy/cherrypy/issues/165:
# "header field names should not be case sensitive sayes the
# rfc. if i set a headerfield in complete lowercase i end up
# with two header fields, one in lowercase, the other in
@@ -377,15 +377,27 @@ class RequestObjectTests(helper.CPWebCase):
self.getPage(uri)
self.assertStatus(200)
- # query string parameters are part of the URI, so if they are wrong
- # for a particular handler, the status MUST be a 404.
error_msgs = [
'Missing parameters',
'Nothing matches the given URI',
'Multiple values for parameters',
'Unexpected query string parameters',
'Unexpected body parameters',
+ 'Invalid path in Request-URI',
+ 'Illegal #fragment in Request-URI',
]
+
+ # uri should be tested for valid absolute path, the status must be 400.
+ for uri, error_idx in (
+ ('invalid/path/without/leading/slash', 5),
+ ('/valid/path#invalid=fragment', 6),
+ ):
+ self.getPage(uri)
+ self.assertStatus(400)
+ self.assertInBody(error_msgs[error_idx])
+
+ # query string parameters are part of the URI, so if they are wrong
+ # for a particular handler, the status MUST be a 404.
for uri, msg in (
('/paramerrors/one_positional', error_msgs[0]),
('/paramerrors/one_positional?foo=foo', error_msgs[0]),
@@ -631,7 +643,7 @@ class RequestObjectTests(helper.CPWebCase):
"en;q=0.7")
# Test malformed header parsing. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/763.
+ # https://github.com/cherrypy/cherrypy/issues/763.
self.getPage("/headerelements/get_elements?headername=Content-Type",
# Note the illegal trailing ";"
headers=[('Content-Type', 'text/html; charset=utf-8;')])
@@ -640,7 +652,7 @@ class RequestObjectTests(helper.CPWebCase):
def test_repeated_headers(self):
# Test that two request headers are collapsed into one.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/542.
+ # See https://github.com/cherrypy/cherrypy/issues/542.
self.getPage("/headers/Accept-Charset",
headers=[("Accept-Charset", "iso-8859-5"),
("Accept-Charset", "unicode-1-1;q=0.8")])
@@ -726,7 +738,7 @@ class RequestObjectTests(helper.CPWebCase):
self.assertBody(b)
# Request a PUT method with a file body but no Content-Type.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/790.
+ # See https://github.com/cherrypy/cherrypy/issues/790.
b = ntob("one thing on top of another")
self.persistent = True
try:
@@ -745,7 +757,7 @@ class RequestObjectTests(helper.CPWebCase):
self.persistent = False
# Request a PUT method with no body whatsoever (not an empty one).
- # See https://bitbucket.org/cherrypy/cherrypy/issue/650.
+ # See https://github.com/cherrypy/cherrypy/issues/650.
# Provide a C-T or webtest will provide one (and a C-L) for us.
h = [("Content-Type", "text/plain")]
self.getPage("/method/reachable", headers=h, method="PUT")
diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py
index d86e5cf5..dc6891bc 100644
--- a/cherrypy/test/test_states.py
+++ b/cherrypy/test/test_states.py
@@ -204,9 +204,25 @@ class ServerStateTests(helper.CPWebCase):
# thread will just die without writing a response.
engine.start()
cherrypy.server.start()
-
+ # From python3.5 a new exception is retuned when the connection
+ # ends abruptly:
+ # http.client.RemoteDisconnected
+ # RemoteDisconnected is a subclass of:
+ # (ConnectionResetError, http.client.BadStatusLine)
+ # and ConnectionResetError is an indirect subclass of:
+ # OSError
+ # From python 3.3 an up socket.error is an alias to OSError
+ # following PEP-3151, therefore http.client.RemoteDisconnected
+ # is considered a socket.error.
+ #
+ # raise_subcls specifies the classes that are not going
+ # to be considered as a socket.error for the retries.
+ # Given that RemoteDisconnected is part BadStatusLine
+ # we can use the same call for all py3 versions without
+ # sideffects. python < 3.5 will raise directly BadStatusLine
+ # which is not a subclass for socket.error/OSError.
try:
- self.getPage("/ctrlc")
+ self.getPage("/ctrlc", raise_subcls=BadStatusLine)
except BadStatusLine:
pass
else:
diff --git a/cherrypy/test/test_static.py b/cherrypy/test/test_static.py
index 0526844f..70412c90 100644
--- a/cherrypy/test/test_static.py
+++ b/cherrypy/test/test_static.py
@@ -11,7 +11,7 @@ bigfile_filepath = os.path.join(curdir, "static", "bigfile.log")
# 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
# test_file_stream.
-BIGFILE_SIZE = 1024 * 1024 * 4
+BIGFILE_SIZE = 1024 * 1024 * 20
import cherrypy
from cherrypy.lib import static
@@ -119,7 +119,7 @@ class StaticTest(helper.CPWebCase):
pass
teardown_server = staticmethod(teardown_server)
- def testStatic(self):
+ def test_static(self):
self.getPage("/static/index.html")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
@@ -279,7 +279,9 @@ class StaticTest(helper.CPWebCase):
#
# At the time of writing, we seem to have encountered
# buffer sizes bigger than 512K, so we've increased
- # BIGFILE_SIZE to 4MB.
+ # BIGFILE_SIZE to 4MB and in 2016 to 20MB.
+ # This test is going to keep failing according to the
+ # improvements in hardware and OS buffers.
if tell_position >= BIGFILE_SIZE:
if read_so_far < (BIGFILE_SIZE / 2):
self.fail(
@@ -333,6 +335,10 @@ class StaticTest(helper.CPWebCase):
self.assertStatus(404)
self.assertInBody("I couldn't find that thing")
+ def test_null_bytes(self):
+ self.getPage("/static/\x00")
+ self.assertStatus('404 Not Found')
+
def error_page_404(status, message, traceback, version):
import os.path
return static.serve_file(os.path.join(curdir, 'static', '404.html'),
diff --git a/cherrypy/test/test_wsgi_unix_socket.py b/cherrypy/test/test_wsgi_unix_socket.py
new file mode 100644
index 00000000..7a829d51
--- /dev/null
+++ b/cherrypy/test/test_wsgi_unix_socket.py
@@ -0,0 +1,106 @@
+import os
+import sys
+import socket
+import atexit
+import tempfile
+
+import cherrypy
+from cherrypy.test import helper
+from cherrypy._cpcompat import HTTPConnection
+
+def usocket_path():
+ fd, path = tempfile.mkstemp('cp_test.sock')
+ os.close(fd)
+ os.remove(path)
+ return path
+
+USOCKET_PATH = usocket_path()
+
+class USocketHTTPConnection(HTTPConnection):
+ """
+ HTTPConnection over a unix socket.
+ """
+
+ def __init__(self, path):
+ HTTPConnection.__init__(self, 'localhost')
+ self.path = path
+
+ def __call__(self, *args, **kwargs):
+ """
+ Catch-all method just to present itself as a constructor for the
+ HTTPConnection.
+ """
+ return self
+
+ def connect(self):
+ """
+ Override the connect method and assign a unix socket as a transport.
+ """
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.connect(self.path)
+ self.sock = sock
+ atexit.register(lambda: os.remove(self.path))
+
+
+def skip_on_windows(method):
+ """
+ Decorator to skip the method call if the test is executing on Windows.
+ """
+ def wrapper(self):
+ if sys.platform == "win32":
+ return self.skip("No UNIX Socket support in Windows.")
+ else:
+ return method(self)
+ wrapper.__doc__ = method.__doc__
+ wrapper.__name__ = method.__name__
+ return wrapper
+
+
+
+class WSGI_UnixSocket_Test(helper.CPWebCase):
+ """
+ Test basic behavior on a cherrypy wsgi server listening
+ on a unix socket.
+
+ It exercises the config option `server.socket_file`.
+ """
+ HTTP_CONN = USocketHTTPConnection(USOCKET_PATH)
+
+
+ @staticmethod
+ def setup_server():
+ class Root(object):
+
+ @cherrypy.expose
+ def index(self):
+ return "Test OK"
+
+ @cherrypy.expose
+ def error(self):
+ raise Exception("Invalid page")
+
+ config = {
+ 'server.socket_file': USOCKET_PATH
+ }
+ cherrypy.config.update(config)
+ cherrypy.tree.mount(Root())
+
+ def tearDown(self):
+ cherrypy.config.update({'server.socket_file': None})
+
+ @skip_on_windows
+ def test_simple_request(self):
+ self.getPage("/")
+ self.assertStatus("200 OK")
+ self.assertInBody("Test OK")
+
+ @skip_on_windows
+ def test_not_found(self):
+ self.getPage("/invalid_path")
+ self.assertStatus("404 Not Found")
+
+ @skip_on_windows
+ def test_internal_error(self):
+ self.getPage("/error")
+ self.assertStatus("500 Internal Server Error")
+ self.assertInBody("Invalid page")
diff --git a/cherrypy/test/test_xmlrpc.py b/cherrypy/test/test_xmlrpc.py
index 8f091ff1..4beed0a3 100644
--- a/cherrypy/test/test_xmlrpc.py
+++ b/cherrypy/test/test_xmlrpc.py
@@ -163,7 +163,7 @@ class XmlRpcTest(helper.CPWebCase):
else:
self.fail("Expected xmlrpclib.Fault")
- # https://bitbucket.org/cherrypy/cherrypy/issue/533
+ # https://github.com/cherrypy/cherrypy/issues/533
# if a method is not found, an xmlrpclib.Fault should be raised
try:
proxy.non_method()
diff --git a/cherrypy/test/webtest.py b/cherrypy/test/webtest.py
index 1fa3f969..959063f5 100644
--- a/cherrypy/test/webtest.py
+++ b/cherrypy/test/webtest.py
@@ -238,8 +238,13 @@ class WebCase(TestCase):
return interface(self.HOST)
def getPage(self, url, headers=None, method="GET", body=None,
- protocol=None):
+ protocol=None, raise_subcls=None):
"""Open the url with debugging support. 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.
"""
ServerError.on = False
@@ -252,7 +257,8 @@ class WebCase(TestCase):
self.time = None
start = time.time()
result = openURL(url, headers, method, body, self.HOST, self.PORT,
- self.HTTP_CONN, protocol or self.PROTOCOL)
+ self.HTTP_CONN, protocol or self.PROTOCOL,
+ raise_subcls)
self.time = time.time() - start
self.status, self.headers, self.body = result
@@ -492,9 +498,15 @@ def shb(response):
def openURL(url, headers=None, method="GET", body=None,
host="127.0.0.1", port=8000, http_conn=HTTPConnection,
- protocol="HTTP/1.1"):
- """Open the given HTTP resource and return status, headers, and body."""
+ protocol="HTTP/1.1", raise_subcls=None):
+ """
+ Open the given HTTP resource and return status, headers, and 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.
+ """
headers = cleanHeaders(headers, method, body, host, port)
# Trying 10 times is simply in case of socket errors.
@@ -510,48 +522,10 @@ def openURL(url, headers=None, method="GET", body=None,
conn._http_vsn_str = protocol
conn._http_vsn = int("".join([x for x in protocol if x.isdigit()]))
- # skip_accept_encoding argument added in python version 2.4
- if sys.version_info < (2, 4):
- def putheader(self, header, value):
- if header == 'Accept-Encoding' and value == 'identity':
- return
- self.__class__.putheader(self, header, value)
- import new
- conn.putheader = new.instancemethod(
- putheader, conn, conn.__class__)
- conn.putrequest(method.upper(), url, skip_host=True)
- elif not py3k:
- conn.putrequest(method.upper(), url, skip_host=True,
- skip_accept_encoding=True)
- else:
- import http.client
- # Replace the stdlib method, which only accepts ASCII url's
-
- def putrequest(self, method, url):
- if (
- self._HTTPConnection__response and
- self._HTTPConnection__response.isclosed()
- ):
- self._HTTPConnection__response = None
-
- if self._HTTPConnection__state == http.client._CS_IDLE:
- self._HTTPConnection__state = (
- http.client._CS_REQ_STARTED)
- else:
- raise http.client.CannotSendRequest()
-
- self._method = method
- if not url:
- url = ntob('/')
- request = ntob(' ').join(
- (method.encode("ASCII"),
- url,
- self._http_vsn_str.encode("ASCII")))
- self._output(request)
- import types
- conn.putrequest = types.MethodType(putrequest, conn)
-
- conn.putrequest(method.upper(), url)
+ if py3k and isinstance(url, bytes):
+ url = url.decode()
+ conn.putrequest(method.upper(), url, skip_host=True,
+ skip_accept_encoding=True)
for key, value in headers:
conn.putheader(key, value.encode("Latin-1"))
@@ -570,10 +544,15 @@ def openURL(url, headers=None, method="GET", body=None,
conn.close()
return s, h, b
- except socket.error:
- time.sleep(0.5)
- if trial == 9:
+ except socket.error as e:
+ if raise_subcls is not None and isinstance(e, raise_subcls):
raise
+ else:
+ time.sleep(0.5)
+ if trial == 9:
+ raise
+
+
# Add any exceptions which your web framework handles
diff --git a/cherrypy/wsgiserver/ssl_pyopenssl.py b/cherrypy/wsgiserver/ssl_pyopenssl.py
index f8f2dafe..62ede265 100644
--- a/cherrypy/wsgiserver/ssl_pyopenssl.py
+++ b/cherrypy/wsgiserver/ssl_pyopenssl.py
@@ -68,7 +68,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
time.sleep(self.ssl_retry)
except SSL.WantWriteError:
time.sleep(self.ssl_retry)
- except SSL.SysCallError, e:
+ except SSL.SysCallError as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
@@ -76,7 +76,7 @@ class SSL_fileobject(wsgiserver.CP_fileobject):
if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
return ""
raise socket.error(errnum)
- except SSL.Error, e:
+ except SSL.Error as e:
if is_reader and e.args == (-1, 'Unexpected EOF'):
return ""
diff --git a/cherrypy/wsgiserver/wsgiserver2.py b/cherrypy/wsgiserver/wsgiserver2.py
index 8d8b04e4..f3ae1f9d 100644
--- a/cherrypy/wsgiserver/wsgiserver2.py
+++ b/cherrypy/wsgiserver/wsgiserver2.py
@@ -75,7 +75,8 @@ __all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer',
'WorkerThread', 'ThreadPool', 'SSLAdapter',
'CherryPyWSGIServer',
'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0',
- 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class']
+ 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class',
+ 'socket_errors_to_ignore']
import os
try:
@@ -83,19 +84,43 @@ try:
except:
import Queue as queue
import re
-import rfc822
+import email.utils
import socket
import sys
+import threading
+import time
+import traceback as traceback_
+import operator
+from urllib import unquote
+import warnings
+import errno
+import logging
+try:
+ # prefer slower Python-based io module
+ import _pyio as io
+except ImportError:
+ # Python 2.6
+ import io
+
+try:
+ import pkg_resources
+except ImportError:
+ pass
+
if 'win' in sys.platform and hasattr(socket, "AF_INET6"):
if not hasattr(socket, 'IPPROTO_IPV6'):
socket.IPPROTO_IPV6 = 41
if not hasattr(socket, 'IPV6_V6ONLY'):
socket.IPV6_V6ONLY = 27
+
+
+DEFAULT_BUFFER_SIZE = io.DEFAULT_BUFFER_SIZE
+
+
try:
- import cStringIO as StringIO
-except ImportError:
- import StringIO
-DEFAULT_BUFFER_SIZE = -1
+ cp_version = pkg_resources.require('cherrypy')[0].version
+except Exception:
+ cp_version = 'unknown'
class FauxSocket(object):
@@ -109,23 +134,6 @@ _fileobject_uses_str_type = isinstance(
socket._fileobject(FauxSocket())._rbuf, basestring)
del FauxSocket # this class is not longer required for anything.
-import threading
-import time
-import traceback
-
-
-def format_exc(limit=None):
- """Like print_exc() but return a string. Backport for Python 2.3."""
- try:
- etype, value, tb = sys.exc_info()
- return ''.join(traceback.format_exception(etype, value, tb, limit))
- finally:
- etype = value = tb = None
-
-import operator
-
-from urllib import unquote
-import warnings
if sys.version_info >= (3, 0):
bytestr = bytes
@@ -165,8 +173,6 @@ ASTERISK = ntob('*')
FORWARD_SLASH = ntob('/')
quoted_slash = re.compile(ntob("(?i)%2F"))
-import errno
-
def plat_specific_errors(*errnames):
"""Return error numbers for all errors in errnames on this platform.
@@ -210,7 +216,6 @@ comma_separated_headers = [
]
-import logging
if not hasattr(logging, 'statistics'):
logging.statistics = {}
@@ -302,7 +307,7 @@ class SizeCheckWrapper(object):
self.bytes_read += len(data)
self._check_length()
res.append(data)
- # See https://bitbucket.org/cherrypy/cherrypy/issue/421
+ # See https://github.com/cherrypy/cherrypy/issues/421
if len(data) < 256 or data[-1:] == LF:
return EMPTY.join(res)
@@ -674,6 +679,10 @@ class HTTPRequest(object):
# uri may be an abs_path (including "http://host.domain.tld");
scheme, authority, path = self.parse_request_uri(uri)
+ if path is None:
+ self.simple_response("400 Bad Request",
+ "Invalid path in Request-URI.")
+ return False
if NUMBER_SIGN in path:
self.simple_response("400 Bad Request",
"Illegal #fragment in Request-URI.")
@@ -798,7 +807,7 @@ class HTTPRequest(object):
if self.inheaders.get("Expect", "") == "100-continue":
# Don't use simple_response here, because it emits headers
# we don't want. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/951
+ # https://github.com/cherrypy/cherrypy/issues/951
msg = self.server.protocol + " 100 Continue\r\n\r\n"
try:
self.conn.wfile.sendall(msg)
@@ -970,7 +979,7 @@ class HTTPRequest(object):
self.rfile.read(remaining)
if "date" not in hkeys:
- self.outheaders.append(("Date", rfc822.formatdate()))
+ self.outheaders.append(("Date", email.utils.formatdate()))
if "server" not in hkeys:
self.outheaders.append(("Server", self.server.server_name))
@@ -1051,7 +1060,7 @@ class CP_fileobject(socket._fileobject):
if size < 0:
# Read until EOF
# reset _rbuf. we consume it via buf.
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
while True:
data = self.recv(rbufsize)
if not data:
@@ -1066,12 +1075,12 @@ class CP_fileobject(socket._fileobject):
# return.
buf.seek(0)
rv = buf.read(size)
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
self._rbuf.write(buf.read())
return rv
# reset _rbuf. we consume it via buf.
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
while True:
left = size - buf_len
# recv() will malloc the amount of memory given as its
@@ -1109,7 +1118,7 @@ class CP_fileobject(socket._fileobject):
buf.seek(0)
bline = buf.readline(size)
if bline.endswith('\n') or len(bline) == size:
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
self._rbuf.write(buf.read())
return bline
del bline
@@ -1120,7 +1129,7 @@ class CP_fileobject(socket._fileobject):
buf.seek(0)
buffers = [buf.read()]
# reset _rbuf. we consume it via buf.
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
data = None
recv = self.recv
while data != "\n":
@@ -1132,7 +1141,7 @@ class CP_fileobject(socket._fileobject):
buf.seek(0, 2) # seek end
# reset _rbuf. we consume it via buf.
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
while True:
data = self.recv(self._rbufsize)
if not data:
@@ -1154,11 +1163,11 @@ class CP_fileobject(socket._fileobject):
if buf_len >= size:
buf.seek(0)
rv = buf.read(size)
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
self._rbuf.write(buf.read())
return rv
# reset _rbuf. we consume it via buf.
- self._rbuf = StringIO.StringIO()
+ self._rbuf = io.BytesIO()
while True:
data = self.recv(self._rbufsize)
if not data:
@@ -1364,7 +1373,7 @@ class HTTPConnection(object):
# Don't error if we're between requests; only error
# if 1) no request has been started at all, or 2) we're
# in the middle of a request.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/853
+ # See https://github.com/cherrypy/cherrypy/issues/853
if (not request_seen) or (req and req.started_request):
# Don't bother writing the 408 if the response
# has already started being written.
@@ -1657,7 +1666,7 @@ class ThreadPool(object):
except (AssertionError,
# Ignore repeated Ctrl-C.
# See
- # https://bitbucket.org/cherrypy/cherrypy/issue/691.
+ # https://github.com/cherrypy/cherrypy/issues/691.
KeyboardInterrupt):
pass
@@ -1757,7 +1766,7 @@ class HTTPServer(object):
timeout = 10
"""The timeout in seconds for accepted connections (default 10)."""
- version = "CherryPy/3.6.1"
+ version = "CherryPy/" + cp_version
"""A version string for the HTTPServer."""
software = None
@@ -1884,25 +1893,6 @@ class HTTPServer(object):
if self.software is None:
self.software = "%s Server" % self.version
- # SSL backward compatibility
- if (self.ssl_adapter is None and
- getattr(self, 'ssl_certificate', None) and
- getattr(self, 'ssl_private_key', None)):
- warnings.warn(
- "SSL attributes are deprecated in CherryPy 3.2, and will "
- "be removed in CherryPy 3.3. Use an ssl_adapter attribute "
- "instead.",
- DeprecationWarning
- )
- try:
- from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
- except ImportError:
- pass
- else:
- self.ssl_adapter = pyOpenSSLAdapter(
- self.ssl_certificate, self.ssl_private_key,
- getattr(self, 'ssl_certificate_chain', None))
-
# Select the appropriate socket
if isinstance(self.bind_addr, basestring):
# AF_UNIX socket
@@ -1915,7 +1905,7 @@ class HTTPServer(object):
# So everyone can access the socket...
try:
- os.chmod(self.bind_addr, 511) # 0777
+ os.chmod(self.bind_addr, 0o777)
except:
pass
@@ -1984,7 +1974,7 @@ class HTTPServer(object):
sys.stderr.write(msg + '\n')
sys.stderr.flush()
if traceback:
- tblines = format_exc()
+ tblines = traceback_.format_exc()
sys.stderr.write(tblines)
sys.stderr.flush()
@@ -2001,7 +1991,7 @@ class HTTPServer(object):
# If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
# activate dual-stack. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/871.
+ # https://github.com/cherrypy/cherrypy/issues/871.
if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6
and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
try:
@@ -2095,15 +2085,15 @@ class HTTPServer(object):
# the call, and I *think* I'm reading it right that Python
# will then go ahead and poll for and handle the signal
# elsewhere. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/707.
+ # https://github.com/cherrypy/cherrypy/issues/707.
return
if x.args[0] in socket_errors_nonblocking:
# Just try again. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/479.
+ # https://github.com/cherrypy/cherrypy/issues/479.
return
if x.args[0] in socket_errors_to_ignore:
# Our socket was closed.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/686.
+ # See https://github.com/cherrypy/cherrypy/issues/686.
return
raise
@@ -2136,7 +2126,7 @@ class HTTPServer(object):
if x.args[0] not in socket_errors_to_ignore:
# Changed to use error code and not message
# See
- # https://bitbucket.org/cherrypy/cherrypy/issue/860.
+ # https://github.com/cherrypy/cherrypy/issues/860.
raise
else:
# Note that we're explicitly NOT using AI_PASSIVE,
@@ -2186,7 +2176,7 @@ ssl_adapters = {
}
-def get_ssl_adapter_class(name='pyopenssl'):
+def get_ssl_adapter_class(name='builtin'):
"""Return an SSL adapter class for the given name."""
adapter = ssl_adapters[name.lower()]
if isinstance(adapter, basestring):
diff --git a/cherrypy/wsgiserver/wsgiserver3.py b/cherrypy/wsgiserver/wsgiserver3.py
index 24e38ad3..eb169d5e 100644
--- a/cherrypy/wsgiserver/wsgiserver3.py
+++ b/cherrypy/wsgiserver/wsgiserver3.py
@@ -75,7 +75,8 @@ __all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer',
'WorkerThread', 'ThreadPool', 'SSLAdapter',
'CherryPyWSGIServer',
'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0',
- 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class']
+ 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class',
+ 'socket_errors_to_ignore']
import os
try:
@@ -86,20 +87,38 @@ import re
import email.utils
import socket
import sys
+import threading
+import time
+import traceback as traceback_
+import errno
+import logging
+try:
+ # prefer slower Python-based io module
+ import _pyio as io
+except ImportError:
+ # Python 2.6
+ import io
+
+try:
+ import pkg_resources
+except ImportError:
+ pass
+
if 'win' in sys.platform and hasattr(socket, "AF_INET6"):
if not hasattr(socket, 'IPPROTO_IPV6'):
socket.IPPROTO_IPV6 = 41
if not hasattr(socket, 'IPV6_V6ONLY'):
socket.IPV6_V6ONLY = 27
-if sys.version_info < (3, 1):
- import io
-else:
- import _pyio as io
+
+
DEFAULT_BUFFER_SIZE = io.DEFAULT_BUFFER_SIZE
-import threading
-import time
-from traceback import format_exc
+
+try:
+ cp_version = pkg_resources.require('cherrypy')[0].version
+except Exception:
+ cp_version = 'unknown'
+
if sys.version_info >= (3, 0):
bytestr = bytes
@@ -139,8 +158,6 @@ ASTERISK = ntob('*')
FORWARD_SLASH = ntob('/')
quoted_slash = re.compile(ntob("(?i)%2F"))
-import errno
-
def plat_specific_errors(*errnames):
"""Return error numbers for all errors in errnames on this platform.
@@ -184,7 +201,6 @@ comma_separated_headers = [
]
-import logging
if not hasattr(logging, 'statistics'):
logging.statistics = {}
@@ -276,7 +292,7 @@ class SizeCheckWrapper(object):
self.bytes_read += len(data)
self._check_length()
res.append(data)
- # See https://bitbucket.org/cherrypy/cherrypy/issue/421
+ # See https://github.com/cherrypy/cherrypy/issues/421
if len(data) < 256 or data[-1:] == LF:
return EMPTY.join(res)
@@ -650,6 +666,10 @@ class HTTPRequest(object):
# uri may be an abs_path (including "http://host.domain.tld");
scheme, authority, path = self.parse_request_uri(uri)
+ if path is None:
+ self.simple_response("400 Bad Request",
+ "Invalid path in Request-URI.")
+ return False
if NUMBER_SIGN in path:
self.simple_response("400 Bad Request",
"Illegal #fragment in Request-URI.")
@@ -775,7 +795,7 @@ class HTTPRequest(object):
if self.inheaders.get(b"Expect", b"") == b"100-continue":
# Don't use simple_response here, because it emits headers
# we don't want. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/951
+ # https://github.com/cherrypy/cherrypy/issues/951
msg = self.server.protocol.encode(
'ascii') + b" 100 Continue\r\n\r\n"
try:
@@ -1079,7 +1099,7 @@ class HTTPConnection(object):
# Don't error if we're between requests; only error
# if 1) no request has been started at all, or 2) we're
# in the middle of a request.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/853
+ # See https://github.com/cherrypy/cherrypy/issues/853
if (not request_seen) or (req and req.started_request):
# Don't bother writing the 408 if the response
# has already started being written.
@@ -1368,7 +1388,7 @@ class ThreadPool(object):
except (AssertionError,
# Ignore repeated Ctrl-C.
# See
- # https://bitbucket.org/cherrypy/cherrypy/issue/691.
+ # https://github.com/cherrypy/cherrypy/issues/691.
KeyboardInterrupt):
pass
@@ -1468,7 +1488,7 @@ class HTTPServer(object):
timeout = 10
"""The timeout in seconds for accepted connections (default 10)."""
- version = "CherryPy/3.6.1"
+ version = "CherryPy/" + cp_version
"""A version string for the HTTPServer."""
software = None
@@ -1608,7 +1628,7 @@ class HTTPServer(object):
# So everyone can access the socket...
try:
- os.chmod(self.bind_addr, 511) # 0777
+ os.chmod(self.bind_addr, 0o777)
except:
pass
@@ -1676,7 +1696,7 @@ class HTTPServer(object):
sys.stderr.write(msg + '\n')
sys.stderr.flush()
if traceback:
- tblines = format_exc()
+ tblines = traceback_.format_exc()
sys.stderr.write(tblines)
sys.stderr.flush()
@@ -1693,7 +1713,7 @@ class HTTPServer(object):
# If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
# activate dual-stack. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/871.
+ # https://github.com/cherrypy/cherrypy/issues/871.
if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6
and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
try:
@@ -1787,15 +1807,15 @@ class HTTPServer(object):
# the call, and I *think* I'm reading it right that Python
# will then go ahead and poll for and handle the signal
# elsewhere. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/707.
+ # https://github.com/cherrypy/cherrypy/issues/707.
return
if x.args[0] in socket_errors_nonblocking:
# Just try again. See
- # https://bitbucket.org/cherrypy/cherrypy/issue/479.
+ # https://github.com/cherrypy/cherrypy/issues/479.
return
if x.args[0] in socket_errors_to_ignore:
# Our socket was closed.
- # See https://bitbucket.org/cherrypy/cherrypy/issue/686.
+ # See https://github.com/cherrypy/cherrypy/issues/686.
return
raise
@@ -1828,7 +1848,7 @@ class HTTPServer(object):
if x.args[0] not in socket_errors_to_ignore:
# Changed to use error code and not message
# See
- # https://bitbucket.org/cherrypy/cherrypy/issue/860.
+ # https://github.com/cherrypy/cherrypy/issues/860.
raise
else:
# Note that we're explicitly NOT using AI_PASSIVE,
@@ -1874,6 +1894,7 @@ class Gateway(object):
# of such classes (in which case they will be lazily loaded).
ssl_adapters = {
'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
+ 'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter',
}
diff --git a/docs/advanced.rst b/docs/advanced.rst
index 2558625e..731fa1bb 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -48,6 +48,8 @@ Obviously, your aliases may be whatever suits your needs.
The alias may be a single string or a list of them.
+.. _restful:
+
RESTful-style dispatching
#########################
@@ -204,7 +206,7 @@ Note that the decorator accepts more than a single binding. For instance:
This would handle the following URL:
-- http://localhost:8080/nirvana/albums/nevermind/track/06/polly
+- http://localhost:8080/nirvana/albums/nevermind/tracks/06/polly
Notice finally how the whole stack of segments is passed to each
page handler so that you have the full context.
diff --git a/docs/basics.rst b/docs/basics.rst
index 92156865..b1896f22 100644
--- a/docs/basics.rst
+++ b/docs/basics.rst
@@ -12,14 +12,14 @@ a CherryPy application, introducing some essential concepts.
The one-minute application example
##################################
-The most basic application you can write with CherryPy
+The most basic application you can write with CherryPy
involves almost all its core concepts.
.. code-block:: python
:linenos:
import cherrypy
-
+
class Root(object):
@cherrypy.expose
def index(self):
@@ -34,16 +34,16 @@ a single import statement as demonstrated in line 1.
Before discussing the meat, let's jump to line 9 which shows,
how to host your application with the CherryPy application server
-and serve it with its builtin HTTP server at the `'/'` path.
+and serve it with its builtin HTTP server at the `'/'` path.
All in one single line. Not bad.
Let's now step back to the actual application. Even though CherryPy
-does not mandate it, most of the time your applications
+does not mandate it, most of the time your applications
will be written as Python classes. Methods of those classes will
be called by CherryPy to respond to client requests. However,
CherryPy needs to be aware that a method can be used that way, we
say the method needs to be :term:`exposed`. This is precisely
-what the :func:`cherrypy.expose()` decorator does in line 4.
+what the :func:`cherrypy.expose()` decorator does in line 4.
Save the snippet in a file named `myapp.py` and run your first
CherryPy application:
@@ -57,14 +57,14 @@ Then point your browser at http://127.0.0.1:8080. Tada!
.. note::
- CherryPy is a small framework that focuses on one single task:
+ CherryPy is a small framework that focuses on one single task:
take a HTTP request and locate the most appropriate
- Python function or method that match the request's URL.
- Unlike other well-known frameworks, CherryPy does not
+ Python function or method that match the request's URL.
+ Unlike other well-known frameworks, CherryPy does not
provide a built-in support for database access, HTML
- templating or any other middleware nifty features.
+ templating or any other middleware nifty features.
- In a nutshell, once CherryPy has found and called an
+ In a nutshell, once CherryPy has found and called an
:term:`exposed` method, it is up to you, as a developer, to
provide the tools to implement your application's logic.
@@ -75,7 +75,7 @@ Then point your browser at http://127.0.0.1:8080. Tada!
The previous example demonstrated the simplicty of the
CherryPy interface but, your application will likely
contain a few other bits and pieces: static service,
- more complex structure, database access, etc.
+ more complex structure, database access, etc.
This will be developed in the tutorial section.
@@ -94,7 +94,7 @@ Single application
^^^^^^^^^^^^^^^^^^
The most straightforward way is to use :func:`cherrypy.quickstart`
-function. It takes at least one argument, the instance of the
+function. It takes at least one argument, the instance of the
application to host. Two other settings are optionals. First, the
base path at which the application will be accessible from. Second,
a config dictionary or file to configure your application.
@@ -112,8 +112,8 @@ the last one provides specific settings for the application.
.. note::
- Notice in the third case how the settings are still
- relative to the application, not where it is made available at,
+ Notice in the third case how the settings are still
+ relative to the application, not where it is made available at,
hence the `{'/': ... }` rather than a `{'/blog': ... }`
@@ -122,25 +122,25 @@ Multiple applications
The :func:`cherrypy.quickstart` approach is fine for a single application,
but lacks the capacity to host several applications with the server.
-To achieve this, one must use the :meth:`cherrypy.tree.mount <cherrypy._cptree.Tree.mount>`
+To achieve this, one must use the :meth:`cherrypy.tree.mount <cherrypy._cptree.Tree.mount>`
function as follows:
.. code-block:: python
cherrypy.tree.mount(Blog(), '/blog', blog_conf)
cherrypy.tree.mount(Forum(), '/forum', forum_conf)
-
+
cherrypy.engine.start()
cherrypy.engine.block()
-Essentially, :meth:`cherrypy.tree.mount <cherrypy._cptree.Tree.mount>`
-takes the same parameters as :func:`cherrypy.quickstart`: an :term:`application`,
-a hosting path segment and a configuration. The last two lines
+Essentially, :meth:`cherrypy.tree.mount <cherrypy._cptree.Tree.mount>`
+takes the same parameters as :func:`cherrypy.quickstart`: an :term:`application`,
+a hosting path segment and a configuration. The last two lines
are simply starting application server.
.. important::
- :func:`cherrypy.quickstart` and :meth:`cherrypy.tree.mount <cherrypy._cptree.Tree.mount>`
+ :func:`cherrypy.quickstart` and :meth:`cherrypy.tree.mount <cherrypy._cptree.Tree.mount>`
are not exclusive. For instance, the previous lines can be written as:
.. code-block:: python
@@ -149,7 +149,7 @@ are simply starting application server.
cherrypy.quickstart(Forum(), '/forum', forum_conf)
.. note::
-
+
You can also :ref:`host foreign WSGI application <hostwsgiapp>`.
@@ -161,11 +161,11 @@ log all incoming requests as well as protocol errors.
To do so, CherryPy manages two loggers:
-- an access one that logs every incoming requests
+- an access one that logs every incoming requests
- an application/error log that traces errors or other application-level messages
Your application may leverage that second logger by calling
-:func:`cherrypy.log() <cherrypy._cplogging.LogManager.error>`.
+:func:`cherrypy.log() <cherrypy._cplogging.LogManager.error>`.
.. code-block:: python
@@ -183,7 +183,7 @@ You can also log an exception:
Both logs are writing to files identified by the following keys
in your configuration:
-- ``log.access_file`` for incoming requests using the
+- ``log.access_file`` for incoming requests using the
`common log format <http://en.wikipedia.org/wiki/Common_Log_Format>`_
- ``log.error_file`` for the other log
@@ -197,42 +197,152 @@ Disable logging
You may be interested in disabling either logs.
-To disable file logging, simply set a en empty string to the
-``log.access_file`` or ``log.error_file`` keys in your
+To disable file logging, simply set a en empty string to the
+``log.access_file`` or ``log.error_file`` keys in your
:ref:`global configuration <globalsettings>`.
To disable, console logging, set ``log.screen`` to `False`.
+.. code-block:: python
+
+ cherrypy.config.update({'log.screen': False,
+ 'log.access_file': '',
+ 'log.error_file': ''})
+
Play along with your other loggers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Your application may obviously already use the :mod:`logging`
-module to trace application level messages. CherryPy will not
-interfere with them as long as your loggers are explicitely
-named. This would work nicely:
+module to trace application level messages. Below is a simple
+example on setting it up.
.. code-block:: python
-
+
import logging
- logger = logging.getLogger('myapp.mypackage')
- logger.setLevel(logging.INFO)
- stream = logging.StreamHandler()
- stream.setLevel(logging.INFO)
- logger.addHandler(stream)
+ import logging.config
+
+ import cherrypy
+
+ logger = logging.getLogger()
+ db_logger = logging.getLogger('db')
+
+ LOG_CONF = {
+ 'version': 1,
+
+ 'formatters': {
+ 'void': {
+ 'format': ''
+ },
+ 'standard': {
+ 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
+ },
+ },
+ 'handlers': {
+ 'default': {
+ 'level':'INFO',
+ 'class':'logging.StreamHandler',
+ 'formatter': 'standard',
+ 'stream': 'ext://sys.stdout'
+ },
+ 'cherrypy_console': {
+ 'level':'INFO',
+ 'class':'logging.StreamHandler',
+ 'formatter': 'void',
+ 'stream': 'ext://sys.stdout'
+ },
+ 'cherrypy_access': {
+ 'level':'INFO',
+ 'class': 'logging.handlers.RotatingFileHandler',
+ 'formatter': 'void',
+ 'filename': 'access.log',
+ 'maxBytes': 10485760,
+ 'backupCount': 20,
+ 'encoding': 'utf8'
+ },
+ 'cherrypy_error': {
+ 'level':'INFO',
+ 'class': 'logging.handlers.RotatingFileHandler',
+ 'formatter': 'void',
+ 'filename': 'errors.log',
+ 'maxBytes': 10485760,
+ 'backupCount': 20,
+ 'encoding': 'utf8'
+ },
+ },
+ 'loggers': {
+ '': {
+ 'handlers': ['default'],
+ 'level': 'INFO'
+ },
+ 'db': {
+ 'handlers': ['default'],
+ 'level': 'INFO' ,
+ 'propagate': False
+ },
+ 'cherrypy.access': {
+ 'handlers': ['cherrypy_access'],
+ 'level': 'INFO',
+ 'propagate': False
+ },
+ 'cherrypy.error': {
+ 'handlers': ['cherrypy_console', 'cherrypy_error'],
+ 'level': 'INFO',
+ 'propagate': False
+ },
+ }
+ }
+
+ class Root(object):
+ @cherrypy.expose
+ def index(self):
+
+ logger.info("boom")
+ db_logger.info("bam")
+ cherrypy.log("bang")
+
+ return "hello world"
+
+ if __name__ == '__main__':
+ cherrypy.config.update({'log.screen': False,
+ 'log.access_file': '',
+ 'log.error_file': ''})
+ cherrypy.engine.unsubscribe('graceful', cherrypy.log.reopen_files)
+ logging.config.dictConfig(LOG_CONF)
+ cherrypy.quickstart(Root())
+
+
+In this snippet, we create a `configuration dictionary <https://docs.python.org/2/library/logging.config.html#logging.config.dictConfig>`_
+that we pass on to the ``logging`` module to configure
+our loggers:
+
+ * the default root logger is associated to a single stream handler
+ * a logger for the db backend with also a single stream handler
+
+In addition, we re-configure the CherryPy loggers:
+
+ * the top-level ``cherrypy.access`` logger to log requests into a file
+ * the ``cherrypy.error`` logger to log everything else into a file
+ and to the console
+
+We also prevent CherryPy from trying to open its log files when
+the autoreloader kicks in. This is not strictly required since we do not
+even let CherryPy open them in the first place. But, this avoids
+wasting time on something useless.
+
.. _config:
Configuring
###########
-CherryPy comes with a fine-grained configuration mechanism and
+CherryPy comes with a fine-grained configuration mechanism and
settings can be set at various levels.
.. seealso::
Once you have the reviewed the basics, please refer
- to the :ref:`in-depth discussion <configindepth>`
+ to the :ref:`in-depth discussion <configindepth>`
around configuration.
.. _globalsettings:
@@ -240,15 +350,15 @@ settings can be set at various levels.
Global server configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^
-To configure the HTTP and application servers,
-use the :meth:`cherrypy.config.update() <cherrypy._cpconfig.Config.update>`
+To configure the HTTP and application servers,
+use the :meth:`cherrypy.config.update() <cherrypy._cpconfig.Config.update>`
method.
.. code-block:: python
cherrypy.config.update({'server.socket_port': 9090})
-The :mod:`cherrypy.config <cherrypy._cpconfig>` object is a dictionary and the
+The :mod:`cherrypy.config <cherrypy._cpconfig>` object is a dictionary and the
update method merges the passed dictionary into it.
You can also pass a file instead (assuming a `server.conf`
@@ -265,8 +375,8 @@ file):
.. warning::
- :meth:`cherrypy.config.update() <cherrypy._cpconfig.Config.update>`
- is not meant to be used to configure the application.
+ :meth:`cherrypy.config.update() <cherrypy._cpconfig.Config.update>`
+ is not meant to be used to configure the application.
It is a common mistake. It is used to configure the server and engine.
.. _perappconf:
@@ -274,7 +384,7 @@ file):
Per-application configuration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-To configure your application, pass in a dictionary or a file
+To configure your application, pass in a dictionary or a file
when you associate your application to the server.
.. code-block:: python
@@ -291,7 +401,7 @@ or via a file (called `app.conf` for instance):
.. code-block:: python
cherrypy.quickstart(myapp, '/', "app.conf")
-
+
Although, you can define most of your configuration in a global
fashion, it is sometimes convenient to define them
where they are applied in the code.
@@ -324,14 +434,14 @@ You can add settings that are not specific to a request URL
and retrieve them from your page handler as follows:
.. code-block:: ini
-
+
[/]
tools.gzip.on: True
[googleapi]
key = "..."
appid = "..."
-
+
.. code-block:: python
class Root(object):
@@ -402,7 +512,7 @@ Extended example:
Using sessions
##############
-Sessions are one of the most common mechanism used by developers to
+Sessions are one of the most common mechanism used by developers to
identify users and synchronize their activity. By default, CherryPy
does not activate sessions because it is not a mandatory feature
to have, to enable it simply add the following settings in your
@@ -416,7 +526,7 @@ configuration:
.. code-block:: python
cherrypy.quickstart(myapp, '/', "app.conf")
-
+
Sessions are, by default, stored in RAM so, if you restart your server
all of your current sessions will be lost. You can store them in memcached
or on the filesystem instead.
@@ -426,7 +536,7 @@ Using sessions in your applications is done as follows:
.. code-block:: python
import cherrypy
-
+
@cherrypy.expose
def index(self):
if 'count' not in cherrypy.session:
@@ -452,7 +562,7 @@ Filesystem backend
Using a filesystem is a simple to not lose your sessions
between reboots. Each session is saved in its own file within
-the given directory.
+the given directory.
.. code-block:: ini
@@ -464,7 +574,7 @@ the given directory.
Memcached backend
^^^^^^^^^^^^^^^^^
-`Memcached <http://memcached.org/>`_ is a popular key-store on top of your RAM,
+`Memcached <http://memcached.org/>`_ is a popular key-store on top of your RAM,
it is distributed and a good choice if you want to
share sessions outside of the process running CherryPy.
@@ -479,8 +589,8 @@ share sessions outside of the process running CherryPy.
Static content serving
######################
-CherryPy can serve your static content such as images, javascript and
-CSS resources, etc.
+CherryPy can serve your static content such as images, javascript and
+CSS resources, etc.
.. note::
@@ -489,7 +599,7 @@ CSS resources, etc.
is not valid, you can simply set more media-types as follows:
.. code-block:: python
-
+
import mimetypes
mimetypes.types_map['.csv'] = 'text/csv'
@@ -505,7 +615,7 @@ You can serve a single file as follows:
tools.staticfile.on = True
tools.staticfile.filename = "/home/site/style.css"
-CherryPy will automatically respond to URLs such as
+CherryPy will automatically respond to URLs such as
`http://hostname/style.css`.
Serving a whole directory
@@ -519,8 +629,8 @@ Serving a whole directory is similar to a single file:
tools.staticdir.on = True
tools.staticdir.dir = "/home/site/static"
-Assuming you have a file at `static/js/my.js`,
-CherryPy will automatically respond to URLs such as
+Assuming you have a file at `static/js/my.js`,
+CherryPy will automatically respond to URLs such as
`http://hostname/static/js/my.js`.
@@ -528,10 +638,10 @@ CherryPy will automatically respond to URLs such as
CherryPy always requires the absolute path to the files or directories
it will serve. If you have several static sections to configure
- but located in the same root directory, you can use the following
+ but located in the same root directory, you can use the following
shortcut:
-
+
.. code-block:: ini
[/]
@@ -555,8 +665,8 @@ To specify an index file, you can use the following:
tools.staticdir.dir = "/home/site/static"
tools.staticdir.index = "index.html"
-Assuming you have a file at `static/index.html`,
-CherryPy will automatically respond to URLs such as
+Assuming you have a file at `static/index.html`,
+CherryPy will automatically respond to URLs such as
`http://hostname/static/` by returning its contents.
@@ -577,7 +687,7 @@ You could for instance write a page handler as follows:
def download(self, filepath):
return serve_file(filepath, "application/x-download", "attachment")
-Assuming the filepath is a valid path on your machine, the
+Assuming the filepath is a valid path on your machine, the
response would be considered as a downloadable content by
the browser.
@@ -649,7 +759,7 @@ SSL or within a closed network.
USERS = {'jon': 'secret'}
- def validate_password(username, password):
+ def validate_password(realm, username, password):
if username in USERS and USERS[username] == password:
return True
return False
@@ -659,13 +769,13 @@ SSL or within a closed network.
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'localhost',
'tools.auth_basic.checkpassword': validate_password
- }
+ }
}
cherrypy.quickstart(myapp, '/', conf)
Simply put, you have to provide a function that will
-be called by CherryPy passing the username and password
+be called by CherryPy passing the username and password
decoded from the request.
The function can read its data from any source it has to: a file,
@@ -679,7 +789,7 @@ Digest authentication differs by the fact the credentials
are not carried on by the request so it's a little more secure
than basic.
-CherryPy's digest support has a similar interface to the
+CherryPy's digest support has a similar interface to the
basic one explained above.
.. code-block:: python
@@ -702,7 +812,7 @@ basic one explained above.
Favicon
#######
-CherryPy serves its own sweet red cherrypy as the default
+CherryPy serves its own sweet red cherrypy as the default
`favicon <http://en.wikipedia.org/wiki/Favicon>`_ using the static file
tool. You can serve your own favicon as follows:
diff --git a/docs/conf.py b/docs/conf.py
index 2c1ceb63..77e53587 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -48,7 +48,7 @@ master_doc = 'index'
# General information about the project.
project = u'CherryPy'
-copyright = u'2014, CherryPy Team'
+copyright = u'2001-2015, CherryPy Team'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
diff --git a/docs/install.rst b/docs/install.rst
index 68dd9692..dec9c007 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -6,7 +6,7 @@ CherryPy is a pure Python library. This has various consequences:
- It can run anywhere Python runs
- It does not require a C compiler
- - It can run on various implementations of the Python language: `CPython <http://python.org/>`_,
+ - It can run on various implementations of the Python language: `CPython <http://python.org/>`_,
`IronPython <http://ironpython.net/>`_, `Jython <http://www.jython.org/>`_ and `PyPy <http://pypy.org/>`_
.. contents::
@@ -45,11 +45,11 @@ CherryPy can be easily installed via common Python package managers such as setu
$ pip install cherrypy
-You may also get the latest CherryPy version by grabbing the source code from BitBucket:
+You may also get the latest CherryPy version by grabbing the source code from Github:
.. code-block:: bash
- $ hg clone https://bitbucket.org/cherrypy/cherrypy
+ $ git clone https://github.com/cherrypy/cherrypy
$ cd cherrypy
$ python setup.py install
diff --git a/docs/tutorials.rst b/docs/tutorials.rst
index ddc2c47b..e50fa1dd 100644
--- a/docs/tutorials.rst
+++ b/docs/tutorials.rst
@@ -5,7 +5,7 @@ Tutorials
This tutorial will walk you through basic but complete CherryPy applications
-that will show you common concepts as well as slightly more adavanced ones.
+that will show you common concepts as well as slightly more advanced ones.
.. contents::
:depth: 4
@@ -130,7 +130,7 @@ Tutorial 3: My URLs have parameters
###################################
In the previous tutorial, we have seen how to create an application
-that could generate a random string. Let's not assume you wish
+that could generate a random string. Let's now assume you wish
to indicate the length of that string dynamically.
.. code-block:: python
@@ -162,7 +162,7 @@ Save this into a file named `tut03.py` and run it as follows:
Go now to http://localhost:8080/generate?length=16 and your browser
will display a generated string of length 16. Notice how
we benefit from Python's default arguments' values to support
-URLs such as http://localhost:8080/password still.
+URLs such as http://localhost:8080/generate still.
In a URL such as this one, the section after `?` is called a
query-string. Traditionally, the query-string is used to
@@ -183,7 +183,7 @@ Tutorial 4: Submit this form
############################
CherryPy is a web framework upon which you build web applications.
-The most traditionnal shape taken by applications is through
+The most traditional shape taken by applications is through
an HTML user-interface speaking to your CherryPy server.
Let's see how to handle HTML forms via the following
@@ -306,7 +306,7 @@ sessions in the process's memory. It supports more persistent
Tutorial 6: What about my javascripts, CSS and images?
######################################################
-Web application are usually also made of static content such
+Web applications are usually also made of static content such
as javascript, CSS files or images. CherryPy provides support
to serve static content to end-users.
@@ -386,7 +386,7 @@ directory structure. Most of the time, this is what you'll end
up doing so this is what the code above demonstrates. First, we
indicate the `root` directory of all of our static content. This
must be an absolute path for security reason. CherryPy will
-complain if you provide only non-absolute paths when looking for a
+complain if you provide only relative paths when looking for a
match to your URLs.
Then we indicate that all URLs which path segment starts with `/static`
@@ -485,7 +485,7 @@ Then we force the responses `content-type` to be `text/plain` and
we finally ensure that `GET` requests will only be responded to clients
that accept that `content-type` by having a `Accept: text/plain`
header set in their request. However, we do this only for that
-HTTP method as it wouldn't have much meaning on the oher methods.
+HTTP method as it wouldn't have much meaning on the other methods.
For the purpose of this tutorial, we will be using a Python client
@@ -534,7 +534,7 @@ on the latter case, that it doesn't exist after we've deleted it.
Lines 12-14 show you how the application reacted when our client requested
the generated string as a JSON format. Since we configured the
web API to only support plain text, it returns the appropriate
-`HTTP error code http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7`
+`HTTP error code <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.7>`_.
.. note::
@@ -544,6 +544,21 @@ web API to only support plain text, it returns the appropriate
session id stored in the request cookie in each subsequent
request. That is handy.
+.. important::
+
+ It's all about RESTful URLs these days, isn't it?
+
+ It is likely your URL will be made of dynamic parts that you
+ will not be able to match to page handlers. For example,
+ ``/library/12/book/15`` cannot be directly handled by the
+ default CherryPy dispatcher since the segments ``12`` and
+ ``15`` will not be matched to any Python callable.
+
+ This can be easily workaround with two handy CherryPy features
+ explained in the :ref:`advanced section <restful>`.
+
+
+
.. _tut08:
@@ -711,6 +726,7 @@ Notice as well how your frontend converses with the backend using
a straightfoward, yet clean, web service API. That same API
could easily be used by non-HTML clients.
+.. _tut09:
Tutorial 9: Data is all my life
###############################
@@ -722,7 +738,7 @@ this is not the right way of keeping your data on the long run.
Sessions are there to identify your user and carry as little
amount of data as necessary for the operation carried by the user.
-To store, persist and query data your need a proper database server.
+To store, persist and query data you need a proper database server.
There exist many to choose from with various paradigm support:
- relational: PostgreSQL, SQLite, MariaDB, Firebird
@@ -750,6 +766,7 @@ So let's simply focus on the application code itself:
import random
import sqlite3
import string
+ import time
import cherrypy
@@ -766,23 +783,27 @@ So let's simply focus on the application code itself:
@cherrypy.tools.accept(media='text/plain')
def GET(self):
with sqlite3.connect(DB_STRING) as c:
- c.execute("SELECT value FROM user_string WHERE session_id=?",
+ cherrypy.session['ts'] = time.time()
+ r = c.execute("SELECT value FROM user_string WHERE session_id=?",
[cherrypy.session.id])
- return c.fetchone()
+ return r.fetchone()
def POST(self, length=8):
some_string = ''.join(random.sample(string.hexdigits, int(length)))
with sqlite3.connect(DB_STRING) as c:
+ cherrypy.session['ts'] = time.time()
c.execute("INSERT INTO user_string VALUES (?, ?)",
[cherrypy.session.id, some_string])
return some_string
def PUT(self, another_string):
with sqlite3.connect(DB_STRING) as c:
+ cherrypy.session['ts'] = time.time()
c.execute("UPDATE user_string SET value=? WHERE session_id=?",
[another_string, cherrypy.session.id])
def DELETE(self):
+ cherrypy.session.pop('ts', None)
with sqlite3.connect(DB_STRING) as c:
c.execute("DELETE FROM user_string WHERE session_id=?",
[cherrypy.session.id])
@@ -847,6 +868,14 @@ A better idea would be to associate the user's login or
more resilient unique identifier. For the sake of our
demo, this should do.
+.. important::
+
+ In this example, we must still set the session to a dummy value
+ so that the session is not `discarded <https://cherrypy.readthedocs.org/en/latest/pkg/cherrypy.lib.html?highlight=fixation#session-fixation-protection>`_
+ on each request by CherryPy. Since we now use the database
+ to store the generated string, we simply store a dummy
+ timestamp inside the session.
+
.. note::
Unfortunately, sqlite in Python forbids us
@@ -859,8 +888,228 @@ demo, this should do.
`SQLAlchemy <http://sqlalchemy.readthedocs.org>`_, to better
support your application's needs.
+.. _tut10:
+
+Tutorial 10: Make it a modern single-page application with React.js
+###################################################################
+
+In the recent years, client-side single-page applications (SPA) have
+gradually eaten server-side generated content web applications's lunch.
+
+This tutorial demonstrates how to integrate with
+`React.js <https://facebook.github.io/react/>`_, a Javascript library
+for SPA released by Facebook in 2013. Please refer to React.js
+documentation to learn more about it.
+
+To demonstrate it, let's use the code from :ref:`tutorial 09 <tut09>`.
+However, we will be replacing the HTML and Javascript code.
+
+First, let's see how our HTML code has changed:
+
+.. code-block:: html
+ :linenos:
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <link href="/static/css/style.css" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.js"></script>
+ <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
+ </head>
+ <body>
+ <div id="generator"></div>
+ <script type="text/javascript" src="static/js/gen.js"></script>
+ </body>
+ </html>
+
+Basically, we have remove the entire Javascript code that was using jQuery.
+Instead, we load the React.js library as well as a new, local,
+Javascript module, named ``gen.js`` and located in the ``public/js``
+directory:
+
+.. code-block:: javascript
+ :linenos:
+
+ var StringGeneratorBox = React.createClass({
+ handleGenerate: function() {
+ var length = this.state.length;
+ this.setState(function() {
+ $.ajax({
+ url: this.props.url,
+ dataType: 'text',
+ type: 'POST',
+ data: {
+ "length": length
+ },
+ success: function(data) {
+ this.setState({
+ length: length,
+ string: data,
+ mode: "edit"
+ });
+ }.bind(this),
+ error: function(xhr, status, err) {
+ console.error(this.props.url,
+ status, err.toString()
+ );
+ }.bind(this)
+ });
+ });
+ },
+ handleEdit: function() {
+ var new_string = this.state.string;
+ this.setState(function() {
+ $.ajax({
+ url: this.props.url,
+ type: 'PUT',
+ data: {
+ "another_string": new_string
+ },
+ success: function() {
+ this.setState({
+ length: new_string.length,
+ string: new_string,
+ mode: "edit"
+ });
+ }.bind(this),
+ error: function(xhr, status, err) {
+ console.error(this.props.url,
+ status, err.toString()
+ );
+ }.bind(this)
+ });
+ });
+ },
+ handleDelete: function() {
+ this.setState(function() {
+ $.ajax({
+ url: this.props.url,
+ type: 'DELETE',
+ success: function() {
+ this.setState({
+ length: "8",
+ string: "",
+ mode: "create"
+ });
+ }.bind(this),
+ error: function(xhr, status, err) {
+ console.error(this.props.url,
+ status, err.toString()
+ );
+ }.bind(this)
+ });
+ });
+ },
+ handleLengthChange: function(length) {
+ this.setState({
+ length: length,
+ string: "",
+ mode: "create"
+ });
+ },
+ handleStringChange: function(new_string) {
+ this.setState({
+ length: new_string.length,
+ string: new_string,
+ mode: "edit"
+ });
+ },
+ getInitialState: function() {
+ return {
+ length: "8",
+ string: "",
+ mode: "create"
+ };
+ },
+ render: function() {
+ return (
+ <div className="stringGenBox">
+ <StringGeneratorForm onCreateString={this.handleGenerate}
+ onReplaceString={this.handleEdit}
+ onDeleteString={this.handleDelete}
+ onLengthChange={this.handleLengthChange}
+ onStringChange={this.handleStringChange}
+ mode={this.state.mode}
+ length={this.state.length}
+ string={this.state.string}/>
+ </div>
+ );
+ }
+ });
+
+ var StringGeneratorForm = React.createClass({
+ handleCreate: function(e) {
+ e.preventDefault();
+ this.props.onCreateString();
+ },
+ handleReplace: function(e) {
+ e.preventDefault();
+ this.props.onReplaceString();
+ },
+ handleDelete: function(e) {
+ e.preventDefault();
+ this.props.onDeleteString();
+ },
+ handleLengthChange: function(e) {
+ e.preventDefault();
+ var length = React.findDOMNode(this.refs.length).value.trim();
+ this.props.onLengthChange(length);
+ },
+ handleStringChange: function(e) {
+ e.preventDefault();
+ var string = React.findDOMNode(this.refs.string).value.trim();
+ this.props.onStringChange(string);
+ },
+ render: function() {
+ if (this.props.mode == "create") {
+ return (
+ <div>
+ <input type="text" ref="length" defaultValue="8" value={this.props.length} onChange={this.handleLengthChange} />
+ <button onClick={this.handleCreate}>Give it now!</button>
+ </div>
+ );
+ } else if (this.props.mode == "edit") {
+ return (
+ <div>
+ <input type="text" ref="string" value={this.props.string} onChange={this.handleStringChange} />
+ <button onClick={this.handleReplace}>Replace</button>
+ <button onClick={this.handleDelete}>Delete it</button>
+ </div>
+ );
+ }
+
+ return null;
+ }
+ });
+
+ React.render(
+ <StringGeneratorBox url="/generator" />,
+ document.getElementById('generator')
+ );
+
+
+Wow! What a lot of code for something so simple, isn't it?
+The entry point is the last few lines where we indicate that we
+want to render the HTML code of the ``StringGeneratorBox`` React.js
+class inside the ``generator`` div.
+
+When the page is rendered, so is that component. Notice how it
+is also made of another component that renders the form itself.
+
+This might be a little over the top for such a simple example
+but hopefully will get you started with React.js in the process.
+
+There is not much to say and, hopefully, the meaning of that code
+is rather clear. The component has an internal `state <https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html>`_
+in which we store the current string as generated/modified by the user.
+
+When the user `changes the content of the input boxes <https://facebook.github.io/react/docs/forms.html>`_,
+the state is updated on the client side. Then, when a button is clicked,
+that state is sent out to the backend server using the API endpoint
+and the appropriate action takes places. Then, the state is updated and so is the view.
+
-Tutorial 10: Organize my code
+Tutorial 11: Organize my code
#############################
CherryPy comes with a powerful architecture
@@ -882,7 +1131,7 @@ In order to understand them, let's imagine you are at a superstore:
to make sure sections are always in order (this is your backend)
In spite of being really simplistic, this is not far from how your
-application behaves. CherryPy helps your structure your application
+application behaves. CherryPy helps you structure your application
in a way that mirrors these high-level ideas.
Dispatchers
diff --git a/release.py b/release.py
deleted file mode 100644
index 26bad57e..00000000
--- a/release.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""
-Use jaraco.packaging with this script to cut a release. After installing
-jaraco.packaging, invoke:
-
-python -m jaraco.packaging.release
-"""
-
-from __future__ import print_function
-
-import sys
-import os
-import shutil
-import importlib
-import textwrap
-
-files_with_versions = (
- 'setup.py',
- 'cherrypy/__init__.py',
- 'cherrypy/wsgiserver/wsgiserver2.py',
- 'cherrypy/wsgiserver/wsgiserver3.py',
-)
-
-
-def check_wheel():
- """
- Ensure 'wheel' is installed (required for bdist_wheel).
- """
- try:
- importlib.import_module('wheel')
- except ImportError:
- print("CherryPy requires 'wheel' be installed to produce wheels.",
- file=sys.stderr)
- raise SystemExit(5)
-
-
-def before_upload():
- check_wheel()
- remove_files()
-
-test_info = textwrap.dedent("""
- Run tests with `nosetests -s ./` on Windows, Linux, and Mac on at least
- Python 2.4, 2.5, 2.7, and 3.2.
- """).lstrip()
-
-dist_commands = 'sdist', 'bdist_wininst', 'bdist_wheel'
-
-
-def remove_files():
- if os.path.isfile('MANIFEST'):
- os.remove('MANIFEST')
- if os.path.isdir('dist'):
- shutil.rmtree('dist')
-
-
-def announce():
- print('Distributions have been uploaded.')
- print('Please ask in IRC for others to help you test this release.')
- print("Please confirm that the distro installs properly "
- "with `easy_install CherryPy=={version}`.".format(**globals()))
- print("Please change the Wiki: Home page (news), CherryPyDownload")
- print("Please announce the release on newsgroups, mailing lists, "
- "and IRC /topic.")
diff --git a/setup.cfg b/setup.cfg
index cd62b8dd..13afd86f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,8 +1,24 @@
+[bumpversion]
+current_version = 5.2.0
+commit = True
+tag = True
+
+[aliases]
+clean_egg_info = egg_info -RDb ''
+release = clean_egg_info sdist bdist_wheel
+
[sdist]
-formats=gztar,zip
+formats = gztar,zip
[nosetests]
-where=cherrypy
-logging-filter=cherrypy
-verbosity=2
-nocapture=True
+where = cherrypy
+logging-filter = cherrypy
+verbosity = 2
+nocapture = True
+
+[egg_info]
+tag_build = .post
+tag_date = 1
+
+[bumpversion:file:setup.py]
+
diff --git a/setup.py b/setup.py
index 0a08ca43..d4267e09 100644
--- a/setup.py
+++ b/setup.py
@@ -36,7 +36,7 @@ class cherrypy_build_py(build_py):
# arguments for the setup command
###############################################################################
name = "CherryPy"
-version = "3.6.1"
+version = "5.2.0"
desc = "Object-Oriented HTTP framework"
long_desc = "CherryPy is a pythonic, object-oriented HTTP framework"
classifiers = [
@@ -49,13 +49,14 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.3",
- "Programming Language :: Python :: 2.4",
- "Programming Language :: Python :: 2.5",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.1",
+ "Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: Implementation",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: Jython",
@@ -111,9 +112,9 @@ cmd_class = dict(
)
if sys.version_info >= (3, 0):
- required_python_version = '3.0'
+ required_python_version = '3.1'
else:
- required_python_version = '2.3'
+ required_python_version = '2.6'
###############################################################################
# end arguments for setup
diff --git a/tox.ini b/tox.ini
index e68f3e2c..2baa1fbd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,9 @@
[tox]
-envlist = py26, py27, py32, py33, pypy
+envlist = py26, py27, py32, py33, py34, py35, pypy
[testenv]
deps = nose
+ mock
Routes
commands =
nosetests -s []