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):
7 All tools are callables that can be used wherever needed.
8 The arguments are straightforward and should be detailed within the
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.
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.
32 """Return the names of all static arguments to the given function."""
33 # Use this instead of importing inspect for less mem overhead.
35 if sys
.version_info
>= (3, 0):
36 if isinstance(func
, types
.MethodType
):
40 if isinstance(func
, types
.MethodType
):
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.")
50 """A registered function for use with CherryPy request-processing hooks.
52 help(tool.callable) should give you more information about this Tool.
57 def __init__(self
, point
, callable, name
=None, priority
=50):
59 self
.callable = callable
61 self
._priority
= priority
62 self
.__doc
__ = self
.callable.__doc
__
66 raise AttributeError(_attr_error
)
67 def _set_on(self
, value
):
68 raise AttributeError(_attr_error
)
69 on
= property(_get_on
, _set_on
)
72 """Copy func parameter names to obj attributes."""
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:
85 # IronPython 1B1 may raise IndexError in some cases,
86 # but if we trap it here it doesn't prevent CP from working.
90 def _merged_args(self
, d
=None):
91 """Return a dict of configuration entries for this Tool."""
97 tm
= cherrypy
.serving
.request
.toolmaps
[self
.namespace
]
99 conf
.update(tm
[self
._name
])
106 def __call__(self
, *args
, **kwargs
):
107 """Compile-time decorator (turn on the tool in config).
112 def whats_my_base(self):
113 return cherrypy.request.base
114 whats_my_base.exposed = True
117 raise TypeError("The %r Tool does not accept positional "
118 "arguments; you must use keyword arguments."
120 def tool_decorator(f
):
121 if not hasattr(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
128 return tool_decorator
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)
139 p
= getattr(self
.callable, "priority", self
._priority
)
140 cherrypy
.serving
.request
.hooks
.attach(self
._point
, self
.callable,
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.
164 nav = tools.staticdir.handler(section="/nav", dir="nav",
167 def handle_func(*a
, **kw
):
168 handled
= self
.callable(*args
, **self
._merged
_args
(kwargs
))
170 raise cherrypy
.NotFound()
171 return cherrypy
.serving
.response
.body
172 handle_func
.exposed
= True
175 def _wrapper(self
, **kwargs
):
176 if self
.callable(**kwargs
):
177 cherrypy
.serving
.request
.handler
= None
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)
188 p
= getattr(self
.callable, "priority", self
._priority
)
189 cherrypy
.serving
.request
.hooks
.attach(self
._point
, self
._wrapper
,
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
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
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
)
232 self
.callable(**self
._merged
_args
())
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
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.
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
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()
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)
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
,
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
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
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()
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
)
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
):
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
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
,
401 _wrapper
.priority
= 20
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
,
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:
428 value
.namespace
= self
.namespace
429 object.__setattr
__(self
, name
, value
)
432 """Populate request.toolmaps from tools specified in config."""
433 cherrypy
.serving
.request
.toolmaps
[self
.namespace
] = map = {}
435 toolname
, arg
= k
.split(".", 1)
436 bucket
= map.setdefault(toolname
, {})
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
)
444 for name
, settings
in map.items():
445 if settings
.get("on", False):
446 tool
= getattr(self
, name
)
450 class DeprecatedTool(Tool
):
453 warnmsg
= "This Tool is deprecated."
455 def __init__(self
, point
, warnmsg
=None):
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
):
464 return tool_decorator
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