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

Source Code for Module wsgiwebapi.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 WSGIWebAPI callables. 
 21   
 22  """ 
 23  __docformat__ = "restructuredtext en" 
 24   
 25  from wsgisupport import Request, Response, HTTPMethodNotAllowed, HTTPError 
 26  import re 
 27   
28 -def _apply_request_checks_and_transforms(request, props):
29 """Apply all the checks and transforms listed in props to a request. 30 31 This is typically called from a decorator, and the props are read from the 32 decorated function. 33 34 """ 35 request_filters = props.get('request_filters', []) 36 for request_filter in request_filters: 37 request = request_filter(request, props) 38 return request
39
40 -def _apply_response_checks_and_transforms(request, response, props):
41 """Apply all the checks and transforms listed in props to a response. 42 43 This is typically called from a decorator, and the props are read from the 44 decorated function. 45 46 """ 47 response_filters = props.get('response_filters', []) 48 for response_filter in response_filters: 49 response = response_filter(request, response, props) 50 return response
51
52 -def _decorate_once(fn):
53 """Decorate a function with the standard decorator pair, if not already decorated. 54 55 """ 56 if hasattr(fn, '_wsgiwebapi_props'): 57 props = fn._wsgiwebapi_props 58 if props.get('decorated', False) == True: 59 return fn, props 60 props = {'decorated': True} 61 def res(*args, **kwargs): 62 if isinstance(args[0], Request): 63 request = args[0] 64 have_self = False 65 else: 66 assert isinstance(args[1], Request) 67 request = args[1] 68 have_self = True 69 70 if request._handler_props is not props: 71 raise RuntimeError("Handler properties do not match decorated properties. Probably missing call to wsgiwebapi.copyprops.") 72 73 request = _apply_request_checks_and_transforms(request, props) 74 75 if not have_self: 76 args = [request] 77 args.extend(args[1:]) 78 else: 79 args = [args[0], request] 80 args.extend(args[2:]) 81 82 response = fn(*args, **kwargs) 83 return _apply_response_checks_and_transforms(request, response, props)
84 res.__doc__ = fn.__doc__ 85 res.__name__ = fn.__name__ 86 res.__dict__.update(fn.__dict__) 87 res._wsgiwebapi_props = props 88 return res, props 89
90 -def _get_props(fn):
91 """Get the WSGIWebAPI properties from an object. 92 93 """ 94 if not hasattr(fn, '_wsgiwebapi_props'): 95 return None 96 return fn._wsgiwebapi_props
97
98 -def jsonreturning(fn):
99 """Decorator to wrap function's return value as JSON. 100 101 Before the decorator is applied, the function should return a structure to 102 be converted to JSON. The decorator will set the content_type 103 automatically (to text/javascript). 104 105 """ 106 fn, props = _decorate_once(fn) 107 import jsonsupport 108 response_filters = props.setdefault('response_filters', []) 109 response_filters.append(jsonsupport.convert_to_json) 110 props['return_type'] = 'JSON' 111 return fn
112
113 -def jsonpreturning(paramname='jsonp', valid=r'^[a-zA-Z._\[\]]*$'):
114 """Decorator to add JSONP support to an API function. 115 116 This adds a query parameter (by default, ``jsonp``, but this may be altered 117 with the `paramname` parameter) to the function. If the parameter is not 118 supplied, the return value is a plain JSON object, otherwise the return 119 value is preceded by the value in the parameter and an open bracket, and 120 followed by a close bracket. The parameter must not be specified multiple 121 times in a single request. 122 123 By default, the ``jsonp`` parameter may only contain upper and lowercase 124 ASCII alphabetic characters (A-Z, a-z), numbers (0-9), full stops (.), 125 underscores (_), and brackets ([ and ]), but this may be altered by setting 126 the `valid` parameter to a string containing a regular expression matching 127 the valid parameter values. To avoid performing any validation of the 128 value of the ``jsonp`` parameter, set the `valid` parameter to None. 129 130 See http://bob.pythonmac.org/archives/2005/12/05/remote-json-jsonp/ for 131 some of the rationale behind JSONP support. 132 133 """ 134 def deco(fn): 135 fn, props = _decorate_once(fn) 136 import jsonsupport 137 response_filters = props.setdefault('response_filters', []) 138 response_filters.append(jsonsupport.convert_to_jsonp) 139 props['return_type'] = 'JSONP' 140 props['return_JSONP_paramname'] = paramname 141 props['return_JSONP_valid'] = valid 142 return fn
143 return deco 144
145 -def _check_allowed_methods(request, props):
146 """Perform check that method is allowed. 147 148 """ 149 allowed_methods = props.get('allowed_methods', None) 150 if allowed_methods is not None and request.method not in allowed_methods: 151 raise HTTPMethodNotAllowed(request.method, allowed_methods) 152 return request
153
154 -def allow_method(method_type, *other_methods):
155 """Decorator to restrict the methods allowed to a specific set. 156 157 May be applied multiple times, to allow more than one method to be allowed. 158 159 If applied at all, any methods other than those specified will result in a 160 405 or 501 error (depending whether the exception is one of the standard 161 known methods). 162 163 (This stores the allowed methods in an attribute of the decorated function, 164 so tht repeated application can allow 165 166 """ 167 168 def deco(fn): 169 fn, props = _decorate_once(fn) 170 request_filters = props.setdefault('request_filters', []) 171 if _check_allowed_methods not in request_filters: 172 request_filters.append(_check_allowed_methods) 173 allowed = props.setdefault('allowed_methods', set()) 174 allowed.add(method_type) 175 for method in other_methods: 176 allowed.add(method) 177 return fn
178 return deco 179 180 allow_GET = allow_method('GET') 181 allow_GET.__doc__ = """Directly equivalent to allow_method('GET').""" 182 allow_HEAD = allow_method('HEAD') 183 allow_HEAD.__doc__ = """Directly equivalent to allow_method('HEAD').""" 184 allow_GETHEAD = allow_method('GET', 'HEAD') 185 allow_GETHEAD.__doc__ = "Directly equivalent to allow_method('GET', 'HEAD')." 186 allow_POST = allow_method('POST') 187 allow_POST.__doc__ = """Directly equivalent to allow_method('POST').""" 188
189 -def param(paramname, minreps=None, maxreps=None, pattern=None, default=None, doc=None):
190 """Decorator to add parameter validation. 191 192 If this is used at all, the ``noparams`` decorator may not also be used. 193 194 This decorator may only be used once for each parameter name. 195 196 - `paramname` is required - the name of the parameter in 197 ``request.params``. 198 - `minreps`: the minimum number of times the parameter must be specified. 199 If omitted or None, the parameter need not be specified. (Note that, if 200 default is specified, it is always valid to supply the parameter exactly 201 0 times, regardless of the setting of this parameter.) 202 - `maxreps`: the maximum number of times the parameter may be specified. 203 If omitted or None, there is no limit on the number of times the 204 parameter may be specified. 205 - `pattern`: a (python regular expression) pattern which the parameter 206 must match. If None, no validation is performed. 207 - `default`: a default value for the parameter list. Note - this is used 208 only if the parameter doesn't occur at all, and is simply entered into 209 ``request.params`` instead of the parameter (it should usually be a 210 sequence, to make the normal values placed into ``request.params``). 211 - `doc`: a documentation string for the parameter. 212 213 """ 214 import validation 215 def deco(fn): 216 fn, props = _decorate_once(fn) 217 request_filters = props.setdefault('request_filters', []) 218 if validation.check_no_params in request_filters: 219 raise RuntimeError("Can't decorate with param and noparams") 220 if validation.check_valid_params not in request_filters: 221 request_filters.append(validation.check_valid_params) 222 constraints = props.setdefault('valid_params', {}) 223 if paramname in constraints: 224 raise RuntimeError("Already set validation constraints for " 225 "parameter '%s'" % paramname) 226 compiled_pattern = None 227 if pattern is not None: 228 compiled_pattern = re.compile(pattern) 229 constraints[paramname] = (minreps, maxreps, pattern, 230 compiled_pattern, default, doc) 231 return fn
232 return deco 233
234 -def noparams(fn):
235 """Decorator to indicate that no parameters may be supplied. 236 237 """ 238 import validation 239 fn, props = _decorate_once(fn) 240 request_filters = props.setdefault('request_filters', []) 241 if validation.check_valid_params in request_filters: 242 raise RuntimeError("Can't decorate with param and noparams") 243 if validation.check_no_params not in request_filters: 244 request_filters.append(validation.check_no_params) 245 props['valid_params'] = {} 246 return fn
247
248 -def pathinfo(*args, **kwargs):
249 """Decorator to indicate the parameter names allowed in pathinfo. 250 251 """ 252 import validation 253 tail = kwargs.get('tail', None) 254 # Check that there aren't any other keyword arguments. 255 for key in kwargs: 256 if key not in ('tail',): 257 raise TypeError("pathinfo decorator got an unexpected keyword" 258 " argument '%s'" % key) 259 260 # Parse the rules 261 param_rules, tail_rules = validation.parse_pathinfo_rules(args, tail) 262 263 def deco(fn): 264 fn, props = _decorate_once(fn) 265 266 # Check that we only decorate once. 267 if props.get('pathinfo_decorated'): 268 raise RuntimeError("Can't decorate with pathinfo twice") 269 props['pathinfo_decorated'] = True 270 271 if len(args) == 0 and tail_rules is None: 272 # Don't set pathinfo_allow; use default behaviour of raising 273 # HTTPNotFound if pathinfo is supplied. 274 return fn 275 276 # Enable checking of path information (rather, disable automatic 277 # complaint). 278 props['pathinfo_allow'] = True 279 280 # Store the allowed parameters. 281 props['pathinfo_params_rules'] = param_rules 282 props['pathinfo_tail_rules'] = tail_rules 283 284 # Add validation filter to request_filters. 285 request_filters = props.setdefault('request_filters', []) 286 if validation.check_pathinfo not in request_filters: 287 request_filters.append(validation.check_pathinfo) 288 289 return fn
290 return deco 291
292 -def copyprops(original_fn, decorated_fn):
293 """Copy the WSGIWebAPI properties from a function to a decorated function. 294 295 If you write your own decorators and apply them to WSGIWebAPI decorated 296 functions, you should call this method in your decorator to copy the 297 WSGIWebAPI properties into your decorated function. If you don't do this, 298 you may get confusing failures, such as pathinfo not being allowed. 299 300 """ 301 if hasattr(original_fn, '_wsgiwebapi_props'): 302 decorated_fn._wsgiwebapi_props = original_fn._wsgiwebapi_props 303 if hasattr(original_fn, '__doc__'): 304 decorated_fn.__doc__ = original_fn.__doc__
305
306 -def decorate(decorator):
307 """Apply a decorator, but also copy the WSGIWebAPI properties across. 308 309 To use this, pass the decorator you wish to apply as a parameter to this 310 decorator. The returned decorator will apply this decorator, and then copy 311 the WSGIWebAPI properties across. 312 313 """ 314 def deco(fn): 315 newfn = decorator(fn) 316 copyprops(fn, newfn) 317 return newfn
318 return deco 319 320 # vim: set fileencoding=utf-8 : 321