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 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 - def connect(self, name, route, controller, **kwargs):
245 self.controllers[name] = controller 246 self.mapper.connect(name, route, controller=name, **kwargs)
247
248 - def redirect(self, url):
250
251 - def __call__(self, path_info):
252 """Set handler and config for the current request.""" 253 func = self.find_handler(path_info) 254 if func: 255 cherrypy.request.handler = LateParamPageHandler(func) 256 else: 257 cherrypy.request.handler = cherrypy.NotFound()
258
259 - def find_handler(self, path_info):
260 """Find the right page handler, and set request.config.""" 261 import routes 262 263 request = cherrypy.request 264 265 config = routes.request_config() 266 config.mapper = self.mapper 267 # Since Routes' mapper.environ is not threadsafe, 268 # we must use a proxy which does JIT lookup. 269 config.mapper.environ = WSGIEnvProxy() 270 config.host = request.headers.get('Host', None) 271 config.protocol = request.scheme 272 config.redirect = self.redirect 273 274 result = self.mapper.match(path_info) 275 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