summaryrefslogtreecommitdiff
path: root/repoze/bfg/configuration.py
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-04-14 02:49:19 +0000
committerChris McDonough <chrism@agendaless.com>2010-04-14 02:49:19 +0000
commitff1213e8f2aed987108ba57aed517c033491b1aa (patch)
treef531544c3373ae7d5b51746987cb373326277a9c /repoze/bfg/configuration.py
parent2b6bc8adfa294f7133680f64df411251afb67dfc (diff)
downloadpyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.tar.gz
pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.tar.bz2
pyramid-ff1213e8f2aed987108ba57aed517c033491b1aa.zip
Add "exception views" work contributed primarily by Andrey Popp by merging the "phash" branch.
Diffstat (limited to 'repoze/bfg/configuration.py')
-rw-r--r--repoze/bfg/configuration.py324
1 files changed, 221 insertions, 103 deletions
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index 025c6e048..3d1c0214d 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -2,6 +2,7 @@ import os
import re
import sys
import threading
+import types
import inspect
from webob import Response
@@ -17,9 +18,8 @@ from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
from repoze.bfg.interfaces import IDebugLogger
from repoze.bfg.interfaces import IDefaultRootFactory
-from repoze.bfg.interfaces import IForbiddenView
+from repoze.bfg.interfaces import IExceptionViewClassifier
from repoze.bfg.interfaces import IMultiView
-from repoze.bfg.interfaces import INotFoundView
from repoze.bfg.interfaces import IPackageOverrides
from repoze.bfg.interfaces import IRendererFactory
from repoze.bfg.interfaces import IRequest
@@ -32,12 +32,14 @@ from repoze.bfg.interfaces import ISettings
from repoze.bfg.interfaces import ITemplateRenderer
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
+from repoze.bfg.interfaces import IViewClassifier
from repoze.bfg import chameleon_text
from repoze.bfg import chameleon_zpt
from repoze.bfg import renderers
from repoze.bfg.authorization import ACLAuthorizationPolicy
from repoze.bfg.compat import all
+from repoze.bfg.compat import md5
from repoze.bfg.compat import walk_packages
from repoze.bfg.events import WSGIApplicationCreatedEvent
from repoze.bfg.exceptions import Forbidden
@@ -59,8 +61,11 @@ from repoze.bfg.traversal import find_interface
from repoze.bfg.urldispatch import RoutesMapper
from repoze.bfg.view import render_view_to_response
from repoze.bfg.view import static
+from repoze.bfg.view import default_notfound_view
+from repoze.bfg.view import default_forbidden_view
-MAX_WEIGHT = 10000
+MAX_ORDER = 1 << 30
+DEFAULT_PHASH = md5().hexdigest()
DEFAULT_RENDERERS = (
('.pt', chameleon_zpt.renderer_factory),
@@ -206,7 +211,8 @@ class Configurator(object):
def _derive_view(self, view, permission=None, predicates=(),
attr=None, renderer_name=None, wrapper_viewname=None,
- viewname=None, accept=None, score=MAX_WEIGHT):
+ viewname=None, accept=None, order=MAX_ORDER,
+ phash=DEFAULT_PHASH):
renderer = self._renderer_from_name(renderer_name)
authn_policy = self.registry.queryUtility(IAuthenticationPolicy)
authz_policy = self.registry.queryUtility(IAuthorizationPolicy)
@@ -220,7 +226,7 @@ class Configurator(object):
authn_policy, authz_policy, settings,
logger)
predicated_view = _predicate_wrap(debug_view, predicates)
- derived_view = _attr_wrap(predicated_view, accept, score)
+ derived_view = _attr_wrap(predicated_view, accept, order, phash)
return derived_view
def _override(self, package, path, override_package, override_prefix,
@@ -235,23 +241,6 @@ class Configurator(object):
name=pkg_name, info=_info)
override.insert(path, override_pkg_name, override_prefix)
-
- def _system_view(self, iface, view=None, attr=None, renderer=None,
- wrapper=None, _info=u''):
- if not view:
- if renderer:
- def view(context, request):
- return {}
- else:
- raise ConfigurationError(
- '"view" argument was not specified and no renderer '
- 'specified')
-
- derived_view = self._derive_view(view, attr=attr,
- renderer_name=renderer,
- wrapper_viewname=wrapper)
- self.registry.registerUtility(derived_view, iface, '', info=_info)
-
def _set_security_policies(self, authentication, authorization=None):
if authorization is None:
authorization = ACLAuthorizationPolicy() # default
@@ -312,6 +301,8 @@ class Configurator(object):
authorization_policy)
for name, renderer in renderers:
self.add_renderer(name, renderer)
+ self.set_notfound_view(default_notfound_view)
+ self.set_forbidden_view(default_forbidden_view)
# getSiteManager is a unit testing dep injection
def hook_zca(self, getSiteManager=None):
@@ -723,14 +714,15 @@ class Configurator(object):
view_info.append(info)
return
- score, predicates = _make_predicates(
+ order, predicates, phash = _make_predicates(
xhr=xhr, request_method=request_method, path_info=path_info,
request_param=request_param, header=header, accept=accept,
containment=containment, request_type=request_type,
custom=custom_predicates)
derived_view = self._derive_view(view, permission, predicates, attr,
- renderer, wrapper, name, accept, score)
+ renderer, wrapper, name, accept, order,
+ phash)
if context is None:
context = for_
@@ -764,41 +756,76 @@ class Configurator(object):
old_view = None
for view_type in (IView, ISecuredView, IMultiView):
- old_view = registered((request_iface, r_context), view_type, name)
+ old_view = registered((IViewClassifier, request_iface, r_context),
+ view_type, name)
if old_view is not None:
break
-
- if old_view is None:
- # No component was registered for any of our I*View
- # interfaces exactly; this is the first view for this
- # triad. We don't need a multiview.
- if hasattr(derived_view, '__call_permissive__'):
+
+ is_exception_view = isexception(context)
+
+ def regclosure():
+ if hasattr(view, '__call_permissive__'):
view_iface = ISecuredView
else:
view_iface = IView
- self.registry.registerAdapter(derived_view,
- (request_iface, context),
- view_iface, name, info=_info)
+ self.registry.registerAdapter(
+ derived_view, (IViewClassifier, request_iface, context),
+ view_iface, name, info=_info)
+ if is_exception_view:
+ self.registry.registerAdapter(
+ derived_view,
+ (IExceptionViewClassifier, request_iface, context),
+ view_iface, name, info=_info)
+
+ is_multiview = IMultiView.providedBy(old_view)
+ old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
+
+ if old_view is None:
+ # - No component was yet registered for any of our I*View
+ # interfaces exactly; this is the first view for this
+ # triad.
+ regclosure()
+
+ elif (not is_multiview) and (old_phash == phash):
+ # - A single view component was previously registered with
+ # the same predicate hash as this view; this registration
+ # is therefore an override.
+ regclosure()
+
else:
+ # - A view or multiview was already registered for this
+ # triad, and the new view is not an override.
+
# XXX we could try to be more efficient here and register
# a non-secured view for a multiview if none of the
# multiview's consituent views have a permission
# associated with them, but this code is getting pretty
# rough already
- if IMultiView.providedBy(old_view):
+ if is_multiview:
multiview = old_view
else:
multiview = MultiView(name)
old_accept = getattr(old_view, '__accept__', None)
- old_score = getattr(old_view, '__score__', MAX_WEIGHT)
- multiview.add(old_view, old_score, old_accept)
- multiview.add(derived_view, score, accept)
+ old_order = getattr(old_view, '__order__', MAX_ORDER)
+ multiview.add(old_view, old_order, old_accept, old_phash)
+ multiview.add(derived_view, order, accept, phash)
for view_type in (IView, ISecuredView):
# unregister any existing views
self.registry.adapters.unregister(
- (request_iface, r_context), view_type, name=name)
- self.registry.registerAdapter(multiview, (request_iface, context),
- IMultiView, name, info=_info)
+ (IViewClassifier, request_iface, r_context),
+ view_type, name=name)
+ if is_exception_view:
+ self.registry.adapters.unregister(
+ (IExceptionViewClassifier, request_iface, r_context),
+ view_type, name=name)
+ self.registry.registerAdapter(
+ multiview, (IViewClassifier, request_iface, context),
+ IMultiView, name, info=_info)
+ if is_exception_view:
+ self.registry.registerAdapter(
+ multiview,
+ (IExceptionViewClassifier, request_iface, context),
+ IMultiView, name, info=_info)
def add_route(self,
name,
@@ -1030,13 +1057,15 @@ class Configurator(object):
"""
# these are route predicates; if they do not match, the next route
# in the routelist will be tried
- _, predicates = _make_predicates(xhr=xhr,
- request_method=request_method,
- path_info=path_info,
- request_param=request_param,
- header=header,
- accept=accept,
- custom=custom_predicates)
+ ignored, predicates, ignored = _make_predicates(
+ xhr=xhr,
+ request_method=request_method,
+ path_info=path_info,
+ request_param=request_param,
+ header=header,
+ accept=accept,
+ custom=custom_predicates
+ )
request_iface = self.registry.queryUtility(IRouteRequest, name=name)
@@ -1172,10 +1201,32 @@ class Configurator(object):
override(package, path, override_package, override_prefix,
_info=_info)
- def set_forbidden_view(self, *arg, **kw):
+ def set_forbidden_view(self, view=None, attr=None, renderer=None,
+ wrapper=None, _info=u''):
""" Add a default forbidden view to the current configuration
state.
+ .. warning:: This method has been deprecated in
+ :mod:`repoze.bfg` 1.3. *Do not use it for new development;
+ it should only be used to support older code bases which
+ depend upon it.* See :ref:`changing_the_forbidden_view` to
+ see how a forbidden view should be registered in new
+ projects.
+
+ ..note:: For backwards compatibility with :mod:`repoze.bfg`
+ 1.2, unlike an 'exception view' as described in
+ :ref:`exception_views`, a ``context, request`` view
+ callable registered using this API should not expect to
+ receive the exception as its first (``context``) argument.
+ Instead it should expect to receive the 'real' context as
+ found via context-finding or ``None`` if no context could
+ be found. The exception causing the registered view to be
+ called is however still available as ``request.exception``.
+ .. warning:: This method has been deprecated in
+ :mod:`repoze.bfg` 1.3. See
+ :ref:`changing_the_forbidden_view` to see how a not found
+ view should be registered in :mod:`repoze.bfg` 1.3+.
+
The ``view`` argument should be a :term:`view callable`.
The ``attr`` argument should be the attribute of the view
@@ -1191,16 +1242,36 @@ class Configurator(object):
The ``wrapper`` argument should be the name of another view
which will wrap this view when rendered (see the ``add_view``
- method's ``wrapper`` argument for a description).
-
- See :ref:`changing_the_forbidden_view` for more
- information."""
- return self._system_view(IForbiddenView, *arg, **kw)
-
- def set_notfound_view(self, *arg, **kw):
+ method's ``wrapper`` argument for a description)."""
+ view = self._derive_view(view, attr=attr, renderer_name=renderer)
+ def bwcompat_view(context, request):
+ ctx = getattr(request, 'context', None)
+ return view(ctx, request)
+ return self.add_view(bwcompat_view, context=Forbidden,
+ wrapper=wrapper, _info=_info)
+
+ def set_notfound_view(self, view=None, attr=None, renderer=None,
+ wrapper=None, _info=u''):
""" Add a default not found view to the current configuration
state.
+ .. warning:: This method has been deprecated in
+ :mod:`repoze.bfg` 1.3. *Do not use it for new development;
+ it should only be used to support older code bases which
+ depend upon it.* See :ref:`changing_the_notfound_view` to
+ see how a not found view should be registered in new
+ projects.
+
+ ..note:: For backwards compatibility with :mod:`repoze.bfg`
+ 1.2, unlike an 'exception view' as described in
+ :ref:`exception_views`, a ``context, request`` view
+ callable registered using this API should not expect to
+ receive the exception as its first (``context``) argument.
+ Instead it should expect to receive the 'real' context as
+ found via context-finding or ``None`` if no context could
+ be found. The exception causing the registered view to be
+ called is however still available as ``request.exception``.
+
The ``view`` argument should be a :term:`view callable`.
The ``attr`` argument should be the attribute of the view
@@ -1217,11 +1288,13 @@ class Configurator(object):
The ``wrapper`` argument should be the name of another view
which will wrap this view when rendered (see the ``add_view``
method's ``wrapper`` argument for a description).
-
- See :ref:`changing_the_notfound_view` for more
- information.
"""
- return self._system_view(INotFoundView, *arg, **kw)
+ view = self._derive_view(view, attr=attr, renderer_name=renderer)
+ def bwcompat_view(context, request):
+ ctx = getattr(request, 'context', None)
+ return view(ctx, request)
+ return self.add_view(bwcompat_view, context=NotFound,
+ wrapper=wrapper, _info=_info)
def add_static_view(self, name, path, cache_max_age=3600, _info=u''):
""" Add a view used to render static resources to the current
@@ -1353,46 +1426,68 @@ class Configurator(object):
def _make_predicates(xhr=None, request_method=None, path_info=None,
request_param=None, header=None, accept=None,
containment=None, request_type=None, custom=()):
- # Predicates are added to the predicate list in (presumed)
+
+ # PREDICATES
+ # ----------
+ #
+ # Given an argument list, a predicate list is computed.
+ # Predicates are added to a predicate list in (presumed)
# computation expense order. All predicates associated with a
- # view must evaluate true for the view to "match" a request.
- # Elsewhere in the code, we evaluate them using a generator
- # expression. The fastest predicate should be evaluated first,
- # then the next fastest, and so on, as if one returns false, the
- # remainder of the predicates won't need to be evaluated.
-
- # Each predicate is associated with a weight value. The weight
- # symbolizes the relative potential "importance" of the predicate
- # to all other predicates. A larger weight indicates greater
- # importance. These weights are subtracted from an aggregate
- # 'weight' variable. The aggregate weight is then divided by the
- # length of the predicate list to compute a "score" for this view.
- # The score represents the ordering in which a "multiview" ( a
+ # view or route must evaluate true for the view or route to
+ # "match" during a request. Elsewhere in the code, we evaluate
+ # predicates using a generator expression. The fastest predicate
+ # should be evaluated first, then the next fastest, and so on, as
+ # if one returns false, the remainder of the predicates won't need
+ # to be evaluated.
+ #
+ # While we compute predicates, we also compute a predicate hash
+ # (aka phash) that can be used by a caller to identify identical
+ # predicate lists.
+ #
+ # ORDERING
+ # --------
+ #
+ # A "order" is computed for the predicate list. An order is
+ # a scoring.
+ #
+ # Each predicate is associated with a weight value, which is a
+ # multiple of 2. The weight of a predicate symbolizes the
+ # relative potential "importance" of the predicate to all other
+ # predicates. A larger weight indicates greater importance.
+ #
+ # All weights for a given predicate list are bitwise ORed together
+ # to create a "score"; this score is then subtracted from
+ # MAX_ORDER and divided by an integer representing the number of
+ # predicates+1 to determine the order.
+ #
+ # The order represents the ordering in which a "multiview" ( a
# collection of views that share the same context/request/name
# triad but differ in other ways via predicates) will attempt to
- # call its set of views. Views with lower scores will be tried
+ # call its set of views. Views with lower orders will be tried
# first. The intent is to a) ensure that views with more
# predicates are always evaluated before views with fewer
# predicates and b) to ensure a stable call ordering of views that
- # share the same number of predicates.
-
- # Views which do not have any predicates get a score of
- # MAX_WEIGHT, meaning that they will be tried very last.
+ # share the same number of predicates. Views which do not have
+ # any predicates get an order of MAX_ORDER, meaning that they will
+ # be tried very last.
predicates = []
- weight = MAX_WEIGHT
+ weights = []
+ h = md5()
if xhr:
def xhr_predicate(context, request):
return request.is_xhr
- weight = weight - 20
+ weights.append(1 << 0)
predicates.append(xhr_predicate)
+ h.update('xhr:%r' % bool(xhr))
if request_method is not None:
def request_method_predicate(context, request):
return request.method == request_method
- weight = weight - 30
+ weights.append(1 << 2)
predicates.append(request_method_predicate)
+ h.update('request_method:%r' % request_method)
if path_info is not None:
try:
@@ -1401,8 +1496,9 @@ def _make_predicates(xhr=None, request_method=None, path_info=None,
raise ConfigurationError(why[0])
def path_info_predicate(context, request):
return path_info_val.match(request.path_info) is not None
- weight = weight - 40
+ weights.append(1 << 3)
predicates.append(path_info_predicate)
+ h.update('path_info:%r' % path_info)
if request_param is not None:
request_param_val = None
@@ -1412,8 +1508,9 @@ def _make_predicates(xhr=None, request_method=None, path_info=None,
if request_param_val is None:
return request_param in request.params
return request.params.get(request_param) == request_param_val
- weight = weight - 50
+ weights.append(1 << 4)
predicates.append(request_param_predicate)
+ h.update('request_param:%r=%r' % (request_param, request_param_val))
if header is not None:
header_name = header
@@ -1429,35 +1526,43 @@ def _make_predicates(xhr=None, request_method=None, path_info=None,
return header_name in request.headers
val = request.headers.get(header_name)
return header_val.match(val) is not None
- weight = weight - 60
+ weights.append(1 << 5)
predicates.append(header_predicate)
+ h.update('header:%r=%r' % (header_name, header_val))
if accept is not None:
def accept_predicate(context, request):
return accept in request.accept
- weight = weight - 70
+ weights.append(1 << 6)
predicates.append(accept_predicate)
+ h.update('accept:%r' % accept)
if containment is not None:
def containment_predicate(context, request):
return find_interface(context, containment) is not None
- weight = weight - 80
+ weights.append(1 << 7)
predicates.append(containment_predicate)
+ h.update('containment:%r' % id(containment))
if request_type is not None:
def request_type_predicate(context, request):
return request_type.providedBy(request)
- weight = weight - 90
+ weights.append(1 << 8)
predicates.append(request_type_predicate)
+ h.update('request_type:%r' % id(request_type))
if custom:
- for predicate in custom:
- weight = weight - 100
+ for num, predicate in enumerate(custom):
predicates.append(predicate)
+ h.update('custom%s:%r' % (num, id(predicate)))
+ weights.append(1 << 9)
- # this will be == MAX_WEIGHT if no predicates
- score = weight / (len(predicates) + 1)
- return score, predicates
+ score = 0
+ for bit in weights:
+ score = score | bit
+ order = (MAX_ORDER - score) / (len(predicates) + 1)
+ phash = h.hexdigest()
+ return order, predicates, phash
class MultiView(object):
implements(IMultiView)
@@ -1468,13 +1573,19 @@ class MultiView(object):
self.views = []
self.accepts = []
- def add(self, view, score, accept=None):
+ def add(self, view, order, accept=None, phash=None):
+ if phash is not None:
+ for i, (s, v, h) in enumerate(list(self.views)):
+ if phash == h:
+ self.views[i] = (order, view, phash)
+ return
+
if accept is None or '*' in accept:
- self.views.append((score, view))
+ self.views.append((order, view, phash))
self.views.sort()
else:
subset = self.media_views.setdefault(accept, [])
- subset.append((score, view))
+ subset.append((order, view, phash))
subset.sort()
accepts = set(self.accepts)
accepts.add(accept)
@@ -1496,7 +1607,7 @@ class MultiView(object):
return self.views
def match(self, context, request):
- for score, view in self.get_views(request):
+ for order, view, phash in self.get_views(request):
if not hasattr(view, '__predicated__'):
return view
if view.__predicated__(context, request):
@@ -1515,7 +1626,7 @@ class MultiView(object):
return view(context, request)
def __call__(self, context, request):
- for score, view in self.get_views(request):
+ for order, view, phash in self.get_views(request):
try:
return view(context, request)
except NotFound:
@@ -1548,7 +1659,7 @@ def decorate_view(wrapped_view, original_view):
except AttributeError:
pass
try:
- wrapped_view.__score__ = original_view.__score__
+ wrapped_view.__order__ = original_view.__order__
except AttributeError:
pass
return True
@@ -1795,19 +1906,26 @@ def _authdebug_view(view, permission, authn_policy, authz_policy, settings,
return wrapped_view
-def _attr_wrap(view, accept, score):
+def _attr_wrap(view, accept, order, phash):
# this is a little silly but we don't want to decorate the original
- # function with attributes that indicate accept and score,
+ # function with attributes that indicate accept, order, and phash,
# so we use a wrapper
- if (accept is None) and (score == MAX_WEIGHT):
+ if (accept is None) and (order == MAX_ORDER) and (phash == DEFAULT_PHASH):
return view # defaults
def attr_view(context, request):
return view(context, request)
attr_view.__accept__ = accept
- attr_view.__score__ = score
+ attr_view.__order__ = order
+ attr_view.__phash__ = phash
decorate_view(attr_view, view)
return attr_view
+def isclass(o):
+ return isinstance(o, (type, types.ClassType))
+
+def isexception(o):
+ return isinstance(o, Exception) or isclass(o) and issubclass(o, Exception)
+
# note that ``options`` is a b/w compat alias for ``settings`` and
# ``Configurator`` is a testing dep inj
def make_app(root_factory, package=None, filename='configure.zcml',