From e8f26928bf5c8fb8490a72436718cedf8fe19281 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 22 Dec 2010 11:28:58 -0500 Subject: factor all view wrapper funcs into a ViewDeriver class --- pyramid/config.py | 589 ++++++++++++++++++++++++------------------- pyramid/tests/test_config.py | 20 +- 2 files changed, 344 insertions(+), 265 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 93123c119..f6746a20e 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -266,10 +266,14 @@ class Configurator(object): renderer_globals_factory=None, default_permission=None, session_factory=None, + view_deriver=None, autocommit=False, ): if package is None: package = caller_package() + if view_deriver is None: + view_deriver = DefaultViewDeriver + self.view_deriver = view_deriver name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver self.package_name = name_resolver.package_name @@ -342,26 +346,20 @@ class Configurator(object): 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 + deriver = self.view_deriver(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) + return deriver() def _override(self, package, path, override_package, override_prefix, PackageOverrides=PackageOverrides): @@ -2619,149 +2617,312 @@ 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__ - try: - wrapped_view.__name__ = original_view.__name__ - except AttributeError: - wrapped_view.__name__ = repr(original_view) - try: - wrapped_view.__permitted__ = original_view.__permitted__ - except AttributeError: - pass - try: - wrapped_view.__call_permissive__ = original_view.__call_permissive__ - except AttributeError: - pass - try: - wrapped_view.__predicated__ = original_view.__predicated__ - except AttributeError: - pass - try: - wrapped_view.__accept__ = original_view.__accept__ - except AttributeError: - pass - try: - wrapped_view.__order__ = original_view.__order__ - except AttributeError: - pass - return True - -def requestonly(class_or_callable, attr=None): +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 """ - if attr is None: - attr = '__call__' - if inspect.isfunction(class_or_callable): - fn = class_or_callable - elif inspect.isclass(class_or_callable): + return DefaultViewDeriver(view, attr=attr).requestonly() + +def preserve_attrs(wrapped): + def inner(self, view): + wrapped_view = wrapped(self, view) + if wrapped_view is view: + return view + wrapped_view.__module__ = view.__module__ + wrapped_view.__doc__ = view.__doc__ try: - fn = class_or_callable.__init__ + wrapped_view.__name__ = view.__name__ except AttributeError: - return False - else: + wrapped_view.__name__ = repr(view) try: - fn = getattr(class_or_callable, attr) + wrapped_view.__permitted__ = view.__permitted__ except AttributeError: - return False + pass + try: + wrapped_view.__call_permissive__ = view.__call_permissive__ + except AttributeError: + pass + try: + wrapped_view.__predicated__ = view.__predicated__ + except AttributeError: + pass + try: + wrapped_view.__accept__ = view.__accept__ + except AttributeError: + pass + try: + wrapped_view.__order__ = view.__order__ + except AttributeError: + pass + return wrapped_view + return inner + +class DefaultViewDeriver(object): + def __init__(self, view, **kw): + self.kw = kw + self.view = view + self.registry = kw.get('registry') + self.helper = None + self.renderer = kw.get('renderer') + if self.renderer is None and self.registry is not None: + # use default renderer if one exists + default_renderer_factory = self.registry.queryUtility( + IRendererFactory) + if default_renderer_factory is not None: + self.renderer = {'name':None, 'package':kw.get('package')} + if self.renderer is not None: + self.helper = RendererHelper(self.renderer['name'], + package=self.renderer['package'], + registry=self.registry) + if self.registry is not None: + self.authn_policy = self.registry.queryUtility( + IAuthenticationPolicy) + self.authz_policy = self.registry.queryUtility( + IAuthorizationPolicy) + self.logger = self.registry.queryUtility(IDebugLogger) + + def __call__(self): + return self.attr_wrapped_view( + self.predicated_view( + self.authdebug_view( + self.secured_view( + self.owrap_view( + self.map_view(self.view)))))) + + def requestonly(self): + view = self.view + attr = self.kw.get('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 - try: - argspec = inspect.getargspec(fn) - except TypeError: - return False + try: + argspec = inspect.getargspec(fn) + except TypeError: + return False - args = argspec[0] - defaults = argspec[3] + args = argspec[0] + defaults = argspec[3] - if hasattr(fn, 'im_func'): - # it's an instance method + if hasattr(fn, 'im_func'): + # it's an instance method + if not args: + return False + args = args[1:] if not args: return False - args = args[1:] - if not args: - return False - - if len(args) == 1: - return True - elif args[0] == 'request': - if len(args) - len(defaults) == 1: + if len(args) == 1: return True - return False + elif args[0] == 'request': + if len(args) - len(defaults) == 1: + return True -def is_response(ob): - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False + return False + -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 - else: - # its __init__ accepts both context and request - def _class_view(context, request): - inst = view(context, request) - if attr is None: - response = inst() + @preserve_attrs + 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 + + @preserve_attrs + 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 + + @preserve_attrs + 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: - 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 + 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 + + @preserve_attrs + 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 + + @preserve_attrs + 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 + + @preserve_attrs + def map_view(self, view): + attr = self.kw.get('attr') + isclass = inspect.isclass(view) + ronly = self.requestonly() + if isclass and ronly: + view = self.adapt_requestonly_class() + elif isclass: + view = self.adapt_class() + elif ronly: + view = self.adapt_requestonly_func() + elif attr: + view = self.adapt_attr() + elif self.helper is not None: + view = self.adapt_rendered() + return view + + def adapt_requestonly_class(self): + # its a class that has an __init__ which only accepts request + view = self.view + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer + 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 + return _class_requestonly_view + + def adapt_class(self): + # its a class that has an __init__ which accepts both context and + # request + view = self.view + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer + 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 + return _class_view + + def adapt_requestonly_func(self): + # its a function or instance that has a __call__ accepts only a + # single request argument + view = self.view + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer def _requestonly_view(context, request): if attr is None: response = view(request) @@ -2780,9 +2941,15 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _requestonly_view - - elif attr: + return _requestonly_view + + def adapt_attr(self): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + view = self.view + attr = self.kw.get('attr') + helper = self.helper + renderer = self.renderer def _attr_view(context, request): response = getattr(view, attr)(context, request) if helper is not None: @@ -2797,9 +2964,14 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _attr_view - - elif helper is not None: + return _attr_view + + def adapt_rendered(self): + # it's a function that has a __call__ that accepts both context and + # request, but requires rendering + view = self.view + helper = self.helper + renderer = self.renderer def _rendered_view(context, request): response = view(context, request) if not is_response(response): @@ -2813,113 +2985,11 @@ def _map_view(view, registry, attr=None, renderer=None): response = helper.render_to_response(response, system, request=request) return response - wrapped_view = _rendered_view - - decorate_view(wrapped_view, view) - return wrapped_view - -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) - - return wrapped_view - -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 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) - - wrapped_view = _authdebug_view - decorate_view(wrapped_view, view) + return _rendered_view - return wrapped_view - -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 _map_view(view, registry, attr=None, renderer=None): + return DefaultViewDeriver(view, registry=registry, attr=attr, + renderer=renderer).map_view(view) def isexception(o): if IInterface.providedBy(o): @@ -2970,3 +3040,8 @@ class PyramidConfigurationMachine(ConfigurationMachine): self._seen_files.add(spec) return True +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/tests/test_config.py b/pyramid/tests/test_config.py index e4ab9a867..812bfffa3 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3788,16 +3788,18 @@ class Test__map_view(unittest.TestCase): 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_preserve_attrs(unittest.TestCase): + def _callFUT(self, fn, view): + from pyramid.config import preserve_attrs + return preserve_attrs(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: @@ -3826,8 +3828,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__) -- cgit v1.2.3 From de68d6666305192c6d0ffc8263f7486446a83f73 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 18:58:34 -0800 Subject: add decorator argument to add_view method to support auto-wrapping view callables with a decorator at view registration time --- pyramid/config.py | 19 ++++++++++++++++--- pyramid/tests/test_config.py | 13 +++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index f6b4a2112..083e2a328 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -1008,9 +1008,9 @@ class Configurator(object): def add_view(self, view=None, name="", for_=None, permission=None, request_type=None, route_name=None, request_method=None, request_param=None, containment=None, attr=None, - renderer=None, wrapper=None, xhr=False, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None): + renderer=None, wrapper=None, decorator=None, xhr=False, + accept=None, header=None, path_info=None, + custom_predicates=(), context=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 +1119,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 @@ -1326,6 +1335,10 @@ class Configurator(object): derived_view = self._derive_view(view, permission, predicates, attr, renderer, wrapper, name, accept, order, phash) + if decorator is not None: + wrapped_view = decorator(derived_view) + decorate_view(wrapped_view, derived_view) + derived_view = wrapped_view registered = self.registry.adapters.registered diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index c129b21ae..a0bdf95ad 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -739,6 +739,19 @@ class ConfiguratorTests(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_decorator(self): + def view(request): + return 'OK' + def view_wrapper(fn): + fn.__assert_wrapped__ = True + return fn + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=view_wrapper) + wrapper = self._getViewCallable(config) + self.assertTrue(getattr(wrapper, '__assert_wrapped__', False)) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + def test_add_view_as_instance(self): class AView: def __call__(self, context, request): -- cgit v1.2.3 From bae647592ee4913ed7e52409c89815c3ebb37ef7 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 21:53:50 -0800 Subject: Recorded description of last change and added self to contributors list. --- CHANGES.txt | 9 +++++++++ CONTRIBUTORS.txt | 1 + 2 files changed, 10 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ebf3bf4e2..01f4adf5b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,12 @@ +1.0a9 (unreleased) + +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 + 1.0a8 (2010-12-27) ================== diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index b48e769a1..443503914 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -117,3 +117,4 @@ Contributors - Casey Duncan, 2010/12/27 +- Rob Miller, 2010/12/28 -- cgit v1.2.3 From 73877f08d25eb77b87f0cd3aa35472165b9b50ba Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 21:54:34 -0800 Subject: Added support for an _action_decorator classmethod on handler classes. --- pyramid/config.py | 10 +++++++--- pyramid/tests/test_config.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index 083e2a328..5c3bf7002 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -941,6 +941,8 @@ class Configurator(object): pattern = route.pattern + action_decorator = getattr(handler, '_action_decorator', None) + path_has_action = ':action' in pattern or '{action}' in pattern if action and path_has_action: @@ -970,7 +972,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 +996,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 diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index a0bdf95ad..22f491eb4 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1977,6 +1977,22 @@ 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 MyView(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}', MyView) + self.assertEqual(len(views), 1) + self.assertEqual(views[0]['decorator'], MyView._action_decorator) + def test_add_handler_doesnt_mutate_expose_dict(self): config = self._makeOne(autocommit=True) views = [] -- cgit v1.2.3 From 744d76312799f2537a200e4972f309c7670a4cef Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 22:18:34 -0800 Subject: record handler _action_decorator classmethod support --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 01f4adf5b..c86594933 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,6 +7,9 @@ Features callable which will decorate the view callable before it is added to the registry +- If a handler class provides an _action_decorator classmethod, use that + as the decorator for each view registration for that handler. + 1.0a8 (2010-12-27) ================== -- cgit v1.2.3 From 91f9e646e86dabd68b4184aa0f48bb60856aec14 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 22:52:41 -0800 Subject: change ``MyView`` to ``MyHandler``, since it's a handler we're mocking --- pyramid/tests/test_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 22f491eb4..2bbd2e4aa 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1983,15 +1983,15 @@ class ConfiguratorTests(unittest.TestCase): def dummy_add_view(**kw): views.append(kw) config.add_view = dummy_add_view - class MyView(object): + 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}', MyView) + config.add_handler('name', '/{action}', MyHandler) self.assertEqual(len(views), 1) - self.assertEqual(views[0]['decorator'], MyView._action_decorator) + self.assertEqual(views[0]['decorator'], MyHandler._action_decorator) def test_add_handler_doesnt_mutate_expose_dict(self): config = self._makeOne(autocommit=True) -- cgit v1.2.3 From 88231cc1b16f1f5a0983dba1dab9b401bbde0c00 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Tue, 28 Dec 2010 22:53:17 -0800 Subject: Enforce that _action_decorator must be a classmethod --- pyramid/config.py | 4 ++++ pyramid/tests/test_config.py | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/pyramid/config.py b/pyramid/config.py index 5c3bf7002..274938225 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -942,6 +942,10 @@ class Configurator(object): pattern = route.pattern action_decorator = getattr(handler, '_action_decorator', None) + if action_decorator is not None and action_decorator.im_self is None: + raise ConfigurationError( + 'The "_action_decorator" method on a handler class MUST be ' + 'defined as a classmethod.') path_has_action = ':action' in pattern or '{action}' in pattern diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 2bbd2e4aa..0a87f4d7f 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1993,6 +1993,17 @@ class ConfiguratorTests(unittest.TestCase): self.assertEqual(len(views), 1) self.assertEqual(views[0]['decorator'], MyHandler._action_decorator) + def test_add_handler_with_action_decorator_no_classmethod(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 = [] -- cgit v1.2.3 From 8739f576ed84bb48cec9c2d4b60e92878a273b1f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 14:47:32 -0500 Subject: factor deriver from mapper --- docs/api/interfaces.rst | 4 + pyramid/config.py | 257 ++++++++++++++++++++++++++---------------------- pyramid/interfaces.py | 21 ++++ 3 files changed, 165 insertions(+), 117 deletions(-) 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 d31011aa1..d3c197008 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 @@ -266,14 +267,10 @@ class Configurator(object): renderer_globals_factory=None, default_permission=None, session_factory=None, - view_deriver=None, autocommit=False, ): if package is None: package = caller_package() - if view_deriver is None: - view_deriver = DefaultViewDeriver - self.view_deriver = view_deriver name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver self.package_name = name_resolver.package_name @@ -342,24 +339,25 @@ 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): view = self.maybe_dotted(view) - deriver = self.view_deriver(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) - return deriver() + 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): @@ -1008,7 +1006,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, 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* @@ -1253,6 +1251,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) @@ -1291,6 +1301,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) @@ -1321,10 +1332,18 @@ class Configurator(object): 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, + view_mapper=view_mapper)(view) + registered = self.registry.adapters.registered # A multiviews is a set of views which are registered for @@ -2631,11 +2650,6 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -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 DefaultViewDeriver(view, attr=attr).requestonly() - def preserve_attrs(wrapped): def inner(self, view): wrapped_view = wrapped(self, view) @@ -2670,81 +2684,27 @@ def preserve_attrs(wrapped): return wrapped_view return inner -class DefaultViewDeriver(object): - def __init__(self, view, **kw): +class ViewDeriver(object): + def __init__(self, **kw): self.kw = kw - self.view = view - self.registry = kw.get('registry') - self.helper = None - self.renderer = kw.get('renderer') - if self.renderer is None and self.registry is not None: - # use default renderer if one exists - default_renderer_factory = self.registry.queryUtility( - IRendererFactory) - if default_renderer_factory is not None: - self.renderer = {'name':None, 'package':kw.get('package')} - if self.renderer is not None: - self.helper = RendererHelper(self.renderer['name'], - package=self.renderer['package'], - registry=self.registry) - if self.registry is not None: - self.authn_policy = self.registry.queryUtility( - IAuthenticationPolicy) - self.authz_policy = self.registry.queryUtility( - IAuthorizationPolicy) - self.logger = self.registry.queryUtility(IDebugLogger) - - def __call__(self): + 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( - self.map_view(self.view)))))) - - def requestonly(self): - view = self.view - attr = self.kw.get('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 - - 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 - - if len(args) == 1: - return True - - elif args[0] == 'request': - if len(args) - len(defaults) == 1: - return True - - return False - + view))))) @preserve_attrs def owrap_view(self, view): @@ -2861,26 +2821,85 @@ class DefaultViewDeriver(object): attr_view.__phash__ = phash return attr_view +class DefaultViewMapper(object): + implements(IViewMapperFactory) + def __init__(self, **kw): + self.kw = kw + self.helper = None + self.renderer = kw.get('renderer') + self.registry = kw.get('registry') + if self.renderer is None and self.registry is not None: + # use default renderer if one exists + default_renderer_factory = self.registry.queryUtility( + IRendererFactory) + if default_renderer_factory is not None: + self.renderer = {'name':None, 'package':kw.get('package')} + if self.renderer is not None: + self.helper = RendererHelper(self.renderer['name'], + package=self.renderer['package'], + registry=self.registry) + + def requestonly(self, view): + attr = self.kw.get('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 + + 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 + + if len(args) == 1: + return True + + elif args[0] == 'request': + if len(args) - len(defaults) == 1: + return True + + return False + @preserve_attrs - def map_view(self, view): + def __call__(self, view): attr = self.kw.get('attr') isclass = inspect.isclass(view) - ronly = self.requestonly() + ronly = self.requestonly(view) if isclass and ronly: - view = self.adapt_requestonly_class() + view = self.map_requestonly_class(view) elif isclass: - view = self.adapt_class() + view = self.map_class(view) elif ronly: - view = self.adapt_requestonly_func() + view = self.map_requestonly_func(view) elif attr: - view = self.adapt_attr() + view = self.map_attr(view) elif self.helper is not None: - view = self.adapt_rendered() + view = self.map_rendered(view) return view - def adapt_requestonly_class(self): + def map_requestonly_class(self, view): # its a class that has an __init__ which only accepts request - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2904,10 +2923,9 @@ class DefaultViewDeriver(object): return response return _class_requestonly_view - def adapt_class(self): + def map_class(self, view): # its a class that has an __init__ which accepts both context and # request - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2930,10 +2948,9 @@ class DefaultViewDeriver(object): return response return _class_view - def adapt_requestonly_func(self): + def map_requestonly_func(self, view): # its a function or instance that has a __call__ accepts only a # single request argument - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2957,10 +2974,9 @@ class DefaultViewDeriver(object): return response return _requestonly_view - def adapt_attr(self): + def map_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr - view = self.view attr = self.kw.get('attr') helper = self.helper renderer = self.renderer @@ -2980,10 +2996,9 @@ class DefaultViewDeriver(object): return response return _attr_view - def adapt_rendered(self): + def map_rendered(self, view): # it's a function that has a __call__ that accepts both context and # request, but requires rendering - view = self.view helper = self.helper renderer = self.renderer def _rendered_view(context, request): @@ -3001,10 +3016,6 @@ class DefaultViewDeriver(object): return response return _rendered_view -def _map_view(view, registry, attr=None, renderer=None): - return DefaultViewDeriver(view, registry=registry, attr=attr, - renderer=renderer).map_view(view) - def isexception(o): if IInterface.providedBy(o): if IException.isEqualOrExtendedBy(o): @@ -3059,3 +3070,15 @@ def is_response(ob): hasattr(ob, 'status') ): return True return False + +# 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) + 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): -- cgit v1.2.3 From 613287762f6ca2f8d50651fc0b4eee2e9a5f8772 Mon Sep 17 00:00:00 2001 From: Rob Miller Date: Wed, 29 Dec 2010 14:22:34 -0800 Subject: adjust tests to work w/ latest merge and changes --- pyramid/tests/test_config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index b094caae9..e088ea2fc 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1985,18 +1985,18 @@ class ConfiguratorTests(unittest.TestCase): config.add_view = dummy_add_view class MyHandler(object): @classmethod - def _action_decorator(cls, fn): # pragma: no cover + 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) + self.assertEqual(views[0]['decorator'], MyHandler.__action_decorator__) - def test_add_handler_with_action_decorator_no_classmethod(self): + 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 + def __action_decorator__(self, fn): # pragma: no cover return fn def action(self): # pragma: no cover return 'response' -- cgit v1.2.3 From 1f8536956af7e122007da369d35924c28dd99c25 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 17:47:35 -0500 Subject: simplify guard logic for __action_decorator__ --- pyramid/config.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index e1005102b..e717556d9 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -4,7 +4,6 @@ import re import sys import threading import traceback -from types import FunctionType import venusian @@ -940,14 +939,17 @@ class Configurator(object): action_decorator = getattr(handler, '__action_decorator__', None) if action_decorator is not None: - class_or_static = getattr(action_decorator, 'im_self', - None) is not None - if not class_or_static: - class_or_static = isinstance(action_decorator, FunctionType) - if not class_or_static: - raise ConfigurationError( - 'The "__action_decorator__" callable on a handler class ' - 'MUST be defined as a classmethod or a staticmethod.') + 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 -- cgit v1.2.3 From 73741f6f97dce4d3f8ee340a708823db75c7aef6 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 18:12:40 -0500 Subject: preserve_attrs->wraps_view, break out preserve_attrs bulk into preserve_view_attrs, make sure decorator preserves view attrs --- pyramid/config.py | 78 +++++++++++++++++++++++--------------------- pyramid/tests/test_config.py | 6 ++-- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index e717556d9..b8b47f686 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2676,40 +2676,43 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -def preserve_attrs(wrapped): +def wraps_view(wrapped): def inner(self, view): wrapped_view = wrapped(self, view) - if wrapped_view is view: - return view - wrapped_view.__module__ = view.__module__ - wrapped_view.__doc__ = view.__doc__ - try: - wrapped_view.__name__ = view.__name__ - except AttributeError: - wrapped_view.__name__ = repr(view) - try: - wrapped_view.__permitted__ = view.__permitted__ - except AttributeError: - pass - try: - wrapped_view.__call_permissive__ = view.__call_permissive__ - except AttributeError: - pass - try: - wrapped_view.__predicated__ = view.__predicated__ - except AttributeError: - pass - try: - wrapped_view.__accept__ = view.__accept__ - except AttributeError: - pass - try: - wrapped_view.__order__ = view.__order__ - except AttributeError: - pass - return wrapped_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__ = view.__name__ + except AttributeError: + wrapped_view.__name__ = repr(view) + try: + wrapped_view.__permitted__ = view.__permitted__ + except AttributeError: + pass + try: + wrapped_view.__call_permissive__ = view.__call_permissive__ + except AttributeError: + pass + try: + wrapped_view.__predicated__ = view.__predicated__ + except AttributeError: + pass + try: + wrapped_view.__accept__ = view.__accept__ + except AttributeError: + pass + try: + wrapped_view.__order__ = view.__order__ + except AttributeError: + pass + return wrapped_view + class ViewDeriver(object): def __init__(self, **kw): self.kw = kw @@ -2732,7 +2735,7 @@ class ViewDeriver(object): self.owrap_view( view))))) - @preserve_attrs + @wraps_view def owrap_view(self, view): wrapper_viewname = self.kw.get('wrapper_viewname') viewname = self.kw.get('viewname') @@ -2752,7 +2755,7 @@ class ViewDeriver(object): return wrapped_response return _owrapped_view - @preserve_attrs + @wraps_view def secured_view(self, view): permission = self.kw.get('permission') if permission == '__no_permission_required__': @@ -2780,7 +2783,7 @@ class ViewDeriver(object): return wrapped_view - @preserve_attrs + @wraps_view def authdebug_view(self, view): wrapped_view = view settings = self.registry.settings @@ -2813,7 +2816,7 @@ class ViewDeriver(object): return wrapped_view - @preserve_attrs + @wraps_view def predicated_view(self, view): predicates = self.kw.get('predicates', ()) if not predicates: @@ -2828,7 +2831,7 @@ class ViewDeriver(object): predicate_wrapper.__predicated__ = checker return predicate_wrapper - @preserve_attrs + @wraps_view def attr_wrapped_view(self, view): kw = self.kw accept, order, phash = (kw.get('accept', None), @@ -2907,7 +2910,7 @@ class DefaultViewMapper(object): return False - @preserve_attrs + @wraps_view def __call__(self, view): attr = self.kw.get('attr') decorator = self.kw.get('decorator') @@ -2924,7 +2927,8 @@ class DefaultViewMapper(object): elif self.helper is not None: view = self.map_rendered(view) if decorator is not None: - view = decorator(view) + decorated_view = decorator(view) + view = preserve_view_attrs(view, decorated_view) return view def map_requestonly_class(self, view): diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index e088ea2fc..80e675623 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -3868,10 +3868,10 @@ class Test__map_view(unittest.TestCase): request = self._makeRequest() self.assertEqual(result(None, request).body, 'Hello!') -class Test_preserve_attrs(unittest.TestCase): +class Test_wraps_view(unittest.TestCase): def _callFUT(self, fn, view): - from pyramid.config import preserve_attrs - return preserve_attrs(fn)(None, view) + from pyramid.config import wraps_view + return wraps_view(fn)(None, view) def test_it_same(self): def view(context, request): -- cgit v1.2.3 From 2953baaebadfb267e1fa98d35605b88ff2274052 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 29 Dec 2010 18:22:37 -0500 Subject: make sure view returned from view mapper preserves all attrs --- pyramid/config.py | 6 +++++- pyramid/tests/test_config.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index b8b47f686..077db28ab 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2910,7 +2910,6 @@ class DefaultViewMapper(object): return False - @wraps_view def __call__(self, view): attr = self.kw.get('attr') decorator = self.kw.get('decorator') @@ -2931,6 +2930,7 @@ class DefaultViewMapper(object): view = preserve_view_attrs(view, decorated_view) return view + @wraps_view def map_requestonly_class(self, view): # its a class that has an __init__ which only accepts request attr = self.kw.get('attr') @@ -2956,6 +2956,7 @@ class DefaultViewMapper(object): return response return _class_requestonly_view + @wraps_view def map_class(self, view): # its a class that has an __init__ which accepts both context and # request @@ -2981,6 +2982,7 @@ class DefaultViewMapper(object): return response return _class_view + @wraps_view def map_requestonly_func(self, view): # its a function or instance that has a __call__ accepts only a # single request argument @@ -3007,6 +3009,7 @@ class DefaultViewMapper(object): return response return _requestonly_view + @wraps_view def map_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr @@ -3029,6 +3032,7 @@ class DefaultViewMapper(object): return response return _attr_view + @wraps_view def map_rendered(self, view): # it's a function that has a __call__ that accepts both context and # request, but requires rendering diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index 80e675623..d237592d5 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -741,14 +741,17 @@ class ConfiguratorTests(unittest.TestCase): def test_add_view_with_decorator(self): def view(request): + """ ABC """ return 'OK' def view_wrapper(fn): - fn.__assert_wrapped__ = True - return 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.assertTrue(getattr(wrapper, '__assert_wrapped__', False)) + self.failIf(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) result = wrapper(None, None) self.assertEqual(result, 'OK') -- cgit v1.2.3 From 95c9f6f331bd3294699969ae399045891c0dc6ad Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 02:07:36 -0500 Subject: - 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). --- CHANGES.txt | 32 ++++++-- TODO.txt | 6 +- pyramid/config.py | 187 ++++++++++++++++--------------------------- pyramid/renderers.py | 12 +++ pyramid/tests/test_config.py | 59 +++++++++----- pyramid/tests/test_view.py | 11 ++- pyramid/view.py | 12 ++- pyramid/zcml.py | 5 -- 8 files changed, 163 insertions(+), 161 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fabb882f7..018de8cf7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -13,12 +13,19 @@ Bug Fixes 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 ``decorator`` keyword argument, a callable + which will decorate the view callable before it is added to the registry. -- If a handler class provides an _action_decorator classmethod, use that - as the decorator for each view registration for that handler. +- ``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 ------------- @@ -27,6 +34,21 @@ 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/TODO.txt b/TODO.txt index ada3c4c2a..5acc923a1 100644 --- a/TODO.txt +++ b/TODO.txt @@ -11,8 +11,10 @@ Must-Have (before 1.0) - Re-make testing.setUp() and testing.tearDown() the canonical APIs for test configuration. -- ``decorator=`` parameter to view_config. This would replace the existing - _map_view "decorator" if it existed (Rob needs). +- Document ``decorator=`` and ``view_mapper`` parameters to add_view. + +- Allow ``decorator=`` and ``view_mapper=`` to be passed via ZCML and the + ``view_config`` decorator. Should-Have ----------- diff --git a/pyramid/config.py b/pyramid/config.py index 077db28ab..a53c21b81 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -345,6 +345,15 @@ class Configurator(object): viewname=None, accept=None, order=MAX_ORDER, phash=DEFAULT_PHASH): view = self.maybe_dotted(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, @@ -753,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 @@ -1284,9 +1290,9 @@ class Configurator(object): 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. + 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) @@ -1338,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_ @@ -1350,7 +1353,17 @@ 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 @@ -2002,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) @@ -2041,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) @@ -2853,23 +2868,12 @@ class ViewDeriver(object): class DefaultViewMapper(object): implements(IViewMapperFactory) def __init__(self, **kw): - self.kw = kw - self.helper = None self.renderer = kw.get('renderer') - self.registry = kw.get('registry') - if self.renderer is None and self.registry is not None: - # use default renderer if one exists - default_renderer_factory = self.registry.queryUtility( - IRendererFactory) - if default_renderer_factory is not None: - self.renderer = {'name':None, 'package':kw.get('package')} - if self.renderer is not None: - self.helper = RendererHelper(self.renderer['name'], - package=self.renderer['package'], - registry=self.registry) + self.attr = kw.get('attr') + self.decorator = kw.get('decorator') def requestonly(self, view): - attr = self.kw.get('attr') + attr = self.attr if attr is None: attr = '__call__' if inspect.isfunction(view): @@ -2911,145 +2915,90 @@ class DefaultViewMapper(object): return False def __call__(self, view): - attr = self.kw.get('attr') - decorator = self.kw.get('decorator') + attr = self.attr + decorator = self.decorator isclass = inspect.isclass(view) ronly = self.requestonly(view) if isclass and ronly: - view = self.map_requestonly_class(view) + view = preserve_view_attrs(view, self.map_requestonly_class(view)) elif isclass: - view = self.map_class(view) + view = preserve_view_attrs(view, self.map_class(view)) elif ronly: - view = self.map_requestonly_func(view) + view = preserve_view_attrs(view, self.map_requestonly_func(view)) elif attr: - view = self.map_attr(view) - elif self.helper is not None: - view = self.map_rendered(view) + view = preserve_view_attrs(view, self.map_attr(view)) + elif self.renderer is not None: + view = preserve_view_attrs(view, self.map_rendered(view)) if decorator is not None: - decorated_view = decorator(view) - view = preserve_view_attrs(view, decorated_view) + view = preserve_view_attrs(view, decorator(view)) return view - @wraps_view def map_requestonly_class(self, view): # its a class that has an __init__ which only accepts request - attr = self.kw.get('attr') - helper = self.helper - renderer = self.renderer + attr = self.attr 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) + 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 - @wraps_view def map_class(self, view): # its a class that has an __init__ which accepts both context and # request - attr = self.kw.get('attr') - helper = self.helper - renderer = self.renderer + attr = self.attr 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) + 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 - @wraps_view def map_requestonly_func(self, view): - # its a function or instance that has a __call__ accepts only a - # single request argument - attr = self.kw.get('attr') - helper = self.helper - renderer = self.renderer + # 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 return _requestonly_view - @wraps_view def map_attr(self, view): # its a function that has a __call__ which accepts both context and # request, but still has an attr - attr = self.kw.get('attr') - helper = self.helper - renderer = self.renderer + 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 return _attr_view - @wraps_view def map_rendered(self, view): # it's a function that has a __call__ that accepts both context and # request, but requires rendering - helper = self.helper - renderer = self.renderer 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 return _rendered_view @@ -3102,12 +3051,6 @@ class PyramidConfigurationMachine(ConfigurationMachine): self._seen_files.add(spec) return True -def is_response(ob): - if ( hasattr(ob, 'app_iter') and hasattr(ob, 'headerlist') and - hasattr(ob, 'status') ): - return True - return False - # b/c def _map_view(view, registry, attr=None, renderer=None): return DefaultViewMapper(registry=registry, attr=attr, @@ -3119,3 +3062,9 @@ def requestonly(view, attr=None): 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/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 d237592d5..b2fa0e329 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -1426,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 @@ -3547,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() @@ -3584,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) @@ -3643,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__) @@ -3685,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__) @@ -3727,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__) @@ -3769,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__) @@ -3802,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!') @@ -3838,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__) @@ -3851,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__) @@ -3863,8 +3879,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__) 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/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, -- cgit v1.2.3 From fcff8cda8f7c60f181e902ca5a349eb8b5655205 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 02:09:23 -0500 Subject: stray header line --- CHANGES.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 018de8cf7..743f20e3b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,3 @@ -======= Next release ============ -- cgit v1.2.3 From ae6513b9e93d876902936c257aef6a506f850aa3 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 03:21:56 -0500 Subject: factor defaultviewmapper to dispatch in a slightly more readable way --- pyramid/config.py | 132 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 73 insertions(+), 59 deletions(-) diff --git a/pyramid/config.py b/pyramid/config.py index a53c21b81..eefdaae1f 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -2872,68 +2872,39 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') self.decorator = kw.get('decorator') - 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 - - 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 - - if len(args) == 1: - return True - - elif args[0] == 'request': - if len(args) - len(defaults) == 1: - return True - - return False - def __call__(self, view): - attr = self.attr decorator = self.decorator - isclass = inspect.isclass(view) - ronly = self.requestonly(view) - if isclass and ronly: - view = preserve_view_attrs(view, self.map_requestonly_class(view)) - elif isclass: + if inspect.isclass(view): view = preserve_view_attrs(view, self.map_class(view)) - elif ronly: - view = preserve_view_attrs(view, self.map_requestonly_func(view)) - elif attr: - view = preserve_view_attrs(view, self.map_attr(view)) - elif self.renderer is not None: - view = preserve_view_attrs(view, self.map_rendered(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_requestonly_class(self, view): + def map_class(self, view): + ronly = self.requestonly(view) + if ronly: + mapped_view = self._map_class_requestonly(view) + else: + 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): @@ -2948,7 +2919,7 @@ class DefaultViewMapper(object): return response return _class_requestonly_view - def map_class(self, view): + def _map_class_native(self, view): # its a class that has an __init__ which accepts both context and # request attr = self.attr @@ -2964,7 +2935,7 @@ class DefaultViewMapper(object): return response return _class_view - def map_requestonly_func(self, view): + def _map_nonclass_requestonly(self, view): # its a function that has a __call__ which accepts only a single # request argument attr = self.attr @@ -2979,7 +2950,7 @@ class DefaultViewMapper(object): return response return _requestonly_view - def map_attr(self, 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 attr = self.attr @@ -2991,7 +2962,7 @@ class DefaultViewMapper(object): return response return _attr_view - def map_rendered(self, view): + 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): @@ -3002,6 +2973,49 @@ class DefaultViewMapper(object): return response 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 + + 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 + + if len(args) == 1: + return True + + elif args[0] == 'request': + if len(args) - len(defaults) == 1: + return True + + return False + + def isexception(o): if IInterface.providedBy(o): if IException.isEqualOrExtendedBy(o): -- cgit v1.2.3 From ae40397a29b6fc3068a61c5e7acc7e3f7d801086 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 30 Dec 2010 17:14:50 -0500 Subject: typos --- pyramid/url.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 -- cgit v1.2.3