From c2c5894d83f4bf43efd476336c7c065cd5984716 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Wed, 15 Apr 2015 10:06:10 -0400 Subject: wip derivations --- pyramid/config/__init__.py | 6 +- pyramid/config/derivations.py | 448 ++++++++++++++++++++++++++++++++++++ pyramid/config/views.py | 520 ++++++------------------------------------ 3 files changed, 527 insertions(+), 447 deletions(-) create mode 100644 pyramid/config/derivations.py diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 549144fab..b5cf35ff0 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -63,6 +63,7 @@ from pyramid.util import ( WeakOrderedSet, action_method, object_description, + TopologicalSorter, ) from pyramid.config.adapters import AdaptersConfiguratorMixin @@ -311,6 +312,7 @@ class Configurator( self.autocommit = autocommit self.route_prefix = route_prefix self.introspection = introspection + self.derivationlist = TopologicalSorter() if registry is None: registry = Registry(self.package_name) self.registry = registry @@ -378,6 +380,7 @@ class Configurator( self.add_default_response_adapters() self.add_default_renderers() self.add_default_view_predicates() + self.add_default_view_derivations() self.add_default_route_predicates() if exceptionresponse_view is not None: @@ -521,10 +524,11 @@ class Configurator( self.registry.registerUtility(predlist, IPredicateList, name=name) return predlist + def _add_predicate(self, type, name, factory, weighs_more_than=None, weighs_less_than=None): factory = self.maybe_dotted(factory) - discriminator = ('%s predicate' % type, name) + discriminator = ('%s option' % type, name) intr = self.introspectable( '%s predicates' % type, discriminator, diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py new file mode 100644 index 000000000..448c0b79b --- /dev/null +++ b/pyramid/config/derivations.py @@ -0,0 +1,448 @@ +import inspect + +from zope.interface import ( + implementer, + provider, + ) + +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.response import Response + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + IDebugLogger, + IResponse, + IViewMapper, + IViewMapperFactory, + ) + +from pyramid.compat import ( + is_bound_method, + is_unbound_method, + ) + +from pyramid.config.util import ( + DEFAULT_PHASH, + MAX_ORDER, + takes_one_arg, + ) + +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, + ) + +from pyramid.util import object_description +from pyramid import renderers + + +def view_description(view): + try: + return view.__text__ + except AttributeError: + # custom view mappers might not add __text__ + return object_description(view) + +def requestonly(view, attr=None): + return takes_one_arg(view, attr=attr, argname='request') + +@implementer(IViewMapper) +@provider(IViewMapperFactory) +class DefaultViewMapper(object): + def __init__(self, **kw): + self.attr = kw.get('attr') + + def __call__(self, view): + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) + + if inspect.isclass(view): + view = self.map_class(view) + else: + view = self.map_nonclass(view) + return view + + def map_class(self, view): + ronly = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_class_requestonly(view) + else: + mapped_view = self.map_class_native(view) + mapped_view.__text__ = 'method %s of %s' % ( + self.attr or '__call__', object_description(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 = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_nonclass_requestonly(view) + elif self.attr: + mapped_view = self.map_nonclass_attr(view) + if inspect.isroutine(mapped_view): + # This branch will be true if the view is a function or a method. + # We potentially mutate an unwrapped object here if it's a + # function. We do this to avoid function call overhead of + # injecting another wrapper. However, we must wrap if the + # function is a bound method because we can't set attributes on a + # bound method. + if is_bound_method(view): + _mapped_view = mapped_view + def mapped_view(context, request): + return _mapped_view(context, request) + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(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) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + 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) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + 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) + return response + return _requestonly_view + + def map_nonclass_attr(self, view): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + def _attr_view(context, request): + response = getattr(view, self.attr)(context, request) + return response + return _attr_view + + +def wraps_view(wrapper): + def inner(view, default, **kw): + wrapper_view = wrapper(view, default, **kw) + return preserve_view_attrs(view, wrapper_view) + return inner + +def preserve_view_attrs(view, wrapper): + if view is None: + return wrapper + + if wrapper is view: + return view + + original_view = getattr(view, '__original_view__', None) + + if original_view is None: + original_view = view + + wrapper.__wraps__ = view + wrapper.__original_view__ = original_view + wrapper.__module__ = view.__module__ + wrapper.__doc__ = view.__doc__ + + try: + wrapper.__name__ = view.__name__ + except AttributeError: + wrapper.__name__ = repr(view) + + # attrs that may not exist on "view", but, if so, must be attached to + # "wrapped view" + for attr in ('__permitted__', '__call_permissive__', '__permission__', + '__predicated__', '__predicates__', '__accept__', + '__order__', '__text__'): + try: + setattr(wrapper, attr, getattr(view, attr)) + except AttributeError: + pass + + return wrapper + +@wraps_view +def mapped_view(view, default, **kw): + mapper = kw.get('mapper') + if mapper is None: + mapper = getattr(view, '__view_mapper__', None) + if mapper is None: + mapper = kw['registry'].queryUtility(IViewMapperFactory) + if mapper is None: + mapper = DefaultViewMapper + + mapped_view = mapper(**kw)(view) + return mapped_view + +@wraps_view +def owrapped_view(view, default, **kw): + wrapper_viewname = kw.get('wrapper_viewname') + viewname = 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 http_cached_view(view, default, **kw): + if kw['registry'].settings.get('prevent_http_cache', False): + return view + + seconds = kw.get('http_cache') + + if seconds is None: + return view + + options = {} + + if isinstance(seconds, (tuple, list)): + try: + seconds, options = seconds + except ValueError: + raise ConfigurationError( + 'If http_cache parameter is a tuple or list, it must be ' + 'in the form (seconds, options); not %s' % (seconds,)) + + def wrapper(context, request): + response = view(context, request) + prevent_caching = getattr(response.cache_control, 'prevent_auto', + False) + if not prevent_caching: + response.cache_expires(seconds, **options) + return response + + return wrapper + +@wraps_view +def secured_view(view, default, **kw): + permission = 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 + authn_policy = kw['registry'].queryUtility(IAuthenticationPolicy) + authz_policy = kw['registry'].queryUtility(IAuthorizationPolicy) + + if authn_policy and authz_policy and (permission is not None): + def _permitted(context, request): + principals = authn_policy.effective_principals(request) + return authz_policy.permits(context, principals, + permission) + def _secured_view(context, request): + result = _permitted(context, request) + if result: + return view(context, request) + view_name = getattr(view, '__name__', view) + msg = getattr( + request, 'authdebug_message', + 'Unauthorized: %s failed permission check' % view_name) + raise HTTPForbidden(msg, result=result) + _secured_view.__call_permissive__ = view + _secured_view.__permitted__ = _permitted + _secured_view.__permission__ = permission + wrapped_view = _secured_view + + return wrapped_view + +@wraps_view +def authdebug_view(view, default, **kw): + wrapped_view = view + settings = kw['registry'].settings + permission = kw.get('permission') + authn_policy = kw['registry'].queryUtility(IAuthenticationPolicy) + authz_policy = kw['registry'].queryUtility(IAuthorizationPolicy) + logger = kw['registry'].queryUtility(IDebugLogger) + if settings and settings.get('debug_authorization', False): + def _authdebug_view(context, request): + view_name = getattr(request, 'view_name', None) + + if authn_policy and authz_policy: + if permission is NO_PERMISSION_REQUIRED: + msg = 'Allowed (NO_PERMISSION_REQUIRED)' + elif 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)) + if logger: logger.debug(msg) + if request is not None: + request.authdebug_message = msg + return view(context, request) + + wrapped_view = _authdebug_view + + return wrapped_view + +@wraps_view +def predicated_view(view, default, **kw): + preds = kw.get('predicates', ()) + if not preds: + return view + def predicate_wrapper(context, request): + for predicate in preds: + if not predicate(context, request): + view_name = getattr(view, '__name__', view) + raise PredicateMismatch( + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.text())) + return view(context, request) + def checker(context, request): + return all((predicate(context, request) for predicate in + preds)) + predicate_wrapper.__predicated__ = checker + predicate_wrapper.__predicates__ = preds + return predicate_wrapper + +@wraps_view +def attr_wrapped_view(view, default, **kw): + kw = 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 + attr_view.__view_attr__ = kw.get('attr') + attr_view.__permission__ = kw.get('permission') + return attr_view + +@wraps_view +def rendered_view(view, default, **kw): + # one way or another this wrapper must produce a Response (unless + # the renderer is a NullRendererHelper) + renderer = kw.get('renderer') + registry = kw['registry'] + if renderer is None: + # register a default renderer if you want super-dynamic + # rendering. registering a default renderer will also allow + # override_renderer to work if a renderer is left unspecified for + # a view registration. + def viewresult_to_response(context, request): + result = view(context, request) + if result.__class__ is Response: # common case + response = result + else: + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + if result is None: + append = (' You may have forgotten to return a value ' + 'from the view callable.') + elif isinstance(result, dict): + append = (' You may have forgotten to define a ' + 'renderer in the view configuration.') + else: + append = '' + + msg = ('Could not convert return value of the view ' + 'callable %s into a response object. ' + 'The value returned was %r.' + append) + + raise ValueError(msg % (view_description(view), result)) + + return response + + return viewresult_to_response + + if renderer is renderers.null_renderer: + return view + + def rendered_view(context, request): + result = view(context, request) + if result.__class__ is Response: # potential common case + response = result + else: + # this must adapt, it can't do a simple interface check + # (avoid trying to render webob responses) + response = registry.queryAdapterOrSelf(result, IResponse) + if response is None: + attrs = getattr(request, '__dict__', {}) + if 'override_renderer' in attrs: + # renderer overridden by newrequest event or other + renderer_name = attrs.pop('override_renderer') + view_renderer = renderers.RendererHelper( + name=renderer_name, + package=kw.get('package'), + registry = registry) + else: + view_renderer = renderer.clone() + if '__view__' in attrs: + view_inst = attrs.pop('__view__') + else: + view_inst = getattr(view, '__original_view__', view) + response = view_renderer.render_view(request, result, view_inst, + context) + return response + + return rendered_view + +@wraps_view +def decorated_view(view, default, **kw): + decorator = kw.get('decorator') + if decorator is None: + return view + return decorator(view) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e2da950be..ac3803712 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -75,6 +75,7 @@ from pyramid.view import ( AppendSlashNotFoundViewFactory, ) +import pyramid.util from pyramid.util import ( object_description, viewdefaults, @@ -82,6 +83,15 @@ from pyramid.util import ( ) import pyramid.config.predicates +import pyramid.config.derivations + +# bw compat +from pyramid.config.derivations import ( + preserve_view_attrs, + view_description, + requestonly, + DefaultViewMapper, +) from pyramid.config.util import ( DEFAULT_PHASH, @@ -92,434 +102,6 @@ from pyramid.config.util import ( urljoin = urlparse.urljoin url_parse = urlparse.urlparse -def view_description(view): - try: - return view.__text__ - except AttributeError: - # custom view mappers might not add __text__ - return object_description(view) - -def wraps_view(wrapper): - def inner(self, view): - wrapper_view = wrapper(self, view) - return preserve_view_attrs(view, wrapper_view) - return inner - -def preserve_view_attrs(view, wrapper): - if view is None: - return wrapper - - if wrapper is view: - return view - - original_view = getattr(view, '__original_view__', None) - - if original_view is None: - original_view = view - - wrapper.__wraps__ = view - wrapper.__original_view__ = original_view - wrapper.__module__ = view.__module__ - wrapper.__doc__ = view.__doc__ - - try: - wrapper.__name__ = view.__name__ - except AttributeError: - wrapper.__name__ = repr(view) - - # attrs that may not exist on "view", but, if so, must be attached to - # "wrapped view" - for attr in ('__permitted__', '__call_permissive__', '__permission__', - '__predicated__', '__predicates__', '__accept__', - '__order__', '__text__'): - try: - setattr(wrapper, attr, getattr(view, attr)) - except AttributeError: - pass - - return wrapper - -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): - return self.attr_wrapped_view( - self.predicated_view( - self.authdebug_view( - self.secured_view( - self.owrapped_view( - self.http_cached_view( - self.decorated_view( - self.rendered_view( - self.mapped_view( - view))))))))) - - @wraps_view - def mapped_view(self, view): - mapper = self.kw.get('mapper') - if mapper is None: - mapper = getattr(view, '__view_mapper__', None) - if mapper is None: - mapper = self.registry.queryUtility(IViewMapperFactory) - if mapper is None: - mapper = DefaultViewMapper - - mapped_view = mapper(**self.kw)(view) - return mapped_view - - @wraps_view - def owrapped_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 http_cached_view(self, view): - if self.registry.settings.get('prevent_http_cache', False): - return view - - seconds = self.kw.get('http_cache') - - if seconds is None: - return view - - options = {} - - if isinstance(seconds, (tuple, list)): - try: - seconds, options = seconds - except ValueError: - raise ConfigurationError( - 'If http_cache parameter is a tuple or list, it must be ' - 'in the form (seconds, options); not %s' % (seconds,)) - - def wrapper(context, request): - response = view(context, request) - prevent_caching = getattr(response.cache_control, 'prevent_auto', - False) - if not prevent_caching: - response.cache_expires(seconds, **options) - return response - - return wrapper - - @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 _permitted(context, request): - principals = self.authn_policy.effective_principals(request) - return self.authz_policy.permits(context, principals, - permission) - def _secured_view(context, request): - result = _permitted(context, request) - if result: - return view(context, request) - view_name = getattr(view, '__name__', view) - msg = getattr( - request, 'authdebug_message', - 'Unauthorized: %s failed permission check' % view_name) - raise HTTPForbidden(msg, result=result) - _secured_view.__call_permissive__ = view - _secured_view.__permitted__ = _permitted - _secured_view.__permission__ = permission - 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 NO_PERMISSION_REQUIRED: - msg = 'Allowed (NO_PERMISSION_REQUIRED)' - elif 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) - - wrapped_view = _authdebug_view - - return wrapped_view - - @wraps_view - def predicated_view(self, view): - preds = self.kw.get('predicates', ()) - if not preds: - return view - def predicate_wrapper(context, request): - for predicate in preds: - if not predicate(context, request): - view_name = getattr(view, '__name__', view) - raise PredicateMismatch( - 'predicate mismatch for view %s (%s)' % ( - view_name, predicate.text())) - return view(context, request) - def checker(context, request): - return all((predicate(context, request) for predicate in - preds)) - predicate_wrapper.__predicated__ = checker - predicate_wrapper.__predicates__ = preds - 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 - attr_view.__view_attr__ = self.kw.get('attr') - attr_view.__permission__ = self.kw.get('permission') - return attr_view - - @wraps_view - def rendered_view(self, view): - # one way or another this wrapper must produce a Response (unless - # the renderer is a NullRendererHelper) - renderer = self.kw.get('renderer') - if renderer is None: - # register a default renderer if you want super-dynamic - # rendering. registering a default renderer will also allow - # override_renderer to work if a renderer is left unspecified for - # a view registration. - return self._response_resolved_view(view) - if renderer is renderers.null_renderer: - return view - return self._rendered_view(view, renderer) - - def _rendered_view(self, view, view_renderer): - def rendered_view(context, request): - result = view(context, request) - if result.__class__ is Response: # potential common case - response = result - else: - registry = self.registry - # this must adapt, it can't do a simple interface check - # (avoid trying to render webob responses) - response = registry.queryAdapterOrSelf(result, IResponse) - if response is None: - attrs = getattr(request, '__dict__', {}) - if 'override_renderer' in attrs: - # renderer overridden by newrequest event or other - renderer_name = attrs.pop('override_renderer') - renderer = renderers.RendererHelper( - name=renderer_name, - package=self.kw.get('package'), - registry = registry) - else: - renderer = view_renderer.clone() - if '__view__' in attrs: - view_inst = attrs.pop('__view__') - else: - view_inst = getattr(view, '__original_view__', view) - response = renderer.render_view(request, result, view_inst, - context) - return response - - return rendered_view - - def _response_resolved_view(self, view): - registry = self.registry - def viewresult_to_response(context, request): - result = view(context, request) - if result.__class__ is Response: # common case - response = result - else: - response = registry.queryAdapterOrSelf(result, IResponse) - if response is None: - if result is None: - append = (' You may have forgotten to return a value ' - 'from the view callable.') - elif isinstance(result, dict): - append = (' You may have forgotten to define a ' - 'renderer in the view configuration.') - else: - append = '' - - msg = ('Could not convert return value of the view ' - 'callable %s into a response object. ' - 'The value returned was %r.' + append) - - raise ValueError(msg % (view_description(view), result)) - - return response - - return viewresult_to_response - - @wraps_view - def decorated_view(self, view): - decorator = self.kw.get('decorator') - if decorator is None: - return view - return decorator(view) - -@implementer(IViewMapper) -@provider(IViewMapperFactory) -class DefaultViewMapper(object): - def __init__(self, **kw): - self.attr = kw.get('attr') - - def __call__(self, view): - if is_unbound_method(view) and self.attr is None: - raise ConfigurationError(( - 'Unbound method calls are not supported, please set the class ' - 'as your `view` and the method as your `attr`' - )) - - if inspect.isclass(view): - view = self.map_class(view) - else: - view = self.map_nonclass(view) - return view - - def map_class(self, view): - ronly = requestonly(view, self.attr) - if ronly: - mapped_view = self.map_class_requestonly(view) - else: - mapped_view = self.map_class_native(view) - mapped_view.__text__ = 'method %s of %s' % ( - self.attr or '__call__', object_description(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 = requestonly(view, self.attr) - if ronly: - mapped_view = self.map_nonclass_requestonly(view) - elif self.attr: - mapped_view = self.map_nonclass_attr(view) - if inspect.isroutine(mapped_view): - # This branch will be true if the view is a function or a method. - # We potentially mutate an unwrapped object here if it's a - # function. We do this to avoid function call overhead of - # injecting another wrapper. However, we must wrap if the - # function is a bound method because we can't set attributes on a - # bound method. - if is_bound_method(view): - _mapped_view = mapped_view - def mapped_view(context, request): - return _mapped_view(context, request) - if self.attr is not None: - mapped_view.__text__ = 'attr %s of %s' % ( - self.attr, object_description(view)) - else: - mapped_view.__text__ = object_description(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) - request.__view__ = inst - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - 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) - request.__view__ = inst - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - 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) - return response - return _requestonly_view - - def map_nonclass_attr(self, view): - # its a function that has a __call__ which accepts both context and - # request, but still has an attr - def _attr_view(context, request): - response = getattr(view, self.attr)(context, request) - return response - return _attr_view - -def requestonly(view, attr=None): - return takes_one_arg(view, attr=attr, argname='request') - @implementer(IMultiView) class MultiView(object): @@ -1226,7 +808,7 @@ class ViewsConfiguratorMixin(object): phash = view_intr['phash'] # __no_permission_required__ handled by _secure_view - deriver = ViewDeriver( + derived_view = self._apply_view_derivations(view, registry=self.registry, permission=permission, predicates=preds, @@ -1242,7 +824,6 @@ class ViewsConfiguratorMixin(object): decorator=decorator, http_cache=http_cache, ) - derived_view = deriver(view) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems # that require it for introspection after manual view lookup; @@ -1401,6 +982,17 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) + def _apply_view_derivations(self, view, **kw): + d = pyramid.config.derivations + # These inner derivations have fixed order + inner_derivers = [('mapped_view', (d.mapped_view, None)), + ('rendered_view', (d.rendered_view, None))] + + for name, val in inner_derivers + self.derivationlist.sorted(): + derivation, default = val + view = derivation(view, default, **kw) + return view + @action_method def add_view_predicate(self, name, factory, weighs_more_than=None, weighs_less_than=None): @@ -1448,6 +1040,43 @@ class ViewsConfiguratorMixin(object): ): self.add_view_predicate(name, factory) + @action_method + def add_view_derivation(self, name, factory, default, + weighs_more_than=None, + weighs_less_than=None): + factory = self.maybe_dotted(factory) + discriminator = ('view option', name) + intr = self.introspectable( + '%s derivation' % type, + discriminator, + '%s derivation named %s' % (type, name), + '%s derivation' % type) + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + self.derivationlist.add(name, (factory, default), + after=weighs_more_than, + before=weighs_less_than) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered early + + def add_default_view_derivations(self): + d = pyramid.config.derivations + derivers = [ + ('decorated_view', d.decorated_view), + ('http_cached_view', d.http_cached_view), + ('owrapped_view', d.owrapped_view), + ('secured_view', d.secured_view), + ('authdebug_view', d.authdebug_view), + ('predicated_view', d.predicated_view), + ] + after = pyramid.util.FIRST + for name, deriver in derivers: + self.add_view_derivation(name, deriver, default=None, weighs_more_than=after) + after = name + def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, @@ -1546,22 +1175,21 @@ class ViewsConfiguratorMixin(object): 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, - mapper=mapper, - decorator=decorator, - http_cache=http_cache) - - return deriver(view) + return self._apply_view_derivations(view, + 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, + mapper=mapper, + decorator=decorator, + http_cache=http_cache) @viewdefaults @action_method -- cgit v1.2.3 From 2fbeb4f9a8912ad20c4042f4e0e73e40927ea72b Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Wed, 15 Apr 2015 11:03:08 -0400 Subject: Add missing imports. --- pyramid/config/derivations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 448c0b79b..14c43dbef 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -32,8 +32,9 @@ from pyramid.exceptions import ( ConfigurationError, PredicateMismatch, ) - +from pyramid.httpexceptions import HTTPForbidden from pyramid.util import object_description +from pyramid.view import render_view_to_response from pyramid import renderers -- cgit v1.2.3 From 7aad4b49bb34fcb0d8bd207ee5d2bea46d665e34 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Wed, 15 Apr 2015 11:35:59 -0400 Subject: add default view derivations to configurator in testing.setUp() --- pyramid/testing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyramid/testing.py b/pyramid/testing.py index 772914f3b..3ddc407ec 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -472,6 +472,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, config.add_default_renderers() config.add_default_view_predicates() config.add_default_route_predicates() + config.add_default_view_derivations() config.commit() global have_zca try: -- cgit v1.2.3 From 1d5a99208aa912296cc4b320ea753c9156dff9df Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Wed, 15 Apr 2015 12:48:12 -0400 Subject: Store derivers in registry, not config, since new configs are created (losing state). --- pyramid/config/__init__.py | 2 -- pyramid/config/views.py | 11 +++++++++-- pyramid/interfaces.py | 3 +++ pyramid/tests/test_integration.py | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index b5cf35ff0..ad72c5614 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -63,7 +63,6 @@ from pyramid.util import ( WeakOrderedSet, action_method, object_description, - TopologicalSorter, ) from pyramid.config.adapters import AdaptersConfiguratorMixin @@ -312,7 +311,6 @@ class Configurator( self.autocommit = autocommit self.route_prefix = route_prefix self.introspection = introspection - self.derivationlist = TopologicalSorter() if registry is None: registry = Registry(self.package_name) self.registry = registry diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ac3803712..b1a49f911 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -28,6 +28,7 @@ from pyramid.interfaces import ( IStaticURLInfo, IView, IViewClassifier, + IViewDerivers, IViewMapper, IViewMapperFactory, PHASE1_CONFIG, @@ -80,6 +81,7 @@ from pyramid.util import ( object_description, viewdefaults, action_method, + TopologicalSorter, ) import pyramid.config.predicates @@ -988,7 +990,8 @@ class ViewsConfiguratorMixin(object): inner_derivers = [('mapped_view', (d.mapped_view, None)), ('rendered_view', (d.rendered_view, None))] - for name, val in inner_derivers + self.derivationlist.sorted(): + derivers = self.registry.queryUtility(IViewDerivers, default=[]) + for name, val in inner_derivers + derivers.sorted(): derivation, default = val view = derivation(view, default, **kw) return view @@ -1056,7 +1059,11 @@ class ViewsConfiguratorMixin(object): intr['weighs_more_than'] = weighs_more_than intr['weighs_less_than'] = weighs_less_than def register(): - self.derivationlist.add(name, (factory, default), + derivers = self.registry.queryUtility(IViewDerivers) + if derivers is None: + derivers = TopologicalSorter() + self.registry.registerUtility(derivers, IViewDerivers) + derivers.add(name, (factory, default), after=weighs_more_than, before=weighs_less_than) self.action(discriminator, register, introspectables=(intr,), diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index bab91b0ee..7ac2663d1 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1184,6 +1184,9 @@ class IJSONAdapter(Interface): class IPredicateList(Interface): """ Interface representing a predicate list """ +class IViewDerivers(Interface): + """ Interface for view derivers list """ + class ICacheBuster(Interface): """ Instances of ``ICacheBuster`` may be provided as arguments to diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index c2786c391..3b7af29a7 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -474,6 +474,7 @@ class TestConflictApp(unittest.TestCase): def _makeConfig(self): from pyramid.config import Configurator config = Configurator() + config.add_default_view_derivations() return config def test_autoresolved_view(self): -- cgit v1.2.3 From 5fcde84d1b4546b8c0474c3465189ee09ccbeeaf Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Wed, 15 Apr 2015 14:45:06 -0400 Subject: Add missed view deriver. Remove unneed config command in test. --- pyramid/config/views.py | 1 + pyramid/tests/test_integration.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b1a49f911..db69c7e01 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1078,6 +1078,7 @@ class ViewsConfiguratorMixin(object): ('secured_view', d.secured_view), ('authdebug_view', d.authdebug_view), ('predicated_view', d.predicated_view), + ('attr_wrapped_view', d.attr_wrapped_view), ] after = pyramid.util.FIRST for name, deriver in derivers: diff --git a/pyramid/tests/test_integration.py b/pyramid/tests/test_integration.py index 3b7af29a7..c2786c391 100644 --- a/pyramid/tests/test_integration.py +++ b/pyramid/tests/test_integration.py @@ -474,7 +474,6 @@ class TestConflictApp(unittest.TestCase): def _makeConfig(self): from pyramid.config import Configurator config = Configurator() - config.add_default_view_derivations() return config def test_autoresolved_view(self): -- cgit v1.2.3 From 46497335df6e87ce681f3e05b82ca5ff32d57f32 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Wed, 15 Apr 2015 14:51:33 -0400 Subject: wip factor out deriver tests --- pyramid/tests/test_config/test_derivations.py | 1128 +++++++++++++++++++++++++ pyramid/tests/test_config/test_views.py | 1128 ------------------------- 2 files changed, 1128 insertions(+), 1128 deletions(-) create mode 100644 pyramid/tests/test_config/test_derivations.py diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py new file mode 100644 index 000000000..668a462b1 --- /dev/null +++ b/pyramid/tests/test_config/test_derivations.py @@ -0,0 +1,1128 @@ +import unittest + +from pyramid import testing + +class TestDeriveView(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + + def _makeRequest(self): + request = DummyRequest() + request.registry = self.config.registry + return request + + def _registerLogger(self): + from pyramid.interfaces import IDebugLogger + logger = DummyLogger() + self.config.registry.registerUtility(logger, IDebugLogger) + return logger + + def _registerSecurityPolicy(self, permissive): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(permissive) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + + def test_function_returns_non_adaptable(self): + def view(request): + return None + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable function ' + 'pyramid.tests.test_config.test_views.view into a response ' + 'object. The value returned was None. You may have forgotten ' + 'to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_non_adaptable_dict(self): + def view(request): + return {'a':1} + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + "Could not convert return value of the view callable function " + "pyramid.tests.test_config.test_views.view into a response " + "object. The value returned was {'a': 1}. You may have " + "forgotten to define a renderer in the view configuration." + ) + else: # pragma: no cover + raise AssertionError + + def test_instance_returns_non_adaptable(self): + class AView(object): + def __call__(self, request): + return None + view = AView() + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + msg = e.args[0] + self.assertTrue(msg.startswith( + 'Could not convert return value of the view callable object ' + ' into a response object. The value returned was None. You ' + 'may have forgotten to return a value from the view callable.')) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_true_Response_no_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + result = self.config.derive_view(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_function_returns_true_Response_with_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + renderer = object() + result = self.config.derive_view(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_requestonly_default_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def __call__(self): + return None + result = self.config.derive_view(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method __call__ of ' + 'class pyramid.tests.test_config.test_views.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_nondefault_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def theviewmethod(self): + return None + result = self.config.derive_view(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method theviewmethod of ' + 'class pyramid.tests.test_config.test_views.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_function(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(result(None, None), response) + + def test_requestonly_function_with_renderer(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_requestonly_function_with_renderer_request_override(self): + def moo(info): + def inner(value, system): + self.assertEqual(value, 'OK') + self.assertEqual(system['request'], request) + self.assertEqual(system['context'], context) + return b'moo' + return inner + def view(request): + return 'OK' + self.config.add_renderer('moo', moo) + result = self.config.derive_view(view, renderer='string') + self.assertFalse(result is view) + request = self._makeRequest() + request.override_renderer = 'moo' + context = testing.DummyResource() + self.assertEqual(result(context, request).body, b'moo') + + def test_requestonly_function_with_renderer_request_has_view(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, 'view') + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + request.__view__ = 'view' + context = testing.DummyResource() + r = result(context, request) + self.assertEqual(r, response) + self.assertFalse(hasattr(request, '__view__')) + + def test_class_without_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def __call__(self): + return response + result = self.config.derive_view(View) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_class_with_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def another(self): + return response + result = self.config.derive_view(View, attr='another') + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_as_function_context_and_request(self): + def view(context, request): + return 'OK' + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(view(None, None), 'OK') + + def test_as_function_requestonly(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_newstyle_class_context_and_request(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_newstyle_class_requestonly(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_context_and_request(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_requestonly(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_instance_context_and_request(self): + response = DummyResponse() + class View: + def __call__(self, context, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_instance_requestonly(self): + response = DummyResponse() + class View: + def __call__(self, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertTrue('test_views' in result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_with_debug_authorization_no_authpol(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_no_permission(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerSecurityPolicy(True) + logger = self._registerLogger() + result = self.config._derive_view(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed (" + "no permission registered)") + + def test_debug_auth_permission_authpol_permitted(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): True") + + def test_debug_auth_permission_authpol_permitted_no_request(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + self.assertEqual(result(None, None), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url None (view name " + "None against context None): True") + + def test_debug_auth_permission_authpol_denied(self): + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertRaises(HTTPForbidden, result, None, request) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): False") + + def test_debug_auth_permission_authpol_denied2(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + permitted = result.__permitted__(None, None) + self.assertEqual(permitted, False) + + def test_debug_auth_permission_authpol_overridden(self): + from pyramid.security import NO_PERMISSION_REQUIRED + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): " + "Allowed (NO_PERMISSION_REQUIRED)") + + def test_secured_view_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_raises_forbidden_no_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_secured_view_raises_forbidden_with_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + def myview(request): pass + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(myview, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: myview failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_no_name(self): + from pyramid.exceptions import PredicateMismatch + response = DummyResponse() + view = lambda *arg: response + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(view, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_name(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(myview, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_exception_has_text_in_detail(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return True + predicate1.text = lambda *arg: 'pred1' + def predicate2(context, request): + return False + predicate2.text = lambda *arg: 'pred2' + result = self.config._derive_view(myview, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (pred2)') + else: # pragma: no cover + raise AssertionError + + def test_with_predicates_all(self): + response = DummyResponse() + view = lambda *arg: response + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result(None, None) + self.assertEqual(next, response) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_checker(self): + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result.__predicated__(None, None) + self.assertEqual(next, True) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_notall(self): + from pyramid.httpexceptions import HTTPNotFound + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + predicate1.text = lambda *arg: 'text' + def predicate2(context, request): + predicates.append(True) + return False + predicate2.text = lambda *arg: 'text' + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + self.assertRaises(HTTPNotFound, result, None, None) + self.assertEqual(predicates, [True, True]) + + def test_with_wrapper_viewname(self): + from pyramid.response import Response + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + def outer_view(context, request): + self.assertEqual(request.wrapped_response, inner_response) + self.assertEqual(request.wrapped_body, inner_response.body) + self.assertEqual(request.wrapped_view.__original_view__, + inner_view) + return Response(b'outer ' + request.wrapped_body) + self.config.registry.registerAdapter( + outer_view, (IViewClassifier, None, None), IView, 'owrap') + result = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + response = result(None, request) + self.assertEqual(response.body, b'outer OK') + + def test_with_wrapper_viewname_notfound(self): + from pyramid.response import Response + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + result = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + request = self._makeRequest() + self.assertRaises(ValueError, wrapped, None, request) + + def test_as_newstyle_class_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config._derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_newstyle_class_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_context_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, context, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_with_view_mapper_config_specified(self): + response = DummyResponse() + class mapper(object): + def __init__(self, **kw): + self.kw = kw + def __call__(self, view): + def wrapped(context, request): + return response + return wrapped + def view(context, request): return 'NOTOK' + result = self.config._derive_view(view, mapper=mapper) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_view_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + def view(context, request): return 'NOTOK' + view.__view_mapper__ = mapper + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_default_mapper_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + self.config.set_view_mapper(mapper) + def view(context, request): return 'NOTOK' + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_attr_wrapped_view_branching_default_phash(self): + from pyramid.config.util import DEFAULT_PHASH + def view(context, request): pass + result = self.config.derive_view(view, phash=DEFAULT_PHASH) + self.assertEqual(result.__wraps__, view) + + def test_attr_wrapped_view_branching_nondefault_phash(self): + def view(context, request): pass + result = self.config.derive_view(view, phash='nondefault') + self.assertNotEqual(result, view) + + def test_http_cached_view_integer(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config.derive_view(inner_view, http_cache=3600) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_timedelta(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config.derive_view(inner_view, + http_cache=datetime.timedelta(hours=1)) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_tuple(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config.derive_view(inner_view, + http_cache=(3600, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') + + def test_http_cached_view_tuple_seconds_None(self): + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(None, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertEqual(headers['Cache-Control'], 'public') + + def test_http_cached_view_prevent_auto_set(self): + from pyramid.response import Response + response = Response() + response.cache_control.prevent_auto = True + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) # doesn't blow up + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_prevent_http_cache_in_settings(self): + self.config.registry.settings['prevent_http_cache'] = True + from pyramid.response import Response + response = Response() + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_view_bad_tuple(self): + def view(request): pass + self.assertRaises(ConfigurationError, self.config.derive_view, + view, http_cache=(None,)) + +from zope.interface import implementer +from pyramid.interfaces import ( + IResponse, + IRequest, + ) + +@implementer(IResponse) +class DummyResponse(object): + content_type = None + default_content_type = None + body = None + +class DummyRequest: + subpath = () + matchdict = None + request_iface = IRequest + + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + self.response = DummyResponse() + +class DummyLogger: + def __init__(self): + self.messages = [] + def info(self, msg): + self.messages.append(msg) + warn = info + debug = info + +class DummySecurityPolicy: + def __init__(self, permitted=True): + self.permitted = permitted + + def effective_principals(self, request): + return [] + + def permits(self, context, principals, permission): + return self.permitted + diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 1c2d300a1..a1c161322 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2466,1135 +2466,7 @@ class TestMultiView(unittest.TestCase): response = mv(context, request) self.assertEqual(response, expected_response) -class TestViewDeriver(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - - def _makeOne(self, **kw): - kw['registry'] = self.config.registry - from pyramid.config.views import ViewDeriver - return ViewDeriver(**kw) - - def _makeRequest(self): - request = DummyRequest() - request.registry = self.config.registry - return request - - def _registerLogger(self): - from pyramid.interfaces import IDebugLogger - logger = DummyLogger() - self.config.registry.registerUtility(logger, IDebugLogger) - return logger - - def _registerSecurityPolicy(self, permissive): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(permissive) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - - def test_function_returns_non_adaptable(self): - def view(request): - return None - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable function ' - 'pyramid.tests.test_config.test_views.view into a response ' - 'object. The value returned was None. You may have forgotten ' - 'to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_function_returns_non_adaptable_dict(self): - def view(request): - return {'a':1} - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - self.assertEqual( - e.args[0], - "Could not convert return value of the view callable function " - "pyramid.tests.test_config.test_views.view into a response " - "object. The value returned was {'a': 1}. You may have " - "forgotten to define a renderer in the view configuration." - ) - else: # pragma: no cover - raise AssertionError - - def test_instance_returns_non_adaptable(self): - class AView(object): - def __call__(self, request): - return None - view = AView() - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - msg = e.args[0] - self.assertTrue(msg.startswith( - 'Could not convert return value of the view callable object ' - ' into a response object. The value returned was None. You ' - 'may have forgotten to return a value from the view callable.')) - else: # pragma: no cover - raise AssertionError - - def test_function_returns_true_Response_no_renderer(self): - from pyramid.response import Response - r = Response('Hello') - def view(request): - return r - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - response = result(None, None) - self.assertEqual(response, r) - - def test_function_returns_true_Response_with_renderer(self): - from pyramid.response import Response - r = Response('Hello') - def view(request): - return r - renderer = object() - deriver = self._makeOne(renderer=renderer) - result = deriver(view) - self.assertFalse(result is view) - response = result(None, None) - self.assertEqual(response, r) - - def test_requestonly_default_method_returns_non_adaptable(self): - request = DummyRequest() - class AView(object): - def __init__(self, request): - pass - def __call__(self): - return None - deriver = self._makeOne() - result = deriver(AView) - self.assertFalse(result is AView) - try: - result(None, request) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable ' - 'method __call__ of ' - 'class pyramid.tests.test_config.test_views.AView into a ' - 'response object. The value returned was None. You may have ' - 'forgotten to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_requestonly_nondefault_method_returns_non_adaptable(self): - request = DummyRequest() - class AView(object): - def __init__(self, request): - pass - def theviewmethod(self): - return None - deriver = self._makeOne(attr='theviewmethod') - result = deriver(AView) - self.assertFalse(result is AView) - try: - result(None, request) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable ' - 'method theviewmethod of ' - 'class pyramid.tests.test_config.test_views.AView into a ' - 'response object. The value returned was None. You may have ' - 'forgotten to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_requestonly_function(self): - response = DummyResponse() - def view(request): - return response - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(result(None, None), response) - - def test_requestonly_function_with_renderer(self): - response = DummyResponse() - class moo(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, 'OK') - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - def view(request): - return 'OK' - deriver = self._makeOne(renderer=moo()) - result = deriver(view) - self.assertFalse(result.__wraps__ is view) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_requestonly_function_with_renderer_request_override(self): - def moo(info): - def inner(value, system): - self.assertEqual(value, 'OK') - self.assertEqual(system['request'], request) - self.assertEqual(system['context'], context) - return b'moo' - return inner - def view(request): - return 'OK' - self.config.add_renderer('moo', moo) - deriver = self._makeOne(renderer='string') - result = deriver(view) - self.assertFalse(result is view) - request = self._makeRequest() - request.override_renderer = 'moo' - context = testing.DummyResource() - self.assertEqual(result(context, request).body, b'moo') - - def test_requestonly_function_with_renderer_request_has_view(self): - response = DummyResponse() - class moo(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, 'OK') - self.assertEqual(view_inst, 'view') - self.assertEqual(ctx, context) - return response - def clone(self): - return self - def view(request): - return 'OK' - deriver = self._makeOne(renderer=moo()) - result = deriver(view) - self.assertFalse(result.__wraps__ is view) - request = self._makeRequest() - request.__view__ = 'view' - context = testing.DummyResource() - r = result(context, request) - self.assertEqual(r, response) - self.assertFalse(hasattr(request, '__view__')) - - def test_class_without_attr(self): - response = DummyResponse() - class View(object): - def __init__(self, request): - pass - def __call__(self): - return response - deriver = self._makeOne() - result = deriver(View) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, View) - - def test_class_with_attr(self): - response = DummyResponse() - class View(object): - def __init__(self, request): - pass - def another(self): - return response - deriver = self._makeOne(attr='another') - result = deriver(View) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, View) - - def test_as_function_context_and_request(self): - def view(context, request): - return 'OK' - deriver = self._makeOne() - result = deriver(view) - self.assertTrue(result.__wraps__ is view) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(view(None, None), 'OK') - - def test_as_function_requestonly(self): - response = DummyResponse() - def view(request): - return response - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_as_newstyle_class_context_and_request(self): - response = DummyResponse() - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return response - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_newstyle_class_requestonly(self): - response = DummyResponse() - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return response - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_oldstyle_class_context_and_request(self): - response = DummyResponse() - class view: - def __init__(self, context, request): - pass - def __call__(self): - return response - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_oldstyle_class_requestonly(self): - response = DummyResponse() - class view: - def __init__(self, context, request): - pass - def __call__(self): - return response - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_instance_context_and_request(self): - response = DummyResponse() - class View: - def __call__(self, context, request): - return response - view = View() - deriver = self._makeOne() - result = deriver(view) - self.assertTrue(result.__wraps__ is view) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_as_instance_requestonly(self): - response = DummyResponse() - class View: - def __call__(self, request): - return response - view = View() - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertTrue('test_views' in result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_with_debug_authorization_no_authpol(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_authn_policy_no_authz_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict(debug_authorization=True) - from pyramid.interfaces import IAuthenticationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - logger = self._registerLogger() - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_authz_policy_no_authn_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict(debug_authorization=True) - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - logger = self._registerLogger() - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_no_permission(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - self._registerSecurityPolicy(True) - logger = self._registerLogger() - deriver = self._makeOne() - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed (" - "no permission registered)") - - def test_debug_auth_permission_authpol_permitted(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(True) - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): True") - - def test_debug_auth_permission_authpol_permitted_no_request(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(True) - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - self.assertEqual(result(None, None), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url None (view name " - "None against context None): True") - - def test_debug_auth_permission_authpol_denied(self): - from pyramid.httpexceptions import HTTPForbidden - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(False) - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertRaises(HTTPForbidden, result, None, request) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): False") - - def test_debug_auth_permission_authpol_denied2(self): - view = lambda *arg: 'OK' - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - self._registerLogger() - self._registerSecurityPolicy(False) - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - permitted = result.__permitted__(None, None) - self.assertEqual(permitted, False) - - def test_debug_auth_permission_authpol_overridden(self): - from pyramid.security import NO_PERMISSION_REQUIRED - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(False) - deriver = self._makeOne(permission=NO_PERMISSION_REQUIRED) - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): " - "Allowed (NO_PERMISSION_REQUIRED)") - - def test_secured_view_authn_policy_no_authz_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - from pyramid.interfaces import IAuthenticationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - - def test_secured_view_authz_policy_no_authn_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - deriver = self._makeOne(permission='view') - result = deriver(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - - def test_secured_view_raises_forbidden_no_name(self): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - from pyramid.httpexceptions import HTTPForbidden - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - deriver = self._makeOne(permission='view') - result = deriver(view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - try: - result(None, request) - except HTTPForbidden as e: - self.assertEqual(e.message, - 'Unauthorized: failed permission check') - else: # pragma: no cover - raise AssertionError - - def test_secured_view_raises_forbidden_with_name(self): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - from pyramid.httpexceptions import HTTPForbidden - def myview(request): pass - self.config.registry.settings = {} - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - deriver = self._makeOne(permission='view') - result = deriver(myview) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - try: - result(None, request) - except HTTPForbidden as e: - self.assertEqual(e.message, - 'Unauthorized: myview failed permission check') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_view_has_no_name(self): - from pyramid.exceptions import PredicateMismatch - response = DummyResponse() - view = lambda *arg: response - def predicate1(context, request): - return False - predicate1.text = lambda *arg: 'text' - deriver = self._makeOne(predicates=[predicate1]) - result = deriver(view) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view (text)') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_view_has_name(self): - from pyramid.exceptions import PredicateMismatch - def myview(request): pass - def predicate1(context, request): - return False - predicate1.text = lambda *arg: 'text' - deriver = self._makeOne(predicates=[predicate1]) - result = deriver(myview) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view myview (text)') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_exception_has_text_in_detail(self): - from pyramid.exceptions import PredicateMismatch - def myview(request): pass - def predicate1(context, request): - return True - predicate1.text = lambda *arg: 'pred1' - def predicate2(context, request): - return False - predicate2.text = lambda *arg: 'pred2' - deriver = self._makeOne(predicates=[predicate1, predicate2]) - result = deriver(myview) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view myview (pred2)') - else: # pragma: no cover - raise AssertionError - - def test_with_predicates_all(self): - response = DummyResponse() - view = lambda *arg: response - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - deriver = self._makeOne(predicates=[predicate1, predicate2]) - result = deriver(view) - request = self._makeRequest() - request.method = 'POST' - next = result(None, None) - self.assertEqual(next, response) - self.assertEqual(predicates, [True, True]) - - def test_with_predicates_checker(self): - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - deriver = self._makeOne(predicates=[predicate1, predicate2]) - result = deriver(view) - request = self._makeRequest() - request.method = 'POST' - next = result.__predicated__(None, None) - self.assertEqual(next, True) - self.assertEqual(predicates, [True, True]) - - def test_with_predicates_notall(self): - from pyramid.httpexceptions import HTTPNotFound - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - predicate1.text = lambda *arg: 'text' - def predicate2(context, request): - predicates.append(True) - return False - predicate2.text = lambda *arg: 'text' - deriver = self._makeOne(predicates=[predicate1, predicate2]) - result = deriver(view) - request = self._makeRequest() - request.method = 'POST' - self.assertRaises(HTTPNotFound, result, None, None) - self.assertEqual(predicates, [True, True]) - - def test_with_wrapper_viewname(self): - from pyramid.response import Response - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - def outer_view(context, request): - self.assertEqual(request.wrapped_response, inner_response) - self.assertEqual(request.wrapped_body, inner_response.body) - self.assertEqual(request.wrapped_view.__original_view__, - inner_view) - return Response(b'outer ' + request.wrapped_body) - self.config.registry.registerAdapter( - outer_view, (IViewClassifier, None, None), IView, 'owrap') - deriver = self._makeOne(viewname='inner', - wrapper_viewname='owrap') - result = deriver(inner_view) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - response = result(None, request) - self.assertEqual(response.body, b'outer OK') - - def test_with_wrapper_viewname_notfound(self): - from pyramid.response import Response - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - deriver = self._makeOne(viewname='inner', wrapper_viewname='owrap') - wrapped = deriver(inner_view) - request = self._makeRequest() - self.assertRaises(ValueError, wrapped, None, request) - - def test_as_newstyle_class_context_and_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View(object): - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - deriver = self._makeOne(renderer=renderer(), attr='index') - result = deriver(View) - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_newstyle_class_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View(object): - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - deriver = self._makeOne(renderer=renderer(), attr='index') - result = deriver(View) - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_oldstyle_cls_context_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - deriver = self._makeOne(renderer=renderer(), attr='index') - result = deriver(View) - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - deriver = self._makeOne(renderer=renderer(), attr='index') - result = deriver(View) - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_instance_context_and_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def index(self, context, request): - return {'a':'1'} - deriver = self._makeOne(renderer=renderer(), attr='index') - view = View() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(result.__module__, view.__module__) - self.assertEqual(result.__doc__, view.__doc__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_instance_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def index(self, request): - return {'a':'1'} - deriver = self._makeOne(renderer=renderer(), attr='index') - view = View() - result = deriver(view) - self.assertFalse(result is view) - self.assertEqual(result.__module__, view.__module__) - self.assertEqual(result.__doc__, view.__doc__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - def test_with_view_mapper_config_specified(self): - response = DummyResponse() - class mapper(object): - def __init__(self, **kw): - self.kw = kw - def __call__(self, view): - def wrapped(context, request): - return response - return wrapped - def view(context, request): return 'NOTOK' - deriver = self._makeOne(mapper=mapper) - result = deriver(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_with_view_mapper_view_specified(self): - from pyramid.response import Response - response = Response() - def mapper(**kw): - def inner(view): - def superinner(context, request): - self.assertEqual(request, None) - return response - return superinner - return inner - def view(context, request): return 'NOTOK' - view.__view_mapper__ = mapper - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_with_view_mapper_default_mapper_specified(self): - from pyramid.response import Response - response = Response() - def mapper(**kw): - def inner(view): - def superinner(context, request): - self.assertEqual(request, None) - return response - return superinner - return inner - self.config.set_view_mapper(mapper) - def view(context, request): return 'NOTOK' - deriver = self._makeOne() - result = deriver(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_attr_wrapped_view_branching_default_phash(self): - from pyramid.config.util import DEFAULT_PHASH - def view(context, request): pass - deriver = self._makeOne(phash=DEFAULT_PHASH) - result = deriver(view) - self.assertEqual(result.__wraps__, view) - - def test_attr_wrapped_view_branching_nondefault_phash(self): - def view(context, request): pass - deriver = self._makeOne(phash='nondefault') - result = deriver(view) - self.assertNotEqual(result, view) - - def test_http_cached_view_integer(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - deriver = self._makeOne(http_cache=3600) - result = deriver(inner_view) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600') - - def test_http_cached_view_timedelta(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - deriver = self._makeOne(http_cache=datetime.timedelta(hours=1)) - result = deriver(inner_view) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600') - - def test_http_cached_view_tuple(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - deriver = self._makeOne(http_cache=(3600, {'public':True})) - result = deriver(inner_view) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') - - def test_http_cached_view_tuple_seconds_None(self): - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - deriver = self._makeOne(http_cache=(None, {'public':True})) - result = deriver(inner_view) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertEqual(headers['Cache-Control'], 'public') - - def test_http_cached_view_prevent_auto_set(self): - from pyramid.response import Response - response = Response() - response.cache_control.prevent_auto = True - def inner_view(context, request): - return response - deriver = self._makeOne(http_cache=3600) - result = deriver(inner_view) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) # doesn't blow up - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertFalse('Cache-Control' in headers) - - def test_http_cached_prevent_http_cache_in_settings(self): - self.config.registry.settings['prevent_http_cache'] = True - from pyramid.response import Response - response = Response() - def inner_view(context, request): - return response - deriver = self._makeOne(http_cache=3600) - result = deriver(inner_view) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertFalse('Cache-Control' in headers) - - def test_http_cached_view_bad_tuple(self): - deriver = self._makeOne(http_cache=(None,)) - def view(request): pass - self.assertRaises(ConfigurationError, deriver, view) class TestDefaultViewMapper(unittest.TestCase): def setUp(self): -- cgit v1.2.3 From fd3bd228bab840aa417c4968a94f50e0ff5d77d6 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Wed, 15 Apr 2015 14:59:14 -0400 Subject: Fix package name in tests. --- pyramid/tests/test_config/test_derivations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 668a462b1..ff3577e12 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -39,7 +39,7 @@ class TestDeriveView(unittest.TestCase): self.assertEqual( e.args[0], 'Could not convert return value of the view callable function ' - 'pyramid.tests.test_config.test_views.view into a response ' + 'pyramid.tests.test_config.test_derivations.view into a response ' 'object. The value returned was None. You may have forgotten ' 'to return a value from the view callable.' ) @@ -57,7 +57,7 @@ class TestDeriveView(unittest.TestCase): self.assertEqual( e.args[0], "Could not convert return value of the view callable function " - "pyramid.tests.test_config.test_views.view into a response " + "pyramid.tests.test_config.test_derivations.view into a response " "object. The value returned was {'a': 1}. You may have " "forgotten to define a renderer in the view configuration." ) @@ -121,7 +121,7 @@ class TestDeriveView(unittest.TestCase): e.args[0], 'Could not convert return value of the view callable ' 'method __call__ of ' - 'class pyramid.tests.test_config.test_views.AView into a ' + 'class pyramid.tests.test_config.test_derivations.AView into a ' 'response object. The value returned was None. You may have ' 'forgotten to return a value from the view callable.' ) -- cgit v1.2.3 From 611aa33342653d5ead73473703f4aad8680ed0df Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Wed, 15 Apr 2015 15:01:18 -0400 Subject: moar deriver test fixes --- pyramid/tests/test_config/test_derivations.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 668a462b1..bda85b00a 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -966,12 +966,12 @@ class TestDeriveView(unittest.TestCase): def test_attr_wrapped_view_branching_default_phash(self): from pyramid.config.util import DEFAULT_PHASH def view(context, request): pass - result = self.config.derive_view(view, phash=DEFAULT_PHASH) + result = self.config._derive_view(view, phash=DEFAULT_PHASH) self.assertEqual(result.__wraps__, view) def test_attr_wrapped_view_branching_nondefault_phash(self): def view(context, request): pass - result = self.config.derive_view(view, phash='nondefault') + result = self.config._derive_view(view, phash='nondefault') self.assertNotEqual(result, view) def test_http_cached_view_integer(self): @@ -980,7 +980,7 @@ class TestDeriveView(unittest.TestCase): response = Response('OK') def inner_view(context, request): return response - result = self.config.derive_view(inner_view, http_cache=3600) + result = self.config._derive_view(inner_view, http_cache=3600) self.assertFalse(result is inner_view) self.assertEqual(inner_view.__module__, result.__module__) self.assertEqual(inner_view.__doc__, result.__doc__) @@ -999,7 +999,7 @@ class TestDeriveView(unittest.TestCase): response = Response('OK') def inner_view(context, request): return response - result = self.config.derive_view(inner_view, + result = self.config._derive_view(inner_view, http_cache=datetime.timedelta(hours=1)) self.assertFalse(result is inner_view) self.assertEqual(inner_view.__module__, result.__module__) @@ -1019,7 +1019,7 @@ class TestDeriveView(unittest.TestCase): response = Response('OK') def inner_view(context, request): return response - result = self.config.derive_view(inner_view, + result = self.config._derive_view(inner_view, http_cache=(3600, {'public':True})) self.assertFalse(result is inner_view) self.assertEqual(inner_view.__module__, result.__module__) @@ -1126,3 +1126,16 @@ class DummySecurityPolicy: def permits(self, context, principals, permission): return self.permitted +def parse_httpdate(s): + import datetime + # cannot use %Z, must use literal GMT; Jython honors timezone + # but CPython does not + return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT") + +def assert_similar_datetime(one, two): + for attr in ('year', 'month', 'day', 'hour', 'minute'): + one_attr = getattr(one, attr) + two_attr = getattr(two, attr) + if not one_attr == two_attr: # pragma: no cover + raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) + -- cgit v1.2.3 From 36d62243465ac9d573bb42f73fb243c9e9ce56c0 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Wed, 15 Apr 2015 15:16:53 -0400 Subject: fix more derivation tests --- pyramid/tests/test_config/test_derivations.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 51c7174f0..f906e1aef 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -77,7 +77,7 @@ class TestDeriveView(unittest.TestCase): msg = e.args[0] self.assertTrue(msg.startswith( 'Could not convert return value of the view callable object ' - ' into a response object. The value returned was None. You ' 'may have forgotten to return a value from the view callable.')) @@ -135,6 +135,8 @@ class TestDeriveView(unittest.TestCase): pass def theviewmethod(self): return None + def __call__(self): + return None result = self.config.derive_view(AView) self.assertFalse(result is AView) try: @@ -144,7 +146,7 @@ class TestDeriveView(unittest.TestCase): e.args[0], 'Could not convert return value of the view callable ' 'method theviewmethod of ' - 'class pyramid.tests.test_config.test_views.AView into a ' + 'class pyramid.tests.test_config.test_derivations.AView into a ' 'response object. The value returned was None. You may have ' 'forgotten to return a value from the view callable.' ) @@ -351,7 +353,7 @@ class TestDeriveView(unittest.TestCase): self.assertFalse(result is view) self.assertEqual(view.__module__, result.__module__) self.assertEqual(view.__doc__, result.__doc__) - self.assertTrue('test_views' in result.__name__) + self.assertTrue('test_derivations' in result.__name__) self.assertFalse(hasattr(result, '__call_permissive__')) self.assertEqual(result(None, None), response) @@ -760,7 +762,7 @@ class TestDeriveView(unittest.TestCase): inner_response = Response('OK') def inner_view(context, request): return inner_response - result = self.config._derive_view(inner_view, viewname='inner', + wrapped = self.config._derive_view(inner_view, viewname='inner', wrapper_viewname='owrap') request = self._makeRequest() self.assertRaises(ValueError, wrapped, None, request) -- cgit v1.2.3 From 488fddedaf9b61d4b13c071196b31bf22b3fed51 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Wed, 15 Apr 2015 15:17:39 -0400 Subject: Fix some tests that had bad capitalization, and fix a missing import. --- pyramid/tests/test_config/test_derivations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 51c7174f0..60212b48d 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1,6 +1,7 @@ import unittest from pyramid import testing +from pyramid.exceptions import ConfigurationError class TestDeriveView(unittest.TestCase): @@ -833,7 +834,7 @@ class TestDeriveView(unittest.TestCase): pass def index(self): return {'a':'1'} - result = self.config.derive_view(view, + result = self.config.derive_view(View, renderer=renderer(), attr='index') self.assertFalse(result is View) self.assertEqual(result.__module__, View.__module__) @@ -859,7 +860,7 @@ class TestDeriveView(unittest.TestCase): pass def index(self): return {'a':'1'} - result = self.config.derive_view(view, + result = self.config.derive_view(View, renderer=renderer(), attr='index') self.assertFalse(result is View) self.assertEqual(result.__module__, View.__module__) -- cgit v1.2.3 From 1224af27c66e5351e2fa779e5085a57b8d3489b1 Mon Sep 17 00:00:00 2001 From: Amos Latteier Date: Wed, 15 Apr 2015 15:29:25 -0400 Subject: start of fixing test. --- pyramid/tests/test_config/test_derivations.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 3d16856f6..319f03414 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -136,8 +136,6 @@ class TestDeriveView(unittest.TestCase): pass def theviewmethod(self): return None - def __call__(self): - return None result = self.config.derive_view(AView) self.assertFalse(result is AView) try: -- cgit v1.2.3 From 96c87b36a39aa492d4ecaab0805d997270bec33b Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Wed, 15 Apr 2015 16:15:36 -0400 Subject: Finish porting original view deriver tests --- pyramid/tests/test_config/test_derivations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 319f03414..97fc4c58a 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -136,7 +136,7 @@ class TestDeriveView(unittest.TestCase): pass def theviewmethod(self): return None - result = self.config.derive_view(AView) + result = self.config.derive_view(AView, attr='theviewmethod') self.assertFalse(result is AView) try: result(None, request) @@ -173,7 +173,7 @@ class TestDeriveView(unittest.TestCase): return self def view(request): return 'OK' - result = self.config.derive_view(view) + result = self.config.derive_view(view, renderer=moo()) self.assertFalse(result.__wraps__ is view) request = self._makeRequest() context = testing.DummyResource() @@ -1081,7 +1081,7 @@ class TestDeriveView(unittest.TestCase): def test_http_cached_view_bad_tuple(self): def view(request): pass - self.assertRaises(ConfigurationError, self.config.derive_view, + self.assertRaises(ConfigurationError, self.config._derive_view, view, http_cache=(None,)) from zope.interface import implementer -- cgit v1.2.3 From 07d4a441adc9074439ec6071542d5d3d61643798 Mon Sep 17 00:00:00 2001 From: Casey Duncan Date: Thu, 16 Apr 2015 12:14:25 -0400 Subject: Tests for adding view derivations --- pyramid/tests/test_config/test_derivations.py | 113 ++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 97fc4c58a..5bbbdd04f 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1084,6 +1084,119 @@ class TestDeriveView(unittest.TestCase): self.assertRaises(ConfigurationError, self.config._derive_view, view, http_cache=(None,)) + +class TestAddDerivation(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + + def test_add_single_derivation(self): + response = DummyResponse() + response.deriv = False + view = lambda *arg: response + + def deriv(view, default, **kw): + self.assertFalse(response.deriv) + self.assertEqual(default, None) + response.deriv = True + return view + + result = self.config._derive_view(view) + self.assertFalse(response.deriv) + self.config.add_view_derivation('test_deriv', deriv, default=None) + + result = self.config._derive_view(view) + self.assertTrue(response.deriv) + + def test_derivation_default(self): + response = DummyResponse() + response.deriv_default = None + test_default = object() + view = lambda *arg: response + + def deriv(view, default, **kw): + response.deriv_default = default + return view + + self.config.add_view_derivation('test_default_deriv', deriv, default=test_default) + result = self.config._derive_view(view) + self.assertEqual(response.deriv_default, test_default) + + def test_override_derivation(self): + flags = {} + + class AView: + def __init__(self): + self.response = DummyResponse() + def __call__(self): + return self.response + + def deriv1(view, default, **kw): + flags['deriv1'] = True + return view + + def deriv2(view, default, **kw): + flags['deriv2'] = True + return view + + view1 = AView() + self.config.add_view_derivation('test_deriv', deriv1, default=None) + result = self.config._derive_view(view1) + self.assertTrue(flags.get('deriv1')) + self.assertFalse(flags.get('deriv2')) + + flags.clear() + view2 = AView() + self.config.add_view_derivation('test_deriv', deriv2, default=None) + result = self.config._derive_view(view2) + self.assertFalse(flags.get('deriv1')) + self.assertTrue(flags.get('deriv2')) + + def test_override_derivation_default(self): + response = DummyResponse() + response.deriv_default = None + test_default1 = 'first default' + test_default2 = 'second default' + view = lambda *arg: response + + def deriv(view, default, **kw): + response.deriv_default = default + return view + + self.config.add_view_derivation('test_default_deriv', deriv, default=test_default1) + result = self.config._derive_view(view) + self.assertEqual(response.deriv_default, test_default1) + self.config.add_view_derivation('test_default_deriv', deriv, default=test_default2) + result = self.config._derive_view(view) + self.assertEqual(response.deriv_default, test_default2) + + def test_add_multi_derivations_ordered(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, default, **kw): + response.deriv.append('deriv1') + return view + + def deriv2(view, default, **kw): + response.deriv.append('deriv2') + return view + + def deriv3(view, default, **kw): + response.deriv.append('deriv3') + return view + + self.config.add_view_derivation('deriv1', deriv1, default=None) + self.config.add_view_derivation('deriv2', deriv2, default=None, weighs_less_than='deriv1') + self.config.add_view_derivation('deriv3', deriv3, default=None, weighs_more_than='deriv2') + result = self.config._derive_view(view) + self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + + from zope.interface import implementer from pyramid.interfaces import ( IResponse, -- cgit v1.2.3 From bf40a3920278b3e7a01ef5403196b35f45cfcb3c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 19 Apr 2015 22:53:34 -0500 Subject: fix some style issues --- pyramid/config/derivations.py | 25 ++++++++++++------------- pyramid/config/views.py | 37 +++++++++++++++---------------------- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 14c43dbef..689b36216 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -272,8 +272,7 @@ def secured_view(view, default, **kw): if authn_policy and authz_policy and (permission is not None): def _permitted(context, request): principals = authn_policy.effective_principals(request) - return authz_policy.permits(context, principals, - permission) + return authz_policy.permits(context, principals, permission) def _secured_view(context, request): result = _permitted(context, request) if result: @@ -308,10 +307,9 @@ def authdebug_view(view, default, **kw): elif permission is None: msg = 'Allowed (no permission registered)' else: - principals = authn_policy.effective_principals( - request) - msg = str(authz_policy.permits(context, principals, - permission)) + principals = authn_policy.effective_principals(request) + msg = str(authz_policy.permits( + context, principals, permission)) else: msg = 'Allowed (no authorization policy in use)' @@ -319,7 +317,8 @@ def authdebug_view(view, default, **kw): url = getattr(request, 'url', None) msg = ('debug_authorization of url %s (view name %r against ' 'context %r): %s' % (url, view_name, context, msg)) - if logger: logger.debug(msg) + if logger: + logger.debug(msg) if request is not None: request.authdebug_message = msg return view(context, request) @@ -338,8 +337,8 @@ def predicated_view(view, default, **kw): if not predicate(context, request): view_name = getattr(view, '__name__', view) raise PredicateMismatch( - 'predicate mismatch for view %s (%s)' % ( - view_name, predicate.text())) + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.text())) return view(context, request) def checker(context, request): return all((predicate(context, request) for predicate in @@ -361,7 +360,7 @@ def attr_wrapped_view(view, default, **kw): (accept is None) and (order == MAX_ORDER) and (phash == DEFAULT_PHASH) - ): + ): return view # defaults def attr_view(context, request): return view(context, request) @@ -428,15 +427,15 @@ def rendered_view(view, default, **kw): view_renderer = renderers.RendererHelper( name=renderer_name, package=kw.get('package'), - registry = registry) + registry=registry) else: view_renderer = renderer.clone() if '__view__' in attrs: view_inst = attrs.pop('__view__') else: view_inst = getattr(view, '__original_view__', view) - response = view_renderer.render_view(request, result, view_inst, - context) + response = view_renderer.render_view( + request, result, view_inst, context) return response return rendered_view diff --git a/pyramid/config/views.py b/pyramid/config/views.py index db69c7e01..7202acf2c 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -7,15 +7,11 @@ from zope.interface import ( Interface, implementedBy, implementer, - provider, ) from zope.interface.interfaces import IInterface from pyramid.interfaces import ( - IAuthenticationPolicy, - IAuthorizationPolicy, - IDebugLogger, IDefaultPermission, IException, IExceptionViewClassifier, @@ -29,7 +25,6 @@ from pyramid.interfaces import ( IView, IViewClassifier, IViewDerivers, - IViewMapper, IViewMapperFactory, PHASE1_CONFIG, ) @@ -42,8 +37,6 @@ from pyramid.compat import ( urlparse, url_quote, WIN, - is_bound_method, - is_unbound_method, is_nonstr_iter, ) @@ -63,22 +56,16 @@ from pyramid.registry import ( Deferred, ) -from pyramid.response import Response - from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry from pyramid.url import parse_url_overrides -from pyramid.view import ( - render_view_to_response, - AppendSlashNotFoundViewFactory, - ) +from pyramid.view import AppendSlashNotFoundViewFactory import pyramid.util from pyramid.util import ( - object_description, viewdefaults, action_method, TopologicalSorter, @@ -87,7 +74,6 @@ from pyramid.util import ( import pyramid.config.predicates import pyramid.config.derivations -# bw compat from pyramid.config.derivations import ( preserve_view_attrs, view_description, @@ -98,12 +84,16 @@ from pyramid.config.derivations import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - takes_one_arg, ) urljoin = urlparse.urljoin url_parse = urlparse.urlparse +DefaultViewMapper = DefaultViewMapper # bw-compat +preserve_view_attrs = preserve_view_attrs # bw-compat +requestonly = requestonly # bw-compat +view_description = view_description # bw-compat + @implementer(IMultiView) class MultiView(object): @@ -710,7 +700,7 @@ class ViewsConfiguratorMixin(object): if isinstance(renderer, string_types): renderer = renderers.RendererHelper( name=renderer, package=self.package, - registry = self.registry) + registry=self.registry) if accept is not None: accept = accept.lower() @@ -810,7 +800,8 @@ class ViewsConfiguratorMixin(object): phash = view_intr['phash'] # __no_permission_required__ handled by _secure_view - derived_view = self._apply_view_derivations(view, + derived_view = self._apply_view_derivations( + view, registry=self.registry, permission=permission, predicates=preds, @@ -1174,7 +1165,7 @@ class ViewsConfiguratorMixin(object): if isinstance(renderer, string_types): renderer = renderers.RendererHelper( name=renderer, package=self.package, - registry = self.registry) + registry=self.registry) if renderer is None: # use default renderer if one exists if self.registry.queryUtility(IRendererFactory) is not None: @@ -1183,7 +1174,8 @@ class ViewsConfiguratorMixin(object): package=self.package, registry=self.registry) - return self._apply_view_derivations(view, + return self._apply_view_derivations( + view, registry=self.registry, permission=permission, predicates=predicates, @@ -1197,7 +1189,8 @@ class ViewsConfiguratorMixin(object): package=self.package, mapper=mapper, decorator=decorator, - http_cache=http_cache) + http_cache=http_cache, + ) @viewdefaults @action_method @@ -1698,7 +1691,7 @@ class StaticURLInfo(object): def register(): registrations = self._get_registrations(config.registry) - names = [ t[0] for t in registrations ] + names = [ t[0] for t in registrations ] if name in names: idx = names.index(name) -- cgit v1.2.3 From 063c3c8c80baf81bfe1a7552013872af0fb57a19 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 19:55:39 -0600 Subject: Views in this test are not called --- pyramid/tests/test_config/test_derivations.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 5bbbdd04f..1c5432166 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1131,8 +1131,6 @@ class TestAddDerivation(unittest.TestCase): class AView: def __init__(self): self.response = DummyResponse() - def __call__(self): - return self.response def deriv1(view, default, **kw): flags['deriv1'] = True -- cgit v1.2.3 From e7edb2e00facbc7b39678525bee1be58365da92a Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 19:56:10 -0600 Subject: We don't need no DummyLogger --- pyramid/tests/test_config/test_views.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a1c161322..0689f19a7 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3067,14 +3067,6 @@ class DummyAccept(object): def __contains__(self, val): return val in self.matches -class DummyLogger: - def __init__(self): - self.messages = [] - def info(self, msg): - self.messages.append(msg) - warn = info - debug = info - class DummySecurityPolicy: def __init__(self, permitted=True): self.permitted = permitted -- cgit v1.2.3 From b3373e092f334944b094025d5b95a2652a995f8d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 19:56:24 -0600 Subject: Remove the DummySecurityPolicy too --- pyramid/tests/test_config/test_views.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 0689f19a7..8135d6207 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3067,16 +3067,6 @@ class DummyAccept(object): def __contains__(self, val): return val in self.matches -class DummySecurityPolicy: - def __init__(self, permitted=True): - self.permitted = permitted - - def effective_principals(self, request): - return [] - - def permits(self, context, principals, permission): - return self.permitted - class DummyConfig: route_prefix = '' def add_route(self, *args, **kw): -- cgit v1.2.3 From 51ee346f982aaa325d22a80fd676ee2a21b985f5 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 21:43:29 -0600 Subject: PredicateList learns how to return the names This is required for future changes, where we need the list of predicate names. --- pyramid/config/util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 0fd9ef4a7..85ce826a4 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -115,6 +115,10 @@ class PredicateList(object): before=weighs_less_than, ) + def names(self): + # Return the list of valid predicate names. + return self.sorter.names + def make(self, config, **kw): # Given a configurator and a list of keywords, a predicate list is # computed. Elsewhere in the code, we evaluate predicates using a -- cgit v1.2.3 From 7fb6f954f406e99a969c5d4d108ed3e5edb6f9cd Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 22:52:40 -0600 Subject: Rename predicates to view_options We also use the previously defined names() method on PredicateList to fetch the list of predicates and pull out all predicate values into it's own dictionary. Then the rest of the values are the view options for the view derivations. At the moment we do not check if the options matches a valid view derivation. No seatbelts for view options. --- pyramid/config/views.py | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 30c73f6cb..5d92d6982 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -205,7 +205,7 @@ class ViewsConfiguratorMixin(object): http_cache=None, match_param=None, check_csrf=None, - **predicates): + **view_options): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -646,16 +646,17 @@ class ViewsConfiguratorMixin(object): obsoletes this argument, but it is kept around for backwards compatibility. - predicates + view_options: - Pass a key/value pair here to use a third-party predicate - registered via - :meth:`pyramid.config.Configurator.add_view_predicate`. More than + Pass a key/value pair here to use a third-party predicate or set a + value for a view derivative option registered via + :meth:`pyramid.config.Configurator.add_view_predicate` or + :meth:`pyramid.config.Configurator.add_view_derivation`. More than one key/value pair can be used at the same time. See :ref:`view_and_route_predicates` for more information about third-party predicates. - .. versionadded: 1.4a1 + .. versionadded: 1.7 """ if custom_predicates: @@ -722,8 +723,8 @@ class ViewsConfiguratorMixin(object): accept = accept.lower() introspectables = [] - pvals = predicates.copy() - pvals.update( + ovals = view_options.copy() + ovals.update( dict( xhr=xhr, request_method=request_method, @@ -742,9 +743,26 @@ class ViewsConfiguratorMixin(object): def discrim_func(): # We need to defer the discriminator until we know what the phash # is. It can't be computed any sooner because thirdparty - # predicates may not yet exist when add_view is called. + # predicates/view derivations may not yet exist when add_view is + # called. + valid_predicates = predlist.names() + pvals = {} + options = {} + + for (k, v) in ovals.items(): + if k in valid_predicates: + pvals[k] = v + else: + options[k] = v + order, preds, phash = predlist.make(self, **pvals) - view_intr.update({'phash':phash, 'order':order, 'predicates':preds}) + + view_intr.update({ + 'phash': phash, + 'order': order, + 'predicates': preds, + 'options': options + }) return ('view', context, name, route_name, phash) discriminator = Deferred(discrim_func) @@ -780,7 +798,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, ) ) - view_intr.update(**predicates) + view_intr.update(**view_options) introspectables.append(view_intr) predlist = self.get_predlist('view') @@ -812,6 +830,7 @@ class ViewsConfiguratorMixin(object): # added by discrim_func above during conflict resolving preds = view_intr['predicates'] + opts = view_intr['options'] order = view_intr['order'] phash = view_intr['phash'] @@ -832,6 +851,7 @@ class ViewsConfiguratorMixin(object): mapper=mapper, decorator=decorator, http_cache=http_cache, + options=opts, ) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems -- cgit v1.2.3 From 174bb5e28cccf8cfd61826c2ba384f43a78df00e Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 22:55:33 -0600 Subject: Add ViewDerivation integration tests This is an integration test in the sense that it adds both a view derivation and then adds a view. That view is then tested to verify that it correctly returns and that the derivations haven't broken anything. --- pyramid/tests/test_config/test_derivations.py | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 1c5432166..f33856e75 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -10,6 +10,7 @@ class TestDeriveView(unittest.TestCase): def tearDown(self): self.config = None + testing.tearDown() def _makeRequest(self): request = DummyRequest() @@ -1092,6 +1093,7 @@ class TestAddDerivation(unittest.TestCase): def tearDown(self): self.config = None + testing.tearDown() def test_add_single_derivation(self): response = DummyResponse() @@ -1194,6 +1196,59 @@ class TestAddDerivation(unittest.TestCase): result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) +class TestDerivationIntegration(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def _getViewCallable(self, config, ctx_iface=None, request_iface=None, + name=''): + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), IView, name=name, + default=None) + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def test_view_options(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, default, **kw): + response.deriv.append(kw['options']['deriv1']) + return view + + def deriv2(view, default, **kw): + response.deriv.append(kw['options']['deriv2']) + return view + + self.config.add_view_derivation('deriv1', deriv1, default=None) + self.config.add_view_derivation('deriv2', deriv2, default=None) + self.config.add_view(view, deriv1='test1', deriv2='test2') + self.config.commit() + + wrapper = self._getViewCallable(self.config) + request = self._makeRequest(self.config) + request.method = 'GET' + self.assertEqual(wrapper(None, request), response) + self.assertEqual(['test1', 'test2'], response.deriv) + from zope.interface import implementer from pyramid.interfaces import ( -- cgit v1.2.3 From c6dffccf958588e113d4c5961b122217b7ac6d0d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 22:56:59 -0600 Subject: Add options to the internal _derive_view This way we can use that in tests. --- pyramid/config/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5d92d6982..17986e275 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1191,7 +1191,7 @@ class ViewsConfiguratorMixin(object): return self._derive_view(view, attr=attr, renderer=renderer) # b/w compat - def _derive_view(self, view, permission=None, predicates=(), + def _derive_view(self, view, permission=None, predicates=(), options=(), attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, @@ -1226,6 +1226,7 @@ class ViewsConfiguratorMixin(object): mapper=mapper, decorator=decorator, http_cache=http_cache, + options=options, ) @viewdefaults -- cgit v1.2.3 From 2efc8202ddbaf0a331ba99d21257aeff47be63f8 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 22:57:27 -0600 Subject: Rename the rest of the **predicates to **view_options --- pyramid/config/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 17986e275..a92d76c1c 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1250,7 +1250,7 @@ class ViewsConfiguratorMixin(object): decorator=None, mapper=None, match_param=None, - **predicates + **view_options ): """ Add a forbidden view to the current configuration state. The view will be called when Pyramid or application code raises a @@ -1280,7 +1280,7 @@ class ViewsConfiguratorMixin(object): .. versionadded:: 1.3 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): - if arg in predicates: + if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_forbidden_view' % arg @@ -1310,7 +1310,7 @@ class ViewsConfiguratorMixin(object): attr=attr, renderer=renderer, ) - settings.update(predicates) + settings.update(view_options) return self.add_view(**settings) set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias @@ -1337,7 +1337,7 @@ class ViewsConfiguratorMixin(object): mapper=None, match_param=None, append_slash=False, - **predicates + **view_options ): """ Add a default Not Found View to the current configuration state. The view will be called when Pyramid or application code raises an @@ -1392,7 +1392,7 @@ class ViewsConfiguratorMixin(object): .. versionadded:: 1.3 """ for arg in ('name', 'permission', 'context', 'for_', 'http_cache'): - if arg in predicates: + if arg in view_options: raise ConfigurationError( '%s may not be used as an argument to add_notfound_view' % arg @@ -1420,7 +1420,7 @@ class ViewsConfiguratorMixin(object): route_name=route_name, permission=NO_PERMISSION_REQUIRED, ) - settings.update(predicates) + settings.update(view_options) if append_slash: view = self._derive_view(view, attr=attr, renderer=renderer) if IResponse.implementedBy(append_slash): -- cgit v1.2.3 From 6de9907e82276e34d86aa6f6fe51274e9a18ae18 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 23:00:49 -0600 Subject: PEP 8 --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index a92d76c1c..b085ef35e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -830,7 +830,7 @@ class ViewsConfiguratorMixin(object): # added by discrim_func above during conflict resolving preds = view_intr['predicates'] - opts = view_intr['options'] + opts = view_intr['options'] order = view_intr['order'] phash = view_intr['phash'] -- cgit v1.2.3 From bd67987ae2146c5a8fcddc6089b5137c6d41ebb5 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 23:11:20 -0600 Subject: Autowrap all derivations --- pyramid/config/derivations.py | 9 --------- pyramid/config/views.py | 3 ++- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 689b36216..b163bf12a 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -193,7 +193,6 @@ def preserve_view_attrs(view, wrapper): return wrapper -@wraps_view def mapped_view(view, default, **kw): mapper = kw.get('mapper') if mapper is None: @@ -206,7 +205,6 @@ def mapped_view(view, default, **kw): mapped_view = mapper(**kw)(view) return mapped_view -@wraps_view def owrapped_view(view, default, **kw): wrapper_viewname = kw.get('wrapper_viewname') viewname = kw.get('viewname') @@ -226,7 +224,6 @@ def owrapped_view(view, default, **kw): return wrapped_response return _owrapped_view -@wraps_view def http_cached_view(view, default, **kw): if kw['registry'].settings.get('prevent_http_cache', False): return view @@ -256,7 +253,6 @@ def http_cached_view(view, default, **kw): return wrapper -@wraps_view def secured_view(view, default, **kw): permission = kw.get('permission') if permission == NO_PERMISSION_REQUIRED: @@ -289,7 +285,6 @@ def secured_view(view, default, **kw): return wrapped_view -@wraps_view def authdebug_view(view, default, **kw): wrapped_view = view settings = kw['registry'].settings @@ -327,7 +322,6 @@ def authdebug_view(view, default, **kw): return wrapped_view -@wraps_view def predicated_view(view, default, **kw): preds = kw.get('predicates', ()) if not preds: @@ -347,7 +341,6 @@ def predicated_view(view, default, **kw): predicate_wrapper.__predicates__ = preds return predicate_wrapper -@wraps_view def attr_wrapped_view(view, default, **kw): kw = kw accept, order, phash = (kw.get('accept', None), @@ -371,7 +364,6 @@ def attr_wrapped_view(view, default, **kw): attr_view.__permission__ = kw.get('permission') return attr_view -@wraps_view def rendered_view(view, default, **kw): # one way or another this wrapper must produce a Response (unless # the renderer is a NullRendererHelper) @@ -440,7 +432,6 @@ def rendered_view(view, default, **kw): return rendered_view -@wraps_view def decorated_view(view, default, **kw): decorator = kw.get('decorator') if decorator is None: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b085ef35e..450c77dfd 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -79,6 +79,7 @@ from pyramid.config.derivations import ( view_description, requestonly, DefaultViewMapper, + wraps_view, ) from pyramid.config.util import ( @@ -1020,7 +1021,7 @@ class ViewsConfiguratorMixin(object): derivers = self.registry.queryUtility(IViewDerivers, default=[]) for name, val in inner_derivers + derivers.sorted(): derivation, default = val - view = derivation(view, default, **kw) + view = wraps_view(derivation)(view, default, **kw) return view @action_method -- cgit v1.2.3 From 3a20b74382c920638418a72789ed7d75b085b2d6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 15 Oct 2015 23:33:19 -0600 Subject: There are two view derivations we can't do without Specifically predicated_view and attr_wrapped_view exist for MultiView to function, without them the view machinery doesn't work. They aren't technically view derivations because they are just containers for some data that the view machinery requires. --- pyramid/config/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 450c77dfd..a09654330 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1018,8 +1018,11 @@ class ViewsConfiguratorMixin(object): inner_derivers = [('mapped_view', (d.mapped_view, None)), ('rendered_view', (d.rendered_view, None))] + outer_derivers = [('predicated_view', (d.predicated_view, None)), + ('attr_wrapped_view', (d.attr_wrapped_view, None)),] + derivers = self.registry.queryUtility(IViewDerivers, default=[]) - for name, val in inner_derivers + derivers.sorted(): + for name, val in inner_derivers + derivers.sorted() + outer_derivers: derivation, default = val view = wraps_view(derivation)(view, default, **kw) return view @@ -1105,8 +1108,6 @@ class ViewsConfiguratorMixin(object): ('owrapped_view', d.owrapped_view), ('secured_view', d.secured_view), ('authdebug_view', d.authdebug_view), - ('predicated_view', d.predicated_view), - ('attr_wrapped_view', d.attr_wrapped_view), ] after = pyramid.util.FIRST for name, deriver in derivers: -- cgit v1.2.3 From c5172f355ec48a8e9a7f63d5711ae34b7572307b Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Fri, 16 Oct 2015 00:00:32 -0600 Subject: Change default to value --- pyramid/config/derivations.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index b163bf12a..c23e5c479 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -154,8 +154,8 @@ class DefaultViewMapper(object): def wraps_view(wrapper): - def inner(view, default, **kw): - wrapper_view = wrapper(view, default, **kw) + def inner(view, value, **kw): + wrapper_view = wrapper(view, value, **kw) return preserve_view_attrs(view, wrapper_view) return inner @@ -193,7 +193,7 @@ def preserve_view_attrs(view, wrapper): return wrapper -def mapped_view(view, default, **kw): +def mapped_view(view, value, **kw): mapper = kw.get('mapper') if mapper is None: mapper = getattr(view, '__view_mapper__', None) @@ -205,7 +205,7 @@ def mapped_view(view, default, **kw): mapped_view = mapper(**kw)(view) return mapped_view -def owrapped_view(view, default, **kw): +def owrapped_view(view, value, **kw): wrapper_viewname = kw.get('wrapper_viewname') viewname = kw.get('viewname') if not wrapper_viewname: @@ -224,7 +224,7 @@ def owrapped_view(view, default, **kw): return wrapped_response return _owrapped_view -def http_cached_view(view, default, **kw): +def http_cached_view(view, value, **kw): if kw['registry'].settings.get('prevent_http_cache', False): return view @@ -253,7 +253,7 @@ def http_cached_view(view, default, **kw): return wrapper -def secured_view(view, default, **kw): +def secured_view(view, value, **kw): permission = kw.get('permission') if permission == NO_PERMISSION_REQUIRED: # allow views registered within configurations that have a @@ -285,7 +285,7 @@ def secured_view(view, default, **kw): return wrapped_view -def authdebug_view(view, default, **kw): +def authdebug_view(view, value, **kw): wrapped_view = view settings = kw['registry'].settings permission = kw.get('permission') @@ -322,7 +322,7 @@ def authdebug_view(view, default, **kw): return wrapped_view -def predicated_view(view, default, **kw): +def predicated_view(view, value, **kw): preds = kw.get('predicates', ()) if not preds: return view @@ -341,7 +341,7 @@ def predicated_view(view, default, **kw): predicate_wrapper.__predicates__ = preds return predicate_wrapper -def attr_wrapped_view(view, default, **kw): +def attr_wrapped_view(view, value, **kw): kw = kw accept, order, phash = (kw.get('accept', None), kw.get('order', MAX_ORDER), @@ -364,7 +364,7 @@ def attr_wrapped_view(view, default, **kw): attr_view.__permission__ = kw.get('permission') return attr_view -def rendered_view(view, default, **kw): +def rendered_view(view, value, **kw): # one way or another this wrapper must produce a Response (unless # the renderer is a NullRendererHelper) renderer = kw.get('renderer') @@ -432,7 +432,7 @@ def rendered_view(view, default, **kw): return rendered_view -def decorated_view(view, default, **kw): +def decorated_view(view, value, **kw): decorator = kw.get('decorator') if decorator is None: return view -- cgit v1.2.3 From 628c748efb8c2a3a88c224cbe1abf4ec0eb8e736 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Fri, 16 Oct 2015 00:00:49 -0600 Subject: options has to be a dictionary --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index a09654330..765f9c816 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1193,7 +1193,7 @@ class ViewsConfiguratorMixin(object): return self._derive_view(view, attr=attr, renderer=renderer) # b/w compat - def _derive_view(self, view, permission=None, predicates=(), options=(), + def _derive_view(self, view, permission=None, predicates=(), options=dict(), attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, -- cgit v1.2.3 From e31224c7f7706b960e3c048b987a39a94cbfaa49 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Fri, 16 Oct 2015 00:03:06 -0600 Subject: View derivations get their name as a kwarg as a value Instead of passing in a default to the view derivation, we attempt to pull it out of the options kwarg, if that doesn't exist, then we pass in the default. This should make it simpler to program view derivations because now you don't need to check to see if the kwarg is different from the default. --- pyramid/config/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 765f9c816..ee35ae217 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1024,7 +1024,8 @@ class ViewsConfiguratorMixin(object): derivers = self.registry.queryUtility(IViewDerivers, default=[]) for name, val in inner_derivers + derivers.sorted() + outer_derivers: derivation, default = val - view = wraps_view(derivation)(view, default, **kw) + value = kw['options'].get(name, default) + view = wraps_view(derivation)(view, value, **kw) return view @action_method -- cgit v1.2.3 From 6b089d80cc0ec057a4d560ca3a303a67f3d5a3ef Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Fri, 16 Oct 2015 00:04:16 -0600 Subject: Add tests for the value aspect of view_derivations --- pyramid/tests/test_config/test_derivations.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index f33856e75..277aad662 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1249,6 +1249,32 @@ class TestDerivationIntegration(unittest.TestCase): self.assertEqual(wrapper(None, request), response) self.assertEqual(['test1', 'test2'], response.deriv) + def test_view_options_default_or_not(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, value, **kw): + response.deriv.append(value) + response.deriv.append(kw['options'].get('deriv1', None)) + return view + + def deriv2(view, value, **kw): + response.deriv.append(value) + response.deriv.append(kw['options'].get('deriv2', None)) + return view + + self.config.add_view_derivation('deriv1', deriv1, default=None) + self.config.add_view_derivation('deriv2', deriv2, default='test2') + self.config.add_view(view, deriv1='test1') + self.config.commit() + + wrapper = self._getViewCallable(self.config) + request = self._makeRequest(self.config) + request.method = 'GET' + self.assertEqual(wrapper(None, request), response) + self.assertEqual(['test1', 'test1', 'test2', None], response.deriv) + from zope.interface import implementer from pyramid.interfaces import ( -- cgit v1.2.3 From c15fe7a1298be06fcb25c1377c8ae18f98bce1ff Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 20 Oct 2015 22:43:23 -0600 Subject: Weighs more than, or less than, is really confusing... So we adopt over/under from tweens. Now it becomes simple to think/reason about stuff again =) --- pyramid/config/views.py | 14 +++++++------- pyramid/tests/test_config/test_derivations.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ee35ae217..0d6f3c43f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1077,8 +1077,8 @@ class ViewsConfiguratorMixin(object): @action_method def add_view_derivation(self, name, factory, default, - weighs_more_than=None, - weighs_less_than=None): + under=None, + over=None): factory = self.maybe_dotted(factory) discriminator = ('view option', name) intr = self.introspectable( @@ -1088,16 +1088,16 @@ class ViewsConfiguratorMixin(object): '%s derivation' % type) intr['name'] = name intr['factory'] = factory - intr['weighs_more_than'] = weighs_more_than - intr['weighs_less_than'] = weighs_less_than + intr['under'] = under + intr['over'] = over def register(): derivers = self.registry.queryUtility(IViewDerivers) if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, (factory, default), - after=weighs_more_than, - before=weighs_less_than) + after=under, + before=over) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered early @@ -1112,7 +1112,7 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_derivation(name, deriver, default=None, weighs_more_than=after) + self.add_view_derivation(name, deriver, default=None, under=after) after = name def derive_view(self, view, attr=None, renderer=None): diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 277aad662..94f570fe2 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1191,8 +1191,8 @@ class TestAddDerivation(unittest.TestCase): return view self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default=None, weighs_less_than='deriv1') - self.config.add_view_derivation('deriv3', deriv3, default=None, weighs_more_than='deriv2') + self.config.add_view_derivation('deriv2', deriv2, default=None, over='deriv1') + self.config.add_view_derivation('deriv3', deriv3, default=None, under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) -- cgit v1.2.3 From d11119922ec91da838b81f0c7ae5855adc596b79 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 20 Oct 2015 22:46:54 -0600 Subject: Add tests to verify we order stuff correctly Specifically we want to make sure that rendered_view always comes first, unless a user overrides it by name. --- pyramid/tests/test_config/test_derivations.py | 93 +++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 94f570fe2..710128dca 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1086,6 +1086,98 @@ class TestDeriveView(unittest.TestCase): view, http_cache=(None,)) +class TestDerivationOrder(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_right_order_user_sorted(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_derivation('deriv1', None, default=None) + self.config.add_view_derivation('deriv2', None, default=None, over='deriv1') + self.config.add_view_derivation('deriv3', None, default=None, under='deriv2') + + derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'rendered_view', + 'deriv2', + 'deriv3', + 'deriv1', + 'decorated_view', + 'http_cached_view', + 'owrapped_view', + 'secured_view', + 'authdebug_view', + ], dlist) + + def test_right_order_implicit(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_derivation('deriv1', None, default=None) + self.config.add_view_derivation('deriv2', None, default=None) + self.config.add_view_derivation('deriv3', None, default=None) + + derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'rendered_view', + 'deriv1', + 'deriv2', + 'deriv3', + 'decorated_view', + 'http_cached_view', + 'owrapped_view', + 'secured_view', + 'authdebug_view', + ], dlist) + + def test_right_order_over_rendered_view(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_derivation('deriv1', None, default=None, over='rendered_view') + + derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual(['deriv1', + 'rendered_view', + 'decorated_view', + 'http_cached_view', + 'owrapped_view', + 'secured_view', + 'authdebug_view', + ], dlist) + + + def test_right_order_over_rendered_view_others(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_derivation('deriv1', None, default=None, over='rendered_view') + self.config.add_view_derivation('deriv2', None, default=None) + self.config.add_view_derivation('deriv3', None, default=None) + + derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual(['deriv1', + 'rendered_view', + 'deriv2', + 'deriv3', + 'decorated_view', + 'http_cached_view', + 'owrapped_view', + 'secured_view', + 'authdebug_view', + ], dlist) + + class TestAddDerivation(unittest.TestCase): def setUp(self): @@ -1196,6 +1288,7 @@ class TestAddDerivation(unittest.TestCase): result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + class TestDerivationIntegration(unittest.TestCase): def setUp(self): self.config = testing.setUp() -- cgit v1.2.3 From a3794dc4ae90205f88652971d441c4db2bdd04da Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 20 Oct 2015 22:47:44 -0600 Subject: rendered_view is now part of the pipeline This means that it can be overriden, and is no longer static. With great power comes great responsibility... This also allows the user to insert view derivations over the rendered_view. This would allow the same sort of functionality as the BeforeRenderer event, by allowing the derivation to modify the Response/dictionary that the view returned. --- pyramid/config/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0d6f3c43f..3a278a94f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1015,8 +1015,7 @@ class ViewsConfiguratorMixin(object): def _apply_view_derivations(self, view, **kw): d = pyramid.config.derivations # These inner derivations have fixed order - inner_derivers = [('mapped_view', (d.mapped_view, None)), - ('rendered_view', (d.rendered_view, None))] + inner_derivers = [('mapped_view', (d.mapped_view, None))] outer_derivers = [('predicated_view', (d.predicated_view, None)), ('attr_wrapped_view', (d.attr_wrapped_view, None)),] @@ -1079,6 +1078,9 @@ class ViewsConfiguratorMixin(object): def add_view_derivation(self, name, factory, default, under=None, over=None): + if under is None and over is None: + over = 'decorated_view' + factory = self.maybe_dotted(factory) discriminator = ('view option', name) intr = self.introspectable( @@ -1115,6 +1117,8 @@ class ViewsConfiguratorMixin(object): self.add_view_derivation(name, deriver, default=None, under=after) after = name + self.add_view_derivation('rendered_view', d.rendered_view, default=None, under=pyramid.util.FIRST, over='decorated_view') + def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, -- cgit v1.2.3 From e1ec631ab2d470b1550640d21c28ddd1387f4045 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Tue, 20 Oct 2015 22:55:41 -0600 Subject: Rename default to value as necessary --- pyramid/tests/test_config/test_derivations.py | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 710128dca..7095d25bf 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1192,9 +1192,9 @@ class TestAddDerivation(unittest.TestCase): response.deriv = False view = lambda *arg: response - def deriv(view, default, **kw): + def deriv(view, value, **kw): self.assertFalse(response.deriv) - self.assertEqual(default, None) + self.assertEqual(value, None) response.deriv = True return view @@ -1207,17 +1207,17 @@ class TestAddDerivation(unittest.TestCase): def test_derivation_default(self): response = DummyResponse() - response.deriv_default = None + response.deriv_value = None test_default = object() view = lambda *arg: response - def deriv(view, default, **kw): - response.deriv_default = default + def deriv(view, value, **kw): + response.deriv_value = value return view self.config.add_view_derivation('test_default_deriv', deriv, default=test_default) result = self.config._derive_view(view) - self.assertEqual(response.deriv_default, test_default) + self.assertEqual(response.deriv_value, test_default) def test_override_derivation(self): flags = {} @@ -1226,11 +1226,11 @@ class TestAddDerivation(unittest.TestCase): def __init__(self): self.response = DummyResponse() - def deriv1(view, default, **kw): + def deriv1(view, value, **kw): flags['deriv1'] = True return view - def deriv2(view, default, **kw): + def deriv2(view, value, **kw): flags['deriv2'] = True return view @@ -1249,36 +1249,36 @@ class TestAddDerivation(unittest.TestCase): def test_override_derivation_default(self): response = DummyResponse() - response.deriv_default = None + response.deriv_value = None test_default1 = 'first default' test_default2 = 'second default' view = lambda *arg: response - def deriv(view, default, **kw): - response.deriv_default = default + def deriv(view, value, **kw): + response.deriv_value = value return view self.config.add_view_derivation('test_default_deriv', deriv, default=test_default1) result = self.config._derive_view(view) - self.assertEqual(response.deriv_default, test_default1) + self.assertEqual(response.deriv_value, test_default1) self.config.add_view_derivation('test_default_deriv', deriv, default=test_default2) result = self.config._derive_view(view) - self.assertEqual(response.deriv_default, test_default2) + self.assertEqual(response.deriv_value, test_default2) def test_add_multi_derivations_ordered(self): response = DummyResponse() view = lambda *arg: response response.deriv = [] - def deriv1(view, default, **kw): + def deriv1(view, value, **kw): response.deriv.append('deriv1') return view - def deriv2(view, default, **kw): + def deriv2(view, value, **kw): response.deriv.append('deriv2') return view - def deriv3(view, default, **kw): + def deriv3(view, value, **kw): response.deriv.append('deriv3') return view @@ -1323,11 +1323,11 @@ class TestDerivationIntegration(unittest.TestCase): view = lambda *arg: response response.deriv = [] - def deriv1(view, default, **kw): + def deriv1(view, value, **kw): response.deriv.append(kw['options']['deriv1']) return view - def deriv2(view, default, **kw): + def deriv2(view, value, **kw): response.deriv.append(kw['options']['deriv2']) return view -- cgit v1.2.3 From 0076004e2e2fd5de1e8193d8c66107672267d16c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 4 Mar 2016 01:47:29 -0600 Subject: define deriver api as (view, info) using IViewDeriverInfo - drop the concept of default values from view derivers (we'll later bring this back with add_view_option(name, default=..) - define a new IViewDeriverInfo (mirroring IRendererInfo) which has attributes on it like the predicate list, registry and dictionary of options - define new deriver api as (view, info) --- pyramid/config/derivations.py | 79 +++++++------ pyramid/config/views.py | 161 +++++++++++++++----------- pyramid/interfaces.py | 16 ++- pyramid/tests/test_config/test_derivations.py | 105 ++++------------- 4 files changed, 169 insertions(+), 192 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index c23e5c479..59efeb0f4 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -154,8 +154,8 @@ class DefaultViewMapper(object): def wraps_view(wrapper): - def inner(view, value, **kw): - wrapper_view = wrapper(view, value, **kw) + def inner(view, info): + wrapper_view = wrapper(view, info) return preserve_view_attrs(view, wrapper_view) return inner @@ -193,21 +193,21 @@ def preserve_view_attrs(view, wrapper): return wrapper -def mapped_view(view, value, **kw): - mapper = kw.get('mapper') +def mapped_view(view, info): + mapper = info.options.get('mapper') if mapper is None: mapper = getattr(view, '__view_mapper__', None) if mapper is None: - mapper = kw['registry'].queryUtility(IViewMapperFactory) + mapper = info.registry.queryUtility(IViewMapperFactory) if mapper is None: mapper = DefaultViewMapper - mapped_view = mapper(**kw)(view) + mapped_view = mapper(**info.options)(view) return mapped_view -def owrapped_view(view, value, **kw): - wrapper_viewname = kw.get('wrapper_viewname') - viewname = kw.get('viewname') +def owrapped_view(view, info): + wrapper_viewname = info.options.get('wrapper_viewname') + viewname = info.options.get('viewname') if not wrapper_viewname: return view def _owrapped_view(context, request): @@ -224,11 +224,11 @@ def owrapped_view(view, value, **kw): return wrapped_response return _owrapped_view -def http_cached_view(view, value, **kw): - if kw['registry'].settings.get('prevent_http_cache', False): +def http_cached_view(view, info): + if info.settings.get('prevent_http_cache', False): return view - seconds = kw.get('http_cache') + seconds = info.options.get('http_cache') if seconds is None: return view @@ -253,8 +253,8 @@ def http_cached_view(view, value, **kw): return wrapper -def secured_view(view, value, **kw): - permission = kw.get('permission') +def secured_view(view, info): + permission = info.options.get('permission') if permission == NO_PERMISSION_REQUIRED: # allow views registered within configurations that have a # default permission to explicitly override the default @@ -262,8 +262,8 @@ def secured_view(view, value, **kw): permission = None wrapped_view = view - authn_policy = kw['registry'].queryUtility(IAuthenticationPolicy) - authz_policy = kw['registry'].queryUtility(IAuthorizationPolicy) + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) if authn_policy and authz_policy and (permission is not None): def _permitted(context, request): @@ -285,13 +285,13 @@ def secured_view(view, value, **kw): return wrapped_view -def authdebug_view(view, value, **kw): +def authdebug_view(view, info): wrapped_view = view - settings = kw['registry'].settings - permission = kw.get('permission') - authn_policy = kw['registry'].queryUtility(IAuthenticationPolicy) - authz_policy = kw['registry'].queryUtility(IAuthorizationPolicy) - logger = kw['registry'].queryUtility(IDebugLogger) + settings = info.settings + permission = info.options.get('permission') + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + logger = info.registry.queryUtility(IDebugLogger) if settings and settings.get('debug_authorization', False): def _authdebug_view(context, request): view_name = getattr(request, 'view_name', None) @@ -322,8 +322,8 @@ def authdebug_view(view, value, **kw): return wrapped_view -def predicated_view(view, value, **kw): - preds = kw.get('predicates', ()) +def predicated_view(view, info): + preds = info.predicates if not preds: return view def predicate_wrapper(context, request): @@ -341,11 +341,11 @@ def predicated_view(view, value, **kw): predicate_wrapper.__predicates__ = preds return predicate_wrapper -def attr_wrapped_view(view, value, **kw): - kw = kw - accept, order, phash = (kw.get('accept', None), - kw.get('order', MAX_ORDER), - kw.get('phash', DEFAULT_PHASH)) +def attr_wrapped_view(view, info): + opts = info.options + accept, order, phash = (opts.get('accept', None), + opts.get('order', MAX_ORDER), + opts.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 @@ -360,15 +360,14 @@ def attr_wrapped_view(view, value, **kw): attr_view.__accept__ = accept attr_view.__order__ = order attr_view.__phash__ = phash - attr_view.__view_attr__ = kw.get('attr') - attr_view.__permission__ = kw.get('permission') + attr_view.__view_attr__ = opts.get('attr') + attr_view.__permission__ = opts.get('permission') return attr_view -def rendered_view(view, value, **kw): +def rendered_view(view, info): # one way or another this wrapper must produce a Response (unless # the renderer is a NullRendererHelper) - renderer = kw.get('renderer') - registry = kw['registry'] + renderer = info.options.get('renderer') if renderer is None: # register a default renderer if you want super-dynamic # rendering. registering a default renderer will also allow @@ -379,7 +378,7 @@ def rendered_view(view, value, **kw): if result.__class__ is Response: # common case response = result else: - response = registry.queryAdapterOrSelf(result, IResponse) + response = info.registry.queryAdapterOrSelf(result, IResponse) if response is None: if result is None: append = (' You may have forgotten to return a value ' @@ -410,7 +409,7 @@ def rendered_view(view, value, **kw): else: # this must adapt, it can't do a simple interface check # (avoid trying to render webob responses) - response = registry.queryAdapterOrSelf(result, IResponse) + response = info.registry.queryAdapterOrSelf(result, IResponse) if response is None: attrs = getattr(request, '__dict__', {}) if 'override_renderer' in attrs: @@ -418,8 +417,8 @@ def rendered_view(view, value, **kw): renderer_name = attrs.pop('override_renderer') view_renderer = renderers.RendererHelper( name=renderer_name, - package=kw.get('package'), - registry=registry) + package=info.package, + registry=info.registry) else: view_renderer = renderer.clone() if '__view__' in attrs: @@ -432,8 +431,8 @@ def rendered_view(view, value, **kw): return rendered_view -def decorated_view(view, value, **kw): - decorator = kw.get('decorator') +def decorated_view(view, info): + decorator = info.options.get('decorator') if decorator is None: return view return decorator(view) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3e27f0bfb..890fd2113 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -27,6 +27,7 @@ from pyramid.interfaces import ( IView, IViewClassifier, IViewDerivers, + IViewDeriverInfo, IViewMapperFactory, PHASE1_CONFIG, ) @@ -42,6 +43,8 @@ from pyramid.compat import ( is_nonstr_iter, ) +from pyramid.decorator import reify + from pyramid.exceptions import ( ConfigurationError, PredicateMismatch, @@ -653,12 +656,17 @@ class ViewsConfiguratorMixin(object): Pass a key/value pair here to use a third-party predicate or set a value for a view derivative option registered via :meth:`pyramid.config.Configurator.add_view_predicate` or - :meth:`pyramid.config.Configurator.add_view_derivation`. More than + :meth:`pyramid.config.Configurator.add_view_option`. More than one key/value pair can be used at the same time. See :ref:`view_and_route_predicates` for more information about third-party predicates. - .. versionadded: 1.7 + .. versionadded: 1.4a1 + + .. versionchanged: 1.7 + + Support setting arbitrary view options. Previously, only + predicate values could be supplied. """ if custom_predicates: @@ -726,21 +734,19 @@ class ViewsConfiguratorMixin(object): introspectables = [] ovals = view_options.copy() - ovals.update( - dict( - xhr=xhr, - request_method=request_method, - path_info=path_info, - request_param=request_param, - header=header, - accept=accept, - containment=containment, - request_type=request_type, - match_param=match_param, - check_csrf=check_csrf, - custom=predvalseq(custom_predicates), - ) - ) + ovals.update(dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + containment=containment, + request_type=request_type, + match_param=match_param, + check_csrf=check_csrf, + custom=predvalseq(custom_predicates), + )) def discrim_func(): # We need to defer the discriminator until we know what the phash @@ -749,13 +755,10 @@ class ViewsConfiguratorMixin(object): # called. valid_predicates = predlist.names() pvals = {} - options = {} for (k, v) in ovals.items(): if k in valid_predicates: pvals[k] = v - else: - options[k] = v order, preds, phash = predlist.make(self, **pvals) @@ -763,7 +766,6 @@ class ViewsConfiguratorMixin(object): 'phash': phash, 'order': order, 'predicates': preds, - 'options': options }) return ('view', context, name, route_name, phash) @@ -781,26 +783,25 @@ class ViewsConfiguratorMixin(object): discriminator, view_desc, 'view') - view_intr.update( - dict(name=name, - context=context, - containment=containment, - request_param=request_param, - request_methods=request_method, - route_name=route_name, - attr=attr, - xhr=xhr, - accept=accept, - header=header, - path_info=path_info, - match_param=match_param, - check_csrf=check_csrf, - callable=view, - mapper=mapper, - decorator=decorator, - ) - ) - view_intr.update(**view_options) + view_intr.update(dict( + name=name, + context=context, + containment=containment, + request_param=request_param, + request_methods=request_method, + route_name=route_name, + attr=attr, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + match_param=match_param, + check_csrf=check_csrf, + callable=view, + mapper=mapper, + decorator=decorator, + )) + view_intr.update(view_options) introspectables.append(view_intr) predlist = self.get_predlist('view') @@ -832,14 +833,12 @@ class ViewsConfiguratorMixin(object): # added by discrim_func above during conflict resolving preds = view_intr['predicates'] - opts = view_intr['options'] order = view_intr['order'] phash = view_intr['phash'] # __no_permission_required__ handled by _secure_view - derived_view = self._apply_view_derivations( + derived_view = self._derive_view( view, - registry=self.registry, permission=permission, predicates=preds, attr=attr, @@ -849,12 +848,11 @@ class ViewsConfiguratorMixin(object): accept=accept, order=order, phash=phash, - package=self.package, - mapper=mapper, decorator=decorator, + mapper=mapper, http_cache=http_cache, - options=opts, - ) + extra_options=view_options, + ) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems # that require it for introspection after manual view lookup; @@ -1013,19 +1011,20 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) - def _apply_view_derivations(self, view, **kw): + def _apply_view_derivations(self, info): d = pyramid.config.derivations # These inner derivations have fixed order - inner_derivers = [('mapped_view', (d.mapped_view, None))] + inner_derivers = [('mapped_view', d.mapped_view)] - outer_derivers = [('predicated_view', (d.predicated_view, None)), - ('attr_wrapped_view', (d.attr_wrapped_view, None)),] + outer_derivers = [('predicated_view', d.predicated_view), + ('attr_wrapped_view', d.attr_wrapped_view)] + view = info.orig_view derivers = self.registry.queryUtility(IViewDerivers, default=[]) - for name, val in inner_derivers + derivers.sorted() + outer_derivers: - derivation, default = val - value = kw['options'].get(name, default) - view = wraps_view(derivation)(view, value, **kw) + for name, derivation in ( + inner_derivers + derivers.sorted() + outer_derivers + ): + view = wraps_view(derivation)(view, info) return view @action_method @@ -1076,7 +1075,9 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_derivation(self, name, factory, default, + def add_view_derivation(self, + name, + factory, under=None, over=None): if under is None and over is None: @@ -1098,9 +1099,7 @@ class ViewsConfiguratorMixin(object): if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) - derivers.add(name, (factory, default), - after=under, - before=over) + derivers.add(name, factory, after=under, before=over) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered early @@ -1115,10 +1114,15 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_derivation(name, deriver, default=None, under=after) + self.add_view_derivation(name, deriver, under=after) after = name - self.add_view_derivation('rendered_view', d.rendered_view, default=None, under=pyramid.util.FIRST, over='decorated_view') + self.add_view_derivation( + 'rendered_view', + d.rendered_view, + under=pyramid.util.FIRST, + over='decorated_view', + ) def derive_view(self, view, attr=None, renderer=None): """ @@ -1199,11 +1203,11 @@ class ViewsConfiguratorMixin(object): return self._derive_view(view, attr=attr, renderer=renderer) # b/w compat - def _derive_view(self, view, permission=None, predicates=(), options=dict(), + 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, decorator=None, - mapper=None, http_cache=None): + mapper=None, http_cache=None, extra_options=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, string_types): @@ -1218,11 +1222,8 @@ class ViewsConfiguratorMixin(object): package=self.package, registry=self.registry) - return self._apply_view_derivations( - view, - registry=self.registry, + options = dict( permission=permission, - predicates=predicates, attr=attr, renderer=renderer, wrapper_viewname=wrapper_viewname, @@ -1230,13 +1231,23 @@ class ViewsConfiguratorMixin(object): accept=accept, order=order, phash=phash, - package=self.package, mapper=mapper, decorator=decorator, http_cache=http_cache, + ) + if extra_options: + options.update(extra_options) + + info = ViewDeriverInfo( + view=view, + registry=self.registry, + package=self.package, + predicates=predicates, options=options, ) + return self._apply_view_derivations(info) + @viewdefaults @action_method def add_forbidden_view( @@ -1624,6 +1635,18 @@ def isexception(o): (inspect.isclass(o) and (issubclass(o, Exception))) ) +@implementer(IViewDeriverInfo) +class ViewDeriverInfo(object): + def __init__(self, view, registry, package, predicates, options): + self.orig_view = view + self.registry = registry + self.package = package + self.predicates = predicates or [] + self.options = options or {} + + @reify + def settings(self): + return self.registry.settings @implementer(IStaticURLInfo) class StaticURLInfo(object): diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 4c3e87f39..5ae7cfbf8 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1189,7 +1189,21 @@ class IPredicateList(Interface): class IViewDerivers(Interface): """ Interface for view derivers list """ - + +class IViewDeriverInfo(Interface): + """ An object implementing this interface is passed to every + :term:`view deriver` during configuration.""" + registry = Attribute('The "current" application registry when the ' + 'view was created') + package = Attribute('The "current package" when the view ' + 'configuration statement was found') + settings = Attribute('The deployment settings dictionary related ' + 'to the current application') + options = Attribute('The view options passed to the view, including any ' + 'default values that were not overriden') + predicates = Attribute('The list of predicates active on the view') + orig_view = Attribute('The original view object being wrapped') + class ICacheBuster(Interface): """ A cache buster modifies the URL generation machinery for diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 7095d25bf..1c7a0be07 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1097,9 +1097,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_user_sorted(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None) - self.config.add_view_derivation('deriv2', None, default=None, over='deriv1') - self.config.add_view_derivation('deriv3', None, default=None, under='deriv2') + self.config.add_view_derivation('deriv1', None) + self.config.add_view_derivation('deriv2', None, over='deriv1') + self.config.add_view_derivation('deriv3', None, under='deriv2') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1119,9 +1119,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_implicit(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None) - self.config.add_view_derivation('deriv2', None, default=None) - self.config.add_view_derivation('deriv3', None, default=None) + self.config.add_view_derivation('deriv1', None) + self.config.add_view_derivation('deriv2', None) + self.config.add_view_derivation('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1141,7 +1141,7 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None, over='rendered_view') + self.config.add_view_derivation('deriv1', None, over='rendered_view') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1159,9 +1159,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, default=None, over='rendered_view') - self.config.add_view_derivation('deriv2', None, default=None) - self.config.add_view_derivation('deriv3', None, default=None) + self.config.add_view_derivation('deriv1', None, over='rendered_view') + self.config.add_view_derivation('deriv2', None) + self.config.add_view_derivation('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1192,33 +1192,18 @@ class TestAddDerivation(unittest.TestCase): response.deriv = False view = lambda *arg: response - def deriv(view, value, **kw): + def deriv(view, info): self.assertFalse(response.deriv) - self.assertEqual(value, None) response.deriv = True return view result = self.config._derive_view(view) self.assertFalse(response.deriv) - self.config.add_view_derivation('test_deriv', deriv, default=None) + self.config.add_view_derivation('test_deriv', deriv) result = self.config._derive_view(view) self.assertTrue(response.deriv) - def test_derivation_default(self): - response = DummyResponse() - response.deriv_value = None - test_default = object() - view = lambda *arg: response - - def deriv(view, value, **kw): - response.deriv_value = value - return view - - self.config.add_view_derivation('test_default_deriv', deriv, default=test_default) - result = self.config._derive_view(view) - self.assertEqual(response.deriv_value, test_default) - def test_override_derivation(self): flags = {} @@ -1235,36 +1220,18 @@ class TestAddDerivation(unittest.TestCase): return view view1 = AView() - self.config.add_view_derivation('test_deriv', deriv1, default=None) + self.config.add_view_derivation('test_deriv', deriv1) result = self.config._derive_view(view1) self.assertTrue(flags.get('deriv1')) self.assertFalse(flags.get('deriv2')) flags.clear() view2 = AView() - self.config.add_view_derivation('test_deriv', deriv2, default=None) + self.config.add_view_derivation('test_deriv', deriv2) result = self.config._derive_view(view2) self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) - def test_override_derivation_default(self): - response = DummyResponse() - response.deriv_value = None - test_default1 = 'first default' - test_default2 = 'second default' - view = lambda *arg: response - - def deriv(view, value, **kw): - response.deriv_value = value - return view - - self.config.add_view_derivation('test_default_deriv', deriv, default=test_default1) - result = self.config._derive_view(view) - self.assertEqual(response.deriv_value, test_default1) - self.config.add_view_derivation('test_default_deriv', deriv, default=test_default2) - result = self.config._derive_view(view) - self.assertEqual(response.deriv_value, test_default2) - def test_add_multi_derivations_ordered(self): response = DummyResponse() view = lambda *arg: response @@ -1282,9 +1249,9 @@ class TestAddDerivation(unittest.TestCase): response.deriv.append('deriv3') return view - self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default=None, over='deriv1') - self.config.add_view_derivation('deriv3', deriv3, default=None, under='deriv2') + self.config.add_view_derivation('deriv1', deriv1) + self.config.add_view_derivation('deriv2', deriv2, over='deriv1') + self.config.add_view_derivation('deriv3', deriv3, under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) @@ -1323,16 +1290,16 @@ class TestDerivationIntegration(unittest.TestCase): view = lambda *arg: response response.deriv = [] - def deriv1(view, value, **kw): - response.deriv.append(kw['options']['deriv1']) + def deriv1(view, info): + response.deriv.append(info.options['deriv1']) return view - def deriv2(view, value, **kw): - response.deriv.append(kw['options']['deriv2']) + def deriv2(view, info): + response.deriv.append(info.options['deriv2']) return view - self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default=None) + self.config.add_view_derivation('deriv1', deriv1) + self.config.add_view_derivation('deriv2', deriv2) self.config.add_view(view, deriv1='test1', deriv2='test2') self.config.commit() @@ -1342,32 +1309,6 @@ class TestDerivationIntegration(unittest.TestCase): self.assertEqual(wrapper(None, request), response) self.assertEqual(['test1', 'test2'], response.deriv) - def test_view_options_default_or_not(self): - response = DummyResponse() - view = lambda *arg: response - response.deriv = [] - - def deriv1(view, value, **kw): - response.deriv.append(value) - response.deriv.append(kw['options'].get('deriv1', None)) - return view - - def deriv2(view, value, **kw): - response.deriv.append(value) - response.deriv.append(kw['options'].get('deriv2', None)) - return view - - self.config.add_view_derivation('deriv1', deriv1, default=None) - self.config.add_view_derivation('deriv2', deriv2, default='test2') - self.config.add_view(view, deriv1='test1') - self.config.commit() - - wrapper = self._getViewCallable(self.config) - request = self._makeRequest(self.config) - request.method = 'GET' - self.assertEqual(wrapper(None, request), response) - self.assertEqual(['test1', 'test1', 'test2', None], response.deriv) - from zope.interface import implementer from pyramid.interfaces import ( -- cgit v1.2.3 From e292cca8a2f07090697435b7fcdfb2d827e88f44 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Mar 2016 23:45:12 -0600 Subject: rename wrapper_viewname and viewname to wrapper and name in deriver options --- pyramid/config/derivations.py | 4 ++-- pyramid/config/views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 59efeb0f4..e6179d35f 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -206,8 +206,8 @@ def mapped_view(view, info): return mapped_view def owrapped_view(view, info): - wrapper_viewname = info.options.get('wrapper_viewname') - viewname = info.options.get('viewname') + wrapper_viewname = info.options.get('wrapper') + viewname = info.options.get('name') if not wrapper_viewname: return view def _owrapped_view(context, request): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 890fd2113..0ffcc81b0 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1226,8 +1226,8 @@ class ViewsConfiguratorMixin(object): permission=permission, attr=attr, renderer=renderer, - wrapper_viewname=wrapper_viewname, - viewname=viewname, + wrapper=wrapper_viewname, + name=viewname, accept=accept, order=order, phash=phash, -- cgit v1.2.3 From dad9505d95824ce65e557d9e29044291ccc3a904 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Mar 2016 23:46:04 -0600 Subject: remove order and phash from deriver options the order and phash are not technically options and are only passed to make the predicated deriver work which is only a deriver as an implementation detail --- pyramid/config/derivations.py | 11 +++++------ pyramid/config/views.py | 7 +++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index e6179d35f..0e109f425 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -342,10 +342,9 @@ def predicated_view(view, info): return predicate_wrapper def attr_wrapped_view(view, info): - opts = info.options - accept, order, phash = (opts.get('accept', None), - opts.get('order', MAX_ORDER), - opts.get('phash', DEFAULT_PHASH)) + accept, order, phash = (info.options.get('accept', None), + getattr(info, 'order', MAX_ORDER), + getattr(info, '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 @@ -360,8 +359,8 @@ def attr_wrapped_view(view, info): attr_view.__accept__ = accept attr_view.__order__ = order attr_view.__phash__ = phash - attr_view.__view_attr__ = opts.get('attr') - attr_view.__permission__ = opts.get('permission') + attr_view.__view_attr__ = info.options.get('attr') + attr_view.__permission__ = info.options.get('permission') return attr_view def rendered_view(view, info): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 0ffcc81b0..fbf91f3ae 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1229,8 +1229,6 @@ class ViewsConfiguratorMixin(object): wrapper=wrapper_viewname, name=viewname, accept=accept, - order=order, - phash=phash, mapper=mapper, decorator=decorator, http_cache=http_cache, @@ -1246,6 +1244,11 @@ class ViewsConfiguratorMixin(object): options=options, ) + # order and phash are only necessary for the predicated view and + # are not really view derivation options + info.order = order + info.phash = phash + return self._apply_view_derivations(info) @viewdefaults -- cgit v1.2.3 From a59312683873a27cf7f8cf9961002765f66cde1f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Mar 2016 23:47:09 -0600 Subject: add new deriver option ``context`` --- pyramid/config/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index fbf91f3ae..4a5408ef7 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -842,6 +842,7 @@ class ViewsConfiguratorMixin(object): permission=permission, predicates=preds, attr=attr, + context=context, renderer=renderer, wrapper_viewname=wrapper, viewname=name, @@ -1207,7 +1208,8 @@ class ViewsConfiguratorMixin(object): attr=None, renderer=None, wrapper_viewname=None, viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH, decorator=None, - mapper=None, http_cache=None, extra_options=None): + mapper=None, http_cache=None, context=None, + extra_options=None): view = self.maybe_dotted(view) mapper = self.maybe_dotted(mapper) if isinstance(renderer, string_types): @@ -1223,6 +1225,7 @@ class ViewsConfiguratorMixin(object): registry=self.registry) options = dict( + context=context, permission=permission, attr=attr, renderer=renderer, -- cgit v1.2.3 From a610d0e52d98fcbeb0bacf6eebc61dbaba2d1cf9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 8 Mar 2016 23:58:22 -0600 Subject: rename info.orig_view to info.original_view --- pyramid/config/views.py | 5 +++-- pyramid/interfaces.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 4a5408ef7..ac95eaa59 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1020,7 +1020,7 @@ class ViewsConfiguratorMixin(object): outer_derivers = [('predicated_view', d.predicated_view), ('attr_wrapped_view', d.attr_wrapped_view)] - view = info.orig_view + view = info.original_view derivers = self.registry.queryUtility(IViewDerivers, default=[]) for name, derivation in ( inner_derivers + derivers.sorted() + outer_derivers @@ -1225,6 +1225,7 @@ class ViewsConfiguratorMixin(object): registry=self.registry) options = dict( + view=view, context=context, permission=permission, attr=attr, @@ -1644,7 +1645,7 @@ def isexception(o): @implementer(IViewDeriverInfo) class ViewDeriverInfo(object): def __init__(self, view, registry, package, predicates, options): - self.orig_view = view + self.original_view = view self.registry = registry self.package = package self.predicates = predicates or [] diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 5ae7cfbf8..6791befce 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1202,7 +1202,7 @@ class IViewDeriverInfo(Interface): options = Attribute('The view options passed to the view, including any ' 'default values that were not overriden') predicates = Attribute('The list of predicates active on the view') - orig_view = Attribute('The original view object being wrapped') + original_view = Attribute('The original view object being wrapped') class ICacheBuster(Interface): """ -- cgit v1.2.3 From ceb1f29815d15537a66bdab67b0deca4206fa4b8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Mar 2016 00:03:05 -0600 Subject: rename view derivations to view derivers --- pyramid/config/__init__.py | 2 +- pyramid/config/views.py | 34 +++++++++----------- pyramid/testing.py | 2 +- pyramid/tests/test_config/test_derivations.py | 46 +++++++++++++-------------- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 621796552..553f32c9b 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -378,7 +378,7 @@ class Configurator( self.add_default_response_adapters() self.add_default_renderers() self.add_default_view_predicates() - self.add_default_view_derivations() + self.add_default_view_derivers() self.add_default_route_predicates() if exceptionresponse_view is not None: diff --git a/pyramid/config/views.py b/pyramid/config/views.py index ac95eaa59..918437f03 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1012,7 +1012,7 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) - def _apply_view_derivations(self, info): + def _apply_view_derivers(self, info): d = pyramid.config.derivations # These inner derivations have fixed order inner_derivers = [('mapped_view', d.mapped_view)] @@ -1076,23 +1076,19 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_derivation(self, - name, - factory, - under=None, - over=None): + def add_view_deriver(self, name, deriver, under=None, over=None): if under is None and over is None: over = 'decorated_view' - factory = self.maybe_dotted(factory) - discriminator = ('view option', name) + deriver = self.maybe_dotted(deriver) + discriminator = ('view deriver', name) intr = self.introspectable( - '%s derivation' % type, - discriminator, - '%s derivation named %s' % (type, name), - '%s derivation' % type) + 'view derivers', + name, + name, + 'view deriver') intr['name'] = name - intr['factory'] = factory + intr['deriver'] = deriver intr['under'] = under intr['over'] = over def register(): @@ -1100,11 +1096,11 @@ class ViewsConfiguratorMixin(object): if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) - derivers.add(name, factory, after=under, before=over) + derivers.add(name, deriver, after=under, before=over) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered early - def add_default_view_derivations(self): + def add_default_view_derivers(self): d = pyramid.config.derivations derivers = [ ('decorated_view', d.decorated_view), @@ -1115,10 +1111,10 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_derivation(name, deriver, under=after) + self.add_view_deriver(name, deriver, under=after) after = name - self.add_view_derivation( + self.add_view_deriver( 'rendered_view', d.rendered_view, under=pyramid.util.FIRST, @@ -1249,11 +1245,11 @@ class ViewsConfiguratorMixin(object): ) # order and phash are only necessary for the predicated view and - # are not really view derivation options + # are not really view deriver options info.order = order info.phash = phash - return self._apply_view_derivations(info) + return self._apply_view_derivers(info) @viewdefaults @action_method diff --git a/pyramid/testing.py b/pyramid/testing.py index f1be04eec..c25105c6c 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -474,8 +474,8 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True, # method. config.add_default_renderers() config.add_default_view_predicates() + config.add_default_view_derivers() config.add_default_route_predicates() - config.add_default_view_derivations() config.commit() global have_zca try: diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 1c7a0be07..9eb8598e4 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1097,9 +1097,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_user_sorted(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None) - self.config.add_view_derivation('deriv2', None, over='deriv1') - self.config.add_view_derivation('deriv3', None, under='deriv2') + self.config.add_view_deriver('deriv1', None) + self.config.add_view_deriver('deriv2', None, over='deriv1') + self.config.add_view_deriver('deriv3', None, under='deriv2') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1119,9 +1119,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_implicit(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None) - self.config.add_view_derivation('deriv2', None) - self.config.add_view_derivation('deriv3', None) + self.config.add_view_deriver('deriv1', None) + self.config.add_view_deriver('deriv2', None) + self.config.add_view_deriver('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1141,7 +1141,7 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, over='rendered_view') + self.config.add_view_deriver('deriv1', None, over='rendered_view') derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1159,9 +1159,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_derivation('deriv1', None, over='rendered_view') - self.config.add_view_derivation('deriv2', None) - self.config.add_view_derivation('deriv3', None) + self.config.add_view_deriver('deriv1', None, over='rendered_view') + self.config.add_view_deriver('deriv2', None) + self.config.add_view_deriver('deriv3', None) derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) derivers_sorted = derivers.sorted() @@ -1178,7 +1178,7 @@ class TestDerivationOrder(unittest.TestCase): ], dlist) -class TestAddDerivation(unittest.TestCase): +class TestAddDeriver(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -1187,7 +1187,7 @@ class TestAddDerivation(unittest.TestCase): self.config = None testing.tearDown() - def test_add_single_derivation(self): + def test_add_single_deriver(self): response = DummyResponse() response.deriv = False view = lambda *arg: response @@ -1199,12 +1199,12 @@ class TestAddDerivation(unittest.TestCase): result = self.config._derive_view(view) self.assertFalse(response.deriv) - self.config.add_view_derivation('test_deriv', deriv) + self.config.add_view_deriver('test_deriv', deriv) result = self.config._derive_view(view) self.assertTrue(response.deriv) - def test_override_derivation(self): + def test_override_deriver(self): flags = {} class AView: @@ -1220,19 +1220,19 @@ class TestAddDerivation(unittest.TestCase): return view view1 = AView() - self.config.add_view_derivation('test_deriv', deriv1) + self.config.add_view_deriver('test_deriv', deriv1) result = self.config._derive_view(view1) self.assertTrue(flags.get('deriv1')) self.assertFalse(flags.get('deriv2')) flags.clear() view2 = AView() - self.config.add_view_derivation('test_deriv', deriv2) + self.config.add_view_deriver('test_deriv', deriv2) result = self.config._derive_view(view2) self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) - def test_add_multi_derivations_ordered(self): + def test_add_multi_derivers_ordered(self): response = DummyResponse() view = lambda *arg: response response.deriv = [] @@ -1249,14 +1249,14 @@ class TestAddDerivation(unittest.TestCase): response.deriv.append('deriv3') return view - self.config.add_view_derivation('deriv1', deriv1) - self.config.add_view_derivation('deriv2', deriv2, over='deriv1') - self.config.add_view_derivation('deriv3', deriv3, under='deriv2') + self.config.add_view_deriver('deriv1', deriv1) + self.config.add_view_deriver('deriv2', deriv2, over='deriv1') + self.config.add_view_deriver('deriv3', deriv3, under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) -class TestDerivationIntegration(unittest.TestCase): +class TestDeriverIntegration(unittest.TestCase): def setUp(self): self.config = testing.setUp() @@ -1298,8 +1298,8 @@ class TestDerivationIntegration(unittest.TestCase): response.deriv.append(info.options['deriv2']) return view - self.config.add_view_derivation('deriv1', deriv1) - self.config.add_view_derivation('deriv2', deriv2) + self.config.add_view_deriver('deriv1', deriv1) + self.config.add_view_deriver('deriv2', deriv2) self.config.add_view(view, deriv1='test1', deriv2='test2') self.config.commit() -- cgit v1.2.3 From ff8c19258b05ed25824ae4a6ae90e62762df8c74 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Mar 2016 00:09:31 -0600 Subject: pass the predicate options to the derivers --- pyramid/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 918437f03..8b4a89388 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -852,7 +852,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, mapper=mapper, http_cache=http_cache, - extra_options=view_options, + extra_options=ovals, ) derived_view.__discriminator__ = lambda *arg: discriminator # __discriminator__ is used by superdynamic systems -- cgit v1.2.3 From 46fd86919578221b339a18de90e926ef12e764d3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Mar 2016 00:17:01 -0600 Subject: expect derivers it was a bug to try ``[].sorted()`` if no derivers, so just expect some for now as the fallback was unused --- pyramid/config/views.py | 8 ++++---- pyramid/tests/test_config/test_derivations.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 8b4a89388..6511ecd1e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -751,7 +751,7 @@ class ViewsConfiguratorMixin(object): def discrim_func(): # We need to defer the discriminator until we know what the phash # is. It can't be computed any sooner because thirdparty - # predicates/view derivations may not yet exist when add_view is + # predicates/view derivers may not yet exist when add_view is # called. valid_predicates = predlist.names() pvals = {} @@ -1021,11 +1021,11 @@ class ViewsConfiguratorMixin(object): ('attr_wrapped_view', d.attr_wrapped_view)] view = info.original_view - derivers = self.registry.queryUtility(IViewDerivers, default=[]) - for name, derivation in ( + derivers = self.registry.getUtility(IViewDerivers) + for name, deriver in ( inner_derivers + derivers.sorted() + outer_derivers ): - view = wraps_view(derivation)(view, info) + view = wraps_view(deriver)(view, info) return view @action_method diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 9eb8598e4..ec7ef1434 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1101,7 +1101,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv2', None, over='deriv1') self.config.add_view_deriver('deriv3', None, under='deriv2') - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ @@ -1123,7 +1123,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv2', None) self.config.add_view_deriver('deriv3', None) - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ @@ -1143,7 +1143,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv1', None, over='rendered_view') - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual(['deriv1', @@ -1163,7 +1163,7 @@ class TestDerivationOrder(unittest.TestCase): self.config.add_view_deriver('deriv2', None) self.config.add_view_deriver('deriv3', None) - derivers = self.config.registry.queryUtility(IViewDerivers, default=[]) + derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual(['deriv1', -- cgit v1.2.3 From e4b931a67455f14d84d415be49e6596d06cc0a42 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Mar 2016 00:46:59 -0600 Subject: add options support to view derivers exposed a new IViewDeriver api with an optional ``options`` list to expose support for new kwargs that may be passed to config.add_view --- docs/api/interfaces.rst | 6 ++++++ pyramid/config/derivations.py | 16 ++++++++++++++++ pyramid/config/views.py | 15 +++++++++++++++ pyramid/interfaces.py | 20 ++++++++++++++++++-- pyramid/tests/test_config/test_derivations.py | 11 ++++++++++- pyramid/util.py | 3 +++ 6 files changed, 68 insertions(+), 3 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index de2a664a4..635d3c5b6 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -91,3 +91,9 @@ Other Interfaces .. autointerface:: ICacheBuster :members: + + .. autointerface:: IViewDeriver + :members: + + .. autointerface:: IViewDeriverInfo + :members: diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py index 0e109f425..99baf46f9 100644 --- a/pyramid/config/derivations.py +++ b/pyramid/config/derivations.py @@ -205,6 +205,8 @@ def mapped_view(view, info): mapped_view = mapper(**info.options)(view) return mapped_view +mapped_view.options = ('mapper', 'attr') + def owrapped_view(view, info): wrapper_viewname = info.options.get('wrapper') viewname = info.options.get('name') @@ -224,6 +226,8 @@ def owrapped_view(view, info): return wrapped_response return _owrapped_view +owrapped_view.options = ('name', 'wrapper') + def http_cached_view(view, info): if info.settings.get('prevent_http_cache', False): return view @@ -253,6 +257,8 @@ def http_cached_view(view, info): return wrapper +http_cached_view.options = ('http_cache',) + def secured_view(view, info): permission = info.options.get('permission') if permission == NO_PERMISSION_REQUIRED: @@ -285,6 +291,8 @@ def secured_view(view, info): return wrapped_view +secured_view.options = ('permission',) + def authdebug_view(view, info): wrapped_view = view settings = info.settings @@ -322,6 +330,8 @@ def authdebug_view(view, info): return wrapped_view +authdebug_view.options = ('permission',) + def predicated_view(view, info): preds = info.predicates if not preds: @@ -363,6 +373,8 @@ def attr_wrapped_view(view, info): attr_view.__permission__ = info.options.get('permission') return attr_view +attr_wrapped_view.options = ('accept', 'attr', 'permission') + def rendered_view(view, info): # one way or another this wrapper must produce a Response (unless # the renderer is a NullRendererHelper) @@ -430,8 +442,12 @@ def rendered_view(view, info): return rendered_view +rendered_view.options = ('renderer',) + def decorated_view(view, info): decorator = info.options.get('decorator') if decorator is None: return view return decorator(view) + +decorated_view.options = ('decorator',) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6511ecd1e..c3e5d360e 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -755,10 +755,15 @@ class ViewsConfiguratorMixin(object): # called. valid_predicates = predlist.names() pvals = {} + dvals = {} for (k, v) in ovals.items(): if k in valid_predicates: pvals[k] = v + else: + dvals[k] = v + + self._check_view_options(**dvals) order, preds, phash = predlist.make(self, **pvals) @@ -1012,6 +1017,16 @@ class ViewsConfiguratorMixin(object): introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) + def _check_view_options(self, **kw): + # we only need to validate deriver options because the predicates + # were checked by the predlist + derivers = self.registry.getUtility(IViewDerivers) + for deriver in derivers.values(): + for opt in getattr(deriver, 'options', []): + kw.pop(opt, None) + if kw: + raise ConfigurationError('Unknown view options: %s' % (kw,)) + def _apply_view_derivers(self, info): d = pyramid.config.derivations # These inner derivations have fixed order diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 6791befce..692bf3d6d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1187,8 +1187,21 @@ class IJSONAdapter(Interface): class IPredicateList(Interface): """ Interface representing a predicate list """ -class IViewDerivers(Interface): - """ Interface for view derivers list """ +class IViewDeriver(Interface): + options = Attribute('An list of supported options to be passed to ' + ':meth:`pyramid.config.Configurator.add_view`. ' + 'This attribute is optional.') + + def __call__(view, info): + """ + Derive a new view from the supplied view. + + View options, package information and registry are available on + ``info``, an instance of :class:`pyramid.interfaces.IViewDeriverInfo`. + + The ``view`` is a callable accepting ``(context, request)``. + + """ class IViewDeriverInfo(Interface): """ An object implementing this interface is passed to every @@ -1204,6 +1217,9 @@ class IViewDeriverInfo(Interface): predicates = Attribute('The list of predicates active on the view') original_view = Attribute('The original view object being wrapped') +class IViewDerivers(Interface): + """ Interface for view derivers list """ + class ICacheBuster(Interface): """ A cache buster modifies the URL generation machinery for diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index ec7ef1434..12fcc300a 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1293,15 +1293,16 @@ class TestDeriverIntegration(unittest.TestCase): def deriv1(view, info): response.deriv.append(info.options['deriv1']) return view + deriv1.options = ('deriv1',) def deriv2(view, info): response.deriv.append(info.options['deriv2']) return view + deriv2.options = ('deriv2',) self.config.add_view_deriver('deriv1', deriv1) self.config.add_view_deriver('deriv2', deriv2) self.config.add_view(view, deriv1='test1', deriv2='test2') - self.config.commit() wrapper = self._getViewCallable(self.config) request = self._makeRequest(self.config) @@ -1309,6 +1310,14 @@ class TestDeriverIntegration(unittest.TestCase): self.assertEqual(wrapper(None, request), response) self.assertEqual(['test1', 'test2'], response.deriv) + def test_unexpected_view_options(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + self.config.add_view_deriver('deriv1', deriv1) + self.assertRaises( + ConfigurationError, + lambda: self.config.add_view(lambda r: {}, deriv1='test1')) + from zope.interface import implementer from pyramid.interfaces import ( diff --git a/pyramid/util.py b/pyramid/util.py index 0a73cedaf..27140936c 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -380,6 +380,9 @@ class TopologicalSorter(object): self.first = first self.last = last + def values(self): + return self.name2val.values() + def remove(self, name): """ Remove a node from the sort input """ self.names.remove(name) -- cgit v1.2.3 From b4147ba4b34b13eaf32f064e9827da733ff6ab7c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 9 Mar 2016 01:01:58 -0600 Subject: add "view deriver" to the glossary --- docs/glossary.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/glossary.rst b/docs/glossary.rst index bbc86db41..9e068a18c 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1092,3 +1092,8 @@ Glossary A technique used when serving a cacheable static asset in order to force a client to query the new version of the asset. See :ref:`cache_busting` for more information. + + view deriver + A view deriver is a composable component of the view pipeline which is + used to create a :term:`view callable`. A view deriver is a callable + implementing the :class:`pyramid.interfaces.IViewDeriver` interface. -- cgit v1.2.3 From 05b2b50706cd303c28233a321ef3dcc2e45486be Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sun, 13 Mar 2016 12:32:03 -0700 Subject: minor update to request processing diagram --- docs/_static/pyramid_request_processing.graffle | 60 ++++++++++++------------ docs/_static/pyramid_request_processing.png | Bin 122854 -> 123953 bytes docs/_static/pyramid_request_processing.svg | 2 +- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/_static/pyramid_request_processing.graffle b/docs/_static/pyramid_request_processing.graffle index 71319610b..16b360543 100644 --- a/docs/_static/pyramid_request_processing.graffle +++ b/docs/_static/pyramid_request_processing.graffle @@ -53,7 +53,7 @@ Creator Steve Piercy DisplayScale - 1 0/72 in = 1 0/72 in + 1 0/72 in = 1.0000 in GraphDocumentVersion 8 GraphicsList @@ -84,8 +84,8 @@ 0 Points - {344.41667175292969, 402.88506673894034} - {375.5, 402.27232108797347} + {344.41668319702148, 402.88506673894034} + {375.5, 402.77232108797347} Style @@ -113,7 +113,7 @@ Tail ID - 169428 + 169509 @@ -243,11 +243,11 @@ Bounds - {{238.8333613077798, 284.99999999999994}, {105.66668701171875, 18.656048080136394}} + {{238.74999618530273, 284.99999999999994}, {105.75002924601222, 18.656048080136394}} Class ShapedGraphic ID - 169425 + 169506 Magnets {0, 1} @@ -296,11 +296,11 @@ Bounds - {{238.75000762939453, 412.15071036499205}, {105.66666412353516, 18.656048080136394}} + {{238.74999618530273, 412.15071036499205}, {105.66668701171875, 18.656048080136394}} Class ShapedGraphic ID - 169426 + 169507 Magnets {0, 1} @@ -349,11 +349,11 @@ Bounds - {{238.75000762939453, 303.65604172230951}, {105.66666412353516, 18.656048080136394}} + {{238.74999618530273, 303.65604172230951}, {105.66668701171875, 18.656048080136394}} Class ShapedGraphic ID - 169427 + 169508 Magnets {0, 1} @@ -402,11 +402,11 @@ Bounds - {{238.75000762939453, 393.55704269887212}, {105.66666412353516, 18.656048080136394}} + {{238.74999618530273, 393.55704269887212}, {105.66668701171875, 18.656048080136394}} Class ShapedGraphic ID - 169428 + 169509 Magnets {0, 1} @@ -453,11 +453,11 @@ Bounds - {{238.75000762939453, 374.90099016834085}, {105.66666412353516, 18.656048080136394}} + {{238.74999618530273, 374.90099016834085}, {105.66668701171875, 18.656048080136394}} Class ShapedGraphic ID - 169429 + 169510 Magnets {0, 1} @@ -504,11 +504,11 @@ Bounds - {{238.75000762939453, 341.36561209044055}, {105.66666412353516, 33.089282989501953}} + {{238.74999618530273, 341.36561209044055}, {105.66668701171875, 33.089282989501953}} Class ShapedGraphic ID - 169430 + 169511 Magnets {0, 1} @@ -555,11 +555,11 @@ Bounds - {{238.75000762939453, 322.26348241170439}, {105.66666412353516, 18.656048080136394}} + {{238.74999618530273, 322.26348241170439}, {105.66668701171875, 18.656048080136394}} Class ShapedGraphic ID - 169431 + 169512 Magnets {0, 1} @@ -606,7 +606,7 @@ ID - 169424 + 169505 Layer 0 @@ -1094,7 +1094,7 @@ 0 Points - {238.75000762939462, 430.80675844512831} + {238.74999618530282, 430.80675844512831} {207.66666666666765, 385.656005859375} Style @@ -1123,7 +1123,7 @@ Tail ID - 169426 + 169507 Info 6 @@ -1144,7 +1144,7 @@ 0 Points - {239.33336141608385, 285.57837549845181} + {239.25039065750093, 285.57837549845181} {207.66666666666777, 353.07514659563753} Style @@ -1173,7 +1173,7 @@ Tail ID - 169425 + 169506 Info 6 @@ -1515,7 +1515,7 @@ {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc -\f0\fs20 \cf0 view} +\f0\fs20 \cf0 view deriver} VerticalPad 0 @@ -1742,7 +1742,7 @@ Bounds - {{375.5, 391}, {105.66666412353516, 22.544642175946908}} + {{375.5, 391.5}, {105.66666412353516, 22.544642175946908}} Class ShapedGraphic ID @@ -9637,7 +9637,7 @@ MasterSheets ModificationDate - 2014-11-23 07:19:11 +0000 + 2016-03-13 08:04:48 +0000 Modifier Steve Piercy NotesVisible @@ -9732,15 +9732,15 @@ SidebarWidth 163 VisibleRegion - {{-231, -226}, {1037, 1186}} + {{152.25, 226.5}, {255.75, 292.75}} Zoom - 1 + 4 ZoomValues Request Processing - 1 - 2 + 4 + 8 diff --git a/docs/_static/pyramid_request_processing.png b/docs/_static/pyramid_request_processing.png index 2fbb1e164..c684255fa 100644 Binary files a/docs/_static/pyramid_request_processing.png and b/docs/_static/pyramid_request_processing.png differ diff --git a/docs/_static/pyramid_request_processing.svg b/docs/_static/pyramid_request_processing.svg index 21bbcb532..d32d5c5bc 100644 --- a/docs/_static/pyramid_request_processing.svg +++ b/docs/_static/pyramid_request_processing.svg @@ -1,3 +1,3 @@ -2014-11-23 07:19ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackviewexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization +2016-03-13 08:04ZRequest Processingno exceptionsmiddleware ingress tween ingresstraversalContextFoundtween egressresponse callbacksfinished callbacksmiddleware egressBeforeRenderRequest ProcessingLegendeventcallbackview deriverexternal process (middleware, tween)internal processview pipelinepredicatesview lookuproute predicatesURL dispatchNewRequestNewResponseview mapper ingressviewview mapper egressresponse adapterdecorators ingressdecorators egressauthorization -- cgit v1.2.3 From 7fc181d89e9553b152618066490d1fb659d45e13 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 13 Mar 2016 16:24:53 -0500 Subject: add examples of view derivers to the glossary --- docs/glossary.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/glossary.rst b/docs/glossary.rst index 9e068a18c..8928254f7 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1097,3 +1097,5 @@ Glossary A view deriver is a composable component of the view pipeline which is used to create a :term:`view callable`. A view deriver is a callable implementing the :class:`pyramid.interfaces.IViewDeriver` interface. + Examples of builtin derivers including view mapper, the permission + checker, and applying a renderer to a dictionary returned from the view. -- cgit v1.2.3 From fdd1f8352fc341bc60e0b7d32dadd2b4109a2b41 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 22:08:04 -0500 Subject: first cut at documenting view derivers --- docs/narr/extconfig.rst | 1 + docs/narr/hooks.rst | 140 ++++++++++++++++++++++++++ pyramid/config/views.py | 14 +-- pyramid/tests/test_config/test_derivations.py | 13 +-- 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index fee8d0d3a..af7d0a349 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -259,6 +259,7 @@ Pre-defined Phases - :meth:`pyramid.config.Configurator.add_route_predicate` - :meth:`pyramid.config.Configurator.add_subscriber_predicate` - :meth:`pyramid.config.Configurator.add_view_predicate` +- :meth:`pyramid.config.Configurator.add_view_deriver` - :meth:`pyramid.config.Configurator.set_authorization_policy` - :meth:`pyramid.config.Configurator.set_default_permission` - :meth:`pyramid.config.Configurator.set_view_mapper` diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 7ff119b53..13daed1fc 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1547,3 +1547,143 @@ in every subscriber registration. It is not the responsibility of the predicate author to make every predicate make sense for every event type; it is the responsibility of the predicate consumer to use predicates that make sense for a particular event type registration. + +.. index:: + single: view derivers + +.. _view_derivers: + +View Derivers +------------- + +Every URL processed by :app:`Pyramid` is matched against a custom view +pipeline. See :ref:`router_chapter` for how this works. The view pipeline +itself is built from the user-supplied :term:`view callable` which is then +composed with :term:`view derivers `. A view deriver is a +composable element of the view pipeline which is used to wrap a view with +added functionality. View derivers are very similar to the ``decorator`` +argument to :meth:`pyramid.config.Configurator.add_view` except that they have +the option to execute for every view in the application. + +Built-in View Derivers +~~~~~~~~~~~~~~~~~~~~~~ + +There are several builtin view derivers that :app:`Pyramid` will automatically +apply to any view. They are defined in order from closest to furthest from +the user-defined :term:`view callable`: + +``mapped_view`` + + Applies the :term:`view mapper` defined by the ``mapper`` option or the + application's default view mapper to the :term:`view callable`. This + is always the closest deriver to the user-defined view and standardizes the + view pipeline interface to accept ``(context, request)`` from all previous + view derivers. + +``decorated_view`` + + Wraps the view with the decorators from the ``decorator`` option. + +``http_cached_view`` + + Applies cache control headers to the response defined by the ``http_cache`` + option. This element is a noop if the ``pyramid.prevent_http_cache`` setting + is enabled or the ``http_cache`` option is ``None``. + +``owrapped_view`` + + Invokes the wrapped view defined by the ``wrapper`` option. + +``secured_view`` + + Enforce the ``permission`` defined on the view. This element is a noop if + no permission is defined. Note there will always be a permission defined + if a default permission was assigned via + :meth:`pyramid.config.Configurator.set_default_permission`. + +``authdebug_view`` + + Used to output useful debugging information when + ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. + +Custom View Derivers +~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.7 + +It is possible to define custom view derivers which will affect all views in +an application. There are many uses for this but most will likely be centered +around monitoring and security. In order to register a custom +:term:`view deriver` you should create a callable that conforms to the +:class:`pyramid.interfaces.IViewDeriver` interface. For example, below +is a callable that can provide timing information for the view pipeline: + +.. code-block:: python + :linenos: + + import time + + def timing_view(view, info): + def wrapper_view(context, request): + start = time.time() + response = view(context, request) + end = time.time() + response.headers['X-View-Performance'] = '%.3f' % (end - start,) + return wrapper_view + + config.add_view_deriver('timing_view', timing_view) + +View derivers are unique in that they have access to most of the options +passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what +to do and they have a chance to affect every view in the application. + +Let's look at one more example which will protect views by requiring a CSRF +token unless ``disable_csrf=True`` is passed to the view: + +.. code-block:: python + :linenos: + + from pyramid.session import check_csrf_token + + def require_csrf_view(view, info): + wrapper_view = view + if not info.options.get('disable_csrf', False): + def wrapper_view(context, request): + if request.method == 'POST': + check_csrf_token(request) + return view(context, request) + return wrapper_view + + require_csrf_view.options = ('disable_csrf',) + + config.add_view_deriver('require_csrf_view', require_csrf_view) + + def myview(request): + return 'protected' + + def my_unprotected_view(request): + return 'unprotected' + + config.add_view(myview, name='safe', renderer='string') + config.add_view(my_unprotected_, name='unsafe', disable_csrf=True, renderer='string') + +Navigating to ``/safe`` with a POST request will then fail when the call to +:func:`pyramid.session.check_csrf_token` raises a +:class:`pyramid.exceptions.BadCSRFToken` exception. However, ``/unsafe`` will +not error. + +Ordering View Derivers +~~~~~~~~~~~~~~~~~~~~~~ + +By default, every new view deriver is added between the ``decorated_view`` +and ``mapped_view`` built-in derivers. It is possible to customize this +ordering using the ``over`` and ``under`` options. Each option can use the +names of other view derivers in order to specify an ordering. There should +rarely be a reason to worry about the ordering of the derivers. + +It is not possible to add a deriver OVER the ``mapped_view`` as the +:term:`view mapper` is intimately tied to the signature of the user-defined +:term:`view callable`. If you simply need to know what the original view +callable was, it can be found as ``info.original_view`` on the provided +:class:`pyramid.interfaces.IViewDeriverInfo` object passed to every view +deriver. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c3e5d360e..b762a711c 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -654,19 +654,19 @@ class ViewsConfiguratorMixin(object): view_options: Pass a key/value pair here to use a third-party predicate or set a - value for a view derivative option registered via - :meth:`pyramid.config.Configurator.add_view_predicate` or - :meth:`pyramid.config.Configurator.add_view_option`. More than - one key/value pair can be used at the same time. See + value for a view deriver. See + :meth:`pyramid.config.Configurator.add_view_predicate` and + :meth:`pyramid.config.Configurator.add_view_deriver`. See :ref:`view_and_route_predicates` for more information about - third-party predicates. + third-party predicates and :ref:`view_derivers` for information + about view derivers. .. versionadded: 1.4a1 .. versionchanged: 1.7 - Support setting arbitrary view options. Previously, only - predicate values could be supplied. + Support setting view deriver options. Previously, only custom + view predicate values could be supplied. """ if custom_predicates: diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 12fcc300a..7202c5bb6 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1,7 +1,12 @@ import unittest +from zope.interface import implementer from pyramid import testing from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import ( + IResponse, + IRequest, + ) class TestDeriveView(unittest.TestCase): @@ -1318,13 +1323,6 @@ class TestDeriverIntegration(unittest.TestCase): ConfigurationError, lambda: self.config.add_view(lambda r: {}, deriv1='test1')) - -from zope.interface import implementer -from pyramid.interfaces import ( - IResponse, - IRequest, - ) - @implementer(IResponse) class DummyResponse(object): content_type = None @@ -1374,4 +1372,3 @@ def assert_similar_datetime(one, two): two_attr = getattr(two, attr) if not one_attr == two_attr: # pragma: no cover raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) - -- cgit v1.2.3 From cbf686706bcc8e471977185a6b27678b924f4dc2 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 22:33:33 -0500 Subject: swap the order of view deriver arguments so that the name is implicit --- pyramid/config/views.py | 14 +++++--- pyramid/tests/test_config/test_derivations.py | 47 ++++++++++++++++----------- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b762a711c..5455c29df 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1091,11 +1091,15 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_deriver(self, name, deriver, under=None, over=None): + def add_view_deriver(self, deriver, name=None, under=None, over=None): + deriver = self.maybe_dotted(deriver) + if under is None and over is None: over = 'decorated_view' - deriver = self.maybe_dotted(deriver) + if name is None: + name = deriver.__name__ + discriminator = ('view deriver', name) intr = self.introspectable( 'view derivers', @@ -1113,7 +1117,7 @@ class ViewsConfiguratorMixin(object): self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, after=under, before=over) self.action(discriminator, register, introspectables=(intr,), - order=PHASE1_CONFIG) # must be registered early + order=PHASE1_CONFIG) # must be registered before add_view def add_default_view_derivers(self): d = pyramid.config.derivations @@ -1126,12 +1130,12 @@ class ViewsConfiguratorMixin(object): ] after = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_deriver(name, deriver, under=after) + self.add_view_deriver(deriver, name=name, under=after) after = name self.add_view_deriver( - 'rendered_view', d.rendered_view, + name='rendered_view', under=pyramid.util.FIRST, over='decorated_view', ) diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 7202c5bb6..e1b6c8bc9 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1102,9 +1102,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_user_sorted(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None) - self.config.add_view_deriver('deriv2', None, over='deriv1') - self.config.add_view_deriver('deriv3', None, under='deriv2') + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2', over='deriv1') + self.config.add_view_deriver(None, 'deriv3', under='deriv2') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1124,9 +1124,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_implicit(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None) - self.config.add_view_deriver('deriv2', None) - self.config.add_view_deriver('deriv3', None) + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1146,7 +1146,7 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None, over='rendered_view') + self.config.add_view_deriver(None, 'deriv1', over='rendered_view') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1164,9 +1164,9 @@ class TestDerivationOrder(unittest.TestCase): def test_right_order_over_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver('deriv1', None, over='rendered_view') - self.config.add_view_deriver('deriv2', None) - self.config.add_view_deriver('deriv3', None) + self.config.add_view_deriver(None, 'deriv1', over='rendered_view') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() @@ -1204,7 +1204,7 @@ class TestAddDeriver(unittest.TestCase): result = self.config._derive_view(view) self.assertFalse(response.deriv) - self.config.add_view_deriver('test_deriv', deriv) + self.config.add_view_deriver(deriv, 'test_deriv') result = self.config._derive_view(view) self.assertTrue(response.deriv) @@ -1225,14 +1225,14 @@ class TestAddDeriver(unittest.TestCase): return view view1 = AView() - self.config.add_view_deriver('test_deriv', deriv1) + self.config.add_view_deriver(deriv1, 'test_deriv') result = self.config._derive_view(view1) self.assertTrue(flags.get('deriv1')) self.assertFalse(flags.get('deriv2')) flags.clear() view2 = AView() - self.config.add_view_deriver('test_deriv', deriv2) + self.config.add_view_deriver(deriv2, 'test_deriv') result = self.config._derive_view(view2) self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) @@ -1254,12 +1254,21 @@ class TestAddDeriver(unittest.TestCase): response.deriv.append('deriv3') return view - self.config.add_view_deriver('deriv1', deriv1) - self.config.add_view_deriver('deriv2', deriv2, over='deriv1') - self.config.add_view_deriver('deriv3', deriv3, under='deriv2') + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2', over='deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', under='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + def test_add_deriver_without_name(self): + from pyramid.interfaces import IViewDerivers + + derivers = self.config.registry.getUtility(IViewDerivers) + self.assertFalse('deriv' in derivers.names) + def deriv(view, info): pass + self.config.add_view_deriver(deriv) + self.assertTrue('deriv' in derivers.names) + class TestDeriverIntegration(unittest.TestCase): def setUp(self): @@ -1305,8 +1314,8 @@ class TestDeriverIntegration(unittest.TestCase): return view deriv2.options = ('deriv2',) - self.config.add_view_deriver('deriv1', deriv1) - self.config.add_view_deriver('deriv2', deriv2) + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2') self.config.add_view(view, deriv1='test1', deriv2='test2') wrapper = self._getViewCallable(self.config) @@ -1318,7 +1327,7 @@ class TestDeriverIntegration(unittest.TestCase): def test_unexpected_view_options(self): from pyramid.exceptions import ConfigurationError def deriv1(view, info): pass - self.config.add_view_deriver('deriv1', deriv1) + self.config.add_view_deriver(deriv1, 'deriv1') self.assertRaises( ConfigurationError, lambda: self.config.add_view(lambda r: {}, deriv1='test1')) -- cgit v1.2.3 From 64bf7eec9b868fbc113341c7f5675c063aea002b Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 22:35:19 -0500 Subject: use the implicit name in the doc examples --- docs/narr/hooks.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 13daed1fc..3f66f4988 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1631,7 +1631,7 @@ is a callable that can provide timing information for the view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver('timing_view', timing_view) + config.add_view_deriver(timing_view) View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1656,7 +1656,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver('require_csrf_view', require_csrf_view) + config.add_view_deriver(require_csrf_view) def myview(request): return 'protected' -- cgit v1.2.3 From a0945399b24fb38607107a55b12b7997723de2a0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 23:25:10 -0500 Subject: do not guess at the name of the view deriver without further discussion --- docs/narr/hooks.rst | 4 ++-- pyramid/config/views.py | 3 --- pyramid/tests/test_config/test_derivations.py | 9 --------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 3f66f4988..e3843cfbd 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1631,7 +1631,7 @@ is a callable that can provide timing information for the view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver(timing_view) + config.add_view_deriver(timing_view, 'timing view') View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1656,7 +1656,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver(require_csrf_view) + config.add_view_deriver(require_csrf_view, 'require_csrf_view') def myview(request): return 'protected' diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5455c29df..6f8b24aa2 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1097,9 +1097,6 @@ class ViewsConfiguratorMixin(object): if under is None and over is None: over = 'decorated_view' - if name is None: - name = deriver.__name__ - discriminator = ('view deriver', name) intr = self.introspectable( 'view derivers', diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index e1b6c8bc9..69d8797f4 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1260,15 +1260,6 @@ class TestAddDeriver(unittest.TestCase): result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) - def test_add_deriver_without_name(self): - from pyramid.interfaces import IViewDerivers - - derivers = self.config.registry.getUtility(IViewDerivers) - self.assertFalse('deriv' in derivers.names) - def deriv(view, info): pass - self.config.add_view_deriver(deriv) - self.assertTrue('deriv' in derivers.names) - class TestDeriverIntegration(unittest.TestCase): def setUp(self): -- cgit v1.2.3 From 35e632635b1b4e0a767024689d69d9469ae98c0f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 14 Mar 2016 23:28:15 -0500 Subject: add a docstring for add_view_deriver and expose the method to the api docs --- docs/api/config.rst | 1 + docs/narr/hooks.rst | 12 +++++++++--- pyramid/config/views.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/api/config.rst b/docs/api/config.rst index ae913d32c..e083dbc68 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -66,6 +66,7 @@ .. automethod:: add_tween .. automethod:: add_route_predicate .. automethod:: add_view_predicate + .. automethod:: add_view_deriver .. automethod:: set_request_factory .. automethod:: set_root_factory .. automethod:: set_session_factory diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index e3843cfbd..a5a03ef95 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1580,6 +1580,10 @@ the user-defined :term:`view callable`: view pipeline interface to accept ``(context, request)`` from all previous view derivers. +``rendered_view`` + + Adapts the result of :term:`view callable` into a :term:`response` object. + ``decorated_view`` Wraps the view with the decorators from the ``decorator`` option. @@ -1615,8 +1619,10 @@ It is possible to define custom view derivers which will affect all views in an application. There are many uses for this but most will likely be centered around monitoring and security. In order to register a custom :term:`view deriver` you should create a callable that conforms to the -:class:`pyramid.interfaces.IViewDeriver` interface. For example, below -is a callable that can provide timing information for the view pipeline: +:class:`pyramid.interfaces.IViewDeriver` interface and then register it with +your application using :meth:`pyramid.config.Configurator.add_view_deriver`. +For example, below is a callable that can provide timing information for the +view pipeline: .. code-block:: python :linenos: @@ -1676,7 +1682,7 @@ Ordering View Derivers ~~~~~~~~~~~~~~~~~~~~~~ By default, every new view deriver is added between the ``decorated_view`` -and ``mapped_view`` built-in derivers. It is possible to customize this +and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should rarely be a reason to worry about the ordering of the derivers. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 6f8b24aa2..e90b95420 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1091,7 +1091,36 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_deriver(self, deriver, name=None, under=None, over=None): + def add_view_deriver(self, deriver, name, under=None, over=None): + """ + .. versionadded:: 1.7 + + Add a :term:`view deriver` to the view pipeline. View derivers are + a feature used by extension authors to wrap views in custom code + controllable by view-specific options. + + ``deriver`` should be a callable conforming to the + :class:`pyramid.interfaces.IViewDeriver` interface. + + ``name`` should be the name of the view deriver. There are no + restrictions on the name of a view deriver. If left unspecified, the + name will be constructed from the name of the ``deriver``. + + The ``under`` and ``over`` options may be used to control the ordering + of view derivers by providing hints about where in the view pipeline + the deriver is used. + + ``under`` means further away from user-defined :term:`view callable`, + and ``over`` means closer to the original :term:`view callable`. + + Specifying neither ``under`` nor ``over`` is equivalent to specifying + ``over='decorated_view'`` and ``under='rendered_view'``, placing the + deriver somewhere between the ``decorated_view`` and ``rendered_view`` + derivers. + + See :ref:`view_derivers` for more information. + + """ deriver = self.maybe_dotted(deriver) if under is None and over is None: -- cgit v1.2.3 From cf9dcb10a02d32cee09c5540c9a3e9590ea4f490 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Mar 2016 00:45:56 -0500 Subject: "over" is closer to ingress and "under" is closer to mapped_view --- pyramid/config/tweens.py | 7 +-- pyramid/config/util.py | 7 +++ pyramid/config/views.py | 46 +++++++++--------- pyramid/tests/test_config/test_derivations.py | 70 ++++++++++++++------------- 4 files changed, 68 insertions(+), 62 deletions(-) diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index cd14c9ff6..8e1800f33 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -18,6 +18,7 @@ from pyramid.tweens import ( from pyramid.config.util import ( action_method, + is_string_or_iterable, TopologicalSorter, ) @@ -122,12 +123,6 @@ class TweensConfiguratorMixin(object): tween_factory = self.maybe_dotted(tween_factory) - def is_string_or_iterable(v): - if isinstance(v, string_types): - return True - if hasattr(v, '__iter__'): - return True - for t, p in [('over', over), ('under', under)]: if p is not None: if not is_string_or_iterable(p): diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 85ce826a4..626e8d5fe 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -5,6 +5,7 @@ from pyramid.compat import ( bytes_, getargspec, is_nonstr_iter, + string_types, ) from pyramid.compat import im_func @@ -23,6 +24,12 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +def is_string_or_iterable(v): + if isinstance(v, string_types): + return True + if hasattr(v, '__iter__'): + return True + def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index e90b95420..db56fa761 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -89,6 +89,7 @@ from pyramid.config.derivations import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, + is_string_or_iterable, ) urljoin = urlparse.urljoin @@ -690,7 +691,7 @@ class ViewsConfiguratorMixin(object): def combine(*decorators): def decorated(view_callable): - # reversed() is allows a more natural ordering in the api + # reversed() allows a more natural ordering in the api for decorator in reversed(decorators): view_callable = decorator(view_callable) return view_callable @@ -1029,16 +1030,15 @@ class ViewsConfiguratorMixin(object): def _apply_view_derivers(self, info): d = pyramid.config.derivations - # These inner derivations have fixed order + # These derivations have fixed order + outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), + ('predicated_view', d.predicated_view)] inner_derivers = [('mapped_view', d.mapped_view)] - outer_derivers = [('predicated_view', d.predicated_view), - ('attr_wrapped_view', d.attr_wrapped_view)] - view = info.original_view derivers = self.registry.getUtility(IViewDerivers) - for name, deriver in ( - inner_derivers + derivers.sorted() + outer_derivers + for name, deriver in reversed( + outer_derivers + derivers.sorted() + inner_derivers ): view = wraps_view(deriver)(view, info) return view @@ -1110,11 +1110,11 @@ class ViewsConfiguratorMixin(object): of view derivers by providing hints about where in the view pipeline the deriver is used. - ``under`` means further away from user-defined :term:`view callable`, - and ``over`` means closer to the original :term:`view callable`. + ``under`` means closer to the user-defined :term:`view callable`, + and ``over`` means closer to view pipeline ingress. Specifying neither ``under`` nor ``over`` is equivalent to specifying - ``over='decorated_view'`` and ``under='rendered_view'``, placing the + ``over='rendered_view'`` and ``under='decorated_view'``, placing the deriver somewhere between the ``decorated_view`` and ``rendered_view`` derivers. @@ -1124,7 +1124,8 @@ class ViewsConfiguratorMixin(object): deriver = self.maybe_dotted(deriver) if under is None and over is None: - over = 'decorated_view' + under = 'decorated_view' + over = 'rendered_view' discriminator = ('view deriver', name) intr = self.introspectable( @@ -1141,29 +1142,30 @@ class ViewsConfiguratorMixin(object): if derivers is None: derivers = TopologicalSorter() self.registry.registerUtility(derivers, IViewDerivers) - derivers.add(name, deriver, after=under, before=over) + derivers.add(name, deriver, before=over, after=under) self.action(discriminator, register, introspectables=(intr,), order=PHASE1_CONFIG) # must be registered before add_view def add_default_view_derivers(self): d = pyramid.config.derivations derivers = [ - ('decorated_view', d.decorated_view), - ('http_cached_view', d.http_cached_view), - ('owrapped_view', d.owrapped_view), - ('secured_view', d.secured_view), ('authdebug_view', d.authdebug_view), + ('secured_view', d.secured_view), + ('owrapped_view', d.owrapped_view), + ('http_cached_view', d.http_cached_view), + ('decorated_view', d.decorated_view), ] - after = pyramid.util.FIRST + last = pyramid.util.FIRST for name, deriver in derivers: - self.add_view_deriver(deriver, name=name, under=after) - after = name + self.add_view_deriver(deriver, name=name, under=last) + last = name + # ensure rendered_view is over LAST self.add_view_deriver( d.rendered_view, - name='rendered_view', - under=pyramid.util.FIRST, - over='decorated_view', + 'rendered_view', + under=last, + over=pyramid.util.LAST, ) def derive_view(self, view, attr=None, renderer=None): diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py index 69d8797f4..d93b37f38 100644 --- a/pyramid/tests/test_config/test_derivations.py +++ b/pyramid/tests/test_config/test_derivations.py @@ -1110,15 +1110,15 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'rendered_view', + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', 'deriv2', 'deriv3', 'deriv1', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', - 'authdebug_view', + 'rendered_view', ], dlist) def test_right_order_implicit(self): @@ -1132,54 +1132,56 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'rendered_view', - 'deriv1', - 'deriv2', - 'deriv3', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'deriv1', + 'rendered_view', ], dlist) - def test_right_order_over_rendered_view(self): + def test_right_order_under_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', over='rendered_view') + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] - self.assertEqual(['deriv1', - 'rendered_view', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', + self.assertEqual([ 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'rendered_view', + 'deriv1', ], dlist) - def test_right_order_over_rendered_view_others(self): + def test_right_order_under_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', over='rendered_view') + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') self.config.add_view_deriver(None, 'deriv2') self.config.add_view_deriver(None, 'deriv3') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] - self.assertEqual(['deriv1', - 'rendered_view', - 'deriv2', - 'deriv3', - 'decorated_view', - 'http_cached_view', - 'owrapped_view', - 'secured_view', + self.assertEqual([ 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'rendered_view', + 'deriv1', ], dlist) @@ -1255,8 +1257,8 @@ class TestAddDeriver(unittest.TestCase): return view self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2', over='deriv1') - self.config.add_view_deriver(deriv3, 'deriv3', under='deriv2') + self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') result = self.config._derive_view(view) self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) -- cgit v1.2.3 From 8ff071aff40df4dccaf8619b55c4ac318c6e0246 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Mar 2016 00:54:41 -0500 Subject: remove unused import --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index db56fa761..1516743ad 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -89,7 +89,6 @@ from pyramid.config.derivations import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - is_string_or_iterable, ) urljoin = urlparse.urljoin -- cgit v1.2.3 From a116948ffe14449ac6ef29145b62eb2976130d83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 17 Mar 2016 01:24:21 -0500 Subject: fix deriver docs to explain ordering issues --- docs/narr/hooks.rst | 81 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a5a03ef95..4a1233244 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1556,6 +1556,8 @@ for a particular event type registration. View Derivers ------------- +.. versionadded:: 1.7 + Every URL processed by :app:`Pyramid` is matched against a custom view pipeline. See :ref:`router_chapter` for how this works. The view pipeline itself is built from the user-supplied :term:`view callable` which is then @@ -1565,28 +1567,33 @@ added functionality. View derivers are very similar to the ``decorator`` argument to :meth:`pyramid.config.Configurator.add_view` except that they have the option to execute for every view in the application. +It is helpful to think of a :term:`view deriver` as middleware for views. +Unlike tweens or WSGI middleware which are scoped to the application itself, +a view deriver is invoked once per view in the application and can use +configuration options from the view to customize its behavior. + Built-in View Derivers ~~~~~~~~~~~~~~~~~~~~~~ There are several builtin view derivers that :app:`Pyramid` will automatically -apply to any view. They are defined in order from closest to furthest from +apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: -``mapped_view`` +``authdebug_view`` - Applies the :term:`view mapper` defined by the ``mapper`` option or the - application's default view mapper to the :term:`view callable`. This - is always the closest deriver to the user-defined view and standardizes the - view pipeline interface to accept ``(context, request)`` from all previous - view derivers. + Used to output useful debugging information when + ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. -``rendered_view`` +``secured_view`` - Adapts the result of :term:`view callable` into a :term:`response` object. + Enforce the ``permission`` defined on the view. This element is a noop if + no permission is defined. Note there will always be a permission defined + if a default permission was assigned via + :meth:`pyramid.config.Configurator.set_default_permission`. -``decorated_view`` +``owrapped_view`` - Wraps the view with the decorators from the ``decorator`` option. + Invokes the wrapped view defined by the ``wrapper`` option. ``http_cached_view`` @@ -1594,27 +1601,26 @@ the user-defined :term:`view callable`: option. This element is a noop if the ``pyramid.prevent_http_cache`` setting is enabled or the ``http_cache`` option is ``None``. -``owrapped_view`` +``decorated_view`` - Invokes the wrapped view defined by the ``wrapper`` option. + Wraps the view with the decorators from the ``decorator`` option. -``secured_view`` +``rendered_view`` - Enforce the ``permission`` defined on the view. This element is a noop if - no permission is defined. Note there will always be a permission defined - if a default permission was assigned via - :meth:`pyramid.config.Configurator.set_default_permission`. + Adapts the result of the :term:`view callable` into a :term:`response` + object. Below this point the result may be any Python object. -``authdebug_view`` +``mapped_view`` - Used to output useful debugging information when - ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. + Applies the :term:`view mapper` defined by the ``mapper`` option or the + application's default view mapper to the :term:`view callable`. This + is always the closest deriver to the user-defined view and standardizes the + view pipeline interface to accept ``(context, request)`` from all previous + view derivers. Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.7 - It is possible to define custom view derivers which will affect all views in an application. There are many uses for this but most will likely be centered around monitoring and security. In order to register a custom @@ -1649,6 +1655,7 @@ token unless ``disable_csrf=True`` is passed to the view: .. code-block:: python :linenos: + from pyramid.response import Response from pyramid.session import check_csrf_token def require_csrf_view(view, info): @@ -1664,14 +1671,14 @@ token unless ``disable_csrf=True`` is passed to the view: config.add_view_deriver(require_csrf_view, 'require_csrf_view') - def myview(request): - return 'protected' + def protected_view(request): + return Response('protected') - def my_unprotected_view(request): - return 'unprotected' + def unprotected_view(request): + return Response('unprotected') - config.add_view(myview, name='safe', renderer='string') - config.add_view(my_unprotected_, name='unsafe', disable_csrf=True, renderer='string') + config.add_view(protected_view, name='safe') + config.add_view(unprotected_view, name='unsafe', disable_csrf=True) Navigating to ``/safe`` with a POST request will then fail when the call to :func:`pyramid.session.check_csrf_token` raises a @@ -1685,11 +1692,23 @@ By default, every new view deriver is added between the ``decorated_view`` and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should -rarely be a reason to worry about the ordering of the derivers. +rarely be a reason to worry about the ordering of the derivers. Both ``over`` +and ``under`` may also be iterables of constraints. For either option, if one +or more constraints was defined, at least one must be satisfied or a +:class:`pyramid.exceptions.ConfigurationError` will be raised. This may be +used to define fallback constraints if another deriver is missing. -It is not possible to add a deriver OVER the ``mapped_view`` as the +It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined :term:`view callable`. If you simply need to know what the original view callable was, it can be found as ``info.original_view`` on the provided :class:`pyramid.interfaces.IViewDeriverInfo` object passed to every view deriver. + +.. warning:: + + Any view derivers defined ``under`` the ``rendered_view`` are not + guaranteed to receive a valid response object. Rather they will receive the + result from the :term:`view mapper` which is likely the original response + returned from the view. This is possibly a dictionary for a renderer but it + may be any Python object that may be adapted into a response. -- cgit v1.2.3 From 6fb44f451abb657716ae0066ed70af66060ef3b8 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Mar 2016 04:32:21 -0700 Subject: polish view derivers docs, minor grammar --- docs/narr/hooks.rst | 51 +++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 4a1233244..a32e94d1a 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1548,6 +1548,7 @@ predicate author to make every predicate make sense for every event type; it is the responsibility of the predicate consumer to use predicates that make sense for a particular event type registration. + .. index:: single: view derivers @@ -1560,35 +1561,36 @@ View Derivers Every URL processed by :app:`Pyramid` is matched against a custom view pipeline. See :ref:`router_chapter` for how this works. The view pipeline -itself is built from the user-supplied :term:`view callable` which is then +itself is built from the user-supplied :term:`view callable`, which is then composed with :term:`view derivers `. A view deriver is a composable element of the view pipeline which is used to wrap a view with added functionality. View derivers are very similar to the ``decorator`` -argument to :meth:`pyramid.config.Configurator.add_view` except that they have +argument to :meth:`pyramid.config.Configurator.add_view`, except that they have the option to execute for every view in the application. It is helpful to think of a :term:`view deriver` as middleware for views. Unlike tweens or WSGI middleware which are scoped to the application itself, -a view deriver is invoked once per view in the application and can use +a view deriver is invoked once per view in the application, and can use configuration options from the view to customize its behavior. Built-in View Derivers ~~~~~~~~~~~~~~~~~~~~~~ -There are several builtin view derivers that :app:`Pyramid` will automatically +There are several built-in view derivers that :app:`Pyramid` will automatically apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: ``authdebug_view`` Used to output useful debugging information when - ``pyramid.debug_authorization`` is enabled. This element is a noop otherwise. + ``pyramid.debug_authorization`` is enabled. This element is a no-op + otherwise. ``secured_view`` - Enforce the ``permission`` defined on the view. This element is a noop if - no permission is defined. Note there will always be a permission defined - if a default permission was assigned via + Enforce the ``permission`` defined on the view. This element is a no-op if no + permission is defined. Note there will always be a permission defined if a + default permission was assigned via :meth:`pyramid.config.Configurator.set_default_permission`. ``owrapped_view`` @@ -1598,7 +1600,7 @@ the user-defined :term:`view callable`: ``http_cached_view`` Applies cache control headers to the response defined by the ``http_cache`` - option. This element is a noop if the ``pyramid.prevent_http_cache`` setting + option. This element is a no-op if the ``pyramid.prevent_http_cache`` setting is enabled or the ``http_cache`` option is ``None``. ``decorated_view`` @@ -1621,11 +1623,11 @@ the user-defined :term:`view callable`: Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ -It is possible to define custom view derivers which will affect all views in -an application. There are many uses for this but most will likely be centered -around monitoring and security. In order to register a custom -:term:`view deriver` you should create a callable that conforms to the -:class:`pyramid.interfaces.IViewDeriver` interface and then register it with +It is possible to define custom view derivers which will affect all views in an +application. There are many uses for this, but most will likely be centered +around monitoring and security. In order to register a custom :term:`view +deriver`, you should create a callable that conforms to the +:class:`pyramid.interfaces.IViewDeriver` interface, and then register it with your application using :meth:`pyramid.config.Configurator.add_view_deriver`. For example, below is a callable that can provide timing information for the view pipeline: @@ -1647,7 +1649,7 @@ view pipeline: View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what -to do and they have a chance to affect every view in the application. +to do, and they have a chance to affect every view in the application. Let's look at one more example which will protect views by requiring a CSRF token unless ``disable_csrf=True`` is passed to the view: @@ -1688,15 +1690,16 @@ not error. Ordering View Derivers ~~~~~~~~~~~~~~~~~~~~~~ -By default, every new view deriver is added between the ``decorated_view`` -and ``rendered_view`` built-in derivers. It is possible to customize this -ordering using the ``over`` and ``under`` options. Each option can use the -names of other view derivers in order to specify an ordering. There should -rarely be a reason to worry about the ordering of the derivers. Both ``over`` -and ``under`` may also be iterables of constraints. For either option, if one -or more constraints was defined, at least one must be satisfied or a -:class:`pyramid.exceptions.ConfigurationError` will be raised. This may be -used to define fallback constraints if another deriver is missing. +By default, every new view deriver is added between the ``decorated_view`` and +``rendered_view`` built-in derivers. It is possible to customize this ordering +using the ``over`` and ``under`` options. Each option can use the names of +other view derivers in order to specify an ordering. There should rarely be a +reason to worry about the ordering of the derivers. + +Both ``over`` and ``under`` may also be iterables of constraints. For either +option, if one or more constraints was defined, at least one must be satisfied, +else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may +be used to define fallback constraints if another deriver is missing. It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined -- cgit v1.2.3 From 890ea8f7d57d680bd07e04bcab9a778d0f12f737 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Mar 2016 04:34:43 -0700 Subject: polish view derivers docs, minor grammar --- docs/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/glossary.rst b/docs/glossary.rst index 8928254f7..5e6aa145c 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1097,5 +1097,5 @@ Glossary A view deriver is a composable component of the view pipeline which is used to create a :term:`view callable`. A view deriver is a callable implementing the :class:`pyramid.interfaces.IViewDeriver` interface. - Examples of builtin derivers including view mapper, the permission + Examples of built-in derivers including view mapper, the permission checker, and applying a renderer to a dictionary returned from the view. -- cgit v1.2.3 From 0b0f7e1a6d411bcc2af615da8e9dec7ea7519152 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Mon, 21 Mar 2016 04:37:15 -0700 Subject: polish view derivers docs, minor grammar --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 692bf3d6d..64bb4b50c 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1188,7 +1188,7 @@ class IPredicateList(Interface): """ Interface representing a predicate list """ class IViewDeriver(Interface): - options = Attribute('An list of supported options to be passed to ' + options = Attribute('A list of supported options to be passed to ' ':meth:`pyramid.config.Configurator.add_view`. ' 'This attribute is optional.') -- cgit v1.2.3 From ecc9d82ef6bd50a08dcbee7286ba4581d3caa902 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 6 Apr 2016 21:22:36 -0500 Subject: rename pyramid.config.derivations to pyramid.viewderivers --- pyramid/config/derivations.py | 453 -------- pyramid/config/views.py | 8 +- pyramid/tests/test_config/test_derivations.py | 1376 ------------------------- pyramid/tests/test_viewderivers.py | 1376 +++++++++++++++++++++++++ pyramid/viewderivers.py | 453 ++++++++ 5 files changed, 1833 insertions(+), 1833 deletions(-) delete mode 100644 pyramid/config/derivations.py delete mode 100644 pyramid/tests/test_config/test_derivations.py create mode 100644 pyramid/tests/test_viewderivers.py create mode 100644 pyramid/viewderivers.py diff --git a/pyramid/config/derivations.py b/pyramid/config/derivations.py deleted file mode 100644 index 99baf46f9..000000000 --- a/pyramid/config/derivations.py +++ /dev/null @@ -1,453 +0,0 @@ -import inspect - -from zope.interface import ( - implementer, - provider, - ) - -from pyramid.security import NO_PERMISSION_REQUIRED -from pyramid.response import Response - -from pyramid.interfaces import ( - IAuthenticationPolicy, - IAuthorizationPolicy, - IDebugLogger, - IResponse, - IViewMapper, - IViewMapperFactory, - ) - -from pyramid.compat import ( - is_bound_method, - is_unbound_method, - ) - -from pyramid.config.util import ( - DEFAULT_PHASH, - MAX_ORDER, - takes_one_arg, - ) - -from pyramid.exceptions import ( - ConfigurationError, - PredicateMismatch, - ) -from pyramid.httpexceptions import HTTPForbidden -from pyramid.util import object_description -from pyramid.view import render_view_to_response -from pyramid import renderers - - -def view_description(view): - try: - return view.__text__ - except AttributeError: - # custom view mappers might not add __text__ - return object_description(view) - -def requestonly(view, attr=None): - return takes_one_arg(view, attr=attr, argname='request') - -@implementer(IViewMapper) -@provider(IViewMapperFactory) -class DefaultViewMapper(object): - def __init__(self, **kw): - self.attr = kw.get('attr') - - def __call__(self, view): - if is_unbound_method(view) and self.attr is None: - raise ConfigurationError(( - 'Unbound method calls are not supported, please set the class ' - 'as your `view` and the method as your `attr`' - )) - - if inspect.isclass(view): - view = self.map_class(view) - else: - view = self.map_nonclass(view) - return view - - def map_class(self, view): - ronly = requestonly(view, self.attr) - if ronly: - mapped_view = self.map_class_requestonly(view) - else: - mapped_view = self.map_class_native(view) - mapped_view.__text__ = 'method %s of %s' % ( - self.attr or '__call__', object_description(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 = requestonly(view, self.attr) - if ronly: - mapped_view = self.map_nonclass_requestonly(view) - elif self.attr: - mapped_view = self.map_nonclass_attr(view) - if inspect.isroutine(mapped_view): - # This branch will be true if the view is a function or a method. - # We potentially mutate an unwrapped object here if it's a - # function. We do this to avoid function call overhead of - # injecting another wrapper. However, we must wrap if the - # function is a bound method because we can't set attributes on a - # bound method. - if is_bound_method(view): - _mapped_view = mapped_view - def mapped_view(context, request): - return _mapped_view(context, request) - if self.attr is not None: - mapped_view.__text__ = 'attr %s of %s' % ( - self.attr, object_description(view)) - else: - mapped_view.__text__ = object_description(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) - request.__view__ = inst - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - 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) - request.__view__ = inst - if attr is None: - response = inst() - else: - response = getattr(inst, attr)() - 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) - return response - return _requestonly_view - - def map_nonclass_attr(self, view): - # its a function that has a __call__ which accepts both context and - # request, but still has an attr - def _attr_view(context, request): - response = getattr(view, self.attr)(context, request) - return response - return _attr_view - - -def wraps_view(wrapper): - def inner(view, info): - wrapper_view = wrapper(view, info) - return preserve_view_attrs(view, wrapper_view) - return inner - -def preserve_view_attrs(view, wrapper): - if view is None: - return wrapper - - if wrapper is view: - return view - - original_view = getattr(view, '__original_view__', None) - - if original_view is None: - original_view = view - - wrapper.__wraps__ = view - wrapper.__original_view__ = original_view - wrapper.__module__ = view.__module__ - wrapper.__doc__ = view.__doc__ - - try: - wrapper.__name__ = view.__name__ - except AttributeError: - wrapper.__name__ = repr(view) - - # attrs that may not exist on "view", but, if so, must be attached to - # "wrapped view" - for attr in ('__permitted__', '__call_permissive__', '__permission__', - '__predicated__', '__predicates__', '__accept__', - '__order__', '__text__'): - try: - setattr(wrapper, attr, getattr(view, attr)) - except AttributeError: - pass - - return wrapper - -def mapped_view(view, info): - mapper = info.options.get('mapper') - if mapper is None: - mapper = getattr(view, '__view_mapper__', None) - if mapper is None: - mapper = info.registry.queryUtility(IViewMapperFactory) - if mapper is None: - mapper = DefaultViewMapper - - mapped_view = mapper(**info.options)(view) - return mapped_view - -mapped_view.options = ('mapper', 'attr') - -def owrapped_view(view, info): - wrapper_viewname = info.options.get('wrapper') - viewname = info.options.get('name') - 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 - -owrapped_view.options = ('name', 'wrapper') - -def http_cached_view(view, info): - if info.settings.get('prevent_http_cache', False): - return view - - seconds = info.options.get('http_cache') - - if seconds is None: - return view - - options = {} - - if isinstance(seconds, (tuple, list)): - try: - seconds, options = seconds - except ValueError: - raise ConfigurationError( - 'If http_cache parameter is a tuple or list, it must be ' - 'in the form (seconds, options); not %s' % (seconds,)) - - def wrapper(context, request): - response = view(context, request) - prevent_caching = getattr(response.cache_control, 'prevent_auto', - False) - if not prevent_caching: - response.cache_expires(seconds, **options) - return response - - return wrapper - -http_cached_view.options = ('http_cache',) - -def secured_view(view, info): - permission = info.options.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 - authn_policy = info.registry.queryUtility(IAuthenticationPolicy) - authz_policy = info.registry.queryUtility(IAuthorizationPolicy) - - if authn_policy and authz_policy and (permission is not None): - def _permitted(context, request): - principals = authn_policy.effective_principals(request) - return authz_policy.permits(context, principals, permission) - def _secured_view(context, request): - result = _permitted(context, request) - if result: - return view(context, request) - view_name = getattr(view, '__name__', view) - msg = getattr( - request, 'authdebug_message', - 'Unauthorized: %s failed permission check' % view_name) - raise HTTPForbidden(msg, result=result) - _secured_view.__call_permissive__ = view - _secured_view.__permitted__ = _permitted - _secured_view.__permission__ = permission - wrapped_view = _secured_view - - return wrapped_view - -secured_view.options = ('permission',) - -def authdebug_view(view, info): - wrapped_view = view - settings = info.settings - permission = info.options.get('permission') - authn_policy = info.registry.queryUtility(IAuthenticationPolicy) - authz_policy = info.registry.queryUtility(IAuthorizationPolicy) - logger = info.registry.queryUtility(IDebugLogger) - if settings and settings.get('debug_authorization', False): - def _authdebug_view(context, request): - view_name = getattr(request, 'view_name', None) - - if authn_policy and authz_policy: - if permission is NO_PERMISSION_REQUIRED: - msg = 'Allowed (NO_PERMISSION_REQUIRED)' - elif 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)) - if logger: - logger.debug(msg) - if request is not None: - request.authdebug_message = msg - return view(context, request) - - wrapped_view = _authdebug_view - - return wrapped_view - -authdebug_view.options = ('permission',) - -def predicated_view(view, info): - preds = info.predicates - if not preds: - return view - def predicate_wrapper(context, request): - for predicate in preds: - if not predicate(context, request): - view_name = getattr(view, '__name__', view) - raise PredicateMismatch( - 'predicate mismatch for view %s (%s)' % ( - view_name, predicate.text())) - return view(context, request) - def checker(context, request): - return all((predicate(context, request) for predicate in - preds)) - predicate_wrapper.__predicated__ = checker - predicate_wrapper.__predicates__ = preds - return predicate_wrapper - -def attr_wrapped_view(view, info): - accept, order, phash = (info.options.get('accept', None), - getattr(info, 'order', MAX_ORDER), - getattr(info, '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 - attr_view.__view_attr__ = info.options.get('attr') - attr_view.__permission__ = info.options.get('permission') - return attr_view - -attr_wrapped_view.options = ('accept', 'attr', 'permission') - -def rendered_view(view, info): - # one way or another this wrapper must produce a Response (unless - # the renderer is a NullRendererHelper) - renderer = info.options.get('renderer') - if renderer is None: - # register a default renderer if you want super-dynamic - # rendering. registering a default renderer will also allow - # override_renderer to work if a renderer is left unspecified for - # a view registration. - def viewresult_to_response(context, request): - result = view(context, request) - if result.__class__ is Response: # common case - response = result - else: - response = info.registry.queryAdapterOrSelf(result, IResponse) - if response is None: - if result is None: - append = (' You may have forgotten to return a value ' - 'from the view callable.') - elif isinstance(result, dict): - append = (' You may have forgotten to define a ' - 'renderer in the view configuration.') - else: - append = '' - - msg = ('Could not convert return value of the view ' - 'callable %s into a response object. ' - 'The value returned was %r.' + append) - - raise ValueError(msg % (view_description(view), result)) - - return response - - return viewresult_to_response - - if renderer is renderers.null_renderer: - return view - - def rendered_view(context, request): - result = view(context, request) - if result.__class__ is Response: # potential common case - response = result - else: - # this must adapt, it can't do a simple interface check - # (avoid trying to render webob responses) - response = info.registry.queryAdapterOrSelf(result, IResponse) - if response is None: - attrs = getattr(request, '__dict__', {}) - if 'override_renderer' in attrs: - # renderer overridden by newrequest event or other - renderer_name = attrs.pop('override_renderer') - view_renderer = renderers.RendererHelper( - name=renderer_name, - package=info.package, - registry=info.registry) - else: - view_renderer = renderer.clone() - if '__view__' in attrs: - view_inst = attrs.pop('__view__') - else: - view_inst = getattr(view, '__original_view__', view) - response = view_renderer.render_view( - request, result, view_inst, context) - return response - - return rendered_view - -rendered_view.options = ('renderer',) - -def decorated_view(view, info): - decorator = info.options.get('decorator') - if decorator is None: - return view - return decorator(view) - -decorated_view.options = ('decorator',) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1516743ad..2a019726f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -76,9 +76,9 @@ from pyramid.util import ( ) import pyramid.config.predicates -import pyramid.config.derivations +import pyramid.viewderivers -from pyramid.config.derivations import ( +from pyramid.viewderivers import ( preserve_view_attrs, view_description, requestonly, @@ -1028,7 +1028,7 @@ class ViewsConfiguratorMixin(object): raise ConfigurationError('Unknown view options: %s' % (kw,)) def _apply_view_derivers(self, info): - d = pyramid.config.derivations + d = pyramid.viewderivers # These derivations have fixed order outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), ('predicated_view', d.predicated_view)] @@ -1146,7 +1146,7 @@ class ViewsConfiguratorMixin(object): order=PHASE1_CONFIG) # must be registered before add_view def add_default_view_derivers(self): - d = pyramid.config.derivations + d = pyramid.viewderivers derivers = [ ('authdebug_view', d.authdebug_view), ('secured_view', d.secured_view), diff --git a/pyramid/tests/test_config/test_derivations.py b/pyramid/tests/test_config/test_derivations.py deleted file mode 100644 index d93b37f38..000000000 --- a/pyramid/tests/test_config/test_derivations.py +++ /dev/null @@ -1,1376 +0,0 @@ -import unittest -from zope.interface import implementer - -from pyramid import testing -from pyramid.exceptions import ConfigurationError -from pyramid.interfaces import ( - IResponse, - IRequest, - ) - -class TestDeriveView(unittest.TestCase): - - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def _makeRequest(self): - request = DummyRequest() - request.registry = self.config.registry - return request - - def _registerLogger(self): - from pyramid.interfaces import IDebugLogger - logger = DummyLogger() - self.config.registry.registerUtility(logger, IDebugLogger) - return logger - - def _registerSecurityPolicy(self, permissive): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(permissive) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - - def test_function_returns_non_adaptable(self): - def view(request): - return None - result = self.config.derive_view(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable function ' - 'pyramid.tests.test_config.test_derivations.view into a response ' - 'object. The value returned was None. You may have forgotten ' - 'to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_function_returns_non_adaptable_dict(self): - def view(request): - return {'a':1} - result = self.config.derive_view(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - self.assertEqual( - e.args[0], - "Could not convert return value of the view callable function " - "pyramid.tests.test_config.test_derivations.view into a response " - "object. The value returned was {'a': 1}. You may have " - "forgotten to define a renderer in the view configuration." - ) - else: # pragma: no cover - raise AssertionError - - def test_instance_returns_non_adaptable(self): - class AView(object): - def __call__(self, request): - return None - view = AView() - result = self.config.derive_view(view) - self.assertFalse(result is view) - try: - result(None, None) - except ValueError as e: - msg = e.args[0] - self.assertTrue(msg.startswith( - 'Could not convert return value of the view callable object ' - ' into a response object. The value returned was None. You ' - 'may have forgotten to return a value from the view callable.')) - else: # pragma: no cover - raise AssertionError - - def test_function_returns_true_Response_no_renderer(self): - from pyramid.response import Response - r = Response('Hello') - def view(request): - return r - result = self.config.derive_view(view) - self.assertFalse(result is view) - response = result(None, None) - self.assertEqual(response, r) - - def test_function_returns_true_Response_with_renderer(self): - from pyramid.response import Response - r = Response('Hello') - def view(request): - return r - renderer = object() - result = self.config.derive_view(view) - self.assertFalse(result is view) - response = result(None, None) - self.assertEqual(response, r) - - def test_requestonly_default_method_returns_non_adaptable(self): - request = DummyRequest() - class AView(object): - def __init__(self, request): - pass - def __call__(self): - return None - result = self.config.derive_view(AView) - self.assertFalse(result is AView) - try: - result(None, request) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable ' - 'method __call__ of ' - 'class pyramid.tests.test_config.test_derivations.AView into a ' - 'response object. The value returned was None. You may have ' - 'forgotten to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_requestonly_nondefault_method_returns_non_adaptable(self): - request = DummyRequest() - class AView(object): - def __init__(self, request): - pass - def theviewmethod(self): - return None - result = self.config.derive_view(AView, attr='theviewmethod') - self.assertFalse(result is AView) - try: - result(None, request) - except ValueError as e: - self.assertEqual( - e.args[0], - 'Could not convert return value of the view callable ' - 'method theviewmethod of ' - 'class pyramid.tests.test_config.test_derivations.AView into a ' - 'response object. The value returned was None. You may have ' - 'forgotten to return a value from the view callable.' - ) - else: # pragma: no cover - raise AssertionError - - def test_requestonly_function(self): - response = DummyResponse() - def view(request): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(result(None, None), response) - - def test_requestonly_function_with_renderer(self): - response = DummyResponse() - class moo(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, 'OK') - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - def view(request): - return 'OK' - result = self.config.derive_view(view, renderer=moo()) - self.assertFalse(result.__wraps__ is view) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_requestonly_function_with_renderer_request_override(self): - def moo(info): - def inner(value, system): - self.assertEqual(value, 'OK') - self.assertEqual(system['request'], request) - self.assertEqual(system['context'], context) - return b'moo' - return inner - def view(request): - return 'OK' - self.config.add_renderer('moo', moo) - result = self.config.derive_view(view, renderer='string') - self.assertFalse(result is view) - request = self._makeRequest() - request.override_renderer = 'moo' - context = testing.DummyResource() - self.assertEqual(result(context, request).body, b'moo') - - def test_requestonly_function_with_renderer_request_has_view(self): - response = DummyResponse() - class moo(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, 'OK') - self.assertEqual(view_inst, 'view') - self.assertEqual(ctx, context) - return response - def clone(self): - return self - def view(request): - return 'OK' - result = self.config.derive_view(view, renderer=moo()) - self.assertFalse(result.__wraps__ is view) - request = self._makeRequest() - request.__view__ = 'view' - context = testing.DummyResource() - r = result(context, request) - self.assertEqual(r, response) - self.assertFalse(hasattr(request, '__view__')) - - def test_class_without_attr(self): - response = DummyResponse() - class View(object): - def __init__(self, request): - pass - def __call__(self): - return response - result = self.config.derive_view(View) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, View) - - def test_class_with_attr(self): - response = DummyResponse() - class View(object): - def __init__(self, request): - pass - def another(self): - return response - result = self.config.derive_view(View, attr='another') - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, View) - - def test_as_function_context_and_request(self): - def view(context, request): - return 'OK' - result = self.config.derive_view(view) - self.assertTrue(result.__wraps__ is view) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(view(None, None), 'OK') - - def test_as_function_requestonly(self): - response = DummyResponse() - def view(request): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_as_newstyle_class_context_and_request(self): - response = DummyResponse() - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_newstyle_class_requestonly(self): - response = DummyResponse() - class view(object): - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_oldstyle_class_context_and_request(self): - response = DummyResponse() - class view: - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_oldstyle_class_requestonly(self): - response = DummyResponse() - class view: - def __init__(self, context, request): - pass - def __call__(self): - return response - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - self.assertEqual(result(None, request), response) - self.assertEqual(request.__view__.__class__, view) - - def test_as_instance_context_and_request(self): - response = DummyResponse() - class View: - def __call__(self, context, request): - return response - view = View() - result = self.config.derive_view(view) - self.assertTrue(result.__wraps__ is view) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_as_instance_requestonly(self): - response = DummyResponse() - class View: - def __call__(self, request): - return response - view = View() - result = self.config.derive_view(view) - self.assertFalse(result is view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertTrue('test_derivations' in result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - self.assertEqual(result(None, None), response) - - def test_with_debug_authorization_no_authpol(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_authn_policy_no_authz_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict(debug_authorization=True) - from pyramid.interfaces import IAuthenticationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - logger = self._registerLogger() - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_authz_policy_no_authn_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict(debug_authorization=True) - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - logger = self._registerLogger() - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed " - "(no authorization policy in use)") - - def test_with_debug_authorization_no_permission(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - self._registerSecurityPolicy(True) - logger = self._registerLogger() - result = self.config._derive_view(view) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): Allowed (" - "no permission registered)") - - def test_debug_auth_permission_authpol_permitted(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(True) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): True") - - def test_debug_auth_permission_authpol_permitted_no_request(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(True) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - self.assertEqual(result(None, None), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url None (view name " - "None against context None): True") - - def test_debug_auth_permission_authpol_denied(self): - from pyramid.httpexceptions import HTTPForbidden - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(False) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertEqual(result.__call_permissive__.__wraps__, view) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertRaises(HTTPForbidden, result, None, request) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): False") - - def test_debug_auth_permission_authpol_denied2(self): - view = lambda *arg: 'OK' - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - self._registerLogger() - self._registerSecurityPolicy(False) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - permitted = result.__permitted__(None, None) - self.assertEqual(permitted, False) - - def test_debug_auth_permission_authpol_overridden(self): - from pyramid.security import NO_PERMISSION_REQUIRED - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = dict( - debug_authorization=True, reload_templates=True) - logger = self._registerLogger() - self._registerSecurityPolicy(False) - result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED) - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - self.assertEqual(len(logger.messages), 1) - self.assertEqual(logger.messages[0], - "debug_authorization of url url (view name " - "'view_name' against context None): " - "Allowed (NO_PERMISSION_REQUIRED)") - - def test_secured_view_authn_policy_no_authz_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - from pyramid.interfaces import IAuthenticationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - - def test_secured_view_authz_policy_no_authn_policy(self): - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - from pyramid.interfaces import IAuthorizationPolicy - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - result = self.config._derive_view(view, permission='view') - self.assertEqual(view.__module__, result.__module__) - self.assertEqual(view.__doc__, result.__doc__) - self.assertEqual(view.__name__, result.__name__) - self.assertFalse(hasattr(result, '__call_permissive__')) - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - self.assertEqual(result(None, request), response) - - def test_secured_view_raises_forbidden_no_name(self): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - from pyramid.httpexceptions import HTTPForbidden - response = DummyResponse() - view = lambda *arg: response - self.config.registry.settings = {} - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - result = self.config._derive_view(view, permission='view') - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - try: - result(None, request) - except HTTPForbidden as e: - self.assertEqual(e.message, - 'Unauthorized: failed permission check') - else: # pragma: no cover - raise AssertionError - - def test_secured_view_raises_forbidden_with_name(self): - from pyramid.interfaces import IAuthenticationPolicy - from pyramid.interfaces import IAuthorizationPolicy - from pyramid.httpexceptions import HTTPForbidden - def myview(request): pass - self.config.registry.settings = {} - policy = DummySecurityPolicy(False) - self.config.registry.registerUtility(policy, IAuthenticationPolicy) - self.config.registry.registerUtility(policy, IAuthorizationPolicy) - result = self.config._derive_view(myview, permission='view') - request = self._makeRequest() - request.view_name = 'view_name' - request.url = 'url' - try: - result(None, request) - except HTTPForbidden as e: - self.assertEqual(e.message, - 'Unauthorized: myview failed permission check') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_view_has_no_name(self): - from pyramid.exceptions import PredicateMismatch - response = DummyResponse() - view = lambda *arg: response - def predicate1(context, request): - return False - predicate1.text = lambda *arg: 'text' - result = self.config._derive_view(view, predicates=[predicate1]) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view (text)') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_view_has_name(self): - from pyramid.exceptions import PredicateMismatch - def myview(request): pass - def predicate1(context, request): - return False - predicate1.text = lambda *arg: 'text' - result = self.config._derive_view(myview, predicates=[predicate1]) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view myview (text)') - else: # pragma: no cover - raise AssertionError - - def test_predicate_mismatch_exception_has_text_in_detail(self): - from pyramid.exceptions import PredicateMismatch - def myview(request): pass - def predicate1(context, request): - return True - predicate1.text = lambda *arg: 'pred1' - def predicate2(context, request): - return False - predicate2.text = lambda *arg: 'pred2' - result = self.config._derive_view(myview, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - try: - result(None, None) - except PredicateMismatch as e: - self.assertEqual(e.detail, - 'predicate mismatch for view myview (pred2)') - else: # pragma: no cover - raise AssertionError - - def test_with_predicates_all(self): - response = DummyResponse() - view = lambda *arg: response - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - result = self.config._derive_view(view, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - next = result(None, None) - self.assertEqual(next, response) - self.assertEqual(predicates, [True, True]) - - def test_with_predicates_checker(self): - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - def predicate2(context, request): - predicates.append(True) - return True - result = self.config._derive_view(view, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - next = result.__predicated__(None, None) - self.assertEqual(next, True) - self.assertEqual(predicates, [True, True]) - - def test_with_predicates_notall(self): - from pyramid.httpexceptions import HTTPNotFound - view = lambda *arg: 'OK' - predicates = [] - def predicate1(context, request): - predicates.append(True) - return True - predicate1.text = lambda *arg: 'text' - def predicate2(context, request): - predicates.append(True) - return False - predicate2.text = lambda *arg: 'text' - result = self.config._derive_view(view, - predicates=[predicate1, predicate2]) - request = self._makeRequest() - request.method = 'POST' - self.assertRaises(HTTPNotFound, result, None, None) - self.assertEqual(predicates, [True, True]) - - def test_with_wrapper_viewname(self): - from pyramid.response import Response - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - def outer_view(context, request): - self.assertEqual(request.wrapped_response, inner_response) - self.assertEqual(request.wrapped_body, inner_response.body) - self.assertEqual(request.wrapped_view.__original_view__, - inner_view) - return Response(b'outer ' + request.wrapped_body) - self.config.registry.registerAdapter( - outer_view, (IViewClassifier, None, None), IView, 'owrap') - result = self.config._derive_view(inner_view, viewname='inner', - wrapper_viewname='owrap') - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - response = result(None, request) - self.assertEqual(response.body, b'outer OK') - - def test_with_wrapper_viewname_notfound(self): - from pyramid.response import Response - inner_response = Response('OK') - def inner_view(context, request): - return inner_response - wrapped = self.config._derive_view(inner_view, viewname='inner', - wrapper_viewname='owrap') - request = self._makeRequest() - self.assertRaises(ValueError, wrapped, None, request) - - def test_as_newstyle_class_context_and_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View(object): - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - result = self.config._derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_newstyle_class_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View(object): - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - result = self.config.derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_oldstyle_cls_context_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def __init__(self, context, request): - pass - def index(self): - return {'a':'1'} - result = self.config.derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst.__class__, View) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def __init__(self, request): - pass - def index(self): - return {'a':'1'} - result = self.config.derive_view(View, - renderer=renderer(), attr='index') - self.assertFalse(result is View) - self.assertEqual(result.__module__, View.__module__) - self.assertEqual(result.__doc__, View.__doc__) - self.assertEqual(result.__name__, View.__name__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_instance_context_and_request_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def index(self, context, request): - return {'a':'1'} - view = View() - result = self.config.derive_view(view, - renderer=renderer(), attr='index') - self.assertFalse(result is view) - self.assertEqual(result.__module__, view.__module__) - self.assertEqual(result.__doc__, view.__doc__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_as_instance_requestonly_attr_and_renderer(self): - response = DummyResponse() - class renderer(object): - def render_view(inself, req, resp, view_inst, ctx): - self.assertEqual(req, request) - self.assertEqual(resp, {'a':'1'}) - self.assertEqual(view_inst, view) - self.assertEqual(ctx, context) - return response - def clone(self): - return self - class View: - def index(self, request): - return {'a':'1'} - view = View() - result = self.config.derive_view(view, - renderer=renderer(), attr='index') - self.assertFalse(result is view) - self.assertEqual(result.__module__, view.__module__) - self.assertEqual(result.__doc__, view.__doc__) - request = self._makeRequest() - context = testing.DummyResource() - self.assertEqual(result(context, request), response) - - def test_with_view_mapper_config_specified(self): - response = DummyResponse() - class mapper(object): - def __init__(self, **kw): - self.kw = kw - def __call__(self, view): - def wrapped(context, request): - return response - return wrapped - def view(context, request): return 'NOTOK' - result = self.config._derive_view(view, mapper=mapper) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_with_view_mapper_view_specified(self): - from pyramid.response import Response - response = Response() - def mapper(**kw): - def inner(view): - def superinner(context, request): - self.assertEqual(request, None) - return response - return superinner - return inner - def view(context, request): return 'NOTOK' - view.__view_mapper__ = mapper - result = self.config.derive_view(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_with_view_mapper_default_mapper_specified(self): - from pyramid.response import Response - response = Response() - def mapper(**kw): - def inner(view): - def superinner(context, request): - self.assertEqual(request, None) - return response - return superinner - return inner - self.config.set_view_mapper(mapper) - def view(context, request): return 'NOTOK' - result = self.config.derive_view(view) - self.assertFalse(result.__wraps__ is view) - self.assertEqual(result(None, None), response) - - def test_attr_wrapped_view_branching_default_phash(self): - from pyramid.config.util import DEFAULT_PHASH - def view(context, request): pass - result = self.config._derive_view(view, phash=DEFAULT_PHASH) - self.assertEqual(result.__wraps__, view) - - def test_attr_wrapped_view_branching_nondefault_phash(self): - def view(context, request): pass - result = self.config._derive_view(view, phash='nondefault') - self.assertNotEqual(result, view) - - def test_http_cached_view_integer(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, http_cache=3600) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600') - - def test_http_cached_view_timedelta(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, - http_cache=datetime.timedelta(hours=1)) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600') - - def test_http_cached_view_tuple(self): - import datetime - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, - http_cache=(3600, {'public':True})) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - expires = parse_httpdate(headers['Expires']) - assert_similar_datetime(expires, when) - self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') - - def test_http_cached_view_tuple_seconds_None(self): - from pyramid.response import Response - response = Response('OK') - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, - http_cache=(None, {'public':True})) - self.assertFalse(result is inner_view) - self.assertEqual(inner_view.__module__, result.__module__) - self.assertEqual(inner_view.__doc__, result.__doc__) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertEqual(headers['Cache-Control'], 'public') - - def test_http_cached_view_prevent_auto_set(self): - from pyramid.response import Response - response = Response() - response.cache_control.prevent_auto = True - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, http_cache=3600) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) # doesn't blow up - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertFalse('Cache-Control' in headers) - - def test_http_cached_prevent_http_cache_in_settings(self): - self.config.registry.settings['prevent_http_cache'] = True - from pyramid.response import Response - response = Response() - def inner_view(context, request): - return response - result = self.config._derive_view(inner_view, http_cache=3600) - request = self._makeRequest() - result = result(None, request) - self.assertEqual(result, response) - headers = dict(result.headerlist) - self.assertFalse('Expires' in headers) - self.assertFalse('Cache-Control' in headers) - - def test_http_cached_view_bad_tuple(self): - def view(request): pass - self.assertRaises(ConfigurationError, self.config._derive_view, - view, http_cache=(None,)) - - -class TestDerivationOrder(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def test_right_order_user_sorted(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1') - self.config.add_view_deriver(None, 'deriv2', over='deriv1') - self.config.add_view_deriver(None, 'deriv3', under='deriv2') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'deriv2', - 'deriv3', - 'deriv1', - 'rendered_view', - ], dlist) - - def test_right_order_implicit(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1') - self.config.add_view_deriver(None, 'deriv2') - self.config.add_view_deriver(None, 'deriv3') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'deriv3', - 'deriv2', - 'deriv1', - 'rendered_view', - ], dlist) - - def test_right_order_under_rendered_view(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'rendered_view', - 'deriv1', - ], dlist) - - - def test_right_order_under_rendered_view_others(self): - from pyramid.interfaces import IViewDerivers - - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') - self.config.add_view_deriver(None, 'deriv2') - self.config.add_view_deriver(None, 'deriv3') - - derivers = self.config.registry.getUtility(IViewDerivers) - derivers_sorted = derivers.sorted() - dlist = [d for (d, _) in derivers_sorted] - self.assertEqual([ - 'authdebug_view', - 'secured_view', - 'owrapped_view', - 'http_cached_view', - 'decorated_view', - 'deriv3', - 'deriv2', - 'rendered_view', - 'deriv1', - ], dlist) - - -class TestAddDeriver(unittest.TestCase): - - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def test_add_single_deriver(self): - response = DummyResponse() - response.deriv = False - view = lambda *arg: response - - def deriv(view, info): - self.assertFalse(response.deriv) - response.deriv = True - return view - - result = self.config._derive_view(view) - self.assertFalse(response.deriv) - self.config.add_view_deriver(deriv, 'test_deriv') - - result = self.config._derive_view(view) - self.assertTrue(response.deriv) - - def test_override_deriver(self): - flags = {} - - class AView: - def __init__(self): - self.response = DummyResponse() - - def deriv1(view, value, **kw): - flags['deriv1'] = True - return view - - def deriv2(view, value, **kw): - flags['deriv2'] = True - return view - - view1 = AView() - self.config.add_view_deriver(deriv1, 'test_deriv') - result = self.config._derive_view(view1) - self.assertTrue(flags.get('deriv1')) - self.assertFalse(flags.get('deriv2')) - - flags.clear() - view2 = AView() - self.config.add_view_deriver(deriv2, 'test_deriv') - result = self.config._derive_view(view2) - self.assertFalse(flags.get('deriv1')) - self.assertTrue(flags.get('deriv2')) - - def test_add_multi_derivers_ordered(self): - response = DummyResponse() - view = lambda *arg: response - response.deriv = [] - - def deriv1(view, value, **kw): - response.deriv.append('deriv1') - return view - - def deriv2(view, value, **kw): - response.deriv.append('deriv2') - return view - - def deriv3(view, value, **kw): - response.deriv.append('deriv3') - return view - - self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') - self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') - result = self.config._derive_view(view) - self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) - - -class TestDeriverIntegration(unittest.TestCase): - def setUp(self): - self.config = testing.setUp() - - def tearDown(self): - self.config = None - testing.tearDown() - - def _getViewCallable(self, config, ctx_iface=None, request_iface=None, - name=''): - from zope.interface import Interface - from pyramid.interfaces import IRequest - from pyramid.interfaces import IView - from pyramid.interfaces import IViewClassifier - from pyramid.interfaces import IExceptionViewClassifier - classifier = IViewClassifier - if ctx_iface is None: - ctx_iface = Interface - if request_iface is None: - request_iface = IRequest - return config.registry.adapters.lookup( - (classifier, request_iface, ctx_iface), IView, name=name, - default=None) - - def _makeRequest(self, config): - request = DummyRequest() - request.registry = config.registry - return request - - def test_view_options(self): - response = DummyResponse() - view = lambda *arg: response - response.deriv = [] - - def deriv1(view, info): - response.deriv.append(info.options['deriv1']) - return view - deriv1.options = ('deriv1',) - - def deriv2(view, info): - response.deriv.append(info.options['deriv2']) - return view - deriv2.options = ('deriv2',) - - self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2') - self.config.add_view(view, deriv1='test1', deriv2='test2') - - wrapper = self._getViewCallable(self.config) - request = self._makeRequest(self.config) - request.method = 'GET' - self.assertEqual(wrapper(None, request), response) - self.assertEqual(['test1', 'test2'], response.deriv) - - def test_unexpected_view_options(self): - from pyramid.exceptions import ConfigurationError - def deriv1(view, info): pass - self.config.add_view_deriver(deriv1, 'deriv1') - self.assertRaises( - ConfigurationError, - lambda: self.config.add_view(lambda r: {}, deriv1='test1')) - -@implementer(IResponse) -class DummyResponse(object): - content_type = None - default_content_type = None - body = None - -class DummyRequest: - subpath = () - matchdict = None - request_iface = IRequest - - def __init__(self, environ=None): - if environ is None: - environ = {} - self.environ = environ - self.params = {} - self.cookies = {} - self.response = DummyResponse() - -class DummyLogger: - def __init__(self): - self.messages = [] - def info(self, msg): - self.messages.append(msg) - warn = info - debug = info - -class DummySecurityPolicy: - def __init__(self, permitted=True): - self.permitted = permitted - - def effective_principals(self, request): - return [] - - def permits(self, context, principals, permission): - return self.permitted - -def parse_httpdate(s): - import datetime - # cannot use %Z, must use literal GMT; Jython honors timezone - # but CPython does not - return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT") - -def assert_similar_datetime(one, two): - for attr in ('year', 'month', 'day', 'hour', 'minute'): - one_attr = getattr(one, attr) - two_attr = getattr(two, attr) - if not one_attr == two_attr: # pragma: no cover - raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py new file mode 100644 index 000000000..dfac827dc --- /dev/null +++ b/pyramid/tests/test_viewderivers.py @@ -0,0 +1,1376 @@ +import unittest +from zope.interface import implementer + +from pyramid import testing +from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import ( + IResponse, + IRequest, + ) + +class TestDeriveView(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def _makeRequest(self): + request = DummyRequest() + request.registry = self.config.registry + return request + + def _registerLogger(self): + from pyramid.interfaces import IDebugLogger + logger = DummyLogger() + self.config.registry.registerUtility(logger, IDebugLogger) + return logger + + def _registerSecurityPolicy(self, permissive): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(permissive) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + + def test_function_returns_non_adaptable(self): + def view(request): + return None + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable function ' + 'pyramid.tests.test_viewderivers.view into a response ' + 'object. The value returned was None. You may have forgotten ' + 'to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_non_adaptable_dict(self): + def view(request): + return {'a':1} + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + "Could not convert return value of the view callable function " + "pyramid.tests.test_viewderivers.view into a response " + "object. The value returned was {'a': 1}. You may have " + "forgotten to define a renderer in the view configuration." + ) + else: # pragma: no cover + raise AssertionError + + def test_instance_returns_non_adaptable(self): + class AView(object): + def __call__(self, request): + return None + view = AView() + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + msg = e.args[0] + self.assertTrue(msg.startswith( + 'Could not convert return value of the view callable object ' + ' into a response object. The value returned was None. You ' + 'may have forgotten to return a value from the view callable.')) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_true_Response_no_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + result = self.config.derive_view(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_function_returns_true_Response_with_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + renderer = object() + result = self.config.derive_view(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_requestonly_default_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def __call__(self): + return None + result = self.config.derive_view(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method __call__ of ' + 'class pyramid.tests.test_viewderivers.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_nondefault_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def theviewmethod(self): + return None + result = self.config.derive_view(AView, attr='theviewmethod') + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method theviewmethod of ' + 'class pyramid.tests.test_viewderivers.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_function(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(result(None, None), response) + + def test_requestonly_function_with_renderer(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_requestonly_function_with_renderer_request_override(self): + def moo(info): + def inner(value, system): + self.assertEqual(value, 'OK') + self.assertEqual(system['request'], request) + self.assertEqual(system['context'], context) + return b'moo' + return inner + def view(request): + return 'OK' + self.config.add_renderer('moo', moo) + result = self.config.derive_view(view, renderer='string') + self.assertFalse(result is view) + request = self._makeRequest() + request.override_renderer = 'moo' + context = testing.DummyResource() + self.assertEqual(result(context, request).body, b'moo') + + def test_requestonly_function_with_renderer_request_has_view(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, 'view') + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + request.__view__ = 'view' + context = testing.DummyResource() + r = result(context, request) + self.assertEqual(r, response) + self.assertFalse(hasattr(request, '__view__')) + + def test_class_without_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def __call__(self): + return response + result = self.config.derive_view(View) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_class_with_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def another(self): + return response + result = self.config.derive_view(View, attr='another') + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_as_function_context_and_request(self): + def view(context, request): + return 'OK' + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(view(None, None), 'OK') + + def test_as_function_requestonly(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_newstyle_class_context_and_request(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_newstyle_class_requestonly(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_context_and_request(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_requestonly(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_instance_context_and_request(self): + response = DummyResponse() + class View: + def __call__(self, context, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_instance_requestonly(self): + response = DummyResponse() + class View: + def __call__(self, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertTrue('test_viewderivers' in result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_with_debug_authorization_no_authpol(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_no_permission(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerSecurityPolicy(True) + logger = self._registerLogger() + result = self.config._derive_view(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed (" + "no permission registered)") + + def test_debug_auth_permission_authpol_permitted(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): True") + + def test_debug_auth_permission_authpol_permitted_no_request(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + self.assertEqual(result(None, None), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url None (view name " + "None against context None): True") + + def test_debug_auth_permission_authpol_denied(self): + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertRaises(HTTPForbidden, result, None, request) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): False") + + def test_debug_auth_permission_authpol_denied2(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + permitted = result.__permitted__(None, None) + self.assertEqual(permitted, False) + + def test_debug_auth_permission_authpol_overridden(self): + from pyramid.security import NO_PERMISSION_REQUIRED + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): " + "Allowed (NO_PERMISSION_REQUIRED)") + + def test_secured_view_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_raises_forbidden_no_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_secured_view_raises_forbidden_with_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + def myview(request): pass + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(myview, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: myview failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_no_name(self): + from pyramid.exceptions import PredicateMismatch + response = DummyResponse() + view = lambda *arg: response + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(view, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_name(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(myview, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_exception_has_text_in_detail(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return True + predicate1.text = lambda *arg: 'pred1' + def predicate2(context, request): + return False + predicate2.text = lambda *arg: 'pred2' + result = self.config._derive_view(myview, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (pred2)') + else: # pragma: no cover + raise AssertionError + + def test_with_predicates_all(self): + response = DummyResponse() + view = lambda *arg: response + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result(None, None) + self.assertEqual(next, response) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_checker(self): + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result.__predicated__(None, None) + self.assertEqual(next, True) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_notall(self): + from pyramid.httpexceptions import HTTPNotFound + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + predicate1.text = lambda *arg: 'text' + def predicate2(context, request): + predicates.append(True) + return False + predicate2.text = lambda *arg: 'text' + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + self.assertRaises(HTTPNotFound, result, None, None) + self.assertEqual(predicates, [True, True]) + + def test_with_wrapper_viewname(self): + from pyramid.response import Response + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + def outer_view(context, request): + self.assertEqual(request.wrapped_response, inner_response) + self.assertEqual(request.wrapped_body, inner_response.body) + self.assertEqual(request.wrapped_view.__original_view__, + inner_view) + return Response(b'outer ' + request.wrapped_body) + self.config.registry.registerAdapter( + outer_view, (IViewClassifier, None, None), IView, 'owrap') + result = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + response = result(None, request) + self.assertEqual(response.body, b'outer OK') + + def test_with_wrapper_viewname_notfound(self): + from pyramid.response import Response + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + wrapped = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + request = self._makeRequest() + self.assertRaises(ValueError, wrapped, None, request) + + def test_as_newstyle_class_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config._derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_newstyle_class_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_context_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, context, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_with_view_mapper_config_specified(self): + response = DummyResponse() + class mapper(object): + def __init__(self, **kw): + self.kw = kw + def __call__(self, view): + def wrapped(context, request): + return response + return wrapped + def view(context, request): return 'NOTOK' + result = self.config._derive_view(view, mapper=mapper) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_view_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + def view(context, request): return 'NOTOK' + view.__view_mapper__ = mapper + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_default_mapper_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + self.config.set_view_mapper(mapper) + def view(context, request): return 'NOTOK' + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_attr_wrapped_view_branching_default_phash(self): + from pyramid.config.util import DEFAULT_PHASH + def view(context, request): pass + result = self.config._derive_view(view, phash=DEFAULT_PHASH) + self.assertEqual(result.__wraps__, view) + + def test_attr_wrapped_view_branching_nondefault_phash(self): + def view(context, request): pass + result = self.config._derive_view(view, phash='nondefault') + self.assertNotEqual(result, view) + + def test_http_cached_view_integer(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_timedelta(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=datetime.timedelta(hours=1)) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_tuple(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(3600, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') + + def test_http_cached_view_tuple_seconds_None(self): + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(None, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertEqual(headers['Cache-Control'], 'public') + + def test_http_cached_view_prevent_auto_set(self): + from pyramid.response import Response + response = Response() + response.cache_control.prevent_auto = True + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) # doesn't blow up + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_prevent_http_cache_in_settings(self): + self.config.registry.settings['prevent_http_cache'] = True + from pyramid.response import Response + response = Response() + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_view_bad_tuple(self): + def view(request): pass + self.assertRaises(ConfigurationError, self.config._derive_view, + view, http_cache=(None,)) + + +class TestDerivationOrder(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_right_order_user_sorted(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2', over='deriv1') + self.config.add_view_deriver(None, 'deriv3', under='deriv2') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv2', + 'deriv3', + 'deriv1', + 'rendered_view', + ], dlist) + + def test_right_order_implicit(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'deriv1', + 'rendered_view', + ], dlist) + + def test_right_order_under_rendered_view(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'rendered_view', + 'deriv1', + ], dlist) + + + def test_right_order_under_rendered_view_others(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'authdebug_view', + 'secured_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'rendered_view', + 'deriv1', + ], dlist) + + +class TestAddDeriver(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_add_single_deriver(self): + response = DummyResponse() + response.deriv = False + view = lambda *arg: response + + def deriv(view, info): + self.assertFalse(response.deriv) + response.deriv = True + return view + + result = self.config._derive_view(view) + self.assertFalse(response.deriv) + self.config.add_view_deriver(deriv, 'test_deriv') + + result = self.config._derive_view(view) + self.assertTrue(response.deriv) + + def test_override_deriver(self): + flags = {} + + class AView: + def __init__(self): + self.response = DummyResponse() + + def deriv1(view, value, **kw): + flags['deriv1'] = True + return view + + def deriv2(view, value, **kw): + flags['deriv2'] = True + return view + + view1 = AView() + self.config.add_view_deriver(deriv1, 'test_deriv') + result = self.config._derive_view(view1) + self.assertTrue(flags.get('deriv1')) + self.assertFalse(flags.get('deriv2')) + + flags.clear() + view2 = AView() + self.config.add_view_deriver(deriv2, 'test_deriv') + result = self.config._derive_view(view2) + self.assertFalse(flags.get('deriv1')) + self.assertTrue(flags.get('deriv2')) + + def test_add_multi_derivers_ordered(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, value, **kw): + response.deriv.append('deriv1') + return view + + def deriv2(view, value, **kw): + response.deriv.append('deriv2') + return view + + def deriv3(view, value, **kw): + response.deriv.append('deriv3') + return view + + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') + result = self.config._derive_view(view) + self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + + +class TestDeriverIntegration(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def _getViewCallable(self, config, ctx_iface=None, request_iface=None, + name=''): + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), IView, name=name, + default=None) + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def test_view_options(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, info): + response.deriv.append(info.options['deriv1']) + return view + deriv1.options = ('deriv1',) + + def deriv2(view, info): + response.deriv.append(info.options['deriv2']) + return view + deriv2.options = ('deriv2',) + + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2') + self.config.add_view(view, deriv1='test1', deriv2='test2') + + wrapper = self._getViewCallable(self.config) + request = self._makeRequest(self.config) + request.method = 'GET' + self.assertEqual(wrapper(None, request), response) + self.assertEqual(['test1', 'test2'], response.deriv) + + def test_unexpected_view_options(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + self.config.add_view_deriver(deriv1, 'deriv1') + self.assertRaises( + ConfigurationError, + lambda: self.config.add_view(lambda r: {}, deriv1='test1')) + +@implementer(IResponse) +class DummyResponse(object): + content_type = None + default_content_type = None + body = None + +class DummyRequest: + subpath = () + matchdict = None + request_iface = IRequest + + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + self.response = DummyResponse() + +class DummyLogger: + def __init__(self): + self.messages = [] + def info(self, msg): + self.messages.append(msg) + warn = info + debug = info + +class DummySecurityPolicy: + def __init__(self, permitted=True): + self.permitted = permitted + + def effective_principals(self, request): + return [] + + def permits(self, context, principals, permission): + return self.permitted + +def parse_httpdate(s): + import datetime + # cannot use %Z, must use literal GMT; Jython honors timezone + # but CPython does not + return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT") + +def assert_similar_datetime(one, two): + for attr in ('year', 'month', 'day', 'hour', 'minute'): + one_attr = getattr(one, attr) + two_attr = getattr(two, attr) + if not one_attr == two_attr: # pragma: no cover + raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py new file mode 100644 index 000000000..99baf46f9 --- /dev/null +++ b/pyramid/viewderivers.py @@ -0,0 +1,453 @@ +import inspect + +from zope.interface import ( + implementer, + provider, + ) + +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.response import Response + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + IDebugLogger, + IResponse, + IViewMapper, + IViewMapperFactory, + ) + +from pyramid.compat import ( + is_bound_method, + is_unbound_method, + ) + +from pyramid.config.util import ( + DEFAULT_PHASH, + MAX_ORDER, + takes_one_arg, + ) + +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, + ) +from pyramid.httpexceptions import HTTPForbidden +from pyramid.util import object_description +from pyramid.view import render_view_to_response +from pyramid import renderers + + +def view_description(view): + try: + return view.__text__ + except AttributeError: + # custom view mappers might not add __text__ + return object_description(view) + +def requestonly(view, attr=None): + return takes_one_arg(view, attr=attr, argname='request') + +@implementer(IViewMapper) +@provider(IViewMapperFactory) +class DefaultViewMapper(object): + def __init__(self, **kw): + self.attr = kw.get('attr') + + def __call__(self, view): + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) + + if inspect.isclass(view): + view = self.map_class(view) + else: + view = self.map_nonclass(view) + return view + + def map_class(self, view): + ronly = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_class_requestonly(view) + else: + mapped_view = self.map_class_native(view) + mapped_view.__text__ = 'method %s of %s' % ( + self.attr or '__call__', object_description(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 = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_nonclass_requestonly(view) + elif self.attr: + mapped_view = self.map_nonclass_attr(view) + if inspect.isroutine(mapped_view): + # This branch will be true if the view is a function or a method. + # We potentially mutate an unwrapped object here if it's a + # function. We do this to avoid function call overhead of + # injecting another wrapper. However, we must wrap if the + # function is a bound method because we can't set attributes on a + # bound method. + if is_bound_method(view): + _mapped_view = mapped_view + def mapped_view(context, request): + return _mapped_view(context, request) + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(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) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + 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) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + 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) + return response + return _requestonly_view + + def map_nonclass_attr(self, view): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + def _attr_view(context, request): + response = getattr(view, self.attr)(context, request) + return response + return _attr_view + + +def wraps_view(wrapper): + def inner(view, info): + wrapper_view = wrapper(view, info) + return preserve_view_attrs(view, wrapper_view) + return inner + +def preserve_view_attrs(view, wrapper): + if view is None: + return wrapper + + if wrapper is view: + return view + + original_view = getattr(view, '__original_view__', None) + + if original_view is None: + original_view = view + + wrapper.__wraps__ = view + wrapper.__original_view__ = original_view + wrapper.__module__ = view.__module__ + wrapper.__doc__ = view.__doc__ + + try: + wrapper.__name__ = view.__name__ + except AttributeError: + wrapper.__name__ = repr(view) + + # attrs that may not exist on "view", but, if so, must be attached to + # "wrapped view" + for attr in ('__permitted__', '__call_permissive__', '__permission__', + '__predicated__', '__predicates__', '__accept__', + '__order__', '__text__'): + try: + setattr(wrapper, attr, getattr(view, attr)) + except AttributeError: + pass + + return wrapper + +def mapped_view(view, info): + mapper = info.options.get('mapper') + if mapper is None: + mapper = getattr(view, '__view_mapper__', None) + if mapper is None: + mapper = info.registry.queryUtility(IViewMapperFactory) + if mapper is None: + mapper = DefaultViewMapper + + mapped_view = mapper(**info.options)(view) + return mapped_view + +mapped_view.options = ('mapper', 'attr') + +def owrapped_view(view, info): + wrapper_viewname = info.options.get('wrapper') + viewname = info.options.get('name') + 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 + +owrapped_view.options = ('name', 'wrapper') + +def http_cached_view(view, info): + if info.settings.get('prevent_http_cache', False): + return view + + seconds = info.options.get('http_cache') + + if seconds is None: + return view + + options = {} + + if isinstance(seconds, (tuple, list)): + try: + seconds, options = seconds + except ValueError: + raise ConfigurationError( + 'If http_cache parameter is a tuple or list, it must be ' + 'in the form (seconds, options); not %s' % (seconds,)) + + def wrapper(context, request): + response = view(context, request) + prevent_caching = getattr(response.cache_control, 'prevent_auto', + False) + if not prevent_caching: + response.cache_expires(seconds, **options) + return response + + return wrapper + +http_cached_view.options = ('http_cache',) + +def secured_view(view, info): + permission = info.options.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 + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + + if authn_policy and authz_policy and (permission is not None): + def _permitted(context, request): + principals = authn_policy.effective_principals(request) + return authz_policy.permits(context, principals, permission) + def _secured_view(context, request): + result = _permitted(context, request) + if result: + return view(context, request) + view_name = getattr(view, '__name__', view) + msg = getattr( + request, 'authdebug_message', + 'Unauthorized: %s failed permission check' % view_name) + raise HTTPForbidden(msg, result=result) + _secured_view.__call_permissive__ = view + _secured_view.__permitted__ = _permitted + _secured_view.__permission__ = permission + wrapped_view = _secured_view + + return wrapped_view + +secured_view.options = ('permission',) + +def authdebug_view(view, info): + wrapped_view = view + settings = info.settings + permission = info.options.get('permission') + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + logger = info.registry.queryUtility(IDebugLogger) + if settings and settings.get('debug_authorization', False): + def _authdebug_view(context, request): + view_name = getattr(request, 'view_name', None) + + if authn_policy and authz_policy: + if permission is NO_PERMISSION_REQUIRED: + msg = 'Allowed (NO_PERMISSION_REQUIRED)' + elif 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)) + if logger: + logger.debug(msg) + if request is not None: + request.authdebug_message = msg + return view(context, request) + + wrapped_view = _authdebug_view + + return wrapped_view + +authdebug_view.options = ('permission',) + +def predicated_view(view, info): + preds = info.predicates + if not preds: + return view + def predicate_wrapper(context, request): + for predicate in preds: + if not predicate(context, request): + view_name = getattr(view, '__name__', view) + raise PredicateMismatch( + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.text())) + return view(context, request) + def checker(context, request): + return all((predicate(context, request) for predicate in + preds)) + predicate_wrapper.__predicated__ = checker + predicate_wrapper.__predicates__ = preds + return predicate_wrapper + +def attr_wrapped_view(view, info): + accept, order, phash = (info.options.get('accept', None), + getattr(info, 'order', MAX_ORDER), + getattr(info, '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 + attr_view.__view_attr__ = info.options.get('attr') + attr_view.__permission__ = info.options.get('permission') + return attr_view + +attr_wrapped_view.options = ('accept', 'attr', 'permission') + +def rendered_view(view, info): + # one way or another this wrapper must produce a Response (unless + # the renderer is a NullRendererHelper) + renderer = info.options.get('renderer') + if renderer is None: + # register a default renderer if you want super-dynamic + # rendering. registering a default renderer will also allow + # override_renderer to work if a renderer is left unspecified for + # a view registration. + def viewresult_to_response(context, request): + result = view(context, request) + if result.__class__ is Response: # common case + response = result + else: + response = info.registry.queryAdapterOrSelf(result, IResponse) + if response is None: + if result is None: + append = (' You may have forgotten to return a value ' + 'from the view callable.') + elif isinstance(result, dict): + append = (' You may have forgotten to define a ' + 'renderer in the view configuration.') + else: + append = '' + + msg = ('Could not convert return value of the view ' + 'callable %s into a response object. ' + 'The value returned was %r.' + append) + + raise ValueError(msg % (view_description(view), result)) + + return response + + return viewresult_to_response + + if renderer is renderers.null_renderer: + return view + + def rendered_view(context, request): + result = view(context, request) + if result.__class__ is Response: # potential common case + response = result + else: + # this must adapt, it can't do a simple interface check + # (avoid trying to render webob responses) + response = info.registry.queryAdapterOrSelf(result, IResponse) + if response is None: + attrs = getattr(request, '__dict__', {}) + if 'override_renderer' in attrs: + # renderer overridden by newrequest event or other + renderer_name = attrs.pop('override_renderer') + view_renderer = renderers.RendererHelper( + name=renderer_name, + package=info.package, + registry=info.registry) + else: + view_renderer = renderer.clone() + if '__view__' in attrs: + view_inst = attrs.pop('__view__') + else: + view_inst = getattr(view, '__original_view__', view) + response = view_renderer.render_view( + request, result, view_inst, context) + return response + + return rendered_view + +rendered_view.options = ('renderer',) + +def decorated_view(view, info): + decorator = info.options.get('decorator') + if decorator is None: + return view + return decorator(view) + +decorated_view.options = ('decorator',) -- cgit v1.2.3 From a3db3cab713fecc0c83c742cfe3f0736b1d94a92 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 7 Apr 2016 01:16:45 -0500 Subject: separate the viewderiver module and allow overriding the mapper --- docs/api/viewderivers.rst | 16 +++++++ docs/narr/hooks.rst | 9 ++-- pyramid/config/views.py | 72 ++++++++++++++++++++++-------- pyramid/tests/test_viewderivers.py | 89 +++++++++++++++++++++++++++++++------- pyramid/viewderivers.py | 16 ++++--- 5 files changed, 156 insertions(+), 46 deletions(-) create mode 100644 docs/api/viewderivers.rst diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst new file mode 100644 index 000000000..a4ec107b6 --- /dev/null +++ b/docs/api/viewderivers.rst @@ -0,0 +1,16 @@ +.. _viewderivers_module: + +:mod:`pyramid.viewderivers` +--------------------------- + +.. automodule:: pyramid.viewderivers + + .. attribute:: INGRESS + + Constant representing the request ingress, for use in ``under`` + arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. + + .. attribute:: MAPPED_VIEW + + Constant representing the closest view deriver, for use in ``over`` + arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index a32e94d1a..3a1ad8363 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1580,12 +1580,6 @@ There are several built-in view derivers that :app:`Pyramid` will automatically apply to any view. Below they are defined in order from furthest to closest to the user-defined :term:`view callable`: -``authdebug_view`` - - Used to output useful debugging information when - ``pyramid.debug_authorization`` is enabled. This element is a no-op - otherwise. - ``secured_view`` Enforce the ``permission`` defined on the view. This element is a no-op if no @@ -1593,6 +1587,9 @@ the user-defined :term:`view callable`: default permission was assigned via :meth:`pyramid.config.Configurator.set_default_permission`. + This element will also output useful debugging information when + ``pyramid.debug_authorization`` is enabled. + ``owrapped_view`` Invokes the wrapped view defined by the ``wrapper`` option. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 2a019726f..1e161177b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -79,6 +79,8 @@ import pyramid.config.predicates import pyramid.viewderivers from pyramid.viewderivers import ( + INGRESS, + MAPPED_VIEW, preserve_view_attrs, view_description, requestonly, @@ -89,6 +91,7 @@ from pyramid.viewderivers import ( from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, + as_sorted_tuple, ) urljoin = urlparse.urljoin @@ -1029,16 +1032,14 @@ class ViewsConfiguratorMixin(object): def _apply_view_derivers(self, info): d = pyramid.viewderivers - # These derivations have fixed order + + # These derivers are not really derivers and so have fixed order outer_derivers = [('attr_wrapped_view', d.attr_wrapped_view), ('predicated_view', d.predicated_view)] - inner_derivers = [('mapped_view', d.mapped_view)] view = info.original_view derivers = self.registry.getUtility(IViewDerivers) - for name, deriver in reversed( - outer_derivers + derivers.sorted() + inner_derivers - ): + for name, deriver in reversed(outer_derivers + derivers.sorted()): view = wraps_view(deriver)(view, info) return view @@ -1090,7 +1091,7 @@ class ViewsConfiguratorMixin(object): self.add_view_predicate(name, factory) @action_method - def add_view_deriver(self, deriver, name, under=None, over=None): + def add_view_deriver(self, deriver, name=None, under=None, over=None): """ .. versionadded:: 1.7 @@ -1105,9 +1106,11 @@ class ViewsConfiguratorMixin(object): restrictions on the name of a view deriver. If left unspecified, the name will be constructed from the name of the ``deriver``. - The ``under`` and ``over`` options may be used to control the ordering + The ``under`` and ``over`` options can be used to control the ordering of view derivers by providing hints about where in the view pipeline - the deriver is used. + the deriver is used. Each option may be a string or a list of strings. + At least one view deriver in each, the over and under directions, must + exist to fully satisfy the constraints. ``under`` means closer to the user-defined :term:`view callable`, and ``over`` means closer to view pipeline ingress. @@ -1122,10 +1125,37 @@ class ViewsConfiguratorMixin(object): """ deriver = self.maybe_dotted(deriver) + if name is None: + name = deriver.__name__ + + if name in (INGRESS,): + raise ConfigurationError('%s is a reserved view deriver name' + % name) + if under is None and over is None: under = 'decorated_view' over = 'rendered_view' + if over is None and name != MAPPED_VIEW: + raise ConfigurationError('must specify an "over" constraint for ' + 'the %s view deriver' % name) + elif over is not None: + over = as_sorted_tuple(over) + + if under is None: + raise ConfigurationError('must specify an "under" constraint for ' + 'the %s view deriver' % name) + else: + under = as_sorted_tuple(under) + + if over is not None and INGRESS in over: + raise ConfigurationError('%s cannot be over view deriver INGRESS' + % name) + + if MAPPED_VIEW in under: + raise ConfigurationError('%s cannot be under view deriver ' + 'MAPPED_VIEW' % name) + discriminator = ('view deriver', name) intr = self.introspectable( 'view derivers', @@ -1139,7 +1169,12 @@ class ViewsConfiguratorMixin(object): def register(): derivers = self.registry.queryUtility(IViewDerivers) if derivers is None: - derivers = TopologicalSorter() + derivers = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=MAPPED_VIEW, + ) self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, before=over, after=under) self.action(discriminator, register, introspectables=(intr,), @@ -1148,24 +1183,23 @@ class ViewsConfiguratorMixin(object): def add_default_view_derivers(self): d = pyramid.viewderivers derivers = [ - ('authdebug_view', d.authdebug_view), ('secured_view', d.secured_view), ('owrapped_view', d.owrapped_view), ('http_cached_view', d.http_cached_view), ('decorated_view', d.decorated_view), + ('rendered_view', d.rendered_view), ] - last = pyramid.util.FIRST + last = INGRESS for name, deriver in derivers: - self.add_view_deriver(deriver, name=name, under=last) + self.add_view_deriver( + deriver, + name=name, + under=last, + over=MAPPED_VIEW, + ) last = name - # ensure rendered_view is over LAST - self.add_view_deriver( - d.rendered_view, - 'rendered_view', - under=last, - over=pyramid.util.LAST, - ) + self.add_view_deriver(d.mapped_view, name=MAPPED_VIEW, under=last) def derive_view(self, view, attr=None, renderer=None): """ diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index dfac827dc..dd142769b 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1103,14 +1103,13 @@ class TestDerivationOrder(unittest.TestCase): from pyramid.interfaces import IViewDerivers self.config.add_view_deriver(None, 'deriv1') - self.config.add_view_deriver(None, 'deriv2', over='deriv1') - self.config.add_view_deriver(None, 'deriv3', under='deriv2') + self.config.add_view_deriver(None, 'deriv2', 'decorated_view', 'deriv1') + self.config.add_view_deriver(None, 'deriv3', 'deriv2', 'deriv1') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1119,6 +1118,7 @@ class TestDerivationOrder(unittest.TestCase): 'deriv3', 'deriv1', 'rendered_view', + 'mapped_view', ], dlist) def test_right_order_implicit(self): @@ -1132,7 +1132,6 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1141,31 +1140,32 @@ class TestDerivationOrder(unittest.TestCase): 'deriv2', 'deriv1', 'rendered_view', + 'mapped_view', ], dlist) def test_right_order_under_rendered_view(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') derivers = self.config.registry.getUtility(IViewDerivers) derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', 'decorated_view', 'rendered_view', 'deriv1', + 'mapped_view', ], dlist) def test_right_order_under_rendered_view_others(self): from pyramid.interfaces import IViewDerivers - self.config.add_view_deriver(None, 'deriv1', under='rendered_view') + self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') self.config.add_view_deriver(None, 'deriv2') self.config.add_view_deriver(None, 'deriv3') @@ -1173,7 +1173,6 @@ class TestDerivationOrder(unittest.TestCase): derivers_sorted = derivers.sorted() dlist = [d for (d, _) in derivers_sorted] self.assertEqual([ - 'authdebug_view', 'secured_view', 'owrapped_view', 'http_cached_view', @@ -1182,6 +1181,7 @@ class TestDerivationOrder(unittest.TestCase): 'deriv2', 'rendered_view', 'deriv1', + 'mapped_view', ], dlist) @@ -1218,11 +1218,11 @@ class TestAddDeriver(unittest.TestCase): def __init__(self): self.response = DummyResponse() - def deriv1(view, value, **kw): + def deriv1(view, info): flags['deriv1'] = True return view - def deriv2(view, value, **kw): + def deriv2(view, info): flags['deriv2'] = True return view @@ -1240,27 +1240,84 @@ class TestAddDeriver(unittest.TestCase): self.assertTrue(flags.get('deriv2')) def test_add_multi_derivers_ordered(self): + from pyramid.viewderivers import INGRESS response = DummyResponse() view = lambda *arg: response response.deriv = [] - def deriv1(view, value, **kw): + def deriv1(view, info): response.deriv.append('deriv1') return view - def deriv2(view, value, **kw): + def deriv2(view, info): response.deriv.append('deriv2') return view - def deriv3(view, value, **kw): + def deriv3(view, info): response.deriv.append('deriv3') return view self.config.add_view_deriver(deriv1, 'deriv1') - self.config.add_view_deriver(deriv2, 'deriv2', under='deriv1') - self.config.add_view_deriver(deriv3, 'deriv3', over='deriv2') + self.config.add_view_deriver(deriv2, 'deriv2', INGRESS, 'deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', 'deriv2', 'deriv1') result = self.config._derive_view(view) - self.assertEqual(response.deriv, ['deriv2', 'deriv3', 'deriv1']) + self.assertEqual(response.deriv, ['deriv1', 'deriv3', 'deriv2']) + + def test_add_deriver_without_name(self): + from pyramid.interfaces import IViewDerivers + def deriv1(view, info): pass + self.config.add_view_deriver(deriv1) + derivers = self.config.registry.getUtility(IViewDerivers) + self.assertTrue('deriv1' in derivers.names) + + def test_add_deriver_reserves_ingress(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS + def deriv1(view, info): pass + self.assertRaises( + ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) + + def test_add_deriver_enforces_over_is_defined(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, under='rendered_view') + except ConfigurationError as ex: + self.assertTrue('must specify an "over" constraint' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_under_is_defined(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, over='rendered_view') + except ConfigurationError as ex: + self.assertTrue('must specify an "under" constraint' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_ingress_is_first(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, under='rendered_view', over=INGRESS) + except ConfigurationError as ex: + self.assertTrue('cannot be over view deriver INGRESS' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_mapped_view_is_last(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver( + deriv1, 'deriv1', 'mapped_view', 'rendered_view') + except ConfigurationError as ex: + self.assertTrue('cannot be under view deriver MAPPED_VIEW' in ex.args[0]) + else: # pragma: no cover + raise AssertionError class TestDeriverIntegration(unittest.TestCase): diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index 99baf46f9..f97099cc8 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -260,6 +260,13 @@ def http_cached_view(view, info): http_cached_view.options = ('http_cache',) def secured_view(view, info): + for wrapper in (_secured_view, _authdebug_view): + view = wraps_view(wrapper)(view, info) + return view + +secured_view.options = ('permission',) + +def _secured_view(view, info): permission = info.options.get('permission') if permission == NO_PERMISSION_REQUIRED: # allow views registered within configurations that have a @@ -291,9 +298,7 @@ def secured_view(view, info): return wrapped_view -secured_view.options = ('permission',) - -def authdebug_view(view, info): +def _authdebug_view(view, info): wrapped_view = view settings = info.settings permission = info.options.get('permission') @@ -330,8 +335,6 @@ def authdebug_view(view, info): return wrapped_view -authdebug_view.options = ('permission',) - def predicated_view(view, info): preds = info.predicates if not preds: @@ -451,3 +454,6 @@ def decorated_view(view, info): return decorator(view) decorated_view.options = ('decorator',) + +MAPPED_VIEW = 'mapped_view' +INGRESS = 'INGRESS' -- cgit v1.2.3 From c231d8174e811eec5a3faeafa5aee60757c6d31f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 8 Apr 2016 00:47:01 -0500 Subject: update constraints for derivers as well as docs --- docs/api/viewderivers.rst | 7 +++--- docs/narr/hooks.rst | 34 +++++++++++++++++++------ pyramid/config/views.py | 51 +++++++++++++++++++------------------- pyramid/tests/test_viewderivers.py | 49 +++++++++++++++++++++--------------- pyramid/viewderivers.py | 2 +- 5 files changed, 85 insertions(+), 58 deletions(-) diff --git a/docs/api/viewderivers.rst b/docs/api/viewderivers.rst index a4ec107b6..2a141501e 100644 --- a/docs/api/viewderivers.rst +++ b/docs/api/viewderivers.rst @@ -10,7 +10,8 @@ Constant representing the request ingress, for use in ``under`` arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. - .. attribute:: MAPPED_VIEW + .. attribute:: VIEW - Constant representing the closest view deriver, for use in ``over`` - arguments to :meth:`pyramid.config.Configurator.add_view_deriver`. + Constant representing the :term:`view callable` at the end of the view + pipeline, for use in ``over`` arguments to + :meth:`pyramid.config.Configurator.add_view_deriver`. diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 3a1ad8363..2c3782387 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -1617,6 +1617,14 @@ the user-defined :term:`view callable`: view pipeline interface to accept ``(context, request)`` from all previous view derivers. +.. warning:: + + Any view derivers defined ``under`` the ``rendered_view`` are not + guaranteed to receive a valid response object. Rather they will receive the + result from the :term:`view mapper` which is likely the original response + returned from the view. This is possibly a dictionary for a renderer but it + may be any Python object that may be adapted into a response. + Custom View Derivers ~~~~~~~~~~~~~~~~~~~~ @@ -1642,7 +1650,7 @@ view pipeline: response.headers['X-View-Performance'] = '%.3f' % (end - start,) return wrapper_view - config.add_view_deriver(timing_view, 'timing view') + config.add_view_deriver(timing_view) View derivers are unique in that they have access to most of the options passed to :meth:`pyramid.config.Configurator.add_view` in order to decide what @@ -1668,7 +1676,7 @@ token unless ``disable_csrf=True`` is passed to the view: require_csrf_view.options = ('disable_csrf',) - config.add_view_deriver(require_csrf_view, 'require_csrf_view') + config.add_view_deriver(require_csrf_view) def protected_view(request): return Response('protected') @@ -1691,13 +1699,19 @@ By default, every new view deriver is added between the ``decorated_view`` and ``rendered_view`` built-in derivers. It is possible to customize this ordering using the ``over`` and ``under`` options. Each option can use the names of other view derivers in order to specify an ordering. There should rarely be a -reason to worry about the ordering of the derivers. +reason to worry about the ordering of the derivers except when the deriver +depends on other operations in the view pipeline. Both ``over`` and ``under`` may also be iterables of constraints. For either option, if one or more constraints was defined, at least one must be satisfied, else a :class:`pyramid.exceptions.ConfigurationError` will be raised. This may be used to define fallback constraints if another deriver is missing. +Two sentinel values exist, :attr:`pyramid.viewderivers.INGRESS` and +:attr:`pyramid.viewderivers.VIEW`, which may be used when specifying +constraints at the edges of the view pipeline. For example, to add a deriver +at the start of the pipeline you may use ``under=INGRESS``. + It is not possible to add a view deriver under the ``mapped_view`` as the :term:`view mapper` is intimately tied to the signature of the user-defined :term:`view callable`. If you simply need to know what the original view @@ -1707,8 +1721,12 @@ deriver. .. warning:: - Any view derivers defined ``under`` the ``rendered_view`` are not - guaranteed to receive a valid response object. Rather they will receive the - result from the :term:`view mapper` which is likely the original response - returned from the view. This is possibly a dictionary for a renderer but it - may be any Python object that may be adapted into a response. + The default constraints for any view deriver are ``over='rendered_view'`` + and ``under='decorated_view'``. When escaping these constraints you must + take care to avoid cyclic dependencies between derivers. For example, if + you want to add a new view deriver before ``secured_view`` then + simply specifying ``over='secured_view'`` is not enough, because the + default is also under ``decorated view`` there will be an unsatisfiable + cycle. You must specify a valid ``under`` constraint as well, such as + ``under=INGRESS`` to fall between INGRESS and ``secured_view`` at the + beginning of the view pipeline. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1e161177b..3f6a9080d 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -80,7 +80,7 @@ import pyramid.viewderivers from pyramid.viewderivers import ( INGRESS, - MAPPED_VIEW, + VIEW, preserve_view_attrs, view_description, requestonly, @@ -1115,9 +1115,12 @@ class ViewsConfiguratorMixin(object): ``under`` means closer to the user-defined :term:`view callable`, and ``over`` means closer to view pipeline ingress. - Specifying neither ``under`` nor ``over`` is equivalent to specifying - ``over='rendered_view'`` and ``under='decorated_view'``, placing the - deriver somewhere between the ``decorated_view`` and ``rendered_view`` + The default value for ``over`` is ``rendered_view`` and ``under`` is + ``decorated_view``. This places the deriver somewhere between the two + in the view pipeline. If the deriver should be placed elsewhere in the + pipeline, such as above ``decorated_view``, then you MUST also specify + ``under`` to something earlier in the order, or a + ``CyclicDependencyError`` will be raised when trying to sort the derivers. See :ref:`view_derivers` for more information. @@ -1128,33 +1131,30 @@ class ViewsConfiguratorMixin(object): if name is None: name = deriver.__name__ - if name in (INGRESS,): + if name in (INGRESS, VIEW): raise ConfigurationError('%s is a reserved view deriver name' % name) - if under is None and over is None: + if under is None: under = 'decorated_view' + + if over is None: over = 'rendered_view' - if over is None and name != MAPPED_VIEW: - raise ConfigurationError('must specify an "over" constraint for ' - 'the %s view deriver' % name) - elif over is not None: - over = as_sorted_tuple(over) + over = as_sorted_tuple(over) + under = as_sorted_tuple(under) - if under is None: - raise ConfigurationError('must specify an "under" constraint for ' - 'the %s view deriver' % name) - else: - under = as_sorted_tuple(under) + if INGRESS in over: + raise ConfigurationError('%s cannot be over INGRESS' % name) - if over is not None and INGRESS in over: - raise ConfigurationError('%s cannot be over view deriver INGRESS' - % name) + # ensure everything is always over mapped_view + if VIEW in over and name != 'mapped_view': + over = as_sorted_tuple(over + ('mapped_view',)) - if MAPPED_VIEW in under: - raise ConfigurationError('%s cannot be under view deriver ' - 'MAPPED_VIEW' % name) + if VIEW in under: + raise ConfigurationError('%s cannot be under VIEW' % name) + if 'mapped_view' in under: + raise ConfigurationError('%s cannot be under "mapped_view"' % name) discriminator = ('view deriver', name) intr = self.introspectable( @@ -1173,7 +1173,7 @@ class ViewsConfiguratorMixin(object): default_before=None, default_after=INGRESS, first=INGRESS, - last=MAPPED_VIEW, + last=VIEW, ) self.registry.registerUtility(derivers, IViewDerivers) derivers.add(name, deriver, before=over, after=under) @@ -1188,6 +1188,7 @@ class ViewsConfiguratorMixin(object): ('http_cached_view', d.http_cached_view), ('decorated_view', d.decorated_view), ('rendered_view', d.rendered_view), + ('mapped_view', d.mapped_view), ] last = INGRESS for name, deriver in derivers: @@ -1195,12 +1196,10 @@ class ViewsConfiguratorMixin(object): deriver, name=name, under=last, - over=MAPPED_VIEW, + over=VIEW, ) last = name - self.add_view_deriver(d.mapped_view, name=MAPPED_VIEW, under=last) - def derive_view(self, view, attr=None, renderer=None): """ Create a :term:`view callable` using the function, instance, diff --git a/pyramid/tests/test_viewderivers.py b/pyramid/tests/test_viewderivers.py index dd142769b..1823beb4d 100644 --- a/pyramid/tests/test_viewderivers.py +++ b/pyramid/tests/test_viewderivers.py @@ -1239,6 +1239,25 @@ class TestAddDeriver(unittest.TestCase): self.assertFalse(flags.get('deriv1')) self.assertTrue(flags.get('deriv2')) + def test_override_mapped_view(self): + from pyramid.viewderivers import VIEW + response = DummyResponse() + view = lambda *arg: response + flags = {} + + def deriv1(view, info): + flags['deriv1'] = True + return view + + result = self.config._derive_view(view) + self.assertFalse(flags.get('deriv1')) + + flags.clear() + self.config.add_view_deriver( + deriv1, name='mapped_view', under='rendered_view', over=VIEW) + result = self.config._derive_view(view) + self.assertTrue(flags.get('deriv1')) + def test_add_multi_derivers_ordered(self): from pyramid.viewderivers import INGRESS response = DummyResponse() @@ -1277,34 +1296,25 @@ class TestAddDeriver(unittest.TestCase): self.assertRaises( ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) - def test_add_deriver_enforces_over_is_defined(self): - from pyramid.exceptions import ConfigurationError - def deriv1(view, info): pass - try: - self.config.add_view_deriver(deriv1, under='rendered_view') - except ConfigurationError as ex: - self.assertTrue('must specify an "over" constraint' in ex.args[0]) - else: # pragma: no cover - raise AssertionError - - def test_add_deriver_enforces_under_is_defined(self): + def test_add_deriver_enforces_ingress_is_first(self): from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS def deriv1(view, info): pass try: - self.config.add_view_deriver(deriv1, over='rendered_view') + self.config.add_view_deriver(deriv1, over=INGRESS) except ConfigurationError as ex: - self.assertTrue('must specify an "under" constraint' in ex.args[0]) + self.assertTrue('cannot be over INGRESS' in ex.args[0]) else: # pragma: no cover raise AssertionError - def test_add_deriver_enforces_ingress_is_first(self): + def test_add_deriver_enforces_view_is_last(self): from pyramid.exceptions import ConfigurationError - from pyramid.viewderivers import INGRESS + from pyramid.viewderivers import VIEW def deriv1(view, info): pass try: - self.config.add_view_deriver(deriv1, under='rendered_view', over=INGRESS) + self.config.add_view_deriver(deriv1, under=VIEW) except ConfigurationError as ex: - self.assertTrue('cannot be over view deriver INGRESS' in ex.args[0]) + self.assertTrue('cannot be under VIEW' in ex.args[0]) else: # pragma: no cover raise AssertionError @@ -1312,10 +1322,9 @@ class TestAddDeriver(unittest.TestCase): from pyramid.exceptions import ConfigurationError def deriv1(view, info): pass try: - self.config.add_view_deriver( - deriv1, 'deriv1', 'mapped_view', 'rendered_view') + self.config.add_view_deriver(deriv1, 'deriv1', under='mapped_view') except ConfigurationError as ex: - self.assertTrue('cannot be under view deriver MAPPED_VIEW' in ex.args[0]) + self.assertTrue('cannot be under "mapped_view"' in ex.args[0]) else: # pragma: no cover raise AssertionError diff --git a/pyramid/viewderivers.py b/pyramid/viewderivers.py index f97099cc8..8061e5d4a 100644 --- a/pyramid/viewderivers.py +++ b/pyramid/viewderivers.py @@ -455,5 +455,5 @@ def decorated_view(view, info): decorated_view.options = ('decorator',) -MAPPED_VIEW = 'mapped_view' +VIEW = 'VIEW' INGRESS = 'INGRESS' -- cgit v1.2.3