OLD | NEW |
(Empty) | |
| 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 = ("CherryPy Tools cannot be turned on directly. Instead, turn them
" |
| 47 "on via config, or use them as decorators on your page handlers."
) |
| 48 |
| 49 class Tool(object): |
| 50 """A registered function for use with CherryPy request-processing hooks. |
| 51 |
| 52 help(tool.callable) should give you more information about this Tool. |
| 53 """ |
| 54 |
| 55 namespace = "tools" |
| 56 |
| 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() |
| 64 |
| 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) |
| 70 |
| 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 |
| 89 |
| 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 = {} |
| 96 |
| 97 tm = cherrypy.serving.request.toolmaps[self.namespace] |
| 98 if self._name in tm: |
| 99 conf.update(tm[self._name]) |
| 100 |
| 101 if "on" in conf: |
| 102 del conf["on"] |
| 103 |
| 104 return conf |
| 105 |
| 106 def __call__(self, *args, **kwargs): |
| 107 """Compile-time decorator (turn on the tool in config). |
| 108 |
| 109 For example:: |
| 110 |
| 111 @tools.proxy() |
| 112 def whats_my_base(self): |
| 113 return cherrypy.request.base |
| 114 whats_my_base.exposed = True |
| 115 """ |
| 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 |
| 129 |
| 130 def _setup(self): |
| 131 """Hook this tool into cherrypy.request. |
| 132 |
| 133 The standard CherryPy request object will automatically call this |
| 134 method when the tool is "turned on" in config. |
| 135 """ |
| 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) |
| 142 |
| 143 |
| 144 class HandlerTool(Tool): |
| 145 """Tool which is called 'before main', that may skip normal handlers. |
| 146 |
| 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. |
| 153 """ |
| 154 |
| 155 def __init__(self, callable, name=None): |
| 156 Tool.__init__(self, 'before_handler', callable, name) |
| 157 |
| 158 def handler(self, *args, **kwargs): |
| 159 """Use this tool as a CherryPy page handler. |
| 160 |
| 161 For example:: |
| 162 |
| 163 class Root: |
| 164 nav = tools.staticdir.handler(section="/nav", dir="nav", |
| 165 root=absDir) |
| 166 """ |
| 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 |
| 174 |
| 175 def _wrapper(self, **kwargs): |
| 176 if self.callable(**kwargs): |
| 177 cherrypy.serving.request.handler = None |
| 178 |
| 179 def _setup(self): |
| 180 """Hook this tool into cherrypy.request. |
| 181 |
| 182 The standard CherryPy request object will automatically call this |
| 183 method when the tool is "turned on" in config. |
| 184 """ |
| 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) |
| 191 |
| 192 |
| 193 class HandlerWrapperTool(Tool): |
| 194 """Tool which wraps request.handler in a provided wrapper function. |
| 195 |
| 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. |
| 200 |
| 201 For example, to allow your 'inner' page handlers to return dicts |
| 202 which then get interpolated into a template:: |
| 203 |
| 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) |
| 210 """ |
| 211 |
| 212 def __init__(self, newhandler, point='before_handler', name=None, priority=5
0): |
| 213 self.newhandler = newhandler |
| 214 self._point = point |
| 215 self._name = name |
| 216 self._priority = priority |
| 217 |
| 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 |
| 223 |
| 224 |
| 225 class ErrorTool(Tool): |
| 226 """Tool which is used to replace the default request.error_response.""" |
| 227 |
| 228 def __init__(self, callable, name=None): |
| 229 Tool.__init__(self, None, callable, name) |
| 230 |
| 231 def _wrapper(self): |
| 232 self.callable(**self._merged_args()) |
| 233 |
| 234 def _setup(self): |
| 235 """Hook this tool into cherrypy.request. |
| 236 |
| 237 The standard CherryPy request object will automatically call this |
| 238 method when the tool is "turned on" in config. |
| 239 """ |
| 240 cherrypy.serving.request.error_response = self._wrapper |
| 241 |
| 242 |
| 243 # Builtin tools # |
| 244 |
| 245 from cherrypy.lib import cptools, encoding, auth, static, jsontools |
| 246 from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc |
| 247 from cherrypy.lib import caching as _caching |
| 248 from cherrypy.lib import auth_basic, auth_digest |
| 249 |
| 250 |
| 251 class SessionTool(Tool): |
| 252 """Session Tool for CherryPy. |
| 253 |
| 254 sessions.locking |
| 255 When 'implicit' (the default), the session will be locked for you, |
| 256 just before running the page handler. |
| 257 |
| 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). |
| 262 |
| 263 When 'explicit' (or any other value), you need to call |
| 264 cherrypy.session.acquire_lock() yourself before using |
| 265 session data. |
| 266 """ |
| 267 |
| 268 def __init__(self): |
| 269 # _sessions.init must be bound after headers are read |
| 270 Tool.__init__(self, 'before_request_body', _sessions.init) |
| 271 |
| 272 def _lock_session(self): |
| 273 cherrypy.serving.session.acquire_lock() |
| 274 |
| 275 def _setup(self): |
| 276 """Hook this tool into cherrypy.request. |
| 277 |
| 278 The standard CherryPy request object will automatically call this |
| 279 method when the tool is "turned on" in config. |
| 280 """ |
| 281 hooks = cherrypy.serving.request.hooks |
| 282 |
| 283 conf = self._merged_args() |
| 284 |
| 285 p = conf.pop("priority", None) |
| 286 if p is None: |
| 287 p = getattr(self.callable, "priority", self._priority) |
| 288 |
| 289 hooks.attach(self._point, self.callable, priority=p, **conf) |
| 290 |
| 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 |
| 301 |
| 302 hooks.attach('before_finalize', _sessions.save) |
| 303 hooks.attach('on_end_request', _sessions.close) |
| 304 |
| 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() |
| 309 |
| 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) |
| 315 |
| 316 |
| 317 |
| 318 |
| 319 class XMLRPCController(object): |
| 320 """A Controller (page handler collection) for XML-RPC. |
| 321 |
| 322 To use it, have your controllers subclass this base class (it will |
| 323 turn on the tool for you). |
| 324 |
| 325 You can also supply the following optional config entries:: |
| 326 |
| 327 tools.xmlrpc.encoding: 'utf-8' |
| 328 tools.xmlrpc.allow_none: 0 |
| 329 |
| 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. |
| 337 |
| 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. |
| 343 |
| 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:: |
| 347 |
| 348 request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() |
| 349 """ |
| 350 |
| 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} |
| 355 |
| 356 def default(self, *vpath, **params): |
| 357 rpcparams, rpcmethod = _xmlrpc.process_body() |
| 358 |
| 359 subhandler = self |
| 360 for attr in str(rpcmethod).split('.'): |
| 361 subhandler = getattr(subhandler, attr, None) |
| 362 |
| 363 if subhandler and getattr(subhandler, "exposed", False): |
| 364 body = subhandler(*(vpath + rpcparams), **params) |
| 365 |
| 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.xmlrpcutil.on_error |
| 371 raise Exception('method "%s" is not supported' % attr) |
| 372 |
| 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 |
| 379 |
| 380 |
| 381 class SessionAuthTool(HandlerTool): |
| 382 |
| 383 def _setargs(self): |
| 384 for name in dir(cptools.SessionAuth): |
| 385 if not name.startswith("__"): |
| 386 setattr(self, name, None) |
| 387 |
| 388 |
| 389 class CachingTool(Tool): |
| 390 """Caching Tool for CherryPy.""" |
| 391 |
| 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 |
| 402 |
| 403 def _setup(self): |
| 404 """Hook caching into cherrypy.request.""" |
| 405 conf = self._merged_args() |
| 406 |
| 407 p = conf.pop("priority", None) |
| 408 cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, |
| 409 priority=p, **conf) |
| 410 |
| 411 |
| 412 |
| 413 class Toolbox(object): |
| 414 """A collection of Tools. |
| 415 |
| 416 This object also functions as a config namespace handler for itself. |
| 417 Custom toolboxes should be added to each Application's toolboxes dict. |
| 418 """ |
| 419 |
| 420 def __init__(self, namespace): |
| 421 self.namespace = namespace |
| 422 |
| 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) |
| 430 |
| 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 |
| 439 |
| 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() |
| 448 |
| 449 |
| 450 class DeprecatedTool(Tool): |
| 451 |
| 452 _name = None |
| 453 warnmsg = "This Tool is deprecated." |
| 454 |
| 455 def __init__(self, point, warnmsg=None): |
| 456 self.point = point |
| 457 if warnmsg is not None: |
| 458 self.warnmsg = warnmsg |
| 459 |
| 460 def __call__(self, *args, **kwargs): |
| 461 warnings.warn(self.warnmsg) |
| 462 def tool_decorator(f): |
| 463 return f |
| 464 return tool_decorator |
| 465 |
| 466 def _setup(self): |
| 467 warnings.warn(self.warnmsg) |
| 468 |
| 469 |
| 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) |
| 509 |
| 510 del _d, cptools, encoding, auth, static |
OLD | NEW |