Package cherrypy :: Module _cpdispatch
[hide private]
[frames] | no frames]

Source Code for Module cherrypy._cpdispatch

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