Package wsgiwapi :: Module decorators
[frames] | no frames]

Source Code for Module wsgiwapi.decorators

  1  # Copyright (c) 2009 Richard Boulton 
  2  # 
  3  # Permission is hereby granted, free of charge, to any person obtaining a copy 
  4  # of this software and associated documentation files (the "Software"), to deal 
  5  # in the Software without restriction, including without limitation the rights 
  6  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
  7  # copies of the Software, and to permit persons to whom the Software is 
  8  # furnished to do so, subject to the following conditions: 
  9  # 
 10  # The above copyright notice and this permission notice shall be included in 
 11  # all copies or substantial portions of the Software. 
 12  # 
 13  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 14  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 15  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 16  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 17  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 18  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 19  # SOFTWARE. 
 20  r"""Decorators of WSGIWAPI callables. 
 21   
 22  """ 
 23  __docformat__ = "restructuredtext en" 
 24   
 25  from html_entity_names import entities 
 26  import re 
 27  import warnings 
 28  from wsgisupport import Request, Response, HTTPMethodNotAllowed, HTTPError 
 29   
30 -def _decorate_once(fn):
31 """Ensure that a function is decorated with the standard decorator. 32 33 Returns a tuple (decorated_function, properties), where decorated_function 34 is the function with the decorator applied, and properties is the WSGIWAPI 35 properties stored on the function. 36 37 """ 38 if hasattr(fn, '_wsgiwapi_props'): 39 props = fn._wsgiwapi_props 40 if props.get('decorated', False) == True: 41 return fn, props 42 props = {'decorated': True} 43 44 # Note: the following wrapper function just checks that the properties on 45 # the callable passed to application match those set here. I think this 46 # will always be true unless a later applied decorator has failed to copy 47 # the properties. 48 49 # It is tempting to remove this check, and just set the properties on the 50 # original callable object, but there is a potential security issue in 51 # doing so: if a later applied decorator _has_ failed to copy the 52 # properties, this would lead to decorators getting lost, which could mean 53 # that code which looks like it is validating parameters is actually 54 # failing to do the validation. 55 56 # Perhaps the best fix would be to make parameters unavailable unless 57 # they've been validated. 58 59 # FIXME - review this. 60 def res(*args, **kwargs): 61 # Check that the decorator has not been applied and then the properties 62 # have been lost (probably by a second decorator which doesn't copy the 63 # properties being applied). 64 if isinstance(args[0], Request): 65 request = args[0] 66 else: 67 request = args[1] 68 if request._handler_props is not props: 69 raise RuntimeError("Handler properties do not match decorated properties. Probably missing call to wsgiwapi.copyprops.") 70 return fn(*args, **kwargs)
71 res.__doc__ = fn.__doc__ 72 res.__name__ = fn.__name__ 73 res.__dict__.update(fn.__dict__) 74 res._wsgiwapi_props = props 75 return res, props 76
77 -def _get_props(fn):
78 """Get the WSGIWAPI properties from an object. 79 80 """ 81 if not hasattr(fn, '_wsgiwapi_props'): 82 return None 83 return fn._wsgiwapi_props
84
85 -def jsonreturning(fn):
86 """Decorator to wrap function's return value as JSON. 87 88 Before the decorator is applied, the function should return a structure to 89 be converted to JSON. The decorator will set the content_type 90 automatically (to text/javascript). 91 92 """ 93 fn, props = _decorate_once(fn) 94 import jsonsupport 95 response_filters = props.setdefault('response_filters', []) 96 response_filters.append(jsonsupport.convert_to_json) 97 props['return_type'] = 'JSON' 98 return fn
99
100 -def jsonpreturning(paramname='jsonp', valid=r'^[a-zA-Z._\[\]]*$'):
101 """Decorator to add JSONP support to an API function. 102 103 This adds a query parameter (by default, ``jsonp``, but this may be altered 104 with the `paramname` parameter) to the function. If the parameter is not 105 supplied, the return value is a plain JSON object, otherwise the return 106 value is preceded by the value in the parameter and an open bracket, and 107 followed by a close bracket. The parameter must not be specified multiple 108 times in a single request. 109 110 By default, the ``jsonp`` parameter may only contain upper and lowercase 111 ASCII alphabetic characters (A-Z, a-z), numbers (0-9), full stops (.), 112 underscores (_), and brackets ([ and ]), but this may be altered by setting 113 the `valid` parameter to a string containing a regular expression matching 114 the valid parameter values. To avoid performing any validation of the 115 value of the ``jsonp`` parameter, set the `valid` parameter to None. 116 117 See http://bob.pythonmac.org/archives/2005/12/05/remote-json-jsonp/ for 118 some of the rationale behind JSONP support. 119 120 """ 121 def deco(fn): 122 fn, props = _decorate_once(fn) 123 import jsonsupport 124 response_filters = props.setdefault('response_filters', []) 125 response_filters.append(jsonsupport.convert_to_jsonp) 126 props['return_type'] = 'JSONP' 127 props['return_JSONP_paramname'] = paramname 128 props['return_JSONP_valid'] = valid 129 return fn
130 return deco 131
132 -def _check_allowed_methods(request, props):
133 """Perform check that method is allowed. 134 135 """ 136 allowed_methods = props.get('allowed_methods', None) 137 if allowed_methods is not None and request.method not in allowed_methods: 138 raise HTTPMethodNotAllowed(request.method, allowed_methods) 139 return request
140
141 -def allow_method(method_type, *other_methods):
142 """Decorator to restrict the methods allowed to a specific set. 143 144 May be applied multiple times, to allow more than one method to be allowed. 145 146 If applied at all, any methods other than those specified will result in a 147 405 or 501 error (depending whether the exception is one of the standard 148 known methods). 149 150 (This stores the allowed methods in an attribute of the decorated function, 151 so that repeated application of the decorator can add to the allowed 152 methods.) 153 154 """ 155 156 def deco(fn): 157 fn, props = _decorate_once(fn) 158 request_filters = props.setdefault('request_filters', []) 159 if _check_allowed_methods not in request_filters: 160 request_filters.append(_check_allowed_methods) 161 allowed = props.setdefault('allowed_methods', set()) 162 allowed.add(method_type) 163 for method in other_methods: 164 allowed.add(method) 165 return fn
166 return deco 167 168 allow_GET = allow_method('GET') 169 allow_GET.__doc__ = """Directly equivalent to allow_method('GET').""" 170 allow_HEAD = allow_method('HEAD') 171 allow_HEAD.__doc__ = """Directly equivalent to allow_method('HEAD').""" 172 allow_GETHEAD = allow_method('GET', 'HEAD') 173 allow_GETHEAD.__doc__ = "Directly equivalent to allow_method('GET', 'HEAD')." 174 allow_POST = allow_method('POST') 175 allow_POST.__doc__ = """Directly equivalent to allow_method('POST').""" 176 allow_PUT = allow_method('PUT') 177 allow_PUT.__doc__ = """Directly equivalent to allow_method('PUT').""" 178 allow_DELETE = allow_method('DELETE') 179 allow_DELETE.__doc__ = """Directly equivalent to allow_method('DELETE').""" 180
181 -def param(paramname, minreps=None, maxreps=None, pattern=None, default=None, doc=None):
182 """Decorator to add parameter validation. 183 184 If this is used at all, the ``noparams`` decorator may not also be used. 185 186 This decorator may only be used once for each parameter name. 187 188 - `paramname` is required - the name of the parameter in 189 ``request.params``. 190 - `minreps`: the minimum number of times the parameter must be specified. 191 If omitted or None, the parameter need not be specified. (Note that, if 192 default is specified, it is always valid to supply the parameter exactly 193 0 times, regardless of the setting of this parameter.) 194 - `maxreps`: the maximum number of times the parameter may be specified. 195 If omitted or None, there is no limit on the number of times the 196 parameter may be specified. 197 - `pattern`: a (python regular expression) pattern which the parameter 198 must match. If None, no validation is performed. 199 - `default`: a default value for the parameter list. Note - this is used 200 only if the parameter doesn't occur at all, and is simply entered into 201 ``request.params`` instead of the parameter (it should usually be a 202 sequence, to make the normal values placed into ``request.params``). 203 - `doc`: a documentation string for the parameter. 204 205 """ 206 import validation 207 def deco(fn): 208 fn, props = _decorate_once(fn) 209 request_filters = props.setdefault('request_filters', []) 210 if validation.check_no_params in request_filters: 211 raise RuntimeError("Can't decorate with param and noparams") 212 if validation.check_valid_params not in request_filters: 213 request_filters.append(validation.check_valid_params) 214 constraints = props.setdefault('valid_params', {}) 215 if paramname in entities: 216 warnings.warn('Parameter name %s is also an HTML entity name. ' 217 'This may lead to problems if resulting URLs are ' 218 'not correctly escaped when copied into HTML. It ' 219 'may be better to use a different parameter name.' 220 % paramname, UserWarning) 221 if paramname in constraints: 222 raise RuntimeError("Already set validation constraints for " 223 "parameter '%s'" % paramname) 224 compiled_pattern = None 225 if pattern is not None: 226 compiled_pattern = re.compile(pattern, re.UNICODE) 227 constraints[paramname] = (minreps, maxreps, pattern, 228 compiled_pattern, default, doc) 229 return fn
230 return deco 231
232 -def noparams(fn):
233 """Decorator to indicate that no parameters may be supplied. 234 235 """ 236 import validation 237 fn, props = _decorate_once(fn) 238 request_filters = props.setdefault('request_filters', []) 239 if validation.check_valid_params in request_filters: 240 raise RuntimeError("Can't decorate with param and noparams") 241 if validation.check_no_params not in request_filters: 242 request_filters.append(validation.check_no_params) 243 props['valid_params'] = {} 244 return fn
245
246 -def pathinfo(*args, **kwargs):
247 """Decorator to indicate the parameter names allowed in pathinfo. 248 249 """ 250 import validation 251 tail = kwargs.get('tail', None) 252 # Check that there aren't any other keyword arguments. 253 for key in kwargs: 254 if key not in ('tail',): 255 raise TypeError("pathinfo decorator got an unexpected keyword" 256 " argument '%s'" % key) 257 258 # Parse the rules 259 param_rules, tail_rules = validation.parse_pathinfo_rules(args, tail) 260 261 def deco(fn): 262 fn, props = _decorate_once(fn) 263 264 # Check that we only decorate once. 265 if props.get('pathinfo_decorated'): 266 raise RuntimeError("Can't decorate with pathinfo twice") 267 props['pathinfo_decorated'] = True 268 269 if len(args) == 0 and tail_rules is None: 270 # Don't set pathinfo_allow; use default behaviour of raising 271 # HTTPNotFound if pathinfo is supplied. 272 return fn 273 274 # Enable checking of path information (rather, disable automatic 275 # complaint). 276 props['pathinfo_allow'] = True 277 278 # Store the allowed parameters. 279 props['pathinfo_params_rules'] = param_rules 280 props['pathinfo_tail_rules'] = tail_rules 281 282 # Add validation filter to request_filters. 283 request_filters = props.setdefault('request_filters', []) 284 if validation.check_pathinfo not in request_filters: 285 request_filters.append(validation.check_pathinfo) 286 287 return fn
288 return deco 289
290 -def rawinput(fn):
291 """Decorator to prevent POST/PUT data being read and parsed. 292 293 If this is used, the request body will not be read by WSGIWAPI. The 294 application code can read it from the `request.input` attribute. This is 295 useful for handling large files. 296 297 """ 298 fn, props = _decorate_once(fn) 299 props['postdata_is_processed'] = True 300 return fn
301
302 -def copyprops(original_fn, decorated_fn):
303 """Copy the WSGIWAPI properties from a function to a decorated function. 304 305 If you write your own decorators and apply them to WSGIWAPI decorated 306 functions, you should call this method in your decorator to copy the 307 WSGIWAPI properties into your decorated function. If you don't do this, 308 you may get confusing failures, such as pathinfo not being allowed. 309 310 """ 311 if hasattr(original_fn, '_wsgiwapi_props'): 312 decorated_fn._wsgiwapi_props = original_fn._wsgiwapi_props 313 if hasattr(original_fn, '__doc__'): 314 decorated_fn.__doc__ = original_fn.__doc__
315
316 -def decorate(decorator):
317 """Apply a decorator, but also copy the WSGIWAPI properties across. 318 319 To use this, pass the decorator you wish to apply as a parameter to this 320 decorator. The returned decorator will apply this decorator, and then copy 321 the WSGIWAPI properties across. 322 323 """ 324 def deco(fn): 325 newfn = decorator(fn) 326 copyprops(fn, newfn) 327 return newfn
328 return deco 329 330 # vim: set fileencoding=utf-8 : 331