cherrypy update

fixes #3348
This commit is contained in:
AdeHub
2024-12-07 19:56:21 +13:00
parent 94d62430a0
commit a09e91ff8a
28 changed files with 512 additions and 108 deletions

View File

@@ -22,8 +22,9 @@ import http.client
def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given
encoding.
"""Convert a native :class:`str` to a :class:`bytes` instance.
The encoding can be changed to non-ASCII optionally.
"""
assert_native(n)
# In Python 3, the native string type is unicode
@@ -31,8 +32,9 @@ def ntob(n, encoding='ISO-8859-1'):
def ntou(n, encoding='ISO-8859-1'):
"""Return the given native string as a unicode string with the given
encoding.
"""Convert a native :class:`str` to a :class:`str` instance.
This doesn't actually do anything.
"""
assert_native(n)
# In Python 3, the native string type is unicode
@@ -48,6 +50,7 @@ def tonative(n, encoding='ISO-8859-1'):
def assert_native(n):
"""Ensure that input is a native :class:`str`."""
if not isinstance(n, str):
raise TypeError('n must be a native str (got %s)' % type(n).__name__)

View File

@@ -25,6 +25,7 @@ class PageHandler(object):
"""Callable which sets response.body."""
def __init__(self, callable, *args, **kwargs):
"""Initialize the page handler."""
self.callable = callable
self.args = args
self.kwargs = kwargs
@@ -36,6 +37,7 @@ class PageHandler(object):
@args.setter
def args(self, args):
"""Set the request arguments in order."""
cherrypy.serving.request.args = args
return cherrypy.serving.request.args
@@ -46,10 +48,12 @@ class PageHandler(object):
@kwargs.setter
def kwargs(self, kwargs):
"""Set the named request keyword arguments as :class:`dict`."""
cherrypy.serving.request.kwargs = kwargs
return cherrypy.serving.request.kwargs
def __call__(self):
"""Invoke an HTTP handler callable for :class:`PageHandler`."""
try:
return self.callable(*self.args, **self.kwargs)
except TypeError:
@@ -203,15 +207,18 @@ try:
import inspect
except ImportError:
def test_callable_spec(callable, args, kwargs): # noqa: F811
"""Do nothing as a no-op."""
return None
else:
def getargspec(callable):
"""Get argument specification using :mod:`inspect`."""
return inspect.getfullargspec(callable)[:4]
class LateParamPageHandler(PageHandler):
"""Page handler callable with delayed request parameters binding.
"""When passing cherrypy.request.params to the page handler, we do not
When passing ``cherrypy.request.params`` to the page handler, we do not
want to capture that dict too early; we want to give tools like the
decoding tool a chance to modify the params dict in-between the lookup
of the handler and the actual calling of the handler. This subclass
@@ -221,7 +228,11 @@ class LateParamPageHandler(PageHandler):
@property
def kwargs(self):
"""Page handler kwargs (with cherrypy.request.params copied in)."""
"""Page handler keyword arguments.
The returned value contains data merged in
from ``cherrypy.request.params``.
"""
kwargs = cherrypy.serving.request.params.copy()
if self._kwargs:
kwargs.update(self._kwargs)
@@ -229,6 +240,7 @@ class LateParamPageHandler(PageHandler):
@kwargs.setter
def kwargs(self, kwargs):
"""Set the named request keyword arguments as :class:`dict`."""
cherrypy.serving.request.kwargs = kwargs
self._kwargs = kwargs
@@ -238,6 +250,7 @@ if sys.version_info < (3, 0):
string.punctuation, '_' * len(string.punctuation))
def validate_translator(t):
"""Ensure the translator is of the correct length and size."""
if not isinstance(t, str) or len(t) != 256:
raise ValueError(
'The translate argument must be a str of len 256.')
@@ -246,6 +259,7 @@ else:
string.punctuation, '_' * len(string.punctuation))
def validate_translator(t):
"""Ensure the translator is of the correct length and size."""
if not isinstance(t, dict):
raise ValueError('The translate argument must be a dict.')
@@ -273,6 +287,7 @@ class Dispatcher(object):
def __init__(self, dispatch_method_name=None,
translate=punctuation_to_underscores):
"""Initialize the HTTP request dispatcher."""
validate_translator(translate)
self.translate = translate
if dispatch_method_name:
@@ -389,7 +404,9 @@ class Dispatcher(object):
object_trail.append([name, node, nodeconf, segleft])
def set_conf():
"""Collapse all object_trail config into cherrypy.request.config.
"""Collapse all ``object_trail`` conf into config.
The config being ``cherrypy.request.config``.
"""
base = cherrypy.config.copy()
# Note that we merge the config from each node
@@ -505,10 +522,12 @@ class RoutesDispatcher(object):
self.mapper.controller_scan = self.controllers.keys
def connect(self, name, route, controller, **kwargs):
"""Mount an HTTP handler into the router."""
self.controllers[name] = controller
self.mapper.connect(name, route, controller=name, **kwargs)
def redirect(self, url):
"""Perform an HTTP redirect to the given URL."""
raise cherrypy.HTTPRedirect(url)
def __call__(self, path_info):
@@ -602,6 +621,7 @@ class RoutesDispatcher(object):
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
"""Chain an HTTP dispatcher variant implementing XML-RPC."""
from cherrypy.lib import xmlrpcutil
def xmlrpc_dispatch(path_info):

View File

@@ -137,7 +137,6 @@ from cherrypy.lib import httputil as _httputil
class CherryPyException(Exception):
"""A base class for CherryPy exceptions."""
pass
class InternalRedirect(CherryPyException):
@@ -150,6 +149,7 @@ class InternalRedirect(CherryPyException):
"""
def __init__(self, path, query_string=''):
"""Initialize the internal redirect exception."""
self.request = cherrypy.serving.request
self.query_string = query_string
@@ -202,6 +202,7 @@ class HTTPRedirect(CherryPyException):
"""The encoding when passed urls are not native strings."""
def __init__(self, urls, status=None, encoding=None):
"""Initialize the HTTP redirect exception."""
self.urls = abs_urls = [
# Note that urljoin will "do the right thing" whether url is:
# 1. a complete URL with host (e.g. "http://www.example.com/test")
@@ -227,7 +228,9 @@ class HTTPRedirect(CherryPyException):
@classproperty
def default_status(cls):
"""The default redirect status for the request.
"""Redirect status for the request.
This is the default handler.
RFC 2616 indicates a 301 response code fits our goal; however,
browser support for 301 is quite messy. Use 302/303 instead. See
@@ -242,8 +245,9 @@ class HTTPRedirect(CherryPyException):
return status
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent
self.
"""Modify ``cherrypy.response`` to represent ``self``.
Modifies status, headers, and body.
CherryPy uses this internally, but you can also use it to create
an HTTPRedirect object and set its output without *raising* the
@@ -366,6 +370,7 @@ class HTTPError(CherryPyException):
"""The HTTP Reason-Phrase string."""
def __init__(self, status=500, message=None):
"""Initialize an HTTP error."""
self.status = status
try:
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
@@ -381,8 +386,9 @@ class HTTPError(CherryPyException):
CherryPyException.__init__(self, status, message)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent
self.
"""Modify ``cherrypy.response`` to represent ``self``.
Modifies status, headers, and body.
CherryPy uses this internally, but you can also use it to create
an HTTPError object and set its output without *raising* the
@@ -408,6 +414,7 @@ class HTTPError(CherryPyException):
_be_ie_unfriendly(self.code)
def get_error_page(self, *args, **kwargs):
"""Compose an HTML page with error information."""
return get_error_page(*args, **kwargs)
def __call__(self):
@@ -432,6 +439,7 @@ class NotFound(HTTPError):
"""
def __init__(self, path=None):
"""Initialize an HTTP Not Found error."""
if path is None:
request = cherrypy.serving.request
path = request.script_name + request.path_info
@@ -600,7 +608,6 @@ def bare_error(extrabody=None):
is set in the body. If extrabody is a string, it will be appended
as-is to the body.
"""
# The whole point of this function is to be a last line-of-defense
# in handling errors. That is, it must not raise any errors itself;
# it cannot be allowed to fail. Therefore, don't add to it!

View File

@@ -1,4 +1,6 @@
"""
CherryPy logging module.
Simple config
=============
@@ -126,12 +128,15 @@ class NullHandler(logging.Handler):
"""A no-op logging handler to silence the logging.lastResort handler."""
def handle(self, record):
"""Handle a log record doing no-op."""
pass
def emit(self, record):
"""Emit a log record doing no-op."""
pass
def createLock(self):
"""Lock log write with no-op."""
self.lock = None
@@ -167,6 +172,7 @@ class LogManager(object):
"""
def __init__(self, appid=None, logger_root='cherrypy'):
"""Initialize a CherryPy log manager."""
self.logger_root = logger_root
self.appid = appid
if appid is None:
@@ -217,11 +223,11 @@ class LogManager(object):
)
def __call__(self, *args, **kwargs):
"""An alias for ``error``."""
"""Record an error log entry."""
return self.error(*args, **kwargs)
def access(self):
"""Write to the access log (in Apache/NCSA Combined Log format).
r"""Write to the access log (in Apache/NCSA Combined Log format).
See the
`apache documentation
@@ -414,7 +420,10 @@ class LogManager(object):
class WSGIErrorHandler(logging.Handler):
"A handler class which writes logging records to environ['wsgi.errors']."
"""A handler class writing logs to WSGI env.
Specifically, the target is ``environ['wsgi.errors']``.
"""
def flush(self):
"""Flushes the stream."""
@@ -450,6 +459,8 @@ class WSGIErrorHandler(logging.Handler):
class LazyRfc3339UtcTime(object):
"""A postponed timestamp string retrieval class."""
def __str__(self):
"""Return datetime in RFC3339 UTC Format."""
iso_formatted_now = datetime.datetime.now(

View File

@@ -72,6 +72,7 @@ from cherrypy.lib import httputil
def setup(req):
"""Execute pre-initialization functions."""
from mod_python import apache
# Run any setup functions defined by a "PythonOption cherrypy.setup"
@@ -140,6 +141,7 @@ _isSetUp = False
def handler(req):
"""Invoke the HTTP handler."""
from mod_python import apache
try:
global _isSetUp
@@ -251,6 +253,7 @@ def handler(req):
def send_response(req, status, headers, body, stream=False):
"""Send the HTTP response to the client."""
# Set response status
req.status = int(status[:3])
@@ -276,17 +279,20 @@ try:
import subprocess
def popen(fullcmd):
"""Invoke a subprocess via :mod:`subprocess`."""
p = subprocess.Popen(fullcmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
close_fds=True)
return p.stdout
except ImportError:
def popen(fullcmd):
"""Invoke a subprocess via :mod:`os`."""
pipein, pipeout = os.popen4(fullcmd)
return pipeout
def read_process(cmd, args=''):
"""Return a subprocess standard output."""
fullcmd = '%s %s' % (cmd, args)
pipeout = popen(fullcmd)
try:
@@ -305,6 +311,7 @@ def read_process(cmd, args=''):
class ModPythonServer(object):
"""A server wrapper for ``mod_python``."""
template = """
# Apache2 server configuration file for running CherryPy with mod_python.
@@ -323,6 +330,7 @@ LoadModule python_module modules/mod_python.so
def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
handler='cherrypy._cpmodpy::handler'):
"""Initialize a ``mod_python`` server."""
self.loc = loc
self.port = port
self.opts = opts
@@ -330,6 +338,7 @@ LoadModule python_module modules/mod_python.so
self.handler = handler
def start(self):
"""Start an Apache2/httpd server."""
opts = ''.join([' PythonOption %s %s\n' % (k, v)
for k, v in self.opts])
conf_data = self.template % {'port': self.port,
@@ -347,5 +356,6 @@ LoadModule python_module modules/mod_python.so
return response
def stop(self):
"""Stop an Apache2/httpd server."""
os.popen('apache -k stop')
self.ready = False

View File

@@ -220,7 +220,9 @@ def process_multipart(entity):
def process_multipart_form_data(entity):
"""Read all multipart/form-data parts into entity.parts or entity.params.
"""Read ``multipart/form-data`` parts.
This function saves them into ``entity.parts`` or ``entity.params``.
"""
process_multipart(entity)
@@ -248,7 +250,7 @@ def process_multipart_form_data(entity):
def _old_process_multipart(entity):
"""The behavior of 3.2 and lower.
"""Behavior of 3.2 and lower.
Deprecated and will be changed in 3.3.
"""
@@ -411,6 +413,7 @@ class Entity(object):
"""
def __init__(self, fp, headers, params=None, parts=None):
"""Initialize an HTTP entity."""
# Make an instance-specific copy of the class processors
# so Tools, etc. can replace them per-request.
self.processors = self.processors.copy()
@@ -479,24 +482,30 @@ class Entity(object):
self.filename = unquote(str(filename), encoding)
def read(self, size=None, fp_out=None):
"""Read bytes from the connection."""
return self.fp.read(size, fp_out)
def readline(self, size=None):
"""Read a line of bytes from the connection."""
return self.fp.readline(size)
def readlines(self, sizehint=None):
"""Read some byte lines from the connection."""
return self.fp.readlines(sizehint)
def __iter__(self):
"""Set up the iterator."""
return self
def __next__(self):
"""Return the next line of bytes."""
line = self.readline()
if not line:
raise StopIteration
return line
def next(self):
"""Return the next line of bytes.""" # FIXME: python 2?
return self.__next__()
def read_into_file(self, fp_out=None):
@@ -564,7 +573,9 @@ class Entity(object):
proc(self)
def default_proc(self):
"""Called if a more-specific processor is not found for the
"""Process unknown data as a fallback.
Called if a more-specific processor is not found for the
``Content-Type``.
"""
# Leave the fp alone for someone else to read. This works fine
@@ -614,6 +625,7 @@ class Part(Entity):
"""
def __init__(self, fp, headers, boundary):
"""Initialize an entity part."""
Entity.__init__(self, fp, headers)
self.boundary = boundary
self.file = None
@@ -621,11 +633,13 @@ class Part(Entity):
@classmethod
def from_fp(cls, fp, boundary):
"""Initialize an entity part from a file handle."""
headers = cls.read_headers(fp)
return cls(fp, headers, boundary)
@classmethod
def read_headers(cls, fp):
"""Read HTTP headers from a file handle."""
headers = httputil.HeaderMap()
while True:
line = fp.readline()
@@ -713,7 +727,9 @@ class Part(Entity):
return fp_out
def default_proc(self):
"""Called if a more-specific processor is not found for the
"""Process unknown data as a fallback.
Called if a more-specific processor is not found for the
``Content-Type``.
"""
if self.filename:
@@ -743,9 +759,11 @@ inf = float('inf')
class SizedReader:
"""A buffered/sized reader."""
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
has_trailers=False):
"""Initialize buffered file handle reader."""
# Wrap our fp in a buffer so peek() works
self.fp = fp
self.length = length
@@ -773,7 +791,6 @@ class SizedReader:
object that supports the 'write' method; all bytes read will be
written to the fp, and None is returned.
"""
if self.length is None:
if size is None:
remaining = inf
@@ -889,6 +906,7 @@ class SizedReader:
return lines
def finish(self):
"""Finalize reading the HTTP trailer headers."""
self.done = True
if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
self.trailers = {}
@@ -946,6 +964,7 @@ class RequestBody(Entity):
"""
def __init__(self, fp, headers, params=None, request_params=None):
"""Initialize a request body entity."""
Entity.__init__(self, fp, headers, params)
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1

View File

@@ -42,6 +42,7 @@ class Hook(object):
callable on each call."""
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
"""Initialize the hook instance."""
self.callback = callback
if failsafe is None:
@@ -55,7 +56,14 @@ class Hook(object):
self.kwargs = kwargs
def __lt__(self, other):
"""
"""Check if this hook's priority is lower than the other's.
:param other: Another object to compare priority with.
:type other: Hook
:returns: Whether the other Hook's priority is higher than this one's.
:rtype: bool
Hooks sort by priority, ascending, such that
hooks of lower priority are run first.
"""
@@ -66,6 +74,7 @@ class Hook(object):
return self.callback(**self.kwargs)
def __repr__(self):
"""Render a string representation of :class:`Hook` instance."""
cls = self.__class__
return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
% (cls.__module__, cls.__name__, self.callback,
@@ -78,12 +87,14 @@ class HookMap(dict):
"""A map of call points to lists of callbacks (Hook objects)."""
def __new__(cls, points=None):
"""Construct a fresh hook map instance."""
d = dict.__new__(cls)
for p in points or []:
d[p] = []
return d
def __init__(self, *a, **kw):
"""Initialize a hook map instance post-construction."""
pass
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
@@ -124,6 +135,7 @@ class HookMap(dict):
raise
def __copy__(self):
"""Duplicate object per the copy protocol."""
newmap = self.__class__()
# We can't just use 'update' because we want copies of the
# mutable values (each is a list) as well.
@@ -133,6 +145,7 @@ class HookMap(dict):
copy = __copy__
def __repr__(self):
"""Render a string representation of :class:`HookMap`."""
cls = self.__class__
return '%s.%s(points=%r)' % (
cls.__module__,
@@ -541,7 +554,9 @@ class Request(object):
self.stage = 'close'
def run(self, method, path, query_string, req_protocol, headers, rfile):
r"""Process the Request. (Core)
r"""Process the request.
*(core)*
method, path, query_string, and req_protocol should be pulled directly
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
@@ -815,6 +830,7 @@ class ResponseBody(object):
'if you wish to return unicode.')
def __get__(self, obj, objclass=None):
"""Return a response body through the descriptor protocol."""
if obj is None:
# When calling on the class instead of an instance...
return self
@@ -822,7 +838,10 @@ class ResponseBody(object):
return obj._body
def __set__(self, obj, value):
# Convert the given value to an iterable object.
"""Set a response body through the descriptor protocol.
Convert the given value to an iterable object.
"""
if isinstance(value, str):
raise ValueError(self.unicode_err)
elif isinstance(value, list):
@@ -874,6 +893,7 @@ class Response(object):
"""If False, buffer the response body."""
def __init__(self):
"""Intialize the HTTP response instance."""
self.status = None
self.header_list = None
self._body = []
@@ -896,8 +916,14 @@ class Response(object):
return new_body
def _flush_body(self):
"""Discard self.body but consume any generator such that any
finalization can occur, such as is required by caching.tee_output()."""
"""Exhaust the body iterator.
:rtype: None
Discard ``self.body`` but consume any generator such that any
finalization can occur, such as is required by
``caching.tee_output()``.
"""
consume(iter(self.body))
def finalize(self):
@@ -951,6 +977,8 @@ class Response(object):
class LazyUUID4(object):
"""A delayed UUID4 string maker."""
def __str__(self):
"""Return UUID4 and keep it for future calls."""
return str(self.uuid4)

View File

@@ -57,6 +57,7 @@ class Tool(object):
namespace = 'tools'
def __init__(self, point, callable, name=None, priority=50):
"""Initialize a CherryPy tool instance."""
self._point = point
self.callable = callable
self._name = name
@@ -66,10 +67,12 @@ class Tool(object):
@property
def on(self):
"""Flag whether the tool is enabled."""
raise AttributeError(_attr_error)
@on.setter
def on(self, value):
"""Set a flag for whether the tool is enabled."""
raise AttributeError(_attr_error)
def _setargs(self):
@@ -133,7 +136,7 @@ class Tool(object):
return tool_decorator
def _setup(self):
"""Hook this tool into cherrypy.request.
"""Wire this tool into ``cherrypy.request``.
The standard CherryPy request object will automatically call
this method when the tool is "turned on" in config.
@@ -159,6 +162,7 @@ class HandlerTool(Tool):
"""
def __init__(self, callable, name=None):
"""Initialize a handler tool."""
Tool.__init__(self, 'before_handler', callable, name)
def handler(self, *args, **kwargs):
@@ -183,7 +187,7 @@ class HandlerTool(Tool):
cherrypy.serving.request.handler = None
def _setup(self):
"""Hook this tool into cherrypy.request.
"""Wire this tool into ``cherrypy.request``.
The standard CherryPy request object will automatically call
this method when the tool is "turned on" in config.
@@ -217,12 +221,14 @@ class HandlerWrapperTool(Tool):
def __init__(self, newhandler, point='before_handler', name=None,
priority=50):
"""Initialize a handler wrapper tool."""
self.newhandler = newhandler
self._point = point
self._name = name
self._priority = priority
def callable(self, *args, **kwargs):
"""Decorate a request handler with a handler tool callable."""
innerfunc = cherrypy.serving.request.handler
def wrap(*args, **kwargs):
@@ -234,13 +240,14 @@ class ErrorTool(Tool):
"""Tool which is used to replace the default request.error_response."""
def __init__(self, callable, name=None):
"""Initialize an error tool."""
Tool.__init__(self, None, callable, name)
def _wrapper(self):
self.callable(**self._merged_args())
def _setup(self):
"""Hook this tool into cherrypy.request.
"""Wire this tool into ``cherrypy.request``.
The standard CherryPy request object will automatically call
this method when the tool is "turned on" in config.
@@ -270,6 +277,7 @@ class SessionTool(Tool):
"""
def __init__(self):
"""Initialize a session tool."""
# _sessions.init must be bound after headers are read
Tool.__init__(self, 'before_request_body', _sessions.init)
@@ -277,7 +285,7 @@ class SessionTool(Tool):
cherrypy.serving.session.acquire_lock()
def _setup(self):
"""Hook this tool into cherrypy.request.
"""Wire this tool into ``cherrypy.request``.
The standard CherryPy request object will automatically call
this method when the tool is "turned on" in config.
@@ -360,6 +368,7 @@ class XMLRPCController(object):
@expose
def default(self, *vpath, **params):
"""Process the unhandled XML-RPC methods."""
rpcparams, rpcmethod = _xmlrpc.process_body()
subhandler = self
@@ -384,7 +393,7 @@ class XMLRPCController(object):
class SessionAuthTool(HandlerTool):
pass
"""An HTTP session authentication tool."""
class CachingTool(Tool):
@@ -402,7 +411,7 @@ class CachingTool(Tool):
_wrapper.priority = 90
def _setup(self):
"""Hook caching into cherrypy.request."""
"""Wire caching into ``cherrypy.request``."""
conf = self._merged_args()
p = conf.pop('priority', None)
@@ -419,9 +428,11 @@ class Toolbox(object):
"""
def __init__(self, namespace):
"""Initialize a toolbox instance."""
self.namespace = namespace
def __setattr__(self, name, value):
"""Set an attribute on this :class:`Toolbox` instance."""
# If the Tool._name is None, supply it from the attribute name.
if isinstance(value, Tool):
if value._name is None:
@@ -449,7 +460,8 @@ class Toolbox(object):
tool._setup()
def register(self, point, **kwargs):
"""
"""Register a hook point handler in the toolbox.
Return a decorator which registers the function
at the given hook point.
"""

View File

@@ -18,8 +18,7 @@ from cherrypy.lib import is_closable_iterator
def downgrade_wsgi_ux_to_1x(environ):
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
"""
"""Return new environ dict for WSGI 1.x from provided WSGI u.x environ."""
env1x = {}
url_encoding = environ[ntou('wsgi.url_encoding')]
@@ -54,10 +53,11 @@ class VirtualHost(object):
cherrypy.tree.graft(vhost)
"""
default = None
"""Required.
The default WSGI application.
default = None
"""The default WSGI application.
Required.
"""
use_x_forwarded_host = True
@@ -67,7 +67,6 @@ class VirtualHost(object):
domains = {}
"""A dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict, and,
if a match is found, the corresponding WSGI application will be
called instead of the default. Note that you often need separate
@@ -76,11 +75,30 @@ class VirtualHost(object):
"""
def __init__(self, default, domains=None, use_x_forwarded_host=True):
"""
Initialize a virtual host app.
:param default: The default WSGI application
:type default: WSGI application
:param use_x_forwarded_host: If True (the default), any
"X-Forwarded-Host" request header will be used instead of the
"Host" header. This is commonly added by HTTP servers (such as
Apache) when proxying.
:type use_x_forwarded_host: Bool, optional
:param domains: A dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict, and,
if a match is found, the corresponding WSGI application will be
called instead of the default. Note that you often need separate
entries for "example.com" and "www.example.com". In addition,
"Host" headers may contain the port number.
:type domains: Dict, optional
"""
self.default = default
self.domains = domains or {}
self.use_x_forwarded_host = use_x_forwarded_host
def __call__(self, environ, start_response):
"""Route WSGI requests based on host names."""
domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host:
domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
@@ -95,10 +113,12 @@ class InternalRedirector(object):
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
def __init__(self, nextapp, recursive=False):
"""Initialize an internal redirector."""
self.nextapp = nextapp
self.recursive = recursive
def __call__(self, environ, start_response):
"""Process internal WSGI request redirects."""
redirections = []
while True:
environ = environ.copy()
@@ -142,10 +162,12 @@ class ExceptionTrapper(object):
"""WSGI middleware that traps exceptions."""
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
"""Initialize exception trapper."""
self.nextapp = nextapp
self.throws = throws
def __call__(self, environ, start_response):
"""Handle exceptions while processing a WSGI request."""
return _TrappedResponse(
self.nextapp,
environ,
@@ -230,6 +252,7 @@ class AppResponse(object):
"""WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp):
"""Initialize the WSGI app response."""
self.cpapp = cpapp
try:
self.environ = environ
@@ -271,9 +294,11 @@ class AppResponse(object):
raise
def __iter__(self):
"""Make an app response iterator."""
return self
def __next__(self):
"""Iterate over the app response."""
return next(self.iter_response)
def close(self):
@@ -346,6 +371,7 @@ class AppResponse(object):
}
def recode_path_qs(self, path, qs):
"""Recode app response path query string."""
# This isn't perfect; if the given PATH_INFO is in the
# wrong encoding, it may fail to match the appropriate config
# section URI. But meh.
@@ -416,6 +442,7 @@ class CPWSGIApp(object):
"""
def __init__(self, cpapp, pipeline=None):
"""Initialize a framework WSGI app wrapper."""
self.cpapp = cpapp
self.pipeline = self.pipeline[:]
if pipeline:
@@ -431,6 +458,7 @@ class CPWSGIApp(object):
return self.response_class(environ, start_response, self.cpapp)
def __call__(self, environ, start_response):
"""Process a WSGI request."""
head = self.head
if head is None:
# Create and nest the WSGI apps in our pipeline (in reverse order).

View File

@@ -0,0 +1,39 @@
"""HTTP header parsing helpers."""
def _parse_param(s):
while s[:1] == ';':
s = s[1:]
end = s.find(';')
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
end = s.find(';', end + 1)
if end < 0:
end = len(s)
f = s[:end]
yield f.strip()
s = s[end:]
def parse_header(line):
"""Parse a ``Content-Type`` like header.
Return the main ``Content-Type`` and a dictionary of options.
Copied from removed stdlib :mod:`cgi` module. See
`cherrypy/cherrypy#2014 (comment)
<https://github.com/cherrypy/cherrypy/issues/2014#issuecomment-1883774891>`_
for background.
"""
parts = _parse_param(';' + line)
key = parts.__next__()
pdict = {}
for p in parts:
i = p.find('=')
if i >= 0:
name = p[:i].strip().lower()
value = p[i + 1:].strip()
if len(value) >= 2 and value[0] == value[-1] == '"':
value = value[1:-1]
value = value.replace('\\\\', '\\').replace('\\"', '"')
pdict[name] = value
return key, pdict

View File

@@ -33,7 +33,9 @@ __date__ = 'April 2009'
def checkpassword_dict(user_password_dict):
"""Returns a checkpassword function which checks credentials
"""Check credentials against a dictionary.
Returns a checkpassword function which checks credentials
against a dictionary of the form: {username : password}.
If you want a simple dictionary-based authentication scheme, use
@@ -48,7 +50,9 @@ def checkpassword_dict(user_password_dict):
def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
"""A CherryPy tool which hooks at before_handler to perform
"""Perform basic auth.
A CherryPy tool which hooks at before_handler to perform
HTTP Basic Access Authentication, as specified in :rfc:`2617`
and :rfc:`7617`.
@@ -69,7 +73,6 @@ def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
returns True, else it returns False.
"""
fallback_charset = 'ISO-8859-1'
if '"' in realm:

View File

@@ -34,6 +34,7 @@ __date__ = 'April 2009'
def md5_hex(s):
"""Return hexdigest of md5sum."""
return md5(ntob(s, 'utf-8')).hexdigest()
@@ -48,6 +49,7 @@ DEFAULT_CHARSET = 'UTF-8'
def TRACE(msg):
"""Log message in TOOLS.AUTH_DIGEST context."""
cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
# Three helper functions for users of the tool, providing three variants
@@ -55,8 +57,9 @@ def TRACE(msg):
def get_ha1_dict_plain(user_password_dict):
"""Return a get_ha1 function which obtains a plaintext password from a
dictionary of the form: {username : password}.
"""Return a get_ha1 function which obtains a plaintext password.
user_password_dict is a dictionary of the form: {username : password}.
If you want a simple dictionary-based authentication scheme, with plaintext
passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
@@ -72,8 +75,9 @@ def get_ha1_dict_plain(user_password_dict):
def get_ha1_dict(user_ha1_dict):
"""Return a get_ha1 function which obtains a HA1 password hash from a
dictionary of the form: {username : HA1}.
"""Return a get_ha1 function which obtains a HA1 password hash.
user_ha1_dict is a dictionary of the form: {username : HA1}.
If you want a dictionary-based authentication scheme, but with
pre-computed HA1 hashes instead of plain-text passwords, use
@@ -87,7 +91,9 @@ def get_ha1_dict(user_ha1_dict):
def get_ha1_file_htdigest(filename):
"""Return a get_ha1 function which obtains a HA1 password hash from a
"""Return a get_ha1 function.
The returned function obtains a HA1 password hash from a
flat file with lines of the same format as that produced by the Apache
htdigest utility. For example, for realm 'wonderland', username 'alice',
and password '4x5istwelve', the htdigest line would be::
@@ -113,7 +119,9 @@ def get_ha1_file_htdigest(filename):
def synthesize_nonce(s, key, timestamp=None):
"""Synthesize a nonce value which resists spoofing and can be checked
"""Synthesize a nonce value.
A nonce value resists spoofing and can be checked
for staleness. Returns a string suitable as the value for 'nonce' in
the www-authenticate header.
@@ -135,7 +143,7 @@ def synthesize_nonce(s, key, timestamp=None):
def H(s):
"""The hash function H."""
"""Return an ``md5`` HEX hash."""
return md5_hex(s)
@@ -152,7 +160,8 @@ def _try_decode_header(header, charset):
class HttpDigestAuthorization(object):
"""
"""Digest Authorization implementation.
Parses a Digest Authorization header and performs
re-calculation of the digest.
"""
@@ -160,10 +169,12 @@ class HttpDigestAuthorization(object):
scheme = 'digest'
def errmsg(self, s):
"""Make an error message for HTTP Digest Authorization."""
return 'Digest Authorization header: %s' % s
@classmethod
def matches(cls, header):
"""Check if header scheme matches auth implementation."""
scheme, _, _ = header.partition(' ')
return scheme.lower() == cls.scheme
@@ -171,6 +182,7 @@ class HttpDigestAuthorization(object):
self, auth_header, http_method,
debug=False, accept_charset=DEFAULT_CHARSET[:],
):
"""Initialize an HTTP Digest Authorization parser."""
self.http_method = http_method
self.debug = debug
@@ -229,10 +241,12 @@ class HttpDigestAuthorization(object):
'neither cnonce nor nc can be present'))
def __str__(self):
"""Render an HTTP Digest Auth header as a string."""
return 'authorization : %s' % self.auth_header
def validate_nonce(self, s, key):
"""Validate the nonce.
Returns True if nonce was generated by synthesize_nonce() and the
timestamp is not spoofed, else returns False.
@@ -298,7 +312,7 @@ class HttpDigestAuthorization(object):
return H(a2)
def request_digest(self, ha1, entity_body=''):
"""Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
"""Calculate the Request-Digest. See :rfc:`2617` section 3.2.2.1.
ha1
The HA1 string obtained from the credentials store.
@@ -351,7 +365,7 @@ def www_authenticate(
realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
stale=False, accept_charset=DEFAULT_CHARSET[:],
):
"""Constructs a WWW-Authenticate header for Digest authentication."""
"""Construct a WWW-Authenticate header for Digest authentication."""
if qop not in valid_qops:
raise ValueError("Unsupported value for qop: '%s'" % qop)
if algorithm not in valid_algorithms:
@@ -374,7 +388,9 @@ def www_authenticate(
def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
"""A CherryPy tool that hooks at before_handler to perform
"""Perform HTTP Digest Access Authentication.
A CherryPy tool that hooks at ``before_handler`` to perform
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Digest' scheme,

View File

@@ -1,4 +1,6 @@
"""
CherryPy cache tooling.
CherryPy implements a simple caching system as a pluggable Tool. This tool
tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
yet, but it's probably good enough for most sites.
@@ -161,6 +163,7 @@ class MemoryCache(Cache):
debug = False
def __init__(self):
"""Initialize in-memory cache store."""
self.clear()
# Run self.expire_cache in a separate daemon thread.
@@ -442,7 +445,6 @@ def expires(secs=0, force=False, debug=False):
If any are already present, none of the above response headers are set.
"""
response = cherrypy.serving.response
headers = response.headers

View File

@@ -2,7 +2,8 @@
To use this module, or the coverage tools in the test suite,
you need to download 'coverage.py', either Gareth Rees' `original
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
implementation
<https://web.archive.org/web/20231108042640/https://garethrees.org/2001/12/04/python-coverage/>`_
or Ned Batchelder's `enhanced version:
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
@@ -22,7 +23,7 @@ it will call ``serve()`` for you.
import re
import sys
import cgi
import html
import os
import os.path
import urllib.parse
@@ -38,6 +39,7 @@ try:
the_coverage = coverage(data_file=localFile)
def start():
"""Start collecting coverage."""
the_coverage.start()
except ImportError:
# Setting the_coverage to None will raise errors
@@ -50,6 +52,7 @@ except ImportError:
'coverage.py could not be imported.')
def start():
"""Start collecting coverage."""
pass
start.priority = 20
@@ -284,8 +287,10 @@ def get_tree(base, exclude, coverage=the_coverage):
class CoverStats(object):
"""HTTP handler for the coverage stats."""
def __init__(self, coverage, root=None):
"""Initialize the coverage stats application."""
self.coverage = coverage
if root is None:
# Guess initial depth. Files outside this path will not be
@@ -295,12 +300,13 @@ class CoverStats(object):
@cherrypy.expose
def index(self):
"""Render the coverage stats index page."""
return TEMPLATE_FRAMESET % self.root.lower()
@cherrypy.expose
def menu(self, base='/', pct='50', showpct='',
exclude=r'python\d\.\d|test|tut\d|tutorial'):
"""Render HTML menu web page."""
# The coverage module uses all-lower-case names.
base = base.lower().rstrip(os.sep)
@@ -334,6 +340,7 @@ class CoverStats(object):
yield '</body></html>'
def annotated_file(self, filename, statements, excluded, missing):
"""Annotate given file with coverage information."""
with open(filename, 'r') as source:
lines = source.readlines()
buffer = []
@@ -352,12 +359,13 @@ class CoverStats(object):
buffer.append((lineno, line))
if empty_the_buffer:
for lno, pastline in buffer:
yield template % (lno, cgi.escape(pastline))
yield template % (lno, html.escape(pastline))
buffer = []
yield template % (lineno, cgi.escape(line))
yield template % (lineno, html.escape(line))
@cherrypy.expose
def report(self, name):
"""Render coverage stats as HTML."""
filename, statements, excluded, missing, _ = self.coverage.analysis2(
name)
pc = _percent(statements, missing)
@@ -374,6 +382,7 @@ class CoverStats(object):
def serve(path=localFile, port=8080, root=None):
"""Serve the coverage app over HTTP."""
if coverage is None:
raise ImportError('The coverage module could not be imported.')
from coverage import coverage

View File

@@ -249,6 +249,7 @@ appstats.update({
def proc_time(s):
"""Compute current HTTP request processing time."""
return time.time() - s['Start Time']
@@ -256,20 +257,24 @@ class ByteCountWrapper(object):
"""Wraps a file-like object, counting the number of bytes read."""
def __init__(self, rfile):
"""Initialize a read byte counter."""
self.rfile = rfile
self.bytes_read = 0
def read(self, size=-1):
"""Read from file, counting bytes."""
data = self.rfile.read(size)
self.bytes_read += len(data)
return data
def readline(self, size=-1):
"""Read a line from file, counting bytes."""
data = self.rfile.readline(size)
self.bytes_read += len(data)
return data
def readlines(self, sizehint=0):
"""Read a list of lines from file, counting bytes."""
# Shamelessly stolen from StringIO
total = 0
lines = []
@@ -283,22 +288,27 @@ class ByteCountWrapper(object):
return lines
def close(self):
"""Close the underlying file object."""
self.rfile.close()
def __iter__(self):
"""Make a file reader iterator."""
return self
def next(self):
"""Return next portion of bytes from the iterated file."""
data = self.rfile.next()
self.bytes_read += len(data)
return data
def average_uriset_time(s):
"""Compute average request processing time within a URI set."""
return s['Count'] and (s['Sum'] / s['Count']) or 0
def _get_threading_ident():
"""Discover the current thread identifier."""
if sys.version_info >= (3, 3):
return threading.get_ident()
return threading._get_ident()
@@ -308,10 +318,11 @@ class StatsTool(cherrypy.Tool):
"""Record various information about the current request."""
def __init__(self):
"""Initialize the statistics gathering tool."""
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
def _setup(self):
"""Hook this tool into cherrypy.request.
"""Plug this tool into ``cherrypy.request``.
The standard CherryPy request object will automatically call
this method when the tool is "turned on" in config.
@@ -404,14 +415,17 @@ missing = object()
def locale_date(v):
"""Format given date per current locale."""
return time.strftime('%c', time.gmtime(v))
def iso_format(v):
"""Format given date as ISO string."""
return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
def pause_resume(ns):
"""Produce pause or resume HTML form maker."""
def _pause_resume(enabled):
pause_disabled = ''
resume_disabled = ''
@@ -433,6 +447,7 @@ def pause_resume(ns):
class StatsPage(object):
"""An app rendering the gathered statistics."""
formatting = {
'CherryPy Applications': {
@@ -474,6 +489,7 @@ class StatsPage(object):
@cherrypy.expose
def index(self):
"""Render the app stats index page."""
# Transform the raw data into pretty output for HTML
yield """
<html>
@@ -672,12 +688,14 @@ table.stats2 th {
if json is not None:
@cherrypy.expose
def data(self):
"""Render statistics as JSON."""
s = extrapolate_statistics(logging.statistics)
cherrypy.response.headers['Content-Type'] = 'application/json'
return json.dumps(s, sort_keys=True, indent=4).encode('utf-8')
@cherrypy.expose
def pause(self, namespace):
"""Pause gathering the statistics."""
logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./')
pause.cp_config = {'tools.allow.on': True,
@@ -685,6 +703,7 @@ table.stats2 th {
@cherrypy.expose
def resume(self, namespace):
"""Resume gathering the statistics."""
logging.statistics.get(namespace, {})['Enabled'] = True
raise cherrypy.HTTPRedirect('./')
resume.cp_config = {'tools.allow.on': True,

View File

@@ -172,7 +172,6 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
default, 'remote' is set to 'X-Forwarded-For'. If you do not want to
rewrite remote.ip, set the 'remote' arg to an empty string.
"""
request = cherrypy.serving.request
if scheme:
@@ -288,23 +287,45 @@ class SessionAuth(object):
debug = False
def check_username_and_password(self, username, password):
pass
"""Assert the login credentials.
:param username: A user name sent from the login form.
:type username: str
:param password: A pass word sent from the login form.
:type password: str
:returns: A non-empty error string if the authentication fails.
:rtype: str
"""
def anonymous(self):
"""Provide a temporary user name for anonymous users."""
pass
def on_login(self, username):
pass
"""Process a successful login event.
:param username: The logged in user name.
:type username: str
"""
def on_logout(self, username):
pass
"""Process a successful logout event.
:param username: The logged out user name.
:type username: str
"""
def on_check(self, username):
pass
"""Process a successful check event.
:param username: The checked user name.
:type username: str
"""
def login_screen(self, from_page='..', username='', error_msg='',
**kwargs):
"""Render the login HTML page."""
return (str("""<html><body>
Message: %(error_msg)s
<form method="post" action="do_login">
@@ -385,6 +406,7 @@ Message: %(error_msg)s
cherrypy.log(template % context, 'TOOLS.SESSAUTH')
def run(self):
"""Perform session authentication."""
request = cherrypy.serving.request
response = cherrypy.serving.response
@@ -592,19 +614,21 @@ def accept(media=None, debug=False):
class MonitoredHeaderMap(_httputil.HeaderMap):
"""An access-tracked HTTP header mapping."""
def transform_key(self, key):
"""Normalize and track an HTTP header name."""
self.accessed_headers.add(key)
return super(MonitoredHeaderMap, self).transform_key(key)
def __init__(self):
"""Initialize a monitored HTTP header mapping."""
self.accessed_headers = set()
super(MonitoredHeaderMap, self).__init__()
def autovary(ignore=None, debug=False):
"""Auto-populate the Vary response header based on request.header access.
"""
"""Populate ``Vary`` response header based on ``request.header`` access."""
request = cherrypy.serving.request
req_h = request.headers

View File

@@ -1,3 +1,4 @@
"""Encoding module."""
import struct
import time
import io
@@ -41,32 +42,41 @@ def decode(encoding=None, default_encoding='utf-8'):
class UTF8StreamEncoder:
"""UTF8 Stream Encoder."""
def __init__(self, iterator):
"""Initialize a UTF-8 stream encoder instance."""
self._iterator = iterator
def __iter__(self):
"""Make a UTF-8-encoded stream iterator."""
return self
def next(self):
"""UTF-8-encode the next chunk of the stream."""
return self.__next__()
def __next__(self):
"""UTF-8-encode the next chunk of the stream."""
res = next(self._iterator)
if isinstance(res, str):
res = res.encode('utf-8')
return res
def close(self):
"""Close the underlying byte stream."""
if is_closable_iterator(self._iterator):
self._iterator.close()
def __getattr__(self, attr):
"""Return the underlying byte stream attribute value."""
if attr.startswith('__'):
raise AttributeError(self, attr)
return getattr(self._iterator, attr)
class ResponseEncoder:
"""An HTTP response payload encoder."""
default_encoding = 'utf-8'
failmsg = 'Response body could not be encoded with %r.'
@@ -77,6 +87,7 @@ class ResponseEncoder:
debug = False
def __init__(self, **kwargs):
"""Initialize HTTP response payload encoder."""
for k, v in kwargs.items():
setattr(self, k, v)
@@ -124,6 +135,7 @@ class ResponseEncoder:
return True
def find_acceptable_charset(self):
"""Deduce acceptable charset for HTTP response."""
request = cherrypy.serving.request
response = cherrypy.serving.response
@@ -219,6 +231,7 @@ class ResponseEncoder:
raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs):
"""Set up encoding for the HTTP response."""
response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs)
@@ -330,6 +343,7 @@ def compress(body, compress_level):
def decompress(body):
"""Decompress a blob of bytes."""
import gzip
zbuf = io.BytesIO()

View File

@@ -1,3 +1,4 @@
"""Garbage collection inspection tooling."""
import gc
import inspect
import sys
@@ -19,6 +20,7 @@ class ReferrerTree(object):
peek_length = 40
def __init__(self, ignore=None, maxdepth=2, maxparents=10):
"""Initialize a referrer tree structure."""
self.ignore = ignore or []
self.ignore.append(inspect.currentframe().f_back)
self.maxdepth = maxdepth
@@ -99,18 +101,23 @@ class ReferrerTree(object):
def get_instances(cls):
"""Return GC instances."""
return [x for x in gc.get_objects() if isinstance(x, cls)]
class RequestCounter(SimplePlugin):
"""An HTTP request counter plugin."""
def start(self):
"""Initialize the internal counter."""
self.count = 0
def before_request(self):
"""Increment the counter before HTTP request."""
self.count += 1
def after_request(self):
"""Decrement the counter after HTTP request."""
self.count -= 1
@@ -119,6 +126,7 @@ request_counter.subscribe()
def get_context(obj):
"""Compute object's runtime context information."""
if isinstance(obj, _cprequest.Request):
return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
elif isinstance(obj, _cprequest.Response):
@@ -144,10 +152,12 @@ class GCRoot(object):
@cherrypy.expose
def index(self):
"""Render the index page HTML content."""
return 'Hello, world!'
@cherrypy.expose
def stats(self):
"""Render garbage collection statistics page HTML content."""
output = ['Statistics:']
for trial in range(10):

View File

@@ -0,0 +1,13 @@
"""Deprecated HTTP header parsing helpers."""
# for compatibility, expose accidentally here
import warnings
from .._private_api.compat.headers import _parse_param, parse_header # noqa
warnings.warn(
'Import `cherrypy._private_api.compat.headers` '
'instead of `cherrypy.lib.headers`',
DeprecationWarning,
stacklevel=2,
)

View File

@@ -12,7 +12,6 @@ import email.utils
import re
import builtins
from binascii import b2a_base64
from cgi import parse_header
from email.header import decode_header
from http.server import BaseHTTPRequestHandler
from urllib.parse import unquote_plus
@@ -22,6 +21,9 @@ import jaraco.collections
import cherrypy
from cherrypy._cpcompat import ntob, ntou
from .._private_api.compat.headers import parse_header
response_codes = BaseHTTPRequestHandler.responses.copy()
# From https://github.com/cherrypy/cherrypy/issues/361
@@ -78,7 +80,6 @@ def get_ranges(headervalue, content_length):
If this function returns an empty list, you should return HTTP 416.
"""
if not headervalue:
return None
@@ -130,25 +131,31 @@ class HeaderElement(object):
"""An element (with parameters) from an HTTP header's element list."""
def __init__(self, value, params=None):
"""Initialize an HTTP header value representation."""
self.value = value
if params is None:
params = {}
self.params = params
def __cmp__(self, other):
"""Compare current HTTP header to another by value only."""
return builtins.cmp(self.value, other.value)
def __lt__(self, other):
"""Check if this header value is less than the other."""
return self.value < other.value
def __str__(self):
"""Render the HTTP header value as a string."""
p = [';%s=%s' % (k, v) for k, v in self.params.items()]
return str('%s%s' % (self.value, ''.join(p)))
def __bytes__(self):
"""Turn the HTTP header value string representation to bytes."""
return ntob(self.__str__())
def __unicode__(self):
"""Render the HTTP header value as a string."""
return ntou(self.__str__())
@staticmethod
@@ -180,6 +187,7 @@ class AcceptElement(HeaderElement):
@classmethod
def from_str(cls, elementstr):
"""Make an :class:`AcceptElement` instance from a string."""
qvalue = None
# The first "q" parameter (if any) separates the initial
# media-range parameter(s) (if any) from the accept-params.
@@ -197,7 +205,7 @@ class AcceptElement(HeaderElement):
@property
def qvalue(self):
'The qvalue, or priority, of this value.'
"""The qvalue, or priority, of this value."""
val = self.params.get('q', '1')
if isinstance(val, HeaderElement):
val = val.value
@@ -215,12 +223,17 @@ class AcceptElement(HeaderElement):
) from val_err
def __cmp__(self, other):
"""Compare current header to another by qvalues then strings."""
diff = builtins.cmp(self.qvalue, other.qvalue)
if diff == 0:
diff = builtins.cmp(str(self), str(other))
return diff
def __lt__(self, other):
"""Check if this header qvalue is less than the other.
This method uses string comparison as the second factor.
"""
if self.qvalue == other.qvalue:
return str(self) < str(other)
else:
@@ -231,7 +244,9 @@ RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
def header_elements(fieldname, fieldvalue):
"""Return a sorted HeaderElement list from a comma-separated header string.
"""Return a sorted :class:`HeaderElement` list.
Constucted from a comma-separated header string.
"""
if not fieldvalue:
return []
@@ -283,7 +298,6 @@ def valid_status(status):
... ) + BaseHTTPRequestHandler.responses[http.client.ACCEPTED]
True
"""
if not status:
status = 200
@@ -322,7 +336,6 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
"""Parse a query given as a string argument.
Arguments:
qs: URL-encoded query string to be parsed
keep_blank_values: flag indicating whether blank values in
@@ -391,6 +404,7 @@ class CaseInsensitiveDict(jaraco.collections.KeyTransformingDict):
@staticmethod
def transform_key(key):
"""Title-case an HTTP header name."""
if key is None:
# TODO(#1830): why?
return 'None'
@@ -444,7 +458,8 @@ class HeaderMap(CaseInsensitiveDict):
@classmethod
def encode_header_items(cls, header_items):
"""
"""Emit tuples of wire-ready HTTP headers.
Prepare the sequence of name, value tuples into a form suitable for
transmitting on the wire for HTTP.
"""
@@ -456,6 +471,7 @@ class HeaderMap(CaseInsensitiveDict):
@classmethod
def encode_header_item(cls, item):
"""Encode an HTTP header for sending over the wire."""
if isinstance(item, str):
item = cls.encode(item)
@@ -501,6 +517,7 @@ class Host(object):
name = 'unknown.tld'
def __init__(self, ip, port, name=None):
"""Initialize a TCP service representation."""
self.ip = ip
self.port = port
if name is None:
@@ -508,11 +525,13 @@ class Host(object):
self.name = name
def __repr__(self):
"""Render a :class:`Host` instance representation."""
return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)
class SanitizedHost(str):
r"""
r"""A normalized host header value.
Wraps a raw host header received from the network in
a sanitized version that elides dangerous characters.
@@ -526,9 +545,11 @@ class SanitizedHost(str):
>>> isinstance(SanitizedHost('foobar'), SanitizedHost)
False
"""
dangerous = re.compile(r'[\n\r]')
def __new__(cls, raw):
"""Construct a new :class:`SanitizedHost` instance."""
sanitized = cls._sanitize(raw)
if sanitized == raw:
return raw
@@ -538,4 +559,5 @@ class SanitizedHost(str):
@classmethod
def _sanitize(cls, raw):
"""Clean up the CR LF chars from input."""
return cls.dangerous.sub('', raw)

View File

@@ -1,3 +1,4 @@
"""JSON tools."""
import cherrypy
from cherrypy import _json as json
from cherrypy._cpcompat import text_or_bytes, ntou
@@ -15,7 +16,8 @@ def json_processor(entity):
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
force=True, debug=False, processor=json_processor):
"""Add a processor to parse JSON request entities:
"""Add a processor to parse JSON request entities.
The default processor places the parsed data into request.json.
Incoming request entities which match the given content_type(s) will
@@ -56,6 +58,7 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
def json_handler(*args, **kwargs):
"""Convert decorated HTTP handler-returned object to JSON string."""
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
return json.encode(value)

View File

@@ -1,15 +1,20 @@
"""Lock acquisition helpers."""
import datetime
class NeverExpires(object):
"""A representation of a never expiring object."""
def expired(self):
"""Communicate that the object hasn't expired."""
return False
class Timer(object):
"""A simple timer that will indicate when an expiration time has passed."""
def __init__(self, expiration):
'Create a timer that expires at `expiration` (UTC datetime)'
"""Create a timer that expires at `expiration` (UTC datetime)."""
self.expiration = expiration
@classmethod
@@ -20,18 +25,21 @@ class Timer(object):
)
def expired(self):
"""Check whether the timer has expired."""
return datetime.datetime.now(
datetime.timezone.utc,
) >= self.expiration
class LockTimeout(Exception):
'An exception when a lock could not be acquired before a timeout period'
"""Exception when a lock could not be acquired before a timeout period."""
class LockChecker(object):
"""Keep track of the time and detect if a timeout has expired."""
def __init__(self, session_id, timeout):
"""Initialize a lock acquisition tracker."""
self.session_id = session_id
if timeout:
self.timer = Timer.after(timeout)
@@ -39,6 +47,7 @@ class LockChecker(object):
self.timer = NeverExpires()
def expired(self):
"""Check whether the lock checker has expired."""
if self.timer.expired():
raise LockTimeout(
'Timeout acquiring lock for %(session_id)s' % vars(self))

View File

@@ -69,8 +69,10 @@ _count = 0
class Profiler(object):
"""A profiling app."""
def __init__(self, path=None):
"""Prepare the profiling app resources."""
if not path:
path = os.path.join(os.path.dirname(__file__), 'profile')
self.path = path
@@ -88,13 +90,19 @@ class Profiler(object):
return result
def statfiles(self):
""":rtype: list of available profiles.
"""Compose a list of statistics file names.
:returns: A list of available profiles.
:rtype: list[str]
"""
return [f for f in os.listdir(self.path)
if f.startswith('cp_') and f.endswith('.prof')]
def stats(self, filename, sortby='cumulative'):
""":rtype stats(index): output of print_stats() for the given profile.
"""Generate statistics from given profile.
:returns: The sorted stats index printout.
:rtype: str
"""
sio = io.StringIO()
if sys.version_info >= (2, 5):
@@ -120,6 +128,7 @@ class Profiler(object):
@cherrypy.expose
def index(self):
"""Render the profiling viewer index page."""
return """<html>
<head><title>CherryPy profile data</title></head>
<frameset cols='200, 1*'>
@@ -131,6 +140,7 @@ class Profiler(object):
@cherrypy.expose
def menu(self):
"""Render the profiler menu page html layout."""
yield '<h2>Profiling runs</h2>'
yield '<p>Click on one of the runs below to see profiling data.</p>'
runs = self.statfiles()
@@ -141,19 +151,23 @@ class Profiler(object):
@cherrypy.expose
def report(self, filename):
"""Render a statistics report."""
cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.stats(filename)
class ProfileAggregator(Profiler):
"""A profiling aggregator app."""
def __init__(self, path=None):
"""Prepare the profiling aggregator app resources."""
Profiler.__init__(self, path)
global _count
self.count = _count = _count + 1
self.profiler = profile.Profile()
def run(self, func, *args, **params):
"""Dump aggeregated profile data into ``self.path``."""
path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
result = self.profiler.runcall(func, *args, **params)
self.profiler.dump_stats(path)
@@ -161,6 +175,7 @@ class ProfileAggregator(Profiler):
class make_app:
"""Profiling WSGI middleware wrapper."""
def __init__(self, nextapp, path=None, aggregate=False):
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
@@ -194,6 +209,7 @@ class make_app:
self.profiler = Profiler(path)
def __call__(self, environ, start_response):
"""Process a WSGI request."""
def gather():
result = []
for line in self.nextapp(environ, start_response):
@@ -203,6 +219,7 @@ class make_app:
def serve(path=None, port=8080):
"""Serve the web app with profiler activated."""
if profile is None or pstats is None:
msg = ('Your installation of Python does not have a profile module. '
"If you're on Debian, try "

View File

@@ -92,10 +92,12 @@ class NamespaceSet(dict):
handler(k, v)
def __repr__(self):
"""Render representation of a :class:`NamespaceSet` instance."""
return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
dict.__repr__(self))
def __copy__(self):
"""Make a copy of this instance."""
newobj = self.__class__()
newobj.update(self)
return newobj
@@ -113,6 +115,7 @@ class Config(dict):
namespaces = NamespaceSet()
def __init__(self, file=None, **kwargs):
"""Initialize a CherryPy :class:`Config`."""
self.reset()
if file is not None:
self.update(file)
@@ -141,20 +144,24 @@ class Config(dict):
self.namespaces(config)
def __setitem__(self, k, v):
"""Assign a config setting."""
dict.__setitem__(self, k, v)
self.namespaces({k: v})
class Parser(configparser.ConfigParser):
"""A parser for the INI-style config file.
"""Sub-class of ConfigParser that keeps the case of options and that
Sub-class of ConfigParser that keeps the case of options and that
raises an exception if the file cannot be read.
"""
def optionxform(self, optionstr):
"""Keep the option names unedited."""
return optionstr
def read(self, filenames):
"""Read the config from files on disk."""
if isinstance(filenames, text_or_bytes):
filenames = [filenames]
for filename in filenames:
@@ -186,6 +193,7 @@ class Parser(configparser.ConfigParser):
return result
def dict_from_file(self, file):
"""Generate a dict from a file."""
if hasattr(file, 'read'):
self.read_file(file)
else:
@@ -235,10 +243,9 @@ class _Builder:
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
"""
"""Emulate ``build_Call`` under Python 3.5."""
# Workaround for python 3.5. _ast.Call signature, docs found at
# https://greentreesnakes.readthedocs.org/en/latest/nodes.html
import ast
callee = self.build(o.func)
args = []
@@ -375,7 +382,6 @@ def modules(modulePath):
def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind('.')
attr_name = full_attribute_name[last_dot + 1:]

View File

@@ -177,6 +177,7 @@ class Session(object):
# --------------------- Session management methods --------------------- #
def __init__(self, id=None, **kwargs):
"""Initialize the session tool."""
self.id_observers = []
self._data = {}
@@ -321,16 +322,19 @@ class Session(object):
# -------------------- Application accessor methods -------------------- #
def __getitem__(self, key):
"""Retrieve a session-stored object."""
if not self.loaded:
self.load()
return self._data[key]
def __setitem__(self, key, value):
"""Store an object in the session."""
if not self.loaded:
self.load()
self._data[key] = value
def __delitem__(self, key):
"""Delete object stored in the session."""
if not self.loaded:
self.load()
del self._data[key]
@@ -349,13 +353,15 @@ class Session(object):
return self._data.pop(key, default)
def __contains__(self, key):
"""Check if the session has an object by key."""
if not self.loaded:
self.load()
return key in self._data
def get(self, key, default=None):
"""D.get(k[,d]) -> D[k] if k in D, else d.
"""Retrieve a session-stored object.
D.get(k[,d]) -> D[k] if k in D, else d.
d defaults to None.
"""
if not self.loaded:
@@ -363,8 +369,9 @@ class Session(object):
return self._data.get(key, default)
def update(self, d):
"""D.update(E) -> None.
"""Update multiple session-stored objects in one go.
D.update(E) -> None.
Update D from E: for k in E: D[k] = E[k].
"""
if not self.loaded:
@@ -372,14 +379,18 @@ class Session(object):
self._data.update(d)
def setdefault(self, key, default=None):
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
"""Set a default session key value.
D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.
"""
if not self.loaded:
self.load()
return self._data.setdefault(key, default)
def clear(self):
"""D.clear() -> None.
"""Clean up the session-stored data.
D.clear() -> None.
Remove all items from D.
"""
if not self.loaded:
@@ -387,25 +398,35 @@ class Session(object):
self._data.clear()
def keys(self):
"""D.keys() -> list of D's keys."""
"""Return an iterable of session keys.
D.keys() -> list of D's keys.
"""
if not self.loaded:
self.load()
return self._data.keys()
def items(self):
"""D.items() -> list of D's (key, value) pairs, as 2-tuples."""
"""Return an iterable of items as tuples.
D.items() -> list of D's (key, value) pairs, as 2-tuples.
"""
if not self.loaded:
self.load()
return self._data.items()
def values(self):
"""D.values() -> list of D's values."""
"""Return an iterable of session objects.
D.values() -> list of D's values.
"""
if not self.loaded:
self.load()
return self._data.values()
class RamSession(Session):
"""A memory-baked session store implementation."""
# Class-level objects. Don't rebind these!
cache = {}
@@ -413,7 +434,6 @@ class RamSession(Session):
def clean_up(self):
"""Clean up expired sessions."""
now = self.now()
for _id, (data, expiration_time) in self.cache.copy().items():
if expiration_time <= now:
@@ -466,8 +486,7 @@ class RamSession(Session):
class FileSession(Session):
"""Implementation of the File backend for sessions
"""Implementation of the file backend for sessions.
storage_path
The folder where session data will be saved. Each session
@@ -485,6 +504,7 @@ class FileSession(Session):
pickle_protocol = pickle.HIGHEST_PROTOCOL
def __init__(self, id=None, **kwargs):
"""Prepare the file session store."""
# The 'storage_path' arg is required for file-based sessions.
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
kwargs.setdefault('lock_timeout', None)
@@ -614,6 +634,7 @@ class FileSession(Session):
class MemcachedSession(Session):
"""A Memcached-baked session store."""
# The most popular memcached client for Python isn't thread-safe.
# Wrap all .get and .set operations in a single lock.
@@ -687,7 +708,6 @@ class MemcachedSession(Session):
def save():
"""Save any changed session data."""
if not hasattr(cherrypy.serving, 'session'):
return
request = cherrypy.serving.request
@@ -783,7 +803,6 @@ def init(storage_type=None, path=None, path_header=None, name='session_id',
and may be specific to the storage type. See the subclass of Session
you're using for more information.
"""
# Py27 compat
storage_class = kwargs.pop('storage_class', RamSession)
@@ -898,7 +917,8 @@ def set_response_cookie(path=None, path_header=None, name='session_id',
def _add_MSIE_max_age_workaround(cookie, timeout):
"""
"""Inject a Microsoft Internet Explorer ``max-age`` workaround.
We'd like to use the "max-age" param as indicated in
http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
save it to disk and the session is lost if people close

View File

@@ -38,6 +38,7 @@ class SimplePlugin(object):
"""
def __init__(self, bus):
"""Initialize a simple plugin."""
self.bus = bus
def subscribe(self):
@@ -93,6 +94,7 @@ class SignalHandler(object):
del k, v
def __init__(self, bus):
"""Initialize a signal handler plugin."""
self.bus = bus
# Set default handlers
self.handlers = {'SIGTERM': self.bus.exit,
@@ -117,8 +119,7 @@ class SignalHandler(object):
self.bus.exit()
def _is_daemonized(self):
"""Return boolean indicating if the current process is
running as a daemon.
"""Check if 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
@@ -223,6 +224,7 @@ class DropPrivileges(SimplePlugin):
"""
def __init__(self, bus, umask=None, uid=None, gid=None):
"""Initialize the privilege dropping plugin."""
SimplePlugin.__init__(self, bus)
self.finalized = False
self.uid = uid
@@ -288,6 +290,7 @@ class DropPrivileges(SimplePlugin):
self._umask = val
def start(self):
"""Drop the process privileges."""
# uid/gid
def current_ids():
"""Return the current (uid, gid) if available."""
@@ -353,6 +356,7 @@ class Daemonizer(SimplePlugin):
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
stderr='/dev/null'):
"""Initialize the daemonizer plugin."""
SimplePlugin.__init__(self, bus)
self.stdin = stdin
self.stdout = stdout
@@ -360,6 +364,7 @@ class Daemonizer(SimplePlugin):
self.finalized = False
def start(self):
"""Attempt to daemonize the process."""
if self.finalized:
self.bus.log('Already deamonized.')
@@ -382,6 +387,7 @@ class Daemonizer(SimplePlugin):
def daemonize(
stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
logger=lambda msg: None):
"""Daemonize the process."""
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
@@ -428,11 +434,13 @@ class PIDFile(SimplePlugin):
"""Maintain a PID file via a WSPBus."""
def __init__(self, bus, pidfile):
"""Initialize the PID file plugin."""
SimplePlugin.__init__(self, bus)
self.pidfile = pidfile
self.finalized = False
def start(self):
"""Write a PID file to disk."""
pid = os.getpid()
if self.finalized:
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
@@ -444,6 +452,7 @@ class PIDFile(SimplePlugin):
start.priority = 70
def exit(self):
"""Delete the PID file from disk."""
try:
os.remove(self.pidfile)
self.bus.log('PID file removed: %r.' % self.pidfile)
@@ -462,11 +471,12 @@ class PerpetualTimer(threading.Timer):
"""
def __init__(self, *args, **kwargs):
"Override parent constructor to allow 'bus' to be provided."
"""Override parent constructor to allow 'bus' to be provided."""
self.bus = kwargs.pop('bus', None)
super(PerpetualTimer, self).__init__(*args, **kwargs)
def run(self):
"""Run an infinitely repeated callable."""
while True:
self.finished.wait(self.interval)
if self.finished.isSet():
@@ -494,6 +504,7 @@ class BackgroundTask(threading.Thread):
"""
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
"""Initialize a background task parameters."""
super(BackgroundTask, self).__init__()
self.interval = interval
self.function = function
@@ -506,9 +517,11 @@ class BackgroundTask(threading.Thread):
self.daemon = True
def cancel(self):
"""Set a task cancellation flag."""
self.running = False
def run(self):
"""Start running the repeated background task in the loop."""
self.running = True
while self.running:
time.sleep(self.interval)
@@ -539,6 +552,7 @@ class Monitor(SimplePlugin):
"""
def __init__(self, bus, callback, frequency=60, name=None):
"""Initialize the monitor plugin."""
SimplePlugin.__init__(self, bus)
self.callback = callback
self.frequency = frequency
@@ -611,6 +625,7 @@ class Autoreloader(Monitor):
"""A regular expression by which to match filenames."""
def __init__(self, bus, frequency=1, match='.*'):
"""Initialize the auto-reloader monitor plugin."""
self.mtimes = {}
self.files = set()
self.match = match
@@ -717,6 +732,7 @@ class ThreadManager(SimplePlugin):
"""A map of {thread ident: index number} pairs."""
def __init__(self, bus):
"""Initialize the thread manager plugin."""
self.threads = {}
SimplePlugin.__init__(self, bus)
self.bus.listeners.setdefault('acquire_thread', set())

View File

@@ -1,4 +1,6 @@
r"""
Server interfaces.
Starting in CherryPy 3.1, cherrypy.server is implemented as an
:ref:`Engine Plugin<plugins>`. It's an instance of
:class:`cherrypy._cpserver.Server`, which is a subclass of
@@ -127,6 +129,8 @@ import portend
class Timeouts:
"""Timeout constants."""
occupied = 5
free = 1
@@ -146,6 +150,7 @@ class ServerAdapter(object):
"""
def __init__(self, bus, httpserver=None, bind_addr=None):
"""Initialize the HTTP server plugin."""
self.bus = bus
self.httpserver = httpserver
self.bind_addr = bind_addr
@@ -153,10 +158,12 @@ class ServerAdapter(object):
self.running = False
def subscribe(self):
"""Subscribe control methods to the bus lifecycle events."""
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
def unsubscribe(self):
"""Unsubcribe control methods to the bus lifecycle events."""
self.bus.unsubscribe('start', self.start)
self.bus.unsubscribe('stop', self.stop)
@@ -212,7 +219,9 @@ class ServerAdapter(object):
return '%s://%s' % (scheme, host)
def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the
"""Start the HTTP server thread.
HTTP servers MUST be running in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an
exception is raised in the httpserver's thread then it's
trapped here, and the bus (and therefore our httpserver)
@@ -258,9 +267,10 @@ class ServerAdapter(object):
@property
def bound_addr(self):
"""
The bind address, or if it's an ephemeral port and the
socket has been bound, return the actual port bound.
"""The bind address.
If it's an ephemeral port and the socket has been bound,
return the actual port bound.
"""
host, port = self.bind_addr
if port == 0 and self.httpserver.socket:
@@ -292,6 +302,7 @@ class FlupCGIServer(object):
"""Adapter for a flup.server.cgi.WSGIServer."""
def __init__(self, *args, **kwargs):
"""Initialize the flup CGI Server plugin."""
self.args = args
self.kwargs = kwargs
self.ready = False
@@ -315,6 +326,7 @@ class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer."""
def __init__(self, *args, **kwargs):
"""Initialize the FCGI server parameters."""
if kwargs.get('bindAddress', None) is None:
import socket
if not hasattr(socket, 'fromfd'):
@@ -360,6 +372,7 @@ class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer."""
def __init__(self, *args, **kwargs):
"""Initialize the SCGI server parameters."""
self.args = args
self.kwargs = kwargs
self.ready = False
@@ -395,7 +408,8 @@ class FlupSCGIServer(object):
@contextlib.contextmanager
def _safe_wait(host, port):
"""
"""Warn when bind interface is ambiguous.
On systems where a loopback interface is not available and the
server is bound to all interfaces, it's difficult to determine
whether the server is in fact occupying the port. In this case,

View File

@@ -17,10 +17,12 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
def __init__(self, bus):
"""Initialize the console control handler."""
self.is_set = False
plugins.SimplePlugin.__init__(self, bus)
def start(self):
"""Register handling of the console control events."""
if self.is_set:
self.bus.log('Handler for console events already set.', level=20)
return
@@ -34,6 +36,7 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
self.is_set = True
def stop(self):
"""Unregister the console control handlers."""
if not self.is_set:
self.bus.log('Handler for console events already off.', level=20)
return
@@ -78,6 +81,7 @@ class Win32Bus(wspbus.Bus):
"""
def __init__(self):
"""Initialize a Win32 bus implementation."""
self.events = {}
wspbus.Bus.__init__(self)
@@ -94,10 +98,12 @@ class Win32Bus(wspbus.Bus):
@property
def state(self):
"""The bus state."""
return self._state
@state.setter
def state(self, value):
"""Set the bus state."""
self._state = value
event = self._get_state_event(value)
win32event.PulseEvent(event)
@@ -144,6 +150,7 @@ control_codes = _ControlCodes({'graceful': 138})
def signal_child(service, command):
"""Send a control command to a service."""
if command == 'stop':
win32serviceutil.StopService(service)
elif command == 'restart':
@@ -165,16 +172,19 @@ class PyWebService(win32serviceutil.ServiceFramework):
_svc_description_ = 'Python Web Service'
def SvcDoRun(self):
"""Start the service."""
from cherrypy import process
process.bus.start()
process.bus.block()
def SvcStop(self):
"""Stop the service."""
from cherrypy import process
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
process.bus.exit()
def SvcOther(self, control):
"""Send a command to the service."""
from cherrypy import process
process.bus.publish(control_codes.key_for(control))