summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-14 04:13:41 -0500
committerChris McDonough <chrism@plope.com>2012-02-14 04:13:41 -0500
commitaf24f7d5f69a74f9887ca6df622ef67c69075988 (patch)
tree15623eb5ee1637316103ca9594d21f083da5d89a
parent7af91f44b6b4d3e74b0f3b6a5c2a59e3bad169f7 (diff)
downloadpyramid-af24f7d5f69a74f9887ca6df622ef67c69075988.tar.gz
pyramid-af24f7d5f69a74f9887ca6df622ef67c69075988.tar.bz2
pyramid-af24f7d5f69a74f9887ca6df622ef67c69075988.zip
- 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.
-rw-r--r--CHANGES.txt6
-rw-r--r--pyramid/config/util.py1
-rw-r--r--pyramid/config/views.py32
-rw-r--r--pyramid/tests/test_config/test_views.py125
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 '
+ '<pyramid.tests.test_config.test_views.AView object at'))
+ self.assertTrue(msg.endswith(
+ '> 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