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.