1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
|
import mimetypes
import os
# See http://bugs.python.org/issue5853 which is a recursion bug
# that seems to effect Python 2.6, Python 2.6.1, and 2.6.2 (a fix
# has been applied on the Python 2 trunk). This workaround should
# really be in Paste if anywhere, but it's easiest to just do it
# here and get it over with to avoid needing to deal with any
# fallout.
if hasattr(mimetypes, 'init'):
mimetypes.init()
from webob.exc import HTTPFound
import venusian
from zope.deprecation import deprecated
from zope.interface import providedBy
from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg.path import package_path
from repoze.bfg.resource import resource_spec_from_abspath
from repoze.bfg.static import static_view as static # B/C
from repoze.bfg.threadlocal import get_current_registry
# b/c imports
from repoze.bfg.security import view_execution_permitted
view_execution_permitted # prevent PyFlakes from complaining
deprecated('view_execution_permitted',
"('from repoze.bfg.view import view_execution_permitted' was "
"deprecated as of repoze.bfg 1.0; instead use 'from "
"repoze.bfg.security import view_execution_permitted')",
)
deprecated('NotFound',
"('from repoze.bfg.view import NotFound' was "
"deprecated as of repoze.bfg 1.1; instead use 'from "
"repoze.bfg.exceptions import NotFound')",
)
static = static # dont yet deprecate this (ever?)
_marker = object()
def render_view_to_response(context, request, name='', secure=True):
""" Call the :term:`view callable` configured with a :term:`view
configuration` that matches the :term:`view name` ``name``
registered against the specified ``context`` and ``request`` and
return a :term:`response` object. This function will return
``None`` if a corresponding :term:`view callable` cannot be found
(when no :term:`view configuration` matches the combination of
``name`` / ``context`` / and ``request``).
If `secure`` is ``True``, and the :term:`view callable` found is
protected by a permission, the permission will be checked before
calling the view function. If the permission check disallows view
execution (based on the current :term:`authorization policy`), a
:exc:`repoze.bfg.exceptions.Forbidden` exception will be raised.
The exception's ``args`` attribute explains why the view access
was disallowed.
If ``secure`` is ``False``, no permission checking is done."""
provides = [IViewClassifier] + map(providedBy, (request, context))
try:
reg = request.registry
except AttributeError:
reg = get_current_registry()
view = reg.adapters.lookup(provides, IView, name=name)
if view is None:
return None
if not secure:
# the view will have a __call_permissive__ attribute if it's
# secured; otherwise it won't.
view = getattr(view, '__call_permissive__', view)
# if this view is secured, it will raise a Forbidden
# appropriately if the executing user does not have the proper
# permission
return view(context, request)
def render_view_to_iterable(context, request, name='', secure=True):
""" Call the :term:`view callable` configured with a :term:`view
configuration` that matches the :term:`view name` ``name``
registered against the specified ``context`` and ``request`` and
return an iterable object which represents the body of a response.
This function will return ``None`` if a corresponding :term:`view
callable` cannot be found (when no :term:`view configuration`
matches the combination of ``name`` / ``context`` / and
``request``). Additionally, this function will raise a
:exc:`ValueError` if a view function is found and called but the
view function's result does not have an ``app_iter`` attribute.
You can usually get the string representation of the return value
of this function by calling ``''.join(iterable)``, or just use
:func:`repoze.bfg.view.render_view` instead.
If ``secure`` is ``True``, and the view is protected by a
permission, the permission will be checked before the view
function is invoked. If the permission check disallows view
execution (based on the current :term:`authentication policy`), a
:exc:`repoze.bfg.exceptions.Forbidden` exception will be raised;
its ``args`` attribute explains why the view access was
disallowed.
If ``secure`` is ``False``, no permission checking is
done."""
response = render_view_to_response(context, request, name, secure)
if response is None:
return None
return response.app_iter
def render_view(context, request, name='', secure=True):
""" Call the :term:`view callable` configured with a :term:`view
configuration` that matches the :term:`view name` ``name``
registered against the specified ``context`` and ``request``
and unwind the view response's ``app_iter`` (see
:ref:`the_response`) into a single string. This function will
return ``None`` if a corresponding :term:`view callable` cannot be
found (when no :term:`view configuration` matches the combination
of ``name`` / ``context`` / and ``request``). Additionally, this
function will raise a :exc:`ValueError` if a view function is
found and called but the view function's result does not have an
``app_iter`` attribute. This function will return ``None`` if a
corresponding view cannot be found.
If ``secure`` is ``True``, and the view is protected by a
permission, the permission will be checked before the view is
invoked. If the permission check disallows view execution (based
on the current :term:`authorization policy`), a
:exc:`repoze.bfg.exceptions.Forbidden` exception will be raised;
its ``args`` attribute explains why the view access was
disallowed.
If ``secure`` is ``False``, no permission checking is done."""
iterable = render_view_to_iterable(context, request, name, secure)
if iterable is None:
return None
return ''.join(iterable)
def is_response(ob):
""" Return ``True`` if ``ob`` implements the interface implied by
:ref:`the_response`. ``False`` if not.
.. note:: this isn't a true interface check (in Zope terms), it's a
duck-typing check, as response objects are not obligated to
actually implement a Zope interface."""
# response objects aren't obligated to implement a Zope interface,
# so we do it the hard way
if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and
hasattr(ob, 'status') ):
if ( hasattr(ob.app_iter, '__iter__') and
hasattr(ob.headerlist, '__iter__') and
isinstance(ob.status, basestring) ) :
return True
return False
class bfg_view(object):
""" A function, class or method :term:`decorator` which allows a
developer to create view registrations nearer to a :term:`view
callable` definition than use of :term:`ZCML` or :term:`imperative
configuration` to do the same.
For example, this code in a module ``views.py``::
from models import MyModel
@bfg_view(name='my_view', context=MyModel, permission='read',
route_name='site1')
def my_view(context, request):
return 'OK'
Might replace the following call to the
:meth:`repoze.bfg.configuration.Configurator.add_view` method::
import views
import models
config.add_view(views.my_view, context=models.MyModel, name='my_view',
permission='read', 'route_name='site1')
Or might replace the following ZCML ``view`` declaration::
<view
for='.models.MyModel'
view='.views.my_view'
name='my_view'
permission='read'
route_name='site1'
/>
The following arguments are supported as arguments to
``bfg_view``: ``context``, ``permission``, ``name``,
``request_type``, ``route_name``, ``request_method``,
``request_param``, ``containment``, ``xhr``, ``accept``,
``header`` and ``path_info``.
``context`` should be a Python object or :term:`dotted Python
name` representing the context type that must be found for this
view to be called. If ``context`` is not supplied, the interface
``zope.interface.Interface`` (matching any context) is used. An
alias for ``context`` is ``for_``.
If ``permission`` is not supplied, no permission is registered for
this view (it's accessible by any caller).
If ``name`` is not supplied, the empty string is used (implying
the default view name).
If ``attr`` is not supplied, ``None`` is used (implying the
function itself if the view is a function, or the ``__call__``
callable attribute if the view is a class).
If ``renderer`` is not supplied, ``None`` is used (meaning that no
renderer is associated with this view).
If ``wrapper`` is not supplied, ``None`` is used (meaning that no
view wrapper is associated with this view).
If ``request_type`` is not supplied, the interface
:class:`repoze.bfg.interfaces.IRequest` is used, implying the
standard request interface type.
If ``route_name`` is not supplied, the view configuration is
considered to be made against a URL that doesn't match any defined
:term:`route`. The use of a ``route_name`` is an advanced
feature, useful only if you're also using :term:`url dispatch`.
If ``request_method`` is not supplied, this view will match a
request with any HTTP ``REQUEST_METHOD``
(GET/POST/PUT/HEAD/DELETE). If this parameter *is* supplied, it
must be a string naming an HTTP ``REQUEST_METHOD``, indicating
that this view will only match when the current request has a
``REQUEST_METHOD`` that matches this value.
If ``request_param`` is not supplied, this view will be called
when a request with any (or no) request GET or POST parameters is
encountered. If the value is present, it must be a string. If
the value supplied to the parameter has no ``=`` sign in it, it
implies that the key must exist in the ``request.params``
dictionary for this view to 'match' the current request. If the value
supplied to the parameter has a ``=`` sign in it, e.g.
``request_params="foo=123"``, then the key (``foo``) must both exist
in the ``request.params`` dictionary, and the value must match the
right hand side of the expression (``123``) for the view to "match" the
current request.
``containment`` should be a Python object or :term:`dotted Python
name` representing a class or interface type which must be found
as one of the context's location parents for this view to be
called. If ``containment`` is not supplied, this view will be
called when the context of the request has any (or no)
:term:`lineage`. If ``containment`` *is* supplied, it must be a
class or :term:`interface`, denoting that the view'matches' the
current request only if any graph :term:`lineage` node possesses
this class or interface.
If ``xhr`` is specified, it must be a boolean value. If the value
is ``True``, the view will only be invoked if the request's
``X-Requested-With`` header has the value ``XMLHttpRequest``.
If ``accept`` is specified, it must be a mimetype value. If
``accept`` is specified, the view will only be invoked if the
``Accept`` HTTP header matches the value requested. See the
description of ``accept`` in :ref:`view_directive` for information
about the allowable composition and matching behavior of this
value.
If ``header`` is specified, it must be a header name or a
``headername:headervalue`` pair. If ``header`` is specified, and
possesses a value the view will only be invoked if an HTTP header
matches the value requested. If ``header`` is specified without a
value (a bare header name only), the view will only be invoked if
the HTTP header exists with any value in the request. See the
description of ``header`` in :ref:`view_directive` for information
about the allowable composition and matching behavior of this
value.
If ``path_info`` is specified, it must be a regular
expression. The view will only be invoked if the ``PATH_INFO``
WSGI environment variable matches the expression.
If ``custom_predicates`` is specified, it must be a sequence of
:term:`predicate` callables (a predicate callable accepts two
arguments: ``context`` and ``request`` and returns ``True`` or
``False``). The view will only be invoked if all custom
predicates return ``True``.
Any individual or all parameters can be omitted. The simplest
``bfg_view`` declaration is::
@bfg_view()
def my_view(...):
...
Such a registration implies that the view name will be
``my_view``, registered for any :term:`context` object, using no
permission, registered against all non-URL-dispatch-based
requests, with any ``REQUEST_METHOD``, any set of request.params
values, without respect to any object in the :term:`lineage`.
The ``bfg_view`` decorator can also be used as a class decorator
in Python 2.6 and better (Python 2.5 and below do not support
class decorators)::
from webob import Response
from repoze.bfg.view import bfg_view
@bfg_view()
class MyView(object):
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self):
return Response('hello from %s!' % self.context)
In Python 2.5 and below, the ``bfg_view`` decorator can still be
used against a class, although not in decorator form::
from webob import Response
from repoze.bfg.view import bfg_view
class MyView(object):
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self):
return Response('hello from %s!' % self.context)
MyView = bfg_view()(MyView)
.. note:: When a view is a class, the calling semantics are
different than when it is a function or another
non-class callable. See :ref:`class_as_view` for more
information.
.. warning:: Using a class as a view is a new feature in 0.8.1+.
The bfg_view decorator can also be used against a class method::
from webob import Response
from repoze.bfg.view import bfg_view
class MyView(object):
def __init__(self, context, request):
self.context = context
self.request = request
@bfg_view(name='hello')
def amethod(self):
return Response('hello from %s!' % self.context)
When the ``bfg_view`` decorator is used against a class method, a
view is registered for the *class* (as described above), so the
class constructor must accept either ``request`` or ``context,
request``. The method which is decorated must return a response
(or rely on a :term:`renderer` to generate one). Using the
decorator against a particular method of a class is equivalent to
using the ``attr`` parameter in a decorator attached to the class
itself. For example, the above registration implied by the
decorator being used against the ``amethod`` method could be
spelled equivalently as::
from webob import Response
from repoze.bfg.view import bfg_view
@bfg_view(attr='amethod', name='hello')
class MyView(object):
def __init__(self, context, request):
self.context = context
self.request = request
def amethod(self):
return Response('hello from %s!' % self.context)
.. warning:: The ability to use the ``bfg_view`` decorator as a
method decorator is new in :mod:`repoze.bfg` version
1.1.
To make use of any ``bfg_view`` declaration, you must perform a
:term:`scan`. To do so, either insert the following boilerplate
into your application registry's ZCML::
<scan package="."/>
See :ref:`scan_directive` for more information about the ZCML
``scan`` directive.
Or, if you don't use ZCML, use the
:meth:`repoze.bfg.configuration.Configurator.scan` method::
config.scan()
"""
venusian = venusian # for testing injection
def __init__(self, name='', request_type=None, for_=None, permission=None,
route_name=None, request_method=None, request_param=None,
containment=None, attr=None, renderer=None, wrapper=None,
xhr=False, accept=None, header=None, path_info=None,
custom_predicates=(), context=None):
self.name = name
self.request_type = request_type
self.context = context or for_
self.permission = permission
self.route_name = route_name
self.request_method = request_method
self.request_param = request_param
self.containment = containment
self.attr = attr
self.renderer = renderer
self.wrapper = wrapper
self.xhr = xhr
self.accept = accept
self.header = header
self.path_info = path_info
self.custom_predicates = custom_predicates
def __call__(self, wrapped):
settings = self.__dict__.copy()
def callback(context, name, ob):
context.config.add_view(view=ob, **settings)
info = self.venusian.attach(wrapped, callback, category='bfg')
if info.scope == 'class':
# if the decorator was attached to a method in a class, or
# otherwise executed at class scope, we need to set an
# 'attr' into the settings if one isn't already in there
if settings['attr'] is None:
settings['attr'] = wrapped.__name__
# try to convert the renderer provided into a fully qualified
# resource specification
abspath = settings.get('renderer')
if abspath is not None and '.' in abspath:
isabs = os.path.isabs(abspath)
if not (':' in abspath and not isabs):
# not already a resource spec
if not isabs:
pp = package_path(info.module)
abspath = os.path.join(pp, abspath)
resource = resource_spec_from_abspath(abspath, info.module)
settings['renderer'] = resource
return wrapped
def default_exceptionresponse_view(context, request):
if not isinstance(context, Exception):
# backwards compat for an exception response view registered via
# config.set_notfound_view or config.set_forbidden_view
# instead of as a proper exception view
context = request.exception or context
return context
class AppendSlashNotFoundViewFactory(object):
""" There can only be one :term:`Not Found view` in any
:mod:`repoze.bfg` application. Even if you use
:func:`repoze.bfg.view.append_slash_notfound_view` as the Not
Found view, :mod:`repoze.bfg` still must generate a ``404 Not
Found`` response when it cannot redirect to a slash-appended URL;
this not found response will be visible to site users.
If you don't care what this 404 response looks like, and you only
need redirections to slash-appended route URLs, you may use the
:func:`repoze.bfg.view.append_slash_notfound_view` object as the
Not Found view. However, if you wish to use a *custom* notfound
view callable when a URL cannot be redirected to a slash-appended
URL, you may wish to use an instance of this class as the Not
Found view, supplying a :term:`view callable` to be used as the
custom notfound view as the first argument to its constructor.
For instance:
.. code-block:: python
from repoze.bfg.exceptions import NotFound
from repoze.bfg.view import AppendSlashNotFoundViewFactory
def notfound_view(context, request):
return HTTPNotFound('It aint there, stop trying!')
custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
config.add_view(custom_append_slash, context=NotFound)
The ``notfound_view`` supplied must adhere to the two-argument
view callable calling convention of ``(context, request)``
(``context`` will be the exception object).
.. note:: This class is new as of :mod:`repoze.bfg` version 1.3.
"""
def __init__(self, notfound_view=None):
if notfound_view is None:
notfound_view = default_exceptionresponse_view
self.notfound_view = notfound_view
def __call__(self, context, request):
if not isinstance(context, Exception):
# backwards compat for an append_notslash_view registered via
# config.set_notfound_view instead of as a proper exception view
context = request.exception
path = request.environ.get('PATH_INFO', '/')
registry = request.registry
mapper = registry.queryUtility(IRoutesMapper)
if mapper is not None and not path.endswith('/'):
slashpath = path + '/'
for route in mapper.get_routes():
if route.match(slashpath) is not None:
return HTTPFound(location=slashpath)
return self.notfound_view(context, request)
append_slash_notfound_view = AppendSlashNotFoundViewFactory()
append_slash_notfound_view.__doc__ = """\
For behavior like Django's ``APPEND_SLASH=True``, use this view as the
:term:`Not Found view` in your application.
When this view is the Not Found view (indicating that no view was
found), and any routes have been defined in the configuration of your
application, if the value of the ``PATH_INFO`` WSGI environment
variable does not already end in a slash, and if the value of
``PATH_INFO`` *plus* a slash matches any route's path, do an HTTP
redirect to the slash-appended PATH_INFO. Note that this will *lose*
``POST`` data information (turning it into a GET), so you shouldn't
rely on this to redirect POST requests.
If you use :term:`ZCML`, add the following to your application's
``configure.zcml`` to use this view as the Not Found view::
<view
context="repoze.bfg.exceptions.NotFound"
view="repoze.bfg.view.append_slash_notfound_view"/>
Or use the
:meth:`repoze.bfg.configuration.Configurator.add_view`
method if you don't use ZCML::
from repoze.bfg.exceptions import NotFound
from repoze.bfg.view import append_slash_notfound_view
config.add_view(append_slash_notfound_view, context=NotFound)
See also :ref:`changing_the_notfound_view`.
.. note:: This function is new as of :mod:`repoze.bfg` version 1.1.
"""
|