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

root/trunk/cherrypy/_cptools.py

Revision 2013 (checked in by fumanchu, 2 weeks ago)

Test and fix for #835 (autotags fail on unicode characters). Bumped up the priority of tools.etags to run after tools.encoding (but before tools.gzip, caching).

  • Property svn:eol-style set to native
Line 
1 """CherryPy tools. A "tool" is any helper, adapted to CP.
2
3 Tools are usually designed to be used in a variety of ways (although some
4 may only offer one if they choose):
5     
6     Library calls:
7         All tools are callables that can be used wherever needed.
8         The arguments are straightforward and should be detailed within the
9         docstring.
10     
11     Function decorators:
12         All tools, when called, may be used as decorators which configure
13         individual CherryPy page handlers (methods on the CherryPy tree).
14         That is, "@tools.anytool()" should "turn on" the tool via the
15         decorated function's _cp_config attribute.
16     
17     CherryPy config:
18         If a tool exposes a "_setup" callable, it will be called
19         once per Request (if the feature is "turned on" via config).
20
21 Tools may be implemented as any object with a namespace. The builtins
22 are generally either modules or instances of the tools.Tool class.
23 """
24
25 import cherrypy
26
27
28 def _getargs(func):
29     """Return the names of all static arguments to the given function."""
30     # Use this instead of importing inspect for less mem overhead.
31     import types
32     if isinstance(func, types.MethodType):
33         func = func.im_func
34     co = func.func_code
35     return co.co_varnames[:co.co_argcount]
36
37
38 class Tool(object):
39     """A registered function for use with CherryPy request-processing hooks.
40     
41     help(tool.callable) should give you more information about this Tool.
42     """
43    
44     namespace = "tools"
45    
46     def __init__(self, point, callable, name=None, priority=50):
47         self._point = point
48         self.callable = callable
49         self._name = name
50         self._priority = priority
51         self.__doc__ = self.callable.__doc__
52         self._setargs()
53    
54     def _setargs(self):
55         """Copy func parameter names to obj attributes."""
56         try:
57             for arg in _getargs(self.callable):
58                 setattr(self, arg, None)
59         except (TypeError, AttributeError):
60             if hasattr(self.callable, "__call__"):
61                 for arg in _getargs(self.callable.__call__):
62                     setattr(self, arg, None)
63         # IronPython 1.0 raises NotImplementedError because
64         # inspect.getargspec tries to access Python bytecode
65         # in co_code attribute.
66         except NotImplementedError:
67             pass
68         # IronPython 1B1 may raise IndexError in some cases,
69         # but if we trap it here it doesn't prevent CP from working.
70         except IndexError:
71             pass
72    
73     def _merged_args(self, d=None):
74         """Return a dict of configuration entries for this Tool."""
75         if d:
76             conf = d.copy()
77         else:
78             conf = {}
79        
80         tm = cherrypy.request.toolmaps[self.namespace]
81         if self._name in tm:
82             conf.update(tm[self._name])
83        
84         if "on" in conf:
85             del conf["on"]
86        
87         return conf
88    
89     def __call__(self, *args, **kwargs):
90         """Compile-time decorator (turn on the tool in config).
91         
92         For example:
93         
94             @tools.proxy()
95             def whats_my_base(self):
96                 return cherrypy.request.base
97             whats_my_base.exposed = True
98         """
99         if args:
100             raise TypeError("The %r Tool does not accept positional "
101                             "arguments; you must use keyword arguments."
102                             % self._name)
103         def tool_decorator(f):
104             if not hasattr(f, "_cp_config"):
105                 f._cp_config = {}
106             subspace = self.namespace + "." + self._name + "."
107             f._cp_config[subspace + "on"] = True
108             for k, v in kwargs.iteritems():
109                 f._cp_config[subspace + k] = v
110             return f
111         return tool_decorator
112    
113     def _setup(self):
114         """Hook this tool into cherrypy.request.
115         
116         The standard CherryPy request object will automatically call this
117         method when the tool is "turned on" in config.
118         """
119         conf = self._merged_args()
120         p = conf.pop("priority", None)
121         if p is None:
122             p = getattr(self.callable, "priority", self._priority)
123         cherrypy.request.hooks.attach(self._point, self.callable,
124                                       priority=p, **conf)
125
126
127 class HandlerTool(Tool):
128     """Tool which is called 'before main', that may skip normal handlers.
129     
130     If the tool successfully handles the request (by setting response.body),
131     if should return True. This will cause CherryPy to skip any 'normal' page
132     handler. If the tool did not handle the request, it should return False
133     to tell CherryPy to continue on and call the normal page handler. If the
134     tool is declared AS a page handler (see the 'handler' method), returning
135     False will raise NotFound.
136     """
137    
138     def __init__(self, callable, name=None):
139         Tool.__init__(self, 'before_handler', callable, name)
140    
141     def handler(self, *args, **kwargs):
142         """Use this tool as a CherryPy page handler.
143         
144         For example:
145             class Root:
146                 nav = tools.staticdir.handler(section="/nav", dir="nav",
147                                               root=absDir)
148         """
149         def handle_func(*a, **kw):
150             handled = self.callable(*args, **self._merged_args(kwargs))
151             if not handled:
152                 raise cherrypy.NotFound()
153             return cherrypy.response.body
154         handle_func.exposed = True
155         return handle_func
156    
157     def _wrapper(self, **kwargs):
158         if self.callable(**kwargs):
159             cherrypy.request.handler = None
160    
161     def _setup(self):
162         """Hook this tool into cherrypy.request.
163         
164         The standard CherryPy request object will automatically call this
165         method when the tool is "turned on" in config.
166         """
167         conf = self._merged_args()
168         p = conf.pop("priority", None)
169         if p is None:
170             p = getattr(self.callable, "priority", self._priority)
171         cherrypy.request.hooks.attach(self._point, self._wrapper,
172                                       priority=p, **conf)
173
174
175 class HandlerWrapperTool(Tool):
176     """Tool which wraps request.handler in a provided wrapper function.
177     
178     The 'newhandler' arg must be a handler wrapper function that takes a
179     'next_handler' argument, plus *args and **kwargs. Like all page handler
180     functions, it must return an iterable for use as cherrypy.response.body.
181     
182     For example, to allow your 'inner' page handlers to return dicts
183     which then get interpolated into a template:
184     
185         def interpolator(next_handler, *args, **kwargs):
186             filename = cherrypy.request.config.get('template')
187             cherrypy.response.template = env.get_template(filename)
188             response_dict = next_handler(*args, **kwargs)
189             return cherrypy.response.template.render(**response_dict)
190         cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
191     """
192    
193     def __init__(self, newhandler, point='before_handler', name=None, priority=50):
194         self.newhandler = newhandler
195         self._point = point
196         self._name = name
197         self._priority = priority
198    
199     def callable(self):
200         innerfunc = cherrypy.request.handler
201         def wrap(*args, **kwargs):
202             return self.newhandler(innerfunc, *args, **kwargs)
203         cherrypy.request.handler = wrap
204
205
206 class ErrorTool(Tool):
207     """Tool which is used to replace the default request.error_response."""
208    
209     def __init__(self, callable, name=None):
210         Tool.__init__(self, None, callable, name)
211    
212     def _wrapper(self):
213         self.callable(**self._merged_args())
214    
215     def _setup(self):
216         """Hook this tool into cherrypy.request.
217         
218         The standard CherryPy request object will automatically call this
219         method when the tool is "turned on" in config.
220         """
221         cherrypy.request.error_response = self._wrapper
222
223
224 #                              Builtin tools                              #
225
226 from cherrypy.lib import cptools, encoding, auth, static, tidy
227 from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
228 from cherrypy.lib import caching as _caching, wsgiapp as _wsgiapp
229
230
231 class SessionTool(Tool):
232     """Session Tool for CherryPy.
233     
234     sessions.locking:
235         When 'implicit' (the default), the session will be locked for you,
236             just before running the page handler.
237         When 'early', the session will be locked before reading the request
238             body. This is off by default for safety reasons; for example,
239             a large upload would block the session, denying an AJAX
240             progress meter (see http://www.cherrypy.org/ticket/630).
241         When 'explicit' (or any other value), you need to call
242             cherrypy.session.acquire_lock() yourself before using
243             session data.
244     """
245    
246     def __init__(self):
247         # _sessions.init must be bound after headers are read
248         Tool.__init__(self, 'before_request_body', _sessions.init)
249    
250     def _lock_session(self):
251         cherrypy.serving.session.acquire_lock()
252    
253     def _setup(self):
254         """Hook this tool into cherrypy.request.
255         
256         The standard CherryPy request object will automatically call this
257         method when the tool is "turned on" in config.
258         """
259         hooks = cherrypy.request.hooks
260        
261         conf = self._merged_args()
262        
263         p = conf.pop("priority", None)
264         if p is None:
265             p = getattr(self.callable, "priority", self._priority)
266        
267         hooks.attach(self._point, self.callable, priority=p, **conf)
268        
269         locking = conf.pop('locking', 'implicit')
270         if locking == 'implicit':
271             hooks.attach('before_handler', self._lock_session)
272         elif locking == 'early':
273             # Lock before the request body (but after _sessions.init runs!)
274             hooks.attach('before_request_body', self._lock_session,
275                          priority=60)
276         else:
277             # Don't lock
278             pass
279        
280         hooks.attach('before_finalize', _sessions.save)
281         hooks.attach('on_end_request', _sessions.close)
282        
283     def regenerate(self):
284         """Drop the current session and make a new one (with a new id)."""
285         sess = cherrypy.serving.session
286         sess.regenerate()
287        
288         # Grab cookie-relevant tool args
289         conf = dict([(k, v) for k, v in self._merged_args().iteritems()
290                      if k in ('path', 'path_header', 'name', 'timeout',
291                               'domain', 'secure')])
292         _sessions.set_response_cookie(**conf)
293
294
295
296
297 class XMLRPCController(object):
298     """A Controller (page handler collection) for XML-RPC.
299     
300     To use it, have your controllers subclass this base class (it will
301     turn on the tool for you).
302     
303     You can also supply the following optional config entries:
304         
305         tools.xmlrpc.encoding: 'utf-8'
306         tools.xmlrpc.allow_none: 0
307     
308     XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
309     appropriate handler must first be performed according to the URL, and
310     then a second dispatch step must take place according to the RPC method
311     specified in the request body. It also allows a superfluous "/RPC2"
312     prefix in the URL, supplies its own handler args in the body, and
313     requires a 200 OK "Fault" response instead of 404 when the desired
314     method is not found.
315     
316     Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
317     This Controller acts as the dispatch target for the first half (based
318     on the URL); it then reads the RPC method from the request body and
319     does its own second dispatch step based on that method. It also reads
320     body params, and returns a Fault on error.
321     
322     The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
323     in your URL's, you can safely skip turning on the XMLRPCDispatcher.
324     Otherwise, you need to use declare it in config:
325         
326         request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
327     """
328    
329     # Note we're hard-coding this into the 'tools' namespace. We could do
330     # a huge amount of work to make it relocatable, but the only reason why
331     # would be if someone actually disabled the default_toolbox. Meh.
332     _cp_config = {'tools.xmlrpc.on': True}
333    
334     def default(self, *vpath, **params):
335         rpcparams, rpcmethod = _xmlrpc.process_body()
336        
337         subhandler = self
338         for attr in str(rpcmethod).split('.'):
339             subhandler = getattr(subhandler, attr, None)
340          
341         if subhandler and getattr(subhandler, "exposed", False):
342             body = subhandler(*(vpath + rpcparams), **params)
343        
344         else:
345             # http://www.cherrypy.org/ticket/533
346             # if a method is not found, an xmlrpclib.Fault should be returned
347             # raising an exception here will do that; see
348             # cherrypy.lib.xmlrpc.on_error
349             raise Exception, 'method "%s" is not supported' % attr
350        
351         conf = cherrypy.request.toolmaps['tools'].get("xmlrpc", {})
352         _xmlrpc.respond(body,
353                         conf.get('encoding', 'utf-8'),
354                         conf.get('allow_none', 0))
355         return cherrypy.response.body
356     default.exposed = True
357
358
359 class WSGIAppTool(HandlerTool):
360     """A tool for running any WSGI middleware/application within CP.
361     
362     Here are the parameters:
363     
364     wsgi_app - any wsgi application callable
365     env_update - a dictionary with arbitrary keys and values to be
366                  merged with the WSGI environ dictionary.
367     
368     Example:
369     
370     class Whatever:
371         _cp_config = {'tools.wsgiapp.on': True,
372                       'tools.wsgiapp.app': some_app,
373                       'tools.wsgiapp.env': app_environ,
374                       }
375     """
376    
377     def _setup(self):
378         # Keep request body intact so the wsgi app can have its way with it.
379         cherrypy.request.process_request_body = False
380         HandlerTool._setup(self)
381
382
383 class SessionAuthTool(HandlerTool):
384    
385     def _setargs(self):
386         for name in dir(cptools.SessionAuth):
387             if not name.startswith("__"):
388                 setattr(self, name, None)
389
390
391 class CachingTool(Tool):
392     """Caching Tool for CherryPy."""
393    
394     def _wrapper(self, invalid_methods=("POST", "PUT", "DELETE"), **kwargs):
395         request = cherrypy.request
396        
397         if not hasattr(cherrypy, "_cache"):
398             # Make a process-wide Cache object.
399             cherrypy._cache = kwargs.pop("cache_class", _caching.MemoryCache)()
400            
401             # Take all remaining kwargs and set them on the Cache object.
402             for k, v in kwargs.iteritems():
403                 setattr(cherrypy._cache, k, v)
404        
405         if _caching.get(invalid_methods=invalid_methods):
406             request.handler = None
407         else:
408             if request.cacheable:
409                 # Note the devious technique here of adding hooks on the fly
410                 request.hooks.attach('before_finalize', _caching.tee_output,
411                                      priority = 90)
412     _wrapper.priority = 20
413    
414     def _setup(self):
415         """Hook caching into cherrypy.request."""
416         conf = self._merged_args()
417        
418         p = conf.pop("priority", None)
419         cherrypy.request.hooks.attach('before_handler', self._wrapper,
420                                       priority=p, **conf)
421
422
423
424 class Toolbox(object):
425     """A collection of Tools.
426     
427     This object also functions as a config namespace handler for itself.
428     Custom toolboxes should be added to each Application's toolboxes dict.
429     """
430    
431     def __init__(self, namespace):
432         self.namespace = namespace
433    
434     def __setattr__(self, name, value):
435         # If the Tool._name is None, supply it from the attribute name.
436         if isinstance(value, Tool):
437             if value._name is None:
438                 value._name = name
439             value.namespace = self.namespace
440         object.__setattr__(self, name, value)
441    
442     def __enter__(self):
443         """Populate request.toolmaps from tools specified in config."""
444         cherrypy.request.toolmaps[self.namespace] = map = {}
445         def populate(k, v):
446             toolname, arg = k.split(".", 1)
447             bucket = map.setdefault(toolname, {})
448             bucket[arg] = v
449         return populate
450    
451     def __exit__(self, exc_type, exc_val, exc_tb):
452         """Run tool._setup() for each tool in our toolmap."""
453         map = cherrypy.request.toolmaps.get(self.namespace)
454         if map:
455             for name, settings in map.items():
456                 if settings.get("on", False):
457                     tool = getattr(self, name)
458                     tool._setup()
459
460
461 default_toolbox = _d = Toolbox("tools")
462 _d.session_auth = SessionAuthTool(cptools.session_auth)
463 _d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
464 _d.response_headers = Tool('on_start_resource', cptools.response_headers)
465 _d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
466 _d.log_headers = Tool('before_error_response', cptools.log_request_headers)
467 _d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
468 _d.err_redirect = ErrorTool(cptools.redirect)
469 _d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
470 _d.decode = Tool('before_handler', encoding.decode)
471 # the order of encoding, gzip, caching is important
472 _d.encode = Tool('before_finalize', encoding.encode, priority=70)
473 _d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
474 _d.staticdir = HandlerTool(static.staticdir)
475 _d.staticfile = HandlerTool(static.staticfile)
476 _d.sessions = SessionTool()
477 _d.xmlrpc = ErrorTool(_xmlrpc.on_error)
478 _d.wsgiapp = WSGIAppTool(_wsgiapp.run)
479 _d.caching = CachingTool('before_handler', _caching.get, 'caching')
480 _d.expires = Tool('before_finalize', _caching.expires)
481 _d.tidy = Tool('before_finalize', tidy.tidy)
482 _d.nsgmls = Tool('before_finalize', tidy.nsgmls)
483 _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
484 _d.referer = Tool('before_request_body', cptools.referer)
485 _d.basic_auth = Tool('on_start_resource', auth.basic_auth)
486 _d.digest_auth = Tool('on_start_resource', auth.digest_auth)
487 _d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
488 _d.flatten = Tool('before_finalize', cptools.flatten)
489 _d.accept = Tool('on_start_resource', cptools.accept)
490 _d.redirect = Tool('on_start_resource', cptools.redirect)
491
492 del _d, cptools, encoding, auth, static, tidy
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets