Fixed python_path problem.
[smonitor.git] / lib / cherrypy / _cpdispatch.py
blob7250ac92a59940cc2670f4811c3772641d14ae5d
1 """CherryPy dispatchers.
3 A 'dispatcher' is the object which looks up the 'page handler' callable
4 and collects config for the current request based on the path_info, other
5 request attributes, and the application architecture. The core calls the
6 dispatcher as early as possible, passing it a 'path_info' argument.
8 The default dispatcher discovers the page handler by matching path_info
9 to a hierarchical arrangement of objects, starting at request.app.root.
10 """
12 import string
13 import sys
14 import types
16 import cherrypy
19 class PageHandler(object):
20 """Callable which sets response.body."""
22 def __init__(self, callable, *args, **kwargs):
23 self.callable = callable
24 self.args = args
25 self.kwargs = kwargs
27 def __call__(self):
28 try:
29 return self.callable(*self.args, **self.kwargs)
30 except TypeError:
31 x = sys.exc_info()[1]
32 try:
33 test_callable_spec(self.callable, self.args, self.kwargs)
34 except cherrypy.HTTPError:
35 raise sys.exc_info()[1]
36 except:
37 raise x
38 raise
41 def test_callable_spec(callable, callable_args, callable_kwargs):
42 """
43 Inspect callable and test to see if the given args are suitable for it.
45 When an error occurs during the handler's invoking stage there are 2
46 erroneous cases:
47 1. Too many parameters passed to a function which doesn't define
48 one of *args or **kwargs.
49 2. Too little parameters are passed to the function.
51 There are 3 sources of parameters to a cherrypy handler.
52 1. query string parameters are passed as keyword parameters to the handler.
53 2. body parameters are also passed as keyword parameters.
54 3. when partial matching occurs, the final path atoms are passed as
55 positional args.
56 Both the query string and path atoms are part of the URI. If they are
57 incorrect, then a 404 Not Found should be raised. Conversely the body
58 parameters are part of the request; if they are invalid a 400 Bad Request.
59 """
60 show_mismatched_params = getattr(
61 cherrypy.serving.request, 'show_mismatched_params', False)
62 try:
63 (args, varargs, varkw, defaults) = inspect.getargspec(callable)
64 except TypeError:
65 if isinstance(callable, object) and hasattr(callable, '__call__'):
66 (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
67 else:
68 # If it wasn't one of our own types, re-raise
69 # the original error
70 raise
72 if args and args[0] == 'self':
73 args = args[1:]
75 arg_usage = dict([(arg, 0,) for arg in args])
76 vararg_usage = 0
77 varkw_usage = 0
78 extra_kwargs = set()
80 for i, value in enumerate(callable_args):
81 try:
82 arg_usage[args[i]] += 1
83 except IndexError:
84 vararg_usage += 1
86 for key in callable_kwargs.keys():
87 try:
88 arg_usage[key] += 1
89 except KeyError:
90 varkw_usage += 1
91 extra_kwargs.add(key)
93 # figure out which args have defaults.
94 args_with_defaults = args[-len(defaults or []):]
95 for i, val in enumerate(defaults or []):
96 # Defaults take effect only when the arg hasn't been used yet.
97 if arg_usage[args_with_defaults[i]] == 0:
98 arg_usage[args_with_defaults[i]] += 1
100 missing_args = []
101 multiple_args = []
102 for key, usage in arg_usage.items():
103 if usage == 0:
104 missing_args.append(key)
105 elif usage > 1:
106 multiple_args.append(key)
108 if missing_args:
109 # In the case where the method allows body arguments
110 # there are 3 potential errors:
111 # 1. not enough query string parameters -> 404
112 # 2. not enough body parameters -> 400
113 # 3. not enough path parts (partial matches) -> 404
115 # We can't actually tell which case it is,
116 # so I'm raising a 404 because that covers 2/3 of the
117 # possibilities
119 # In the case where the method does not allow body
120 # arguments it's definitely a 404.
121 message = None
122 if show_mismatched_params:
123 message="Missing parameters: %s" % ",".join(missing_args)
124 raise cherrypy.HTTPError(404, message=message)
126 # the extra positional arguments come from the path - 404 Not Found
127 if not varargs and vararg_usage > 0:
128 raise cherrypy.HTTPError(404)
130 body_params = cherrypy.serving.request.body.params or {}
131 body_params = set(body_params.keys())
132 qs_params = set(callable_kwargs.keys()) - body_params
134 if multiple_args:
135 if qs_params.intersection(set(multiple_args)):
136 # If any of the multiple parameters came from the query string then
137 # it's a 404 Not Found
138 error = 404
139 else:
140 # Otherwise it's a 400 Bad Request
141 error = 400
143 message = None
144 if show_mismatched_params:
145 message="Multiple values for parameters: "\
146 "%s" % ",".join(multiple_args)
147 raise cherrypy.HTTPError(error, message=message)
149 if not varkw and varkw_usage > 0:
151 # If there were extra query string parameters, it's a 404 Not Found
152 extra_qs_params = set(qs_params).intersection(extra_kwargs)
153 if extra_qs_params:
154 message = None
155 if show_mismatched_params:
156 message="Unexpected query string "\
157 "parameters: %s" % ", ".join(extra_qs_params)
158 raise cherrypy.HTTPError(404, message=message)
160 # If there were any extra body parameters, it's a 400 Not Found
161 extra_body_params = set(body_params).intersection(extra_kwargs)
162 if extra_body_params:
163 message = None
164 if show_mismatched_params:
165 message="Unexpected body parameters: "\
166 "%s" % ", ".join(extra_body_params)
167 raise cherrypy.HTTPError(400, message=message)
170 try:
171 import inspect
172 except ImportError:
173 test_callable_spec = lambda callable, args, kwargs: None
177 class LateParamPageHandler(PageHandler):
178 """When passing cherrypy.request.params to the page handler, we do not
179 want to capture that dict too early; we want to give tools like the
180 decoding tool a chance to modify the params dict in-between the lookup
181 of the handler and the actual calling of the handler. This subclass
182 takes that into account, and allows request.params to be 'bound late'
183 (it's more complicated than that, but that's the effect).
186 def _get_kwargs(self):
187 kwargs = cherrypy.serving.request.params.copy()
188 if self._kwargs:
189 kwargs.update(self._kwargs)
190 return kwargs
192 def _set_kwargs(self, kwargs):
193 self._kwargs = kwargs
195 kwargs = property(_get_kwargs, _set_kwargs,
196 doc='page handler kwargs (with '
197 'cherrypy.request.params copied in)')
200 punctuation_to_underscores = string.maketrans(
201 string.punctuation, '_' * len(string.punctuation))
203 class Dispatcher(object):
204 """CherryPy Dispatcher which walks a tree of objects to find a handler.
206 The tree is rooted at cherrypy.request.app.root, and each hierarchical
207 component in the path_info argument is matched to a corresponding nested
208 attribute of the root object. Matching handlers must have an 'exposed'
209 attribute which evaluates to True. The special method name "index"
210 matches a URI which ends in a slash ("/"). The special method name
211 "default" may match a portion of the path_info (but only when no longer
212 substring of the path_info matches some other object).
214 This is the default, built-in dispatcher for CherryPy.
217 dispatch_method_name = '_cp_dispatch'
219 The name of the dispatch method that nodes may optionally implement
220 to provide their own dynamic dispatch algorithm.
223 def __init__(self, dispatch_method_name=None,
224 translate=punctuation_to_underscores):
225 if not isinstance(translate, str) or len(translate) != 256:
226 raise ValueError("The translate argument must be a str of len 256.")
227 self.translate = translate
228 if dispatch_method_name:
229 self.dispatch_method_name = dispatch_method_name
231 def __call__(self, path_info):
232 """Set handler and config for the current request."""
233 request = cherrypy.serving.request
234 func, vpath = self.find_handler(path_info)
236 if func:
237 # Decode any leftover %2F in the virtual_path atoms.
238 vpath = [x.replace("%2F", "/") for x in vpath]
239 request.handler = LateParamPageHandler(func, *vpath)
240 else:
241 request.handler = cherrypy.NotFound()
243 def find_handler(self, path):
244 """Return the appropriate page handler, plus any virtual path.
246 This will return two objects. The first will be a callable,
247 which can be used to generate page output. Any parameters from
248 the query string or request body will be sent to that callable
249 as keyword arguments.
251 The callable is found by traversing the application's tree,
252 starting from cherrypy.request.app.root, and matching path
253 components to successive objects in the tree. For example, the
254 URL "/path/to/handler" might return root.path.to.handler.
256 The second object returned will be a list of names which are
257 'virtual path' components: parts of the URL which are dynamic,
258 and were not used when looking up the handler.
259 These virtual path components are passed to the handler as
260 positional arguments.
262 request = cherrypy.serving.request
263 app = request.app
264 root = app.root
265 dispatch_name = self.dispatch_method_name
267 # Get config for the root object/path.
268 fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
269 fullpath_len = len(fullpath)
270 segleft = fullpath_len
271 nodeconf = {}
272 if hasattr(root, "_cp_config"):
273 nodeconf.update(root._cp_config)
274 if "/" in app.config:
275 nodeconf.update(app.config["/"])
276 object_trail = [['root', root, nodeconf, segleft]]
278 node = root
279 iternames = fullpath[:]
280 while iternames:
281 name = iternames[0]
282 # map to legal Python identifiers (e.g. replace '.' with '_')
283 objname = name.translate(self.translate)
285 nodeconf = {}
286 subnode = getattr(node, objname, None)
287 pre_len = len(iternames)
288 if subnode is None:
289 dispatch = getattr(node, dispatch_name, None)
290 if dispatch and hasattr(dispatch, '__call__') and not \
291 getattr(dispatch, 'exposed', False) and \
292 pre_len > 1:
293 #Don't expose the hidden 'index' token to _cp_dispatch
294 #We skip this if pre_len == 1 since it makes no sense
295 #to call a dispatcher when we have no tokens left.
296 index_name = iternames.pop()
297 subnode = dispatch(vpath=iternames)
298 iternames.append(index_name)
299 else:
300 #We didn't find a path, but keep processing in case there
301 #is a default() handler.
302 iternames.pop(0)
303 else:
304 #We found the path, remove the vpath entry
305 iternames.pop(0)
306 segleft = len(iternames)
307 if segleft > pre_len:
308 #No path segment was removed. Raise an error.
309 raise cherrypy.CherryPyException(
310 "A vpath segment was added. Custom dispatchers may only "
311 + "remove elements. While trying to process "
312 + "{0} in {1}".format(name, fullpath)
314 elif segleft == pre_len:
315 #Assume that the handler used the current path segment, but
316 #did not pop it. This allows things like
317 #return getattr(self, vpath[0], None)
318 iternames.pop(0)
319 segleft -= 1
320 node = subnode
322 if node is not None:
323 # Get _cp_config attached to this node.
324 if hasattr(node, "_cp_config"):
325 nodeconf.update(node._cp_config)
327 # Mix in values from app.config for this path.
328 existing_len = fullpath_len - pre_len
329 if existing_len != 0:
330 curpath = '/' + '/'.join(fullpath[0:existing_len])
331 else:
332 curpath = ''
333 new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
334 for seg in new_segs:
335 curpath += '/' + seg
336 if curpath in app.config:
337 nodeconf.update(app.config[curpath])
339 object_trail.append([name, node, nodeconf, segleft])
341 def set_conf():
342 """Collapse all object_trail config into cherrypy.request.config."""
343 base = cherrypy.config.copy()
344 # Note that we merge the config from each node
345 # even if that node was None.
346 for name, obj, conf, segleft in object_trail:
347 base.update(conf)
348 if 'tools.staticdir.dir' in conf:
349 base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
350 return base
352 # Try successive objects (reverse order)
353 num_candidates = len(object_trail) - 1
354 for i in range(num_candidates, -1, -1):
356 name, candidate, nodeconf, segleft = object_trail[i]
357 if candidate is None:
358 continue
360 # Try a "default" method on the current leaf.
361 if hasattr(candidate, "default"):
362 defhandler = candidate.default
363 if getattr(defhandler, 'exposed', False):
364 # Insert any extra _cp_config from the default handler.
365 conf = getattr(defhandler, "_cp_config", {})
366 object_trail.insert(i+1, ["default", defhandler, conf, segleft])
367 request.config = set_conf()
368 # See http://www.cherrypy.org/ticket/613
369 request.is_index = path.endswith("/")
370 return defhandler, fullpath[fullpath_len - segleft:-1]
372 # Uncomment the next line to restrict positional params to "default".
373 # if i < num_candidates - 2: continue
375 # Try the current leaf.
376 if getattr(candidate, 'exposed', False):
377 request.config = set_conf()
378 if i == num_candidates:
379 # We found the extra ".index". Mark request so tools
380 # can redirect if path_info has no trailing slash.
381 request.is_index = True
382 else:
383 # We're not at an 'index' handler. Mark request so tools
384 # can redirect if path_info has NO trailing slash.
385 # Note that this also includes handlers which take
386 # positional parameters (virtual paths).
387 request.is_index = False
388 return candidate, fullpath[fullpath_len - segleft:-1]
390 # We didn't find anything
391 request.config = set_conf()
392 return None, []
395 class MethodDispatcher(Dispatcher):
396 """Additional dispatch based on cherrypy.request.method.upper().
398 Methods named GET, POST, etc will be called on an exposed class.
399 The method names must be all caps; the appropriate Allow header
400 will be output showing all capitalized method names as allowable
401 HTTP verbs.
403 Note that the containing class must be exposed, not the methods.
406 def __call__(self, path_info):
407 """Set handler and config for the current request."""
408 request = cherrypy.serving.request
409 resource, vpath = self.find_handler(path_info)
411 if resource:
412 # Set Allow header
413 avail = [m for m in dir(resource) if m.isupper()]
414 if "GET" in avail and "HEAD" not in avail:
415 avail.append("HEAD")
416 avail.sort()
417 cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
419 # Find the subhandler
420 meth = request.method.upper()
421 func = getattr(resource, meth, None)
422 if func is None and meth == "HEAD":
423 func = getattr(resource, "GET", None)
424 if func:
425 # Grab any _cp_config on the subhandler.
426 if hasattr(func, "_cp_config"):
427 request.config.update(func._cp_config)
429 # Decode any leftover %2F in the virtual_path atoms.
430 vpath = [x.replace("%2F", "/") for x in vpath]
431 request.handler = LateParamPageHandler(func, *vpath)
432 else:
433 request.handler = cherrypy.HTTPError(405)
434 else:
435 request.handler = cherrypy.NotFound()
438 class RoutesDispatcher(object):
439 """A Routes based dispatcher for CherryPy."""
441 def __init__(self, full_result=False):
443 Routes dispatcher
445 Set full_result to True if you wish the controller
446 and the action to be passed on to the page handler
447 parameters. By default they won't be.
449 import routes
450 self.full_result = full_result
451 self.controllers = {}
452 self.mapper = routes.Mapper()
453 self.mapper.controller_scan = self.controllers.keys
455 def connect(self, name, route, controller, **kwargs):
456 self.controllers[name] = controller
457 self.mapper.connect(name, route, controller=name, **kwargs)
459 def redirect(self, url):
460 raise cherrypy.HTTPRedirect(url)
462 def __call__(self, path_info):
463 """Set handler and config for the current request."""
464 func = self.find_handler(path_info)
465 if func:
466 cherrypy.serving.request.handler = LateParamPageHandler(func)
467 else:
468 cherrypy.serving.request.handler = cherrypy.NotFound()
470 def find_handler(self, path_info):
471 """Find the right page handler, and set request.config."""
472 import routes
474 request = cherrypy.serving.request
476 config = routes.request_config()
477 config.mapper = self.mapper
478 if hasattr(request, 'wsgi_environ'):
479 config.environ = request.wsgi_environ
480 config.host = request.headers.get('Host', None)
481 config.protocol = request.scheme
482 config.redirect = self.redirect
484 result = self.mapper.match(path_info)
486 config.mapper_dict = result
487 params = {}
488 if result:
489 params = result.copy()
490 if not self.full_result:
491 params.pop('controller', None)
492 params.pop('action', None)
493 request.params.update(params)
495 # Get config for the root object/path.
496 request.config = base = cherrypy.config.copy()
497 curpath = ""
499 def merge(nodeconf):
500 if 'tools.staticdir.dir' in nodeconf:
501 nodeconf['tools.staticdir.section'] = curpath or "/"
502 base.update(nodeconf)
504 app = request.app
505 root = app.root
506 if hasattr(root, "_cp_config"):
507 merge(root._cp_config)
508 if "/" in app.config:
509 merge(app.config["/"])
511 # Mix in values from app.config.
512 atoms = [x for x in path_info.split("/") if x]
513 if atoms:
514 last = atoms.pop()
515 else:
516 last = None
517 for atom in atoms:
518 curpath = "/".join((curpath, atom))
519 if curpath in app.config:
520 merge(app.config[curpath])
522 handler = None
523 if result:
524 controller = result.get('controller')
525 controller = self.controllers.get(controller, controller)
526 if controller:
527 if isinstance(controller, (type, types.ClassType)):
528 controller = controller()
529 # Get config from the controller.
530 if hasattr(controller, "_cp_config"):
531 merge(controller._cp_config)
533 action = result.get('action')
534 if action is not None:
535 handler = getattr(controller, action, None)
536 # Get config from the handler
537 if hasattr(handler, "_cp_config"):
538 merge(handler._cp_config)
539 else:
540 handler = controller
542 # Do the last path atom here so it can
543 # override the controller's _cp_config.
544 if last:
545 curpath = "/".join((curpath, last))
546 if curpath in app.config:
547 merge(app.config[curpath])
549 return handler
552 def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
553 from cherrypy.lib import xmlrpc
554 def xmlrpc_dispatch(path_info):
555 path_info = xmlrpc.patched_path(path_info)
556 return next_dispatcher(path_info)
557 return xmlrpc_dispatch
560 def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
562 Select a different handler based on the Host header.
564 This can be useful when running multiple sites within one CP server.
565 It allows several domains to point to different parts of a single
566 website structure. For example::
568 http://www.domain.example -> root
569 http://www.domain2.example -> root/domain2/
570 http://www.domain2.example:443 -> root/secure
572 can be accomplished via the following config::
575 request.dispatch = cherrypy.dispatch.VirtualHost(
576 **{'www.domain2.example': '/domain2',
577 'www.domain2.example:443': '/secure',
580 next_dispatcher
581 The next dispatcher object in the dispatch chain.
582 The VirtualHost dispatcher adds a prefix to the URL and calls
583 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
585 use_x_forwarded_host
586 If True (the default), any "X-Forwarded-Host"
587 request header will be used instead of the "Host" header. This
588 is commonly added by HTTP servers (such as Apache) when proxying.
590 ``**domains``
591 A dict of {host header value: virtual prefix} pairs.
592 The incoming "Host" request header is looked up in this dict,
593 and, if a match is found, the corresponding "virtual prefix"
594 value will be prepended to the URL path before calling the
595 next dispatcher. Note that you often need separate entries
596 for "example.com" and "www.example.com". In addition, "Host"
597 headers may contain the port number.
599 from cherrypy.lib import httputil
600 def vhost_dispatch(path_info):
601 request = cherrypy.serving.request
602 header = request.headers.get
604 domain = header('Host', '')
605 if use_x_forwarded_host:
606 domain = header("X-Forwarded-Host", domain)
608 prefix = domains.get(domain, "")
609 if prefix:
610 path_info = httputil.urljoin(prefix, path_info)
612 result = next_dispatcher(path_info)
614 # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
615 section = request.config.get('tools.staticdir.section')
616 if section:
617 section = section[len(prefix):]
618 request.config['tools.staticdir.section'] = section
620 return result
621 return vhost_dispatch