kinto 19.5.0__py3-none-any.whl → 19.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kinto might be problematic. Click here for more details.

Files changed (37) hide show
  1. kinto/core/__init__.py +3 -3
  2. kinto/core/cornice/__init__.py +93 -0
  3. kinto/core/cornice/cors.py +144 -0
  4. kinto/core/cornice/errors.py +40 -0
  5. kinto/core/cornice/pyramidhook.py +373 -0
  6. kinto/core/cornice/renderer.py +89 -0
  7. kinto/core/cornice/resource.py +205 -0
  8. kinto/core/cornice/service.py +641 -0
  9. kinto/core/cornice/util.py +138 -0
  10. kinto/core/cornice/validators/__init__.py +94 -0
  11. kinto/core/cornice/validators/_colander.py +142 -0
  12. kinto/core/cornice/validators/_marshmallow.py +182 -0
  13. kinto/core/cornice_swagger/__init__.py +92 -0
  14. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  15. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  16. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  17. kinto/core/cornice_swagger/converters/schema.py +249 -0
  18. kinto/core/cornice_swagger/swagger.py +725 -0
  19. kinto/core/cornice_swagger/templates/index.html +73 -0
  20. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  21. kinto/core/cornice_swagger/util.py +42 -0
  22. kinto/core/cornice_swagger/views.py +78 -0
  23. kinto/core/openapi.py +2 -3
  24. kinto/core/resource/viewset.py +1 -1
  25. kinto/core/testing.py +1 -1
  26. kinto/core/utils.py +3 -2
  27. kinto/core/views/batch.py +1 -1
  28. kinto/core/views/openapi.py +1 -1
  29. kinto/plugins/flush.py +1 -1
  30. kinto/plugins/openid/views.py +1 -1
  31. kinto/views/contribute.py +2 -1
  32. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/METADATA +2 -4
  33. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/RECORD +37 -16
  34. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/LICENSE +0 -0
  35. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/WHEEL +0 -0
  36. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/entry_points.txt +0 -0
  37. {kinto-19.5.0.dist-info → kinto-19.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,641 @@
1
+ # This Source Code Form is subject to the terms of the Mozilla Public
2
+ # License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
+ # You can obtain one at http://mozilla.org/MPL/2.0/.
4
+ import functools
5
+
6
+ import venusian
7
+ from pyramid.exceptions import ConfigurationError
8
+ from pyramid.interfaces import IRendererFactory
9
+ from pyramid.response import Response
10
+
11
+ from kinto.core.cornice.util import func_name, is_string, to_list
12
+ from kinto.core.cornice.validators import (
13
+ DEFAULT_FILTERS,
14
+ DEFAULT_VALIDATORS,
15
+ )
16
+
17
+
18
+ SERVICES = []
19
+
20
+
21
+ def clear_services():
22
+ SERVICES[:] = []
23
+
24
+
25
+ def get_services(names=None, exclude=None):
26
+ def _keep(service):
27
+ if exclude is not None and service.name in exclude:
28
+ # excluded !
29
+ return False
30
+
31
+ # in white list or no white list provided
32
+ return names is None or service.name in names
33
+
34
+ return [service for service in SERVICES if _keep(service)]
35
+
36
+
37
+ class Service(object):
38
+ """Contains a service definition (in the definition attribute).
39
+
40
+ A service is composed of a path and many potential methods, associated
41
+ with context.
42
+
43
+ All the class attributes defined in this class or in children are
44
+ considered default values.
45
+
46
+ :param name:
47
+ The name of the service. Should be unique among all the services.
48
+
49
+ :param path:
50
+ The path the service is available at. Should also be unique.
51
+
52
+ :param pyramid_route:
53
+ Use existing pyramid route instead of creating new one.
54
+
55
+ :param renderer:
56
+ The renderer that should be used by this service. Default value is
57
+ 'cornicejson'.
58
+
59
+ :param description:
60
+ The description of what the webservice does. This is primarily intended
61
+ for documentation purposes.
62
+
63
+ :param validators:
64
+ A list of callables to pass the request into before passing it to the
65
+ associated view.
66
+
67
+ :param filters:
68
+ A list of callables to pass the response into before returning it to
69
+ the client.
70
+
71
+ :param accept:
72
+ A list of ``Accept`` header values accepted for this service
73
+ (or method if overwritten when defining a method).
74
+ It can also be a callable, in which case the values will be
75
+ discovered at runtime. If a callable is passed, it should be able
76
+ to take the request as a first argument.
77
+
78
+ :param content_type:
79
+ A list of ``Content-Type`` header values accepted for this service
80
+ (or method if overwritten when defining a method).
81
+ It can also be a callable, in which case the values will be
82
+ discovered at runtime. If a callable is passed, it should be able
83
+ to take the request as a first argument.
84
+
85
+ :param factory:
86
+ A factory returning callables which return boolean values. The
87
+ callables take the request as their first argument and return boolean
88
+ values.
89
+
90
+ :param permission:
91
+ As for ``pyramid.config.Configurator.add_view()``.
92
+ Note: `permission` can also be applied
93
+ to instance method decorators such as :meth:`~get` and :meth:`~put`.
94
+
95
+ :param klass:
96
+ The class to use when resolving views (if they are not callables)
97
+
98
+ :param error_handler:
99
+ A callable which is used to render responses following validation
100
+ failures. By default it will call the registered renderer
101
+ `render_errors` method.
102
+
103
+ :param traverse:
104
+ A traversal pattern that will be passed on route declaration and that
105
+ will be used as the traversal path.
106
+
107
+ There are also a number of parameters that are related to the support of
108
+ CORS (Cross Origin Resource Sharing). You can read the CORS specification
109
+ at http://www.w3.org/TR/cors/
110
+
111
+ :param cors_enabled:
112
+ To use if you especially want to disable CORS support for a particular
113
+ service / method.
114
+
115
+ :param cors_origins:
116
+ The list of origins for CORS. You can use wildcards here if needed,
117
+ e.g. ('list', 'of', '\\*.domain').
118
+
119
+ :param cors_headers:
120
+ The list of headers supported for the services.
121
+
122
+ :param cors_credentials:
123
+ Should the client send credential information (False by default).
124
+
125
+ :param cors_max_age:
126
+ Indicates how long the results of a preflight request can be cached in
127
+ a preflight result cache.
128
+
129
+ :param cors_expose_all_headers:
130
+ If set to True, all the headers will be exposed and considered valid
131
+ ones (Default: True). If set to False, all the headers need be
132
+ explicitly mentioned with the cors_headers parameter.
133
+
134
+ :param cors_policy:
135
+ It may be easier to have an external object containing all the policy
136
+ information related to CORS, e.g::
137
+
138
+ >>> cors_policy = {'origins': ('*',), 'max_age': 42,
139
+ ... 'credentials': True}
140
+
141
+ You can pass a dict here and all the values will be
142
+ unpacked and considered rather than the parameters starting by `cors_`
143
+ here.
144
+
145
+ See
146
+ https://pyramid.readthedocs.io/en/1.0-branch/glossary.html#term-acl
147
+ for more information about ACLs.
148
+
149
+ Service cornice instances also have methods :meth:`~get`, :meth:`~post`,
150
+ :meth:`~put`, :meth:`~options` and :meth:`~delete` are decorators that can
151
+ be used to decorate views.
152
+ """
153
+
154
+ renderer = "cornicejson"
155
+ default_validators = DEFAULT_VALIDATORS
156
+ default_filters = DEFAULT_FILTERS
157
+
158
+ mandatory_arguments = ("renderer",)
159
+ list_arguments = ("validators", "filters", "cors_headers", "cors_origins")
160
+
161
+ def __repr__(self):
162
+ return "<Service %s at %s>" % (self.name, self.pyramid_route or self.path)
163
+
164
+ def __init__(
165
+ self,
166
+ name,
167
+ path=None,
168
+ description=None,
169
+ cors_policy=None,
170
+ depth=1,
171
+ pyramid_route=None,
172
+ **kw,
173
+ ):
174
+ self.name = name
175
+ self.path = path
176
+ self.pyramid_route = pyramid_route
177
+
178
+ if not self.path and not self.pyramid_route:
179
+ raise TypeError("You need to pass path or pyramid_route arg")
180
+
181
+ self.description = description
182
+ self.cors_expose_all_headers = True
183
+ self._cors_enabled = None
184
+
185
+ if cors_policy:
186
+ for key, value in cors_policy.items():
187
+ kw.setdefault("cors_" + key, value)
188
+
189
+ for key in self.list_arguments:
190
+ # default_{validators,filters} and {filters,validators} don't
191
+ # have to be mutables, so we need to create a new list from them
192
+ extra = to_list(kw.get(key, []))
193
+ kw[key] = []
194
+ kw[key].extend(getattr(self, "default_%s" % key, []))
195
+ kw[key].extend(extra)
196
+
197
+ self.arguments = self.get_arguments(kw)
198
+ for key, value in self.arguments.items():
199
+ # avoid squashing Service.decorator if ``decorator``
200
+ # argument is used to specify a default pyramid view
201
+ # decorator
202
+ if key != "decorator":
203
+ setattr(self, key, value)
204
+
205
+ if hasattr(self, "acl"):
206
+ raise ConfigurationError("'acl' is not supported")
207
+
208
+ # instantiate some variables we use to keep track of what's defined for
209
+ # this service.
210
+ self.defined_methods = []
211
+ self.definitions = []
212
+
213
+ # add this service to the list of available services
214
+ SERVICES.append(self)
215
+
216
+ # this callback will be called when config.scan (from pyramid) will
217
+ # be triggered.
218
+ def callback(context, name, ob):
219
+ config = context.config.with_package(info.module)
220
+ config.add_cornice_service(self)
221
+
222
+ info = venusian.attach(self, callback, category="pyramid", depth=depth)
223
+
224
+ def default_error_handler(self, request):
225
+ """Default error_handler.
226
+
227
+ Uses the renderer for the service to render `request.errors`.
228
+ Only works if the registered renderer for the Service exposes the
229
+ method `render_errors`, which is implemented by default by
230
+ :class:`cornice.renderer.CorniceRenderer`.
231
+
232
+ :param request: the current Request.
233
+ """
234
+ renderer = request.registry.queryUtility(IRendererFactory, name=self.renderer)
235
+ return renderer.render_errors(request)
236
+
237
+ def get_arguments(self, conf=None):
238
+ """Return a dictionary of arguments. Takes arguments from the :param
239
+ conf: param and merges it with the arguments passed in the constructor.
240
+
241
+ :param conf: the dictionary to use.
242
+ """
243
+ if conf is None:
244
+ conf = {}
245
+
246
+ arguments = {}
247
+ for arg in self.mandatory_arguments:
248
+ # get the value from the passed conf, then from the instance, then
249
+ # from the default class settings.
250
+ arguments[arg] = conf.pop(arg, getattr(self, arg, None))
251
+
252
+ for arg in self.list_arguments:
253
+ # rather than overwriting, extend the defined lists if
254
+ # any. take care of re-creating the lists before appending
255
+ # items to them, to avoid modifications to the already
256
+ # existing ones
257
+ value = list(getattr(self, arg, []))
258
+ if arg in conf:
259
+ value.extend(to_list(conf.pop(arg)))
260
+ arguments[arg] = value
261
+
262
+ # Allow custom error handler
263
+ arguments["error_handler"] = conf.pop(
264
+ "error_handler", getattr(self, "error_handler", self.default_error_handler)
265
+ )
266
+
267
+ # exclude some validators or filters
268
+ if "exclude" in conf:
269
+ for item in to_list(conf.pop("exclude")):
270
+ for container in arguments["validators"], arguments["filters"]:
271
+ if item in container:
272
+ container.remove(item)
273
+
274
+ # also include the other key,value pair we don't know anything about
275
+ arguments.update(conf)
276
+
277
+ # if some keys have been defined service-wide, then we need to add
278
+ # them to the returned dict.
279
+ if hasattr(self, "arguments"):
280
+ for key, value in self.arguments.items():
281
+ if key not in arguments:
282
+ arguments[key] = value
283
+
284
+ return arguments
285
+
286
+ def add_view(self, method, view, **kwargs):
287
+ """Add a view to a method and arguments.
288
+
289
+ All the :class:`Service` keyword params except `name` and `path`
290
+ can be overwritten here. Additionally,
291
+ :meth:`~cornice.service.Service.api` has following keyword params:
292
+
293
+ :param method: The request method. Should be one of 'GET', 'POST',
294
+ 'PUT', 'DELETE', 'OPTIONS', 'TRACE', or 'CONNECT'.
295
+ :param view: the view to hook to
296
+ :param **kwargs: additional configuration for this view,
297
+ including `permission`.
298
+ """
299
+ method = method.upper()
300
+
301
+ if "klass" in kwargs and not callable(view):
302
+ view = _UnboundView(kwargs["klass"], view)
303
+
304
+ args = self.get_arguments(kwargs)
305
+
306
+ # remove 'factory' if present,
307
+ # it's not a valid pyramid view param
308
+ if "factory" in args:
309
+ del args["factory"]
310
+
311
+ if hasattr(self, "get_view_wrapper"):
312
+ view = self.get_view_wrapper(kwargs)(view)
313
+ self.definitions.append((method, view, args))
314
+
315
+ # keep track of the defined methods for the service
316
+ if method not in self.defined_methods:
317
+ self.defined_methods.append(method)
318
+
319
+ # auto-define a HEAD method if we have a definition for GET.
320
+ if method == "GET":
321
+ self.definitions.append(("HEAD", view, args))
322
+ if "HEAD" not in self.defined_methods:
323
+ self.defined_methods.append("HEAD")
324
+
325
+ def decorator(self, method, **kwargs):
326
+ """Add the ability to define methods using python's decorators
327
+ syntax.
328
+
329
+ For instance, it is possible to do this with this method::
330
+
331
+ service = Service("blah", "/blah")
332
+ @service.decorator("get", accept="application/json")
333
+ def my_view(request):
334
+ pass
335
+ """
336
+
337
+ def wrapper(view):
338
+ self.add_view(method, view, **kwargs)
339
+ return view
340
+
341
+ return wrapper
342
+
343
+ def get(self, **kwargs):
344
+ """Add the ability to define get using python's decorators
345
+ syntax.
346
+
347
+ For instance, it is possible to do this with this method::
348
+
349
+ service = Service("blah", "/blah")
350
+ @service.get(accept="application/json")
351
+ def my_view(request):
352
+ pass
353
+ """
354
+ return self.decorator("GET", **kwargs)
355
+
356
+ def post(self, **kwargs):
357
+ """Add the ability to define post using python's decorators
358
+ syntax.
359
+
360
+ For instance, it is possible to do this with this method::
361
+
362
+ service = Service("blah", "/blah")
363
+ @service.post(accept="application/json")
364
+ def my_view(request):
365
+ pass
366
+ """
367
+ return self.decorator("POST", **kwargs)
368
+
369
+ def put(self, **kwargs):
370
+ """Add the ability to define put using python's decorators
371
+ syntax.
372
+
373
+ For instance, it is possible to do this with this method::
374
+
375
+ service = Service("blah", "/blah")
376
+ @service.put(accept="application/json")
377
+ def my_view(request):
378
+ pass
379
+ """
380
+ return self.decorator("PUT", **kwargs)
381
+
382
+ def delete(self, **kwargs):
383
+ """Add the ability to define delete using python's decorators
384
+ syntax.
385
+
386
+ For instance, it is possible to do this with this method::
387
+
388
+ service = Service("blah", "/blah")
389
+ @service.delete(accept="application/json")
390
+ def my_view(request):
391
+ pass
392
+ """
393
+ return self.decorator("DELETE", **kwargs)
394
+
395
+ def options(self, **kwargs):
396
+ """Add the ability to define options using python's decorators
397
+ syntax.
398
+
399
+ For instance, it is possible to do this with this method::
400
+
401
+ service = Service("blah", "/blah")
402
+ @service.options(accept="application/json")
403
+ def my_view(request):
404
+ pass
405
+ """
406
+ return self.decorator("OPTIONS", **kwargs)
407
+
408
+ def patch(self, **kwargs):
409
+ """Add the ability to define patch using python's decorators
410
+ syntax.
411
+
412
+ For instance, it is possible to do this with this method::
413
+
414
+ service = Service("blah", "/blah")
415
+ @service.patch(accept="application/json")
416
+ def my_view(request):
417
+ pass
418
+ """
419
+ return self.decorator("PATCH", **kwargs)
420
+
421
+ def filter_argumentlist(self, method, argname, filter_callables=False):
422
+ """
423
+ Helper method to ``get_acceptable`` and ``get_contenttypes``. DRY.
424
+ """
425
+ result = []
426
+ for meth, view, args in self.definitions:
427
+ if meth.upper() == method.upper():
428
+ result_tmp = to_list(args.get(argname))
429
+ if filter_callables:
430
+ result_tmp = [a for a in result_tmp if not callable(a)]
431
+ result.extend(result_tmp)
432
+ return result
433
+
434
+ def get_acceptable(self, method, filter_callables=False):
435
+ """return a list of acceptable egress content-type headers that were
436
+ defined for this service.
437
+
438
+ :param method: the method to get the acceptable egress content-types
439
+ for.
440
+ :param filter_callables: it is possible to give acceptable
441
+ content-types dynamically, with callables.
442
+ This toggles filtering the callables (default:
443
+ False)
444
+ """
445
+ return self.filter_argumentlist(method, "accept", filter_callables)
446
+
447
+ def get_contenttypes(self, method, filter_callables=False):
448
+ """return a list of supported ingress content-type headers that were
449
+ defined for this service.
450
+
451
+ :param method: the method to get the supported ingress content-types
452
+ for.
453
+ :param filter_callables: it is possible to give supported
454
+ content-types dynamically, with callables.
455
+ This toggles filtering the callables (default:
456
+ False)
457
+ """
458
+ return self.filter_argumentlist(method, "content_type", filter_callables)
459
+
460
+ def get_validators(self, method):
461
+ """return a list of validators for the given method.
462
+
463
+ :param method: the method to get the validators for.
464
+ """
465
+ validators = []
466
+ for meth, view, args in self.definitions:
467
+ if meth.upper() == method.upper() and "validators" in args:
468
+ for validator in args["validators"]:
469
+ if validator not in validators:
470
+ validators.append(validator)
471
+ return validators
472
+
473
+ @property
474
+ def cors_enabled(self):
475
+ if self._cors_enabled is False:
476
+ return False
477
+
478
+ return bool(self.cors_origins or self._cors_enabled)
479
+
480
+ @cors_enabled.setter
481
+ def cors_enabled(self, value):
482
+ self._cors_enabled = value
483
+
484
+ def cors_supported_headers_for(self, method=None):
485
+ """Return an iterable of supported headers for this service.
486
+
487
+ The supported headers are defined by the :param headers: argument
488
+ that is passed to services or methods, at definition time.
489
+ """
490
+ headers = set()
491
+ for meth, _, args in self.definitions:
492
+ if args.get("cors_enabled", True):
493
+ exposed_headers = args.get("cors_headers", ())
494
+ if method is not None:
495
+ if meth.upper() == method.upper():
496
+ return set(exposed_headers)
497
+ else:
498
+ headers |= set(exposed_headers)
499
+ return headers
500
+
501
+ @property
502
+ def cors_supported_methods(self):
503
+ """Return an iterable of methods supported by CORS"""
504
+ methods = []
505
+ for meth, _, args in self.definitions:
506
+ if args.get("cors_enabled", True) and meth not in methods:
507
+ methods.append(meth)
508
+ return methods
509
+
510
+ @property
511
+ def cors_supported_origins(self):
512
+ origins = set(getattr(self, "cors_origins", ()))
513
+ for _, _, args in self.definitions:
514
+ origins |= set(args.get("cors_origins", ()))
515
+ return origins
516
+
517
+ def cors_origins_for(self, method):
518
+ """Return the list of origins supported for a given HTTP method"""
519
+ origins = set()
520
+ for meth, view, args in self.definitions:
521
+ if meth.upper() == method.upper():
522
+ origins |= set(args.get("cors_origins", ()))
523
+
524
+ if not origins:
525
+ origins = self.cors_origins
526
+ return origins
527
+
528
+ def cors_support_credentials_for(self, method=None):
529
+ """Returns if the given method support credentials.
530
+
531
+ :param method:
532
+ The method to check the credentials support for
533
+ """
534
+ for meth, view, args in self.definitions:
535
+ if method and meth.upper() == method.upper():
536
+ return args.get("cors_credentials", False)
537
+
538
+ if getattr(self, "cors_credentials", False):
539
+ return self.cors_credentials
540
+ return False
541
+
542
+ def cors_max_age_for(self, method=None):
543
+ max_age = None
544
+ for meth, view, args in self.definitions:
545
+ if method and meth.upper() == method.upper():
546
+ max_age = args.get("cors_max_age", None)
547
+ break
548
+
549
+ if max_age is None:
550
+ max_age = getattr(self, "cors_max_age", None)
551
+ return max_age
552
+
553
+
554
+ def decorate_view(view, args, method, route_args={}):
555
+ """Decorate a given view with cornice niceties.
556
+
557
+ This function returns a function with the same signature than the one
558
+ you give as :param view:
559
+
560
+ :param view: the view to decorate
561
+ :param args: the args to use for the decoration
562
+ :param method: the HTTP method
563
+ :param route_args: the args used for the associated route
564
+ """
565
+
566
+ def wrapper(request):
567
+ # if the args contain a klass argument then use it to resolve the view
568
+ # location (if the view argument isn't a callable)
569
+ ob = None
570
+ view_ = view
571
+ if "klass" in args and not callable(view):
572
+ # XXX: given that request.context exists and root-factory
573
+ # only expects request param, having params seems unnecessary
574
+ # ob = args['klass'](request)
575
+ params = dict(request=request)
576
+ if "factory" in route_args:
577
+ params["context"] = request.context
578
+ ob = args["klass"](**params)
579
+ if is_string(view):
580
+ view_ = getattr(ob, view.lower())
581
+ elif isinstance(view, _UnboundView):
582
+ view_ = view.make_bound_view(ob)
583
+
584
+ # the validators can either be a list of callables or contain some
585
+ # non-callable values. In which case we want to resolve them using the
586
+ # object if any
587
+ validators = args.get("validators", ())
588
+ for validator in validators:
589
+ if is_string(validator) and ob is not None:
590
+ validator = getattr(ob, validator)
591
+ validator(request, **args)
592
+
593
+ # only call the view if we don't have validation errors
594
+ if len(request.errors) == 0:
595
+ try:
596
+ # If we have an object, it already has the request.
597
+ if ob:
598
+ response = view_()
599
+ else:
600
+ response = view_(request)
601
+ except Exception:
602
+ # cors headers need to be set if an exception was raised
603
+ request.info["cors_checked"] = False
604
+ raise
605
+
606
+ # check for errors and return them if any
607
+ if len(request.errors) > 0:
608
+ # We already checked for CORS, but since the response is created
609
+ # again, we want to do that again before returning the response.
610
+ request.info["cors_checked"] = False
611
+ return args["error_handler"](request)
612
+
613
+ # if the view returns its own response, cors headers need to be set
614
+ if isinstance(response, Response):
615
+ request.info["cors_checked"] = False
616
+
617
+ # We can't apply filters at this level, since "response" may not have
618
+ # been rendered into a proper Response object yet. Instead, give the
619
+ # request a reference to its api_kwargs so that a tween can apply them.
620
+ # We also pass the object we created (if any) so we can use it to find
621
+ # the filters that are in fact methods.
622
+ request.cornice_args = (args, ob)
623
+ return response
624
+
625
+ # return the wrapper, not the function, keep the same signature
626
+ if not is_string(view):
627
+ functools.update_wrapper(wrapper, view)
628
+
629
+ # Set the wrapper name to something useful
630
+ wrapper.__name__ = "{0}__{1}".format(func_name(view), method)
631
+ return wrapper
632
+
633
+
634
+ class _UnboundView(object):
635
+ def __init__(self, klass, view):
636
+ self.unbound_view = getattr(klass, view.lower())
637
+ functools.update_wrapper(self, self.unbound_view)
638
+ self.__name__ = func_name(self.unbound_view)
639
+
640
+ def make_bound_view(self, ob):
641
+ return functools.partial(self.unbound_view, ob)