Package cherrypy :: Module _cptools
[hide private]
[frames] | no frames]

Source Code for Module cherrypy._cptools

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