Fixed python_path problem.
[smonitor.git] / lib / cherrypy / _cptools.py
blobd3eab059186ecdca20b754e3843b47dffe2df3d0
1 """CherryPy tools. A "tool" is any helper, adapted to CP.
3 Tools are usually designed to be used in a variety of ways (although some
4 may only offer one if they choose):
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.
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.
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).
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 """
25 import sys
26 import warnings
28 import cherrypy
31 def _getargs(func):
32 """Return the names of all static arguments to the given function."""
33 # Use this instead of importing inspect for less mem overhead.
34 import types
35 if sys.version_info >= (3, 0):
36 if isinstance(func, types.MethodType):
37 func = func.__func__
38 co = func.__code__
39 else:
40 if isinstance(func, types.MethodType):
41 func = func.im_func
42 co = func.func_code
43 return co.co_varnames[:co.co_argcount]
46 _attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them "
47 "on via config, or use them as decorators on your page handlers.")
49 class Tool(object):
50 """A registered function for use with CherryPy request-processing hooks.
52 help(tool.callable) should give you more information about this Tool.
53 """
55 namespace = "tools"
57 def __init__(self, point, callable, name=None, priority=50):
58 self._point = point
59 self.callable = callable
60 self._name = name
61 self._priority = priority
62 self.__doc__ = self.callable.__doc__
63 self._setargs()
65 def _get_on(self):
66 raise AttributeError(_attr_error)
67 def _set_on(self, value):
68 raise AttributeError(_attr_error)
69 on = property(_get_on, _set_on)
71 def _setargs(self):
72 """Copy func parameter names to obj attributes."""
73 try:
74 for arg in _getargs(self.callable):
75 setattr(self, arg, None)
76 except (TypeError, AttributeError):
77 if hasattr(self.callable, "__call__"):
78 for arg in _getargs(self.callable.__call__):
79 setattr(self, arg, None)
80 # IronPython 1.0 raises NotImplementedError because
81 # inspect.getargspec tries to access Python bytecode
82 # in co_code attribute.
83 except NotImplementedError:
84 pass
85 # IronPython 1B1 may raise IndexError in some cases,
86 # but if we trap it here it doesn't prevent CP from working.
87 except IndexError:
88 pass
90 def _merged_args(self, d=None):
91 """Return a dict of configuration entries for this Tool."""
92 if d:
93 conf = d.copy()
94 else:
95 conf = {}
97 tm = cherrypy.serving.request.toolmaps[self.namespace]
98 if self._name in tm:
99 conf.update(tm[self._name])
101 if "on" in conf:
102 del conf["on"]
104 return conf
106 def __call__(self, *args, **kwargs):
107 """Compile-time decorator (turn on the tool in config).
109 For example::
111 @tools.proxy()
112 def whats_my_base(self):
113 return cherrypy.request.base
114 whats_my_base.exposed = True
116 if args:
117 raise TypeError("The %r Tool does not accept positional "
118 "arguments; you must use keyword arguments."
119 % self._name)
120 def tool_decorator(f):
121 if not hasattr(f, "_cp_config"):
122 f._cp_config = {}
123 subspace = self.namespace + "." + self._name + "."
124 f._cp_config[subspace + "on"] = True
125 for k, v in kwargs.items():
126 f._cp_config[subspace + k] = v
127 return f
128 return tool_decorator
130 def _setup(self):
131 """Hook this tool into cherrypy.request.
133 The standard CherryPy request object will automatically call this
134 method when the tool is "turned on" in config.
136 conf = self._merged_args()
137 p = conf.pop("priority", None)
138 if p is None:
139 p = getattr(self.callable, "priority", self._priority)
140 cherrypy.serving.request.hooks.attach(self._point, self.callable,
141 priority=p, **conf)
144 class HandlerTool(Tool):
145 """Tool which is called 'before main', that may skip normal handlers.
147 If the tool successfully handles the request (by setting response.body),
148 if should return True. This will cause CherryPy to skip any 'normal' page
149 handler. If the tool did not handle the request, it should return False
150 to tell CherryPy to continue on and call the normal page handler. If the
151 tool is declared AS a page handler (see the 'handler' method), returning
152 False will raise NotFound.
155 def __init__(self, callable, name=None):
156 Tool.__init__(self, 'before_handler', callable, name)
158 def handler(self, *args, **kwargs):
159 """Use this tool as a CherryPy page handler.
161 For example::
163 class Root:
164 nav = tools.staticdir.handler(section="/nav", dir="nav",
165 root=absDir)
167 def handle_func(*a, **kw):
168 handled = self.callable(*args, **self._merged_args(kwargs))
169 if not handled:
170 raise cherrypy.NotFound()
171 return cherrypy.serving.response.body
172 handle_func.exposed = True
173 return handle_func
175 def _wrapper(self, **kwargs):
176 if self.callable(**kwargs):
177 cherrypy.serving.request.handler = None
179 def _setup(self):
180 """Hook this tool into cherrypy.request.
182 The standard CherryPy request object will automatically call this
183 method when the tool is "turned on" in config.
185 conf = self._merged_args()
186 p = conf.pop("priority", None)
187 if p is None:
188 p = getattr(self.callable, "priority", self._priority)
189 cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
190 priority=p, **conf)
193 class HandlerWrapperTool(Tool):
194 """Tool which wraps request.handler in a provided wrapper function.
196 The 'newhandler' arg must be a handler wrapper function that takes a
197 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
198 page handler
199 functions, it must return an iterable for use as cherrypy.response.body.
201 For example, to allow your 'inner' page handlers to return dicts
202 which then get interpolated into a template::
204 def interpolator(next_handler, *args, **kwargs):
205 filename = cherrypy.request.config.get('template')
206 cherrypy.response.template = env.get_template(filename)
207 response_dict = next_handler(*args, **kwargs)
208 return cherrypy.response.template.render(**response_dict)
209 cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
212 def __init__(self, newhandler, point='before_handler', name=None, priority=50):
213 self.newhandler = newhandler
214 self._point = point
215 self._name = name
216 self._priority = priority
218 def callable(self, debug=False):
219 innerfunc = cherrypy.serving.request.handler
220 def wrap(*args, **kwargs):
221 return self.newhandler(innerfunc, *args, **kwargs)
222 cherrypy.serving.request.handler = wrap
225 class ErrorTool(Tool):
226 """Tool which is used to replace the default request.error_response."""
228 def __init__(self, callable, name=None):
229 Tool.__init__(self, None, callable, name)
231 def _wrapper(self):
232 self.callable(**self._merged_args())
234 def _setup(self):
235 """Hook this tool into cherrypy.request.
237 The standard CherryPy request object will automatically call this
238 method when the tool is "turned on" in config.
240 cherrypy.serving.request.error_response = self._wrapper
243 # Builtin tools #
245 from cherrypy.lib import cptools, encoding, auth, static, jsontools
246 from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
247 from cherrypy.lib import caching as _caching
248 from cherrypy.lib import auth_basic, auth_digest
251 class SessionTool(Tool):
252 """Session Tool for CherryPy.
254 sessions.locking
255 When 'implicit' (the default), the session will be locked for you,
256 just before running the page handler.
258 When 'early', the session will be locked before reading the request
259 body. This is off by default for safety reasons; for example,
260 a large upload would block the session, denying an AJAX
261 progress meter (see http://www.cherrypy.org/ticket/630).
263 When 'explicit' (or any other value), you need to call
264 cherrypy.session.acquire_lock() yourself before using
265 session data.
268 def __init__(self):
269 # _sessions.init must be bound after headers are read
270 Tool.__init__(self, 'before_request_body', _sessions.init)
272 def _lock_session(self):
273 cherrypy.serving.session.acquire_lock()
275 def _setup(self):
276 """Hook this tool into cherrypy.request.
278 The standard CherryPy request object will automatically call this
279 method when the tool is "turned on" in config.
281 hooks = cherrypy.serving.request.hooks
283 conf = self._merged_args()
285 p = conf.pop("priority", None)
286 if p is None:
287 p = getattr(self.callable, "priority", self._priority)
289 hooks.attach(self._point, self.callable, priority=p, **conf)
291 locking = conf.pop('locking', 'implicit')
292 if locking == 'implicit':
293 hooks.attach('before_handler', self._lock_session)
294 elif locking == 'early':
295 # Lock before the request body (but after _sessions.init runs!)
296 hooks.attach('before_request_body', self._lock_session,
297 priority=60)
298 else:
299 # Don't lock
300 pass
302 hooks.attach('before_finalize', _sessions.save)
303 hooks.attach('on_end_request', _sessions.close)
305 def regenerate(self):
306 """Drop the current session and make a new one (with a new id)."""
307 sess = cherrypy.serving.session
308 sess.regenerate()
310 # Grab cookie-relevant tool args
311 conf = dict([(k, v) for k, v in self._merged_args().items()
312 if k in ('path', 'path_header', 'name', 'timeout',
313 'domain', 'secure')])
314 _sessions.set_response_cookie(**conf)
319 class XMLRPCController(object):
320 """A Controller (page handler collection) for XML-RPC.
322 To use it, have your controllers subclass this base class (it will
323 turn on the tool for you).
325 You can also supply the following optional config entries::
327 tools.xmlrpc.encoding: 'utf-8'
328 tools.xmlrpc.allow_none: 0
330 XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
331 appropriate handler must first be performed according to the URL, and
332 then a second dispatch step must take place according to the RPC method
333 specified in the request body. It also allows a superfluous "/RPC2"
334 prefix in the URL, supplies its own handler args in the body, and
335 requires a 200 OK "Fault" response instead of 404 when the desired
336 method is not found.
338 Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
339 This Controller acts as the dispatch target for the first half (based
340 on the URL); it then reads the RPC method from the request body and
341 does its own second dispatch step based on that method. It also reads
342 body params, and returns a Fault on error.
344 The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
345 in your URL's, you can safely skip turning on the XMLRPCDispatcher.
346 Otherwise, you need to use declare it in config::
348 request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
351 # Note we're hard-coding this into the 'tools' namespace. We could do
352 # a huge amount of work to make it relocatable, but the only reason why
353 # would be if someone actually disabled the default_toolbox. Meh.
354 _cp_config = {'tools.xmlrpc.on': True}
356 def default(self, *vpath, **params):
357 rpcparams, rpcmethod = _xmlrpc.process_body()
359 subhandler = self
360 for attr in str(rpcmethod).split('.'):
361 subhandler = getattr(subhandler, attr, None)
363 if subhandler and getattr(subhandler, "exposed", False):
364 body = subhandler(*(vpath + rpcparams), **params)
366 else:
367 # http://www.cherrypy.org/ticket/533
368 # if a method is not found, an xmlrpclib.Fault should be returned
369 # raising an exception here will do that; see
370 # cherrypy.lib.xmlrpc.on_error
371 raise Exception('method "%s" is not supported' % attr)
373 conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
374 _xmlrpc.respond(body,
375 conf.get('encoding', 'utf-8'),
376 conf.get('allow_none', 0))
377 return cherrypy.serving.response.body
378 default.exposed = True
381 class SessionAuthTool(HandlerTool):
383 def _setargs(self):
384 for name in dir(cptools.SessionAuth):
385 if not name.startswith("__"):
386 setattr(self, name, None)
389 class CachingTool(Tool):
390 """Caching Tool for CherryPy."""
392 def _wrapper(self, **kwargs):
393 request = cherrypy.serving.request
394 if _caching.get(**kwargs):
395 request.handler = None
396 else:
397 if request.cacheable:
398 # Note the devious technique here of adding hooks on the fly
399 request.hooks.attach('before_finalize', _caching.tee_output,
400 priority = 90)
401 _wrapper.priority = 20
403 def _setup(self):
404 """Hook caching into cherrypy.request."""
405 conf = self._merged_args()
407 p = conf.pop("priority", None)
408 cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
409 priority=p, **conf)
413 class Toolbox(object):
414 """A collection of Tools.
416 This object also functions as a config namespace handler for itself.
417 Custom toolboxes should be added to each Application's toolboxes dict.
420 def __init__(self, namespace):
421 self.namespace = namespace
423 def __setattr__(self, name, value):
424 # If the Tool._name is None, supply it from the attribute name.
425 if isinstance(value, Tool):
426 if value._name is None:
427 value._name = name
428 value.namespace = self.namespace
429 object.__setattr__(self, name, value)
431 def __enter__(self):
432 """Populate request.toolmaps from tools specified in config."""
433 cherrypy.serving.request.toolmaps[self.namespace] = map = {}
434 def populate(k, v):
435 toolname, arg = k.split(".", 1)
436 bucket = map.setdefault(toolname, {})
437 bucket[arg] = v
438 return populate
440 def __exit__(self, exc_type, exc_val, exc_tb):
441 """Run tool._setup() for each tool in our toolmap."""
442 map = cherrypy.serving.request.toolmaps.get(self.namespace)
443 if map:
444 for name, settings in map.items():
445 if settings.get("on", False):
446 tool = getattr(self, name)
447 tool._setup()
450 class DeprecatedTool(Tool):
452 _name = None
453 warnmsg = "This Tool is deprecated."
455 def __init__(self, point, warnmsg=None):
456 self.point = point
457 if warnmsg is not None:
458 self.warnmsg = warnmsg
460 def __call__(self, *args, **kwargs):
461 warnings.warn(self.warnmsg)
462 def tool_decorator(f):
463 return f
464 return tool_decorator
466 def _setup(self):
467 warnings.warn(self.warnmsg)
470 default_toolbox = _d = Toolbox("tools")
471 _d.session_auth = SessionAuthTool(cptools.session_auth)
472 _d.allow = Tool('on_start_resource', cptools.allow)
473 _d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
474 _d.response_headers = Tool('on_start_resource', cptools.response_headers)
475 _d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
476 _d.log_headers = Tool('before_error_response', cptools.log_request_headers)
477 _d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
478 _d.err_redirect = ErrorTool(cptools.redirect)
479 _d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
480 _d.decode = Tool('before_request_body', encoding.decode)
481 # the order of encoding, gzip, caching is important
482 _d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70)
483 _d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
484 _d.staticdir = HandlerTool(static.staticdir)
485 _d.staticfile = HandlerTool(static.staticfile)
486 _d.sessions = SessionTool()
487 _d.xmlrpc = ErrorTool(_xmlrpc.on_error)
488 _d.caching = CachingTool('before_handler', _caching.get, 'caching')
489 _d.expires = Tool('before_finalize', _caching.expires)
490 _d.tidy = DeprecatedTool('before_finalize',
491 "The tidy tool has been removed from the standard distribution of CherryPy. "
492 "The most recent version can be found at http://tools.cherrypy.org/browser.")
493 _d.nsgmls = DeprecatedTool('before_finalize',
494 "The nsgmls tool has been removed from the standard distribution of CherryPy. "
495 "The most recent version can be found at http://tools.cherrypy.org/browser.")
496 _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
497 _d.referer = Tool('before_request_body', cptools.referer)
498 _d.basic_auth = Tool('on_start_resource', auth.basic_auth)
499 _d.digest_auth = Tool('on_start_resource', auth.digest_auth)
500 _d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
501 _d.flatten = Tool('before_finalize', cptools.flatten)
502 _d.accept = Tool('on_start_resource', cptools.accept)
503 _d.redirect = Tool('on_start_resource', cptools.redirect)
504 _d.autovary = Tool('on_start_resource', cptools.autovary, priority=0)
505 _d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
506 _d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
507 _d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
508 _d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
510 del _d, cptools, encoding, auth, static