summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2010-12-31 02:51:44 -0500
committerChris McDonough <chrism@plope.com>2010-12-31 02:51:44 -0500
commitc26f03e1a920e950976e06c1a7f3b2e5e46c0754 (patch)
treedeb68628ecf37f74c068528f4a7cb5893eb5269b
parentb26badf557847bf5a55f896c63a3b6a97b468936 (diff)
parent4f355b634b32b77c6b35ddc31dfa772f075bf2ee (diff)
downloadpyramid-c26f03e1a920e950976e06c1a7f3b2e5e46c0754.tar.gz
pyramid-c26f03e1a920e950976e06c1a7f3b2e5e46c0754.tar.bz2
pyramid-c26f03e1a920e950976e06c1a7f3b2e5e46c0754.zip
Merge branch 'viewderiver'
Conflicts: pyramid/config.py
-rw-r--r--CHANGES.txt33
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--TODO.txt6
-rw-r--r--docs/api/interfaces.rst4
-rw-r--r--pyramid/config.py686
-rw-r--r--pyramid/httpexceptions.py1
-rw-r--r--pyramid/interfaces.py21
-rw-r--r--pyramid/renderers.py12
-rw-r--r--pyramid/tests/test_config.py122
-rw-r--r--pyramid/tests/test_view.py11
-rw-r--r--pyramid/url.py4
-rw-r--r--pyramid/view.py12
-rw-r--r--pyramid/zcml.py5
13 files changed, 578 insertions, 341 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index e7ecad31a..743f20e3b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,23 @@ Bug Fixes
Instead of trying to resolve the view, if it cannot, it will now just print
``<unknown>``.
+Features
+--------
+
+- ``config.add_view`` now accepts a ``decorator`` keyword argument, a callable
+ which will decorate the view callable before it is added to the registry.
+
+- ``config.add_view`` now accepts a ``view_mapper`` keyword argument, which
+ should be a class which implements the new
+ ``pyramid.interfaces.IViewMapperFactory`` interface. Use of an alternate
+ view mapper allows objects that are meant to be used as view callables to
+ have an arbitrary argument list and an arbitrary result. This feature will
+ be used by Pyramid extension developers, not by "civilians".
+
+- If a handler class provides an __action_decorator__ attribute (usually a
+ classmethod or staticmethod), use that as the decorator for each view
+ registration for that handler.
+
Documentation
-------------
@@ -16,6 +33,22 @@ Documentation
removed from the tutorials section. It was moved to the
``pyramid_tutorials`` Github repository.
+Internals
+---------
+
+- The "view derivation" code is now factored into a set of classes rather
+ than a large number of standalone functions (a side effect of the
+ ``view_mapper`` refactoring).
+
+- The ``pyramid.renderer.RendererHelper`` class has grown a ``render_view``
+ method, which is used by the default view mapper (a side effect of the
+ ``view_mapper`` refactoring).
+
+- The object passed as ``renderer`` to the "view deriver" is now an instance
+ of ``pyramid.renderers.RendererHelper`` rather than a dictionary (a side
+ effect of ``view_mapper`` refactoring).
+
+
1.0a8 (2010-12-27)
==================
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index ab75197e7..7b0364b6d 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -117,4 +117,6 @@ Contributors
- Casey Duncan, 2010/12/27
+- Rob Miller, 2010/12/28
+
- Marius Gedminas, 2010/12/31
diff --git a/TODO.txt b/TODO.txt
index ada3c4c2a..5acc923a1 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -11,8 +11,10 @@ Must-Have (before 1.0)
- Re-make testing.setUp() and testing.tearDown() the canonical APIs for test
configuration.
-- ``decorator=`` parameter to view_config. This would replace the existing
- _map_view "decorator" if it existed (Rob needs).
+- Document ``decorator=`` and ``view_mapper`` parameters to add_view.
+
+- Allow ``decorator=`` and ``view_mapper=`` to be passed via ZCML and the
+ ``view_config`` decorator.
Should-Have
-----------
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index b3c14e5f7..3ce926230 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -35,3 +35,7 @@ Other Interfaces
.. autointerface:: ITemplateRenderer
+ .. autointerface:: IViewMapperFactory
+
+ .. autointerface:: IViewMapper
+
diff --git a/pyramid/config.py b/pyramid/config.py
index 01341d92b..ee34adae1 100644
--- a/pyramid/config.py
+++ b/pyramid/config.py
@@ -45,6 +45,7 @@ from pyramid.interfaces import ITranslationDirectories
from pyramid.interfaces import ITraverser
from pyramid.interfaces import IView
from pyramid.interfaces import IViewClassifier
+from pyramid.interfaces import IViewMapperFactory
try:
from pyramid import chameleon_text
@@ -338,30 +339,34 @@ class Configurator(object):
def _split_spec(self, path_or_spec):
return resolve_asset_spec(path_or_spec, self.package_name)
+ # b/w compat
def _derive_view(self, view, permission=None, predicates=(),
attr=None, renderer=None, wrapper_viewname=None,
viewname=None, accept=None, order=MAX_ORDER,
phash=DEFAULT_PHASH):
- if renderer is None: # use default renderer if one exists
- default_renderer_factory = self.registry.queryUtility(
- IRendererFactory)
- if default_renderer_factory is not None:
- renderer = {'name':None, 'package':self.package}
view = self.maybe_dotted(view)
- authn_policy = self.registry.queryUtility(IAuthenticationPolicy)
- authz_policy = self.registry.queryUtility(IAuthorizationPolicy)
- settings = self.registry.settings
- logger = self.registry.queryUtility(IDebugLogger)
- mapped_view = _map_view(view, self.registry, attr, renderer)
- owrapped_view = _owrap_view(mapped_view, viewname, wrapper_viewname)
- secured_view = _secure_view(owrapped_view, permission,
- authn_policy, authz_policy)
- debug_view = _authdebug_view(secured_view, permission,
- authn_policy, authz_policy, settings,
- logger)
- predicated_view = _predicate_wrap(debug_view, predicates)
- derived_view = _attr_wrap(predicated_view, accept, order, phash)
- return derived_view
+ if isinstance(renderer, basestring):
+ renderer = RendererHelper(name=renderer, package=self.package,
+ registry = self.registry)
+ if renderer is None:
+ # use default renderer if one exists
+ if self.registry.queryUtility(IRendererFactory) is not None:
+ renderer = RendererHelper(name=None,
+ package=self.package,
+ registry=self.registry)
+ deriver = ViewDeriver(
+ registry=self.registry,
+ permission=permission,
+ predicates=predicates,
+ attr=attr,
+ renderer=renderer,
+ wrapper_viewname=wrapper_viewname,
+ viewname=viewname,
+ accept=accept,
+ order=order,
+ phash=phash,
+ package=self.package)
+ return deriver(view)
def _override(self, package, path, override_package, override_prefix,
PackageOverrides=PackageOverrides):
@@ -757,9 +762,6 @@ class Configurator(object):
a :term:`response` object. If a ``renderer`` argument is not
supplied, the user-supplied view must itself return a
:term:`response` object. """
-
- if renderer is not None and not isinstance(renderer, dict):
- renderer = {'name':renderer, 'package':self.package}
return self._derive_view(view, attr=attr, renderer=renderer)
@action_method
@@ -941,6 +943,20 @@ class Configurator(object):
pattern = route.pattern
+ action_decorator = getattr(handler, '__action_decorator__', None)
+ if action_decorator is not None:
+ if hasattr(action_decorator, 'im_self'):
+ # instance methods have an im_self == None
+ # classmethods have an im_self == cls
+ # staticmethods have no im_self
+ # instances have no im_self
+ if action_decorator.im_self is not handler:
+ raise ConfigurationError(
+ 'The "__action_decorator__" attribute of a handler '
+ 'must not be an instance method (must be a '
+ 'staticmethod, classmethod, function, or an instance '
+ 'which is a callable')
+
path_has_action = ':action' in pattern or '{action}' in pattern
if action and path_has_action:
@@ -970,7 +986,8 @@ class Configurator(object):
preds.append(ActionPredicate(action))
view_args['custom_predicates'] = preds
self.add_view(view=handler, attr=method_name,
- route_name=route_name, **view_args)
+ route_name=route_name,
+ decorator=action_decorator, **view_args)
else:
method_name = action
if method_name is None:
@@ -993,14 +1010,15 @@ class Configurator(object):
view_args = expose_config.copy()
del view_args['name']
self.add_view(view=handler, attr=meth_name,
- route_name=route_name, **view_args)
+ route_name=route_name,
+ decorator=action_decorator, **view_args)
# Now register the method itself
method = getattr(handler, method_name, None)
configs = getattr(method, '__exposed__', [{}])
for expose_config in configs:
self.add_view(view=handler, attr=action, route_name=route_name,
- **expose_config)
+ decorator=action_decorator, **expose_config)
return route
@@ -1010,7 +1028,7 @@ class Configurator(object):
request_param=None, containment=None, attr=None,
renderer=None, wrapper=None, xhr=False, accept=None,
header=None, path_info=None, custom_predicates=(),
- context=None):
+ context=None, decorator=None, view_mapper=None):
""" Add a :term:`view configuration` to the current
configuration state. Arguments to ``add_view`` are broken
down below into *predicate* arguments and *non-predicate*
@@ -1119,6 +1137,15 @@ class Configurator(object):
view is the same context and request of the inner view. If
this attribute is unspecified, no view wrapping is done.
+ decorator
+
+ A function which will be used to decorate the registered
+ :term:`view callable`. The decorator function will be
+ called with the view callable as a single argument, and it
+ must return a replacement view callable which accepts the
+ same arguments and returns the same type of values as the
+ original function.
+
Predicate Arguments
name
@@ -1255,6 +1282,18 @@ class Configurator(object):
the context and/or the request. If all callables return
``True``, the associated view callable will be considered
viable for a given request.
+
+ view_mapper
+
+ A class implementing the
+ :class:`pyramid.interfaces.IViewMapperFactory` interface, which
+ performs view argument and response mapping. By default it is
+ ``None``, which indicates that the view should use the default view
+ mapper. This plug-point is useful for Pyramid extension
+ developers, but it's not very useful for 'civilians' who are
+ just developing stock Pyramid applications. Pay no attention to
+ the man behind the curtain.
+
"""
view = self.maybe_dotted(view)
context = self.maybe_dotted(context)
@@ -1293,6 +1332,7 @@ class Configurator(object):
renderer=renderer, wrapper=wrapper, xhr=xhr, accept=accept,
header=header, path_info=path_info,
custom_predicates=custom_predicates, context=context,
+ view_mapper = view_mapper,
)
view_info = deferred_views.setdefault(route_name, [])
view_info.append(info)
@@ -1304,9 +1344,6 @@ class Configurator(object):
containment=containment, request_type=request_type,
custom=custom_predicates)
- if renderer is not None and not isinstance(renderer, dict):
- renderer = {'name':renderer, 'package':self.package}
-
if context is None:
context = for_
@@ -1316,16 +1353,35 @@ class Configurator(object):
if not IInterface.providedBy(r_context):
r_context = implementedBy(r_context)
- def register(permission=permission):
+ if isinstance(renderer, basestring):
+ renderer = RendererHelper(name=renderer, package=self.package,
+ registry = self.registry)
+
+ def register(permission=permission, renderer=renderer):
+ if renderer is None:
+ # use default renderer if one exists
+ if self.registry.queryUtility(IRendererFactory) is not None:
+ renderer = RendererHelper(name=None,
+ package=self.package,
+ registry=self.registry)
if permission is None:
# intent: will be None if no default permission is registered
permission = self.registry.queryUtility(IDefaultPermission)
# NO_PERMISSION_REQUIRED handled by _secure_view
- derived_view = self._derive_view(view, permission, predicates, attr,
- renderer, wrapper, name, accept,
- order, phash)
+ derived_view = ViewDeriver(registry=self.registry,
+ permission=permission,
+ predicates=predicates,
+ attr=attr,
+ renderer=renderer,
+ wrapper_viewname=wrapper,
+ viewname=name,
+ accept=accept,
+ order=order,
+ phash=phash,
+ decorator=decorator,
+ view_mapper=view_mapper)(view)
registered = self.registry.adapters.registered
@@ -1959,8 +2015,9 @@ 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)."""
- if renderer is not None and not isinstance(renderer, dict):
- renderer = {'name':renderer, 'package':self.package}
+ if isinstance(renderer, basestring):
+ renderer = RendererHelper(name=renderer, package=self.package,
+ registry = self.registry)
view = self._derive_view(view, attr=attr, renderer=renderer)
def bwcompat_view(context, request):
context = getattr(request, 'context', None)
@@ -1998,8 +2055,9 @@ class Configurator(object):
which will wrap this view when rendered (see the ``add_view``
method's ``wrapper`` argument for a description).
"""
- if renderer is not None and not isinstance(renderer, dict):
- renderer = {'name':renderer, 'package':self.package}
+ if isinstance(renderer, basestring):
+ renderer = RendererHelper(name=renderer, package=self.package,
+ registry=self.registry)
view = self._derive_view(view, attr=attr, renderer=renderer)
def bwcompat_view(context, request):
context = getattr(request, 'context', None)
@@ -2628,307 +2686,330 @@ class MultiView(object):
continue
raise PredicateMismatch(self.name)
-def decorate_view(wrapped_view, original_view):
- if wrapped_view is original_view:
- return False
- wrapped_view.__module__ = original_view.__module__
- wrapped_view.__doc__ = original_view.__doc__
+def wraps_view(wrapped):
+ def inner(self, view):
+ wrapped_view = wrapped(self, view)
+ return preserve_view_attrs(view, wrapped_view)
+ return inner
+
+def preserve_view_attrs(view, wrapped_view):
+ if wrapped_view is view:
+ return view
+ wrapped_view.__module__ = view.__module__
+ wrapped_view.__doc__ = view.__doc__
try:
- wrapped_view.__name__ = original_view.__name__
+ wrapped_view.__name__ = view.__name__
except AttributeError:
- wrapped_view.__name__ = repr(original_view)
+ wrapped_view.__name__ = repr(view)
try:
- wrapped_view.__permitted__ = original_view.__permitted__
+ wrapped_view.__permitted__ = view.__permitted__
except AttributeError:
pass
try:
- wrapped_view.__call_permissive__ = original_view.__call_permissive__
+ wrapped_view.__call_permissive__ = view.__call_permissive__
except AttributeError:
pass
try:
- wrapped_view.__predicated__ = original_view.__predicated__
+ wrapped_view.__predicated__ = view.__predicated__
except AttributeError:
pass
try:
- wrapped_view.__accept__ = original_view.__accept__
+ wrapped_view.__accept__ = view.__accept__
except AttributeError:
pass
try:
- wrapped_view.__order__ = original_view.__order__
+ wrapped_view.__order__ = view.__order__
except AttributeError:
pass
- return True
-
-def requestonly(class_or_callable, attr=None):
- """ Return true of the class or callable accepts only a request argument,
- as opposed to something that accepts context, request """
- if attr is None:
- attr = '__call__'
- if inspect.isfunction(class_or_callable):
- fn = class_or_callable
- elif inspect.isclass(class_or_callable):
- try:
- fn = class_or_callable.__init__
- except AttributeError:
- return False
- else:
- try:
- fn = getattr(class_or_callable, attr)
- except AttributeError:
- return False
-
- try:
- argspec = inspect.getargspec(fn)
- except TypeError:
- return False
-
- args = argspec[0]
- defaults = argspec[3]
-
- if hasattr(fn, 'im_func'):
- # it's an instance method
- if not args:
- return False
- args = args[1:]
- if not args:
- return False
+ return wrapped_view
- if len(args) == 1:
- return True
+class ViewDeriver(object):
+ def __init__(self, **kw):
+ self.kw = kw
+ self.registry = kw['registry']
+ self.authn_policy = self.registry.queryUtility(
+ IAuthenticationPolicy)
+ self.authz_policy = self.registry.queryUtility(
+ IAuthorizationPolicy)
+ self.logger = self.registry.queryUtility(IDebugLogger)
+
+ def __call__(self, view):
+ mapper = self.kw.get('view_mapper')
+ if mapper is None:
+ mapper = DefaultViewMapper
+ view = mapper(**self.kw)(view)
+ return self.attr_wrapped_view(
+ self.predicated_view(
+ self.authdebug_view(
+ self.secured_view(
+ self.owrap_view(
+ view)))))
+
+ @wraps_view
+ def owrap_view(self, view):
+ wrapper_viewname = self.kw.get('wrapper_viewname')
+ viewname = self.kw.get('viewname')
+ if not wrapper_viewname:
+ return view
+ def _owrapped_view(context, request):
+ response = view(context, request)
+ request.wrapped_response = response
+ request.wrapped_body = response.body
+ request.wrapped_view = view
+ wrapped_response = render_view_to_response(context, request,
+ wrapper_viewname)
+ if wrapped_response is None:
+ raise ValueError(
+ 'No wrapper view named %r found when executing view '
+ 'named %r' % (wrapper_viewname, viewname))
+ return wrapped_response
+ return _owrapped_view
+
+ @wraps_view
+ def secured_view(self, view):
+ permission = self.kw.get('permission')
+ if permission == '__no_permission_required__':
+ # allow views registered within configurations that have a
+ # default permission to explicitly override the default
+ # permission, replacing it with no permission at all
+ permission = None
+
+ wrapped_view = view
+ if self.authn_policy and self.authz_policy and (permission is not None):
+ def _secured_view(context, request):
+ principals = self.authn_policy.effective_principals(request)
+ if self.authz_policy.permits(context, principals, permission):
+ return view(context, request)
+ msg = getattr(request, 'authdebug_message',
+ 'Unauthorized: %s failed permission check' % view)
+ raise Forbidden(msg)
+ _secured_view.__call_permissive__ = view
+ def _permitted(context, request):
+ principals = self.authn_policy.effective_principals(request)
+ return self.authz_policy.permits(context, principals,
+ permission)
+ _secured_view.__permitted__ = _permitted
+ wrapped_view = _secured_view
+
+ return wrapped_view
+
+ @wraps_view
+ def authdebug_view(self, view):
+ wrapped_view = view
+ settings = self.registry.settings
+ permission = self.kw.get('permission')
+ if settings and settings.get('debug_authorization', False):
+ def _authdebug_view(context, request):
+ view_name = getattr(request, 'view_name', None)
+
+ if self.authn_policy and self.authz_policy:
+ if permission is None:
+ msg = 'Allowed (no permission registered)'
+ else:
+ principals = self.authn_policy.effective_principals(
+ request)
+ msg = str(self.authz_policy.permits(context, principals,
+ permission))
+ else:
+ msg = 'Allowed (no authorization policy in use)'
+
+ view_name = getattr(request, 'view_name', None)
+ url = getattr(request, 'url', None)
+ msg = ('debug_authorization of url %s (view name %r against '
+ 'context %r): %s' % (url, view_name, context, msg))
+ self.logger and self.logger.debug(msg)
+ if request is not None:
+ request.authdebug_message = msg
+ return view(context, request)
- elif args[0] == 'request':
- if len(args) - len(defaults) == 1:
- return True
+ wrapped_view = _authdebug_view
- return False
+ return wrapped_view
-def is_response(ob):
- if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and
- hasattr(ob, 'status') ):
- return True
- return False
+ @wraps_view
+ def predicated_view(self, view):
+ predicates = self.kw.get('predicates', ())
+ if not predicates:
+ return view
+ def predicate_wrapper(context, request):
+ if all((predicate(context, request) for predicate in predicates)):
+ return view(context, request)
+ raise PredicateMismatch('predicate mismatch for view %s' % view)
+ def checker(context, request):
+ return all((predicate(context, request) for predicate in
+ predicates))
+ predicate_wrapper.__predicated__ = checker
+ return predicate_wrapper
+
+ @wraps_view
+ def attr_wrapped_view(self, view):
+ kw = self.kw
+ accept, order, phash = (kw.get('accept', None),
+ kw.get('order', MAX_ORDER),
+ kw.get('phash', DEFAULT_PHASH))
+ # this is a little silly but we don't want to decorate the original
+ # function with attributes that indicate accept, order, and phash,
+ # so we use a wrapper
+ 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.__order__ = order
+ attr_view.__phash__ = phash
+ return attr_view
+
+class DefaultViewMapper(object):
+ implements(IViewMapperFactory)
+ def __init__(self, **kw):
+ self.renderer = kw.get('renderer')
+ self.attr = kw.get('attr')
+ self.decorator = kw.get('decorator')
+
+ def __call__(self, view):
+ decorator = self.decorator
+ if inspect.isclass(view):
+ view = preserve_view_attrs(view, self.map_class(view))
+ else:
+ view = preserve_view_attrs(view, self.map_nonclass(view))
+ if decorator is not None:
+ view = preserve_view_attrs(view, decorator(view))
+ return view
-def _map_view(view, registry, attr=None, renderer=None):
- wrapped_view = view
-
- helper = None
-
- if renderer is not None:
- helper = RendererHelper(renderer['name'],
- package=renderer['package'],
- registry=registry)
-
- if inspect.isclass(view):
- # If the object we've located is a class, turn it into a
- # function that operates like a Zope view (when it's invoked,
- # construct an instance using 'context' and 'request' as
- # position arguments, then immediately invoke the __call__
- # method of the instance with no arguments; __call__ should
- # return an IResponse).
- if requestonly(view, attr):
- # its __init__ accepts only a single request argument,
- # instead of both context and request
- def _class_requestonly_view(context, request):
- inst = view(request)
- if attr is None:
- response = inst()
- else:
- response = getattr(inst, attr)()
- if helper is not None:
- if not is_response(response):
- system = {
- 'view':inst,
- 'renderer_name':renderer['name'], # b/c
- 'renderer_info':renderer,
- 'context':context,
- 'request':request
- }
- response = helper.render_to_response(response, system,
- request=request)
- return response
- wrapped_view = _class_requestonly_view
+ def map_class(self, view):
+ ronly = self.requestonly(view)
+ if ronly:
+ mapped_view = self._map_class_requestonly(view)
else:
- # its __init__ accepts both context and request
- def _class_view(context, request):
- inst = view(context, request)
- if attr is None:
- response = inst()
- else:
- response = getattr(inst, attr)()
- if helper is not None:
- if not is_response(response):
- system = {'view':inst,
- 'renderer_name':renderer['name'], # b/c
- 'renderer_info':renderer,
- 'context':context,
- 'request':request
- }
- response = helper.render_to_response(response, system,
- request=request)
- return response
- wrapped_view = _class_view
-
- elif requestonly(view, attr):
- # its __call__ accepts only a single request argument,
- # instead of both context and request
+ mapped_view = self._map_class_native(view)
+ return mapped_view
+
+ def map_nonclass(self, view):
+ # We do more work here than appears necessary to avoid wrapping the
+ # view unless it actually requires wrapping (to avoid function call
+ # overhead).
+ mapped_view = view
+ ronly = self.requestonly(view)
+ if ronly:
+ mapped_view = self._map_nonclass_requestonly(view)
+ elif self.attr:
+ mapped_view = self._map_nonclass_attr(view)
+ elif self.renderer is not None:
+ mapped_view = self._map_nonclass_rendered(view)
+ return mapped_view
+
+ def _map_class_requestonly(self, view):
+ # its a class that has an __init__ which only accepts request
+ attr = self.attr
+ def _class_requestonly_view(context, request):
+ inst = view(request)
+ if attr is None:
+ response = inst()
+ else:
+ response = getattr(inst, attr)()
+ if self.renderer is not None and not is_response(response):
+ response = self.renderer.render_view(request, response, view,
+ context)
+ return response
+ return _class_requestonly_view
+
+ def _map_class_native(self, view):
+ # its a class that has an __init__ which accepts both context and
+ # request
+ attr = self.attr
+ def _class_view(context, request):
+ inst = view(context, request)
+ if attr is None:
+ response = inst()
+ else:
+ response = getattr(inst, attr)()
+ if self.renderer is not None and not is_response(response):
+ response = self.renderer.render_view(request, response, view,
+ context)
+ return response
+ return _class_view
+
+ def _map_nonclass_requestonly(self, view):
+ # its a function that has a __call__ which accepts only a single
+ # request argument
+ attr = self.attr
def _requestonly_view(context, request):
if attr is None:
response = view(request)
else:
response = getattr(view, attr)(request)
-
- if helper is not None:
- if not is_response(response):
- system = {
- 'view':view,
- 'renderer_name':renderer['name'],
- 'renderer_info':renderer,
- 'context':context,
- 'request':request
- }
- response = helper.render_to_response(response, system,
- request=request)
+ if self.renderer is not None and not is_response(response):
+ response = self.renderer.render_view(request, response, view,
+ context)
return response
- wrapped_view = _requestonly_view
+ return _requestonly_view
- elif attr:
+ def _map_nonclass_attr(self, view):
+ # its a function that has a __call__ which accepts both context and
+ # request, but still has an attr
+ attr = self.attr
def _attr_view(context, request):
response = getattr(view, attr)(context, request)
- if helper is not None:
- if not is_response(response):
- system = {
- 'view':view,
- 'renderer_name':renderer['name'],
- 'renderer_info':renderer,
- 'context':context,
- 'request':request
- }
- response = helper.render_to_response(response, system,
- request=request)
+ if self.renderer is not None and not is_response(response):
+ response = self.renderer.render_view(request, response, view,
+ context)
return response
- wrapped_view = _attr_view
+ return _attr_view
- elif helper is not None:
+ def _map_nonclass_rendered(self, view):
+ # it's a function that has a __call__ that accepts both context and
+ # request, but requires rendering
def _rendered_view(context, request):
response = view(context, request)
- if not is_response(response):
- system = {
- 'view':view,
- 'renderer_name':renderer['name'], # b/c
- 'renderer_info':renderer,
- 'context':context,
- 'request':request
- }
- response = helper.render_to_response(response, system,
- request=request)
+ if self.renderer is not None and not is_response(response):
+ response = self.renderer.render_view(request, response, view,
+ context)
return response
- wrapped_view = _rendered_view
-
- decorate_view(wrapped_view, view)
- return wrapped_view
+ return _rendered_view
+
+ def requestonly(self, view):
+ attr = self.attr
+ if attr is None:
+ attr = '__call__'
+ if inspect.isfunction(view):
+ fn = view
+ elif inspect.isclass(view):
+ try:
+ fn = view.__init__
+ except AttributeError:
+ return False
+ else:
+ try:
+ fn = getattr(view, attr)
+ except AttributeError:
+ return False
-def _owrap_view(view, viewname, wrapper_viewname):
- if not wrapper_viewname:
- return view
- def _owrapped_view(context, request):
- response = view(context, request)
- request.wrapped_response = response
- request.wrapped_body = response.body
- request.wrapped_view = view
- wrapped_response = render_view_to_response(context, request,
- wrapper_viewname)
- if wrapped_response is None:
- raise ValueError(
- 'No wrapper view named %r found when executing view '
- 'named %r' % (wrapper_viewname, viewname))
- return wrapped_response
- decorate_view(_owrapped_view, view)
- return _owrapped_view
-
-def _predicate_wrap(view, predicates):
- if not predicates:
- return view
- def predicate_wrapper(context, request):
- if all((predicate(context, request) for predicate in predicates)):
- return view(context, request)
- raise PredicateMismatch('predicate mismatch for view %s' % view)
- def checker(context, request):
- return all((predicate(context, request) for predicate in
- predicates))
- predicate_wrapper.__predicated__ = checker
- decorate_view(predicate_wrapper, view)
- return predicate_wrapper
-
-def _secure_view(view, permission, authn_policy, authz_policy):
- if permission == '__no_permission_required__':
- # allow views registered within configurations that have a
- # default permission to explicitly override the default
- # permission, replacing it with no permission at all
- permission = None
-
- wrapped_view = view
- if authn_policy and authz_policy and (permission is not None):
- def _secured_view(context, request):
- principals = authn_policy.effective_principals(request)
- if authz_policy.permits(context, principals, permission):
- return view(context, request)
- msg = getattr(request, 'authdebug_message',
- 'Unauthorized: %s failed permission check' % view)
- raise Forbidden(msg)
- _secured_view.__call_permissive__ = view
- def _permitted(context, request):
- principals = authn_policy.effective_principals(request)
- return authz_policy.permits(context, principals, permission)
- _secured_view.__permitted__ = _permitted
- wrapped_view = _secured_view
- decorate_view(wrapped_view, view)
+ try:
+ argspec = inspect.getargspec(fn)
+ except TypeError:
+ return False
- return wrapped_view
+ args = argspec[0]
+ defaults = argspec[3]
-def _authdebug_view(view, permission, authn_policy, authz_policy, settings,
- logger):
- wrapped_view = view
- if settings and settings.get('debug_authorization', False):
- def _authdebug_view(context, request):
- view_name = getattr(request, 'view_name', None)
+ if hasattr(fn, 'im_func'):
+ # it's an instance method
+ if not args:
+ return False
+ args = args[1:]
+ if not args:
+ return False
- if authn_policy and authz_policy:
- if permission is None:
- msg = 'Allowed (no permission registered)'
- else:
- principals = authn_policy.effective_principals(request)
- msg = str(authz_policy.permits(context, principals,
- permission))
- else:
- msg = 'Allowed (no authorization policy in use)'
-
- view_name = getattr(request, 'view_name', None)
- url = getattr(request, 'url', None)
- msg = ('debug_authorization of url %s (view name %r against '
- 'context %r): %s' % (url, view_name, context, msg))
- logger and logger.debug(msg)
- if request is not None:
- request.authdebug_message = msg
- return view(context, request)
+ if len(args) == 1:
+ return True
- wrapped_view = _authdebug_view
- decorate_view(wrapped_view, view)
+ elif args[0] == 'request':
+ if len(args) - len(defaults) == 1:
+ return True
- return wrapped_view
+ return False
-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, order, and phash,
- # so we use a wrapper
- 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.__order__ = order
- attr_view.__phash__ = phash
- decorate_view(attr_view, view)
- return attr_view
def isexception(o):
if IInterface.providedBy(o):
@@ -2984,3 +3065,20 @@ def translator(msg):
localizer = get_localizer(request)
return localizer.translate(msg)
+# b/c
+def _map_view(view, registry, attr=None, renderer=None):
+ return DefaultViewMapper(registry=registry, attr=attr,
+ renderer=renderer)(view)
+
+# b/c
+def requestonly(view, attr=None):
+ """ Return true of the class or callable accepts only a request argument,
+ as opposed to something that accepts context, request """
+ return DefaultViewMapper(attr=attr).requestonly(view)
+
+def is_response(ob):
+ if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and
+ hasattr(ob, 'status') ):
+ return True
+ return False
+
diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py
index 6d05f9475..f56910b53 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -3,6 +3,7 @@ from webob.exc import status_map
# Parent classes
from webob.exc import HTTPException
+from webob.exc import WSGIHTTPException
from webob.exc import HTTPOk
from webob.exc import HTTPRedirection
from webob.exc import HTTPError
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index 32359ca94..10a324b28 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -120,6 +120,27 @@ class ITemplateRenderer(IRenderer):
accepts arbitrary keyword arguments and returns a string or
unicode object """
+class IViewMapper(Interface):
+ def __call__(self, object):
+ """ Provided with an arbitrary object (a function, class, or
+ instance), returns a callable with the call signature ``(context,
+ request)``. The callable returned should itself return a Response
+ object. An IViewMapper is returned by
+ :class:`pyramid.interfaces.IViewMapperFactory`."""
+
+class IViewMapperFactory(Interface):
+ def __call__(self, **kw):
+ """
+ Return an object which implements
+ :class:`pyramid.interfaces.IViewMapper`. ``kw`` will be a dictionary
+ containing view-specific arguments, such as ``permission``,
+ ``predicates``, ``attr``, ``renderer``, and other items. An
+ IViewMapperFactory is used by
+ :meth:`pyramid.config.Configurator.add_view` to provide a plugpoint
+ to extension developers who want to modify potential view callable
+ invocation signatures and response values.
+ """
+
# internal interfaces
class IRequest(Interface):
diff --git a/pyramid/renderers.py b/pyramid/renderers.py
index c7fe86452..2e0514b01 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -282,6 +282,18 @@ class RendererHelper(object):
def get_renderer(self):
return self.renderer
+ def render_view(self, request, response, view, context):
+ system = {
+ 'view':view,
+ 'renderer_name':self.name, # b/c
+ 'renderer_info':{'name':self.name, 'package':self.package},
+ 'context':context,
+ 'request':request
+ }
+ return self.render_to_response(response, system,
+ request=request)
+
+
def render(self, value, system_values, request=None):
renderer = self.renderer
if system_values is None:
diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py
index c129b21ae..b2fa0e329 100644
--- a/pyramid/tests/test_config.py
+++ b/pyramid/tests/test_config.py
@@ -739,6 +739,22 @@ class ConfiguratorTests(unittest.TestCase):
result = wrapper(None, None)
self.assertEqual(result, 'OK')
+ def test_add_view_with_decorator(self):
+ def view(request):
+ """ ABC """
+ return 'OK'
+ def view_wrapper(fn):
+ def inner(context, request):
+ return fn(context, request)
+ return inner
+ config = self._makeOne(autocommit=True)
+ config.add_view(view=view, decorator=view_wrapper)
+ wrapper = self._getViewCallable(config)
+ self.failIf(wrapper is view)
+ self.assertEqual(wrapper.__doc__, view.__doc__)
+ result = wrapper(None, None)
+ self.assertEqual(result, 'OK')
+
def test_add_view_as_instance(self):
class AView:
def __call__(self, context, request):
@@ -1410,6 +1426,29 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(result.name, fixture)
self.assertEqual(result.settings, settings)
+ def test_add_view_with_default_renderer(self):
+ import pyramid.tests
+ from pyramid.interfaces import ISettings
+ class view(object):
+ def __init__(self, context, request):
+ self.request = request
+ self.context = context
+
+ def __call__(self):
+ return {'a':'1'}
+ config = self._makeOne(autocommit=True)
+ class moo(object):
+ def __init__(self, *arg, **kw):
+ pass
+ def __call__(self, *arg, **kw):
+ return 'moo'
+ config.add_renderer(None, moo)
+ config.add_view(view=view)
+ wrapper = self._getViewCallable(config)
+ request = self._makeRequest(config)
+ result = wrapper(None, request)
+ self.assertEqual(result.body, 'moo')
+
def test_add_view_with_template_renderer_no_callable(self):
import pyramid.tests
from pyramid.interfaces import ISettings
@@ -1964,6 +2003,33 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(view['attr'], 'action')
self.assertEqual(view['view'], MyView)
+ def test_add_handler_with_action_decorator(self):
+ config = self._makeOne(autocommit=True)
+ views = []
+ def dummy_add_view(**kw):
+ views.append(kw)
+ config.add_view = dummy_add_view
+ class MyHandler(object):
+ @classmethod
+ def __action_decorator__(cls, fn): # pragma: no cover
+ return fn
+ def action(self): # pragma: no cover
+ return 'response'
+ config.add_handler('name', '/{action}', MyHandler)
+ self.assertEqual(len(views), 1)
+ self.assertEqual(views[0]['decorator'], MyHandler.__action_decorator__)
+
+ def test_add_handler_with_action_decorator_fail_on_instancemethod(self):
+ config = self._makeOne(autocommit=True)
+ class MyHandler(object):
+ def __action_decorator__(self, fn): # pragma: no cover
+ return fn
+ def action(self): # pragma: no cover
+ return 'response'
+ from pyramid.exceptions import ConfigurationError
+ self.assertRaises(ConfigurationError, config.add_handler,
+ 'name', '/{action}', MyHandler)
+
def test_add_handler_doesnt_mutate_expose_dict(self):
config = self._makeOne(autocommit=True)
views = []
@@ -3504,16 +3570,17 @@ class Test__map_view(unittest.TestCase):
def _registerRenderer(self, typ='.txt'):
from pyramid.interfaces import IRendererFactory
from pyramid.interfaces import ITemplateRenderer
+ from pyramid.renderers import RendererHelper
from zope.interface import implements
- class Renderer:
+ class DummyRenderer:
implements(ITemplateRenderer)
- spec = 'abc' + typ
def __init__(self, path):
self.__class__.path = path
def __call__(self, *arg):
return 'Hello!'
- self.registry.registerUtility(Renderer, IRendererFactory, name=typ)
- return Renderer
+ self.registry.registerUtility(DummyRenderer, IRendererFactory, name=typ)
+ renderer = RendererHelper(name='abc' + typ, registry=self.registry)
+ return renderer
def _makeRequest(self):
request = DummyRequest()
@@ -3541,8 +3608,7 @@ class Test__map_view(unittest.TestCase):
def test__map_view_as_function_with_attr_and_renderer(self):
renderer = self._registerRenderer()
view = lambda *arg: 'OK'
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='__name__', renderer=info)
+ result = self._callFUT(view, attr='__name__', renderer=renderer)
self.failIf(result is view)
self.assertRaises(TypeError, result, None, None)
@@ -3600,8 +3666,7 @@ class Test__map_view(unittest.TestCase):
pass
def index(self):
return {'a':'1'}
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='index', renderer=info)
+ result = self._callFUT(view, attr='index', renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -3642,8 +3707,7 @@ class Test__map_view(unittest.TestCase):
pass
def index(self):
return {'a':'1'}
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='index', renderer=info)
+ result = self._callFUT(view, attr='index', renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -3684,8 +3748,7 @@ class Test__map_view(unittest.TestCase):
pass
def index(self):
return {'a':'1'}
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='index', renderer=info)
+ result = self._callFUT(view, attr='index', renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -3726,8 +3789,7 @@ class Test__map_view(unittest.TestCase):
pass
def index(self):
return {'a':'1'}
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='index', renderer=info)
+ result = self._callFUT(view, attr='index', renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -3759,8 +3821,7 @@ class Test__map_view(unittest.TestCase):
def index(self, context, request):
return {'a':'1'}
view = View()
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='index', renderer=info)
+ result = self._callFUT(view, attr='index', renderer=renderer)
self.failIf(result is view)
request = self._makeRequest()
self.assertEqual(result(None, request).body, 'Hello!')
@@ -3795,8 +3856,7 @@ class Test__map_view(unittest.TestCase):
def index(self, request):
return {'a':'1'}
view = View()
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, attr='index', renderer=info)
+ result = self._callFUT(view, attr='index', renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -3808,8 +3868,7 @@ class Test__map_view(unittest.TestCase):
renderer = self._registerRenderer()
def view(context, request):
return {'a':'1'}
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, renderer=info)
+ result = self._callFUT(view, renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
@@ -3820,24 +3879,25 @@ class Test__map_view(unittest.TestCase):
renderer = self._registerRenderer()
def view(context, request):
return {'a':'1'}
- info = {'name':renderer.spec, 'package':None}
- result = self._callFUT(view, renderer=info)
+ result = self._callFUT(view, renderer=renderer)
self.failIf(result is view)
self.assertEqual(view.__module__, result.__module__)
self.assertEqual(view.__doc__, result.__doc__)
request = self._makeRequest()
self.assertEqual(result(None, request).body, 'Hello!')
-class Test_decorate_view(unittest.TestCase):
- def _callFUT(self, wrapped, original):
- from pyramid.config import decorate_view
- return decorate_view(wrapped, original)
+class Test_wraps_view(unittest.TestCase):
+ def _callFUT(self, fn, view):
+ from pyramid.config import wraps_view
+ return wraps_view(fn)(None, view)
def test_it_same(self):
def view(context, request):
""" """
- result = self._callFUT(view, view)
- self.assertEqual(result, False)
+ def afunc(self, view):
+ return view
+ result = self._callFUT(afunc, view)
+ self.failUnless(result is view)
def test_it_different(self):
class DummyView1:
@@ -3866,8 +3926,10 @@ class Test_decorate_view(unittest.TestCase):
""" """
view1 = DummyView1()
view2 = DummyView2()
- result = self._callFUT(view1, view2)
- self.assertEqual(result, True)
+ def afunc(self, view):
+ return view1
+ result = self._callFUT(afunc, view2)
+ self.assertEqual(result, view1)
self.failUnless(view1.__doc__ is view2.__doc__)
self.failUnless(view1.__module__ is view2.__module__)
self.failUnless(view1.__name__ is view2.__name__)
diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py
index 79e363756..7fc066319 100644
--- a/pyramid/tests/test_view.py
+++ b/pyramid/tests/test_view.py
@@ -317,10 +317,9 @@ class TestViewConfigDecorator(unittest.TestCase):
settings = call_venusian(venusian)
self.assertEqual(len(settings), 1)
renderer = settings[0]['renderer']
- self.assertEqual(renderer,
- {'name':'fixtures/minimal.pt',
- 'package':pyramid.tests,
- })
+ self.assertEqual(renderer.name, 'fixtures/minimal.pt')
+ self.assertEqual(renderer.package, pyramid.tests)
+ self.assertEqual(renderer.registry.__class__, DummyRegistry)
def test_call_with_renderer_dict(self):
decorator = self._makeOne(renderer={'a':1})
@@ -494,9 +493,13 @@ class DummyVenusian(object):
self.attachments.append((wrapped, callback, category))
return self.info
+class DummyRegistry(object):
+ pass
+
class DummyConfig(object):
def __init__(self):
self.settings = []
+ self.registry = DummyRegistry()
def add_view(self, **kw):
self.settings.append(kw)
diff --git a/pyramid/url.py b/pyramid/url.py
index e1eaaaa1e..ac569eecb 100644
--- a/pyramid/url.py
+++ b/pyramid/url.py
@@ -53,7 +53,7 @@ def route_url(route_name, request, *elements, **kw):
``*remainder`` replacement value, it is tacked on to the URL
untouched.
- If a keyword argument ``_query`` is present, it will used to
+ If a keyword argument ``_query`` is present, it will be used to
compose a query string that will be tacked on to the end of the
URL. The value of ``_query`` must be a sequence of two-tuples
*or* a data structure with an ``.items()`` method that returns a
@@ -221,7 +221,7 @@ def resource_url(resource, request, *elements, **kw):
``elements`` are used, the generated URL will *not*
end in trailing a slash.
- If a keyword argument ``query`` is present, it will used to
+ If a keyword argument ``query`` is present, it will be used to
compose a query string that will be tacked on to the end of the
URL. The value of ``query`` must be a sequence of two-tuples *or*
a data structure with an ``.items()`` method that returns a
diff --git a/pyramid/view.py b/pyramid/view.py
index 3dc110863..776185d8b 100644
--- a/pyramid/view.py
+++ b/pyramid/view.py
@@ -17,8 +17,10 @@ from zope.interface import providedBy
from pyramid.interfaces import IRoutesMapper
from pyramid.interfaces import IView
from pyramid.interfaces import IViewClassifier
+from pyramid.interfaces import IRendererFactory
from pyramid.httpexceptions import HTTPFound
+from pyramid.renderers import RendererHelper
from pyramid.static import static_view
from pyramid.threadlocal import get_current_registry
@@ -404,6 +406,12 @@ class view_config(object):
settings = self.__dict__.copy()
def callback(context, name, ob):
+ renderer = settings.get('renderer')
+ if isinstance(renderer, basestring):
+ renderer = RendererHelper(name=renderer,
+ package=info.module,
+ registry=context.config.registry)
+ settings['renderer'] = renderer
context.config.add_view(view=ob, **settings)
info = self.venusian.attach(wrapped, callback, category='pyramid')
@@ -415,10 +423,6 @@ class view_config(object):
if settings['attr'] is None:
settings['attr'] = wrapped.__name__
- renderer_name = settings.get('renderer')
- if renderer_name is not None and not isinstance(renderer_name, dict):
- settings['renderer'] = {'name':renderer_name,
- 'package':info.module}
settings['_info'] = info.codeinfo
return wrapped
diff --git a/pyramid/zcml.py b/pyramid/zcml.py
index f668e3b4b..a2088e505 100644
--- a/pyramid/zcml.py
+++ b/pyramid/zcml.py
@@ -161,12 +161,7 @@ def view(
cacheable=True, # not used, here for b/w compat < 0.8
):
- if renderer is not None:
- package = getattr(_context, 'package', None)
- renderer = {'name':renderer, 'package':package}
-
context = context or for_
-
config = Configurator.with_context(_context)
config.add_view(
permission=permission, context=context, view=view, name=name,