From af24f7d5f69a74f9887ca6df622ef67c69075988 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 14 Feb 2012 04:13:41 -0500 Subject: - Better error messages when a view callable returns a value that cannot be converted to a response (for example, when a view callable returns a dictionary without a renderer defined, or doesn't return any value at all). The error message now contains information about the view callable itself as well as the result of calling it. --- CHANGES.txt | 6 ++ pyramid/config/util.py | 1 - pyramid/config/views.py | 32 +++++++- pyramid/tests/test_config/test_views.py | 125 ++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 5 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index fcd54217f..411681d81 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -11,6 +11,12 @@ Features http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for more information about how to use the ``ignore`` argument to ``scan``. +- Better error messages when a view callable returns a value that cannot be + converted to a response (for example, when a view callable returns a + dictionary without a renderer defined, or doesn't return any value at all). + The error message now contains information about the view callable itself + as well as the result of calling it. + Dependencies ------------ diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 6b7aa2fa1..4c7ecd359 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -6,7 +6,6 @@ from zope.interface import implementer from pyramid.interfaces import IActionInfo from pyramid.compat import ( - string_types, bytes_, is_nonstr_iter, ) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 86b139e3e..1988b532b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -55,6 +55,7 @@ from pyramid.security import NO_PERMISSION_REQUIRED from pyramid.static import static_view from pyramid.threadlocal import get_current_registry from pyramid.view import render_view_to_response +from pyramid.util import object_description from pyramid.config.util import ( DEFAULT_PHASH, @@ -67,6 +68,12 @@ from pyramid.config.util import ( urljoin = urlparse.urljoin url_parse = urlparse.urlparse +def view_description(view): + try: + return view.__text__ + except AttributeError: + return object_description(view) + def wraps_view(wrapper): def inner(self, view): wrapper_view = wrapper(self, view) @@ -99,7 +106,7 @@ def preserve_view_attrs(view, wrapper): # "wrapped view" for attr in ('__permitted__', '__call_permissive__', '__permission__', '__predicated__', '__predicates__', '__accept__', - '__order__'): + '__order__', '__text__'): try: setattr(wrapper, attr, getattr(view, attr)) except AttributeError: @@ -343,9 +350,19 @@ class ViewDeriver(object): result = view(context, request) response = registry.queryAdapterOrSelf(result, IResponse) if response is None: - raise ValueError( - 'Could not convert %r return value "%s" into a ' - 'response object' % (view,result,)) + if result is None: + append = (' You may have forgotten to return a value from ' + 'the view callable.') + elif isinstance(result, dict): + append = (' You may have forgotten to define a renderer in ' + 'the view configuration.') + else: + append = '' + msg = ('Could not convert return value of the view callable %s ' + 'into a response object. ' + 'The value returned was %r.' + append) + + raise ValueError(msg % (view_description(view), result)) return response return viewresult_to_response @@ -376,6 +393,8 @@ class DefaultViewMapper(object): mapped_view = self.map_class_requestonly(view) else: mapped_view = self.map_class_native(view) + mapped_view.__text__ = 'method %s of %s' % ( + self.attr or '__call__', object_description(view)) return mapped_view def map_nonclass(self, view): @@ -388,6 +407,11 @@ class DefaultViewMapper(object): mapped_view = self.map_nonclass_requestonly(view) elif self.attr: mapped_view = self.map_nonclass_attr(view) + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(view) return mapped_view def map_class_requestonly(self, view): diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 4af29325a..eb18d5c84 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2282,6 +2282,113 @@ class TestViewDeriver(unittest.TestCase): self.config.registry.registerUtility(policy, IAuthenticationPolicy) self.config.registry.registerUtility(policy, IAuthorizationPolicy) + def test_function_returns_non_adaptable(self): + def view(request): + return None + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable function ' + 'pyramid.tests.test_config.test_views.view into a response ' + 'object. The value returned was None. You may have forgotten ' + 'to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_non_adaptable_dict(self): + def view(request): + return {'a':1} + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + "Could not convert return value of the view callable function " + "pyramid.tests.test_config.test_views.view into a response " + "object. The value returned was {'a': 1}. You may have " + "forgotten to define a renderer in the view configuration." + ) + else: # pragma: no cover + raise AssertionError + + def test_instance_returns_non_adaptable(self): + class AView(object): + def __call__(self, request): + return None + view = AView() + deriver = self._makeOne() + result = deriver(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + msg = e.args[0] + self.assertTrue(msg.startswith( + 'Could not convert return value of the view callable object ' + ' into a response object. The value returned was None. You ' + 'may have forgotten to return a value from the view callable.')) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_default_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def __call__(self): + return None + deriver = self._makeOne() + result = deriver(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method __call__ of ' + 'class pyramid.tests.test_config.test_views.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_nondefault_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def theviewmethod(self): + return None + deriver = self._makeOne(attr='theviewmethod') + result = deriver(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method theviewmethod of ' + 'class pyramid.tests.test_config.test_views.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + def test_requestonly_function(self): response = DummyResponse() def view(request): @@ -3689,6 +3796,24 @@ class TestStaticURLInfo(unittest.TestCase): view_attr='attr') self.assertEqual(config.view_kw['attr'], 'attr') +class Test_view_description(unittest.TestCase): + def _callFUT(self, view): + from pyramid.config.views import view_description + return view_description(view) + + def test_with_text(self): + def view(): pass + view.__text__ = 'some text' + result = self._callFUT(view) + self.assertEqual(result, 'some text') + + def test_without_text(self): + def view(): pass + result = self._callFUT(view) + self.assertEqual(result, + 'function pyramid.tests.test_config.test_views.view') + + class DummyRegistry: pass -- cgit v1.2.3