source file: /Library/Python/2.3/site-packages/CherryPy-3.0.1-py2.3.egg/cherrypy/_cpdispatch.py
file stats: 196 lines, 75 executed: 38.3% covered
   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 cherrypy
  13. 
  14. 
  15. class PageHandler(object):
  16.     """Callable which sets response.body."""
  17. 
  18.     def __init__(self, callable, *args, **kwargs):
  19.         self.callable = callable
  20.         self.args = args
  21.         self.kwargs = kwargs
  22. 
  23.     def __call__(self):
  24.         return self.callable(*self.args, **self.kwargs)
  25. 
  26. 
  27. class LateParamPageHandler(PageHandler):
  28.     """When passing cherrypy.request.params to the page handler, we do not
  29.     want to capture that dict too early; we want to give tools like the
  30.     decoding tool a chance to modify the params dict in-between the lookup
  31.     of the handler and the actual calling of the handler. This subclass
  32.     takes that into account, and allows request.params to be 'bound late'
  33.     (it's more complicated than that, but that's the effect).
  34.     """
  35. 
  36.     def _get_kwargs(self):
  37.         kwargs = cherrypy.request.params.copy()
  38.         if self._kwargs:
  39.             kwargs.update(self._kwargs)
  40.         return kwargs
  41. 
  42.     def _set_kwargs(self, kwargs):
  43.         self._kwargs = kwargs
  44. 
  45.     kwargs = property(_get_kwargs, _set_kwargs,
  46.                       doc='page handler kwargs (with '
  47.                       'cherrypy.request.params copied in)')
  48. 
  49. 
  50. class Dispatcher(object):
  51.     """CherryPy Dispatcher which walks a tree of objects to find a handler.
  52. 
  53.     The tree is rooted at cherrypy.request.app.root, and each hierarchical
  54.     component in the path_info argument is matched to a corresponding nested
  55.     attribute of the root object. Matching handlers must have an 'exposed'
  56.     attribute which evaluates to True. The special method name "index"
  57.     matches a URI which ends in a slash ("/"). The special method name
  58.     "default" may match a portion of the path_info (but only when no longer
  59.     substring of the path_info matches some other object).
  60. 
  61.     This is the default, built-in dispatcher for CherryPy.
  62.     """
  63. 
  64.     def __call__(self, path_info):
  65.         """Set handler and config for the current request."""
  66.         request = cherrypy.request
  67.         func, vpath = self.find_handler(path_info)
  68. 
  69.         if func:
  70.             # Decode any leftover %2F in the virtual_path atoms.
  71.             vpath = [x.replace("%2F", "/") for x in vpath]
  72.             request.handler = LateParamPageHandler(func, *vpath)
  73.         else:
  74.             request.handler = cherrypy.NotFound()
  75. 
  76.     def find_handler(self, path):
  77.         """Return the appropriate page handler, plus any virtual path.
  78. 
  79.         This will return two objects. The first will be a callable,
  80.         which can be used to generate page output. Any parameters from
  81.         the query string or request body will be sent to that callable
  82.         as keyword arguments.
  83. 
  84.         The callable is found by traversing the application's tree,
  85.         starting from cherrypy.request.app.root, and matching path
  86.         components to successive objects in the tree. For example, the
  87.         URL "/path/to/handler" might return root.path.to.handler.
  88. 
  89.         The second object returned will be a list of names which are
  90.         'virtual path' components: parts of the URL which are dynamic,
  91.         and were not used when looking up the handler.
  92.         These virtual path components are passed to the handler as
  93.         positional arguments.
  94.         """
  95.         request = cherrypy.request
  96.         app = request.app
  97.         root = app.root
  98. 
  99.         # Get config for the root object/path.
 100.         curpath = ""
 101.         nodeconf = {}
 102.         if hasattr(root, "_cp_config"):
 103.             nodeconf.update(root._cp_config)
 104.         if "/" in app.config:
 105.             nodeconf.update(app.config["/"])
 106.         object_trail = [['root', root, nodeconf, curpath]]
 107. 
 108.         node = root
 109.         names = [x for x in path.strip('/').split('/') if x] + ['index']
 110.         for name in names:
 111.             # map to legal Python identifiers (replace '.' with '_')
 112.             objname = name.replace('.', '_')
 113. 
 114.             nodeconf = {}
 115.             node = getattr(node, objname, None)
 116.             if node is not None:
 117.                 # Get _cp_config attached to this node.
 118.                 if hasattr(node, "_cp_config"):
 119.                     nodeconf.update(node._cp_config)
 120. 
 121.             # Mix in values from app.config for this path.
 122.             curpath = "/".join((curpath, name))
 123.             if curpath in app.config:
 124.                 nodeconf.update(app.config[curpath])
 125. 
 126.             object_trail.append([name, node, nodeconf, curpath])
 127. 
 128.         def set_conf():
 129.             """Collapse all object_trail config into cherrypy.request.config."""
 130.             base = cherrypy.config.copy()
 131.             # Note that we merge the config from each node
 132.             # even if that node was None.
 133.             for name, obj, conf, curpath in object_trail:
 134.                 base.update(conf)
 135.                 if 'tools.staticdir.dir' in conf:
 136.                     base['tools.staticdir.section'] = curpath
 137.             return base
 138. 
 139.         # Try successive objects (reverse order)
 140.         num_candidates = len(object_trail) - 1
 141.         for i in xrange(num_candidates, -1, -1):
 142. 
 143.             name, candidate, nodeconf, curpath = object_trail[i]
 144.             if candidate is None:
 145.                 continue
 146. 
 147.             # Try a "default" method on the current leaf.
 148.             if hasattr(candidate, "default"):
 149.                 defhandler = candidate.default
 150.                 if getattr(defhandler, 'exposed', False):
 151.                     # Insert any extra _cp_config from the default handler.
 152.                     conf = getattr(defhandler, "_cp_config", {})
 153.                     object_trail.insert(i+1, ["default", defhandler, conf, curpath])
 154.                     request.config = set_conf()
 155.                     # See http://www.cherrypy.org/ticket/613
 156.                     request.is_index = path.endswith("/")
 157.                     return defhandler, names[i:-1]
 158. 
 159.             # Uncomment the next line to restrict positional params to "default".
 160.             # if i < num_candidates - 2: continue
 161. 
 162.             # Try the current leaf.
 163.             if getattr(candidate, 'exposed', False):
 164.                 request.config = set_conf()
 165.                 if i == num_candidates:
 166.                     # We found the extra ".index". Mark request so tools
 167.                     # can redirect if path_info has no trailing slash.
 168.                     request.is_index = True
 169.                 else:
 170.                     # We're not at an 'index' handler. Mark request so tools
 171.                     # can redirect if path_info has NO trailing slash.
 172.                     # Note that this also includes handlers which take
 173.                     # positional parameters (virtual paths).
 174.                     request.is_index = False
 175.                 return candidate, names[i:-1]
 176. 
 177.         # We didn't find anything
 178.         request.config = set_conf()
 179.         return None, []
 180. 
 181. 
 182. class MethodDispatcher(Dispatcher):
 183.     """Additional dispatch based on cherrypy.request.method.upper().
 184. 
 185.     Methods named GET, POST, etc will be called on an exposed class.
 186.     The method names must be all caps; the appropriate Allow header
 187.     will be output showing all capitalized method names as allowable
 188.     HTTP verbs.
 189. 
 190.     Note that the containing class must be exposed, not the methods.
 191.     """
 192. 
 193.     def __call__(self, path_info):
 194.         """Set handler and config for the current request."""
 195.         request = cherrypy.request
 196.         resource, vpath = self.find_handler(path_info)
 197. 
 198.         if resource:
 199.             # Set Allow header
 200.             avail = [m for m in dir(resource) if m.isupper()]
 201.             if "GET" in avail and "HEAD" not in avail:
 202.                 avail.append("HEAD")
 203.             avail.sort()
 204.             cherrypy.response.headers['Allow'] = ", ".join(avail)
 205. 
 206.             # Find the subhandler
 207.             meth = request.method.upper()
 208.             func = getattr(resource, meth, None)
 209.             if func is None and meth == "HEAD":
 210.                 func = getattr(resource, "GET", None)
 211.             if func:
 212.                 # Decode any leftover %2F in the virtual_path atoms.
 213.                 vpath = [x.replace("%2F", "/") for x in vpath]
 214.                 request.handler = LateParamPageHandler(func, *vpath)
 215.             else:
 216.                 request.handler = cherrypy.HTTPError(405)
 217.         else:
 218.             request.handler = cherrypy.NotFound()
 219. 
 220. 
 221. class WSGIEnvProxy(object):
 222. 
 223.     def __getattr__(self, key):
 224.         return getattr(cherrypy.request.wsgi_environ, key)
 225. 
 226. 
 227. class RoutesDispatcher(object):
 228.     """A Routes based dispatcher for CherryPy."""
 229. 
 230.     def __init__(self, full_result=False):
 231.         """
 232.         Routes dispatcher
 233. 
 234.         Set full_result to True if you wish the controller
 235.         and the action to be passed on to the page handler
 236.         parameters. By default they won't be.
 237.         """
 238.         import routes
 239.         self.full_result = full_result
 240.         self.controllers = {}
 241.         self.mapper = routes.Mapper()
 242.         self.mapper.controller_scan = self.controllers.keys
 243. 
 244.         # Since Routes' mapper.environ is not threadsafe,
 245.         # we must use a proxy which does JIT lookup.
 246.         self.mapper.environ = WSGIEnvProxy()
 247. 
 248.     def connect(self, name, route, controller, **kwargs):
 249.         self.controllers[name] = controller
 250.         self.mapper.connect(name, route, controller=name, **kwargs)
 251. 
 252.     def redirect(self, url):
 253.         raise cherrypy.HTTPRedirect(url)
 254. 
 255.     def __call__(self, path_info):
 256.         """Set handler and config for the current request."""
 257.         func = self.find_handler(path_info)
 258.         if func:
 259.             cherrypy.request.handler = LateParamPageHandler(func)
 260.         else:
 261.             cherrypy.request.handler = cherrypy.NotFound()
 262. 
 263.     def find_handler(self, path_info):
 264.         """Find the right page handler, and set request.config."""
 265.         import routes
 266. 
 267.         request = cherrypy.request
 268. 
 269.         config = routes.request_config()
 270.         config.mapper = self.mapper
 271.         config.host = request.headers.get('Host', None)
 272.         config.protocol = request.scheme
 273.         config.redirect = self.redirect
 274. 
 275.         result = self.mapper.match(path_info)
 276.         config.mapper_dict = result
 277.         params = {}
 278.         if result:
 279.             params = result.copy()
 280.         if not self.full_result:
 281.             params.pop('controller', None)
 282.             params.pop('action', None)
 283.         request.params.update(params)
 284. 
 285.         # Get config for the root object/path.
 286.         request.config = base = cherrypy.config.copy()
 287.         curpath = ""
 288. 
 289.         def merge(nodeconf):
 290.             if 'tools.staticdir.dir' in nodeconf:
 291.                 nodeconf['tools.staticdir.section'] = curpath or "/"
 292.             base.update(nodeconf)
 293. 
 294.         app = request.app
 295.         root = app.root
 296.         if hasattr(root, "_cp_config"):
 297.             merge(root._cp_config)
 298.         if "/" in app.config:
 299.             merge(app.config["/"])
 300. 
 301.         # Mix in values from app.config.
 302.         atoms = [x for x in path_info.split("/") if x]
 303.         if atoms:
 304.             last = atoms.pop()
 305.         else:
 306.             last = None
 307.         for atom in atoms:
 308.             curpath = "/".join((curpath, atom))
 309.             if curpath in app.config:
 310.                 merge(app.config[curpath])
 311. 
 312.         handler = None
 313.         if result:
 314.             controller = result.get('controller', None)
 315.             controller = self.controllers.get(controller)
 316.             if controller:
 317.                 # Get config from the controller.
 318.                 if hasattr(controller, "_cp_config"):
 319.                     merge(controller._cp_config)
 320. 
 321.             action = result.get('action', None)
 322.             if action is not None:
 323.                 handler = getattr(controller, action)
 324.                 # Get config from the handler
 325.                 if hasattr(handler, "_cp_config"):
 326.                     merge(handler._cp_config)
 327. 
 328.         # Do the last path atom here so it can
 329.         # override the controller's _cp_config.
 330.         if last:
 331.             curpath = "/".join((curpath, last))
 332.             if curpath in app.config:
 333.                 merge(app.config[curpath])
 334. 
 335.         return handler
 336. 
 337. 
 338. def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
 339.     from cherrypy.lib import xmlrpc
 340.     def xmlrpc_dispatch(path_info):
 341.         path_info = xmlrpc.patched_path(path_info)
 342.         return next_dispatcher(path_info)
 343.     return xmlrpc_dispatch
 344. 
 345. 
 346. def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
 347.     """Select a different handler based on the Host header.
 348. 
 349.     Useful when running multiple sites within one CP server.
 350. 
 351.     From http://groups.google.com/group/cherrypy-users/browse_thread/thread/f393540fe278e54d:
 352. 
 353.     For various reasons I need several domains to point to different parts of a
 354.     single website structure as well as to their own "homepage"   EG
 355. 
 356.     http://www.mydom1.com  ->  root
 357.     http://www.mydom2.com  ->  root/mydom2/
 358.     http://www.mydom3.com  ->  root/mydom3/
 359.     http://www.mydom4.com  ->  under construction page
 360. 
 361.     but also to have  http://www.mydom1.com/mydom2/  etc to be valid pages in
 362.     their own right.
 363.     """
 364.     from cherrypy.lib import http
 365.     def vhost_dispatch(path_info):
 366.         header = cherrypy.request.headers.get
 367. 
 368.         domain = header('Host', '')
 369.         if use_x_forwarded_host:
 370.             domain = header("X-Forwarded-Host", domain)
 371. 
 372.         prefix = domains.get(domain, "")
 373.         if prefix:
 374.             path_info = http.urljoin(prefix, path_info)
 375. 
 376.         result = next_dispatcher(path_info)
 377. 
 378.         # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
 379.         section = cherrypy.request.config.get('tools.staticdir.section')
 380.         if section:
 381.             section = section[len(prefix):]
 382.             cherrypy.request.config['tools.staticdir.section'] = section
 383. 
 384.         return result
 385.     return vhost_dispatch
 386.