1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60 def res(*args, **kwargs):
61
62
63
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
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
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
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
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
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
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
247 """Decorator to indicate the parameter names allowed in pathinfo.
248
249 """
250 import validation
251 tail = kwargs.get('tail', None)
252
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
259 param_rules, tail_rules = validation.parse_pathinfo_rules(args, tail)
260
261 def deco(fn):
262 fn, props = _decorate_once(fn)
263
264
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
271
272 return fn
273
274
275
276 props['pathinfo_allow'] = True
277
278
279 props['pathinfo_params_rules'] = param_rules
280 props['pathinfo_tail_rules'] = tail_rules
281
282
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
301
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
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
331