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