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

root/trunk/cherrypy/_cptools.py

Revision 1951 (checked in by fumanchu, 3 weeks ago)

Fix for #805 (Remove import of inspect).

  • 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 ErrorTool(Tool):
176     """Tool which is used to replace the default request.error_response."""
177    
178     def __init__(self, callable, name=None):
179         Tool.__init__(self, None, callable, name)
180    
181     def _wrapper(self):
182         self.callable(**self._merged_args())
183    
184     def _setup(self):
185         """Hook this tool into cherrypy.request.
186         
187         The standard CherryPy request object will automatically call this
188         method when the tool is "turned on" in config.
189         """
190         cherrypy.request.error_response = self._wrapper
191
192
193 #                              Builtin tools                              #
194
195 from cherrypy.lib import cptools, encoding, auth, static, tidy
196 from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
197 from cherrypy.lib import caching as _caching, wsgiapp as _wsgiapp
198
199
200 class SessionTool(Tool):
201     """Session Tool for CherryPy.
202     
203     sessions.locking:
204         When 'implicit' (the default), the session will be locked for you,
205             just before running the page handler.
206         When 'early', the session will be locked before reading the request
207             body. This is off by default for safety reasons; for example,
208             a large upload would block the session, denying an AJAX
209             progress meter (see http://www.cherrypy.org/ticket/630).
210         When 'explicit' (or any other value), you need to call
211             cherrypy.session.acquire_lock() yourself before using
212             session data.
213     """
214    
215     def __init__(self):
216         # _sessions.init must be bound after headers are read
217         Tool.__init__(self, 'before_request_body', _sessions.init)
218    
219     def _lock_session(self):
220         cherrypy.serving.session.acquire_lock()
221    
222     def _setup(self):
223         """Hook this tool into cherrypy.request.
224         
225         The standard CherryPy request object will automatically call this
226         method when the tool is "turned on" in config.
227         """
228         hooks = cherrypy.request.hooks
229        
230         conf = self._merged_args()
231        
232         p = conf.pop("priority", None)
233         if p is None:
234             p = getattr(self.callable, "priority", self._priority)
235        
236         hooks.attach(self._point, self.callable, priority=p, **conf)
237        
238         locking = conf.pop('locking', 'implicit')
239         if locking == 'implicit':
240             hooks.attach('before_handler', self._lock_session)
241         elif locking == 'early':
242             # Lock before the request body (but after _sessions.init runs!)
243             hooks.attach('before_request_body', self._lock_session,
244                          priority=60)
245         else:
246             # Don't lock
247             pass
248        
249         hooks.attach('before_finalize', _sessions.save)
250         hooks.attach('on_end_request', _sessions.close)
251        
252     def regenerate(self):
253         """Drop the current session and make a new one (with a new id)."""
254         sess = cherrypy.serving.session
255         sess.regenerate()
256        
257         # Grab cookie-relevant tool args
258         conf = dict([(k, v) for k, v in self._merged_args().iteritems()
259                      if k in ('path', 'path_header', 'name', 'timeout',
260                               'domain', 'secure')])
261         _sessions.set_response_cookie(**conf)
262
263
264
265
266 class XMLRPCController(object):
267     """A Controller (page handler collection) for XML-RPC.
268     
269     To use it, have your controllers subclass this base class (it will
270     turn on the tool for you).
271     
272     You can also supply the following optional config entries:
273         
274         tools.xmlrpc.encoding: 'utf-8'
275         tools.xmlrpc.allow_none: 0
276     
277     XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
278     appropriate handler must first be performed according to the URL, and
279     then a second dispatch step must take place according to the RPC method
280     specified in the request body. It also allows a superfluous "/RPC2"
281     prefix in the URL, supplies its own handler args in the body, and
282     requires a 200 OK "Fault" response instead of 404 when the desired
283     method is not found.
284     
285     Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
286     This Controller acts as the dispatch target for the first half (based
287     on the URL); it then reads the RPC method from the request body and
288     does its own second dispatch step based on that method. It also reads
289     body params, and returns a Fault on error.
290     
291     The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
292     in your URL's, you can safely skip turning on the XMLRPCDispatcher.
293     Otherwise, you need to use declare it in config:
294         
295         request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
296     """
297    
298     # Note we're hard-coding this into the 'tools' namespace. We could do
299     # a huge amount of work to make it relocatable, but the only reason why
300     # would be if someone actually disabled the default_toolbox. Meh.
301     _cp_config = {'tools.xmlrpc.on': True}
302    
303     def default(self, *vpath, **params):
304         rpcparams, rpcmethod = _xmlrpc.process_body()
305        
306         subhandler = self
307         for attr in str(rpcmethod).split('.'):
308             subhandler = getattr(subhandler, attr, None)
309          
310         if subhandler and getattr(subhandler, "exposed", False):
311             body = subhandler(*(vpath + rpcparams), **params)
312        
313         else:
314             # http://www.cherrypy.org/ticket/533
315             # if a method is not found, an xmlrpclib.Fault should be returned
316             # raising an exception here will do that; see
317             # cherrypy.lib.xmlrpc.on_error
318             raise Exception, 'method "%s" is not supported' % attr
319        
320         conf = cherrypy.request.toolmaps['tools'].get("xmlrpc", {})
321         _xmlrpc.respond(body,
322                         conf.get('encoding', 'utf-8'),
323                         conf.get('allow_none', 0))
324         return cherrypy.response.body
325     default.exposed = True
326
327
328 class WSGIAppTool(HandlerTool):
329     """A tool for running any WSGI middleware/application within CP.
330     
331     Here are the parameters:
332     
333     wsgi_app - any wsgi application callable
334     env_update - a dictionary with arbitrary keys and values to be
335                  merged with the WSGI environ dictionary.
336     
337     Example:
338     
339     class Whatever:
340         _cp_config = {'tools.wsgiapp.on': True,
341                       'tools.wsgiapp.app': some_app,
342                       'tools.wsgiapp.env': app_environ,
343                       }
344     """
345    
346     def _setup(self):
347         # Keep request body intact so the wsgi app can have its way with it.
348         cherrypy.request.process_request_body = False
349         HandlerTool._setup(self)
350
351
352 class SessionAuthTool(HandlerTool):
353    
354     def _setargs(self):
355         for name in dir(cptools.SessionAuth):
356             if not name.startswith("__"):
357                 setattr(self, name, None)
358
359
360 class CachingTool(Tool):
361     """Caching Tool for CherryPy."""
362    
363     def _wrapper(self, invalid_methods=("POST", "PUT", "DELETE"), **kwargs):
364         request = cherrypy.request
365        
366         if not hasattr(cherrypy, "_cache"):
367             # Make a process-wide Cache object.
368             cherrypy._cache = kwargs.pop("cache_class", _caching.MemoryCache)()
369            
370             # Take all remaining kwargs and set them on the Cache object.
371             for k, v in kwargs.iteritems():
372                 setattr(cherrypy._cache, k, v)
373        
374         if _caching.get(invalid_methods=invalid_methods):
375             request.handler = None
376         else:
377             if request.cacheable:
378                 # Note the devious technique here of adding hooks on the fly
379                 request.hooks.attach('before_finalize', _caching.tee_output,
380                                      priority = 90)
381     _wrapper.priority = 20
382    
383     def _setup(self):
384         """Hook caching into cherrypy.request."""
385         conf = self._merged_args()
386        
387         p = conf.pop("priority", None)
388         cherrypy.request.hooks.attach('before_handler', self._wrapper,
389                                       priority=p, **conf)
390
391
392
393 class Toolbox(object):
394     """A collection of Tools.
395     
396     This object also functions as a config namespace handler for itself.
397     Custom toolboxes should be added to each Application's toolboxes dict.
398     """
399    
400     def __init__(self, namespace):
401         self.namespace = namespace
402    
403     def __setattr__(self, name, value):
404         # If the Tool._name is None, supply it from the attribute name.
405         if isinstance(value, Tool):
406             if value._name is None:
407                 value._name = name
408             value.namespace = self.namespace
409         object.__setattr__(self, name, value)
410    
411     def __enter__(self):
412         """Populate request.toolmaps from tools specified in config."""
413         cherrypy.request.toolmaps[self.namespace] = map = {}
414         def populate(k, v):
415             toolname, arg = k.split(".", 1)
416             bucket = map.setdefault(toolname, {})
417             bucket[arg] = v
418         return populate
419    
420     def __exit__(self, exc_type, exc_val, exc_tb):
421         """Run tool._setup() for each tool in our toolmap."""
422         map = cherrypy.request.toolmaps.get(self.namespace)
423         if map:
424             for name, settings in map.items():
425                 if settings.get("on", False):
426                     tool = getattr(self, name)
427                     tool._setup()
428
429
430 default_toolbox = _d = Toolbox("tools")
431 _d.session_auth = SessionAuthTool(cptools.session_auth)
432 _d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
433 _d.response_headers = Tool('on_start_resource', cptools.response_headers)
434 _d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
435 _d.log_headers = Tool('before_error_response', cptools.log_request_headers)
436 _d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
437 _d.err_redirect = ErrorTool(cptools.redirect)
438 _d.etags = Tool('before_finalize', cptools.validate_etags)
439 _d.decode = Tool('before_handler', encoding.decode)
440 # the order of encoding, gzip, caching is important
441 _d.encode = Tool('before_finalize', encoding.encode, priority=70)
442 _d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
443 _d.staticdir = HandlerTool(static.staticdir)
444 _d.staticfile = HandlerTool(static.staticfile)
445 _d.sessions = SessionTool()
446 _d.xmlrpc = ErrorTool(_xmlrpc.on_error)
447 _d.wsgiapp = WSGIAppTool(_wsgiapp.run)
448 _d.caching = CachingTool('before_handler', _caching.get, 'caching')
449 _d.expires = Tool('before_finalize', _caching.expires)
450 _d.tidy = Tool('before_finalize', tidy.tidy)
451 _d.nsgmls = Tool('before_finalize', tidy.nsgmls)
452 _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
453 _d.referer = Tool('before_request_body', cptools.referer)
454 _d.basic_auth = Tool('on_start_resource', auth.basic_auth)
455 _d.digest_auth = Tool('on_start_resource', auth.digest_auth)
456 _d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
457 _d.flatten = Tool('before_finalize', cptools.flatten)
458 _d.accept = Tool('on_start_resource', cptools.accept)
459
460 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