Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/tags/cherrypy-3.0.3/cherrypy/_cpdispatch.py

Revision 1625 (checked in by lawouach, 2 years ago)

Fixed #663

  • Property svn:eol-style set to native
Line 
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):
249         raise cherrypy.HTTPRedirect(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
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets