| Index: third_party/cherrypy/_cpdispatch.py
|
| ===================================================================
|
| --- third_party/cherrypy/_cpdispatch.py (revision 0)
|
| +++ third_party/cherrypy/_cpdispatch.py (revision 0)
|
| @@ -0,0 +1,636 @@
|
| +"""CherryPy dispatchers.
|
| +
|
| +A 'dispatcher' is the object which looks up the 'page handler' callable
|
| +and collects config for the current request based on the path_info, other
|
| +request attributes, and the application architecture. The core calls the
|
| +dispatcher as early as possible, passing it a 'path_info' argument.
|
| +
|
| +The default dispatcher discovers the page handler by matching path_info
|
| +to a hierarchical arrangement of objects, starting at request.app.root.
|
| +"""
|
| +
|
| +import string
|
| +import sys
|
| +import types
|
| +try:
|
| + classtype = (type, types.ClassType)
|
| +except AttributeError:
|
| + classtype = type
|
| +
|
| +import cherrypy
|
| +from cherrypy._cpcompat import set
|
| +
|
| +
|
| +class PageHandler(object):
|
| + """Callable which sets response.body."""
|
| +
|
| + def __init__(self, callable, *args, **kwargs):
|
| + self.callable = callable
|
| + self.args = args
|
| + self.kwargs = kwargs
|
| +
|
| + def __call__(self):
|
| + try:
|
| + return self.callable(*self.args, **self.kwargs)
|
| + except TypeError:
|
| + x = sys.exc_info()[1]
|
| + try:
|
| + test_callable_spec(self.callable, self.args, self.kwargs)
|
| + except cherrypy.HTTPError:
|
| + raise sys.exc_info()[1]
|
| + except:
|
| + raise x
|
| + raise
|
| +
|
| +
|
| +def test_callable_spec(callable, callable_args, callable_kwargs):
|
| + """
|
| + Inspect callable and test to see if the given args are suitable for it.
|
| +
|
| + When an error occurs during the handler's invoking stage there are 2
|
| + erroneous cases:
|
| + 1. Too many parameters passed to a function which doesn't define
|
| + one of *args or **kwargs.
|
| + 2. Too little parameters are passed to the function.
|
| +
|
| + There are 3 sources of parameters to a cherrypy handler.
|
| + 1. query string parameters are passed as keyword parameters to the handler.
|
| + 2. body parameters are also passed as keyword parameters.
|
| + 3. when partial matching occurs, the final path atoms are passed as
|
| + positional args.
|
| + Both the query string and path atoms are part of the URI. If they are
|
| + incorrect, then a 404 Not Found should be raised. Conversely the body
|
| + parameters are part of the request; if they are invalid a 400 Bad Request.
|
| + """
|
| + show_mismatched_params = getattr(
|
| + cherrypy.serving.request, 'show_mismatched_params', False)
|
| + try:
|
| + (args, varargs, varkw, defaults) = inspect.getargspec(callable)
|
| + except TypeError:
|
| + if isinstance(callable, object) and hasattr(callable, '__call__'):
|
| + (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
|
| + else:
|
| + # If it wasn't one of our own types, re-raise
|
| + # the original error
|
| + raise
|
| +
|
| + if args and args[0] == 'self':
|
| + args = args[1:]
|
| +
|
| + arg_usage = dict([(arg, 0,) for arg in args])
|
| + vararg_usage = 0
|
| + varkw_usage = 0
|
| + extra_kwargs = set()
|
| +
|
| + for i, value in enumerate(callable_args):
|
| + try:
|
| + arg_usage[args[i]] += 1
|
| + except IndexError:
|
| + vararg_usage += 1
|
| +
|
| + for key in callable_kwargs.keys():
|
| + try:
|
| + arg_usage[key] += 1
|
| + except KeyError:
|
| + varkw_usage += 1
|
| + extra_kwargs.add(key)
|
| +
|
| + # figure out which args have defaults.
|
| + args_with_defaults = args[-len(defaults or []):]
|
| + for i, val in enumerate(defaults or []):
|
| + # Defaults take effect only when the arg hasn't been used yet.
|
| + if arg_usage[args_with_defaults[i]] == 0:
|
| + arg_usage[args_with_defaults[i]] += 1
|
| +
|
| + missing_args = []
|
| + multiple_args = []
|
| + for key, usage in arg_usage.items():
|
| + if usage == 0:
|
| + missing_args.append(key)
|
| + elif usage > 1:
|
| + multiple_args.append(key)
|
| +
|
| + if missing_args:
|
| + # In the case where the method allows body arguments
|
| + # there are 3 potential errors:
|
| + # 1. not enough query string parameters -> 404
|
| + # 2. not enough body parameters -> 400
|
| + # 3. not enough path parts (partial matches) -> 404
|
| + #
|
| + # We can't actually tell which case it is,
|
| + # so I'm raising a 404 because that covers 2/3 of the
|
| + # possibilities
|
| + #
|
| + # In the case where the method does not allow body
|
| + # arguments it's definitely a 404.
|
| + message = None
|
| + if show_mismatched_params:
|
| + message="Missing parameters: %s" % ",".join(missing_args)
|
| + raise cherrypy.HTTPError(404, message=message)
|
| +
|
| + # the extra positional arguments come from the path - 404 Not Found
|
| + if not varargs and vararg_usage > 0:
|
| + raise cherrypy.HTTPError(404)
|
| +
|
| + body_params = cherrypy.serving.request.body.params or {}
|
| + body_params = set(body_params.keys())
|
| + qs_params = set(callable_kwargs.keys()) - body_params
|
| +
|
| + if multiple_args:
|
| + if qs_params.intersection(set(multiple_args)):
|
| + # If any of the multiple parameters came from the query string then
|
| + # it's a 404 Not Found
|
| + error = 404
|
| + else:
|
| + # Otherwise it's a 400 Bad Request
|
| + error = 400
|
| +
|
| + message = None
|
| + if show_mismatched_params:
|
| + message="Multiple values for parameters: "\
|
| + "%s" % ",".join(multiple_args)
|
| + raise cherrypy.HTTPError(error, message=message)
|
| +
|
| + if not varkw and varkw_usage > 0:
|
| +
|
| + # If there were extra query string parameters, it's a 404 Not Found
|
| + extra_qs_params = set(qs_params).intersection(extra_kwargs)
|
| + if extra_qs_params:
|
| + message = None
|
| + if show_mismatched_params:
|
| + message="Unexpected query string "\
|
| + "parameters: %s" % ", ".join(extra_qs_params)
|
| + raise cherrypy.HTTPError(404, message=message)
|
| +
|
| + # If there were any extra body parameters, it's a 400 Not Found
|
| + extra_body_params = set(body_params).intersection(extra_kwargs)
|
| + if extra_body_params:
|
| + message = None
|
| + if show_mismatched_params:
|
| + message="Unexpected body parameters: "\
|
| + "%s" % ", ".join(extra_body_params)
|
| + raise cherrypy.HTTPError(400, message=message)
|
| +
|
| +
|
| +try:
|
| + import inspect
|
| +except ImportError:
|
| + test_callable_spec = lambda callable, args, kwargs: None
|
| +
|
| +
|
| +
|
| +class LateParamPageHandler(PageHandler):
|
| + """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
|
| + takes that into account, and allows request.params to be 'bound late'
|
| + (it's more complicated than that, but that's the effect).
|
| + """
|
| +
|
| + def _get_kwargs(self):
|
| + kwargs = cherrypy.serving.request.params.copy()
|
| + if self._kwargs:
|
| + kwargs.update(self._kwargs)
|
| + return kwargs
|
| +
|
| + def _set_kwargs(self, kwargs):
|
| + self._kwargs = kwargs
|
| +
|
| + kwargs = property(_get_kwargs, _set_kwargs,
|
| + doc='page handler kwargs (with '
|
| + 'cherrypy.request.params copied in)')
|
| +
|
| +
|
| +if sys.version_info < (3, 0):
|
| + punctuation_to_underscores = string.maketrans(
|
| + string.punctuation, '_' * len(string.punctuation))
|
| + def validate_translator(t):
|
| + if not isinstance(t, str) or len(t) != 256:
|
| + raise ValueError("The translate argument must be a str of len 256.")
|
| +else:
|
| + punctuation_to_underscores = str.maketrans(
|
| + string.punctuation, '_' * len(string.punctuation))
|
| + def validate_translator(t):
|
| + if not isinstance(t, dict):
|
| + raise ValueError("The translate argument must be a dict.")
|
| +
|
| +class Dispatcher(object):
|
| + """CherryPy Dispatcher which walks a tree of objects to find a handler.
|
| +
|
| + The tree is rooted at cherrypy.request.app.root, and each hierarchical
|
| + component in the path_info argument is matched to a corresponding nested
|
| + attribute of the root object. Matching handlers must have an 'exposed'
|
| + attribute which evaluates to True. The special method name "index"
|
| + matches a URI which ends in a slash ("/"). The special method name
|
| + "default" may match a portion of the path_info (but only when no longer
|
| + substring of the path_info matches some other object).
|
| +
|
| + This is the default, built-in dispatcher for CherryPy.
|
| + """
|
| +
|
| + dispatch_method_name = '_cp_dispatch'
|
| + """
|
| + The name of the dispatch method that nodes may optionally implement
|
| + to provide their own dynamic dispatch algorithm.
|
| + """
|
| +
|
| + def __init__(self, dispatch_method_name=None,
|
| + translate=punctuation_to_underscores):
|
| + validate_translator(translate)
|
| + self.translate = translate
|
| + if dispatch_method_name:
|
| + self.dispatch_method_name = dispatch_method_name
|
| +
|
| + def __call__(self, path_info):
|
| + """Set handler and config for the current request."""
|
| + request = cherrypy.serving.request
|
| + func, vpath = self.find_handler(path_info)
|
| +
|
| + if func:
|
| + # Decode any leftover %2F in the virtual_path atoms.
|
| + vpath = [x.replace("%2F", "/") for x in vpath]
|
| + request.handler = LateParamPageHandler(func, *vpath)
|
| + else:
|
| + request.handler = cherrypy.NotFound()
|
| +
|
| + def find_handler(self, path):
|
| + """Return the appropriate page handler, plus any virtual path.
|
| +
|
| + This will return two objects. The first will be a callable,
|
| + which can be used to generate page output. Any parameters from
|
| + the query string or request body will be sent to that callable
|
| + as keyword arguments.
|
| +
|
| + The callable is found by traversing the application's tree,
|
| + starting from cherrypy.request.app.root, and matching path
|
| + components to successive objects in the tree. For example, the
|
| + URL "/path/to/handler" might return root.path.to.handler.
|
| +
|
| + The second object returned will be a list of names which are
|
| + 'virtual path' components: parts of the URL which are dynamic,
|
| + and were not used when looking up the handler.
|
| + These virtual path components are passed to the handler as
|
| + positional arguments.
|
| + """
|
| + request = cherrypy.serving.request
|
| + app = request.app
|
| + root = app.root
|
| + dispatch_name = self.dispatch_method_name
|
| +
|
| + # Get config for the root object/path.
|
| + fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
|
| + fullpath_len = len(fullpath)
|
| + segleft = fullpath_len
|
| + nodeconf = {}
|
| + if hasattr(root, "_cp_config"):
|
| + nodeconf.update(root._cp_config)
|
| + if "/" in app.config:
|
| + nodeconf.update(app.config["/"])
|
| + object_trail = [['root', root, nodeconf, segleft]]
|
| +
|
| + node = root
|
| + iternames = fullpath[:]
|
| + while iternames:
|
| + name = iternames[0]
|
| + # map to legal Python identifiers (e.g. replace '.' with '_')
|
| + objname = name.translate(self.translate)
|
| +
|
| + nodeconf = {}
|
| + subnode = getattr(node, objname, None)
|
| + pre_len = len(iternames)
|
| + if subnode is None:
|
| + dispatch = getattr(node, dispatch_name, None)
|
| + if dispatch and hasattr(dispatch, '__call__') and not \
|
| + getattr(dispatch, 'exposed', False) and \
|
| + pre_len > 1:
|
| + #Don't expose the hidden 'index' token to _cp_dispatch
|
| + #We skip this if pre_len == 1 since it makes no sense
|
| + #to call a dispatcher when we have no tokens left.
|
| + index_name = iternames.pop()
|
| + subnode = dispatch(vpath=iternames)
|
| + iternames.append(index_name)
|
| + else:
|
| + #We didn't find a path, but keep processing in case there
|
| + #is a default() handler.
|
| + iternames.pop(0)
|
| + else:
|
| + #We found the path, remove the vpath entry
|
| + iternames.pop(0)
|
| + segleft = len(iternames)
|
| + if segleft > pre_len:
|
| + #No path segment was removed. Raise an error.
|
| + raise cherrypy.CherryPyException(
|
| + "A vpath segment was added. Custom dispatchers may only "
|
| + + "remove elements. While trying to process "
|
| + + "{0} in {1}".format(name, fullpath)
|
| + )
|
| + elif segleft == pre_len:
|
| + #Assume that the handler used the current path segment, but
|
| + #did not pop it. This allows things like
|
| + #return getattr(self, vpath[0], None)
|
| + iternames.pop(0)
|
| + segleft -= 1
|
| + node = subnode
|
| +
|
| + if node is not None:
|
| + # Get _cp_config attached to this node.
|
| + if hasattr(node, "_cp_config"):
|
| + nodeconf.update(node._cp_config)
|
| +
|
| + # Mix in values from app.config for this path.
|
| + existing_len = fullpath_len - pre_len
|
| + if existing_len != 0:
|
| + curpath = '/' + '/'.join(fullpath[0:existing_len])
|
| + else:
|
| + curpath = ''
|
| + new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
|
| + for seg in new_segs:
|
| + curpath += '/' + seg
|
| + if curpath in app.config:
|
| + nodeconf.update(app.config[curpath])
|
| +
|
| + object_trail.append([name, node, nodeconf, segleft])
|
| +
|
| + def set_conf():
|
| + """Collapse all object_trail config into cherrypy.request.config."""
|
| + base = cherrypy.config.copy()
|
| + # Note that we merge the config from each node
|
| + # even if that node was None.
|
| + for name, obj, conf, segleft in object_trail:
|
| + base.update(conf)
|
| + if 'tools.staticdir.dir' in conf:
|
| + base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
|
| + return base
|
| +
|
| + # Try successive objects (reverse order)
|
| + num_candidates = len(object_trail) - 1
|
| + for i in range(num_candidates, -1, -1):
|
| +
|
| + name, candidate, nodeconf, segleft = object_trail[i]
|
| + if candidate is None:
|
| + continue
|
| +
|
| + # Try a "default" method on the current leaf.
|
| + if hasattr(candidate, "default"):
|
| + defhandler = candidate.default
|
| + if getattr(defhandler, 'exposed', False):
|
| + # Insert any extra _cp_config from the default handler.
|
| + conf = getattr(defhandler, "_cp_config", {})
|
| + object_trail.insert(i+1, ["default", defhandler, conf, segleft])
|
| + request.config = set_conf()
|
| + # See http://www.cherrypy.org/ticket/613
|
| + request.is_index = path.endswith("/")
|
| + return defhandler, fullpath[fullpath_len - segleft:-1]
|
| +
|
| + # Uncomment the next line to restrict positional params to "default".
|
| + # if i < num_candidates - 2: continue
|
| +
|
| + # Try the current leaf.
|
| + if getattr(candidate, 'exposed', False):
|
| + request.config = set_conf()
|
| + if i == num_candidates:
|
| + # We found the extra ".index". Mark request so tools
|
| + # can redirect if path_info has no trailing slash.
|
| + request.is_index = True
|
| + else:
|
| + # We're not at an 'index' handler. Mark request so tools
|
| + # can redirect if path_info has NO trailing slash.
|
| + # Note that this also includes handlers which take
|
| + # positional parameters (virtual paths).
|
| + request.is_index = False
|
| + return candidate, fullpath[fullpath_len - segleft:-1]
|
| +
|
| + # We didn't find anything
|
| + request.config = set_conf()
|
| + return None, []
|
| +
|
| +
|
| +class MethodDispatcher(Dispatcher):
|
| + """Additional dispatch based on cherrypy.request.method.upper().
|
| +
|
| + Methods named GET, POST, etc will be called on an exposed class.
|
| + The method names must be all caps; the appropriate Allow header
|
| + will be output showing all capitalized method names as allowable
|
| + HTTP verbs.
|
| +
|
| + Note that the containing class must be exposed, not the methods.
|
| + """
|
| +
|
| + def __call__(self, path_info):
|
| + """Set handler and config for the current request."""
|
| + request = cherrypy.serving.request
|
| + resource, vpath = self.find_handler(path_info)
|
| +
|
| + if resource:
|
| + # Set Allow header
|
| + avail = [m for m in dir(resource) if m.isupper()]
|
| + if "GET" in avail and "HEAD" not in avail:
|
| + avail.append("HEAD")
|
| + avail.sort()
|
| + cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
|
| +
|
| + # Find the subhandler
|
| + meth = request.method.upper()
|
| + func = getattr(resource, meth, None)
|
| + if func is None and meth == "HEAD":
|
| + func = getattr(resource, "GET", None)
|
| + if func:
|
| + # Grab any _cp_config on the subhandler.
|
| + if hasattr(func, "_cp_config"):
|
| + request.config.update(func._cp_config)
|
| +
|
| + # Decode any leftover %2F in the virtual_path atoms.
|
| + vpath = [x.replace("%2F", "/") for x in vpath]
|
| + request.handler = LateParamPageHandler(func, *vpath)
|
| + else:
|
| + request.handler = cherrypy.HTTPError(405)
|
| + else:
|
| + request.handler = cherrypy.NotFound()
|
| +
|
| +
|
| +class RoutesDispatcher(object):
|
| + """A Routes based dispatcher for CherryPy."""
|
| +
|
| + def __init__(self, full_result=False):
|
| + """
|
| + Routes dispatcher
|
| +
|
| + Set full_result to True if you wish the controller
|
| + and the action to be passed on to the page handler
|
| + parameters. By default they won't be.
|
| + """
|
| + import routes
|
| + self.full_result = full_result
|
| + self.controllers = {}
|
| + self.mapper = routes.Mapper()
|
| + self.mapper.controller_scan = self.controllers.keys
|
| +
|
| + def connect(self, name, route, controller, **kwargs):
|
| + self.controllers[name] = controller
|
| + self.mapper.connect(name, route, controller=name, **kwargs)
|
| +
|
| + def redirect(self, url):
|
| + raise cherrypy.HTTPRedirect(url)
|
| +
|
| + def __call__(self, path_info):
|
| + """Set handler and config for the current request."""
|
| + func = self.find_handler(path_info)
|
| + if func:
|
| + cherrypy.serving.request.handler = LateParamPageHandler(func)
|
| + else:
|
| + cherrypy.serving.request.handler = cherrypy.NotFound()
|
| +
|
| + def find_handler(self, path_info):
|
| + """Find the right page handler, and set request.config."""
|
| + import routes
|
| +
|
| + request = cherrypy.serving.request
|
| +
|
| + config = routes.request_config()
|
| + config.mapper = self.mapper
|
| + if hasattr(request, 'wsgi_environ'):
|
| + config.environ = request.wsgi_environ
|
| + config.host = request.headers.get('Host', None)
|
| + config.protocol = request.scheme
|
| + config.redirect = self.redirect
|
| +
|
| + result = self.mapper.match(path_info)
|
| +
|
| + config.mapper_dict = result
|
| + params = {}
|
| + if result:
|
| + params = result.copy()
|
| + if not self.full_result:
|
| + params.pop('controller', None)
|
| + params.pop('action', None)
|
| + request.params.update(params)
|
| +
|
| + # Get config for the root object/path.
|
| + request.config = base = cherrypy.config.copy()
|
| + curpath = ""
|
| +
|
| + def merge(nodeconf):
|
| + if 'tools.staticdir.dir' in nodeconf:
|
| + nodeconf['tools.staticdir.section'] = curpath or "/"
|
| + base.update(nodeconf)
|
| +
|
| + app = request.app
|
| + root = app.root
|
| + if hasattr(root, "_cp_config"):
|
| + merge(root._cp_config)
|
| + if "/" in app.config:
|
| + merge(app.config["/"])
|
| +
|
| + # Mix in values from app.config.
|
| + atoms = [x for x in path_info.split("/") if x]
|
| + if atoms:
|
| + last = atoms.pop()
|
| + else:
|
| + last = None
|
| + for atom in atoms:
|
| + curpath = "/".join((curpath, atom))
|
| + if curpath in app.config:
|
| + merge(app.config[curpath])
|
| +
|
| + handler = None
|
| + if result:
|
| + controller = result.get('controller')
|
| + controller = self.controllers.get(controller, controller)
|
| + if controller:
|
| + if isinstance(controller, classtype):
|
| + controller = controller()
|
| + # Get config from the controller.
|
| + if hasattr(controller, "_cp_config"):
|
| + merge(controller._cp_config)
|
| +
|
| + action = result.get('action')
|
| + if action is not None:
|
| + handler = getattr(controller, action, None)
|
| + # Get config from the handler
|
| + if hasattr(handler, "_cp_config"):
|
| + merge(handler._cp_config)
|
| + else:
|
| + handler = controller
|
| +
|
| + # Do the last path atom here so it can
|
| + # override the controller's _cp_config.
|
| + if last:
|
| + curpath = "/".join((curpath, last))
|
| + if curpath in app.config:
|
| + merge(app.config[curpath])
|
| +
|
| + return handler
|
| +
|
| +
|
| +def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
|
| + from cherrypy.lib import xmlrpcutil
|
| + def xmlrpc_dispatch(path_info):
|
| + path_info = xmlrpcutil.patched_path(path_info)
|
| + return next_dispatcher(path_info)
|
| + return xmlrpc_dispatch
|
| +
|
| +
|
| +def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
|
| + """
|
| + Select a different handler based on the Host header.
|
| +
|
| + This can be useful when running multiple sites within one CP server.
|
| + It allows several domains to point to different parts of a single
|
| + website structure. For example::
|
| +
|
| + http://www.domain.example -> root
|
| + http://www.domain2.example -> root/domain2/
|
| + http://www.domain2.example:443 -> root/secure
|
| +
|
| + can be accomplished via the following config::
|
| +
|
| + [/]
|
| + request.dispatch = cherrypy.dispatch.VirtualHost(
|
| + **{'www.domain2.example': '/domain2',
|
| + 'www.domain2.example:443': '/secure',
|
| + })
|
| +
|
| + next_dispatcher
|
| + The next dispatcher object in the dispatch chain.
|
| + The VirtualHost dispatcher adds a prefix to the URL and calls
|
| + another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
|
| +
|
| + 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.
|
| +
|
| + ``**domains``
|
| + A dict of {host header value: virtual prefix} pairs.
|
| + The incoming "Host" request header is looked up in this dict,
|
| + and, if a match is found, the corresponding "virtual prefix"
|
| + value will be prepended to the URL path before calling the
|
| + next dispatcher. Note that you often need separate entries
|
| + for "example.com" and "www.example.com". In addition, "Host"
|
| + headers may contain the port number.
|
| + """
|
| + from cherrypy.lib import httputil
|
| + def vhost_dispatch(path_info):
|
| + request = cherrypy.serving.request
|
| + header = request.headers.get
|
| +
|
| + domain = header('Host', '')
|
| + if use_x_forwarded_host:
|
| + domain = header("X-Forwarded-Host", domain)
|
| +
|
| + prefix = domains.get(domain, "")
|
| + if prefix:
|
| + path_info = httputil.urljoin(prefix, path_info)
|
| +
|
| + result = next_dispatcher(path_info)
|
| +
|
| + # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
|
| + section = request.config.get('tools.staticdir.section')
|
| + if section:
|
| + section = section[len(prefix):]
|
| + request.config['tools.staticdir.section'] = section
|
| +
|
| + return result
|
| + return vhost_dispatch
|
| +
|
|
|
| Property changes on: third_party/cherrypy/_cpdispatch.py
|
| ___________________________________________________________________
|
| Added: svn:eol-style
|
| + LF
|
|
|
|
|