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