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

root/tags/cherrypy-3.1.0beta2/cherrypy/_cptools.py

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

Fix for #745 (Make a builtin tool which logs all hooks for a given request).

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