summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2012-02-14 04:15:09 -0500
committerChris McDonough <chrism@plope.com>2012-02-14 04:15:09 -0500
commit0dd383d5c160460a66cef5fce46a4d4e7c6fe167 (patch)
treefe7c2a0f7516e1c8b59adfa747441e0d8db30061
parentad3c25bac043e9d14ce8ffe1b03ae6d0e92b3b0e (diff)
parent9ed1e0ba957c36f6ae29c25ffeaa6c2c02f716a9 (diff)
downloadpyramid-0dd383d5c160460a66cef5fce46a4d4e7c6fe167.tar.gz
pyramid-0dd383d5c160460a66cef5fce46a4d4e7c6fe167.tar.bz2
pyramid-0dd383d5c160460a66cef5fce46a4d4e7c6fe167.zip
Merge branch 'master' into 1.3-branch
-rw-r--r--CHANGES.txt6
-rw-r--r--docs/designdefense.rst4
-rw-r--r--docs/narr/views.rst2
-rw-r--r--pyramid/config/util.py5
-rw-r--r--pyramid/config/views.py32
-rw-r--r--pyramid/tests/test_config/test_views.py125
-rw-r--r--pyramid/urldispatch.py2
7 files changed, 165 insertions, 11 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/docs/designdefense.rst b/docs/designdefense.rst
index 59b0e5a2d..84b6be907 100644
--- a/docs/designdefense.rst
+++ b/docs/designdefense.rst
@@ -219,7 +219,7 @@ is this:
Using such wrappers, we strive to always hide the ZCA API from application
developers. Application developers should just never know about the ZCA API:
they should call a Python function with some object germane to the domain as
-an argument, and it should returns a result. A corollary that follows is
+an argument, and it should return a result. A corollary that follows is
that any reader of an application that has been written using :app:`Pyramid`
needn't understand the ZCA API either.
@@ -720,7 +720,7 @@ microframeworks and Django boast.
The :mod:`zope.component`, package on which :app:`Pyramid` depends has
transitive dependencies on several other packages (:mod:`zope.event`, and
:mod:`zope.interface`). :app:`Pyramid` also has its own direct dependencies,
-such as :term:`PasteDeploy`, :term:`Chameleon`, :term:`Mako` :term:`WebOb`,
+such as :term:`PasteDeploy`, :term:`Chameleon`, :term:`Mako`, :term:`WebOb`,
:mod:`zope.deprecation` and some of these in turn have their own transitive
dependencies.
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index fa34cca61..dbc702de8 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -30,7 +30,7 @@ View Callables
View callables are, at the risk of sounding obvious, callable Python
objects. Specifically, view callables can be functions, classes, or instances
-that implement an ``__call__`` method (making the instance callable).
+that implement a ``__call__`` method (making the instance callable).
View callables must, at a minimum, accept a single argument named
``request``. This argument represents a :app:`Pyramid` :term:`Request`
diff --git a/pyramid/config/util.py b/pyramid/config/util.py
index b39fb8ee0..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,
)
@@ -44,7 +43,7 @@ def action_method(wrapped):
self._ainfo = []
info = kw.pop('_info', None)
# backframes for outer decorators to actionmethods
- backframes = kw.pop('_backframes', 2)
+ backframes = kw.pop('_backframes', 2)
if is_nonstr_iter(info) and len(info) == 4:
# _info permitted as extract_stack tuple
info = ActionInfo(*info)
@@ -132,7 +131,7 @@ def make_predicates(xhr=None, request_method=None, path_info=None,
request_method = sorted(request_method)
def request_method_predicate(context, request):
return request.method in request_method
- text = "request method = %s" % repr(request_method)
+ text = "request method = %r" % request_method
request_method_predicate.__text__ = text
weights.append(1 << 2)
predicates.append(request_method_predicate)
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index a87ab54c7..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 view return value "%s" into a '
- 'response object' % (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
diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py
index bd1da8f71..009804280 100644
--- a/pyramid/urldispatch.py
+++ b/pyramid/urldispatch.py
@@ -14,13 +14,13 @@ from pyramid.compat import (
string_types,
binary_type,
is_nonstr_iter,
+ decode_path_info,
)
from pyramid.exceptions import URLDecodeError
from pyramid.traversal import (
quote_path_segment,
- decode_path_info,
split_path_info,
)