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

root/branches/598-sendall/cherrypy/_cpdispatch.py

Revision 1791 (checked in by fumanchu, 1 year ago)

Fix for #719 (RoutesDispatcher? not working correctly).

  • 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 RoutesDispatcher(object):
222     """A Routes based dispatcher for CherryPy."""
223    
224     def __init__(self, full_result=False):
225         """
226         Routes dispatcher
227
228         Set full_result to True if you wish the controller
229         and the action to be passed on to the page handler
230         parameters. By default they won't be.
231         """
232         import routes
233         self.full_result = full_result
234         self.controllers = {}
235         self.mapper = routes.Mapper()
236         self.mapper.controller_scan = self.controllers.keys
237        
238     def connect(self, name, route, controller, **kwargs):
239         self.controllers[name] = controller
240         self.mapper.connect(name, route, controller=name, **kwargs)
241    
242     def redirect(self, url):
243         raise cherrypy.HTTPRedirect(url)
244    
245     def __call__(self, path_info):
246         """Set handler and config for the current request."""
247         func = self.find_handler(path_info)
248         if func:
249             cherrypy.request.handler = LateParamPageHandler(func)
250         else:
251             cherrypy.request.handler = cherrypy.NotFound()
252    
253     def find_handler(self, path_info):
254         """Find the right page handler, and set request.config."""
255         import routes
256        
257         request = cherrypy.request
258        
259         config = routes.request_config()
260         config.mapper = self.mapper
261         if hasattr(cherrypy.request, 'wsgi_environ'):
262             config.environ = cherrypy.request.wsgi_environ
263         config.host = request.headers.get('Host', None)
264         config.protocol = request.scheme
265         config.redirect = self.redirect
266        
267         result = self.mapper.match(path_info)
268        
269         config.mapper_dict = result
270         params = {}
271         if result:
272             params = result.copy()
273         if not self.full_result:
274             params.pop('controller', None)
275             params.pop('action', None)
276         request.params.update(params)
277        
278         # Get config for the root object/path.
279         request.config = base = cherrypy.config.copy()
280         curpath = ""
281        
282         def merge(nodeconf):
283             if 'tools.staticdir.dir' in nodeconf:
284                 nodeconf['tools.staticdir.section'] = curpath or "/"
285             base.update(nodeconf)
286        
287         app = request.app
288         root = app.root
289         if hasattr(root, "_cp_config"):
290             merge(root._cp_config)
291         if "/" in app.config:
292             merge(app.config["/"])
293        
294         # Mix in values from app.config.
295         atoms = [x for x in path_info.split("/") if x]
296         if atoms:
297             last = atoms.pop()
298         else:
299             last = None
300         for atom in atoms:
301             curpath = "/".join((curpath, atom))
302             if curpath in app.config:
303                 merge(app.config[curpath])
304        
305         handler = None
306         if result:
307             controller = result.get('controller', None)
308             controller = self.controllers.get(controller)
309             if controller:
310                 # Get config from the controller.
311                 if hasattr(controller, "_cp_config"):
312                     merge(controller._cp_config)
313            
314             action = result.get('action', None)
315             if action is not None:
316                 handler = getattr(controller, action)
317                 # Get config from the handler
318                 if hasattr(handler, "_cp_config"):
319                     merge(handler._cp_config)
320                    
321         # Do the last path atom here so it can
322         # override the controller's _cp_config.
323         if last:
324             curpath = "/".join((curpath, last))
325             if curpath in app.config:
326                 merge(app.config[curpath])
327        
328         return handler
329
330
331 def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
332     from cherrypy.lib import xmlrpc
333     def xmlrpc_dispatch(path_info):
334         path_info = xmlrpc.patched_path(path_info)
335         return next_dispatcher(path_info)
336     return xmlrpc_dispatch
337
338
339 def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
340     """Select a different handler based on the Host header.
341     
342     This can be useful when running multiple sites within one CP server.
343     It allows several domains to point to different parts of a single
344     website structure. For example:
345     
346         http://www.domain.example  ->  root
347         http://www.domain2.example  ->  root/domain2/
348         http://www.domain2.example:443  ->  root/secure
349     
350     can be accomplished via the following config:
351     
352         [/]
353         request.dispatch = cherrypy.dispatch.VirtualHost(
354             **{'www.domain2.example': '/domain2',
355                'www.domain2.example:443': '/secure',
356               })
357     
358     next_dispatcher: the next dispatcher object in the dispatch chain.
359         The VirtualHost dispatcher adds a prefix to the URL and calls
360         another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
361     
362     use_x_forwarded_host: if True (the default), any "X-Forwarded-Host"
363         request header will be used instead of the "Host" header. This
364         is commonly added by HTTP servers (such as Apache) when proxying.
365     
366     **domains: a dict of {host header value: virtual prefix} pairs.
367         The incoming "Host" request header is looked up in this dict,
368         and, if a match is found, the corresponding "virtual prefix"
369         value will be prepended to the URL path before calling the
370         next dispatcher. Note that you often need separate entries
371         for "example.com" and "www.example.com". In addition, "Host"
372         headers may contain the port number.
373     """
374     from cherrypy.lib import http
375     def vhost_dispatch(path_info):
376         header = cherrypy.request.headers.get
377        
378         domain = header('Host', '')
379         if use_x_forwarded_host:
380             domain = header("X-Forwarded-Host", domain)
381        
382         prefix = domains.get(domain, "")
383         if prefix:
384             path_info = http.urljoin(prefix, path_info)
385        
386         result = next_dispatcher(path_info)
387        
388         # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
389         section = cherrypy.request.config.get('tools.staticdir.section')
390         if section:
391             section = section[len(prefix):]
392             cherrypy.request.config['tools.staticdir.section'] = section
393        
394         return result
395     return vhost_dispatch
396
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets