Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(331)

Side by Side Diff: third_party/cherrypy/_cpdispatch.py

Issue 9368042: Add CherryPy to third_party. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build/
Patch Set: '' Created 8 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/cherrypy/_cpconfig.py ('k') | third_party/cherrypy/_cperror.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
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 string
13 import sys
14 import types
15 try:
16 classtype = (type, types.ClassType)
17 except AttributeError:
18 classtype = type
19
20 import cherrypy
21 from cherrypy._cpcompat import set
22
23
24 class PageHandler(object):
25 """Callable which sets response.body."""
26
27 def __init__(self, callable, *args, **kwargs):
28 self.callable = callable
29 self.args = args
30 self.kwargs = kwargs
31
32 def __call__(self):
33 try:
34 return self.callable(*self.args, **self.kwargs)
35 except TypeError:
36 x = sys.exc_info()[1]
37 try:
38 test_callable_spec(self.callable, self.args, self.kwargs)
39 except cherrypy.HTTPError:
40 raise sys.exc_info()[1]
41 except:
42 raise x
43 raise
44
45
46 def test_callable_spec(callable, callable_args, callable_kwargs):
47 """
48 Inspect callable and test to see if the given args are suitable for it.
49
50 When an error occurs during the handler's invoking stage there are 2
51 erroneous cases:
52 1. Too many parameters passed to a function which doesn't define
53 one of *args or **kwargs.
54 2. Too little parameters are passed to the function.
55
56 There are 3 sources of parameters to a cherrypy handler.
57 1. query string parameters are passed as keyword parameters to the handler.
58 2. body parameters are also passed as keyword parameters.
59 3. when partial matching occurs, the final path atoms are passed as
60 positional args.
61 Both the query string and path atoms are part of the URI. If they are
62 incorrect, then a 404 Not Found should be raised. Conversely the body
63 parameters are part of the request; if they are invalid a 400 Bad Request.
64 """
65 show_mismatched_params = getattr(
66 cherrypy.serving.request, 'show_mismatched_params', False)
67 try:
68 (args, varargs, varkw, defaults) = inspect.getargspec(callable)
69 except TypeError:
70 if isinstance(callable, object) and hasattr(callable, '__call__'):
71 (args, varargs, varkw, defaults) = inspect.getargspec(callable.__cal l__)
72 else:
73 # If it wasn't one of our own types, re-raise
74 # the original error
75 raise
76
77 if args and args[0] == 'self':
78 args = args[1:]
79
80 arg_usage = dict([(arg, 0,) for arg in args])
81 vararg_usage = 0
82 varkw_usage = 0
83 extra_kwargs = set()
84
85 for i, value in enumerate(callable_args):
86 try:
87 arg_usage[args[i]] += 1
88 except IndexError:
89 vararg_usage += 1
90
91 for key in callable_kwargs.keys():
92 try:
93 arg_usage[key] += 1
94 except KeyError:
95 varkw_usage += 1
96 extra_kwargs.add(key)
97
98 # figure out which args have defaults.
99 args_with_defaults = args[-len(defaults or []):]
100 for i, val in enumerate(defaults or []):
101 # Defaults take effect only when the arg hasn't been used yet.
102 if arg_usage[args_with_defaults[i]] == 0:
103 arg_usage[args_with_defaults[i]] += 1
104
105 missing_args = []
106 multiple_args = []
107 for key, usage in arg_usage.items():
108 if usage == 0:
109 missing_args.append(key)
110 elif usage > 1:
111 multiple_args.append(key)
112
113 if missing_args:
114 # In the case where the method allows body arguments
115 # there are 3 potential errors:
116 # 1. not enough query string parameters -> 404
117 # 2. not enough body parameters -> 400
118 # 3. not enough path parts (partial matches) -> 404
119 #
120 # We can't actually tell which case it is,
121 # so I'm raising a 404 because that covers 2/3 of the
122 # possibilities
123 #
124 # In the case where the method does not allow body
125 # arguments it's definitely a 404.
126 message = None
127 if show_mismatched_params:
128 message="Missing parameters: %s" % ",".join(missing_args)
129 raise cherrypy.HTTPError(404, message=message)
130
131 # the extra positional arguments come from the path - 404 Not Found
132 if not varargs and vararg_usage > 0:
133 raise cherrypy.HTTPError(404)
134
135 body_params = cherrypy.serving.request.body.params or {}
136 body_params = set(body_params.keys())
137 qs_params = set(callable_kwargs.keys()) - body_params
138
139 if multiple_args:
140 if qs_params.intersection(set(multiple_args)):
141 # If any of the multiple parameters came from the query string then
142 # it's a 404 Not Found
143 error = 404
144 else:
145 # Otherwise it's a 400 Bad Request
146 error = 400
147
148 message = None
149 if show_mismatched_params:
150 message="Multiple values for parameters: "\
151 "%s" % ",".join(multiple_args)
152 raise cherrypy.HTTPError(error, message=message)
153
154 if not varkw and varkw_usage > 0:
155
156 # If there were extra query string parameters, it's a 404 Not Found
157 extra_qs_params = set(qs_params).intersection(extra_kwargs)
158 if extra_qs_params:
159 message = None
160 if show_mismatched_params:
161 message="Unexpected query string "\
162 "parameters: %s" % ", ".join(extra_qs_params)
163 raise cherrypy.HTTPError(404, message=message)
164
165 # If there were any extra body parameters, it's a 400 Not Found
166 extra_body_params = set(body_params).intersection(extra_kwargs)
167 if extra_body_params:
168 message = None
169 if show_mismatched_params:
170 message="Unexpected body parameters: "\
171 "%s" % ", ".join(extra_body_params)
172 raise cherrypy.HTTPError(400, message=message)
173
174
175 try:
176 import inspect
177 except ImportError:
178 test_callable_spec = lambda callable, args, kwargs: None
179
180
181
182 class LateParamPageHandler(PageHandler):
183 """When passing cherrypy.request.params to the page handler, we do not
184 want to capture that dict too early; we want to give tools like the
185 decoding tool a chance to modify the params dict in-between the lookup
186 of the handler and the actual calling of the handler. This subclass
187 takes that into account, and allows request.params to be 'bound late'
188 (it's more complicated than that, but that's the effect).
189 """
190
191 def _get_kwargs(self):
192 kwargs = cherrypy.serving.request.params.copy()
193 if self._kwargs:
194 kwargs.update(self._kwargs)
195 return kwargs
196
197 def _set_kwargs(self, kwargs):
198 self._kwargs = kwargs
199
200 kwargs = property(_get_kwargs, _set_kwargs,
201 doc='page handler kwargs (with '
202 'cherrypy.request.params copied in)')
203
204
205 if sys.version_info < (3, 0):
206 punctuation_to_underscores = string.maketrans(
207 string.punctuation, '_' * len(string.punctuation))
208 def validate_translator(t):
209 if not isinstance(t, str) or len(t) != 256:
210 raise ValueError("The translate argument must be a str of len 256.")
211 else:
212 punctuation_to_underscores = str.maketrans(
213 string.punctuation, '_' * len(string.punctuation))
214 def validate_translator(t):
215 if not isinstance(t, dict):
216 raise ValueError("The translate argument must be a dict.")
217
218 class Dispatcher(object):
219 """CherryPy Dispatcher which walks a tree of objects to find a handler.
220
221 The tree is rooted at cherrypy.request.app.root, and each hierarchical
222 component in the path_info argument is matched to a corresponding nested
223 attribute of the root object. Matching handlers must have an 'exposed'
224 attribute which evaluates to True. The special method name "index"
225 matches a URI which ends in a slash ("/"). The special method name
226 "default" may match a portion of the path_info (but only when no longer
227 substring of the path_info matches some other object).
228
229 This is the default, built-in dispatcher for CherryPy.
230 """
231
232 dispatch_method_name = '_cp_dispatch'
233 """
234 The name of the dispatch method that nodes may optionally implement
235 to provide their own dynamic dispatch algorithm.
236 """
237
238 def __init__(self, dispatch_method_name=None,
239 translate=punctuation_to_underscores):
240 validate_translator(translate)
241 self.translate = translate
242 if dispatch_method_name:
243 self.dispatch_method_name = dispatch_method_name
244
245 def __call__(self, path_info):
246 """Set handler and config for the current request."""
247 request = cherrypy.serving.request
248 func, vpath = self.find_handler(path_info)
249
250 if func:
251 # Decode any leftover %2F in the virtual_path atoms.
252 vpath = [x.replace("%2F", "/") for x in vpath]
253 request.handler = LateParamPageHandler(func, *vpath)
254 else:
255 request.handler = cherrypy.NotFound()
256
257 def find_handler(self, path):
258 """Return the appropriate page handler, plus any virtual path.
259
260 This will return two objects. The first will be a callable,
261 which can be used to generate page output. Any parameters from
262 the query string or request body will be sent to that callable
263 as keyword arguments.
264
265 The callable is found by traversing the application's tree,
266 starting from cherrypy.request.app.root, and matching path
267 components to successive objects in the tree. For example, the
268 URL "/path/to/handler" might return root.path.to.handler.
269
270 The second object returned will be a list of names which are
271 'virtual path' components: parts of the URL which are dynamic,
272 and were not used when looking up the handler.
273 These virtual path components are passed to the handler as
274 positional arguments.
275 """
276 request = cherrypy.serving.request
277 app = request.app
278 root = app.root
279 dispatch_name = self.dispatch_method_name
280
281 # Get config for the root object/path.
282 fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
283 fullpath_len = len(fullpath)
284 segleft = fullpath_len
285 nodeconf = {}
286 if hasattr(root, "_cp_config"):
287 nodeconf.update(root._cp_config)
288 if "/" in app.config:
289 nodeconf.update(app.config["/"])
290 object_trail = [['root', root, nodeconf, segleft]]
291
292 node = root
293 iternames = fullpath[:]
294 while iternames:
295 name = iternames[0]
296 # map to legal Python identifiers (e.g. replace '.' with '_')
297 objname = name.translate(self.translate)
298
299 nodeconf = {}
300 subnode = getattr(node, objname, None)
301 pre_len = len(iternames)
302 if subnode is None:
303 dispatch = getattr(node, dispatch_name, None)
304 if dispatch and hasattr(dispatch, '__call__') and not \
305 getattr(dispatch, 'exposed', False) and \
306 pre_len > 1:
307 #Don't expose the hidden 'index' token to _cp_dispatch
308 #We skip this if pre_len == 1 since it makes no sense
309 #to call a dispatcher when we have no tokens left.
310 index_name = iternames.pop()
311 subnode = dispatch(vpath=iternames)
312 iternames.append(index_name)
313 else:
314 #We didn't find a path, but keep processing in case there
315 #is a default() handler.
316 iternames.pop(0)
317 else:
318 #We found the path, remove the vpath entry
319 iternames.pop(0)
320 segleft = len(iternames)
321 if segleft > pre_len:
322 #No path segment was removed. Raise an error.
323 raise cherrypy.CherryPyException(
324 "A vpath segment was added. Custom dispatchers may only "
325 + "remove elements. While trying to process "
326 + "{0} in {1}".format(name, fullpath)
327 )
328 elif segleft == pre_len:
329 #Assume that the handler used the current path segment, but
330 #did not pop it. This allows things like
331 #return getattr(self, vpath[0], None)
332 iternames.pop(0)
333 segleft -= 1
334 node = subnode
335
336 if node is not None:
337 # Get _cp_config attached to this node.
338 if hasattr(node, "_cp_config"):
339 nodeconf.update(node._cp_config)
340
341 # Mix in values from app.config for this path.
342 existing_len = fullpath_len - pre_len
343 if existing_len != 0:
344 curpath = '/' + '/'.join(fullpath[0:existing_len])
345 else:
346 curpath = ''
347 new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
348 for seg in new_segs:
349 curpath += '/' + seg
350 if curpath in app.config:
351 nodeconf.update(app.config[curpath])
352
353 object_trail.append([name, node, nodeconf, segleft])
354
355 def set_conf():
356 """Collapse all object_trail config into cherrypy.request.config."""
357 base = cherrypy.config.copy()
358 # Note that we merge the config from each node
359 # even if that node was None.
360 for name, obj, conf, segleft in object_trail:
361 base.update(conf)
362 if 'tools.staticdir.dir' in conf:
363 base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0: fullpath_len - segleft])
364 return base
365
366 # Try successive objects (reverse order)
367 num_candidates = len(object_trail) - 1
368 for i in range(num_candidates, -1, -1):
369
370 name, candidate, nodeconf, segleft = object_trail[i]
371 if candidate is None:
372 continue
373
374 # Try a "default" method on the current leaf.
375 if hasattr(candidate, "default"):
376 defhandler = candidate.default
377 if getattr(defhandler, 'exposed', False):
378 # Insert any extra _cp_config from the default handler.
379 conf = getattr(defhandler, "_cp_config", {})
380 object_trail.insert(i+1, ["default", defhandler, conf, segle ft])
381 request.config = set_conf()
382 # See http://www.cherrypy.org/ticket/613
383 request.is_index = path.endswith("/")
384 return defhandler, fullpath[fullpath_len - segleft:-1]
385
386 # Uncomment the next line to restrict positional params to "default" .
387 # if i < num_candidates - 2: continue
388
389 # Try the current leaf.
390 if getattr(candidate, 'exposed', False):
391 request.config = set_conf()
392 if i == num_candidates:
393 # We found the extra ".index". Mark request so tools
394 # can redirect if path_info has no trailing slash.
395 request.is_index = True
396 else:
397 # We're not at an 'index' handler. Mark request so tools
398 # can redirect if path_info has NO trailing slash.
399 # Note that this also includes handlers which take
400 # positional parameters (virtual paths).
401 request.is_index = False
402 return candidate, fullpath[fullpath_len - segleft:-1]
403
404 # We didn't find anything
405 request.config = set_conf()
406 return None, []
407
408
409 class MethodDispatcher(Dispatcher):
410 """Additional dispatch based on cherrypy.request.method.upper().
411
412 Methods named GET, POST, etc will be called on an exposed class.
413 The method names must be all caps; the appropriate Allow header
414 will be output showing all capitalized method names as allowable
415 HTTP verbs.
416
417 Note that the containing class must be exposed, not the methods.
418 """
419
420 def __call__(self, path_info):
421 """Set handler and config for the current request."""
422 request = cherrypy.serving.request
423 resource, vpath = self.find_handler(path_info)
424
425 if resource:
426 # Set Allow header
427 avail = [m for m in dir(resource) if m.isupper()]
428 if "GET" in avail and "HEAD" not in avail:
429 avail.append("HEAD")
430 avail.sort()
431 cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
432
433 # Find the subhandler
434 meth = request.method.upper()
435 func = getattr(resource, meth, None)
436 if func is None and meth == "HEAD":
437 func = getattr(resource, "GET", None)
438 if func:
439 # Grab any _cp_config on the subhandler.
440 if hasattr(func, "_cp_config"):
441 request.config.update(func._cp_config)
442
443 # Decode any leftover %2F in the virtual_path atoms.
444 vpath = [x.replace("%2F", "/") for x in vpath]
445 request.handler = LateParamPageHandler(func, *vpath)
446 else:
447 request.handler = cherrypy.HTTPError(405)
448 else:
449 request.handler = cherrypy.NotFound()
450
451
452 class RoutesDispatcher(object):
453 """A Routes based dispatcher for CherryPy."""
454
455 def __init__(self, full_result=False):
456 """
457 Routes dispatcher
458
459 Set full_result to True if you wish the controller
460 and the action to be passed on to the page handler
461 parameters. By default they won't be.
462 """
463 import routes
464 self.full_result = full_result
465 self.controllers = {}
466 self.mapper = routes.Mapper()
467 self.mapper.controller_scan = self.controllers.keys
468
469 def connect(self, name, route, controller, **kwargs):
470 self.controllers[name] = controller
471 self.mapper.connect(name, route, controller=name, **kwargs)
472
473 def redirect(self, url):
474 raise cherrypy.HTTPRedirect(url)
475
476 def __call__(self, path_info):
477 """Set handler and config for the current request."""
478 func = self.find_handler(path_info)
479 if func:
480 cherrypy.serving.request.handler = LateParamPageHandler(func)
481 else:
482 cherrypy.serving.request.handler = cherrypy.NotFound()
483
484 def find_handler(self, path_info):
485 """Find the right page handler, and set request.config."""
486 import routes
487
488 request = cherrypy.serving.request
489
490 config = routes.request_config()
491 config.mapper = self.mapper
492 if hasattr(request, 'wsgi_environ'):
493 config.environ = request.wsgi_environ
494 config.host = request.headers.get('Host', None)
495 config.protocol = request.scheme
496 config.redirect = self.redirect
497
498 result = self.mapper.match(path_info)
499
500 config.mapper_dict = result
501 params = {}
502 if result:
503 params = result.copy()
504 if not self.full_result:
505 params.pop('controller', None)
506 params.pop('action', None)
507 request.params.update(params)
508
509 # Get config for the root object/path.
510 request.config = base = cherrypy.config.copy()
511 curpath = ""
512
513 def merge(nodeconf):
514 if 'tools.staticdir.dir' in nodeconf:
515 nodeconf['tools.staticdir.section'] = curpath or "/"
516 base.update(nodeconf)
517
518 app = request.app
519 root = app.root
520 if hasattr(root, "_cp_config"):
521 merge(root._cp_config)
522 if "/" in app.config:
523 merge(app.config["/"])
524
525 # Mix in values from app.config.
526 atoms = [x for x in path_info.split("/") if x]
527 if atoms:
528 last = atoms.pop()
529 else:
530 last = None
531 for atom in atoms:
532 curpath = "/".join((curpath, atom))
533 if curpath in app.config:
534 merge(app.config[curpath])
535
536 handler = None
537 if result:
538 controller = result.get('controller')
539 controller = self.controllers.get(controller, controller)
540 if controller:
541 if isinstance(controller, classtype):
542 controller = controller()
543 # Get config from the controller.
544 if hasattr(controller, "_cp_config"):
545 merge(controller._cp_config)
546
547 action = result.get('action')
548 if action is not None:
549 handler = getattr(controller, action, None)
550 # Get config from the handler
551 if hasattr(handler, "_cp_config"):
552 merge(handler._cp_config)
553 else:
554 handler = controller
555
556 # Do the last path atom here so it can
557 # override the controller's _cp_config.
558 if last:
559 curpath = "/".join((curpath, last))
560 if curpath in app.config:
561 merge(app.config[curpath])
562
563 return handler
564
565
566 def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
567 from cherrypy.lib import xmlrpcutil
568 def xmlrpc_dispatch(path_info):
569 path_info = xmlrpcutil.patched_path(path_info)
570 return next_dispatcher(path_info)
571 return xmlrpc_dispatch
572
573
574 def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domai ns):
575 """
576 Select a different handler based on the Host header.
577
578 This can be useful when running multiple sites within one CP server.
579 It allows several domains to point to different parts of a single
580 website structure. For example::
581
582 http://www.domain.example -> root
583 http://www.domain2.example -> root/domain2/
584 http://www.domain2.example:443 -> root/secure
585
586 can be accomplished via the following config::
587
588 [/]
589 request.dispatch = cherrypy.dispatch.VirtualHost(
590 **{'www.domain2.example': '/domain2',
591 'www.domain2.example:443': '/secure',
592 })
593
594 next_dispatcher
595 The next dispatcher object in the dispatch chain.
596 The VirtualHost dispatcher adds a prefix to the URL and calls
597 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
598
599 use_x_forwarded_host
600 If True (the default), any "X-Forwarded-Host"
601 request header will be used instead of the "Host" header. This
602 is commonly added by HTTP servers (such as Apache) when proxying.
603
604 ``**domains``
605 A dict of {host header value: virtual prefix} pairs.
606 The incoming "Host" request header is looked up in this dict,
607 and, if a match is found, the corresponding "virtual prefix"
608 value will be prepended to the URL path before calling the
609 next dispatcher. Note that you often need separate entries
610 for "example.com" and "www.example.com". In addition, "Host"
611 headers may contain the port number.
612 """
613 from cherrypy.lib import httputil
614 def vhost_dispatch(path_info):
615 request = cherrypy.serving.request
616 header = request.headers.get
617
618 domain = header('Host', '')
619 if use_x_forwarded_host:
620 domain = header("X-Forwarded-Host", domain)
621
622 prefix = domains.get(domain, "")
623 if prefix:
624 path_info = httputil.urljoin(prefix, path_info)
625
626 result = next_dispatcher(path_info)
627
628 # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
629 section = request.config.get('tools.staticdir.section')
630 if section:
631 section = section[len(prefix):]
632 request.config['tools.staticdir.section'] = section
633
634 return result
635 return vhost_dispatch
636
OLDNEW
« no previous file with comments | « third_party/cherrypy/_cpconfig.py ('k') | third_party/cherrypy/_cperror.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698